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
7 changes: 7 additions & 0 deletions .changeset/dry-run-project-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"cf": patch
---

Avoid project setup changes during deploy dry runs

Pass `--dry-run` to framework setup and Wrangler config conversion for `cf deploy`, `cf workers versions create`, and `cf workers triggers deploy`. If setup is needed, show the planned changes and skip the build and upload.
37 changes: 37 additions & 0 deletions packages/cli/src/__tests__/commands/deploy/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { existsSync, readFileSync } from "node:fs";
import {
mockConsoleMethods,
runInTempDir,
Expand Down Expand Up @@ -1137,6 +1138,42 @@ describe("cf deploy", () => {
});

describe("--dry-run", () => {
it("keeps existing project files unchanged and skips the build when setup is needed", async () => {
const packageJson = JSON.stringify({
name: "astro-project",
scripts: { deploy: "astro build && wrangler deploy" },
dependencies: {
astro: "7.3.5",
"@astrojs/cloudflare": "14.3.3",
},
devDependencies: { cf: "1.0.0-beta.6", wrangler: "^4.142.0" },
});
const tsconfig = '{"include":["src/**/*"]}\n';
const lockfile = "existing lockfile\n";
const requests = recordRequests();
await seed({
"package.json": packageJson,
"package-lock.json": lockfile,
"tsconfig.json": tsconfig,
"wrangler.jsonc": '{"name":"astro-project"}',
"node_modules/astro/package.json": JSON.stringify({
name: "astro",
version: "7.3.5",
}),
});

const { exitCode } = await runCf(["deploy", "--dry-run"]);

expect(exitCode).toBe(0);
expect(readFileSync("package.json", "utf8")).toBe(packageJson);
expect(readFileSync("package-lock.json", "utf8")).toBe(lockfile);
expect(readFileSync("tsconfig.json", "utf8")).toBe(tsconfig);
expect(existsSync("public/.assetsignore")).toBe(false);
expect(buildDelegateWasCalled()).toBe(false);
expect(requests).toEqual([]);
expect(std.out).toContain("Autoconfig process run in dry-run mode");
});

it("does not upload the worker", async () => {
const upload = mockWorkerUpload();
await seed({
Expand Down
27 changes: 26 additions & 1 deletion packages/cli/src/__tests__/commands/migrate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ describe("cf migrate", () => {
expect(migrateWranglerToCf).not.toHaveBeenCalled();
});

it("offers to run the same migration for project workflows", async () => {
it("offers to convert a Wrangler config before project setup", async () => {
await seed({ "wrangler.jsonc": "{}" });
const confirmMigration = vi.fn().mockResolvedValue(true);

Expand All @@ -191,6 +191,31 @@ describe("cf migrate", () => {
);
});

it("dry-runs an accepted Wrangler config conversion before project setup", async () => {
await seed({ "wrangler.jsonc": "{}" });
const confirmMigration = vi.fn().mockResolvedValue(true);

await expect(
maybeMigrateWranglerProject(
process.cwd(),
confirmMigration,
"stdout",
true
)
).resolves.toBe(true);

expect(migrateWranglerToCf).toHaveBeenCalledWith(
path.join(process.cwd(), "wrangler.jsonc"),
{
bundler: "wrangler",
dryRun: true,
force: false,
installDependencies: true,
}
);
expect(migrateWranglerToCf).toHaveBeenCalledOnce();
});

it("uses Vite for automatic migration when the plugin is declared", async () => {
await seed({
"config/package.json": JSON.stringify({
Expand Down
21 changes: 21 additions & 0 deletions packages/cli/src/__tests__/commands/triggers-deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,27 @@ describe("cf workers triggers deploy", () => {
expect(requests).toEqual([]);
});

it("skips the build and trigger deployment when setup is needed during a dry run", async () => {
const requests = recordRequests();
await seed({
"package.json": JSON.stringify({
name: "astro-project",
dependencies: { astro: "7.3.5" },
}),
"node_modules/astro/package.json": JSON.stringify({
name: "astro",
version: "7.3.5",
}),
});

const { exitCode } = await runCf([...TRIGGERS_DEPLOY_COMMAND, "--dry-run"]);

expect(exitCode).toBe(0);
expect(buildDelegateWasCalled()).toBe(false);
expect(requests).toEqual([]);
expect(std.out).toContain("Autoconfig process run in dry-run mode");
});

it("builds and deploys scheduled triggers with --local=false", async () => {
let schedulesBody: unknown;
msw.use(
Expand Down
58 changes: 57 additions & 1 deletion packages/cli/src/__tests__/lib/autoconfig-migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ describe("project preparation", () => {
expect(mocks.maybeMigrateWranglerProject).not.toHaveBeenCalled();
});

it("does not run setup for a configured project during a dry run", async () => {
const configuredDetails = { ...unconfiguredDetails, configured: true };
mocks.getDetailsForAutoConfig.mockResolvedValue(configuredDetails);

await expect(prepareProject("/project", { dryRun: true })).resolves.toEqual(
{
details: configuredDetails,
}
);
expect(mocks.maybeMigrateWranglerProject).not.toHaveBeenCalled();
expect(mocks.runAutoConfig).not.toHaveBeenCalled();
});

it("offers migration even when autoconfig cannot analyze the legacy project", async () => {
const configuredDetails = {
...unconfiguredDetails,
Expand All @@ -67,7 +80,7 @@ describe("project preparation", () => {
expect(mocks.runAutoConfig).not.toHaveBeenCalled();
});

it("runs autoconfig when migration is unavailable or declined", async () => {
it("runs framework setup when Wrangler config conversion did not run", async () => {
const configuration = {
scripts: {},
outputDir: "dist",
Expand All @@ -83,4 +96,47 @@ describe("project preparation", () => {
});
expect(mocks.runAutoConfig).toHaveBeenCalledOnce();
});

it("dry-runs framework setup when Wrangler config conversion did not run", async () => {
mocks.getDetailsForAutoConfig.mockResolvedValue(unconfiguredDetails);
mocks.maybeMigrateWranglerProject.mockResolvedValue(false);
mocks.runAutoConfig.mockResolvedValue({ buildCommand: "npm run build" });

await expect(
prepareProject("/project", { dryRun: true })
).resolves.toMatchObject({
details: unconfiguredDetails,
setupNeeded: true,
});
expect(mocks.runAutoConfig).toHaveBeenCalledWith(
unconfiguredDetails,
expect.objectContaining({ dryRun: true, runBuild: false })
);
expect(mocks.maybeMigrateWranglerProject).toHaveBeenCalledWith(
"/project",
expect.any(Function),
undefined,
true
);
});

it("stops after an accepted Wrangler config conversion dry run", async () => {
mocks.getDetailsForAutoConfig.mockResolvedValue(unconfiguredDetails);
mocks.maybeMigrateWranglerProject.mockResolvedValue(true);

await expect(prepareProject("/project", { dryRun: true })).resolves.toEqual(
{
details: unconfiguredDetails,
setupNeeded: true,
}
);
expect(mocks.maybeMigrateWranglerProject).toHaveBeenCalledWith(
"/project",
expect.any(Function),
undefined,
true
);
expect(mocks.getDetailsForAutoConfig).toHaveBeenCalledOnce();
expect(mocks.runAutoConfig).not.toHaveBeenCalled();
});
});
16 changes: 13 additions & 3 deletions packages/cli/src/commands/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,25 @@ interface RunBuildOptions extends CommandOutputOptions {
// Validate the Worker the caller will consume, so an invalid default
// Worker cannot block a workflow that selected another one.
worker?: string;
dryRun?: boolean;
}

export async function runBuild(
mode?: string,
{ worker: selectedWorker, ...options }: RunBuildOptions = {},
{ worker: selectedWorker, dryRun = false, ...options }: RunBuildOptions = {},
ctx: { isPreview?: boolean } = {}
): Promise<void> {
): Promise<"built" | "setup-needed"> {
const output = options.output ?? "stdout";
const cwd = process.cwd();
const { details, configuration } = await prepareProject(cwd, options);
const { details, configuration, setupNeeded } = await prepareProject(cwd, {
...options,
dryRun,
});
if (setupNeeded) {
// A Wrangler config conversion or framework setup was dry-run, so the
// configuration the build needs has not been written yet.
return "setup-needed";
Comment thread
petebacondarwin marked this conversation as resolved.
}
const buildCommand = configuration?.buildCommand ?? details?.buildCommand;
const env: Record<string, string> = {
...details?.env,
Expand Down Expand Up @@ -91,6 +100,7 @@ export async function runBuild(
output: output === "stderr" ? process.stderr : undefined,
});
}
return "built";
}

function formatImplName(discovered: DiscoveredImpl): string {
Expand Down
15 changes: 11 additions & 4 deletions packages/cli/src/commands/deploy/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,14 @@ type UploadArgs = SharedUploadArgs & {
export async function runUpload(argv: UploadArgs, ctx: UploadCommand) {
// Delegate the build before applying cf's dotenv values.
if (!argv.prebuilt) {
await runBuild(argv.mode, { worker: argv.worker });
const build = await runBuild(argv.mode, {
worker: argv.worker,
dryRun: argv["dry-run"],
});
if (build === "setup-needed") {
clack.log.success("--dry-run: exiting now.");
return;
}
clack.log.message("", { spacing: 0 });
}

Expand Down Expand Up @@ -213,7 +220,7 @@ async function uploadBuildOutput(argv: UploadArgs, ctx: UploadCommand) {
);
}

clack.log.success(
argv["dry-run"] ? "Dry run complete" : `${ctx.command} complete`
);
if (!argv["dry-run"]) {
clack.log.success(`${ctx.command} complete`);
}
}
15 changes: 11 additions & 4 deletions packages/cli/src/commands/workers/triggers/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ const triggersDeployCommand: CommandModule<
}

if (!argv.prebuilt) {
await runBuild(argv.mode, { worker: argv.worker });
const build = await runBuild(argv.mode, {
worker: argv.worker,
dryRun: argv["dry-run"],
});
if (build === "setup-needed") {
clack.log.success("--dry-run: exiting now.");
return;
}
clack.log.message("", { spacing: 0 });
}

Expand Down Expand Up @@ -100,9 +107,9 @@ async function deployTriggers(argv: TriggersDeployArgs): Promise<void> {
await triggersDeploy(
createTriggerProps(worker, wranglerConfig, accountId, argv)
);
clack.log.success(
argv["dry-run"] ? "Dry run complete" : "Trigger deploy complete"
);
if (!argv["dry-run"]) {
clack.log.success("Trigger deploy complete");
}
}

export default triggersDeployCommand;
41 changes: 31 additions & 10 deletions packages/cli/src/lib/autoconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export interface CommandOutputOptions {
output?: CommandOutput;
}

interface ProjectPreparationOptions extends CommandOutputOptions {
dryRun?: boolean;
}

export interface RunProjectCommandOptions extends CommandOutputOptions {
env?: Readonly<Record<string, string>>;
args?: readonly string[];
Expand Down Expand Up @@ -77,45 +81,62 @@ export async function analyzeProject(
}
}

/**
* Run framework autoconfig after detection, applying or dry-running setup
* without building.
*/
export async function configureProject(
details: AutoConfigDetails,
options: CommandOutputOptions = {}
options: ProjectPreparationOptions = {}
): Promise<AutoConfigSummary> {
return runAutoConfig(details, {
target: "cf",
context: createAutoConfigContext(options),
dryRun: options.dryRun,
runBuild: false,
});
}

/**
* Choose project setup before a build: an accepted Wrangler config conversion
* takes precedence over framework setup. In a dry run, `setupNeeded` means
* a setup route was selected but not applied, so callers must skip the build.
*/
export async function prepareProject(
Comment thread
petebacondarwin marked this conversation as resolved.
cwd: string,
options: CommandOutputOptions = {}
options: ProjectPreparationOptions = {}
): Promise<{
details: AutoConfigDetails | undefined;
configuration?: AutoConfigSummary;
setupNeeded?: true;
}> {
let details = await analyzeProject(cwd, options);
if (details?.configured) {
return { details };
}

const context = createAutoConfigContext(options);
if (
await maybeMigrateWranglerProject(
cwd,
(text, confirmOptions) => context.dialogs.confirm(text, confirmOptions),
options.output
)
) {
const migrationRan = await maybeMigrateWranglerProject(
cwd,
(text, confirmOptions) => context.dialogs.confirm(text, confirmOptions),
options.output,
options.dryRun
);
if (migrationRan) {
if (options.dryRun) {
return { details, setupNeeded: true };
}
details = await analyzeProject(cwd, options);
return { details };
}

return {
details,
...(details
? { configuration: await configureProject(details, options) }
? {
configuration: await configureProject(details, options),
...(options.dryRun ? { setupNeeded: true as const } : {}),
}
: {}),
};
}
Expand Down
Loading
Loading