Skip to content

Commit 3d9c244

Browse files
authored
Add script for tagging all npm packages for a release (#8041)
* Add script for tagging all npm packages for a release * Review changes * Get list of packages to tag by listing public yarn workspaces * Format
1 parent 3027d26 commit 3d9c244

File tree

3 files changed

+120
-1
lines changed

3 files changed

+120
-1
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ To build a new version and release it on NPM, follow these steps:
447447
1. Verify that the playground bundle for the new version is now present on the settings tab in https://rescript-lang.org/try.
448448
1. Run `npm info rescript` to verify that the new version is now present with tag "ci".
449449
1. Test the new version.
450-
1. Tag the new version as appropriate (`latest` or `next`): `npm dist-tag add rescript@<version> <tag>`
450+
1. Tag all packages for the new version as appropriate (`latest` or `next`): `./scripts/npmRelease.js --version <version> --tag <tag>`
451451
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`.
452452
1. Create a PR with the following changes to prepare for development of the next version:
453453
- Increment the `EXPECTED_VERSION` number in `yarn.config.cjs` for the next version.

lib_dev/process.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const signals = {
2525
export const {
2626
shell,
2727
node,
28+
npm,
2829
yarn,
2930
mocha,
3031
bsc,
@@ -118,6 +119,18 @@ export function setup(cwd = process.cwd()) {
118119
return exec("node", [script, ...args], options);
119120
},
120121

122+
/**
123+
* Execute npm command
124+
*
125+
* @param {string} command
126+
* @param {string[]} [args]
127+
* @param {ExecOptions} [options]
128+
* @return {Promise<ExecResult>}
129+
*/
130+
npm(command, args = [], options = {}) {
131+
return exec("npm", [...command.split(" "), ...args], options);
132+
},
133+
121134
/**
122135
* Execute Yarn command
123136
*

scripts/npmRelease.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Tag a published version of the main ReScript packages with a given dist-tag.
4+
*
5+
* Usage:
6+
* node scripts/npmRelease.js --version 12.0.1 --tag next
7+
* node scripts/npmRelease.js --version 12.0.1 --tag latest --otp 123456
8+
*
9+
* - Runs `npm dist-tag add` for every non-private workspace (same as CI publish)
10+
* reusing the same OTP so you only get prompted once.
11+
* - Pass `--dry-run` to see the commands without executing them.
12+
*/
13+
import process from "node:process";
14+
import readline from "node:readline/promises";
15+
import { parseArgs } from "node:util";
16+
import { npm, yarn } from "../lib_dev/process.js";
17+
18+
async function promptForOtp(existingOtp) {
19+
if (existingOtp) {
20+
return existingOtp;
21+
}
22+
const rl = readline.createInterface({
23+
input: process.stdin,
24+
output: process.stdout,
25+
});
26+
const answer = await rl.question("npm one-time password: ");
27+
rl.close();
28+
return answer.trim();
29+
}
30+
31+
async function getPublicWorkspaces() {
32+
const { stdout } = await yarn("workspaces", [
33+
"list",
34+
"--no-private",
35+
"--json",
36+
]);
37+
return stdout
38+
.split("\n")
39+
.filter(Boolean)
40+
.map(line => JSON.parse(line))
41+
.map(entry => entry.name);
42+
}
43+
44+
async function runDistTag(pkgName, version, tag, otp, dryRun) {
45+
const spec = `${pkgName}@${version}`;
46+
const args = ["dist-tag", "add", spec, tag, "--otp", otp];
47+
if (dryRun) {
48+
console.log(`[dry-run] npm ${args.join(" ")}`);
49+
return;
50+
}
51+
console.log(`Tagging ${spec} as ${tag}...`);
52+
await npm("dist-tag", ["add", spec, tag, "--otp", otp], {
53+
stdio: "inherit",
54+
throwOnFail: true,
55+
});
56+
}
57+
58+
async function main() {
59+
try {
60+
const { values } = parseArgs({
61+
args: process.argv.slice(2),
62+
strict: true,
63+
options: {
64+
version: { type: "string", short: "v" },
65+
tag: { type: "string", short: "t" },
66+
otp: { type: "string" },
67+
"dry-run": { type: "boolean" },
68+
},
69+
});
70+
if (!values.version || !values.tag) {
71+
console.error(
72+
"Usage: node scripts/npmRelease.js --version <version> --tag <tag> [--otp <code>] [--dry-run]",
73+
);
74+
process.exitCode = 1;
75+
return;
76+
}
77+
const workspaces = await getPublicWorkspaces();
78+
if (workspaces.length === 0) {
79+
throw new Error("No public workspaces found.");
80+
}
81+
82+
const otp = await promptForOtp(values.otp);
83+
if (!otp) {
84+
throw new Error("OTP is required to publish dist-tags.");
85+
}
86+
for (const workspace of workspaces) {
87+
await runDistTag(
88+
workspace,
89+
values.version,
90+
values.tag,
91+
otp,
92+
Boolean(values["dry-run"]),
93+
);
94+
}
95+
if (values["dry-run"]) {
96+
console.log("Dry run complete.");
97+
} else {
98+
console.log("All packages tagged successfully.");
99+
}
100+
} catch (error) {
101+
console.error(error.message || error);
102+
process.exitCode = 1;
103+
}
104+
}
105+
106+
await main();

0 commit comments

Comments
 (0)