From 835e3e98f24cc0d8b3eb091ad19ecd1fd2940070 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Fri, 7 Aug 2026 15:36:54 +0300 Subject: [PATCH] fix(deploy): put ingress on the proxy, and make the lint script runnable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three artefacts disagreed about where nginx runs. design.md §6.2 and the vhost both said the hanzalova proxy; the API bound 127.0.0.1 and the workflow rsynced the dashboard to bob. That combination deploys green and then serves nothing, since a proxy on another host cannot reach bob's loopback. Resolve it the way design.md already stated: nginx on the proxy, dashboard shipped there, API bound 0.0.0.0 behind firewalld and the mesh. The health probe now runs from the proxy over the mesh rather than from bob's loopback, so it fails when firewalld is closed instead of passing regardless. infra-setup.sh grows a proxy grant scoped to static files alone, and the nginx vhost install as a manual step — it needs a certificate, and nothing was telling the operator to install it at all. npm run lint had never run: eslint 9 needs a flat config and there was none. Add it, ignoring the ts-rs generated bindings, and run it in CI so it stays true. Untrack dashboard/tsconfig.tsbuildinfo, a build artifact that would have put a spurious diff in every pull request tireless opens. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013TxK1CWPkFXqdcXMJ4hVe6 --- .gitea/workflows/deploy.yaml | 29 +++++++++++++++--- .gitignore | 4 +++ dashboard/eslint.config.js | 34 +++++++++++++++++++++ dashboard/package-lock.json | 20 +++++++++++-- dashboard/package.json | 1 + dashboard/tsconfig.tsbuildinfo | 2 +- script/infra-setup.sh | 55 ++++++++++++++++++++++++++++++++-- 7 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 dashboard/eslint.config.js diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 0aafd73..01eeb65 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -15,6 +15,10 @@ concurrency: env: API_HOST: bob.hanzalova.internal API_PORT: "23296" + # Ingress is the office proxy, not bob (doc/plan/design.md §6.2): nginx there + # serves the static dashboard and reverse-proxies /v1 across the mesh. The + # dashboard therefore ships to the proxy, and only the binaries ship to bob. + WEB_HOST: hanzalova.internal WEB_ROOT: /var/www/tireless VITE_API_BASE_URL: "" @@ -41,6 +45,7 @@ jobs: working-directory: dashboard run: | npm ci + npm run lint npm run build - uses: actions/upload-artifact@v3 @@ -72,6 +77,7 @@ jobs: StrictHostKeyChecking accept-new EOF ssh gitea_ci@"$API_HOST" hostname -f + ssh gitea_ci@"$WEB_HOST" hostname -f - name: render config env: @@ -105,15 +111,21 @@ jobs: done rsync $R asset/firewalld/tireless-api.xml \ gitea_ci@"$API_HOST":/etc/firewalld/services/tireless-api.xml + + - name: ship dashboard + run: | + # To the proxy, not to bob — that is where nginx serves it from. + R="--rsync-path=sudo rsync --mkpath" rsync $R -a --delete dashboard/dist/ \ - gitea_ci@"$API_HOST":"$WEB_ROOT/" + gitea_ci@"$WEB_HOST":"$WEB_ROOT/" + ssh gitea_ci@"$WEB_HOST" "sudo restorecon -R $WEB_ROOT" - name: apply system state run: | ssh gitea_ci@"$API_HOST" bash -euo pipefail <=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { "version": "9.39.5", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", @@ -2277,9 +2291,9 @@ } }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "17.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", + "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", "dev": true, "license": "MIT", "engines": { diff --git a/dashboard/package.json b/dashboard/package.json index 0a8ba8b..cf9fa38 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -24,6 +24,7 @@ "eslint": "^9.17.0", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.16", + "globals": "^17.9.0", "prettier": "^3.4.2", "typescript": "~5.7.2", "typescript-eslint": "^8.18.0", diff --git a/dashboard/tsconfig.tsbuildinfo b/dashboard/tsconfig.tsbuildinfo index dffcc0b..514f1ea 100644 --- a/dashboard/tsconfig.tsbuildinfo +++ b/dashboard/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/generated/AgentKind.ts","./src/api/generated/AgentRun.ts","./src/api/generated/BillingMode.ts","./src/api/generated/Forge.ts","./src/api/generated/IssueRef.ts","./src/api/generated/Job.ts","./src/api/generated/JobKind.ts","./src/api/generated/JobState.ts","./src/api/generated/LabelProtocol.ts","./src/api/generated/PollSchedule.ts","./src/api/generated/PullRequestRef.ts","./src/api/generated/RunOutcome.ts","./src/api/generated/TrackedRepo.ts","./src/routes/Jobs.tsx","./src/routes/Lanes.tsx","./src/routes/Repos.tsx"],"version":"5.7.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/generated/Acceptance.ts","./src/api/generated/AgentKind.ts","./src/api/generated/AgentRun.ts","./src/api/generated/BillingMode.ts","./src/api/generated/ChildSpec.ts","./src/api/generated/FileTouch.ts","./src/api/generated/Forge.ts","./src/api/generated/IssueRef.ts","./src/api/generated/Job.ts","./src/api/generated/JobKind.ts","./src/api/generated/JobState.ts","./src/api/generated/LabelProtocol.ts","./src/api/generated/PlanSpec.ts","./src/api/generated/PlannedChild.ts","./src/api/generated/PollSchedule.ts","./src/api/generated/PullRequestRef.ts","./src/api/generated/RunOutcome.ts","./src/api/generated/TrackedRepo.ts","./src/routes/Jobs.tsx","./src/routes/Lanes.tsx","./src/routes/Repos.tsx"],"version":"5.7.3"} \ No newline at end of file diff --git a/script/infra-setup.sh b/script/infra-setup.sh index 9eebd51..8743999 100755 --- a/script/infra-setup.sh +++ b/script/infra-setup.sh @@ -15,6 +15,11 @@ set -euo pipefail APP=tireless API_HOST="${API_HOST:-bob.hanzalova.internal}" API_PORT="${API_PORT:-23296}" +# Ingress runs on the office proxy, not on bob (doc/plan/design.md §6.2). The +# dashboard is served from there and /v1 is reverse-proxied across the mesh, so +# the proxy needs its own (much smaller) deploy grant. +WEB_HOST="${WEB_HOST:-hanzalova.internal}" +WEB_ROOT="${WEB_ROOT:-/var/www/tireless}" RUNNER_PUBKEY="${RUNNER_PUBKEY:-$HOME/.ssh/id_gitea_ci.pub}" info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } @@ -96,7 +101,39 @@ SUDOERS provision_host "$API_HOST" # --------------------------------------------------------------------------- -# 3. Manual steps that cannot be automated +# 3. Proxy host: web root and a deploy grant scoped to it alone +# --------------------------------------------------------------------------- +provision_web_host() { + local host="$1" + info "provisioning ingress on $host" + + if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "$host" true; then + warn "$host unreachable; skipping (re-run once it is back)" + return 0 + fi + + ssh "$host" 'sudo useradd --system --create-home --home-dir /var/lib/gitea_ci \ + --shell /usr/sbin/nologin gitea_ci || echo "gitea_ci already exists"' + ssh "$host" 'sudo install -d -o gitea_ci -g gitea_ci -m 0700 /var/lib/gitea_ci/.ssh' + rsync --rsync-path 'sudo rsync' --chown gitea_ci:gitea_ci --chmod 0600 \ + "$RUNNER_PUBKEY" "$host:/var/lib/gitea_ci/.ssh/authorized_keys" + ssh "$host" "sudo install -d -o root -g root -m 0755 $WEB_ROOT" + + # Deliberately narrower than the API host's grant: the proxy only ever + # receives static files. It gets no systemctl, no binaries, no config. + ssh "$host" "sudo tee /etc/sudoers.d/${APP}_web_gitea_ci >/dev/null" <.conf