diff --git a/.ado/apple-pr.yml b/.ado/apple-pr.yml index bb345df8ee62f3..417834ed149f89 100644 --- a/.ado/apple-pr.yml +++ b/.ado/apple-pr.yml @@ -27,8 +27,6 @@ stages: jobs: - template: /.ado/jobs/test-javascript.yml@self - - template: /.ado/jobs/npm-publish-dry-run.yml@self - # - stage: Integration # dependsOn: [] # jobs: diff --git a/.ado/jobs/npm-publish-dry-run.yml b/.ado/jobs/npm-publish-dry-run.yml deleted file mode 100644 index 66c14a3cfaaf61..00000000000000 --- a/.ado/jobs/npm-publish-dry-run.yml +++ /dev/null @@ -1,14 +0,0 @@ -jobs: -- job: NPMPublishDryRun - displayName: NPM Publish Dry Run - pool: - vmImage: $(VmImageApple) - timeoutInMinutes: 60 # how long to run the job before automatically cancelling - cancelTimeoutInMinutes: 5 # how much time to give 'run always even if cancelled tasks' before killing them - steps: - - checkout: self # self represents the repo where the initial Pipelines YAML file was found - clean: true # whether to fetch clean each time - fetchFilter: blob:none # partial clone for faster clones while maintaining history - persistCredentials: true # set to 'true' to leave the OAuth token in the Git config after the initial fetch - - - template: /.ado/templates/npm-publish-steps.yml@self diff --git a/.ado/jobs/npm-publish.yml b/.ado/jobs/npm-publish.yml new file mode 100644 index 00000000000000..c43af09e3f843b --- /dev/null +++ b/.ado/jobs/npm-publish.yml @@ -0,0 +1,72 @@ +jobs: +- job: NPMPublish + displayName: NPM Publish + pool: + name: cxeiss-ubuntu-20-04-large + image: cxe-ubuntu-20-04-1es-pt + os: linux + variables: + - name: BUILDSECMON_OPT_IN + value: true + timeoutInMinutes: 90 + cancelTimeoutInMinutes: 5 + templateContext: + outputs: + - output: pipelineArtifact + targetPath: $(System.DefaultWorkingDirectory) + artifactName: github-npm-js-publish + steps: + - checkout: self + clean: true + fetchFilter: blob:none + persistCredentials: true + + - template: /.ado/templates/configure-git.yml@self + + - script: | + PUBLISH_TAG=$(jq -r '.release.version.generatorOptions.currentVersionResolverMetadata.tag' nx.json) + echo "##vso[task.setvariable variable=publishTag]$PUBLISH_TAG" + echo "Using publish tag from nx.json: $PUBLISH_TAG" + displayName: Read publish tag from nx.json + + - script: | + yarn install + displayName: Install npm dependencies + + - script: | + node .ado/scripts/prepublish-check.mjs --verbose --skip-auth --tag $(publishTag) + displayName: Verify release config + + - script: | + echo Target branch: $(System.PullRequest.TargetBranch) + yarn nx release --dry-run --verbose + displayName: Version and publish packages (dry run) + condition: and(succeeded(), ne(variables['publish_react_native_macos'], '1')) + + # Disable Nightly publishing on the main branch + - ${{ if endsWith(variables['Build.SourceBranchName'], '-stable') }}: + - script: | + echo "//registry.npmjs.org/:_authToken=$(npmAuthToken)" > ~/.npmrc + node .ado/scripts/prepublish-check.mjs --verbose --tag $(publishTag) + displayName: Set and validate npm auth + condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1')) + + - script: | + git switch $(Build.SourceBranchName) + yarn nx release --skip-publish --verbose + env: + GITHUB_TOKEN: $(githubAuthToken) + displayName: Version Packages and Github Release + condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1')) + + - script: | + if [[ -f .rnm-publish ]]; then + yarn nx release publish --tag ${{ parameters['publishTag'] }} --excludeTaskDependencies + fi + displayName: Publish packages + condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1')) + + - script: | + rm -f ~/.npmrc + displayName: Remove npmrc if it exists + condition: always() diff --git a/.ado/scripts/npmAddUser.mjs b/.ado/scripts/npmAddUser.mjs new file mode 100644 index 00000000000000..3a62339ab9400a --- /dev/null +++ b/.ado/scripts/npmAddUser.mjs @@ -0,0 +1,31 @@ +#!/usr/bin/env node +// @ts-check + +import * as assert from "node:assert/strict"; +import { exec } from "node:child_process"; + +const { [2]: username, [3]: password, [4]: email, [5]: registry } = process.argv; +assert.ok(username, "Please specify username"); +assert.ok(password, "Please specify password"); +assert.ok(email, "Please specify email"); + +const child = exec(`npm adduser${registry ? ` --registry ${registry}` : ""}`); +assert.ok(child.stdout, "Missing stdout on child process"); + +child.stdout.on("data", d => { + assert.ok(child.stdin, "Missing stdin on child process"); + + process.stdout.write(d); + process.stdout.write("\n"); + + const data = d.toString(); + if (data.match(/username/i)) { + child.stdin.write(username + "\n"); + } else if (data.match(/password/i)) { + child.stdin.write(password + "\n"); + } else if (data.match(/email/i)) { + child.stdin.write(email + "\n"); + } else if (data.match(/logged in as/i)) { + child.stdin.end(); + } +}); diff --git a/.ado/scripts/prepublish-check.mjs b/.ado/scripts/prepublish-check.mjs index 35222585c728de..17525865aed0a5 100644 --- a/.ado/scripts/prepublish-check.mjs +++ b/.ado/scripts/prepublish-check.mjs @@ -3,9 +3,9 @@ import { spawnSync } from "node:child_process"; import * as fs from "node:fs"; import * as util from "node:util"; -const ADO_PUBLISH_PIPELINE = ".ado/templates/npm-publish-steps.yml"; const NX_CONFIG_FILE = "nx.json"; +const NPM_DEFEAULT_REGISTRY = "https://registry.npmjs.org/" const NPM_TAG_NEXT = "next"; const NPM_TAG_NIGHTLY = "nightly"; const RNMACOS_LATEST = "react-native-macos@latest"; @@ -21,8 +21,18 @@ const RNMACOS_NEXT = "react-native-macos@next"; * }; * }; * }} NxConfig; - * @typedef {{ tag?: string; update?: boolean; verbose?: boolean; }} Options; - * @typedef {{ npmTag: string; prerelease?: string; isNewTag?: boolean; }} TagInfo; + * @typedef {{ + * "mock-branch"?: string; + * "skip-auth"?: boolean; + * tag?: string; + * update?: boolean; + * verbose?: boolean; + * }} Options; + * @typedef {{ + * npmTag: string; + * prerelease?: string; + * isNewTag?: boolean; + * }} TagInfo; */ /** @@ -80,6 +90,38 @@ function loadNxConfig(configFile) { return JSON.parse(nx); } +function verifyNpmAuth(registry = NPM_DEFEAULT_REGISTRY) { + const npmErrorRegex = /npm error code (\w+)/; + const spawnOptions = { + stdio: /** @type {const} */ ("pipe"), + shell: true, + windowsVerbatimArguments: true, + }; + + const whoamiArgs = ["whoami", "--registry", registry]; + const whoami = spawnSync("npm", whoamiArgs, spawnOptions); + if (whoami.status !== 0) { + const error = whoami.stderr.toString(); + const m = error.match(npmErrorRegex); + switch (m && m[1]) { + case "EINVALIDNPMTOKEN": + throw new Error(`Invalid auth token for npm registry: ${registry}`); + case "ENEEDAUTH": + throw new Error(`Missing auth token for npm registry: ${registry}`); + default: + throw new Error(error); + } + } + + const tokenArgs = ["token", "list", "--registry", registry]; + const token = spawnSync("npm", tokenArgs, spawnOptions); + if (token.status !== 0) { + const error = token.stderr.toString(); + const m = error.match(npmErrorRegex); + throw new Error(m ? `Auth token for '${registry}' returned error code ${m[1]}` : error); + } +} + /** * Returns a numerical value for a given version string. * @param {string} version @@ -91,17 +133,39 @@ function versionToNumber(version) { } /** - * Returns the currently checked out branch. Note that this function prefers - * predefined CI environment variables over local clone. + * Returns the target branch name. If not targetting any branches (e.g., when + * executing this script locally), `undefined` is returned. + * @returns {string | undefined} + */ +function getTargetBranch() { + // https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables-devops-services + const adoTargetBranchName = process.env["SYSTEM_PULLREQUEST_TARGETBRANCH"]; + return adoTargetBranchName?.replace(/^refs\/heads\//, ""); +} + +/** + * Returns the current branch name. In a pull request, the target branch name is + * returned. + * @param {Options} options * @returns {string} */ -function getCurrentBranch() { +function getCurrentBranch(options) { + const adoTargetBranchName = getTargetBranch(); + if (adoTargetBranchName) { + return adoTargetBranchName; + } + // https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables-devops-services const adoSourceBranchName = process.env["BUILD_SOURCEBRANCHNAME"]; if (adoSourceBranchName) { return adoSourceBranchName.replace(/^refs\/heads\//, ""); } + const { "mock-branch": mockBranch } = options; + if (mockBranch) { + return mockBranch; + } + // Depending on how the repo was cloned, HEAD may not exist. We only use this // method as fallback. const { stdout } = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"]); @@ -177,31 +241,15 @@ function getTagForStableBranch(branch, { tag }, log) { return { npmTag: NPM_TAG_NEXT, prerelease: "rc" }; } -/** - * @param {string} file - * @param {string} tag - * @returns {void} - */ -function verifyPublishPipeline(file, tag) { - const data = fs.readFileSync(file, { encoding: "utf-8" }); - const m = data.match(/publishTag: '(latest|next|nightly|v\d+\.\d+-stable)'/); - if (!m) { - throw new Error(`${file}: Could not find npm publish tag`); - } - - if (m[1] !== tag) { - throw new Error(`${file}: 'publishTag' must be set to '${tag}'`); - } -} - /** * Verifies the configuration and enables publishing on CI. * @param {NxConfig} config * @param {string} currentBranch * @param {TagInfo} tag + * @param {Options} options * @returns {asserts config is NxConfig["release"]} */ -function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNewTag }) { +function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNewTag }, options) { /** @type {string[]} */ const errors = []; @@ -244,7 +292,7 @@ function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNe generatorOptions.fallbackCurrentVersionResolver = "disk"; } } else if (typeof generatorOptions.fallbackCurrentVersionResolver === "string") { - errors.push("'release.version.generatorOptions.fallbackCurrentVersionResolver' must be unset"); + errors.push("'release.version.generatorOptions.fallbackCurrentVersionResolver' must be removed"); generatorOptions.fallbackCurrentVersionResolver = undefined; } @@ -253,8 +301,16 @@ function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNe throw new Error("Nx Release is not correctly configured for the current branch"); } - verifyPublishPipeline(ADO_PUBLISH_PIPELINE, tag); - enablePublishingOnAzurePipelines(); + if (options["skip-auth"]) { + info("Skipped npm auth validation"); + } else { + verifyNpmAuth(); + } + + // Don't enable publishing in PRs + if (!getTargetBranch()) { + enablePublishingOnAzurePipelines(); + } } /** @@ -262,7 +318,7 @@ function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNe * @returns {number} */ function main(options) { - const branch = getCurrentBranch(); + const branch = getCurrentBranch(options); if (!branch) { error("Could not get current branch"); return 1; @@ -273,10 +329,11 @@ function main(options) { const config = loadNxConfig(NX_CONFIG_FILE); try { if (isMainBranch(branch)) { - enablePublishing(config, branch, { npmTag: NPM_TAG_NIGHTLY, prerelease: NPM_TAG_NIGHTLY }); + const info = { npmTag: NPM_TAG_NIGHTLY, prerelease: NPM_TAG_NIGHTLY }; + enablePublishing(config, branch, info, options); } else if (isStableBranch(branch)) { const tag = getTagForStableBranch(branch, options, logger); - enablePublishing(config, branch, tag); + enablePublishing(config, branch, tag, options); } } catch (e) { if (options.update) { @@ -296,6 +353,13 @@ function main(options) { const { values } = util.parseArgs({ args: process.argv.slice(2), options: { + "mock-branch": { + type: "string", + }, + "skip-auth": { + type: "boolean", + default: false, + }, tag: { type: "string", default: NPM_TAG_NEXT, diff --git a/.ado/templates/apple-tools-setup.yml b/.ado/templates/apple-tools-setup.yml index f001e1b99bdcd8..5ceec73192883f 100644 --- a/.ado/templates/apple-tools-setup.yml +++ b/.ado/templates/apple-tools-setup.yml @@ -1,7 +1,7 @@ steps: - - task: NodeTool@0 + - task: UseNode@1 inputs: - versionSpec: '23.x' + version: '23.x' - script: | brew bundle --file .ado/Brewfile diff --git a/.ado/templates/npm-publish-steps.yml b/.ado/templates/npm-publish-steps.yml deleted file mode 100644 index 35df81f7834868..00000000000000 --- a/.ado/templates/npm-publish-steps.yml +++ /dev/null @@ -1,39 +0,0 @@ -parameters: - # If this is a new stable branch, change `publishTag` to `latest` when going stable - publishTag: 'v0.76-stable' - -steps: - - template: /.ado/templates/configure-git.yml@self - - - script: | - yarn install - displayName: Install npm dependencies - - - script: | - node .ado/scripts/prepublish-check.mjs --verbose --tag ${{ parameters['publishTag'] }} - displayName: Verify release config - - - script: | - echo Target branch: $(System.PullRequest.TargetBranch) - yarn nx release --dry-run --verbose - displayName: Version and publish packages (dry run) - condition: and(succeeded(), ne(variables['publish_react_native_macos'], '1')) - - - script: | - git switch $(Build.SourceBranchName) - yarn nx release --skip-publish --verbose - env: - GITHUB_TOKEN: $(githubAuthToken) - displayName: Version Packages and Github Release - condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1')) - - - script: | - echo "//registry.npmjs.org/:_authToken=$(npmAuthToken)" > ~/.npmrc - yarn nx release publish --tag ${{ parameters['publishTag'] }} --excludeTaskDependencies - displayName: Publish packages - condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1')) - - - script: | - rm -f ~/.npmrc - displayName: Remove npmrc if it exists - condition: always() diff --git a/.github/workflows/microsoft-npm-publish-dry-run.yml b/.github/workflows/microsoft-npm-publish-dry-run.yml new file mode 100644 index 00000000000000..6bf7d34cf5c08e --- /dev/null +++ b/.github/workflows/microsoft-npm-publish-dry-run.yml @@ -0,0 +1,39 @@ +name: NPM Publish Dry Run + +on: + pull_request: + branches: + - main + - '*-stable' + paths-ignore: + - '*.md' + +jobs: + npm-publish-dry-run: + name: "NPM Publish (Dry Run)" + runs-on: ubuntu-24.04 + env: + PUBLISH_TAG: 'latest' + steps: + - uses: actions/checkout@v4 + with: + filter: blob:none + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Configure git + run: | + git config --global user.email "53619745+rnbot@users.noreply.github.com" + git config --global user.name "React-Native Bot" + git remote set-url origin https://rnbot:${{ secrets.GITHUB_TOKEN }}@github.com/microsoft/react-native-macos + - name: Install dependencies + run: yarn + - name: Verify release config + run: | + node .ado/scripts/prepublish-check.mjs --verbose --tag ${{ env.PUBLISH_TAG }} + + - name: Version and publish packages (dry run) + run: | + echo "Target branch: ${{ github.base_ref }}" + yarn nx release --dry-run --verbose diff --git a/.github/workflows/microsoft-pr.yml b/.github/workflows/microsoft-pr.yml new file mode 100644 index 00000000000000..50fed45a7ef32e --- /dev/null +++ b/.github/workflows/microsoft-pr.yml @@ -0,0 +1,65 @@ +name: PR + +on: + pull_request: + types: [opened, synchronize, edited] + branches: [ "main", "*-stable", "release/*" ] + +concurrency: + # Ensure single build of a pull request. `main` should not be affected. + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + lint-commit: + name: "Lint PR title" + permissions: {} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + filter: blob:none + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: '22' + # We lint the PR title instead of the commit message to avoid script injection attacks. + # Using environment variables prevents potential security vulnerabilities as described in: + # https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#example-of-a-script-injection-attack + - name: Lint PR title + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + echo "$PR_TITLE" | npx @rnx-kit/commitlint-lite@2.0.0 + npm-publish-dry-run: + name: "NPM Publish (Dry Run)" + permissions: {} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + filter: blob:none + fetch-depth: 0 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Read publish tag from nx.json + id: config + run: | + PUBLISH_TAG=$(jq -r '.release.version.generatorOptions.currentVersionResolverMetadata.tag' nx.json) + echo "publishTag=$PUBLISH_TAG" >> $GITHUB_OUTPUT + echo "Using publish tag from nx.json: $PUBLISH_TAG" + - name: Configure git + run: | + git config --global user.email "53619745+rnbot@users.noreply.github.com" + git config --global user.name "React-Native Bot" + git remote set-url origin https://rnbot:${{ secrets.GITHUB_TOKEN }}@github.com/microsoft/react-native-macos + - name: Install dependencies + run: yarn + - name: Verify release config + run: | + node .ado/scripts/prepublish-check.mjs --verbose --skip-auth --tag ${{ steps.config.outputs.publishTag }} + - name: Version and publish packages (dry run) + run: | + echo "Target branch: ${{ github.base_ref }}" + yarn nx release --dry-run --verbose diff --git a/.github/workflows/npm-publish-dry-run.yml b/.github/workflows/npm-publish-dry-run.yml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/.nx/version-plans/version-plan-1751411273803.md b/.nx/version-plans/version-plan-1751411273803.md new file mode 100644 index 00000000000000..9ce72849dfe060 --- /dev/null +++ b/.nx/version-plans/version-plan-1751411273803.md @@ -0,0 +1,6 @@ +--- +react-native-macos: patch +'@react-native-mac/virtualized-lists': patch +--- + +new patch release diff --git a/docs/Releases.md b/docs/Releases.md index e8d4ed268a54be..c70a7a795be474 100644 --- a/docs/Releases.md +++ b/docs/Releases.md @@ -72,13 +72,7 @@ git push -u origin HEAD ## Marking a release candidate stable -Prepare the publish pipeline: - -```sh -sed -i '' "s/publishTag: 'next'/publishTag: 'latest'/" .ado/templates/npm-publish.yml -``` - -Update the release configuration: +Prepare the release configuration: ```sh node .ado/scripts/prepublish-check.mjs --tag latest --update