diff --git a/.changeset/hungry-cycles-hide.md b/.changeset/hungry-cycles-hide.md new file mode 100644 index 0000000000..22c6360c14 --- /dev/null +++ b/.changeset/hungry-cycles-hide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Adds `createWhenEmpty` option to `publish:github:pull-request` action. diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.test.ts index 26469dfcb4..fb2763db51 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.test.ts @@ -617,13 +617,7 @@ describe('publish:github:pull-request examples', () => { expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123); }); - it('Create a pull request with all parameters', async () => { - mockDir.setContent({ - [workspacePath]: { - source: { 'foo.txt': 'Hello there!' }, - irrelevant: { 'bar.txt': 'Nothing to see here' }, - }, - }); + it('Does not create an empty pull request', async () => { const input = yaml.parse(examples[12].example).steps[0].input; await action.handler({ @@ -638,8 +632,56 @@ describe('publish:github:pull-request examples', () => { title: 'Create my new app', body: 'This PR is really good', head: 'new-app', - draft: true, + draft: undefined, + createWhenEmpty: false, + changes: [ + { + commit: 'Create my new app', + files: { + 'file.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + + expect(fakeClient.rest.pulls.requestReviewers).not.toHaveBeenCalled(); + expect(mockContext.output).toHaveBeenCalledTimes(3); + expect(mockContext.output).toHaveBeenCalledWith('targetBranchName', 'main'); + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123); + }); + + it('Create a pull request with all parameters', async () => { + mockDir.setContent({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!' }, + irrelevant: { 'bar.txt': 'Nothing to see here' }, + }, + }); + const input = yaml.parse(examples[13].example).steps[0].input; + + await action.handler({ + ...mockContext, + workspacePath, + input, + }); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + title: 'Create my new app', + body: 'This PR is really good', + head: 'new-app', base: 'test', + draft: true, + createWhenEmpty: true, changes: [ { commit: 'Commit for foo changes', diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.ts index b6d31e732b..e35e6d9a81 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.ts @@ -23,7 +23,7 @@ export const examples: TemplateExample[] = [ steps: [ { action: 'publish:github:pull-request', - name: 'Create a pull reuqest', + name: 'Create a pull request', input: { repoUrl: 'github.com?repo=repo&owner=owner', branchName: 'new-app', @@ -40,7 +40,7 @@ export const examples: TemplateExample[] = [ steps: [ { action: 'publish:github:pull-request', - name: 'Create a pull reuqest with target branch name', + name: 'Create a pull request with target branch name', input: { repoUrl: 'github.com?repo=repo&owner=owner', branchName: 'new-app', @@ -58,7 +58,7 @@ export const examples: TemplateExample[] = [ steps: [ { action: 'publish:github:pull-request', - name: 'Create a pull reuqest as draft', + name: 'Create a pull request as draft', input: { repoUrl: 'github.com?repo=repo&owner=owner', branchName: 'new-app', @@ -76,7 +76,7 @@ export const examples: TemplateExample[] = [ steps: [ { action: 'publish:github:pull-request', - name: 'Create a pull reuqest with target path', + name: 'Create a pull request with target path', input: { repoUrl: 'github.com?repo=repo&owner=owner', branchName: 'new-app', @@ -94,7 +94,7 @@ export const examples: TemplateExample[] = [ steps: [ { action: 'publish:github:pull-request', - name: 'Create a pull reuqest with source path', + name: 'Create a pull request with source path', input: { repoUrl: 'github.com?repo=repo&owner=owner', branchName: 'new-app', @@ -112,7 +112,7 @@ export const examples: TemplateExample[] = [ steps: [ { action: 'publish:github:pull-request', - name: 'Create a pull reuqest', + name: 'Create a pull request', input: { repoUrl: 'github.com?repo=repo&owner=owner', branchName: 'new-app', @@ -130,7 +130,7 @@ export const examples: TemplateExample[] = [ steps: [ { action: 'publish:github:pull-request', - name: 'Create a pull reuqest with reviewers', + name: 'Create a pull request with reviewers', input: { repoUrl: 'github.com?repo=repo&owner=owner', branchName: 'new-app', @@ -148,7 +148,7 @@ export const examples: TemplateExample[] = [ steps: [ { action: 'publish:github:pull-request', - name: 'Create a pull reuqest with team reviewers', + name: 'Create a pull request with team reviewers', input: { repoUrl: 'github.com?repo=repo&owner=owner', branchName: 'new-app', @@ -166,7 +166,7 @@ export const examples: TemplateExample[] = [ steps: [ { action: 'publish:github:pull-request', - name: 'Create a pull reuqest', + name: 'Create a pull request', input: { repoUrl: 'github.com?repo=repo&owner=owner', branchName: 'new-app', @@ -184,7 +184,7 @@ export const examples: TemplateExample[] = [ steps: [ { action: 'publish:github:pull-request', - name: 'Create a pull reuqest', + name: 'Create a pull request', input: { repoUrl: 'github.com?repo=repo&owner=owner', branchName: 'new-app', @@ -203,7 +203,7 @@ export const examples: TemplateExample[] = [ steps: [ { action: 'publish:github:pull-request', - name: 'Create a pull reuqest', + name: 'Create a pull request', input: { repoUrl: 'github.com?repo=repo&owner=owner', branchName: 'new-app', @@ -223,7 +223,7 @@ export const examples: TemplateExample[] = [ steps: [ { action: 'publish:github:pull-request', - name: 'Create a pull reuqest', + name: 'Create a pull request', input: { repoUrl: 'github.com?repo=repo&owner=owner', branchName: 'new-app', @@ -237,13 +237,31 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Do not create empty pull request', + example: yaml.stringify({ + steps: [ + { + action: 'publish:github:pull-request', + name: 'Create a pull request', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + branchName: 'new-app', + title: 'Create my new app', + description: 'This PR is really good', + createWhenEmpty: false, + }, + }, + ], + }), + }, { description: 'Create a pull request with all parameters', example: yaml.stringify({ steps: [ { action: 'publish:github:pull-request', - name: 'Create a pull reuqest', + name: 'Create a pull request', input: { repoUrl: 'github.com?repo=repo&owner=owner', branchName: 'new-app', @@ -259,6 +277,7 @@ export const examples: TemplateExample[] = [ commitMessage: 'Commit for foo changes', gitAuthorName: 'Foo Bar', gitAuthorEmail: 'foo@bar.example', + createWhenEmpty: true, }, }, ], diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts index ccb1efa29b..9133b0d300 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts @@ -1062,4 +1062,119 @@ describe('createPublishGithubPullRequestAction', () => { }); }); }); + + describe('with createWhenEmpty equals true', () => { + let input: GithubPullRequestActionInput; + let ctx: ActionContext; + + beforeEach(() => { + input = { + repoUrl: 'github.com?owner=myorg&repo=myrepo', + title: 'Create my new app', + branchName: 'new-app', + description: 'This PR is really good', + createWhenEmpty: true, + }; + + mockDir.setContent({ + [workspacePath]: { 'file.txt': 'Hello there!' }, + }); + + ctx = createMockActionContext({ input, workspacePath }); + }); + it('creates a pull request', async () => { + await instance.handler(ctx); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'myorg', + repo: 'myrepo', + title: 'Create my new app', + head: 'new-app', + body: 'This PR is really good', + createWhenEmpty: true, + changes: [ + { + commit: 'Create my new app', + files: { + 'file.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + }); + + it('creates outputs for the pull request url and number', async () => { + await instance.handler(ctx); + + expect(ctx.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + expect(ctx.output).toHaveBeenCalledWith('pullRequestNumber', 123); + }); + + it('throws when creating a pull request fails', async () => { + fakeClient.createPullRequest.mockResolvedValueOnce(null); + + await expect(instance.handler(ctx)).rejects.toThrow( + 'null response from Github', + ); + }); + }); + + describe('with createWhenEmpty equals false', () => { + let input: GithubPullRequestActionInput; + let ctx: ActionContext; + + beforeEach(() => { + fakeClient.createPullRequest.mockResolvedValueOnce(null); + input = { + repoUrl: 'github.com?owner=myorg&repo=myrepo', + title: 'Create my new app', + branchName: 'new-app', + description: 'This PR is really good', + createWhenEmpty: false, + }; + + mockDir.setContent({ + [workspacePath]: { 'file.txt': 'Hello there!' }, + }); + + ctx = createMockActionContext({ input, workspacePath }); + }); + it('creates a pull request', async () => { + await instance.handler(ctx); + + expect(fakeClient.createPullRequest).toHaveBeenCalledWith({ + owner: 'myorg', + repo: 'myrepo', + title: 'Create my new app', + head: 'new-app', + body: 'This PR is really good', + createWhenEmpty: false, + changes: [ + { + commit: 'Create my new app', + files: { + 'file.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + }); + + it('does not create outputs for the pull request url and number', async () => { + await instance.handler(ctx); + + expect(ctx.output).not.toHaveBeenCalled(); + }); + }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts index 44830b3047..2f3662328d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts @@ -145,6 +145,7 @@ export const createPublishGithubPullRequestAction = ( gitAuthorName?: string; gitAuthorEmail?: string; forceEmptyGitAuthor?: boolean; + createWhenEmpty?: boolean; }>({ id: 'publish:github:pull-request', examples, @@ -251,10 +252,16 @@ export const createPublishGithubPullRequestAction = ( description: 'Forces the author to be empty. This is useful when using a Github App, it permit the commit to be verified on Github', }, + createWhenEmpty: { + type: 'boolean', + title: 'Create When Empty', + description: + 'Set whether to create pull request when there are no changes to commit. The default value is true', + }, }, }, output: { - required: ['remoteUrl'], + required: [], type: 'object', properties: { targetBranchName: { @@ -293,6 +300,7 @@ export const createPublishGithubPullRequestAction = ( gitAuthorEmail, gitAuthorName, forceEmptyGitAuthor, + createWhenEmpty, } = ctx.input; const { owner, repo, host } = parseRepoUrl(repoUrl, integrations); @@ -379,6 +387,7 @@ export const createPublishGithubPullRequestAction = ( draft, update, forceFork, + createWhenEmpty, }; const gitAuthorInfo = { @@ -417,6 +426,11 @@ export const createPublishGithubPullRequestAction = ( } const response = await client.createPullRequest(createOptions); + if (createWhenEmpty === false && !response) { + ctx.logger.info('No changes to commit, pull request was not created'); + return; + } + if (!response) { throw new GithubResponseError('null response from Github'); }