Skip to content

Commit 2b400fe

Browse files
authored
chore: verify npm auth token in prepublish script (#2436)
1 parent a8de281 commit 2b400fe

File tree

4 files changed

+110
-32
lines changed

4 files changed

+110
-32
lines changed

.ado/scripts/npmAddUser.mjs

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,19 @@
11
#!/usr/bin/env node
22
// @ts-check
33

4+
import * as assert from "node:assert/strict";
45
import { exec } from "node:child_process";
56

6-
/**
7-
* @template T
8-
* @param {T} arg
9-
* @param {string} message
10-
* @returns {asserts arg is NonNullable<T>}
11-
*/
12-
function assert(arg, message) {
13-
if (!arg) {
14-
throw new Error(message);
15-
}
16-
}
17-
187
const { [2]: username, [3]: password, [4]: email, [5]: registry } = process.argv;
19-
assert(username, "Please specify username");
20-
assert(password, "Please specify password");
21-
assert(email, "Please specify email");
8+
assert.ok(username, "Please specify username");
9+
assert.ok(password, "Please specify password");
10+
assert.ok(email, "Please specify email");
2211

2312
const child = exec(`npm adduser${registry ? ` --registry ${registry}` : ""}`);
24-
assert(child.stdout, "Missing stdout on child process");
13+
assert.ok(child.stdout, "Missing stdout on child process");
2514

2615
child.stdout.on("data", d => {
27-
assert(child.stdin, "Missing stdin on child process");
16+
assert.ok(child.stdin, "Missing stdin on child process");
2817

2918
process.stdout.write(d);
3019
process.stdout.write("\n");

.ado/scripts/prepublish-check.mjs

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as util from "node:util";
66
const ADO_PUBLISH_PIPELINE = ".ado/templates/npm-publish-steps.yml";
77
const NX_CONFIG_FILE = "nx.json";
88

9+
const NPM_DEFEAULT_REGISTRY = "https://registry.npmjs.org/"
910
const NPM_TAG_NEXT = "next";
1011
const NPM_TAG_NIGHTLY = "nightly";
1112
const RNMACOS_LATEST = "react-native-macos@latest";
@@ -21,8 +22,18 @@ const RNMACOS_NEXT = "react-native-macos@next";
2122
* };
2223
* };
2324
* }} NxConfig;
24-
* @typedef {{ tag?: string; update?: boolean; verbose?: boolean; }} Options;
25-
* @typedef {{ npmTag: string; prerelease?: string; isNewTag?: boolean; }} TagInfo;
25+
* @typedef {{
26+
* "mock-branch"?: string;
27+
* "skip-auth"?: boolean;
28+
* tag?: string;
29+
* update?: boolean;
30+
* verbose?: boolean;
31+
* }} Options;
32+
* @typedef {{
33+
* npmTag: string;
34+
* prerelease?: string;
35+
* isNewTag?: boolean;
36+
* }} TagInfo;
2637
*/
2738

2839
/**
@@ -80,6 +91,38 @@ function loadNxConfig(configFile) {
8091
return JSON.parse(nx);
8192
}
8293

94+
function verifyNpmAuth(registry = NPM_DEFEAULT_REGISTRY) {
95+
const npmErrorRegex = /npm error code (\w+)/;
96+
const spawnOptions = {
97+
stdio: /** @type {const} */ ("pipe"),
98+
shell: true,
99+
windowsVerbatimArguments: true,
100+
};
101+
102+
const whoamiArgs = ["whoami", "--registry", registry];
103+
const whoami = spawnSync("npm", whoamiArgs, spawnOptions);
104+
if (whoami.status !== 0) {
105+
const error = whoami.stderr.toString();
106+
const m = error.match(npmErrorRegex);
107+
switch (m && m[1]) {
108+
case "EINVALIDNPMTOKEN":
109+
throw new Error(`Invalid auth token for npm registry: ${registry}`);
110+
case "ENEEDAUTH":
111+
throw new Error(`Missing auth token for npm registry: ${registry}`);
112+
default:
113+
throw new Error(error);
114+
}
115+
}
116+
117+
const tokenArgs = ["token", "list", "--registry", registry];
118+
const token = spawnSync("npm", tokenArgs, spawnOptions);
119+
if (token.status !== 0) {
120+
const error = token.stderr.toString();
121+
const m = error.match(npmErrorRegex);
122+
throw new Error(m ? `Auth token for '${registry}' returned error code ${m[1]}` : error);
123+
}
124+
}
125+
83126
/**
84127
* Returns a numerical value for a given version string.
85128
* @param {string} version
@@ -91,17 +134,39 @@ function versionToNumber(version) {
91134
}
92135

93136
/**
94-
* Returns the currently checked out branch. Note that this function prefers
95-
* predefined CI environment variables over local clone.
137+
* Returns the target branch name. If not targetting any branches (e.g., when
138+
* executing this script locally), `undefined` is returned.
139+
* @returns {string | undefined}
140+
*/
141+
function getTargetBranch() {
142+
// https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables-devops-services
143+
const adoTargetBranchName = process.env["SYSTEM_PULLREQUEST_TARGETBRANCH"];
144+
return adoTargetBranchName?.replace(/^refs\/heads\//, "");
145+
}
146+
147+
/**
148+
* Returns the current branch name. In a pull request, the target branch name is
149+
* returned.
150+
* @param {Options} options
96151
* @returns {string}
97152
*/
98-
function getCurrentBranch() {
153+
function getCurrentBranch(options) {
154+
const adoTargetBranchName = getTargetBranch();
155+
if (adoTargetBranchName) {
156+
return adoTargetBranchName;
157+
}
158+
99159
// https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#build-variables-devops-services
100160
const adoSourceBranchName = process.env["BUILD_SOURCEBRANCHNAME"];
101161
if (adoSourceBranchName) {
102162
return adoSourceBranchName.replace(/^refs\/heads\//, "");
103163
}
104164

165+
const { "mock-branch": mockBranch } = options;
166+
if (mockBranch) {
167+
return mockBranch;
168+
}
169+
105170
// Depending on how the repo was cloned, HEAD may not exist. We only use this
106171
// method as fallback.
107172
const { stdout } = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
@@ -199,9 +264,10 @@ function verifyPublishPipeline(file, tag) {
199264
* @param {NxConfig} config
200265
* @param {string} currentBranch
201266
* @param {TagInfo} tag
267+
* @param {Options} options
202268
* @returns {asserts config is NxConfig["release"]}
203269
*/
204-
function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNewTag }) {
270+
function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNewTag }, options) {
205271
/** @type {string[]} */
206272
const errors = [];
207273

@@ -244,7 +310,7 @@ function enablePublishing(config, currentBranch, { npmTag: tag, prerelease, isNe
244310
generatorOptions.fallbackCurrentVersionResolver = "disk";
245311
}
246312
} else if (typeof generatorOptions.fallbackCurrentVersionResolver === "string") {
247-
errors.push("'release.version.generatorOptions.fallbackCurrentVersionResolver' must be unset");
313+
errors.push("'release.version.generatorOptions.fallbackCurrentVersionResolver' must be removed");
248314
generatorOptions.fallbackCurrentVersionResolver = undefined;
249315
}
250316

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

322+
if (options["skip-auth"]) {
323+
info("Skipped npm auth validation");
324+
} else {
325+
verifyNpmAuth();
326+
}
327+
256328
verifyPublishPipeline(ADO_PUBLISH_PIPELINE, tag);
257-
enablePublishingOnAzurePipelines();
329+
330+
// Don't enable publishing in PRs
331+
if (!getTargetBranch()) {
332+
enablePublishingOnAzurePipelines();
333+
}
258334
}
259335

260336
/**
261337
* @param {Options} options
262338
* @returns {number}
263339
*/
264340
function main(options) {
265-
const branch = getCurrentBranch();
341+
const branch = getCurrentBranch(options);
266342
if (!branch) {
267343
error("Could not get current branch");
268344
return 1;
@@ -273,10 +349,11 @@ function main(options) {
273349
const config = loadNxConfig(NX_CONFIG_FILE);
274350
try {
275351
if (isMainBranch(branch)) {
276-
enablePublishing(config, branch, { npmTag: NPM_TAG_NIGHTLY, prerelease: NPM_TAG_NIGHTLY });
352+
const info = { npmTag: NPM_TAG_NIGHTLY, prerelease: NPM_TAG_NIGHTLY };
353+
enablePublishing(config, branch, info, options);
277354
} else if (isStableBranch(branch)) {
278355
const tag = getTagForStableBranch(branch, options, logger);
279-
enablePublishing(config, branch, tag);
356+
enablePublishing(config, branch, tag, options);
280357
}
281358
} catch (e) {
282359
if (options.update) {
@@ -296,6 +373,13 @@ function main(options) {
296373
const { values } = util.parseArgs({
297374
args: process.argv.slice(2),
298375
options: {
376+
"mock-branch": {
377+
type: "string",
378+
},
379+
"skip-auth": {
380+
type: "boolean",
381+
default: false,
382+
},
299383
tag: {
300384
type: "string",
301385
default: NPM_TAG_NEXT,

.ado/templates/apple-tools-setup.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
steps:
2-
- task: NodeTool@0
2+
- task: UseNode@1
33
inputs:
4-
versionSpec: '23.x'
4+
version: '23.x'
55

66
- script: |
77
brew bundle --file .ado/Brewfile

.ado/templates/npm-publish-steps.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ steps:
1515
displayName: Install npm dependencies
1616
1717
- script: |
18-
node .ado/scripts/prepublish-check.mjs --verbose --tag ${{ parameters['publishTag'] }}
18+
node .ado/scripts/prepublish-check.mjs --verbose --skip-auth --tag ${{ parameters['publishTag'] }}
1919
displayName: Verify release config
2020
2121
- script: |
@@ -26,6 +26,12 @@ steps:
2626
2727
# Disable Nightly publishing on the main branch
2828
- ${{ if endsWith(variables['Build.SourceBranchName'], '-stable') }}:
29+
- script: |
30+
echo "//registry.npmjs.org/:_authToken=$(npmAuthToken)" > ~/.npmrc
31+
node .ado/scripts/prepublish-check.mjs --verbose --tag ${{ parameters['publishTag'] }}
32+
displayName: Set and validate npm auth
33+
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))
34+
2935
- script: |
3036
git switch $(Build.SourceBranchName)
3137
yarn nx release --skip-publish --verbose
@@ -35,7 +41,6 @@ steps:
3541
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))
3642
3743
- script: |
38-
echo "//registry.npmjs.org/:_authToken=$(npmAuthToken)" > ~/.npmrc
3944
yarn nx release publish --excludeTaskDependencies
4045
displayName: Publish packages
4146
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))

0 commit comments

Comments
 (0)