feat: add release infrastructure with changesets [3/4] - #45
Conversation
e6a73f6 to
3bcc5ea
Compare
2bb7c54 to
ba9a09c
Compare
|
3bcc5ea to
cb764f6
Compare
ba9a09c to
0aa883a
Compare
cb764f6 to
fb82e61
Compare
0aa883a to
51be70a
Compare
51be70a to
2b7f7c7
Compare
📝 WalkthroughWalkthroughAdds Changesets configuration and documentation, new CI workflows for Release and Snapshot, and an actionlint runner label. Introduces package scripts and a devDependency for Changesets, plus Bun/TypeScript release tooling: 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@scripts/releaser/index.ts`:
- Around line 126-145: Replace the local-only tag check and blind release
creation with a remote-aware, idempotent flow: keep tagName and tagCheck but
also check the remote via git ls-remote --tags origin tagName (or "git ls-remote
--refs origin refs/tags/${tagName}") and skip git push if the remote tag exists;
when pushing, only run(["git","push","origin",tagName]) if remote missing. For
releases, call run(["gh","release","create", tagName, "--generate-notes",
...files]) with the build artifact paths taken from ARTIFACTS_DIR (globbing the
built files) so assets are uploaded; wrap the gh release create call in a
try/catch and on failure detect if the release already exists (use
run(["gh","release","view", tagName]) or inspect the error), and if it exists
call run(["gh","release","upload", tagName, ...files, "--clobber"]) to
idempotently upload artifacts. Use the existing run(), tagName, tagCheck, and
ARTIFACTS_DIR symbols to locate where to implement these changes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 555c6e67-74d2-434a-a30a-809b44b4b76f
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (9)
.changeset/README.md.changeset/config.json.github/workflows/release.yml.github/workflows/snapshot.ymlpackage.jsonscripts/check-release.tsscripts/lib/npm.tsscripts/releaser/index.tsscripts/snapshot.ts
Add the complete release pipeline: - Changesets for versioning (stable, canary, snapshot channels) - Releaser script for npm publish, git tags, and GitHub Releases - CI workflows for stable/canary releases and PR snapshot comments
2b7f7c7 to
d849ce6
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
.github/workflows/release.yml (1)
61-112:⚠️ Potential issue | 🔴 CriticalStable release recovery is still not safe to retry.
publish-npmdelegates toscripts/releaser/index.ts, which only checks the local checkout before pushingv${version}, and this asset job has no explicit overwrite/skip handling if some binaries were already uploaded on an earlier attempt. A partial success can therefore leave the stable release stuck instead of recoverable by rerunning the failed jobs. Please make the tag/release creation and asset upload steps explicitly idempotent before merging.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/release.yml around lines 61 - 112, The release pipeline can leave a half-complete stable release that cannot be recovered because tag/release creation and asset uploads are not idempotent; update scripts/releaser/index.ts and the workflow steps (publish-npm and upload-github-assets) so tag creation is safe to retry (check for existing tag v${{ needs.versioning.outputs.version }}, update it or force-push the tag instead of failing) and make GitHub Release/asset uploads idempotent by using the GitHub CLI to create-or-edit the release (query with gh release view and gh release create only if missing, or gh release edit) and upload assets with overwrite semantics (gh release upload --clobber or delete existing asset before upload) instead of blindly uploading; ensure these changes are applied where tag logic exists in scripts/releaser/index.ts and where gh release upload is invoked in the upload-github-assets job.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/snapshot.yml:
- Line 20: The workflow uses an unrecognized self-hosted runner label
"blacksmith-2vcpu-ubuntu-2404" which breaks actionlint; fix by either adding an
actionlint configuration that declares this custom runner label (so actionlint
accepts "blacksmith-2vcpu-ubuntu-2404") or replace every occurrence of the label
with a supported GitHub runner like "ubuntu-latest" or "ubuntu-2404" (ensure you
update the same label consistently across all workflows where it's used).
- Around line 34-41: Add an explicit fork-guard so the publish job cannot run
against forked PRs: detect forks using the PR head repo fields (e.g.
github.event.pull_request.head.repo.fork or compare
github.event.pull_request.head.repo.full_name to github.repository) and add a
conditional to the publish job (the job named "publish") so it only runs when
the PR head repo is the same as the base repo; also ensure steps that fetch and
use the PR head SHA (the "Get PR head SHA" step and the sha variable) remain
unchanged for non-fork PRs but are skipped for forked PRs to prevent
OIDC/write-sensitive actions from executing on forked code.
In `@scripts/releaser/index.ts`:
- Around line 69-71: generatePlatformPackage() currently unconditionally copies
the root LICENSE via licensePath = join(import.meta.dir, "../../LICENSE") which
will ENOENT if the LICENSE is added in a later stacked PR; make the operation
safe by checking existence before copying: use fs.stat/fs.access (or try/catch
around copyFile) to skip the copy if the LICENSE is missing and log a warning,
or alternatively vendor a copy of the needed LICENSE into this stack and change
licensePath to point to the local copy; update the code around
copyFile(join(dir, "LICENSE")) so it either conditionally performs the copy when
the file exists or uses the local packaged LICENSE.
---
Duplicate comments:
In @.github/workflows/release.yml:
- Around line 61-112: The release pipeline can leave a half-complete stable
release that cannot be recovered because tag/release creation and asset uploads
are not idempotent; update scripts/releaser/index.ts and the workflow steps
(publish-npm and upload-github-assets) so tag creation is safe to retry (check
for existing tag v${{ needs.versioning.outputs.version }}, update it or
force-push the tag instead of failing) and make GitHub Release/asset uploads
idempotent by using the GitHub CLI to create-or-edit the release (query with gh
release view and gh release create only if missing, or gh release edit) and
upload assets with overwrite semantics (gh release upload --clobber or delete
existing asset before upload) instead of blindly uploading; ensure these changes
are applied where tag logic exists in scripts/releaser/index.ts and where gh
release upload is invoked in the upload-github-assets job.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 888311dc-8b72-47a8-9c3a-0f2fd69e0a41
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (9)
.changeset/README.md.changeset/config.json.github/workflows/release.yml.github/workflows/snapshot.ymlpackage.jsonscripts/check-release.tsscripts/lib/npm.tsscripts/releaser/index.tsscripts/snapshot.ts
- Remove LICENSE copy from platform packages (added in PR #46) - Make stable release tag/release creation idempotent (remote-aware tag check, independent release existence check) - Add --clobber to GitHub Release asset uploads for safe reruns - Block snapshot publish on fork PRs to prevent OIDC token exposure - Fix notify-failure to fire even when publish job is skipped
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/release.yml:
- Around line 29-40: Update the release workflow to prevent stable releases when
Changesets created a Version Packages PR by gating the stable release path on
the Changesets step output: check the step with id "changesets" and require
steps.changesets.outputs.hasChangesets == 'false' before running the
stable-release branch; also adjust the existing "Check if release needed" logic
(step id "check" which runs scripts/check-release.ts) and/or the "build" job’s
conditional so the canary path continues to run only when release_created !=
'true' or when steps.changesets.outputs.hasChangesets == 'true', ensuring the
stable path waits until the Version Packages PR is merged.
In @.github/workflows/snapshot.yml:
- Around line 35-69: The workflow currently checks of whether a PR is from a
fork in the "Get PR head SHA" step (outputs sha and is_fork) but still
unconditionally runs actions/checkout and downstream jobs against the fork SHA;
add conditional guards to avoid executing untrusted fork code: update the
checkout step (the actions/checkout@v4 usage in the snapshot job) to run only
when the PR head is not a fork by adding a condition referencing
steps.pr.outputs.is_fork (e.g. if: steps.pr.outputs.is_fork != 'true'), and add
the same guard to the build and smoke-test jobs (jobs named build and
smoke-test) so both jobs skip when needs.snapshot.outputs.is_fork == 'true';
keep using the existing outputs (sha/is_fork) from the Get PR head SHA step and
ensure the publish job retains its existing fork guard.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: b91a30ba-7ca2-4862-b188-69c5e682b68a
📒 Files selected for processing (3)
.github/workflows/release.yml.github/workflows/snapshot.ymlscripts/releaser/index.ts
|
If the plan is to open source, I recommend we get a review of the workflows from #team-security |
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
.github/workflows/snapshot.yml (1)
53-69:⚠️ Potential issue | 🟠 MajorFork code executes in
snapshotjob before privilege-gated jobs.While the
build,smoke-test, andpublishjobs correctly gate onis_fork != 'true', thesnapshotjob still checks out and runs fork PR code at lines 53-69 (bun installandbun run version-packages:snapshot). Although this job doesn't haveid-token: write, running untrusted code on self-hosted runners can still be risky (supply-chain attacks, credential exfiltration from runner environment).Consider either:
- Moving the fork check earlier and skipping checkout entirely for forks, or
- Accepting the risk given the
MEMBER/OWNERgating at line 19If the
author_associationcheck at line 19 is considered sufficient (only trusted org members can trigger), this may be acceptable—but document this trust assumption explicitly.🔒 Option 1: Skip checkout for forks
- name: Get PR head SHA id: pr # ... existing code ... + - name: Abort for fork PRs + if: steps.pr.outputs.is_fork == 'true' + run: | + echo "::error::Snapshot builds are not supported for fork PRs" + exit 1 - uses: actions/checkout@v4 + if: steps.pr.outputs.is_fork != 'true' with: ref: ${{ steps.pr.outputs.sha }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/snapshot.yml around lines 53 - 69, The snapshot job currently checks out and executes fork PR code (actions/checkout@v4, oven-sh/setup-bun, bun install, bun run version-packages:snapshot) before any fork gating; update the workflow so untrusted fork code is never run by either (A) moving the existing fork gate (the is_fork check derived from the pr step / author_association logic) up to the start of the snapshot job so the job is skipped entirely for forks, or (B) adding explicit conditionals (e.g., guarding the actions/checkout and the bun install / bun run steps) so those steps only run when is_fork != 'true' or when author_association indicates a trusted MEMBER/OWNER; also, if you choose to rely on the author_association check, add a short comment documenting that trust assumption.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/release.yml:
- Around line 184-188: Add a POSIX-compliant trailing newline to the end of
.github/workflows/release.yml by ensuring the file ends with a single newline
character after the last line (the "Publish canary packages" run block that sets
CANARY_VERSION and ARTIFACTS_DIR), so the file terminates with a newline rather
than ending abruptly.
- Around line 107-113: The release upload loop assumes the GitHub release tag
stored in variable tag ("v${{ needs.versioning.outputs.version }}") exists; add
a pre-check that uses the tag variable and the gh command to verify the release
(or tag) exists before entering the for loop and either create the release/tag
or exit with a clear error via the workflow runner; update the block containing
tag, the for loop, and gh release upload to first run a check (using the same
tag variable) and only proceed to gh release upload for each artifact if the
check succeeds.
---
Duplicate comments:
In @.github/workflows/snapshot.yml:
- Around line 53-69: The snapshot job currently checks out and executes fork PR
code (actions/checkout@v4, oven-sh/setup-bun, bun install, bun run
version-packages:snapshot) before any fork gating; update the workflow so
untrusted fork code is never run by either (A) moving the existing fork gate
(the is_fork check derived from the pr step / author_association logic) up to
the start of the snapshot job so the job is skipped entirely for forks, or (B)
adding explicit conditionals (e.g., guarding the actions/checkout and the bun
install / bun run steps) so those steps only run when is_fork != 'true' or when
author_association indicates a trusted MEMBER/OWNER; also, if you choose to rely
on the author_association check, add a short comment documenting that trust
assumption.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: c3754200-2257-4366-9965-b0ab084fd07c
📒 Files selected for processing (3)
.github/actionlint.yaml.github/workflows/release.yml.github/workflows/snapshot.yml
| run: | | ||
| tag="v${{ needs.versioning.outputs.version }}" | ||
| for dir in dist/artifacts/clerk-*/; do | ||
| target=${dir#dist/artifacts/clerk-} && target=${target%/} | ||
| ext=""; [[ "$target" == win32-* ]] && ext=".exe" | ||
| gh release upload "$tag" "${dir}clerk${ext}#clerk-${target}${ext}" --clobber | ||
| done |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider adding error handling for missing release tag.
The script assumes the release tag v${version} already exists. If the tag wasn't created by a previous step (e.g., publish-npm failed to create it), this step will fail. Consider adding a check or ensuring the releaser script creates the tag before this job runs.
🛡️ Optional: Add existence check
run: |
tag="v${{ needs.versioning.outputs.version }}"
+ if ! gh release view "$tag" &>/dev/null; then
+ echo "::error::Release $tag does not exist"
+ exit 1
+ fi
for dir in dist/artifacts/clerk-*/; do📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| run: | | |
| tag="v${{ needs.versioning.outputs.version }}" | |
| for dir in dist/artifacts/clerk-*/; do | |
| target=${dir#dist/artifacts/clerk-} && target=${target%/} | |
| ext=""; [[ "$target" == win32-* ]] && ext=".exe" | |
| gh release upload "$tag" "${dir}clerk${ext}#clerk-${target}${ext}" --clobber | |
| done | |
| run: | | |
| tag="v${{ needs.versioning.outputs.version }}" | |
| if ! gh release view "$tag" &>/dev/null; then | |
| echo "::error::Release $tag does not exist" | |
| exit 1 | |
| fi | |
| for dir in dist/artifacts/clerk-*/; do | |
| target=${dir#dist/artifacts/clerk-} && target=${target%/} | |
| ext=""; [[ "$target" == win32-* ]] && ext=".exe" | |
| gh release upload "$tag" "${dir}clerk${ext}#clerk-${target}${ext}" --clobber | |
| done |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/release.yml around lines 107 - 113, The release upload
loop assumes the GitHub release tag stored in variable tag ("v${{
needs.versioning.outputs.version }}") exists; add a pre-check that uses the tag
variable and the gh command to verify the release (or tag) exists before
entering the for loop and either create the release/tag or exit with a clear
error via the workflow runner; update the block containing tag, the for loop,
and gh release upload to first run a check (using the same tag variable) and
only proceed to gh release upload for each artifact if the check succeeds.
| - name: Publish canary packages | ||
| run: bun run release:canary --version "$CANARY_VERSION" | ||
| env: | ||
| CANARY_VERSION: ${{ needs.canary-version.outputs.version }} | ||
| ARTIFACTS_DIR: ${{ github.workspace }}/dist/artifacts |
There was a problem hiding this comment.
Missing trailing newline at end of file.
The file ends at line 188 without a trailing newline. POSIX text files should end with a newline.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/release.yml around lines 184 - 188, Add a POSIX-compliant
trailing newline to the end of .github/workflows/release.yml by ensuring the
file ends with a single newline character after the last line (the "Publish
canary packages" run block that sets CANARY_VERSION and ARTIFACTS_DIR), so the
file terminates with a newline rather than ending abruptly.
Summary
#43 and #47 give us a monorepo structure and cross-compiled binaries, but no way to get them to users. We need an automated release pipeline that handles versioning, npm publishing of 9+ packages (1 wrapper + 8 platform packages), and GitHub Releases -- across three channels with different triggers and safeguards.
This PR adds the full release infrastructure:
.changeset/config.json,@changesets/cli) for version management. Contributors add changeset files to PRs; thechangesets/actionbot creates a "Version Packages" PR that bumps versions when merged.scripts/releaser/index.ts-- the core publish script. Generates a platform package for each target (with the correctos/cpu/libcfields), publishes them to npm, updates the wrapper'soptionalDependencies, publishes the wrapper, creates a git tag, and uploads binaries to a GitHub Release. Supports--dry-runand--tagfor channel selection.scripts/snapshot.ts-- versions packages for snapshot and canary channels using Changesets snapshot mode, producing monotonically-sortable versions like0.1.0-snapshot.v20260313145959.scripts/check-release.ts-- detects whether the current version is already published on npm, used by CI to decide whether to trigger a stable release.scripts/lib/npm.ts-- sharedisPublished()helper that queries the npm registry.release.yml-- CI workflow for stable and canary releases. On push to main: runscheck-release.ts, if unpublished triggers build → smoke-test → publish. If no stable release needed, publishes a canary instead.snapshot.yml-- CI workflow triggered by!snapshotcomments on PRs. Builds from the PR branch, smoke-tests, publishes snapshot packages, and posts the install command back as a PR comment.Merge instructions
This is part of a 4-PR stack (#43, #47, #45, #46). After squash-merging this PR, retarget #46 to
mainbefore deleting this branch, then:Then merge #46.
Test plan
format:check,lint,build,testrelease.ymlworkflow triggers and job dependenciessnapshot.ymlcomment-trigger logic and PR comment formatscripts/releaser/index.tspublish flow handles all 3 channels (stable, canary, snapshot)Summary by CodeRabbit
Documentation
Chores
Style