flotilla/scripts/release/lib/pipeline.mjs

153 lines
4.4 KiB
JavaScript

import {existsSync, readFileSync} from "node:fs"
import {join} from "node:path"
import {parseArgs} from "node:util"
import {followUps, git, name, notes, repository, root, version} from "./context.mjs"
import {ask, bold, dim, fail, green, red, yellow} from "./shell.mjs"
export const release = async (command, steps) => {
let args
try {
args = parseArgs({
options: {
check: {type: "boolean", default: false},
yes: {type: "boolean", short: "y", default: false},
},
allowPositionals: true,
})
} catch (error) {
fail(`${error.message}\nUsage: ${command} [--check] [--yes] [step...]`)
}
const {values: options, positionals: chosen} = args
const unknownStep = chosen.find(step => !steps.some(({name}) => name === step))
if (unknownStep) {
fail(`Unknown step ${unknownStep}. Steps: ${steps.map(step => step.name).join(", ")}`)
}
const selected = chosen.length > 0 ? steps.filter(step => chosen.includes(step.name)) : steps
const width = Math.max(...selected.map(step => step.name.length))
const problems = selected.map(step => ({step, missing: step.missing?.() ?? []}))
const warnings = []
// pnpm keeps a copy of the lockfile it last installed from, so any difference means a pull or
// checkout since then changed dependencies the build would silently go without
const installed = join(root, "node_modules/.pnpm/lock.yaml")
if (
!existsSync(installed) ||
readFileSync(installed, "utf-8") !== readFileSync(join(root, "pnpm-lock.yaml"), "utf-8")
) {
problems.push({
missing: ["node_modules doesn't match pnpm-lock.yaml: pnpm install --frozen-lockfile"],
})
}
if (!notes) {
problems.push({missing: [`CHANGELOG.md has no "# ${version}" section`]})
}
if (git("rev-parse", `refs/tags/${version}`)) {
const pushed = git("ls-remote", "--tags", "origin", `refs/tags/${version}`)
if (pushed === undefined) {
warnings.push("couldn't reach origin to check whether the tag is pushed")
} else if (!pushed) {
problems.push({missing: [`the ${version} tag is not on origin: git push origin ${version}`]})
}
if (git("rev-parse", "HEAD") !== git("rev-parse", `refs/tags/${version}^{commit}`)) {
warnings.push(`HEAD is not the ${version} tag, so the build won't match what you tagged`)
}
} else {
problems.push({
missing: [`there is no ${version} tag: git tag ${version} && git push origin ${version}`],
})
}
if (git("status", "--porcelain")) {
warnings.push("the working tree has uncommitted changes")
}
console.log(bold(`\n${name} ${version} -> ${repository.host}${repository.pathname}\n`))
for (const step of selected) {
console.log(` ${step.name.padEnd(width)} ${step.title}`)
}
if (warnings.length > 0) {
console.log("")
for (const warning of warnings) {
console.log(yellow(` ! ${warning}`))
}
}
const blocked = problems.filter(({missing}) => missing.length > 0)
if (blocked.length > 0) {
console.log("")
for (const {step, missing} of blocked) {
console.log(red(` x ${step ? `${step.name}: missing ${missing.join(", ")}` : missing[0]}`))
for (const line of step?.setup ?? []) {
console.log(dim(` ${line}`))
}
}
fail("\nNothing ran.")
}
if (options.check) {
console.log(green("\nReady to go."))
process.exit(0)
}
if (!options.yes) {
if (!process.stdin.isTTY) {
fail("Not a terminal; pass --yes to run unattended")
}
const answer = await ask(`\nRelease ${version}? [y/N] `)
if (!["y", "yes"].includes(answer.trim().toLowerCase())) {
fail("Aborted.")
}
}
const done = []
for (const [index, step] of selected.entries()) {
console.log(bold(`\n> ${step.title}`))
const started = Date.now()
try {
await step.run()
} catch (error) {
console.error(red(`\n${step.name} failed: ${error.message}`))
const remaining = selected.slice(index).map(remainingStep => remainingStep.name)
fail(`Pick up where this left off with: ${command} ${remaining.join(" ")}`)
}
done.push(`${step.name.padEnd(width)} ${Math.round((Date.now() - started) / 1000)}s`)
}
console.log(bold(`\n${name} ${version}\n`))
for (const line of done) {
console.log(` ${green("done")} ${line}`)
}
if (followUps.length > 0) {
console.log(bold("\nLeft to do by hand"))
for (const followUp of followUps) {
console.log(` - ${followUp}`)
}
}
}