diff --git a/.ado/jobs/npm-publish.yml b/.ado/jobs/npm-publish.yml index 01b83311b41c0f..502f4fd16f6611 100644 --- a/.ado/jobs/npm-publish.yml +++ b/.ado/jobs/npm-publish.yml @@ -47,6 +47,9 @@ jobs: - script: | echo Target branch: $(System.PullRequest.TargetBranch) yarn nx release --dry-run --verbose + + # Show what additional tags would be applied + 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')) @@ -83,6 +86,11 @@ jobs: displayName: Publish packages condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1')) + - script: | + 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')) + - script: | if [ "$(USE_YARN_FOR_PUBLISH)" = "true" ]; then echo "Cleaning up yarn npm configuration" 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 4ea23f41d8960d..013ae53b22b491 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,57 @@ 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 = 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 }; + const npmTag = branch; + 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 = 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}`); + // 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 { npmTag: NPM_TAG_NEXT, prerelease: "rc" }; + return { npmTags: [NPM_TAG_NEXT], prerelease: "rc" }; } /** @@ -330,11 +341,12 @@ 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, ...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 @@ -358,23 +370,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) { @@ -388,6 +387,17 @@ function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNe 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(); @@ -410,10 +420,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..2c1b4500b3ccf4 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 @@ -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,9 @@ jobs: run: | echo "Target branch: ${{ github.base_ref }}" yarn nx release --dry-run --verbose + + # Show what additional tags would be applied + node .ado/scripts/apply-additional-tags.mjs --tags "${{ steps.prepublish.outputs.additionalTags }}" --dry-run yarn-constraints: name: "Check Yarn Constraints" diff --git a/.nx/version-plans/version-plan-1768362735076.md b/.nx/version-plans/version-plan-1768362735076.md new file mode 100644 index 00000000000000..4af7c1c39c07bb --- /dev/null +++ b/.nx/version-plans/version-plan-1768362735076.md @@ -0,0 +1,5 @@ +--- +__default__: patch +--- + +Release 0.81.0 diff --git a/nx.json b/nx.json index ac8c7e7b71cab3..06f7fc86f30775 100644 --- a/nx.json +++ b/nx.json @@ -21,9 +21,8 @@ "versionActionsOptions": { "currentVersionResolver": "registry", "currentVersionResolverMetadata": { - "tag": "next" - }, - "preid": "rc" + "tag": "latest" + } }, "useLegacyVersioning": false }