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
14 changes: 13 additions & 1 deletion scripts/preflight-production-deploy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -386,17 +386,29 @@ export async function preflightProductionDeploy({
try {
const secretOutput = await runCommandImpl(["secret", "list", "--name", PRODUCTION_DATABASE_NAME, "--config", configPath, "--format", "json"]);
secrets = verifyWorkerSecretInventory(secretOutput);
} catch {
throw fixedError("Cloudflare Worker secret inventory preflight failed");
}
try {
const inventoryOutput = await runCommandImpl(["d1", "list", "--config", configPath, "--json"]);
verifyD1Inventory(inventoryOutput, identity.databaseId);
} catch {
throw fixedError("Cloudflare D1 identity preflight failed");
}
try {
const schemaOutput = await runCommandImpl([
"d1", "execute", "DB", "--remote", "--config", configPath,
"--command", D1_READ_ONLY_PROBE, "--json",
]);
migrations = verifyD1SchemaAndMigrations(schemaOutput, localMigrationNames);
} catch {
throw fixedError("Cloudflare D1 schema preflight failed");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Name migration-ledger failures in the D1 stage

When the remote command succeeds but verifyD1SchemaAndMigrations rejects the migration ledger or foreign-key results, this catch rewrites the failure as Cloudflare D1 schema preflight failed. That incorrectly directs operators toward schema drift even when the schema signature passed and the applied migration filenames are the only problem; keep the message redacted, but name the combined schema/migration-ledger/integrity stage.

Useful? React with 👍 / 👎.

}
try {
const domainOutput = await fetchDomainsImpl({ accountId, apiToken });
verifyCustomDomainInventory(domainOutput, identity.publicHost);
} catch {
throw fixedError("Cloudflare production preflight command failed");
throw fixedError("Cloudflare custom domain preflight failed");
}
log(`Production preflight confirmed ${secrets.requiredSecretCount} required Worker secret names and ${migrations.migrationCount} applied D1 migrations.`);
return { requiredSecretCount: secrets.requiredSecretCount, migrationCount: migrations.migrationCount };
Expand Down
66 changes: 52 additions & 14 deletions tests/production-deploy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@ test("production preflight rejects secret, D1 identity, schema, integrity, and m
assert.throws(() => validateLocalMigrationNames(["1_initial.sql"]), /unexpected structure/);
});

test("preflight validates local migrations before remote access and redacts injected command failures", async () => {
test("preflight validates local migrations before remote access and reports only the failed safe stage", async () => {
let remoteCallCount = 0;
const sourceConfig = await readFile("wrangler.toml", "utf8");
const productionConfig = renderProductionConfig(
Expand All @@ -676,22 +676,60 @@ test("preflight validates local migrations before remote access and redacts inje
assert.equal(remoteCallCount, 0);

const injectedPrivateValue = "injected-private-command-output";
await assert.rejects(
preflightProductionDeploy({
accountId: "synthetic-account-id",
apiToken: "synthetic-api-token",
readFileImpl: async () => productionConfig,
listMigrationsImpl: async () => ["0001_initial.sql"],
const inventory = JSON.stringify(REQUIRED_WORKER_SECRET_NAMES.map((name) => ({ name, type: "secret_text" })));
const databaseIdentity = JSON.stringify([{ name: "workout-tracker", uuid: SYNTHETIC_DATABASE_ID }]);
const stages = [
{
expectedMessage: "Cloudflare Worker secret inventory preflight failed",
runCommandImpl: async () => { throw new Error(injectedPrivateValue); },
fetchDomainsImpl: async () => { throw new Error("unreachable"); },
},
{
expectedMessage: "Cloudflare D1 identity preflight failed",
runCommandImpl: async (arguments_) => {
if (arguments_[0] === "secret") return inventory;
throw new Error(injectedPrivateValue);
},
fetchDomainsImpl: async () => { throw new Error("unreachable"); },
},
{
expectedMessage: "Cloudflare D1 schema preflight failed",
runCommandImpl: async (arguments_) => {
if (arguments_[0] === "secret") return inventory;
if (arguments_[1] === "list") return databaseIdentity;
throw new Error(injectedPrivateValue);
},
fetchDomainsImpl: async () => { throw new Error("unreachable"); },
},
{
expectedMessage: "Cloudflare custom domain preflight failed",
runCommandImpl: async (arguments_) => {
if (arguments_[0] === "secret") return inventory;
if (arguments_[1] === "list") return databaseIdentity;
return d1ProbeOutput(["0001_initial.sql"]);
},
fetchDomainsImpl: async () => { throw new Error(injectedPrivateValue); },
log: () => {},
}),
(error) => {
assert.ok(error instanceof Error);
assert.doesNotMatch(error.message, new RegExp(injectedPrivateValue));
return true;
},
);
];
for (const stage of stages) {
await assert.rejects(
preflightProductionDeploy({
accountId: "synthetic-account-id",
apiToken: "synthetic-api-token",
readFileImpl: async () => productionConfig,
listMigrationsImpl: async () => ["0001_initial.sql"],
runCommandImpl: stage.runCommandImpl,
fetchDomainsImpl: stage.fetchDomainsImpl,
log: () => {},
}),
(error) => {
assert.ok(error instanceof Error);
assert.equal(error.message, stage.expectedMessage);
assert.doesNotMatch(error.message, new RegExp(injectedPrivateValue));
return true;
},
);
}
});

test("production deploy wrapper captures Wrangler output and invokes strict deploy without a shell", async () => {
Expand Down
Loading