From d9efe697dd40048a26efdb8ebccfc242174663b3 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 27 Nov 2025 17:49:38 +0100 Subject: [PATCH 1/4] Add script for tagging all npm packages for a release --- CONTRIBUTING.md | 2 +- scripts/npmRelease.js | 105 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100755 scripts/npmRelease.js 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/scripts/npmRelease.js b/scripts/npmRelease.js new file mode 100755 index 0000000000..c9747c0b7a --- /dev/null +++ b/scripts/npmRelease.js @@ -0,0 +1,105 @@ +#!/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: rescript, @rescript/runtime, and all platform + * optional packages, reusing the same OTP so you only get prompted once. + * - Pass `--dry-run` to see the commands without executing them. + */ +import { spawn } from "node:child_process"; +import process from "node:process"; +import readline from "node:readline/promises"; +import { parseArgs } from "node:util"; + +const packages = [ + "rescript", + "@rescript/runtime", + "@rescript/darwin-arm64", + "@rescript/darwin-x64", + "@rescript/linux-arm64", + "@rescript/linux-x64", + "@rescript/win32-x64", +]; + +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 runDistTag(pkg, version, tag, otp, dryRun) { + const spec = `${pkg}@${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 new Promise((resolve, reject) => { + const child = spawn("npm", args, { stdio: "inherit" }); + child.on("exit", code => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`npm dist-tag failed for ${spec} (exit ${code})`)); + } + }); + child.on("error", reject); + }); +} + +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 otp = await promptForOtp(values.otp); + if (!otp) { + throw new Error("OTP is required to publish dist-tags."); + } + for (const pkg of packages) { + await runDistTag( + pkg, + 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(); From b7473374ce47609ec77106ad6ca146a1fb163d35 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 27 Nov 2025 19:39:23 +0100 Subject: [PATCH 2/4] Review changes --- lib_dev/process.js | 13 +++++++++++++ scripts/npmRelease.js | 15 ++++----------- 2 files changed, 17 insertions(+), 11 deletions(-) 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 index c9747c0b7a..b72ce3b411 100755 --- a/scripts/npmRelease.js +++ b/scripts/npmRelease.js @@ -10,10 +10,10 @@ * optional packages, reusing the same OTP so you only get prompted once. * - Pass `--dry-run` to see the commands without executing them. */ -import { spawn } from "node:child_process"; import process from "node:process"; import readline from "node:readline/promises"; import { parseArgs } from "node:util"; +import { npm } from "../lib_dev/process.js"; const packages = [ "rescript", @@ -46,16 +46,9 @@ async function runDistTag(pkg, version, tag, otp, dryRun) { return; } console.log(`Tagging ${spec} as ${tag}...`); - await new Promise((resolve, reject) => { - const child = spawn("npm", args, { stdio: "inherit" }); - child.on("exit", code => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`npm dist-tag failed for ${spec} (exit ${code})`)); - } - }); - child.on("error", reject); + await npm("dist-tag", ["add", spec, tag, "--otp", otp], { + stdio: "inherit", + throwOnFail: true, }); } From 81d1f781ffcc74a0b555d4294b1832e155a7ade9 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 29 Nov 2025 21:16:32 +0100 Subject: [PATCH 3/4] Get list of packages to tag by listing public yarn workspaces --- scripts/npmRelease.js | 44 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/scripts/npmRelease.js b/scripts/npmRelease.js index b72ce3b411..333d1c3816 100755 --- a/scripts/npmRelease.js +++ b/scripts/npmRelease.js @@ -6,24 +6,14 @@ * 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: rescript, @rescript/runtime, and all platform - * optional packages, reusing the same OTP so you only get prompted once. + * - 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 } from "../lib_dev/process.js"; - -const packages = [ - "rescript", - "@rescript/runtime", - "@rescript/darwin-arm64", - "@rescript/darwin-x64", - "@rescript/linux-arm64", - "@rescript/linux-x64", - "@rescript/win32-x64", -]; +import { npm, yarn } from "../lib_dev/process.js"; async function promptForOtp(existingOtp) { if (existingOtp) { @@ -38,8 +28,17 @@ async function promptForOtp(existingOtp) { return answer.trim(); } -async function runDistTag(pkg, version, tag, otp, dryRun) { - const spec = `${pkg}@${version}`; +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(" ")}`); @@ -71,18 +70,17 @@ async function main() { 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 pkg of packages) { - await runDistTag( - pkg, - values.version, - values.tag, - otp, - Boolean(values["dry-run"]), - ); + 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."); From 3a270ae20526af6c46459e74445633c1b08cf7b9 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 29 Nov 2025 21:51:51 +0100 Subject: [PATCH 4/4] Format --- scripts/npmRelease.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/npmRelease.js b/scripts/npmRelease.js index 333d1c3816..a6a1313283 100755 --- a/scripts/npmRelease.js +++ b/scripts/npmRelease.js @@ -29,7 +29,11 @@ async function promptForOtp(existingOtp) { } async function getPublicWorkspaces() { - const { stdout } = await yarn("workspaces", ["list", "--no-private", "--json"]); + const { stdout } = await yarn("workspaces", [ + "list", + "--no-private", + "--json", + ]); return stdout .split("\n") .filter(Boolean) @@ -80,7 +84,13 @@ async function main() { 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"])); + await runDistTag( + workspace, + values.version, + values.tag, + otp, + Boolean(values["dry-run"]), + ); } if (values["dry-run"]) { console.log("Dry run complete.");