diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b087b1203..330a76bc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,12 +26,12 @@ jobs: # Intended as a required status check on main together with ui-smoke, # Gitleaks, and one migration replay gate. verify: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 25 steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@v6 with: persist-credentials: false @@ -42,6 +42,9 @@ jobs: cache: npm cache-dependency-path: package-lock.json + - name: Workflow action pin check + run: npm run check:github-actions + - name: Setup Deno uses: denoland/setup-deno@v2 with: @@ -99,12 +102,12 @@ jobs: # rerun does not repeat them. The runner boots its own dev server, so no # build step is needed here. ui-smoke: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 25 steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@v6 with: persist-credentials: false @@ -146,12 +149,12 @@ jobs: # required and reliable, keep this advisory or path-filtered rather than # requiring both replay gates on every PR. db-reset-verify: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@v6 with: persist-credentials: false @@ -193,12 +196,12 @@ jobs: release-browser-matrix: if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') needs: [verify, ui-smoke, db-reset-verify] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 70 steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/codex-autofix-review-comments.yml b/.github/workflows/codex-autofix-review-comments.yml index e34f6c537..8514c28e8 100644 --- a/.github/workflows/codex-autofix-review-comments.yml +++ b/.github/workflows/codex-autofix-review-comments.yml @@ -16,7 +16,7 @@ concurrency: jobs: request-codex-autoresolve: name: Request Codex auto-resolve - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 if: > github.event.pull_request.state == 'open' && contains(github.event.comment.user.login, 'chatgpt-codex-connector') @@ -38,7 +38,7 @@ jobs: if ( sourceBody.includes("codex-autoresolve:") || sourceBody.includes("codex-autoresolve-pr:") || - sourceBody.includes("@codex resolve all review comments") + sourceBody.includes("@codex resolve actionable Codex review findings for this pull request and current head") ) { core.notice("Skipping Codex auto-resolve request because the source comment already looks like an auto-resolve request."); return; @@ -75,7 +75,7 @@ jobs: const prompt = [ marker, ``, - "@codex resolve all review comments using the repository instructions. Always fix P0 and P1 findings. For P2 and lower findings, decide whether each is worth fixing automatically. Fix clear, scoped, low-risk issues with the best minimal change; otherwise reply explaining why the issue is deferred or not actionable. After each fix or decision, resolve the review conversation if supported. Do not use external APIs, paid services, credentials, dependency changes, or broad refactors unless explicitly authorized. Add targeted tests where behavior changes and run the narrowest relevant validation.", + "@codex resolve actionable Codex review findings for this pull request and current head using the repository instructions. Always fix P0 and P1 findings. For P2 and lower findings, decide whether each is worth fixing automatically. Fix clear, scoped, low-risk issues with the best minimal change; otherwise reply explaining why the issue is deferred or not actionable. Do not update the branch from main, address unrelated reviews, broaden scope, or create more than one scoped fix commit unless explicitly asked. After each fix or decision, resolve the review conversation if supported. Do not use external APIs, paid services, credentials, dependency changes, or broad refactors unless explicitly authorized. Add targeted tests where behavior changes and run the narrowest relevant validation.", ].join("\n\n"); try { diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 296b83f7f..475e22254 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -47,11 +47,11 @@ permissions: jobs: app-image: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@v6 with: persist-credentials: false @@ -72,11 +72,11 @@ jobs: NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=placeholder-ci-publishable-key worker-image: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 30 steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/eval-canary.yml b/.github/workflows/eval-canary.yml index 0c2afd5e0..dcf871e53 100644 --- a/.github/workflows/eval-canary.yml +++ b/.github/workflows/eval-canary.yml @@ -55,12 +55,12 @@ env: jobs: eval-canary: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 45 steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/live-drift.yml b/.github/workflows/live-drift.yml index 29ad44b8b..ea8df7fd3 100644 --- a/.github/workflows/live-drift.yml +++ b/.github/workflows/live-drift.yml @@ -23,12 +23,12 @@ env: jobs: live-drift: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/sast.yml b/.github/workflows/sast.yml index a5a2bb4a3..183f81e2f 100644 --- a/.github/workflows/sast.yml +++ b/.github/workflows/sast.yml @@ -24,13 +24,13 @@ jobs: # the merge. Runs OSS-engine only — no Semgrep account or token required. semgrep: name: Semgrep - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 15 container: image: semgrep/semgrep:1.168.0 steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@v6 with: persist-credentials: false diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index cad2d8263..14f4d18af 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -20,11 +20,11 @@ permissions: jobs: gitleaks: name: Gitleaks - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - name: Checkout - uses: actions/checkout@v7 + uses: actions/checkout@v6 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/summary.yml b/.github/workflows/summary.yml index 7caee0390..ca3e00810 100644 --- a/.github/workflows/summary.yml +++ b/.github/workflows/summary.yml @@ -10,7 +10,7 @@ concurrency: jobs: summary: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 10 permissions: issues: write @@ -49,7 +49,7 @@ jobs: - name: Comment with AI summary if: github.event.issue.state == 'open' - uses: peter-evans/create-or-update-comment@v6 + uses: peter-evans/create-or-update-comment@v5 with: token: ${{ secrets.GITHUB_TOKEN }} issue-number: ${{ github.event.issue.number }} diff --git a/package.json b/package.json index fce7d434a..58afcbc87 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,11 @@ "test:e2e:accessibility": "node scripts/run-playwright.mjs tests/ui-accessibility.spec.ts --project=chromium", "test:e2e:chromium": "node scripts/run-playwright.mjs --project=chromium", "test:e2e:visual": "node scripts/run-playwright.mjs --config=playwright.visual.config.ts", - "verify:cheap": "npm run check:runtime && npm run sitemap:check && npm run lint && npm run typecheck && npm run test", + "verify:cheap": "npm run check:runtime && npm run check:github-actions && npm run sitemap:check && npm run lint && npm run typecheck && npm run test", "verify:ui": "npm run check:runtime && npm run test:e2e:chromium", "verify:release": "npm run check:runtime && npm run lint && npm run typecheck && npm run test && npm run build && npm run test:e2e && npm run check:production-readiness && npm run governance:release && npm run eval:quality:release", "ci:env-check": "node scripts/check-ci-env.mjs", + "check:github-actions": "node scripts/check-github-action-pins.mjs", "sitemap:update": "tsx scripts/generate-site-map.ts", "sitemap:check": "tsx scripts/generate-site-map.ts --check", "check:runtime": "tsx scripts/check-runtime.ts", diff --git a/scripts/check-codex-autofix-workflow.mjs b/scripts/check-codex-autofix-workflow.mjs index 75a6a6e34..d7e5ffa20 100644 --- a/scripts/check-codex-autofix-workflow.mjs +++ b/scripts/check-codex-autofix-workflow.mjs @@ -4,6 +4,8 @@ const workflowPath = ".github/workflows/codex-autofix-review-comments.yml"; const workflow = fs.readFileSync(workflowPath, "utf8"); const failures = []; +const scopedResolveCommand = "@codex resolve actionable Codex review findings for this pull request and current head"; +const scopedResolvePrompt = `${scopedResolveCommand} using the repository instructions. Always fix P0 and P1 findings. For P2 and lower findings, decide whether each is worth fixing automatically. Fix clear, scoped, low-risk issues with the best minimal change; otherwise reply explaining why the issue is deferred or not actionable. Do not update the branch from main, address unrelated reviews, broaden scope, or create more than one scoped fix commit unless explicitly asked. After each fix or decision, resolve the review conversation if supported. Do not use external APIs, paid services, credentials, dependency changes, or broad refactors unless explicitly authorized. Add targeted tests where behavior changes and run the narrowest relevant validation.`; const forbiddenPatterns = [ { @@ -22,6 +24,10 @@ const forbiddenPatterns = [ pattern: /^\s*pull_request_review:/m, message: "Do not trigger Codex auto-resolve from whole pull_request_review events.", }, + { + pattern: /@codex resolve all review comments/, + message: "Do not use the broad Codex resolve-all command; use the scoped actionable-findings command.", + }, ]; for (const { pattern, message } of forbiddenPatterns) { @@ -38,8 +44,12 @@ if (!workflow.includes("codex-autoresolve-pr:${pr.number}")) { failures.push("Codex auto-resolve marker must be scoped to the pull request, not the head SHA."); } -if (!workflow.includes('sourceBody.includes("@codex resolve all review comments")')) { - failures.push("Codex auto-resolve workflow must skip comments that already look like resolve requests."); +if (!workflow.includes(`sourceBody.includes("${scopedResolveCommand}")`)) { + failures.push("Codex auto-resolve workflow must skip comments that already look like scoped resolve requests."); +} + +if (!workflow.includes(`"${scopedResolvePrompt}"`)) { + failures.push("Codex auto-resolve workflow must emit the scoped actionable-findings resolve prompt."); } if (failures.length > 0) { diff --git a/scripts/check-github-action-pins.mjs b/scripts/check-github-action-pins.mjs new file mode 100644 index 000000000..04be42f3f --- /dev/null +++ b/scripts/check-github-action-pins.mjs @@ -0,0 +1,65 @@ +import { readdirSync, readFileSync } from "node:fs"; +import path from "node:path"; + +const workflowDir = path.join(process.cwd(), ".github", "workflows"); + +const supportedMajorRanges = new Map([ + [ + "actions/checkout", + { + min: 4, + max: 6, + reason: + "v6 is the currently supported and documented major that this repo has vetted; upgrading to v7 should be a deliberate, reviewed decision.", + }, + ], + [ + "peter-evans/create-or-update-comment", + { + min: 5, + max: 5, + reason: "create-or-update-comment v6 is not a documented published major for this workflow.", + }, + ], +]); + +const usesPattern = /^\s*uses:\s*([^@\s]+)@v(\d+)\s*(?:#.*)?$/; +const runsOnLatestPattern = /^\s*runs-on:\s*ubuntu-latest\s*(?:#.*)?$/; +const failures = []; + +for (const fileName of readdirSync(workflowDir) + .filter((name) => /\.ya?ml$/i.test(name)) + .sort()) { + const filePath = path.join(workflowDir, fileName); + const lines = readFileSync(filePath, "utf8").split(/\r?\n/); + + lines.forEach((line, index) => { + if (runsOnLatestPattern.test(line)) { + failures.push( + `${fileName}:${index + 1}: runs-on uses ubuntu-latest. Pin GitHub-hosted Linux jobs to ubuntu-24.04 so CI is not tied to the moving ubuntu-latest alias.`, + ); + } + + const match = line.match(usesPattern); + if (!match) return; + + const [, action, rawMajor] = match; + const range = supportedMajorRanges.get(action); + if (!range) return; + + const major = Number(rawMajor); + if (!Number.isInteger(major) || major < range.min || major > range.max) { + failures.push( + `${fileName}:${index + 1}: ${action}@v${rawMajor} is outside supported range v${range.min}-v${range.max}. ${range.reason}`, + ); + } + }); +} + +if (failures.length > 0) { + console.error("GitHub Actions pin check failed:"); + for (const failure of failures) console.error(`- ${failure}`); + process.exit(1); +} + +console.log("GitHub Actions pin check passed.");