Skip to content

Commit 206ed6e

Browse files
ci: use Lage to schedule publish tasks (#4022)
* ci: use Lage to schedule publish tasks * Add zx as dev dependency to scripts * update checkChangesetPresence * update lage * update changeeset check * format * update yarn * format --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 28b73c1 commit 206ed6e

File tree

11 files changed

+278
-298
lines changed

11 files changed

+278
-298
lines changed

.ado/azure-pipelines.publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ extends:
8484
- script: |
8585
# https://github.com/changesets/changesets/issues/432
8686
# We can't use `changeset publish` because it doesn't support workspaces, so we have to publish each package individually
87-
yarn workspaces foreach -vv --all --topological --no-private npm publish --tolerate-republish
87+
yarn lage publish --verbose --grouped --reporter azureDevops
8888
displayName: 'Publish NPM Packages'
8989
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'), not(${{ parameters.skipNpmPublish }}))
9090

.changeset/config.json

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,5 @@
55
"linked": [],
66
"access": "public",
77
"baseBranch": "origin/main",
8-
"ignore": [
9-
"@fluentui-react-native/dependency-profiles",
10-
"@fluentui-react-native/e2e-testing",
11-
"@fluentui-react-native/tester",
12-
"@fluentui-react-native/tester-core",
13-
"@fluentui-react-native/tester-win32",
14-
"@fluentui-react-native/tester-win32-81",
15-
"@fluentui-react-native/test-*"
16-
]
8+
"ignore": []
179
}
Lines changed: 36 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,22 @@
1-
#!/usr/bin/env zx
2-
import 'zx/globals';
1+
#!/usr/bin/env node
2+
// @ts-ignore
3+
import { styleText } from 'node:util';
4+
import { $, echo, fs } from 'zx';
35

46
/**
57
* Validate changesets in CI
68
*
7-
* Checks:
8-
* 1. Changesets are present (PRs require changesets)
9-
* 2. No major version bumps (breaking changes disallowed)
9+
* Checks for major version bumps (breaking changes disallowed).
10+
* Private/ignored packages (per .changeset/config.json) are excluded automatically.
1011
*/
1112

12-
// ANSI color codes
13-
const colors = {
14-
red: (msg: string) => `\x1b[31m${msg}\x1b[0m`,
15-
green: (msg: string) => `\x1b[32m${msg}\x1b[0m`,
16-
yellow: (msg: string) => `\x1b[33m${msg}\x1b[0m`,
17-
};
13+
const tmpDir = fs.mkdtempSync('/tmp/changeset-status-');
14+
const STATUS_FILE = `${tmpDir}/status.json`;
1815

19-
// Logging helpers
2016
const log = {
21-
error: (msg: string) => echo(colors.red(msg)),
22-
success: (msg: string) => echo(colors.green(msg)),
23-
warn: (msg: string) => echo(colors.yellow(msg)),
17+
error: (msg: string) => echo(styleText('red', msg)),
18+
success: (msg: string) => echo(styleText('green', msg)),
19+
warn: (msg: string) => echo(styleText('yellow', msg)),
2420
info: (msg: string) => echo(msg),
2521
};
2622

@@ -35,83 +31,37 @@ interface ChangesetStatusOutput {
3531
changesets: string[];
3632
}
3733

38-
async function checkChangesetPresence() {
39-
log.info('\n🔍 Checking for changeset presence...\n');
40-
41-
const result = await $`yarn changeset status --since=origin/main 2>&1`.nothrow();
42-
43-
if (result.exitCode !== 0) {
44-
log.error('❌ Changeset validation failed\n');
45-
echo(result.stdout);
46-
return false;
47-
}
48-
49-
log.success('✅ Changesets found');
50-
return true;
51-
}
52-
53-
async function checkForMajorBumps() {
54-
log.info('\n🔍 Checking for major version bumps...\n');
55-
56-
const result = await $`yarn changeset status --output bumps.json`.nothrow();
57-
58-
// If no changesets, skip major bump check
59-
if (result.exitCode !== 0 && result.stdout.includes('no changesets were found')) {
60-
log.warn('No changesets found (skipping major check)');
61-
return true;
62-
}
63-
64-
// Other errors
65-
if (result.exitCode !== 0 || !fs.existsSync('bumps.json')) {
66-
log.error('❌ Failed to check for major bumps\n');
67-
if (result.stderr) log.info(result.stderr);
68-
return false;
69-
}
70-
71-
const bumpsData: ChangesetStatusOutput = JSON.parse(fs.readFileSync('bumps.json', 'utf-8'));
72-
fs.unlinkSync('bumps.json');
73-
74-
const majorBumps = bumpsData.releases.filter((release) => release.type === 'major');
75-
76-
if (majorBumps.length > 0) {
77-
log.error('❌ Major version bumps detected!\n');
78-
for (const release of majorBumps) {
79-
log.error(` ${release.name}: major`);
80-
if (release.changesets.length > 0) {
81-
log.error(` (from changesets: ${release.changesets.join(', ')})`);
82-
}
83-
}
84-
log.error('\nMajor version bumps are not allowed.');
85-
log.warn('If you need to make a breaking change, please discuss with the team first.\n');
86-
return false;
87-
}
88-
89-
log.success('✅ No major version bumps found');
90-
return true;
91-
}
92-
93-
// Main execution
9434
log.info(`\n${'='.repeat(60)}`);
9535
log.info('Changesets Validation');
96-
log.info(`${'='.repeat(60)}`);
97-
98-
const results = {
99-
presence: await checkChangesetPresence(),
100-
majorBumps: await checkForMajorBumps(),
101-
};
102-
103-
log.info(`\n${'='.repeat(60)}`);
104-
log.info('Validation Results:');
10536
log.info(`${'='.repeat(60)}\n`);
10637

107-
log.info(`Changeset presence: ${results.presence ? '✅ PASS' : '❌ FAIL'}`);
108-
log.info(`Major version check: ${results.majorBumps ? '✅ PASS' : '❌ FAIL'}\n`);
38+
// Pre-write empty state so changeset status always has a file to overwrite
39+
fs.writeJsonSync(STATUS_FILE, { releases: [], changesets: [] });
40+
41+
await $`yarn changeset status --since=origin/main --output ${STATUS_FILE}`.nothrow();
10942

110-
const allPassed = results.presence && results.majorBumps;
43+
const data: ChangesetStatusOutput = fs.readJsonSync(STATUS_FILE);
44+
fs.removeSync(tmpDir);
11145

112-
if (!allPassed) {
113-
log.error('Validation failed!\n');
46+
// Fail: major version bumps
47+
const majorBumps = data.releases.filter((release) => release.type === 'major');
48+
if (majorBumps.length > 0) {
49+
log.error('❌ Major version bumps detected!\n');
50+
for (const release of majorBumps) {
51+
log.error(` ${release.name}: major`);
52+
if (release.changesets.length > 0) {
53+
log.error(` (from changesets: ${release.changesets.join(', ')})`);
54+
}
55+
}
56+
log.error('\nMajor version bumps are not allowed.');
57+
log.warn('If you need to make a breaking change, please discuss with the team first.\n');
11458
throw new Error('Validation failed');
11559
}
11660

117-
log.success('All validations passed! ✅\n');
61+
// Pass
62+
if (data.releases.length === 0) {
63+
log.info('ℹ️ No public packages changed — no changeset required');
64+
} else {
65+
log.success(`✅ Changesets found (${data.releases.map((r) => r.name).join(', ')})`);
66+
}
67+
log.success('\nAll validations passed! ✅\n');

.github/workflows/pr.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,7 @@ jobs:
325325
run: yarn build
326326

327327
- name: Simulate publish
328-
run: |
329-
yarn workspaces foreach -vv --all --topological --no-private npm publish --tolerate-republish --dry-run
328+
run: yarn lage publish-dry-run --verbose --grouped
330329

331330
test-links:
332331
name: Test repo links
Lines changed: 191 additions & 191 deletions
Large diffs are not rendered by default.

.yarnrc.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,4 +302,4 @@ plugins:
302302
path: .yarn/plugins/@rnx-kit/yarn-plugin-dynamic-extensions.cjs
303303
spec: "https://raw.githubusercontent.com/microsoft/rnx-kit/main/incubator/yarn-plugin-dynamic-extensions/index.js"
304304

305-
yarnPath: .yarn/releases/yarn-4.11.0.cjs
305+
yarnPath: .yarn/releases/yarn-4.12.0.cjs

lage.config.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,23 @@ const config = {
4646
inputs: [],
4747
outputs: [],
4848
},
49+
publish: {
50+
dependsOn: ['^publish'],
51+
type: 'worker',
52+
options: {
53+
worker: 'scripts/src/worker/publish.js',
54+
},
55+
cache: false,
56+
},
57+
'publish-dry-run': {
58+
dependsOn: ['^publish-dry-run'],
59+
type: 'worker',
60+
options: {
61+
worker: 'scripts/src/worker/publish.js',
62+
dryRun: true,
63+
},
64+
cache: false,
65+
},
4966
},
5067
};
5168

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"cross-env": "catalog:",
5151
"eslint": "^9.39.2",
5252
"eslint-plugin-import": "^2.32.0",
53-
"lage": "^2.0.0",
53+
"lage": "^2.14.19",
5454
"markdown-link-check": "^3.8.7",
5555
"oxfmt": "^0.35.0",
5656
"typescript": "^5.8.0",
@@ -62,7 +62,7 @@
6262
"engines": {
6363
"node": ">=22.12"
6464
},
65-
"packageManager": "yarn@4.11.0",
65+
"packageManager": "yarn@4.12.0",
6666
"rnx-kit": {
6767
"kitType": "library"
6868
}

scripts/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
"react": "18.2.0",
4848
"react-native": "^0.74.0",
4949
"typescript": "^5.8.0",
50-
"workspace-tools": "^0.26.3"
50+
"workspace-tools": "^0.26.3",
51+
"zx": "^8.2.4"
5152
},
5253
"bundlesize": [
5354
{

scripts/src/worker/publish.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// @ts-check
2+
3+
import { $, fs } from 'zx';
4+
import { join } from 'node:path';
5+
6+
/**
7+
* @param {{ target: { cwd: string; label: string }, options: { dryRun?: boolean } }} data
8+
*/
9+
export async function run({ target, options }) {
10+
const pkg = await fs.readJson(join(target.cwd, 'package.json'));
11+
12+
if (pkg.private) {
13+
return;
14+
}
15+
16+
const dryRun = options?.dryRun ?? false;
17+
const args = ['--tolerate-republish', ...(dryRun ? ['--dry-run'] : [])];
18+
19+
await $({ cwd: target.cwd, verbose: true })`yarn npm publish ${args}`;
20+
}

0 commit comments

Comments
 (0)