Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
eb5b8e7
refactor(deploy): extract poll core into status.ts
wyattjoh May 28, 2026
7314f67
refactor(deploy): move state resolution into status.ts, rename dnsCom…
wyattjoh May 28, 2026
76e9f3c
feat(deploy): add resolveDeployState discriminator
wyattjoh May 28, 2026
52050cf
feat(deploy): add buildDeployStatusReport payload builder
wyattjoh May 28, 2026
3d406df
feat(deploy): add clerk deploy check command
wyattjoh May 28, 2026
a619953
feat(deploy): tailor agent-mode deploy into a read-only handoff
wyattjoh May 28, 2026
10990d3
feat(deploy): register clerk deploy check subcommand
wyattjoh May 28, 2026
20871b5
docs(deploy): document deploy check and agent handoff
wyattjoh May 28, 2026
6a9758c
fix(deploy): surface agent status read failures
wyattjoh May 28, 2026
1cc3d64
docs(clerk-cli): document deploy agent workflow
wyattjoh May 28, 2026
e423a63
fix(deploy): avoid backoff in agent check
wyattjoh May 28, 2026
cc0d804
fix(deploy): check domain status as one DNS verification
wyattjoh May 28, 2026
a201308
fix(deploy): include domains URL in agent next action
wyattjoh May 28, 2026
94ba6e4
fix(deploy): prompt agents to open domains URL
wyattjoh May 28, 2026
a7e24fb
docs(clerk-cli): warn deploy wizard needs a terminal
wyattjoh May 28, 2026
3f355d9
fix(cli): address deploy review follow-ups
wyattjoh May 29, 2026
b282011
fix(deploy): persist live production instance metadata
wyattjoh May 29, 2026
ae2e086
chore: added link for codex
wyattjoh May 29, 2026
dbfeca5
docs(clerk-cli): clarify deploy check agent workflow
wyattjoh May 29, 2026
14ff7c0
refactor(deploy): remove unused status check helper
wyattjoh May 29, 2026
1274f60
test(deploy): avoid leaking deploy check mocks
wyattjoh May 29, 2026
94ea5e2
docs(testing): document bun test isolation
wyattjoh May 29, 2026
53e0a5a
docs: remove hidden bird command from readme
wyattjoh May 29, 2026
a419b77
feat(deploy): rename status check command
wyattjoh May 29, 2026
ba8fcdd
fix(deploy): humanize status dashboard guidance
wyattjoh May 29, 2026
19caa25
refactor(deploy): tighten status resolution
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.
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.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +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 [options] Deploy a Clerk application to production
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
9 changes: 9 additions & 0 deletions packages/cli-core/src/cli-program.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ test("deploy relies on global options", () => {
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
11 changes: 10 additions & 1 deletion packages/cli-core/src/cli-program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ 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 @@ -926,7 +927,15 @@ Tutorial — enable completions for your shell:
])
.action(update);

program.command("deploy").description("Deploy a Clerk application to production").action(deploy);
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);

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
1 change: 0 additions & 1 deletion packages/cli-core/src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ async function performOAuthFlow(): Promise<UserInfo> {
// Critical fallback: the OAuth callback can't complete unless the user
// reaches the authorize URL somehow.
const urlString = authorizeUrl.toString();
log.debug(`Opening browser to URL: ${urlString}`);
const result = await openBrowser(urlString);
if (!result.ok) {
log.warn(
Expand Down
Loading