Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .github/workflows/apply-release-notes.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
name: Apply release notes

# Approval-based publish. When a member of the supabase/cli team approves a
# release-notes PR (head ref `release-notes/v<VERSION>`), this workflow pushes
# the proposed notes to the GitHub Release body for the corresponding tag,
# comments the release URL on the PR, and closes the PR without merging. The
# release-notes file never lands on `main`.
#
# Mirrors the fast-forward job in release.yml, which already gates on a
# `pull_request_review` + `approved` event.

on:
pull_request_review:
types: [submitted]

permissions:
contents: read

jobs:
apply:
# `state == 'open'` makes re-approvals on an already-closed PR a no-op
# (a reviewer can re-approve from the GitHub UI even after close).
if: |
github.event.review.state == 'approved' &&
startsWith(github.event.pull_request.head.ref, 'release-notes/') &&
github.event.pull_request.base.ref == 'main' &&
github.event.pull_request.state == 'open'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
# App token: needs `orgs/.../teams/.../memberships` read (the org-installed
# App has it), repo write to edit the release, and PR write to comment
# and close. Matches release.yml's fast-forward step.
- id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}

- name: Authorize approver against supabase/cli team
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APPROVER: ${{ github.event.review.user.login }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# Fail closed: any response other than an active membership means the
# approval is ignored. We post a comment so the reviewer sees why their
# approval didn't apply, then exit 0 so the workflow isn't flagged red.
run: |
set -euo pipefail
status=$(gh api \
-H "Accept: application/vnd.github+json" \
"orgs/supabase/teams/cli/memberships/${APPROVER}" \
--jq '.state' 2>/dev/null || true)
if [ "$status" != "active" ]; then
echo "Approver @${APPROVER} is not an active supabase/cli team member (state='${status:-none}'); ignoring approval." >&2
gh pr comment "$PR_NUMBER" --repo "${{ github.repository }}" --body \
"@${APPROVER} is not an active \`supabase/cli\` team member, so this approval was ignored. Ask a team member to approve to publish the notes."
exit 0
fi
echo "AUTHORIZED=true" >> "$GITHUB_ENV"

# Checkout the PR head so any reviewer edits made in the GitHub UI before
# approval are captured. apply-release-notes.ts reads from the working
# tree.
- if: env.AUTHORIZED == 'true'
uses: useblacksmith/checkout@41cdeedae8edb2e684ba22896a5fd2a3cb85db6b # v1
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 1
persist-credentials: false

- if: env.AUTHORIZED == 'true'
uses: ./.github/actions/setup

- name: Apply notes, comment, and close
if: env.AUTHORIZED == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
PR_NUMBER: ${{ github.event.pull_request.number }}
APPROVER: ${{ github.event.review.user.login }}
# The branch is named `release-notes/v<VERSION>`, so the tag is just
# the basename. apply-release-notes.ts validates the file's existence.
run: |
set -euo pipefail
tag="${HEAD_REF##release-notes/}"
if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(beta|alpha)\.[0-9]+)?$ ]]; then
echo "Unexpected head ref '$HEAD_REF'; cannot derive tag." >&2
exit 1
fi
echo "==> Applying notes for $tag"
pnpm exec bun apps/cli/scripts/apply-release-notes.ts --tag "$tag"
release_url="https://github.com/${{ github.repository }}/releases/tag/${tag}"
gh pr comment "$PR_NUMBER" --repo "${{ github.repository }}" --body \
"Applied to [${tag}](${release_url}) after approval by @${APPROVER}."
gh pr close "$PR_NUMBER" --repo "${{ github.repository }}" --delete-branch
75 changes: 75 additions & 0 deletions .github/workflows/propose-release-notes.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
name: Propose release notes

# Runs after backfill-release-notes lands the raw semantic-release block in the
# GitHub Release body. Re-derives that block, asks Claude to rewrite it into
# user-centric notes per tools/release/release-notes-prompt.md, and opens a PR
# adding release-notes/v<VERSION>.md. Merging the PR triggers
# apply-release-notes.yml, which pushes the file's contents to the GH Release.
#
# Stable releases only on the automatic release pipeline — prerelease tags
# (-beta./-alpha.) keep the raw body unless this workflow is triggered
# manually from the Actions tab (workflow_dispatch).

on:
workflow_call:
inputs:
tag:
description: Release tag to propose notes for (e.g. v2.101.0)
required: true
type: string
non_blocking:
description: Do not fail the workflow run when proposing fails (release pipeline)
required: false
type: boolean
default: false
workflow_dispatch:
inputs:
tag:
description: Release tag to propose notes for (e.g. v2.101.0 or v2.99.0-beta.1)
required: true
type: string

permissions:
contents: read

jobs:
propose:
# workflow_call (release pipeline): skip prereleases. workflow_dispatch:
# allow any tag so reviewers can opt in for beta/alpha from the Actions tab.
if: ${{ github.event_name == 'workflow_dispatch' || (!contains(inputs.tag, '-beta.') && !contains(inputs.tag, '-alpha.')) }}
runs-on: ubuntu-latest
continue-on-error: ${{ inputs.non_blocking }}
permissions:
contents: write
pull-requests: write
env:
TAG: ${{ inputs.tag }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
steps:
# App token gets us push to a protected default branch *and* PR creation
# under the App identity, matching the rest of release.yml.
- id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}

- uses: useblacksmith/checkout@41cdeedae8edb2e684ba22896a5fd2a3cb85db6b # v1
with:
# Full history + tags so backfill-release-notes.ts can reach the
# commit graph it needs (semantic-release walks notes back to the
# last release on the channel).
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}

- uses: ./.github/actions/setup

- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Propose release notes
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: pnpm exec bun apps/cli/scripts/propose-release-notes.ts --tag "${TAG}" --apply
14 changes: 14 additions & 0 deletions .github/workflows/release-shared.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -323,6 +323,20 @@ jobs:
apply: true
non_blocking: true

# Once the raw semantic-release block is in the release body, ask Claude to
# rewrite it into user-centric notes and open a PR for human approval. Stable
# releases only on this path — prereleases keep the raw body. Non-blocking so
# an LLM hiccup never gates a published release; reviewers can propose beta/
# alpha notes manually from the Actions tab (workflow_dispatch).
propose-release-notes:
uses: ./.github/workflows/propose-release-notes.yml
needs: backfill-release-notes
if: ${{ !inputs.dry_run && !inputs.prerelease && needs.backfill-release-notes.result == 'success' }}
with:
tag: v${{ inputs.version }}
non_blocking: true
secrets: inherit

publish-homebrew:
needs: publish
if: ${{ !inputs.dry_run && inputs.publish_brew_scoop }}
Expand Down
8 changes: 7 additions & 1 deletion apps/cli/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,10 +37,13 @@
"fix:all": "nx run-many -t lint:fix fmt:fix knip:fix --projects=$npm_package_name"
},
"devDependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.3.146",
"@anthropic-ai/sdk": "^0.97.1",
"@clack/prompts": "^1.4.0",
"@effect/atom-react": "catalog:",
"@effect/platform-bun": "catalog:",
"@effect/vitest": "catalog:",
"@modelcontextprotocol/sdk": "^1.29.0",
"@napi-rs/keyring": "^1.3.0",
"@parcel/watcher": "^2.5.6",
"@supabase/api": "workspace:*",
Expand DownExpand Up@@ -120,7 +123,10 @@
"oxfmt",
"oxlint",
"oxlint-tsgolint",
"semantic-release"
"semantic-release",
"@anthropic-ai/claude-agent-sdk",
"@anthropic-ai/sdk",
"@modelcontextprotocol/sdk"
]
},
"nx": {
Expand Down
37 changes: 37 additions & 0 deletions apps/cli/scripts/apply-release-notes.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bun
// Push the contents of release-notes/v<VERSION>.md to the GitHub Release body
// for tag v<VERSION>. Invoked from apply-release-notes.yml after a
// release-notes PR is merged to main.
//
// Usage:
// bun apps/cli/scripts/apply-release-notes.ts --tag v2.101.0
import { $ } from "bun";
import { existsSync } from "node:fs";
import path from "node:path";
import process from "node:process";
import { parseArgs } from "node:util";

const { values } = parseArgs({
options: {
tag: { type: "string" },
},
strict: true,
});

const tag = values.tag;
if (!tag) {
console.error("--tag is required (e.g. --tag v2.101.0)");
process.exit(2);
}
const version = tag.replace(/^v/, "");

const repoRoot = (await $`git rev-parse --show-toplevel`.text()).trim();
const notesPath = path.join(repoRoot, "release-notes", `v${version}.md`);
if (!existsSync(notesPath)) {
console.error(`No notes file at ${path.relative(repoRoot, notesPath)}`);
process.exit(1);
}

console.error(`==> Updating GitHub Release body for ${tag}`);
await $`gh release edit ${tag} --notes-file ${notesPath}`.cwd(repoRoot);
console.error(`==> Done`);
Loading
Loading