Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ This is a Bun workspace monorepo:
- `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.
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, pre-release install methods, and PR guidelines.

## Bun

Expand Down
129 changes: 129 additions & 0 deletions CONTRIBUTING.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
# Contributing

When contributing to this repository, please first discuss the change you wish to make via issue or any other method with the owners of this repository before making a change.

## Developing locally

### Prerequisites

- [Bun](https://bun.sh/) (latest version)
- [Git](https://git-scm.com/)

### Setting up your local environment

1. Clone the repo

```sh
git clone https://github.com/clerk/cli-new
```

2. Install dependencies

```sh
cd cli-new
bun install
```

3. Run the CLI from source (fastest, no compilation)

```sh
bun run dev
```

4. Or compile a native binary and run it

```sh
bun run start -- --help
```

### Installing pre-release versions

In addition to running from source, you can install pre-release builds published to npm.

**Canary** — published automatically on every push to `main` that does not trigger a stable release:

```sh
npm install -g clerk@canary
```

**Snapshot** — published on-demand from PR branches by commenting `!snapshot` (or `!snapshot <name>`) on a pull request. The commenter must be a member or owner of the repository's organization. The exact version to install is posted as a PR comment after publishing:

```sh
npm install -g clerk@<version>
```

See [docs/releasing.md](docs/releasing.md) for full details on release channels and version formats.

### CI checks

After modifying files, run these commands to match what CI enforces on pull requests:

```sh
bun run format # Format with oxfmt (writes changes)
bun run lint # Lint with oxlint
bun test # Run all tests
```

### Writing tests

When changing functionality or adding new code, add or update tests to verify the new behavior. Tests use Bun's built-in test runner:

```sh
bun test
```

Check for existing `*.test.ts` files near the code you're modifying.

## Opening a pull request

1. Search for open or closed [pull requests](https://github.com/clerk/cli-new/pulls) that relate to your submission to avoid duplicating effort
2. Create your feature branch (`git checkout -b feat/amazing_feature`)
3. Write tests to verify your change
4. If your change affects user-facing behavior, add a changeset (`bunx changeset`)
5. Commit your changes using [conventional commits](https://www.conventionalcommits.org/) (`git commit -m 'feat: add amazing feature'`)
6. Push to the branch (`git push origin feat/amazing_feature`)
7. [Open a pull request](https://github.com/clerk/cli-new/compare?expand=1)

### Changesets

We use [Changesets](https://github.com/changesets/changesets) for versioning and changelog generation. When your PR 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.

If your change is internal-only (CI, tests, docs, refactoring), you can skip the changeset.

For more details, see [Adding a Changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

### Commit messages

All commit messages must follow the [conventional commits](https://www.conventionalcommits.org/) specification:

```
<type>[optional scope]: <description>
```

Common types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `ci`, `build`, `perf`.

### Notes on pull requests

- Prefer multiple small PRs with related changes over large PRs
- Always include a description explaining what the PR does and why
- For bug fixes, include steps to reproduce the issue or a screen recording

## Issues and feature requests

Found a bug or want to suggest a feature? [Submit an issue on GitHub](https://github.com/clerk/cli-new/issues). Before creating an issue, search the issue archive to avoid duplicates.

## Publishing packages

_Note: Only Clerk employees can publish packages._

See [docs/releasing.md](docs/releasing.md) for the full release flow.

## License

By contributing to Clerk, you agree that your contributions will be licensed under its [MIT License](LICENSE).
21 changes: 21 additions & 0 deletions LICENSE
Original file line numberDiff line numberDiff line change
@@ -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.
8 changes: 8 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,14 @@

The Clerk command-line interface.

## Installation

```sh
npm install -g clerk
```

## Usage

```
Usage: clerk [options] [command]

Expand Down
204 changes: 204 additions & 0 deletions docs/releasing.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
# Release Flow

This document describes how the Clerk CLI is built, versioned, and published.

## Overview

```
push to main
→ 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 stable release needed) canary.ts versions packages
→ build → smoke-test subset → publish @canary

PR comment "!snapshot [name]"
→ 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

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`, 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.

## Release Channels

### Stable (`@latest`)

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. `scripts/canary.ts` uses Changesets snapshot mode to produce versions in the format `x.y.z-canary.v<YYYYMMDDHHmmss>` (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 <name>`) 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-<name>.v<YYYYMMDDHHmmss>` (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>` (version is posted as a PR comment after publishing)

## Versioning

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:

- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages)

## Build Pipeline

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

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=<bun_target>`
2. Injects the version via `--define "CLI_VERSION=\"$CLI_VERSION\""`
3. Verifies the binary format using `file` output
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.

Publishing and GitHub Release upload are gated on all smoke tests passing.

### 3. Publish npm Job

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)
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 <tag>` -- publish with a specific npm dist-tag (e.g., `canary`, `snapshot`); defaults to `latest`
- `--version <version>` -- override the version read from `package.json`

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`)
- `GH_TOKEN` -- GitHub token used by the releaser to create tags and releases (stable only)

#### 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

Attaches the compiled binaries to the GitHub Release for direct download. Binaries are uploaded with display names following the `clerk-<target>` 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 (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 |
| `.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 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 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

```sh
# 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
```

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:

```sh
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.
- **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.
- **CI build check**: Every PR to `main` runs a JS bundle build to catch bundler-specific failures before merge.
6 changes: 5 additions & 1 deletion scripts/releaser/index.ts
Original file line numberDiff line numberDiff line change
@@ -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 { parseArgs } from "node:util";
import { type Target, targets, SCOPE, PKG_PREFIX } from "./targets.ts";
Expand DownExpand Up@@ -66,6 +66,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"));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return dir;
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ This is a Bun workspace monorepo:
- `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.
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, pre-release install methods, and PR guidelines.

## Bun

Expand Down
129 changes: 129 additions & 0 deletions CONTRIBUTING.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
# Contributing

When contributing to this repository, please first discuss the change you wish to make via issue or any other method with the owners of this repository before making a change.

## Developing locally

### Prerequisites

- [Bun](https://bun.sh/) (latest version)
- [Git](https://git-scm.com/)

### Setting up your local environment

1. Clone the repo

```sh
git clone https://github.com/clerk/cli-new
```

2. Install dependencies

```sh
cd cli-new
bun install
```

3. Run the CLI from source (fastest, no compilation)

```sh
bun run dev
```

4. Or compile a native binary and run it

```sh
bun run start -- --help
```

### Installing pre-release versions

In addition to running from source, you can install pre-release builds published to npm.

**Canary** — published automatically on every push to `main` that does not trigger a stable release:

```sh
npm install -g clerk@canary
```

**Snapshot** — published on-demand from PR branches by commenting `!snapshot` (or `!snapshot <name>`) on a pull request. The commenter must be a member or owner of the repository's organization. The exact version to install is posted as a PR comment after publishing:

```sh
npm install -g clerk@<version>
```

See [docs/releasing.md](docs/releasing.md) for full details on release channels and version formats.

### CI checks

After modifying files, run these commands to match what CI enforces on pull requests:

```sh
bun run format # Format with oxfmt (writes changes)
bun run lint # Lint with oxlint
bun test # Run all tests
```

### Writing tests

When changing functionality or adding new code, add or update tests to verify the new behavior. Tests use Bun's built-in test runner:

```sh
bun test
```

Check for existing `*.test.ts` files near the code you're modifying.

## Opening a pull request

1. Search for open or closed [pull requests](https://github.com/clerk/cli-new/pulls) that relate to your submission to avoid duplicating effort
2. Create your feature branch (`git checkout -b feat/amazing_feature`)
3. Write tests to verify your change
4. If your change affects user-facing behavior, add a changeset (`bunx changeset`)
5. Commit your changes using [conventional commits](https://www.conventionalcommits.org/) (`git commit -m 'feat: add amazing feature'`)
6. Push to the branch (`git push origin feat/amazing_feature`)
7. [Open a pull request](https://github.com/clerk/cli-new/compare?expand=1)

### Changesets

We use [Changesets](https://github.com/changesets/changesets) for versioning and changelog generation. When your PR 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.

If your change is internal-only (CI, tests, docs, refactoring), you can skip the changeset.

For more details, see [Adding a Changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

### Commit messages

All commit messages must follow the [conventional commits](https://www.conventionalcommits.org/) specification:

```
<type>[optional scope]: <description>
```

Common types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `ci`, `build`, `perf`.

### Notes on pull requests

- Prefer multiple small PRs with related changes over large PRs
- Always include a description explaining what the PR does and why
- For bug fixes, include steps to reproduce the issue or a screen recording

## Issues and feature requests

Found a bug or want to suggest a feature? [Submit an issue on GitHub](https://github.com/clerk/cli-new/issues). Before creating an issue, search the issue archive to avoid duplicates.

## Publishing packages

_Note: Only Clerk employees can publish packages._

See [docs/releasing.md](docs/releasing.md) for the full release flow.

## License

By contributing to Clerk, you agree that your contributions will be licensed under its [MIT License](LICENSE).
21 changes: 21 additions & 0 deletions LICENSE
Original file line numberDiff line numberDiff line change
@@ -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.
8 changes: 8 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,14 @@

The Clerk command-line interface.

## Installation

```sh
npm install -g clerk
```

## Usage

```
Usage: clerk [options] [command]

Expand Down
204 changes: 204 additions & 0 deletions docs/releasing.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
# Release Flow

This document describes how the Clerk CLI is built, versioned, and published.

## Overview

```
push to main
→ 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 stable release needed) canary.ts versions packages
→ build → smoke-test subset → publish @canary

PR comment "!snapshot [name]"
→ 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

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`, 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.

## Release Channels

### Stable (`@latest`)

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. `scripts/canary.ts` uses Changesets snapshot mode to produce versions in the format `x.y.z-canary.v<YYYYMMDDHHmmss>` (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 <name>`) 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-<name>.v<YYYYMMDDHHmmss>` (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>` (version is posted as a PR comment after publishing)

## Versioning

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:

- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages)

## Build Pipeline

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

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=<bun_target>`
2. Injects the version via `--define "CLI_VERSION=\"$CLI_VERSION\""`
3. Verifies the binary format using `file` output
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.

Publishing and GitHub Release upload are gated on all smoke tests passing.

### 3. Publish npm Job

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)
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 <tag>` -- publish with a specific npm dist-tag (e.g., `canary`, `snapshot`); defaults to `latest`
- `--version <version>` -- override the version read from `package.json`

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`)
- `GH_TOKEN` -- GitHub token used by the releaser to create tags and releases (stable only)

#### 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

Attaches the compiled binaries to the GitHub Release for direct download. Binaries are uploaded with display names following the `clerk-<target>` 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 (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 |
| `.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 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 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

```sh
# 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
```

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:

```sh
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.
- **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.
- **CI build check**: Every PR to `main` runs a JS bundle build to catch bundler-specific failures before merge.
6 changes: 5 additions & 1 deletion scripts/releaser/index.ts
Original file line numberDiff line numberDiff line change
@@ -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 { parseArgs } from "node:util";
import { type Target, targets, SCOPE, PKG_PREFIX } from "./targets.ts";
Expand DownExpand Up@@ -66,6 +66,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"));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return dir;
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ This is a Bun workspace monorepo:
- `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.
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, pre-release install methods, and PR guidelines.

## Bun

Expand Down
129 changes: 129 additions & 0 deletions CONTRIBUTING.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
# Contributing

When contributing to this repository, please first discuss the change you wish to make via issue or any other method with the owners of this repository before making a change.

## Developing locally

### Prerequisites

- [Bun](https://bun.sh/) (latest version)
- [Git](https://git-scm.com/)

### Setting up your local environment

1. Clone the repo

```sh
git clone https://github.com/clerk/cli-new
```

2. Install dependencies

```sh
cd cli-new
bun install
```

3. Run the CLI from source (fastest, no compilation)

```sh
bun run dev
```

4. Or compile a native binary and run it

```sh
bun run start -- --help
```

### Installing pre-release versions

In addition to running from source, you can install pre-release builds published to npm.

**Canary** — published automatically on every push to `main` that does not trigger a stable release:

```sh
npm install -g clerk@canary
```

**Snapshot** — published on-demand from PR branches by commenting `!snapshot` (or `!snapshot <name>`) on a pull request. The commenter must be a member or owner of the repository's organization. The exact version to install is posted as a PR comment after publishing:

```sh
npm install -g clerk@<version>
```

See [docs/releasing.md](docs/releasing.md) for full details on release channels and version formats.

### CI checks

After modifying files, run these commands to match what CI enforces on pull requests:

```sh
bun run format # Format with oxfmt (writes changes)
bun run lint # Lint with oxlint
bun test # Run all tests
```

### Writing tests

When changing functionality or adding new code, add or update tests to verify the new behavior. Tests use Bun's built-in test runner:

```sh
bun test
```

Check for existing `*.test.ts` files near the code you're modifying.

## Opening a pull request

1. Search for open or closed [pull requests](https://github.com/clerk/cli-new/pulls) that relate to your submission to avoid duplicating effort
2. Create your feature branch (`git checkout -b feat/amazing_feature`)
3. Write tests to verify your change
4. If your change affects user-facing behavior, add a changeset (`bunx changeset`)
5. Commit your changes using [conventional commits](https://www.conventionalcommits.org/) (`git commit -m 'feat: add amazing feature'`)
6. Push to the branch (`git push origin feat/amazing_feature`)
7. [Open a pull request](https://github.com/clerk/cli-new/compare?expand=1)

### Changesets

We use [Changesets](https://github.com/changesets/changesets) for versioning and changelog generation. When your PR 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.

If your change is internal-only (CI, tests, docs, refactoring), you can skip the changeset.

For more details, see [Adding a Changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

### Commit messages

All commit messages must follow the [conventional commits](https://www.conventionalcommits.org/) specification:

```
<type>[optional scope]: <description>
```

Common types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `ci`, `build`, `perf`.

### Notes on pull requests

- Prefer multiple small PRs with related changes over large PRs
- Always include a description explaining what the PR does and why
- For bug fixes, include steps to reproduce the issue or a screen recording

## Issues and feature requests

Found a bug or want to suggest a feature? [Submit an issue on GitHub](https://github.com/clerk/cli-new/issues). Before creating an issue, search the issue archive to avoid duplicates.

## Publishing packages

_Note: Only Clerk employees can publish packages._

See [docs/releasing.md](docs/releasing.md) for the full release flow.

## License

By contributing to Clerk, you agree that your contributions will be licensed under its [MIT License](LICENSE).
21 changes: 21 additions & 0 deletions LICENSE
Original file line numberDiff line numberDiff line change
@@ -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.
8 changes: 8 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,14 @@

The Clerk command-line interface.

## Installation

```sh
npm install -g clerk
```

## Usage

```
Usage: clerk [options] [command]

Expand Down
204 changes: 204 additions & 0 deletions docs/releasing.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
# Release Flow

This document describes how the Clerk CLI is built, versioned, and published.

## Overview

```
push to main
→ 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 stable release needed) canary.ts versions packages
→ build → smoke-test subset → publish @canary

PR comment "!snapshot [name]"
→ 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

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`, 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.

## Release Channels

### Stable (`@latest`)

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. `scripts/canary.ts` uses Changesets snapshot mode to produce versions in the format `x.y.z-canary.v<YYYYMMDDHHmmss>` (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 <name>`) 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-<name>.v<YYYYMMDDHHmmss>` (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>` (version is posted as a PR comment after publishing)

## Versioning

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:

- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages)

## Build Pipeline

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

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=<bun_target>`
2. Injects the version via `--define "CLI_VERSION=\"$CLI_VERSION\""`
3. Verifies the binary format using `file` output
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.

Publishing and GitHub Release upload are gated on all smoke tests passing.

### 3. Publish npm Job

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)
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 <tag>` -- publish with a specific npm dist-tag (e.g., `canary`, `snapshot`); defaults to `latest`
- `--version <version>` -- override the version read from `package.json`

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`)
- `GH_TOKEN` -- GitHub token used by the releaser to create tags and releases (stable only)

#### 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

Attaches the compiled binaries to the GitHub Release for direct download. Binaries are uploaded with display names following the `clerk-<target>` 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 (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 |
| `.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 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 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

```sh
# 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
```

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:

```sh
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.
- **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.
- **CI build check**: Every PR to `main` runs a JS bundle build to catch bundler-specific failures before merge.
6 changes: 5 additions & 1 deletion scripts/releaser/index.ts
Original file line numberDiff line numberDiff line change
@@ -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 { parseArgs } from "node:util";
import { type Target, targets, SCOPE, PKG_PREFIX } from "./targets.ts";
Expand DownExpand Up@@ -66,6 +66,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"));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return dir;
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ This is a Bun workspace monorepo:
- `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.
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, pre-release install methods, and PR guidelines.

## Bun

Expand Down
129 changes: 129 additions & 0 deletions CONTRIBUTING.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
# Contributing

When contributing to this repository, please first discuss the change you wish to make via issue or any other method with the owners of this repository before making a change.

## Developing locally

### Prerequisites

- [Bun](https://bun.sh/) (latest version)
- [Git](https://git-scm.com/)

### Setting up your local environment

1. Clone the repo

```sh
git clone https://github.com/clerk/cli-new
```

2. Install dependencies

```sh
cd cli-new
bun install
```

3. Run the CLI from source (fastest, no compilation)

```sh
bun run dev
```

4. Or compile a native binary and run it

```sh
bun run start -- --help
```

### Installing pre-release versions

In addition to running from source, you can install pre-release builds published to npm.

**Canary** — published automatically on every push to `main` that does not trigger a stable release:

```sh
npm install -g clerk@canary
```

**Snapshot** — published on-demand from PR branches by commenting `!snapshot` (or `!snapshot <name>`) on a pull request. The commenter must be a member or owner of the repository's organization. The exact version to install is posted as a PR comment after publishing:

```sh
npm install -g clerk@<version>
```

See [docs/releasing.md](docs/releasing.md) for full details on release channels and version formats.

### CI checks

After modifying files, run these commands to match what CI enforces on pull requests:

```sh
bun run format # Format with oxfmt (writes changes)
bun run lint # Lint with oxlint
bun test # Run all tests
```

### Writing tests

When changing functionality or adding new code, add or update tests to verify the new behavior. Tests use Bun's built-in test runner:

```sh
bun test
```

Check for existing `*.test.ts` files near the code you're modifying.

## Opening a pull request

1. Search for open or closed [pull requests](https://github.com/clerk/cli-new/pulls) that relate to your submission to avoid duplicating effort
2. Create your feature branch (`git checkout -b feat/amazing_feature`)
3. Write tests to verify your change
4. If your change affects user-facing behavior, add a changeset (`bunx changeset`)
5. Commit your changes using [conventional commits](https://www.conventionalcommits.org/) (`git commit -m 'feat: add amazing feature'`)
6. Push to the branch (`git push origin feat/amazing_feature`)
7. [Open a pull request](https://github.com/clerk/cli-new/compare?expand=1)

### Changesets

We use [Changesets](https://github.com/changesets/changesets) for versioning and changelog generation. When your PR 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.

If your change is internal-only (CI, tests, docs, refactoring), you can skip the changeset.

For more details, see [Adding a Changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

### Commit messages

All commit messages must follow the [conventional commits](https://www.conventionalcommits.org/) specification:

```
<type>[optional scope]: <description>
```

Common types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `ci`, `build`, `perf`.

### Notes on pull requests

- Prefer multiple small PRs with related changes over large PRs
- Always include a description explaining what the PR does and why
- For bug fixes, include steps to reproduce the issue or a screen recording

## Issues and feature requests

Found a bug or want to suggest a feature? [Submit an issue on GitHub](https://github.com/clerk/cli-new/issues). Before creating an issue, search the issue archive to avoid duplicates.

## Publishing packages

_Note: Only Clerk employees can publish packages._

See [docs/releasing.md](docs/releasing.md) for the full release flow.

## License

By contributing to Clerk, you agree that your contributions will be licensed under its [MIT License](LICENSE).
21 changes: 21 additions & 0 deletions LICENSE
Original file line numberDiff line numberDiff line change
@@ -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.
8 changes: 8 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,14 @@

The Clerk command-line interface.

## Installation

```sh
npm install -g clerk
```

## Usage

```
Usage: clerk [options] [command]

Expand Down
204 changes: 204 additions & 0 deletions docs/releasing.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
# Release Flow

This document describes how the Clerk CLI is built, versioned, and published.

## Overview

```
push to main
→ 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 stable release needed) canary.ts versions packages
→ build → smoke-test subset → publish @canary

PR comment "!snapshot [name]"
→ 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

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`, 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.

## Release Channels

### Stable (`@latest`)

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. `scripts/canary.ts` uses Changesets snapshot mode to produce versions in the format `x.y.z-canary.v<YYYYMMDDHHmmss>` (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 <name>`) 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-<name>.v<YYYYMMDDHHmmss>` (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>` (version is posted as a PR comment after publishing)

## Versioning

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:

- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages)

## Build Pipeline

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

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=<bun_target>`
2. Injects the version via `--define "CLI_VERSION=\"$CLI_VERSION\""`
3. Verifies the binary format using `file` output
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.

Publishing and GitHub Release upload are gated on all smoke tests passing.

### 3. Publish npm Job

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)
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 <tag>` -- publish with a specific npm dist-tag (e.g., `canary`, `snapshot`); defaults to `latest`
- `--version <version>` -- override the version read from `package.json`

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`)
- `GH_TOKEN` -- GitHub token used by the releaser to create tags and releases (stable only)

#### 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

Attaches the compiled binaries to the GitHub Release for direct download. Binaries are uploaded with display names following the `clerk-<target>` 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 (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 |
| `.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 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 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

```sh
# 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
```

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:

```sh
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.
- **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.
- **CI build check**: Every PR to `main` runs a JS bundle build to catch bundler-specific failures before merge.
6 changes: 5 additions & 1 deletion scripts/releaser/index.ts
Original file line numberDiff line numberDiff line change
@@ -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 { parseArgs } from "node:util";
import { type Target, targets, SCOPE, PKG_PREFIX } from "./targets.ts";
Expand DownExpand Up@@ -66,6 +66,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"));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return dir;
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ This is a Bun workspace monorepo:
- `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.
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, pre-release install methods, and PR guidelines.

## Bun

Expand Down
129 changes: 129 additions & 0 deletions CONTRIBUTING.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
# Contributing

When contributing to this repository, please first discuss the change you wish to make via issue or any other method with the owners of this repository before making a change.

## Developing locally

### Prerequisites

- [Bun](https://bun.sh/) (latest version)
- [Git](https://git-scm.com/)

### Setting up your local environment

1. Clone the repo

```sh
git clone https://github.com/clerk/cli-new
```

2. Install dependencies

```sh
cd cli-new
bun install
```

3. Run the CLI from source (fastest, no compilation)

```sh
bun run dev
```

4. Or compile a native binary and run it

```sh
bun run start -- --help
```

### Installing pre-release versions

In addition to running from source, you can install pre-release builds published to npm.

**Canary** — published automatically on every push to `main` that does not trigger a stable release:

```sh
npm install -g clerk@canary
```

**Snapshot** — published on-demand from PR branches by commenting `!snapshot` (or `!snapshot <name>`) on a pull request. The commenter must be a member or owner of the repository's organization. The exact version to install is posted as a PR comment after publishing:

```sh
npm install -g clerk@<version>
```

See [docs/releasing.md](docs/releasing.md) for full details on release channels and version formats.

### CI checks

After modifying files, run these commands to match what CI enforces on pull requests:

```sh
bun run format # Format with oxfmt (writes changes)
bun run lint # Lint with oxlint
bun test # Run all tests
```

### Writing tests

When changing functionality or adding new code, add or update tests to verify the new behavior. Tests use Bun's built-in test runner:

```sh
bun test
```

Check for existing `*.test.ts` files near the code you're modifying.

## Opening a pull request

1. Search for open or closed [pull requests](https://github.com/clerk/cli-new/pulls) that relate to your submission to avoid duplicating effort
2. Create your feature branch (`git checkout -b feat/amazing_feature`)
3. Write tests to verify your change
4. If your change affects user-facing behavior, add a changeset (`bunx changeset`)
5. Commit your changes using [conventional commits](https://www.conventionalcommits.org/) (`git commit -m 'feat: add amazing feature'`)
6. Push to the branch (`git push origin feat/amazing_feature`)
7. [Open a pull request](https://github.com/clerk/cli-new/compare?expand=1)

### Changesets

We use [Changesets](https://github.com/changesets/changesets) for versioning and changelog generation. When your PR 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.

If your change is internal-only (CI, tests, docs, refactoring), you can skip the changeset.

For more details, see [Adding a Changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

### Commit messages

All commit messages must follow the [conventional commits](https://www.conventionalcommits.org/) specification:

```
<type>[optional scope]: <description>
```

Common types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `ci`, `build`, `perf`.

### Notes on pull requests

- Prefer multiple small PRs with related changes over large PRs
- Always include a description explaining what the PR does and why
- For bug fixes, include steps to reproduce the issue or a screen recording

## Issues and feature requests

Found a bug or want to suggest a feature? [Submit an issue on GitHub](https://github.com/clerk/cli-new/issues). Before creating an issue, search the issue archive to avoid duplicates.

## Publishing packages

_Note: Only Clerk employees can publish packages._

See [docs/releasing.md](docs/releasing.md) for the full release flow.

## License

By contributing to Clerk, you agree that your contributions will be licensed under its [MIT License](LICENSE).
21 changes: 21 additions & 0 deletions LICENSE
Original file line numberDiff line numberDiff line change
@@ -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.
8 changes: 8 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,14 @@

The Clerk command-line interface.

## Installation

```sh
npm install -g clerk
```

## Usage

```
Usage: clerk [options] [command]

Expand Down
204 changes: 204 additions & 0 deletions docs/releasing.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
# Release Flow

This document describes how the Clerk CLI is built, versioned, and published.

## Overview

```
push to main
→ 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 stable release needed) canary.ts versions packages
→ build → smoke-test subset → publish @canary

PR comment "!snapshot [name]"
→ 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

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`, 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.

## Release Channels

### Stable (`@latest`)

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. `scripts/canary.ts` uses Changesets snapshot mode to produce versions in the format `x.y.z-canary.v<YYYYMMDDHHmmss>` (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 <name>`) 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-<name>.v<YYYYMMDDHHmmss>` (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>` (version is posted as a PR comment after publishing)

## Versioning

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:

- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages)

## Build Pipeline

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

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=<bun_target>`
2. Injects the version via `--define "CLI_VERSION=\"$CLI_VERSION\""`
3. Verifies the binary format using `file` output
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.

Publishing and GitHub Release upload are gated on all smoke tests passing.

### 3. Publish npm Job

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)
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 <tag>` -- publish with a specific npm dist-tag (e.g., `canary`, `snapshot`); defaults to `latest`
- `--version <version>` -- override the version read from `package.json`

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`)
- `GH_TOKEN` -- GitHub token used by the releaser to create tags and releases (stable only)

#### 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

Attaches the compiled binaries to the GitHub Release for direct download. Binaries are uploaded with display names following the `clerk-<target>` 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 (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 |
| `.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 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 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

```sh
# 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
```

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:

```sh
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.
- **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.
- **CI build check**: Every PR to `main` runs a JS bundle build to catch bundler-specific failures before merge.
6 changes: 5 additions & 1 deletion scripts/releaser/index.ts
Original file line numberDiff line numberDiff line change
@@ -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 { parseArgs } from "node:util";
import { type Target, targets, SCOPE, PKG_PREFIX } from "./targets.ts";
Expand DownExpand Up@@ -66,6 +66,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"));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return dir;
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ This is a Bun workspace monorepo:
- `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.
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, pre-release install methods, and PR guidelines.

## Bun

Expand Down
129 changes: 129 additions & 0 deletions CONTRIBUTING.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
# Contributing

When contributing to this repository, please first discuss the change you wish to make via issue or any other method with the owners of this repository before making a change.

## Developing locally

### Prerequisites

- [Bun](https://bun.sh/) (latest version)
- [Git](https://git-scm.com/)

### Setting up your local environment

1. Clone the repo

```sh
git clone https://github.com/clerk/cli-new
```

2. Install dependencies

```sh
cd cli-new
bun install
```

3. Run the CLI from source (fastest, no compilation)

```sh
bun run dev
```

4. Or compile a native binary and run it

```sh
bun run start -- --help
```

### Installing pre-release versions

In addition to running from source, you can install pre-release builds published to npm.

**Canary** — published automatically on every push to `main` that does not trigger a stable release:

```sh
npm install -g clerk@canary
```

**Snapshot** — published on-demand from PR branches by commenting `!snapshot` (or `!snapshot <name>`) on a pull request. The commenter must be a member or owner of the repository's organization. The exact version to install is posted as a PR comment after publishing:

```sh
npm install -g clerk@<version>
```

See [docs/releasing.md](docs/releasing.md) for full details on release channels and version formats.

### CI checks

After modifying files, run these commands to match what CI enforces on pull requests:

```sh
bun run format # Format with oxfmt (writes changes)
bun run lint # Lint with oxlint
bun test # Run all tests
```

### Writing tests

When changing functionality or adding new code, add or update tests to verify the new behavior. Tests use Bun's built-in test runner:

```sh
bun test
```

Check for existing `*.test.ts` files near the code you're modifying.

## Opening a pull request

1. Search for open or closed [pull requests](https://github.com/clerk/cli-new/pulls) that relate to your submission to avoid duplicating effort
2. Create your feature branch (`git checkout -b feat/amazing_feature`)
3. Write tests to verify your change
4. If your change affects user-facing behavior, add a changeset (`bunx changeset`)
5. Commit your changes using [conventional commits](https://www.conventionalcommits.org/) (`git commit -m 'feat: add amazing feature'`)
6. Push to the branch (`git push origin feat/amazing_feature`)
7. [Open a pull request](https://github.com/clerk/cli-new/compare?expand=1)

### Changesets

We use [Changesets](https://github.com/changesets/changesets) for versioning and changelog generation. When your PR 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.

If your change is internal-only (CI, tests, docs, refactoring), you can skip the changeset.

For more details, see [Adding a Changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

### Commit messages

All commit messages must follow the [conventional commits](https://www.conventionalcommits.org/) specification:

```
<type>[optional scope]: <description>
```

Common types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `ci`, `build`, `perf`.

### Notes on pull requests

- Prefer multiple small PRs with related changes over large PRs
- Always include a description explaining what the PR does and why
- For bug fixes, include steps to reproduce the issue or a screen recording

## Issues and feature requests

Found a bug or want to suggest a feature? [Submit an issue on GitHub](https://github.com/clerk/cli-new/issues). Before creating an issue, search the issue archive to avoid duplicates.

## Publishing packages

_Note: Only Clerk employees can publish packages._

See [docs/releasing.md](docs/releasing.md) for the full release flow.

## License

By contributing to Clerk, you agree that your contributions will be licensed under its [MIT License](LICENSE).
21 changes: 21 additions & 0 deletions LICENSE
Original file line numberDiff line numberDiff line change
@@ -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.
8 changes: 8 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,14 @@

The Clerk command-line interface.

## Installation

```sh
npm install -g clerk
```

## Usage

```
Usage: clerk [options] [command]

Expand Down
204 changes: 204 additions & 0 deletions docs/releasing.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
# Release Flow

This document describes how the Clerk CLI is built, versioned, and published.

## Overview

```
push to main
→ 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 stable release needed) canary.ts versions packages
→ build → smoke-test subset → publish @canary

PR comment "!snapshot [name]"
→ 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

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`, 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.

## Release Channels

### Stable (`@latest`)

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. `scripts/canary.ts` uses Changesets snapshot mode to produce versions in the format `x.y.z-canary.v<YYYYMMDDHHmmss>` (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 <name>`) 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-<name>.v<YYYYMMDDHHmmss>` (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>` (version is posted as a PR comment after publishing)

## Versioning

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:

- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages)

## Build Pipeline

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

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=<bun_target>`
2. Injects the version via `--define "CLI_VERSION=\"$CLI_VERSION\""`
3. Verifies the binary format using `file` output
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.

Publishing and GitHub Release upload are gated on all smoke tests passing.

### 3. Publish npm Job

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)
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 <tag>` -- publish with a specific npm dist-tag (e.g., `canary`, `snapshot`); defaults to `latest`
- `--version <version>` -- override the version read from `package.json`

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`)
- `GH_TOKEN` -- GitHub token used by the releaser to create tags and releases (stable only)

#### 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

Attaches the compiled binaries to the GitHub Release for direct download. Binaries are uploaded with display names following the `clerk-<target>` 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 (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 |
| `.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 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 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

```sh
# 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
```

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:

```sh
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.
- **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.
- **CI build check**: Every PR to `main` runs a JS bundle build to catch bundler-specific failures before merge.
6 changes: 5 additions & 1 deletion scripts/releaser/index.ts
Original file line numberDiff line numberDiff line change
@@ -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 { parseArgs } from "node:util";
import { type Target, targets, SCOPE, PKG_PREFIX } from "./targets.ts";
Expand DownExpand Up@@ -66,6 +66,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"));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return dir;
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ This is a Bun workspace monorepo:
- `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.
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, pre-release install methods, and PR guidelines.

## Bun

Expand Down
129 changes: 129 additions & 0 deletions CONTRIBUTING.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
# Contributing

When contributing to this repository, please first discuss the change you wish to make via issue or any other method with the owners of this repository before making a change.

## Developing locally

### Prerequisites

- [Bun](https://bun.sh/) (latest version)
- [Git](https://git-scm.com/)

### Setting up your local environment

1. Clone the repo

```sh
git clone https://github.com/clerk/cli-new
```

2. Install dependencies

```sh
cd cli-new
bun install
```

3. Run the CLI from source (fastest, no compilation)

```sh
bun run dev
```

4. Or compile a native binary and run it

```sh
bun run start -- --help
```

### Installing pre-release versions

In addition to running from source, you can install pre-release builds published to npm.

**Canary** — published automatically on every push to `main` that does not trigger a stable release:

```sh
npm install -g clerk@canary
```

**Snapshot** — published on-demand from PR branches by commenting `!snapshot` (or `!snapshot <name>`) on a pull request. The commenter must be a member or owner of the repository's organization. The exact version to install is posted as a PR comment after publishing:

```sh
npm install -g clerk@<version>
```

See [docs/releasing.md](docs/releasing.md) for full details on release channels and version formats.

### CI checks

After modifying files, run these commands to match what CI enforces on pull requests:

```sh
bun run format # Format with oxfmt (writes changes)
bun run lint # Lint with oxlint
bun test # Run all tests
```

### Writing tests

When changing functionality or adding new code, add or update tests to verify the new behavior. Tests use Bun's built-in test runner:

```sh
bun test
```

Check for existing `*.test.ts` files near the code you're modifying.

## Opening a pull request

1. Search for open or closed [pull requests](https://github.com/clerk/cli-new/pulls) that relate to your submission to avoid duplicating effort
2. Create your feature branch (`git checkout -b feat/amazing_feature`)
3. Write tests to verify your change
4. If your change affects user-facing behavior, add a changeset (`bunx changeset`)
5. Commit your changes using [conventional commits](https://www.conventionalcommits.org/) (`git commit -m 'feat: add amazing feature'`)
6. Push to the branch (`git push origin feat/amazing_feature`)
7. [Open a pull request](https://github.com/clerk/cli-new/compare?expand=1)

### Changesets

We use [Changesets](https://github.com/changesets/changesets) for versioning and changelog generation. When your PR 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.

If your change is internal-only (CI, tests, docs, refactoring), you can skip the changeset.

For more details, see [Adding a Changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

### Commit messages

All commit messages must follow the [conventional commits](https://www.conventionalcommits.org/) specification:

```
<type>[optional scope]: <description>
```

Common types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `ci`, `build`, `perf`.

### Notes on pull requests

- Prefer multiple small PRs with related changes over large PRs
- Always include a description explaining what the PR does and why
- For bug fixes, include steps to reproduce the issue or a screen recording

## Issues and feature requests

Found a bug or want to suggest a feature? [Submit an issue on GitHub](https://github.com/clerk/cli-new/issues). Before creating an issue, search the issue archive to avoid duplicates.

## Publishing packages

_Note: Only Clerk employees can publish packages._

See [docs/releasing.md](docs/releasing.md) for the full release flow.

## License

By contributing to Clerk, you agree that your contributions will be licensed under its [MIT License](LICENSE).
21 changes: 21 additions & 0 deletions LICENSE
Original file line numberDiff line numberDiff line change
@@ -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.
8 changes: 8 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,14 @@

The Clerk command-line interface.

## Installation

```sh
npm install -g clerk
```

## Usage

```
Usage: clerk [options] [command]

Expand Down
204 changes: 204 additions & 0 deletions docs/releasing.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
# Release Flow

This document describes how the Clerk CLI is built, versioned, and published.

## Overview

```
push to main
→ 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 stable release needed) canary.ts versions packages
→ build → smoke-test subset → publish @canary

PR comment "!snapshot [name]"
→ 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

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`, 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.

## Release Channels

### Stable (`@latest`)

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. `scripts/canary.ts` uses Changesets snapshot mode to produce versions in the format `x.y.z-canary.v<YYYYMMDDHHmmss>` (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 <name>`) 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-<name>.v<YYYYMMDDHHmmss>` (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>` (version is posted as a PR comment after publishing)

## Versioning

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:

- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages)

## Build Pipeline

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

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=<bun_target>`
2. Injects the version via `--define "CLI_VERSION=\"$CLI_VERSION\""`
3. Verifies the binary format using `file` output
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.

Publishing and GitHub Release upload are gated on all smoke tests passing.

### 3. Publish npm Job

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)
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 <tag>` -- publish with a specific npm dist-tag (e.g., `canary`, `snapshot`); defaults to `latest`
- `--version <version>` -- override the version read from `package.json`

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`)
- `GH_TOKEN` -- GitHub token used by the releaser to create tags and releases (stable only)

#### 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

Attaches the compiled binaries to the GitHub Release for direct download. Binaries are uploaded with display names following the `clerk-<target>` 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 (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 |
| `.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 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 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

```sh
# 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
```

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:

```sh
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.
- **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.
- **CI build check**: Every PR to `main` runs a JS bundle build to catch bundler-specific failures before merge.
6 changes: 5 additions & 1 deletion scripts/releaser/index.ts
Original file line numberDiff line numberDiff line change
@@ -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 { parseArgs } from "node:util";
import { type Target, targets, SCOPE, PKG_PREFIX } from "./targets.ts";
Expand DownExpand Up@@ -66,6 +66,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"));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return dir;
}

Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ This is a Bun workspace monorepo:
- `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.
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, pre-release install methods, and PR guidelines.

## Bun

Expand Down
129 changes: 129 additions & 0 deletions CONTRIBUTING.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
# Contributing

When contributing to this repository, please first discuss the change you wish to make via issue or any other method with the owners of this repository before making a change.

## Developing locally

### Prerequisites

- [Bun](https://bun.sh/) (latest version)
- [Git](https://git-scm.com/)

### Setting up your local environment

1. Clone the repo

```sh
git clone https://github.com/clerk/cli-new
```

2. Install dependencies

```sh
cd cli-new
bun install
```

3. Run the CLI from source (fastest, no compilation)

```sh
bun run dev
```

4. Or compile a native binary and run it

```sh
bun run start -- --help
```

### Installing pre-release versions

In addition to running from source, you can install pre-release builds published to npm.

**Canary** — published automatically on every push to `main` that does not trigger a stable release:

```sh
npm install -g clerk@canary
```

**Snapshot** — published on-demand from PR branches by commenting `!snapshot` (or `!snapshot <name>`) on a pull request. The commenter must be a member or owner of the repository's organization. The exact version to install is posted as a PR comment after publishing:

```sh
npm install -g clerk@<version>
```

See [docs/releasing.md](docs/releasing.md) for full details on release channels and version formats.

### CI checks

After modifying files, run these commands to match what CI enforces on pull requests:

```sh
bun run format # Format with oxfmt (writes changes)
bun run lint # Lint with oxlint
bun test # Run all tests
```

### Writing tests

When changing functionality or adding new code, add or update tests to verify the new behavior. Tests use Bun's built-in test runner:

```sh
bun test
```

Check for existing `*.test.ts` files near the code you're modifying.

## Opening a pull request

1. Search for open or closed [pull requests](https://github.com/clerk/cli-new/pulls) that relate to your submission to avoid duplicating effort
2. Create your feature branch (`git checkout -b feat/amazing_feature`)
3. Write tests to verify your change
4. If your change affects user-facing behavior, add a changeset (`bunx changeset`)
5. Commit your changes using [conventional commits](https://www.conventionalcommits.org/) (`git commit -m 'feat: add amazing feature'`)
6. Push to the branch (`git push origin feat/amazing_feature`)
7. [Open a pull request](https://github.com/clerk/cli-new/compare?expand=1)

### Changesets

We use [Changesets](https://github.com/changesets/changesets) for versioning and changelog generation. When your PR 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.

If your change is internal-only (CI, tests, docs, refactoring), you can skip the changeset.

For more details, see [Adding a Changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

### Commit messages

All commit messages must follow the [conventional commits](https://www.conventionalcommits.org/) specification:

```
<type>[optional scope]: <description>
```

Common types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `ci`, `build`, `perf`.

### Notes on pull requests

- Prefer multiple small PRs with related changes over large PRs
- Always include a description explaining what the PR does and why
- For bug fixes, include steps to reproduce the issue or a screen recording

## Issues and feature requests

Found a bug or want to suggest a feature? [Submit an issue on GitHub](https://github.com/clerk/cli-new/issues). Before creating an issue, search the issue archive to avoid duplicates.

## Publishing packages

_Note: Only Clerk employees can publish packages._

See [docs/releasing.md](docs/releasing.md) for the full release flow.

## License

By contributing to Clerk, you agree that your contributions will be licensed under its [MIT License](LICENSE).
21 changes: 21 additions & 0 deletions LICENSE
Original file line numberDiff line numberDiff line change
@@ -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.
8 changes: 8 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,14 @@

The Clerk command-line interface.

## Installation

```sh
npm install -g clerk
```

## Usage

```
Usage: clerk [options] [command]

Expand Down
204 changes: 204 additions & 0 deletions docs/releasing.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
# Release Flow

This document describes how the Clerk CLI is built, versioned, and published.

## Overview

```
push to main
→ 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 stable release needed) canary.ts versions packages
→ build → smoke-test subset → publish @canary

PR comment "!snapshot [name]"
→ 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

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`, 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.

## Release Channels

### Stable (`@latest`)

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. `scripts/canary.ts` uses Changesets snapshot mode to produce versions in the format `x.y.z-canary.v<YYYYMMDDHHmmss>` (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 <name>`) 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-<name>.v<YYYYMMDDHHmmss>` (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>` (version is posted as a PR comment after publishing)

## Versioning

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:

- `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages)

## Build Pipeline

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

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=<bun_target>`
2. Injects the version via `--define "CLI_VERSION=\"$CLI_VERSION\""`
3. Verifies the binary format using `file` output
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.

Publishing and GitHub Release upload are gated on all smoke tests passing.

### 3. Publish npm Job

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)
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 <tag>` -- publish with a specific npm dist-tag (e.g., `canary`, `snapshot`); defaults to `latest`
- `--version <version>` -- override the version read from `package.json`

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`)
- `GH_TOKEN` -- GitHub token used by the releaser to create tags and releases (stable only)

#### 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

Attaches the compiled binaries to the GitHub Release for direct download. Binaries are uploaded with display names following the `clerk-<target>` 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 (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 |
| `.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 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 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

```sh
# 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
```

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:

```sh
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.
- **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.
- **CI build check**: Every PR to `main` runs a JS bundle build to catch bundler-specific failures before merge.
6 changes: 5 additions & 1 deletion scripts/releaser/index.ts
Original file line numberDiff line numberDiff line change
@@ -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 { parseArgs } from "node:util";
import { type Target, targets, SCOPE, PKG_PREFIX } from "./targets.ts";
Expand DownExpand Up@@ -66,6 +66,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"));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return dir;
}

Expand Down
Loading