diff --git a/.github/changeset-preview/action.yml b/.github/changeset-preview/action.yml index 8a32dde..0248f81 100644 --- a/.github/changeset-preview/action.yml +++ b/.github/changeset-preview/action.yml @@ -3,6 +3,10 @@ description: Generates comment on a PR showing expected version impact runs: using: composite steps: + - name: Install dependencies + shell: bash + run: npm install + working-directory: ${{ github.action_path }} - name: Preview version bumps shell: bash run: node ${{ github.action_path }}/preview-changeset-versions.mjs --output /tmp/changeset-preview.md diff --git a/.github/changeset-preview/package.json b/.github/changeset-preview/package.json new file mode 100644 index 0000000..d388a20 --- /dev/null +++ b/.github/changeset-preview/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@changesets/get-release-plan": "^4.0.15" + } +} diff --git a/.github/changeset-preview/preview-changeset-versions.mjs b/.github/changeset-preview/preview-changeset-versions.mjs index 2569314..fd63185 100644 --- a/.github/changeset-preview/preview-changeset-versions.mjs +++ b/.github/changeset-preview/preview-changeset-versions.mjs @@ -1,81 +1,21 @@ #!/usr/bin/env node /** - * Preview the version bumps that `changeset version` will produce. - * - * Workflow: - * 1. Snapshot every workspace package's current version - * 2. Run `changeset version` (mutates package.json files) - * 3. Diff against the snapshot - * 4. Print a markdown summary (or write to --output file) - * - * This script is meant to run in CI on a disposable checkout — it does NOT - * revert the changes it makes. + * Uses `@changesets/get-release-plan` to get the version bumps and formats it as markdown. */ -import { execSync } from 'node:child_process' -import { readdirSync, readFileSync, writeFileSync } from 'node:fs' -import { join, resolve } from 'node:path' +import { writeFileSync } from 'node:fs' +import { resolve } from 'node:path' import { parseArgs } from 'node:util' +import getReleasePlan from '@changesets/get-release-plan' const ROOT = resolve(import.meta.dirname, '..', '..') -const PACKAGES_DIR = join(ROOT, 'packages') - -function readPackageVersions() { - const versions = new Map() - for (const dir of readdirSync(PACKAGES_DIR, { withFileTypes: true })) { - if (!dir.isDirectory()) continue - const pkgPath = join(PACKAGES_DIR, dir.name, 'package.json') - try { - const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) - if (pkg.name && pkg.version && pkg.private !== true) { - versions.set(pkg.name, pkg.version) - } - } catch { - // skip packages without a valid package.json - } - } - return versions +function reasonRank(reason) { + return reason === 'Changeset' ? 2 : 1 } -function readChangesetEntries() { - const changesetDir = join(ROOT, '.changeset') - const explicit = new Map() - for (const file of readdirSync(changesetDir)) { - if (file === 'config.json' || file === 'README.md' || !file.endsWith('.md')) - continue - const content = readFileSync(join(changesetDir, file), 'utf8') - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/) - if (!frontmatterMatch) continue - for (const line of frontmatterMatch[1].split('\n')) { - const match = line.match(/^['"]?([^'"]+)['"]?\s*:\s*(major|minor|patch)/) - if (match) { - const [, name, bump] = match - const existing = explicit.get(name) - // keep the highest bump if a package appears in multiple changesets - if (!existing || bumpRank(bump) > bumpRank(existing)) { - explicit.set(name, bump) - } - } - } - } - return explicit -} - -function bumpRank(bump) { - return bump === 'major' ? 3 : bump === 'minor' ? 2 : 1 -} - -function bumpType(oldVersion, newVersion) { - const [oMaj, oMin] = oldVersion.split('.').map(Number) - const [nMaj, nMin] = newVersion.split('.').map(Number) - if (nMaj > oMaj) return 'major' - if (nMin > oMin) return 'minor' - return 'patch' -} - -function main() { +async function main() { const { values } = parseArgs({ args: process.argv.slice(2), options: { @@ -85,10 +25,10 @@ function main() { allowPositionals: false, }) - // 1. Read explicit changeset entries - const explicit = readChangesetEntries() + const releasePlan = await getReleasePlan(ROOT) + const releases = releasePlan.releases - if (explicit.size === 0) { + if (releases.length === 0) { const msg = 'No changeset entries found. Merging this PR will not cause a version bump for any packages.\n' process.stdout.write(msg) @@ -99,42 +39,19 @@ function main() { return } - // 2. Snapshot current versions - const before = readPackageVersions() - - // 3. Temporarily swap changeset config to skip changelog generation - // (the GitHub changelog plugin requires a token we don't need for previews) - const configPath = join(ROOT, '.changeset', 'config.json') - const originalConfig = readFileSync(configPath, 'utf8') - try { - const config = JSON.parse(originalConfig) - config.changelog = false - writeFileSync(configPath, JSON.stringify(config, null, 2)) - - // 4. Run changeset version - execSync('pnpm changeset version', { cwd: ROOT, stdio: 'pipe' }) - } finally { - // Always restore the original config - writeFileSync(configPath, originalConfig) - } - - // 5. Read new versions - const after = readPackageVersions() - // 6. Diff const bumps = [] - for (const [name, newVersion] of after) { - const oldVersion = before.get(name) - if (!oldVersion || oldVersion === newVersion) continue - const bump = bumpType(oldVersion, newVersion) - const source = explicit.has(name) ? explicit.get(name) : 'dependency' - bumps.push({ name, oldVersion, newVersion, bump, source }) + for (const release of releases) { + if (release.oldVersion === release.newVersion) continue + const reason = release.changesets.length !== 0 ? 'Changeset' : 'Dependent' + bumps.push({ ...release, reason }) } - // Sort: major first, then minor, then patch; within each group alphabetical + // Order by reason and name bumps.sort( (a, b) => - bumpRank(b.bump) - bumpRank(a.bump) || a.name.localeCompare(b.name), + reasonRank(b.reason) - reasonRank(a.reason) || + a.name.localeCompare(b.name), ) // 7. Build markdown @@ -145,41 +62,53 @@ function main() { 'No version changes detected. Merging this PR will not cause a version bump for any packages.', ) } else { - const explicitBumps = bumps.filter((b) => b.source !== 'dependency') - const dependencyBumps = bumps.filter((b) => b.source === 'dependency') + const majorBumps = bumps.filter((b) => b.type === 'major') + const minorBumps = bumps.filter((b) => b.type === 'minor') + const patchBumps = bumps.filter((b) => b.type === 'patch') + const directBumps = bumps.filter((b) => b.reason === 'Changeset') + const indirectBumps = bumps.filter((b) => b.reason === 'Dependent') lines.push( - `**${explicitBumps.length}** package(s) bumped directly, **${dependencyBumps.length}** bumped as dependents.`, + `**${directBumps.length}** package(s) bumped directly, **${indirectBumps.length}** bumped as dependents.`, ) lines.push('') - if (explicitBumps.length > 0) { - lines.push('### Direct bumps') + if (majorBumps.length > 0) { + lines.push('### 🟥 Major bumps') + lines.push('') + lines.push('| Package | Version | Reason |') + lines.push('| --- | --- | --- |') + for (const b of majorBumps) { + lines.push( + `| \`${b.name}\` | ${b.oldVersion} → ${b.newVersion} | ${b.reason} |`, + ) + } + lines.push('') + } + + if (minorBumps.length > 0) { + lines.push('### 🟨 Minor bumps') lines.push('') - lines.push('| Package | Bump | Version |') + lines.push('| Package | Version | Reason |') lines.push('| --- | --- | --- |') - for (const b of explicitBumps) { + for (const b of minorBumps) { lines.push( - `| \`${b.name}\` | **${b.bump}** | ${b.oldVersion} → ${b.newVersion} |`, + `| \`${b.name}\` | ${b.oldVersion} → ${b.newVersion} | ${b.reason} |`, ) } lines.push('') } - if (dependencyBumps.length > 0) { - lines.push( - '
', - `Dependency bumps (${dependencyBumps.length})`, - '', - '| Package | Bump | Version |', - '| --- | --- | --- |', - ) - for (const b of dependencyBumps) { + if (patchBumps.length > 0) { + lines.push('### 🟩 Patch bumps') + lines.push('') + lines.push('| Package | Version | Reason |') + lines.push('| --- | --- | --- |') + for (const b of patchBumps) { lines.push( - `| \`${b.name}\` | ${b.bump} | ${b.oldVersion} → ${b.newVersion} |`, + `| \`${b.name}\` | ${b.oldVersion} → ${b.newVersion} | ${b.reason} |`, ) } - lines.push('', '
') } } diff --git a/.github/changeset-preview/upsert-pr-comment.mjs b/.github/changeset-preview/upsert-pr-comment.mjs index 7d10630..cb914c6 100644 --- a/.github/changeset-preview/upsert-pr-comment.mjs +++ b/.github/changeset-preview/upsert-pr-comment.mjs @@ -111,8 +111,6 @@ async function main() { const rawBody = await fsp.readFile(bodyPath, 'utf8') const body = `${args.marker}\n## 🚀 Changeset Version Preview\n\n${rawBody}` - process.stdout.write(body) - const comments = await listIssueComments({ apiUrl: args.apiUrl, token: args.token,