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