Skip to content

Compare changes

Choose two branches to see what one has that the other does not, then open a pull request for it.

Open a pull request

1 commit

4 files+40−50/4 viewed
+5−2
538538 undone. The next plan sees the older commit and deploys what changed since,
539539 so revert the commit on `main` too, or the next push brings it back.
540540 - **To a commit:** check it out and `node scripts/deploy.mjs deploy --only
541− <units> --force`. Migrations never run backwards: a migration that needs
541+ <units> --rollback`. Without `--rollback` the tool refuses any unit whose
542+ live commit is newer than the one checked out, even with `--force`, so a
543+ re-run of an old workflow run (or an old checkout) never rolls production
544+ back by accident. Migrations never run backwards: a migration that needs
542545 undoing is a new migration.
543546 - **The runner's image:** a rollback of the Worker does not roll back the
544− container image. Redeploy the older commit (`--only runner --force`): its
547+ container image. Redeploy the older commit (`--only runner --rollback`): its
545548 image's tag is the hash of that commit's source, which is still in the
546549 registry, so nothing is built. A bad base is undone by reverting the
547550 commit that changed `services/runner/base.json`.
+3−2
6161 import { decide, git, planJson, pool, table } from "./deploy/plan.mjs";
6262 import { ROOT, byStage, codeStages, findWranglerConfigs, npmCiArgs, npmWorkspace, pick, problems, resolvedStack } from "./deploy/stack.mjs";
6363
64−const USAGE = "usage: node scripts/deploy.mjs plan|deploy|build|migrate|manifest|doctor|install|build-base|image [--all] [--only a,b] [--skip a,b] [--force] [--concurrency N] [--stage S] [--json]";
64+const USAGE = "usage: node scripts/deploy.mjs plan|deploy|build|migrate|manifest|doctor|install|build-base|image [--all] [--only a,b] [--skip a,b] [--force] [--rollback] [--concurrency N] [--stage S] [--json]";
6565
6666 function parseArgs(argv) {
6767 const opts = { command: argv[0], only: [], skip: [], concurrency: 4, force: false, all: false, json: false };
7575 else if (flag === "--stage") opts.stage = value();
7676 else if (flag === "--all") opts.all = true;
7777 else if (flag === "--force") opts.force = true;
78+ else if (flag === "--rollback") opts.rollback = true;
7879 else if (flag === "--json") opts.json = true;
7980 else if (flag === "--check") opts.check = true;
8081 else if (flag === "--no-migrations") opts.noMigrations = true;
167168 if (found && !found.sha && !found.missing && !found.error) live[unit.id] = { ...found, sha: since, assumed: true };
168169 }
169170 }
170− const decisions = decide(units, { live, head, force: opts.force || opts.all });
171+ const decisions = decide(units, { live, head, force: opts.force || opts.all, rollback: opts.rollback });
171172 return { head, units, live, migrations, decisions };
172173 }
173174
+10−0
566566 const bare = resolveStack(loadStack(), { cargo: new Map(), npm: new Map() });
567567 assert.deepEqual(bare.units.find((u) => u.id === "events").dependsOn, []);
568568 });
569+
570+test("decide: a commit older than what runs is never deployed by accident, even forced; --rollback means it", () => {
571+ const newer = { has: () => true, changed: () => ["services/events/src/lib.rs"], show: () => "", isAncestor: (older, newer) => older === HEAD && newer === OLD };
572+ const refused = decide([unit("events")], { live: { events: { sha: OLD } }, head: HEAD, force: true, gitApi: newer });
573+ assert.equal(refused[0].deploy, false);
574+ assert.match(refused[0].reason, /newer than this commit/);
575+ const meant = decide([unit("events")], { live: { events: { sha: OLD } }, head: HEAD, rollback: true, gitApi: newer });
576+ assert.equal(meant[0].deploy, true);
577+ assert.match(meant[0].reason, /rolling back/);
578+});
+22−1
3131 }
3232 },
3333 changed: (from, to) => run(["diff", "--name-only", "--no-renames", from, to]).split("\n").filter(Boolean),
34+ /** Whether `older` is in `newer`'s history (and not the same commit). */
35+ isAncestor: (older, newer) => {
36+ if (older === newer) return false;
37+ try {
38+ run(["merge-base", "--is-ancestor", older, newer]);
39+ return true;
40+ } catch {
41+ return false;
42+ }
43+ },
3444 /** Uncommitted and untracked files (not ignored ones). */
3545 dirty: () =>
3646 run(["status", "--porcelain", "--untracked-files=all"])
5565 * changed files that touch it; `image` says whether its Containers image
5666 * must be built.
5767 */
58−export function decide(units, { live, head, force = false, gitApi = git }) {
68+export function decide(units, { live, head, force = false, rollback = false, gitApi = git }) {
5969 const diffs = new Map();
6070 const changedSince = (sha) => {
6171 if (!diffs.has(sha)) diffs.set(sha, gitApi.changed(sha, head));
93103 decision.reason = force ? "forced; already at this commit" : "up to date";
94104 return decision;
95105 }
106+ // What runs is newer than this commit: deploying would roll it back
107+ // (a re-run of an old workflow run, say). Never by accident: only with
108+ // --rollback, whatever --force says.
109+ if (gitApi.has(found.sha) && gitApi.isAncestor?.(head, found.sha)) {
110+ decision.deploy = rollback;
111+ decision.reason = rollback
112+ ? `rolling back from ${found.sha.slice(0, 12)}`
113+ : `runs ${found.sha.slice(0, 12)}, which is newer than this commit; deploying would roll it back (pass --rollback to mean it)`;
114+ decision.image = rollback && Boolean(unit.image);
115+ return decision;
116+ }
96117 if (!gitApi.has(found.sha)) {
97118 decision.deploy = true;
98119 decision.reason = `runs ${found.sha.slice(0, 12)}, which this checkout does not have (fetch full history)`;