Skip to content
Draft
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
58 changes: 58 additions & 0 deletions .github/workflows/apply-release-notes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Apply release notes

# When a PR that touches release-notes/v<VERSION>.md is merged to main, push
# the contents of each touched file to the corresponding GitHub Release body.
# The proposer (propose-release-notes.yml) creates these PRs; reviewers approve
# by merging.

on:
pull_request:
types: [closed]
branches: [main]
paths:
- "release-notes/**"

permissions:
contents: read

jobs:
apply:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: useblacksmith/checkout@41cdeedae8edb2e684ba22896a5fd2a3cb85db6b # v1
with:
ref: main
# Fetch only the merge commit; release-notes files are checked in by
# the PR and we don't need history here.
fetch-depth: 1
persist-credentials: false

- uses: ./.github/actions/setup

- name: Apply each touched release-notes file
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# `gh pr view --json files` returns every file the PR touched. Filter
# to release-notes/v<VERSION>.md, derive the tag from the basename,
# and invoke the apply script per file. Tolerates PRs that bundle
# multiple version files, though in practice the proposer creates one
# file per PR.
run: |
set -euo pipefail
files=$(gh pr view "$PR_NUMBER" --repo "${{ github.repository }}" \
--json files --jq '.files[].path' | \
grep -E '^release-notes/v[0-9]+\.[0-9]+\.[0-9]+\.md$' || true)
if [ -z "$files" ]; then
echo "No release-notes/ files in PR #$PR_NUMBER; nothing to apply." >&2
exit 0
fi
while IFS= read -r file; do
base=$(basename "$file" .md)
tag="${base}"
echo "==> Applying $file to release $tag"
pnpm exec bun apps/cli/scripts/apply-release-notes.ts --tag "$tag"
done <<< "$files"
73 changes: 73 additions & 0 deletions .github/workflows/propose-release-notes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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 — prerelease tags (-beta./-alpha.) keep the raw body.

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)
required: true
type: string

permissions:
contents: read

jobs:
propose:
# Belt-and-suspenders: the script also refuses prerelease tags, but cheap
# to short-circuit here so we never pay for an LLM call we'd discard.
if: ${{ !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 number Diff line number Diff line change
Expand Up @@ -343,6 +343,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 — prereleases keep the raw body. Non-blocking so an LLM
# hiccup never gates a published release; reviewers can always rerun the
# workflow by hand from the Actions tab.
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 number Diff line number Diff 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 Down Expand Up @@ -116,7 +119,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 number Diff line number Diff 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