From b81dda40169a6700a69d32c5d5294e1f6a91ab66 Mon Sep 17 00:00:00 2001 From: Bojan Rajh Date: Tue, 28 Oct 2025 13:32:47 +0100 Subject: [PATCH] feat: new `body` option for custom PR body, with placeholders --- .changeset/afraid-otters-go.md | 5 ++ README.md | 1 + action.yml | 3 + src/index.ts | 1 + src/run.test.ts | 104 ++++++++++++++++++++++++++++++++- src/run.ts | 45 +++++++++----- 6 files changed, 142 insertions(+), 17 deletions(-) create mode 100644 .changeset/afraid-otters-go.md diff --git a/.changeset/afraid-otters-go.md b/.changeset/afraid-otters-go.md new file mode 100644 index 00000000..d230ac59 --- /dev/null +++ b/.changeset/afraid-otters-go.md @@ -0,0 +1,5 @@ +--- +"@changesets/action": minor +--- + +Added `body` option for custom PR body with default placeholders diff --git a/README.md b/README.md index 5ea2e5e5..6d0ec597 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This action for [Changesets](https://github.com/changesets/changesets) creates a - version - The command to update version, edit CHANGELOG, read and delete changesets. Default to `changeset version` if not provided - commit - The commit message to use. Default to `Version Packages` - title - The pull request title. Default to `Version Packages` +- body - The pull request body. Default to the current implementation. Available placeholders: ``, ``, ``, `` - setupGitUser - Sets up the git user for commits as `"github-actions[bot]"`. Default to `true` - createGithubReleases - A boolean value to indicate whether to create Github releases after `publish` or not. Default to `true` - commitMode - Specifies 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 GPG-signed and attributed to the user or app who owns the `GITHUB_TOKEN`. Default to `git-cli`. diff --git a/action.yml b/action.yml index 5b74beca..4fe292ba 100644 --- a/action.yml +++ b/action.yml @@ -20,6 +20,9 @@ inputs: title: description: The pull request title. Default to `Version Packages` required: false + body: + description: The pull request body. Default to the current implementation. Available placeholders: ``, ``, ``, `` + required: false setupGitUser: description: Sets up the git user for commits as `"github-actions[bot]"`. Default to `true` required: false diff --git a/src/index.ts b/src/index.ts index 1b0f63ab..e3d56563 100644 --- a/src/index.ts +++ b/src/index.ts @@ -123,6 +123,7 @@ const getOptionalInput = (name: string) => core.getInput(name) || undefined; git, octokit, prTitle: getOptionalInput("title"), + prBody: getOptionalInput("body"), commitMessage: getOptionalInput("commit"), hasPublishScript, branch: getOptionalInput("branch"), diff --git a/src/run.test.ts b/src/run.test.ts index 927be138..e7abecbf 100644 --- a/src/run.test.ts +++ b/src/run.test.ts @@ -6,7 +6,7 @@ import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { Git } from "./git.ts"; import { setupOctokit } from "./octokit.ts"; -import { runVersion } from "./run.ts"; +import { runVersion, getVersionPrBody } from "./run.ts"; vi.mock("@actions/github", () => ({ context: { @@ -273,3 +273,105 @@ fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis. ); }); }); + +describe("getVersionPrBody", () => { + // Sample data for testing + const mockChangedPackagesInfo = [ + { + highestLevel: 1, + private: false, + content: "### Minor Changes\n\n- Added awesome feature", + header: "## test-package@1.1.0" + } + ]; + + it("uses default behavior when prBody is undefined", async () => { + const result = await getVersionPrBody({ + hasPublishScript: false, + preState: undefined, + changedPackagesInfo: mockChangedPackagesInfo, + prBodyMaxCharacters: 10000, + prBody: undefined, + branch: "main" + }); + + expect(result).toContain("This PR was opened by the [Changesets release]"); + expect(result).toContain("# Releases"); + expect(result).toContain("## test-package@1.1.0"); + expect(result).toContain("### Minor Changes"); + expect(result).toContain("- Added awesome feature"); + // Should not contain placeholder comments when using default behavior + expect(result).not.toContain(""); + expect(result).not.toContain(""); + }); + + it("replaces placeholders when prBody contains only placeholders", async () => { + const customPrBody = "\n\n\n\n\n\n"; + + const result = await getVersionPrBody({ + hasPublishScript: true, + preState: undefined, + changedPackagesInfo: mockChangedPackagesInfo, + prBodyMaxCharacters: 10000, + prBody: customPrBody, + branch: "main" + }); + + expect(result).toContain("This PR was opened by the [Changesets release]"); + expect(result).toContain("the packages will be published to npm automatically"); + expect(result).toContain("# Releases"); + expect(result).toContain("## test-package@1.1.0"); + expect(result).toContain("### Minor Changes"); + expect(result).toContain("- Added awesome feature"); + // Should not contain placeholder comments after replacement + expect(result).not.toContain(""); + expect(result).not.toContain(""); + }); + + it("uses custom text around placeholders", async () => { + const customPrBody = `🚀 **Custom Release PR** 🚀 + + + +⚠️ **Important Notes:** +This is a custom PR body with additional context. + + + +📦 **Package Updates:** + + + + +✅ **Ready to merge when you are!** +Please review the changes above before merging.`; + + const result = await getVersionPrBody({ + hasPublishScript: false, + preState: undefined, + changedPackagesInfo: mockChangedPackagesInfo, + prBodyMaxCharacters: 10000, + prBody: customPrBody, + branch: "develop" + }); + + // Should contain custom text + expect(result).toContain("🚀 **Custom Release PR** 🚀"); + expect(result).toContain("⚠️ **Important Notes:**"); + expect(result).toContain("This is a custom PR body with additional context."); + expect(result).toContain("📦 **Package Updates:**"); + expect(result).toContain("✅ **Ready to merge when you are!**"); + expect(result).toContain("Please review the changes above before merging."); + + // Should still contain replaced content + expect(result).toContain("This PR was opened by the [Changesets release]"); + expect(result).toContain("publish to npm yourself"); + expect(result).toContain("# Releases"); + expect(result).toContain("## test-package@1.1.0"); + expect(result).toContain("### Minor Changes"); + + // Should not contain placeholder comments after replacement + expect(result).not.toContain(""); + expect(result).not.toContain(""); + }); +}); diff --git a/src/run.ts b/src/run.ts index 18ec1da4..b0e00199 100644 --- a/src/run.ts +++ b/src/run.ts @@ -188,6 +188,7 @@ type GetMessageOptions = { header: string; }[]; prBodyMaxCharacters: number; + prBody?: string; preState?: PreState; }; @@ -196,6 +197,7 @@ export async function getVersionPrBody({ preState, changedPackagesInfo, prBodyMaxCharacters, + prBody, branch, }: GetMessageOptions) { let messageHeader = `This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and ${ @@ -214,34 +216,42 @@ export async function getVersionPrBody({ : ""; let messageReleasesHeading = `# Releases`; - let fullMessage = [ - messageHeader, - messagePrestate, - messageReleasesHeading, + function useOrBuildTemplate(lines: string[]) { + if (!prBody) { + return [ + messageHeader, + messagePrestate, + messageReleasesHeading, + ...lines, + ].join('\n'); + } + + return prBody + .replace('', messageHeader) + .replace('', messagePrestate) + .replace('', messageReleasesHeading) + .replace('', lines.join('\n')); + } + + let fullMessage = useOrBuildTemplate([ ...changedPackagesInfo.map((info) => `${info.header}\n\n${info.content}`), - ].join("\n"); + ]); // Check that the message does not exceed the size limit. // If not, omit the changelog entries of each package. if (fullMessage.length > prBodyMaxCharacters) { - fullMessage = [ - messageHeader, - messagePrestate, - messageReleasesHeading, + fullMessage = useOrBuildTemplate([ `\n> The changelog information of each package has been omitted from this message, as the content exceeds the size limit.\n`, ...changedPackagesInfo.map((info) => `${info.header}\n\n`), - ].join("\n"); + ]); } // Check (again) that the message is within the size limit. // If not, omit all release content this time. if (fullMessage.length > prBodyMaxCharacters) { - fullMessage = [ - messageHeader, - messagePrestate, - messageReleasesHeading, + fullMessage = useOrBuildTemplate([ `\n> All release information have been omitted from this message, as the content exceeds the size limit.`, - ].join("\n"); + ]); } return fullMessage; @@ -253,6 +263,7 @@ type VersionOptions = { octokit: Octokit; cwd?: string; prTitle?: string; + prBody?: string; commitMessage?: string; hasPublishScript?: boolean; prBodyMaxCharacters?: number; @@ -269,6 +280,7 @@ export async function runVersion({ octokit, cwd = process.cwd(), prTitle = "Version Packages", + prBody, commitMessage = "Version Packages", hasPublishScript = false, prBodyMaxCharacters = MAX_CHARACTERS_PER_MESSAGE, @@ -348,12 +360,13 @@ export async function runVersion({ .filter((x) => x) .sort(sortTheThings); - let prBody = await getVersionPrBody({ + prBody = await getVersionPrBody({ hasPublishScript, preState, branch, changedPackagesInfo, prBodyMaxCharacters, + prBody, }); if (existingPullRequests.data.length === 0) {