From 92c2d7fe6244458c944f494be708c8fc93a1f263 Mon Sep 17 00:00:00 2001 From: Furcan <49978011+furcan@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:18:31 +0300 Subject: [PATCH 1/4] feat(utils): primitive type-checkers --- src/utils.test.ts | 72 +++++++++++++++++++++++++++++++++++++++++++++++ src/utils.ts | 8 ++++++ 2 files changed, 80 insertions(+) diff --git a/src/utils.test.ts b/src/utils.test.ts index fe212526..92adba7b 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -6,6 +6,8 @@ import { sortTheThings, throwOnRemovedCommitModeInput, validateChangesetsCliVersion, + isObject, + isString, } from "./utils.ts"; let changelog = `# @keystone-alpha/email @@ -170,3 +172,73 @@ test("throws when the project has Changesets CLI v2 installed", async () => { "This version of the Changesets action is designed to work with Changesets CLI v3. Changesets CLI v2 is not supported; use Changesets action v1 instead, which is compatible with CLI v2.", ); }); + +test.each([ + { + input: null, + expected: false, + }, + { + input: undefined, + expected: false, + }, + { + input: "changesets", + expected: false, + }, + { + input: 1, + expected: false, + }, + { + input: true, + expected: false, + }, + { + input: [], + expected: false, + }, + { + input: {}, + expected: true, + }, +])("isObject: $input should return $expected", ({ input, expected }) => { + expect(isObject(input)).toBe(expected); +}); + +test.each([ + { + input: null, + expected: false, + }, + { + input: undefined, + expected: false, + }, + { + input: "changesets", + expected: true, + }, + { + input: 1, + expected: false, + }, + { + input: true, + expected: false, + }, + { + input: [], + expected: false, + }, + { + input: {}, + expected: false, + }, + { + input: () => "test", + expected: false, + }, +])("isString: $input should return $expected", ({ input, expected }) => { + expect(isString(input)).toBe(expected); +}); diff --git a/src/utils.ts b/src/utils.ts index 3ea38143..42cdbaf1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -284,3 +284,11 @@ export async function downloadArtifact( return result.downloadPath; } + +export function isObject(value: unknown): value is T { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export function isString(value: unknown): value is string { + return typeof value === "string"; +} From 887d918060c706d97e391d060327ba34c8e511d2 Mon Sep 17 00:00:00 2001 From: Furcan <49978011+furcan@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:19:16 +0300 Subject: [PATCH 2/4] feat(version): allow and deliver pr-body as optional-string --- src/version/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/version/index.ts b/src/version/index.ts index 699f390a..baed9ffd 100644 --- a/src/version/index.ts +++ b/src/version/index.ts @@ -23,6 +23,7 @@ async function main() { const script = getOptionalInput("script"); const commitMessage = getRequiredInput("commit-message"); const prTitle = getRequiredInput("pr-title"); + const prBody = getOptionalInput("pr-body"); const prDraft = getOptionalInput("pr-draft"); const prBaseBranch = getOptionalInput("pr-base-branch"); const pushWithGitCli = core.getBooleanInput("push-with-git-cli"); @@ -42,6 +43,7 @@ async function main() { github, cwd, prTitle, + prBody, commitMessage, // TODO: Use neutral message for PR description hasPublishScript: true, From 1612cd2a88a2e69b536bae3abf36fae6814b698a Mon Sep 17 00:00:00 2001 From: Furcan <49978011+furcan@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:19:49 +0300 Subject: [PATCH 3/4] feat(types): run.ts: non-executable code --- src/run.types.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/run.types.ts diff --git a/src/run.types.ts b/src/run.types.ts new file mode 100644 index 00000000..ed3e2dde --- /dev/null +++ b/src/run.types.ts @@ -0,0 +1,33 @@ +import type { PreState } from "@changesets/types"; +import type { GitHub } from "./github.ts"; + +export type GetVersionPrBodyProps = { + hasPublishScript: boolean; + branch: string; + changedPackagesInfo: { + highestLevel: number; + private: boolean; + content: string; + header: string; + }[]; + prBody?: string; + prBodyMaxCharacters: number; + preState?: PreState; +}; + +export type RunVersionProps = { + script?: string; + github: GitHub; + cwd?: string; + prTitle?: string; + prBody?: string; + commitMessage?: string; + hasPublishScript?: boolean; + prBodyMaxCharacters?: number; + prDraft?: "always" | "create"; + branch?: string; +}; + +export type RunVersionResult = { + pullRequestNumber: number; +}; From 6df6b3f74a2839534c419ef37676e822c1ce017d Mon Sep 17 00:00:00 2001 From: Furcan <49978011+furcan@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:21:07 +0300 Subject: [PATCH 4/4] feat(run): run.ts: consume the optional pr-body --- src/run.test.ts | 66 ++++++++++++++++++++++++++++++++++ src/run.ts | 95 ++++++++++++++++++++++--------------------------- 2 files changed, 109 insertions(+), 52 deletions(-) diff --git a/src/run.test.ts b/src/run.test.ts index 42299c96..9f3b8106 100644 --- a/src/run.test.ts +++ b/src/run.test.ts @@ -487,4 +487,70 @@ fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis. expect(mockedGraphql.mock.calls[0]).toMatchSnapshot(); }); + + it.each([true, false])( + 'uses consumer given PR Body instead of default, also respects to PacakgesInfo included "%s"', + async (hasPacakgesInfo) => { + await using fixture = await createSimpleProjectFixture(); + const cwd = fixture.path; + + mockedGithubMethods.pulls.list.mockImplementationOnce(() => ({ + data: [], + })); + + mockedGithubMethods.pulls.create.mockImplementationOnce(() => ({ + data: { number: 123 }, + })); + + await writeChangesets( + [ + { + releases: [ + { + name: "changesets-dev-simple-project-pkg-a", + type: "minor", + }, + { + name: "changesets-dev-simple-project-pkg-b", + type: "minor", + }, + ], + summary: "Awesome feature", + }, + ], + cwd, + ); + + await runVersion({ + github: createGithub(cwd), + prBody: hasPacakgesInfo + ? "THIS_AND_THAT \n{PACKAGES_INFO}" + : "THIS_AND_THAT", + cwd, + }); + + if (hasPacakgesInfo) { + const prBody: string = mockedGithubMethods.pulls.create.mock.calls + .at(0) + ?.at(0).body; + + expect(prBody.startsWith("THIS_AND_THAT \n")).toBe(true); + expect(prBody).toContain("changesets-dev-simple-project-pkg-a"); + expect(prBody).toContain("changesets-dev-simple-project-pkg-b"); + expect(prBody).toContain("Awesome feature"); + } else { + expect( + mockedGithubMethods.pulls.create.mock.calls.at(0)?.at(0), + ).toStrictEqual({ + base: "some-branch", + body: "THIS_AND_THAT", + draft: false, + head: "changeset-release/some-branch", + owner: "changesets", + repo: "action", + title: "Version Packages", + }); + } + }, + ); }); diff --git a/src/run.ts b/src/run.ts index 37dd2806..aef0e43e 100644 --- a/src/run.ts +++ b/src/run.ts @@ -10,11 +10,15 @@ import { type ExecOutput, } from "@actions/exec"; import { context } from "@actions/github"; -import type { PreState } from "@changesets/types"; import { type Package, getPackages } from "@manypkg/get-packages"; import type { GitHub } from "./github.ts"; import type { Octokit } from "./octokit.ts"; import readChangesetState from "./readChangesetState.ts"; +import type { + GetVersionPrBodyProps, + RunVersionProps, + RunVersionResult, +} from "./run.types.ts"; import { execChangesetsCli, getChangedPackages, @@ -23,6 +27,8 @@ import { getVersionsByDirectory, isErrorWithCode, sortTheThings, + isObject, + isString, } from "./utils.ts"; // GitHub Issues/PRs messages have a max size limit on the @@ -92,10 +98,6 @@ type PublishResult = exitCode: number; }; -function isObject(value: unknown) { - return typeof value === "object" && value !== null; -} - function isChangesetsOutputEvent( value: unknown, ): value is ChangesetsOutputEvent { @@ -259,33 +261,33 @@ export async function runPublish({ return { published: false, exitCode: changesetPublishOutput.exitCode }; } -type GetMessageOptions = { - hasPublishScript: boolean; - branch: string; - changedPackagesInfo: { - highestLevel: number; - private: boolean; - content: string; - header: string; - }[]; - prBodyMaxCharacters: number; - preState?: PreState; -}; - export async function getVersionPrBody({ hasPublishScript, preState, changedPackagesInfo, + prBody, prBodyMaxCharacters, branch, -}: GetMessageOptions) { +}: GetVersionPrBodyProps): Promise { + if (isString(prBody)) { + const packagesInfo: string = changedPackagesInfo + .map(({ header, content }) => `${header}\n\n${content}`) + .join("\n"); + + const prBodyText = prBody.replace("{PACKAGES_INFO}", packagesInfo); + + return prBodyText.length > prBodyMaxCharacters + ? "This PR Body content exceeds the size limit." + : prBodyText; + } + 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 ${ hasPublishScript ? `the packages will be published to npm automatically` : `publish to npm yourself or [setup this action to publish automatically](https://github.com/changesets/action#with-publishing)` }. If you're not ready to do a release yet, that's fine, whenever you add more changesets to ${branch}, this PR will be updated. `; - let messagePrestate = !!preState + let messagePrestate = isObject(preState) ? `⚠️⚠️⚠️⚠️⚠️⚠️ \`${branch}\` is currently in **pre mode** so this branch has prereleases rather than normal releases. If you want to exit prereleases, run \`changeset pre exit\` on \`${branch}\`. @@ -328,41 +330,26 @@ export async function getVersionPrBody({ return fullMessage; } -type VersionOptions = { - script?: string; - github: GitHub; - cwd?: string; - prTitle?: string; - commitMessage?: string; - hasPublishScript?: boolean; - prBodyMaxCharacters?: number; - prDraft?: "always" | "create"; - branch?: string; -}; - -type RunVersionResult = { - pullRequestNumber: number; -}; - export async function runVersion({ script, github, cwd = process.cwd(), prTitle = "Version Packages", + prBody = undefined, commitMessage = "Version Packages", hasPublishScript = false, prBodyMaxCharacters = MAX_CHARACTERS_PER_MESSAGE, branch = context.ref.replace("refs/heads/", ""), prDraft, -}: VersionOptions): Promise { +}: RunVersionProps): Promise { const { octokit } = github; - let versionBranch = `changeset-release/${branch}`; + const versionBranch = `changeset-release/${branch}`; - let { preState } = await readChangesetState(cwd); + const { preState } = await readChangesetState(cwd); await github.prepareBranch(versionBranch); - let versionsByDirectory = await getVersionsByDirectory(cwd); + const versionsByDirectory = await getVersionsByDirectory(cwd); const env = { ...process.env, GITHUB_TOKEN: github.getToken() }; @@ -372,15 +359,18 @@ export async function runVersion({ await execChangesetsCli(["version"], { cwd, env }); } - let changedPackages = await getChangedPackages(cwd, versionsByDirectory); - let changedPackagesInfoPromises = Promise.all( + const changedPackages = await getChangedPackages(cwd, versionsByDirectory); + const changedPackagesInfoPromises = Promise.all( changedPackages.map(async (pkg) => { - let changelogContents = await fs.readFile( + const changelogContents = await fs.readFile( path.join(pkg.dir, "CHANGELOG.md"), "utf8", ); - let entry = getChangelogEntry(changelogContents, pkg.packageJson.version); + const entry = getChangelogEntry( + changelogContents, + pkg.packageJson.version, + ); return { highestLevel: entry.highestLevel, private: !!pkg.packageJson.private, @@ -390,10 +380,10 @@ export async function runVersion({ }), ); - const finalPrTitle = `${prTitle}${!!preState ? ` (${preState.tag})` : ""}`; - const finalCommitMessage = `${commitMessage}${ - !!preState ? ` (${preState.tag})` : "" - }`; + const isPreState = isObject(preState); + const finalPreState = isPreState ? ` (${preState.tag})` : ""; + const finalPrTitle = `${prTitle}${finalPreState}`; + const finalCommitMessage = `${commitMessage}${finalPreState}`; const existingPullRequests = await octokit.rest.pulls.list({ ...context.repo, @@ -415,14 +405,15 @@ export async function runVersion({ }); const changedPackagesInfo = (await changedPackagesInfoPromises) - .filter((x) => x) + .filter(Boolean) .sort(sortTheThings); - let prBody = await getVersionPrBody({ + const finalPrBody = await getVersionPrBody({ hasPublishScript, preState, branch, changedPackagesInfo, + prBody, prBodyMaxCharacters, }); @@ -432,7 +423,7 @@ export async function runVersion({ base: branch, head: versionBranch, title: finalPrTitle, - body: prBody, + body: finalPrBody, draft: prDraft !== undefined, ...context.repo, }); @@ -483,7 +474,7 @@ export async function runVersion({ await octokit.graphql(updatePullRequestMutation, { pullRequestId: pullRequest.node_id, title: finalPrTitle, - body: prBody, + body: finalPrBody, }); return {