Skip to content

Commit 697e23e

Browse files
Saadnajmiclaude
andauthored
chore: migrate to changesets (#2839)
## Summary: `nx release` has proven to be a bit more cumbersome than we like. I just migrated FluentUI React Native to changesets (microsoft/fluentui-react-native#4003), let's do the same here. We can adopt some of the changes from #2807 , specifically around updating `prepublish-check.mjs` into an all-in-one script, now named, `configure-publish.mts`. ## Test Plan: There should be an updated --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b36cb68 commit 697e23e

File tree

16 files changed

+1513
-1777
lines changed

16 files changed

+1513
-1777
lines changed

.ado/jobs/npm-publish.yml

Lines changed: 10 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ jobs:
88
variables:
99
- name: BUILDSECMON_OPT_IN
1010
value: true
11-
- name: USE_YARN_FOR_PUBLISH
12-
value: false
13-
11+
1412
timeoutInMinutes: 90
1513
cancelTimeoutInMinutes: 5
1614
templateContext:
@@ -31,63 +29,25 @@ jobs:
3129

3230
- template: /.ado/templates/configure-git.yml@self
3331

34-
- script: |
35-
PUBLISH_TAG=$(jq -r '.release.version.versionActionsOptions.currentVersionResolverMetadata.tag' nx.json)
36-
if [ -z "$PUBLISH_TAG" ] || [ "$PUBLISH_TAG" = "null" ]; then
37-
echo "Error: Failed to read publish tag from nx.json"
38-
exit 1
39-
fi
40-
echo "##vso[task.setvariable variable=publishTag]$PUBLISH_TAG"
41-
echo "Using publish tag from nx.json: $PUBLISH_TAG"
42-
displayName: Read publish tag from nx.json
43-
4432
- script: |
4533
yarn install
4634
displayName: Install npm dependencies
4735
4836
- script: |
49-
node .ado/scripts/prepublish-check.mjs --verbose --skip-auth --tag $(publishTag)
37+
node .ado/scripts/configure-publish.mts --verbose --skip-auth
5038
displayName: Verify release config
5139
52-
- script: |
53-
echo Target branch: $(System.PullRequest.TargetBranch)
54-
yarn nx release --dry-run --verbose
55-
56-
# Show what additional tags would be applied
57-
node .ado/scripts/apply-additional-tags.mjs --tags "$(additionalTags)" --dry-run
58-
displayName: Version and publish packages (dry run)
59-
condition: and(succeeded(), ne(variables['publish_react_native_macos'], '1'))
60-
6140
# Disable Nightly publishing on the main branch
6241
- ${{ if endsWith(variables['Build.SourceBranchName'], '-stable') }}:
6342
- script: |
64-
git switch $(Build.SourceBranchName)
65-
yarn nx release --skip-publish --verbose
66-
env:
67-
GITHUB_TOKEN: $(githubAuthToken)
68-
displayName: Version Packages and Github Release
43+
yarn config set npmPublishAccess public
44+
yarn config set npmPublishRegistry "https://registry.npmjs.org"
45+
yarn config set npmAuthToken $(npmAuthToken)
46+
displayName: Configure yarn for npm publishing
6947
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))
7048
7149
- script: |
72-
set -eox pipefail
73-
if [[ -f .rnm-publish ]]; then
74-
# https://github.com/microsoft/react-native-macos/issues/2580
75-
# `nx release publish` gets confused by the output of RNM's prepack script.
76-
# Let's call publish directly instead on the packages we want to publish.
77-
# yarn nx release publish --tag $(publishTag) --excludeTaskDependencies
78-
if [ "$(USE_YARN_FOR_PUBLISH)" = "true" ]; then
79-
echo "Configuring yarn for npm publishing"
80-
yarn config set npmPublishRegistry "https://registry.npmjs.org"
81-
yarn config set npmAuthToken $(npmAuthToken)
82-
echo "Publishing with yarn npm publish"
83-
yarn ./packages/virtualized-lists npm publish --tag $(publishTag)
84-
yarn ./packages/react-native npm publish --tag $(publishTag)
85-
else
86-
echo "Publishing with npm publish"
87-
npm publish ./packages/virtualized-lists --tag $(publishTag) --registry https://registry.npmjs.org/ --//registry.npmjs.org/:_authToken=$(npmAuthToken)
88-
npm publish ./packages/react-native --tag $(publishTag) --registry https://registry.npmjs.org/ --//registry.npmjs.org/:_authToken=$(npmAuthToken)
89-
fi
90-
fi
50+
yarn workspaces foreach -vv --all --topological --no-private npm publish --tag $(publishTag) --tolerate-republish
9151
displayName: Publish packages
9252
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))
9353
@@ -97,13 +57,8 @@ jobs:
9757
condition: and(succeeded(), eq(variables['publish_react_native_macos'], '1'))
9858
9959
- script: |
100-
if [ "$(USE_YARN_FOR_PUBLISH)" = "true" ]; then
101-
echo "Cleaning up yarn npm configuration"
102-
yarn config unset npmAuthToken || true
103-
yarn config unset npmPublishRegistry || true
104-
else
105-
echo "Cleaning up npm configuration"
106-
rm -f ~/.npmrc
107-
fi
60+
yarn config unset npmPublishAccess || true
61+
yarn config unset npmAuthToken || true
62+
yarn config unset npmPublishRegistry || true
10863
displayName: Remove NPM auth configuration
10964
condition: always()

.ado/scripts/configure-publish.mts

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
#!/usr/bin/env node
2+
import { $, argv, echo, fs } from 'zx';
3+
import { resolve } from 'node:path';
4+
5+
const NPM_DEFAULT_REGISTRY = 'https://registry.npmjs.org/';
6+
const NPM_TAG_NEXT = 'next';
7+
8+
export type ReleaseState = 'STABLE_IS_LATEST' | 'STABLE_IS_NEW' | 'STABLE_IS_OLD';
9+
10+
export interface ReleaseStateInfo {
11+
state: ReleaseState;
12+
currentVersion: number;
13+
latestVersion: number;
14+
nextVersion: number;
15+
}
16+
17+
export interface TagInfo {
18+
npmTags: string[];
19+
prerelease?: string;
20+
}
21+
22+
interface Options {
23+
'mock-branch'?: string;
24+
'skip-auth'?: boolean;
25+
tag?: string;
26+
verbose?: boolean;
27+
}
28+
29+
/**
30+
* Exports a variable, `publish_react_native_macos`, to signal that we want to
31+
* enable publishing on Azure Pipelines.
32+
*/
33+
function enablePublishingOnAzurePipelines() {
34+
echo(`##vso[task.setvariable variable=publish_react_native_macos]1`);
35+
}
36+
37+
export function isMainBranch(branch: string): boolean {
38+
return branch === 'main';
39+
}
40+
41+
export function isStableBranch(branch: string): boolean {
42+
return /^\d+\.\d+-stable$/.test(branch);
43+
}
44+
45+
export function versionToNumber(version: string): number {
46+
const [major, minor] = version.split('-')[0].split('.');
47+
return Number(major) * 1000 + Number(minor);
48+
}
49+
50+
function getTargetBranch(): string | undefined {
51+
// Azure Pipelines
52+
if (process.env['TF_BUILD'] === 'True') {
53+
const targetBranch = process.env['SYSTEM_PULLREQUEST_TARGETBRANCH'];
54+
return targetBranch?.replace(/^refs\/heads\//, '');
55+
}
56+
57+
// GitHub Actions
58+
if (process.env['GITHUB_ACTIONS'] === 'true') {
59+
return process.env['GITHUB_BASE_REF'];
60+
}
61+
62+
return undefined;
63+
}
64+
65+
async function getCurrentBranch(options: Options): Promise<string> {
66+
const targetBranch = getTargetBranch();
67+
if (targetBranch) {
68+
return targetBranch;
69+
}
70+
71+
// Azure DevOps Pipelines
72+
if (process.env['TF_BUILD'] === 'True') {
73+
const sourceBranch = process.env['BUILD_SOURCEBRANCHNAME'];
74+
if (sourceBranch) {
75+
return sourceBranch.replace(/^refs\/heads\//, '');
76+
}
77+
}
78+
79+
// GitHub Actions
80+
if (process.env['GITHUB_ACTIONS'] === 'true') {
81+
const headRef = process.env['GITHUB_HEAD_REF'];
82+
if (headRef) return headRef;
83+
84+
const ref = process.env['GITHUB_REF'];
85+
if (ref) return ref.replace(/^refs\/heads\//, '');
86+
}
87+
88+
if (options['mock-branch']) {
89+
return options['mock-branch'];
90+
}
91+
92+
const result = await $`git rev-parse --abbrev-ref HEAD`;
93+
return result.stdout.trim();
94+
}
95+
96+
function getPublishedVersionSync(tag: 'latest' | 'next'): number {
97+
const result = $.sync`npm view react-native-macos@${tag} version`;
98+
return versionToNumber(result.stdout.trim());
99+
}
100+
101+
export function getReleaseState(
102+
branch: string,
103+
getVersion: (tag: 'latest' | 'next') => number = getPublishedVersionSync,
104+
): ReleaseStateInfo {
105+
if (!isStableBranch(branch)) {
106+
throw new Error('Expected a stable branch');
107+
}
108+
109+
const latestVersion = getVersion('latest');
110+
const nextVersion = getVersion('next');
111+
const currentVersion = versionToNumber(branch);
112+
113+
let state: ReleaseState;
114+
if (currentVersion === latestVersion) {
115+
state = 'STABLE_IS_LATEST';
116+
} else if (currentVersion < latestVersion) {
117+
state = 'STABLE_IS_OLD';
118+
} else {
119+
state = 'STABLE_IS_NEW';
120+
}
121+
122+
return { state, currentVersion, latestVersion, nextVersion };
123+
}
124+
125+
export function getPublishTags(
126+
stateInfo: ReleaseStateInfo,
127+
branch: string,
128+
tag: string = NPM_TAG_NEXT,
129+
): TagInfo {
130+
const { state, currentVersion, nextVersion } = stateInfo;
131+
132+
switch (state) {
133+
case 'STABLE_IS_LATEST':
134+
// Patching the current latest version
135+
return { npmTags: ['latest', branch] };
136+
137+
case 'STABLE_IS_OLD':
138+
// Patching an older stable version
139+
return { npmTags: [branch] };
140+
141+
case 'STABLE_IS_NEW': {
142+
if (tag === 'latest') {
143+
// Promoting this branch to latest
144+
const npmTags = ['latest', branch];
145+
if (currentVersion > nextVersion) {
146+
npmTags.push(NPM_TAG_NEXT);
147+
}
148+
return { npmTags };
149+
}
150+
151+
// Publishing a release candidate
152+
if (currentVersion < nextVersion) {
153+
throw new Error(
154+
`Current version cannot be a release candidate because it is too old: ${currentVersion} < ${nextVersion}`,
155+
);
156+
}
157+
158+
return { npmTags: [NPM_TAG_NEXT], prerelease: 'rc' };
159+
}
160+
}
161+
}
162+
163+
async function verifyNpmAuth(registry = NPM_DEFAULT_REGISTRY) {
164+
const whoami = await $`npm whoami --registry ${registry}`.nothrow();
165+
if (whoami.exitCode !== 0) {
166+
const errText = whoami.stderr;
167+
const m = errText.match(/npm error code (\w+)/);
168+
const errorCode = m && m[1];
169+
switch (errorCode) {
170+
case 'EINVALIDNPMTOKEN':
171+
throw new Error(`Invalid auth token for npm registry: ${registry}`);
172+
case 'ENEEDAUTH':
173+
throw new Error(`Missing auth token for npm registry: ${registry}`);
174+
default:
175+
throw new Error(errText);
176+
}
177+
}
178+
}
179+
180+
async function enablePublishing(tagInfo: TagInfo, options: Options) {
181+
const [primaryTag, ...additionalTags] = tagInfo.npmTags;
182+
183+
// Output publishTag for subsequent pipeline steps
184+
echo(`##vso[task.setvariable variable=publishTag]${primaryTag}`);
185+
if (process.env['GITHUB_OUTPUT']) {
186+
fs.appendFileSync(process.env['GITHUB_OUTPUT'], `publishTag=${primaryTag}\n`);
187+
}
188+
189+
// Output additional tags
190+
if (additionalTags.length > 0) {
191+
const tagsValue = additionalTags.join(',');
192+
echo(`##vso[task.setvariable variable=additionalTags]${tagsValue}`);
193+
if (process.env['GITHUB_OUTPUT']) {
194+
fs.appendFileSync(process.env['GITHUB_OUTPUT'], `additionalTags=${tagsValue}\n`);
195+
}
196+
}
197+
198+
if (options['skip-auth']) {
199+
echo('ℹ️ Skipped npm auth validation');
200+
} else {
201+
await verifyNpmAuth();
202+
}
203+
204+
// Don't enable publishing in PRs
205+
if (!getTargetBranch()) {
206+
enablePublishingOnAzurePipelines();
207+
}
208+
}
209+
210+
const isDirectRun =
211+
process.argv[1] != null &&
212+
resolve(process.argv[1]) === new URL(import.meta.url).pathname;
213+
214+
if (isDirectRun) {
215+
// Parse CLI args using zx's argv (minimist)
216+
const options: Options = {
217+
'mock-branch': argv['mock-branch'] as string | undefined,
218+
'skip-auth': Boolean(argv['skip-auth']),
219+
tag: typeof argv['tag'] === 'string' ? argv['tag'] : NPM_TAG_NEXT,
220+
verbose: Boolean(argv['verbose']),
221+
};
222+
223+
const branch = await getCurrentBranch(options);
224+
if (!branch) {
225+
echo('❌ Could not get current branch');
226+
process.exit(1);
227+
}
228+
229+
const log = options.verbose ? (msg: string) => echo(`ℹ️ ${msg}`) : () => {};
230+
231+
try {
232+
if (isMainBranch(branch)) {
233+
// Nightlies are currently disabled — skip publishing from main
234+
echo('ℹ️ On main branch — nightly publishing is currently disabled');
235+
} else if (isStableBranch(branch)) {
236+
const stateInfo = getReleaseState(branch);
237+
log(`react-native-macos@latest: ${stateInfo.latestVersion}`);
238+
log(`react-native-macos@next: ${stateInfo.nextVersion}`);
239+
log(`Current version: ${stateInfo.currentVersion}`);
240+
log(`Release state: ${stateInfo.state}`);
241+
242+
const tagInfo = getPublishTags(stateInfo, branch, options.tag);
243+
log(`Expected npm tags: ${tagInfo.npmTags.join(', ')}`);
244+
245+
await enablePublishing(tagInfo, options);
246+
} else {
247+
echo(`ℹ️ Branch '${branch}' is not main or a stable branch — skipping`);
248+
}
249+
} catch (e) {
250+
echo(`❌ ${(e as Error).message}`);
251+
process.exit(1);
252+
}
253+
}

0 commit comments

Comments
 (0)