Skip to content

Commit 6b05c90

Browse files
ci: use @changesets/get-release-plan (#358)
* WIP: use @changesets/get-release-plan * Tweak reason * Add colours * Don't double log * Sort by name only * Update sorting * Fix spacing * Undo temp changes * Update description
1 parent 820782c commit 6b05c90

4 files changed

Lines changed: 57 additions & 121 deletions

File tree

.github/changeset-preview/action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ description: Generates comment on a PR showing expected version impact
33
runs:
44
using: composite
55
steps:
6+
- name: Install dependencies
7+
shell: bash
8+
run: npm install
9+
working-directory: ${{ github.action_path }}
610
- name: Preview version bumps
711
shell: bash
812
run: node ${{ github.action_path }}/preview-changeset-versions.mjs --output /tmp/changeset-preview.md
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"dependencies": {
3+
"@changesets/get-release-plan": "^4.0.15"
4+
}
5+
}

.github/changeset-preview/preview-changeset-versions.mjs

Lines changed: 48 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,21 @@
11
#!/usr/bin/env node
22

33
/**
4-
* Preview the version bumps that `changeset version` will produce.
5-
*
6-
* Workflow:
7-
* 1. Snapshot every workspace package's current version
8-
* 2. Run `changeset version` (mutates package.json files)
9-
* 3. Diff against the snapshot
10-
* 4. Print a markdown summary (or write to --output file)
11-
*
12-
* This script is meant to run in CI on a disposable checkout — it does NOT
13-
* revert the changes it makes.
4+
* Uses `@changesets/get-release-plan` to get the version bumps and formats it as markdown.
145
*/
156

16-
import { execSync } from 'node:child_process'
17-
import { readdirSync, readFileSync, writeFileSync } from 'node:fs'
18-
import { join, resolve } from 'node:path'
7+
import { writeFileSync } from 'node:fs'
8+
import { resolve } from 'node:path'
199
import { parseArgs } from 'node:util'
10+
import getReleasePlan from '@changesets/get-release-plan'
2011

2112
const ROOT = resolve(import.meta.dirname, '..', '..')
2213

23-
const PACKAGES_DIR = join(ROOT, 'packages')
24-
25-
function readPackageVersions() {
26-
const versions = new Map()
27-
for (const dir of readdirSync(PACKAGES_DIR, { withFileTypes: true })) {
28-
if (!dir.isDirectory()) continue
29-
const pkgPath = join(PACKAGES_DIR, dir.name, 'package.json')
30-
try {
31-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
32-
if (pkg.name && pkg.version && pkg.private !== true) {
33-
versions.set(pkg.name, pkg.version)
34-
}
35-
} catch {
36-
// skip packages without a valid package.json
37-
}
38-
}
39-
return versions
14+
function reasonRank(reason) {
15+
return reason === 'Changeset' ? 2 : 1
4016
}
4117

42-
function readChangesetEntries() {
43-
const changesetDir = join(ROOT, '.changeset')
44-
const explicit = new Map()
45-
for (const file of readdirSync(changesetDir)) {
46-
if (file === 'config.json' || file === 'README.md' || !file.endsWith('.md'))
47-
continue
48-
const content = readFileSync(join(changesetDir, file), 'utf8')
49-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/)
50-
if (!frontmatterMatch) continue
51-
for (const line of frontmatterMatch[1].split('\n')) {
52-
const match = line.match(/^['"]?([^'"]+)['"]?\s*:\s*(major|minor|patch)/)
53-
if (match) {
54-
const [, name, bump] = match
55-
const existing = explicit.get(name)
56-
// keep the highest bump if a package appears in multiple changesets
57-
if (!existing || bumpRank(bump) > bumpRank(existing)) {
58-
explicit.set(name, bump)
59-
}
60-
}
61-
}
62-
}
63-
return explicit
64-
}
65-
66-
function bumpRank(bump) {
67-
return bump === 'major' ? 3 : bump === 'minor' ? 2 : 1
68-
}
69-
70-
function bumpType(oldVersion, newVersion) {
71-
const [oMaj, oMin] = oldVersion.split('.').map(Number)
72-
const [nMaj, nMin] = newVersion.split('.').map(Number)
73-
if (nMaj > oMaj) return 'major'
74-
if (nMin > oMin) return 'minor'
75-
return 'patch'
76-
}
77-
78-
function main() {
18+
async function main() {
7919
const { values } = parseArgs({
8020
args: process.argv.slice(2),
8121
options: {
@@ -85,10 +25,10 @@ function main() {
8525
allowPositionals: false,
8626
})
8727

88-
// 1. Read explicit changeset entries
89-
const explicit = readChangesetEntries()
28+
const releasePlan = await getReleasePlan(ROOT)
29+
const releases = releasePlan.releases
9030

91-
if (explicit.size === 0) {
31+
if (releases.length === 0) {
9232
const msg =
9333
'No changeset entries found. Merging this PR will not cause a version bump for any packages.\n'
9434
process.stdout.write(msg)
@@ -99,42 +39,19 @@ function main() {
9939
return
10040
}
10141

102-
// 2. Snapshot current versions
103-
const before = readPackageVersions()
104-
105-
// 3. Temporarily swap changeset config to skip changelog generation
106-
// (the GitHub changelog plugin requires a token we don't need for previews)
107-
const configPath = join(ROOT, '.changeset', 'config.json')
108-
const originalConfig = readFileSync(configPath, 'utf8')
109-
try {
110-
const config = JSON.parse(originalConfig)
111-
config.changelog = false
112-
writeFileSync(configPath, JSON.stringify(config, null, 2))
113-
114-
// 4. Run changeset version
115-
execSync('pnpm changeset version', { cwd: ROOT, stdio: 'pipe' })
116-
} finally {
117-
// Always restore the original config
118-
writeFileSync(configPath, originalConfig)
119-
}
120-
121-
// 5. Read new versions
122-
const after = readPackageVersions()
123-
12442
// 6. Diff
12543
const bumps = []
126-
for (const [name, newVersion] of after) {
127-
const oldVersion = before.get(name)
128-
if (!oldVersion || oldVersion === newVersion) continue
129-
const bump = bumpType(oldVersion, newVersion)
130-
const source = explicit.has(name) ? explicit.get(name) : 'dependency'
131-
bumps.push({ name, oldVersion, newVersion, bump, source })
44+
for (const release of releases) {
45+
if (release.oldVersion === release.newVersion) continue
46+
const reason = release.changesets.length !== 0 ? 'Changeset' : 'Dependent'
47+
bumps.push({ ...release, reason })
13248
}
13349

134-
// Sort: major first, then minor, then patch; within each group alphabetical
50+
// Order by reason and name
13551
bumps.sort(
13652
(a, b) =>
137-
bumpRank(b.bump) - bumpRank(a.bump) || a.name.localeCompare(b.name),
53+
reasonRank(b.reason) - reasonRank(a.reason) ||
54+
a.name.localeCompare(b.name),
13855
)
13956

14057
// 7. Build markdown
@@ -145,41 +62,53 @@ function main() {
14562
'No version changes detected. Merging this PR will not cause a version bump for any packages.',
14663
)
14764
} else {
148-
const explicitBumps = bumps.filter((b) => b.source !== 'dependency')
149-
const dependencyBumps = bumps.filter((b) => b.source === 'dependency')
65+
const majorBumps = bumps.filter((b) => b.type === 'major')
66+
const minorBumps = bumps.filter((b) => b.type === 'minor')
67+
const patchBumps = bumps.filter((b) => b.type === 'patch')
68+
const directBumps = bumps.filter((b) => b.reason === 'Changeset')
69+
const indirectBumps = bumps.filter((b) => b.reason === 'Dependent')
15070

15171
lines.push(
152-
`**${explicitBumps.length}** package(s) bumped directly, **${dependencyBumps.length}** bumped as dependents.`,
72+
`**${directBumps.length}** package(s) bumped directly, **${indirectBumps.length}** bumped as dependents.`,
15373
)
15474
lines.push('')
15575

156-
if (explicitBumps.length > 0) {
157-
lines.push('### Direct bumps')
76+
if (majorBumps.length > 0) {
77+
lines.push('### 🟥 Major bumps')
78+
lines.push('')
79+
lines.push('| Package | Version | Reason |')
80+
lines.push('| --- | --- | --- |')
81+
for (const b of majorBumps) {
82+
lines.push(
83+
`| \`${b.name}\` | ${b.oldVersion}${b.newVersion} | ${b.reason} |`,
84+
)
85+
}
86+
lines.push('')
87+
}
88+
89+
if (minorBumps.length > 0) {
90+
lines.push('### 🟨 Minor bumps')
15891
lines.push('')
159-
lines.push('| Package | Bump | Version |')
92+
lines.push('| Package | Version | Reason |')
16093
lines.push('| --- | --- | --- |')
161-
for (const b of explicitBumps) {
94+
for (const b of minorBumps) {
16295
lines.push(
163-
`| \`${b.name}\` | **${b.bump}** | ${b.oldVersion} ${b.newVersion} |`,
96+
`| \`${b.name}\` | ${b.oldVersion} ${b.newVersion} | ${b.reason} |`,
16497
)
16598
}
16699
lines.push('')
167100
}
168101

169-
if (dependencyBumps.length > 0) {
170-
lines.push(
171-
'<details>',
172-
`<summary>Dependency bumps (${dependencyBumps.length})</summary>`,
173-
'',
174-
'| Package | Bump | Version |',
175-
'| --- | --- | --- |',
176-
)
177-
for (const b of dependencyBumps) {
102+
if (patchBumps.length > 0) {
103+
lines.push('### 🟩 Patch bumps')
104+
lines.push('')
105+
lines.push('| Package | Version | Reason |')
106+
lines.push('| --- | --- | --- |')
107+
for (const b of patchBumps) {
178108
lines.push(
179-
`| \`${b.name}\` | ${b.bump} | ${b.oldVersion} ${b.newVersion} |`,
109+
`| \`${b.name}\` | ${b.oldVersion} ${b.newVersion} | ${b.reason} |`,
180110
)
181111
}
182-
lines.push('', '</details>')
183112
}
184113
}
185114

.github/changeset-preview/upsert-pr-comment.mjs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,6 @@ async function main() {
111111
const rawBody = await fsp.readFile(bodyPath, 'utf8')
112112
const body = `${args.marker}\n## 🚀 Changeset Version Preview\n\n${rawBody}`
113113

114-
process.stdout.write(body)
115-
116114
const comments = await listIssueComments({
117115
apiUrl: args.apiUrl,
118116
token: args.token,

0 commit comments

Comments
 (0)