-
-
Notifications
You must be signed in to change notification settings - Fork 424
fix(tools): add preflight checks and rollback to release script #5235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,8 +3,9 @@ | |||||||||||||||||||||||||||
| const { Builder } = require("./builder.cjs"); | ||||||||||||||||||||||||||||
| const cmdPrompt = require("prompt"); | ||||||||||||||||||||||||||||
| const colors = require("colors"); | ||||||||||||||||||||||||||||
| const { exec } = require("child_process"); | ||||||||||||||||||||||||||||
| const { exec, spawn } = require("child_process"); | ||||||||||||||||||||||||||||
| const loading = require("loading-indicator"); | ||||||||||||||||||||||||||||
| const fs = require("fs"); | ||||||||||||||||||||||||||||
| const DEBUG = false; | ||||||||||||||||||||||||||||
| const vnu = require("vnu-jar"); | ||||||||||||||||||||||||||||
| const path = require("path"); | ||||||||||||||||||||||||||||
|
|
@@ -329,10 +330,159 @@ const indicators = new Map([ | |||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async function preflight() { | ||||||||||||||||||||||||||||
| console.log(colors.cyan("\n Preflight checks\n")); | ||||||||||||||||||||||||||||
| const errors = []; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Java (needed for vnu HTML validator) | ||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| await toExecPromise("java -version", { timeout: 10000, showOutput: false }); | ||||||||||||||||||||||||||||
| console.log(colors.green(" ✓ Java runtime")); | ||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||
| errors.push( | ||||||||||||||||||||||||||||
| "Java runtime not found (required by vnu HTML validator).\n" + | ||||||||||||||||||||||||||||
| " Install: brew install java\n" + | ||||||||||||||||||||||||||||
| " Then: sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk" + | ||||||||||||||||||||||||||||
| " /Library/Java/JavaVirtualMachines/openjdk.jdk" | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Puppeteer Chrome (needed for respec2html) | ||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| const chromePath = await toExecPromise( | ||||||||||||||||||||||||||||
| "node -e 'import(\"puppeteer\").then(p => process.stdout.write(p.executablePath()))'", | ||||||||||||||||||||||||||||
| { timeout: 15000, showOutput: false } | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| if (!fs.existsSync(chromePath.trim())) { | ||||||||||||||||||||||||||||
| throw new Error("Chrome binary missing"); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| console.log(colors.green(" ✓ Puppeteer Chrome")); | ||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||
| errors.push( | ||||||||||||||||||||||||||||
| "Puppeteer Chrome not found (required by respec2html).\n" + | ||||||||||||||||||||||||||||
| " Install: npx puppeteer browsers install chrome" | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // gh-pages branch must resolve unambiguously to origin | ||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| const branches = await git("branch -r --list */gh-pages"); | ||||||||||||||||||||||||||||
| const remotes = branches | ||||||||||||||||||||||||||||
| .trim() | ||||||||||||||||||||||||||||
| .split("\n") | ||||||||||||||||||||||||||||
| .filter(line => line.trim()); | ||||||||||||||||||||||||||||
| if (remotes.length > 1) { | ||||||||||||||||||||||||||||
| const defaultRemote = await git("config checkout.defaultRemote").catch( | ||||||||||||||||||||||||||||
| () => "" | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| if (!defaultRemote.trim()) { | ||||||||||||||||||||||||||||
| errors.push( | ||||||||||||||||||||||||||||
| `"gh-pages" exists on ${remotes.length} remotes:\n${remotes.map(r => ` ${r.trim()}`).join("\n")}\n Fix: git config checkout.defaultRemote origin` | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||
| console.log(colors.green(" ✓ gh-pages branch (via defaultRemote)")); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||
| console.log(colors.green(" ✓ gh-pages branch")); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||
| errors.push("Could not verify gh-pages branch status."); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (errors.length) { | ||||||||||||||||||||||||||||
| console.log(colors.red("\n ❌ Preflight failed:\n")); | ||||||||||||||||||||||||||||
| errors.forEach((err, i) => { | ||||||||||||||||||||||||||||
| console.log(colors.red(` ${i + 1}. ${err}\n`)); | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
| throw new Error("Fix the issues above and try again."); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| console.log(colors.green("\n ✅ All preflight checks passed.\n")); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Runs a command interactively (stdio inherited), needed for npm publish OTP. | ||||||||||||||||||||||||||||
| * @param {string} cmd | ||||||||||||||||||||||||||||
| * @returns {Promise<void>} | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| function toSpawnPromise(cmd) { | ||||||||||||||||||||||||||||
| const [program, ...args] = cmd.split(" "); | ||||||||||||||||||||||||||||
| console.log(colors.cyan(`Run: ${cmd}`)); | ||||||||||||||||||||||||||||
| if (DEBUG) return Promise.resolve(); | ||||||||||||||||||||||||||||
| return new Promise((resolve, reject) => { | ||||||||||||||||||||||||||||
| const proc = spawn(program, args, { stdio: "inherit" }); | ||||||||||||||||||||||||||||
| proc.on("close", code => { | ||||||||||||||||||||||||||||
| if (code !== 0) { | ||||||||||||||||||||||||||||
| reject(new Error(`Command failed with exit code ${code}: ${cmd}`)); | ||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||
| resolve(); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
| proc.on("error", reject); | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * @param {string} version | ||||||||||||||||||||||||||||
| * @param {string} mainHead | ||||||||||||||||||||||||||||
| * @param {string} ghPagesHead | ||||||||||||||||||||||||||||
| * @param {string} initialBranch | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| async function rollback(version, mainHead, ghPagesHead, initialBranch) { | ||||||||||||||||||||||||||||
| console.log(colors.yellow("\n ⏪ Rolling back local changes...\n")); | ||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| const currentBranch = await getCurrentBranch(); | ||||||||||||||||||||||||||||
| if (currentBranch !== "main") { | ||||||||||||||||||||||||||||
| await git("checkout main"); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||
| // best effort | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| await git(`tag -d "v${version}"`); | ||||||||||||||||||||||||||||
| console.log(colors.yellow(` Deleted tag v${version}`)); | ||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||
| // tag may not exist yet | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if (mainHead) { | ||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| await git(`reset --hard ${mainHead}`); | ||||||||||||||||||||||||||||
| console.log(colors.yellow(` Reset main to ${mainHead.slice(0, 8)}`)); | ||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||
| console.log(colors.red(" Failed to reset main — check manually.")); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if (ghPagesHead) { | ||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| await git("checkout gh-pages"); | ||||||||||||||||||||||||||||
| await git(`reset --hard ${ghPagesHead}`); | ||||||||||||||||||||||||||||
| console.log( | ||||||||||||||||||||||||||||
| colors.yellow(` Reset gh-pages to ${ghPagesHead.slice(0, 8)}`) | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| await git("checkout main"); | ||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||
| console.log(colors.red(" Failed to reset gh-pages — check manually.")); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| const currentBranch = await getCurrentBranch(); | ||||||||||||||||||||||||||||
| if (initialBranch !== currentBranch) { | ||||||||||||||||||||||||||||
| await git(`checkout ${initialBranch}`); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||
| // best effort | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const run = async () => { | ||||||||||||||||||||||||||||
| const initialBranch = await getCurrentBranch(); | ||||||||||||||||||||||||||||
| let version = ""; | ||||||||||||||||||||||||||||
| let mainHead = ""; | ||||||||||||||||||||||||||||
| let ghPagesHead = ""; | ||||||||||||||||||||||||||||
| let pushed = false; | ||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| // 1. Confirm maintainer is on up-to-date and on the main branch () | ||||||||||||||||||||||||||||
| await preflight(); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // 1. Confirm maintainer is on up-to-date and on the main branch | ||||||||||||||||||||||||||||
| indicators.get("remote-update").show(); | ||||||||||||||||||||||||||||
| await git("remote update"); | ||||||||||||||||||||||||||||
| indicators.get("remote-update").hide(); | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
| await preflight(); | |
| // 1. Confirm maintainer is on up-to-date and on the main branch | |
| indicators.get("remote-update").show(); | |
| await git("remote update"); | |
| indicators.get("remote-update").hide(); | |
| indicators.get("remote-update").show(); | |
| await git("remote update"); | |
| indicators.get("remote-update").hide(); | |
| await preflight(); | |
| // 1. Confirm maintainer is on up-to-date and on the main branch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied the suggestion — git remote update now runs before preflight() so the gh-pages branch check has current remote refs.
Copilot
AI
Apr 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pushed is only set to true after all three git push commands succeed. If git push origin main succeeds but a later push fails (e.g., gh-pages or tags), the catch block will treat it as "not pushed" and perform a local rollback, leaving the remote partially updated and the local repo reset behind it. Consider marking the point-of-no-return after the first successful push (or tracking which refs were pushed) and adjusting the failure message/rollback behavior accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed — pushed = true is now set immediately after git push origin main succeeds (the first push), so if gh-pages or tags push fails, the catch block won't attempt a local rollback against an already-updated remote.
Uh oh!
There was an error while loading. Please reload this page.