Refactor/Fix release script (#13619)

This commit is contained in:
fisker Cheung authored and GitHub committed 2022-10-14 17:17:47 +08:00
1 parent 8df3b5ee60
commit 76d0890142
23 files changed
+289 -202

No files matched your search

+2
View File
@@ -13,3 +13,5 @@
/website/static/playground.js
/website/static/lib/
/scripts/benchmark/*/
**/.yarn/**
**/.pnp.*
+1 -1
View File
@@ -25,7 +25,7 @@ package-lock.json
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*
**/.pnp.*
.nyc_output
.devcontainer
# When installing software on gitpod.io, `core.*` files are generated
+2 -1
View File
@@ -2,7 +2,8 @@
.cache/
coverage/
.nyc_output/
.yarn/
**/.yarn/**
**/.pnp.*
/dist*/
node_modules/
+1 -1
View File
@@ -9,7 +9,7 @@
"author": "James Long",
"type": "module",
"license": "MIT",
"main": "./index.cjs",
"main": "./src/index.cjs",
"browser": "./standalone.js",
"unpkg": "./standalone.js",
"exports": {
+6
View File
@@ -8,6 +8,12 @@ node ./scripts/release/release.js --version NEW_VERSION
The script has its own `package.json` so we can reinstall the root's `node_modules/` while making the release.
## Flags
- `--manual` Manual run release process instead of publish from GitHub actions
- `--dry` Dry run
- `--skip-dependencies-install` Skip dependencies installation
## Credits
This script was inspired by [React's release script](https://github.com/facebook/react/tree/001f9ef/scripts/release).
+2 -2
View File
@@ -26,5 +26,5 @@ await runCommand("git fetch --tags");
// Install script's dependencies before any require
await runCommand("yarn");
const { default: run } = await import("./run.js");
await run();
// Ready to run
await import("./run.js");
+70 -55
View File
@@ -1,61 +1,76 @@
import chalk from "chalk";
import semver from "semver";
import { outdent } from "outdent";
import { runGit, readJson } from "./utils.js";
import parseArguments from "./parse-arguments.js";
import { logPromise } from "./utils.js";
import * as steps from "./steps/index.js";
const importDefault = async (module) => (await import(module)).default;
async function run() {
const params = parseArguments();
const { stdout: previousVersion } = await runGit([
"describe",
"--tags",
"--abbrev=0",
]);
if (semver.parse(previousVersion) === null) {
throw new Error(`Unexpected previousVersion: ${previousVersion}`);
} else {
params.previousVersion = previousVersion;
params.previousVersionOnDefaultBranch = (
await readJson("package.json")
).version;
}
const steps = await Promise.all(
[
"./steps/validate-new-version.js",
"./steps/check-git-status.js",
!params.skipDependenciesInstall && "./steps/install-dependencies.js",
params.manual && "./steps/run-tests.js",
"./steps/update-version.js",
params.manual && "./steps/generate-bundles.js",
"./steps/update-changelog.js",
"./steps/push-to-git.js",
params.manual
? "./steps/publish-to-npm.js"
: "./steps/wait-for-bot-release.js",
"./steps/show-instructions-after-npm-publish.js",
"./steps/update-dependents-count.js",
"./steps/bump-prettier.js",
"./steps/post-publish-steps.js",
]
.filter(Boolean)
.map((step) => importDefault(step))
);
try {
for (const step of steps) {
await step(params);
}
} catch (error) {
const message = outdent.string(error.message.trim());
const stack = error.stack.replace(message, "");
console.error(`${chalk.red("error")} ${message}\n${stack}`);
process.exitCode = 1;
}
const params = parseArguments();
const { stdout: previousVersion } = await runGit([
"describe",
"--tags",
"--abbrev=0",
]);
if (semver.parse(previousVersion) === null) {
throw new Error(`Unexpected previousVersion: ${previousVersion}`);
} else {
params.previousVersion = previousVersion;
params.previousVersionOnDefaultBranch = (
await readJson("package.json")
).version;
}
export default run;
for (let step of [
{
name: "Validating new version",
process: steps.validateNewVersion,
},
{
name: "Checking git status",
process: steps.checkGitStatus,
skip: params.dry,
},
{
name: "Installing NPM dependencies",
process: steps.installDependencies,
skip: params.dry || params.skipDependenciesInstall || !params.manual,
},
{
name: "Linting files",
process: steps.lintFiles,
skip: params.dry,
},
{
name: "Running tests",
process: steps.runTests,
skip: params.dry || !params.manual,
},
{
name: "Bumping version",
process: steps.updateVersion,
skip: params.dry,
},
steps.generateBundles,
steps.updateChangelog,
{
name: "Committing and pushing to remote",
process: steps.pushToGit,
skip: params.dry,
},
params.manual ? steps.publishToNpm : steps.waitForBotRelease,
steps.showInstructionsAfterNpmPublish,
steps.updateDependentsCount,
steps.bumpPrettier,
steps.postPublishSteps,
]) {
if (typeof step === "function") {
step = { process: step };
}
const runStep = () => step.process(params);
if (step.name) {
await logPromise(step.name, runStep, step.skip);
} else if (!step.skip) {
await runStep();
}
}
+11 -3
View File
@@ -1,20 +1,28 @@
import chalk from "chalk";
import { runYarn, logPromise, readJson } from "../utils.js";
export default async function generateBundles({ version }) {
export default async function generateBundles({ dry, version, manual }) {
if (!manual) {
return;
}
await logPromise(
"Generating bundles",
runYarn(["build", "--clean", "--print-size", "--compare-size"])
);
const builtPkg = await readJson("dist/package.json");
if (builtPkg.version !== version) {
if (!dry && builtPkg.version !== version) {
throw new Error(
`Expected ${version} in dist/package.json but found ${builtPkg.version}`
);
}
await logPromise("Running tests on generated bundles", runYarn("test:dist"));
await logPromise(
"Running tests on generated bundles",
() => runYarn("test:dist"),
/* shouldSkip */ dry
);
console.log(chalk.green.bold("Build successful!\n"));
}
+15
View File
@@ -0,0 +1,15 @@
export { default as validateNewVersion } from "./validate-new-version.js";
export { default as checkGitStatus } from "./check-git-status.js";
export { default as installDependencies } from "./install-dependencies.js";
export { default as lintFiles } from "./lint-files.js";
export { default as runTests } from "./run-tests.js";
export { default as updateVersion } from "./update-version.js";
export { default as generateBundles } from "./generate-bundles.js";
export { default as updateChangelog } from "./update-changelog.js";
export { default as pushToGit } from "./push-to-git.js";
export { default as publishToNpm } from "./publish-to-npm.js";
export { default as waitForBotRelease } from "./wait-for-bot-release.js";
export { default as showInstructionsAfterNpmPublish } from "./show-instructions-after-npm-publish.js";
export { default as updateDependentsCount } from "./update-dependents-count.js";
export { default as bumpPrettier } from "./bump-prettier.js";
export { default as postPublishSteps } from "./post-publish-steps.js";
@@ -1,7 +1,7 @@
import { execa } from "execa";
import { runYarn, runGit, logPromise } from "../utils.js";
import { runYarn, runGit } from "../utils.js";
async function install() {
export default async function installDependencies() {
await execa("rm", ["-rf", "node_modules"]);
await runYarn(["install"]);
@@ -15,7 +15,3 @@ async function install() {
);
}
}
export default function installDependencies() {
return logPromise("Installing NPM dependencies", install());
}
+3
View File
@@ -0,0 +1,3 @@
import { runYarn } from "../utils.js";
export default () => runYarn("lint");
+24 -20
View File
@@ -3,8 +3,6 @@ import outdent from "outdent";
import { execa } from "execa";
import { fetchText, logPromise } from "../utils.js";
const outdentString = outdent.string;
const SCHEMA_REPO = "SchemaStore/schemastore";
const SCHEMA_PATH = "src/schemas/json/prettierrc.json";
const RAW_URL = `https://raw.githubusercontent.com/${SCHEMA_REPO}/master/${SCHEMA_PATH}`;
@@ -25,40 +23,46 @@ async function checkSchema() {
return;
}
return outdentString(chalk`
{bold.underline The schema in {yellow SchemaStore} needs an update.}
- Open {cyan.underline ${EDIT_URL}}
- Run {yellow node scripts/generate-schema.mjs} and copy the new schema
return outdent`
${chalk.bold.underline(
"The schema in {yellow SchemaStore"
)} needs an update.}
- Open ${chalk.cyan.underline(EDIT_URL)}
- Run ${chalk.yellow(
"node scripts/generate-schema.mjs"
)} and copy the new schema
- Paste it on GitHub interface
- Open a PR
`);
`;
}
function twitterAnnouncement() {
return outdentString(chalk`
{bold.underline Announce on Twitter}
- Open {cyan.underline https://tweetdeck.twitter.com}
return outdent`
${chalk.bold.underline("Announce on Twitter")}
- Open ${chalk.cyan.underline("https://tweetdeck.twitter.com")}
- Make sure you are tweeting from the {yellow @PrettierCode} account.
- Tweet about the release, including the blog post URL.
`);
`;
}
export default async function postPublishSteps() {
const steps = [await checkSchema(), twitterAnnouncement()].filter(Boolean);
export default async function postPublishSteps({ dry }) {
console.log(chalk.bold.green("The script has finished!\n"));
if (steps.length === 0) {
if (dry) {
return;
}
const steps = [await checkSchema(), twitterAnnouncement()].filter(Boolean);
console.log(
outdentString(chalk`
{yellow.bold The following ${
steps.length === 1 ? "step is" : "steps are"
} optional.}
outdent`
${chalk.yellow.bold(
`The following ${
steps.length === 1 ? "step is" : "steps are"
} optional.`
)}
${steps.join("\n\n")}
`)
`
);
}
+27 -19
View File
@@ -1,19 +1,35 @@
import { execa } from "execa";
import enquirer from "enquirer";
import { logPromise } from "../utils.js";
import { waitForEnter } from "../utils.js";
export default async function publishToNpm({ dry }) {
console.log(`Ready to publish to NPM${dry ? "(--dry-run)" : ""}`);
await waitForEnter();
const commonArgs = ["publish"];
if (dry) {
commonArgs.push("--dry-run");
}
/**
* Retry "npm publish" when to enter OTP is failed.
*/
async function retryNpmPublish() {
const runNpmPublish = async () => {
const { otp } = await enquirer.prompt({
type: "input",
name: "otp",
message: "Please enter your npm OTP",
});
await execa("npm", ["publish", "--otp", otp], { cwd: "./dist" });
const args = [...commonArgs];
if (!dry) {
const { otp } = await enquirer.prompt({
type: "input",
name: "otp",
message: "Please enter your npm OTP",
});
args.push("--otp", otp);
}
await execa("npm", args, { cwd: "./dist" });
};
/**
* Retry "npm publish" when to enter OTP is failed.
*/
for (let i = 5; i > 0; i--) {
try {
return await runNpmPublish();
@@ -26,11 +42,3 @@ async function retryNpmPublish() {
}
}
}
export default async function publishToNpm({ dry }) {
if (dry) {
return;
}
await logPromise("Publishing to npm", retryNpmPublish());
}
+2 -10
View File
@@ -1,16 +1,8 @@
import { runGit, logPromise } from "../utils.js";
import { runGit } from "../utils.js";
async function pushGit({ version, repo }) {
export default async function pushToGit({ version, repo }) {
await runGit(["commit", "-am", `Release ${version}`]);
await runGit(["tag", "-a", version, "-m", `Release ${version}`]);
await runGit(["push", "--repo", repo]);
await runGit(["push", "--tags", "--repo", repo]);
}
export default function pushToGit(params) {
if (params.dry) {
return;
}
return logPromise("Committing and pushing to remote", pushGit(params));
}
+2 -6
View File
@@ -1,7 +1,3 @@
import { runYarn, logPromise } from "../utils.js";
import { runYarn } from "../utils.js";
export default async function runTests() {
await logPromise("Running linter", runYarn("lint:eslint"));
await logPromise("Running Prettier on docs", runYarn("lint:prettier"));
await logPromise("Running tests", runYarn("test"));
}
export default () => runYarn("test");
@@ -7,8 +7,6 @@ import {
waitForEnter,
} from "../utils.js";
const outdentString = outdent.string;
const RELEASE_URL_BASE = "https://github.com/prettier/prettier/releases/new?";
export function getReleaseUrl(version, previousVersion) {
const semverDiff = semver.diff(version, previousVersion);
@@ -42,22 +40,24 @@ export default async function showInstructionsAfterNpmPublish({
const releaseUrl = getReleaseUrl(version, previousVersion);
console.log(
outdentString(chalk`
{green.bold Prettier ${version} published!}
outdent`
${chalk.green.bold(`Prettier ${version} published!`)}
{yellow.bold Some manual steps are necessary.}
${chalk.yellow.bold("Some manual steps are necessary.")}
{bold.underline Create a GitHub Release}
- Go to {cyan.underline ${releaseUrl}}
- Press {bgGreen.black Publish release }
${chalk.bold.underline("Create a GitHub Release")}
- Go to ${chalk.cyan.underline(releaseUrl)}
- Press ${chalk.bgGreen.black("Publish release ")}
{bold.underline Test the new release}
- In a new session, run {yellow npm i prettier@latest} in another directory
${chalk.bold.underline("Test the new release")}
- In a new session, run ${chalk.yellow(
"npm i prettier@latest"
)} in another directory
- Test the API and CLI
After that, we can proceed to bump this repo's Prettier dependency.
Press ENTER to continue.
`)
`
);
await waitForEnter();
}
+14 -10
View File
@@ -1,7 +1,6 @@
import fs from "node:fs";
import { execa } from "execa";
import chalk from "chalk";
import outdent from "outdent";
import semver from "semver";
import {
waitForEnter,
@@ -11,8 +10,6 @@ import {
getChangelogContent,
} from "../utils.js";
const outdentString = outdent.string;
function writeChangelog(params) {
const changelog = fs.readFileSync("CHANGELOG.md", "utf8");
const newEntry = `# ${params.version}\n\n` + getChangelogContent(params);
@@ -30,7 +27,15 @@ async function getChangelogForPatch({ version, previousVersion }) {
return changelog;
}
export default async function updateChangelog({ version, previousVersion }) {
export default async function updateChangelog({
dry,
version,
previousVersion,
}) {
if (dry) {
return;
}
const semverDiff = semver.diff(version, previousVersion);
if (semverDiff !== "patch") {
@@ -45,11 +50,11 @@ export default async function updateChangelog({ version, previousVersion }) {
return;
}
console.warn(
outdentString(chalk`
{yellow warning} The file {bold ${blogPost.file}} doesn't exist, but it will be referenced in {bold CHANGELOG.md}. Make sure to create it later.
Press ENTER to continue.
`)
`${chalk.yellow("warning")} The file ${chalk.bold(
blogPost.file
)} doesn't exist, but it will be referenced in ${chalk.bold(
"CHANGELOG.md"
)}. Make sure to create it later.`
);
} else {
const body = await getChangelogForPatch({
@@ -61,7 +66,6 @@ export default async function updateChangelog({ version, previousVersion }) {
previousVersion,
body,
});
console.log("Press ENTER to continue.");
}
await waitForEnter();
@@ -71,7 +71,11 @@ function formatNumber(value) {
return Math.floor(value / 1e5) / 10 + " million";
}
export default async function updateDependentsCount() {
export default async function updateDependentsCount({ dry }) {
if (dry) {
return;
}
try {
await update();
} catch (error) {
+2 -12
View File
@@ -1,12 +1,6 @@
import {
runYarn,
logPromise,
readJson,
writeJson,
processFile,
} from "../utils.js";
import { runYarn, readJson, writeJson, processFile } from "../utils.js";
async function bump({ version }) {
export default async function updateVersion({ version }) {
const pkg = await readJson("package.json");
pkg.version = version;
await writeJson("package.json", pkg);
@@ -34,7 +28,3 @@ async function bump({ version }) {
cwd: "./website",
});
}
export default async function updateVersion(params) {
await logPromise("Bumping version", bump(params));
}
@@ -2,13 +2,19 @@ import chalk from "chalk";
import semver from "semver";
export default function validateNewVersion({ version, previousVersion }) {
if (!version) {
throw new Error("'--version' is required");
}
if (!semver.valid(version)) {
throw new Error("Invalid version specified");
throw new Error(
`Invalid version '${chalk.red.underline(version)}' specified`
);
}
if (!semver.gt(version, previousVersion)) {
throw new Error(
`Version ${chalk.yellow(version)} has already been published`
`Version '${chalk.yellow.underline(version)}' has already been published`
);
}
}
+28 -22
View File
@@ -2,8 +2,6 @@ import chalk from "chalk";
import outdent from "outdent";
import { waitForEnter, logPromise } from "../utils.js";
const outdentString = outdent.string;
export async function isVersionReleased(version) {
const response = await fetch("https://registry.npmjs.org/prettier/");
const result = await response.json();
@@ -23,8 +21,6 @@ async function checkBotPermission() {
return maintainers.some(({ name }) => name === "prettier-bot");
}
checkBotPermission();
const sleep = () =>
new Promise((resolve) => {
setTimeout(resolve, 30_000);
@@ -37,35 +33,45 @@ export default async function waitForBotRelease({ dry, version }) {
if (!(await checkBotPermission())) {
console.log(
outdentString(chalk/* indent */ `
1. Go to {green.underline https://www.npmjs.com/package/prettier/access}
2. Add "{yellow prettier-bot}" as prettier package maintainer.
Press ENTER to continue.
`)
outdent`
1. Go to ${chalk.green.underline(
"https://www.npmjs.com/package/prettier/access"
)}
2. Add "${chalk.yellow("prettier-bot")}" as prettier package maintainer.
`
);
await waitForEnter();
}
console.log(
outdentString(chalk/* indent */ `
1. Go to {green.underline https://www.npmjs.com/package/prettier/access}
2. Make sure "{yellow Publishing access}" section is set to "{yellow Require two-factor authentication or automation tokens}".
Press ENTER to continue.
`)
outdent`
1. Go to ${chalk.green.underline(
"https://www.npmjs.com/package/prettier/access"
)}
2. Make sure "${chalk.yellow(
"Publishing access"
)}" section is set to "${chalk.yellow(
"Require two-factor authentication or automation tokens"
)}".
`
);
await waitForEnter();
console.log(
outdentString(chalk/* indent */ `
1. Go to {green.underline https://github.com/prettier/release-workflow/actions/workflows/release.yml}
2. Click "{green Run workflow}" button, type "{yellow.underline ${version}}" in "Version to release", uncheck all checkboxes, hit the "{bgGreen Run workflow}" button.
Press ENTER to continue.
`)
outdent`
1. Go to ${chalk.green.underline(
"https://github.com/prettier/release-workflow/actions/workflows/release.yml"
)}
2. Click "${chalk.green(
"Run workflow"
)}" button, type "${chalk.yellow.underline(
version
)}" in "Version to release", uncheck all checkboxes, hit the "${chalk.bgGreen(
"Run workflow"
)}" button.
`
);
await waitForEnter();
@@ -4,12 +4,20 @@ import chalk from "chalk";
import validateNewVersion from "../steps/validate-new-version.js";
describe("validate-new-version", () => {
it("throws error for missing version", () => {
assert.throws(
() => {
validateNewVersion({});
},
{ message: "'--version' is required" }
);
});
it("throws error for invalid semver", () => {
assert.throws(
() => {
validateNewVersion({ version: "foo" });
},
{ message: "Invalid version specified" }
{ message: `Invalid version '${chalk.red.underline("foo")}' specified` }
);
});
it("throws error when version isn't greater than prev version", () => {
@@ -17,7 +25,11 @@ describe("validate-new-version", () => {
() => {
validateNewVersion({ version: "0.0.1", previousVersion: "0.0.2" });
},
{ message: `Version ${chalk.yellow("0.0.1")} has already been published` }
{
message: `Version '${chalk.yellow(
"0.0.1"
)}' has already been published`,
}
);
});
});
+36 -17
View File
@@ -2,38 +2,54 @@ import fs from "node:fs";
import readline from "node:readline";
import chalk from "chalk";
import { execa } from "execa";
import stringWidth from "string-width";
import outdent from "outdent";
import getFormattedDate from "./get-formatted-date.js";
readline.emitKeypressEvents(process.stdin);
const OK = chalk.bgGreen.black(" DONE ");
const FAIL = chalk.bgRed.black(" FAIL ");
const statusConfig = [
{ color: "bgGreen", text: "DONE" },
{ color: "bgRed", text: "FAIL" },
{ color: "bgGray", text: "SKIPPED" },
];
const maxLength = Math.max(...statusConfig.map(({ text }) => text.length)) + 2;
const padStatusText = (text) => {
while (text.length < maxLength) {
text = text.length % 2 ? `${text} ` : ` ${text}`;
}
return text;
};
const status = {};
for (const { color, text } of statusConfig) {
status[text] = chalk[color].black(padStatusText(text));
}
function fitTerminal(input) {
const columns = Math.min(process.stdout.columns, 80);
const WIDTH = columns - stringWidth(OK) + 1;
function fitTerminal(input, suffix = "") {
const columns = Math.min(process.stdout.columns || 40, 80);
const WIDTH = columns - maxLength + 1;
if (input.length < WIDTH) {
input += chalk.dim(".").repeat(WIDTH - input.length - 1);
const repeatCount = Math.max(WIDTH - input.length - 1 - suffix.length, 0);
input += chalk.dim(".").repeat(repeatCount) + suffix;
}
return input;
}
async function logPromise(name, promiseOrAsyncFunction) {
const promise =
typeof promiseOrAsyncFunction === "function"
? promiseOrAsyncFunction()
: promiseOrAsyncFunction;
async function logPromise(name, promiseOrAsyncFunction, shouldSkip = false) {
process.stdout.write(fitTerminal(name));
if (shouldSkip) {
process.stdout.write(`${status.SKIPPED}\n`);
return;
}
try {
const result = await promise;
process.stdout.write(`${OK}\n`);
const result = await (typeof promiseOrAsyncFunction === "function"
? promiseOrAsyncFunction()
: promiseOrAsyncFunction);
process.stdout.write(`${status.DONE}\n`);
return result;
} catch (error) {
process.stdout.write(`${FAIL}\n`);
process.stdout.write(`${status.FAIL}\n`);
throw error;
}
}
@@ -42,7 +58,7 @@ async function runYarn(args, options) {
args = Array.isArray(args) ? args : [args];
try {
return await execa("yarn", ["--silent", ...args], options);
return await execa("yarn", ["--silent", "run", ...args], options);
} catch (error) {
throw new Error(`\`yarn ${args.join(" ")}\` failed\n${error.stdout}`);
}
@@ -54,6 +70,9 @@ function runGit(args, options) {
}
function waitForEnter() {
console.log();
console.log(chalk.gray("Press ENTER to continue."));
process.stdin.setRawMode(true);
return new Promise((resolve, reject) => {