From 9671e4f94937342ef0ab02eb2c460c05e71d4e68 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 10:41:35 -0600 Subject: [PATCH 01/44] feat: add release infrastructure with cross-compiled binaries Restructure into bun workspace monorepo, add npm wrapper package with platform binary shim, release-please configuration, cross-compilation release workflow, native smoke tests, musl/Alpine support, and MIT license. --- .github/workflows/ci.yml | 10 +- .github/workflows/release.yml | 183 ++++++++++++++++++ .gitignore | 3 + .release-please-manifest.json | 3 + LICENSE | 21 ++ README.md | 33 ++-- bun.lock | 32 ++- docs/releasing.md | 118 +++++++++++ package.json | 34 ++-- {mocks => packages/cli-core/mocks}/bun.lock | 0 {mocks => packages/cli-core/mocks}/index.html | 0 .../cli-core/mocks}/package.json | 4 +- .../cli-core/mocks}/src/main.ts | 11 +- .../cli-core/mocks}/src/style.css | 4 +- .../cli-core/mocks}/tsconfig.json | 0 packages/cli-core/package.json | 23 +++ {src => packages/cli-core/src}/cli-program.ts | 13 +- {src => packages/cli-core/src}/cli.ts | 2 +- .../cli-core/src}/commands/api/README.md | 0 .../cli-core/src}/commands/api/bapi.test.ts | 0 .../cli-core/src}/commands/api/bapi.ts | 0 .../src}/commands/api/catalog.test.ts | 0 .../cli-core/src}/commands/api/catalog.ts | 0 .../cli-core/src}/commands/api/index.test.ts | 0 .../cli-core/src}/commands/api/index.ts | 0 .../src}/commands/api/interactive.test.ts | 0 .../cli-core/src}/commands/api/interactive.ts | 0 .../cli-core/src}/commands/api/ls.test.ts | 0 .../cli-core/src}/commands/api/ls.ts | 0 .../cli-core/src}/commands/auth/README.md | 0 .../cli-core/src}/commands/auth/login.test.ts | 0 .../cli-core/src}/commands/auth/login.ts | 0 .../src}/commands/auth/logout.test.ts | 0 .../cli-core/src}/commands/auth/logout.ts | 0 .../cli-core/src}/commands/config/README.md | 0 .../src}/commands/config/pull.test.ts | 0 .../cli-core/src}/commands/config/pull.ts | 0 .../src}/commands/config/push.test.ts | 0 .../cli-core/src}/commands/config/push.ts | 0 .../src}/commands/config/schema.test.ts | 0 .../cli-core/src}/commands/config/schema.ts | 0 .../cli-core/src}/commands/deploy/README.md | 0 .../src}/commands/deploy/index.test.ts | 0 .../cli-core/src}/commands/deploy/index.ts | 0 .../cli-core/src}/commands/doctor/README.md | 0 .../cli-core/src}/commands/doctor/checks.ts | 0 .../src}/commands/doctor/context.test.ts | 0 .../cli-core/src}/commands/doctor/context.ts | 0 .../src}/commands/doctor/doctor.test.ts | 0 .../cli-core/src}/commands/doctor/format.ts | 0 .../cli-core/src}/commands/doctor/index.ts | 0 .../cli-core/src}/commands/doctor/types.ts | 0 .../cli-core/src}/commands/env/README.md | 0 .../cli-core/src}/commands/env/pull.test.ts | 0 .../cli-core/src}/commands/env/pull.ts | 0 .../cli-core/src}/commands/init/README.md | 0 .../cli-core/src}/commands/init/index.ts | 0 .../cli-core/src}/commands/link/README.md | 0 .../cli-core/src}/commands/link/index.test.ts | 0 .../cli-core/src}/commands/link/index.ts | 0 .../cli-core/src}/commands/unlink/README.md | 0 .../src}/commands/unlink/index.test.ts | 0 .../cli-core/src}/commands/unlink/index.ts | 0 .../cli-core/src}/commands/whoami/README.md | 0 .../src}/commands/whoami/index.test.ts | 0 .../cli-core/src}/commands/whoami/index.ts | 0 packages/cli-core/src/globals.d.ts | 1 + .../cli-core/src}/lib/auth-server.test.ts | 0 .../cli-core/src}/lib/auth-server.ts | 0 .../cli-core/src}/lib/autolink.test.ts | 0 .../cli-core/src}/lib/autolink.ts | 0 {src => packages/cli-core/src}/lib/color.ts | 0 .../cli-core/src}/lib/config.test.ts | 0 {src => packages/cli-core/src}/lib/config.ts | 0 .../cli-core/src}/lib/constants.ts | 0 .../src}/lib/credential-store.test.ts | 0 .../cli-core/src}/lib/credential-store.ts | 0 .../cli-core/src}/lib/dotenv.test.ts | 0 {src => packages/cli-core/src}/lib/dotenv.ts | 0 {src => packages/cli-core/src}/lib/errors.ts | 0 .../cli-core/src}/lib/framework.test.ts | 0 .../cli-core/src}/lib/framework.ts | 0 .../cli-core/src}/lib/git.test.ts | 0 {src => packages/cli-core/src}/lib/git.ts | 0 .../cli-core/src}/lib/pkce.test.ts | 0 {src => packages/cli-core/src}/lib/pkce.ts | 0 .../cli-core/src}/lib/plapi.test.ts | 0 {src => packages/cli-core/src}/lib/plapi.ts | 0 .../cli-core/src}/lib/token-exchange.test.ts | 0 .../cli-core/src}/lib/token-exchange.ts | 0 {src => packages/cli-core/src}/mode.test.ts | 0 {src => packages/cli-core/src}/mode.ts | 0 .../src}/test/integration/agent-mode.test.ts | 0 .../src}/test/integration/api-queries.test.ts | 0 .../test/integration/auth-lifecycle.test.ts | 0 .../integration/config-management.test.ts | 0 .../src}/test/integration/config-put.test.ts | 0 .../test/integration/deploy-to-prod.test.ts | 0 .../src}/test/integration/dry-run.test.ts | 0 .../src}/test/integration/env-merge.test.ts | 0 .../test/integration/error-recovery.test.ts | 0 .../src}/test/integration/onboard.test.ts | 0 .../src}/test/integration/switch-apps.test.ts | 0 .../cli-core/src}/test/lib/http.ts | 0 .../cli-core/src}/test/lib/setup.ts | 2 +- {src => packages/cli-core/src}/test/stubs.ts | 0 .../cli-core/tsconfig.json | 0 packages/cli/bin/clerk | 63 ++++++ packages/cli/package.json | 32 +++ release-please-config.json | 7 + scripts/releaser/index.ts | 117 +++++++++++ scripts/releaser/targets.ts | 23 +++ 112 files changed, 668 insertions(+), 71 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .release-please-manifest.json create mode 100644 LICENSE create mode 100644 docs/releasing.md rename {mocks => packages/cli-core/mocks}/bun.lock (100%) rename {mocks => packages/cli-core/mocks}/index.html (100%) rename {mocks => packages/cli-core/mocks}/package.json (75%) rename {mocks => packages/cli-core/mocks}/src/main.ts (96%) rename {mocks => packages/cli-core/mocks}/src/style.css (97%) rename {mocks => packages/cli-core/mocks}/tsconfig.json (100%) create mode 100644 packages/cli-core/package.json rename {src => packages/cli-core/src}/cli-program.ts (95%) rename {src => packages/cli-core/src}/cli.ts (84%) rename {src => packages/cli-core/src}/commands/api/README.md (100%) rename {src => packages/cli-core/src}/commands/api/bapi.test.ts (100%) rename {src => packages/cli-core/src}/commands/api/bapi.ts (100%) rename {src => packages/cli-core/src}/commands/api/catalog.test.ts (100%) rename {src => packages/cli-core/src}/commands/api/catalog.ts (100%) rename {src => packages/cli-core/src}/commands/api/index.test.ts (100%) rename {src => packages/cli-core/src}/commands/api/index.ts (100%) rename {src => packages/cli-core/src}/commands/api/interactive.test.ts (100%) rename {src => packages/cli-core/src}/commands/api/interactive.ts (100%) rename {src => packages/cli-core/src}/commands/api/ls.test.ts (100%) rename {src => packages/cli-core/src}/commands/api/ls.ts (100%) rename {src => packages/cli-core/src}/commands/auth/README.md (100%) rename {src => packages/cli-core/src}/commands/auth/login.test.ts (100%) rename {src => packages/cli-core/src}/commands/auth/login.ts (100%) rename {src => packages/cli-core/src}/commands/auth/logout.test.ts (100%) rename {src => packages/cli-core/src}/commands/auth/logout.ts (100%) rename {src => packages/cli-core/src}/commands/config/README.md (100%) rename {src => packages/cli-core/src}/commands/config/pull.test.ts (100%) rename {src => packages/cli-core/src}/commands/config/pull.ts (100%) rename {src => packages/cli-core/src}/commands/config/push.test.ts (100%) rename {src => packages/cli-core/src}/commands/config/push.ts (100%) rename {src => packages/cli-core/src}/commands/config/schema.test.ts (100%) rename {src => packages/cli-core/src}/commands/config/schema.ts (100%) rename {src => packages/cli-core/src}/commands/deploy/README.md (100%) rename {src => packages/cli-core/src}/commands/deploy/index.test.ts (100%) rename {src => packages/cli-core/src}/commands/deploy/index.ts (100%) rename {src => packages/cli-core/src}/commands/doctor/README.md (100%) rename {src => packages/cli-core/src}/commands/doctor/checks.ts (100%) rename {src => packages/cli-core/src}/commands/doctor/context.test.ts (100%) rename {src => packages/cli-core/src}/commands/doctor/context.ts (100%) rename {src => packages/cli-core/src}/commands/doctor/doctor.test.ts (100%) rename {src => packages/cli-core/src}/commands/doctor/format.ts (100%) rename {src => packages/cli-core/src}/commands/doctor/index.ts (100%) rename {src => packages/cli-core/src}/commands/doctor/types.ts (100%) rename {src => packages/cli-core/src}/commands/env/README.md (100%) rename {src => packages/cli-core/src}/commands/env/pull.test.ts (100%) rename {src => packages/cli-core/src}/commands/env/pull.ts (100%) rename {src => packages/cli-core/src}/commands/init/README.md (100%) rename {src => packages/cli-core/src}/commands/init/index.ts (100%) rename {src => packages/cli-core/src}/commands/link/README.md (100%) rename {src => packages/cli-core/src}/commands/link/index.test.ts (100%) rename {src => packages/cli-core/src}/commands/link/index.ts (100%) rename {src => packages/cli-core/src}/commands/unlink/README.md (100%) rename {src => packages/cli-core/src}/commands/unlink/index.test.ts (100%) rename {src => packages/cli-core/src}/commands/unlink/index.ts (100%) rename {src => packages/cli-core/src}/commands/whoami/README.md (100%) rename {src => packages/cli-core/src}/commands/whoami/index.test.ts (100%) rename {src => packages/cli-core/src}/commands/whoami/index.ts (100%) create mode 100644 packages/cli-core/src/globals.d.ts rename {src => packages/cli-core/src}/lib/auth-server.test.ts (100%) rename {src => packages/cli-core/src}/lib/auth-server.ts (100%) rename {src => packages/cli-core/src}/lib/autolink.test.ts (100%) rename {src => packages/cli-core/src}/lib/autolink.ts (100%) rename {src => packages/cli-core/src}/lib/color.ts (100%) rename {src => packages/cli-core/src}/lib/config.test.ts (100%) rename {src => packages/cli-core/src}/lib/config.ts (100%) rename {src => packages/cli-core/src}/lib/constants.ts (100%) rename {src => packages/cli-core/src}/lib/credential-store.test.ts (100%) rename {src => packages/cli-core/src}/lib/credential-store.ts (100%) rename {src => packages/cli-core/src}/lib/dotenv.test.ts (100%) rename {src => packages/cli-core/src}/lib/dotenv.ts (100%) rename {src => packages/cli-core/src}/lib/errors.ts (100%) rename {src => packages/cli-core/src}/lib/framework.test.ts (100%) rename {src => packages/cli-core/src}/lib/framework.ts (100%) rename {src => packages/cli-core/src}/lib/git.test.ts (100%) rename {src => packages/cli-core/src}/lib/git.ts (100%) rename {src => packages/cli-core/src}/lib/pkce.test.ts (100%) rename {src => packages/cli-core/src}/lib/pkce.ts (100%) rename {src => packages/cli-core/src}/lib/plapi.test.ts (100%) rename {src => packages/cli-core/src}/lib/plapi.ts (100%) rename {src => packages/cli-core/src}/lib/token-exchange.test.ts (100%) rename {src => packages/cli-core/src}/lib/token-exchange.ts (100%) rename {src => packages/cli-core/src}/mode.test.ts (100%) rename {src => packages/cli-core/src}/mode.ts (100%) rename {src => packages/cli-core/src}/test/integration/agent-mode.test.ts (100%) rename {src => packages/cli-core/src}/test/integration/api-queries.test.ts (100%) rename {src => packages/cli-core/src}/test/integration/auth-lifecycle.test.ts (100%) rename {src => packages/cli-core/src}/test/integration/config-management.test.ts (100%) rename {src => packages/cli-core/src}/test/integration/config-put.test.ts (100%) rename {src => packages/cli-core/src}/test/integration/deploy-to-prod.test.ts (100%) rename {src => packages/cli-core/src}/test/integration/dry-run.test.ts (100%) rename {src => packages/cli-core/src}/test/integration/env-merge.test.ts (100%) rename {src => packages/cli-core/src}/test/integration/error-recovery.test.ts (100%) rename {src => packages/cli-core/src}/test/integration/onboard.test.ts (100%) rename {src => packages/cli-core/src}/test/integration/switch-apps.test.ts (100%) rename {src => packages/cli-core/src}/test/lib/http.ts (100%) rename {src => packages/cli-core/src}/test/lib/setup.ts (99%) rename {src => packages/cli-core/src}/test/stubs.ts (100%) rename tsconfig.json => packages/cli-core/tsconfig.json (100%) create mode 100755 packages/cli/bin/clerk create mode 100644 packages/cli/package.json create mode 100644 release-please-config.json create mode 100644 scripts/releaser/index.ts create mode 100644 scripts/releaser/targets.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60c8fddad..7b062f134 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,12 +20,4 @@ jobs: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 - run: bun install --frozen-lockfile - - name: Run tests - run: | - failed=0 - for f in $(find src -name '*.test.ts' | sort); do - if ! bun test "$f"; then - failed=1 - fi - done - exit $failed + - run: bun run --filter @clerk/cli-core test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..8e8f4b779 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,183 @@ +name: Release + +on: + push: + branches: [main] + +permissions: + contents: write + pull-requests: write + id-token: write + +jobs: + release-please: + runs-on: blacksmith-2vcpu-ubuntu-2404 + outputs: + release_created: ${{ steps.release.outputs['packages/cli--release_created'] }} + tag_name: ${{ steps.release.outputs['packages/cli--tag_name'] }} + version: ${{ steps.release.outputs['packages/cli--version'] }} + steps: + - id: release + uses: googleapis/release-please-action@v4 + + build: + needs: release-please + if: ${{ needs.release-please.outputs.release_created == 'true' }} + runs-on: blacksmith-2vcpu-ubuntu-2404 + strategy: + fail-fast: false + matrix: + # Keep in sync with scripts/releaser/targets.ts + include: + - target: darwin-arm64 + bun_target: bun-darwin-arm64 + ext: "" + - target: darwin-x64 + bun_target: bun-darwin-x64 + ext: "" + - target: linux-arm64 + bun_target: bun-linux-arm64 + ext: "" + - target: linux-arm64-musl + bun_target: bun-linux-arm64-musl + ext: "" + - target: linux-x64 + bun_target: bun-linux-x64 + ext: "" + - target: linux-x64-musl + bun_target: bun-linux-x64-musl + ext: "" + - target: win32-arm64 + bun_target: bun-windows-arm64 + ext: ".exe" + - target: win32-x64 + bun_target: bun-windows-x64 + ext: ".exe" + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + + - name: Build compiled binary + env: + CLI_VERSION: ${{ needs.release-please.outputs.version }} + run: | + bun build --compile \ + --no-compile-autoload-dotenv \ + --target=${{ matrix.bun_target }} \ + --define "CLI_VERSION=\"$CLI_VERSION\"" \ + ./packages/cli-core/src/cli.ts \ + --outfile clerk${{ matrix.ext }} + + - name: Verify binary format + run: | + expected_format="${{ matrix.target }}" + binary="clerk${{ matrix.ext }}" + file_output=$(file "$binary") + echo "Binary: $file_output" + case "$expected_format" in + darwin-arm64) echo "$file_output" | grep -q "Mach-O.*arm64" ;; + darwin-x64) echo "$file_output" | grep -q "Mach-O.*x86_64" ;; + linux-arm64*) echo "$file_output" | grep -q "ELF.*ARM aarch64" ;; + linux-x64*) echo "$file_output" | grep -q "ELF.*x86-64" ;; + win32-arm64) echo "$file_output" | grep -q "PE32+.*Aarch64" ;; + win32-x64) echo "$file_output" | grep -q "PE32+.*x86-64" ;; + *) echo "Unknown target: $expected_format" && exit 1 ;; + esac + echo "Binary format verified for $expected_format" + + - uses: actions/upload-artifact@v4 + with: + name: clerk-${{ matrix.target }} + path: clerk${{ matrix.ext }} + + smoke-test: + needs: build + strategy: + fail-fast: false + matrix: + include: + - target: darwin-arm64 + runner: macos-latest + bin: ./clerk + - target: darwin-x64 + runner: macos-13 + bin: ./clerk + - target: linux-arm64 + runner: ubuntu-24.04-arm + bin: ./clerk + - target: linux-arm64-musl + runner: ubuntu-24.04-arm + bin: ./clerk + docker: alpine + - target: linux-x64 + runner: ubuntu-latest + bin: ./clerk + - target: linux-x64-musl + runner: ubuntu-latest + bin: ./clerk + docker: alpine + - target: win32-x64 + runner: windows-latest + bin: ./clerk.exe + # win32-arm64: no GitHub-hosted ARM Windows runner available + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/download-artifact@v4 + with: + name: clerk-${{ matrix.target }} + - name: Make executable + if: runner.os != 'Windows' + run: chmod +x ${{ matrix.bin }} + - name: Smoke test (native) + if: ${{ !matrix.docker }} + shell: bash + run: | + version=$(${{ matrix.bin }} --version) + echo "Binary reports $version" + - name: Smoke test (docker) + if: ${{ matrix.docker }} + run: | + docker run --rm -v "$PWD:/work" ${{ matrix.docker }} /work/${{ matrix.bin }} --version + + publish-npm: + needs: [release-please, build, smoke-test] + runs-on: blacksmith-2vcpu-ubuntu-2404 + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - uses: actions/setup-node@v4 + with: + node-version: "22" + registry-url: "https://registry.npmjs.org" + - run: bun install --frozen-lockfile + + - uses: actions/download-artifact@v4 + with: + path: dist/artifacts + + - name: Publish packages + run: bun run scripts/releaser/index.ts + env: + ARTIFACTS_DIR: ${{ github.workspace }}/dist/artifacts + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + upload-github-assets: + needs: [release-please, build, smoke-test, publish-npm] + runs-on: blacksmith-2vcpu-ubuntu-2404 + steps: + - uses: actions/download-artifact@v4 + with: + path: dist/artifacts + + - name: Upload binaries to GitHub Release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + tag="${{ needs.release-please.outputs.tag_name }}" + 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}" + done diff --git a/.gitignore b/.gitignore index a14702c40..db941ea17 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store + +# Bun compile artifacts +*.bun-build diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 000000000..cacc6b142 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + "packages/cli": "0.0.1" +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..49e46cae1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Clerk, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 8a26fdbc2..6f5dfec97 100644 --- a/README.md +++ b/README.md @@ -8,31 +8,36 @@ Usage: clerk [options] [command] Clerk CLI Options: - -V, --version Display version - --mode Force interaction mode (human or agent). - Defaults to auto-detect based on TTY. - -h, --help Display help for command + -V, --version output the version number + --mode Force interaction mode (human or agent). + Defaults to auto-detect based on TTY. + --verbose Show detailed error output + -h, --help display help for command Commands: - init [options] Initialize Clerk in your project - auth Manage authentication + init Initialize Clerk in your project + auth Manage authentication login|signup Log in to your Clerk account logout|signout Log out of your Clerk account - link [options] Link this project to a Clerk application - unlink [options] Unlink this project from its Clerk application - whoami Show the current logged-in user - config Manage instance configuration + link [options] Link this project to a Clerk application + unlink [options] Unlink this project from its Clerk + application + whoami Show the current logged-in user + env Manage environment variables + pull [options] Pull environment variables from Clerk to .env.local + config Manage instance configuration pull [options] Pull instance configuration from Clerk schema [options] Pull instance config schema from Clerk patch [options] Partially update instance configuration (PATCH) put [options] Replace entire instance configuration (PUT) - env Manage environment variables - pull [options] Pull environment variables from Clerk to .env.local - api [options] [endpoint] [filter] Make authenticated requests to the Clerk API + api [options] [endpoint] [filter] Make authenticated requests to the Clerk + API ls [filter] List available API endpoints (no args) Interactive request builder (TTY only) - doctor [options] Check your project's Clerk integration health + doctor [options] Check your project's Clerk integration + health deploy [options] Deploy your Clerk application (hidden) + help [command] display help for command clerk init --prompt Output a prompt for an AI agent to integrate Clerk diff --git a/bun.lock b/bun.lock index 1bc728390..22e5a6da1 100644 --- a/bun.lock +++ b/bun.lock @@ -4,16 +4,8 @@ "workspaces": { "": { "name": "marseille", - "dependencies": { - "@commander-js/extra-typings": "^14.0.0", - "@inquirer/prompts": "^8.2.0", - "@napi-rs/keyring": "^1.2.0", - "commander": "^14.0.3", - "env-paths": "^4.0.0", - "yaml": "^2.8.2", - }, "devDependencies": { - "@types/bun": "latest", + "@types/bun": "^1.3.9", "nano-staged": "^0.9.0", "oxfmt": "^0.36.0", "oxlint": "^1.51.0", @@ -22,9 +14,29 @@ "typescript": "^5", }, }, + "packages/cli": { + "name": "@clerk/cli", + "version": "0.0.1", + "bin": { + "clerk": "./bin/clerk", + }, + }, + "packages/cli-core": { + "name": "@clerk/cli-core", + "version": "0.0.0", + "dependencies": { + "@inquirer/prompts": "^8.2.0", + "@napi-rs/keyring": "^1.2.0", + "commander": "^14.0.3", + "env-paths": "^4.0.0", + "yaml": "^2.8.2", + }, + }, }, "packages": { - "@commander-js/extra-typings": ["@commander-js/extra-typings@14.0.0", "", { "peerDependencies": { "commander": "~14.0.0" } }, "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg=="], + "@clerk/cli": ["@clerk/cli@workspace:packages/cli"], + + "@clerk/cli-core": ["@clerk/cli-core@workspace:packages/cli-core"], "@inquirer/ansi": ["@inquirer/ansi@2.0.3", "", {}, "sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw=="], diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 000000000..5d6e351fb --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,118 @@ +# Release Flow + +This document describes how the Clerk CLI is built, versioned, and published. + +## Overview + +``` +push to main + → release-please creates/updates a version PR + → merge version PR + → release-please creates a GitHub Release + tag + → build job: cross-compile binaries (6 targets) + → smoke-test job: verify binaries on native runners (5 targets) + → publish-npm: generate platform packages + publish wrapper + → upload-github-assets: attach binaries to the GitHub Release +``` + +## Architecture + +The CLI is distributed as an npm wrapper package (`@clerk/cli`) plus one platform-specific package per target (e.g., `@clerk/cli-darwin-arm64`). The full list of platform targets is defined in [`scripts/releaser/targets.ts`](../scripts/releaser/targets.ts). + +When a user runs `npm install -g @clerk/cli`, npm installs the wrapper plus the matching platform package via `optionalDependencies`. The wrapper's `bin/clerk` shim resolves the binary from the platform package using `require.resolve()`. + +Target names follow Node.js's `${process.platform}-${process.arch}` convention so the shim can derive package names without a lookup table. + +## Versioning + +Versioning is managed by [release-please](https://github.com/googleapis/release-please). On every push to `main`, it either creates a new version PR or updates an existing one based on conventional commit messages. Merging that PR triggers a GitHub Release. + +Configuration: + +- `release-please-config.json` — release-please settings (release type, package path) +- `.release-please-manifest.json` — tracks the current version + +## Build Pipeline + +The release workflow (`.github/workflows/release.yml`) runs when release-please creates a release. Build and publish jobs run on Blacksmith runners (`blacksmith-2vcpu-ubuntu-2404`); smoke tests run on platform-native GitHub-hosted runners. + +### 1. Build Job (matrix) + +Runs once per target platform on a Blacksmith runner. Each job: + +1. Cross-compiles the CLI using `bun build --compile --no-compile-autoload-dotenv --target=` +2. Injects the version via `--define "CLI_VERSION=\"$CLI_VERSION\""` +3. Verifies the binary format using `file` output +4. Uploads the binary as a GitHub Actions artifact + +### 2. Smoke Test Job (matrix) + +Downloads each compiled binary and runs `--version` on a native runner for that platform to verify the binary actually executes. The target-to-runner mapping is defined in the `smoke-test` matrix in [`.github/workflows/release.yml`](../.github/workflows/release.yml). + +Not all targets may have a native runner available (e.g., `win32-arm64` is skipped because there is no GitHub-hosted ARM Windows runner). + +Publishing and GitHub Release upload are gated on all smoke tests passing. + +### 3. Publish npm Job + +Runs the releaser script (`scripts/releaser/index.ts`): + +1. Reads the version from `packages/cli/package.json` +2. For each target, generates a platform package in `dist/platform-packages/`: + - Creates `package.json` with `os`/`cpu` fields for npm platform selection + - Copies the compiled binary from the build artifacts +3. Publishes each platform package with `--provenance --access public` +4. Temporarily mutates the wrapper `package.json` to add `optionalDependencies` and remove `private: true`, publishes it, then restores the original file + +All publishes are idempotent — the script checks `npm view` before publishing and skips already-published versions. + +### 4. Upload GitHub Assets Job + +Attaches the compiled binaries to the GitHub Release for direct download. Binaries are uploaded with display names following the `clerk-` convention (e.g., `clerk-darwin-arm64`, `clerk-win32-x64.exe`). + +## Key Files + +| File | Purpose | +| ------------------------------------ | ------------------------------------------------------------------------------ | +| `packages/cli/bin/clerk` | CJS shim that resolves and spawns the platform binary | +| `packages/cli/package.json` | Wrapper package (has `prepublishOnly` guard against accidental direct publish) | +| `packages/cli-core/src/cli.ts` | CLI entrypoint (reads `CLI_VERSION` global at runtime) | +| `packages/cli-core/src/globals.d.ts` | TypeScript declaration for the `CLI_VERSION` compile-time define | +| `scripts/releaser/index.ts` | Generates platform packages and publishes everything to npm | +| `scripts/releaser/targets.ts` | Target definitions — must be kept in sync with the workflow matrix | +| `.github/workflows/release.yml` | GitHub Actions release workflow | + +## Keeping Targets in Sync + +The target list exists in three places that must stay in sync: + +1. `scripts/releaser/targets.ts` — used by the releaser to generate platform packages +2. `.github/workflows/release.yml` build matrix — compiles binaries (maps target names to Bun cross-compile targets, e.g., `win32-x64` → `bun-windows-x64`) +3. `.github/workflows/release.yml` smoke-test matrix — verifies binaries on native runners + +If you add or remove a target, update all three. Note that the smoke-test matrix may not cover every target if a native runner isn't available (e.g., `win32-arm64`). + +## Local Development + +To build a compiled binary locally for your native platform: + +```sh +bun run --filter @clerk/cli-core build:compile +./packages/cli-core/dist/clerk --version # prints "0.0.0-dev" +``` + +This does not inject a version (falls back to `0.0.0-dev`) and only builds for your current platform. The release workflow handles cross-compilation and version injection. + +To test the releaser without publishing: + +```sh +bun run scripts/releaser/index.ts --dry-run +``` + +## Safeguards + +- **`prepublishOnly` guard**: The wrapper `package.json` has a `prepublishOnly` script that exits with an error, preventing accidental `npm publish` from the package directory. The releaser bypasses this with `--ignore-scripts`. +- **`private: true`**: Both `packages/cli` and `packages/cli-core` are marked private. The releaser removes this flag from the wrapper before publishing and restores it afterward. +- **Idempotent publishing**: The releaser checks npm before publishing and skips already-published versions, making it safe to re-run. +- **Binary format verification**: The build job verifies each compiled binary matches its expected architecture before uploading. +- **Native smoke tests**: Each binary is executed on a native runner for its platform before publishing. This catches cross-compilation issues that format checks alone would miss. diff --git a/package.json b/package.json index 3cff281b4..b13216b4b 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,20 @@ { - "name": "@clerk/cli", - "version": "0.0.1", - "description": "Clerk CLI", - "bin": { - "clerk": "./dist/cli.js" - }, - "type": "module", + "name": "cli-new", + "private": true, + "workspaces": [ + "packages/*" + ], "scripts": { - "build": "bun build ./src/cli.ts --outfile ./dist/cli.js --target node --external @napi-rs/keyring", - "dev": "bun run ./src/cli.ts", - "test": "bun test", - "lint": "oxlint src/", - "format": "oxfmt --write src/", - "format:check": "oxfmt --check src/", + "build": "bun run --filter @clerk/cli-core build", + "dev": "bun run --filter @clerk/cli-core dev", + "test": "bun run --filter @clerk/cli-core test", + "lint": "bun run --filter @clerk/cli-core lint", + "format": "bun run --filter @clerk/cli-core format", + "format:check": "bun run --filter @clerk/cli-core format:check", "prepare": "git config core.hooksPath .hooks" }, - "dependencies": { - "@commander-js/extra-typings": "^14.0.0", - "@inquirer/prompts": "^8.2.0", - "@napi-rs/keyring": "^1.2.0", - "commander": "^14.0.3", - "env-paths": "^4.0.0", - "yaml": "^2.8.2" - }, "devDependencies": { - "@types/bun": "latest", + "@types/bun": "^1.3.9", "nano-staged": "^0.9.0", "oxfmt": "^0.36.0", "oxlint": "^1.51.0" diff --git a/mocks/bun.lock b/packages/cli-core/mocks/bun.lock similarity index 100% rename from mocks/bun.lock rename to packages/cli-core/mocks/bun.lock diff --git a/mocks/index.html b/packages/cli-core/mocks/index.html similarity index 100% rename from mocks/index.html rename to packages/cli-core/mocks/index.html diff --git a/mocks/package.json b/packages/cli-core/mocks/package.json similarity index 75% rename from mocks/package.json rename to packages/cli-core/mocks/package.json index 15c083e69..d7f08b022 100644 --- a/mocks/package.json +++ b/packages/cli-core/mocks/package.json @@ -6,7 +6,7 @@ "dev": "vite" }, "devDependencies": { - "vite": "^6.3.5", - "typescript": "^5" + "typescript": "^5", + "vite": "^6.3.5" } } diff --git a/mocks/src/main.ts b/packages/cli-core/mocks/src/main.ts similarity index 96% rename from mocks/src/main.ts rename to packages/cli-core/mocks/src/main.ts index 0a67e7135..0df3200aa 100644 --- a/mocks/src/main.ts +++ b/packages/cli-core/mocks/src/main.ts @@ -4,12 +4,7 @@ const app = document.getElementById("app")!; const params = new URLSearchParams(window.location.search); const callbackPort = params.get("callback_port"); -type Screen = - | "sign-in" - | "sign-up" - | "select-app" - | "success-new" - | "success-existing"; +type Screen = "sign-in" | "sign-up" | "select-app" | "success-new" | "success-existing"; function render(screen: Screen) { switch (screen) { @@ -135,9 +130,7 @@ function renderSelectApp() { document.getElementById("app-list")!.addEventListener("click", (e) => { const li = (e.target as HTMLElement).closest("li"); if (!li) return; - document - .querySelectorAll(".app-list li") - .forEach((el) => el.classList.remove("selected")); + document.querySelectorAll(".app-list li").forEach((el) => el.classList.remove("selected")); li.classList.add("selected"); selected = li.dataset.id!; linkBtn.disabled = false; diff --git a/mocks/src/style.css b/packages/cli-core/mocks/src/style.css similarity index 97% rename from mocks/src/style.css rename to packages/cli-core/mocks/src/style.css index 95eef2241..afa90783e 100644 --- a/mocks/src/style.css +++ b/packages/cli-core/mocks/src/style.css @@ -119,7 +119,9 @@ body { margin-bottom: 8px; cursor: pointer; font-size: 14px; - transition: border-color 0.15s, background 0.15s; + transition: + border-color 0.15s, + background 0.15s; } .app-list li:hover, diff --git a/mocks/tsconfig.json b/packages/cli-core/mocks/tsconfig.json similarity index 100% rename from mocks/tsconfig.json rename to packages/cli-core/mocks/tsconfig.json diff --git a/packages/cli-core/package.json b/packages/cli-core/package.json new file mode 100644 index 000000000..6658fd321 --- /dev/null +++ b/packages/cli-core/package.json @@ -0,0 +1,23 @@ +{ + "name": "@clerk/cli-core", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "bun build ./src/cli.ts --outfile ./dist/cli.js --target node --external @napi-rs/keyring", + "build:compile": "bun build --compile --no-compile-autoload-dotenv ./src/cli.ts --outfile ./dist/clerk", + "dev": "bun run ./src/cli.ts", + "test": "bun test", + "lint": "oxlint src/", + "format": "oxfmt --write src/", + "format:check": "oxfmt --check src/" + }, + "dependencies": { + "@commander-js/extra-typings": "^14.0.0", + "@inquirer/prompts": "^8.2.0", + "@napi-rs/keyring": "^1.2.0", + "commander": "^14.0.3", + "env-paths": "^4.0.0", + "yaml": "^2.8.2" + } +} diff --git a/src/cli-program.ts b/packages/cli-core/src/cli-program.ts similarity index 95% rename from src/cli-program.ts rename to packages/cli-core/src/cli-program.ts index 79570f263..f6b77f893 100644 --- a/src/cli-program.ts +++ b/packages/cli-core/src/cli-program.ts @@ -16,11 +16,20 @@ import { doctor } from "./commands/doctor/index.js"; import { CliError, UserAbortError, ApiError, EXIT_CODE, throwUsageError } from "./lib/errors.js"; import { red } from "./lib/color.js"; -export function createProgram() { +async function getDevVersion(): Promise { + try { + const pkg = await Bun.file(`${import.meta.dir}/../../cli/package.json`).json(); + return `${pkg.version}-dev`; + } catch { + return "0.0.0-dev"; + } +} + +export async function createProgram() { const program = new Command() .name("clerk") .description("Clerk CLI") - .version(require("../package.json").version) + .version(typeof CLI_VERSION !== "undefined" ? CLI_VERSION : await getDevVersion()) .option( "--mode ", "Force interaction mode (human or agent). Defaults to auto-detect based on TTY.", diff --git a/src/cli.ts b/packages/cli-core/src/cli.ts similarity index 84% rename from src/cli.ts rename to packages/cli-core/src/cli.ts index 5c730fe28..525cc2a59 100644 --- a/src/cli.ts +++ b/packages/cli-core/src/cli.ts @@ -2,4 +2,4 @@ import { createProgram, runProgram } from "./cli-program.js"; import { EXIT_CODE } from "./lib/errors.js"; process.on("SIGINT", () => process.exit(EXIT_CODE.SIGINT)); -runProgram(createProgram()); +runProgram(await createProgram()); diff --git a/src/commands/api/README.md b/packages/cli-core/src/commands/api/README.md similarity index 100% rename from src/commands/api/README.md rename to packages/cli-core/src/commands/api/README.md diff --git a/src/commands/api/bapi.test.ts b/packages/cli-core/src/commands/api/bapi.test.ts similarity index 100% rename from src/commands/api/bapi.test.ts rename to packages/cli-core/src/commands/api/bapi.test.ts diff --git a/src/commands/api/bapi.ts b/packages/cli-core/src/commands/api/bapi.ts similarity index 100% rename from src/commands/api/bapi.ts rename to packages/cli-core/src/commands/api/bapi.ts diff --git a/src/commands/api/catalog.test.ts b/packages/cli-core/src/commands/api/catalog.test.ts similarity index 100% rename from src/commands/api/catalog.test.ts rename to packages/cli-core/src/commands/api/catalog.test.ts diff --git a/src/commands/api/catalog.ts b/packages/cli-core/src/commands/api/catalog.ts similarity index 100% rename from src/commands/api/catalog.ts rename to packages/cli-core/src/commands/api/catalog.ts diff --git a/src/commands/api/index.test.ts b/packages/cli-core/src/commands/api/index.test.ts similarity index 100% rename from src/commands/api/index.test.ts rename to packages/cli-core/src/commands/api/index.test.ts diff --git a/src/commands/api/index.ts b/packages/cli-core/src/commands/api/index.ts similarity index 100% rename from src/commands/api/index.ts rename to packages/cli-core/src/commands/api/index.ts diff --git a/src/commands/api/interactive.test.ts b/packages/cli-core/src/commands/api/interactive.test.ts similarity index 100% rename from src/commands/api/interactive.test.ts rename to packages/cli-core/src/commands/api/interactive.test.ts diff --git a/src/commands/api/interactive.ts b/packages/cli-core/src/commands/api/interactive.ts similarity index 100% rename from src/commands/api/interactive.ts rename to packages/cli-core/src/commands/api/interactive.ts diff --git a/src/commands/api/ls.test.ts b/packages/cli-core/src/commands/api/ls.test.ts similarity index 100% rename from src/commands/api/ls.test.ts rename to packages/cli-core/src/commands/api/ls.test.ts diff --git a/src/commands/api/ls.ts b/packages/cli-core/src/commands/api/ls.ts similarity index 100% rename from src/commands/api/ls.ts rename to packages/cli-core/src/commands/api/ls.ts diff --git a/src/commands/auth/README.md b/packages/cli-core/src/commands/auth/README.md similarity index 100% rename from src/commands/auth/README.md rename to packages/cli-core/src/commands/auth/README.md diff --git a/src/commands/auth/login.test.ts b/packages/cli-core/src/commands/auth/login.test.ts similarity index 100% rename from src/commands/auth/login.test.ts rename to packages/cli-core/src/commands/auth/login.test.ts diff --git a/src/commands/auth/login.ts b/packages/cli-core/src/commands/auth/login.ts similarity index 100% rename from src/commands/auth/login.ts rename to packages/cli-core/src/commands/auth/login.ts diff --git a/src/commands/auth/logout.test.ts b/packages/cli-core/src/commands/auth/logout.test.ts similarity index 100% rename from src/commands/auth/logout.test.ts rename to packages/cli-core/src/commands/auth/logout.test.ts diff --git a/src/commands/auth/logout.ts b/packages/cli-core/src/commands/auth/logout.ts similarity index 100% rename from src/commands/auth/logout.ts rename to packages/cli-core/src/commands/auth/logout.ts diff --git a/src/commands/config/README.md b/packages/cli-core/src/commands/config/README.md similarity index 100% rename from src/commands/config/README.md rename to packages/cli-core/src/commands/config/README.md diff --git a/src/commands/config/pull.test.ts b/packages/cli-core/src/commands/config/pull.test.ts similarity index 100% rename from src/commands/config/pull.test.ts rename to packages/cli-core/src/commands/config/pull.test.ts diff --git a/src/commands/config/pull.ts b/packages/cli-core/src/commands/config/pull.ts similarity index 100% rename from src/commands/config/pull.ts rename to packages/cli-core/src/commands/config/pull.ts diff --git a/src/commands/config/push.test.ts b/packages/cli-core/src/commands/config/push.test.ts similarity index 100% rename from src/commands/config/push.test.ts rename to packages/cli-core/src/commands/config/push.test.ts diff --git a/src/commands/config/push.ts b/packages/cli-core/src/commands/config/push.ts similarity index 100% rename from src/commands/config/push.ts rename to packages/cli-core/src/commands/config/push.ts diff --git a/src/commands/config/schema.test.ts b/packages/cli-core/src/commands/config/schema.test.ts similarity index 100% rename from src/commands/config/schema.test.ts rename to packages/cli-core/src/commands/config/schema.test.ts diff --git a/src/commands/config/schema.ts b/packages/cli-core/src/commands/config/schema.ts similarity index 100% rename from src/commands/config/schema.ts rename to packages/cli-core/src/commands/config/schema.ts diff --git a/src/commands/deploy/README.md b/packages/cli-core/src/commands/deploy/README.md similarity index 100% rename from src/commands/deploy/README.md rename to packages/cli-core/src/commands/deploy/README.md diff --git a/src/commands/deploy/index.test.ts b/packages/cli-core/src/commands/deploy/index.test.ts similarity index 100% rename from src/commands/deploy/index.test.ts rename to packages/cli-core/src/commands/deploy/index.test.ts diff --git a/src/commands/deploy/index.ts b/packages/cli-core/src/commands/deploy/index.ts similarity index 100% rename from src/commands/deploy/index.ts rename to packages/cli-core/src/commands/deploy/index.ts diff --git a/src/commands/doctor/README.md b/packages/cli-core/src/commands/doctor/README.md similarity index 100% rename from src/commands/doctor/README.md rename to packages/cli-core/src/commands/doctor/README.md diff --git a/src/commands/doctor/checks.ts b/packages/cli-core/src/commands/doctor/checks.ts similarity index 100% rename from src/commands/doctor/checks.ts rename to packages/cli-core/src/commands/doctor/checks.ts diff --git a/src/commands/doctor/context.test.ts b/packages/cli-core/src/commands/doctor/context.test.ts similarity index 100% rename from src/commands/doctor/context.test.ts rename to packages/cli-core/src/commands/doctor/context.test.ts diff --git a/src/commands/doctor/context.ts b/packages/cli-core/src/commands/doctor/context.ts similarity index 100% rename from src/commands/doctor/context.ts rename to packages/cli-core/src/commands/doctor/context.ts diff --git a/src/commands/doctor/doctor.test.ts b/packages/cli-core/src/commands/doctor/doctor.test.ts similarity index 100% rename from src/commands/doctor/doctor.test.ts rename to packages/cli-core/src/commands/doctor/doctor.test.ts diff --git a/src/commands/doctor/format.ts b/packages/cli-core/src/commands/doctor/format.ts similarity index 100% rename from src/commands/doctor/format.ts rename to packages/cli-core/src/commands/doctor/format.ts diff --git a/src/commands/doctor/index.ts b/packages/cli-core/src/commands/doctor/index.ts similarity index 100% rename from src/commands/doctor/index.ts rename to packages/cli-core/src/commands/doctor/index.ts diff --git a/src/commands/doctor/types.ts b/packages/cli-core/src/commands/doctor/types.ts similarity index 100% rename from src/commands/doctor/types.ts rename to packages/cli-core/src/commands/doctor/types.ts diff --git a/src/commands/env/README.md b/packages/cli-core/src/commands/env/README.md similarity index 100% rename from src/commands/env/README.md rename to packages/cli-core/src/commands/env/README.md diff --git a/src/commands/env/pull.test.ts b/packages/cli-core/src/commands/env/pull.test.ts similarity index 100% rename from src/commands/env/pull.test.ts rename to packages/cli-core/src/commands/env/pull.test.ts diff --git a/src/commands/env/pull.ts b/packages/cli-core/src/commands/env/pull.ts similarity index 100% rename from src/commands/env/pull.ts rename to packages/cli-core/src/commands/env/pull.ts diff --git a/src/commands/init/README.md b/packages/cli-core/src/commands/init/README.md similarity index 100% rename from src/commands/init/README.md rename to packages/cli-core/src/commands/init/README.md diff --git a/src/commands/init/index.ts b/packages/cli-core/src/commands/init/index.ts similarity index 100% rename from src/commands/init/index.ts rename to packages/cli-core/src/commands/init/index.ts diff --git a/src/commands/link/README.md b/packages/cli-core/src/commands/link/README.md similarity index 100% rename from src/commands/link/README.md rename to packages/cli-core/src/commands/link/README.md diff --git a/src/commands/link/index.test.ts b/packages/cli-core/src/commands/link/index.test.ts similarity index 100% rename from src/commands/link/index.test.ts rename to packages/cli-core/src/commands/link/index.test.ts diff --git a/src/commands/link/index.ts b/packages/cli-core/src/commands/link/index.ts similarity index 100% rename from src/commands/link/index.ts rename to packages/cli-core/src/commands/link/index.ts diff --git a/src/commands/unlink/README.md b/packages/cli-core/src/commands/unlink/README.md similarity index 100% rename from src/commands/unlink/README.md rename to packages/cli-core/src/commands/unlink/README.md diff --git a/src/commands/unlink/index.test.ts b/packages/cli-core/src/commands/unlink/index.test.ts similarity index 100% rename from src/commands/unlink/index.test.ts rename to packages/cli-core/src/commands/unlink/index.test.ts diff --git a/src/commands/unlink/index.ts b/packages/cli-core/src/commands/unlink/index.ts similarity index 100% rename from src/commands/unlink/index.ts rename to packages/cli-core/src/commands/unlink/index.ts diff --git a/src/commands/whoami/README.md b/packages/cli-core/src/commands/whoami/README.md similarity index 100% rename from src/commands/whoami/README.md rename to packages/cli-core/src/commands/whoami/README.md diff --git a/src/commands/whoami/index.test.ts b/packages/cli-core/src/commands/whoami/index.test.ts similarity index 100% rename from src/commands/whoami/index.test.ts rename to packages/cli-core/src/commands/whoami/index.test.ts diff --git a/src/commands/whoami/index.ts b/packages/cli-core/src/commands/whoami/index.ts similarity index 100% rename from src/commands/whoami/index.ts rename to packages/cli-core/src/commands/whoami/index.ts diff --git a/packages/cli-core/src/globals.d.ts b/packages/cli-core/src/globals.d.ts new file mode 100644 index 000000000..f6a3a72c5 --- /dev/null +++ b/packages/cli-core/src/globals.d.ts @@ -0,0 +1 @@ +declare const CLI_VERSION: string | undefined; diff --git a/src/lib/auth-server.test.ts b/packages/cli-core/src/lib/auth-server.test.ts similarity index 100% rename from src/lib/auth-server.test.ts rename to packages/cli-core/src/lib/auth-server.test.ts diff --git a/src/lib/auth-server.ts b/packages/cli-core/src/lib/auth-server.ts similarity index 100% rename from src/lib/auth-server.ts rename to packages/cli-core/src/lib/auth-server.ts diff --git a/src/lib/autolink.test.ts b/packages/cli-core/src/lib/autolink.test.ts similarity index 100% rename from src/lib/autolink.test.ts rename to packages/cli-core/src/lib/autolink.test.ts diff --git a/src/lib/autolink.ts b/packages/cli-core/src/lib/autolink.ts similarity index 100% rename from src/lib/autolink.ts rename to packages/cli-core/src/lib/autolink.ts diff --git a/src/lib/color.ts b/packages/cli-core/src/lib/color.ts similarity index 100% rename from src/lib/color.ts rename to packages/cli-core/src/lib/color.ts diff --git a/src/lib/config.test.ts b/packages/cli-core/src/lib/config.test.ts similarity index 100% rename from src/lib/config.test.ts rename to packages/cli-core/src/lib/config.test.ts diff --git a/src/lib/config.ts b/packages/cli-core/src/lib/config.ts similarity index 100% rename from src/lib/config.ts rename to packages/cli-core/src/lib/config.ts diff --git a/src/lib/constants.ts b/packages/cli-core/src/lib/constants.ts similarity index 100% rename from src/lib/constants.ts rename to packages/cli-core/src/lib/constants.ts diff --git a/src/lib/credential-store.test.ts b/packages/cli-core/src/lib/credential-store.test.ts similarity index 100% rename from src/lib/credential-store.test.ts rename to packages/cli-core/src/lib/credential-store.test.ts diff --git a/src/lib/credential-store.ts b/packages/cli-core/src/lib/credential-store.ts similarity index 100% rename from src/lib/credential-store.ts rename to packages/cli-core/src/lib/credential-store.ts diff --git a/src/lib/dotenv.test.ts b/packages/cli-core/src/lib/dotenv.test.ts similarity index 100% rename from src/lib/dotenv.test.ts rename to packages/cli-core/src/lib/dotenv.test.ts diff --git a/src/lib/dotenv.ts b/packages/cli-core/src/lib/dotenv.ts similarity index 100% rename from src/lib/dotenv.ts rename to packages/cli-core/src/lib/dotenv.ts diff --git a/src/lib/errors.ts b/packages/cli-core/src/lib/errors.ts similarity index 100% rename from src/lib/errors.ts rename to packages/cli-core/src/lib/errors.ts diff --git a/src/lib/framework.test.ts b/packages/cli-core/src/lib/framework.test.ts similarity index 100% rename from src/lib/framework.test.ts rename to packages/cli-core/src/lib/framework.test.ts diff --git a/src/lib/framework.ts b/packages/cli-core/src/lib/framework.ts similarity index 100% rename from src/lib/framework.ts rename to packages/cli-core/src/lib/framework.ts diff --git a/src/lib/git.test.ts b/packages/cli-core/src/lib/git.test.ts similarity index 100% rename from src/lib/git.test.ts rename to packages/cli-core/src/lib/git.test.ts diff --git a/src/lib/git.ts b/packages/cli-core/src/lib/git.ts similarity index 100% rename from src/lib/git.ts rename to packages/cli-core/src/lib/git.ts diff --git a/src/lib/pkce.test.ts b/packages/cli-core/src/lib/pkce.test.ts similarity index 100% rename from src/lib/pkce.test.ts rename to packages/cli-core/src/lib/pkce.test.ts diff --git a/src/lib/pkce.ts b/packages/cli-core/src/lib/pkce.ts similarity index 100% rename from src/lib/pkce.ts rename to packages/cli-core/src/lib/pkce.ts diff --git a/src/lib/plapi.test.ts b/packages/cli-core/src/lib/plapi.test.ts similarity index 100% rename from src/lib/plapi.test.ts rename to packages/cli-core/src/lib/plapi.test.ts diff --git a/src/lib/plapi.ts b/packages/cli-core/src/lib/plapi.ts similarity index 100% rename from src/lib/plapi.ts rename to packages/cli-core/src/lib/plapi.ts diff --git a/src/lib/token-exchange.test.ts b/packages/cli-core/src/lib/token-exchange.test.ts similarity index 100% rename from src/lib/token-exchange.test.ts rename to packages/cli-core/src/lib/token-exchange.test.ts diff --git a/src/lib/token-exchange.ts b/packages/cli-core/src/lib/token-exchange.ts similarity index 100% rename from src/lib/token-exchange.ts rename to packages/cli-core/src/lib/token-exchange.ts diff --git a/src/mode.test.ts b/packages/cli-core/src/mode.test.ts similarity index 100% rename from src/mode.test.ts rename to packages/cli-core/src/mode.test.ts diff --git a/src/mode.ts b/packages/cli-core/src/mode.ts similarity index 100% rename from src/mode.ts rename to packages/cli-core/src/mode.ts diff --git a/src/test/integration/agent-mode.test.ts b/packages/cli-core/src/test/integration/agent-mode.test.ts similarity index 100% rename from src/test/integration/agent-mode.test.ts rename to packages/cli-core/src/test/integration/agent-mode.test.ts diff --git a/src/test/integration/api-queries.test.ts b/packages/cli-core/src/test/integration/api-queries.test.ts similarity index 100% rename from src/test/integration/api-queries.test.ts rename to packages/cli-core/src/test/integration/api-queries.test.ts diff --git a/src/test/integration/auth-lifecycle.test.ts b/packages/cli-core/src/test/integration/auth-lifecycle.test.ts similarity index 100% rename from src/test/integration/auth-lifecycle.test.ts rename to packages/cli-core/src/test/integration/auth-lifecycle.test.ts diff --git a/src/test/integration/config-management.test.ts b/packages/cli-core/src/test/integration/config-management.test.ts similarity index 100% rename from src/test/integration/config-management.test.ts rename to packages/cli-core/src/test/integration/config-management.test.ts diff --git a/src/test/integration/config-put.test.ts b/packages/cli-core/src/test/integration/config-put.test.ts similarity index 100% rename from src/test/integration/config-put.test.ts rename to packages/cli-core/src/test/integration/config-put.test.ts diff --git a/src/test/integration/deploy-to-prod.test.ts b/packages/cli-core/src/test/integration/deploy-to-prod.test.ts similarity index 100% rename from src/test/integration/deploy-to-prod.test.ts rename to packages/cli-core/src/test/integration/deploy-to-prod.test.ts diff --git a/src/test/integration/dry-run.test.ts b/packages/cli-core/src/test/integration/dry-run.test.ts similarity index 100% rename from src/test/integration/dry-run.test.ts rename to packages/cli-core/src/test/integration/dry-run.test.ts diff --git a/src/test/integration/env-merge.test.ts b/packages/cli-core/src/test/integration/env-merge.test.ts similarity index 100% rename from src/test/integration/env-merge.test.ts rename to packages/cli-core/src/test/integration/env-merge.test.ts diff --git a/src/test/integration/error-recovery.test.ts b/packages/cli-core/src/test/integration/error-recovery.test.ts similarity index 100% rename from src/test/integration/error-recovery.test.ts rename to packages/cli-core/src/test/integration/error-recovery.test.ts diff --git a/src/test/integration/onboard.test.ts b/packages/cli-core/src/test/integration/onboard.test.ts similarity index 100% rename from src/test/integration/onboard.test.ts rename to packages/cli-core/src/test/integration/onboard.test.ts diff --git a/src/test/integration/switch-apps.test.ts b/packages/cli-core/src/test/integration/switch-apps.test.ts similarity index 100% rename from src/test/integration/switch-apps.test.ts rename to packages/cli-core/src/test/integration/switch-apps.test.ts diff --git a/src/test/lib/http.ts b/packages/cli-core/src/test/lib/http.ts similarity index 100% rename from src/test/lib/http.ts rename to packages/cli-core/src/test/lib/http.ts diff --git a/src/test/lib/setup.ts b/packages/cli-core/src/test/lib/setup.ts similarity index 99% rename from src/test/lib/setup.ts rename to packages/cli-core/src/test/lib/setup.ts index 43726c452..3d37ac229 100644 --- a/src/test/lib/setup.ts +++ b/packages/cli-core/src/test/lib/setup.ts @@ -379,7 +379,7 @@ export interface CLIResult { async function execCLI(...args: string[]): Promise { const { createProgram, runProgram } = await import("../../cli-program.ts"); - const program = createProgram(); + const program = await createProgram(); program.exitOverride(); if (!currentHarness) { diff --git a/src/test/stubs.ts b/packages/cli-core/src/test/stubs.ts similarity index 100% rename from src/test/stubs.ts rename to packages/cli-core/src/test/stubs.ts diff --git a/tsconfig.json b/packages/cli-core/tsconfig.json similarity index 100% rename from tsconfig.json rename to packages/cli-core/tsconfig.json diff --git a/packages/cli/bin/clerk b/packages/cli/bin/clerk new file mode 100755 index 000000000..852b0395f --- /dev/null +++ b/packages/cli/bin/clerk @@ -0,0 +1,63 @@ +#!/usr/bin/env node + +"use strict"; + +const { spawn } = require("node:child_process"); +const { platform, arch, argv, exit } = require("node:process"); + +function isMusl() { + if (platform !== "linux") return false; + try { + return require("fs").readFileSync("/usr/bin/ldd", "utf-8").includes("musl"); + } catch {} + try { + const report = process.report.getReport(); + if (report.header && report.header.glibcVersionRuntime) return false; + if (Array.isArray(report.sharedObjects)) { + return report.sharedObjects.some((f) => f.includes("libc.musl-") || f.includes("ld-musl-")); + } + } catch {} + return false; +} + +const suffix = isMusl() ? "-musl" : ""; +const key = `${platform}-${arch}${suffix}`; +const pkg = `@clerk/cli-${key}`; +const ext = platform === "win32" ? ".exe" : ""; + +let binPath; +try { + binPath = require.resolve(`${pkg}/bin/clerk${ext}`); +} catch { + console.error( + `Unsupported or missing platform: ${key}\n` + + `Expected package: ${pkg}\n` + + `Try reinstalling with: npm install -g @clerk/cli`, + ); + exit(1); +} + +const child = spawn(binPath, argv.slice(2), { stdio: "inherit" }); + +let exited = false; + +child.on("error", (err) => { + if (exited) return; + exited = true; + console.error(`Failed to start Clerk CLI: ${err.message}`); + exit(1); +}); + +const signals = platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"]; +for (const sig of signals) { + process.on(sig, () => child.kill(sig)); +} + +child.on("close", (code, signal) => { + if (exited) return; + exited = true; + if (code !== null) exit(code); + // 128 + signal number is the conventional exit code for signal termination + const signum = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 }; + exit(128 + (signum[signal] ?? 1)); +}); diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 000000000..00e01a49b --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,32 @@ +{ + "name": "@clerk/cli", + "version": "0.0.1", + "private": true, + "description": "Clerk CLI", + "keywords": [ + "authentication", + "clerk", + "cli" + ], + "homepage": "https://clerk.com/docs", + "bugs": { + "url": "https://github.com/clerk/cli/issues" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/clerk/cli.git" + }, + "bin": { + "clerk": "./bin/clerk" + }, + "files": [ + "bin" + ], + "scripts": { + "prepublishOnly": "echo 'Use scripts/releaser instead' && exit 1" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 000000000..608891d33 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "node", + "packages": { + "packages/cli": {} + } +} diff --git a/scripts/releaser/index.ts b/scripts/releaser/index.ts new file mode 100644 index 000000000..6366f233a --- /dev/null +++ b/scripts/releaser/index.ts @@ -0,0 +1,117 @@ +import { mkdir, cp, rm, chmod } from "node:fs/promises"; +import { join } from "node:path"; +import { type Target, targets, SCOPE, PKG_PREFIX } from "./targets.ts"; + +const DIST_DIR = join(import.meta.dir, "../../dist/platform-packages"); +const ARTIFACTS_DIR = process.env.ARTIFACTS_DIR ?? join(import.meta.dir, "../../dist/artifacts"); +const WRAPPER_PKG_PATH = join(import.meta.dir, "../../packages/cli/package.json"); + +async function readVersion(): Promise { + const pkg = await Bun.file(WRAPPER_PKG_PATH).json(); + return pkg.version; +} + +function packageName(targetName: string): string { + return `${SCOPE}/${PKG_PREFIX}-${targetName}`; +} + +function isPublished(name: string, version: string): boolean { + const result = Bun.spawnSync(["npm", "view", `${name}@${version}`, "version"], { + stdio: ["ignore", "pipe", "pipe"], + }); + + if (result.exitCode === 0) return true; + + // "npm view" exits non-zero for both "not found" and real errors (network, auth). + // Treat only E404 / "not found" as unpublished; propagate everything else. + const stderr = result.stderr.toString(); + if (stderr.includes("E404") || stderr.includes("is not in this registry")) { + return false; + } + + throw new Error(`npm view ${name}@${version} failed (exit ${result.exitCode}): ${stderr.trim()}`); +} + +async function generatePlatformPackage(target: Target, version: string): Promise { + const dir = join(DIST_DIR, target.name); + const binDir = join(dir, "bin"); + + await mkdir(binDir, { recursive: true }); + + const ext = target.os === "win32" ? ".exe" : ""; + const binaryName = `clerk${ext}`; + const artifactPath = join(ARTIFACTS_DIR, `clerk-${target.name}`, binaryName); + const destPath = join(binDir, binaryName); + await cp(artifactPath, destPath); + await chmod(destPath, 0o755); + + const pkg: Record = { + name: packageName(target.name), + version, + description: `Clerk CLI binary for ${target.name}`, + license: "MIT", + repository: { type: "git", url: "https://github.com/clerk/cli.git" }, + homepage: "https://clerk.com/docs", + os: [target.os], + cpu: [target.cpu], + preferUnplugged: true, + }; + if (target.libc) { + pkg.libc = [target.libc]; + } + await Bun.write(join(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n"); + + return dir; +} + +function publish(dir: string, dryRun: boolean): void { + const flags = ["npm", "publish", "--provenance", "--access", "public", "--ignore-scripts"]; + if (dryRun) flags.push("--dry-run"); + const result = Bun.spawnSync(flags, { cwd: dir, stdio: ["ignore", "pipe", "pipe"] }); + if (result.exitCode !== 0) { + const stderr = result.stderr.toString().trim(); + throw new Error(`npm publish failed (exit ${result.exitCode})${stderr ? `: ${stderr}` : ""}`); + } +} + +const dryRun = process.argv.includes("--dry-run"); +const version = await readVersion(); +console.log(`Publishing version ${version}${dryRun ? " (dry run)" : ""}`); + +await rm(DIST_DIR, { recursive: true, force: true }); + +for (const target of targets) { + const name = packageName(target.name); + if (isPublished(name, version)) { + console.log(`Skipping ${name}@${version} (already published)`); + continue; + } + console.log(`Publishing ${name}@${version}...`); + const dir = await generatePlatformPackage(target, version); + publish(dir, dryRun); +} + +// Build wrapper package.json for publishing: add optionalDependencies from targets and remove private flag. +// This mutation is intentional — the repo omits optionalDependencies while the published package includes them. +// We restore the original file after publishing (or on failure) so the working tree stays clean. +const wrapperRaw = await Bun.file(WRAPPER_PKG_PATH).text(); +try { + const wrapperPkg = JSON.parse(wrapperRaw); + wrapperPkg.optionalDependencies = Object.fromEntries( + targets.map((t) => [packageName(t.name), version]), + ); + delete wrapperPkg.private; + await Bun.write(WRAPPER_PKG_PATH, JSON.stringify(wrapperPkg, null, 2) + "\n"); + + const wrapperName = `${SCOPE}/${PKG_PREFIX}`; + if (isPublished(wrapperName, version)) { + console.log(`Skipping ${wrapperName}@${version} (already published)`); + } else { + console.log(`Publishing ${wrapperName}@${version}...`); + publish(join(import.meta.dir, "../../packages/cli"), dryRun); + } +} finally { + await Bun.write(WRAPPER_PKG_PATH, wrapperRaw); +} + +console.log("Done!"); diff --git a/scripts/releaser/targets.ts b/scripts/releaser/targets.ts new file mode 100644 index 000000000..bdf6c30ef --- /dev/null +++ b/scripts/releaser/targets.ts @@ -0,0 +1,23 @@ +export interface Target { + name: string; + os: string; + cpu: string; + libc?: string; +} + +// Target names use Node.js ${process.platform}-${process.arch} convention so the wrapper +// shim (packages/cli/bin/clerk) can derive package names without a lookup table. +// Keep in sync with .github/workflows/release.yml matrix. +export const targets: Target[] = [ + { name: "darwin-arm64", os: "darwin", cpu: "arm64" }, + { name: "darwin-x64", os: "darwin", cpu: "x64" }, + { name: "linux-arm64", os: "linux", cpu: "arm64", libc: "glibc" }, + { name: "linux-arm64-musl", os: "linux", cpu: "arm64", libc: "musl" }, + { name: "linux-x64", os: "linux", cpu: "x64", libc: "glibc" }, + { name: "linux-x64-musl", os: "linux", cpu: "x64", libc: "musl" }, + { name: "win32-arm64", os: "win32", cpu: "arm64" }, + { name: "win32-x64", os: "win32", cpu: "x64" }, +]; + +export const SCOPE = "@clerk"; +export const PKG_PREFIX = "cli"; From 458ad767440a10d45e13b9897419a55803b8fd4c Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 11:11:11 -0600 Subject: [PATCH 02/44] feat: add canary and snapshot release channels Add canary releases on every push to main (when no stable release is created) and snapshot releases triggered by `!snapshot` PR comments. The releaser script now accepts --tag and --version flags to support publishing pre-release versions to npm. Also fixes pre-existing lint failures: oxlintrc override globs and dead variable declarations in link tests. --- .github/workflows/release.yml | 104 +++++++++++ .github/workflows/snapshot.yml | 172 ++++++++++++++++++ .oxlintrc.json | 2 +- docs/releasing.md | 48 ++++- package.json | 6 +- .../cli-core/src/commands/link/index.test.ts | 2 - scripts/releaser/index.ts | 29 ++- 7 files changed, 345 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/snapshot.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e8f4b779..ff59ecd49 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,6 +20,8 @@ jobs: - id: release uses: googleapis/release-please-action@v4 + # ─── Stable release ──────────────────────────────────────────────── + build: needs: release-please if: ${{ needs.release-please.outputs.release_created == 'true' }} @@ -181,3 +183,105 @@ jobs: ext=""; [[ "$target" == win32-* ]] && ext=".exe" gh release upload "$tag" "${dir}clerk${ext}#clerk-${target}${ext}" done + + # ─── Canary release ──────────────────────────────────────────────── + + canary-version: + needs: release-please + if: ${{ needs.release-please.outputs.release_created != 'true' }} + runs-on: blacksmith-2vcpu-ubuntu-2404 + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - uses: actions/checkout@v4 + - name: Compute canary version + id: version + run: | + base=$(jq -r '.["packages/cli"]' .release-please-manifest.json) + sha=$(git rev-parse --short=7 HEAD) + echo "version=${base}-canary.${sha}" >> "$GITHUB_OUTPUT" + + canary-build: + needs: canary-version + runs-on: blacksmith-2vcpu-ubuntu-2404 + strategy: + fail-fast: false + matrix: + # Keep in sync with scripts/releaser/targets.ts + include: + - target: darwin-arm64 + bun_target: bun-darwin-arm64 + ext: "" + - target: darwin-x64 + bun_target: bun-darwin-x64 + ext: "" + - target: linux-arm64 + bun_target: bun-linux-arm64 + ext: "" + - target: linux-arm64-musl + bun_target: bun-linux-arm64-musl + ext: "" + - target: linux-x64 + bun_target: bun-linux-x64 + ext: "" + - target: linux-x64-musl + bun_target: bun-linux-x64-musl + ext: "" + - target: win32-arm64 + bun_target: bun-windows-arm64 + ext: ".exe" + - target: win32-x64 + bun_target: bun-windows-x64 + ext: ".exe" + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + + - name: Build compiled binary + env: + CLI_VERSION: ${{ needs.canary-version.outputs.version }} + run: | + bun build --compile \ + --no-compile-autoload-dotenv \ + --target=${{ matrix.bun_target }} \ + --define "CLI_VERSION=\"$CLI_VERSION\"" \ + ./packages/cli-core/src/cli.ts \ + --outfile clerk${{ matrix.ext }} + + - uses: actions/upload-artifact@v4 + with: + name: clerk-canary-${{ matrix.target }} + path: clerk${{ matrix.ext }} + + canary-publish: + needs: [canary-version, canary-build] + runs-on: blacksmith-2vcpu-ubuntu-2404 + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - uses: actions/setup-node@v4 + with: + node-version: "22" + registry-url: "https://registry.npmjs.org" + - run: bun install --frozen-lockfile + + - uses: actions/download-artifact@v4 + with: + pattern: clerk-canary-* + path: dist/artifacts + + - name: Rename artifact directories + run: | + cd dist/artifacts + for dir in clerk-canary-*/; do + target=${dir#clerk-canary-} && target=${target%/} + mv "$dir" "clerk-${target}" + done + + - name: Publish canary packages + run: bun run scripts/releaser/index.ts --tag canary --version "$CANARY_VERSION" + env: + CANARY_VERSION: ${{ needs.canary-version.outputs.version }} + ARTIFACTS_DIR: ${{ github.workspace }}/dist/artifacts + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml new file mode 100644 index 000000000..739172c94 --- /dev/null +++ b/.github/workflows/snapshot.yml @@ -0,0 +1,172 @@ +name: Snapshot Release + +on: + issue_comment: + types: [created] + +permissions: + contents: read + pull-requests: write + id-token: write + +jobs: + snapshot: + if: >- + github.event.issue.pull_request && + startsWith(github.event.comment.body, '!snapshot') && + contains(fromJSON('["MEMBER","OWNER"]'), github.event.comment.author_association) + runs-on: blacksmith-2vcpu-ubuntu-2404 + outputs: + version: ${{ steps.version.outputs.version }} + sha: ${{ steps.pr.outputs.sha }} + steps: + - name: React to comment + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + gh api "repos/${GH_REPO}/issues/comments/${{ github.event.comment.id }}/reactions" \ + -f content=eyes --silent + + - name: Get PR head SHA + id: pr + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + sha=$(gh api "repos/${GH_REPO}/pulls/${{ github.event.issue.number }}" --jq '.head.sha') + echo "sha=${sha}" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v4 + with: + ref: ${{ steps.pr.outputs.sha }} + + - name: Compute snapshot version + id: version + run: | + body="${{ github.event.comment.body }}" + # Extract optional name from "!snapshot " — default to "snapshot" + name=$(echo "$body" | sed 's/^!snapshot[[:space:]]*//' | tr -cd 'a-zA-Z0-9-') + if [ -z "$name" ]; then name="snapshot"; fi + base=$(jq -r '.["packages/cli"]' .release-please-manifest.json) + sha=$(echo "${{ steps.pr.outputs.sha }}" | cut -c1-7) + echo "version=${base}-${name}.${sha}" >> "$GITHUB_OUTPUT" + + build: + needs: snapshot + runs-on: blacksmith-2vcpu-ubuntu-2404 + strategy: + fail-fast: false + matrix: + # Keep in sync with scripts/releaser/targets.ts + include: + - target: darwin-arm64 + bun_target: bun-darwin-arm64 + ext: "" + - target: darwin-x64 + bun_target: bun-darwin-x64 + ext: "" + - target: linux-arm64 + bun_target: bun-linux-arm64 + ext: "" + - target: linux-arm64-musl + bun_target: bun-linux-arm64-musl + ext: "" + - target: linux-x64 + bun_target: bun-linux-x64 + ext: "" + - target: linux-x64-musl + bun_target: bun-linux-x64-musl + ext: "" + - target: win32-arm64 + bun_target: bun-windows-arm64 + ext: ".exe" + - target: win32-x64 + bun_target: bun-windows-x64 + ext: ".exe" + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.snapshot.outputs.sha }} + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + + - name: Build compiled binary + env: + CLI_VERSION: ${{ needs.snapshot.outputs.version }} + run: | + bun build --compile \ + --no-compile-autoload-dotenv \ + --target=${{ matrix.bun_target }} \ + --define "CLI_VERSION=\"$CLI_VERSION\"" \ + ./packages/cli-core/src/cli.ts \ + --outfile clerk${{ matrix.ext }} + + - uses: actions/upload-artifact@v4 + with: + name: clerk-snapshot-${{ matrix.target }} + path: clerk${{ matrix.ext }} + + publish: + needs: [snapshot, build] + runs-on: blacksmith-2vcpu-ubuntu-2404 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.snapshot.outputs.sha }} + - uses: oven-sh/setup-bun@v2 + - uses: actions/setup-node@v4 + with: + node-version: "22" + registry-url: "https://registry.npmjs.org" + - run: bun install --frozen-lockfile + + - uses: actions/download-artifact@v4 + with: + pattern: clerk-snapshot-* + path: dist/artifacts + + - name: Rename artifact directories + run: | + cd dist/artifacts + for dir in clerk-snapshot-*/; do + target=${dir#clerk-snapshot-} && target=${target%/} + mv "$dir" "clerk-${target}" + done + + - name: Publish snapshot packages + run: bun run scripts/releaser/index.ts --tag snapshot --version "$SNAPSHOT_VERSION" + env: + SNAPSHOT_VERSION: ${{ needs.snapshot.outputs.version }} + ARTIFACTS_DIR: ${{ github.workspace }}/dist/artifacts + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: React with rocket + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + gh api "repos/${GH_REPO}/issues/comments/${{ github.event.comment.id }}/reactions" \ + -f content=rocket --silent + + - name: Post installation comment + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + SNAPSHOT_VERSION: ${{ needs.snapshot.outputs.version }} + run: | + sha=$(echo "${SNAPSHOT_VERSION}" | rev | cut -d. -f1 | rev) + gh pr comment "${{ github.event.issue.number }}" --repo "${GH_REPO}" --body "$(cat < Published from ${sha} + EOF + )" diff --git a/.oxlintrc.json b/.oxlintrc.json index a58d866f8..d0553dd4d 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -4,7 +4,7 @@ }, "overrides": [ { - "files": ["src/cli.ts", "src/cli-program.ts"], + "files": ["**/cli.ts", "**/cli-program.ts"], "rules": { "unicorn/no-process-exit": "off" } diff --git a/docs/releasing.md b/docs/releasing.md index 5d6e351fb..737818a47 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -9,10 +9,16 @@ push to main → release-please creates/updates a version PR → merge version PR → release-please creates a GitHub Release + tag - → build job: cross-compile binaries (6 targets) - → smoke-test job: verify binaries on native runners (5 targets) + → build job: cross-compile binaries (8 targets) + → smoke-test job: verify binaries on native runners → publish-npm: generate platform packages + publish wrapper → upload-github-assets: attach binaries to the GitHub Release + → (if no release created) canary job: build + publish @canary + +PR comment "!snapshot [name]" + → build job: cross-compile binaries from PR branch + → publish-npm: publish @snapshot packages + → post installation comment on PR ``` ## Architecture @@ -23,6 +29,26 @@ When a user runs `npm install -g @clerk/cli`, npm installs the wrapper plus the Target names follow Node.js's `${process.platform}-${process.arch}` convention so the shim can derive package names without a lookup table. +## Release Channels + +### Stable (`@latest`) + +Published when a release-please version PR is merged. Includes full smoke testing on native runners before publishing. Binaries are also attached to the GitHub Release. + +Install: `npm install -g @clerk/cli` + +### Canary (`@canary`) + +Published automatically on every push to `main` that does **not** trigger a stable release. Canary versions use the format `x.y.z-canary.` (e.g., `0.0.1-canary.abc1234`). Smoke tests are skipped for faster feedback. + +Install: `npm install -g @clerk/cli@canary` + +### Snapshot (`@snapshot`) + +Published on-demand from PR branches by commenting `!snapshot` (or `!snapshot `) on a pull request. The commenter must be a member or owner of the repository's organization. Snapshot versions use the format `x.y.z-.` (e.g., `0.0.1-snapshot.abc1234` or `0.0.1-my-feature.abc1234`). + +Install: `npm install -g @clerk/cli@` (version is posted as a PR comment after publishing) + ## Versioning Versioning is managed by [release-please](https://github.com/googleapis/release-please). On every push to `main`, it either creates a new version PR or updates an existing one based on conventional commit messages. Merging that PR triggers a GitHub Release. @@ -57,13 +83,19 @@ Publishing and GitHub Release upload are gated on all smoke tests passing. Runs the releaser script (`scripts/releaser/index.ts`): -1. Reads the version from `packages/cli/package.json` +1. Reads the version from `packages/cli/package.json` (or uses `--version` override for canary/snapshot) 2. For each target, generates a platform package in `dist/platform-packages/`: - Creates `package.json` with `os`/`cpu` fields for npm platform selection - Copies the compiled binary from the build artifacts 3. Publishes each platform package with `--provenance --access public` 4. Temporarily mutates the wrapper `package.json` to add `optionalDependencies` and remove `private: true`, publishes it, then restores the original file +The releaser accepts these flags: + +- `--dry-run` — simulate publishing without actually uploading to npm +- `--tag ` — publish with a specific npm dist-tag (e.g., `canary`, `snapshot`); defaults to `latest` +- `--version ` — override the version read from `package.json` + All publishes are idempotent — the script checks `npm view` before publishing and skips already-published versions. ### 4. Upload GitHub Assets Job @@ -80,17 +112,20 @@ Attaches the compiled binaries to the GitHub Release for direct download. Binari | `packages/cli-core/src/globals.d.ts` | TypeScript declaration for the `CLI_VERSION` compile-time define | | `scripts/releaser/index.ts` | Generates platform packages and publishes everything to npm | | `scripts/releaser/targets.ts` | Target definitions — must be kept in sync with the workflow matrix | -| `.github/workflows/release.yml` | GitHub Actions release workflow | +| `.github/workflows/release.yml` | GitHub Actions release + canary workflow | +| `.github/workflows/snapshot.yml` | GitHub Actions snapshot workflow (triggered by PR comments) | ## Keeping Targets in Sync -The target list exists in three places that must stay in sync: +The target list exists in these places that must stay in sync: 1. `scripts/releaser/targets.ts` — used by the releaser to generate platform packages 2. `.github/workflows/release.yml` build matrix — compiles binaries (maps target names to Bun cross-compile targets, e.g., `win32-x64` → `bun-windows-x64`) 3. `.github/workflows/release.yml` smoke-test matrix — verifies binaries on native runners +4. `.github/workflows/release.yml` canary-build matrix — compiles canary binaries +5. `.github/workflows/snapshot.yml` build matrix — compiles snapshot binaries -If you add or remove a target, update all three. Note that the smoke-test matrix may not cover every target if a native runner isn't available (e.g., `win32-arm64`). +If you add or remove a target, update all of these. Note that the smoke-test matrix may not cover every target if a native runner isn't available (e.g., `win32-arm64`). ## Local Development @@ -116,3 +151,4 @@ bun run scripts/releaser/index.ts --dry-run - **Idempotent publishing**: The releaser checks npm before publishing and skips already-published versions, making it safe to re-run. - **Binary format verification**: The build job verifies each compiled binary matches its expected architecture before uploading. - **Native smoke tests**: Each binary is executed on a native runner for its platform before publishing. This catches cross-compilation issues that format checks alone would miss. +- **Org membership check**: Snapshot releases require the commenter to be a `MEMBER` or `OWNER` of the repository's organization, verified via `author_association`. diff --git a/package.json b/package.json index b13216b4b..d4b06e46f 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,9 @@ "build": "bun run --filter @clerk/cli-core build", "dev": "bun run --filter @clerk/cli-core dev", "test": "bun run --filter @clerk/cli-core test", - "lint": "bun run --filter @clerk/cli-core lint", - "format": "bun run --filter @clerk/cli-core format", - "format:check": "bun run --filter @clerk/cli-core format:check", + "lint": "bun run --filter @clerk/cli-core lint && oxlint scripts/", + "format": "bun run --filter @clerk/cli-core format && oxfmt --write scripts/", + "format:check": "bun run --filter @clerk/cli-core format:check && oxfmt --check scripts/", "prepare": "git config core.hooksPath .hooks" }, "devDependencies": { diff --git a/packages/cli-core/src/commands/link/index.test.ts b/packages/cli-core/src/commands/link/index.test.ts index c985e583c..71dcaedf4 100644 --- a/packages/cli-core/src/commands/link/index.test.ts +++ b/packages/cli-core/src/commands/link/index.test.ts @@ -103,7 +103,6 @@ const mockApp = { describe("link", () => { let consoleSpy: ReturnType; - let errorSpy: ReturnType; afterEach(() => { _modeOverride = undefined; @@ -131,7 +130,6 @@ describe("link", () => { mockSearch.mockReset(); mockConfirm.mockReset(); consoleSpy?.mockRestore(); - errorSpy?.mockRestore(); }); describe("agent mode", () => { diff --git a/scripts/releaser/index.ts b/scripts/releaser/index.ts index 6366f233a..ed6099d56 100644 --- a/scripts/releaser/index.ts +++ b/scripts/releaser/index.ts @@ -6,6 +6,19 @@ const DIST_DIR = join(import.meta.dir, "../../dist/platform-packages"); const ARTIFACTS_DIR = process.env.ARTIFACTS_DIR ?? join(import.meta.dir, "../../dist/artifacts"); const WRAPPER_PKG_PATH = join(import.meta.dir, "../../packages/cli/package.json"); +function parseArgs(): { dryRun: boolean; tag?: string; versionOverride?: string } { + const args = process.argv.slice(2); + const dryRun = args.includes("--dry-run"); + + const tagIdx = args.indexOf("--tag"); + const tag = tagIdx !== -1 ? args[tagIdx + 1] : undefined; + + const versionIdx = args.indexOf("--version"); + const versionOverride = versionIdx !== -1 ? args[versionIdx + 1] : undefined; + + return { dryRun, tag, versionOverride }; +} + async function readVersion(): Promise { const pkg = await Bun.file(WRAPPER_PKG_PATH).json(); return pkg.version; @@ -64,8 +77,9 @@ async function generatePlatformPackage(target: Target, version: string): Promise return dir; } -function publish(dir: string, dryRun: boolean): void { +function publish(dir: string, dryRun: boolean, tag?: string): void { const flags = ["npm", "publish", "--provenance", "--access", "public", "--ignore-scripts"]; + if (tag) flags.push("--tag", tag); if (dryRun) flags.push("--dry-run"); const result = Bun.spawnSync(flags, { cwd: dir, stdio: ["ignore", "pipe", "pipe"] }); if (result.exitCode !== 0) { @@ -74,9 +88,11 @@ function publish(dir: string, dryRun: boolean): void { } } -const dryRun = process.argv.includes("--dry-run"); -const version = await readVersion(); -console.log(`Publishing version ${version}${dryRun ? " (dry run)" : ""}`); +const { dryRun, tag, versionOverride } = parseArgs(); +const version = versionOverride ?? (await readVersion()); +console.log( + `Publishing version ${version}${tag ? ` (tag: ${tag})` : ""}${dryRun ? " (dry run)" : ""}`, +); await rm(DIST_DIR, { recursive: true, force: true }); @@ -88,7 +104,7 @@ for (const target of targets) { } console.log(`Publishing ${name}@${version}...`); const dir = await generatePlatformPackage(target, version); - publish(dir, dryRun); + publish(dir, dryRun, tag); } // Build wrapper package.json for publishing: add optionalDependencies from targets and remove private flag. @@ -97,6 +113,7 @@ for (const target of targets) { const wrapperRaw = await Bun.file(WRAPPER_PKG_PATH).text(); try { const wrapperPkg = JSON.parse(wrapperRaw); + wrapperPkg.version = version; wrapperPkg.optionalDependencies = Object.fromEntries( targets.map((t) => [packageName(t.name), version]), ); @@ -108,7 +125,7 @@ try { console.log(`Skipping ${wrapperName}@${version} (already published)`); } else { console.log(`Publishing ${wrapperName}@${version}...`); - publish(join(import.meta.dir, "../../packages/cli"), dryRun); + publish(join(import.meta.dir, "../../packages/cli"), dryRun, tag); } } finally { await Bun.write(WRAPPER_PKG_PATH, wrapperRaw); From f3560b24c486afbb64094ab868e8faa0576c678b Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 11:23:54 -0600 Subject: [PATCH 03/44] fix: add .ts extension to config imports in test files Bun on Linux treats extensionless imports (e.g., "../../lib/config") and explicit imports ("../../lib/config.ts") as separate module instances. This caused _setConfigDir() called in tests to modify a different module instance than the one used by production code, making resolveProfile() always return undefined in CI. --- packages/cli-core/src/commands/config/pull.test.ts | 2 +- packages/cli-core/src/commands/config/push.test.ts | 2 +- packages/cli-core/src/commands/config/schema.test.ts | 2 +- packages/cli-core/src/lib/config.test.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli-core/src/commands/config/pull.test.ts b/packages/cli-core/src/commands/config/pull.test.ts index d90a9f38e..ca4b010f4 100644 --- a/packages/cli-core/src/commands/config/pull.test.ts +++ b/packages/cli-core/src/commands/config/pull.test.ts @@ -2,7 +2,7 @@ import { test, expect, describe, beforeEach, afterEach, spyOn, mock } from "bun: import { mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { _setConfigDir, setProfile } from "../../lib/config"; +import { _setConfigDir, setProfile } from "../../lib/config.ts"; import { credentialStoreStubs, gitStubs, stubFetch } from "../../test/stubs.ts"; mock.module("../../lib/credential-store.ts", () => credentialStoreStubs); diff --git a/packages/cli-core/src/commands/config/push.test.ts b/packages/cli-core/src/commands/config/push.test.ts index 03931c84c..9282f5a15 100644 --- a/packages/cli-core/src/commands/config/push.test.ts +++ b/packages/cli-core/src/commands/config/push.test.ts @@ -2,7 +2,7 @@ import { test, expect, describe, beforeEach, afterEach, spyOn, mock } from "bun: import { mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { _setConfigDir, setProfile } from "../../lib/config"; +import { _setConfigDir, setProfile } from "../../lib/config.ts"; import { credentialStoreStubs, gitStubs, promptsStubs, stubFetch } from "../../test/stubs.ts"; mock.module("../../lib/credential-store.ts", () => credentialStoreStubs); diff --git a/packages/cli-core/src/commands/config/schema.test.ts b/packages/cli-core/src/commands/config/schema.test.ts index 0d4658ee3..8de0aba40 100644 --- a/packages/cli-core/src/commands/config/schema.test.ts +++ b/packages/cli-core/src/commands/config/schema.test.ts @@ -2,7 +2,7 @@ import { test, expect, describe, beforeEach, afterEach, spyOn, mock } from "bun: import { mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { _setConfigDir, setProfile } from "../../lib/config"; +import { _setConfigDir, setProfile } from "../../lib/config.ts"; import { credentialStoreStubs, gitStubs, stubFetch } from "../../test/stubs.ts"; mock.module("../../lib/credential-store.ts", () => credentialStoreStubs); diff --git a/packages/cli-core/src/lib/config.test.ts b/packages/cli-core/src/lib/config.test.ts index 32d6815d0..ae4b77503 100644 --- a/packages/cli-core/src/lib/config.test.ts +++ b/packages/cli-core/src/lib/config.test.ts @@ -12,7 +12,7 @@ import { resolveInstanceId, _setConfigDir, type Profile, -} from "./config"; +} from "./config.ts"; import { join } from "node:path"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; From d4ebb7738fe7943f6feb9d7ef3a0c285936c76f8 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 11:39:20 -0600 Subject: [PATCH 04/44] fix: normalize all relative imports to use .ts extensions Bun on Linux treats different import specifiers for the same file (e.g., "./mode.js", "./mode", "./mode.ts") as separate module instances. This broke mock.module() in tests and _setConfigDir() state sharing, causing 86 test failures in CI. Normalize all relative imports across production and test code to consistently use .ts extensions. --- packages/cli-core/src/cli-program.ts | 32 +++++++++---------- packages/cli-core/src/cli.ts | 4 +-- .../cli-core/src/commands/api/bapi.test.ts | 4 +-- .../cli-core/src/commands/api/catalog.test.ts | 8 ++++- .../cli-core/src/commands/api/index.test.ts | 6 ++-- .../src/commands/api/interactive.test.ts | 12 +++---- packages/cli-core/src/commands/api/ls.test.ts | 4 +-- .../cli-core/src/commands/config/pull.test.ts | 2 +- .../cli-core/src/commands/config/push.test.ts | 4 +-- .../src/commands/config/schema.test.ts | 2 +- .../cli-core/src/commands/deploy/index.ts | 4 +-- .../cli-core/src/commands/env/pull.test.ts | 4 +-- packages/cli-core/src/commands/init/index.ts | 12 +++---- packages/cli-core/src/commands/link/index.ts | 18 +++++------ .../cli-core/src/commands/unlink/index.ts | 10 +++--- packages/cli-core/src/lib/auth-server.test.ts | 2 +- packages/cli-core/src/lib/dotenv.test.ts | 2 +- packages/cli-core/src/lib/errors.ts | 2 +- packages/cli-core/src/lib/framework.test.ts | 2 +- packages/cli-core/src/lib/pkce.test.ts | 2 +- .../cli-core/src/lib/token-exchange.test.ts | 2 +- packages/cli-core/src/mode.test.ts | 2 +- 22 files changed, 73 insertions(+), 67 deletions(-) diff --git a/packages/cli-core/src/cli-program.ts b/packages/cli-core/src/cli-program.ts index f6b77f893..b6c2b0329 100644 --- a/packages/cli-core/src/cli-program.ts +++ b/packages/cli-core/src/cli-program.ts @@ -1,20 +1,20 @@ import { Command } from "@commander-js/extra-typings"; -import { setMode, type Mode } from "./mode.js"; -import { init } from "./commands/init/index.js"; -import { login } from "./commands/auth/login.js"; -import { logout } from "./commands/auth/logout.js"; -import { whoami } from "./commands/whoami/index.js"; -import { pull } from "./commands/env/pull.js"; -import { deploy } from "./commands/deploy/index.js"; -import { configPull } from "./commands/config/pull.js"; -import { configPatch, configPut } from "./commands/config/push.js"; -import { configSchema } from "./commands/config/schema.js"; -import { api } from "./commands/api/index.js"; -import { link } from "./commands/link/index.js"; -import { unlink } from "./commands/unlink/index.js"; -import { doctor } from "./commands/doctor/index.js"; -import { CliError, UserAbortError, ApiError, EXIT_CODE, throwUsageError } from "./lib/errors.js"; -import { red } from "./lib/color.js"; +import { setMode, type Mode } from "./mode.ts"; +import { init } from "./commands/init/index.ts"; +import { login } from "./commands/auth/login.ts"; +import { logout } from "./commands/auth/logout.ts"; +import { whoami } from "./commands/whoami/index.ts"; +import { pull } from "./commands/env/pull.ts"; +import { deploy } from "./commands/deploy/index.ts"; +import { configPull } from "./commands/config/pull.ts"; +import { configPatch, configPut } from "./commands/config/push.ts"; +import { configSchema } from "./commands/config/schema.ts"; +import { api } from "./commands/api/index.ts"; +import { link } from "./commands/link/index.ts"; +import { unlink } from "./commands/unlink/index.ts"; +import { doctor } from "./commands/doctor/index.ts"; +import { CliError, UserAbortError, ApiError, EXIT_CODE, throwUsageError } from "./lib/errors.ts"; +import { red } from "./lib/color.ts"; async function getDevVersion(): Promise { try { diff --git a/packages/cli-core/src/cli.ts b/packages/cli-core/src/cli.ts index 525cc2a59..2a21ee4c9 100644 --- a/packages/cli-core/src/cli.ts +++ b/packages/cli-core/src/cli.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { createProgram, runProgram } from "./cli-program.js"; -import { EXIT_CODE } from "./lib/errors.js"; +import { createProgram, runProgram } from "./cli-program.ts"; +import { EXIT_CODE } from "./lib/errors.ts"; process.on("SIGINT", () => process.exit(EXIT_CODE.SIGINT)); runProgram(await createProgram()); diff --git a/packages/cli-core/src/commands/api/bapi.test.ts b/packages/cli-core/src/commands/api/bapi.test.ts index 9cdffaa56..ae6082d6b 100644 --- a/packages/cli-core/src/commands/api/bapi.test.ts +++ b/packages/cli-core/src/commands/api/bapi.test.ts @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach } from "bun:test"; import { stubFetch } from "../../test/stubs.ts"; -import { bapiRequest } from "./bapi"; -import { BapiError } from "../../lib/errors"; +import { bapiRequest } from "./bapi.ts"; +import { BapiError } from "../../lib/errors.ts"; describe("bapi", () => { const originalFetch = globalThis.fetch; diff --git a/packages/cli-core/src/commands/api/catalog.test.ts b/packages/cli-core/src/commands/api/catalog.test.ts index 4997a837f..3ee74def3 100644 --- a/packages/cli-core/src/commands/api/catalog.test.ts +++ b/packages/cli-core/src/commands/api/catalog.test.ts @@ -3,7 +3,13 @@ import { stubFetch } from "../../test/stubs.ts"; import { mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { parseSpec, loadCatalog, filterEndpoints, endpointsByTag, _setCacheDir } from "./catalog"; +import { + parseSpec, + loadCatalog, + filterEndpoints, + endpointsByTag, + _setCacheDir, +} from "./catalog.ts"; const MINIMAL_SPEC = ` openapi: "3.0.0" diff --git a/packages/cli-core/src/commands/api/index.test.ts b/packages/cli-core/src/commands/api/index.test.ts index 6dbd17f37..ec0d3e655 100644 --- a/packages/cli-core/src/commands/api/index.test.ts +++ b/packages/cli-core/src/commands/api/index.test.ts @@ -53,8 +53,8 @@ mock.module("../../lib/config.ts", () => ({ mock.module("@inquirer/prompts", () => promptsStubs); -const { _setConfigDir } = (await import("../../lib/config")) as any; -const { setMode } = (await import("../../mode")) as any; +const { _setConfigDir } = (await import("../../lib/config.ts")) as any; +const { setMode } = (await import("../../mode.ts")) as any; describe("api command", () => { const originalEnv = { ...process.env }; @@ -107,7 +107,7 @@ describe("api command", () => { }); async function runApi(endpoint: string, options: Record = {}) { - const { api } = await import("./index"); + const { api } = await import("./index.ts"); return api(endpoint, undefined, options); } diff --git a/packages/cli-core/src/commands/api/interactive.test.ts b/packages/cli-core/src/commands/api/interactive.test.ts index 814096d78..ecb813050 100644 --- a/packages/cli-core/src/commands/api/interactive.test.ts +++ b/packages/cli-core/src/commands/api/interactive.test.ts @@ -14,8 +14,8 @@ mock.module("../../mode.ts", () => ({ isHuman: () => _mode !== "agent", })); -const { parseSpec, _setCacheDir } = (await import("./catalog")) as any; -const { setMode } = (await import("../../mode")) as any; +const { parseSpec, _setCacheDir } = (await import("./catalog.ts")) as any; +const { setMode } = (await import("../../mode.ts")) as any; const MINIMAL_SPEC = ` openapi: "3.0.0" @@ -125,7 +125,7 @@ describe("apiInteractive", () => { test("shows help and returns in agent mode", async () => { setMode("agent"); - const { apiInteractive } = await import("./interactive"); + const { apiInteractive } = await import("./interactive.ts"); await apiInteractive({}); expect(errorSpy).toHaveBeenCalledWith( @@ -136,7 +136,7 @@ describe("apiInteractive", () => { test("completes full flow for GET endpoint (no body, no params)", async () => { setMode("human"); - const { apiInteractive } = await import("./interactive"); + const { apiInteractive } = await import("./interactive.ts"); // Step 1: select tag "Users" selectResponses.push("Users"); @@ -162,7 +162,7 @@ describe("apiInteractive", () => { test("prompts for path parameters", async () => { setMode("human"); - const { apiInteractive } = await import("./interactive"); + const { apiInteractive } = await import("./interactive.ts"); selectResponses.push("Users"); selectResponses.push({ @@ -185,7 +185,7 @@ describe("apiInteractive", () => { test("aborts when user declines confirmation", async () => { setMode("human"); - const { apiInteractive } = await import("./interactive"); + const { apiInteractive } = await import("./interactive.ts"); selectResponses.push("Users"); selectResponses.push({ diff --git a/packages/cli-core/src/commands/api/ls.test.ts b/packages/cli-core/src/commands/api/ls.test.ts index 72dd13e76..d2da3ef33 100644 --- a/packages/cli-core/src/commands/api/ls.test.ts +++ b/packages/cli-core/src/commands/api/ls.test.ts @@ -2,9 +2,9 @@ import { test, expect, describe, beforeEach, afterEach, spyOn } from "bun:test"; import { mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { parseSpec, _setCacheDir } from "./catalog"; +import { parseSpec, _setCacheDir } from "./catalog.ts"; import { stubFetch } from "../../test/stubs.ts"; -import { apiLs } from "./ls"; +import { apiLs } from "./ls.ts"; const MINIMAL_SPEC = ` openapi: "3.0.0" diff --git a/packages/cli-core/src/commands/config/pull.test.ts b/packages/cli-core/src/commands/config/pull.test.ts index ca4b010f4..addf26443 100644 --- a/packages/cli-core/src/commands/config/pull.test.ts +++ b/packages/cli-core/src/commands/config/pull.test.ts @@ -48,7 +48,7 @@ describe("config pull", () => { // Dynamically import to get fresh module state async function runConfigPull(options: { instance?: string; output?: string } = {}) { - const { configPull } = await import("./pull"); + const { configPull } = await import("./pull.ts"); return configPull(options); } diff --git a/packages/cli-core/src/commands/config/push.test.ts b/packages/cli-core/src/commands/config/push.test.ts index 9282f5a15..cfe210e28 100644 --- a/packages/cli-core/src/commands/config/push.test.ts +++ b/packages/cli-core/src/commands/config/push.test.ts @@ -56,7 +56,7 @@ describe("config push", () => { yes?: boolean; } = {}, ) { - const { configPatch } = await import("./push"); + const { configPatch } = await import("./push.ts"); return configPatch(options); } @@ -69,7 +69,7 @@ describe("config push", () => { yes?: boolean; } = {}, ) { - const { configPut } = await import("./push"); + const { configPut } = await import("./push.ts"); return configPut(options); } diff --git a/packages/cli-core/src/commands/config/schema.test.ts b/packages/cli-core/src/commands/config/schema.test.ts index 8de0aba40..2341edee9 100644 --- a/packages/cli-core/src/commands/config/schema.test.ts +++ b/packages/cli-core/src/commands/config/schema.test.ts @@ -54,7 +54,7 @@ describe("config schema", () => { async function runConfigSchema( options: { instance?: string; output?: string; keys?: string[] } = {}, ) { - const { configSchema } = await import("./schema"); + const { configSchema } = await import("./schema.ts"); return configSchema(options); } diff --git a/packages/cli-core/src/commands/deploy/index.ts b/packages/cli-core/src/commands/deploy/index.ts index a8fc4319a..043cf095e 100644 --- a/packages/cli-core/src/commands/deploy/index.ts +++ b/packages/cli-core/src/commands/deploy/index.ts @@ -1,6 +1,6 @@ import { select, input, confirm, password } from "@inquirer/prompts"; -import { isAgent } from "../../mode.js"; -import { dim, bold, cyan, green, blue, yellow } from "../../lib/color.js"; +import { isAgent } from "../../mode.ts"; +import { dim, bold, cyan, green, blue, yellow } from "../../lib/color.ts"; const DEPLOY_PROMPT = `You are deploying a Clerk application to production. Follow these steps: diff --git a/packages/cli-core/src/commands/env/pull.test.ts b/packages/cli-core/src/commands/env/pull.test.ts index d14b2024b..3b2de69b9 100644 --- a/packages/cli-core/src/commands/env/pull.test.ts +++ b/packages/cli-core/src/commands/env/pull.test.ts @@ -36,7 +36,7 @@ mock.module("../../lib/config.ts", () => ({ }, })); -const { _setConfigDir, setProfile } = (await import("../../lib/config")) as any; +const { _setConfigDir, setProfile } = (await import("../../lib/config.ts")) as any; describe("env pull", () => { const originalEnv = { ...process.env }; @@ -99,7 +99,7 @@ describe("env pull", () => { }); async function runEnvPull(options: { instance?: string; file?: string } = {}) { - const { pull } = await import("./pull"); + const { pull } = await import("./pull.ts"); return pull(options); } diff --git a/packages/cli-core/src/commands/init/index.ts b/packages/cli-core/src/commands/init/index.ts index 9a151249f..5e8cb7ef1 100644 --- a/packages/cli-core/src/commands/init/index.ts +++ b/packages/cli-core/src/commands/init/index.ts @@ -1,10 +1,10 @@ import { join } from "node:path"; -import { login } from "../auth/login.js"; -import { link } from "../link/index.js"; -import { pull } from "../env/pull.js"; -import { detectFramework } from "../../lib/framework.js"; -import { isAgent } from "../../mode.js"; -import { dim, cyan } from "../../lib/color.js"; +import { login } from "../auth/login.ts"; +import { link } from "../link/index.ts"; +import { pull } from "../env/pull.ts"; +import { detectFramework } from "../../lib/framework.ts"; +import { isAgent } from "../../mode.ts"; +import { dim, cyan } from "../../lib/color.ts"; const AGENT_PROMPT = `You are integrating Clerk authentication into an existing project. Follow these steps: diff --git a/packages/cli-core/src/commands/link/index.ts b/packages/cli-core/src/commands/link/index.ts index 3203cc242..06424874c 100644 --- a/packages/cli-core/src/commands/link/index.ts +++ b/packages/cli-core/src/commands/link/index.ts @@ -1,14 +1,14 @@ import { basename } from "node:path"; import { search, confirm } from "@inquirer/prompts"; -import { isAgent } from "../../mode.js"; -import { getToken } from "../../lib/credential-store.js"; -import { login } from "../auth/login.js"; -import { listApplications, fetchApplication, type Application } from "../../lib/plapi.js"; -import { setProfile, resolveProfile, moveProfile } from "../../lib/config.js"; -import { autolink, findClerkKeys, matchKeyToApp } from "../../lib/autolink.js"; -import { getGitRepoIdentifier, getGitRepoRoot, getGitNormalizedRemote } from "../../lib/git.js"; -import { dim, cyan } from "../../lib/color.js"; -import { CliError } from "../../lib/errors.js"; +import { isAgent } from "../../mode.ts"; +import { getToken } from "../../lib/credential-store.ts"; +import { login } from "../auth/login.ts"; +import { listApplications, fetchApplication, type Application } from "../../lib/plapi.ts"; +import { setProfile, resolveProfile, moveProfile } from "../../lib/config.ts"; +import { autolink, findClerkKeys, matchKeyToApp } from "../../lib/autolink.ts"; +import { getGitRepoIdentifier, getGitRepoRoot, getGitNormalizedRemote } from "../../lib/git.ts"; +import { dim, cyan } from "../../lib/color.ts"; +import { CliError } from "../../lib/errors.ts"; const AGENT_PROMPT = `You are linking a Clerk application to the current project directory. diff --git a/packages/cli-core/src/commands/unlink/index.ts b/packages/cli-core/src/commands/unlink/index.ts index 50e6c0608..012cf57a7 100644 --- a/packages/cli-core/src/commands/unlink/index.ts +++ b/packages/cli-core/src/commands/unlink/index.ts @@ -1,9 +1,9 @@ import { confirm } from "@inquirer/prompts"; -import { isAgent, isHuman } from "../../mode.js"; -import { resolveProfile, removeProfile } from "../../lib/config.js"; -import { getGitRepoRoot } from "../../lib/git.js"; -import { dim, cyan } from "../../lib/color.js"; -import { CliError, throwUserAbort } from "../../lib/errors.js"; +import { isAgent, isHuman } from "../../mode.ts"; +import { resolveProfile, removeProfile } from "../../lib/config.ts"; +import { getGitRepoRoot } from "../../lib/git.ts"; +import { dim, cyan } from "../../lib/color.ts"; +import { CliError, throwUserAbort } from "../../lib/errors.ts"; const AGENT_PROMPT = `You are unlinking a Clerk application from the current project directory. diff --git a/packages/cli-core/src/lib/auth-server.test.ts b/packages/cli-core/src/lib/auth-server.test.ts index cd9cf22a7..17e1ffaf4 100644 --- a/packages/cli-core/src/lib/auth-server.test.ts +++ b/packages/cli-core/src/lib/auth-server.test.ts @@ -1,5 +1,5 @@ import { test, expect, describe } from "bun:test"; -import { startAuthServer } from "./auth-server"; +import { startAuthServer } from "./auth-server.ts"; describe("auth-server", () => { test("starts on a random port", () => { diff --git a/packages/cli-core/src/lib/dotenv.test.ts b/packages/cli-core/src/lib/dotenv.test.ts index 53f887419..cffd30cff 100644 --- a/packages/cli-core/src/lib/dotenv.test.ts +++ b/packages/cli-core/src/lib/dotenv.test.ts @@ -1,5 +1,5 @@ import { test, expect, describe } from "bun:test"; -import { parseEnvFile, mergeEnvVars, serializeEnvFile } from "./dotenv"; +import { parseEnvFile, mergeEnvVars, serializeEnvFile } from "./dotenv.ts"; describe("parseEnvFile", () => { test("returns empty array for empty string", () => { diff --git a/packages/cli-core/src/lib/errors.ts b/packages/cli-core/src/lib/errors.ts index 0992b2b58..4f843dd76 100644 --- a/packages/cli-core/src/lib/errors.ts +++ b/packages/cli-core/src/lib/errors.ts @@ -1,4 +1,4 @@ -import { isAgent } from "../mode"; +import { isAgent } from "../mode.ts"; /** Standard process exit codes used by the CLI. */ export const EXIT_CODE = { diff --git a/packages/cli-core/src/lib/framework.test.ts b/packages/cli-core/src/lib/framework.test.ts index c214bd82a..45c2eb6ce 100644 --- a/packages/cli-core/src/lib/framework.test.ts +++ b/packages/cli-core/src/lib/framework.test.ts @@ -2,7 +2,7 @@ import { test, expect, describe, beforeEach, afterEach } from "bun:test"; import { mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { detectPublishableKeyName, detectFramework } from "./framework"; +import { detectPublishableKeyName, detectFramework } from "./framework.ts"; function writePkg(dir: string, deps: Record, devDeps?: Record) { return Bun.write( diff --git a/packages/cli-core/src/lib/pkce.test.ts b/packages/cli-core/src/lib/pkce.test.ts index b1351c126..bc61b42cb 100644 --- a/packages/cli-core/src/lib/pkce.test.ts +++ b/packages/cli-core/src/lib/pkce.test.ts @@ -1,5 +1,5 @@ import { test, expect, describe } from "bun:test"; -import { generateCodeVerifier, generateCodeChallenge, generateState } from "./pkce"; +import { generateCodeVerifier, generateCodeChallenge, generateState } from "./pkce.ts"; describe("PKCE", () => { test("generateCodeVerifier returns a 43-char string", () => { diff --git a/packages/cli-core/src/lib/token-exchange.test.ts b/packages/cli-core/src/lib/token-exchange.test.ts index ab1191053..5c0609b21 100644 --- a/packages/cli-core/src/lib/token-exchange.test.ts +++ b/packages/cli-core/src/lib/token-exchange.test.ts @@ -1,5 +1,5 @@ import { test, expect, describe, afterEach, mock } from "bun:test"; -import { exchangeCodeForToken, fetchUserInfo } from "./token-exchange"; +import { exchangeCodeForToken, fetchUserInfo } from "./token-exchange.ts"; const originalFetch = globalThis.fetch; diff --git a/packages/cli-core/src/mode.test.ts b/packages/cli-core/src/mode.test.ts index 0a6d4fd6d..c943b8272 100644 --- a/packages/cli-core/src/mode.test.ts +++ b/packages/cli-core/src/mode.test.ts @@ -2,7 +2,7 @@ import { test, expect, afterEach, describe } from "bun:test"; // Re-import fresh module per test by using dynamic import // But since bun caches modules, we test the exported functions directly -import { getMode, setMode, isHuman, isAgent } from "./mode"; +import { getMode, setMode, isHuman, isAgent } from "./mode.ts"; describe("mode detection", () => { const originalEnv = process.env.CLERK_MODE; From 5f2b957c505a242139d423a0387a5b86ac94ee55 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 12:06:28 -0600 Subject: [PATCH 05/44] fix: isolate test files to work around Bun mock.module leak Bun's mock.module() leaks between test files when run in a single process (oven-sh/bun#12823), causing 89 failures on Linux with fresh node_modules. Run each test file in its own bun test invocation. --- packages/cli-core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli-core/package.json b/packages/cli-core/package.json index 6658fd321..0bba16252 100644 --- a/packages/cli-core/package.json +++ b/packages/cli-core/package.json @@ -7,7 +7,7 @@ "build": "bun build ./src/cli.ts --outfile ./dist/cli.js --target node --external @napi-rs/keyring", "build:compile": "bun build --compile --no-compile-autoload-dotenv ./src/cli.ts --outfile ./dist/clerk", "dev": "bun run ./src/cli.ts", - "test": "bun test", + "test": "for f in $(find src -name '*.test.ts' | sort); do bun test \"$f\" || exit 1; done", "lint": "oxlint src/", "format": "oxfmt --write src/", "format:check": "oxfmt --check src/" From e9268b3fe73c4e3fee62877871f3d9eef6a81fd3 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 12:53:39 -0600 Subject: [PATCH 06/44] fix: harden workflows and clean up code issues - Fix shell injection in snapshot workflow by using env var instead of direct interpolation of comment body - Fix docs URL check in CliError (docs.clerk.com -> clerk.com/docs) - Deduplicate build matrix into reusable build-binaries.yml workflow - Use specific paths in .oxlintrc.json override instead of ** globs - Fix heredoc indentation in snapshot PR comment - Remove unnecessary async wrapper on login action - Use path.join() instead of template literal for path construction - Add musl detection fallback warning in CLI launcher - Rename root package from cli-new to @clerk/cli-workspace --- .github/workflows/build-binaries.yml | 87 ++++++++++++++++++ .github/workflows/release.yml | 127 +++------------------------ .github/workflows/snapshot.yml | 80 ++++------------- .oxlintrc.json | 2 +- package.json | 2 +- packages/cli-core/src/cli-program.ts | 3 +- packages/cli-core/src/lib/errors.ts | 2 +- packages/cli/bin/clerk | 4 + 8 files changed, 123 insertions(+), 184 deletions(-) create mode 100644 .github/workflows/build-binaries.yml diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml new file mode 100644 index 000000000..33c058541 --- /dev/null +++ b/.github/workflows/build-binaries.yml @@ -0,0 +1,87 @@ +name: Build Binaries + +on: + workflow_call: + inputs: + version: + required: true + type: string + ref: + required: true + type: string + artifact-prefix: + required: true + type: string + description: "Prefix for artifact names (e.g. 'clerk', 'clerk-canary', 'clerk-snapshot')" + +jobs: + build: + runs-on: blacksmith-2vcpu-ubuntu-2404 + strategy: + fail-fast: false + matrix: + # Keep in sync with scripts/releaser/targets.ts + include: + - target: darwin-arm64 + bun_target: bun-darwin-arm64 + ext: "" + - target: darwin-x64 + bun_target: bun-darwin-x64 + ext: "" + - target: linux-arm64 + bun_target: bun-linux-arm64 + ext: "" + - target: linux-arm64-musl + bun_target: bun-linux-arm64-musl + ext: "" + - target: linux-x64 + bun_target: bun-linux-x64 + ext: "" + - target: linux-x64-musl + bun_target: bun-linux-x64-musl + ext: "" + - target: win32-arm64 + bun_target: bun-windows-arm64 + ext: ".exe" + - target: win32-x64 + bun_target: bun-windows-x64 + ext: ".exe" + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + + - name: Build compiled binary + env: + CLI_VERSION: ${{ inputs.version }} + run: | + bun build --compile \ + --no-compile-autoload-dotenv \ + --target=${{ matrix.bun_target }} \ + --define "CLI_VERSION=\"$CLI_VERSION\"" \ + ./packages/cli-core/src/cli.ts \ + --outfile clerk${{ matrix.ext }} + + - name: Verify binary format + run: | + expected_format="${{ matrix.target }}" + binary="clerk${{ matrix.ext }}" + file_output=$(file "$binary") + echo "Binary: $file_output" + case "$expected_format" in + darwin-arm64) echo "$file_output" | grep -q "Mach-O.*arm64" ;; + darwin-x64) echo "$file_output" | grep -q "Mach-O.*x86_64" ;; + linux-arm64*) echo "$file_output" | grep -q "ELF.*ARM aarch64" ;; + linux-x64*) echo "$file_output" | grep -q "ELF.*x86-64" ;; + win32-arm64) echo "$file_output" | grep -q "PE32+.*Aarch64" ;; + win32-x64) echo "$file_output" | grep -q "PE32+.*x86-64" ;; + *) echo "Unknown target: $expected_format" && exit 1 ;; + esac + echo "Binary format verified for $expected_format" + + - uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-prefix }}-${{ matrix.target }} + path: clerk${{ matrix.ext }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ff59ecd49..4f9d1b98d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,73 +25,11 @@ jobs: build: needs: release-please if: ${{ needs.release-please.outputs.release_created == 'true' }} - runs-on: blacksmith-2vcpu-ubuntu-2404 - strategy: - fail-fast: false - matrix: - # Keep in sync with scripts/releaser/targets.ts - include: - - target: darwin-arm64 - bun_target: bun-darwin-arm64 - ext: "" - - target: darwin-x64 - bun_target: bun-darwin-x64 - ext: "" - - target: linux-arm64 - bun_target: bun-linux-arm64 - ext: "" - - target: linux-arm64-musl - bun_target: bun-linux-arm64-musl - ext: "" - - target: linux-x64 - bun_target: bun-linux-x64 - ext: "" - - target: linux-x64-musl - bun_target: bun-linux-x64-musl - ext: "" - - target: win32-arm64 - bun_target: bun-windows-arm64 - ext: ".exe" - - target: win32-x64 - bun_target: bun-windows-x64 - ext: ".exe" - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - - run: bun install --frozen-lockfile - - - name: Build compiled binary - env: - CLI_VERSION: ${{ needs.release-please.outputs.version }} - run: | - bun build --compile \ - --no-compile-autoload-dotenv \ - --target=${{ matrix.bun_target }} \ - --define "CLI_VERSION=\"$CLI_VERSION\"" \ - ./packages/cli-core/src/cli.ts \ - --outfile clerk${{ matrix.ext }} - - - name: Verify binary format - run: | - expected_format="${{ matrix.target }}" - binary="clerk${{ matrix.ext }}" - file_output=$(file "$binary") - echo "Binary: $file_output" - case "$expected_format" in - darwin-arm64) echo "$file_output" | grep -q "Mach-O.*arm64" ;; - darwin-x64) echo "$file_output" | grep -q "Mach-O.*x86_64" ;; - linux-arm64*) echo "$file_output" | grep -q "ELF.*ARM aarch64" ;; - linux-x64*) echo "$file_output" | grep -q "ELF.*x86-64" ;; - win32-arm64) echo "$file_output" | grep -q "PE32+.*Aarch64" ;; - win32-x64) echo "$file_output" | grep -q "PE32+.*x86-64" ;; - *) echo "Unknown target: $expected_format" && exit 1 ;; - esac - echo "Binary format verified for $expected_format" - - - uses: actions/upload-artifact@v4 - with: - name: clerk-${{ matrix.target }} - path: clerk${{ matrix.ext }} + uses: ./.github/workflows/build-binaries.yml + with: + version: ${{ needs.release-please.outputs.version }} + ref: ${{ github.sha }} + artifact-prefix: clerk smoke-test: needs: build @@ -203,56 +141,11 @@ jobs: canary-build: needs: canary-version - runs-on: blacksmith-2vcpu-ubuntu-2404 - strategy: - fail-fast: false - matrix: - # Keep in sync with scripts/releaser/targets.ts - include: - - target: darwin-arm64 - bun_target: bun-darwin-arm64 - ext: "" - - target: darwin-x64 - bun_target: bun-darwin-x64 - ext: "" - - target: linux-arm64 - bun_target: bun-linux-arm64 - ext: "" - - target: linux-arm64-musl - bun_target: bun-linux-arm64-musl - ext: "" - - target: linux-x64 - bun_target: bun-linux-x64 - ext: "" - - target: linux-x64-musl - bun_target: bun-linux-x64-musl - ext: "" - - target: win32-arm64 - bun_target: bun-windows-arm64 - ext: ".exe" - - target: win32-x64 - bun_target: bun-windows-x64 - ext: ".exe" - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - - run: bun install --frozen-lockfile - - - name: Build compiled binary - env: - CLI_VERSION: ${{ needs.canary-version.outputs.version }} - run: | - bun build --compile \ - --no-compile-autoload-dotenv \ - --target=${{ matrix.bun_target }} \ - --define "CLI_VERSION=\"$CLI_VERSION\"" \ - ./packages/cli-core/src/cli.ts \ - --outfile clerk${{ matrix.ext }} - - - uses: actions/upload-artifact@v4 - with: - name: clerk-canary-${{ matrix.target }} - path: clerk${{ matrix.ext }} + uses: ./.github/workflows/build-binaries.yml + with: + version: ${{ needs.canary-version.outputs.version }} + ref: ${{ github.sha }} + artifact-prefix: clerk-canary canary-publish: needs: [canary-version, canary-build] diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 739172c94..91bc1c86b 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -43,10 +43,11 @@ jobs: - name: Compute snapshot version id: version + env: + COMMENT_BODY: ${{ github.event.comment.body }} run: | - body="${{ github.event.comment.body }}" # Extract optional name from "!snapshot " — default to "snapshot" - name=$(echo "$body" | sed 's/^!snapshot[[:space:]]*//' | tr -cd 'a-zA-Z0-9-') + name=$(echo "$COMMENT_BODY" | sed 's/^!snapshot[[:space:]]*//' | tr -cd 'a-zA-Z0-9-') if [ -z "$name" ]; then name="snapshot"; fi base=$(jq -r '.["packages/cli"]' .release-please-manifest.json) sha=$(echo "${{ steps.pr.outputs.sha }}" | cut -c1-7) @@ -54,58 +55,11 @@ jobs: build: needs: snapshot - runs-on: blacksmith-2vcpu-ubuntu-2404 - strategy: - fail-fast: false - matrix: - # Keep in sync with scripts/releaser/targets.ts - include: - - target: darwin-arm64 - bun_target: bun-darwin-arm64 - ext: "" - - target: darwin-x64 - bun_target: bun-darwin-x64 - ext: "" - - target: linux-arm64 - bun_target: bun-linux-arm64 - ext: "" - - target: linux-arm64-musl - bun_target: bun-linux-arm64-musl - ext: "" - - target: linux-x64 - bun_target: bun-linux-x64 - ext: "" - - target: linux-x64-musl - bun_target: bun-linux-x64-musl - ext: "" - - target: win32-arm64 - bun_target: bun-windows-arm64 - ext: ".exe" - - target: win32-x64 - bun_target: bun-windows-x64 - ext: ".exe" - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ needs.snapshot.outputs.sha }} - - uses: oven-sh/setup-bun@v2 - - run: bun install --frozen-lockfile - - - name: Build compiled binary - env: - CLI_VERSION: ${{ needs.snapshot.outputs.version }} - run: | - bun build --compile \ - --no-compile-autoload-dotenv \ - --target=${{ matrix.bun_target }} \ - --define "CLI_VERSION=\"$CLI_VERSION\"" \ - ./packages/cli-core/src/cli.ts \ - --outfile clerk${{ matrix.ext }} - - - uses: actions/upload-artifact@v4 - with: - name: clerk-snapshot-${{ matrix.target }} - path: clerk${{ matrix.ext }} + uses: ./.github/workflows/build-binaries.yml + with: + version: ${{ needs.snapshot.outputs.version }} + ref: ${{ needs.snapshot.outputs.sha }} + artifact-prefix: clerk-snapshot publish: needs: [snapshot, build] @@ -157,16 +111,16 @@ jobs: run: | sha=$(echo "${SNAPSHOT_VERSION}" | rev | cut -d. -f1 | rev) gh pr comment "${{ github.event.issue.number }}" --repo "${GH_REPO}" --body "$(cat < Published from ${sha} - EOF +> Published from ${sha} +EOF )" diff --git a/.oxlintrc.json b/.oxlintrc.json index d0553dd4d..49a8b8d73 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -4,7 +4,7 @@ }, "overrides": [ { - "files": ["**/cli.ts", "**/cli-program.ts"], + "files": ["packages/cli-core/src/cli.ts", "packages/cli-core/src/cli-program.ts"], "rules": { "unicorn/no-process-exit": "off" } diff --git a/package.json b/package.json index d4b06e46f..dffa59ad7 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "cli-new", + "name": "@clerk/cli-workspace", "private": true, "workspaces": [ "packages/*" diff --git a/packages/cli-core/src/cli-program.ts b/packages/cli-core/src/cli-program.ts index b6c2b0329..90d973841 100644 --- a/packages/cli-core/src/cli-program.ts +++ b/packages/cli-core/src/cli-program.ts @@ -1,3 +1,4 @@ +import { join } from "node:path"; import { Command } from "@commander-js/extra-typings"; import { setMode, type Mode } from "./mode.ts"; import { init } from "./commands/init/index.ts"; @@ -18,7 +19,7 @@ import { red } from "./lib/color.ts"; async function getDevVersion(): Promise { try { - const pkg = await Bun.file(`${import.meta.dir}/../../cli/package.json`).json(); + const pkg = await Bun.file(join(import.meta.dir, "..", "..", "cli", "package.json")).json(); return `${pkg.version}-dev`; } catch { return "0.0.0-dev"; diff --git a/packages/cli-core/src/lib/errors.ts b/packages/cli-core/src/lib/errors.ts index 4f843dd76..138513c88 100644 --- a/packages/cli-core/src/lib/errors.ts +++ b/packages/cli-core/src/lib/errors.ts @@ -54,7 +54,7 @@ export class CliError extends Error { // without a .md extension, add .md to get the raw markdown URL. if ( isAgent() && - this.docsUrl.startsWith("https://docs.clerk.com/") && + this.docsUrl.startsWith("https://clerk.com/docs/") && !this.docsUrl.endsWith(".md") ) { this.docsUrl += ".md"; diff --git a/packages/cli/bin/clerk b/packages/cli/bin/clerk index 852b0395f..7fa0c4f75 100755 --- a/packages/cli/bin/clerk +++ b/packages/cli/bin/clerk @@ -17,6 +17,10 @@ function isMusl() { return report.sharedObjects.some((f) => f.includes("libc.musl-") || f.includes("ld-musl-")); } } catch {} + console.warn( + "Warning: Could not detect libc variant on Linux. Defaulting to glibc. " + + "If the binary fails to start, try installing the musl variant manually.", + ); return false; } From dc801a0124bb6db9646c6bbe1fcf91df8dc66fc9 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 13:21:32 -0600 Subject: [PATCH 07/44] fix: align release flow with JS monorepo conventions - Use NPM_CONFIG_PROVENANCE env var instead of --provenance flag for trusted publishing, matching the JS monorepo pattern - Add explicit id-token: write permissions to all publish jobs - Switch snapshot versions from SHA-based to datetime-based format (x.y.z-name.v) for monotonic semver ordering - Add bun run build to CI for pre-merge bundle validation - Update release documentation to reflect all changes --- .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 10 ++++++++++ .github/workflows/snapshot.yml | 14 ++++++++++---- docs/releasing.md | 16 ++++++++++++---- scripts/releaser/index.ts | 2 +- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b062f134..09d737b8e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,7 @@ jobs: - run: bun install --frozen-lockfile - run: bun run format:check - run: bun run lint + - run: bun run build test: runs-on: blacksmith-2vcpu-ubuntu-2404 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4f9d1b98d..c8d572fbc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,6 +83,9 @@ jobs: publish-npm: needs: [release-please, build, smoke-test] runs-on: blacksmith-2vcpu-ubuntu-2404 + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 @@ -101,10 +104,13 @@ jobs: env: ARTIFACTS_DIR: ${{ github.workspace }}/dist/artifacts NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_PROVENANCE: true upload-github-assets: needs: [release-please, build, smoke-test, publish-npm] runs-on: blacksmith-2vcpu-ubuntu-2404 + permissions: + contents: write steps: - uses: actions/download-artifact@v4 with: @@ -150,6 +156,9 @@ jobs: canary-publish: needs: [canary-version, canary-build] runs-on: blacksmith-2vcpu-ubuntu-2404 + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 @@ -178,3 +187,4 @@ jobs: CANARY_VERSION: ${{ needs.canary-version.outputs.version }} ARTIFACTS_DIR: ${{ github.workspace }}/dist/artifacts NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_PROVENANCE: true diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 91bc1c86b..8b11d2118 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -50,8 +50,8 @@ jobs: name=$(echo "$COMMENT_BODY" | sed 's/^!snapshot[[:space:]]*//' | tr -cd 'a-zA-Z0-9-') if [ -z "$name" ]; then name="snapshot"; fi base=$(jq -r '.["packages/cli"]' .release-please-manifest.json) - sha=$(echo "${{ steps.pr.outputs.sha }}" | cut -c1-7) - echo "version=${base}-${name}.${sha}" >> "$GITHUB_OUTPUT" + datetime=$(date -u +%Y%m%d%H%M%S) + echo "version=${base}-${name}.v${datetime}" >> "$GITHUB_OUTPUT" build: needs: snapshot @@ -64,6 +64,10 @@ jobs: publish: needs: [snapshot, build] runs-on: blacksmith-2vcpu-ubuntu-2404 + permissions: + contents: read + pull-requests: write + id-token: write steps: - uses: actions/checkout@v4 with: @@ -94,6 +98,7 @@ jobs: SNAPSHOT_VERSION: ${{ needs.snapshot.outputs.version }} ARTIFACTS_DIR: ${{ github.workspace }}/dist/artifacts NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_PROVENANCE: true - name: React with rocket env: @@ -108,8 +113,9 @@ jobs: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} SNAPSHOT_VERSION: ${{ needs.snapshot.outputs.version }} + SNAPSHOT_SHA: ${{ needs.snapshot.outputs.sha }} run: | - sha=$(echo "${SNAPSHOT_VERSION}" | rev | cut -d. -f1 | rev) + short_sha=$(echo "${SNAPSHOT_SHA}" | cut -c1-7) gh pr comment "${{ github.event.issue.number }}" --repo "${GH_REPO}" --body "$(cat < Published from ${sha} +> Published from ${short_sha} EOF )" diff --git a/docs/releasing.md b/docs/releasing.md index 737818a47..b6bb92d88 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -45,7 +45,7 @@ Install: `npm install -g @clerk/cli@canary` ### Snapshot (`@snapshot`) -Published on-demand from PR branches by commenting `!snapshot` (or `!snapshot `) on a pull request. The commenter must be a member or owner of the repository's organization. Snapshot versions use the format `x.y.z-.` (e.g., `0.0.1-snapshot.abc1234` or `0.0.1-my-feature.abc1234`). +Published on-demand from PR branches by commenting `!snapshot` (or `!snapshot `) on a pull request. The commenter must be a member or owner of the repository's organization. Snapshot versions use the format `x.y.z-.v` (e.g., `0.0.1-snapshot.v20260313145959` or `0.0.1-my-feature.v20260313145959`). The datetime format ensures multiple snapshots from the same PR sort monotonically in semver. Install: `npm install -g @clerk/cli@` (version is posted as a PR comment after publishing) @@ -87,7 +87,7 @@ Runs the releaser script (`scripts/releaser/index.ts`): 2. For each target, generates a platform package in `dist/platform-packages/`: - Creates `package.json` with `os`/`cpu` fields for npm platform selection - Copies the compiled binary from the build artifacts -3. Publishes each platform package with `--provenance --access public` +3. Publishes each platform package with `--access public` (provenance attestation is enabled via the `NPM_CONFIG_PROVENANCE=true` environment variable set in CI, which uses GitHub's OIDC token with `id-token: write` permission) 4. Temporarily mutates the wrapper `package.json` to add `optionalDependencies` and remove `private: true`, publishes it, then restores the original file The releaser accepts these flags: @@ -98,6 +98,14 @@ The releaser accepts these flags: All publishes are idempotent — the script checks `npm view` before publishing and skips already-published versions. +#### Environment Variables + +The releaser script and publish workflow steps use these environment variables: + +- `ARTIFACTS_DIR` — path to directory containing compiled binaries from the build job (defaults to `./dist/artifacts`) +- `NODE_AUTH_TOKEN` — npm authentication token for publishing (set from `secrets.NPM_TOKEN` in workflows) +- `NPM_CONFIG_PROVENANCE` — set to `true` to enable provenance attestation (requires `id-token: write` permission) + ### 4. Upload GitHub Assets Job Attaches the compiled binaries to the GitHub Release for direct download. Binaries are uploaded with display names following the `clerk-` convention (e.g., `clerk-darwin-arm64`, `clerk-win32-x64.exe`). @@ -124,8 +132,7 @@ The target list exists in these places that must stay in sync: 3. `.github/workflows/release.yml` smoke-test matrix — verifies binaries on native runners 4. `.github/workflows/release.yml` canary-build matrix — compiles canary binaries 5. `.github/workflows/snapshot.yml` build matrix — compiles snapshot binaries - -If you add or remove a target, update all of these. Note that the smoke-test matrix may not cover every target if a native runner isn't available (e.g., `win32-arm64`). + If you add or remove a target, update all of these. Note that the smoke-test matrix may not cover every target if a native runner isn't available (e.g., `win32-arm64`). ## Local Development @@ -152,3 +159,4 @@ bun run scripts/releaser/index.ts --dry-run - **Binary format verification**: The build job verifies each compiled binary matches its expected architecture before uploading. - **Native smoke tests**: Each binary is executed on a native runner for its platform before publishing. This catches cross-compilation issues that format checks alone would miss. - **Org membership check**: Snapshot releases require the commenter to be a `MEMBER` or `OWNER` of the repository's organization, verified via `author_association`. +- **CI build check**: Every PR to `main` runs a JS bundle build to catch bundler-specific failures before merge. diff --git a/scripts/releaser/index.ts b/scripts/releaser/index.ts index ed6099d56..f71ab21a2 100644 --- a/scripts/releaser/index.ts +++ b/scripts/releaser/index.ts @@ -78,7 +78,7 @@ async function generatePlatformPackage(target: Target, version: string): Promise } function publish(dir: string, dryRun: boolean, tag?: string): void { - const flags = ["npm", "publish", "--provenance", "--access", "public", "--ignore-scripts"]; + const flags = ["npm", "publish", "--access", "public", "--ignore-scripts"]; if (tag) flags.push("--tag", tag); if (dryRun) flags.push("--dry-run"); const result = Bun.spawnSync(flags, { cwd: dir, stdio: ["ignore", "pipe", "pipe"] }); From d6da7fe574ad7570c95d51ccb21cf63635eb1e70 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 13:38:59 -0600 Subject: [PATCH 08/44] docs: update release docs and add musl detection references Update releasing.md to reflect the build-binaries.yml reusable workflow extraction and document musl Docker-based smoke testing. Add reference comments to the bin/clerk shim explaining the musl detection approach. --- docs/releasing.md | 34 +++++++++++++++++----------------- packages/cli/bin/clerk | 6 ++++++ 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/docs/releasing.md b/docs/releasing.md index b6bb92d88..e0e0852b3 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -60,11 +60,11 @@ Configuration: ## Build Pipeline -The release workflow (`.github/workflows/release.yml`) runs when release-please creates a release. Build and publish jobs run on Blacksmith runners (`blacksmith-2vcpu-ubuntu-2404`); smoke tests run on platform-native GitHub-hosted runners. +The release workflow (`.github/workflows/release.yml`) runs when release-please creates a release. Binary compilation is handled by a reusable workflow (`.github/workflows/build-binaries.yml`) shared across stable, canary, and snapshot pipelines. Build and publish jobs run on Blacksmith runners (`blacksmith-2vcpu-ubuntu-2404`); smoke tests run on platform-native GitHub-hosted runners. ### 1. Build Job (matrix) -Runs once per target platform on a Blacksmith runner. Each job: +Defined in [`.github/workflows/build-binaries.yml`](../.github/workflows/build-binaries.yml) and called by the release, canary, and snapshot pipelines. Runs once per target platform on a Blacksmith runner. Each job: 1. Cross-compiles the CLI using `bun build --compile --no-compile-autoload-dotenv --target=` 2. Injects the version via `--define "CLI_VERSION=\"$CLI_VERSION\""` @@ -73,7 +73,7 @@ Runs once per target platform on a Blacksmith runner. Each job: ### 2. Smoke Test Job (matrix) -Downloads each compiled binary and runs `--version` on a native runner for that platform to verify the binary actually executes. The target-to-runner mapping is defined in the `smoke-test` matrix in [`.github/workflows/release.yml`](../.github/workflows/release.yml). +Downloads each compiled binary and runs `--version` to verify the binary actually executes. glibc targets run natively on a platform-matched GitHub-hosted runner; musl targets run inside an Alpine Docker container on a Linux runner. The target-to-runner mapping is defined in the `smoke-test` matrix in [`.github/workflows/release.yml`](../.github/workflows/release.yml). Not all targets may have a native runner available (e.g., `win32-arm64` is skipped because there is no GitHub-hosted ARM Windows runner). @@ -112,27 +112,27 @@ Attaches the compiled binaries to the GitHub Release for direct download. Binari ## Key Files -| File | Purpose | -| ------------------------------------ | ------------------------------------------------------------------------------ | -| `packages/cli/bin/clerk` | CJS shim that resolves and spawns the platform binary | -| `packages/cli/package.json` | Wrapper package (has `prepublishOnly` guard against accidental direct publish) | -| `packages/cli-core/src/cli.ts` | CLI entrypoint (reads `CLI_VERSION` global at runtime) | -| `packages/cli-core/src/globals.d.ts` | TypeScript declaration for the `CLI_VERSION` compile-time define | -| `scripts/releaser/index.ts` | Generates platform packages and publishes everything to npm | -| `scripts/releaser/targets.ts` | Target definitions — must be kept in sync with the workflow matrix | -| `.github/workflows/release.yml` | GitHub Actions release + canary workflow | -| `.github/workflows/snapshot.yml` | GitHub Actions snapshot workflow (triggered by PR comments) | +| File | Purpose | +| -------------------------------------- | ------------------------------------------------------------------------------ | +| `packages/cli/bin/clerk` | CJS shim that resolves and spawns the platform binary | +| `packages/cli/package.json` | Wrapper package (has `prepublishOnly` guard against accidental direct publish) | +| `packages/cli-core/src/cli.ts` | CLI entrypoint (reads `CLI_VERSION` global at runtime) | +| `packages/cli-core/src/globals.d.ts` | TypeScript declaration for the `CLI_VERSION` compile-time define | +| `scripts/releaser/index.ts` | Generates platform packages and publishes everything to npm | +| `scripts/releaser/targets.ts` | Target definitions — must be kept in sync with the workflow matrix | +| `.github/workflows/build-binaries.yml` | Reusable workflow for cross-compiling binaries (called by release + snapshot) | +| `.github/workflows/release.yml` | GitHub Actions release + canary workflow | +| `.github/workflows/snapshot.yml` | GitHub Actions snapshot workflow (triggered by PR comments) | ## Keeping Targets in Sync The target list exists in these places that must stay in sync: 1. `scripts/releaser/targets.ts` — used by the releaser to generate platform packages -2. `.github/workflows/release.yml` build matrix — compiles binaries (maps target names to Bun cross-compile targets, e.g., `win32-x64` → `bun-windows-x64`) +2. `.github/workflows/build-binaries.yml` build matrix — compiles binaries (maps target names to Bun cross-compile targets, e.g., `win32-x64` → `bun-windows-x64`) 3. `.github/workflows/release.yml` smoke-test matrix — verifies binaries on native runners -4. `.github/workflows/release.yml` canary-build matrix — compiles canary binaries -5. `.github/workflows/snapshot.yml` build matrix — compiles snapshot binaries - If you add or remove a target, update all of these. Note that the smoke-test matrix may not cover every target if a native runner isn't available (e.g., `win32-arm64`). + +If you add or remove a target, update all of these. Note that the smoke-test matrix may not cover every target if a native runner isn't available (e.g., `win32-arm64`). ## Local Development diff --git a/packages/cli/bin/clerk b/packages/cli/bin/clerk index 7fa0c4f75..2ad5ee942 100755 --- a/packages/cli/bin/clerk +++ b/packages/cli/bin/clerk @@ -5,11 +5,17 @@ const { spawn } = require("node:child_process"); const { platform, arch, argv, exit } = require("node:process"); +// Detect musl vs glibc to select the correct prebuilt binary. +// Approach based on detect-libc: https://github.com/lovell/detect-libc function isMusl() { if (platform !== "linux") return false; + // On musl systems, ldd is a shell script (or the dynamic linker itself) + // containing "musl". Similar to rustup's `ldd --version | grep musl`. try { return require("fs").readFileSync("/usr/bin/ldd", "utf-8").includes("musl"); } catch {} + // Fallback: Node's diagnostic report exposes libc info without child processes. + // https://nodejs.org/api/report.html#reportgetreport try { const report = process.report.getReport(); if (report.header && report.header.glibcVersionRuntime) return false; From 88ddd5375454ca13b348ef3a33f57efb2b2527ca Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 13:48:25 -0600 Subject: [PATCH 09/44] fix: address code review findings - Regenerate bun.lock with correct workspace name - Add canary smoke test (linux-x64) before publish - Add explicit files field to platform packages - Add root-level build:compile script --- .github/workflows/release.yml | 16 +++++- bun.lock | 94 ++++++++++++++++------------------- package.json | 1 + scripts/releaser/index.ts | 1 + 4 files changed, 60 insertions(+), 52 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c8d572fbc..ac0998b2a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -153,8 +153,22 @@ jobs: ref: ${{ github.sha }} artifact-prefix: clerk-canary + canary-smoke-test: + needs: canary-build + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: clerk-canary-linux-x64 + - name: Make executable + run: chmod +x ./clerk + - name: Smoke test + run: | + version=$(./clerk --version) + echo "Binary reports $version" + canary-publish: - needs: [canary-version, canary-build] + needs: [canary-version, canary-build, canary-smoke-test] runs-on: blacksmith-2vcpu-ubuntu-2404 permissions: contents: read diff --git a/bun.lock b/bun.lock index 22e5a6da1..c3bd47b93 100644 --- a/bun.lock +++ b/bun.lock @@ -1,9 +1,9 @@ { "lockfileVersion": 1, - "configVersion": 0, + "configVersion": 1, "workspaces": { "": { - "name": "marseille", + "name": "@clerk/cli-workspace", "devDependencies": { "@types/bun": "^1.3.9", "nano-staged": "^0.9.0", @@ -40,33 +40,33 @@ "@inquirer/ansi": ["@inquirer/ansi@2.0.3", "", {}, "sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw=="], - "@inquirer/checkbox": ["@inquirer/checkbox@5.0.4", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/core": "^11.1.1", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-DrAMU3YBGMUAp6ArwTIp/25CNDtDbxk7UjIrrtM25JVVrlVYlVzHh5HR1BDFu9JMyUoZ4ZanzeaHqNDttf3gVg=="], + "@inquirer/checkbox": ["@inquirer/checkbox@5.1.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/core": "^11.1.5", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-/HjF1LN0a1h4/OFsbGKHNDtWICFU/dqXCdym719HFTyJo9IG7Otr+ziGWc9S0iQuohRZllh+WprSgd5UW5Fw0g=="], - "@inquirer/confirm": ["@inquirer/confirm@6.0.4", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-WdaPe7foUnoGYvXzH4jp4wH/3l+dBhZ3uwhKjXjwdrq5tEIFaANxj6zrGHxLdsIA0yKM0kFPVcEalOZXBB5ISA=="], + "@inquirer/confirm": ["@inquirer/confirm@6.0.8", "", { "dependencies": { "@inquirer/core": "^11.1.5", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Di6dgmiZ9xCSUxWUReWTqDtbhXCuG2MQm2xmgSAIruzQzBqNf49b8E07/vbCYY506kDe8BiwJbegXweG8M1klw=="], - "@inquirer/core": ["@inquirer/core@11.1.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3", "cli-width": "^4.1.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^9.0.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-hV9o15UxX46OyQAtaoMqAOxGR8RVl1aZtDx1jHbCtSJy1tBdTfKxLPKf7utsE4cRy4tcmCQ4+vdV+ca+oNxqNA=="], + "@inquirer/core": ["@inquirer/core@11.1.5", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QQPAX+lka8GyLcZ7u7Nb1h6q72iZ/oy0blilC3IB2nSt1Qqxp7akt94Jqhi/DzARuN3Eo9QwJRvtl4tmVe4T5A=="], - "@inquirer/editor": ["@inquirer/editor@5.0.4", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/external-editor": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QI3Jfqcv6UO2/VJaEFONH8Im1ll++Xn/AJTBn9Xf+qx2M+H8KZAdQ5sAe2vtYlo+mLW+d7JaMJB4qWtK4BG3pw=="], + "@inquirer/editor": ["@inquirer/editor@5.0.8", "", { "dependencies": { "@inquirer/core": "^11.1.5", "@inquirer/external-editor": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-sLcpbb9B3XqUEGrj1N66KwhDhEckzZ4nI/W6SvLXyBX8Wic3LDLENlWRvkOGpCPoserabe+MxQkpiMoI8irvyA=="], - "@inquirer/expand": ["@inquirer/expand@5.0.4", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-0I/16YwPPP0Co7a5MsomlZLpch48NzYfToyqYAOWtBmaXSB80RiNQ1J+0xx2eG+Wfxt0nHtpEWSRr6CzNVnOGg=="], + "@inquirer/expand": ["@inquirer/expand@5.0.8", "", { "dependencies": { "@inquirer/core": "^11.1.5", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QieW3F1prNw3j+hxO7/NKkG1pk3oz7pOB6+5Upwu3OIwADfPX0oZVppsqlL+Vl/uBHHDSOBY0BirLctLnXwGGg=="], "@inquirer/external-editor": ["@inquirer/external-editor@2.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w=="], "@inquirer/figures": ["@inquirer/figures@2.0.3", "", {}, "sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g=="], - "@inquirer/input": ["@inquirer/input@5.0.4", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-4B3s3jvTREDFvXWit92Yc6jF1RJMDy2VpSqKtm4We2oVU65YOh2szY5/G14h4fHlyQdpUmazU5MPCFZPRJ0AOw=="], + "@inquirer/input": ["@inquirer/input@5.0.8", "", { "dependencies": { "@inquirer/core": "^11.1.5", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-p0IJslw0AmedLEkOU+yrEX3Aj2RTpQq7ZOf8nc1DIhjzaxRWrrgeuE5Kyh39fVRgtcACaMXx/9WNo8+GjgBOfw=="], - "@inquirer/number": ["@inquirer/number@4.0.4", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-CmMp9LF5HwE+G/xWsC333TlCzYYbXMkcADkKzcawh49fg2a1ryLc7JL1NJYYt1lJ+8f4slikNjJM9TEL/AljYQ=="], + "@inquirer/number": ["@inquirer/number@4.0.8", "", { "dependencies": { "@inquirer/core": "^11.1.5", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-uGLiQah9A0F9UIvJBX52m0CnqtLaym0WpT9V4YZrjZ+YRDKZdwwoEPz06N6w8ChE2lrnsdyhY9sL+Y690Kh9gQ=="], - "@inquirer/password": ["@inquirer/password@5.0.4", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-ZCEPyVYvHK4W4p2Gy6sTp9nqsdHQCfiPXIP9LbJVW4yCinnxL/dDDmPaEZVysGrj8vxVReRnpfS2fOeODe9zjg=="], + "@inquirer/password": ["@inquirer/password@5.0.8", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/core": "^11.1.5", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-zt1sF4lYLdvPqvmvHdmjOzuUUjuCQ897pdUCO8RbXMUDKXJTTyOQgtn23le+jwcb+MpHl3VAFvzIdxRAf6aPlA=="], - "@inquirer/prompts": ["@inquirer/prompts@8.2.0", "", { "dependencies": { "@inquirer/checkbox": "^5.0.4", "@inquirer/confirm": "^6.0.4", "@inquirer/editor": "^5.0.4", "@inquirer/expand": "^5.0.4", "@inquirer/input": "^5.0.4", "@inquirer/number": "^4.0.4", "@inquirer/password": "^5.0.4", "@inquirer/rawlist": "^5.2.0", "@inquirer/search": "^4.1.0", "@inquirer/select": "^5.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-rqTzOprAj55a27jctS3vhvDDJzYXsr33WXTjODgVOru21NvBo9yIgLIAf7SBdSV0WERVly3dR6TWyp7ZHkvKFA=="], + "@inquirer/prompts": ["@inquirer/prompts@8.3.0", "", { "dependencies": { "@inquirer/checkbox": "^5.1.0", "@inquirer/confirm": "^6.0.8", "@inquirer/editor": "^5.0.8", "@inquirer/expand": "^5.0.8", "@inquirer/input": "^5.0.8", "@inquirer/number": "^4.0.8", "@inquirer/password": "^5.0.8", "@inquirer/rawlist": "^5.2.4", "@inquirer/search": "^4.1.4", "@inquirer/select": "^5.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-JAj66kjdH/F1+B7LCigjARbwstt3SNUOSzMdjpsvwJmzunK88gJeXmcm95L9nw1KynvFVuY4SzXh/3Y0lvtgSg=="], - "@inquirer/rawlist": ["@inquirer/rawlist@5.2.0", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-CciqGoOUMrFo6HxvOtU5uL8fkjCmzyeB6fG7O1vdVAZVSopUBYECOwevDBlqNLyyYmzpm2Gsn/7nLrpruy9RFg=="], + "@inquirer/rawlist": ["@inquirer/rawlist@5.2.4", "", { "dependencies": { "@inquirer/core": "^11.1.5", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-fTuJ5Cq9W286isLxwj6GGyfTjx1Zdk4qppVEPexFuA6yioCCXS4V1zfKroQqw7QdbDPN73xs2DiIAlo55+kBqg=="], - "@inquirer/search": ["@inquirer/search@4.1.0", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-EAzemfiP4IFvIuWnrHpgZs9lAhWDA0GM3l9F4t4mTQ22IFtzfrk8xbkMLcAN7gmVML9O/i+Hzu8yOUyAaL6BKA=="], + "@inquirer/search": ["@inquirer/search@4.1.4", "", { "dependencies": { "@inquirer/core": "^11.1.5", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-9yPTxq7LPmYjrGn3DRuaPuPbmC6u3fiWcsE9ggfLcdgO/ICHYgxq7mEy1yJ39brVvgXhtOtvDVjDh9slJxE4LQ=="], - "@inquirer/select": ["@inquirer/select@5.0.4", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/core": "^11.1.1", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-s8KoGpPYMEQ6WXc0dT9blX2NtIulMdLOO3LA1UKOiv7KFWzlJ6eLkEYTDBIi+JkyKXyn8t/CD6TinxGjyLt57g=="], + "@inquirer/select": ["@inquirer/select@5.1.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/core": "^11.1.5", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-OyYbKnchS1u+zRe14LpYrN8S0wH1vD0p2yKISvSsJdH2TpI87fh4eZdWnpdbrGauCRWDph3NwxRmM4Pcm/hx1Q=="], "@inquirer/type": ["@inquirer/type@4.0.3", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw=="], @@ -134,53 +134,49 @@ "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.36.0", "", { "os": "win32", "cpu": "x64" }, "sha512-MoyeQ9S36ZTz/4bDhOKJgOBIDROd4dQ5AkT9iezhEaUBxAPdNX9Oq0jD8OSnCj3G4wam/XNxVWKMA52kmzmPtQ=="], - "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.51.0", "", { "os": "android", "cpu": "arm" }, "sha512-jJYIqbx4sX+suIxWstc4P7SzhEwb4ArWA2KVrmEuu9vH2i0qM6QIHz/ehmbGE4/2fZbpuMuBzTl7UkfNoqiSgw=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.53.0", "", { "os": "android", "cpu": "arm" }, "sha512-JC89/jAx4d2zhDIbK8MC4L659FN1WiMXMBkNg7b33KXSkYpUgcbf+0nz7+EPRg+VwWiZVfaoFkNHJ7RXYb5Neg=="], - "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.51.0", "", { "os": "android", "cpu": "arm64" }, "sha512-GtXyBCcH4ti98YdiMNCrpBNGitx87EjEWxevnyhcBK12k/Vu4EzSB45rzSC4fGFUD6sQgeaxItRCEEWeVwPafw=="], + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.53.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CY+pZfi+uyeU7AwFrEnjsNT+VfxYmKLMuk7bVxArd8f+09hQbJb8f7C7EpvTfNqrCK1J8zZlaYI4LltmEctgbQ=="], - "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.51.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3QJbeYaMHn6Bh2XeBXuITSsbnIctyTjvHf5nRjKYrT9pPeErNIpp5VDEeAXC0CZSwSVTsc8WOSDwgrAI24JolQ=="], + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.53.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0aqsC4HDQ94oI6kMz64iaOJ1f3bCVArxvaHJGOScBvFz6CcQedXi5b70Xg09CYjKNaHA56dW0QJfoZ/111kz1A=="], - "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.51.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-NzErhMaTEN1cY0E8C5APy74lw5VwsNfJfVPBMWPVQLqAbO0k4FFLjvHURvkUL+Y18Wu+8Vs1kbqPh2hjXYA4pg=="], + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.53.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-e+KvuaWtnisyWojO/t5qKDbp2dvVpg+1dl4MGnTb21QpY4+4+9Y1XmZPaztcA2XNvy4BIaXFW+9JH9tMpSBqUg=="], - "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.51.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-msAIh3vPAoKoHlOE/oe6Q5C/n9umypv/k81lED82ibrJotn+3YG2Qp1kiR8o/Dg5iOEU97c6tl0utxcyFenpFw=="], + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.53.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hpU0ZHVeblFjmZDfgi9BxhhCpURh0KjoFy5V+Tvp9sg/fRcnMUEfaJrgz+jQfOX4jctlVWrAs1ANs91+5iV+zA=="], - "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.51.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CqQPcvqYyMe9ZBot2stjGogEzk1z8gGAngIX7srSzrzexmXixwVxBdFZyxTVM0CjGfDeV+Ru0w25/WNjlMM2Hw=="], + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.53.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ccKxOpw+X4xa2pO+qbTOpxQ2x1+Ag3ViRQMnWt3gHp1LcpNgS1xd6GYc3OvehmHtrXqEV3YGczZ0I1qpBB4/2A=="], - "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.51.0", "", { "os": "linux", "cpu": "arm" }, "sha512-dstrlYQgZMnyOssxSbolGCge/sDbko12N/35RBNuqLpoPbft2aeBidBAb0dvQlyBd9RJ6u8D4o4Eh8Un6iTgyQ=="], + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.53.0", "", { "os": "linux", "cpu": "arm" }, "sha512-UBkBvmzSmlyH2ZObQMDKW/TuyTmUtP/XClPUyU2YLwj0qLopZTZxnDz4VG5d3wz1HQuZXO0o1QqsnQUW1v4a6Q=="], - "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.51.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-QEjUpXO7d35rP1/raLGGbAsBLLGZIzV3ZbeSjqWlD3oRnxpRIZ6iL4o51XQHkconn3uKssc+1VKdtHJ81BBhDA=="], + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.53.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-PQJJ1izoH9p61las6rZ0BWOznAhTDMmdUPL2IEBLuXFwhy2mSloYHvRkk39PSYJ1DyG+trqU5Z9ZbtHSGH6plg=="], - "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.51.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-YSJua5irtG4DoMAjUapDTPhkQLHhBIY0G9JqlZS6/SZPzqDkPku/1GdWs0D6h/wyx0Iz31lNCfIaWKBQhzP0wQ=="], + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.53.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-GXI1o4Thn/rtnRIL38BwrDMwVcUbIHKCsOixIWf/CkU3fCG3MXFzFTtDMt+34ik0Qk452d8kcpksL0w/hUkMZA=="], - "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.51.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-7L4Wj2IEUNDETKssB9IDYt16T6WlF+X2jgC/hBq3diGHda9vJLpAgb09+D3quFq7TdkFtI7hwz/jmuQmQFPc1Q=="], + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.53.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Uahk7IVs2yBamCgeJ3XKpKT9Vh+de0pDKISFKnjEcI3c/w2CFHk1+W6Q6G3KI56HGwE9PWCp6ayhA9whXWkNIQ=="], - "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.51.0", "", { "os": "linux", "cpu": "none" }, "sha512-cBUHqtOXy76G41lOB401qpFoKx1xq17qYkhWrLSM7eEjiHM9sOtYqpr6ZdqCnN9s6ZpzudX4EkeHOFH2E9q0vA=="], + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.53.0", "", { "os": "linux", "cpu": "none" }, "sha512-sWtcU9UkrKMWsGKdFy8R6jkm9Q0VVG1VCpxVuh0HzRQQi3ENI1Nh5CkpsdfUs2MKRcOoHKbXqTscunuXjhxoxQ=="], - "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.51.0", "", { "os": "linux", "cpu": "none" }, "sha512-WKbg8CysgZcHfZX0ixQFBRSBvFZUHa3SBnEjHY2FVYt2nbNJEjzTxA3ZR5wMU0NOCNKIAFUFvAh5/XJKPRJuJg=="], + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.53.0", "", { "os": "linux", "cpu": "none" }, "sha512-aXew1+HDvCdExijX/8NBVC854zJwxhKP3l9AHFSHQNo4EanlHtzDMIlIvP3raUkL0vXtFCkTFYezzU5HjstB8A=="], - "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.51.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-N1QRUvJTxqXNSu35YOufdjsAVmKVx5bkrggOWAhTWBc3J4qjcBwr1IfyLh/6YCg8sYRSR1GraldS9jUgJL/U4A=="], + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.53.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-rVpyBSqPGou9sITcsoXqUoGBUH74bxYLYOAGUqN599Zu6BQBlBU9hh3bJQ/20D1xrhhrsbiCpVPvXpLPM5nL1w=="], - "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.51.0", "", { "os": "linux", "cpu": "x64" }, "sha512-e0Mz0DizsCoqNIjeOg6OUKe8JKJWZ5zZlwsd05Bmr51Jo3AOL4UJnPvwKumr4BBtBrDZkCmOLhCvDGm95nJM2g=="], + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.53.0", "", { "os": "linux", "cpu": "x64" }, "sha512-eOyeQ8qFQ2geXmlWJuXAOaek0hFhbMLlYsU457NMLKDRoC43Xf+eDPZ9Yk0n9jDaGJ5zBl/3Dy8wo41cnIXuLA=="], - "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.51.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wD8HGTWhYBKXvRDvoBVB1y+fEYV01samhWQSy1Zkxq2vpezvMnjaFKRuiP6tBNITLGuffbNDEXOwcAhJ3gI5Ug=="], + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.53.0", "", { "os": "linux", "cpu": "x64" }, "sha512-S6rBArW/zD1tob8M9PwKYrRmz+j1ss1+wjbRAJCWKd7TC3JB6noDiA95pIj9zOZVVp04MIzy5qymnYusrEyXzg=="], - "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.51.0", "", { "os": "none", "cpu": "arm64" }, "sha512-5NSwQ2hDEJ0GPXqikjWtwzgAQCsS7P9aLMNenjjKa+gknN3lTCwwwERsT6lKXSirfU3jLjexA2XQvQALh5h27w=="], + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.53.0", "", { "os": "none", "cpu": "arm64" }, "sha512-sd/A0Ny5sN0D/MJtlk7w2jGY4bJQou7gToa9WZF7Sj6HTyVzvlzKJWiOHfr4SulVk4ndiFQ8rKmF9rXP0EcF3A=="], - "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.51.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-JEZyah1M0RHMw8d+jjSSJmSmO8sABA1J1RtrHYujGPeCkYg1NeH0TGuClpe2h5QtioRTaF57y/TZfn/2IFV6fA=="], + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.53.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-QC3q7b51Er/ZurEFcFzc7RpQ/YEoEBLJuCp3WoOzhSHHH/nkUKFy+igOxlj1z3LayhEZPDQQ7sXvv2PM2cdG3Q=="], - "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.51.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-q3cEoKH6kwjz/WRyHwSf0nlD2F5Qw536kCXvmlSu+kaShzgrA0ojmh45CA81qL+7udfCaZL2SdKCZlLiGBVFlg=="], + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.53.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-3OvLgOqwd705hWHV2i8ni80pilvg6BUgpC2+xtVu++e/q28LKVohGh5J5QYJOrRMfWmxK0M/AUu43vUw62LAKQ=="], - "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.51.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Q14+fOGb9T28nWF/0EUsYqERiRA7cl1oy4TJrGmLaqhm+aO2cV+JttboHI3CbdeMCAyDI1+NoSlrM7Melhp/cw=="], + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.53.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xTiOkntexCdJytZ7ArIIgl3vGW5ujMM3sJNM7/+iqGAVJagCqjFFWn68HRWRLeyT66c95uR+CeFmQFI6mLQqDw=="], - "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], - "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], + "@types/node": ["@types/node@25.4.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="], - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], @@ -188,11 +184,13 @@ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - "env-paths": ["env-paths@4.0.0", "", { "dependencies": { "is-safe-filename": "^0.1.0" } }, "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw=="], - "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], @@ -204,7 +202,7 @@ "oxfmt": ["oxfmt@0.36.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.36.0", "@oxfmt/binding-android-arm64": "0.36.0", "@oxfmt/binding-darwin-arm64": "0.36.0", "@oxfmt/binding-darwin-x64": "0.36.0", "@oxfmt/binding-freebsd-x64": "0.36.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.36.0", "@oxfmt/binding-linux-arm-musleabihf": "0.36.0", "@oxfmt/binding-linux-arm64-gnu": "0.36.0", "@oxfmt/binding-linux-arm64-musl": "0.36.0", "@oxfmt/binding-linux-ppc64-gnu": "0.36.0", "@oxfmt/binding-linux-riscv64-gnu": "0.36.0", "@oxfmt/binding-linux-riscv64-musl": "0.36.0", "@oxfmt/binding-linux-s390x-gnu": "0.36.0", "@oxfmt/binding-linux-x64-gnu": "0.36.0", "@oxfmt/binding-linux-x64-musl": "0.36.0", "@oxfmt/binding-openharmony-arm64": "0.36.0", "@oxfmt/binding-win32-arm64-msvc": "0.36.0", "@oxfmt/binding-win32-ia32-msvc": "0.36.0", "@oxfmt/binding-win32-x64-msvc": "0.36.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-/ejJ+KoSW6J9bcNT9a9UtJSJNWhJ3yOLSBLbkoFHJs/8CZjmaZVZAJe4YgO1KMJlKpNQasrn/G9JQUEZI3p0EQ=="], - "oxlint": ["oxlint@1.51.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.51.0", "@oxlint/binding-android-arm64": "1.51.0", "@oxlint/binding-darwin-arm64": "1.51.0", "@oxlint/binding-darwin-x64": "1.51.0", "@oxlint/binding-freebsd-x64": "1.51.0", "@oxlint/binding-linux-arm-gnueabihf": "1.51.0", "@oxlint/binding-linux-arm-musleabihf": "1.51.0", "@oxlint/binding-linux-arm64-gnu": "1.51.0", "@oxlint/binding-linux-arm64-musl": "1.51.0", "@oxlint/binding-linux-ppc64-gnu": "1.51.0", "@oxlint/binding-linux-riscv64-gnu": "1.51.0", "@oxlint/binding-linux-riscv64-musl": "1.51.0", "@oxlint/binding-linux-s390x-gnu": "1.51.0", "@oxlint/binding-linux-x64-gnu": "1.51.0", "@oxlint/binding-linux-x64-musl": "1.51.0", "@oxlint/binding-openharmony-arm64": "1.51.0", "@oxlint/binding-win32-arm64-msvc": "1.51.0", "@oxlint/binding-win32-ia32-msvc": "1.51.0", "@oxlint/binding-win32-x64-msvc": "1.51.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-g6DNPaV9/WI9MoX2XllafxQuxwY1TV++j7hP8fTJByVBuCoVtm3dy9f/2vtH/HU40JztcgWF4G7ua+gkainklQ=="], + "oxlint": ["oxlint@1.53.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.53.0", "@oxlint/binding-android-arm64": "1.53.0", "@oxlint/binding-darwin-arm64": "1.53.0", "@oxlint/binding-darwin-x64": "1.53.0", "@oxlint/binding-freebsd-x64": "1.53.0", "@oxlint/binding-linux-arm-gnueabihf": "1.53.0", "@oxlint/binding-linux-arm-musleabihf": "1.53.0", "@oxlint/binding-linux-arm64-gnu": "1.53.0", "@oxlint/binding-linux-arm64-musl": "1.53.0", "@oxlint/binding-linux-ppc64-gnu": "1.53.0", "@oxlint/binding-linux-riscv64-gnu": "1.53.0", "@oxlint/binding-linux-riscv64-musl": "1.53.0", "@oxlint/binding-linux-s390x-gnu": "1.53.0", "@oxlint/binding-linux-x64-gnu": "1.53.0", "@oxlint/binding-linux-x64-musl": "1.53.0", "@oxlint/binding-openharmony-arm64": "1.53.0", "@oxlint/binding-win32-arm64-msvc": "1.53.0", "@oxlint/binding-win32-ia32-msvc": "1.53.0", "@oxlint/binding-win32-x64-msvc": "1.53.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-TLW0PzGbpO1JxUnuy1pIqVPjQUGh4fNfxu5XJbdFIRFVaJ0UFzTjjk/hSFTMRxN6lZub53xL/IwJNEkrh7VtDg=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -212,17 +210,11 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - - "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], } diff --git a/package.json b/package.json index dffa59ad7..0f7134bf6 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "lint": "bun run --filter @clerk/cli-core lint && oxlint scripts/", "format": "bun run --filter @clerk/cli-core format && oxfmt --write scripts/", "format:check": "bun run --filter @clerk/cli-core format:check && oxfmt --check scripts/", + "build:compile": "bun run --filter @clerk/cli-core build:compile", "prepare": "git config core.hooksPath .hooks" }, "devDependencies": { diff --git a/scripts/releaser/index.ts b/scripts/releaser/index.ts index f71ab21a2..aed0f2374 100644 --- a/scripts/releaser/index.ts +++ b/scripts/releaser/index.ts @@ -68,6 +68,7 @@ async function generatePlatformPackage(target: Target, version: string): Promise os: [target.os], cpu: [target.cpu], preferUnplugged: true, + files: ["bin"], }; if (target.libc) { pkg.libc = [target.libc]; From 31b83b7d04cad0706ab0b298a7309972efa49024 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 14:01:52 -0600 Subject: [PATCH 10/44] fix: add snapshot smoke test, guard signal forwarding, fix docs - Add linux-x64 smoke test to snapshot workflow before publishing - Guard npm shim signal forwarding with `exited` flag to prevent double-kill after child process exits - Fix targets.ts comment to reference build-binaries.yml (not release.yml) - Document win32-arm64 as best-effort (untested) in workflow and releasing docs --- .github/workflows/release.yml | 4 +++- .github/workflows/snapshot.yml | 16 +++++++++++++++- docs/releasing.md | 9 +++++---- packages/cli/bin/clerk | 2 +- scripts/releaser/targets.ts | 2 +- 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ac0998b2a..64fabf490 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,9 @@ jobs: - target: win32-x64 runner: windows-latest bin: ./clerk.exe - # win32-arm64: no GitHub-hosted ARM Windows runner available + # win32-arm64: no GitHub-hosted ARM Windows runner available — binary is + # published untested (best-effort). Build-time format verification confirms + # it is a valid PE32+/Aarch64 binary but execution is not validated. runs-on: ${{ matrix.runner }} steps: - uses: actions/download-artifact@v4 diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 8b11d2118..0d8d0db3a 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -61,8 +61,22 @@ jobs: ref: ${{ needs.snapshot.outputs.sha }} artifact-prefix: clerk-snapshot + smoke-test: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: clerk-snapshot-linux-x64 + - name: Make executable + run: chmod +x ./clerk + - name: Smoke test + run: | + version=$(./clerk --version) + echo "Binary reports $version" + publish: - needs: [snapshot, build] + needs: [snapshot, build, smoke-test] runs-on: blacksmith-2vcpu-ubuntu-2404 permissions: contents: read diff --git a/docs/releasing.md b/docs/releasing.md index e0e0852b3..c42919799 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -17,8 +17,9 @@ push to main PR comment "!snapshot [name]" → build job: cross-compile binaries from PR branch - → publish-npm: publish @snapshot packages - → post installation comment on PR + → smoke-test job: verify linux-x64 binary + → publish-npm: publish @snapshot packages + → post installation comment on PR ``` ## Architecture @@ -39,7 +40,7 @@ Install: `npm install -g @clerk/cli` ### Canary (`@canary`) -Published automatically on every push to `main` that does **not** trigger a stable release. Canary versions use the format `x.y.z-canary.` (e.g., `0.0.1-canary.abc1234`). Smoke tests are skipped for faster feedback. +Published automatically on every push to `main` that does **not** trigger a stable release. Canary versions use the format `x.y.z-canary.` (e.g., `0.0.1-canary.abc1234`). A single linux-x64 smoke test runs before publishing. Install: `npm install -g @clerk/cli@canary` @@ -75,7 +76,7 @@ Defined in [`.github/workflows/build-binaries.yml`](../.github/workflows/build-b Downloads each compiled binary and runs `--version` to verify the binary actually executes. glibc targets run natively on a platform-matched GitHub-hosted runner; musl targets run inside an Alpine Docker container on a Linux runner. The target-to-runner mapping is defined in the `smoke-test` matrix in [`.github/workflows/release.yml`](../.github/workflows/release.yml). -Not all targets may have a native runner available (e.g., `win32-arm64` is skipped because there is no GitHub-hosted ARM Windows runner). +Not all targets have a native runner available. `win32-arm64` is published as best-effort — the build job verifies it is a valid PE32+/Aarch64 binary via `file` output, but no execution-level smoke test runs because there is no GitHub-hosted ARM Windows runner. Publishing and GitHub Release upload are gated on all smoke tests passing. diff --git a/packages/cli/bin/clerk b/packages/cli/bin/clerk index 2ad5ee942..065b4df0f 100755 --- a/packages/cli/bin/clerk +++ b/packages/cli/bin/clerk @@ -60,7 +60,7 @@ child.on("error", (err) => { const signals = platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"]; for (const sig of signals) { - process.on(sig, () => child.kill(sig)); + process.on(sig, () => { if (!exited) child.kill(sig); }); } child.on("close", (code, signal) => { diff --git a/scripts/releaser/targets.ts b/scripts/releaser/targets.ts index bdf6c30ef..37084786c 100644 --- a/scripts/releaser/targets.ts +++ b/scripts/releaser/targets.ts @@ -7,7 +7,7 @@ export interface Target { // Target names use Node.js ${process.platform}-${process.arch} convention so the wrapper // shim (packages/cli/bin/clerk) can derive package names without a lookup table. -// Keep in sync with .github/workflows/release.yml matrix. +// Keep in sync with .github/workflows/build-binaries.yml matrix. export const targets: Target[] = [ { name: "darwin-arm64", os: "darwin", cpu: "arm64" }, { name: "darwin-x64", os: "darwin", cpu: "x64" }, From bffd158bf1fae5f8d16196314e28fda614a75b44 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 14:47:50 -0600 Subject: [PATCH 11/44] feat: migrate npm publishing to OIDC trusted publishing Switch publish jobs from Blacksmith to GitHub-hosted runners (ubuntu-latest) to enable npm OIDC trusted publishing. This eliminates the NPM_TOKEN secret and authenticates via GitHub's OIDC provider instead. - Move publish-npm, canary-publish, snapshot publish, and upload-github-assets jobs to ubuntu-latest - Add npm upgrade step (>= 11.5.1 required for OIDC) - Remove NODE_AUTH_TOKEN and NPM_CONFIG_PROVENANCE env vars - Keep Blacksmith for build, CI, and compute-heavy jobs - Update releasing docs with OIDC details and first-publish caveat --- .github/workflows/release.yml | 16 +++++++++------- .github/workflows/snapshot.yml | 7 ++++--- docs/releasing.md | 11 ++++++++--- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 64fabf490..26e4fd05a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,7 +84,8 @@ jobs: publish-npm: needs: [release-please, build, smoke-test] - runs-on: blacksmith-2vcpu-ubuntu-2404 + # Must run on GitHub-hosted runner for npm OIDC trusted publishing + runs-on: ubuntu-latest permissions: contents: read id-token: write @@ -95,6 +96,8 @@ jobs: with: node-version: "22" registry-url: "https://registry.npmjs.org" + - name: Upgrade npm for OIDC trusted publishing + run: npm install -g npm@latest - run: bun install --frozen-lockfile - uses: actions/download-artifact@v4 @@ -105,12 +108,10 @@ jobs: run: bun run scripts/releaser/index.ts env: ARTIFACTS_DIR: ${{ github.workspace }}/dist/artifacts - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - NPM_CONFIG_PROVENANCE: true upload-github-assets: needs: [release-please, build, smoke-test, publish-npm] - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ubuntu-latest permissions: contents: write steps: @@ -171,7 +172,8 @@ jobs: canary-publish: needs: [canary-version, canary-build, canary-smoke-test] - runs-on: blacksmith-2vcpu-ubuntu-2404 + # Must run on GitHub-hosted runner for npm OIDC trusted publishing + runs-on: ubuntu-latest permissions: contents: read id-token: write @@ -182,6 +184,8 @@ jobs: with: node-version: "22" registry-url: "https://registry.npmjs.org" + - name: Upgrade npm for OIDC trusted publishing + run: npm install -g npm@latest - run: bun install --frozen-lockfile - uses: actions/download-artifact@v4 @@ -202,5 +206,3 @@ jobs: env: CANARY_VERSION: ${{ needs.canary-version.outputs.version }} ARTIFACTS_DIR: ${{ github.workspace }}/dist/artifacts - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - NPM_CONFIG_PROVENANCE: true diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 0d8d0db3a..8b8a86a6c 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -77,7 +77,8 @@ jobs: publish: needs: [snapshot, build, smoke-test] - runs-on: blacksmith-2vcpu-ubuntu-2404 + # Must run on GitHub-hosted runner for npm OIDC trusted publishing + runs-on: ubuntu-latest permissions: contents: read pull-requests: write @@ -91,6 +92,8 @@ jobs: with: node-version: "22" registry-url: "https://registry.npmjs.org" + - name: Upgrade npm for OIDC trusted publishing + run: npm install -g npm@latest - run: bun install --frozen-lockfile - uses: actions/download-artifact@v4 @@ -111,8 +114,6 @@ jobs: env: SNAPSHOT_VERSION: ${{ needs.snapshot.outputs.version }} ARTIFACTS_DIR: ${{ github.workspace }}/dist/artifacts - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - NPM_CONFIG_PROVENANCE: true - name: React with rocket env: diff --git a/docs/releasing.md b/docs/releasing.md index c42919799..4a2a0fda4 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -88,7 +88,7 @@ Runs the releaser script (`scripts/releaser/index.ts`): 2. For each target, generates a platform package in `dist/platform-packages/`: - Creates `package.json` with `os`/`cpu` fields for npm platform selection - Copies the compiled binary from the build artifacts -3. Publishes each platform package with `--access public` (provenance attestation is enabled via the `NPM_CONFIG_PROVENANCE=true` environment variable set in CI, which uses GitHub's OIDC token with `id-token: write` permission) +3. Publishes each platform package with `--access public` (authentication and provenance use npm OIDC trusted publishing — no `NPM_TOKEN` secret needed, just `id-token: write` permission on a GitHub-hosted runner) 4. Temporarily mutates the wrapper `package.json` to add `optionalDependencies` and remove `private: true`, publishes it, then restores the original file The releaser accepts these flags: @@ -104,8 +104,12 @@ All publishes are idempotent — the script checks `npm view` before publishing The releaser script and publish workflow steps use these environment variables: - `ARTIFACTS_DIR` — path to directory containing compiled binaries from the build job (defaults to `./dist/artifacts`) -- `NODE_AUTH_TOKEN` — npm authentication token for publishing (set from `secrets.NPM_TOKEN` in workflows) -- `NPM_CONFIG_PROVENANCE` — set to `true` to enable provenance attestation (requires `id-token: write` permission) + +#### npm Authentication + +Publishing uses [npm OIDC trusted publishing](https://docs.npmjs.com/trusted-publishers/) instead of stored secrets. The publish jobs run on GitHub-hosted runners with `id-token: write` permission, and npm >= 11.5.1 automatically authenticates via GitHub's OIDC provider. Each package must have a trusted publisher configured on npmjs.com pointing to the correct workflow file. + +> **First publish**: New packages cannot use trusted publishing until they exist on npm. The very first stable release requires a one-time `NODE_AUTH_TOKEN` with a granular access token. After that, configure trusted publishers for all packages and remove the token. ### 4. Upload GitHub Assets Job @@ -160,4 +164,5 @@ bun run scripts/releaser/index.ts --dry-run - **Binary format verification**: The build job verifies each compiled binary matches its expected architecture before uploading. - **Native smoke tests**: Each binary is executed on a native runner for its platform before publishing. This catches cross-compilation issues that format checks alone would miss. - **Org membership check**: Snapshot releases require the commenter to be a `MEMBER` or `OWNER` of the repository's organization, verified via `author_association`. +- **OIDC trusted publishing**: Publish jobs authenticate via GitHub's OIDC provider instead of stored npm tokens. This eliminates secret rotation, prevents token exfiltration, and scopes publish permissions to specific workflow files. - **CI build check**: Every PR to `main` runs a JS bundle build to catch bundler-specific failures before merge. From a14084c95f611ad9edac958c1f38ef765251e8be Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 15:10:41 -0600 Subject: [PATCH 12/44] refactor: simplify shim by using spawnSync instead of async spawn Replaces the async spawn with signal forwarding, event handlers, and exited guard with a single spawnSync call. The child process receives terminal signals directly via the shared process group. --- packages/cli/bin/clerk | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/packages/cli/bin/clerk b/packages/cli/bin/clerk index 065b4df0f..e6e03c9db 100755 --- a/packages/cli/bin/clerk +++ b/packages/cli/bin/clerk @@ -2,7 +2,7 @@ "use strict"; -const { spawn } = require("node:child_process"); +const { spawnSync } = require("node:child_process"); const { platform, arch, argv, exit } = require("node:process"); // Detect musl vs glibc to select the correct prebuilt binary. @@ -47,27 +47,13 @@ try { exit(1); } -const child = spawn(binPath, argv.slice(2), { stdio: "inherit" }); - -let exited = false; - -child.on("error", (err) => { - if (exited) return; - exited = true; - console.error(`Failed to start Clerk CLI: ${err.message}`); +const { status, signal, error } = spawnSync(binPath, argv.slice(2), { stdio: "inherit" }); +if (error) { + console.error(`Failed to start Clerk CLI: ${error.message}`); exit(1); -}); - -const signals = platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"]; -for (const sig of signals) { - process.on(sig, () => { if (!exited) child.kill(sig); }); } - -child.on("close", (code, signal) => { - if (exited) return; - exited = true; - if (code !== null) exit(code); - // 128 + signal number is the conventional exit code for signal termination +if (signal) { const signum = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 }; exit(128 + (signum[signal] ?? 1)); -}); +} +exit(status ?? 1); From a187670b58cc01f5ec9f63ef13f2dbb5567dcc2e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 15:15:35 -0600 Subject: [PATCH 13/44] chore: update license year to 2026 --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 49e46cae1..daceccfbc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Clerk, Inc. +Copyright (c) 2026 Clerk, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 45abd2bfd159eaa601fad0841caefd510c31b943 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 15:28:19 -0600 Subject: [PATCH 14/44] refactor: use static import for dev version and add npm provenance Replace runtime Bun.file() path resolution with a static import of package.json so the version is embedded at compile time. This makes createProgram() synchronous and eliminates the 0.0.0-dev fallback. Also add --provenance flag to npm publish for SLSA attestation. --- packages/cli-core/src/cli-program.ts | 15 +++------------ packages/cli-core/src/cli.ts | 2 +- packages/cli-core/src/test/lib/setup.ts | 2 +- scripts/releaser/index.ts | 2 +- 4 files changed, 6 insertions(+), 15 deletions(-) diff --git a/packages/cli-core/src/cli-program.ts b/packages/cli-core/src/cli-program.ts index 90d973841..4d11b31f3 100644 --- a/packages/cli-core/src/cli-program.ts +++ b/packages/cli-core/src/cli-program.ts @@ -1,4 +1,3 @@ -import { join } from "node:path"; import { Command } from "@commander-js/extra-typings"; import { setMode, type Mode } from "./mode.ts"; import { init } from "./commands/init/index.ts"; @@ -16,21 +15,13 @@ import { unlink } from "./commands/unlink/index.ts"; import { doctor } from "./commands/doctor/index.ts"; import { CliError, UserAbortError, ApiError, EXIT_CODE, throwUsageError } from "./lib/errors.ts"; import { red } from "./lib/color.ts"; +import wrapperPkg from "../../cli/package.json"; -async function getDevVersion(): Promise { - try { - const pkg = await Bun.file(join(import.meta.dir, "..", "..", "cli", "package.json")).json(); - return `${pkg.version}-dev`; - } catch { - return "0.0.0-dev"; - } -} - -export async function createProgram() { +export function createProgram() { const program = new Command() .name("clerk") .description("Clerk CLI") - .version(typeof CLI_VERSION !== "undefined" ? CLI_VERSION : await getDevVersion()) + .version(typeof CLI_VERSION !== "undefined" ? CLI_VERSION : `${wrapperPkg.version}-dev`) .option( "--mode ", "Force interaction mode (human or agent). Defaults to auto-detect based on TTY.", diff --git a/packages/cli-core/src/cli.ts b/packages/cli-core/src/cli.ts index 2a21ee4c9..7fe65d77c 100644 --- a/packages/cli-core/src/cli.ts +++ b/packages/cli-core/src/cli.ts @@ -2,4 +2,4 @@ import { createProgram, runProgram } from "./cli-program.ts"; import { EXIT_CODE } from "./lib/errors.ts"; process.on("SIGINT", () => process.exit(EXIT_CODE.SIGINT)); -runProgram(await createProgram()); +runProgram(createProgram()); diff --git a/packages/cli-core/src/test/lib/setup.ts b/packages/cli-core/src/test/lib/setup.ts index 3d37ac229..43726c452 100644 --- a/packages/cli-core/src/test/lib/setup.ts +++ b/packages/cli-core/src/test/lib/setup.ts @@ -379,7 +379,7 @@ export interface CLIResult { async function execCLI(...args: string[]): Promise { const { createProgram, runProgram } = await import("../../cli-program.ts"); - const program = await createProgram(); + const program = createProgram(); program.exitOverride(); if (!currentHarness) { diff --git a/scripts/releaser/index.ts b/scripts/releaser/index.ts index aed0f2374..5dab15f53 100644 --- a/scripts/releaser/index.ts +++ b/scripts/releaser/index.ts @@ -79,7 +79,7 @@ async function generatePlatformPackage(target: Target, version: string): Promise } function publish(dir: string, dryRun: boolean, tag?: string): void { - const flags = ["npm", "publish", "--access", "public", "--ignore-scripts"]; + const flags = ["npm", "publish", "--access", "public", "--provenance", "--ignore-scripts"]; if (tag) flags.push("--tag", tag); if (dryRun) flags.push("--dry-run"); const result = Bun.spawnSync(flags, { cwd: dir, stdio: ["ignore", "pipe", "pipe"] }); From 2967063f02ef7020d7a700b0ac29f04a6cc47f55 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 15:46:20 -0600 Subject: [PATCH 15/44] fix: address code review findings from release infrastructure - Remove cross-package import in cli-program.ts, use static "0.0.0-dev" - Add version assertions to all smoke tests (stable, canary, snapshot) - Expand canary smoke test matrix to include darwin-arm64 - Use os.constants.signals for complete signal number mapping in shim - Add root tsconfig.json with project references for editor support - Document snapshot author_association restriction as intentional - Document test script workaround for Bun mock.module leak --- .github/workflows/release.yml | 46 ++++++++++++++++++++++------ .github/workflows/snapshot.yml | 14 +++++++-- packages/cli-core/package.json | 2 +- packages/cli-core/src/cli-program.ts | 3 +- packages/cli-core/tsconfig.json | 1 + packages/cli/bin/clerk | 4 +-- tsconfig.json | 4 +++ 7 files changed, 56 insertions(+), 18 deletions(-) create mode 100644 tsconfig.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 26e4fd05a..2f74b01c3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,7 +32,7 @@ jobs: artifact-prefix: clerk smoke-test: - needs: build + needs: [release-please, build] strategy: fail-fast: false matrix: @@ -75,12 +75,23 @@ jobs: if: ${{ !matrix.docker }} shell: bash run: | - version=$(${{ matrix.bin }} --version) - echo "Binary reports $version" + expected="${{ needs.release-please.outputs.version }}" + actual=$(${{ matrix.bin }} --version) + echo "Binary reports $actual (expected $expected)" + if [ "$actual" != "$expected" ]; then + echo "::error::Version mismatch: expected $expected, got $actual" + exit 1 + fi - name: Smoke test (docker) if: ${{ matrix.docker }} run: | - docker run --rm -v "$PWD:/work" ${{ matrix.docker }} /work/${{ matrix.bin }} --version + expected="${{ needs.release-please.outputs.version }}" + actual=$(docker run --rm -v "$PWD:/work" ${{ matrix.docker }} /work/${{ matrix.bin }} --version) + echo "Binary reports $actual (expected $expected)" + if [ "$actual" != "$expected" ]; then + echo "::error::Version mismatch: expected $expected, got $actual" + exit 1 + fi publish-npm: needs: [release-please, build, smoke-test] @@ -157,18 +168,33 @@ jobs: artifact-prefix: clerk-canary canary-smoke-test: - needs: canary-build - runs-on: ubuntu-latest + needs: [canary-version, canary-build] + strategy: + fail-fast: false + matrix: + include: + - target: darwin-arm64 + runner: macos-latest + bin: ./clerk + - target: linux-x64 + runner: ubuntu-latest + bin: ./clerk + runs-on: ${{ matrix.runner }} steps: - uses: actions/download-artifact@v4 with: - name: clerk-canary-linux-x64 + name: clerk-canary-${{ matrix.target }} - name: Make executable - run: chmod +x ./clerk + run: chmod +x ${{ matrix.bin }} - name: Smoke test run: | - version=$(./clerk --version) - echo "Binary reports $version" + expected="${{ needs.canary-version.outputs.version }}" + actual=$(${{ matrix.bin }} --version) + echo "Binary reports $actual (expected $expected)" + if [ "$actual" != "$expected" ]; then + echo "::error::Version mismatch: expected $expected, got $actual" + exit 1 + fi canary-publish: needs: [canary-version, canary-build, canary-smoke-test] diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 8b8a86a6c..924031055 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -11,6 +11,9 @@ permissions: jobs: snapshot: + # Intentionally restricted to MEMBER and OWNER — external collaborators + # (COLLABORATOR association) cannot trigger snapshots. This is a deliberate + # security boundary: snapshot publishing has npm write access via OIDC. if: >- github.event.issue.pull_request && startsWith(github.event.comment.body, '!snapshot') && @@ -62,7 +65,7 @@ jobs: artifact-prefix: clerk-snapshot smoke-test: - needs: build + needs: [snapshot, build] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 @@ -72,8 +75,13 @@ jobs: run: chmod +x ./clerk - name: Smoke test run: | - version=$(./clerk --version) - echo "Binary reports $version" + expected="${{ needs.snapshot.outputs.version }}" + actual=$(./clerk --version) + echo "Binary reports $actual (expected $expected)" + if [ "$actual" != "$expected" ]; then + echo "::error::Version mismatch: expected $expected, got $actual" + exit 1 + fi publish: needs: [snapshot, build, smoke-test] diff --git a/packages/cli-core/package.json b/packages/cli-core/package.json index 0bba16252..06bf5b445 100644 --- a/packages/cli-core/package.json +++ b/packages/cli-core/package.json @@ -7,7 +7,7 @@ "build": "bun build ./src/cli.ts --outfile ./dist/cli.js --target node --external @napi-rs/keyring", "build:compile": "bun build --compile --no-compile-autoload-dotenv ./src/cli.ts --outfile ./dist/clerk", "dev": "bun run ./src/cli.ts", - "test": "for f in $(find src -name '*.test.ts' | sort); do bun test \"$f\" || exit 1; done", + "test": "for f in $(find src -name '*.test.ts' | sort); do bun test \"$f\" || exit 1; done; # Runs each test file individually to work around Bun mock.module state leaking between files (see 1e85a13)", "lint": "oxlint src/", "format": "oxfmt --write src/", "format:check": "oxfmt --check src/" diff --git a/packages/cli-core/src/cli-program.ts b/packages/cli-core/src/cli-program.ts index 4d11b31f3..3af2487bc 100644 --- a/packages/cli-core/src/cli-program.ts +++ b/packages/cli-core/src/cli-program.ts @@ -15,13 +15,12 @@ import { unlink } from "./commands/unlink/index.ts"; import { doctor } from "./commands/doctor/index.ts"; import { CliError, UserAbortError, ApiError, EXIT_CODE, throwUsageError } from "./lib/errors.ts"; import { red } from "./lib/color.ts"; -import wrapperPkg from "../../cli/package.json"; export function createProgram() { const program = new Command() .name("clerk") .description("Clerk CLI") - .version(typeof CLI_VERSION !== "undefined" ? CLI_VERSION : `${wrapperPkg.version}-dev`) + .version(typeof CLI_VERSION !== "undefined" ? CLI_VERSION : "0.0.0-dev") .option( "--mode ", "Force interaction mode (human or agent). Defaults to auto-detect based on TTY.", diff --git a/packages/cli-core/tsconfig.json b/packages/cli-core/tsconfig.json index bfa0fead5..b63857fda 100644 --- a/packages/cli-core/tsconfig.json +++ b/packages/cli-core/tsconfig.json @@ -13,6 +13,7 @@ "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, + "composite": true, // Best practices "strict": true, diff --git a/packages/cli/bin/clerk b/packages/cli/bin/clerk index e6e03c9db..4a9413573 100755 --- a/packages/cli/bin/clerk +++ b/packages/cli/bin/clerk @@ -3,6 +3,7 @@ "use strict"; const { spawnSync } = require("node:child_process"); +const { constants } = require("node:os"); const { platform, arch, argv, exit } = require("node:process"); // Detect musl vs glibc to select the correct prebuilt binary. @@ -53,7 +54,6 @@ if (error) { exit(1); } if (signal) { - const signum = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 }; - exit(128 + (signum[signal] ?? 1)); + exit(128 + (constants.signals[signal] ?? 1)); } exit(status ?? 1); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..3001087cd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "packages/cli-core" }] +} From 7ffc7f370786a53217ebb6941c813b506a3b8050 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 16:09:50 -0600 Subject: [PATCH 16/44] fix: address code review findings from release and packaging - Replace defunct macos-13 runner with macos-15-large for darwin-x64 smoke test - Add artifact download pattern filter to stable publish/upload jobs - Add workflow concurrency control to prevent canary publish races - Include LICENSE in wrapper and platform packages for npm compliance - Check multiple ldd paths for musl detection (/usr/bin/ldd, /bin/ldd) - Bump Node engine minimum to >=20.0.0 (Node 18 is EOL) - Add linux-x64-musl to canary smoke test matrix with docker support --- .github/workflows/release.yml | 30 ++++++++++++++++++++++++++++-- packages/cli/LICENSE | 21 +++++++++++++++++++++ packages/cli/bin/clerk | 7 ++++++- packages/cli/package.json | 5 +++-- scripts/releaser/index.ts | 6 +++++- 5 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 packages/cli/LICENSE diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f74b01c3..01a58848f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,10 @@ on: push: branches: [main] +concurrency: + group: release + cancel-in-progress: false + permissions: contents: write pull-requests: write @@ -41,7 +45,7 @@ jobs: runner: macos-latest bin: ./clerk - target: darwin-x64 - runner: macos-13 + runner: macos-15-large bin: ./clerk - target: linux-arm64 runner: ubuntu-24.04-arm @@ -113,6 +117,7 @@ jobs: - uses: actions/download-artifact@v4 with: + pattern: clerk-* path: dist/artifacts - name: Publish packages @@ -128,6 +133,7 @@ jobs: steps: - uses: actions/download-artifact@v4 with: + pattern: clerk-* path: dist/artifacts - name: Upload binaries to GitHub Release @@ -167,6 +173,9 @@ jobs: ref: ${{ github.sha }} artifact-prefix: clerk-canary + # Canary smoke tests cover a subset of targets for speed/cost. Full matrix runs + # on stable releases. darwin-arm64 + linux-x64 cover the two most common dev + # platforms; musl is included since it runs on the same ubuntu-latest runner. canary-smoke-test: needs: [canary-version, canary-build] strategy: @@ -179,14 +188,21 @@ jobs: - target: linux-x64 runner: ubuntu-latest bin: ./clerk + - target: linux-x64-musl + runner: ubuntu-latest + bin: ./clerk + docker: alpine runs-on: ${{ matrix.runner }} steps: - uses: actions/download-artifact@v4 with: name: clerk-canary-${{ matrix.target }} - name: Make executable + if: ${{ !matrix.docker }} run: chmod +x ${{ matrix.bin }} - - name: Smoke test + - name: Smoke test (native) + if: ${{ !matrix.docker }} + shell: bash run: | expected="${{ needs.canary-version.outputs.version }}" actual=$(${{ matrix.bin }} --version) @@ -195,6 +211,16 @@ jobs: echo "::error::Version mismatch: expected $expected, got $actual" exit 1 fi + - name: Smoke test (docker) + if: ${{ matrix.docker }} + run: | + expected="${{ needs.canary-version.outputs.version }}" + actual=$(docker run --rm -v "$PWD:/work" ${{ matrix.docker }} /work/${{ matrix.bin }} --version) + echo "Binary reports $actual (expected $expected)" + if [ "$actual" != "$expected" ]; then + echo "::error::Version mismatch: expected $expected, got $actual" + exit 1 + fi canary-publish: needs: [canary-version, canary-build, canary-smoke-test] diff --git a/packages/cli/LICENSE b/packages/cli/LICENSE new file mode 100644 index 000000000..daceccfbc --- /dev/null +++ b/packages/cli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Clerk, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/cli/bin/clerk b/packages/cli/bin/clerk index 4a9413573..25ae691f0 100755 --- a/packages/cli/bin/clerk +++ b/packages/cli/bin/clerk @@ -13,7 +13,12 @@ function isMusl() { // On musl systems, ldd is a shell script (or the dynamic linker itself) // containing "musl". Similar to rustup's `ldd --version | grep musl`. try { - return require("fs").readFileSync("/usr/bin/ldd", "utf-8").includes("musl"); + // Check common ldd locations; on musl systems the file contains "musl". + for (const p of ["/usr/bin/ldd", "/bin/ldd"]) { + try { + if (require("fs").readFileSync(p, "utf-8").includes("musl")) return true; + } catch {} + } } catch {} // Fallback: Node's diagnostic report exposes libc info without child processes. // https://nodejs.org/api/report.html#reportgetreport diff --git a/packages/cli/package.json b/packages/cli/package.json index 00e01a49b..46bbcbcb8 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -21,12 +21,13 @@ "clerk": "./bin/clerk" }, "files": [ - "bin" + "bin", + "LICENSE" ], "scripts": { "prepublishOnly": "echo 'Use scripts/releaser instead' && exit 1" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } } diff --git a/scripts/releaser/index.ts b/scripts/releaser/index.ts index 5dab15f53..462073f9c 100644 --- a/scripts/releaser/index.ts +++ b/scripts/releaser/index.ts @@ -1,4 +1,4 @@ -import { mkdir, cp, rm, chmod } from "node:fs/promises"; +import { mkdir, cp, rm, chmod, copyFile } from "node:fs/promises"; import { join } from "node:path"; import { type Target, targets, SCOPE, PKG_PREFIX } from "./targets.ts"; @@ -75,6 +75,10 @@ async function generatePlatformPackage(target: Target, version: string): Promise } await Bun.write(join(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n"); + // Include the LICENSE file in each platform package. + const licensePath = join(import.meta.dir, "../../LICENSE"); + await copyFile(licensePath, join(dir, "LICENSE")); + return dir; } From 1b238a9fea2d7b276950280181903f26ff6b2064 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 16:55:33 -0600 Subject: [PATCH 17/44] refactor: extract smoke tests into reusable workflow and simplify tsconfig Extract inline smoke test matrices from release.yml and snapshot.yml into a shared reusable workflow (smoke-test.yml) with preset-based matrix resolution. Merge duplicate native/docker smoke test steps into a single step. Pin npm to v11 for OIDC trusted publishing. Add snapshot failure notification job. Flatten tsconfig to remove project references. --- .github/workflows/release.yml | 119 ++++--------------------------- .github/workflows/smoke-test.yml | 90 +++++++++++++++++++++++ .github/workflows/snapshot.yml | 49 ++++++++----- docs/releasing.md | 11 +-- packages/cli-core/tsconfig.json | 4 +- tsconfig.json | 29 +++++++- 6 files changed, 169 insertions(+), 133 deletions(-) create mode 100644 .github/workflows/smoke-test.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01a58848f..1127df5fa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,65 +37,11 @@ jobs: smoke-test: needs: [release-please, build] - strategy: - fail-fast: false - matrix: - include: - - target: darwin-arm64 - runner: macos-latest - bin: ./clerk - - target: darwin-x64 - runner: macos-15-large - bin: ./clerk - - target: linux-arm64 - runner: ubuntu-24.04-arm - bin: ./clerk - - target: linux-arm64-musl - runner: ubuntu-24.04-arm - bin: ./clerk - docker: alpine - - target: linux-x64 - runner: ubuntu-latest - bin: ./clerk - - target: linux-x64-musl - runner: ubuntu-latest - bin: ./clerk - docker: alpine - - target: win32-x64 - runner: windows-latest - bin: ./clerk.exe - # win32-arm64: no GitHub-hosted ARM Windows runner available — binary is - # published untested (best-effort). Build-time format verification confirms - # it is a valid PE32+/Aarch64 binary but execution is not validated. - runs-on: ${{ matrix.runner }} - steps: - - uses: actions/download-artifact@v4 - with: - name: clerk-${{ matrix.target }} - - name: Make executable - if: runner.os != 'Windows' - run: chmod +x ${{ matrix.bin }} - - name: Smoke test (native) - if: ${{ !matrix.docker }} - shell: bash - run: | - expected="${{ needs.release-please.outputs.version }}" - actual=$(${{ matrix.bin }} --version) - echo "Binary reports $actual (expected $expected)" - if [ "$actual" != "$expected" ]; then - echo "::error::Version mismatch: expected $expected, got $actual" - exit 1 - fi - - name: Smoke test (docker) - if: ${{ matrix.docker }} - run: | - expected="${{ needs.release-please.outputs.version }}" - actual=$(docker run --rm -v "$PWD:/work" ${{ matrix.docker }} /work/${{ matrix.bin }} --version) - echo "Binary reports $actual (expected $expected)" - if [ "$actual" != "$expected" ]; then - echo "::error::Version mismatch: expected $expected, got $actual" - exit 1 - fi + uses: ./.github/workflows/smoke-test.yml + with: + version: ${{ needs.release-please.outputs.version }} + artifact-prefix: clerk + preset: stable publish-npm: needs: [release-please, build, smoke-test] @@ -112,7 +58,7 @@ jobs: node-version: "22" registry-url: "https://registry.npmjs.org" - name: Upgrade npm for OIDC trusted publishing - run: npm install -g npm@latest + run: npm install -g npm@11 - run: bun install --frozen-lockfile - uses: actions/download-artifact@v4 @@ -173,54 +119,13 @@ jobs: ref: ${{ github.sha }} artifact-prefix: clerk-canary - # Canary smoke tests cover a subset of targets for speed/cost. Full matrix runs - # on stable releases. darwin-arm64 + linux-x64 cover the two most common dev - # platforms; musl is included since it runs on the same ubuntu-latest runner. canary-smoke-test: needs: [canary-version, canary-build] - strategy: - fail-fast: false - matrix: - include: - - target: darwin-arm64 - runner: macos-latest - bin: ./clerk - - target: linux-x64 - runner: ubuntu-latest - bin: ./clerk - - target: linux-x64-musl - runner: ubuntu-latest - bin: ./clerk - docker: alpine - runs-on: ${{ matrix.runner }} - steps: - - uses: actions/download-artifact@v4 - with: - name: clerk-canary-${{ matrix.target }} - - name: Make executable - if: ${{ !matrix.docker }} - run: chmod +x ${{ matrix.bin }} - - name: Smoke test (native) - if: ${{ !matrix.docker }} - shell: bash - run: | - expected="${{ needs.canary-version.outputs.version }}" - actual=$(${{ matrix.bin }} --version) - echo "Binary reports $actual (expected $expected)" - if [ "$actual" != "$expected" ]; then - echo "::error::Version mismatch: expected $expected, got $actual" - exit 1 - fi - - name: Smoke test (docker) - if: ${{ matrix.docker }} - run: | - expected="${{ needs.canary-version.outputs.version }}" - actual=$(docker run --rm -v "$PWD:/work" ${{ matrix.docker }} /work/${{ matrix.bin }} --version) - echo "Binary reports $actual (expected $expected)" - if [ "$actual" != "$expected" ]; then - echo "::error::Version mismatch: expected $expected, got $actual" - exit 1 - fi + uses: ./.github/workflows/smoke-test.yml + with: + version: ${{ needs.canary-version.outputs.version }} + artifact-prefix: clerk-canary + preset: canary canary-publish: needs: [canary-version, canary-build, canary-smoke-test] @@ -237,7 +142,7 @@ jobs: node-version: "22" registry-url: "https://registry.npmjs.org" - name: Upgrade npm for OIDC trusted publishing - run: npm install -g npm@latest + run: npm install -g npm@11 - run: bun install --frozen-lockfile - uses: actions/download-artifact@v4 diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml new file mode 100644 index 000000000..d5e421e9d --- /dev/null +++ b/.github/workflows/smoke-test.yml @@ -0,0 +1,90 @@ +name: Smoke Test + +on: + workflow_call: + inputs: + version: + required: true + type: string + artifact-prefix: + required: true + type: string + preset: + required: true + type: string + description: > + Matrix preset name. One of: stable (all 7 testable targets), + canary (darwin-arm64 + linux-x64 + linux-x64-musl), or + snapshot (linux-x64 only). + +jobs: + resolve-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.resolve.outputs.matrix }} + steps: + - id: resolve + run: | + case "${{ inputs.preset }}" in + stable) + # All testable targets. win32-arm64 is excluded — no GitHub-hosted + # ARM Windows runner available. Build-time format verification + # confirms it is a valid PE32+/Aarch64 binary. + matrix='[ + {"target":"darwin-arm64","runner":"macos-latest","bin":"./clerk"}, + {"target":"darwin-x64","runner":"macos-15-large","bin":"./clerk"}, + {"target":"linux-arm64","runner":"ubuntu-24.04-arm","bin":"./clerk"}, + {"target":"linux-arm64-musl","runner":"ubuntu-24.04-arm","bin":"./clerk","docker":"alpine"}, + {"target":"linux-x64","runner":"ubuntu-latest","bin":"./clerk"}, + {"target":"linux-x64-musl","runner":"ubuntu-latest","bin":"./clerk","docker":"alpine"}, + {"target":"win32-x64","runner":"windows-latest","bin":"./clerk.exe"} + ]' + ;; + canary) + # Subset for speed/cost. darwin-arm64 + linux-x64 cover the two + # most common dev platforms; musl is included since it runs on the + # same ubuntu-latest runner. + matrix='[ + {"target":"darwin-arm64","runner":"macos-latest","bin":"./clerk"}, + {"target":"linux-x64","runner":"ubuntu-latest","bin":"./clerk"}, + {"target":"linux-x64-musl","runner":"ubuntu-latest","bin":"./clerk","docker":"alpine"} + ]' + ;; + snapshot) + matrix='[{"target":"linux-x64","runner":"ubuntu-latest","bin":"./clerk"}]' + ;; + *) + echo "::error::Unknown preset: ${{ inputs.preset }}" + exit 1 + ;; + esac + echo "matrix=${matrix}" >> "$GITHUB_OUTPUT" + + smoke-test: + needs: resolve-matrix + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.resolve-matrix.outputs.matrix) }} + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/download-artifact@v4 + with: + name: ${{ inputs.artifact-prefix }}-${{ matrix.target }} + - name: Make executable + if: runner.os != 'Windows' + run: chmod +x ${{ matrix.bin }} + - name: Smoke test + shell: bash + run: | + expected="${{ inputs.version }}" + if [ -n "${{ matrix.docker }}" ]; then + actual=$(docker run --rm -v "$PWD:/work" ${{ matrix.docker }} /work/${{ matrix.bin }} --version) + else + actual=$(${{ matrix.bin }} --version) + fi + echo "Binary reports $actual (expected $expected)" + if [ "$actual" != "$expected" ]; then + echo "::error::Version mismatch: expected $expected, got $actual" + exit 1 + fi diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 924031055..388a512a5 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -66,22 +66,11 @@ jobs: smoke-test: needs: [snapshot, build] - runs-on: ubuntu-latest - steps: - - uses: actions/download-artifact@v4 - with: - name: clerk-snapshot-linux-x64 - - name: Make executable - run: chmod +x ./clerk - - name: Smoke test - run: | - expected="${{ needs.snapshot.outputs.version }}" - actual=$(./clerk --version) - echo "Binary reports $actual (expected $expected)" - if [ "$actual" != "$expected" ]; then - echo "::error::Version mismatch: expected $expected, got $actual" - exit 1 - fi + uses: ./.github/workflows/smoke-test.yml + with: + version: ${{ needs.snapshot.outputs.version }} + artifact-prefix: clerk-snapshot + preset: snapshot publish: needs: [snapshot, build, smoke-test] @@ -101,7 +90,7 @@ jobs: node-version: "22" registry-url: "https://registry.npmjs.org" - name: Upgrade npm for OIDC trusted publishing - run: npm install -g npm@latest + run: npm install -g npm@11 - run: bun install --frozen-lockfile - uses: actions/download-artifact@v4 @@ -153,3 +142,29 @@ npm install -g @clerk/cli@${SNAPSHOT_VERSION} > Published from ${short_sha} EOF )" + + notify-failure: + needs: [snapshot, build, smoke-test, publish] + if: failure() + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: React with confused emoji + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + gh api "repos/${GH_REPO}/issues/comments/${{ github.event.comment.id }}/reactions" \ + -f content=confused --silent + + - name: Post failure comment + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + gh pr comment "${{ github.event.issue.number }}" --repo "${GH_REPO}" --body \ + "## Snapshot failed + + The snapshot publish workflow failed. [View the workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details." diff --git a/docs/releasing.md b/docs/releasing.md index 4a2a0fda4..535243b80 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -13,7 +13,7 @@ push to main → smoke-test job: verify binaries on native runners → publish-npm: generate platform packages + publish wrapper → upload-github-assets: attach binaries to the GitHub Release - → (if no release created) canary job: build + publish @canary + → (if no release created) canary job: build + smoke-test subset + publish @canary PR comment "!snapshot [name]" → build job: cross-compile binaries from PR branch @@ -40,7 +40,7 @@ Install: `npm install -g @clerk/cli` ### Canary (`@canary`) -Published automatically on every push to `main` that does **not** trigger a stable release. Canary versions use the format `x.y.z-canary.` (e.g., `0.0.1-canary.abc1234`). A single linux-x64 smoke test runs before publishing. +Published automatically on every push to `main` that does **not** trigger a stable release. Canary versions use the format `x.y.z-canary.` (e.g., `0.0.1-canary.abc1234`). A subset of smoke tests (darwin-arm64, linux-x64, linux-x64-musl) runs before publishing. Install: `npm install -g @clerk/cli@canary` @@ -74,7 +74,7 @@ Defined in [`.github/workflows/build-binaries.yml`](../.github/workflows/build-b ### 2. Smoke Test Job (matrix) -Downloads each compiled binary and runs `--version` to verify the binary actually executes. glibc targets run natively on a platform-matched GitHub-hosted runner; musl targets run inside an Alpine Docker container on a Linux runner. The target-to-runner mapping is defined in the `smoke-test` matrix in [`.github/workflows/release.yml`](../.github/workflows/release.yml). +Downloads each compiled binary and runs `--version` to verify the binary actually executes. Smoke testing is handled by a reusable workflow (`.github/workflows/smoke-test.yml`) shared across stable, canary, and snapshot pipelines. Each caller passes a preset name (`stable`, `canary`, or `snapshot`); the reusable workflow resolves the preset to a target matrix internally. glibc targets run natively on a platform-matched GitHub-hosted runner; musl targets run inside an Alpine Docker container on a Linux runner. Not all targets have a native runner available. `win32-arm64` is published as best-effort — the build job verifies it is a valid PE32+/Aarch64 binary via `file` output, but no execution-level smoke test runs because there is no GitHub-hosted ARM Windows runner. @@ -126,6 +126,7 @@ Attaches the compiled binaries to the GitHub Release for direct download. Binari | `scripts/releaser/index.ts` | Generates platform packages and publishes everything to npm | | `scripts/releaser/targets.ts` | Target definitions — must be kept in sync with the workflow matrix | | `.github/workflows/build-binaries.yml` | Reusable workflow for cross-compiling binaries (called by release + snapshot) | +| `.github/workflows/smoke-test.yml` | Reusable workflow for smoke-testing binaries (called by release + snapshot) | | `.github/workflows/release.yml` | GitHub Actions release + canary workflow | | `.github/workflows/snapshot.yml` | GitHub Actions snapshot workflow (triggered by PR comments) | @@ -135,9 +136,9 @@ The target list exists in these places that must stay in sync: 1. `scripts/releaser/targets.ts` — used by the releaser to generate platform packages 2. `.github/workflows/build-binaries.yml` build matrix — compiles binaries (maps target names to Bun cross-compile targets, e.g., `win32-x64` → `bun-windows-x64`) -3. `.github/workflows/release.yml` smoke-test matrix — verifies binaries on native runners +3. `.github/workflows/smoke-test.yml` preset definitions — defines the target matrix for each preset (`stable`, `canary`, `snapshot`) -If you add or remove a target, update all of these. Note that the smoke-test matrix may not cover every target if a native runner isn't available (e.g., `win32-arm64`). +If you add or remove a target, update all of these. Note that the smoke-test presets may not cover every target if a native runner isn't available (e.g., `win32-arm64`). ## Local Development diff --git a/packages/cli-core/tsconfig.json b/packages/cli-core/tsconfig.json index b63857fda..190ca9268 100644 --- a/packages/cli-core/tsconfig.json +++ b/packages/cli-core/tsconfig.json @@ -13,7 +13,6 @@ "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, - "composite": true, // Best practices "strict": true, @@ -26,5 +25,6 @@ "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false - } + }, + "include": ["src"] } diff --git a/tsconfig.json b/tsconfig.json index 3001087cd..bfa0fead5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,29 @@ { - "files": [], - "references": [{ "path": "packages/cli-core" }] + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } } From 3f52d84108d21d632dc56d96fb5cc60c768153db Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 17:14:42 -0600 Subject: [PATCH 18/44] fix: move typescript from peerDependencies to devDependencies A private workspace root should not declare peerDependencies since it is never consumed as a dependency by another package. --- bun.lock | 2 -- package.json | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/bun.lock b/bun.lock index c3bd47b93..81110f5a9 100644 --- a/bun.lock +++ b/bun.lock @@ -9,8 +9,6 @@ "nano-staged": "^0.9.0", "oxfmt": "^0.36.0", "oxlint": "^1.51.0", - }, - "peerDependencies": { "typescript": "^5", }, }, diff --git a/package.json b/package.json index 0f7134bf6..b2aaf3465 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,7 @@ "@types/bun": "^1.3.9", "nano-staged": "^0.9.0", "oxfmt": "^0.36.0", - "oxlint": "^1.51.0" - }, - "peerDependencies": { + "oxlint": "^1.51.0", "typescript": "^5" }, "nano-staged": { From 975ab13281f2f86f77ae9ca4b36f96f2f096fabe Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 17:24:30 -0600 Subject: [PATCH 19/44] fix: add job timeouts, set tag format, and reduce artifact retention - Add timeout-minutes to all 13 jobs across 5 workflow files to prevent stuck builds from consuming 6 hours of runner time - Set include-component-in-tag to false in release-please config so tags use clean v0.x.x format instead of cli-v0.x.x - Set artifact retention-days to 1 since build artifacts are consumed immediately by downstream jobs within the same workflow run --- .github/workflows/build-binaries.yml | 2 ++ .github/workflows/ci.yml | 2 ++ .github/workflows/release.yml | 5 +++++ .github/workflows/smoke-test.yml | 2 ++ .github/workflows/snapshot.yml | 3 +++ release-please-config.json | 1 + 6 files changed, 15 insertions(+) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 33c058541..192396c67 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -17,6 +17,7 @@ on: jobs: build: runs-on: blacksmith-2vcpu-ubuntu-2404 + timeout-minutes: 10 strategy: fail-fast: false matrix: @@ -85,3 +86,4 @@ jobs: with: name: ${{ inputs.artifact-prefix }}-${{ matrix.target }} path: clerk${{ matrix.ext }} + retention-days: 1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09d737b8e..b6cf4adea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: jobs: lint: runs-on: blacksmith-2vcpu-ubuntu-2404 + timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 @@ -17,6 +18,7 @@ jobs: test: runs-on: blacksmith-2vcpu-ubuntu-2404 + timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1127df5fa..7acb0f66a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,7 @@ permissions: jobs: release-please: runs-on: blacksmith-2vcpu-ubuntu-2404 + timeout-minutes: 5 outputs: release_created: ${{ steps.release.outputs['packages/cli--release_created'] }} tag_name: ${{ steps.release.outputs['packages/cli--tag_name'] }} @@ -47,6 +48,7 @@ jobs: needs: [release-please, build, smoke-test] # Must run on GitHub-hosted runner for npm OIDC trusted publishing runs-on: ubuntu-latest + timeout-minutes: 15 permissions: contents: read id-token: write @@ -74,6 +76,7 @@ jobs: upload-github-assets: needs: [release-please, build, smoke-test, publish-npm] runs-on: ubuntu-latest + timeout-minutes: 10 permissions: contents: write steps: @@ -100,6 +103,7 @@ jobs: needs: release-please if: ${{ needs.release-please.outputs.release_created != 'true' }} runs-on: blacksmith-2vcpu-ubuntu-2404 + timeout-minutes: 5 outputs: version: ${{ steps.version.outputs.version }} steps: @@ -131,6 +135,7 @@ jobs: needs: [canary-version, canary-build, canary-smoke-test] # Must run on GitHub-hosted runner for npm OIDC trusted publishing runs-on: ubuntu-latest + timeout-minutes: 15 permissions: contents: read id-token: write diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index d5e421e9d..15b751dc1 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -20,6 +20,7 @@ on: jobs: resolve-matrix: runs-on: ubuntu-latest + timeout-minutes: 5 outputs: matrix: ${{ steps.resolve.outputs.matrix }} steps: @@ -67,6 +68,7 @@ jobs: matrix: include: ${{ fromJSON(needs.resolve-matrix.outputs.matrix) }} runs-on: ${{ matrix.runner }} + timeout-minutes: 10 steps: - uses: actions/download-artifact@v4 with: diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 388a512a5..24ec6ec12 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -19,6 +19,7 @@ jobs: startsWith(github.event.comment.body, '!snapshot') && contains(fromJSON('["MEMBER","OWNER"]'), github.event.comment.author_association) runs-on: blacksmith-2vcpu-ubuntu-2404 + timeout-minutes: 5 outputs: version: ${{ steps.version.outputs.version }} sha: ${{ steps.pr.outputs.sha }} @@ -76,6 +77,7 @@ jobs: needs: [snapshot, build, smoke-test] # Must run on GitHub-hosted runner for npm OIDC trusted publishing runs-on: ubuntu-latest + timeout-minutes: 15 permissions: contents: read pull-requests: write @@ -147,6 +149,7 @@ EOF needs: [snapshot, build, smoke-test, publish] if: failure() runs-on: ubuntu-latest + timeout-minutes: 5 permissions: contents: read pull-requests: write diff --git a/release-please-config.json b/release-please-config.json index 608891d33..d86d6d95c 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,6 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", "release-type": "node", + "include-component-in-tag": false, "packages": { "packages/cli": {} } From 1a82814a7ed26499d8d269781c7f2568c808fad6 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 17:45:23 -0600 Subject: [PATCH 20/44] docs: update CLAUDE.md paths for monorepo structure --- CLAUDE.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 27593e8f9..807668a61 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -124,7 +124,7 @@ CI runs `bun run format:check` (fails if unformatted), `bun run lint`, and `bun ## Commands -Every CLI command lives in its own directory under `src/commands//`. Each directory must contain a `README.md` that documents: +Every CLI command lives in its own directory under `packages/cli-core/src/commands//`. Each directory must contain a `README.md` that documents: - What the command does - Usage and options @@ -133,15 +133,15 @@ Every CLI command lives in its own directory under `src/commands//`. Each When adding a new command, create its directory and README. When modifying a command's behavior, options, or API calls, update its README to match. -When creating or modifying a command, evaluate whether it needs an agent mode. Commands with interactive prompts (menus, wizards, multi-step flows) should check `isAgent()` from `src/mode.ts` and, when in agent mode, output a structured prompt that an AI agent can follow instead of running the interactive flow. Commands that are already non-interactive (e.g., single API calls, browser-based OAuth) typically don't need agent mode. +When creating or modifying a command, evaluate whether it needs an agent mode. Commands with interactive prompts (menus, wizards, multi-step flows) should check `isAgent()` from `packages/cli-core/src/mode.ts` and, when in agent mode, output a structured prompt that an AI agent can follow instead of running the interactive flow. Commands that are already non-interactive (e.g., single API calls, browser-based OAuth) typically don't need agent mode. ### Root README -`README.md` at the project root contains the CLI help output. When commands are added, removed, or their options change, update the help output in `README.md` to stay in sync. You can regenerate it by running `bun run src/cli.ts --help`. +`README.md` at the project root contains the CLI help output. When commands are added, removed, or their options change, update the help output in `README.md` to stay in sync. You can regenerate it by running `bun run dev -- --help`. ## Error Handling -All error classes and helpers live in `src/lib/errors.ts`. The global error handler in `src/cli.ts` catches thrown errors and formats them for the user. **Never call `console.error` + `process.exit` directly in commands** — throw an error instead and let the global handler deal with output and exit codes. +All error classes and helpers live in `packages/cli-core/src/lib/errors.ts`. The global error handler in `packages/cli-core/src/cli.ts` catches thrown errors and formats them for the user. **Never call `console.error` + `process.exit` directly in commands** — throw an error instead and let the global handler deal with output and exit codes. ### Known failures — `CliError` @@ -196,4 +196,4 @@ const config = await withApiContext( ### API error classes -`BapiError` and `PlapiError` (both extend `ApiError`) are thrown by the API helpers in `src/commands/api/bapi.ts` and `src/lib/plapi.ts` respectively. Don't construct these in commands — they're thrown automatically by the fetch wrappers. Use `withApiContext` to add context when calling those helpers. +`BapiError` and `PlapiError` (both extend `ApiError`) are thrown by the API helpers in `packages/cli-core/src/commands/api/bapi.ts` and `packages/cli-core/src/lib/plapi.ts` respectively. Don't construct these in commands — they're thrown automatically by the fetch wrappers. Use `withApiContext` to add context when calling those helpers. From 9b0508d12990f754717f245579b7de756280d5fc Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 13 Mar 2026 17:56:14 -0600 Subject: [PATCH 21/44] fix: harden release scripts and update CLAUDE.md documentation - Validate --tag and --version args in releaser to prevent silent undefined - Guard base version extraction in canary/snapshot workflows against malformed manifest - Remove redundant outer try/catch in shim's isMusl() detection - Add project structure, versioning, and releasing docs sections to CLAUDE.md --- .github/workflows/release.yml | 4 ++++ .github/workflows/snapshot.yml | 4 ++++ CLAUDE.md | 16 ++++++++++++++++ packages/cli/bin/clerk | 14 ++++++-------- scripts/releaser/index.ts | 2 ++ 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7acb0f66a..93b6ec70f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -112,6 +112,10 @@ jobs: id: version run: | base=$(jq -r '.["packages/cli"]' .release-please-manifest.json) + if [ -z "$base" ] || [ "$base" = "null" ]; then + echo "::error::Could not read base version from .release-please-manifest.json" + exit 1 + fi sha=$(git rev-parse --short=7 HEAD) echo "version=${base}-canary.${sha}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 24ec6ec12..760c42393 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -54,6 +54,10 @@ jobs: name=$(echo "$COMMENT_BODY" | sed 's/^!snapshot[[:space:]]*//' | tr -cd 'a-zA-Z0-9-') if [ -z "$name" ]; then name="snapshot"; fi base=$(jq -r '.["packages/cli"]' .release-please-manifest.json) + if [ -z "$base" ] || [ "$base" = "null" ]; then + echo "::error::Could not read base version from .release-please-manifest.json" + exit 1 + fi datetime=$(date -u +%Y%m%d%H%M%S) echo "version=${base}-${name}.v${datetime}" >> "$GITHUB_OUTPUT" diff --git a/CLAUDE.md b/CLAUDE.md index 807668a61..5f9f635c4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,6 +4,18 @@ globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json" alwaysApply: false --- +## Project Structure + +This is a Bun workspace monorepo: + +- `packages/cli-core/` — CLI source code, commands, and tests +- `packages/cli/` — npm wrapper package with platform binary shim (not run directly during development; do not add command logic here) +- `scripts/releaser/` — release publishing script that generates platform packages and publishes to npm + +See [docs/releasing.md](docs/releasing.md) for the full release flow, channels, and safeguards. + +## Bun + Default to using Bun instead of Node.js. - Use `bun ` instead of `node ` or `ts-node ` @@ -122,6 +134,10 @@ bun test # Run all tests CI runs `bun run format:check` (fails if unformatted), `bun run lint`, and `bun test` on every PR to `main`. +## Versioning + +The `CLI_VERSION` global is injected at compile time via `bun build --compile --define "CLI_VERSION=..."`. Local `build:compile` omits it, so the binary reports `0.0.0-dev`. The CI release workflow injects the real version. + ## Commands Every CLI command lives in its own directory under `packages/cli-core/src/commands//`. Each directory must contain a `README.md` that documents: diff --git a/packages/cli/bin/clerk b/packages/cli/bin/clerk index 25ae691f0..24b0d62c0 100755 --- a/packages/cli/bin/clerk +++ b/packages/cli/bin/clerk @@ -12,14 +12,12 @@ function isMusl() { if (platform !== "linux") return false; // On musl systems, ldd is a shell script (or the dynamic linker itself) // containing "musl". Similar to rustup's `ldd --version | grep musl`. - try { - // Check common ldd locations; on musl systems the file contains "musl". - for (const p of ["/usr/bin/ldd", "/bin/ldd"]) { - try { - if (require("fs").readFileSync(p, "utf-8").includes("musl")) return true; - } catch {} - } - } catch {} + // Check common ldd locations; on musl systems the file contains "musl". + for (const p of ["/usr/bin/ldd", "/bin/ldd"]) { + try { + if (require("fs").readFileSync(p, "utf-8").includes("musl")) return true; + } catch {} + } // Fallback: Node's diagnostic report exposes libc info without child processes. // https://nodejs.org/api/report.html#reportgetreport try { diff --git a/scripts/releaser/index.ts b/scripts/releaser/index.ts index 462073f9c..cad19fab1 100644 --- a/scripts/releaser/index.ts +++ b/scripts/releaser/index.ts @@ -12,9 +12,11 @@ function parseArgs(): { dryRun: boolean; tag?: string; versionOverride?: string const tagIdx = args.indexOf("--tag"); const tag = tagIdx !== -1 ? args[tagIdx + 1] : undefined; + if (tagIdx !== -1 && !tag) throw new Error("--tag requires a value"); const versionIdx = args.indexOf("--version"); const versionOverride = versionIdx !== -1 ? args[versionIdx + 1] : undefined; + if (versionIdx !== -1 && !versionOverride) throw new Error("--version requires a value"); return { dryRun, tag, versionOverride }; } From 27df13e216df69f4c709824ba69b23f8dd554034 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 16 Mar 2026 12:38:22 -0600 Subject: [PATCH 22/44] fix: update bun.lock and fix errorSpy reference after rebase --- bun.lock | 3 +++ packages/cli-core/src/commands/link/index.test.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index 81110f5a9..c48c9bbf5 100644 --- a/bun.lock +++ b/bun.lock @@ -23,6 +23,7 @@ "name": "@clerk/cli-core", "version": "0.0.0", "dependencies": { + "@commander-js/extra-typings": "^14.0.0", "@inquirer/prompts": "^8.2.0", "@napi-rs/keyring": "^1.2.0", "commander": "^14.0.3", @@ -36,6 +37,8 @@ "@clerk/cli-core": ["@clerk/cli-core@workspace:packages/cli-core"], + "@commander-js/extra-typings": ["@commander-js/extra-typings@14.0.0", "", { "peerDependencies": { "commander": "~14.0.0" } }, "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg=="], + "@inquirer/ansi": ["@inquirer/ansi@2.0.3", "", {}, "sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw=="], "@inquirer/checkbox": ["@inquirer/checkbox@5.1.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/core": "^11.1.5", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-/HjF1LN0a1h4/OFsbGKHNDtWICFU/dqXCdym719HFTyJo9IG7Otr+ziGWc9S0iQuohRZllh+WprSgd5UW5Fw0g=="], diff --git a/packages/cli-core/src/commands/link/index.test.ts b/packages/cli-core/src/commands/link/index.test.ts index 71dcaedf4..52d89f67d 100644 --- a/packages/cli-core/src/commands/link/index.test.ts +++ b/packages/cli-core/src/commands/link/index.test.ts @@ -698,7 +698,7 @@ describe("link", () => { profile: { workspaceId: "", appId: "app_detected", instances: { development: "ins_1" } }, }); consoleSpy = spyOn(console, "log").mockImplementation(() => {}); - errorSpy = spyOn(console, "error").mockImplementation(() => {}); + const errorSpy = spyOn(console, "error").mockImplementation(() => {}); await link({ skipIfLinked: true }); From 8749cace6ea1ee4f5567bba514f39856e8bd2d73 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 17 Mar 2026 10:43:30 -0600 Subject: [PATCH 23/44] refactor: rename npm wrapper package from @clerk/cli to clerk Platform-specific packages (@clerk/cli-{target}) remain unchanged under the @clerk scope. --- .github/workflows/snapshot.yml | 4 ++-- README.md | 2 +- bun.lock | 6 +++--- docs/releasing.md | 10 +++++----- packages/cli/bin/clerk | 2 +- packages/cli/package.json | 2 +- scripts/releaser/index.ts | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 760c42393..9d465e29b 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -138,12 +138,12 @@ jobs: ## Snapshot published \`\`\`sh -npm install -g @clerk/cli@${SNAPSHOT_VERSION} +npm install -g clerk@${SNAPSHOT_VERSION} \`\`\` | Package | Version | |---------|---------| -| \`@clerk/cli\` | \`${SNAPSHOT_VERSION}\` | +| \`clerk\` | \`${SNAPSHOT_VERSION}\` | > Published from ${short_sha} EOF diff --git a/README.md b/README.md index 6f5dfec97..2df9f28e6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# @clerk/cli +# clerk The Clerk command-line interface. diff --git a/bun.lock b/bun.lock index c48c9bbf5..0d79081a3 100644 --- a/bun.lock +++ b/bun.lock @@ -13,7 +13,7 @@ }, }, "packages/cli": { - "name": "@clerk/cli", + "name": "clerk", "version": "0.0.1", "bin": { "clerk": "./bin/clerk", @@ -33,8 +33,6 @@ }, }, "packages": { - "@clerk/cli": ["@clerk/cli@workspace:packages/cli"], - "@clerk/cli-core": ["@clerk/cli-core@workspace:packages/cli-core"], "@commander-js/extra-typings": ["@commander-js/extra-typings@14.0.0", "", { "peerDependencies": { "commander": "~14.0.0" } }, "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg=="], @@ -181,6 +179,8 @@ "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + "clerk": ["clerk@workspace:packages/cli"], + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], diff --git a/docs/releasing.md b/docs/releasing.md index 535243b80..8b23422b6 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -24,9 +24,9 @@ PR comment "!snapshot [name]" ## Architecture -The CLI is distributed as an npm wrapper package (`@clerk/cli`) plus one platform-specific package per target (e.g., `@clerk/cli-darwin-arm64`). The full list of platform targets is defined in [`scripts/releaser/targets.ts`](../scripts/releaser/targets.ts). +The CLI is distributed as an npm wrapper package (`clerk`) plus one platform-specific package per target (e.g., `@clerk/cli-darwin-arm64`). The full list of platform targets is defined in [`scripts/releaser/targets.ts`](../scripts/releaser/targets.ts). -When a user runs `npm install -g @clerk/cli`, npm installs the wrapper plus the matching platform package via `optionalDependencies`. The wrapper's `bin/clerk` shim resolves the binary from the platform package using `require.resolve()`. +When a user runs `npm install -g clerk`, npm installs the wrapper plus the matching platform package via `optionalDependencies`. The wrapper's `bin/clerk` shim resolves the binary from the platform package using `require.resolve()`. Target names follow Node.js's `${process.platform}-${process.arch}` convention so the shim can derive package names without a lookup table. @@ -36,19 +36,19 @@ Target names follow Node.js's `${process.platform}-${process.arch}` convention s Published when a release-please version PR is merged. Includes full smoke testing on native runners before publishing. Binaries are also attached to the GitHub Release. -Install: `npm install -g @clerk/cli` +Install: `npm install -g clerk` ### Canary (`@canary`) Published automatically on every push to `main` that does **not** trigger a stable release. Canary versions use the format `x.y.z-canary.` (e.g., `0.0.1-canary.abc1234`). A subset of smoke tests (darwin-arm64, linux-x64, linux-x64-musl) runs before publishing. -Install: `npm install -g @clerk/cli@canary` +Install: `npm install -g clerk@canary` ### Snapshot (`@snapshot`) Published on-demand from PR branches by commenting `!snapshot` (or `!snapshot `) on a pull request. The commenter must be a member or owner of the repository's organization. Snapshot versions use the format `x.y.z-.v` (e.g., `0.0.1-snapshot.v20260313145959` or `0.0.1-my-feature.v20260313145959`). The datetime format ensures multiple snapshots from the same PR sort monotonically in semver. -Install: `npm install -g @clerk/cli@` (version is posted as a PR comment after publishing) +Install: `npm install -g clerk@` (version is posted as a PR comment after publishing) ## Versioning diff --git a/packages/cli/bin/clerk b/packages/cli/bin/clerk index 24b0d62c0..1b2d1d50d 100755 --- a/packages/cli/bin/clerk +++ b/packages/cli/bin/clerk @@ -46,7 +46,7 @@ try { console.error( `Unsupported or missing platform: ${key}\n` + `Expected package: ${pkg}\n` + - `Try reinstalling with: npm install -g @clerk/cli`, + `Try reinstalling with: npm install -g clerk`, ); exit(1); } diff --git a/packages/cli/package.json b/packages/cli/package.json index 46bbcbcb8..02055657b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,5 +1,5 @@ { - "name": "@clerk/cli", + "name": "clerk", "version": "0.0.1", "private": true, "description": "Clerk CLI", diff --git a/scripts/releaser/index.ts b/scripts/releaser/index.ts index cad19fab1..c866b8e03 100644 --- a/scripts/releaser/index.ts +++ b/scripts/releaser/index.ts @@ -127,7 +127,7 @@ try { delete wrapperPkg.private; await Bun.write(WRAPPER_PKG_PATH, JSON.stringify(wrapperPkg, null, 2) + "\n"); - const wrapperName = `${SCOPE}/${PKG_PREFIX}`; + const wrapperName = "clerk"; if (isPublished(wrapperName, version)) { console.log(`Skipping ${wrapperName}@${version} (already published)`); } else { From d7994507699c979ad64974a8a0c0c56fa481d4a3 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 11:01:01 -0600 Subject: [PATCH 24/44] fix: remove unused errorSpy variable in link test --- packages/cli-core/src/commands/link/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli-core/src/commands/link/index.test.ts b/packages/cli-core/src/commands/link/index.test.ts index 52d89f67d..343acf45c 100644 --- a/packages/cli-core/src/commands/link/index.test.ts +++ b/packages/cli-core/src/commands/link/index.test.ts @@ -698,7 +698,7 @@ describe("link", () => { profile: { workspaceId: "", appId: "app_detected", instances: { development: "ins_1" } }, }); consoleSpy = spyOn(console, "log").mockImplementation(() => {}); - const errorSpy = spyOn(console, "error").mockImplementation(() => {}); + spyOn(console, "error").mockImplementation(() => {}); await link({ skipIfLinked: true }); From 0599b1ecdf9f7925c9ec5dfd863dec72c29f4c3b Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 11:38:50 -0600 Subject: [PATCH 25/44] ci: narrow workflow permissions and fix multiline output - release.yml: reduce workflow-level permissions to contents:read, move write scopes to the release-please job that actually needs them - snapshot.yml: remove workflow-level id-token:write (only the publish job needs it, and it already declares it) - smoke-test.yml: use heredoc syntax for multiline GITHUB_OUTPUT to prevent truncation of the matrix JSON at the first newline --- .github/workflows/release.yml | 7 ++++--- .github/workflows/smoke-test.yml | 6 +++++- .github/workflows/snapshot.yml | 1 - 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93b6ec70f..36f424bd0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,14 +9,15 @@ concurrency: cancel-in-progress: false permissions: - contents: write - pull-requests: write - id-token: write + contents: read jobs: release-please: runs-on: blacksmith-2vcpu-ubuntu-2404 timeout-minutes: 5 + permissions: + contents: write + pull-requests: write outputs: release_created: ${{ steps.release.outputs['packages/cli--release_created'] }} tag_name: ${{ steps.release.outputs['packages/cli--tag_name'] }} diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index 15b751dc1..58f177d61 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -59,7 +59,11 @@ jobs: exit 1 ;; esac - echo "matrix=${matrix}" >> "$GITHUB_OUTPUT" + { + echo "matrix<> "$GITHUB_OUTPUT" smoke-test: needs: resolve-matrix diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 9d465e29b..24ce8a418 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -7,7 +7,6 @@ on: permissions: contents: read pull-requests: write - id-token: write jobs: snapshot: From fa6d7f9b5bb2e84775c485f4ab974423918bd880 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 11:45:28 -0600 Subject: [PATCH 26/44] fix(ci): remove unintended leading spaces in snapshot failure comment --- .github/workflows/snapshot.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 24ce8a418..b0a616441 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -170,7 +170,6 @@ EOF GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} run: | - gh pr comment "${{ github.event.issue.number }}" --repo "${GH_REPO}" --body \ - "## Snapshot failed + gh pr comment "${{ github.event.issue.number }}" --repo "${GH_REPO}" --body "## Snapshot failed The snapshot publish workflow failed. [View the workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details." From cebde2cdc5e85bd84d57169cb21c893ad3414ffa Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:08:55 -0600 Subject: [PATCH 27/44] refactor: add bunTarget and ext fields to target definitions --- scripts/releaser/index.ts | 2 +- scripts/releaser/targets.ts | 46 ++++++++++++++++++++++++++++++------- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/scripts/releaser/index.ts b/scripts/releaser/index.ts index c866b8e03..e3b648641 100644 --- a/scripts/releaser/index.ts +++ b/scripts/releaser/index.ts @@ -53,7 +53,7 @@ async function generatePlatformPackage(target: Target, version: string): Promise await mkdir(binDir, { recursive: true }); - const ext = target.os === "win32" ? ".exe" : ""; + const ext = target.ext; const binaryName = `clerk${ext}`; const artifactPath = join(ARTIFACTS_DIR, `clerk-${target.name}`, binaryName); const destPath = join(binDir, binaryName); diff --git a/scripts/releaser/targets.ts b/scripts/releaser/targets.ts index 37084786c..e8d2158ef 100644 --- a/scripts/releaser/targets.ts +++ b/scripts/releaser/targets.ts @@ -1,22 +1,52 @@ export interface Target { name: string; + bunTarget: string; os: string; cpu: string; libc?: string; + ext: string; } // Target names use Node.js ${process.platform}-${process.arch} convention so the wrapper // shim (packages/cli/bin/clerk) can derive package names without a lookup table. // Keep in sync with .github/workflows/build-binaries.yml matrix. export const targets: Target[] = [ - { name: "darwin-arm64", os: "darwin", cpu: "arm64" }, - { name: "darwin-x64", os: "darwin", cpu: "x64" }, - { name: "linux-arm64", os: "linux", cpu: "arm64", libc: "glibc" }, - { name: "linux-arm64-musl", os: "linux", cpu: "arm64", libc: "musl" }, - { name: "linux-x64", os: "linux", cpu: "x64", libc: "glibc" }, - { name: "linux-x64-musl", os: "linux", cpu: "x64", libc: "musl" }, - { name: "win32-arm64", os: "win32", cpu: "arm64" }, - { name: "win32-x64", os: "win32", cpu: "x64" }, + { name: "darwin-arm64", bunTarget: "bun-darwin-arm64", os: "darwin", cpu: "arm64", ext: "" }, + { name: "darwin-x64", bunTarget: "bun-darwin-x64", os: "darwin", cpu: "x64", ext: "" }, + { + name: "linux-arm64", + bunTarget: "bun-linux-arm64", + os: "linux", + cpu: "arm64", + libc: "glibc", + ext: "", + }, + { + name: "linux-arm64-musl", + bunTarget: "bun-linux-arm64-musl", + os: "linux", + cpu: "arm64", + libc: "musl", + ext: "", + }, + { + name: "linux-x64", + bunTarget: "bun-linux-x64", + os: "linux", + cpu: "x64", + libc: "glibc", + ext: "", + }, + { + name: "linux-x64-musl", + bunTarget: "bun-linux-x64-musl", + os: "linux", + cpu: "x64", + libc: "musl", + ext: "", + }, + { name: "win32-arm64", bunTarget: "bun-windows-arm64", os: "win32", cpu: "arm64", ext: ".exe" }, + { name: "win32-x64", bunTarget: "bun-windows-x64", os: "win32", cpu: "x64", ext: ".exe" }, ]; export const SCOPE = "@clerk"; From 109556f633bf2f635d85256b104ff431ae83263f Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:10:59 -0600 Subject: [PATCH 28/44] feat: add check-release script for stable release detection --- scripts/check-release.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 scripts/check-release.ts diff --git a/scripts/check-release.ts b/scripts/check-release.ts new file mode 100644 index 000000000..e322c25b2 --- /dev/null +++ b/scripts/check-release.ts @@ -0,0 +1,27 @@ +import { join } from "node:path"; +import { appendFile } from "node:fs/promises"; + +const WRAPPER_PKG_PATH = join(import.meta.dir, "../packages/cli/package.json"); +const GITHUB_OUTPUT = process.env.GITHUB_OUTPUT; + +const pkg = await Bun.file(WRAPPER_PKG_PATH).json(); +const version: string = pkg.version; + +// Check if this version is already published on npm +const result = Bun.spawnSync(["npm", "view", `clerk@${version}`, "version"], { + stdio: ["ignore", "pipe", "pipe"], +}); + +const isPublished = result.exitCode === 0 && result.stdout.toString().trim() === version; + +if (!isPublished) { + console.log(`Version ${version} is not published — triggering stable release.`); + if (GITHUB_OUTPUT) { + await appendFile(GITHUB_OUTPUT, `release_created=true\nversion=${version}\n`); + } +} else { + console.log(`Version ${version} is already published — skipping stable release.`); + if (GITHUB_OUTPUT) { + await appendFile(GITHUB_OUTPUT, `release_created=false\n`); + } +} From f482ff0d6c9c96ef5c7419f1f61f750b79a990a9 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:11:06 -0600 Subject: [PATCH 29/44] feat: add changesets configuration --- .changeset/README.md | 11 +++ .changeset/config.json | 14 +++ bun.lock | 195 ++++++++++++++++++++++++++++++++++++++++- package.json | 1 + 4 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 000000000..5d1b929f2 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,11 @@ +# Changesets + +This directory is used by [Changesets](https://github.com/changesets/changesets) to manage versioning and changelogs. + +To add a changeset, run: + +```sh +bunx changeset +``` + +This will prompt you to select the packages that changed and the type of change (patch, minor, major). A markdown file will be created in this directory describing the change — commit it with your PR. diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 000000000..3f9f18799 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "linked": [], + "access": "public", + "baseBranch": "origin/main", + "updateInternalDependencies": "patch", + "ignore": ["@clerk/cli-core"], + "snapshot": { + "useCalculatedVersion": true, + "prereleaseTemplate": "{tag}.v{datetime}" + } +} diff --git a/bun.lock b/bun.lock index 0d79081a3..8a48e12a4 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "@clerk/cli-workspace", "devDependencies": { + "@changesets/cli": "^2.29.4", "@types/bun": "^1.3.9", "nano-staged": "^0.9.0", "oxfmt": "^0.36.0", @@ -33,6 +34,42 @@ }, }, "packages": { + "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + + "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.1.0", "", { "dependencies": { "@changesets/config": "^3.1.3", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ=="], + + "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.9", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ=="], + + "@changesets/changelog-git": ["@changesets/changelog-git@0.2.1", "", { "dependencies": { "@changesets/types": "^6.1.0" } }, "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q=="], + + "@changesets/cli": ["@changesets/cli@2.30.0", "", { "dependencies": { "@changesets/apply-release-plan": "^7.1.0", "@changesets/assemble-release-plan": "^6.0.9", "@changesets/changelog-git": "^0.2.1", "@changesets/config": "^3.1.3", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/get-release-plan": "^4.0.15", "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@changesets/write": "^0.4.0", "@inquirer/external-editor": "^1.0.2", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "enquirer": "^2.4.1", "fs-extra": "^7.0.1", "mri": "^1.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { "changeset": "bin.js" } }, "sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA=="], + + "@changesets/config": ["@changesets/config@3.1.3", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/logger": "^0.1.1", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.8" } }, "sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw=="], + + "@changesets/errors": ["@changesets/errors@0.2.0", "", { "dependencies": { "extendable-error": "^0.1.5" } }, "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow=="], + + "@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.3", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "picocolors": "^1.1.0", "semver": "^7.5.3" } }, "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ=="], + + "@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.15", "", { "dependencies": { "@changesets/assemble-release-plan": "^6.0.9", "@changesets/config": "^3.1.3", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g=="], + + "@changesets/get-version-range-type": ["@changesets/get-version-range-type@0.4.0", "", {}, "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ=="], + + "@changesets/git": ["@changesets/git@3.0.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.8", "spawndamnit": "^3.0.1" } }, "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw=="], + + "@changesets/logger": ["@changesets/logger@0.1.1", "", { "dependencies": { "picocolors": "^1.1.0" } }, "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg=="], + + "@changesets/parse": ["@changesets/parse@0.4.3", "", { "dependencies": { "@changesets/types": "^6.1.0", "js-yaml": "^4.1.1" } }, "sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A=="], + + "@changesets/pre": ["@changesets/pre@2.0.2", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug=="], + + "@changesets/read": ["@changesets/read@0.6.7", "", { "dependencies": { "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/parse": "^0.4.3", "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0", "picocolors": "^1.1.0" } }, "sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA=="], + + "@changesets/should-skip-package": ["@changesets/should-skip-package@0.1.2", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw=="], + + "@changesets/types": ["@changesets/types@6.1.0", "", {}, "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA=="], + + "@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="], + "@clerk/cli-core": ["@clerk/cli-core@workspace:packages/cli-core"], "@commander-js/extra-typings": ["@commander-js/extra-typings@14.0.0", "", { "peerDependencies": { "commander": "~14.0.0" } }, "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg=="], @@ -49,7 +86,7 @@ "@inquirer/expand": ["@inquirer/expand@5.0.8", "", { "dependencies": { "@inquirer/core": "^11.1.5", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QieW3F1prNw3j+hxO7/NKkG1pk3oz7pOB6+5Upwu3OIwADfPX0oZVppsqlL+Vl/uBHHDSOBY0BirLctLnXwGGg=="], - "@inquirer/external-editor": ["@inquirer/external-editor@2.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w=="], + "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], "@inquirer/figures": ["@inquirer/figures@2.0.3", "", {}, "sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g=="], @@ -69,6 +106,10 @@ "@inquirer/type": ["@inquirer/type@4.0.3", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw=="], + "@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@types/node": "^12.7.1", "find-up": "^4.1.0", "fs-extra": "^8.1.0" } }, "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA=="], + + "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], + "@napi-rs/keyring": ["@napi-rs/keyring@1.2.0", "", { "optionalDependencies": { "@napi-rs/keyring-darwin-arm64": "1.2.0", "@napi-rs/keyring-darwin-x64": "1.2.0", "@napi-rs/keyring-freebsd-x64": "1.2.0", "@napi-rs/keyring-linux-arm-gnueabihf": "1.2.0", "@napi-rs/keyring-linux-arm64-gnu": "1.2.0", "@napi-rs/keyring-linux-arm64-musl": "1.2.0", "@napi-rs/keyring-linux-riscv64-gnu": "1.2.0", "@napi-rs/keyring-linux-x64-gnu": "1.2.0", "@napi-rs/keyring-linux-x64-musl": "1.2.0", "@napi-rs/keyring-win32-arm64-msvc": "1.2.0", "@napi-rs/keyring-win32-ia32-msvc": "1.2.0", "@napi-rs/keyring-win32-x64-msvc": "1.2.0" } }, "sha512-d0d4Oyxm+v980PEq1ZH2PmS6cvpMIRc17eYpiU47KgW+lzxklMu6+HOEOPmxrpnF/XQZ0+Q78I2mgMhbIIo/dg=="], "@napi-rs/keyring-darwin-arm64": ["@napi-rs/keyring-darwin-arm64@1.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CA83rDeyONDADO25JLZsh3eHY8yTEtm/RS6ecPsY+1v+dSawzT9GywBMu2r6uOp1IEhQs/xAfxgybGAFr17lSA=="], @@ -95,6 +136,12 @@ "@napi-rs/keyring-win32-x64-msvc": ["@napi-rs/keyring-win32-x64-msvc@1.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xFlx/TsmqmCwNU9v+AVnEJgoEAlBYgzFF5Ihz1rMpPAt4qQWWkMd4sCyM1gMJ1A/GnRqRegDiQpwaxGUHFtFbA=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.36.0", "", { "os": "android", "cpu": "arm" }, "sha512-Z4yVHJWx/swHHjtr0dXrBZb6LxS+qNz1qdza222mWwPTUK4L790+5i3LTgjx3KYGBzcYpjaiZBw4vOx94dH7MQ=="], "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.36.0", "", { "os": "android", "cpu": "arm64" }, "sha512-3ElCJRFNPQl7jexf2CAa9XmAm8eC5JPrIDSjc9jSchkVSFTEqyL0NtZinBB2h1a4i4JgP1oGl/5G5n8YR4FN8Q=="], @@ -175,6 +222,18 @@ "@types/node": ["@types/node@25.4.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="], + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], @@ -185,38 +244,172 @@ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + "env-paths": ["env-paths@4.0.0", "", { "dependencies": { "is-safe-filename": "^0.1.0" } }, "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw=="], + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="], + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "human-id": ["human-id@4.1.3", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-safe-filename": ["is-safe-filename@0.1.1", "", {}, "sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g=="], + "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="], + + "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], "nano-staged": ["nano-staged@0.9.0", "", { "dependencies": { "picocolors": "^1.0.0" }, "bin": { "nano-staged": "lib/bin.js" } }, "sha512-0JfyX4i0Vp5HhC9RDtJ1kp7psz8CFuS3Gya3Z6WZv//QCwA9dPzi1S803VdR0c0P6R7sSvweZ5mSJmYQ/N+loQ=="], + "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], + "oxfmt": ["oxfmt@0.36.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.36.0", "@oxfmt/binding-android-arm64": "0.36.0", "@oxfmt/binding-darwin-arm64": "0.36.0", "@oxfmt/binding-darwin-x64": "0.36.0", "@oxfmt/binding-freebsd-x64": "0.36.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.36.0", "@oxfmt/binding-linux-arm-musleabihf": "0.36.0", "@oxfmt/binding-linux-arm64-gnu": "0.36.0", "@oxfmt/binding-linux-arm64-musl": "0.36.0", "@oxfmt/binding-linux-ppc64-gnu": "0.36.0", "@oxfmt/binding-linux-riscv64-gnu": "0.36.0", "@oxfmt/binding-linux-riscv64-musl": "0.36.0", "@oxfmt/binding-linux-s390x-gnu": "0.36.0", "@oxfmt/binding-linux-x64-gnu": "0.36.0", "@oxfmt/binding-linux-x64-musl": "0.36.0", "@oxfmt/binding-openharmony-arm64": "0.36.0", "@oxfmt/binding-win32-arm64-msvc": "0.36.0", "@oxfmt/binding-win32-ia32-msvc": "0.36.0", "@oxfmt/binding-win32-x64-msvc": "0.36.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-/ejJ+KoSW6J9bcNT9a9UtJSJNWhJ3yOLSBLbkoFHJs/8CZjmaZVZAJe4YgO1KMJlKpNQasrn/G9JQUEZI3p0EQ=="], "oxlint": ["oxlint@1.53.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.53.0", "@oxlint/binding-android-arm64": "1.53.0", "@oxlint/binding-darwin-arm64": "1.53.0", "@oxlint/binding-darwin-x64": "1.53.0", "@oxlint/binding-freebsd-x64": "1.53.0", "@oxlint/binding-linux-arm-gnueabihf": "1.53.0", "@oxlint/binding-linux-arm-musleabihf": "1.53.0", "@oxlint/binding-linux-arm64-gnu": "1.53.0", "@oxlint/binding-linux-arm64-musl": "1.53.0", "@oxlint/binding-linux-ppc64-gnu": "1.53.0", "@oxlint/binding-linux-riscv64-gnu": "1.53.0", "@oxlint/binding-linux-riscv64-musl": "1.53.0", "@oxlint/binding-linux-s390x-gnu": "1.53.0", "@oxlint/binding-linux-x64-gnu": "1.53.0", "@oxlint/binding-linux-x64-musl": "1.53.0", "@oxlint/binding-openharmony-arm64": "1.53.0", "@oxlint/binding-win32-arm64-msvc": "1.53.0", "@oxlint/binding-win32-ia32-msvc": "1.53.0", "@oxlint/binding-win32-x64-msvc": "1.53.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.15.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-TLW0PzGbpO1JxUnuy1pIqVPjQUGh4fNfxu5XJbdFIRFVaJ0UFzTjjk/hSFTMRxN6lZub53xL/IwJNEkrh7VtDg=="], + "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "^2.0.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="], + + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "^0.2.7" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + + "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], + "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + + "@inquirer/editor/@inquirer/external-editor": ["@inquirer/external-editor@2.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w=="], + + "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], + + "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "@manypkg/get-packages/@changesets/types": ["@changesets/types@4.1.0", "", {}, "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw=="], + + "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + + "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], } } diff --git a/package.json b/package.json index b2aaf3465..e33cf65d2 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "prepare": "git config core.hooksPath .hooks" }, "devDependencies": { + "@changesets/cli": "^2.29.4", "@types/bun": "^1.3.9", "nano-staged": "^0.9.0", "oxfmt": "^0.36.0", From 4199333a78008c74745be9c58287a5a1795b472a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:11:43 -0600 Subject: [PATCH 30/44] feat: add cross-compile build script --- scripts/build.ts | 96 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 scripts/build.ts diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 000000000..97f6bde0e --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,96 @@ +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { targets } from "./releaser/targets.ts"; + +const VERIFY_PATTERNS: Record = { + "darwin-arm64": /Mach-O.*arm64/, + "darwin-x64": /Mach-O.*x86_64/, + "linux-arm64": /ELF.*ARM aarch64/, + "linux-arm64-musl": /ELF.*ARM aarch64/, + "linux-x64": /ELF.*x86-64/, + "linux-x64-musl": /ELF.*x86-64/, + "win32-arm64": /PE32\+.*Aarch64/, + "win32-x64": /PE32\+.*x86-64/, +}; + +function getArg(args: string[], name: string): string | undefined { + for (const arg of args) { + if (arg.startsWith(`${name}=`)) return arg.slice(name.length + 1); + } + const idx = args.indexOf(name); + if (idx === -1) return undefined; + const value = args[idx + 1]; + if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`); + return value; +} + +function parseArgs(): { targetFilter?: string; version: string } { + const args = process.argv.slice(2); + const targetFilter = getArg(args, "--target"); + const version = getArg(args, "--version") ?? "0.0.0-dev"; + return { targetFilter, version }; +} + +const { targetFilter, version } = parseArgs(); + +const selectedTargets = targetFilter + ? targets.filter((t) => t.bunTarget === targetFilter || t.name === targetFilter) + : targets; + +if (selectedTargets.length === 0) { + throw new Error( + `Unknown target: ${targetFilter}\nAvailable targets: ${targets.map((t) => t.bunTarget).join(", ")}`, + ); +} + +console.log(`Building ${selectedTargets.length} target(s) with version ${version}\n`); + +let failed = false; +for (const target of selectedTargets) { + const outDir = join("dist", "artifacts", target.name); + const outFile = join(outDir, `clerk${target.ext}`); + + await mkdir(outDir, { recursive: true }); + + console.log(`Building ${target.name} (${target.bunTarget})...`); + const buildResult = Bun.spawnSync( + [ + "bun", + "build", + "--compile", + "--no-compile-autoload-dotenv", + `--target=${target.bunTarget}`, + `--define`, + `CLI_VERSION="${version}"`, + "./packages/cli-core/src/cli.ts", + "--outfile", + outFile, + ], + { stdio: ["ignore", "pipe", "pipe"] }, + ); + + if (buildResult.exitCode !== 0) { + console.error(` FAIL: ${buildResult.stderr.toString().trim()}`); + failed = true; + continue; + } + + // Verify binary format + const fileResult = Bun.spawnSync(["file", outFile], { stdio: ["ignore", "pipe", "pipe"] }); + const fileOutput = fileResult.stdout.toString(); + const pattern = VERIFY_PATTERNS[target.name]; + if (!pattern || !pattern.test(fileOutput)) { + console.error(` FAIL: binary format mismatch for ${target.name}`); + console.error(` file output: ${fileOutput.trim()}`); + failed = true; + continue; + } + + console.log(` OK: ${outFile}`); +} + +if (failed) { + throw new Error("Some targets failed to build."); +} + +console.log("\nAll targets built successfully."); From 507f311f46283cfbcd3fca588f9ab1103ed12bad Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:12:11 -0600 Subject: [PATCH 31/44] feat: add canary versioning script --- scripts/canary.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 scripts/canary.ts diff --git a/scripts/canary.ts b/scripts/canary.ts new file mode 100644 index 000000000..adcab803f --- /dev/null +++ b/scripts/canary.ts @@ -0,0 +1,45 @@ +import { join } from "node:path"; + +const CHANGESET_CONFIG = join(import.meta.dir, "../.changeset/config.json"); +const WRAPPER_PKG = join(import.meta.dir, "../packages/cli/package.json"); + +// Step 1: Temporarily disable changelog generation +const configRaw = await Bun.file(CHANGESET_CONFIG).text(); +const config = JSON.parse(configRaw); +config.changelog = false; +await Bun.write(CHANGESET_CONFIG, JSON.stringify(config, null, 2) + "\n"); + +try { + // Step 2: Exit prerelease mode if active + Bun.spawnSync(["bunx", "changeset", "pre", "exit"], { + stdio: ["ignore", "pipe", "pipe"], + }); + + // Step 3: Bump versions to clear any pre state + Bun.spawnSync(["bunx", "changeset", "version"], { + stdio: ["ignore", "pipe", "pipe"], + }); + + // Step 4: Create temp changeset forcing a patch bump on clerk + const snapshot = `---\n"clerk": patch\n---\n\nCanary release\n`; + await Bun.write(join(import.meta.dir, "../.changeset/canary-temp.md"), snapshot); + + // Step 5: Run changeset version --snapshot canary + const result = Bun.spawnSync(["bunx", "changeset", "version", "--snapshot", "canary"], { + stdio: ["ignore", "pipe", "pipe"], + }); + + if (result.exitCode !== 0) { + console.error(`changeset version failed: ${result.stderr.toString().trim()}`); + console.log("success=0"); + } else { + const pkg = await Bun.file(WRAPPER_PKG).json(); + console.log(`Canary version: ${pkg.version}`); + console.log("success=1"); + } +} finally { + // Step 6: Restore original config from git + Bun.spawnSync(["git", "checkout", "HEAD", "--", CHANGESET_CONFIG], { + stdio: ["ignore", "pipe", "pipe"], + }); +} From e89d2b07131468f4f245cdad9434506b17d59506 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:12:26 -0600 Subject: [PATCH 32/44] feat: add snapshot versioning script --- scripts/snapshot.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 scripts/snapshot.ts diff --git a/scripts/snapshot.ts b/scripts/snapshot.ts new file mode 100644 index 000000000..e88fe260a --- /dev/null +++ b/scripts/snapshot.ts @@ -0,0 +1,64 @@ +import { join } from "node:path"; + +const CHANGESET_CONFIG = join(import.meta.dir, "../.changeset/config.json"); +const WRAPPER_PKG = join(import.meta.dir, "../packages/cli/package.json"); + +// Parse --name flag +function parseName(): string { + const args = process.argv.slice(2); + const nameIdx = args.indexOf("--name"); + if (nameIdx !== -1 && args[nameIdx + 1]) { + return args[nameIdx + 1]; + } + // Also accept positional: bun run scripts/snapshot.ts my-feature + const positional = args.find((a) => !a.startsWith("-")); + return positional || "snapshot"; +} + +const name = parseName(); + +// Validate kebab-case +if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name)) { + throw new Error(`Invalid snapshot name: ${name} (must be kebab-case)`); +} + +// Temporarily disable changelog generation +const configRaw = await Bun.file(CHANGESET_CONFIG).text(); +const config = JSON.parse(configRaw); +config.changelog = false; +await Bun.write(CHANGESET_CONFIG, JSON.stringify(config, null, 2) + "\n"); + +try { + // Exit prerelease mode if active + Bun.spawnSync(["bunx", "changeset", "pre", "exit"], { + stdio: ["ignore", "pipe", "pipe"], + }); + + // Bump versions to clear pre state + Bun.spawnSync(["bunx", "changeset", "version"], { + stdio: ["ignore", "pipe", "pipe"], + }); + + // Create temp changeset + const snapshot = `---\n"clerk": patch\n---\n\nSnapshot release\n`; + await Bun.write(join(import.meta.dir, "../.changeset/snapshot-temp.md"), snapshot); + + // Run changeset version --snapshot + const result = Bun.spawnSync(["bunx", "changeset", "version", "--snapshot", name], { + stdio: ["ignore", "pipe", "pipe"], + }); + + if (result.exitCode !== 0) { + console.error(`changeset version failed: ${result.stderr.toString().trim()}`); + console.log("success=0"); + } else { + const pkg = await Bun.file(WRAPPER_PKG).json(); + console.log(`Snapshot version: ${pkg.version}`); + console.log("success=1"); + } +} finally { + // Restore config + Bun.spawnSync(["git", "checkout", "HEAD", "--", CHANGESET_CONFIG], { + stdio: ["ignore", "pipe", "pipe"], + }); +} From 7d4e38fe1d8b541c1ebad93316161ca8ed770dfd Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:13:26 -0600 Subject: [PATCH 33/44] feat: add git tag and GitHub Release creation to releaser --- scripts/releaser/index.ts | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/scripts/releaser/index.ts b/scripts/releaser/index.ts index e3b648641..a2701d807 100644 --- a/scripts/releaser/index.ts +++ b/scripts/releaser/index.ts @@ -138,4 +138,42 @@ try { await Bun.write(WRAPPER_PKG_PATH, wrapperRaw); } +// Create git tag and GitHub Release for stable releases (no --tag flag). +// Canary (--tag canary) and snapshot (--tag snapshot) skip this. +if (!tag && !dryRun) { + const tagName = `v${version}`; + + // Check if tag already exists (idempotency) + const tagCheck = Bun.spawnSync(["git", "rev-parse", tagName], { + stdio: ["ignore", "pipe", "pipe"], + }); + + if (tagCheck.exitCode === 0) { + console.log(`Tag ${tagName} already exists, skipping.`); + } else { + console.log(`Creating tag ${tagName}...`); + const tagResult = Bun.spawnSync(["git", "tag", tagName], { + stdio: ["ignore", "pipe", "pipe"], + }); + if (tagResult.exitCode !== 0) { + throw new Error(`git tag failed: ${tagResult.stderr.toString().trim()}`); + } + + const pushResult = Bun.spawnSync(["git", "push", "origin", tagName], { + stdio: ["ignore", "pipe", "pipe"], + }); + if (pushResult.exitCode !== 0) { + throw new Error(`git push tag failed: ${pushResult.stderr.toString().trim()}`); + } + + console.log(`Creating GitHub Release for ${tagName}...`); + const releaseResult = Bun.spawnSync(["gh", "release", "create", tagName, "--generate-notes"], { + stdio: ["ignore", "pipe", "pipe"], + }); + if (releaseResult.exitCode !== 0) { + throw new Error(`gh release create failed: ${releaseResult.stderr.toString().trim()}`); + } + } +} + console.log("Done!"); From 6490b267e31f12371ef94f04cfbeb4542410d32a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:13:35 -0600 Subject: [PATCH 34/44] feat: add release, version, and build scripts to root package.json --- package.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/package.json b/package.json index e33cf65d2..a52a027c3 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,14 @@ "format": "bun run --filter @clerk/cli-core format && oxfmt --write scripts/", "format:check": "bun run --filter @clerk/cli-core format:check && oxfmt --check scripts/", "build:compile": "bun run --filter @clerk/cli-core build:compile", + "version-packages": "bunx changeset version", + "version-packages:canary": "bun run scripts/canary.ts", + "version-packages:snapshot": "bun run scripts/snapshot.ts", + "release": "bun run scripts/releaser/index.ts", + "release:canary": "bun run scripts/releaser/index.ts --tag canary", + "release:snapshot": "bun run scripts/releaser/index.ts --tag snapshot", + "build:compile:all": "bun run scripts/build.ts", + "start": "bun run build:compile && ./packages/cli-core/dist/clerk", "prepare": "git config core.hooksPath .hooks" }, "devDependencies": { From 58efb327f39f6e1f5be7a5307d787c15249acca5 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:14:32 -0600 Subject: [PATCH 35/44] refactor: consolidate build-binaries into single sequential job --- .github/workflows/build-binaries.yml | 100 ++++++++++++--------------- 1 file changed, 45 insertions(+), 55 deletions(-) diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 192396c67..6511f5d02 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -18,35 +18,6 @@ jobs: build: runs-on: blacksmith-2vcpu-ubuntu-2404 timeout-minutes: 10 - strategy: - fail-fast: false - matrix: - # Keep in sync with scripts/releaser/targets.ts - include: - - target: darwin-arm64 - bun_target: bun-darwin-arm64 - ext: "" - - target: darwin-x64 - bun_target: bun-darwin-x64 - ext: "" - - target: linux-arm64 - bun_target: bun-linux-arm64 - ext: "" - - target: linux-arm64-musl - bun_target: bun-linux-arm64-musl - ext: "" - - target: linux-x64 - bun_target: bun-linux-x64 - ext: "" - - target: linux-x64-musl - bun_target: bun-linux-x64-musl - ext: "" - - target: win32-arm64 - bun_target: bun-windows-arm64 - ext: ".exe" - - target: win32-x64 - bun_target: bun-windows-x64 - ext: ".exe" steps: - uses: actions/checkout@v4 with: @@ -54,36 +25,55 @@ jobs: - uses: oven-sh/setup-bun@v2 - run: bun install --frozen-lockfile - - name: Build compiled binary + - name: Build all targets env: CLI_VERSION: ${{ inputs.version }} - run: | - bun build --compile \ - --no-compile-autoload-dotenv \ - --target=${{ matrix.bun_target }} \ - --define "CLI_VERSION=\"$CLI_VERSION\"" \ - ./packages/cli-core/src/cli.ts \ - --outfile clerk${{ matrix.ext }} + run: bun run scripts/build.ts --version="${CLI_VERSION}" - - name: Verify binary format - run: | - expected_format="${{ matrix.target }}" - binary="clerk${{ matrix.ext }}" - file_output=$(file "$binary") - echo "Binary: $file_output" - case "$expected_format" in - darwin-arm64) echo "$file_output" | grep -q "Mach-O.*arm64" ;; - darwin-x64) echo "$file_output" | grep -q "Mach-O.*x86_64" ;; - linux-arm64*) echo "$file_output" | grep -q "ELF.*ARM aarch64" ;; - linux-x64*) echo "$file_output" | grep -q "ELF.*x86-64" ;; - win32-arm64) echo "$file_output" | grep -q "PE32+.*Aarch64" ;; - win32-x64) echo "$file_output" | grep -q "PE32+.*x86-64" ;; - *) echo "Unknown target: $expected_format" && exit 1 ;; - esac - echo "Binary format verified for $expected_format" + - uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-prefix }}-darwin-arm64 + path: dist/artifacts/darwin-arm64/clerk + retention-days: 1 + + - uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-prefix }}-darwin-x64 + path: dist/artifacts/darwin-x64/clerk + retention-days: 1 + + - uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-prefix }}-linux-arm64 + path: dist/artifacts/linux-arm64/clerk + retention-days: 1 + + - uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-prefix }}-linux-arm64-musl + path: dist/artifacts/linux-arm64-musl/clerk + retention-days: 1 + + - uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-prefix }}-linux-x64 + path: dist/artifacts/linux-x64/clerk + retention-days: 1 + + - uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-prefix }}-linux-x64-musl + path: dist/artifacts/linux-x64-musl/clerk + retention-days: 1 + + - uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-prefix }}-win32-arm64 + path: dist/artifacts/win32-arm64/clerk.exe + retention-days: 1 - uses: actions/upload-artifact@v4 with: - name: ${{ inputs.artifact-prefix }}-${{ matrix.target }} - path: clerk${{ matrix.ext }} + name: ${{ inputs.artifact-prefix }}-win32-x64 + path: dist/artifacts/win32-x64/clerk.exe retention-days: 1 From edc36cbddd7cdf9c7f967f5b7752711ba9c36f31 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:15:06 -0600 Subject: [PATCH 36/44] feat: replace release-please with changesets/action in release workflow --- .github/workflows/release.yml | 67 ++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 36f424bd0..490405220 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,46 +12,59 @@ permissions: contents: read jobs: - release-please: + versioning: runs-on: blacksmith-2vcpu-ubuntu-2404 timeout-minutes: 5 permissions: contents: write pull-requests: write outputs: - release_created: ${{ steps.release.outputs['packages/cli--release_created'] }} - tag_name: ${{ steps.release.outputs['packages/cli--tag_name'] }} - version: ${{ steps.release.outputs['packages/cli--version'] }} + release_created: ${{ steps.check.outputs.release_created }} + version: ${{ steps.check.outputs.version }} steps: - - id: release - uses: googleapis/release-please-action@v4 + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + + - id: changesets + uses: changesets/action@v1 + with: + version: bun run version-packages + commit: "ci(repo): version packages" + title: "ci(repo): Version Packages" + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Check if release needed + id: check + run: bun run scripts/check-release.ts # ─── Stable release ──────────────────────────────────────────────── build: - needs: release-please - if: ${{ needs.release-please.outputs.release_created == 'true' }} + needs: versioning + if: ${{ needs.versioning.outputs.release_created == 'true' }} uses: ./.github/workflows/build-binaries.yml with: - version: ${{ needs.release-please.outputs.version }} + version: ${{ needs.versioning.outputs.version }} ref: ${{ github.sha }} artifact-prefix: clerk smoke-test: - needs: [release-please, build] + needs: [versioning, build] uses: ./.github/workflows/smoke-test.yml with: - version: ${{ needs.release-please.outputs.version }} + version: ${{ needs.versioning.outputs.version }} artifact-prefix: clerk preset: stable publish-npm: - needs: [release-please, build, smoke-test] + needs: [versioning, build, smoke-test] # Must run on GitHub-hosted runner for npm OIDC trusted publishing runs-on: ubuntu-latest timeout-minutes: 15 permissions: - contents: read + contents: write id-token: write steps: - uses: actions/checkout@v4 @@ -70,12 +83,13 @@ jobs: path: dist/artifacts - name: Publish packages - run: bun run scripts/releaser/index.ts + run: bun run release env: ARTIFACTS_DIR: ${{ github.workspace }}/dist/artifacts + GH_TOKEN: ${{ github.token }} upload-github-assets: - needs: [release-please, build, smoke-test, publish-npm] + needs: [versioning, build, smoke-test, publish-npm] runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -91,7 +105,7 @@ jobs: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} run: | - tag="${{ needs.release-please.outputs.tag_name }}" + 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" @@ -101,24 +115,27 @@ jobs: # ─── Canary release ──────────────────────────────────────────────── canary-version: - needs: release-please - if: ${{ needs.release-please.outputs.release_created != 'true' }} + needs: versioning + if: ${{ needs.versioning.outputs.release_created != 'true' }} runs-on: blacksmith-2vcpu-ubuntu-2404 timeout-minutes: 5 outputs: version: ${{ steps.version.outputs.version }} steps: - uses: actions/checkout@v4 - - name: Compute canary version + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile + + - name: Version packages for canary id: version run: | - base=$(jq -r '.["packages/cli"]' .release-please-manifest.json) - if [ -z "$base" ] || [ "$base" = "null" ]; then - echo "::error::Could not read base version from .release-please-manifest.json" + output=$(bun run version-packages:canary) + if echo "$output" | grep -q "success=0"; then + echo "::error::Canary versioning failed" exit 1 fi - sha=$(git rev-parse --short=7 HEAD) - echo "version=${base}-canary.${sha}" >> "$GITHUB_OUTPUT" + version=$(jq -r '.version' packages/cli/package.json) + echo "version=${version}" >> "$GITHUB_OUTPUT" canary-build: needs: canary-version @@ -169,7 +186,7 @@ jobs: done - name: Publish canary packages - run: bun run scripts/releaser/index.ts --tag canary --version "$CANARY_VERSION" + run: bun run release:canary --version "$CANARY_VERSION" env: CANARY_VERSION: ${{ needs.canary-version.outputs.version }} ARTIFACTS_DIR: ${{ github.workspace }}/dist/artifacts From 55ee209220353e92d27ed5eb46e4669189c06589 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:15:16 -0600 Subject: [PATCH 37/44] refactor: use changeset snapshot script and fix failure comment indentation --- .github/workflows/snapshot.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index b0a616441..7d015081c 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -43,6 +43,8 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ steps.pr.outputs.sha }} + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile - name: Compute snapshot version id: version @@ -52,13 +54,13 @@ jobs: # Extract optional name from "!snapshot " — default to "snapshot" name=$(echo "$COMMENT_BODY" | sed 's/^!snapshot[[:space:]]*//' | tr -cd 'a-zA-Z0-9-') if [ -z "$name" ]; then name="snapshot"; fi - base=$(jq -r '.["packages/cli"]' .release-please-manifest.json) - if [ -z "$base" ] || [ "$base" = "null" ]; then - echo "::error::Could not read base version from .release-please-manifest.json" + output=$(bun run version-packages:snapshot -- "$name") + if echo "$output" | grep -q "success=0"; then + echo "::error::Snapshot versioning failed" exit 1 fi - datetime=$(date -u +%Y%m%d%H%M%S) - echo "version=${base}-${name}.v${datetime}" >> "$GITHUB_OUTPUT" + version=$(jq -r '.version' packages/cli/package.json) + echo "version=${version}" >> "$GITHUB_OUTPUT" build: needs: snapshot @@ -112,7 +114,7 @@ jobs: done - name: Publish snapshot packages - run: bun run scripts/releaser/index.ts --tag snapshot --version "$SNAPSHOT_VERSION" + run: bun run release:snapshot --version "$SNAPSHOT_VERSION" env: SNAPSHOT_VERSION: ${{ needs.snapshot.outputs.version }} ARTIFACTS_DIR: ${{ github.workspace }}/dist/artifacts @@ -170,6 +172,9 @@ EOF GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} run: | - gh pr comment "${{ github.event.issue.number }}" --repo "${GH_REPO}" --body "## Snapshot failed + gh pr comment "${{ github.event.issue.number }}" --repo "${GH_REPO}" --body "$(cat < Date: Thu, 19 Mar 2026 16:15:55 -0600 Subject: [PATCH 38/44] chore: remove release-please configuration files --- .release-please-manifest.json | 3 --- release-please-config.json | 8 -------- 2 files changed, 11 deletions(-) delete mode 100644 .release-please-manifest.json delete mode 100644 release-please-config.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json deleted file mode 100644 index cacc6b142..000000000 --- a/.release-please-manifest.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "packages/cli": "0.0.1" -} diff --git a/release-please-config.json b/release-please-config.json deleted file mode 100644 index d86d6d95c..000000000 --- a/release-please-config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", - "release-type": "node", - "include-component-in-tag": false, - "packages": { - "packages/cli": {} - } -} From f30646c7faae5581f45596f7c751314c188d07b8 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:17:55 -0600 Subject: [PATCH 39/44] docs: update releasing.md for changesets migration --- docs/releasing.md | 111 ++++++++++++++++++++++++++++++---------------- 1 file changed, 73 insertions(+), 38 deletions(-) diff --git a/docs/releasing.md b/docs/releasing.md index 8b23422b6..a74c42e6f 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -6,20 +6,22 @@ This document describes how the Clerk CLI is built, versioned, and published. ``` push to main - → release-please creates/updates a version PR - → merge version PR - → release-please creates a GitHub Release + tag - → build job: cross-compile binaries (8 targets) + → changesets/action creates/updates "Version Packages" PR + → merge "Version Packages" PR + → check-release.ts detects unpublished version + → build job: cross-compile all 8 targets (~5.5s total) → smoke-test job: verify binaries on native runners → publish-npm: generate platform packages + publish wrapper → upload-github-assets: attach binaries to the GitHub Release - → (if no release created) canary job: build + smoke-test subset + publish @canary + → (if no stable release needed) canary.ts versions packages + → build → smoke-test subset → publish @canary PR comment "!snapshot [name]" - → build job: cross-compile binaries from PR branch - → smoke-test job: verify linux-x64 binary - → publish-npm: publish @snapshot packages - → post installation comment on PR + → snapshot.ts versions packages from PR branch + → build job: cross-compile binaries + → smoke-test job: verify linux-x64 binary + → publish-npm: publish @snapshot packages + → post installation comment on PR ``` ## Architecture @@ -34,76 +36,85 @@ Target names follow Node.js's `${process.platform}-${process.arch}` convention s ### Stable (`@latest`) -Published when a release-please version PR is merged. Includes full smoke testing on native runners before publishing. Binaries are also attached to the GitHub Release. +Published when the "Version Packages" PR (created by `changesets/action`) is merged and `check-release.ts` detects that the version in `packages/cli/package.json` is not yet published on npm. Includes full smoke testing on native runners before publishing. Binaries are also attached to a GitHub Release created by the releaser script. Install: `npm install -g clerk` ### Canary (`@canary`) -Published automatically on every push to `main` that does **not** trigger a stable release. Canary versions use the format `x.y.z-canary.` (e.g., `0.0.1-canary.abc1234`). A subset of smoke tests (darwin-arm64, linux-x64, linux-x64-musl) runs before publishing. +Published automatically on every push to `main` that does **not** trigger a stable release. `scripts/canary.ts` uses Changesets snapshot mode to produce versions in the format `x.y.z-canary.v` (e.g., `0.0.1-canary.v20260313145959`). A subset of smoke tests (darwin-arm64, linux-x64, linux-x64-musl) runs before publishing. Install: `npm install -g clerk@canary` ### Snapshot (`@snapshot`) -Published on-demand from PR branches by commenting `!snapshot` (or `!snapshot `) on a pull request. The commenter must be a member or owner of the repository's organization. Snapshot versions use the format `x.y.z-.v` (e.g., `0.0.1-snapshot.v20260313145959` or `0.0.1-my-feature.v20260313145959`). The datetime format ensures multiple snapshots from the same PR sort monotonically in semver. +Published on-demand from PR branches by commenting `!snapshot` (or `!snapshot `) on a pull request. The commenter must be a member or owner of the repository's organization. `scripts/snapshot.ts` uses Changesets snapshot mode to produce versions in the format `x.y.z-.v` (e.g., `0.0.1-snapshot.v20260313145959` or `0.0.1-my-feature.v20260313145959`). The datetime format ensures multiple snapshots from the same PR sort monotonically in semver. Install: `npm install -g clerk@` (version is posted as a PR comment after publishing) ## Versioning -Versioning is managed by [release-please](https://github.com/googleapis/release-please). On every push to `main`, it either creates a new version PR or updates an existing one based on conventional commit messages. Merging that PR triggers a GitHub Release. +Versioning is managed by [Changesets](https://github.com/changesets/changesets). Contributors add changeset files to their PRs by running: + +```sh +bunx changeset +``` + +This launches an interactive prompt that asks which packages changed and whether it is a patch, minor, or major bump. The resulting markdown file is committed with the PR. + +On every push to `main`, the `changesets/action@v1` GitHub Action either creates a new "Version Packages" PR or updates an existing one. That PR aggregates all pending changesets, bumps versions, and updates changelogs. Merging it triggers a stable release (detected by `scripts/check-release.ts`). Configuration: -- `release-please-config.json` — release-please settings (release type, package path) -- `.release-please-manifest.json` — tracks the current version +- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages) ## Build Pipeline -The release workflow (`.github/workflows/release.yml`) runs when release-please creates a release. Binary compilation is handled by a reusable workflow (`.github/workflows/build-binaries.yml`) shared across stable, canary, and snapshot pipelines. Build and publish jobs run on Blacksmith runners (`blacksmith-2vcpu-ubuntu-2404`); smoke tests run on platform-native GitHub-hosted runners. +The release workflow (`.github/workflows/release.yml`) runs on every push to `main`. Binary compilation is handled by a reusable workflow (`.github/workflows/build-binaries.yml`) shared across stable, canary, and snapshot pipelines. Build jobs run on Blacksmith runners (`blacksmith-2vcpu-ubuntu-2404`); smoke tests run on platform-native GitHub-hosted runners. -### 1. Build Job (matrix) +### 1. Build Job -Defined in [`.github/workflows/build-binaries.yml`](../.github/workflows/build-binaries.yml) and called by the release, canary, and snapshot pipelines. Runs once per target platform on a Blacksmith runner. Each job: +Defined in [`.github/workflows/build-binaries.yml`](../.github/workflows/build-binaries.yml) and called by the release, canary, and snapshot pipelines. Runs as a **single sequential job** on a Blacksmith runner that cross-compiles all 8 targets in ~5.5 seconds total using `scripts/build.ts`. For each target, the script: 1. Cross-compiles the CLI using `bun build --compile --no-compile-autoload-dotenv --target=` 2. Injects the version via `--define "CLI_VERSION=\"$CLI_VERSION\""` 3. Verifies the binary format using `file` output -4. Uploads the binary as a GitHub Actions artifact +4. The workflow then uploads each binary as a separate GitHub Actions artifact ### 2. Smoke Test Job (matrix) Downloads each compiled binary and runs `--version` to verify the binary actually executes. Smoke testing is handled by a reusable workflow (`.github/workflows/smoke-test.yml`) shared across stable, canary, and snapshot pipelines. Each caller passes a preset name (`stable`, `canary`, or `snapshot`); the reusable workflow resolves the preset to a target matrix internally. glibc targets run natively on a platform-matched GitHub-hosted runner; musl targets run inside an Alpine Docker container on a Linux runner. -Not all targets have a native runner available. `win32-arm64` is published as best-effort — the build job verifies it is a valid PE32+/Aarch64 binary via `file` output, but no execution-level smoke test runs because there is no GitHub-hosted ARM Windows runner. +Not all targets have a native runner available. `win32-arm64` is published as best-effort -- the build job verifies it is a valid PE32+/Aarch64 binary via `file` output, but no execution-level smoke test runs because there is no GitHub-hosted ARM Windows runner. Publishing and GitHub Release upload are gated on all smoke tests passing. ### 3. Publish npm Job -Runs the releaser script (`scripts/releaser/index.ts`): +Runs the releaser script (`scripts/releaser/index.ts`) via `bun run release` (stable), `bun run release:canary` (canary), or `bun run release:snapshot` (snapshot): 1. Reads the version from `packages/cli/package.json` (or uses `--version` override for canary/snapshot) 2. For each target, generates a platform package in `dist/platform-packages/`: - Creates `package.json` with `os`/`cpu` fields for npm platform selection - Copies the compiled binary from the build artifacts -3. Publishes each platform package with `--access public` (authentication and provenance use npm OIDC trusted publishing — no `NPM_TOKEN` secret needed, just `id-token: write` permission on a GitHub-hosted runner) +3. Publishes each platform package with `--access public` (authentication and provenance use npm OIDC trusted publishing -- no `NPM_TOKEN` secret needed, just `id-token: write` permission on a GitHub-hosted runner) 4. Temporarily mutates the wrapper `package.json` to add `optionalDependencies` and remove `private: true`, publishes it, then restores the original file +5. For stable releases, creates a Git tag and GitHub Release via the GitHub API The releaser accepts these flags: -- `--dry-run` — simulate publishing without actually uploading to npm -- `--tag ` — publish with a specific npm dist-tag (e.g., `canary`, `snapshot`); defaults to `latest` -- `--version ` — override the version read from `package.json` +- `--dry-run` -- simulate publishing without actually uploading to npm +- `--tag ` -- publish with a specific npm dist-tag (e.g., `canary`, `snapshot`); defaults to `latest` +- `--version ` -- override the version read from `package.json` -All publishes are idempotent — the script checks `npm view` before publishing and skips already-published versions. +All publishes are idempotent -- the script checks `npm view` before publishing and skips already-published versions. #### Environment Variables The releaser script and publish workflow steps use these environment variables: -- `ARTIFACTS_DIR` — path to directory containing compiled binaries from the build job (defaults to `./dist/artifacts`) +- `ARTIFACTS_DIR` -- path to directory containing compiled binaries from the build job (defaults to `./dist/artifacts`) +- `GH_TOKEN` -- GitHub token used by the releaser to create tags and releases (stable only) #### npm Authentication @@ -124,7 +135,12 @@ Attaches the compiled binaries to the GitHub Release for direct download. Binari | `packages/cli-core/src/cli.ts` | CLI entrypoint (reads `CLI_VERSION` global at runtime) | | `packages/cli-core/src/globals.d.ts` | TypeScript declaration for the `CLI_VERSION` compile-time define | | `scripts/releaser/index.ts` | Generates platform packages and publishes everything to npm | -| `scripts/releaser/targets.ts` | Target definitions — must be kept in sync with the workflow matrix | +| `scripts/releaser/targets.ts` | Target definitions (used by both releaser and build.ts) | +| `scripts/build.ts` | Cross-compiles CLI binaries for all 8 platform targets | +| `scripts/canary.ts` | Versions packages for canary channel using Changesets snapshots | +| `scripts/snapshot.ts` | Versions packages for snapshot channel using Changesets snapshots | +| `scripts/check-release.ts` | Detects if a stable release is needed (compares version to npm registry) | +| `.changeset/config.json` | Changesets configuration | | `.github/workflows/build-binaries.yml` | Reusable workflow for cross-compiling binaries (called by release + snapshot) | | `.github/workflows/smoke-test.yml` | Reusable workflow for smoke-testing binaries (called by release + snapshot) | | `.github/workflows/release.yml` | GitHub Actions release + canary workflow | @@ -134,22 +150,28 @@ Attaches the compiled binaries to the GitHub Release for direct download. Binari The target list exists in these places that must stay in sync: -1. `scripts/releaser/targets.ts` — used by the releaser to generate platform packages -2. `.github/workflows/build-binaries.yml` build matrix — compiles binaries (maps target names to Bun cross-compile targets, e.g., `win32-x64` → `bun-windows-x64`) -3. `.github/workflows/smoke-test.yml` preset definitions — defines the target matrix for each preset (`stable`, `canary`, `snapshot`) +1. `scripts/releaser/targets.ts` -- used by the releaser to generate platform packages and by `scripts/build.ts` to cross-compile binaries +2. `.github/workflows/smoke-test.yml` preset definitions -- defines the target matrix for each preset (`stable`, `canary`, `snapshot`) -If you add or remove a target, update all of these. Note that the smoke-test presets may not cover every target if a native runner isn't available (e.g., `win32-arm64`). +If you add or remove a target, update both of these. Note that the smoke-test presets may not cover every target if a native runner isn't available (e.g., `win32-arm64`). ## Local Development -To build a compiled binary locally for your native platform: - ```sh -bun run --filter @clerk/cli-core build:compile -./packages/cli-core/dist/clerk --version # prints "0.0.0-dev" +# Run CLI source directly (fastest, no compilation) +bun run dev + +# Compile a native binary and run it +bun run start -- --help + +# Cross-compile all 8 targets to dist/artifacts/ +bun run build:compile:all + +# Cross-compile a single target +bun run scripts/build.ts --target=bun-darwin-arm64 ``` -This does not inject a version (falls back to `0.0.0-dev`) and only builds for your current platform. The release workflow handles cross-compilation and version injection. +The `dev` and `start` commands do not inject a version (falls back to `0.0.0-dev`). The release workflow handles version injection. To test the releaser without publishing: @@ -157,12 +179,25 @@ To test the releaser without publishing: bun run scripts/releaser/index.ts --dry-run ``` +## Contributing + +When submitting a PR that changes user-facing behavior, add a changeset: + +```sh +bunx changeset +``` + +Follow the interactive prompts to select the affected package (`clerk`) and the bump type (patch, minor, or major). Commit the generated `.changeset/*.md` file with your PR. The `changesets/action` bot will incorporate it into the next "Version Packages" PR automatically. + +If your change is internal-only (CI, tests, docs, refactoring), you can skip the changeset -- the bot will note that no packages need version bumps. + ## Safeguards - **`prepublishOnly` guard**: The wrapper `package.json` has a `prepublishOnly` script that exits with an error, preventing accidental `npm publish` from the package directory. The releaser bypasses this with `--ignore-scripts`. - **`private: true`**: Both `packages/cli` and `packages/cli-core` are marked private. The releaser removes this flag from the wrapper before publishing and restores it afterward. - **Idempotent publishing**: The releaser checks npm before publishing and skips already-published versions, making it safe to re-run. -- **Binary format verification**: The build job verifies each compiled binary matches its expected architecture before uploading. +- **Release detection**: `scripts/check-release.ts` compares the version in `package.json` against the npm registry, ensuring stable releases only trigger when there is genuinely a new version to publish. +- **Binary format verification**: The build script verifies each compiled binary matches its expected architecture before uploading. - **Native smoke tests**: Each binary is executed on a native runner for its platform before publishing. This catches cross-compilation issues that format checks alone would miss. - **Org membership check**: Snapshot releases require the commenter to be a `MEMBER` or `OWNER` of the repository's organization, verified via `author_association`. - **OIDC trusted publishing**: Publish jobs authenticate via GitHub's OIDC provider instead of stored npm tokens. This eliminates secret rotation, prevents token exfiltration, and scopes publish permissions to specific workflow files. From 077275bd3fdbcf139b62d9d79f1ba9f875ebbc07 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:24:28 -0600 Subject: [PATCH 40/44] fix(ci): replace heredocs with echo+body-file in snapshot.yml comments The heredoc patterns used for PR comments placed content at column 1 inside YAML literal block scalars, which is invalid YAML per the spec. While GitHub Actions' Go-based parser tolerated this, strict parsers (PyYAML, yaml npm package) reject it. Switch to echo statements writing to a temp file with --body-file to avoid YAML/shell quoting conflicts entirely. --- .github/workflows/snapshot.yml | 43 ++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 7d015081c..bb4194781 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -133,22 +133,23 @@ jobs: GH_REPO: ${{ github.repository }} SNAPSHOT_VERSION: ${{ needs.snapshot.outputs.version }} SNAPSHOT_SHA: ${{ needs.snapshot.outputs.sha }} + PR_NUMBER: ${{ github.event.issue.number }} run: | short_sha=$(echo "${SNAPSHOT_SHA}" | cut -c1-7) - gh pr comment "${{ github.event.issue.number }}" --repo "${GH_REPO}" --body "$(cat < Published from ${short_sha} -EOF - )" + { + echo '## Snapshot published' + echo '' + echo '```sh' + echo "npm install -g clerk@${SNAPSHOT_VERSION}" + echo '```' + echo '' + echo '| Package | Version |' + echo '|---------|---------|' + echo "| \`clerk\` | \`${SNAPSHOT_VERSION}\` |" + echo '' + echo "> Published from ${short_sha}" + } > /tmp/comment-body.md + gh pr comment "${PR_NUMBER}" --repo "${GH_REPO}" --body-file /tmp/comment-body.md notify-failure: needs: [snapshot, build, smoke-test, publish] @@ -171,10 +172,12 @@ EOF env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.issue.number }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | - gh pr comment "${{ github.event.issue.number }}" --repo "${GH_REPO}" --body "$(cat < /tmp/comment-body.md + gh pr comment "${PR_NUMBER}" --repo "${GH_REPO}" --body-file /tmp/comment-body.md From 2d01b44560c832f5a1e21e589f005738fcf546e3 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:26:39 -0600 Subject: [PATCH 41/44] =?UTF-8?q?fix:=20address=20review=20findings=20?= =?UTF-8?q?=E2=80=94=20error=20handling=20and=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - check-release.ts: distinguish npm E404 from network errors - canary.ts/snapshot.ts: throw on failure instead of stdout success=0 - release.yml/snapshot.yml: simplify version steps (scripts exit non-zero) - targets.ts: update stale comment referencing removed matrix --- .github/workflows/release.yml | 6 +----- .github/workflows/snapshot.yml | 6 +----- scripts/canary.ts | 10 ++++------ scripts/check-release.ts | 13 ++++++++++++- scripts/releaser/targets.ts | 2 +- scripts/snapshot.ts | 10 ++++------ 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 490405220..c9a2eb3fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -129,11 +129,7 @@ jobs: - name: Version packages for canary id: version run: | - output=$(bun run version-packages:canary) - if echo "$output" | grep -q "success=0"; then - echo "::error::Canary versioning failed" - exit 1 - fi + bun run version-packages:canary version=$(jq -r '.version' packages/cli/package.json) echo "version=${version}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index bb4194781..076864c1f 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -54,11 +54,7 @@ jobs: # Extract optional name from "!snapshot " — default to "snapshot" name=$(echo "$COMMENT_BODY" | sed 's/^!snapshot[[:space:]]*//' | tr -cd 'a-zA-Z0-9-') if [ -z "$name" ]; then name="snapshot"; fi - output=$(bun run version-packages:snapshot -- "$name") - if echo "$output" | grep -q "success=0"; then - echo "::error::Snapshot versioning failed" - exit 1 - fi + bun run version-packages:snapshot -- "$name" version=$(jq -r '.version' packages/cli/package.json) echo "version=${version}" >> "$GITHUB_OUTPUT" diff --git a/scripts/canary.ts b/scripts/canary.ts index adcab803f..4e3b691cc 100644 --- a/scripts/canary.ts +++ b/scripts/canary.ts @@ -30,13 +30,11 @@ try { }); if (result.exitCode !== 0) { - console.error(`changeset version failed: ${result.stderr.toString().trim()}`); - console.log("success=0"); - } else { - const pkg = await Bun.file(WRAPPER_PKG).json(); - console.log(`Canary version: ${pkg.version}`); - console.log("success=1"); + throw new Error(`changeset version failed: ${result.stderr.toString().trim()}`); } + + const pkg = await Bun.file(WRAPPER_PKG).json(); + console.log(`Canary version: ${pkg.version}`); } finally { // Step 6: Restore original config from git Bun.spawnSync(["git", "checkout", "HEAD", "--", CHANGESET_CONFIG], { diff --git a/scripts/check-release.ts b/scripts/check-release.ts index e322c25b2..b5616d31a 100644 --- a/scripts/check-release.ts +++ b/scripts/check-release.ts @@ -12,7 +12,18 @@ const result = Bun.spawnSync(["npm", "view", `clerk@${version}`, "version"], { stdio: ["ignore", "pipe", "pipe"], }); -const isPublished = result.exitCode === 0 && result.stdout.toString().trim() === version; +let isPublished: boolean; +if (result.exitCode === 0) { + isPublished = result.stdout.toString().trim() === version; +} else { + // Distinguish "not found" (E404) from real errors (network, auth) + const stderr = result.stderr.toString(); + if (stderr.includes("E404") || stderr.includes("is not in this registry")) { + isPublished = false; + } else { + throw new Error(`npm view clerk@${version} failed (exit ${result.exitCode}): ${stderr.trim()}`); + } +} if (!isPublished) { console.log(`Version ${version} is not published — triggering stable release.`); diff --git a/scripts/releaser/targets.ts b/scripts/releaser/targets.ts index e8d2158ef..221f45ad0 100644 --- a/scripts/releaser/targets.ts +++ b/scripts/releaser/targets.ts @@ -9,7 +9,7 @@ export interface Target { // Target names use Node.js ${process.platform}-${process.arch} convention so the wrapper // shim (packages/cli/bin/clerk) can derive package names without a lookup table. -// Keep in sync with .github/workflows/build-binaries.yml matrix. +// Used by scripts/build.ts and scripts/releaser/index.ts. export const targets: Target[] = [ { name: "darwin-arm64", bunTarget: "bun-darwin-arm64", os: "darwin", cpu: "arm64", ext: "" }, { name: "darwin-x64", bunTarget: "bun-darwin-x64", os: "darwin", cpu: "x64", ext: "" }, diff --git a/scripts/snapshot.ts b/scripts/snapshot.ts index e88fe260a..590a1ede8 100644 --- a/scripts/snapshot.ts +++ b/scripts/snapshot.ts @@ -49,13 +49,11 @@ try { }); if (result.exitCode !== 0) { - console.error(`changeset version failed: ${result.stderr.toString().trim()}`); - console.log("success=0"); - } else { - const pkg = await Bun.file(WRAPPER_PKG).json(); - console.log(`Snapshot version: ${pkg.version}`); - console.log("success=1"); + throw new Error(`changeset version failed: ${result.stderr.toString().trim()}`); } + + const pkg = await Bun.file(WRAPPER_PKG).json(); + console.log(`Snapshot version: ${pkg.version}`); } finally { // Restore config Bun.spawnSync(["git", "checkout", "HEAD", "--", CHANGESET_CONFIG], { From 66c394c7917654c7fbe00b116be4b694f4481232 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:32:43 -0600 Subject: [PATCH 42/44] refactor: consolidate canary.ts into snapshot.ts --- package.json | 2 +- scripts/canary.ts | 43 ------------------------------------------- 2 files changed, 1 insertion(+), 44 deletions(-) delete mode 100644 scripts/canary.ts diff --git a/package.json b/package.json index a52a027c3..7e8603fdc 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "format:check": "bun run --filter @clerk/cli-core format:check && oxfmt --check scripts/", "build:compile": "bun run --filter @clerk/cli-core build:compile", "version-packages": "bunx changeset version", - "version-packages:canary": "bun run scripts/canary.ts", + "version-packages:canary": "bun run scripts/snapshot.ts -- canary", "version-packages:snapshot": "bun run scripts/snapshot.ts", "release": "bun run scripts/releaser/index.ts", "release:canary": "bun run scripts/releaser/index.ts --tag canary", diff --git a/scripts/canary.ts b/scripts/canary.ts deleted file mode 100644 index 4e3b691cc..000000000 --- a/scripts/canary.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { join } from "node:path"; - -const CHANGESET_CONFIG = join(import.meta.dir, "../.changeset/config.json"); -const WRAPPER_PKG = join(import.meta.dir, "../packages/cli/package.json"); - -// Step 1: Temporarily disable changelog generation -const configRaw = await Bun.file(CHANGESET_CONFIG).text(); -const config = JSON.parse(configRaw); -config.changelog = false; -await Bun.write(CHANGESET_CONFIG, JSON.stringify(config, null, 2) + "\n"); - -try { - // Step 2: Exit prerelease mode if active - Bun.spawnSync(["bunx", "changeset", "pre", "exit"], { - stdio: ["ignore", "pipe", "pipe"], - }); - - // Step 3: Bump versions to clear any pre state - Bun.spawnSync(["bunx", "changeset", "version"], { - stdio: ["ignore", "pipe", "pipe"], - }); - - // Step 4: Create temp changeset forcing a patch bump on clerk - const snapshot = `---\n"clerk": patch\n---\n\nCanary release\n`; - await Bun.write(join(import.meta.dir, "../.changeset/canary-temp.md"), snapshot); - - // Step 5: Run changeset version --snapshot canary - const result = Bun.spawnSync(["bunx", "changeset", "version", "--snapshot", "canary"], { - stdio: ["ignore", "pipe", "pipe"], - }); - - if (result.exitCode !== 0) { - throw new Error(`changeset version failed: ${result.stderr.toString().trim()}`); - } - - const pkg = await Bun.file(WRAPPER_PKG).json(); - console.log(`Canary version: ${pkg.version}`); -} finally { - // Step 6: Restore original config from git - Bun.spawnSync(["git", "checkout", "HEAD", "--", CHANGESET_CONFIG], { - stdio: ["ignore", "pipe", "pipe"], - }); -} From fd316a6cc8d7b5adea3d79b6064634faf16b6363 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:34:07 -0600 Subject: [PATCH 43/44] refactor: extract shared isPublished helper, inline readVersion, remove ext alias --- scripts/check-release.ts | 34 +++++-------------- scripts/lib/npm.ts | 18 ++++++++++ scripts/releaser/index.ts | 69 ++++++++++++--------------------------- 3 files changed, 47 insertions(+), 74 deletions(-) create mode 100644 scripts/lib/npm.ts diff --git a/scripts/check-release.ts b/scripts/check-release.ts index b5616d31a..aeb3597d9 100644 --- a/scripts/check-release.ts +++ b/scripts/check-release.ts @@ -1,38 +1,22 @@ import { join } from "node:path"; import { appendFile } from "node:fs/promises"; +import { isPublished } from "./lib/npm.ts"; const WRAPPER_PKG_PATH = join(import.meta.dir, "../packages/cli/package.json"); const GITHUB_OUTPUT = process.env.GITHUB_OUTPUT; const pkg = await Bun.file(WRAPPER_PKG_PATH).json(); const version: string = pkg.version; +const published = isPublished("clerk", version); -// Check if this version is already published on npm -const result = Bun.spawnSync(["npm", "view", `clerk@${version}`, "version"], { - stdio: ["ignore", "pipe", "pipe"], -}); - -let isPublished: boolean; -if (result.exitCode === 0) { - isPublished = result.stdout.toString().trim() === version; +if (published) { + console.log(`Version ${version} is already published — skipping stable release.`); } else { - // Distinguish "not found" (E404) from real errors (network, auth) - const stderr = result.stderr.toString(); - if (stderr.includes("E404") || stderr.includes("is not in this registry")) { - isPublished = false; - } else { - throw new Error(`npm view clerk@${version} failed (exit ${result.exitCode}): ${stderr.trim()}`); - } + console.log(`Version ${version} is not published — triggering stable release.`); } -if (!isPublished) { - console.log(`Version ${version} is not published — triggering stable release.`); - if (GITHUB_OUTPUT) { - await appendFile(GITHUB_OUTPUT, `release_created=true\nversion=${version}\n`); - } -} else { - console.log(`Version ${version} is already published — skipping stable release.`); - if (GITHUB_OUTPUT) { - await appendFile(GITHUB_OUTPUT, `release_created=false\n`); - } +if (GITHUB_OUTPUT) { + const lines = [`release_created=${!published}`]; + if (!published) lines.push(`version=${version}`); + await appendFile(GITHUB_OUTPUT, lines.join("\n") + "\n"); } diff --git a/scripts/lib/npm.ts b/scripts/lib/npm.ts new file mode 100644 index 000000000..5363266e6 --- /dev/null +++ b/scripts/lib/npm.ts @@ -0,0 +1,18 @@ +/** + * Check if a package version is published on npm. + * Distinguishes "not found" (E404) from real errors (network, auth). + */ +export function isPublished(name: string, version: string): boolean { + const result = Bun.spawnSync(["npm", "view", `${name}@${version}`, "version"], { + stdio: ["ignore", "pipe", "pipe"], + }); + + if (result.exitCode === 0) return true; + + const stderr = result.stderr.toString(); + if (stderr.includes("E404") || stderr.includes("is not in this registry")) { + return false; + } + + throw new Error(`npm view ${name}@${version} failed (exit ${result.exitCode}): ${stderr.trim()}`); +} diff --git a/scripts/releaser/index.ts b/scripts/releaser/index.ts index a2701d807..fdd14749b 100644 --- a/scripts/releaser/index.ts +++ b/scripts/releaser/index.ts @@ -1,11 +1,25 @@ import { mkdir, cp, rm, chmod, copyFile } from "node:fs/promises"; import { join } from "node:path"; import { type Target, targets, SCOPE, PKG_PREFIX } from "./targets.ts"; +import { isPublished } from "../lib/npm.ts"; const DIST_DIR = join(import.meta.dir, "../../dist/platform-packages"); const ARTIFACTS_DIR = process.env.ARTIFACTS_DIR ?? join(import.meta.dir, "../../dist/artifacts"); const WRAPPER_PKG_PATH = join(import.meta.dir, "../../packages/cli/package.json"); +function run(cmd: string[], opts?: { cwd?: string }): void { + const result = Bun.spawnSync(cmd, { + cwd: opts?.cwd, + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.exitCode !== 0) { + const stderr = result.stderr.toString().trim(); + throw new Error( + `${cmd.join(" ")} failed (exit ${result.exitCode})${stderr ? `: ${stderr}` : ""}`, + ); + } +} + function parseArgs(): { dryRun: boolean; tag?: string; versionOverride?: string } { const args = process.argv.slice(2); const dryRun = args.includes("--dry-run"); @@ -21,40 +35,17 @@ function parseArgs(): { dryRun: boolean; tag?: string; versionOverride?: string return { dryRun, tag, versionOverride }; } -async function readVersion(): Promise { - const pkg = await Bun.file(WRAPPER_PKG_PATH).json(); - return pkg.version; -} - function packageName(targetName: string): string { return `${SCOPE}/${PKG_PREFIX}-${targetName}`; } -function isPublished(name: string, version: string): boolean { - const result = Bun.spawnSync(["npm", "view", `${name}@${version}`, "version"], { - stdio: ["ignore", "pipe", "pipe"], - }); - - if (result.exitCode === 0) return true; - - // "npm view" exits non-zero for both "not found" and real errors (network, auth). - // Treat only E404 / "not found" as unpublished; propagate everything else. - const stderr = result.stderr.toString(); - if (stderr.includes("E404") || stderr.includes("is not in this registry")) { - return false; - } - - throw new Error(`npm view ${name}@${version} failed (exit ${result.exitCode}): ${stderr.trim()}`); -} - async function generatePlatformPackage(target: Target, version: string): Promise { const dir = join(DIST_DIR, target.name); const binDir = join(dir, "bin"); await mkdir(binDir, { recursive: true }); - const ext = target.ext; - const binaryName = `clerk${ext}`; + const binaryName = `clerk${target.ext}`; const artifactPath = join(ARTIFACTS_DIR, `clerk-${target.name}`, binaryName); const destPath = join(binDir, binaryName); await cp(artifactPath, destPath); @@ -88,15 +79,11 @@ function publish(dir: string, dryRun: boolean, tag?: string): void { const flags = ["npm", "publish", "--access", "public", "--provenance", "--ignore-scripts"]; if (tag) flags.push("--tag", tag); if (dryRun) flags.push("--dry-run"); - const result = Bun.spawnSync(flags, { cwd: dir, stdio: ["ignore", "pipe", "pipe"] }); - if (result.exitCode !== 0) { - const stderr = result.stderr.toString().trim(); - throw new Error(`npm publish failed (exit ${result.exitCode})${stderr ? `: ${stderr}` : ""}`); - } + run(flags, { cwd: dir }); } const { dryRun, tag, versionOverride } = parseArgs(); -const version = versionOverride ?? (await readVersion()); +const version = versionOverride ?? (await Bun.file(WRAPPER_PKG_PATH).json()).version; console.log( `Publishing version ${version}${tag ? ` (tag: ${tag})` : ""}${dryRun ? " (dry run)" : ""}`, ); @@ -152,27 +139,11 @@ if (!tag && !dryRun) { console.log(`Tag ${tagName} already exists, skipping.`); } else { console.log(`Creating tag ${tagName}...`); - const tagResult = Bun.spawnSync(["git", "tag", tagName], { - stdio: ["ignore", "pipe", "pipe"], - }); - if (tagResult.exitCode !== 0) { - throw new Error(`git tag failed: ${tagResult.stderr.toString().trim()}`); - } - - const pushResult = Bun.spawnSync(["git", "push", "origin", tagName], { - stdio: ["ignore", "pipe", "pipe"], - }); - if (pushResult.exitCode !== 0) { - throw new Error(`git push tag failed: ${pushResult.stderr.toString().trim()}`); - } + run(["git", "tag", tagName]); + run(["git", "push", "origin", tagName]); console.log(`Creating GitHub Release for ${tagName}...`); - const releaseResult = Bun.spawnSync(["gh", "release", "create", tagName, "--generate-notes"], { - stdio: ["ignore", "pipe", "pipe"], - }); - if (releaseResult.exitCode !== 0) { - throw new Error(`gh release create failed: ${releaseResult.stderr.toString().trim()}`); - } + run(["gh", "release", "create", tagName, "--generate-notes"]); } } From 0847f2688e4499ba8927e524ecaf17482bebc9a4 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 19 Mar 2026 16:35:47 -0600 Subject: [PATCH 44/44] refactor: standardize arg parsing with node:util parseArgs, co-locate verify patterns --- scripts/build.ts | 42 ++++++++++--------------------------- scripts/releaser/index.ts | 26 +++++++++++------------ scripts/releaser/targets.ts | 42 +++++++++++++++++++++++++++++++++---- scripts/snapshot.ts | 21 ++++++++----------- 4 files changed, 70 insertions(+), 61 deletions(-) diff --git a/scripts/build.ts b/scripts/build.ts index 97f6bde0e..871efbfa6 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -1,37 +1,18 @@ import { mkdir } from "node:fs/promises"; import { join } from "node:path"; +import { parseArgs } from "node:util"; import { targets } from "./releaser/targets.ts"; -const VERIFY_PATTERNS: Record = { - "darwin-arm64": /Mach-O.*arm64/, - "darwin-x64": /Mach-O.*x86_64/, - "linux-arm64": /ELF.*ARM aarch64/, - "linux-arm64-musl": /ELF.*ARM aarch64/, - "linux-x64": /ELF.*x86-64/, - "linux-x64-musl": /ELF.*x86-64/, - "win32-arm64": /PE32\+.*Aarch64/, - "win32-x64": /PE32\+.*x86-64/, -}; +const { values } = parseArgs({ + args: Bun.argv.slice(2), + options: { + target: { type: "string" }, + version: { type: "string", default: "0.0.0-dev" }, + }, +}); -function getArg(args: string[], name: string): string | undefined { - for (const arg of args) { - if (arg.startsWith(`${name}=`)) return arg.slice(name.length + 1); - } - const idx = args.indexOf(name); - if (idx === -1) return undefined; - const value = args[idx + 1]; - if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`); - return value; -} - -function parseArgs(): { targetFilter?: string; version: string } { - const args = process.argv.slice(2); - const targetFilter = getArg(args, "--target"); - const version = getArg(args, "--version") ?? "0.0.0-dev"; - return { targetFilter, version }; -} - -const { targetFilter, version } = parseArgs(); +const targetFilter = values.target; +const version = values.version!; const selectedTargets = targetFilter ? targets.filter((t) => t.bunTarget === targetFilter || t.name === targetFilter) @@ -78,8 +59,7 @@ for (const target of selectedTargets) { // Verify binary format const fileResult = Bun.spawnSync(["file", outFile], { stdio: ["ignore", "pipe", "pipe"] }); const fileOutput = fileResult.stdout.toString(); - const pattern = VERIFY_PATTERNS[target.name]; - if (!pattern || !pattern.test(fileOutput)) { + if (!target.verifyPattern.test(fileOutput)) { console.error(` FAIL: binary format mismatch for ${target.name}`); console.error(` file output: ${fileOutput.trim()}`); failed = true; diff --git a/scripts/releaser/index.ts b/scripts/releaser/index.ts index fdd14749b..71b0f648f 100644 --- a/scripts/releaser/index.ts +++ b/scripts/releaser/index.ts @@ -1,5 +1,6 @@ import { mkdir, cp, rm, chmod, copyFile } from "node:fs/promises"; import { join } from "node:path"; +import { parseArgs } from "node:util"; import { type Target, targets, SCOPE, PKG_PREFIX } from "./targets.ts"; import { isPublished } from "../lib/npm.ts"; @@ -20,19 +21,16 @@ function run(cmd: string[], opts?: { cwd?: string }): void { } } -function parseArgs(): { dryRun: boolean; tag?: string; versionOverride?: string } { - const args = process.argv.slice(2); - const dryRun = args.includes("--dry-run"); - - const tagIdx = args.indexOf("--tag"); - const tag = tagIdx !== -1 ? args[tagIdx + 1] : undefined; - if (tagIdx !== -1 && !tag) throw new Error("--tag requires a value"); - - const versionIdx = args.indexOf("--version"); - const versionOverride = versionIdx !== -1 ? args[versionIdx + 1] : undefined; - if (versionIdx !== -1 && !versionOverride) throw new Error("--version requires a value"); - - return { dryRun, tag, versionOverride }; +function parseCliArgs(): { dryRun: boolean; tag?: string; versionOverride?: string } { + const { values } = parseArgs({ + args: Bun.argv.slice(2), + options: { + "dry-run": { type: "boolean", default: false }, + tag: { type: "string" }, + version: { type: "string" }, + }, + }); + return { dryRun: values["dry-run"]!, tag: values.tag, versionOverride: values.version }; } function packageName(targetName: string): string { @@ -82,7 +80,7 @@ function publish(dir: string, dryRun: boolean, tag?: string): void { run(flags, { cwd: dir }); } -const { dryRun, tag, versionOverride } = parseArgs(); +const { dryRun, tag, versionOverride } = parseCliArgs(); const version = versionOverride ?? (await Bun.file(WRAPPER_PKG_PATH).json()).version; console.log( `Publishing version ${version}${tag ? ` (tag: ${tag})` : ""}${dryRun ? " (dry run)" : ""}`, diff --git a/scripts/releaser/targets.ts b/scripts/releaser/targets.ts index 221f45ad0..4c24fc0dd 100644 --- a/scripts/releaser/targets.ts +++ b/scripts/releaser/targets.ts @@ -5,14 +5,30 @@ export interface Target { cpu: string; libc?: string; ext: string; + /** Regex to verify the compiled binary format via `file` output. */ + verifyPattern: RegExp; } // Target names use Node.js ${process.platform}-${process.arch} convention so the wrapper // shim (packages/cli/bin/clerk) can derive package names without a lookup table. // Used by scripts/build.ts and scripts/releaser/index.ts. export const targets: Target[] = [ - { name: "darwin-arm64", bunTarget: "bun-darwin-arm64", os: "darwin", cpu: "arm64", ext: "" }, - { name: "darwin-x64", bunTarget: "bun-darwin-x64", os: "darwin", cpu: "x64", ext: "" }, + { + name: "darwin-arm64", + bunTarget: "bun-darwin-arm64", + os: "darwin", + cpu: "arm64", + ext: "", + verifyPattern: /Mach-O.*arm64/, + }, + { + name: "darwin-x64", + bunTarget: "bun-darwin-x64", + os: "darwin", + cpu: "x64", + ext: "", + verifyPattern: /Mach-O.*x86_64/, + }, { name: "linux-arm64", bunTarget: "bun-linux-arm64", @@ -20,6 +36,7 @@ export const targets: Target[] = [ cpu: "arm64", libc: "glibc", ext: "", + verifyPattern: /ELF.*ARM aarch64/, }, { name: "linux-arm64-musl", @@ -28,6 +45,7 @@ export const targets: Target[] = [ cpu: "arm64", libc: "musl", ext: "", + verifyPattern: /ELF.*ARM aarch64/, }, { name: "linux-x64", @@ -36,6 +54,7 @@ export const targets: Target[] = [ cpu: "x64", libc: "glibc", ext: "", + verifyPattern: /ELF.*x86-64/, }, { name: "linux-x64-musl", @@ -44,9 +63,24 @@ export const targets: Target[] = [ cpu: "x64", libc: "musl", ext: "", + verifyPattern: /ELF.*x86-64/, + }, + { + name: "win32-arm64", + bunTarget: "bun-windows-arm64", + os: "win32", + cpu: "arm64", + ext: ".exe", + verifyPattern: /PE32\+.*Aarch64/, + }, + { + name: "win32-x64", + bunTarget: "bun-windows-x64", + os: "win32", + cpu: "x64", + ext: ".exe", + verifyPattern: /PE32\+.*x86-64/, }, - { name: "win32-arm64", bunTarget: "bun-windows-arm64", os: "win32", cpu: "arm64", ext: ".exe" }, - { name: "win32-x64", bunTarget: "bun-windows-x64", os: "win32", cpu: "x64", ext: ".exe" }, ]; export const SCOPE = "@clerk"; diff --git a/scripts/snapshot.ts b/scripts/snapshot.ts index 590a1ede8..11832cad9 100644 --- a/scripts/snapshot.ts +++ b/scripts/snapshot.ts @@ -1,21 +1,18 @@ import { join } from "node:path"; +import { parseArgs } from "node:util"; const CHANGESET_CONFIG = join(import.meta.dir, "../.changeset/config.json"); const WRAPPER_PKG = join(import.meta.dir, "../packages/cli/package.json"); -// Parse --name flag -function parseName(): string { - const args = process.argv.slice(2); - const nameIdx = args.indexOf("--name"); - if (nameIdx !== -1 && args[nameIdx + 1]) { - return args[nameIdx + 1]; - } - // Also accept positional: bun run scripts/snapshot.ts my-feature - const positional = args.find((a) => !a.startsWith("-")); - return positional || "snapshot"; -} +const { values, positionals } = parseArgs({ + args: Bun.argv.slice(2), + options: { + name: { type: "string" }, + }, + allowPositionals: true, +}); -const name = parseName(); +const name = values.name || positionals[0] || "snapshot"; // Validate kebab-case if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name)) {