Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 6 additions & 17 deletions .ado/scripts/npmAddUser.mjs
Original file line number Diff line number Diff line change
@@ -1,30 +1,19 @@
#!/usr/bin/env node
// @ts-check

import * as assert from "node:assert/strict";
import { exec } from "node:child_process";

/**
* @template T
* @param {T} arg
* @param {string} message
* @returns {asserts arg is NonNullable<T>}
*/
function assert(arg, message) {
if (!arg) {
throw new Error(message);
}
}

const { [2]: username, [3]: password, [4]: email, [5]: registry } = process.argv;
assert(username, "Please specify username");
assert(password, "Please specify password");
assert(email, "Please specify email");
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(child.stdout, "Missing stdout on child process");
assert.ok(child.stdout, "Missing stdout on child process");

child.stdout.on("data", d => {
assert(child.stdin, "Missing stdin on child process");
assert.ok(child.stdin, "Missing stdin on child process");

process.stdout.write(d);
process.stdout.write("\n");
Expand Down
106 changes: 95 additions & 11 deletions .ado/scripts/prepublish-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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";
Expand All @@ -21,8 +22,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;
*/

/**
Expand Down Expand Up @@ -80,6 +91,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
Expand All @@ -91,17 +134,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"]);
Expand Down Expand Up @@ -199,9 +264,10 @@ function verifyPublishPipeline(file, tag) {
* @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 = [];

Expand Down Expand Up @@ -244,7 +310,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;
}

Expand All @@ -253,16 +319,26 @@ function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNe
throw new Error("Nx Release is not correctly configured for the current branch");
}

if (options["skip-auth"]) {
info("Skipped npm auth validation");
} else {
verifyNpmAuth();
}

verifyPublishPipeline(ADO_PUBLISH_PIPELINE, tag);
enablePublishingOnAzurePipelines();

// Don't enable publishing in PRs
if (!getTargetBranch()) {
enablePublishingOnAzurePipelines();
}
}

/**
* @param {Options} options
* @returns {number}
*/
function main(options) {
const branch = getCurrentBranch();
const branch = getCurrentBranch(options);
if (!branch) {
error("Could not get current branch");
return 1;
Expand All @@ -273,10 +349,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) {
Expand All @@ -296,6 +373,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,
Expand Down
4 changes: 2 additions & 2 deletions .ado/templates/apple-tools-setup.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
steps:
- task: NodeTool@0
- task: UseNode@1
inputs:
versionSpec: '23.x'
version: '23.x'

- script: |
brew bundle --file .ado/Brewfile
Expand Down
9 changes: 7 additions & 2 deletions .ado/templates/npm-publish-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ steps:
displayName: Install npm dependencies

- script: |
node .ado/scripts/prepublish-check.mjs --verbose --tag ${{ parameters['publishTag'] }}
node .ado/scripts/prepublish-check.mjs --verbose --skip-auth --tag ${{ parameters['publishTag'] }}
displayName: Verify release config

- script: |
Expand All @@ -26,6 +26,12 @@ steps:

# 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 ${{ parameters['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
Expand All @@ -35,7 +41,6 @@ steps:
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))

- script: |
echo "//registry.npmjs.org/:_authToken=$(npmAuthToken)" > ~/.npmrc
yarn nx release publish --excludeTaskDependencies
displayName: Publish packages
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))
Expand Down
Loading