diff --git a/.github/ai-models.json b/.github/ai-models.json new file mode 100644 index 0000000000..73431a1116 --- /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 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", + "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 167cdd408b..5140c4142b 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -26,12 +26,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 @@ -73,6 +67,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. @@ -95,6 +92,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 @@ -110,6 +110,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 @@ -266,6 +268,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 @@ -371,6 +375,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 @@ -525,6 +531,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 @@ -537,6 +546,7 @@ jobs: with: ref: develop persist-credentials: false + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: ".bun-version" @@ -562,6 +572,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 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/.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 f36ffb85ec..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. @@ -487,6 +487,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 c2eaa7cdf1..c1ab14505e 100644 --- a/apps/cli/scripts/propose-release-notes.ts +++ b/apps/cli/scripts/propose-release-notes.ts @@ -13,6 +13,7 @@ import { existsSync } from "node:fs"; import path from "node:path"; import process from "node:process"; import { parseArgs } from "node:util"; +import aiModels from "../../../.github/ai-models.json"; const { values } = parseArgs({ options: { @@ -20,7 +21,7 @@ const { values } = parseArgs({ "dry-run": { type: "boolean", default: false }, apply: { type: "boolean", default: false }, "render-only": { type: "boolean", default: false }, - model: { type: "string", default: "claude-haiku-5-5" }, + model: { type: "string", default: aiModels.claude.haiku }, }, strict: true, });