Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/deploy-skip-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"playground-cli": minor
---

`dot deploy` now asks whether to run the build step before deploying, defaulting to "yes" so the common case is still a single Enter press. Pass `--no-build` to skip the build non-interactively (useful when you've already built the project and just want to re-upload existing artifacts from `buildDir`). The confirm screen and headless summary both show whether the run will rebuild or reuse existing artifacts.
63 changes: 52 additions & 11 deletions src/commands/deploy/DeployScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ export interface DeployScreenInputs {
buildDir: string | null;
mode: SignerMode | null;
publishToPlayground: boolean | null;
skipBuild: boolean | null;
userSigner: ResolvedSigner | null;
onDone: (outcome: DeployOutcome | null) => void;
}

type Stage =
| { kind: "prompt-build" }
| { kind: "prompt-signer" }
| { kind: "prompt-buildDir" }
| { kind: "prompt-domain" }
Expand All @@ -57,6 +59,7 @@ interface Resolved {
buildDir: string;
domain: string;
publishToPlayground: boolean;
skipBuild: boolean;
}

export function DeployScreen({
Expand All @@ -65,20 +68,28 @@ export function DeployScreen({
buildDir: initialBuildDir,
mode: initialMode,
publishToPlayground: initialPublish,
skipBuild: initialSkipBuild,
userSigner,
onDone,
}: DeployScreenInputs) {
const [mode, setMode] = useState<SignerMode | null>(initialMode);
const [buildDir, setBuildDir] = useState<string | null>(initialBuildDir);
const [domain, setDomain] = useState<string | null>(initialDomain);
const [publishToPlayground, setPublishToPlayground] = useState<boolean | null>(initialPublish);
const [skipBuild, setSkipBuild] = useState<boolean | null>(initialSkipBuild);
const [domainError, setDomainError] = useState<string | null>(null);
// Captured from the availability check; feeds `resolveSignerSetup` so
// the summary card shows the correct phone-approval count (register +
// PoP upgrade = 4 DotNS taps, vs register alone = 3, vs update = 1).
const [plan, setPlan] = useState<DeployPlan | null>(null);
const [stage, setStage] = useState<Stage>(() =>
pickInitialStage(initialMode, initialBuildDir, initialDomain, initialPublish),
pickInitialStage(
initialSkipBuild,
initialMode,
initialBuildDir,
initialDomain,
initialPublish,
),
);

// Passed down to RunningStage; read back on completion for the sparkline.
Expand All @@ -87,20 +98,27 @@ export function DeployScreen({
const finalChunkTimingsRef = useRef<number[]>([]);

const advance = (
nextSkipBuild: boolean | null = skipBuild,
nextMode: SignerMode | null = mode,
nextBuildDir: string | null = buildDir,
nextDomain: string | null = domain,
nextPublish: boolean | null = publishToPlayground,
) => {
const s = pickNextStage(nextMode, nextBuildDir, nextDomain, nextPublish);
const s = pickNextStage(nextSkipBuild, nextMode, nextBuildDir, nextDomain, nextPublish);
setStage(s);
};

const resolved = useMemo<Resolved | null>(() => {
if (mode === null || buildDir === null || domain === null || publishToPlayground === null)
if (
mode === null ||
buildDir === null ||
domain === null ||
publishToPlayground === null ||
skipBuild === null
)
return null;
return { mode, buildDir, domain, publishToPlayground };
}, [mode, buildDir, domain, publishToPlayground]);
return { mode, buildDir, domain, publishToPlayground, skipBuild };
}, [mode, buildDir, domain, publishToPlayground, skipBuild]);

// Dynamic terminal tab title: subtitle becomes the domain once we know it.
const headerSubtitle = resolved?.domain ?? domain ?? undefined;
Expand All @@ -114,6 +132,21 @@ export function DeployScreen({
right={VERSION_LABEL}
/>

{stage.kind === "prompt-build" && (
<Select<boolean>
label="build before deploy?"
options={[
{ value: false, label: "yes", hint: "rebuild the project" },
{ value: true, label: "no", hint: "use existing build in buildDir" },
]}
initialIndex={0}
onSelect={(skip) => {
setSkipBuild(skip);
advance(skip);
}}
/>
)}

{stage.kind === "prompt-signer" && (
<Select<SignerMode>
label="signer"
Expand All @@ -131,7 +164,7 @@ export function DeployScreen({
]}
onSelect={(m) => {
setMode(m);
advance(m);
advance(skipBuild, m);
}}
/>
)}
Expand All @@ -142,7 +175,7 @@ export function DeployScreen({
initial={DEFAULT_BUILD_DIR}
onSubmit={(v) => {
setBuildDir(v);
advance(mode, v);
advance(skipBuild, mode, v);
}}
/>
)}
Expand Down Expand Up @@ -174,7 +207,7 @@ export function DeployScreen({
onAvailable={(result) => {
setDomain(result.fullDomain);
setPlan(result.plan);
advance(mode, buildDir, result.fullDomain);
advance(skipBuild, mode, buildDir, result.fullDomain);
}}
onUnavailable={(reason) => {
setDomainError(reason);
Expand All @@ -193,7 +226,7 @@ export function DeployScreen({
initialIndex={0}
onSelect={(yes) => {
setPublishToPlayground(yes);
advance(mode, buildDir, domain, yes);
advance(skipBuild, mode, buildDir, domain, yes);
}}
/>
)}
Expand Down Expand Up @@ -248,20 +281,23 @@ export function DeployScreen({
// ── Stage pickers ────────────────────────────────────────────────────────────

function pickInitialStage(
skipBuild: boolean | null,
mode: SignerMode | null,
buildDir: string | null,
domain: string | null,
publish: boolean | null,
): Stage {
return pickNextStage(mode, buildDir, domain, publish);
return pickNextStage(skipBuild, mode, buildDir, domain, publish);
}

function pickNextStage(
skipBuild: boolean | null,
mode: SignerMode | null,
buildDir: string | null,
domain: string | null,
publish: boolean | null,
): Stage {
if (skipBuild === null) return { kind: "prompt-build" };
if (mode === null) return { kind: "prompt-signer" };
if (buildDir === null) return { kind: "prompt-buildDir" };
if (domain === null) return { kind: "prompt-domain" };
Expand Down Expand Up @@ -369,6 +405,7 @@ function ConfirmStage({
mode: inputs.mode,
domain: inputs.domain.replace(/\.dot$/, "") + ".dot",
buildDir: inputs.buildDir,
skipBuild: inputs.skipBuild,
publishToPlayground: inputs.publishToPlayground,
approvals: "approvals" in setup ? setup.approvals : [],
});
Expand Down Expand Up @@ -465,7 +502,10 @@ function RunningStage({
onError: (message: string) => void;
}) {
const initialPhases: Record<DeployPhase, PhaseState> = {
build: { status: "pending" },
build: {
status: inputs.skipBuild ? "complete" : "pending",
detail: inputs.skipBuild ? "skipped" : undefined,
},
"storage-and-dotns": { status: "pending" },
playground: {
status: inputs.publishToPlayground ? "pending" : "complete",
Expand Down Expand Up @@ -518,6 +558,7 @@ function RunningStage({
const outcome = await runDeploy({
projectDir,
buildDir: inputs.buildDir,
skipBuild: inputs.skipBuild,
domain: inputs.domain,
mode: inputs.mode,
publishToPlayground: inputs.publishToPlayground,
Expand Down
13 changes: 13 additions & 0 deletions src/commands/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ interface DeployOpts {
domain?: string;
buildDir?: string;
playground?: boolean;
/**
* Commander's auto-negated boolean: defaults to `true`; `--no-build` flips it to `false`.
* We never check for `undefined` here since commander always provides a boolean when
* a `--no-foo` option is declared.
*/
build?: boolean;
env?: Env;
/** Project root. Hidden — defaults to cwd. */
dir?: string;
Expand All @@ -46,6 +52,7 @@ export const deployCommand = new Command("deploy")
"--buildDir <path>",
`Directory containing build artifacts (default: ${DEFAULT_BUILD_DIR})`,
)
.option("--no-build", "Skip the build step and deploy existing artifacts in buildDir")
.option("--playground", "Publish to the playground registry")
.option("--suri <suri>", "Secret URI for the user signer (e.g. //Alice for dev)")
.addOption(
Expand Down Expand Up @@ -216,6 +223,7 @@ async function runHeadless(ctx: {
const publishToPlayground = Boolean(ctx.opts.playground);
const domain = ctx.opts.domain as string;
const buildDir = ctx.opts.buildDir as string;
const skipBuild = ctx.opts.build === false;

// Check availability BEFORE we build + upload, so CI fails fast on a
// Reserved / already-taken name without wasting a chunk upload.
Expand All @@ -239,6 +247,7 @@ async function runHeadless(ctx: {
mode,
domain: availability.fullDomain,
buildDir,
skipBuild,
publishToPlayground,
approvals: setup.approvals,
});
Expand All @@ -247,6 +256,7 @@ async function runHeadless(ctx: {
const outcome = await runDeploy({
projectDir: ctx.projectDir,
buildDir,
skipBuild,
domain,
mode,
publishToPlayground,
Expand Down Expand Up @@ -275,6 +285,9 @@ function runInteractive(ctx: {
mode: (ctx.opts.signer as SignerMode | undefined) ?? null,
publishToPlayground:
ctx.opts.playground !== undefined ? Boolean(ctx.opts.playground) : null,
// Only pre-fill when the user explicitly asked to skip via `--no-build`;
// otherwise show the prompt so they can hit Enter on the default "yes".
skipBuild: ctx.opts.build === false ? true : null,
userSigner: ctx.userSigner,
onDone: (outcome: DeployOutcome | null) => {
if (settled) return;
Expand Down
27 changes: 27 additions & 0 deletions src/commands/deploy/summary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe("buildSummaryView", () => {
mode: "dev",
domain: "my-app.dot",
buildDir: "dist",
skipBuild: false,
publishToPlayground: false,
approvals: [],
});
Expand All @@ -20,6 +21,7 @@ describe("buildSummaryView", () => {
mode: "dev",
domain: "my-app.dot",
buildDir: "dist",
skipBuild: false,
publishToPlayground: true,
approvals: [{ phase: "playground", label: "Publish to Playground registry" }],
});
Expand All @@ -32,6 +34,7 @@ describe("buildSummaryView", () => {
mode: "phone",
domain: "my-app.dot",
buildDir: "dist",
skipBuild: false,
publishToPlayground: true,
approvals: [
{ phase: "dotns", label: "Reserve domain (DotNS commitment)" },
Expand All @@ -48,6 +51,28 @@ describe("buildSummaryView", () => {
"4. Publish to Playground registry",
]);
});

it("Build row reflects the skipBuild flag", () => {
const rebuild = buildSummaryView({
mode: "dev",
domain: "my-app.dot",
buildDir: "dist",
skipBuild: false,
publishToPlayground: false,
approvals: [],
});
expect(rebuild.rows.find((r) => r.label === "Build")?.value).toBe("rebuild first");

const skip = buildSummaryView({
mode: "dev",
domain: "my-app.dot",
buildDir: "dist",
skipBuild: true,
publishToPlayground: false,
approvals: [],
});
expect(skip.rows.find((r) => r.label === "Build")?.value).toBe("skip (use existing)");
});
});

describe("renderSummaryText", () => {
Expand All @@ -57,6 +82,7 @@ describe("renderSummaryText", () => {
mode: "dev",
domain: "my-app.dot",
buildDir: "dist",
skipBuild: false,
publishToPlayground: false,
approvals: [],
}),
Expand All @@ -70,6 +96,7 @@ describe("renderSummaryText", () => {
mode: "phone",
domain: "x.dot",
buildDir: "dist",
skipBuild: false,
publishToPlayground: false,
approvals: [
{ phase: "dotns", label: "Reserve domain" },
Expand Down
2 changes: 2 additions & 0 deletions src/commands/deploy/summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface SummaryInputs {
mode: SignerMode;
domain: string;
buildDir: string;
skipBuild: boolean;
publishToPlayground: boolean;
approvals: DeployApproval[];
}
Expand All @@ -31,6 +32,7 @@ export function buildSummaryView(input: SummaryInputs): SummaryView {
headline: `Deploying ${input.domain}`,
rows: [
{ label: "Signer", value: MODE_LABEL[input.mode] },
{ label: "Build", value: input.skipBuild ? "skip (use existing)" : "rebuild first" },
{ label: "Build dir", value: input.buildDir },
{
label: "Publish",
Expand Down
Loading