Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
5d9fa8a
feat(deploy): implement resumable deploy wizard
wyattjoh May 6, 2026
4c840ea
fix(deploy): address review feedback on resumable wizard
wyattjoh May 6, 2026
4ce12ff
refactor(deploy): isolate lifecycle api calls
wyattjoh May 6, 2026
34e6d0f
feat(deploy): resolve production state from API
wyattjoh May 6, 2026
ec9bb58
fix(deploy): route test failures through api path
wyattjoh May 6, 2026
b23fae6
fix(deploy): remove gutter tone plumbing
wyattjoh May 6, 2026
b440934
fix(deploy): require human mode for production setup
wyattjoh May 12, 2026
dbb69ab
refactor(deploy): route lifecycle test failures through api mock
wyattjoh May 12, 2026
cc63c63
refactor(deploy): extract mock api into its own module
wyattjoh May 13, 2026
87528e4
refactor(deploy): route lifecycle through live PLAPI and map errors
rafa-thayto May 19, 2026
3ea1cb2
feat(deploy): replace DNS handoff confirm with verify-or-skip choice
wyattjoh May 19, 2026
412c648
feat(deploy): refresh DNS verification for current API surface
wyattjoh May 19, 2026
eb27846
refactor(deploy): drop ./api.ts and ./mock.ts test indirection
wyattjoh May 20, 2026
0860638
feat(deploy): add bindZoneFile formatter for DNS records export
wyattjoh May 20, 2026
0e1fd41
feat(deploy): add per-component spinner label lookup
wyattjoh May 20, 2026
13094cb
feat(deploy): sequence DNS verification feedback per component
wyattjoh May 20, 2026
e850c0e
feat(deploy): show DNS records on the resume verification path
wyattjoh May 20, 2026
93747be
feat(deploy): offer BIND zone export after DNS records
wyattjoh May 20, 2026
b9461db
docs(deploy): document per-component verification and BIND export
wyattjoh May 20, 2026
6ea0789
docs(changeset): document clerk deploy production wizard
wyattjoh May 20, 2026
d9fc05e
refactor(plapi): drop unused is_secondary from CreateProductionInstan…
wyattjoh May 13, 2026
f65cea4
refactor(plapi): allow null active_domain and guard in deploy wizard
wyattjoh May 21, 2026
f5b089d
chore: merge origin/main into deploy branch
wyattjoh May 21, 2026
c749d58
fix(deploy): remove Domain Connect prompt and address review feedback
wyattjoh May 21, 2026
13ea5f4
fix(deploy): use domain status for verification
wyattjoh May 25, 2026
cf0ee36
fix(deploy): defer DNS verification until after OAuth
wyattjoh May 25, 2026
4d6a0fa
fix(deploy): refresh dns checks before polling
wyattjoh May 26, 2026
1540046
fix(deploy): report dns verification pauses
wyattjoh May 26, 2026
4b6e0e0
feat(deploy): type instance config schemas
wyattjoh May 26, 2026
3724c57
feat(deploy): derive oauth provider descriptors from schema
wyattjoh May 26, 2026
8d5d132
feat(deploy): use schema-driven oauth setup
wyattjoh May 26, 2026
c8bf983
feat(deploy): resume oauth setup from schema descriptors
wyattjoh May 26, 2026
06061e7
feat(deploy): preserve oauth provider special prompts
wyattjoh May 26, 2026
dda5ef1
docs(deploy): document schema-driven oauth setup
wyattjoh May 26, 2026
819ec4e
fix(deploy): warn generically for unsupported oauth providers
wyattjoh May 26, 2026
c2bbb44
fix(deploy): ignore optional oauth schema fields
wyattjoh May 26, 2026
894eea0
fix(deploy): support public oauth providers
wyattjoh May 26, 2026
65a08ff
fix(deploy): use verbose for debug output
wyattjoh May 26, 2026
0c7b159
fix(deploy): remove oauth provider allowlist
wyattjoh May 26, 2026
6c00613
refactor(deploy): derive oauth compatibility metadata
wyattjoh May 26, 2026
97b2585
refactor(deploy): simplify oauth provider overrides
wyattjoh May 26, 2026
c735a0b
fix(ci): retry transient cleanup API failures
wyattjoh May 27, 2026
0012b73
fix(deploy): adapt to platform instance response
wyattjoh May 27, 2026
d1f7bf9
fix(deploy): update retry status copy
wyattjoh May 27, 2026
fa5821c
Update packages/cli-core/src/commands/deploy/copy.ts
wyattjoh May 27, 2026
a14bd09
Revert "Update packages/cli-core/src/commands/deploy/copy.ts"
wyattjoh May 28, 2026
33e1b78
fix(review): address deploy PR feedback
wyattjoh May 28, 2026
c647893
fix(deploy): link domain settings in next steps
wyattjoh May 28, 2026
5891fa2
Merge remote-tracking branch 'origin/main' into wyattjoh/deploy
wyattjoh May 28, 2026
688f965
feat(deploy): add read-only deploy verification (#312)
wyattjoh May 29, 2026
64a18bd
fix(deploy): label DKIM records as email in DNS handoff
wyattjoh May 29, 2026
63478a9
refactor(deploy): drop unreachable OAuth completion guards
wyattjoh May 29, 2026
c28a25f
fix(deploy): re-prompt Google OAuth JSON after docs
wyattjoh May 29, 2026
878b726
fix(deploy): re-prompt oauth credentials after walkthrough
wyattjoh May 29, 2026
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 .agents/skills/audit-clerk-skill
1 change: 1 addition & 0 deletions .agents/skills/changesets
5 changes: 5 additions & 0 deletions .changeset/deploy-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk deploy status`, a read-only command that verifies a production deploy, including DNS, SSL, email DNS, and OAuth credential completeness. Agent-mode `clerk deploy` now emits a tailored read-only handoff instead of a hard usage error.
12 changes: 12 additions & 0 deletions .changeset/deploy-wizard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"clerk": minor
---

Add `clerk deploy`, an interactive wizard that promotes a Clerk application from development to production.

- Walks through cloning the development instance, creating the production instance, and configuring CNAME records.
- Verifies mail, DNS, and SSL one component at a time so each step's status is visible while polling.
- Optionally exports the DNS records as a BIND zone file at `./clerk-<domain>.zone` for import into providers like Cloudflare, Route 53, and Google Cloud DNS.
- Resumes from the next pending step on subsequent runs, including reshowing the CNAME records when DNS is not yet verified.
- Uses provider schemas to collect production OAuth credentials for broader built-in provider support.
- Returns users to credential choices after opening provider docs, including Google JSON import when supported.
20 changes: 10 additions & 10 deletions .claude/rules/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ Adjust the relative path to `lib/log.ts` based on the file's location under `pac

## Which method to use

| Method | Stream | When to use |
| --------------- | ---------- | ----------------------------------------------- |
| `log.data()` | **stdout** | Pipeable output (JSON, lists, machine-readable) |
| `log.info()` | stderr | Status messages |
| `log.success()` | stderr | Completion confirmations (green) |
| `log.warn()` | stderr | Warnings (yellow) |
| `log.error()` | stderr | Errors (red, auto-prefixed `error:`) |
| `log.debug()` | stderr | Diagnostic info, only with `--verbose` |
| `log.raw()` | stderr | Machine-readable JSON for agent mode |
| Method | Stream | When to use |
| --------------- | ---------- | ------------------------------------------------ |
| `log.data()` | **stdout** | Pipeable output (JSON, lists, machine-readable) |
| `log.info()` | stderr | Status messages |
| `log.success()` | stderr | Completion confirmations (green) |
| `log.warn()` | stderr | Warnings (yellow) |
| `log.error()` | stderr | Errors (red, auto-prefixed `error:`) |
| `log.debug()` | stderr | Diagnostic info, only with `--verbose` |
| `log.raw()` | stderr | Machine-readable JSON for agent mode |
| `log.ui()` | stderr | Pre-formatted UI (spinner, intro/outro brackets) |
| `log.blank()` | stderr | Blank line |
| `log.blank()` | stderr | Blank line |

`log.data()` writes to **stdout** — this is what gets piped (e.g., `clerk apps list | jq`). Everything else writes to **stderr** as UI for humans. Never mix these.

Expand Down
2 changes: 2 additions & 0 deletions .claude/rules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ bun run test

This runs each unit and integration test file as a separate `bun test` subprocess via `scripts/run-tests.ts`, isolating module state between files. E2E fixtures are excluded and require separate setup (see `rules/e2e.md`).

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures.

Prefer `spyOn()` for mocking, and always restore spies in `afterAll` with `mockRestore()`.

Never use `for` or `forEach` loops inside a single test to verify multiple inputs or cases — use `test.each` (or `it.each` / `describe.each`) so each case is its own reported test case with its own name, setup/teardown, and pinpointed failure output.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ Locally, prefer `bun run test:e2e:op` so secrets are injected from 1Password in-

CI runs `bun run format:check` (fails if unformatted), `bun run lint`, `bun test`, and `bun run test:e2e` on every PR to `main`. E2E tests only run for PRs from the same repository (not external forks) and target the production Clerk API with a dedicated test application.

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures. Prefer `bun run test` for the full suite because it already isolates test files through `scripts/run-tests.ts`.

## Versioning

The `CLI_VERSION` global is injected at compile time via `bun build --compile --define "CLI_VERSION=..."`. Local `build:compile` omits it, so the binary reports `0.0.0-dev`. The CI release workflow injects the real version.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Commands:
completion [shell] Generate shell autocompletion script
skill Manage the bundled Clerk CLI agent skill
update [options] Update the Clerk CLI to the latest version
deploy Deploy a Clerk application to production
help [command] Display help for command

Give AI agents better Clerk context: install the Clerk skills
Expand Down
17 changes: 17 additions & 0 deletions packages/cli-core/src/cli-program.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@ test("users list exposes common filters and pagination options", () => {
);
});

test("deploy relies on global options", () => {
const program = createProgram();
const deploy = program.commands.find((command) => command.name() === "deploy")!;
const optionNames = deploy.options.map((option) => option.long);

expect(optionNames).toEqual([]);
});

test("deploy status exposes wait option", () => {
const program = createProgram();
const deploy = program.commands.find((command) => command.name() === "deploy")!;
const status = deploy.commands.find((command) => command.name() === "status")!;
const optionNames = status.options.map((option) => option.long);

expect(optionNames).toContain("--wait");
});

describe("parseIntegerOption (via users list --limit / --offset)", () => {
function parseUsersList(args: readonly string[]) {
return createProgram().parseAsync(["users", "list", ...args], { from: "user" });
Expand Down
18 changes: 15 additions & 3 deletions packages/cli-core/src/cli-program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@ import {
PlapiError,
FapiError,
EXIT_CODE,
isPromptExitError,
throwUsageError,
} from "./lib/errors.ts";
import { clerkHelpConfig } from "./lib/help.ts";
import { ExitPromptError } from "@inquirer/core";
import { isAgent } from "./mode.ts";
import { log } from "./lib/log.ts";
import { maybeNotifyUpdate, getCurrentVersion } from "./lib/update-check.ts";
import { update } from "./commands/update/index.ts";
import { deploy } from "./commands/deploy/index.ts";
import { deployStatus } from "./commands/deploy/status-command.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
Expand Down Expand Up @@ -119,7 +121,7 @@ Give AI agents better Clerk context: install the Clerk skills

program.hook("preAction", async () => {
// Reset log level at the start of each command invocation so a previous
// --verbose or --debug flag doesn't leak into subsequent runs.
// --verbose doesn't leak into subsequent runs.
setLogLevel("info");
const opts = program.opts();
if (opts.verbose) {
Expand Down Expand Up @@ -925,6 +927,16 @@ Tutorial — enable completions for your shell:
])
.action(update);

const deployCmd = program
.command("deploy")
.description("Deploy a Clerk application to production");
deployCmd.command("run", { isDefault: true, hidden: true }).action(deploy);
deployCmd
.command("status")
.description("Show production deploy status (read-only)")
.option("--wait", "Wait for DNS, SSL, and email DNS verification with retries")
.action(deployStatus);

registerExtras(program);

return program;
Expand Down Expand Up @@ -1012,7 +1024,7 @@ export async function runProgram(
} catch (error) {
const verbose = program.opts().verbose ?? false;

if (error instanceof UserAbortError || error instanceof ExitPromptError) {
if (error instanceof UserAbortError || isPromptExitError(error)) {
process.exit(EXIT_CODE.SUCCESS);
}

Expand Down
41 changes: 41 additions & 0 deletions packages/cli-core/src/commands/auth/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ mock.module("../../lib/autoclaim.ts", () => ({
attemptAutoclaim: async () => ({ status: "not_keyless" }),
}));

const { setLogLevel } = await import("../../lib/log.ts");
const { login } = await import("./login.ts");

describe("login", () => {
Expand All @@ -114,6 +115,7 @@ describe("login", () => {
mockEnsureFirstApplication.mockResolvedValue(undefined);
mockIsHuman.mockReturnValue(false);
mockOpenBrowser.mockResolvedValue({ ok: true, launcher: "test" });
setLogLevel("info");
consoleSpy?.mockRestore();
consoleErrorSpy?.mockRestore();
try {
Expand Down Expand Up @@ -592,6 +594,45 @@ describe("login", () => {
expect(parsed.searchParams.get("clerk_client")).toBe("cli");
});

test("does not emit the OAuth authorize URL through debug logging", async () => {
setLogLevel("debug");
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();

const mockServer = {
port: 54321,
waitForCallback: mock().mockResolvedValue({ code: "fresh-auth-code" }),
stop: mock(),
};
mockStartAuthServer.mockReturnValue(mockServer);

mockExchangeCodeForToken.mockResolvedValue({
access_token: "new-access-token",
token_type: "Bearer",
expires_in: 3600,
refresh_token: "new-refresh-token",
});
mockCreateOAuthSession.mockReturnValue({
accessToken: "new-access-token",
refreshToken: "new-refresh-token",
expiresAt: 123,
tokenType: "Bearer",
});
mockStoreToken.mockResolvedValue(undefined);
mockFetchUserInfo.mockResolvedValue({
userId: "user_new",
email: "new@example.com",
});
mockSetAuth.mockResolvedValue(undefined);

consoleSpy = spyOn(console, "log").mockImplementation(() => {});
await runLogin({ showNextSteps: false });

expect(captured.err).not.toContain("https://test.example.com/oauth/authorize");
expect(captured.err).not.toContain("test-state-value");
expect(captured.err).not.toContain("test-code-challenge");
});

test("calls ensureFirstApplication after a successful OAuth flow", async () => {
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();
Expand Down
Loading