Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/eight-clocks-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@changesets/action": minor
---

Add new `/select-mode`, `/version`, and `/publish` sub-actions to better control version and publish steps
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"release:pr": "node --experimental-strip-types ./scripts/release-pr.ts"
},
"dependencies": {
"@actions/artifact": "^6.2.1",
"@actions/core": "^3.0.1",
"@actions/exec": "^3.0.0",
"@actions/github": "^9.1.1",
Expand Down
945 changes: 943 additions & 2 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions publish/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# changesets/action/publish

TODO
26 changes: 26 additions & 0 deletions publish/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Changesets - Publish
description: Publish packages to npm
runs:
using: node24
main: ../dist/publish.js
inputs:
github-token:
description: "The GitHub token to use for authentication. Defaults to the GitHub-provided token."
required: false
default: ${{ github.token }}
script:
description: "The command to use to publish packages"
required: false
create-github-releases:
description: "Whether to create Github releases after publish"
required: false
default: true
outputs:
published:
description: "A boolean value to indicate whether a publishing has happened or not"
publishedPackages:
description: >
A JSON array to present the published packages. The format is `[{"name": "@xx/xx", "version": "1.2.0"}, {"name": "@xx/xy", "version": "0.8.9"}]`
branding:
icon: package
color: blue
3 changes: 3 additions & 0 deletions rolldown.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export default defineConfig({
index: "src/index.ts",
["pr-status"]: "src/pr-status/index.ts",
["pr-comment"]: "src/pr-comment/index.ts",
["select-mode"]: "src/select-mode/index.ts",
version: "src/version/index.ts",
publish: "src/publish/index.ts",
},
output: {
dir: "dist",
Expand Down
3 changes: 3 additions & 0 deletions select-mode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# changesets/action/select-mode

TODO
14 changes: 14 additions & 0 deletions select-mode/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Changesets - Select Mode
description: Whether to version or publish in the current repo state
runs:
using: node24
main: ../dist/select-mode.js
inputs: {}
outputs:
mode:
description: "The mode to use for the current repo state: 'version', 'publish', or 'none'."
publish-plan-artifact-id:
description: "Artifact id for the generated publish plan when mode is `publish`"
branding:
icon: package
color: blue
55 changes: 55 additions & 0 deletions src/publish/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as core from "@actions/core";
import { Git } from "../git.ts";
import { setupOctokit } from "../octokit.ts";
import { runPublish } 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 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 result = await runPublish({
script,
githubToken,
git,
octokit,
createGithubReleases,
cwd,
});

if (result.published) {
core.setOutput("published", "true");
core.setOutput(
"publishedPackages",
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(", ")}`
: ""
}`,
);
process.exit(result.exitCode);
}
}
48 changes: 31 additions & 17 deletions src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,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";
Expand Down Expand Up @@ -58,7 +63,7 @@ const createRelease = async (
};

type PublishOptions = {
script: string;
script?: string;
githubToken: string;
octokit: Octokit;
createGithubReleases: boolean;
Expand Down Expand Up @@ -87,11 +92,29 @@ export async function runPublish({
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 changesetsCliBin = require.resolve("@changesets/cli/bin.js", {
paths: [cwd],
});
changesetPublishOutput = await getExecOutput(
"node",
[changesetsCliBin, "publish"],
execOptions,
);
}

let { packages, tool } = await getPackages(cwd);
let releasedPackages: Package[] = [];
Expand Down Expand Up @@ -277,19 +300,10 @@ 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,
},
);
const changesetsCliBin = require.resolve("@changesets/cli/bin.js", {
paths: [cwd],
});
await exec("node", [changesetsCliBin, "version"], { cwd, env });
}

let changedPackages = await getChangedPackages(cwd, versionsByDirectory);
Expand Down
133 changes: 133 additions & 0 deletions src/select-mode/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import fs from "node:fs/promises";
import { createRequire } from "node:module";
import os from "node:os";
import path from "node:path";
import artifact from "@actions/artifact";
import * as core from "@actions/core";
import { exec } from "tinyexec";
import readChangesetState from "../readChangesetState.ts";

const require = createRequire(import.meta.url);

type ModeResult =
| {
mode: "none";
}
| {
mode: "version";
}
| {
mode: "publish";
publishPlanPath: string;
};

type PublishPlan = {
version: number;
plan: 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,
},
);
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`,
);
const changesetsCliBin = require.resolve("@changesets/cli/bin.js", {
paths: [cwd],
});

await exec(
"node",
[changesetsCliBin, "publish-plan", "--output", publishPlanPath],
{
throwOnError: true,
nodeOptions: { cwd, env: process.env },
},
);

const publishPlan = await readPublishPlan(publishPlanPath);
if (publishPlan.plan.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 {
version: plan.version,
plan: plan.plan,
};
}
56 changes: 56 additions & 0 deletions src/version/index.ts
Original file line number Diff line number Diff line change
@@ -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 baseBranch = core.getInput("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: baseBranch,
});

core.setOutput("pr-number", String(pullRequestNumber));
}
Loading