diff --git a/.github/workflows/ingestion-autopilot.yml b/.github/workflows/ingestion-autopilot.yml index a7276509fc..cd1ff5c576 100644 --- a/.github/workflows/ingestion-autopilot.yml +++ b/.github/workflows/ingestion-autopilot.yml @@ -4,20 +4,20 @@ # or an explicitly requested recovery fails. # # ENABLED IN DRY-RUN: the 6-hourly schedule is active, but recovery only APPLIES when -# BOTH the dispatch input `apply=true` AND repo variable INGESTION_AUTOPILOT_APPLY == +# BOTH the dispatch payload `apply=true` AND repo variable INGESTION_AUTOPILOT_APPLY == # "true". The variable is intentionally unset, so scheduled runs are read-only probes # that alert on detected stuck/stranded work, unreachable Supabase, or (with apply) # failed recovery. To allow real recovery after reviewing a dry-run, set -# INGESTION_AUTOPILOT_APPLY=true and dispatch with apply=true. +# INGESTION_AUTOPILOT_APPLY=true and send an `ingestion-autopilot` +# repository dispatch with `client_payload.apply=true`. name: Ingestion Autopilot on: - workflow_dispatch: - inputs: - apply: - description: "Apply recovery (requires repo var INGESTION_AUTOPILOT_APPLY=true)" - required: false - default: "false" + # repository_dispatch always loads this workflow from the trusted default + # branch. Do not replace it with workflow_dispatch: manual branch selection + # would let same-repository writers alter a secret-bearing workflow definition. + repository_dispatch: + types: [ingestion-autopilot] schedule: # Every 6 hours. - cron: "0 */6 * * *" @@ -35,7 +35,6 @@ env: SUPABASE_PROJECT_REF: sjrfecxgysukkwxsowpy SUPABASE_PROJECT_NAME: Clinical KB Database NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: placeholder-ci-anon-key - SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} jobs: ingestion-autopilot: @@ -48,6 +47,8 @@ jobs: persist-credentials: false - name: Preflight required secrets + env: + SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} run: | if [ -z "$SUPABASE_SERVICE_ROLE_KEY" ]; then echo "::error::Ingestion autopilot cannot run — missing repo secret SUPABASE_SERVICE_ROLE_KEY" @@ -70,8 +71,9 @@ jobs: - name: Run autopilot id: autopilot env: - # Apply only when BOTH the dispatch input and the repo variable allow it. - APPLY_REQUESTED: ${{ github.event.inputs.apply }} + SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} + # Apply only when BOTH the trusted dispatch payload and repo variable allow it. + APPLY_REQUESTED: ${{ github.event.client_payload.apply || 'false' }} APPLY_ALLOWED: ${{ vars.INGESTION_AUTOPILOT_APPLY }} run: | if [ "$APPLY_REQUESTED" = "true" ] && [ "$APPLY_ALLOWED" = "true" ]; then @@ -81,7 +83,7 @@ jobs: echo "Scheduled dry-run; alert if recoverable work is detected." npm run ingestion:autopilot -- --alert-on-stuck else - echo "Dry-run (set repo var INGESTION_AUTOPILOT_APPLY=true and dispatch apply=true to recover)." + echo "Dry-run (set repo var INGESTION_AUTOPILOT_APPLY=true and dispatch client_payload.apply=true to recover)." npm run ingestion:autopilot fi diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 19fb773163..4e2967bb02 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -588,6 +588,7 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-02 | claude/ds-v2-values | d33721b436c05014e8ef1f362a945fe9574e676e | DS V2 PR-V values | Wave 2 self-verified before push: 5a close-out (no value deltas remain after decision J2; contract metrics unchanged at rawColorLiterals 2 / literalShadowClasses 0 / legacyTapClasses 0), 5b tap knob 2.75rem to 3rem in @theme with --tap-min reduced to var(--spacing-tap) across 426 *-tap sites and three pins flipped in-commit, 5c --radius-md 8px to 10px across 243 rounded-md sites with the 4px-grid pin naming both half-steps and 14 arbitrary radius literals absorbed. Two carve-outs recorded in SPEC 4.10/4.6: the phone composer stays 44px below 431px, and the 2px mode-nav hairline stays off the ladder. An offline.html tap bump was reverted before push because it would have forced a service-worker CACHE_VERSION bump. Known-red local gate: tests/ui-smoke.spec.ts:2221 phone runway pin fails on unmodified origin/main here too (maxOffset 99 at base vs 111 at this head, reproduced in a pristine capture worktree) while CI run 30720360137 is green on the same tree - plan finding L, CI binding. | typecheck exit 0; verify:pr-local PASS with 4880 passed 3 skipped (4883) in 471 files, production build compiled, client-bundle secret scan passed, eval:rag:offline fixture line plus 574/574 (better than the J4 known-red baseline); verify:ui 346 passed 1 failed (the known-red phone runway pin); verify:phone-chrome --full=never contracts 116/116 and ui-tools 87/87 with the same single known-red test; test:e2e:critical 15/15; design-system contract, branch-review-ledger, outstanding-issues, sitemap, docs link/index/inventory/scripts all exit 0 | | 2026-08-02 | claude/ds-v2-values | 48012d359b84daae347201697c82f3e433c182c5 | PR #1571 review-and-fix (ds-v2-values tap/radius) | fixed: Tools submit + account-setup close onto h-tap; phone composer input 44px pin; gate 2 demoted to implemented-partial; SPEC 407→426; short-runway/short-answer smoke pins retuned; verify:cheap + verify:pr-local + 2 smoke tests green; CI Production UI in progress | verify:cheap 471/4879; verify:pr-local build+fixtures; test:e2e 2 phone smoke passed; design-system-contract raw 2/0/0 | | 2026-08-02 | claude/ds-v2-values | d2a29198415292a28ed1dbf1c740c94f6c12ddb3 | PR #1571 review-and-fix (ds-v2-values tap/radius) | fixed+clarified: gate2 objective acknowledges h-10/unwired audit not blocked; overlay 430px assertion scoped to media block; prior tip 48012d35 Production UI+PR required green | verify:cheap; verify:pr-local; 2 smoke; UI overlay contract; CI Production UI (1)(2)(3)+PR required SUCCESS on 48012d35 | +| 2026-08-02 | codex/fix-manual-workflow-service-role-key-exposure / PR #1572 | 6cc0b6ffdae3b515a05a17f2a87e5f51e5347263 | PR review + fix (ingestion-autopilot service-role exposure) | Reviewed secret-hardening: workflow/job-level SUPABASE_SERVICE_ROLE_KEY removed, manual dispatch limited to default branch, checkout pinned to default_branch, secret scoped to Preflight + Run autopilot only. Fixed CI blockers: merged origin/main (branch was ~1144 behind; Gitleaks needed run-gitleaks-pinned.mjs), registered tests/ingestion-autopilot-workflow.test.ts in test:ci-workflows, Prettier + stronger step-only secret assertions. No P0/P1 residual in the hardened workflow; residual risk is intentional inability to dry-run workflow_dispatch from non-default branches until merge. | npm run test:ci-workflows 206/206; focused ci-cache-safety + ingestion-autopilot-workflow 21/21; check:github-actions; prettier --check; git diff --check. No OpenAI/Supabase/provider calls. | | 2026-08-02 | claude/ds-v2-answer-safety | 8ad91e3f0104b89b83a54255687408cae574ee88 | DS V2 PR-E slices 6+7+8: answer safety, form foundation, announcements | Clinical governance review: no P0; 8 findings fixed in-branch; P1-1 strengthened; #208/#209/#210 deferred and recorded. Zero product imports - nothing adopted. | verify:pr-local exit 0 (475 files / 4960 passed, offline RAG 23 suites / 574 passed); verify:ui 342 passed / 5 failed not attributable (no product import); e2e:critical 15 passed | | 2026-08-02 | claude/ds-v2-architecture | 157373700281f81f136275c56c9a82fb22776d99 | PR-Arch Wave4 motion/z/overlays/print | local gates green; frontend-ui-reviewer API-limited — glance required before auto-merge | unit 4972p; e2e:critical 15p; verify:ui 347p; verify:pr-local 0; eval:rag:offline pass | | 2026-08-02 | claude/ds-v2-architecture | 93bb4b4756a7fad22f93008325f2c0f72471b0db | PR-Arch Wave4 motion/z/overlays/print | local gates green; frontend-ui-reviewer API-limited — glance required before auto-merge | unit 4972p; e2e:critical 15p; verify:ui 347p; verify:pr-local 0; eval:rag:offline pass | @@ -602,3 +603,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-02 | codex/mcp-config-hardening-merge | 30aac79e655225f41006d5739fc4e91ef7791514 | MCP Cloud config hardening | Supersedes prior review; synced main and retained parser hardening with Windows Cloud test coverage | check:codex-cloud; Cloud/Python tests 22 passed | | 2026-08-03 | claude/ds-v2-adopt | 407c8e74a240fbc2e0469b5a8334f55afd61e60d | bugbot PR #1595 | findings: P2 empty-sources fallback fixed; no P0/P1; residuals #217/#224 EmptyState heading, clipboard metadata unwired, #216 AnswerCard deferred | npm test 5061 passed; vitest answer surfaces 112 passed; no cursor[bot] Bugbot threads on PR | | 2026-08-03 | claude/ds-v2-adopt | cb898bec11082c533aae5f42fbd415de1ff0f5e4 | review-and-fix PR #1595 | fixed P1 stale Open source nav + P2 clipboard metadata + RadioGroup id; dispositioned #225 favourites triple live; merge-tree clean | vitest 111 passed (answer-copy, cited-href, clipboard-product, ui-v2-components, ui-v2-answer-safety) | +| 2026-08-04 | codex/fix-manual-workflow-service-role-key-exposure / PR #1572 | 6cc0b6ffdae3b515a05a17f2a87e5f51e5347263 | PR review + fix (ingestion-autopilot service-role exposure) (supersedes 2026-08-02) | Supersedes the earlier row to record decisive gate results; historical review outcome otherwise unchanged. | PASS: npm run test:ci-workflows — 206/206 passed; PASS: focused ci-cache-safety + ingestion-autopilot-workflow — 21/21 passed; PASS: npm run check:github-actions — GitHub Actions pin check passed; PASS: prettier --check — all matched files use Prettier code style; PASS: git diff --check — clean. No provider calls. | diff --git a/package.json b/package.json index cd7dd021bc..73b2eeafe1 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "test:coverage": "node scripts/run-vitest.mjs run --coverage", "test:coverage:node": "node scripts/run-vitest.mjs run --project=node --coverage", "test:coverage:ui": "node scripts/run-vitest.mjs run --project=jsdom --coverage", - "test:ci-workflows": "node scripts/run-vitest.mjs run tests/ci-cache-safety.test.ts tests/authenticated-live-workflow.test.ts tests/codex-autofix-workflow.test.ts tests/eval-canary-workflow.test.ts tests/container-ci-contract.test.ts tests/test-runner-safety.test.ts tests/installed-lock-parity.test.ts tests/railway-config.test.ts tests/ingestion-autopilot.test.ts tests/check-lighthouse-budget.test.ts tests/offline-release-profile.test.ts", + "test:ci-workflows": "node scripts/run-vitest.mjs run tests/ci-cache-safety.test.ts tests/authenticated-live-workflow.test.ts tests/codex-autofix-workflow.test.ts tests/eval-canary-workflow.test.ts tests/container-ci-contract.test.ts tests/test-runner-safety.test.ts tests/installed-lock-parity.test.ts tests/railway-config.test.ts tests/ingestion-autopilot.test.ts tests/ingestion-autopilot-workflow.test.ts tests/check-lighthouse-budget.test.ts tests/offline-release-profile.test.ts", "test:e2e": "node scripts/run-playwright.mjs", "test:e2e:all": "node scripts/run-playwright.mjs", "test:e2e:accessibility": "node scripts/run-playwright.mjs tests/ui-accessibility.spec.ts --project=chromium", diff --git a/tests/ingestion-autopilot-workflow.test.ts b/tests/ingestion-autopilot-workflow.test.ts new file mode 100644 index 0000000000..4722171449 --- /dev/null +++ b/tests/ingestion-autopilot-workflow.test.ts @@ -0,0 +1,50 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { describe, expect, it } from "vitest"; + +const workflow = readFileSync(join(process.cwd(), ".github", "workflows", "ingestion-autopilot.yml"), "utf8").replace( + /\r\n/g, + "\n", +); + +describe("ingestion autopilot workflow secret handling", () => { + it("does not expose the Supabase service-role key to checkout or install steps", () => { + // Workflow-level and job-level env both use fewer than 10 spaces; only + // step-scoped env under Preflight / Run autopilot should inject the secret. + expect(workflow).not.toMatch(/^(?: {0,6})env:\n(?: {2,8}[^\n]+\n)* {2,8}SUPABASE_SERVICE_ROLE_KEY:/m); + + expect(workflow).not.toMatch(/^ workflow_dispatch:/m); + expect(workflow).toContain("repository_dispatch:"); + expect(workflow).toContain("types: [ingestion-autopilot]"); + expect(workflow).toContain("APPLY_REQUESTED: ${{ github.event.client_payload.apply || 'false' }}"); + + const secretLines = workflow.split("\n").filter((line) => line.includes("SUPABASE_SERVICE_ROLE_KEY:")); + expect(secretLines).toEqual([ + " SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}", + " SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}", + ]); + + const installIndex = workflow.indexOf(" - name: Install dependencies"); + const checkoutIndex = workflow.indexOf(" - name: Checkout"); + const preflightIndex = workflow.indexOf(" - name: Preflight required secrets"); + const setupIndex = workflow.indexOf(" - name: Setup Node.js"); + const autopilotIndex = workflow.indexOf(" - name: Run autopilot"); + expect(checkoutIndex).toBeGreaterThan(-1); + expect(preflightIndex).toBeGreaterThan(checkoutIndex); + expect(setupIndex).toBeGreaterThan(preflightIndex); + expect(installIndex).toBeGreaterThan(-1); + expect(autopilotIndex).toBeGreaterThan(installIndex); + + const checkoutBlock = workflow.slice(checkoutIndex, preflightIndex); + const preflightBlock = workflow.slice(preflightIndex, setupIndex); + expect(checkoutBlock).not.toContain("SUPABASE_SERVICE_ROLE_KEY"); + expect(preflightBlock).toContain("SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}"); + + const installBlock = workflow.slice(installIndex, autopilotIndex); + expect(installBlock).not.toContain("SUPABASE_SERVICE_ROLE_KEY"); + + const autopilotBlock = workflow.slice(autopilotIndex); + expect(autopilotBlock).toContain("SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}"); + }); +});