Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6bb21b2
Fix misleading text in clerk deploy wizard and help
shane-kercheval Sep 17, 2026
bfcefa0
Address review of clerk deploy wording changes
shane-kercheval Sep 17, 2026
e612a8b
Fix contradictions found in deploy wording self-review
shane-kercheval Sep 17, 2026
78a549d
Address reviewer round on deploy self-review fixes
shane-kercheval Sep 17, 2026
cc961ea
Keep every DNS record in the zone file on resume
shane-kercheval Sep 17, 2026
71e72f6
Separate the confirmation screen from the typed domain
shane-kercheval Sep 17, 2026
33d8316
Name each DNS record host one way and link the Domains page
shane-kercheval Sep 17, 2026
f8157f3
Tell users to choose a consent-screen name, not reuse Clerk's
shane-kercheval Sep 17, 2026
c43d72a
Close a skipped-DNS deploy with "DNS pending", not "Success"
shane-kercheval Sep 17, 2026
f6a0f3b
Only promise an OAuth step on the DNS screen when one is coming
shane-kercheval Sep 17, 2026
704e590
Say what the DNS check looks for
shane-kercheval Sep 17, 2026
d68c414
Name the DNS provider and both options after a failed check
shane-kercheval Sep 17, 2026
ddb5fb7
Say what is outstanding when the DNS screen has no records to add
shane-kercheval Sep 17, 2026
255ad36
Build the DNS screen's next-step sentence in one place
shane-kercheval Sep 17, 2026
53b33a0
Mention email records only when one is listed
shane-kercheval Sep 17, 2026
241bbd1
Write both resume sentences out instead of patching one
shane-kercheval Sep 17, 2026
57c189b
Name the DNS handoff's two record lists
shane-kercheval Sep 17, 2026
d69e056
Describe the Google consent-screen tip accurately
shane-kercheval Sep 17, 2026
605db2c
Classify and label record hosts from one table
shane-kercheval Sep 17, 2026
bd35679
Wrap the wizard's new prose to fit an 80-column terminal
shane-kercheval Sep 18, 2026
b9b06f7
Render human and agent next steps from one classification
shane-kercheval Sep 18, 2026
5cf71d2
Fix remaining overflow in the updated deploy and login messages
shane-kercheval Sep 18, 2026
2acf81a
Tighten the deploy status report's types
shane-kercheval Sep 18, 2026
869b544
Correct two stale claims in the deploy docs
shane-kercheval Sep 18, 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
17 changes: 17 additions & 0 deletions .changeset/deploy-wizard-copy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"clerk": patch
---

`clerk deploy` copy fixes:

- `clerk deploy --help` describes what the bare command does and the JSON report it prints under an agent.
- The preamble says a hosting provider's generated URL can't be the production domain.
- The confirmation screen lists all five DNS record hosts, including DKIM, and says DNS records will be needed for them once the instance exists.
- The DNS check reports records as "not found yet" with a minutes-not-days expectation, tells you what to do based on what's actually pending, and links the Dashboard Domains page for changing the domain.
- `clerk auth login` prints the claimed app's Dashboard URL; the wizard prints the new production instance's URL and its next steps say the pulled keys go on the host alongside the other Clerk variables.
- The Google walkthrough adds a tip explaining that the OAuth consent screen's app name is what users see when they sign in, and to choose the name they should see.
- The DNS check footer points at the "Check again" prompt that follows it instead of telling you to quit and re-run; the closing screen no longer says "Production ready", "sign up at your domain", or "Success" when DNS verification was skipped.
- Resuming the wizard shows only the DNS records still outstanding, not ones Clerk already verified. When none are outstanding, the DNS screen says what is (the SSL certificate, Clerk finalizing, or a record list Clerk didn't return) instead of a "Configure DNS" page with no records, and the closing screen says "Not yet verified" rather than "DNS pending" so it is right when DNS is done and something else is pending.
- Each DNS record host is named the same way on every screen, the note about what Clerk manages moved off the rows the user has to add, the Domains-page pointer carries its link, and the closing line says what happens next instead of implying that skipping the check finishes the deploy, and no longer promises an OAuth step on resume when OAuth has already run.
- Agent-mode `nextAction` tells the agent to add pending DNS records instead of polling, at `complete` says the production keys still have to reach the host, and names OAuth providers the CLI could not configure so an agent doesn't report OAuth as done. Human-mode `clerk deploy status` prints the pending records, never says "ask the user", and resumes with `clerk deploy` rather than a flag that only affects agents.
- The wizard's new sentences wrap at 76 columns so they stay inside the gutter on an 80-column terminal. The agent JSON report gains a `urls` field with the production instance's Dashboard page and its Domains page. Human-mode `deploy status` now renders its sentence from the same classification of the report as `nextAction` rather than by rewriting that sentence; both read the same as before this change.
39 changes: 39 additions & 0 deletions packages/cli-core/src/cli-program.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,45 @@ test("deploy relies on global options", () => {
expect(optionNames).toEqual([]);
});

test("deploy --help describes the bare command, not only the status subcommand", () => {
// The wizard is a hidden default subcommand, so without the long
// description the help lists only `status` and reads as if the CLI can only
// watch a deploy. Both headless test agents concluded exactly that.
const program = createProgram();
program.configureOutput({ getOutHelpWidth: () => 80 });
const deploy = program.commands.find((command) => command.name() === "deploy")!;
deploy.configureOutput({ getOutHelpWidth: () => 80 });
const help = deploy.helpInformation().replace(new RegExp(String.raw`\x1b\[[0-9;]*m`, "g"), "");

expect(help).toBe(
[
"Usage: clerk deploy [options] [command]",
"",
"Deploy a Clerk application to production.",
"",
"Running `clerk deploy` with no subcommand starts an interactive setup that",
"creates the production instance, prints the DNS records you must add, collects",
"production OAuth credentials, and verifies the domain. It needs a terminal;",
"re-run it at any time to resume where you left off.",
"",
"When run by an agent (or without a TTY), it is read-only: it prints a JSON",
"status report with the current state and a `nextAction` field saying what to",
"do next. `clerk deploy status` prints the same report; add `--wait` to keep",
"checking until DNS, SSL, and email DNS are verified.",
"",
"Options:",
" -h, --help Display help for command",
"",
"Commands:",
" help [command] Display help for command",
" status [options] Show production deploy status (read-only)",
"",
].join("\n"),
);
// The root `clerk --help` table keeps the one-line summary.
expect(deploy.summary()).toBe("Deploy a Clerk application to production");
});

test("deploy status exposes wait option", () => {
const program = createProgram();
const deploy = program.commands.find((command) => command.name() === "deploy")!;
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-core/src/commands/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ When `clerk init` runs in accountless mode it writes `.clerk/keyless.json` conta
- **404** — claim token expired or application already deleted; breadcrumb is cleared and a warning is shown.
- **403** — authenticated account has no active organization; breadcrumb is cleared and a warning is shown.
- **Any other error** — treated as transient; breadcrumb is preserved so the next login retries.
- **Success** — application is claimed and linked, `.env` is updated via `clerk env pull`, breadcrumb is deleted.
- **Success** — application is claimed and linked, `.env` is updated via `clerk env pull`, breadcrumb is deleted. The command prints the Dashboard URL of the claimed app's development instance, since this is the first time the app has a home in an account.

#### API Endpoints

Expand Down
90 changes: 89 additions & 1 deletion packages/cli-core/src/commands/auth/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,12 @@ mock.module("../../lib/first-application.ts", () => ({
ensureFirstApplication: () => mockEnsureFirstApplication(),
}));

const mockAttemptAutoclaim = mock(async (_cwd: string): Promise<unknown> => ({
status: "not_keyless",
}));

mock.module("../../lib/autoclaim.ts", () => ({
attemptAutoclaim: async () => ({ status: "not_keyless" }),
attemptAutoclaim: (cwd: string) => mockAttemptAutoclaim(cwd),
}));

const { setLogLevel } = await import("../../lib/log.ts");
Expand Down Expand Up @@ -127,6 +131,8 @@ describe("login", () => {
mockOpenBrowser.mockReset();
mockEnsureFirstApplication.mockReset();
mockEnsureFirstApplication.mockResolvedValue(undefined);
mockAttemptAutoclaim.mockReset();
mockAttemptAutoclaim.mockResolvedValue({ status: "not_keyless" });
mockIsHuman.mockReturnValue(false);
mockOpenBrowser.mockResolvedValue({ ok: true, launcher: "test" });
mockRevokeToken.mockResolvedValue("revoked");
Expand Down Expand Up @@ -521,6 +527,88 @@ describe("login", () => {
expect(captured.err).toContain("Linked to `app_abc123`");
});

test("prints where the claimed app now lives in the Dashboard", async () => {
// First time this app has a home in an account; nothing else in the flow
// says where it is, so the claim line has to.
mockGetValidToken.mockResolvedValue(null);
mockOAuthSuccess();
mockResolveProfile.mockResolvedValue(undefined);
mockAttemptAutoclaim.mockResolvedValue({
status: "claimed",
envPulled: true,
app: {
application_id: "app_claimed",
name: "bad-agent",
instances: [
{
instance_id: "ins_dev_claimed",
environment_type: "development",
publishable_key: "pk_test_claimed",
},
],
},
});

await runLogin();

expect(captured.err).toContain("Claimed and linked application: `bad-agent`");
// The URL sits on its own indented line so the sentence fits the frame.
expect(captured.err).toMatch(
/Your app now lives in your Clerk account:\n[^\n]*? {2}https:\/\/dashboard\.clerk\.com\/apps\/app_claimed\/instances\/ins_dev_claimed/,
);
});

test("links to the development instance even when production is listed first", async () => {
mockGetValidToken.mockResolvedValue(null);
mockOAuthSuccess();
mockResolveProfile.mockResolvedValue(undefined);
mockAttemptAutoclaim.mockResolvedValue({
status: "claimed",
envPulled: true,
app: {
application_id: "app_claimed",
name: "bad-agent",
instances: [
{
instance_id: "ins_prod_claimed",
environment_type: "production",
publishable_key: "pk_live_claimed",
},
{
instance_id: "ins_dev_claimed",
environment_type: "development",
publishable_key: "pk_test_claimed",
},
],
},
});

await runLogin();

expect(captured.err).toContain(
"https://dashboard.clerk.com/apps/app_claimed/instances/ins_dev_claimed",
);
expect(captured.err).not.toContain("ins_prod_claimed");
});

test("a claim response without instances still reports the claim and does not throw", async () => {
// The Dashboard line is a nice-to-have; a missing array in API JSON must
// not turn a claim that already succeeded server-side into a failed login.
mockGetValidToken.mockResolvedValue(null);
mockOAuthSuccess();
mockResolveProfile.mockResolvedValue(undefined);
mockAttemptAutoclaim.mockResolvedValue({
status: "claimed",
envPulled: true,
app: { application_id: "app_claimed", name: "bad-agent" },
});

await runLogin();

expect(captured.err).toContain("Claimed and linked application: `bad-agent`");
expect(captured.err).not.toContain("Your app now lives");
});

test("shows default next steps when not linked", async () => {
mockGetValidToken.mockResolvedValue(null);
mockOAuthSuccess();
Expand Down
13 changes: 12 additions & 1 deletion packages/cli-core/src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
revokeToken,
type UserInfo,
} from "../../lib/token-exchange.ts";
import { getOAuthConfig } from "../../lib/environment.ts";
import { buildDashboardUrl, getOAuthConfig } from "../../lib/environment.ts";
import {
createOAuthSession,
getStoredSession,
Expand Down Expand Up @@ -230,6 +230,17 @@ async function handleAutoclaim(cwd: string): Promise<AutoclaimResult> {
if (result.status === "claimed") {
const label = result.app.name || result.app.application_id;
log.success(`Claimed and linked application: \`${label}\``);
// First time this app has a home in an account; say where it is.
// Deserialized API JSON; a missing array must not fail a claim that
// already succeeded server-side.
const development = result.app.instances?.find(
(instance) => instance.environment_type === "development",
);
if (development) {
// URL on its own line: with it, the sentence is wider than the frame.
log.info("Your app now lives in your Clerk account:");
log.info(` ${buildDashboardUrl(result.app.application_id, development.instance_id)}`);
}
}

const warning = CLAIM_WARNINGS[result.status];
Expand Down
13 changes: 7 additions & 6 deletions packages/cli-core/src/commands/deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ In agent mode, `clerk deploy status` emits JSON on stdout with:
- `complete`: `true` only when the domain is verified and all supported OAuth providers enabled in development have production credentials.
- `state`: `complete`, `domain_pending`, `oauth_pending`, `domain_provisioning`, `not_started`, or `interrupted`.
- `domainStatus`: per-component DNS, SSL, and email DNS status when a domain exists.
- `pendingDnsRecords`: CNAME records still tied to pending DNS-backed checks.
- `pendingDnsRecords`: CNAME records still tied to pending DNS-backed checks, each with `host`, `value`, and Clerk's `required` flag (some targets are optional).
- `oauth`: configured, pending, and unsupported provider slugs.
- `nextAction`: the next step an agent should present to the user, including the Clerk Dashboard domains URL when a production instance exists. Agents should ask whether to open that URL for the user.
- `urls`: the production instance's Dashboard page (`instance`) and its Domains page (`domains`), or `null` before a production instance exists. The same URLs appear in `nextAction` prose; this field is the one to read programmatically.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- `nextAction`: the next step an agent should present to the user. While domain setup remains (`domain_provisioning` and `domain_pending`) it includes the Clerk Dashboard domains URL, and agents should ask whether to open that URL for the user; `not_started`, `interrupted`, and `oauth_pending` carry no URL, and `complete` carries the instance root instead. While DNS or email DNS records are unverified it says to add the records in `pendingDnsRecords` at the domain's DNS provider rather than to keep polling; if that list is empty (the API returned no CNAME targets) it says so and points at the Dashboard Domains page instead; when only SSL is pending it says to wait. At `complete` it says the production keys still have to reach the host (`clerk env pull --instance prod`, alongside the other Clerk variables in the env file) and to sign up on the domain to confirm — "complete" is Clerk's side only — and links the instance root (users, settings, billing) instead of the domains page, since nothing is left to monitor there. At `complete` and `oauth_pending` it also names any providers in `oauth.unsupported`, since `oauth.complete` covers only what the CLI manages and those providers' sign-in fails in production until configured in the Dashboard. `oauth_pending` carries no Domains URL (the domain is verified). Human mode prints its own sentence, rendered from the same classification of the report (`deployNextStep` in `status.ts`) rather than by rewriting the agent's: no unsupported-provider clause (the warning row above already says it), no "ask the user" (the reader is the user), no `--wait` (human mode always waits; the wizard is what resumes setup), and, when records are pending, the records themselves printed first so the sentence only says what happens once they are added. Before a production instance exists, human mode omits the OAuth row rather than printing "pending: none" for something that was never checked.

Exit codes:

Expand Down Expand Up @@ -176,10 +177,10 @@ Most providers ask for `client_id` and `client_secret`. Provider-specific schema

The CLI keeps small local overrides for provider setup details that schema does not fully describe:

| Provider | Override |
| -------- | ------------------------------------------------------------------------------------------ |
| Google | Optional Google Cloud Console JSON import and OAuth consent screen warning |
| Apple | `.p8` file import, production-required `team_id` and `key_id`, native-only field omissions |
| Provider | Override |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Google | Optional Google Cloud Console JSON import, OAuth consent screen warning, and a tip that the consent-screen name is what users see, so choose the name you want them to see |
| Apple | `.p8` file import, production-required `team_id` and `key_id`, native-only field omissions |

For Google, the wizard can load `client_id` and `client_secret` from the top-level `web` object in a Google Cloud Console OAuth client JSON file, or from `installed` for desktop-style client downloads. The file contents are used in memory and are not written to CLI config.

Expand Down
Loading
Loading