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/afraid-otters-go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@changesets/action": minor
---

Added `body` option for custom PR body with default placeholders
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<!-- header -->`, `<!-- prestate -->`, `<!-- releasesHeading -->`, `<!-- body -->`
- 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`.
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<!-- header -->`, `<!-- prestate -->`, `<!-- releasesHeading -->`, `<!-- body -->`
required: false
setupGitUser:
description: Sets up the git user for commits as `"github-actions[bot]"`. Default to `true`
required: false
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
104 changes: 103 additions & 1 deletion src/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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("<!-- header -->");
expect(result).not.toContain("<!-- body -->");
});

it("replaces placeholders when prBody contains only placeholders", async () => {
const customPrBody = "<!-- header -->\n\n<!-- prestate -->\n\n<!-- releasesHeading -->\n\n<!-- body -->";

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("<!-- header -->");
expect(result).not.toContain("<!-- body -->");
});

it("uses custom text around placeholders", async () => {
const customPrBody = `🚀 **Custom Release PR** 🚀

<!-- header -->

⚠️ **Important Notes:**
This is a custom PR body with additional context.

<!-- prestate -->

📦 **Package Updates:**
<!-- releasesHeading -->

<!-- body -->

✅ **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("<!-- header -->");
expect(result).not.toContain("<!-- body -->");
});
});
45 changes: 29 additions & 16 deletions src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ type GetMessageOptions = {
header: string;
}[];
prBodyMaxCharacters: number;
prBody?: string;
preState?: PreState;
};

Expand All @@ -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 ${
Expand All @@ -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('<!-- header -->', messageHeader)
.replace('<!-- prestate -->', messagePrestate)
.replace('<!-- releasesHeading -->', messageReleasesHeading)
.replace('<!-- body -->', 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;
Expand All @@ -253,6 +263,7 @@ type VersionOptions = {
octokit: Octokit;
cwd?: string;
prTitle?: string;
prBody?: string;
commitMessage?: string;
hasPublishScript?: boolean;
prBodyMaxCharacters?: number;
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down