Uh oh!
There was an error while loading. Please reload this page.
feat: add release infrastructure with changesets [3/4] - #45
Conversation
e6a73f6 to
3bcc5eaCompare2bb7c54 to
ba9a09cComparewyattjoh
commented
Mar 20, 2026
|
3bcc5ea to
cb764f6Compareba9a09c to
0aa883aComparecb764f6 to
fb82e61Compare0aa883a to
51be70aCompare51be70a to
2b7f7c7Compare📝 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
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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
d849ce6CompareThere 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
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- 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
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
jfoshee
commented
Mar 20, 2026
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