Refactor the scripts to work as advertised

- Correct docs generation to always fetch its dependency
- Don't try to close a handle that's already been closed by other methods
- Allow the release script to actually be testable.
This commit is contained in:
Isiah Meadows 2019-08-17 15:22:35 -04:00
parent 30ad45caa1
commit 22e6d37a26
12 changed files with 483 additions and 241 deletions

View file

@ -1,5 +1,4 @@
#!/usr/bin/env node
/* eslint-disable no-process-exit */
"use strict"
// This is my temporary hack to simplify deployment until I fix the underlying
@ -11,73 +10,57 @@
// helpful to create a release on Travis vs locally, aside from a couple extra
// potential 2FA prompts by npm during login and publish.
if (require.main !== module) {
throw new Error("This is a script, not a module!")
}
const path = require("path")
const fs = require("fs")
const {promises: fsp} = require("fs")
const readline = require("readline")
const {execFileSync} = require("child_process")
const rimraf = require("rimraf")
const {promisify} = require("util")
const rimraf = promisify(require("rimraf"))
const semver = require("semver")
const upstream = require("./_upstream")
const updateDocs = require("./update-docs")
// Fake it until it works with this.
upstream.fetch.remote = "origin"
function showHelp() {
console.error(`
node scripts/release increment [ --preid id ]
node scripts/release increment [ --preid id ] [ --publish ]
Invoke as \`scripts/release.sh\` to invoke the release sequence, specifying the
version increment via \`increment\` (required). Here's how they all work:
Invoke as 'scripts/release.sh' to invoke the release sequence, specifying the
version increment via 'increment' (required). Pass '--publish' to push the
change and publish it, instead of just logging the commands used to push the
release.
- \`major\` increments from 1.0.0 or 2.0.0-beta.0 to 2.0.0
- \`minor\` increments from 1.0.0 to 1.1.0
- \`patch\` increments from 1.0.0 to 1.0.1
- \`premajor\` increments from 1.0.0 to 2.0.0-beta.0
- \`preminor\` increments from 1.0.0 to 1.1.0-beta.0
- \`prepatch\` increments from 1.0.0 to 1.0.1-beta.0
- \`prerelease\` increments from 2.0.0-beta.0 to 2.0.0-beta.1
Here's how each increment type works:
\`--preid beta\` specifies the \`beta\` part above (default). It's required for
all \`pre*\` increment types except \`prerelease\`.
- 'major' increments from 1.0.0 or 2.0.0-beta.0 to 2.0.0
- 'minor' increments from 1.0.0 to 1.1.0
- 'patch' increments from 1.0.0 to 1.0.1
- 'premajor' increments from 1.0.0 to 2.0.0-beta.0
- 'preminor' increments from 1.0.0 to 1.1.0-beta.0
- 'prepatch' increments from 1.0.0 to 1.0.1-beta.0
- 'prerelease' increments from 2.0.0-beta.0 to 2.0.0-beta.1
See the docs for \`npm version\` <https://docs.npmjs.com/cli/version> for
details on the \`increment\` parameter.
`.trim())
process.exit(0)
}
'--preid beta' specifies the 'beta' part above (default). It's required for all
'pre*' increment types except 'prerelease'.
function bail(...args) {
console.error(...args)
process.exit(1)
See the docs for 'npm version' <https://docs.npmjs.com/cli/version> for details
on the 'increment' parameter.
`)
}
const rootDir = path.dirname(__dirname)
const p = (...args) => path.resolve(rootDir, ...args)
function readVersion() {
return JSON.parse(fs.readFileSync(p("../package.json"), "utf-8")).version
function fail(...args) {
console.error(...args)
return 1
}
const parsed = require("minimist")(process.argv.slice(2), {
boolean: ["help"],
alias: {help: ["h", "?"]},
string: ["preid"],
"--": true,
})
parsed._ = parsed._.concat(parsed["--"])
if (parsed.help || !parsed._.length) showHelp()
const publishType = parsed._[0]
const publishPreid = parsed.preid
const publishArgs = publishType.startsWith("pre") ? ["--tag", "next"] : []
let releaseArgs = []
if (publishType.startsWith("pre") && publishType !== "prerelease") {
if (publishPreid == null) {
bail("`pre*` increments other than `prerelease` require `--preid`")
}
releaseArgs = [`--preid=${publishPreid}`]
}
function exec(cmd, args, opts) {
function execCommand(cmd, args, opts) {
console.error()
console.error(["executing:", cmd, ...args].join(" "))
return execFileSync(cmd, args, {
windowsHide: true,
stdio: "inherit",
@ -86,83 +69,212 @@ function exec(cmd, args, opts) {
})
}
function git(...cmd) { return execCommand("git", cmd) }
function npm(...cmd) { return execCommand("npm", cmd) }
function npmConfig(key) { return npm("config", "get", key).trim() }
function getChanges() {
const result = exec("git", ["status", "-z"], {
return execCommand("git", ["status", "-z"], {
stdio: ["inherit", "pipe", "inherit"],
})
return result.split(/\0/g).filter(Boolean)
.split(/\0/g)
.filter((l) => (/\S/).test(l))
}
if (getChanges().length) {
bail("Error: Tree must not be dirty to start!")
}
const upstream = require("./_upstream")
exec("git", ["checkout", "next"])
exec("git", ["pull", "--rebase", upstream.fetch.branch, "next"])
// Because I'm too lazy to make everything async.
exec("read", ["-rsp", `
Update "Upcoming" in \`docs/change-log.md\`. If moving a prerelease to stable,
also replace all references to \`mithril@next\` to \`mithril\`, including in
Flems snippets. Press enter once ready to continue.
`.trim()], {shell: true})
// Verify the changelog was updated
let changelogUpdated = false
let treeDirty = false
for (const line of getChanges()) {
switch (line) {
case " M CHANGELOG.md":
case "M CHANGELOG.md":
case "MM CHANGELOG.md":
changelogUpdated = true
break
default:
treeDirty = true
async function release({increment, preid, publish}) {
if (!(/^prerelease$|^(pre)?(major|minor|patch)$/).test(increment)) {
return fail(`Invalid increment: ${increment}`)
}
if ((/^pre(major|minor|patch)/).test(increment) && preid == null) {
return fail(`'${increment}' must include a '--preid'`)
}
if (getChanges().length) {
return fail("Tree must be clean to start!")
}
if (upstream.push == null) {
return fail("You must have an upstream to push to!")
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
// Update local `master` and `next`.
git("fetch", upstream.fetch.remote, "master", "next")
// Make sure we're on the current `next` and merge any docs fixes and
// similar that have landed in upstream `master`.
git("checkout", "next")
git("pull", "--rebase", upstream.fetch.remote, "next")
git(
"pull", "--allow-unrelated-histories",
upstream.fetch.remote, "master"
)
// Note: we're doing our own semver incrementing.
const packageJson = JSON.parse(
await fsp.readFile(p("package.json"), "utf-8")
)
const version = semver.inc(packageJson.version, increment, preid)
console.error(`
Copy the parts listed in "Upcoming" to a new section "### v${version}" in
docs/change-log.md and clear that section out.
`)
for (;;) {
await new Promise((resolve) => rl.question(
"Press <Enter> once ready to continue or Ctrl+C to abort.",
// Ignore any input.
() => resolve(),
))
// Verify the changelog was updated, and give a chance to retry if it's
// prematurely continued.
const changes = getChanges()
const isChangelog = /^[ M][ M] docs\/change-log\.md$/
const errors = []
console.log("changes", changes)
if (!changes.some((l) => isChangelog.test(l))) {
errors.push("Changelog must be updated!")
}
if (changes.some((l) => !isChangelog.test(l))) {
errors.push("Tree must not be otherwise dirty!")
}
if (!errors.length) break
console.error(errors.join("\n"))
}
await rimraf(p("node_modules"))
npm("install-test")
npm("run", "build")
console.log("*** Build done ***")
// Update the package file.
packageJson.version = version
await fsp.writeFile(p("package.json"), "utf-8",
JSON.stringify(packageJson, null, 2)
)
// Commit and tag the new release, with the appropriate CLI flag if the
// commit needs signed.
git("add", ".")
git(
"commit",
...npmConfig("sign-git-tag") === "true" ? ["--gpg-sign"] : [],
"--message", `v${version}`,
)
git("tag", `v${version}`)
// Update `master` to reflect the current state of `next`.
git("checkout", "master")
git("reset", "--hard", "next")
git("checkout", "next")
if (publish) {
// TODO: switch this to just do the push, and use the following Travis
// config. This also conveniently keeps private stuff out of the build
// scripts and just in build config, avoiding the grief that led to this
// file's existence.
//
// ```yml
// # See https://docs.travis-ci.com/user/deployment/npm/ for details on
// # `api_key:` for the npm provider.
// # See https://docs.travis-ci.com/user/deployment/pages/ for details
// # on `github_token:` for the pages provider.
// after_success: >
// [ "$TRAVIS_BRANCH" == "master" ] && node scripts/generate-docs
//
// deploy:
// - provider: npm
// skip_cleanup: true
// email: 'contact@isiahmeadows.com'
// api_key:
// secure: 'output of `travis encrypt NPM_AUTH_TOKEN`'
// on:
// tags: true
// condition: "$TRAVIS_TAG != *-*"
// - provider: npm
// skip_cleanup: true
// tag: next
// email: 'contact@isiahmeadows.com'
// api_key:
// secure: 'output of `travis encrypt NPM_AUTH_TOKEN`'
// on:
// tags: true
// condition: "$TRAVIS_TAG == *-*"
// - provider: pages
// skip_cleanup: true
// github_token:
// secure: 'output of `travis encrypt GITHUB_AUTH_TOKEN`'
// local_dir: dist
// fqdn: mithril.js.org
// committer_from_gh: true
// on:
// tags: false
// branch: master
// ```
npm("login")
if (increment.startsWith("pre")) {
npm("publish", "--tag", "next")
} else {
npm("publish")
}
npm("logout")
// Only push after successful publish
git(
"push", "--atomic", "origin",
"+next:master", "next:next", `next:refs/tags/v${version}`,
)
git(
"push", "--atomic", upstream.push.remote,
"+next:master", "next:next", `next:refs/tags/v${version}`,
)
await updateDocs()
} else {
const remote = upstream.push.remote
console.error(`
npm login
npm publish${increment.startsWith("pre") ? " --tag next" : ""}
npm logout
git push --atomic origin +next:master next:next next:refs/tags/v${version}
git push --atomic ${remote} +next:master next:next next:refs/tags/v${version}
npm run release:docs
`)
}
console.error(`
Don't forget to update the latest release! You can find it here:
https://github.com/MithrilJS/mithril.js/releases/tag/v${version}
`)
return 0
}
if (!changelogUpdated || treeDirty) {
if (!changelogUpdated) console.error("Error: Changelog must be updated!")
if (!treeDirty) console.error("Error: Tree must not be otherwise dirty!")
process.exit(1)
/* eslint-disable global-require */
if (require.main === module) {
require("./_command")({async exec() {
const parsed = require("minimist")(process.argv.slice(2), {
boolean: ["help", "publish"],
alias: {help: ["h", "?"]},
string: ["preid"],
})
if (parsed.help || !parsed._.length) showHelp()
else {
await release({
increment: parsed._[0],
preid: parsed.preid,
publish: parsed.publish,
})
}
}})
}
exec("git", ["add", "."])
exec("git", ["commit", "-m", "Preparing for release"])
exec("git", ["checkout", "master"])
exec("git", ["pull", "--rebase", upstream.fetch.branch, "master"])
// There may be merge conflicts with `index.js` and/or the bundle - just ignore
// them. Whatever they have is canon, as is the case with everything else.
exec("git", ["merge", "next", "--strategy-option=theirs"])
rimraf.sync(p("node_modules"))
exec("npm", ["install-test"])
exec("npm", ["version", "-m", "v%s", publishType, ...releaseArgs])
exec("git", ["checkout", "next"])
exec("git", ["checkout", "master", "--", "mithril.js", "mithril.min.js"])
// That's already been updated in `master`.
exec("git", ["commit", "-m", `Generated bundles for ${readVersion()} [skip ci]`])
exec("git", ["checkout", "master"])
console.log("publish args: ", publishArgs)
console.log("push all: ", upstream.push.branch)
// exec("npm", ["login"])
// exec("npm", ["publish", ...publishArgs])
// exec("npm", ["logout"])
//
// // Only push after successful publish
// exec("git", ["push", "--follow-tags", "origin", "master:master", "next:next"])
// exec("git", ["push", "--follow-tags", upstream.push.branch, "master:master", "next:next"])
exec("git", ["checkout", "next"])
console.log("update docs")
// require("./update-docs")()