Skip to content

Commit f730dcf

Browse files
Saadnajmiclaude
andcommitted
fix: address bugs in changesets migration
- Fix config/script mismatch: enable commit in config.json so postbump script's `git reset --soft HEAD~1` has a commit to undo - Replace validate-changesets.mts with unified change.mts (matches FURN's latest pattern) with auto-detected base branch via --since - Fix dry-run publish using ADO $(publishTag) syntax in GitHub Actions - Skip version bump PRs in check-changesets job - Add fetch-depth/persist-credentials to version workflow checkout Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0784e74 commit f730dcf

File tree

7 files changed

+208
-159
lines changed

7 files changed

+208
-159
lines changed

.changeset/config.json

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
{
2-
"$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json",
2+
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
3+
"access": "public",
4+
"baseBranch": "origin/main",
35
"changelog": "@changesets/cli/changelog",
4-
"commit": false,
5-
"fixed": [],
6+
"commit": [
7+
"@changesets/cli/commit",
8+
{
9+
"skipCI": false
10+
}
11+
],
12+
"ignore": [],
613
"linked": [],
7-
"access": "public",
8-
"baseBranch": "main",
9-
"updateInternalDependencies": "patch",
10-
"ignore": []
14+
"privatePackages": {
15+
"tag": false,
16+
"version": false
17+
}
1118
}

.github/scripts/change.mts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#!/usr/bin/env node
2+
// @ts-ignore
3+
import { parseArgs, styleText } from 'node:util';
4+
5+
import { $, echo, fs } from 'zx';
6+
7+
/**
8+
* Wrapper around `changeset add` (default) and `changeset status` validation (--check).
9+
*
10+
* Without --check: runs `changeset add` interactively with the correct upstream remote
11+
* auto-detected from package.json's repository URL, temporarily patched into config.json.
12+
*
13+
* With --check (CI mode): validates that all changed public packages have changesets and that
14+
* no major version bumps are introduced.
15+
*/
16+
17+
interface ChangesetStatusOutput {
18+
releases: Array<{
19+
name: string;
20+
type: 'major' | 'minor' | 'patch' | 'none';
21+
oldVersion: string;
22+
newVersion: string;
23+
changesets: string[];
24+
}>;
25+
changesets: string[];
26+
}
27+
28+
const log = {
29+
error: (msg: string) => echo(styleText('red', msg)),
30+
success: (msg: string) => echo(styleText('green', msg)),
31+
warn: (msg: string) => echo(styleText('yellow', msg)),
32+
info: (msg: string) => echo(msg),
33+
};
34+
35+
/** Find the remote that matches the repo's own URL (works for forks and CI alike). */
36+
async function getBaseBranch(): Promise<string> {
37+
const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf-8'));
38+
const repoUrl: string = pkg.repository?.url ?? '';
39+
// Extract "org/repo" from https://github.com/org/repo.git or git@github.com:org/repo.git
40+
const repoPath = repoUrl.match(/github\.com[:/](.+?)(?:\.git)?$/)?.[1] ?? '';
41+
42+
const remotes = (await $`git remote -v`.quiet()).stdout;
43+
const remote = (repoPath && remotes.match(new RegExp(`^(\\S+)\\s+.*${repoPath}`, 'm'))?.[1]) ?? 'origin';
44+
45+
// In CI, use the PR target branch (e.g., origin/0.81-stable)
46+
if (process.env['GITHUB_BASE_REF']) {
47+
return `${remote}/${process.env['GITHUB_BASE_REF']}`;
48+
}
49+
50+
return `${remote}/main`;
51+
}
52+
53+
/** Run `changeset status` and return the output and exit code. */
54+
async function getChangesetStatus(baseBranch: string): Promise<{ data: ChangesetStatusOutput; exitCode: number }> {
55+
const STATUS_FILE = 'changeset-status.json';
56+
57+
fs.writeJsonSync(STATUS_FILE, { releases: [], changesets: [] });
58+
const result = await $`yarn changeset status --since=${baseBranch} --output ${STATUS_FILE}`.nothrow();
59+
const data: ChangesetStatusOutput = fs.readJsonSync(STATUS_FILE);
60+
fs.removeSync(STATUS_FILE);
61+
62+
return { data, exitCode: result.exitCode ?? 1 };
63+
}
64+
65+
/** Returns names of all private workspace packages. */
66+
async function getPrivatePackages(): Promise<Set<string>> {
67+
const output = (await $`yarn workspaces list --json`.quiet()).stdout;
68+
const workspaces = output
69+
.trim()
70+
.split('\n')
71+
.filter(Boolean)
72+
.map((line) => JSON.parse(line))
73+
.filter((e): e is { location: string; name: string } => typeof e.location === 'string' && typeof e.name === 'string');
74+
75+
const names = workspaces
76+
.filter(({ location }) => {
77+
try {
78+
return fs.readJsonSync(`${location}/package.json`).private === true;
79+
} catch {
80+
return false;
81+
}
82+
})
83+
.map(({ name }) => name);
84+
85+
return new Set(names);
86+
}
87+
88+
/** Fail if any .changeset/*.md file bumps a private package. */
89+
function checkNoPrivatePackageBumps(privatePackages: Set<string>): void {
90+
const files = fs.readdirSync('.changeset').filter((f: string) => f.endsWith('.md'));
91+
for (const file of files) {
92+
const content = fs.readFileSync(`.changeset/${file}`, 'utf-8');
93+
const frontmatter = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? '';
94+
const bumped = frontmatter
95+
.split('\n')
96+
.map((line: string) =>
97+
line
98+
.split(':')[0]
99+
.trim()
100+
.replace(/^['"]|['"]$/g, ''),
101+
)
102+
.filter(Boolean);
103+
const privateBumps = bumped.filter((name: string) => privatePackages.has(name));
104+
if (privateBumps.length > 0) {
105+
log.error(`Changeset ${file} bumps private packages: ${privateBumps.join(', ')}`);
106+
log.warn('Remove the private package entries from the changeset.\n');
107+
process.exit(1);
108+
}
109+
}
110+
}
111+
112+
function checkMajorBumps(releases: ChangesetStatusOutput['releases']): void {
113+
const majorBumps = releases.filter((r) => r.type === 'major');
114+
if (majorBumps.length === 0) return;
115+
116+
log.error('Major version bumps detected!\n');
117+
for (const release of majorBumps) {
118+
log.error(` ${release.name}: major`);
119+
if (release.changesets.length > 0) {
120+
log.error(` (from changesets: ${release.changesets.join(', ')})`);
121+
}
122+
}
123+
log.error('\nMajor version bumps are not allowed.');
124+
log.warn('If you need to make a breaking change, please discuss with the team first.\n');
125+
process.exit(1);
126+
}
127+
128+
/** Validate that all changed public packages have changesets and no major bumps are introduced. */
129+
async function runCheck(baseBranch: string): Promise<void> {
130+
log.info(`Validating changesets against ${baseBranch}...\n`);
131+
132+
checkNoPrivatePackageBumps(await getPrivatePackages());
133+
134+
const { data, exitCode } = await getChangesetStatus(baseBranch);
135+
136+
if (exitCode !== 0) {
137+
log.error('Some packages have been changed but no changesets were found.');
138+
log.warn('Run `yarn change` to create a changeset, or `yarn changeset --empty` if no release is needed.\n');
139+
process.exit(1);
140+
}
141+
142+
checkMajorBumps(data.releases);
143+
144+
const packages = data.releases.map((r) => r.name).join(', ');
145+
log.success(packages ? `All validations passed (${packages})` : 'All validations passed');
146+
}
147+
148+
/** Run `changeset add` interactively against the correct upstream base branch. */
149+
async function runAdd(baseBranch: string): Promise<void> {
150+
await $({ stdio: 'inherit' })`yarn changeset --since ${baseBranch}`;
151+
}
152+
153+
const { values: args } = parseArgs({ options: { check: { type: 'boolean', default: false } } });
154+
155+
const baseBranch = await getBaseBranch();
156+
157+
if (args.check) {
158+
await runCheck(baseBranch);
159+
} else {
160+
await runAdd(baseBranch);
161+
}

.github/scripts/validate-changesets.mts

Lines changed: 0 additions & 117 deletions
This file was deleted.

.github/workflows/microsoft-changesets-version.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ jobs:
1515
steps:
1616
- name: Checkout
1717
uses: actions/checkout@v6
18+
with:
19+
filter: blob:none
20+
fetch-depth: 0
21+
persist-credentials: false
1822

1923
- name: Setup toolchain
2024
uses: ./.github/actions/microsoft-setup-toolchain

.github/workflows/microsoft-pr.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,16 @@ jobs:
6161
run: yarn build
6262
- name: Simulate publish (dry run)
6363
run: |
64-
yarn workspaces foreach -vv --all --topological --no-private npm publish --tag $(publishTag) --tolerate-republish --dry-run
64+
yarn workspaces foreach -vv --all --topological --no-private npm publish --tag dry-run --tolerate-republish --dry-run
6565
6666
check-changesets:
6767
name: "Check for Changesets"
6868
permissions: {}
6969
runs-on: ubuntu-latest
70-
# Only required for PRs targeting stable branches
71-
if: ${{ endsWith(github.base_ref, '-stable') }}
70+
# Only required for PRs targeting stable branches; skip the version bump PR itself
71+
if: >-
72+
${{ endsWith(github.base_ref, '-stable')
73+
&& !startsWith(github.head_ref, 'changeset-release/') }}
7274
steps:
7375
- uses: actions/checkout@v4
7476
with:
@@ -81,7 +83,7 @@ jobs:
8183
- name: Install dependencies
8284
run: yarn install --immutable
8385
- name: Validate changesets
84-
run: yarn changeset:validate
86+
run: yarn change:check
8587

8688
yarn-constraints:
8789
name: "Check Yarn Constraints"

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@
3535
"fantom": "./scripts/fantom.sh",
3636
"trigger-react-native-release": "node ./scripts/releases-local/trigger-react-native-release.js",
3737
"update-lock": "npx yarn-deduplicate",
38+
"change": "node .github/scripts/change.mts",
39+
"change:check": "node .github/scripts/change.mts --check",
3840
"changeset": "changeset",
39-
"changeset:version": "node .github/scripts/changeset-version-with-postbump.mts",
40-
"changeset:validate": "node .github/scripts/validate-changesets.mts"
41+
"changeset:version": "node .github/scripts/changeset-version-with-postbump.mts"
4142
},
4243
"workspaces": [
4344
"packages/*",

0 commit comments

Comments
 (0)