Validate that projects use Changesets CLI v3 and direct Changesets CLI v2 users to changesets/action@v1 (#699)

* Validate that projects use Changesets CLI v3 and direct Changesets CLI v2 users to `changesets/action@v1`

* better spy/mocking

* fmt

* shuffle
This commit is contained in:
Mateusz Burzyński
2026-07-23 09:34:32 +00:00
committed by GitHub
parent 84d78c68f9
commit 5b307d3df2
9 changed files with 143 additions and 23 deletions
+5 -3
View File
@@ -8,9 +8,14 @@ import {
getOptionalInput,
getRequiredInput,
throwOnRenamedInputs,
validateChangesetsCliVersion,
} from "./utils.ts";
(async () => {
// If the user needs to change the cwd, set `working-directory` in the step instead
const cwd = process.cwd();
await validateChangesetsCliVersion(cwd);
throwOnRenamedInputs({
publish: "publish-script",
version: "version-script",
@@ -32,9 +37,6 @@ import {
);
}
// If the user needs to change the cwd, set `working-directory` in the step instead
const cwd = process.cwd();
const commitMode = getOptionalInput("commit-mode") ?? "git-cli";
const prDraft = getOptionalInput("pr-draft");
if (commitMode !== "git-cli" && commitMode !== "github-api") {
+5 -2
View File
@@ -7,6 +7,7 @@ import {
downloadArtifact,
execChangesetsCli,
getOptionalInput,
validateChangesetsCliVersion,
} from "../utils.ts";
try {
@@ -16,10 +17,12 @@ try {
}
async function main() {
const publishPlanArtifactId = getOptionalInput("publish-plan-artifact-id");
// If the user needs to change the cwd, set `working-directory` in the step instead
const cwd = process.cwd();
await validateChangesetsCliVersion(cwd);
const publishPlanArtifactId = getOptionalInput("publish-plan-artifact-id");
const tmpDir = process.env.RUNNER_TEMP ?? (await fs.realpath(os.tmpdir()));
const outDir = path.join(tmpDir, `changeset-pack-${Date.now()}`);
+5 -3
View File
@@ -7,6 +7,7 @@ import {
downloadArtifact,
getOptionalInput,
getRequiredInput,
validateChangesetsCliVersion,
} from "../utils.ts";
try {
@@ -16,6 +17,10 @@ try {
}
async function main() {
// If the user needs to change the cwd, set `working-directory` in the step instead
const cwd = process.cwd();
await validateChangesetsCliVersion(cwd);
const githubToken = getRequiredInput("github-token");
const script = getOptionalInput("script");
const packDirArtifactId = getOptionalInput("pack-dir-artifact-id");
@@ -30,9 +35,6 @@ async function main() {
);
}
// If the user needs to change the cwd, set `working-directory` in the step instead
const cwd = process.cwd();
// NOTE: Always use API mode here as publish does not need a commit-mode.
const github = new GitHub({ cwd, githubToken, commitMode: "github-api" });
+3 -5
View File
@@ -21,10 +21,8 @@ vi.mock("@actions/github", () => ({
graphql: mockedGraphql,
}),
}));
vi.mock("@actions/core", async (importOriginal) => ({
...(await importOriginal<typeof import("@actions/core")>()),
warning: vi.fn(),
}));
vi.mock("@actions/core", { spy: true });
const mockedWarning = vi.mocked(core.warning);
vi.mock("@changesets/ghcommit");
let mockedGithubMethods = {
@@ -126,7 +124,7 @@ describe("publish", () => {
});
expect(result).toEqual({ published: false, exitCode: 0 });
expect(core.warning).toHaveBeenCalledWith(
expect(mockedWarning).toHaveBeenCalledWith(
expect.stringContaining(
"GitHub releases and git tags cannot be created without this output",
),
+7 -5
View File
@@ -4,7 +4,7 @@ import path from "node:path";
import artifact from "@actions/artifact";
import * as core from "@actions/core";
import readChangesetState from "../readChangesetState.ts";
import { execChangesetsCli } from "../utils.ts";
import { execChangesetsCli, validateChangesetsCliVersion } from "../utils.ts";
type ModeResult =
| {
@@ -27,7 +27,10 @@ try {
}
async function main() {
const result = await getMode();
const cwd = process.cwd();
await validateChangesetsCliVersion(cwd);
const result = await getMode(cwd);
core.setOutput("mode", result.mode);
if (result.mode === "publish") {
const publishPlanArtifact = await artifact.uploadArtifact(
@@ -48,8 +51,8 @@ async function main() {
}
}
async function getMode(): Promise<ModeResult> {
const { changesets } = await readChangesetState();
async function getMode(cwd: string): Promise<ModeResult> {
const { changesets } = await readChangesetState(cwd);
if (changesets.length > 0) {
const hasNonEmptyChangesets = changesets.some(
@@ -61,7 +64,6 @@ async function getMode(): Promise<ModeResult> {
return { mode: "none" };
}
const cwd = process.cwd();
const publishPlanPath = path.join(
process.env.RUNNER_TEMP ?? (await fs.realpath(os.tmpdir())),
`changeset-publish-plan-${Date.now()}`,
+53 -1
View File
@@ -1,5 +1,11 @@
import { createFixture } from "fs-fixture";
import { expect, test } from "vitest";
import { BumpLevels, getChangelogEntry, sortTheThings } from "./utils.ts";
import {
BumpLevels,
getChangelogEntry,
sortTheThings,
validateChangesetsCliVersion,
} from "./utils.ts";
let changelog = `# @keystone-alpha/email
@@ -100,3 +106,49 @@ test("it sorts the things right", () => {
];
expect(things.sort(sortTheThings)).toMatchSnapshot();
});
test("throws when the project declares Changesets CLI v2", async () => {
await using fixture = await createFixture({
"package.json": JSON.stringify({
name: "project",
devDependencies: { "@changesets/cli": "^2.29.7" },
}),
"package-lock.json": "",
});
await expect(validateChangesetsCliVersion(fixture.path)).rejects.toThrow(
"This version of the Changesets action is designed to work with Changesets CLI v3. Changesets CLI v2 is not supported; use Changesets action v1 instead, which is compatible with CLI v2.",
);
});
test("accepts a Changesets CLI v3 contract without an installed CLI", async () => {
await using fixture = await createFixture({
"package.json": JSON.stringify({
name: "project",
devDependencies: { "@changesets/cli": "^3.0.0" },
}),
"package-lock.json": "",
});
await expect(
validateChangesetsCliVersion(fixture.path),
).resolves.toBeUndefined();
});
test("throws when the project has Changesets CLI v2 installed", async () => {
await using fixture = await createFixture({
"package.json": JSON.stringify({
name: "project",
devDependencies: { "@changesets/cli": "^3.0.0" },
}),
"package-lock.json": "",
"node_modules/@changesets/cli/package.json": JSON.stringify({
name: "@changesets/cli",
version: "2.29.7",
}),
});
await expect(validateChangesetsCliVersion(fixture.path)).rejects.toThrow(
"This version of the Changesets action is designed to work with Changesets CLI v3. Changesets CLI v2 is not supported; use Changesets action v1 instead, which is compatible with CLI v2.",
);
});
+51
View File
@@ -10,6 +10,8 @@ import {
type ExecOutput,
} from "@actions/exec";
import { getPackages, type Package } from "@manypkg/get-packages";
import major from "semver/functions/major.js";
import subset from "semver/ranges/subset.js";
const require = createRequire(import.meta.url);
@@ -154,6 +156,55 @@ export function throwOnRenamedInputs(renames: Record<string, string>) {
}
}
const changesetsCliCompatibilityError =
"This version of the Changesets action is designed to work with Changesets CLI v3. " +
"Changesets CLI v2 is not supported; use Changesets action v1 instead, which is compatible with CLI v2.";
export async function validateChangesetsCliVersion(cwd: string) {
const { rootPackage } = await getPackages(cwd);
const packageJson = rootPackage?.packageJson;
const declaredVersion =
packageJson?.devDependencies?.["@changesets/cli"] ??
packageJson?.dependencies?.["@changesets/cli"];
if (typeof declaredVersion === "string") {
const range = declaredVersion.startsWith("workspace:")
? declaredVersion.slice("workspace:".length)
: declaredVersion;
let isV2 = false;
try {
isV2 = subset(range, ">=2.0.0-0 <3.0.0-0", {
includePrerelease: true,
});
} catch {
// it could be a non-semver protocol
}
if (isV2) {
throw new Error(changesetsCliCompatibilityError);
}
}
let cliPackageJson;
try {
cliPackageJson = require(
require.resolve("@changesets/cli/package.json", { paths: [cwd] }),
);
} catch {
return;
}
if (
typeof cliPackageJson.version === "string" &&
major(cliPackageJson.version) === 2
) {
throw new Error(changesetsCliCompatibilityError);
}
}
function resolveChangesetsCli(cwd: string) {
return require.resolve("@changesets/cli/bin.js", {
paths: [cwd],
+9 -4
View File
@@ -1,7 +1,11 @@
import * as core from "@actions/core";
import { GitHub } from "../github.ts";
import { runVersion } from "../run.ts";
import { getOptionalInput, getRequiredInput } from "../utils.ts";
import {
getOptionalInput,
getRequiredInput,
validateChangesetsCliVersion,
} from "../utils.ts";
try {
await main();
@@ -10,6 +14,10 @@ try {
}
async function main() {
// If the user needs to change the cwd, set `working-directory` in the step instead
const cwd = process.cwd();
await validateChangesetsCliVersion(cwd);
const githubToken = getRequiredInput("github-token");
const script = getOptionalInput("script");
const commitMessage = getRequiredInput("commit-message");
@@ -27,9 +35,6 @@ async function main() {
throw new Error(`Invalid commit-mode input: ${commitMode}`);
}
// If the user needs to change the cwd, set `working-directory` in the step instead
const cwd = process.cwd();
const github = new GitHub({
cwd,
githubToken,