From 38d48709c466ec13934f3a6ff909b6a7de811335 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 14 Jan 2026 14:38:15 -0600 Subject: [PATCH 1/5] ci: automate setting stable and next tags --- .ado/jobs/npm-publish.yml | 32 +++++++++++ .ado/scripts/prepublish-check.mjs | 92 +++++++++++++++++------------- .github/workflows/microsoft-pr.yml | 13 +++++ 3 files changed, 98 insertions(+), 39 deletions(-) diff --git a/.ado/jobs/npm-publish.yml b/.ado/jobs/npm-publish.yml index 01b83311b41c0f..ded358f89342a3 100644 --- a/.ado/jobs/npm-publish.yml +++ b/.ado/jobs/npm-publish.yml @@ -47,6 +47,18 @@ jobs: - script: | echo Target branch: $(System.PullRequest.TargetBranch) yarn nx release --dry-run --verbose + + # Show what additional tags would be applied + if [ -n "$(additionalTags)" ]; then + echo "" + echo "=== Additional dist-tags that would be applied ===" + VERSION=$(jq -r '.version' ./packages/react-native/package.json) + IFS=',' read -ra TAGS <<< "$(additionalTags)" + for tag in "${TAGS[@]}"; do + echo " @react-native-macos/virtualized-lists@$VERSION -> $tag" + echo " react-native-macos@$VERSION -> $tag" + done + fi displayName: Version and publish packages (dry run) condition: and(succeeded(), ne(variables['publish_react_native_macos'], '1')) @@ -83,6 +95,26 @@ jobs: displayName: Publish packages condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1')) + - script: | + set -eox pipefail + if [[ -z "$(additionalTags)" ]]; then + echo "No additional tags to apply" + exit 0 + fi + + # Get the version from package.json + VERSION=$(jq -r '.version' ./packages/react-native/package.json) + + # Apply additional tags + IFS=',' read -ra TAGS <<< "$(additionalTags)" + for tag in "${TAGS[@]}"; do + echo "Adding dist-tag '$tag' to packages" + npm dist-tag add "@react-native-macos/virtualized-lists@$VERSION" "$tag" --registry https://registry.npmjs.org/ --//registry.npmjs.org/:_authToken=$(npmAuthToken) + npm dist-tag add "react-native-macos@$VERSION" "$tag" --registry https://registry.npmjs.org/ --//registry.npmjs.org/:_authToken=$(npmAuthToken) + done + displayName: Apply additional dist-tags + condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1')) + - script: | if [ "$(USE_YARN_FOR_PUBLISH)" = "true" ]; then echo "Cleaning up yarn npm configuration" diff --git a/.ado/scripts/prepublish-check.mjs b/.ado/scripts/prepublish-check.mjs index 4ea23f41d8960d..cf6ca306ce02b0 100644 --- a/.ado/scripts/prepublish-check.mjs +++ b/.ado/scripts/prepublish-check.mjs @@ -27,9 +27,8 @@ const RNMACOS_NEXT = "react-native-macos@next"; * verbose?: boolean; * }} Options; * @typedef {{ - * npmTag: string; + * npmTags: string[]; * prerelease?: string; - * isNewTag?: boolean; * }} TagInfo; */ @@ -264,7 +263,12 @@ function getPublishedVersion(tag) { } /** - * Returns the npm tag and prerelease identifier for the specified branch. + * Returns the npm tags and prerelease identifier for the specified branch. + * + * The first tag in the array is used for the initial publish. When promoting + * to `latest`, also includes additional tags to apply: + * - The version-specific stable tag (e.g., `v0.81-stable`) + * - The `next` tag if the current `next` version is lower * * @privateRemarks * Note that the current implementation treats minor versions as major. If @@ -276,50 +280,60 @@ function getPublishedVersion(tag) { * @param {typeof info} log * @returns {TagInfo} */ -function getTagForStableBranch(branch, { tag }, log) { +function getTagsForStableBranch(branch, { tag }, log) { if (!isStableBranch(branch)) { throw new Error("Expected a stable branch"); } const latestVersion = getPublishedVersion("latest"); + const nextVersion = getPublishedVersion("next"); const currentVersion = versionToNumber(branch); log(`${RNMACOS_LATEST}: ${latestVersion}`); + log(`${RNMACOS_NEXT}: ${nextVersion}`); log(`Current version: ${currentVersion}`); // Patching latest version if (currentVersion === latestVersion) { - const npmTag = "latest"; - log(`Expected npm tag: ${npmTag}`); - return { npmTag }; + const versionTag = "v" + branch; + log(`Expected npm tags: latest, ${versionTag}`); + return { npmTags: ["latest", versionTag] }; } // Demoting or patching an older stable version if (currentVersion < latestVersion) { const npmTag = "v" + branch; - log(`Expected npm tag: ${npmTag}`); - // If we're demoting a branch, we will need to create a new tag. This will - // make Nx trip if we don't specify a fallback. In all other scenarios, the - // tags should exist and therefore prefer it to fail. - return { npmTag, isNewTag: true }; + log(`Expected npm tags: ${npmTag}`); + return { npmTags: [npmTag] }; } // Publishing a new latest version if (tag === "latest") { - log(`Expected npm tag: ${tag}`); - return { npmTag: tag }; + // When promoting to latest, also add the version-specific stable tag + const versionTag = "v" + branch; + const npmTags = ["latest", versionTag]; + + // Also add "next" tag if the current next version is lower + if (currentVersion > nextVersion) { + npmTags.push(NPM_TAG_NEXT); + } + + log(`Expected npm tags: ${npmTags.join(", ")}`); + return { npmTags }; } // Publishing a release candidate - const nextVersion = getPublishedVersion("next"); - log(`${RNMACOS_NEXT}: ${nextVersion}`); - log(`Expected npm tag: ${NPM_TAG_NEXT}`); + if (currentVersion > latestVersion) { + log(`Expected npm tags: ${NPM_TAG_NEXT}`); - if (currentVersion < nextVersion) { - throw new Error(`Current version cannot be a release candidate because it is too old: ${currentVersion} < ${nextVersion}`); + if (currentVersion < nextVersion) { + throw new Error(`Current version cannot be a release candidate because it is too old: ${currentVersion} < ${nextVersion}`); + } + + return { npmTags: [NPM_TAG_NEXT], prerelease: "rc" }; } - return { npmTag: NPM_TAG_NEXT, prerelease: "rc" }; + throw new Error(`Unexpected state: currentVersion=${currentVersion}, latestVersion=${latestVersion}, nextVersion=${nextVersion}, tag=${tag}`); } /** @@ -330,11 +344,13 @@ function getTagForStableBranch(branch, { tag }, log) { * @param {Options} options * @returns {asserts config is NxConfig["release"]} */ -function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNewTag }, options) { +function enablePublishing(config, currentBranch, { npmTags, prerelease }, options) { /** @type {string[]} */ const errors = []; const { defaultBase, release } = config; + const primaryTag = npmTags[0]; + const additionalTags = npmTags.slice(1); // `defaultBase` determines what we diff against when looking for tags or // released version and must therefore be set to either the main branch or one @@ -358,23 +374,10 @@ function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNe // What the published version should be tagged as e.g., "latest" or "nightly". const currentVersionResolverMetadata = /** @type {{ tag?: string }} */ (versionActionsOptions.currentVersionResolverMetadata || {}); - if (currentVersionResolverMetadata.tag !== tag) { - errors.push(`'release.version.versionActionsOptions.currentVersionResolverMetadata.tag' must be set to '${tag}'`); + if (currentVersionResolverMetadata.tag !== primaryTag) { + errors.push(`'release.version.versionActionsOptions.currentVersionResolverMetadata.tag' must be set to '${primaryTag}'`); versionActionsOptions.currentVersionResolverMetadata ??= {}; - /** @type {any} */ (versionActionsOptions.currentVersionResolverMetadata).tag = tag; - } - - // If we're demoting a branch, we will need to create a new tag. This will - // make Nx trip if we don't specify a fallback. In all other scenarios, the - // tags should exist and therefore prefer it to fail. - if (isNewTag) { - if (versionActionsOptions.fallbackCurrentVersionResolver !== "disk") { - errors.push("'release.version.versionActionsOptions.fallbackCurrentVersionResolver' must be set to 'disk'"); - versionActionsOptions.fallbackCurrentVersionResolver = "disk"; - } - } else if (typeof versionActionsOptions.fallbackCurrentVersionResolver === "string") { - errors.push("'release.version.versionActionsOptions.fallbackCurrentVersionResolver' must be removed"); - versionActionsOptions.fallbackCurrentVersionResolver = undefined; + /** @type {any} */ (versionActionsOptions.currentVersionResolverMetadata).tag = primaryTag; } if (errors.length > 0) { @@ -391,6 +394,17 @@ function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNe // Don't enable publishing in PRs if (!getTargetBranch()) { enablePublishingOnAzurePipelines(); + + // Output additional tags as pipeline/workflow variable + if (additionalTags.length > 0) { + const tagsValue = additionalTags.join(","); + // Azure Pipelines + console.log(`##vso[task.setvariable variable=additionalTags]${tagsValue}`); + // GitHub Actions + if (process.env["GITHUB_OUTPUT"]) { + fs.appendFileSync(process.env["GITHUB_OUTPUT"], `additionalTags=${tagsValue}\n`); + } + } } } @@ -410,10 +424,10 @@ function main(options) { const config = loadNxConfig(NX_CONFIG_FILE); try { if (isMainBranch(branch)) { - const info = { npmTag: NPM_TAG_NIGHTLY, prerelease: NPM_TAG_NIGHTLY }; + const info = { npmTags: [NPM_TAG_NIGHTLY], prerelease: NPM_TAG_NIGHTLY }; enablePublishing(config, branch, info, options); } else if (isStableBranch(branch)) { - const tag = getTagForStableBranch(branch, options, logger); + const tag = getTagsForStableBranch(branch, options, logger); enablePublishing(config, branch, tag, options); } } catch (e) { diff --git a/.github/workflows/microsoft-pr.yml b/.github/workflows/microsoft-pr.yml index ef918ae5c07ac1..de02090c341d25 100644 --- a/.github/workflows/microsoft-pr.yml +++ b/.github/workflows/microsoft-pr.yml @@ -67,6 +67,7 @@ jobs: - name: Install dependencies run: yarn - name: Verify release config + id: prepublish run: | node .ado/scripts/prepublish-check.mjs --verbose --skip-auth --tag ${{ steps.config.outputs.publishTag }} @@ -74,6 +75,18 @@ jobs: run: | echo "Target branch: ${{ github.base_ref }}" yarn nx release --dry-run --verbose + + # Show what additional tags would be applied + if [ -n "${{ steps.prepublish.outputs.additionalTags }}" ]; then + echo "" + echo "=== Additional dist-tags that would be applied ===" + VERSION=$(jq -r '.version' ./packages/react-native/package.json) + IFS=',' read -ra TAGS <<< "${{ steps.prepublish.outputs.additionalTags }}" + for tag in "${TAGS[@]}"; do + echo " @react-native-macos/virtualized-lists@$VERSION -> $tag" + echo " react-native-macos@$VERSION -> $tag" + done + fi yarn-constraints: name: "Check Yarn Constraints" From 6bfef02696b8fd8ede00009944f9ec9692a94cec Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 14 Jan 2026 17:27:18 -0600 Subject: [PATCH 2/5] f --- .github/workflows/microsoft-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/microsoft-pr.yml b/.github/workflows/microsoft-pr.yml index de02090c341d25..3db4149af5eff9 100644 --- a/.github/workflows/microsoft-pr.yml +++ b/.github/workflows/microsoft-pr.yml @@ -56,7 +56,7 @@ jobs: - name: Read publish tag from nx.json id: config run: | - PUBLISH_TAG=$(jq -r '.release.version.generatorOptions.currentVersionResolverMetadata.tag' nx.json) + PUBLISH_TAG=$(jq -r '.release.version.versionActionsOptions.currentVersionResolverMetadata.tag' nx.json) echo "publishTag=$PUBLISH_TAG" >> $GITHUB_OUTPUT echo "Using publish tag from nx.json: $PUBLISH_TAG" - name: Configure git From c9835c3609b85a8ea867a5f3ea6f450bc57f7c01 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Thu, 15 Jan 2026 13:26:43 -0600 Subject: [PATCH 3/5] PR feedback --- .ado/jobs/npm-publish.yml | 28 +------ .ado/scripts/apply-additional-tags.mjs | 102 +++++++++++++++++++++++++ .ado/scripts/prepublish-check.mjs | 16 ++-- .github/workflows/microsoft-pr.yml | 11 +-- 4 files changed, 111 insertions(+), 46 deletions(-) create mode 100644 .ado/scripts/apply-additional-tags.mjs diff --git a/.ado/jobs/npm-publish.yml b/.ado/jobs/npm-publish.yml index ded358f89342a3..502f4fd16f6611 100644 --- a/.ado/jobs/npm-publish.yml +++ b/.ado/jobs/npm-publish.yml @@ -49,16 +49,7 @@ jobs: yarn nx release --dry-run --verbose # Show what additional tags would be applied - if [ -n "$(additionalTags)" ]; then - echo "" - echo "=== Additional dist-tags that would be applied ===" - VERSION=$(jq -r '.version' ./packages/react-native/package.json) - IFS=',' read -ra TAGS <<< "$(additionalTags)" - for tag in "${TAGS[@]}"; do - echo " @react-native-macos/virtualized-lists@$VERSION -> $tag" - echo " react-native-macos@$VERSION -> $tag" - done - fi + node .ado/scripts/apply-additional-tags.mjs --tags "$(additionalTags)" --dry-run displayName: Version and publish packages (dry run) condition: and(succeeded(), ne(variables['publish_react_native_macos'], '1')) @@ -96,22 +87,7 @@ jobs: condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1')) - script: | - set -eox pipefail - if [[ -z "$(additionalTags)" ]]; then - echo "No additional tags to apply" - exit 0 - fi - - # Get the version from package.json - VERSION=$(jq -r '.version' ./packages/react-native/package.json) - - # Apply additional tags - IFS=',' read -ra TAGS <<< "$(additionalTags)" - for tag in "${TAGS[@]}"; do - echo "Adding dist-tag '$tag' to packages" - npm dist-tag add "@react-native-macos/virtualized-lists@$VERSION" "$tag" --registry https://registry.npmjs.org/ --//registry.npmjs.org/:_authToken=$(npmAuthToken) - npm dist-tag add "react-native-macos@$VERSION" "$tag" --registry https://registry.npmjs.org/ --//registry.npmjs.org/:_authToken=$(npmAuthToken) - done + node .ado/scripts/apply-additional-tags.mjs --tags "$(additionalTags)" --token "$(npmAuthToken)" displayName: Apply additional dist-tags condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1')) diff --git a/.ado/scripts/apply-additional-tags.mjs b/.ado/scripts/apply-additional-tags.mjs new file mode 100644 index 00000000000000..73bb0f35829f4a --- /dev/null +++ b/.ado/scripts/apply-additional-tags.mjs @@ -0,0 +1,102 @@ +// @ts-check +import { spawnSync } from "node:child_process"; +import * as fs from "node:fs"; +import * as util from "node:util"; + +/** + * Apply additional dist-tags to published packages + * Usage: node apply-additional-tags.mjs --tags --token + * node apply-additional-tags.mjs --tags --dry-run + * Where tags is a comma-separated list of tags (e.g., "next,v0.79-stable") + */ + +const registry = "https://registry.npmjs.org/"; +const packages = [ + "@react-native-macos/virtualized-lists", + "react-native-macos", +]; + +/** + * @typedef {{ + * tags?: string; + * token?: string; + * "dry-run"?: boolean; + * }} Options; + */ + +/** + * @param {Options} options + * @returns {number} + */ +function main({ tags, token, "dry-run": dryRun }) { + if (!tags) { + console.log("No additional tags to apply"); + return 0; + } + + if (!dryRun && !token) { + console.error("Error: npm auth token is required (use --dry-run to preview)"); + return 1; + } + + const packageJson = JSON.parse( + fs.readFileSync("./packages/react-native/package.json", "utf-8") + ); + const version = packageJson.version; + + if (dryRun) { + console.log(""); + console.log("=== Additional dist-tags that would be applied ==="); + for (const tag of tags.split(",")) { + for (const pkg of packages) { + console.log(` ${pkg}@${version} -> ${tag}`); + } + } + return 0; + } + + for (const tag of tags.split(",")) { + for (const pkg of packages) { + console.log(`Adding dist-tag '${tag}' to ${pkg}@${version}`); + const result = spawnSync( + "npm", + [ + "dist-tag", + "add", + `${pkg}@${version}`, + tag, + "--registry", + registry, + `--//registry.npmjs.org/:_authToken=${token}`, + ], + { stdio: "inherit", shell: true } + ); + + if (result.status !== 0) { + console.error(`Failed to add dist-tag '${tag}' to ${pkg}@${version}`); + return 1; + } + } + } + + return 0; +} + +const { values } = util.parseArgs({ + args: process.argv.slice(2), + options: { + tags: { + type: "string", + }, + token: { + type: "string", + }, + "dry-run": { + type: "boolean", + default: false, + }, + }, + strict: true, +}); + +process.exitCode = main(values); diff --git a/.ado/scripts/prepublish-check.mjs b/.ado/scripts/prepublish-check.mjs index cf6ca306ce02b0..7e3b5dcd7e8061 100644 --- a/.ado/scripts/prepublish-check.mjs +++ b/.ado/scripts/prepublish-check.mjs @@ -323,17 +323,14 @@ function getTagsForStableBranch(branch, { tag }, log) { } // Publishing a release candidate - if (currentVersion > latestVersion) { - log(`Expected npm tags: ${NPM_TAG_NEXT}`); + // currentVersion > latestVersion + log(`Expected npm tags: ${NPM_TAG_NEXT}`); - if (currentVersion < nextVersion) { - throw new Error(`Current version cannot be a release candidate because it is too old: ${currentVersion} < ${nextVersion}`); - } - - return { npmTags: [NPM_TAG_NEXT], prerelease: "rc" }; + if (currentVersion < nextVersion) { + throw new Error(`Current version cannot be a release candidate because it is too old: ${currentVersion} < ${nextVersion}`); } - throw new Error(`Unexpected state: currentVersion=${currentVersion}, latestVersion=${latestVersion}, nextVersion=${nextVersion}, tag=${tag}`); + return { npmTags: [NPM_TAG_NEXT], prerelease: "rc" }; } /** @@ -349,8 +346,7 @@ function enablePublishing(config, currentBranch, { npmTags, prerelease }, option const errors = []; const { defaultBase, release } = config; - const primaryTag = npmTags[0]; - const additionalTags = npmTags.slice(1); + const [primaryTag, ...additionalTags] = npmTags; // `defaultBase` determines what we diff against when looking for tags or // released version and must therefore be set to either the main branch or one diff --git a/.github/workflows/microsoft-pr.yml b/.github/workflows/microsoft-pr.yml index 3db4149af5eff9..2c1b4500b3ccf4 100644 --- a/.github/workflows/microsoft-pr.yml +++ b/.github/workflows/microsoft-pr.yml @@ -77,16 +77,7 @@ jobs: yarn nx release --dry-run --verbose # Show what additional tags would be applied - if [ -n "${{ steps.prepublish.outputs.additionalTags }}" ]; then - echo "" - echo "=== Additional dist-tags that would be applied ===" - VERSION=$(jq -r '.version' ./packages/react-native/package.json) - IFS=',' read -ra TAGS <<< "${{ steps.prepublish.outputs.additionalTags }}" - for tag in "${TAGS[@]}"; do - echo " @react-native-macos/virtualized-lists@$VERSION -> $tag" - echo " react-native-macos@$VERSION -> $tag" - done - fi + node .ado/scripts/apply-additional-tags.mjs --tags "${{ steps.prepublish.outputs.additionalTags }}" --dry-run yarn-constraints: name: "Check Yarn Constraints" From d8e3cefe644e3695013803684cb15da3b3bd9837 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Thu, 15 Jan 2026 13:34:18 -0600 Subject: [PATCH 4/5] always output tags --- .ado/scripts/prepublish-check.mjs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.ado/scripts/prepublish-check.mjs b/.ado/scripts/prepublish-check.mjs index 7e3b5dcd7e8061..e2ca54f8234f42 100644 --- a/.ado/scripts/prepublish-check.mjs +++ b/.ado/scripts/prepublish-check.mjs @@ -387,20 +387,20 @@ function enablePublishing(config, currentBranch, { npmTags, prerelease }, option verifyNpmAuth(); } + // Output additional tags as pipeline/workflow variable + if (additionalTags.length > 0) { + const tagsValue = additionalTags.join(","); + // Azure Pipelines + console.log(`##vso[task.setvariable variable=additionalTags]${tagsValue}`); + // GitHub Actions + if (process.env["GITHUB_OUTPUT"]) { + fs.appendFileSync(process.env["GITHUB_OUTPUT"], `additionalTags=${tagsValue}\n`); + } + } + // Don't enable publishing in PRs if (!getTargetBranch()) { enablePublishingOnAzurePipelines(); - - // Output additional tags as pipeline/workflow variable - if (additionalTags.length > 0) { - const tagsValue = additionalTags.join(","); - // Azure Pipelines - console.log(`##vso[task.setvariable variable=additionalTags]${tagsValue}`); - // GitHub Actions - if (process.env["GITHUB_OUTPUT"]) { - fs.appendFileSync(process.env["GITHUB_OUTPUT"], `additionalTags=${tagsValue}\n`); - } - } } } From b46a81f7c41c0e679ee267c7d6c259b48db8f9d3 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Fri, 16 Jan 2026 10:54:19 -0600 Subject: [PATCH 5/5] remove the "v" prefix --- .ado/scripts/prepublish-check.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.ado/scripts/prepublish-check.mjs b/.ado/scripts/prepublish-check.mjs index e2ca54f8234f42..013ae53b22b491 100644 --- a/.ado/scripts/prepublish-check.mjs +++ b/.ado/scripts/prepublish-check.mjs @@ -295,14 +295,14 @@ function getTagsForStableBranch(branch, { tag }, log) { // Patching latest version if (currentVersion === latestVersion) { - const versionTag = "v" + branch; + const versionTag = branch; log(`Expected npm tags: latest, ${versionTag}`); return { npmTags: ["latest", versionTag] }; } // Demoting or patching an older stable version if (currentVersion < latestVersion) { - const npmTag = "v" + branch; + const npmTag = branch; log(`Expected npm tags: ${npmTag}`); return { npmTags: [npmTag] }; } @@ -310,7 +310,7 @@ function getTagsForStableBranch(branch, { tag }, log) { // Publishing a new latest version if (tag === "latest") { // When promoting to latest, also add the version-specific stable tag - const versionTag = "v" + branch; + const versionTag = branch; const npmTags = ["latest", versionTag]; // Also add "next" tag if the current next version is lower