From 52f3ec871aa58e1ae7d0025d9ae89f1a86bd8efa Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 24 Jun 2026 10:11:00 -0500 Subject: [PATCH 1/2] fix release script --- .github/workflows/release.yml | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e9f7ed..487f894 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,15 +19,18 @@ concurrency: cancel-in-progress: false permissions: - id-token: write - contents: write - pull-requests: write - issues: write + contents: read jobs: release: name: Release runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + pull-requests: write + issues: write + # do not attempt running in forks if: github.repository_owner == 'vercel-labs' steps: - name: Configure Git @@ -50,17 +53,20 @@ jobs: node-version-file: .nvmrc cache: pnpm + # Double install: @konsistent/common-conventions's build script invokes the + # `konsistent-convention` CLI shipped by @konsistent/convention. The bin + # symlink can only be created once @konsistent/convention/dist/cli.js + # exists, so we install, build the producer, wipe node_modules, then + # reinstall to link the bin. pnpm 11's warm-reinstall skips re-linking + # if node_modules looks current (--force isn't enough), so the wipe is + # required. Outside this monorepo, npm/pnpm consumers install the + # already-built tarball and never hit this. - name: Install Dependencies - run: pnpm install --frozen-lockfile - - # The @konsistent/common-conventions build script invokes the konsistent-convention - # CLI shipped by @konsistent/convention, whose bin symlink only materializes after a - # build. See .github/workflows/ci.yml for the same workaround. - - name: Pre-build convention package - run: pnpm -F @konsistent/convention build - - - name: Re-install to link convention bin - run: pnpm install --frozen-lockfile + run: | + pnpm install --frozen-lockfile + pnpm -F @konsistent/convention build + rm -rf node_modules packages/*/node_modules e2e/fixtures/*/node_modules + pnpm install --frozen-lockfile # Normal release path - name: Create Release Pull Request or Publish to npm From ac8b61dfec115075e09493eeccc6fb9cdf90c991 Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Wed, 24 Jun 2026 11:23:27 -0500 Subject: [PATCH 2/2] require major changeset when package version is 0.0.0 --- tools/scripts/verify-changesets.mjs | 83 +++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/tools/scripts/verify-changesets.mjs b/tools/scripts/verify-changesets.mjs index 0b7f331..5a481b1 100644 --- a/tools/scripts/verify-changesets.mjs +++ b/tools/scripts/verify-changesets.mjs @@ -2,7 +2,6 @@ import fs from "node:fs/promises"; import path from "node:path"; const ROOT = path.resolve(import.meta.dirname, "..", ".."); -const ALLOWED_BUMPS = new Set(["patch"]); const NEWLINE_RE = /\r?\n/; const FRONTMATTER_RE = /^---\n([\s\S]+?)\n---/; const QUOTE_RE = /^['"]|['"]$/g; @@ -23,8 +22,54 @@ async function writeSummary(body) { await fs.appendFile(file, `${body}\n`); } -async function parseChangeset(relativePath) { - const absolutePath = path.join(ROOT, relativePath); +async function getPackageVersions({ root }) { + const packagesPath = path.join(root, "packages"); + let packageDirs; + try { + packageDirs = await fs.readdir(packagesPath, { withFileTypes: true }); + } catch (error) { + if (error && typeof error === "object" && error.code === "ENOENT") { + return new Map(); + } + throw error; + } + const versions = new Map(); + for (const packageDir of packageDirs) { + if (!packageDir.isDirectory()) { + continue; + } + const packageJsonPath = path.join( + packagesPath, + packageDir.name, + "package.json" + ); + let packageJson; + try { + packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8")); + } catch (error) { + if (error && typeof error === "object" && error.code === "ENOENT") { + continue; + } + throw error; + } + if ( + packageJson && + typeof packageJson.name === "string" && + typeof packageJson.version === "string" + ) { + versions.set(packageJson.name, packageJson.version); + } + } + return versions; +} + +function isInvalidBump({ bump, packageVersions, pkg }) { + const expected = packageVersions.get(pkg) === "0.0.0" ? "major" : "patch"; + return bump !== expected; +} + +async function parseChangeset({ relativePath, root }) { + const absolutePath = path.join(root, relativePath); const stat = await fs.lstat(absolutePath); if (stat.isSymbolicLink()) { throw new Error( @@ -65,11 +110,13 @@ async function parseChangeset(relativePath) { return bumps; } -async function main() { - const packageFiles = splitLines(process.env.PACKAGE_FILES); - const addedChangesets = splitLines(process.env.ADDED_CHANGESETS); - const allChangedChangesets = splitLines(process.env.ALL_CHANGED_CHANGESETS); - +async function verifyChangesets({ + addedChangesets, + allChangedChangesets, + packageFiles, + root, +}) { + const packageVersions = await getPackageVersions({ root }); const errors = []; if (packageFiles.length > 0 && addedChangesets.length === 0) { @@ -91,9 +138,9 @@ async function main() { continue; } try { - const bumps = await parseChangeset(file); - const invalid = Object.entries(bumps).filter( - ([, bump]) => !ALLOWED_BUMPS.has(bump) + const bumps = await parseChangeset({ relativePath: file, root }); + const invalid = Object.entries(bumps).filter(([pkg, bump]) => + isInvalidBump({ bump, packageVersions, pkg }) ); if (invalid.length > 0) { errors.push( @@ -110,6 +157,20 @@ async function main() { } } + return errors; +} + +async function main() { + const packageFiles = splitLines(process.env.PACKAGE_FILES); + const addedChangesets = splitLines(process.env.ADDED_CHANGESETS); + const allChangedChangesets = splitLines(process.env.ALL_CHANGED_CHANGESETS); + const errors = await verifyChangesets({ + addedChangesets, + allChangedChangesets, + packageFiles, + root: ROOT, + }); + if (errors.length > 0) { await writeSummary( `## Changeset verification failed\n\n${errors.join("\n\n")}`