diff --git a/.changeset/eight-clocks-pull.md b/.changeset/eight-clocks-pull.md new file mode 100644 index 00000000..76a279e5 --- /dev/null +++ b/.changeset/eight-clocks-pull.md @@ -0,0 +1,5 @@ +--- +"@changesets/action": minor +--- + +Add new `/select-mode`, `/version`, and `/publish` sub-actions to better control version and publish steps diff --git a/publish/README.md b/publish/README.md new file mode 100644 index 00000000..2850488a --- /dev/null +++ b/publish/README.md @@ -0,0 +1,3 @@ +# changesets/action/publish + +TODO diff --git a/publish/action.yml b/publish/action.yml new file mode 100644 index 00000000..33e168c0 --- /dev/null +++ b/publish/action.yml @@ -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 diff --git a/rolldown.config.js b/rolldown.config.js index 814f4f4b..c2b2941a 100644 --- a/rolldown.config.js +++ b/rolldown.config.js @@ -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", diff --git a/select-mode/README.md b/select-mode/README.md new file mode 100644 index 00000000..f741cc8c --- /dev/null +++ b/select-mode/README.md @@ -0,0 +1,3 @@ +# changesets/action/select-mode + +TODO diff --git a/select-mode/action.yml b/select-mode/action.yml new file mode 100644 index 00000000..cbeeeba9 --- /dev/null +++ b/select-mode/action.yml @@ -0,0 +1,12 @@ +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'." +branding: + icon: package + color: blue diff --git a/src/publish/index.ts b/src/publish/index.ts new file mode 100644 index 00000000..a3c3c510 --- /dev/null +++ b/src/publish/index.ts @@ -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); + } +} diff --git a/src/run.ts b/src/run.ts index 5f91d7af..56fa667c 100644 --- a/src/run.ts +++ b/src/run.ts @@ -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"; @@ -58,7 +63,7 @@ const createRelease = async ( }; type PublishOptions = { - script: string; + script?: string; githubToken: string; octokit: Octokit; createGithubReleases: boolean; @@ -87,11 +92,29 @@ export async function runPublish({ createGithubReleases, cwd, }: PublishOptions): Promise { - 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[] = []; @@ -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); diff --git a/src/select-mode/index.ts b/src/select-mode/index.ts new file mode 100644 index 00000000..651925b1 --- /dev/null +++ b/src/select-mode/index.ts @@ -0,0 +1,30 @@ +import * as core from "@actions/core"; +import readChangesetState from "../readChangesetState.ts"; + +try { + await main(); +} catch (err) { + core.setFailed((err as Error).message); +} + +async function main() { + const mode = await getMode(); + core.setOutput("mode", mode); +} + +async function getMode(): Promise<"version" | "publish" | "none"> { + const { changesets } = await readChangesetState(); + + if (changesets.length > 0) { + const hasNonEmptyChangesets = changesets.some( + (changeset) => changeset.releases.length > 0, + ); + if (hasNonEmptyChangesets) { + return "version"; + } else { + return "none"; + } + } else { + return "publish"; + } +} diff --git a/src/version/index.ts b/src/version/index.ts new file mode 100644 index 00000000..985fd048 --- /dev/null +++ b/src/version/index.ts @@ -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)); +} diff --git a/version/README.md b/version/README.md new file mode 100644 index 00000000..f0efb2bb --- /dev/null +++ b/version/README.md @@ -0,0 +1,3 @@ +# changesets/action/version + +TODO diff --git a/version/action.yml b/version/action.yml new file mode 100644 index 00000000..40732abe --- /dev/null +++ b/version/action.yml @@ -0,0 +1,43 @@ +name: Changesets - Version +description: Version packages +runs: + using: node24 + main: ../dist/version.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 + commit-message: + description: "The commit message. Default to `Version Packages`" + required: false + default: "Version Packages" + pr-title: + description: "The pull request title. Default to `Version Packages`" + required: false + default: "Version Packages" + pr-draft: + description: "Controls draft PR behavior. Use 'create' to create new version PRs as draft, or 'always' to also convert existing version PRs back to draft when updating them." + required: false + base-branch: + description: "Sets the base branch of the PR. Defaults to `github.ref_name`." + required: false + commit-mode: + description: > + An enum to specify the commit mode. Use "git-cli" to push changes using the Git CLI, + or "github-api" to push changes via the GitHub API. When using "github-api", + all commits and tags are signed using GitHub's GPG key and attributed to the user + or app who owns the GITHUB_TOKEN. + required: false + default: "git-cli" + setup-git-user: + description: Sets up the git user for commits as `"github-actions[bot]"`. Default to `true` + required: false + default: true +outputs: {} +branding: + icon: package + color: blue