convoy: migrate CI to self-hosted axiom runners (briefs 1+2) #132
2 changed files with 26 additions and 15 deletions
|
|
@ -7,7 +7,7 @@ prerequisites:
|
||||||
- CT 111 (`ci-runner`) provisioned and online on axiom (`192.168.68.111`)
|
- CT 111 (`ci-runner`) provisioned and online on axiom (`192.168.68.111`)
|
||||||
- 2× `axiom-runner-*` registered + Idle in Settings → Actions → Runners
|
- 2× `axiom-runner-*` registered + Idle in Settings → Actions → Runners
|
||||||
- `deckhearth_ci` Postgres user + tracking script wired on CT 102
|
- `deckhearth_ci` Postgres user + tracking script wired on CT 102
|
||||||
- `HOMELAB_CI_POSTGRES_BASE_URL` set as a GitHub Actions repo secret (value: `postgres://deckhearth_ci:<pw>@192.168.68.102:5432`)
|
- `HOMELAB_CI_POSTGRES_PASSWORD` set as a GitHub Actions repo secret (password only — `PGHOST`/`PGUSER`/`PGPORT` are hardcoded in the workflow). Earlier draft of this convoy used a single `HOMELAB_CI_POSTGRES_BASE_URL` URL secret, but during Brief 1+2 validation `psql "$URL/postgres"` failed with `invalid option -- '/'` — the URL-parse path was fragile. Split secret + standard `PG*` env vars sidesteps it.
|
||||||
- Repo Settings → Actions → General → **"Require approval for all outside collaborators"** = enabled
|
- Repo Settings → Actions → General → **"Require approval for all outside collaborators"** = enabled
|
||||||
related_axiom_artifacts:
|
related_axiom_artifacts:
|
||||||
- axiom-server/proxmox/ct111/docker-compose.yml
|
- axiom-server/proxmox/ct111/docker-compose.yml
|
||||||
|
|
@ -80,18 +80,21 @@ env:
|
||||||
POSTGRES_URL: postgres://postgres:postgres@localhost:5432/deckhearth_test
|
POSTGRES_URL: postgres://postgres:postgres@localhost:5432/deckhearth_test
|
||||||
```
|
```
|
||||||
|
|
||||||
Proposed:
|
Shipped (post-validation revision):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
env:
|
env:
|
||||||
# HOMELAB_CI_POSTGRES_BASE_URL = postgres://deckhearth_ci:<pw>@192.168.68.102:5432
|
PGHOST: 192.168.68.102
|
||||||
# (no DB name — we create a per-run DB to keep parallel runs isolated)
|
PGPORT: '5432'
|
||||||
PGBASE: ${{ secrets.HOMELAB_CI_POSTGRES_BASE_URL }}
|
PGUSER: deckhearth_ci
|
||||||
|
PGPASSWORD: ${{ secrets.HOMELAB_CI_POSTGRES_PASSWORD }}
|
||||||
DBNAME: ci_run_${{ github.run_id }}_${{ github.run_attempt }}
|
DBNAME: ci_run_${{ github.run_id }}_${{ github.run_attempt }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with: { node-version: '20', cache: npm }
|
with: { node-version: '20', cache: npm }
|
||||||
|
- name: Install postgresql-client
|
||||||
|
run: sudo apt-get update -qq && sudo apt-get install -y -qq postgresql-client
|
||||||
- name: Cache node_modules
|
- name: Cache node_modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|
@ -100,14 +103,16 @@ steps:
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Create per-run database
|
- name: Create per-run database
|
||||||
run: |
|
run: |
|
||||||
psql "$PGBASE/postgres" -c "CREATE DATABASE \"$DBNAME\";"
|
psql -d postgres -c "CREATE DATABASE \"$DBNAME\";"
|
||||||
echo "POSTGRES_URL=$PGBASE/$DBNAME" >> .env.local
|
echo "POSTGRES_URL=postgres://$PGUSER:$PGPASSWORD@$PGHOST:$PGPORT/$DBNAME" >> .env.local
|
||||||
- run: npm run migrate up
|
- run: npm run migrate up
|
||||||
- name: Drop per-run database (always)
|
- name: Drop per-run database (always)
|
||||||
if: always()
|
if: always()
|
||||||
run: psql "$PGBASE/postgres" -c "DROP DATABASE IF EXISTS \"$DBNAME\";"
|
run: psql -d postgres -c "DROP DATABASE IF EXISTS \"$DBNAME\";"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`psql` reads `PG*` env vars automatically so we never need to assemble a connection URL on the psql command line (which was the failure mode in the first validation attempt). `node-pg-migrate` still wants a `POSTGRES_URL`, hence the inline URL written to `.env.local`. The runner is ephemeral so leaking the password into `.env.local` is bounded to that single job.
|
||||||
|
|
||||||
Why per-run DB:
|
Why per-run DB:
|
||||||
- Two PRs migrating in parallel don't collide.
|
- Two PRs migrating in parallel don't collide.
|
||||||
- A failed migration leaves a dirty DB behind — `if: always()` ensures cleanup.
|
- A failed migration leaves a dirty DB behind — `if: always()` ensures cleanup.
|
||||||
|
|
@ -144,7 +149,7 @@ Why per-run DB:
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 1 | CT 111 down → PRs queue indefinitely | medium | Beszel alerts on CT 111 down; D5 revert path documented |
|
| 1 | CT 111 down → PRs queue indefinitely | medium | Beszel alerts on CT 111 down; D5 revert path documented |
|
||||||
| 2 | PAT expires silently → new jobs fail registration | medium | Calendar reminder at PAT mint time (90 days); runner logs surface failure on next restart |
|
| 2 | PAT expires silently → new jobs fail registration | medium | Calendar reminder at PAT mint time (90 days); runner logs surface failure on next restart |
|
||||||
| 3 | Malicious PR exfiltrates from runner | low (repo-scoped, "require approval" enabled) | Repo Settings gate; runner has no creds beyond `secrets.HOMELAB_CI_POSTGRES_BASE_URL` (scoped to `deckhearth_ci` schema, no other DB access) |
|
| 3 | Malicious PR exfiltrates from runner | low (repo-scoped, "require approval" enabled) | Repo Settings gate; runner has no creds beyond `secrets.HOMELAB_CI_POSTGRES_PASSWORD` (scoped to `deckhearth_ci`, CREATEDB but no superuser, no access to other apps' databases) |
|
||||||
| 4 | Per-run DB litter on CT 102 if `if: always()` cleanup itself fails | low | Add a weekly cron on CT 102: `psql ... -c "DROP DATABASE IF EXISTS …" FOREACH ci_run_* older than 7d` |
|
| 4 | Per-run DB litter on CT 102 if `if: always()` cleanup itself fails | low | Add a weekly cron on CT 102: `psql ... -c "DROP DATABASE IF EXISTS …" FOREACH ci_run_* older than 7d` |
|
||||||
| 5 | Cache poisoning across runs (shared `~/.npm` between runner-1 and runner-2) | low | `npm ci` validates against `package-lock.json` checksum; corrupt cache is self-healing |
|
| 5 | Cache poisoning across runs (shared `~/.npm` between runner-1 and runner-2) | low | `npm ci` validates against `package-lock.json` checksum; corrupt cache is self-healing |
|
||||||
| 6 | Two ephemeral runners insufficient for peak load (5+ jobs per PR) | medium | Add `runner-3:` block in CT 111 compose; ~200 MB RAM per slot |
|
| 6 | Two ephemeral runners insufficient for peak load (5+ jobs per PR) | medium | Add `runner-3:` block in CT 111 compose; ~200 MB RAM per slot |
|
||||||
|
|
|
||||||
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
|
|
@ -371,12 +371,18 @@ jobs:
|
||||||
runs-on: [self-hosted, axiom]
|
runs-on: [self-hosted, axiom]
|
||||||
# Postgres lives on CT 102 (axiom homelab). Per-run database keeps parallel
|
# Postgres lives on CT 102 (axiom homelab). Per-run database keeps parallel
|
||||||
# PRs isolated and a `DROP DATABASE` in `always()` keeps the catalog tidy.
|
# PRs isolated and a `DROP DATABASE` in `always()` keeps the catalog tidy.
|
||||||
# `HOMELAB_CI_POSTGRES_BASE_URL` is a repo secret of the shape
|
# `HOMELAB_CI_POSTGRES_PASSWORD` is a repo secret — password only. The
|
||||||
# `postgres://deckhearth_ci:<pw>@192.168.68.102:5432` — no DB name. The
|
|
||||||
# `deckhearth_ci` Postgres role has CREATEDB but no superuser; a
|
# `deckhearth_ci` Postgres role has CREATEDB but no superuser; a
|
||||||
# compromised runner can't reach other apps' databases.
|
# compromised runner can't reach other apps' databases.
|
||||||
|
#
|
||||||
|
# psql picks PGHOST/PGUSER/PGPASSWORD/PGPORT up automatically, so we
|
||||||
|
# don't build a URL and we sidestep any URL-parsing quirks around
|
||||||
|
# special chars in the connection string.
|
||||||
env:
|
env:
|
||||||
PGBASE: ${{ secrets.HOMELAB_CI_POSTGRES_BASE_URL }}
|
PGHOST: 192.168.68.102
|
||||||
|
PGPORT: '5432'
|
||||||
|
PGUSER: deckhearth_ci
|
||||||
|
PGPASSWORD: ${{ secrets.HOMELAB_CI_POSTGRES_PASSWORD }}
|
||||||
DBNAME: ci_run_${{ github.run_id }}_${{ github.run_attempt }}
|
DBNAME: ci_run_${{ github.run_id }}_${{ github.run_attempt }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
@ -400,12 +406,12 @@ jobs:
|
||||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||||
- name: Create per-run database
|
- name: Create per-run database
|
||||||
run: |
|
run: |
|
||||||
psql "$PGBASE/postgres" -c "CREATE DATABASE \"$DBNAME\";"
|
psql -d postgres -c "CREATE DATABASE \"$DBNAME\";"
|
||||||
echo "POSTGRES_URL=$PGBASE/$DBNAME" >> .env.local
|
echo "POSTGRES_URL=postgres://$PGUSER:$PGPASSWORD@$PGHOST:$PGPORT/$DBNAME" >> .env.local
|
||||||
- run: npm run migrate up
|
- run: npm run migrate up
|
||||||
- name: Drop per-run database
|
- name: Drop per-run database
|
||||||
if: always()
|
if: always()
|
||||||
run: psql "$PGBASE/postgres" -c "DROP DATABASE IF EXISTS \"$DBNAME\";"
|
run: psql -d postgres -c "DROP DATABASE IF EXISTS \"$DBNAME\";"
|
||||||
|
|
||||||
test:
|
test:
|
||||||
name: Unit tests (vitest)
|
name: Unit tests (vitest)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue