Skip to content

Commit 342a238

Browse files
authored
ci: changesets ignores private packages (#4043)
* ci: changesets ignores private packages * format
1 parent 6c9c536 commit 342a238

2 files changed

Lines changed: 85 additions & 37 deletions

File tree

.changeset/config.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
{
22
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
3+
"access": "public",
4+
"baseBranch": "origin/main",
35
"changelog": "@changesets/cli/changelog",
46
"commit": [
57
"@changesets/cli/commit",
68
{
79
"skipCI": false
810
}
911
],
12+
"ignore": [],
1013
"linked": [],
11-
"access": "public",
12-
"baseBranch": "origin/main",
13-
"ignore": []
14+
"privatePackages": {
15+
"tag": false,
16+
"version": false
17+
}
1418
}

.github/scripts/change.mts

Lines changed: 78 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { $, echo, fs } from 'zx';
1010
* Without --check: runs `changeset add` interactively with the correct upstream remote
1111
* auto-detected from package.json's repository URL, temporarily patched into config.json.
1212
*
13-
* With --check (CI mode): validates that all changed packages have changesets and that
13+
* With --check (CI mode): validates that all changed public packages have changesets and that
1414
* no major version bumps are introduced.
1515
*/
1616

@@ -44,19 +44,62 @@ async function getBaseBranch(): Promise<string> {
4444
return `${remote}/main`;
4545
}
4646

47-
/** Run `changeset add` interactively, temporarily patching config.json with the correct baseBranch. */
48-
async function runAdd(baseBranch: string): Promise<void> {
49-
const configPath = '.changeset/config.json';
50-
const config = fs.readJsonSync(configPath);
51-
const originalBaseBranch: string = config.baseBranch;
52-
config.baseBranch = baseBranch;
53-
fs.writeJsonSync(configPath, config, { spaces: 2 });
47+
/** Run `changeset status` and return the output and exit code. */
48+
async function getChangesetStatus(baseBranch: string): Promise<{ data: ChangesetStatusOutput; exitCode: number }> {
49+
const STATUS_FILE = 'changeset-status.json';
5450

55-
try {
56-
await $({ stdio: 'inherit' })`yarn changeset`;
57-
} finally {
58-
config.baseBranch = originalBaseBranch;
59-
fs.writeJsonSync(configPath, config, { spaces: 2 });
51+
fs.writeJsonSync(STATUS_FILE, { releases: [], changesets: [] });
52+
const result = await $`yarn changeset status --since=${baseBranch} --output ${STATUS_FILE}`.nothrow();
53+
const data: ChangesetStatusOutput = fs.readJsonSync(STATUS_FILE);
54+
fs.removeSync(STATUS_FILE);
55+
56+
return { data, exitCode: result.exitCode ?? 1 };
57+
}
58+
59+
/** Returns names of all private workspace packages. */
60+
async function getPrivatePackages(): Promise<Set<string>> {
61+
const output = (await $`yarn workspaces list --json`.quiet()).stdout;
62+
const workspaces = output
63+
.trim()
64+
.split('\n')
65+
.filter(Boolean)
66+
.map((line) => JSON.parse(line))
67+
.filter((e): e is { location: string; name: string } => typeof e.location === 'string' && typeof e.name === 'string');
68+
69+
const names = workspaces
70+
.filter(({ location }) => {
71+
try {
72+
return fs.readJsonSync(`${location}/package.json`).private === true;
73+
} catch {
74+
return false;
75+
}
76+
})
77+
.map(({ name }) => name);
78+
79+
return new Set(names);
80+
}
81+
82+
/** Fail if any .changeset/*.md file bumps a private package. */
83+
function checkNoPrivatePackageBumps(privatePackages: Set<string>): void {
84+
const files = fs.readdirSync('.changeset').filter((f: string) => f.endsWith('.md'));
85+
for (const file of files) {
86+
const content = fs.readFileSync(`.changeset/${file}`, 'utf-8');
87+
const frontmatter = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? '';
88+
const bumped = frontmatter
89+
.split('\n')
90+
.map((line: string) =>
91+
line
92+
.split(':')[0]
93+
.trim()
94+
.replace(/^['"]|['"]$/g, ''),
95+
)
96+
.filter(Boolean);
97+
const privateBumps = bumped.filter((name: string) => privatePackages.has(name));
98+
if (privateBumps.length > 0) {
99+
log.error(`❌ Changeset ${file} bumps private packages: ${privateBumps.join(', ')}`);
100+
log.warn('Remove the private package entries from the changeset.\n');
101+
process.exit(1);
102+
}
60103
}
61104
}
62105

@@ -76,39 +119,40 @@ function checkMajorBumps(releases: ChangesetStatusOutput['releases']): void {
76119
process.exit(1);
77120
}
78121

79-
/** Validate that all changed packages have changesets and no major bumps are introduced. */
80-
async function runCheck(baseBranch: string): Promise<void> {
81-
const STATUS_FILE = 'changeset-status.json';
122+
/** Run `changeset add` interactively, temporarily patching config.json with the correct baseBranch. */
123+
async function runAdd(baseBranch: string): Promise<void> {
124+
const configPath = '.changeset/config.json';
125+
const config = fs.readJsonSync(configPath);
126+
const originalBaseBranch: string = config.baseBranch;
127+
config.baseBranch = baseBranch;
128+
fs.writeJsonSync(configPath, config, { spaces: 2 });
82129

83-
log.info(`\n${'='.repeat(60)}`);
84-
log.info('Changesets Validation');
85-
log.info(`${'='.repeat(60)}\n`);
86-
log.info(`Comparing against ${baseBranch}\n`);
130+
try {
131+
await $({ stdio: 'inherit' })`yarn changeset`;
132+
} finally {
133+
config.baseBranch = originalBaseBranch;
134+
fs.writeJsonSync(configPath, config, { spaces: 2 });
135+
}
136+
}
87137
88-
// Pre-write empty state so changeset status always has a file to overwrite
89-
fs.writeJsonSync(STATUS_FILE, { releases: [], changesets: [] });
138+
/** Validate that all changed public packages have changesets and no major bumps are introduced. */
139+
async function runCheck(baseBranch: string): Promise<void> {
140+
log.info(`Validating changesets against ${baseBranch}...\n`);
90141
91-
const statusResult = await $`yarn changeset status --since=${baseBranch} --output ${STATUS_FILE}`.nothrow();
142+
checkNoPrivatePackageBumps(await getPrivatePackages());
92143
93-
const data: ChangesetStatusOutput = fs.readJsonSync(STATUS_FILE);
94-
fs.removeSync(STATUS_FILE);
144+
const { data, exitCode } = await getChangesetStatus(baseBranch);
95145
96-
// Fail: packages changed but no changeset written
97-
if (statusResult.exitCode !== 0) {
146+
if (exitCode !== 0) {
98147
log.error('❌ Some packages have been changed but no changesets were found.');
99148
log.warn('Run `yarn change` to create a changeset, or `yarn changeset --empty` if no release is needed.\n');
100149
process.exit(1);
101150
}
102151
103152
checkMajorBumps(data.releases);
104153
105-
// Pass
106-
if (data.releases.length === 0) {
107-
log.info('ℹ️ No public packages changed — no changeset required');
108-
} else {
109-
log.success(`✅ Changesets found (${data.releases.map((r) => r.name).join(', ')})`);
110-
}
111-
log.success('\nAll validations passed! ✅\n');
154+
const packages = data.releases.map((r) => r.name).join(', ');
155+
log.success(packages ? ` All validations passed (${packages})` : '✅ All validations passed');
112156
}
113157

114158
const { values: args } = parseArgs({ options: { check: { type: 'boolean', default: false } } });

0 commit comments

Comments
 (0)