From 7e56523a2d0e2a174f754a6e24d13a35516faf40 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 21 Apr 2026 14:44:02 -0600 Subject: [PATCH 1/3] feat(release): prepare clerk 1.0.0 release --- .changeset/clerk-1-0-0.md | 7 ++++ .changeset/release-process-test.md | 5 --- .github/release-notes/v1.0.0.md | 27 +++++++++++++ README.md | 4 -- docs/releasing.md | 5 ++- scripts/lib/release-notes.test.ts | 64 ++++++++++++++++++++++++++++++ scripts/lib/release-notes.ts | 30 ++++++++++++++ scripts/releaser.ts | 8 +++- 8 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 .changeset/clerk-1-0-0.md delete mode 100644 .changeset/release-process-test.md create mode 100644 .github/release-notes/v1.0.0.md create mode 100644 scripts/lib/release-notes.test.ts create mode 100644 scripts/lib/release-notes.ts diff --git a/.changeset/clerk-1-0-0.md b/.changeset/clerk-1-0-0.md new file mode 100644 index 00000000..ba89d8d1 --- /dev/null +++ b/.changeset/clerk-1-0-0.md @@ -0,0 +1,7 @@ +--- +"clerk": major +--- + +Release Clerk CLI 1.0 as the first stable `1.x` line. + +This milestone rolls up the recent improvements to bootstrap flows, authentication and keyless claiming, bundled agent skills, PATH-aware updates, interactive prompts, and docs into a stable baseline for the standalone `clerk` CLI. diff --git a/.changeset/release-process-test.md b/.changeset/release-process-test.md deleted file mode 100644 index a3bd4045..00000000 --- a/.changeset/release-process-test.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"clerk": patch ---- - -Test patch release flow. diff --git a/.github/release-notes/v1.0.0.md b/.github/release-notes/v1.0.0.md new file mode 100644 index 00000000..13a53ce2 --- /dev/null +++ b/.github/release-notes/v1.0.0.md @@ -0,0 +1,27 @@ +# Clerk CLI 1.0 + +Clerk CLI `1.0.0` is the first stable `1.x` release of the standalone `clerk` command line tool. + +This release marks the CLI as a complete, supported interface for working with Clerk from the terminal: bootstrapping new projects, authenticating, linking apps, pulling environment variables, inspecting and updating instance configuration, making direct API requests, running integration health checks, switching environments, opening dashboard resources, installing the bundled Clerk agent skill, and updating the CLI itself. + +Core workflows available in Clerk CLI `1.0.0` include: + +- `clerk init` to bootstrap Clerk into supported projects and starters +- `clerk auth`, `clerk whoami`, `clerk link`, and `clerk unlink` for account and project association +- `clerk env pull` for pulling Clerk environment variables into local env files +- `clerk config pull`, `schema`, `patch`, and `put` for instance configuration management +- `clerk api` and `clerk apps` for direct platform access from the terminal +- `clerk doctor` for integration diagnostics and guided fixes +- `clerk open`, `clerk switch-env`, `clerk completion`, `clerk skill install`, and `clerk update` for day-to-day developer workflows + +Install Clerk CLI: + +```sh +# Homebrew (macOS / Linux) +brew install clerk/stable/clerk + +# npm +npm install -g clerk +``` + +See the autogenerated notes below for the full PR-by-PR changelog included in this `1.0.0` release. diff --git a/README.md b/README.md index ea8d29e1..7a6d6e7a 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,3 @@ clerk update $ clerk update --yes Update without confirmation prompt $ clerk update --all Update every clerk install on PATH ``` - -## Open Questions - -- How do we keep types in sync with PLAPI? diff --git a/docs/releasing.md b/docs/releasing.md index 1c00cd24..e6bfd2f0 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -95,6 +95,8 @@ This launches an interactive prompt that asks which packages changed and whether 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`). +Changeset summaries drive the package changelog and the contents of the generated "Version Packages" PR. They do **not** control the GitHub Release body for stable releases; that comes from GitHub's autogenerated release notes, optionally prepended with a checked-in intro file at `.github/release-notes/vX.Y.Z.md`. + Configuration: - `.changeset/config.json` -- Changesets configuration (access, snapshot template, ignored packages) @@ -166,7 +168,7 @@ Runs the releaser script (`scripts/releaser/index.ts`) via `bun run release` (st - 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 +5. For stable releases, creates a Git tag and GitHub Release via the GitHub API. If `.github/release-notes/vX.Y.Z.md` exists for that version, its contents are prepended ahead of GitHub's autogenerated notes. The releaser accepts these flags: @@ -211,6 +213,7 @@ Install: `brew install clerk/stable/clerk` | `packages/cli-core/src/globals.d.ts` | TypeScript declaration for the `CLI_VERSION` compile-time define | | `install.sh` | Shell install script — downloads binary from GitHub Releases | | `scripts/releaser/index.ts` | Generates platform packages and publishes everything to npm | +| `.github/release-notes/vX.Y.Z.md` | Optional version-specific intro prepended to stable GitHub Release notes | | `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/sign-macos.ts` | Signs and notarizes macOS binaries (keychain, codesign, notarytool) | diff --git a/scripts/lib/release-notes.test.ts b/scripts/lib/release-notes.test.ts new file mode 100644 index 00000000..31eb9bed --- /dev/null +++ b/scripts/lib/release-notes.test.ts @@ -0,0 +1,64 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtemp, rm } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { getStableReleaseCreateArgs, stableReleaseNotesPath } from "./release-notes.ts"; + +let tempDir: string | undefined; + +afterEach(async () => { + if (tempDir) { + await rm(tempDir, { recursive: true, force: true }); + tempDir = undefined; + } +}); + +describe("stableReleaseNotesPath", () => { + test("builds the version-specific markdown path", () => { + expect(stableReleaseNotesPath("1.0.0", "/tmp/release-notes")).toBe( + "/tmp/release-notes/v1.0.0.md", + ); + }); +}); + +describe("getStableReleaseCreateArgs", () => { + test("uses plain generated notes when no intro file exists", async () => { + tempDir = await mkdtemp(join(tmpdir(), "release-notes-")); + + expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual([ + "gh", + "release", + "create", + "v1.0.0", + "--generate-notes", + ]); + }); + + test("prepends version-specific notes when the intro file exists", async () => { + tempDir = await mkdtemp(join(tmpdir(), "release-notes-")); + await Bun.write(join(tempDir, "v1.0.0.md"), "Intro line\n\n- Highlight"); + + expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual([ + "gh", + "release", + "create", + "v1.0.0", + "--generate-notes", + "--notes", + "Intro line\n\n- Highlight", + ]); + }); + + test("ignores empty intro files", async () => { + tempDir = await mkdtemp(join(tmpdir(), "release-notes-")); + await Bun.write(join(tempDir, "v1.0.0.md"), "\n \n"); + + expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual([ + "gh", + "release", + "create", + "v1.0.0", + "--generate-notes", + ]); + }); +}); diff --git a/scripts/lib/release-notes.ts b/scripts/lib/release-notes.ts new file mode 100644 index 00000000..d8539182 --- /dev/null +++ b/scripts/lib/release-notes.ts @@ -0,0 +1,30 @@ +import { join } from "node:path"; + +const DEFAULT_STABLE_RELEASE_NOTES_DIR = join(import.meta.dir, "../../.github/release-notes"); + +export function stableReleaseNotesPath( + version: string, + notesDir = DEFAULT_STABLE_RELEASE_NOTES_DIR, +): string { + return join(notesDir, `v${version}.md`); +} + +export async function getStableReleaseCreateArgs( + version: string, + notesDir = DEFAULT_STABLE_RELEASE_NOTES_DIR, +): Promise { + const tagName = `v${version}`; + const args = ["gh", "release", "create", tagName, "--generate-notes"]; + const notesPath = stableReleaseNotesPath(version, notesDir); + + if (!(await Bun.file(notesPath).exists())) { + return args; + } + + const notes = (await Bun.file(notesPath).text()).trim(); + if (!notes) { + return args; + } + + return [...args, "--notes", notes]; +} diff --git a/scripts/releaser.ts b/scripts/releaser.ts index 88daa947..d983bb2d 100644 --- a/scripts/releaser.ts +++ b/scripts/releaser.ts @@ -3,6 +3,7 @@ import { join } from "node:path"; import { parseArgs } from "node:util"; import { type Target, targets, SCOPE, PKG_PREFIX } from "./lib/targets.ts"; import { run, isPublished, publish } from "./lib/npm.ts"; +import { getStableReleaseCreateArgs, stableReleaseNotesPath } from "./lib/release-notes.ts"; const DIST_DIR = join(import.meta.dir, "../dist/platform-packages"); const ARTIFACTS_DIR = process.env.ARTIFACTS_DIR ?? join(import.meta.dir, "../dist/artifacts"); @@ -136,7 +137,12 @@ if (!tag && !dryRun) { console.log(`GitHub Release for ${tagName} already exists, skipping.`); } else { console.log(`Creating GitHub Release for ${tagName}...`); - await run(["gh", "release", "create", tagName, "--generate-notes"]); + const releaseArgs = await getStableReleaseCreateArgs(version); + const notesPath = stableReleaseNotesPath(version); + if (releaseArgs.includes("--notes")) { + console.log(`Prepending release intro from ${notesPath}...`); + } + await run(releaseArgs); } } From e83f30e3df3d71ea7f307218339c57f241c17622 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 21 Apr 2026 15:16:04 -0600 Subject: [PATCH 2/3] docs(release-notes): soften v1.0.0 intro wording --- .github/release-notes/v1.0.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/release-notes/v1.0.0.md b/.github/release-notes/v1.0.0.md index 13a53ce2..6e5b6827 100644 --- a/.github/release-notes/v1.0.0.md +++ b/.github/release-notes/v1.0.0.md @@ -2,7 +2,7 @@ Clerk CLI `1.0.0` is the first stable `1.x` release of the standalone `clerk` command line tool. -This release marks the CLI as a complete, supported interface for working with Clerk from the terminal: bootstrapping new projects, authenticating, linking apps, pulling environment variables, inspecting and updating instance configuration, making direct API requests, running integration health checks, switching environments, opening dashboard resources, installing the bundled Clerk agent skill, and updating the CLI itself. +This release marks the CLI as a stable, supported interface for working with Clerk from the terminal: bootstrapping new projects, authenticating, linking apps, pulling environment variables, inspecting and updating instance configuration, making direct API requests, running integration health checks, switching environments, opening dashboard resources, installing the bundled Clerk agent skill, and updating the CLI itself. Core workflows available in Clerk CLI `1.0.0` include: From 5c23f1285848d38f36f480b3d489188392f36593 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 21 Apr 2026 15:29:26 -0600 Subject: [PATCH 3/3] refactor(release-notes): return notesPath from arg builder Collapse the two places that derived the release-notes path (the lib and the releaser call site) into a single return value. The caller now logs whatever path the lib reports it used, so the filename convention lives in one module. Also drops the now-unused `stableReleaseNotesPath` export and scopes the test's `tempDir` to the describe block that uses it. --- scripts/lib/release-notes.test.ts | 75 +++++++++++++------------------ scripts/lib/release-notes.ts | 20 ++++----- scripts/releaser.ts | 7 ++- 3 files changed, 42 insertions(+), 60 deletions(-) diff --git a/scripts/lib/release-notes.test.ts b/scripts/lib/release-notes.test.ts index 31eb9bed..8e1c807b 100644 --- a/scripts/lib/release-notes.test.ts +++ b/scripts/lib/release-notes.test.ts @@ -1,64 +1,49 @@ -import { afterEach, describe, expect, test } from "bun:test"; +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { getStableReleaseCreateArgs, stableReleaseNotesPath } from "./release-notes.ts"; +import { getStableReleaseCreateArgs } from "./release-notes.ts"; -let tempDir: string | undefined; +describe("getStableReleaseCreateArgs", () => { + let tempDir: string; -afterEach(async () => { - if (tempDir) { - await rm(tempDir, { recursive: true, force: true }); - tempDir = undefined; - } -}); + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), "release-notes-")); + }); -describe("stableReleaseNotesPath", () => { - test("builds the version-specific markdown path", () => { - expect(stableReleaseNotesPath("1.0.0", "/tmp/release-notes")).toBe( - "/tmp/release-notes/v1.0.0.md", - ); + afterEach(async () => { + await rm(tempDir, { recursive: true, force: true }); }); -}); -describe("getStableReleaseCreateArgs", () => { test("uses plain generated notes when no intro file exists", async () => { - tempDir = await mkdtemp(join(tmpdir(), "release-notes-")); - - expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual([ - "gh", - "release", - "create", - "v1.0.0", - "--generate-notes", - ]); + expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({ + args: ["gh", "release", "create", "v1.0.0", "--generate-notes"], + }); }); test("prepends version-specific notes when the intro file exists", async () => { - tempDir = await mkdtemp(join(tmpdir(), "release-notes-")); - await Bun.write(join(tempDir, "v1.0.0.md"), "Intro line\n\n- Highlight"); - - expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual([ - "gh", - "release", - "create", - "v1.0.0", - "--generate-notes", - "--notes", - "Intro line\n\n- Highlight", - ]); + const notesPath = join(tempDir, "v1.0.0.md"); + await Bun.write(notesPath, "Intro line\n\n- Highlight"); + + expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({ + args: [ + "gh", + "release", + "create", + "v1.0.0", + "--generate-notes", + "--notes", + "Intro line\n\n- Highlight", + ], + notesPath, + }); }); test("ignores empty intro files", async () => { - tempDir = await mkdtemp(join(tmpdir(), "release-notes-")); await Bun.write(join(tempDir, "v1.0.0.md"), "\n \n"); - expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual([ - "gh", - "release", - "create", - "v1.0.0", - "--generate-notes", - ]); + expect(await getStableReleaseCreateArgs("1.0.0", tempDir)).toEqual({ + args: ["gh", "release", "create", "v1.0.0", "--generate-notes"], + }); }); }); diff --git a/scripts/lib/release-notes.ts b/scripts/lib/release-notes.ts index d8539182..121cde3d 100644 --- a/scripts/lib/release-notes.ts +++ b/scripts/lib/release-notes.ts @@ -2,29 +2,27 @@ import { join } from "node:path"; const DEFAULT_STABLE_RELEASE_NOTES_DIR = join(import.meta.dir, "../../.github/release-notes"); -export function stableReleaseNotesPath( - version: string, - notesDir = DEFAULT_STABLE_RELEASE_NOTES_DIR, -): string { - return join(notesDir, `v${version}.md`); -} +export type StableReleaseCreateArgs = { + args: string[]; + notesPath?: string; +}; export async function getStableReleaseCreateArgs( version: string, notesDir = DEFAULT_STABLE_RELEASE_NOTES_DIR, -): Promise { +): Promise { const tagName = `v${version}`; const args = ["gh", "release", "create", tagName, "--generate-notes"]; - const notesPath = stableReleaseNotesPath(version, notesDir); + const notesPath = join(notesDir, `v${version}.md`); if (!(await Bun.file(notesPath).exists())) { - return args; + return { args }; } const notes = (await Bun.file(notesPath).text()).trim(); if (!notes) { - return args; + return { args }; } - return [...args, "--notes", notes]; + return { args: [...args, "--notes", notes], notesPath }; } diff --git a/scripts/releaser.ts b/scripts/releaser.ts index d983bb2d..7c8f6fed 100644 --- a/scripts/releaser.ts +++ b/scripts/releaser.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { parseArgs } from "node:util"; import { type Target, targets, SCOPE, PKG_PREFIX } from "./lib/targets.ts"; import { run, isPublished, publish } from "./lib/npm.ts"; -import { getStableReleaseCreateArgs, stableReleaseNotesPath } from "./lib/release-notes.ts"; +import { getStableReleaseCreateArgs } from "./lib/release-notes.ts"; const DIST_DIR = join(import.meta.dir, "../dist/platform-packages"); const ARTIFACTS_DIR = process.env.ARTIFACTS_DIR ?? join(import.meta.dir, "../dist/artifacts"); @@ -137,9 +137,8 @@ if (!tag && !dryRun) { console.log(`GitHub Release for ${tagName} already exists, skipping.`); } else { console.log(`Creating GitHub Release for ${tagName}...`); - const releaseArgs = await getStableReleaseCreateArgs(version); - const notesPath = stableReleaseNotesPath(version); - if (releaseArgs.includes("--notes")) { + const { args: releaseArgs, notesPath } = await getStableReleaseCreateArgs(version); + if (notesPath) { console.log(`Prepending release intro from ${notesPath}...`); } await run(releaseArgs);