convoy: forbidden-pattern gate + AGENTS.md docs (briefs 3+4) (#133)
* convoy: forbidden-pattern gate + docs (briefs 3+4) Closes out the migrate-ci-to-self-hosted convoy with the two defensive follow-ups Brief 1+2 (PR #132) intentionally deferred. Brief 3 — Check 8 of `forbidden-patterns` in ci.yml. Greps `.github/workflows/` for `runs-on: ubuntu-latest` and fails unless the match is in the documented allowlist (currently `agent-context-drift.yml` only, per Decision D4). Self-tested locally against the post-migration tree: 0 violations. Renames the job from "Forbidden patterns (7 checks)" → "(8 checks)" and normalizes the older "Check N/6" labels to "N/8" for consistency (the inherited mix of `/6` and `/7` was a known cosmetic from the unify-glass-panel-surfaces convoy). Brief 4 — AGENTS.md § 6 and § 7 updates: - § 6 "CI behavior": Playwright smoke runtime range updated to cover post-migration cold vs. warm cache (was a stale 59s figure from pre-migration ubuntu-latest). - § 6 new top-level bullet "Self-hosted runner pool" alongside "CI minute optimizations" — covers where runners live, where caches are bind-mounted on CT 111, the Postgres rewire on CT 102, and the agent-context-drift.yml exemption + how Check 8 enforces it. - § 7 new bullet for the operational story: PAT rotation cadence + the D5 one-line `sed` revert path for when axiom is offline mid-PR-storm. Cross-references the axiom-server CT 111 README and the Beszel down alert. Convoy doc — status flipped queued → shipped, shipped_in lists both PRs, and follow_ups makes the cleanup-stale-ci-runs-cron + seed-visual-baselines-on-linux items machine-greppable. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: drop placeholder comment now PR #133 number is known Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f228c096fd
commit
e933140560
3 changed files with 82 additions and 11 deletions
|
|
@ -1,8 +1,15 @@
|
||||||
---
|
---
|
||||||
slug: migrate-ci-to-self-hosted
|
slug: migrate-ci-to-self-hosted
|
||||||
status: queued
|
status: shipped
|
||||||
opened: 2026-06-05
|
opened: 2026-06-05
|
||||||
|
shipped: 2026-06-05
|
||||||
owner: rstillw
|
owner: rstillw
|
||||||
|
shipped_in:
|
||||||
|
- PR #132 (Briefs 1+2 — workflow migration + migrate-job rewire)
|
||||||
|
- PR #133 (Briefs 3+4 — forbidden-pattern gate + AGENTS.md docs)
|
||||||
|
follow_ups:
|
||||||
|
- cleanup-stale-ci-runs-cron (weekly GC on CT 102 for ci_run_* DBs older than 7d; Risk #4 defensive)
|
||||||
|
- seed-visual-baselines-on-linux (now easier with axiom; see queued-follow-ups below)
|
||||||
prerequisites:
|
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
|
||||||
|
|
|
||||||
66
.github/workflows/ci.yml
vendored
66
.github/workflows/ci.yml
vendored
|
|
@ -116,7 +116,7 @@ jobs:
|
||||||
# (matches the previous independent-jobs behavior; nicer than
|
# (matches the previous independent-jobs behavior; nicer than
|
||||||
# stop-at-first-failure).
|
# stop-at-first-failure).
|
||||||
forbidden-patterns:
|
forbidden-patterns:
|
||||||
name: Forbidden patterns (7 checks)
|
name: Forbidden patterns (8 checks)
|
||||||
runs-on: [self-hosted, axiom]
|
runs-on: [self-hosted, axiom]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
@ -125,7 +125,7 @@ jobs:
|
||||||
FAIL=0
|
FAIL=0
|
||||||
|
|
||||||
# ---------- (1) No dev endpoints in pages/api ----------
|
# ---------- (1) No dev endpoints in pages/api ----------
|
||||||
echo "::group::Check 1/6: No dev endpoints in pages/api"
|
echo "::group::Check 1/8: No dev endpoints in pages/api"
|
||||||
BAD_PATHS=(
|
BAD_PATHS=(
|
||||||
"pages/api/simple.js"
|
"pages/api/simple.js"
|
||||||
"pages/api/test-auth.js"
|
"pages/api/test-auth.js"
|
||||||
|
|
@ -153,7 +153,7 @@ jobs:
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
|
|
||||||
# ---------- (2) No wildcard CORS in pages/api ----------
|
# ---------- (2) No wildcard CORS in pages/api ----------
|
||||||
echo "::group::Check 2/6: No wildcard CORS in pages/api"
|
echo "::group::Check 2/8: No wildcard CORS in pages/api"
|
||||||
MATCHES=$(grep -rEn 'Access-Control-Allow-(Origin|Methods|Headers)' pages/api/ 2>/dev/null || true)
|
MATCHES=$(grep -rEn 'Access-Control-Allow-(Origin|Methods|Headers)' pages/api/ 2>/dev/null || true)
|
||||||
if [ -n "$MATCHES" ]; then
|
if [ -n "$MATCHES" ]; then
|
||||||
echo "::error::Forbidden CORS headers present under pages/api/. Remove them — same-origin Vercel deployment does not need CORS."
|
echo "::error::Forbidden CORS headers present under pages/api/. Remove them — same-origin Vercel deployment does not need CORS."
|
||||||
|
|
@ -169,7 +169,7 @@ jobs:
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
|
|
||||||
# ---------- (3) No client-side LLM key leakage ----------
|
# ---------- (3) No client-side LLM key leakage ----------
|
||||||
echo "::group::Check 3/6: No client-side LLM key leakage"
|
echo "::group::Check 3/8: No client-side LLM key leakage"
|
||||||
if [ -f pages/api/config/gemini.js ]; then
|
if [ -f pages/api/config/gemini.js ]; then
|
||||||
echo "::error file=pages/api/config/gemini.js::Forbidden config endpoint — do not return API keys to browsers."
|
echo "::error file=pages/api/config/gemini.js::Forbidden config endpoint — do not return API keys to browsers."
|
||||||
FAIL=1
|
FAIL=1
|
||||||
|
|
@ -225,7 +225,7 @@ jobs:
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
|
|
||||||
# ---------- (4) No legacy modal shells ----------
|
# ---------- (4) No legacy modal shells ----------
|
||||||
echo "::group::Check 4/6: No new modal shells without <Modal> primitive"
|
echo "::group::Check 4/8: No new modal shells without <Modal> primitive"
|
||||||
MODAL_FOUND=()
|
MODAL_FOUND=()
|
||||||
while IFS= read -r file; do
|
while IFS= read -r file; do
|
||||||
MODAL_FOUND+=("$file")
|
MODAL_FOUND+=("$file")
|
||||||
|
|
@ -244,7 +244,7 @@ jobs:
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
|
|
||||||
# ---------- (5) No deprecated color aliases ----------
|
# ---------- (5) No deprecated color aliases ----------
|
||||||
echo "::group::Check 5/6: No use of deprecated color aliases"
|
echo "::group::Check 5/8: No use of deprecated color aliases"
|
||||||
PATTERNS=(
|
PATTERNS=(
|
||||||
'gradient-text-purple'
|
'gradient-text-purple'
|
||||||
'gradient-text-pink'
|
'gradient-text-pink'
|
||||||
|
|
@ -278,7 +278,7 @@ jobs:
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
|
|
||||||
# ---------- (6) No stale ownership/collection copy ----------
|
# ---------- (6) No stale ownership/collection copy ----------
|
||||||
echo "::group::Check 6/6: No stale ownership/collection copy"
|
echo "::group::Check 6/8: No stale ownership/collection copy"
|
||||||
STRING_PATTERNS=(
|
STRING_PATTERNS=(
|
||||||
'Mark Owned'
|
'Mark Owned'
|
||||||
'Owned Cards'
|
'Owned Cards'
|
||||||
|
|
@ -307,7 +307,7 @@ jobs:
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
|
|
||||||
# ---------- (7) No bespoke var(--glass-surface-*) inline styles ----------
|
# ---------- (7) No bespoke var(--glass-surface-*) inline styles ----------
|
||||||
echo "::group::Check 7/7: No bespoke var(--glass-surface-*) inline styles"
|
echo "::group::Check 7/8: No bespoke var(--glass-surface-*) inline styles"
|
||||||
# unify-glass-panel-surfaces convoy, Brief 7 (PR sequence after #126).
|
# unify-glass-panel-surfaces convoy, Brief 7 (PR sequence after #126).
|
||||||
# After Briefs 1-6 land, every panel-shaped surface in the app
|
# After Briefs 1-6 land, every panel-shaped surface in the app
|
||||||
# composes via .glass-panel / .glass-panel-strong /
|
# composes via .glass-panel / .glass-panel-strong /
|
||||||
|
|
@ -359,12 +359,60 @@ jobs:
|
||||||
fi
|
fi
|
||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
|
|
||||||
|
# ---------- (8) No drift back to `runs-on: ubuntu-latest` ----------
|
||||||
|
# migrate-ci-to-self-hosted convoy, Brief 3 (2026-06-05). After
|
||||||
|
# Brief 1+2 moved 4 of 5 workflows to `[self-hosted, axiom]` on
|
||||||
|
# CT 111, this check is the regression gate: anything other than
|
||||||
|
# `agent-context-drift.yml` reintroducing `runs-on: ubuntu-latest`
|
||||||
|
# silently re-bills GitHub Actions minutes.
|
||||||
|
#
|
||||||
|
# Allowlist (single entry, explicit):
|
||||||
|
# - .github/workflows/agent-context-drift.yml
|
||||||
|
# That file's weekly cron stays on `ubuntu-latest` per Decision D4
|
||||||
|
# of the convoy: trading ~2 min/month of billed CI for resilience
|
||||||
|
# so the cron runs even when axiom is down for maintenance.
|
||||||
|
#
|
||||||
|
# Operator override: if you DO need to add a fourth always-billed
|
||||||
|
# workflow (rare — discuss it in a convoy first), append the file
|
||||||
|
# to RUNS_ON_ALLOWLIST below in the same PR.
|
||||||
|
echo "::group::Check 8/8: No drift back to runs-on: ubuntu-latest outside agent-context-drift.yml"
|
||||||
|
RUNS_ON_ALLOWLIST=(
|
||||||
|
".github/workflows/agent-context-drift.yml"
|
||||||
|
)
|
||||||
|
RUNS_ON_FOUND=()
|
||||||
|
while IFS= read -r match; do
|
||||||
|
file=$(echo "$match" | cut -d: -f1)
|
||||||
|
skip=false
|
||||||
|
for allowed in "${RUNS_ON_ALLOWLIST[@]}"; do
|
||||||
|
if [ "$file" = "$allowed" ]; then
|
||||||
|
skip=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$skip" = true ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
RUNS_ON_FOUND+=("$match")
|
||||||
|
done < <(grep -rEn '^\s*runs-on:\s*ubuntu-latest' .github/workflows/ 2>/dev/null || true)
|
||||||
|
if [ ${#RUNS_ON_FOUND[@]} -gt 0 ]; then
|
||||||
|
echo "::error::Forbidden \`runs-on: ubuntu-latest\` outside the documented allowlist (currently agent-context-drift.yml only). Use \`runs-on: [self-hosted, axiom]\` to keep CI on the homelab pool (CT 111). See .convoys/migrate-ci-to-self-hosted.md and AGENTS.md § 7 for the revert path if axiom is down."
|
||||||
|
for match in "${RUNS_ON_FOUND[@]}"; do
|
||||||
|
file=$(echo "$match" | cut -d: -f1)
|
||||||
|
lineno=$(echo "$match" | cut -d: -f2)
|
||||||
|
echo "::error file=${file},line=${lineno}::Replace with [self-hosted, axiom] or add this file to the RUNS_ON_ALLOWLIST in ci.yml."
|
||||||
|
done
|
||||||
|
FAIL=1
|
||||||
|
else
|
||||||
|
echo "OK: no drift to ubuntu-latest outside the allowlist."
|
||||||
|
fi
|
||||||
|
echo "::endgroup::"
|
||||||
|
|
||||||
# ---------- Final exit ----------
|
# ---------- Final exit ----------
|
||||||
if [ "$FAIL" -ne 0 ]; then
|
if [ "$FAIL" -ne 0 ]; then
|
||||||
echo "::error::One or more forbidden-pattern checks failed. See annotations above."
|
echo "::error::One or more forbidden-pattern checks failed. See annotations above."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "All 7 forbidden-pattern checks passed."
|
echo "All 8 forbidden-pattern checks passed."
|
||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
name: Migrations apply (node-pg-migrate)
|
name: Migrations apply (node-pg-migrate)
|
||||||
|
|
|
||||||
18
AGENTS.md
18
AGENTS.md
|
|
@ -163,13 +163,19 @@ Code graph is indexed by `user-code-review-graph` MCP (122 files, 628 nodes, 560
|
||||||
Mac-generated baselines will NOT match Linux CI — `playwright.config.js`'s custom `snapshotPathTemplate` has no `{platform}` token, so a Mac update silently overwrites the canonical Linux baseline. Tracked as the queued `seed-visual-baselines-on-linux` convoy (see `.convoys/ship-readiness.md` § Queued convoys).
|
Mac-generated baselines will NOT match Linux CI — `playwright.config.js`'s custom `snapshotPathTemplate` has no `{platform}` token, so a Mac update silently overwrites the canonical Linux baseline. Tracked as the queued `seed-visual-baselines-on-linux` convoy (see `.convoys/ship-readiness.md` § Queued convoys).
|
||||||
- **CI behavior:**
|
- **CI behavior:**
|
||||||
- **Vitest:** the `test:` job in `.github/workflows/ci.yml` runs `npm run test:run` on every PR and push to `main` and is **blocking** (no `|| true`, no `continue-on-error`). A red test job blocks merge.
|
- **Vitest:** the `test:` job in `.github/workflows/ci.yml` runs `npm run test:run` on every PR and push to `main` and is **blocking** (no `|| true`, no `continue-on-error`). A red test job blocks merge.
|
||||||
- **Playwright smoke:** runs on every PR via `preview-smoke.yml`. Gate skip via `pipeline: skip smoke` in the PR body (handled in the `gate:` job's Decide step via env-var routing — see § 7's shell-injection note). Last measured runtime: 59s end-to-end, 3/3 tests pass in 2.9s (PR #18 post-merge run).
|
- **Playwright smoke:** runs on every PR via `preview-smoke.yml`. Gate skip via `pipeline: skip smoke` in the PR body (handled in the `gate:` job's Decide step via env-var routing — see § 7's shell-injection note). Pre-migration runtime: 59s end-to-end on `ubuntu-latest` (PR #18 post-merge run). Post-migration on the axiom pool: cold-cache first run ~6 min (Chromium download); warm cache thereafter ~1–2 min.
|
||||||
- **Screenshot diff:** runs only on PRs touching `pages/**` / `components/**` / `styles/**` / `tailwind.config.js` / `postcss.config.js` via `visual-diff.yml`. **First `Screenshot diff` run after `adopt-playwright-smoke` will fail at the test step** because no baseline exists yet; `continue-on-error: true` swallows the failure and the comment-on-PR step posts "Visual Diff — view run" with empty artifacts. That is the documented Decision-4 end state of `adopt-playwright-smoke`, not a regression — it stays that way until `seed-visual-baselines-on-linux` lands.
|
- **Screenshot diff:** runs only on PRs touching `pages/**` / `components/**` / `styles/**` / `tailwind.config.js` / `postcss.config.js` via `visual-diff.yml`. **First `Screenshot diff` run after `adopt-playwright-smoke` will fail at the test step** because no baseline exists yet; `continue-on-error: true` swallows the failure and the comment-on-PR step posts "Visual Diff — view run" with empty artifacts. That is the documented Decision-4 end state of `adopt-playwright-smoke`, not a regression — it stays that way until `seed-visual-baselines-on-linux` lands.
|
||||||
- **CI minute optimizations (slash-ci-minutes convoy, 2026-06-04):**
|
- **CI minute optimizations (slash-ci-minutes convoy, 2026-06-04):**
|
||||||
- **Doc-only PRs skip ALL of ci.yml + preview-smoke.yml.** Both workflows carry `paths-ignore` for `.convoys/**`, `**/*.md`, `docs/**`, `AGENTS.md`, `.cursor/**`, and `README.md`. A pure-docs PR triggers zero GitHub Actions jobs (Vercel still builds — it's not on the Actions billing). `visual-diff.yml` was already cost-conscious via a positive `paths:` allowlist and is unchanged.
|
- **Doc-only PRs skip ALL of ci.yml + preview-smoke.yml.** Both workflows carry `paths-ignore` for `.convoys/**`, `**/*.md`, `docs/**`, `AGENTS.md`, `.cursor/**`, and `README.md`. A pure-docs PR triggers zero GitHub Actions jobs (Vercel still builds — it's not on the Actions billing). `visual-diff.yml` was already cost-conscious via a positive `paths:` allowlist and is unchanged.
|
||||||
- **The 6 grep-only forbidden-* jobs collapsed into one.** They previously ran as 6 independent jobs (each with its own `actions/checkout`); the consolidated `forbidden-patterns` job runs all 6 checks as labeled `::group::` sections in a single bash step, with a FAIL flag at the bottom so every violation across all 6 checks still surfaces in one run (same diagnostic behavior, ~5/6 of the per-PR checkout overhead removed). The 6 original job names (`forbidden-endpoints`, `forbidden-cors-headers`, `forbidden-client-side-llm-keys`, `forbidden-modal-shell-without-primitive`, `forbidden-deprecated-color-aliases`, `forbidden-stale-strings`) no longer appear in the checks list — references in this file (e.g. CI job `forbidden-stale-strings` blocks ...) are now informational, not check-name lookups. `pr-health-rollup.yml` was unaffected because it only looks up `Lint` and `Schema map up to date` by name.
|
- **The 6 grep-only forbidden-* jobs collapsed into one.** They previously ran as 6 independent jobs (each with its own `actions/checkout`); the consolidated `forbidden-patterns` job runs all 6 checks as labeled `::group::` sections in a single bash step, with a FAIL flag at the bottom so every violation across all 6 checks still surfaces in one run (same diagnostic behavior, ~5/6 of the per-PR checkout overhead removed). The 6 original job names (`forbidden-endpoints`, `forbidden-cors-headers`, `forbidden-client-side-llm-keys`, `forbidden-modal-shell-without-primitive`, `forbidden-deprecated-color-aliases`, `forbidden-stale-strings`) no longer appear in the checks list — references in this file (e.g. CI job `forbidden-stale-strings` blocks ...) are now informational, not check-name lookups. `pr-health-rollup.yml` was unaffected because it only looks up `Lint` and `Schema map up to date` by name.
|
||||||
- **`node_modules` cached between runs** in lint / test / migrate / preview-smoke / visual-diff. Keyed on `package-lock.json` hash so any dep change invalidates correctly. Cuts `npm ci` from ~30-45s to ~3-5s on cache hit. `actions/setup-node@v4`'s built-in `cache: npm` is layered above this (caches `~/.npm`) — both stay because the `setup-node` cache helps on cache-miss days too.
|
- **`node_modules` cached between runs** in lint / test / migrate / preview-smoke / visual-diff. Keyed on `package-lock.json` hash so any dep change invalidates correctly. Cuts `npm ci` from ~30-45s to ~3-5s on cache hit. `actions/setup-node@v4`'s built-in `cache: npm` is layered above this (caches `~/.npm`) — both stay because the `setup-node` cache helps on cache-miss days too.
|
||||||
- **Playwright browsers cached** in `preview-smoke.yml` + `visual-diff.yml`. Keyed on the resolved `@playwright/test` version from `package-lock.json`. Cache invalidates automatically on any Playwright version bump. On cache hit, only the system deps install (`npx playwright install-deps chromium`) runs — saves ~15-25s/run.
|
- **Playwright browsers cached** in `preview-smoke.yml` + `visual-diff.yml`. Keyed on the resolved `@playwright/test` version from `package-lock.json`. Cache invalidates automatically on any Playwright version bump. On cache hit, only the system deps install (`npx playwright install-deps chromium`) runs — saves ~15-25s/run.
|
||||||
|
- **Self-hosted runner pool (migrate-ci-to-self-hosted convoy, 2026-06-05):**
|
||||||
|
- **4 of 5 workflows run on the axiom homelab.** `ci.yml`, `preview-smoke.yml`, `visual-diff.yml`, `pr-health-rollup.yml` use `runs-on: [self-hosted, axiom]` and execute on CT 111 in the `axiom-server` Proxmox homelab (`axiom-runner-1..4`, registered org-scoped to `stwl-labs`, ephemeral one-job-per-container via `myoung34/github-runner`). Net effect: tcg-vault CI no longer consumes GitHub Actions minutes for those four workflows.
|
||||||
|
- **`agent-context-drift.yml` deliberately stays on `ubuntu-latest`** per Decision D4 of the convoy — it's a weekly cron, costs ~2 min/month, and must run even when axiom is down for maintenance. **Check 8 of `forbidden-patterns` enforces this** as a strict allowlist (anything else reintroducing `runs-on: ubuntu-latest` fails CI).
|
||||||
|
- **Cache mounts live on the CT 111 host** and are bind-mounted into every runner container, so they survive across the ephemeral-runner lifecycle and are shared across `axiom-runner-1..4`. Paths (on CT 111): `/opt/appdata/gha-runner/shared-cache/{npm,pnpm,yarn,pip,playwright,buildx}` and the per-runner workdirs under `/opt/appdata/gha-runner/runner-N/`. The `actions/cache@v4` keys above still apply on top — the bind mounts just keep the underlying tooling caches (`~/.npm`, `~/.cache/ms-playwright`) primed across jobs.
|
||||||
|
- **Migrate job uses CT 102 shared Postgres** instead of an in-runner `services.postgres` container. `HOMELAB_CI_POSTGRES_PASSWORD` repo secret (password only — `PGHOST`/`PGUSER`/`PGPORT` are hardcoded in `ci.yml`). Each run creates a per-run database named `ci_run_${run_id}_${run_attempt}` and drops it in an `if: always()` cleanup step so failed migrations don't leak DBs. The `deckhearth_ci` role has `CREATEDB` but no superuser; a compromised runner can't reach other apps' databases on CT 102.
|
||||||
|
- **Cross-references:** convoy decisions + risks in [`.convoys/migrate-ci-to-self-hosted.md`](.convoys/migrate-ci-to-self-hosted.md); homelab-side infra in `axiom-server/proxmox/ct111/README.md`; revert path in § 7 below.
|
||||||
- **Manual QA:** `TESTING_GUIDE.md` still applies for flows not yet covered by automated tests (scanner camera path, card-import jobs, multi-step UI wizards). The automated smoke + visual suite is steadily eclipsing it; `TESTING_GUIDE.md` will be renamed to `docs/MANUAL_QA.md` and trimmed to truly-manual-only flows in a future cleanup convoy (see `.convoys/ship-readiness.md` § Role-doc-writer findings).
|
- **Manual QA:** `TESTING_GUIDE.md` still applies for flows not yet covered by automated tests (scanner camera path, card-import jobs, multi-step UI wizards). The automated smoke + visual suite is steadily eclipsing it; `TESTING_GUIDE.md` will be renamed to `docs/MANUAL_QA.md` and trimmed to truly-manual-only flows in a future cleanup convoy (see `.convoys/ship-readiness.md` § Role-doc-writer findings).
|
||||||
|
|
||||||
## 7. Deployment
|
## 7. Deployment
|
||||||
|
|
@ -183,6 +189,16 @@ Code graph is indexed by `user-code-review-graph` MCP (122 files, 628 nodes, 560
|
||||||
|
|
||||||
Do not log or echo the value. If the operator rotates the token in the Vercel dashboard, re-seed the GitHub secret via `gh secret set VERCEL_AUTOMATION_BYPASS_SECRET --body "<new value>"`. See `.convoys/fix-vercel-deployment-protection-in-ci.md` and `.convoys/adopt-playwright-smoke.md`.
|
Do not log or echo the value. If the operator rotates the token in the Vercel dashboard, re-seed the GitHub secret via `gh secret set VERCEL_AUTOMATION_BYPASS_SECRET --body "<new value>"`. See `.convoys/fix-vercel-deployment-protection-in-ci.md` and `.convoys/adopt-playwright-smoke.md`.
|
||||||
- **Shell-injection hardening in workflow YAML.** Never inline `${{ github.event.* }}` directly into a `run:` block — route the value through the step's `env:` block and quote it (`"$VAR_NAME"`) in shell. PR #17's CI validation caught a real syntax error from a PR body containing `(` because the gate-job's Decide step inlined `${{ github.event.pull_request.body }}` straight into bash; commit `b6f8688` swept both `preview-smoke.yml` and `visual-diff.yml` to the `env:` + quoted-shell pattern. This is GitHub's official Security Hardening guidance ("Security hardening for GitHub Actions" → "Using a third-party action"). Apply to any new workflow that reads PR body / title / branch name / commit messages in shell.
|
- **Shell-injection hardening in workflow YAML.** Never inline `${{ github.event.* }}` directly into a `run:` block — route the value through the step's `env:` block and quote it (`"$VAR_NAME"`) in shell. PR #17's CI validation caught a real syntax error from a PR body containing `(` because the gate-job's Decide step inlined `${{ github.event.pull_request.body }}` straight into bash; commit `b6f8688` swept both `preview-smoke.yml` and `visual-diff.yml` to the `env:` + quoted-shell pattern. This is GitHub's official Security Hardening guidance ("Security hardening for GitHub Actions" → "Using a third-party action"). Apply to any new workflow that reads PR body / title / branch name / commit messages in shell.
|
||||||
|
- **CI runs on the axiom homelab (CT 111).** Four of the five workflows execute on `[self-hosted, axiom]` runners managed in the `axiom-server` repo (`proxmox/ct111/`). Day-to-day this is invisible — pushes still trigger jobs and Vercel still builds previews — but two operational notes matter:
|
||||||
|
1. **PAT rotation.** The runners authenticate to GitHub via an org-scoped PAT stored on CT 111 at `/opt/appdata/gha-runner/.env` (key `GH_PAT`, scopes `admin:org`, `repo`, `workflow`). Rotate every 90 days. After updating the value on CT 111, run `./proxmox/scripts/sync.sh restart 111` to re-register all 4 runners. If the PAT lapses silently, new jobs fail registration immediately; check `./proxmox/scripts/sync.sh logs 111 gha-runner-1` for `Http response code: NotFound` to confirm.
|
||||||
|
2. **1-line revert path (D5) — when axiom is offline mid-PR-storm.** If CT 111 is down for maintenance, hardware swap, or any reason, and a hot fix needs CI to land, swap every `[self-hosted, axiom]` back to `ubuntu-latest`:
|
||||||
|
|
||||||
|
```
|
||||||
|
sed -i '' 's/\[self-hosted, axiom\]/ubuntu-latest/g' .github/workflows/*.yml
|
||||||
|
# macOS sed needs the empty -i '' argument; on Linux it's `sed -i 's/...//g' ...`.
|
||||||
|
```
|
||||||
|
|
||||||
|
This re-bills GitHub Actions minutes for the duration of the outage. Commit the change directly to `main` (or to the affected PR's branch), let CI run, and revert the sed result once axiom is back. The `forbidden-patterns` Check 8 will block the next normal PR until the revert lands — that's intentional: the gate exists exactly to surface this drift, not to silently re-bill minutes for weeks. Beszel alerts on CT 111 down (axiom-server CT 101) so you usually know before a PR notices.
|
||||||
|
|
||||||
## 8. Code graph
|
## 8. Code graph
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue