Skip to content

Commit 35b22f1

Browse files
authored
Add secure release workflow (#24)
## Summary Adds a manual GitHub Actions release workflow that bumps all package versions, commits and tags `vX.Y.Z`, rebuilds the workspace packages, and publishes npm packages using trusted publishing with provenance. ## Context The workflow is started with a configurable `patch`, `minor`, or `major` bump. It validates package versions are aligned, creates the `release: vX.Y.Z` commit and matching tag, then publishes from the checked-out tag. Release actions are pinned to commit SHAs, dependency caching is not configured, and publish authentication relies on GitHub OIDC via `id-token: write` instead of npm tokens. ## Backward Compatibility Existing commands, package scripts, package contents, saved artifacts, daemon behavior, and default CLI output are unchanged. The new workflow only adds an opt-in manual release path and does not alter CI defaults. npm trusted publishers must be configured for each package to accept this repository/workflow before publishing will succeed. ## Risks The workflow pushes release commits and tags from GitHub Actions, so branch protection rules must allow the configured `GITHUB_TOKEN` path or the prepare job will fail. Publishing depends on npm trusted publishing configuration matching this workflow. The release job intentionally avoids caches, so installs may be slower but are less exposed to cache poisoning. ## Manual testing From the Actions tab, run the `Release` workflow on the release branch with a `patch`, `minor`, or `major` input. Verify it creates a commit named `release: vX.Y.Z`, creates tag `vX.Y.Z`, builds all packages, and publishes `@agent-cdp/protocol`, `@agent-cdp/sdk`, and `agent-cdp` with provenance and without npm token secrets.
1 parent 4e76f4a commit 35b22f1

1 file changed

Lines changed: 248 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
bump:
7+
description: Version bump to apply to all packages
8+
required: true
9+
type: choice
10+
options:
11+
- patch
12+
- minor
13+
- major
14+
15+
permissions:
16+
contents: read
17+
18+
concurrency:
19+
group: release
20+
cancel-in-progress: false
21+
22+
jobs:
23+
prepare:
24+
name: Prepare release
25+
runs-on: ubuntu-latest
26+
permissions:
27+
contents: write
28+
outputs:
29+
version: ${{ steps.bump-version.outputs.version }}
30+
commit-sha: ${{ steps.commit-release.outputs.commit-sha }}
31+
steps:
32+
- name: Verify release branch
33+
env:
34+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
35+
RELEASE_BRANCH: ${{ github.ref_name }}
36+
run: |
37+
set -euo pipefail
38+
39+
if [ "$RELEASE_BRANCH" != "$DEFAULT_BRANCH" ]; then
40+
printf 'Release workflow must run from default branch %s, got %s\n' "$DEFAULT_BRANCH" "$RELEASE_BRANCH" >&2
41+
exit 1
42+
fi
43+
44+
- name: Checkout
45+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
46+
with:
47+
fetch-depth: 0
48+
49+
- name: Setup pnpm
50+
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
51+
with:
52+
version: 9.15.3
53+
54+
- name: Setup Node.js
55+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
56+
with:
57+
node-version: 24
58+
59+
- name: Install dependencies
60+
run: pnpm install --frozen-lockfile
61+
62+
- name: Bump package versions
63+
id: bump-version
64+
env:
65+
RELEASE_BUMP: ${{ inputs.bump }}
66+
run: |
67+
set -euo pipefail
68+
69+
case "$RELEASE_BUMP" in
70+
patch|minor|major) ;;
71+
*)
72+
printf 'Invalid release bump: %s\n' "$RELEASE_BUMP" >&2
73+
exit 1
74+
;;
75+
esac
76+
77+
next_version=$(node --input-type=module <<'NODE'
78+
import { readFileSync, writeFileSync } from 'node:fs';
79+
80+
const packagePaths = [
81+
'packages/protocol/package.json',
82+
'packages/sdk/package.json',
83+
'packages/agent-cdp/package.json',
84+
];
85+
86+
const packages = packagePaths.map((path) => ({
87+
path,
88+
data: JSON.parse(readFileSync(path, 'utf8')),
89+
}));
90+
91+
const versions = new Set(packages.map((pkg) => pkg.data.version));
92+
if (versions.size !== 1) {
93+
throw new Error(`Package versions must match before release: ${[...versions].join(', ')}`);
94+
}
95+
96+
const [major, minor, patch] = packages[0].data.version.split('.').map(Number);
97+
if (![major, minor, patch].every(Number.isInteger)) {
98+
throw new Error(`Invalid package version: ${packages[0].data.version}`);
99+
}
100+
101+
const bump = process.env.RELEASE_BUMP;
102+
const next = {
103+
major: `${major + 1}.0.0`,
104+
minor: `${major}.${minor + 1}.0`,
105+
patch: `${major}.${minor}.${patch + 1}`,
106+
}[bump];
107+
108+
for (const pkg of packages) {
109+
pkg.data.version = next;
110+
writeFileSync(pkg.path, `${JSON.stringify(pkg.data, null, 2)}\n`);
111+
}
112+
113+
console.log(next);
114+
NODE
115+
)
116+
117+
git fetch --tags --force
118+
if git rev-parse --quiet --verify "refs/tags/v${next_version}" >/dev/null; then
119+
printf 'Tag v%s already exists\n' "$next_version" >&2
120+
exit 1
121+
fi
122+
123+
printf 'version=%s\n' "$next_version" >>"$GITHUB_OUTPUT"
124+
125+
- name: Build all packages
126+
run: pnpm run build
127+
128+
- name: Commit and tag release
129+
id: commit-release
130+
env:
131+
RELEASE_VERSION: ${{ steps.bump-version.outputs.version }}
132+
RELEASE_BRANCH: ${{ github.ref_name }}
133+
run: |
134+
set -euo pipefail
135+
136+
git config user.name 'github-actions[bot]'
137+
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
138+
git add packages/*/package.json
139+
git commit -m "release: v${RELEASE_VERSION}"
140+
git tag "v${RELEASE_VERSION}"
141+
printf 'commit-sha=%s\n' "$(git rev-parse HEAD)" >>"$GITHUB_OUTPUT"
142+
git push origin "HEAD:${RELEASE_BRANCH}"
143+
git push origin "v${RELEASE_VERSION}"
144+
145+
publish:
146+
name: Publish packages
147+
runs-on: ubuntu-latest
148+
needs: prepare
149+
permissions:
150+
contents: read
151+
id-token: write
152+
steps:
153+
- name: Checkout release commit
154+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
155+
with:
156+
ref: ${{ needs.prepare.outputs.commit-sha }}
157+
persist-credentials: false
158+
159+
- name: Verify release tag
160+
env:
161+
GH_TOKEN: ${{ github.token }}
162+
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
163+
RELEASE_COMMIT_SHA: ${{ needs.prepare.outputs.commit-sha }}
164+
run: |
165+
set -euo pipefail
166+
167+
tag_commit_sha=$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/v${RELEASE_VERSION}" --jq '.object.sha')
168+
if [ "$tag_commit_sha" != "$RELEASE_COMMIT_SHA" ]; then
169+
printf 'Release tag v%s points to %s, expected %s\n' "$RELEASE_VERSION" "$tag_commit_sha" "$RELEASE_COMMIT_SHA" >&2
170+
exit 1
171+
fi
172+
173+
- name: Setup pnpm
174+
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
175+
with:
176+
version: 9.15.3
177+
178+
- name: Setup Node.js
179+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
180+
with:
181+
node-version: 24
182+
registry-url: https://registry.npmjs.org
183+
184+
- name: Install dependencies
185+
run: pnpm install --frozen-lockfile
186+
187+
- name: Build all packages
188+
run: pnpm run build
189+
190+
- name: Pack packages
191+
env:
192+
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
193+
run: |
194+
set -euo pipefail
195+
196+
mkdir -p "$RUNNER_TEMP/packs"
197+
pnpm --dir packages/protocol pack --pack-destination "$RUNNER_TEMP/packs"
198+
pnpm --dir packages/sdk pack --pack-destination "$RUNNER_TEMP/packs"
199+
pnpm --dir packages/agent-cdp pack --pack-destination "$RUNNER_TEMP/packs"
200+
201+
test -f "$RUNNER_TEMP/packs/agent-cdp-protocol-${RELEASE_VERSION}.tgz"
202+
test -f "$RUNNER_TEMP/packs/agent-cdp-sdk-${RELEASE_VERSION}.tgz"
203+
test -f "$RUNNER_TEMP/packs/agent-cdp-${RELEASE_VERSION}.tgz"
204+
205+
- name: Publish packages with provenance
206+
env:
207+
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
208+
run: |
209+
set -euo pipefail
210+
211+
npm publish "$RUNNER_TEMP/packs/agent-cdp-protocol-${RELEASE_VERSION}.tgz" --provenance --access public
212+
npm publish "$RUNNER_TEMP/packs/agent-cdp-sdk-${RELEASE_VERSION}.tgz" --provenance --access public
213+
npm publish "$RUNNER_TEMP/packs/agent-cdp-${RELEASE_VERSION}.tgz" --provenance --access public
214+
215+
github-release:
216+
name: Create GitHub release
217+
runs-on: ubuntu-latest
218+
needs:
219+
- prepare
220+
- publish
221+
permissions:
222+
contents: write
223+
steps:
224+
- name: Verify release tag
225+
env:
226+
GH_TOKEN: ${{ github.token }}
227+
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
228+
RELEASE_COMMIT_SHA: ${{ needs.prepare.outputs.commit-sha }}
229+
run: |
230+
set -euo pipefail
231+
232+
tag_commit_sha=$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/v${RELEASE_VERSION}" --jq '.object.sha')
233+
if [ "$tag_commit_sha" != "$RELEASE_COMMIT_SHA" ]; then
234+
printf 'Release tag v%s points to %s, expected %s\n' "$RELEASE_VERSION" "$tag_commit_sha" "$RELEASE_COMMIT_SHA" >&2
235+
exit 1
236+
fi
237+
238+
- name: Create GitHub release
239+
env:
240+
GH_TOKEN: ${{ github.token }}
241+
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
242+
run: |
243+
set -euo pipefail
244+
245+
gh release create "v${RELEASE_VERSION}" \
246+
--title "v${RELEASE_VERSION}" \
247+
--generate-notes \
248+
--verify-tag

0 commit comments

Comments
 (0)