Skip to content

Commit 17877b1

Browse files
committed
refactor(deploy): extract mock api into its own module
1 parent e8cebec commit 17877b1

6 files changed

Lines changed: 391 additions & 214 deletions

File tree

packages/cli-core/src/commands/deploy/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Deploy Command
22

3-
> **API-resolved state, mocked lifecycle.** Human mode resolves the linked application, production domains, deploy status, and instance config from the API layer on each run. Application/domain/config reads use live PLAPI helpers; production lifecycle calls (`validate_cloning`, `production_instance`, `deploy_status`, `ssl_retry`, `mail_retry`) plus production config PATCH still go through `commands/deploy/api.ts`, where they are mocked with the real Platform API request/response shapes.
3+
> **API-resolved state, mocked lifecycle.** Human mode resolves the linked application, production domains, deploy status, and instance config from the API layer on each run. Application/domain/config reads use live PLAPI helpers; production lifecycle calls (`validate_cloning`, `production_instance`, `deploy_status`, `ssl_retry`, `mail_retry`) plus production config PATCH still go through the dispatchers in `commands/deploy/api.ts`, which route to `commands/deploy/mock.ts`, where they are mocked with the real Platform API request/response shapes. All test-flag plumbing and failure-injection helpers also live in `mock.ts` so the surface to delete when the real backend lands is contained to one file.
44
55
Guides a user through deploying their Clerk application to production.
66

@@ -47,7 +47,7 @@ The production-instance lifecycle still calls the helpers in `commands/deploy/ap
4747

4848
This keeps `clerk deploy` from drifting away from the server-side source of truth once these endpoints are backed by production data. Each run resolves the current production instance, domain, deploy status, and OAuth config from the API layer, then prints a checked-off plan before completing the next unfinished action. Re-running `clerk deploy` after production is fully configured shows every deploy action checked off and prints production next steps.
4949

50-
Mocked lifecycle endpoints in `commands/deploy/api.ts` pause for ~2s before returning so spinners and the deploy-status poll feel like real network calls.
50+
Mocked lifecycle endpoints in `commands/deploy/mock.ts` pause for ~2s before returning so spinners and the deploy-status poll feel like real network calls.
5151

5252
If the user presses Ctrl-C after the production instance has been created, the wizard tells them to run `clerk deploy` again and exits with SIGINT code 130. The next run derives the current DNS or OAuth step from API state and resumes without starting another production instance.
5353

packages/cli-core/src/commands/deploy/api.test.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,11 @@ mock.module("../../lib/sleep.ts", () => ({
2222
}));
2323

2424
const deployApiModulePath = "./api.ts?adapter-test";
25-
const {
26-
createProductionInstance,
27-
configureMockDeployApi,
28-
getDeployStatus,
29-
patchInstanceConfig,
30-
validateCloning,
31-
_resetDeployStatusMock,
32-
} = (await import(deployApiModulePath)) as typeof import("./api.ts");
25+
const apiModule = (await import(deployApiModulePath)) as typeof import("./api.ts");
26+
const mockModule = (await import("./mock.ts")) as typeof import("./mock.ts");
27+
const { createProductionInstance, getDeployStatus, patchInstanceConfig, validateCloning } =
28+
apiModule;
29+
const { configureMockDeployApi, _resetDeployStatusMock } = mockModule;
3330

3431
describe("deploy api adapter", () => {
3532
beforeEach(() => {

packages/cli-core/src/commands/deploy/api.ts

Lines changed: 4 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
* locally.
99
*/
1010

11-
import { sleep } from "../../lib/sleep.ts";
12-
import { PlapiError } from "../../lib/errors.ts";
1311
import {
1412
createProductionInstance as liveCreateProductionInstance,
1513
getDeployStatus as liveGetDeployStatus,
@@ -23,6 +21,9 @@ import {
2321
type ProductionInstanceResponse,
2422
type ValidateCloningParams,
2523
} from "../../lib/plapi.ts";
24+
import { mockDeployApi } from "./mock.ts";
25+
26+
export { configureMockDeployApi } from "./mock.ts";
2627

2728
export type {
2829
CnameTarget,
@@ -32,7 +33,7 @@ export type {
3233
ValidateCloningParams,
3334
} from "../../lib/plapi.ts";
3435

35-
type DeployApi = {
36+
export type DeployApi = {
3637
createProductionInstance: (
3738
applicationId: string,
3839
params: CreateProductionInstanceParams,
@@ -48,113 +49,6 @@ type DeployApi = {
4849
) => Promise<Record<string, unknown>>;
4950
};
5051

51-
const MOCK_PRODUCTION_INSTANCE_ID = "MOCKED_NOT_REAL_FIXME";
52-
const MOCK_DOMAIN_ID = "MOCKED_NOT_REAL_FIXME";
53-
const MOCK_PUBLISHABLE_KEY = "MOCKED_NOT_REAL_FIXME";
54-
const MOCK_SECRET_KEY = "MOCKED_NOT_REAL_FIXME";
55-
const MOCK_LATENCY_MS = 2000;
56-
const MOCK_INCOMPLETE_POLLS = 2;
57-
58-
type DeployApiMockOptions = {
59-
failValidateCloning?: boolean;
60-
failCreateProductionInstance?: boolean;
61-
failDnsVerification?: boolean;
62-
failOAuthSave?: boolean;
63-
};
64-
65-
let mockOptions: DeployApiMockOptions = {};
66-
67-
export function configureMockDeployApi(options: DeployApiMockOptions = {}): void {
68-
mockOptions = { ...options };
69-
}
70-
71-
function simulatedDeployApiFailure(step: string): PlapiError {
72-
return new PlapiError(
73-
500,
74-
JSON.stringify({ errors: [{ message: `Simulated deploy failure: ${step}.` }] }),
75-
"clerk deploy test flag",
76-
);
77-
}
78-
79-
async function simulateServerLatency(): Promise<void> {
80-
await sleep(MOCK_LATENCY_MS);
81-
}
82-
83-
function defaultCnameTargets(domain: string): CnameTarget[] {
84-
return [
85-
{ host: `clerk.${domain}`, value: "frontend-api.clerk.services", required: true },
86-
{ host: `accounts.${domain}`, value: "accounts.clerk.services", required: true },
87-
{
88-
host: `clkmail.${domain}`,
89-
value: `mail.${domain}.nam1.clerk.services`,
90-
required: true,
91-
},
92-
];
93-
}
94-
95-
const deployStatusPollCounts = new Map<string, number>();
96-
97-
export function _resetDeployStatusMock(): void {
98-
deployStatusPollCounts.clear();
99-
configureMockDeployApi();
100-
}
101-
102-
export const mockDeployApi: DeployApi = {
103-
async createProductionInstance(_applicationId, params) {
104-
await simulateServerLatency();
105-
if (mockOptions.failCreateProductionInstance) {
106-
throw simulatedDeployApiFailure("production instance creation");
107-
}
108-
return {
109-
instance_id: MOCK_PRODUCTION_INSTANCE_ID,
110-
environment_type: "production",
111-
active_domain: {
112-
id: MOCK_DOMAIN_ID,
113-
name: params.home_url,
114-
},
115-
secret_key: MOCK_SECRET_KEY,
116-
publishable_key: MOCK_PUBLISHABLE_KEY,
117-
cname_targets: defaultCnameTargets(params.home_url),
118-
};
119-
},
120-
121-
async validateCloning() {
122-
await simulateServerLatency();
123-
if (mockOptions.failValidateCloning) {
124-
throw simulatedDeployApiFailure("cloning validation");
125-
}
126-
},
127-
128-
async getDeployStatus(applicationId, envOrInsId) {
129-
await simulateServerLatency();
130-
if (mockOptions.failDnsVerification) {
131-
throw simulatedDeployApiFailure("DNS verification");
132-
}
133-
const key = `${applicationId}:${envOrInsId}`;
134-
const count = (deployStatusPollCounts.get(key) ?? 0) + 1;
135-
deployStatusPollCounts.set(key, count);
136-
return {
137-
status: count > MOCK_INCOMPLETE_POLLS ? "complete" : "incomplete",
138-
};
139-
},
140-
141-
async retryApplicationDomainSSL() {
142-
await simulateServerLatency();
143-
},
144-
145-
async retryApplicationDomainMail() {
146-
await simulateServerLatency();
147-
},
148-
149-
async patchInstanceConfig() {
150-
await simulateServerLatency();
151-
if (mockOptions.failOAuthSave) {
152-
throw simulatedDeployApiFailure("OAuth credential save");
153-
}
154-
return {};
155-
},
156-
};
157-
15852
export const liveDeployApi: DeployApi = {
15953
createProductionInstance: liveCreateProductionInstance,
16054
validateCloning: liveValidateCloning,

packages/cli-core/src/commands/deploy/index.ts

Lines changed: 10 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,9 @@ import { isAgent } from "../../mode.ts";
22
import { isInsideGutter, log } from "../../lib/log.ts";
33
import { sleep } from "../../lib/sleep.ts";
44
import { bar, intro, outro, withSpinner } from "../../lib/spinner.ts";
5-
import {
6-
PlapiError,
7-
UserAbortError,
8-
isPromptExitError,
9-
throwUsageError,
10-
} from "../../lib/errors.ts";
5+
import { UserAbortError, isPromptExitError, throwUsageError } from "../../lib/errors.ts";
116
import { resolveProfile, setProfile } from "../../lib/config.ts";
127
import {
13-
type Application,
148
fetchApplication,
159
fetchInstanceConfig,
1610
listApplicationDomains,
@@ -25,6 +19,15 @@ import {
2519
type CnameTarget,
2620
type ProductionInstanceResponse,
2721
} from "./api.ts";
22+
import {
23+
mockProductionDomain,
24+
mockProductionInstanceConfig,
25+
resolveTestDeployFlags,
26+
simulatedDeployApiFailure,
27+
withMockProductionInstance,
28+
withTestFailureAfterApiCall,
29+
type DeployTestFlags,
30+
} from "./mock.ts";
2831
import { domainConnectUrl } from "./domain-connect.ts";
2932
import {
3033
INTRO_PREAMBLE,
@@ -75,16 +78,6 @@ type DeployOptions = {
7578
testFailOAuthSave?: boolean;
7679
};
7780

78-
type DeployTestFlags = Pick<
79-
DeployContext,
80-
"testForceProductionInstance" | "testFailProductionInstanceCheck" | "testFailDomainLookup"
81-
> & {
82-
testFailValidateCloning?: boolean;
83-
testFailCreateProductionInstance?: boolean;
84-
testFailDnsVerification?: boolean;
85-
testFailOAuthSave?: boolean;
86-
};
87-
8881
const DEPLOY_STATUS_POLL_INTERVAL_MS = 3000;
8982
const DEPLOY_STATUS_MAX_POLLS = 100;
9083

@@ -159,18 +152,6 @@ async function resolveDeployContext(options: DeployOptions): Promise<DeployConte
159152
};
160153
}
161154

162-
function resolveTestDeployFlags(options: DeployOptions): DeployTestFlags {
163-
return {
164-
testForceProductionInstance: options.testForceProductionInstance === true,
165-
testFailProductionInstanceCheck: options.testFailProductionInstanceCheck === true,
166-
testFailDomainLookup: options.testFailDomainLookup === true,
167-
testFailValidateCloning: options.testFailValidateCloning === true,
168-
testFailCreateProductionInstance: options.testFailCreateProductionInstance === true,
169-
testFailDnsVerification: options.testFailDnsVerification === true,
170-
testFailOAuthSave: options.testFailOAuthSave === true,
171-
};
172-
}
173-
174155
function resolveCommandTestFlags(
175156
testFlags: DeployTestFlags,
176157
): Pick<
@@ -193,26 +174,6 @@ function configureDeployApiMocks(testFlags: DeployTestFlags): void {
193174
});
194175
}
195176

196-
function simulatedDeployApiFailure(step: string): PlapiError {
197-
return new PlapiError(
198-
500,
199-
JSON.stringify({ errors: [{ message: `Simulated deploy failure: ${step}.` }] }),
200-
"clerk deploy test flag",
201-
);
202-
}
203-
204-
async function withTestFailureAfterApiCall<T>(
205-
promise: Promise<T>,
206-
shouldFail: boolean | undefined,
207-
step: string,
208-
): Promise<T> {
209-
const result = await promise;
210-
if (shouldFail) {
211-
throw simulatedDeployApiFailure(step);
212-
}
213-
return result;
214-
}
215-
216177
async function resolveLiveApplicationContext(
217178
profile: DeployContext["profile"],
218179
options: { forceMockProductionInstance?: boolean } = {},
@@ -236,23 +197,6 @@ async function resolveLiveApplicationContext(
236197
};
237198
}
238199

239-
function withMockProductionInstance(app: Application): Application {
240-
if (app.instances.some((entry) => entry.environment_type === "production")) {
241-
return app;
242-
}
243-
return {
244-
...app,
245-
instances: [
246-
...app.instances,
247-
{
248-
instance_id: "ins_prod_mock",
249-
environment_type: "production",
250-
publishable_key: "pk_live_test",
251-
},
252-
],
253-
};
254-
}
255-
256200
async function runDeploy(ctx: DeployContext): Promise<void> {
257201
if (!ctx.appId || !ctx.developmentInstanceId) {
258202
log.blank();
@@ -488,34 +432,6 @@ async function loadProductionDomain(ctx: DeployContext): Promise<ApplicationDoma
488432
return domains.data.find((domain) => !domain.is_satellite) ?? domains.data[0];
489433
}
490434

491-
function mockProductionDomain(): ApplicationDomain {
492-
return {
493-
object: "domain",
494-
id: "dmn_prod_mock",
495-
name: "example.com",
496-
is_satellite: false,
497-
is_provider_domain: false,
498-
frontend_api_url: "https://clerk.example.com",
499-
accounts_portal_url: "https://accounts.example.com",
500-
development_origin: "",
501-
cname_targets: [
502-
{ host: "clerk.example.com", value: "frontend-api.clerk.services", required: true },
503-
{ host: "accounts.example.com", value: "accounts.clerk.services", required: true },
504-
{
505-
host: "clkmail.example.com",
506-
value: "mail.example.com.nam1.clerk.services",
507-
required: true,
508-
},
509-
],
510-
created_at: "2026-05-06T00:00:00Z",
511-
updated_at: "2026-05-06T00:00:00Z",
512-
};
513-
}
514-
515-
function mockProductionInstanceConfig(): Record<string, unknown> {
516-
return {};
517-
}
518-
519435
function hasProductionOAuthCredentials(
520436
config: Record<string, unknown>,
521437
provider: OAuthProvider,

0 commit comments

Comments
 (0)