From 255a12d271b78863a2fd199d15f243905e438bdf Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 13 Oct 2026 11:50:27 +0200 Subject: [PATCH 1/3] ci: centralize Claude and Codex model ids in .github/ai-models.json Co-Authored-By: Claude Opus 5.5 --- .github/ai-models.json | 11 +++++++++++ .github/scripts/ai-review/post-review.ts | 4 ++-- .github/workflows/ai-review.yml | 23 +++++++++++++++-------- apps/cli/scripts/propose-release-notes.ts | 6 +++++- 4 files changed, 33 insertions(+), 11 deletions(-) create mode 100644 .github/ai-models.json diff --git a/.github/ai-models.json b/.github/ai-models.json new file mode 100644 index 0000000000..370b524b7f --- /dev/null +++ b/.github/ai-models.json @@ -0,0 +1,11 @@ +{ + "$comment": "Model IDs by family and tier. A new model can also need the pinned Claude Code or Codex CLI version in .github/workflows/ai-review.yml bumped.", + "claude": { + "opus": "claude-opus-5-5", + "sonnet": "claude-sonnet-5-5", + "haiku": "claude-haiku-5-5" + }, + "codex": { + "sol": "gpt-6.1-sol" + } +} diff --git a/.github/scripts/ai-review/post-review.ts b/.github/scripts/ai-review/post-review.ts index 47a73a943e..6a7d1dbd1e 100644 --- a/.github/scripts/ai-review/post-review.ts +++ b/.github/scripts/ai-review/post-review.ts @@ -603,8 +603,8 @@ export function renderInlineComment(finding: MergedFinding): string { export interface ReviewFooterInfo { trigger: Trigger; runUrl: string; - /** e.g. `` `claude-fable-5` + `gpt-5.6-sol` ``. Sourced from the workflow's - * model env vars so there's one source of truth for the model names. */ + /** e.g. `` `claude-fable-5` + `gpt-5.6-sol` ``. Sourced from + * `.github/ai-models.json` through the workflow's model env vars. */ modelsFooter: string; } diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index 768cdf34b5..c38593ad91 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -24,12 +24,6 @@ on: permissions: {} -# One source of truth for the model names; resolve, claude-review, codex-review, and -# post-review's footer all read these instead of hardcoding them separately. -env: - CLAUDE_MODEL: claude-opus-5-5 - CODEX_MODEL: gpt-6.1-sol - # Non-command issue_comment events fire for every comment on every PR, so grouping by PR number # alone would let any comment cancel an in-flight review; give those runs their own per-run # group instead. The command check is exact equality (mirroring resolve.ts), so a near-miss like @@ -125,6 +119,9 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Load AI models + run: jq -r '"CLAUDE_MODEL=\(.claude.opus)", "CODEX_MODEL=\(.codex.sol)"' trusted/.github/ai-models.json >> "$GITHUB_ENV" + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: # The PR head's own `.bun-version` is untrusted — it could select a @@ -272,6 +269,9 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Load AI models + run: jq -r '"CLAUDE_MODEL=\(.claude.opus)", "CODEX_MODEL=\(.codex.sol)"' .github/ai-models.json >> "$GITHUB_ENV" + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: ".bun-version" @@ -383,6 +383,9 @@ jobs: fetch-depth: 0 persist-credentials: false + - name: Load AI models + run: jq -r '"CLAUDE_MODEL=\(.claude.opus)", "CODEX_MODEL=\(.codex.sol)"' trusted/.github/ai-models.json >> "$GITHUB_ENV" + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: # The PR head's own `.bun-version` is untrusted; read it from trusted. @@ -531,6 +534,10 @@ jobs: with: ref: develop persist-credentials: false + + - name: Load AI models + run: jq -r '"CLAUDE_MODEL=\(.claude.opus)", "CODEX_MODEL=\(.codex.sol)"' .github/ai-models.json >> "$GITHUB_ENV" + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: ".bun-version" @@ -556,6 +563,6 @@ jobs: PR_DIFF_PATH: /tmp/ai-review/pr.diff TRIGGER: ${{ needs.resolve.outputs.trigger }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - # CLAUDE_MODEL / CODEX_MODEL are inherited from the workflow-level `env:` block above, - # so the footer never drifts from what actually ran. + # CLAUDE_MODEL / CODEX_MODEL come from the "Load AI models" step, the same source the + # review jobs read, so the footer never drifts from what actually ran. run: bun .github/scripts/ai-review/post-review.ts post diff --git a/apps/cli/scripts/propose-release-notes.ts b/apps/cli/scripts/propose-release-notes.ts index 53a67338a0..a4f90e8c3e 100644 --- a/apps/cli/scripts/propose-release-notes.ts +++ b/apps/cli/scripts/propose-release-notes.ts @@ -14,13 +14,17 @@ import path from "node:path"; import process from "node:process"; import { parseArgs } from "node:util"; +const aiModels: { claude: { haiku: string } } = JSON.parse( + await Bun.file(new URL("../../../.github/ai-models.json", import.meta.url)).text(), +); + const { values } = parseArgs({ options: { tag: { type: "string" }, "dry-run": { type: "boolean", default: false }, apply: { type: "boolean", default: false }, "render-only": { type: "boolean", default: false }, - model: { type: "string", default: "claude-haiku-4-5-20251001" }, + model: { type: "string", default: aiModels.claude.haiku }, }, strict: true, }); From 0418c41354ff84f35d6701bd12c96f54ac0432d5 Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 13 Oct 2026 12:27:49 +0200 Subject: [PATCH 2/3] ci: load AI models once per review run and type-check the release-notes import Co-Authored-By: Claude Opus 5.5 --- .github/ai-models.json | 2 +- .github/workflows/ai-review.yml | 31 +++++++++++++---------- apps/cli/docs/release-process.md | 1 + apps/cli/scripts/propose-release-notes.ts | 5 +--- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/.github/ai-models.json b/.github/ai-models.json index 370b524b7f..73431a1116 100644 --- a/.github/ai-models.json +++ b/.github/ai-models.json @@ -1,5 +1,5 @@ { - "$comment": "Model IDs by family and tier. A new model can also need the pinned Claude Code or Codex CLI version in .github/workflows/ai-review.yml bumped.", + "$comment": "Model IDs by family and tier. A new model can also need the Claude Code or Codex CLI version pinned in the workflows that read this file bumped.", "claude": { "opus": "claude-opus-5-5", "sonnet": "claude-sonnet-5-5", diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index c38593ad91..856c60746c 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -65,6 +65,9 @@ jobs: pr_number: ${{ steps.resolve.outputs.pr_number }} head_ref: ${{ steps.resolve.outputs.head_ref }} trigger: ${{ steps.resolve.outputs.trigger }} + # Read once here so every job in the run uses the same models, even if the file changes mid-run. + claude_model: ${{ steps.models.outputs.claude_model }} + codex_model: ${{ steps.models.outputs.codex_model }} steps: # Pinned to the default ref explicitly — this job must keep running only trusted repo code, # even though the pull_request trigger above hands it PR-authored event payloads. @@ -87,6 +90,9 @@ jobs: COMMENT_AUTHOR_ASSOCIATION: ${{ github.event.comment.author_association }} COMMENT_BODY: ${{ github.event.comment.body }} run: bun .github/scripts/ai-review/resolve.ts + - name: Load AI models + id: models + run: jq -r '"claude_model=\(.claude.opus)", "codex_model=\(.codex.sol)"' .github/ai-models.json >> "$GITHUB_OUTPUT" claude-review: name: Claude review @@ -102,6 +108,8 @@ jobs: permissions: contents: read pull-requests: read + env: + CLAUDE_MODEL: ${{ needs.resolve.outputs.claude_model }} steps: - name: Checkout PR head (untrusted; review subject matter only) uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -119,9 +127,6 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Load AI models - run: jq -r '"CLAUDE_MODEL=\(.claude.opus)", "CODEX_MODEL=\(.codex.sol)"' trusted/.github/ai-models.json >> "$GITHUB_ENV" - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: # The PR head's own `.bun-version` is untrusted — it could select a @@ -261,6 +266,8 @@ jobs: pull-requests: read timeout-minutes: 45 runs-on: ubuntu-latest + env: + CODEX_MODEL: ${{ needs.resolve.outputs.codex_model }} steps: - name: Checkout default branch (trusted; the only checkout this job needs) uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -269,9 +276,6 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Load AI models - run: jq -r '"CLAUDE_MODEL=\(.claude.opus)", "CODEX_MODEL=\(.codex.sol)"' .github/ai-models.json >> "$GITHUB_ENV" - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: ".bun-version" @@ -366,6 +370,8 @@ jobs: pull-requests: read timeout-minutes: 45 runs-on: ubuntu-latest + env: + CODEX_MODEL: ${{ needs.resolve.outputs.codex_model }} steps: - name: Checkout PR head (untrusted; read-only, for verify-by-reading) uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -383,9 +389,6 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Load AI models - run: jq -r '"CLAUDE_MODEL=\(.claude.opus)", "CODEX_MODEL=\(.codex.sol)"' trusted/.github/ai-models.json >> "$GITHUB_ENV" - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: # The PR head's own `.bun-version` is untrusted; read it from trusted. @@ -522,6 +525,9 @@ jobs: timeout-minutes: 10 permissions: pull-requests: write + env: + CLAUDE_MODEL: ${{ needs.resolve.outputs.claude_model }} + CODEX_MODEL: ${{ needs.resolve.outputs.codex_model }} steps: # Security-critical: this is the only job with write permission, so it must only ever # execute trusted base-branch code — never the PR head. Checking out `develop` explicitly @@ -535,9 +541,6 @@ jobs: ref: develop persist-credentials: false - - name: Load AI models - run: jq -r '"CLAUDE_MODEL=\(.claude.opus)", "CODEX_MODEL=\(.codex.sol)"' .github/ai-models.json >> "$GITHUB_ENV" - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: ".bun-version" @@ -563,6 +566,6 @@ jobs: PR_DIFF_PATH: /tmp/ai-review/pr.diff TRIGGER: ${{ needs.resolve.outputs.trigger }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - # CLAUDE_MODEL / CODEX_MODEL come from the "Load AI models" step, the same source the - # review jobs read, so the footer never drifts from what actually ran. + # CLAUDE_MODEL / CODEX_MODEL are the job-level values from `resolve`, the same ones the + # review jobs ran with, so the footer never drifts from what actually ran. run: bun .github/scripts/ai-review/post-review.ts post diff --git a/apps/cli/docs/release-process.md b/apps/cli/docs/release-process.md index 25791ba379..ad24d02af1 100644 --- a/apps/cli/docs/release-process.md +++ b/apps/cli/docs/release-process.md @@ -479,6 +479,7 @@ Keep two to three stable majors supported at the same time, roughly six to twelv - Live e2e workflows: `live-e2e.yml`, `live-e2e-suite.yml`, `live-e2e-gate.yml`, `live-e2e-notify.yml`. - PR gating on `v*.x`: `test.yml`, `lint-pull-request.yml`, `run-ci.yml`, `branch-policy.yml`. - Composite action `.github/actions/setup`. +- Model ids in `.github/ai-models.json`, read by `propose-release-notes.ts`. - Scripts in `apps/cli/scripts/`: `build.ts`, `build-binary.ts`, `compile-options.ts`, `macos-signing.ts`, `publish.ts`, `release-channels.ts`, `sync-versions.ts`, `update-homebrew.ts`, `update-scoop.ts`, `upload-release-assets.ts`, `analyze-commits-title.js`, `backfill-release-notes.ts`, `propose-release-notes.ts`. - The `release` block in `apps/cli/package.json` and the smoke tests under `apps/cli/tests/`. diff --git a/apps/cli/scripts/propose-release-notes.ts b/apps/cli/scripts/propose-release-notes.ts index a4f90e8c3e..c1ab14505e 100644 --- a/apps/cli/scripts/propose-release-notes.ts +++ b/apps/cli/scripts/propose-release-notes.ts @@ -13,10 +13,7 @@ import { existsSync } from "node:fs"; import path from "node:path"; import process from "node:process"; import { parseArgs } from "node:util"; - -const aiModels: { claude: { haiku: string } } = JSON.parse( - await Bun.file(new URL("../../../.github/ai-models.json", import.meta.url)).text(), -); +import aiModels from "../../../.github/ai-models.json"; const { values } = parseArgs({ options: { From abe0d4bde0db23c3d8df35c952db5ac17edf11d2 Mon Sep 17 00:00:00 2001 From: avallete Date: Tue, 13 Oct 2026 12:30:21 +0200 Subject: [PATCH 3/3] ci: read sync agent's Claude model from ai-models.json Co-Authored-By: Claude Opus 5.5 --- .github/workflows/sync-branches.yml | 8 ++++++-- .github/workflows/sync-repair.yml | 4 +++- apps/cli/docs/release-process.md | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sync-branches.yml b/.github/workflows/sync-branches.yml index db3f258131..022f59abc8 100644 --- a/.github/workflows/sync-branches.yml +++ b/.github/workflows/sync-branches.yml @@ -104,7 +104,6 @@ jobs: outputs: resolved: ${{ steps.resolve.outputs.resolved }} env: - CLAUDE_MODEL: claude-opus-5-5 OUTPUT_DIR: ${{ github.workspace }}/sync-agent RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} steps: @@ -113,6 +112,9 @@ jobs: path: trusted persist-credentials: false + - name: Load Claude model + run: echo "CLAUDE_MODEL=$(jq -r .claude.opus trusted/.github/ai-models.json)" >> "$GITHUB_ENV" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: work @@ -229,7 +231,6 @@ jobs: permissions: contents: read env: - CLAUDE_MODEL: claude-opus-5-5 OUTPUT_DIR: ${{ github.workspace }}/sync-agent steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -237,6 +238,9 @@ jobs: path: trusted persist-credentials: false + - name: Load Claude model + run: echo "CLAUDE_MODEL=$(jq -r .claude.opus trusted/.github/ai-models.json)" >> "$GITHUB_ENV" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: work diff --git a/.github/workflows/sync-repair.yml b/.github/workflows/sync-repair.yml index cab596657d..530507652d 100644 --- a/.github/workflows/sync-repair.yml +++ b/.github/workflows/sync-repair.yml @@ -41,7 +41,6 @@ jobs: checks: read actions: read env: - CLAUDE_MODEL: claude-opus-5-5 OUTPUT_DIR: ${{ github.workspace }}/sync-agent RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} steps: @@ -50,6 +49,9 @@ jobs: path: trusted persist-credentials: false + - name: Load Claude model + run: echo "CLAUDE_MODEL=$(jq -r .claude.opus trusted/.github/ai-models.json)" >> "$GITHUB_ENV" + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: trusted/.bun-version diff --git a/apps/cli/docs/release-process.md b/apps/cli/docs/release-process.md index 7ba85b4863..753cef6099 100644 --- a/apps/cli/docs/release-process.md +++ b/apps/cli/docs/release-process.md @@ -354,7 +354,7 @@ git push origin sync/develop-into-next #### Claude-resolved `develop-into-next` -A `develop-into-next` conflict PR is resolved by Claude (`claude-opus-5-5`) in the same run. The `Resolve conflicts with Claude` job merges `next` into the sync branch and runs Claude Code with file read, search, and edit tools only: no shell, no web tools, no `.git` edits, and no GitHub write token. A `pnpm-lock.yaml` conflict keeps `next`'s lockfile, which the next job regenerates. `Run quick checks on the resolution` formats the merged tree and runs `check:all` and unit tests in a job with no secrets, and `Fix the quick checks with Claude` fixes what fails once, without re-running them. This catches code on one side that still uses something the other renamed or removed; integration and end-to-end failures are left to the PR's CI and the repair loop below. The jobs holding the Anthropic key never execute the merged tree, and the tree passes between jobs as a git bundle. The `Publish the resolution` job pushes the latest finished stage from a fresh checkout with the release App token, comments the resolution and any check that failed, and starts the AI review. Choices between `develop` and `next` behavior are listed as decisions for `@supabase/cli`. If Claude cannot resolve the merge, it comments why and the PR waits for a person as above. +A `develop-into-next` conflict PR is resolved by Claude (the `claude.opus` model in `.github/ai-models.json`) in the same run. The `Resolve conflicts with Claude` job merges `next` into the sync branch and runs Claude Code with file read, search, and edit tools only: no shell, no web tools, no `.git` edits, and no GitHub write token. A `pnpm-lock.yaml` conflict keeps `next`'s lockfile, which the next job regenerates. `Run quick checks on the resolution` formats the merged tree and runs `check:all` and unit tests in a job with no secrets, and `Fix the quick checks with Claude` fixes what fails once, without re-running them. This catches code on one side that still uses something the other renamed or removed; integration and end-to-end failures are left to the PR's CI and the repair loop below. The jobs holding the Anthropic key never execute the merged tree, and the tree passes between jobs as a git bundle. The `Publish the resolution` job pushes the latest finished stage from a fresh checkout with the release App token, comments the resolution and any check that failed, and starts the AI review. Choices between `develop` and `next` behavior are listed as decisions for `@supabase/cli`. If Claude cannot resolve the merge, it comments why and the PR waits for a person as above. [`sync-repair.yml`](../../../.github/workflows/sync-repair.yml) (`Sync repair`) runs when a CI workflow or the AI review finishes, and hourly. Once every check on the PR head has finished and no `AI Review` run for the PR is still going, it gives Claude the failed jobs' log tails and the AI review's unanswered threads. Claude fixes what the merge broke and replies on each finding, declining findings the merge did not introduce. The fix is pushed as a `chore(repo)` commit, fixed threads are resolved, and a round comment is posted. A head gets another round only for new review findings. When a round leaves its head failing, or after two rounds, the bot asks `@supabase/cli` to take over and stops repairing that PR. The repair loop reads only CI output and the AI review's own threads, never other people's comments, and only acts on PRs Claude resolved.