Skip to content

Commit 387784c

Browse files
committed
ci: add bespoke stateless release system
Replace changesets with a simple, stateless release system: Stable release (PR-gated): Actions → "create release PR" → pick patch/minor/major → CI runs → merge → publishes to npm, creates git tag + GitHub Release Prerelease (ad-hoc): Actions → "publish / prerelease" → publishes current version with -canary.<suffix|timestamp> to npm under "canary" tag Key features: - All 12 core @copilotkit/* packages share a single version - AI-generated release notes via Anthropic API - Notion draft for team editing before merge - Notion link commented on the release PR - Guards: concurrent release PR check, version > npm check, clean semver check, canary-only prerelease tag - release/publish/v* branch pattern (hard to accidentally match) - TypeScript throughout (tsx runner) - release.config.json with versionedTogether/versionedIndependently
1 parent ab74b73 commit 387784c

13 files changed

Lines changed: 1299 additions & 0 deletions

.github/workflows/prerelease.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: publish / prerelease
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
suffix:
7+
description: "Version suffix (e.g. 'fix-user-issue'). Leave blank for timestamp."
8+
required: false
9+
type: string
10+
dry_run:
11+
description: "Dry run (don't actually publish)"
12+
required: false
13+
default: false
14+
type: boolean
15+
16+
concurrency:
17+
group: ${{ github.workflow }}-${{ github.ref }}
18+
cancel-in-progress: false
19+
20+
permissions:
21+
contents: read
22+
23+
env:
24+
NX_VERBOSE_LOGGING: true
25+
26+
jobs:
27+
prerelease:
28+
runs-on: ubuntu-latest
29+
timeout-minutes: 20
30+
steps:
31+
- name: Checkout Repo
32+
uses: actions/checkout@v4
33+
34+
- name: Setup pnpm
35+
uses: pnpm/action-setup@v4
36+
with:
37+
version: "10.13.1"
38+
39+
- name: Setup Node
40+
uses: actions/setup-node@v4
41+
with:
42+
node-version: 20.x
43+
registry-url: https://registry.npmjs.org
44+
env:
45+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
46+
47+
- name: Configure npm auth
48+
run: |
49+
npm config set "//registry.npmjs.org/:_authToken" "${NPM_TOKEN}"
50+
env:
51+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
52+
53+
- name: Install Dependencies
54+
run: pnpm install --frozen-lockfile
55+
56+
- name: Publish prerelease
57+
run: |
58+
ARGS=""
59+
if [ -n "${{ inputs.suffix }}" ]; then
60+
ARGS="--suffix ${{ inputs.suffix }}"
61+
fi
62+
if [ "${{ inputs.dry_run }}" == "true" ]; then
63+
ARGS="$ARGS --dry-run"
64+
fi
65+
pnpm tsx scripts/release/prerelease.ts $ARGS
66+
env:
67+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
68+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
name: publish release
2+
3+
on:
4+
pull_request:
5+
types: [closed]
6+
branches: [main]
7+
8+
concurrency:
9+
group: publish-release
10+
cancel-in-progress: false
11+
12+
permissions:
13+
contents: write
14+
15+
env:
16+
NX_VERBOSE_LOGGING: true
17+
18+
jobs:
19+
publish:
20+
# Only run when a release PR is merged (not just closed)
21+
if: >
22+
github.event.pull_request.merged == true &&
23+
startsWith(github.event.pull_request.head.ref, 'release/publish/v')
24+
runs-on: ubuntu-latest
25+
timeout-minutes: 20
26+
steps:
27+
- name: Checkout Repo
28+
uses: actions/checkout@v4
29+
with:
30+
fetch-depth: 0
31+
token: ${{ secrets.GITHUB_TOKEN }}
32+
33+
- name: Setup pnpm
34+
uses: pnpm/action-setup@v4
35+
with:
36+
version: "10.13.1"
37+
38+
- name: Setup Node
39+
uses: actions/setup-node@v4
40+
with:
41+
node-version: 20.x
42+
registry-url: https://registry.npmjs.org
43+
env:
44+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
45+
46+
- name: Configure npm auth
47+
run: |
48+
npm config set "//registry.npmjs.org/:_authToken" "${NPM_TOKEN}"
49+
env:
50+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
51+
52+
- name: Install Dependencies
53+
run: pnpm install --frozen-lockfile
54+
55+
- name: Publish to npm
56+
id: publish
57+
run: pnpm tsx scripts/release/publish-release.ts
58+
env:
59+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
60+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
61+
NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }}
62+
63+
- name: Configure git user
64+
run: |
65+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
66+
git config --global user.name "github-actions[bot]"
67+
68+
- name: Create and push git tag
69+
run: |
70+
TAG="v${{ steps.publish.outputs.version }}"
71+
git tag -a "$TAG" -m "Release ${{ steps.publish.outputs.version }}"
72+
git push origin "$TAG"
73+
74+
- name: Create GitHub Release
75+
uses: actions/github-script@v7
76+
env:
77+
RELEASE_TAG: v${{ steps.publish.outputs.version }}
78+
RELEASE_NAME: v${{ steps.publish.outputs.version }}
79+
with:
80+
github-token: ${{ secrets.GITHUB_TOKEN }}
81+
script: |
82+
const fs = require("fs");
83+
const { owner, repo } = context.repo;
84+
const tag = process.env.RELEASE_TAG;
85+
const name = process.env.RELEASE_NAME;
86+
87+
let body = "";
88+
try {
89+
body = fs.readFileSync("./release-notes.md", "utf8");
90+
} catch {
91+
body = `Release ${name}`;
92+
}
93+
94+
try {
95+
const existing = await github.rest.repos.getReleaseByTag({ owner, repo, tag });
96+
await github.rest.repos.updateRelease({
97+
owner, repo,
98+
release_id: existing.data.id,
99+
tag_name: tag, name, body,
100+
draft: false, prerelease: false,
101+
});
102+
} catch (error) {
103+
if (error.status !== 404) throw error;
104+
await github.rest.repos.createRelease({
105+
owner, repo,
106+
tag_name: tag, name, body,
107+
draft: false, prerelease: false,
108+
});
109+
}
110+
111+
- name: Release summary
112+
run: |
113+
echo "## Release Published" >> $GITHUB_STEP_SUMMARY
114+
echo "" >> $GITHUB_STEP_SUMMARY
115+
echo "**Version:** ${{ steps.publish.outputs.version }}" >> $GITHUB_STEP_SUMMARY
116+
echo "**Tag:** v${{ steps.publish.outputs.version }}" >> $GITHUB_STEP_SUMMARY
117+
echo "**npm:** https://www.npmjs.com/package/@copilotkit/react-core/v/${{ steps.publish.outputs.version }}" >> $GITHUB_STEP_SUMMARY
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
name: create release PR
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
bump:
7+
description: "Version bump level"
8+
required: true
9+
type: choice
10+
options:
11+
- patch
12+
- minor
13+
- major
14+
dry_run:
15+
description: "Dry run (preview without creating PR)"
16+
required: false
17+
default: false
18+
type: boolean
19+
20+
concurrency:
21+
group: release-pr
22+
cancel-in-progress: false
23+
24+
permissions:
25+
contents: write
26+
pull-requests: write
27+
28+
env:
29+
NX_VERBOSE_LOGGING: true
30+
31+
jobs:
32+
create-release-pr:
33+
runs-on: ubuntu-latest
34+
timeout-minutes: 15
35+
steps:
36+
- name: Check for existing release PR
37+
uses: actions/github-script@v7
38+
with:
39+
github-token: ${{ secrets.GITHUB_TOKEN }}
40+
script: |
41+
const { owner, repo } = context.repo;
42+
const { data: prs } = await github.rest.pulls.list({
43+
owner,
44+
repo,
45+
state: "open",
46+
head_prefix: `${owner}:release/publish/v`,
47+
});
48+
49+
const releasePRs = prs.filter(pr => pr.head.ref.startsWith("release/publish/v"));
50+
if (releasePRs.length > 0) {
51+
const existing = releasePRs.map(pr => ` - #${pr.number}: ${pr.title} (${pr.html_url})`).join("\n");
52+
core.setFailed(
53+
`An open release PR already exists. Close or merge it before creating a new one:\n${existing}`
54+
);
55+
}
56+
57+
- name: Checkout Repo
58+
uses: actions/checkout@v4
59+
with:
60+
fetch-depth: 0
61+
token: ${{ secrets.GITHUB_TOKEN }}
62+
63+
- name: Setup pnpm
64+
uses: pnpm/action-setup@v4
65+
with:
66+
version: "10.13.1"
67+
68+
- name: Setup Node
69+
uses: actions/setup-node@v4
70+
with:
71+
node-version: 20.x
72+
73+
- name: Install Dependencies
74+
run: pnpm install --frozen-lockfile
75+
76+
- name: Prepare release
77+
id: prepare
78+
run: |
79+
if [ "${{ inputs.dry_run }}" == "true" ]; then
80+
pnpm tsx scripts/release/prepare-release.ts --bump ${{ inputs.bump }} --dry-run
81+
else
82+
pnpm tsx scripts/release/prepare-release.ts --bump ${{ inputs.bump }}
83+
fi
84+
85+
- name: Generate AI release notes
86+
if: inputs.dry_run != true
87+
id: ai_notes
88+
run: pnpm tsx scripts/release/generate-ai-release-notes.ts "${{ steps.prepare.outputs.version }}"
89+
env:
90+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
91+
NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }}
92+
NOTION_RELEASE_NOTES_PAGE: ${{ secrets.NOTION_RELEASE_NOTES_PAGE }}
93+
94+
- name: Create release PR
95+
if: inputs.dry_run != true
96+
id: create_pr
97+
uses: peter-evans/create-pull-request@v8
98+
with:
99+
token: ${{ secrets.GITHUB_TOKEN }}
100+
branch: release/publish/v${{ steps.prepare.outputs.version }}
101+
delete-branch: true
102+
commit-message: "chore: release v${{ steps.prepare.outputs.version }}"
103+
title: "chore: release v${{ steps.prepare.outputs.version }}"
104+
body: |
105+
## Release v${{ steps.prepare.outputs.version }}
106+
107+
**Bump:** `${{ inputs.bump }}`
108+
109+
---
110+
111+
### How this release process works
112+
113+
1. **This PR was created automatically** by the "Create Release PR" workflow.
114+
It bumped all `@copilotkit/*` package versions to `${{ steps.prepare.outputs.version }}`
115+
and generated AI-enhanced release notes.
116+
117+
2. **CI runs on this PR** — the full test suite (unit tests, lint, type checks, build)
118+
must pass before merging. This is the review gate.
119+
120+
3. **Review the release notes** in `release-notes.md` in this PR.
121+
If a Notion draft was created, you can edit the release notes there before merging.
122+
123+
4. **When this PR is merged**, the `publish-release` workflow automatically:
124+
- Builds all packages
125+
- Publishes all `@copilotkit/*` packages to npm at version `${{ steps.prepare.outputs.version }}`
126+
- Creates git tag `v${{ steps.prepare.outputs.version }}`
127+
- Creates a GitHub Release with the final release notes
128+
129+
### Before merging
130+
131+
- [ ] CI is green (tests, lint, types, build)
132+
- [ ] Version bumps look correct
133+
- [ ] Release notes are accurate (edit in Notion if a draft was created)
134+
135+
---
136+
137+
> **Do not merge until CI is fully green.** The full test suite runs automatically on this PR.
138+
labels: release
139+
140+
- name: Comment Notion link on PR
141+
if: inputs.dry_run != true && steps.ai_notes.outputs.notion_url
142+
uses: actions/github-script@v7
143+
env:
144+
NOTION_URL: ${{ steps.ai_notes.outputs.notion_url }}
145+
PR_NUMBER: ${{ steps.create_pr.outputs.pull-request-number }}
146+
with:
147+
github-token: ${{ secrets.GITHUB_TOKEN }}
148+
script: |
149+
const { owner, repo } = context.repo;
150+
const prNumber = parseInt(process.env.PR_NUMBER, 10);
151+
const notionUrl = process.env.NOTION_URL;
152+
153+
if (prNumber && notionUrl) {
154+
await github.rest.issues.createComment({
155+
owner,
156+
repo,
157+
issue_number: prNumber,
158+
body: `📝 **Release notes draft:** ${notionUrl}\n\nYou can edit the release notes in Notion before merging. The final content will be used for the GitHub Release.`,
159+
});
160+
}

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ lefthook-local.yml
5353
# Private agent instructions (not shared with the team)
5454
private-agents.md
5555

56+
# Release artifacts
57+
release-notes.md
58+
release-notes-notion.json
59+
5660
# Binary artifacts
5761
*.dSYM/
5862
*.exe

release.config.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"prereleaseTag": "canary",
3+
"versionedTogether": [
4+
"@copilotkit/a2ui-renderer",
5+
"@copilotkit/core",
6+
"@copilotkit/react-core",
7+
"@copilotkit/react-textarea",
8+
"@copilotkit/react-ui",
9+
"@copilotkit/runtime",
10+
"@copilotkit/runtime-client-gql",
11+
"@copilotkit/sdk-js",
12+
"@copilotkit/shared",
13+
"@copilotkit/sqlite-runner",
14+
"@copilotkit/voice",
15+
"@copilotkit/web-inspector"
16+
],
17+
"versionedIndependently": ["@copilotkitnext/angular", "copilotkit"]
18+
}

0 commit comments

Comments
 (0)