diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e86e6fe8f4..96a539c1ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -447,7 +447,7 @@ To build a new version and release it on NPM, follow these steps: 1. Verify that the playground bundle for the new version is now present on the settings tab in https://rescript-lang.org/try. 1. Run `npm info rescript` to verify that the new version is now present with tag "ci". 1. Test the new version. -1. Tag the new version as appropriate (`latest` or `next`): `npm dist-tag add rescript@ ` +1. Tag all packages for the new version as appropriate (`latest` or `next`): `./scripts/npmRelease.js --version --tag ` 1. Create a release entry for the version tag on the [Github Releases page](https://github.com/rescript-lang/rescript-compiler/releases), copying the changes from `CHANGELOG.md`. 1. Create a PR with the following changes to prepare for development of the next version: - Increment the `EXPECTED_VERSION` number in `yarn.config.cjs` for the next version. diff --git a/lib_dev/process.js b/lib_dev/process.js index 0dbddd4881..ac24088871 100644 --- a/lib_dev/process.js +++ b/lib_dev/process.js @@ -25,6 +25,7 @@ const signals = { export const { shell, node, + npm, yarn, mocha, bsc, @@ -118,6 +119,18 @@ export function setup(cwd = process.cwd()) { return exec("node", [script, ...args], options); }, + /** + * Execute npm command + * + * @param {string} command + * @param {string[]} [args] + * @param {ExecOptions} [options] + * @return {Promise} + */ + npm(command, args = [], options = {}) { + return exec("npm", [...command.split(" "), ...args], options); + }, + /** * Execute Yarn command * diff --git a/scripts/npmRelease.js b/scripts/npmRelease.js new file mode 100755 index 0000000000..a6a1313283 --- /dev/null +++ b/scripts/npmRelease.js @@ -0,0 +1,106 @@ +#!/usr/bin/env node +/** + * Tag a published version of the main ReScript packages with a given dist-tag. + * + * Usage: + * node scripts/npmRelease.js --version 12.0.1 --tag next + * node scripts/npmRelease.js --version 12.0.1 --tag latest --otp 123456 + * + * - Runs `npm dist-tag add` for every non-private workspace (same as CI publish) + * reusing the same OTP so you only get prompted once. + * - Pass `--dry-run` to see the commands without executing them. + */ +import process from "node:process"; +import readline from "node:readline/promises"; +import { parseArgs } from "node:util"; +import { npm, yarn } from "../lib_dev/process.js"; + +async function promptForOtp(existingOtp) { + if (existingOtp) { + return existingOtp; + } + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + const answer = await rl.question("npm one-time password: "); + rl.close(); + return answer.trim(); +} + +async function getPublicWorkspaces() { + const { stdout } = await yarn("workspaces", [ + "list", + "--no-private", + "--json", + ]); + return stdout + .split("\n") + .filter(Boolean) + .map(line => JSON.parse(line)) + .map(entry => entry.name); +} + +async function runDistTag(pkgName, version, tag, otp, dryRun) { + const spec = `${pkgName}@${version}`; + const args = ["dist-tag", "add", spec, tag, "--otp", otp]; + if (dryRun) { + console.log(`[dry-run] npm ${args.join(" ")}`); + return; + } + console.log(`Tagging ${spec} as ${tag}...`); + await npm("dist-tag", ["add", spec, tag, "--otp", otp], { + stdio: "inherit", + throwOnFail: true, + }); +} + +async function main() { + try { + const { values } = parseArgs({ + args: process.argv.slice(2), + strict: true, + options: { + version: { type: "string", short: "v" }, + tag: { type: "string", short: "t" }, + otp: { type: "string" }, + "dry-run": { type: "boolean" }, + }, + }); + if (!values.version || !values.tag) { + console.error( + "Usage: node scripts/npmRelease.js --version --tag [--otp ] [--dry-run]", + ); + process.exitCode = 1; + return; + } + const workspaces = await getPublicWorkspaces(); + if (workspaces.length === 0) { + throw new Error("No public workspaces found."); + } + + const otp = await promptForOtp(values.otp); + if (!otp) { + throw new Error("OTP is required to publish dist-tags."); + } + for (const workspace of workspaces) { + await runDistTag( + workspace, + values.version, + values.tag, + otp, + Boolean(values["dry-run"]), + ); + } + if (values["dry-run"]) { + console.log("Dry run complete."); + } else { + console.log("All packages tagged successfully."); + } + } catch (error) { + console.error(error.message || error); + process.exitCode = 1; + } +} + +await main();