Skip to content

Commit 368edae

Browse files
authored
ci: automate setting npm tags (microsoft#2803)
## Summary: One part of our publish process we have not yet automated is setting the `v0.xx-stable` and `next` tags when a new release is promoted or demoted. Let's automate this by specifying additional tags in our CI that should be set when we publish a release. Now, a new `latest` or patch to `latest` will also always set is `0.xx-stable` tag. Note: Previously, if `0.xx` was tagged as `latest`, we would not have had a `v0.xx-stable` tag. Now we will always have both `latest` and the `0.xx-stable` tag. ## Test Plan: I duplicated this change to microsoft#2800 and microsoft#2801 . We can look at the dry run output for both to make sure release is behaving as expected.
1 parent c0d393f commit 368edae

4 files changed

Lines changed: 163 additions & 39 deletions

File tree

.ado/jobs/npm-publish.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ jobs:
4747
- script: |
4848
echo Target branch: $(System.PullRequest.TargetBranch)
4949
yarn nx release --dry-run --verbose
50+
51+
# Show what additional tags would be applied
52+
node .ado/scripts/apply-additional-tags.mjs --tags "$(additionalTags)" --dry-run
5053
displayName: Version and publish packages (dry run)
5154
condition: and(succeeded(), ne(variables['publish_react_native_macos'], '1'))
5255
@@ -83,6 +86,11 @@ jobs:
8386
displayName: Publish packages
8487
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))
8588
89+
- script: |
90+
node .ado/scripts/apply-additional-tags.mjs --tags "$(additionalTags)" --token "$(npmAuthToken)"
91+
displayName: Apply additional dist-tags
92+
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))
93+
8694
- script: |
8795
if [ "$(USE_YARN_FOR_PUBLISH)" = "true" ]; then
8896
echo "Cleaning up yarn npm configuration"
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// @ts-check
2+
import { spawnSync } from "node:child_process";
3+
import * as fs from "node:fs";
4+
import * as util from "node:util";
5+
6+
/**
7+
* Apply additional dist-tags to published packages
8+
* Usage: node apply-additional-tags.mjs --tags <tags> --token <token>
9+
* node apply-additional-tags.mjs --tags <tags> --dry-run
10+
* Where tags is a comma-separated list of tags (e.g., "next,v0.79-stable")
11+
*/
12+
13+
const registry = "https://registry.npmjs.org/";
14+
const packages = [
15+
"@react-native-macos/virtualized-lists",
16+
"react-native-macos",
17+
];
18+
19+
/**
20+
* @typedef {{
21+
* tags?: string;
22+
* token?: string;
23+
* "dry-run"?: boolean;
24+
* }} Options;
25+
*/
26+
27+
/**
28+
* @param {Options} options
29+
* @returns {number}
30+
*/
31+
function main({ tags, token, "dry-run": dryRun }) {
32+
if (!tags) {
33+
console.log("No additional tags to apply");
34+
return 0;
35+
}
36+
37+
if (!dryRun && !token) {
38+
console.error("Error: npm auth token is required (use --dry-run to preview)");
39+
return 1;
40+
}
41+
42+
const packageJson = JSON.parse(
43+
fs.readFileSync("./packages/react-native/package.json", "utf-8")
44+
);
45+
const version = packageJson.version;
46+
47+
if (dryRun) {
48+
console.log("");
49+
console.log("=== Additional dist-tags that would be applied ===");
50+
for (const tag of tags.split(",")) {
51+
for (const pkg of packages) {
52+
console.log(` ${pkg}@${version} -> ${tag}`);
53+
}
54+
}
55+
return 0;
56+
}
57+
58+
for (const tag of tags.split(",")) {
59+
for (const pkg of packages) {
60+
console.log(`Adding dist-tag '${tag}' to ${pkg}@${version}`);
61+
const result = spawnSync(
62+
"npm",
63+
[
64+
"dist-tag",
65+
"add",
66+
`${pkg}@${version}`,
67+
tag,
68+
"--registry",
69+
registry,
70+
`--//registry.npmjs.org/:_authToken=${token}`,
71+
],
72+
{ stdio: "inherit", shell: true }
73+
);
74+
75+
if (result.status !== 0) {
76+
console.error(`Failed to add dist-tag '${tag}' to ${pkg}@${version}`);
77+
return 1;
78+
}
79+
}
80+
}
81+
82+
return 0;
83+
}
84+
85+
const { values } = util.parseArgs({
86+
args: process.argv.slice(2),
87+
options: {
88+
tags: {
89+
type: "string",
90+
},
91+
token: {
92+
type: "string",
93+
},
94+
"dry-run": {
95+
type: "boolean",
96+
default: false,
97+
},
98+
},
99+
strict: true,
100+
});
101+
102+
process.exitCode = main(values);

.ado/scripts/prepublish-check.mjs

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,8 @@ const RNMACOS_NEXT = "react-native-macos@next";
2727
* verbose?: boolean;
2828
* }} Options;
2929
* @typedef {{
30-
* npmTag: string;
30+
* npmTags: string[];
3131
* prerelease?: string;
32-
* isNewTag?: boolean;
3332
* }} TagInfo;
3433
*/
3534

@@ -264,7 +263,12 @@ function getPublishedVersion(tag) {
264263
}
265264

266265
/**
267-
* Returns the npm tag and prerelease identifier for the specified branch.
266+
* Returns the npm tags and prerelease identifier for the specified branch.
267+
*
268+
* The first tag in the array is used for the initial publish. When promoting
269+
* to `latest`, also includes additional tags to apply:
270+
* - The version-specific stable tag (e.g., `v0.81-stable`)
271+
* - The `next` tag if the current `next` version is lower
268272
*
269273
* @privateRemarks
270274
* Note that the current implementation treats minor versions as major. If
@@ -276,50 +280,57 @@ function getPublishedVersion(tag) {
276280
* @param {typeof info} log
277281
* @returns {TagInfo}
278282
*/
279-
function getTagForStableBranch(branch, { tag }, log) {
283+
function getTagsForStableBranch(branch, { tag }, log) {
280284
if (!isStableBranch(branch)) {
281285
throw new Error("Expected a stable branch");
282286
}
283287

284288
const latestVersion = getPublishedVersion("latest");
289+
const nextVersion = getPublishedVersion("next");
285290
const currentVersion = versionToNumber(branch);
286291

287292
log(`${RNMACOS_LATEST}: ${latestVersion}`);
293+
log(`${RNMACOS_NEXT}: ${nextVersion}`);
288294
log(`Current version: ${currentVersion}`);
289295

290296
// Patching latest version
291297
if (currentVersion === latestVersion) {
292-
const npmTag = "latest";
293-
log(`Expected npm tag: ${npmTag}`);
294-
return { npmTag };
298+
const versionTag = branch;
299+
log(`Expected npm tags: latest, ${versionTag}`);
300+
return { npmTags: ["latest", versionTag] };
295301
}
296302

297303
// Demoting or patching an older stable version
298304
if (currentVersion < latestVersion) {
299-
const npmTag = "v" + branch;
300-
log(`Expected npm tag: ${npmTag}`);
301-
// If we're demoting a branch, we will need to create a new tag. This will
302-
// make Nx trip if we don't specify a fallback. In all other scenarios, the
303-
// tags should exist and therefore prefer it to fail.
304-
return { npmTag, isNewTag: true };
305+
const npmTag = branch;
306+
log(`Expected npm tags: ${npmTag}`);
307+
return { npmTags: [npmTag] };
305308
}
306309

307310
// Publishing a new latest version
308311
if (tag === "latest") {
309-
log(`Expected npm tag: ${tag}`);
310-
return { npmTag: tag };
312+
// When promoting to latest, also add the version-specific stable tag
313+
const versionTag = branch;
314+
const npmTags = ["latest", versionTag];
315+
316+
// Also add "next" tag if the current next version is lower
317+
if (currentVersion > nextVersion) {
318+
npmTags.push(NPM_TAG_NEXT);
319+
}
320+
321+
log(`Expected npm tags: ${npmTags.join(", ")}`);
322+
return { npmTags };
311323
}
312324

313325
// Publishing a release candidate
314-
const nextVersion = getPublishedVersion("next");
315-
log(`${RNMACOS_NEXT}: ${nextVersion}`);
316-
log(`Expected npm tag: ${NPM_TAG_NEXT}`);
326+
// currentVersion > latestVersion
327+
log(`Expected npm tags: ${NPM_TAG_NEXT}`);
317328

318329
if (currentVersion < nextVersion) {
319330
throw new Error(`Current version cannot be a release candidate because it is too old: ${currentVersion} < ${nextVersion}`);
320331
}
321332

322-
return { npmTag: NPM_TAG_NEXT, prerelease: "rc" };
333+
return { npmTags: [NPM_TAG_NEXT], prerelease: "rc" };
323334
}
324335

325336
/**
@@ -330,11 +341,12 @@ function getTagForStableBranch(branch, { tag }, log) {
330341
* @param {Options} options
331342
* @returns {asserts config is NxConfig["release"]}
332343
*/
333-
function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNewTag }, options) {
344+
function enablePublishing(config, currentBranch, { npmTags, prerelease }, options) {
334345
/** @type {string[]} */
335346
const errors = [];
336347

337348
const { defaultBase, release } = config;
349+
const [primaryTag, ...additionalTags] = npmTags;
338350

339351
// `defaultBase` determines what we diff against when looking for tags or
340352
// released version and must therefore be set to either the main branch or one
@@ -358,23 +370,10 @@ function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNe
358370

359371
// What the published version should be tagged as e.g., "latest" or "nightly".
360372
const currentVersionResolverMetadata = /** @type {{ tag?: string }} */ (versionActionsOptions.currentVersionResolverMetadata || {});
361-
if (currentVersionResolverMetadata.tag !== tag) {
362-
errors.push(`'release.version.versionActionsOptions.currentVersionResolverMetadata.tag' must be set to '${tag}'`);
373+
if (currentVersionResolverMetadata.tag !== primaryTag) {
374+
errors.push(`'release.version.versionActionsOptions.currentVersionResolverMetadata.tag' must be set to '${primaryTag}'`);
363375
versionActionsOptions.currentVersionResolverMetadata ??= {};
364-
/** @type {any} */ (versionActionsOptions.currentVersionResolverMetadata).tag = tag;
365-
}
366-
367-
// If we're demoting a branch, we will need to create a new tag. This will
368-
// make Nx trip if we don't specify a fallback. In all other scenarios, the
369-
// tags should exist and therefore prefer it to fail.
370-
if (isNewTag) {
371-
if (versionActionsOptions.fallbackCurrentVersionResolver !== "disk") {
372-
errors.push("'release.version.versionActionsOptions.fallbackCurrentVersionResolver' must be set to 'disk'");
373-
versionActionsOptions.fallbackCurrentVersionResolver = "disk";
374-
}
375-
} else if (typeof versionActionsOptions.fallbackCurrentVersionResolver === "string") {
376-
errors.push("'release.version.versionActionsOptions.fallbackCurrentVersionResolver' must be removed");
377-
versionActionsOptions.fallbackCurrentVersionResolver = undefined;
376+
/** @type {any} */ (versionActionsOptions.currentVersionResolverMetadata).tag = primaryTag;
378377
}
379378

380379
if (errors.length > 0) {
@@ -388,6 +387,17 @@ function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNe
388387
verifyNpmAuth();
389388
}
390389

390+
// Output additional tags as pipeline/workflow variable
391+
if (additionalTags.length > 0) {
392+
const tagsValue = additionalTags.join(",");
393+
// Azure Pipelines
394+
console.log(`##vso[task.setvariable variable=additionalTags]${tagsValue}`);
395+
// GitHub Actions
396+
if (process.env["GITHUB_OUTPUT"]) {
397+
fs.appendFileSync(process.env["GITHUB_OUTPUT"], `additionalTags=${tagsValue}\n`);
398+
}
399+
}
400+
391401
// Don't enable publishing in PRs
392402
if (!getTargetBranch()) {
393403
enablePublishingOnAzurePipelines();
@@ -410,10 +420,10 @@ function main(options) {
410420
const config = loadNxConfig(NX_CONFIG_FILE);
411421
try {
412422
if (isMainBranch(branch)) {
413-
const info = { npmTag: NPM_TAG_NIGHTLY, prerelease: NPM_TAG_NIGHTLY };
423+
const info = { npmTags: [NPM_TAG_NIGHTLY], prerelease: NPM_TAG_NIGHTLY };
414424
enablePublishing(config, branch, info, options);
415425
} else if (isStableBranch(branch)) {
416-
const tag = getTagForStableBranch(branch, options, logger);
426+
const tag = getTagsForStableBranch(branch, options, logger);
417427
enablePublishing(config, branch, tag, options);
418428
}
419429
} catch (e) {

.github/workflows/microsoft-pr.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ jobs:
5656
- name: Read publish tag from nx.json
5757
id: config
5858
run: |
59-
PUBLISH_TAG=$(jq -r '.release.version.generatorOptions.currentVersionResolverMetadata.tag' nx.json)
59+
PUBLISH_TAG=$(jq -r '.release.version.versionActionsOptions.currentVersionResolverMetadata.tag' nx.json)
6060
echo "publishTag=$PUBLISH_TAG" >> $GITHUB_OUTPUT
6161
echo "Using publish tag from nx.json: $PUBLISH_TAG"
6262
- name: Configure git
@@ -67,13 +67,17 @@ jobs:
6767
- name: Install dependencies
6868
run: yarn
6969
- name: Verify release config
70+
id: prepublish
7071
run: |
7172
node .ado/scripts/prepublish-check.mjs --verbose --skip-auth --tag ${{ steps.config.outputs.publishTag }}
7273
7374
- name: Version and publish packages (dry run)
7475
run: |
7576
echo "Target branch: ${{ github.base_ref }}"
7677
yarn nx release --dry-run --verbose
78+
79+
# Show what additional tags would be applied
80+
node .ado/scripts/apply-additional-tags.mjs --tags "${{ steps.prepublish.outputs.additionalTags }}" --dry-run
7781
7882
yarn-constraints:
7983
name: "Check Yarn Constraints"

0 commit comments

Comments
 (0)