Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 20 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
83 changes: 72 additions & 11 deletions tools/scripts/verify-changesets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(
Expand Down Expand Up @@ -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) {
Expand All @@ -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(
Expand All @@ -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")}`
Expand Down