mirror of
https://github.com/changesets/action
synced 2026-10-26 16:39:56 +08:00
Add select-mode, version, pack and publish sub-actions (#656)
* Add select-mode, version, and publish sub-actions * Rename * No github-token needed for select-mode for now * extend select-mode with publish-plan check and upload * Rename base-branch to pr-base-branch * add `pack` subaction * `--from-pack` support in the publish subaction * rename CLI arg * update rolldown config * rename pack dir input/output * tweak retendion days --------- Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
This commit is contained in:
co-authored by
Mateusz Burzyński
parent
5649ff41b8
commit
a12d90de83
@@ -0,0 +1,82 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import artifact from "@actions/artifact";
|
||||
import * as core from "@actions/core";
|
||||
import { downloadArtifact, execChangesetsCli } from "../utils.ts";
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (err) {
|
||||
core.setFailed((err as Error).message);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const publishPlanArtifactId = core.getInput("publish-plan-artifact-id");
|
||||
|
||||
// If the user needs to change the cwd, set `working-directory` in the step instead
|
||||
const cwd = process.cwd();
|
||||
const tmpDir = process.env.RUNNER_TEMP ?? (await fs.realpath(os.tmpdir()));
|
||||
const outDir = path.join(tmpDir, `changeset-pack-${Date.now()}`);
|
||||
|
||||
await pack(cwd, {
|
||||
outDir,
|
||||
publishPlanPath: publishPlanArtifactId
|
||||
? await downloadPublishPlanArtifact(tmpDir, Number(publishPlanArtifactId))
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const packDirArtifact = await artifact.uploadArtifact(
|
||||
`changeset-pack-${Date.now()}`,
|
||||
await getFiles(outDir),
|
||||
outDir,
|
||||
{
|
||||
retentionDays: 30,
|
||||
},
|
||||
);
|
||||
if (packDirArtifact.id === undefined) {
|
||||
throw new Error("Packed artifact upload did not return an artifact id");
|
||||
}
|
||||
core.setOutput("pack-dir-artifact-id", String(packDirArtifact.id));
|
||||
}
|
||||
|
||||
async function pack(
|
||||
cwd: string,
|
||||
args: {
|
||||
outDir: string;
|
||||
publishPlanPath?: string;
|
||||
},
|
||||
) {
|
||||
const cliArgs = ["pack", "--out-dir", args.outDir];
|
||||
if (args.publishPlanPath) {
|
||||
cliArgs.push("--from-plan", args.publishPlanPath);
|
||||
}
|
||||
|
||||
await execChangesetsCli(cliArgs, {
|
||||
cwd,
|
||||
env: process.env,
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadPublishPlanArtifact(tmpDir: string, artifactId: number) {
|
||||
const downloadPath = await downloadArtifact(
|
||||
tmpDir,
|
||||
artifactId,
|
||||
"changeset-publish-plan",
|
||||
);
|
||||
return path.join(downloadPath, "publish-plan.json");
|
||||
}
|
||||
|
||||
async function getFiles(dir: string): Promise<string[]> {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
const files = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const entryPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
return getFiles(entryPath);
|
||||
}
|
||||
return [entryPath];
|
||||
}),
|
||||
);
|
||||
return files.flat();
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import * as core from "@actions/core";
|
||||
import { Git } from "../git.ts";
|
||||
import { setupOctokit } from "../octokit.ts";
|
||||
import { runPublish } from "../run.ts";
|
||||
import { downloadArtifact } from "../utils.ts";
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (err) {
|
||||
core.setFailed((err as Error).message);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const githubToken = core.getInput("github-token", { required: true });
|
||||
const script = core.getInput("script");
|
||||
const packDirArtifactId = core.getInput("pack-dir-artifact-id");
|
||||
const createGithubReleases = core.getBooleanInput("create-github-releases");
|
||||
|
||||
// If the user needs to change the cwd, set `working-directory` in the step instead
|
||||
const cwd = process.cwd();
|
||||
|
||||
const octokit = setupOctokit(githubToken);
|
||||
// NOTE: Always pass octokit here as publish does not need a commit-mode
|
||||
const git = new Git({ octokit, cwd });
|
||||
|
||||
const fromPackDir = packDirArtifactId
|
||||
? await downloadArtifact(
|
||||
process.env.RUNNER_TEMP ?? (await fs.realpath(os.tmpdir())),
|
||||
Number(packDirArtifactId),
|
||||
"changeset-pack",
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const result = await runPublish({
|
||||
script,
|
||||
githubToken,
|
||||
git,
|
||||
octokit,
|
||||
createGithubReleases,
|
||||
cwd,
|
||||
fromPackDir,
|
||||
});
|
||||
|
||||
if (result.published) {
|
||||
core.setOutput("published", "true");
|
||||
core.setOutput(
|
||||
"published-packages",
|
||||
JSON.stringify(result.publishedPackages),
|
||||
);
|
||||
} else {
|
||||
core.setOutput("published", "false");
|
||||
}
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Publish command exited with code ${result.exitCode}${
|
||||
result.published
|
||||
? `, but some packages were published: ${result.publishedPackages
|
||||
.map((p) => `${p.name}@${p.version}`)
|
||||
.join(", ")}`
|
||||
: ""
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+32
-20
@@ -1,8 +1,12 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import * as core from "@actions/core";
|
||||
import { exec, getExecOutput } from "@actions/exec";
|
||||
import {
|
||||
exec,
|
||||
getExecOutput,
|
||||
type ExecOptions,
|
||||
type ExecOutput,
|
||||
} from "@actions/exec";
|
||||
import * as github from "@actions/github";
|
||||
import type { PreState } from "@changesets/types";
|
||||
import { type Package, getPackages } from "@manypkg/get-packages";
|
||||
@@ -10,15 +14,15 @@ import { Git } from "./git.ts";
|
||||
import type { Octokit } from "./octokit.ts";
|
||||
import readChangesetState from "./readChangesetState.ts";
|
||||
import {
|
||||
execChangesetsCli,
|
||||
getChangedPackages,
|
||||
getChangelogEntry,
|
||||
getExecOutputChangesetsCli,
|
||||
getVersionsByDirectory,
|
||||
isErrorWithCode,
|
||||
sortTheThings,
|
||||
} from "./utils.ts";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
// GitHub Issues/PRs messages have a max size limit on the
|
||||
// message body payload.
|
||||
// `body is too long (maximum is 65536 characters)`.
|
||||
@@ -58,7 +62,8 @@ const createRelease = async (
|
||||
};
|
||||
|
||||
type PublishOptions = {
|
||||
script: string;
|
||||
script?: string;
|
||||
fromPackDir?: string;
|
||||
githubToken: string;
|
||||
octokit: Octokit;
|
||||
createGithubReleases: boolean;
|
||||
@@ -81,17 +86,36 @@ type PublishResult =
|
||||
|
||||
export async function runPublish({
|
||||
script,
|
||||
fromPackDir,
|
||||
githubToken,
|
||||
git,
|
||||
octokit,
|
||||
createGithubReleases,
|
||||
cwd,
|
||||
}: PublishOptions): Promise<PublishResult> {
|
||||
let changesetPublishOutput = await getExecOutput(script, undefined, {
|
||||
let changesetPublishOutput: ExecOutput;
|
||||
const execOptions: ExecOptions = {
|
||||
cwd,
|
||||
ignoreReturnCode: true,
|
||||
env: { ...process.env, GITHUB_TOKEN: githubToken },
|
||||
});
|
||||
};
|
||||
|
||||
if (script) {
|
||||
changesetPublishOutput = await getExecOutput(
|
||||
script,
|
||||
undefined,
|
||||
execOptions,
|
||||
);
|
||||
} else {
|
||||
const args = ["publish"];
|
||||
if (fromPackDir) {
|
||||
args.push("--from-pack-dir", fromPackDir);
|
||||
}
|
||||
changesetPublishOutput = await getExecOutputChangesetsCli(
|
||||
args,
|
||||
execOptions,
|
||||
);
|
||||
}
|
||||
|
||||
let { packages, tool } = await getPackages(cwd);
|
||||
let releasedPackages: Package[] = [];
|
||||
@@ -277,19 +301,7 @@ export async function runVersion({
|
||||
if (script) {
|
||||
await exec(script, undefined, { cwd, env });
|
||||
} else {
|
||||
await exec(
|
||||
"node",
|
||||
[
|
||||
require.resolve("@changesets/cli/bin.js", {
|
||||
paths: [cwd],
|
||||
}),
|
||||
"version",
|
||||
],
|
||||
{
|
||||
cwd,
|
||||
env,
|
||||
},
|
||||
);
|
||||
await execChangesetsCli(["version"], { cwd, env });
|
||||
}
|
||||
|
||||
let changedPackages = await getChangedPackages(cwd, versionsByDirectory);
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
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";
|
||||
|
||||
type ModeResult =
|
||||
| {
|
||||
mode: "none";
|
||||
}
|
||||
| {
|
||||
mode: "version";
|
||||
}
|
||||
| {
|
||||
mode: "publish";
|
||||
publishPlanPath: string;
|
||||
};
|
||||
|
||||
type PublishPlan = unknown[];
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (err) {
|
||||
core.setFailed((err as Error).message);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const result = await getMode();
|
||||
core.setOutput("mode", result.mode);
|
||||
if (result.mode === "publish") {
|
||||
const publishPlanArtifact = await artifact.uploadArtifact(
|
||||
path.basename(result.publishPlanPath, ".json"),
|
||||
[result.publishPlanPath],
|
||||
path.dirname(result.publishPlanPath),
|
||||
{
|
||||
skipArchive: true,
|
||||
retentionDays: 30,
|
||||
},
|
||||
);
|
||||
if (publishPlanArtifact.id === undefined) {
|
||||
throw new Error(
|
||||
"Publish plan artifact upload did not return an artifact id",
|
||||
);
|
||||
}
|
||||
core.setOutput("publish-plan-artifact-id", String(publishPlanArtifact.id));
|
||||
}
|
||||
}
|
||||
|
||||
async function getMode(): Promise<ModeResult> {
|
||||
const { changesets } = await readChangesetState();
|
||||
|
||||
if (changesets.length > 0) {
|
||||
const hasNonEmptyChangesets = changesets.some(
|
||||
(changeset) => changeset.releases.length > 0,
|
||||
);
|
||||
if (hasNonEmptyChangesets) {
|
||||
return { mode: "version" };
|
||||
}
|
||||
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()}.json`,
|
||||
);
|
||||
await execChangesetsCli(["publish-plan", "--output", publishPlanPath], {
|
||||
cwd,
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
const publishPlan = await readPublishPlan(publishPlanPath);
|
||||
if (publishPlan.length === 0) {
|
||||
return { mode: "none" };
|
||||
}
|
||||
|
||||
return {
|
||||
mode: "publish",
|
||||
publishPlanPath,
|
||||
};
|
||||
}
|
||||
|
||||
async function readPublishPlan(publishPlanPath: string): Promise<PublishPlan> {
|
||||
let rawPlan: string;
|
||||
try {
|
||||
rawPlan = await fs.readFile(publishPlanPath, "utf8");
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to read publish plan at ${publishPlanPath}`, {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
|
||||
let plan: unknown;
|
||||
try {
|
||||
plan = JSON.parse(rawPlan);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to parse publish plan at ${publishPlanPath}`, {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
typeof plan !== "object" ||
|
||||
plan === null ||
|
||||
!("version" in plan) ||
|
||||
typeof plan.version !== "number" ||
|
||||
!("plan" in plan) ||
|
||||
!Array.isArray(plan.plan)
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid publish plan at ${publishPlanPath}: expected { version: number; plan: unknown[] }`,
|
||||
);
|
||||
}
|
||||
return plan.plan;
|
||||
}
|
||||
@@ -1,6 +1,17 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import artifact from "@actions/artifact";
|
||||
import {
|
||||
exec,
|
||||
getExecOutput,
|
||||
type ExecOptions as ActionsExecOptions,
|
||||
type ExecOutput,
|
||||
} from "@actions/exec";
|
||||
import { getPackages, type Package } from "@manypkg/get-packages";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
export const BumpLevels = {
|
||||
dep: 0,
|
||||
patch: 1,
|
||||
@@ -111,3 +122,57 @@ export function fileExists(filePath: string) {
|
||||
() => false,
|
||||
);
|
||||
}
|
||||
|
||||
function resolveChangesetsCli(cwd: string) {
|
||||
return require.resolve("@changesets/cli/bin.js", {
|
||||
paths: [cwd],
|
||||
});
|
||||
}
|
||||
|
||||
interface ExecOptions extends Omit<ActionsExecOptions, "env"> {
|
||||
env?: Record<string, string | undefined>;
|
||||
}
|
||||
|
||||
export function execChangesetsCli(args: string[], options?: ExecOptions) {
|
||||
return exec(
|
||||
"node",
|
||||
[resolveChangesetsCli(options?.cwd ?? process.cwd()), ...args],
|
||||
options as ActionsExecOptions,
|
||||
);
|
||||
}
|
||||
|
||||
export function getExecOutputChangesetsCli(
|
||||
args: string[],
|
||||
options?: ExecOptions,
|
||||
): Promise<ExecOutput> {
|
||||
return getExecOutput(
|
||||
"node",
|
||||
[resolveChangesetsCli(options?.cwd ?? process.cwd()), ...args],
|
||||
options as ActionsExecOptions,
|
||||
);
|
||||
}
|
||||
|
||||
export async function downloadArtifact(
|
||||
tmpDir: string,
|
||||
artifactId: number,
|
||||
name: string,
|
||||
) {
|
||||
if (!Number.isInteger(artifactId) || artifactId <= 0) {
|
||||
throw new Error(
|
||||
`Invalid ${JSON.stringify(name)} artifact id: ${artifactId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const downloadPath = path.join(tmpDir, `${name}-${artifactId}-${Date.now()}`);
|
||||
const result = await artifact.downloadArtifact(artifactId, {
|
||||
path: downloadPath,
|
||||
});
|
||||
|
||||
if (!result.downloadPath) {
|
||||
throw new Error(
|
||||
`${JSON.stringify(name)} artifact download did not return a path for artifact ${artifactId}`,
|
||||
);
|
||||
}
|
||||
|
||||
return result.downloadPath;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as core from "@actions/core";
|
||||
import { Git } from "../git.ts";
|
||||
import { setupOctokit } from "../octokit.ts";
|
||||
import { runVersion } from "../run.ts";
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (err) {
|
||||
core.setFailed((err as Error).message);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const githubToken = core.getInput("github-token", { required: true });
|
||||
const script = core.getInput("script");
|
||||
const commitMessage = core.getInput("commit-message", { required: true });
|
||||
const prTitle = core.getInput("pr-title", { required: true });
|
||||
const prDraft = core.getInput("pr-draft") || undefined;
|
||||
const prBaseBranch = core.getInput("pr-base-branch");
|
||||
const commitMode = core.getInput("commit-mode") || "git-cli";
|
||||
const setupGitUser = core.getBooleanInput("setup-git-user");
|
||||
|
||||
// Validations
|
||||
if (prDraft !== undefined && prDraft !== "always" && prDraft !== "create") {
|
||||
throw new Error(`Invalid pr-draft input: ${prDraft}`);
|
||||
}
|
||||
|
||||
// If the user needs to change the cwd, set `working-directory` in the step instead
|
||||
const cwd = process.cwd();
|
||||
|
||||
const octokit = setupOctokit(githubToken);
|
||||
const git = new Git({
|
||||
octokit: commitMode === "github-api" ? octokit : undefined,
|
||||
cwd,
|
||||
});
|
||||
|
||||
if (setupGitUser) {
|
||||
core.info("setting git user");
|
||||
await git.setupUser();
|
||||
}
|
||||
|
||||
const { pullRequestNumber } = await runVersion({
|
||||
script,
|
||||
githubToken,
|
||||
git,
|
||||
octokit,
|
||||
cwd,
|
||||
prTitle,
|
||||
commitMessage,
|
||||
// TODO: Use neutral message for PR description
|
||||
hasPublishScript: true,
|
||||
prDraft,
|
||||
branch: prBaseBranch,
|
||||
});
|
||||
|
||||
core.setOutput("pr-number", String(pullRequestNumber));
|
||||
}
|
||||
Reference in New Issue
Block a user