diff --git a/.changeset/brown-cycles-attack.md b/.changeset/brown-cycles-attack.md new file mode 100644 index 00000000..5692aae4 --- /dev/null +++ b/.changeset/brown-cycles-attack.md @@ -0,0 +1,5 @@ +--- +"@changesets/action": minor +--- + +Support draft version PR modes with a new `prDraft` input. Use `create` to create new version PRs as drafts, or `always` to also convert existing version PRs back to draft when updating them. diff --git a/README.md b/README.md index ebfda456..cf9c6b0e 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ This action for [Changesets](https://github.com/changesets/changesets) creates a - 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`. - cwd - Changes node's `process.cwd()` if the project is not located on the root. Default to `process.cwd()` +- prDraft - 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. By default, version PRs are not forced into draft mode. ### Outputs diff --git a/action.yml b/action.yml index 89fe87a7..9f7d3869 100644 --- a/action.yml +++ b/action.yml @@ -43,6 +43,9 @@ inputs: branch: description: Sets the branch in which the action will run. Default to `github.ref_name` if not provided required: false + prDraft: + 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 outputs: published: description: A boolean value to indicate whether a publishing is happened or not diff --git a/src/__snapshots__/run.test.ts.snap b/src/__snapshots__/run.test.ts.snap index aa9b5d9b..956ed69f 100644 --- a/src/__snapshots__/run.test.ts.snap +++ b/src/__snapshots__/run.test.ts.snap @@ -1,5 +1,28 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`version > creates a draft PR when prDraft is "create" 1`] = ` +[ + { + "base": "some-branch", + "body": "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 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 some-branch, this PR will be updated. + + +# Releases +## simple-project-pkg-a@1.1.0 + +### Minor Changes + +- Awesome feature +", + "draft": true, + "head": "changeset-release/some-branch", + "owner": "changesets", + "repo": "action", + "title": "Version Packages", + }, +] +`; + exports[`version > creates simple PR 1`] = ` [ { @@ -25,6 +48,7 @@ exports[`version > creates simple PR 1`] = ` - Awesome feature ", + "draft": false, "head": "changeset-release/some-branch", "owner": "changesets", "repo": "action", @@ -43,6 +67,7 @@ exports[`version > does not include any release information if a message with si # Releases > All release information have been omitted from this message, as the content exceeds the size limit.", + "draft": false, "head": "changeset-release/some-branch", "owner": "changesets", "repo": "action", @@ -65,6 +90,7 @@ exports[`version > does not include changelog entries if full message exceeds si ## simple-project-pkg-a@1.1.0 ", + "draft": false, "head": "changeset-release/some-branch", "owner": "changesets", "repo": "action", @@ -87,6 +113,7 @@ exports[`version > doesn't include ignored package that got a dependency update - Awesome feature ", + "draft": false, "head": "changeset-release/some-branch", "owner": "changesets", "repo": "action", @@ -109,6 +136,7 @@ exports[`version > only includes bumped packages in the PR body 1`] = ` - Awesome feature ", + "draft": false, "head": "changeset-release/some-branch", "owner": "changesets", "repo": "action", @@ -116,3 +144,94 @@ exports[`version > only includes bumped packages in the PR body 1`] = ` }, ] `; + +exports[`version > updates an existing PR via GraphQL and converts it to draft when prDraft is "always" 1`] = ` +[ + " + mutation UpdatePullRequest( + $pullRequestId: ID! + $title: String! + $body: String! + ) { + + convertPullRequestToDraft( + input: { + pullRequestId: $pullRequestId + } + ) { + pullRequest { + id + } + } + + updatePullRequest( + input: { + pullRequestId: $pullRequestId + title: $title + body: $body + state: OPEN + } + ) { + pullRequest { + id + } + } + } + ", + { + "body": "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 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 some-branch, this PR will be updated. + + +# Releases +## simple-project-pkg-a@1.1.0 + +### Minor Changes + +- Awesome feature +", + "pullRequestId": "PR_kwDOA", + "title": "Version Packages", + }, +] +`; + +exports[`version > updates an existing PR via GraphQL without converting it to draft when prDraft is "create" 1`] = ` +[ + " + mutation UpdatePullRequest( + $pullRequestId: ID! + $title: String! + $body: String! + ) { + + + updatePullRequest( + input: { + pullRequestId: $pullRequestId + title: $title + body: $body + state: OPEN + } + ) { + pullRequest { + id + } + } + } + ", + { + "body": "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 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 some-branch, this PR will be updated. + + +# Releases +## simple-project-pkg-a@1.1.0 + +### Minor Changes + +- Awesome feature +", + "pullRequestId": "PR_kwDOA", + "title": "Version Packages", + }, +] +`; diff --git a/src/index.ts b/src/index.ts index d8d1cc9d..32d3334a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,10 +24,15 @@ const getOptionalInput = (name: string) => core.getInput(name) || undefined; const octokit = setupOctokit(githubToken); const commitMode = getOptionalInput("commitMode") ?? "git-cli"; + const prDraft = getOptionalInput("prDraft"); if (commitMode !== "git-cli" && commitMode !== "github-api") { core.setFailed(`Invalid commit mode: ${commitMode}`); return; } + if (prDraft !== undefined && prDraft !== "always" && prDraft !== "create") { + core.setFailed(`Invalid prDraft: ${prDraft}`); + return; + } const git = new Git({ octokit: commitMode === "github-api" ? octokit : undefined, cwd, @@ -147,6 +152,7 @@ const getOptionalInput = (name: string) => core.getInput(name) || undefined; prTitle: getOptionalInput("title"), commitMessage: getOptionalInput("commit"), hasPublishScript, + prDraft, branch: getOptionalInput("branch"), }); diff --git a/src/run.test.ts b/src/run.test.ts index 90c9fc00..34fba34d 100644 --- a/src/run.test.ts +++ b/src/run.test.ts @@ -19,6 +19,7 @@ vi.mock("@actions/github", () => ({ }, getOctokit: () => ({ rest: mockedGithubMethods, + graphql: mockedGraphql, }), })); vi.mock("./git.ts"); @@ -33,6 +34,7 @@ let mockedGithubMethods = { createRelease: vi.fn(), }, }; +let mockedGraphql = vi.fn(); let f = fixturez(import.meta.dirname); @@ -90,6 +92,42 @@ describe("version", () => { expect(mockedGithubMethods.pulls.create.mock.calls[0]).toMatchSnapshot(); }); + it('creates a draft PR when prDraft is "create"', async () => { + let cwd = f.copy("simple-project"); + await linkNodeModules(cwd); + + mockedGithubMethods.pulls.list.mockImplementationOnce(() => ({ data: [] })); + + mockedGithubMethods.pulls.create.mockImplementationOnce(() => ({ + data: { number: 123 }, + })); + + await writeChangesets( + [ + { + releases: [ + { + name: "simple-project-pkg-a", + type: "minor", + }, + ], + summary: "Awesome feature", + }, + ], + cwd + ); + + await runVersion({ + octokit: setupOctokit("@@GITHUB_TOKEN"), + githubToken: "@@GITHUB_TOKEN", + git: new Git({ cwd }), + cwd, + prDraft: "create", + }); + + expect(mockedGithubMethods.pulls.create.mock.calls[0]).toMatchSnapshot(); + }); + it("only includes bumped packages in the PR body", async () => { let cwd = f.copy("simple-project"); await linkNodeModules(cwd); @@ -277,4 +315,72 @@ fluminis divesque vulnere aquis parce lapsis rabie si visa fulmineis. /All release information have been omitted from this message, as the content exceeds the size limit/ ); }); + + it('updates an existing PR via GraphQL without converting it to draft when prDraft is "create"', async () => { + let cwd = f.copy("simple-project"); + await linkNodeModules(cwd); + + mockedGithubMethods.pulls.list.mockImplementationOnce(() => ({ + data: [{ number: 123, node_id: "PR_kwDOA" }], + })); + + await writeChangesets( + [ + { + releases: [ + { + name: "simple-project-pkg-a", + type: "minor", + }, + ], + summary: "Awesome feature", + }, + ], + cwd + ); + + await runVersion({ + octokit: setupOctokit("@@GITHUB_TOKEN"), + githubToken: "@@GITHUB_TOKEN", + git: new Git({ cwd }), + cwd, + prDraft: "create", + }); + + expect(mockedGraphql.mock.calls[0]).toMatchSnapshot(); + }); + + it('updates an existing PR via GraphQL and converts it to draft when prDraft is "always"', async () => { + let cwd = f.copy("simple-project"); + await linkNodeModules(cwd); + + mockedGithubMethods.pulls.list.mockImplementationOnce(() => ({ + data: [{ number: 123, node_id: "PR_kwDOA" }], + })); + + await writeChangesets( + [ + { + releases: [ + { + name: "simple-project-pkg-a", + type: "minor", + }, + ], + summary: "Awesome feature", + }, + ], + cwd + ); + + await runVersion({ + octokit: setupOctokit("@@GITHUB_TOKEN"), + githubToken: "@@GITHUB_TOKEN", + git: new Git({ cwd }), + cwd, + prDraft: "always", + }); + + expect(mockedGraphql.mock.calls[0]).toMatchSnapshot(); + }); }); diff --git a/src/run.ts b/src/run.ts index 5ddb33e3..e779f582 100644 --- a/src/run.ts +++ b/src/run.ts @@ -259,6 +259,7 @@ type VersionOptions = { commitMessage?: string; hasPublishScript?: boolean; prBodyMaxCharacters?: number; + prDraft?: "always" | "create"; branch?: string; }; @@ -277,6 +278,7 @@ export async function runVersion({ hasPublishScript = false, prBodyMaxCharacters = MAX_CHARACTERS_PER_MESSAGE, branch = github.context.ref.replace("refs/heads/", ""), + prDraft, }: VersionOptions): Promise { let versionBranch = `changeset-release/${branch}`; @@ -376,6 +378,7 @@ export async function runVersion({ head: versionBranch, title: finalPrTitle, body: prBody, + draft: prDraft !== undefined, ...github.context.repo, }); @@ -386,12 +389,46 @@ export async function runVersion({ const [pullRequest] = existingPullRequests.data; core.info(`updating found pull request #${pullRequest.number}`); - await octokit.rest.pulls.update({ - pull_number: pullRequest.number, + const convertPullRequestToDraftMutation = + prDraft === "always" + ? ` + convertPullRequestToDraft( + input: { + pullRequestId: $pullRequestId + } + ) { + pullRequest { + id + } + }` + : ""; + const updatePullRequestMutation = ` + mutation UpdatePullRequest( + $pullRequestId: ID! + $title: String! + $body: String! + ) { + ${convertPullRequestToDraftMutation} + + updatePullRequest( + input: { + pullRequestId: $pullRequestId + title: $title + body: $body + state: OPEN + } + ) { + pullRequest { + id + } + } + } + `; + + await octokit.graphql(updatePullRequestMutation, { + pullRequestId: pullRequest.node_id, title: finalPrTitle, body: prBody, - ...github.context.repo, - state: "open", }); return {