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
36 changes: 36 additions & 0 deletions .github/actions/changeset-policy/check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,41 @@ export const validateWorkflowChangelogOwnership = (directory = ".github/workflow
}
};

const BUN_INSTALL = /(?:^|\s)bun\s+(?:install|i)(?:\s|$)/u;
const FROZEN_LOCKFILE = /(?:^|\s)--frozen-lockfile(?:\s|$)/u;
const BUN_LOCKFILE = /(?:^|[\s"'`\\/])bun\.lockb?(?:$|\s|["'`])/u;
const DELETION_COMMAND = /(?:^|\s)(?:rm|unlink|del|remove-item)(?:\s|$)/iu;

export const validateChangesetVersionCommand = (file, command) => {
if (typeof command !== "string" || command.trim().length === 0) {
fail(`${file} must define 'changeset:version' as a non-empty command.`);
}

for (const segment of command.split(/&&|\|\||[;|\n]/u)) {
const normalized = segment.trim();
if (BUN_LOCKFILE.test(normalized) && DELETION_COMMAND.test(normalized)) {
fail(
`${file} 'changeset:version' must not delete bun.lock or bun.lockb. ` +
"Versioning must preserve the committed lockfile.",
);
}
if (BUN_INSTALL.test(normalized) && !FROZEN_LOCKFILE.test(normalized)) {
fail(
`${file} 'changeset:version' must not regenerate the Bun lockfile. ` +
"Remove `bun install`, or use `bun install --frozen-lockfile` when an install is required.",
);
}
}
};

export const validateChangesetVersionPolicy = (file = "package.json") => {
if (!existsSync(file)) return;
const manifest = JSON.parse(readFileSync(file, "utf8"));
const command = manifest?.scripts?.["changeset:version"];
if (command === undefined) return;
validateChangesetVersionCommand(file, command);
};

const run = (command, args) =>
execFileSync(command, args, {
encoding: "utf8",
Expand Down Expand Up @@ -157,6 +192,7 @@ const main = (environment) => {
validateChangeset(file, readFileSync(file, "utf8"), packageNames);
}

validateChangesetVersionPolicy();
validateWorkflowChangelogOwnership();

const runtimeChanged =
Expand Down
50 changes: 50 additions & 0 deletions .github/actions/changeset-policy/check.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
isGeneratedVersionPullRequest,
validateChangelogOwnership,
validateChangeset,
validateChangesetVersionCommand,
} from "./check.mjs";

const packages = new Set(["@stll/core", "plain-package"]);
Expand Down Expand Up @@ -135,3 +136,52 @@ test("does not mistake another job's input for the finalizer setting", () => {
/second, conflicting writer/,
);
});

test("accepts lock-preserving changeset version commands", () => {
validateChangesetVersionCommand(
"package.json",
"changeset version && node scripts/sync-changeset-version.mjs",
);
validateChangesetVersionCommand(
"package.json",
"changeset version && bun install --frozen-lockfile",
);
});

test("rejects deleting a Bun lockfile during changeset versioning", () => {
assert.throws(
() =>
validateChangesetVersionCommand(
"package.json",
"changeset version && rm -f bun.lock && bun install",
),
/must not delete bun\.lock or bun\.lockb/,
);
assert.throws(
() =>
validateChangesetVersionCommand(
"package.json",
"changeset version; Remove-Item ./bun.lockb",
),
/must not delete bun\.lock or bun\.lockb/,
);
});

test("rejects Bun lockfile regeneration during changeset versioning", () => {
assert.throws(
() =>
validateChangesetVersionCommand(
"package.json",
"changeset version && bun install",
),
/must not regenerate the Bun lockfile/,
);
assert.throws(
() =>
validateChangesetVersionCommand(
"package.json",
"bun install --frozen-lockfile && changeset version && bun i",
),
/must not regenerate the Bun lockfile/,
);
});
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,9 @@ jobs:
The caller must provide `changeset`, `changeset:version`, and a `.changeset/config.json`.
For hybrid repositories, `changeset:version` must synchronize the selected package
version into every npm, Cargo, Python, and central `VERSION` surface before the
generated PR is committed.
generated PR is committed. The version command must preserve the committed Bun
lockfile: deleting `bun.lock`/`bun.lockb` or running an unfrozen `bun install` is
rejected by the shared policy.

### Independent npm package releases

Expand Down
Loading