mirror of
https://github.com/changesets/action
synced 2026-10-26 16:39:56 +08:00
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:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@changesets/action": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Validate that projects use Changesets CLI v3 and direct Changesets CLI v2 users to `changesets/action@v1`.
|
||||||
+5
-3
@@ -8,9 +8,14 @@ import {
|
|||||||
getOptionalInput,
|
getOptionalInput,
|
||||||
getRequiredInput,
|
getRequiredInput,
|
||||||
throwOnRenamedInputs,
|
throwOnRenamedInputs,
|
||||||
|
validateChangesetsCliVersion,
|
||||||
} from "./utils.ts";
|
} from "./utils.ts";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
// If the user needs to change the cwd, set `working-directory` in the step instead
|
||||||
|
const cwd = process.cwd();
|
||||||
|
await validateChangesetsCliVersion(cwd);
|
||||||
|
|
||||||
throwOnRenamedInputs({
|
throwOnRenamedInputs({
|
||||||
publish: "publish-script",
|
publish: "publish-script",
|
||||||
version: "version-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 commitMode = getOptionalInput("commit-mode") ?? "git-cli";
|
||||||
const prDraft = getOptionalInput("pr-draft");
|
const prDraft = getOptionalInput("pr-draft");
|
||||||
if (commitMode !== "git-cli" && commitMode !== "github-api") {
|
if (commitMode !== "git-cli" && commitMode !== "github-api") {
|
||||||
|
|||||||
+5
-2
@@ -7,6 +7,7 @@ import {
|
|||||||
downloadArtifact,
|
downloadArtifact,
|
||||||
execChangesetsCli,
|
execChangesetsCli,
|
||||||
getOptionalInput,
|
getOptionalInput,
|
||||||
|
validateChangesetsCliVersion,
|
||||||
} from "../utils.ts";
|
} from "../utils.ts";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -16,10 +17,12 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
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
|
// If the user needs to change the cwd, set `working-directory` in the step instead
|
||||||
const cwd = process.cwd();
|
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 tmpDir = process.env.RUNNER_TEMP ?? (await fs.realpath(os.tmpdir()));
|
||||||
const outDir = path.join(tmpDir, `changeset-pack-${Date.now()}`);
|
const outDir = path.join(tmpDir, `changeset-pack-${Date.now()}`);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
downloadArtifact,
|
downloadArtifact,
|
||||||
getOptionalInput,
|
getOptionalInput,
|
||||||
getRequiredInput,
|
getRequiredInput,
|
||||||
|
validateChangesetsCliVersion,
|
||||||
} from "../utils.ts";
|
} from "../utils.ts";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -16,6 +17,10 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
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 githubToken = getRequiredInput("github-token");
|
||||||
const script = getOptionalInput("script");
|
const script = getOptionalInput("script");
|
||||||
const packDirArtifactId = getOptionalInput("pack-dir-artifact-id");
|
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.
|
// NOTE: Always use API mode here as publish does not need a commit-mode.
|
||||||
const github = new GitHub({ cwd, githubToken, commitMode: "github-api" });
|
const github = new GitHub({ cwd, githubToken, commitMode: "github-api" });
|
||||||
|
|
||||||
|
|||||||
+3
-5
@@ -21,10 +21,8 @@ vi.mock("@actions/github", () => ({
|
|||||||
graphql: mockedGraphql,
|
graphql: mockedGraphql,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
vi.mock("@actions/core", async (importOriginal) => ({
|
vi.mock("@actions/core", { spy: true });
|
||||||
...(await importOriginal<typeof import("@actions/core")>()),
|
const mockedWarning = vi.mocked(core.warning);
|
||||||
warning: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("@changesets/ghcommit");
|
vi.mock("@changesets/ghcommit");
|
||||||
|
|
||||||
let mockedGithubMethods = {
|
let mockedGithubMethods = {
|
||||||
@@ -126,7 +124,7 @@ describe("publish", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toEqual({ published: false, exitCode: 0 });
|
expect(result).toEqual({ published: false, exitCode: 0 });
|
||||||
expect(core.warning).toHaveBeenCalledWith(
|
expect(mockedWarning).toHaveBeenCalledWith(
|
||||||
expect.stringContaining(
|
expect.stringContaining(
|
||||||
"GitHub releases and git tags cannot be created without this output",
|
"GitHub releases and git tags cannot be created without this output",
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import path from "node:path";
|
|||||||
import artifact from "@actions/artifact";
|
import artifact from "@actions/artifact";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import readChangesetState from "../readChangesetState.ts";
|
import readChangesetState from "../readChangesetState.ts";
|
||||||
import { execChangesetsCli } from "../utils.ts";
|
import { execChangesetsCli, validateChangesetsCliVersion } from "../utils.ts";
|
||||||
|
|
||||||
type ModeResult =
|
type ModeResult =
|
||||||
| {
|
| {
|
||||||
@@ -27,7 +27,10 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const result = await getMode();
|
const cwd = process.cwd();
|
||||||
|
await validateChangesetsCliVersion(cwd);
|
||||||
|
|
||||||
|
const result = await getMode(cwd);
|
||||||
core.setOutput("mode", result.mode);
|
core.setOutput("mode", result.mode);
|
||||||
if (result.mode === "publish") {
|
if (result.mode === "publish") {
|
||||||
const publishPlanArtifact = await artifact.uploadArtifact(
|
const publishPlanArtifact = await artifact.uploadArtifact(
|
||||||
@@ -48,8 +51,8 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMode(): Promise<ModeResult> {
|
async function getMode(cwd: string): Promise<ModeResult> {
|
||||||
const { changesets } = await readChangesetState();
|
const { changesets } = await readChangesetState(cwd);
|
||||||
|
|
||||||
if (changesets.length > 0) {
|
if (changesets.length > 0) {
|
||||||
const hasNonEmptyChangesets = changesets.some(
|
const hasNonEmptyChangesets = changesets.some(
|
||||||
@@ -61,7 +64,6 @@ async function getMode(): Promise<ModeResult> {
|
|||||||
return { mode: "none" };
|
return { mode: "none" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const publishPlanPath = path.join(
|
const publishPlanPath = path.join(
|
||||||
process.env.RUNNER_TEMP ?? (await fs.realpath(os.tmpdir())),
|
process.env.RUNNER_TEMP ?? (await fs.realpath(os.tmpdir())),
|
||||||
`changeset-publish-plan-${Date.now()}`,
|
`changeset-publish-plan-${Date.now()}`,
|
||||||
|
|||||||
+53
-1
@@ -1,5 +1,11 @@
|
|||||||
|
import { createFixture } from "fs-fixture";
|
||||||
import { expect, test } from "vitest";
|
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
|
let changelog = `# @keystone-alpha/email
|
||||||
|
|
||||||
@@ -100,3 +106,49 @@ test("it sorts the things right", () => {
|
|||||||
];
|
];
|
||||||
expect(things.sort(sortTheThings)).toMatchSnapshot();
|
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.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
type ExecOutput,
|
type ExecOutput,
|
||||||
} from "@actions/exec";
|
} from "@actions/exec";
|
||||||
import { getPackages, type Package } from "@manypkg/get-packages";
|
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);
|
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) {
|
function resolveChangesetsCli(cwd: string) {
|
||||||
return require.resolve("@changesets/cli/bin.js", {
|
return require.resolve("@changesets/cli/bin.js", {
|
||||||
paths: [cwd],
|
paths: [cwd],
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { GitHub } from "../github.ts";
|
import { GitHub } from "../github.ts";
|
||||||
import { runVersion } from "../run.ts";
|
import { runVersion } from "../run.ts";
|
||||||
import { getOptionalInput, getRequiredInput } from "../utils.ts";
|
import {
|
||||||
|
getOptionalInput,
|
||||||
|
getRequiredInput,
|
||||||
|
validateChangesetsCliVersion,
|
||||||
|
} from "../utils.ts";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await main();
|
await main();
|
||||||
@@ -10,6 +14,10 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
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 githubToken = getRequiredInput("github-token");
|
||||||
const script = getOptionalInput("script");
|
const script = getOptionalInput("script");
|
||||||
const commitMessage = getRequiredInput("commit-message");
|
const commitMessage = getRequiredInput("commit-message");
|
||||||
@@ -27,9 +35,6 @@ async function main() {
|
|||||||
throw new Error(`Invalid commit-mode input: ${commitMode}`);
|
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({
|
const github = new GitHub({
|
||||||
cwd,
|
cwd,
|
||||||
githubToken,
|
githubToken,
|
||||||
|
|||||||
Reference in New Issue
Block a user