Compare changes
Choose two branches to see what one has that the other does not, then open a pull request for it.
1 commit
4 files+40−50/4 viewed
| 538 | 538 | undone. The next plan sees the older commit and deploys what changed since, | |
| 539 | 539 | so revert the commit on `main` too, or the next push brings it back. | |
| 540 | 540 | - **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 | |
| 542 | 545 | undoing is a new migration. | |
| 543 | 546 | - **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 | |
| 545 | 548 | image's tag is the hash of that commit's source, which is still in the | |
| 546 | 549 | registry, so nothing is built. A bad base is undone by reverting the | |
| 547 | 550 | commit that changed `services/runner/base.json`. |
| 61 | 61 | import { decide, git, planJson, pool, table } from "./deploy/plan.mjs"; | |
| 62 | 62 | import { ROOT, byStage, codeStages, findWranglerConfigs, npmCiArgs, npmWorkspace, pick, problems, resolvedStack } from "./deploy/stack.mjs"; | |
| 63 | 63 | ||
| 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]"; | |
| 65 | 65 | ||
| 66 | 66 | function parseArgs(argv) { | |
| 67 | 67 | const opts = { command: argv[0], only: [], skip: [], concurrency: 4, force: false, all: false, json: false }; | |
| 75 | 75 | else if (flag === "--stage") opts.stage = value(); | |
| 76 | 76 | else if (flag === "--all") opts.all = true; | |
| 77 | 77 | else if (flag === "--force") opts.force = true; | |
| 78 | + | else if (flag === "--rollback") opts.rollback = true; | |
| 78 | 79 | else if (flag === "--json") opts.json = true; | |
| 79 | 80 | else if (flag === "--check") opts.check = true; | |
| 80 | 81 | else if (flag === "--no-migrations") opts.noMigrations = true; | |
| 167 | 168 | if (found && !found.sha && !found.missing && !found.error) live[unit.id] = { ...found, sha: since, assumed: true }; | |
| 168 | 169 | } | |
| 169 | 170 | } | |
| 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 }); | |
| 171 | 172 | return { head, units, live, migrations, decisions }; | |
| 172 | 173 | } | |
| 173 | 174 |
| 566 | 566 | const bare = resolveStack(loadStack(), { cargo: new Map(), npm: new Map() }); | |
| 567 | 567 | assert.deepEqual(bare.units.find((u) => u.id === "events").dependsOn, []); | |
| 568 | 568 | }); | |
| 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 | + | }); |
| 31 | 31 | } | |
| 32 | 32 | }, | |
| 33 | 33 | 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 | + | }, | |
| 34 | 44 | /** Uncommitted and untracked files (not ignored ones). */ | |
| 35 | 45 | dirty: () => | |
| 36 | 46 | run(["status", "--porcelain", "--untracked-files=all"]) | |
| 55 | 65 | * changed files that touch it; `image` says whether its Containers image | |
| 56 | 66 | * must be built. | |
| 57 | 67 | */ | |
| 58 | − | export function decide(units, { live, head, force = false, gitApi = git }) { | |
| 68 | + | export function decide(units, { live, head, force = false, rollback = false, gitApi = git }) { | |
| 59 | 69 | const diffs = new Map(); | |
| 60 | 70 | const changedSince = (sha) => { | |
| 61 | 71 | if (!diffs.has(sha)) diffs.set(sha, gitApi.changed(sha, head)); | |
| 93 | 103 | decision.reason = force ? "forced; already at this commit" : "up to date"; | |
| 94 | 104 | return decision; | |
| 95 | 105 | } | |
| 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 | + | } | |
| 96 | 117 | if (!gitApi.has(found.sha)) { | |
| 97 | 118 | decision.deploy = true; | |
| 98 | 119 | decision.reason = `runs ${found.sha.slice(0, 12)}, which this checkout does not have (fetch full history)`; |