diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31d6546..aac8251 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,11 @@ jobs: - name: Install dependencies run: npm ci + # The deployed service serves src/ as committed, without npm, so the + # vendored files must match what vendor-libs regenerates. + - name: Vendored files in sync + run: npm run vendor-libs && git diff --exit-code + - name: Lint run: npm run lint diff --git a/.gitignore b/.gitignore index 90a20cb..56501b2 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,9 @@ AUDIT-*.md # Local service configuration (token, port). Commit .env.example instead. .env +# Node binary pinned by `just deploy` +/deploy/bin/ + # Log files *.log diff --git a/CHANGELOG.md b/CHANGELOG.md index 39761a7..b5db4bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Changed + +* **The ingest service deploys with `just` instead of shell scripts.** `install-service.sh` and `update-service.sh` are gone; `just deploy` replaces both, and `just status`, `logs`, `down` and `uninstall` cover the rest (see [SERVICE.md](SERVICE.md)). The service no longer needs `npm install` on the server, and node upgrades are picked up on the next deploy. + ## 1.17.1 — 2026-08-12 ### Fixes diff --git a/SERVICE.md b/SERVICE.md index 6b04913..3f77dcb 100644 --- a/SERVICE.md +++ b/SERVICE.md @@ -33,6 +33,26 @@ Open the viewer URL in a browser. POST a graph (see [API.md](API.md)) and it renders immediately; subsequent pushes replace the current graph live via Server-Sent Events. +## Deploying as a systemd service + +On a Linux host, from the checkout, as your normal user (commands that need +root use `sudo` themselves; requires [`just`](https://github.com/casey/just)): + +```bash +just deploy # pull (ff-only), pin node, install/refresh the unit, restart +just status # systemctl status +just logs # follow the journal +just down # stop the service +just uninstall # disable and remove the unit; code and .env stay +``` + +The unit is versioned at [`deploy/graph-lens-lite.service`](deploy/graph-lens-lite.service). +`just deploy` renders it with the deploying user and checkout path, and runs +the server with a copy of that user's `node` (the nvm default, if nvm is +installed) kept at `deploy/bin/node`. After upgrading node, run `just deploy` +again to repin it. The service needs no `npm install`: it uses only node +builtins and the committed `src/` tree. Rerunning `just deploy` is safe. + ## Configuration All configuration is environment-based (see [`.env.example`](.env.example)). diff --git a/deploy/graph-lens-lite.service b/deploy/graph-lens-lite.service new file mode 100644 index 0000000..8c5da93 --- /dev/null +++ b/deploy/graph-lens-lite.service @@ -0,0 +1,26 @@ +# Rendered by `just deploy`: @USER@, @GROUP@ and @DIR@ are filled from the +# deploying user and this checkout, so the repo carries no host specifics. +[Unit] +Description=Graph Lens Lite Ingest Service +After=network.target + +[Service] +Type=simple +User=@USER@ +Group=@GROUP@ +WorkingDirectory=@DIR@ +# Pinned copy made by `just deploy`, so nvm upgrades can't break the unit. +ExecStart="@DIR@/deploy/bin/node" --env-file-if-exists=.env server/index.js +Restart=on-failure +RestartSec=5 +Environment=NODE_ENV=production + +NoNewPrivileges=true +ProtectSystem=full +# No ReadWritePaths: the service holds its graph in memory and never writes to +# disk, so granting write over the whole checkout only widens what a +# compromise of it could reach. +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/install-service.sh b/install-service.sh deleted file mode 100755 index e5dea38..0000000 --- a/install-service.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [ "$(id -u)" -ne 0 ]; then - echo "ERROR: must run as root (use sudo)." >&2 - exit 1 -fi - -WORKING_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SERVICE_NAME="graph-lens-lite" -UNIT_PATH="/etc/systemd/system/${SERVICE_NAME}.service" -RUN_USER="${SUDO_USER:-$(stat -c '%U' "$WORKING_DIR")}" -RUN_GROUP="$(id -gn "$RUN_USER")" - -# Resolve npm/node from the service user's environment (handles nvm). -resolve_bin() { - local bin="$1" - sudo -u "$RUN_USER" bash -lc "command -v ${bin}" 2>/dev/null \ - || sudo -u "$RUN_USER" bash -c \ - "export NVM_DIR=\"\$HOME/.nvm\"; [ -s \"\$NVM_DIR/nvm.sh\" ] && . \"\$NVM_DIR/nvm.sh\" >/dev/null 2>&1; command -v ${bin}" 2>/dev/null \ - || true -} - -NPM_BIN="$(resolve_bin npm)" -NODE_BIN="$(resolve_bin node)" -if [ -z "$NPM_BIN" ] || [ -z "$NODE_BIN" ]; then - echo "ERROR: could not locate npm/node for user '${RUN_USER}'." >&2 - echo "Ensure node is installed and on that user's login PATH (e.g. via nvm)." >&2 - exit 1 -fi -NODE_DIR="$(dirname "$NODE_BIN")" -echo "Using npm: ${NPM_BIN}" -echo "Using node dir: ${NODE_DIR}" - -read -r -d '' UNIT < "$UNIT_PATH" - -systemctl daemon-reload -systemctl enable --now "$SERVICE_NAME" - -echo "Installed ${SERVICE_NAME}." -echo " Status: systemctl status ${SERVICE_NAME}" -echo " Logs: journalctl -u ${SERVICE_NAME} -f" -echo " Restart: systemctl restart ${SERVICE_NAME}" -echo " Remove: systemctl disable --now ${SERVICE_NAME} && rm ${UNIT_PATH} && systemctl daemon-reload" -echo "Note: re-run this script after upgrading node (e.g. nvm install) to repin paths." diff --git a/justfile b/justfile new file mode 100644 index 0000000..61dc0e5 --- /dev/null +++ b/justfile @@ -0,0 +1,75 @@ +# Deploy contract: help / deploy / status / logs / down +set shell := ["bash", "-euo", "pipefail", "-c"] + +service := "graph-lens-lite" +branch := "main" + +[private] +default: help + +# list available recipes +help: + @just --list --unsorted + +# pull code, pin node, refresh unit, restart service +deploy: pull build unit + sudo systemctl restart {{service}} + +# show service status +status: + systemctl status {{service}} --no-pager + +# follow service logs +logs: + journalctl -u {{service}} -f -n 100 + +# stop the service +[confirm] +down: + sudo systemctl stop {{service}} + +# disable and remove the service; code and .env are kept +[confirm] +uninstall: + sudo systemctl disable --now {{service}} + sudo rm -f /etc/systemd/system/{{service}}.service + sudo systemctl daemon-reload + +# fast-forward the current branch; asks before deploying a non-deploy branch +[private] +pull: + #!/usr/bin/env bash + set -euo pipefail + current=$(git rev-parse --abbrev-ref HEAD) + if [ "$current" != "{{branch}}" ]; then + read -rp "WARNING: on branch '$current', not '{{branch}}'. Continue? [y/N] " confirm + [[ "$confirm" =~ ^[Yy]$ ]] || exit 1 + fi + git pull --ff-only + +# The server needs only node builtins and the committed src/, so there is no +# npm install here; CI guards that the vendored files in src/ stay in sync. +# Pin the node the unit runs: nvm's default is copied out of nvm's paths (-C +# skips the write when unchanged); a system node is symlinked so package +# upgrades flow through. +[private] +build: + #!/usr/bin/env bash + set -eo pipefail + mkdir -p deploy/bin + nvm_sh="${NVM_DIR:-$HOME/.nvm}/nvm.sh" + if [ -s "$nvm_sh" ]; then + . "$nvm_sh" --no-use + nvm use default --silent + install -C -m755 "$(node -p process.execPath)" deploy/bin/node + else + node_bin=$(command -v node) || { echo "node not found on PATH and no nvm" >&2; exit 1; } + ln -sfn "$node_bin" deploy/bin/node + fi + +# render the versioned unit for this user and checkout, install, enable +[private] +unit: + sed -e "s|@USER@|$(id -un)|g" -e "s|@GROUP@|$(id -gn)|g" -e "s|@DIR@|{{justfile_directory()}}|g" deploy/{{service}}.service | sudo tee /etc/systemd/system/{{service}}.service >/dev/null + sudo systemctl daemon-reload + sudo systemctl enable {{service}} diff --git a/update-service.sh b/update-service.sh deleted file mode 100755 index b9fa62f..0000000 --- a/update-service.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -cd "$(dirname "${BASH_SOURCE[0]}")" - -SERVICE_NAME="graph-lens-lite" - -branch=$(git rev-parse --abbrev-ref HEAD) -if [ "$branch" != "main" ]; then - echo "WARNING: on branch '${branch}', not 'main'." - read -rp "Continue? [y/N] " confirm - [[ "$confirm" =~ ^[Yy]$ ]] || exit 1 -fi - -git pull -npm install - -sudo systemctl restart "$SERVICE_NAME" -echo "Updated and restarted ${SERVICE_NAME}."