diff --git a/.changeset/spicy-steaks-swim.md b/.changeset/spicy-steaks-swim.md new file mode 100644 index 0000000000..b2323c9b34 --- /dev/null +++ b/.changeset/spicy-steaks-swim.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Added optional assignees parameter to `publish:github:pull-request` action diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index 2d49e1a5bd..219506d948 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -435,6 +435,7 @@ export const createPublishGithubPullRequestAction: ( sourcePath?: string; token?: string; reviewers?: string[]; + assignees?: string[]; teamReviewers?: string[]; commitMessage?: string; update?: boolean; 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 fb2763db51..bbbbed6511 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 @@ -32,6 +32,9 @@ const mockOctokit = { pulls: { requestReviewers: jest.fn(), }, + issues: { + addAssignees: jest.fn(), + }, }, }; @@ -61,6 +64,7 @@ describe('publish:github:pull-request examples', () => { createPullRequest: jest.Mock; rest: { pulls: { requestReviewers: jest.Mock }; + issues: { addAssignees: jest.Mock }; }; }; const mockDir = createMockDirectory(); @@ -90,6 +94,9 @@ describe('publish:github:pull-request examples', () => { pulls: { requestReviewers: jest.fn(async (_: any) => ({ data: {} })), }, + issues: { + addAssignees: jest.fn(async (_: any) => ({ data: {} })), + }, }, }; @@ -716,4 +723,50 @@ describe('publish:github:pull-request examples', () => { ); expect(mockContext.output).toHaveBeenCalledWith('pullRequestNumber', 123); }); + + it('Create a pull request with assignees', async () => { + const input = yaml.parse(examples[14].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', + draft: undefined, + changes: [ + { + commit: 'Create my new app', + files: { + 'file.txt': { + content: Buffer.from('Hello there!').toString('base64'), + encoding: 'base64', + mode: '100644', + }, + }, + }, + ], + }); + + expect(fakeClient.rest.issues.addAssignees).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repo', + issue_number: 123, + assignees: ['foobar'], + }); + + 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); + }); }); 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 e35e6d9a81..8fd5b7543c 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.examples.ts @@ -283,4 +283,22 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Create a pull request with assignees', + example: yaml.stringify({ + steps: [ + { + action: 'publish:github:pull-request', + name: 'Create a pull request with reviewers', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + branchName: 'new-app', + title: 'Create my new app', + description: 'This PR is really good', + assignees: ['foobar'], + }, + }, + ], + }), + }, ]; 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 9d14747189..44e4cb17b3 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.test.ts @@ -40,6 +40,7 @@ describe('createPublishGithubPullRequestAction', () => { createPullRequest: jest.Mock; rest: { pulls: { requestReviewers: jest.Mock }; + issues: { addAssignees: jest.Mock }; }; }; let config: Config; @@ -72,6 +73,9 @@ describe('createPublishGithubPullRequestAction', () => { pulls: { requestReviewers: jest.fn(async (_: any) => ({ data: {} })), }, + issues: { + addAssignees: jest.fn(async (_: any) => ({ data: {} })), + }, }, }; const clientFactory = jest.fn(async () => fakeClient as any); @@ -115,6 +119,9 @@ describe('createPublishGithubPullRequestAction', () => { pulls: { requestReviewers: jest.fn(async (_: any) => ({ data: {} })), }, + issues: { + addAssignees: jest.fn(async (_: any) => ({ data: {} })), + }, }, }; @@ -422,6 +429,50 @@ describe('createPublishGithubPullRequestAction', () => { }); }); + describe('with assignees', () => { + 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', + assignees: ['user1', 'user2'], + }; + + mockDir.setContent({ [workspacePath]: {} }); + + ctx = createMockActionContext({ input, workspacePath }); + }); + + it('creates a pull request and adds the assignees', async () => { + await instance.handler(ctx); + + expect(fakeClient.createPullRequest).toHaveBeenCalled(); + expect(fakeClient.rest.issues.addAssignees).toHaveBeenCalledWith({ + owner: 'myorg', + repo: 'myrepo', + issue_number: 123, + assignees: ['user1', 'user2'], + }); + }); + it('creates outputs for the pull request url and number even if adding assignees fails', async () => { + fakeClient.rest.issues.addAssignees.mockImplementation(() => { + throw new Error('a random error'); + }); + + await instance.handler(ctx); + + expect(ctx.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://github.com/myorg/myrepo/pull/123', + ); + expect(ctx.output).toHaveBeenCalledWith('pullRequestNumber', 123); + }); + }); + describe('with broken symlink', () => { let input: GithubPullRequestActionInput; let ctx: ActionContext; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts index c0ff0fb3d4..adf80c0e0a 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts @@ -139,6 +139,7 @@ export const createPublishGithubPullRequestAction = ( sourcePath?: string; token?: string; reviewers?: string[]; + assignees?: string[]; teamReviewers?: string[]; commitMessage?: string; update?: boolean; @@ -212,6 +213,15 @@ export const createPublishGithubPullRequestAction = ( description: 'The users that will be added as reviewers to the pull request', }, + assignees: { + title: 'Pull Request Assignees', + type: 'array', + items: { + type: 'string', + }, + description: + 'The users that will be added as assignees to the pull request', + }, teamReviewers: { title: 'Pull Request Team Reviewers', type: 'array', @@ -295,6 +305,7 @@ export const createPublishGithubPullRequestAction = ( sourcePath, token: providedToken, reviewers, + assignees, teamReviewers, commitMessage, update, @@ -453,8 +464,8 @@ export const createPublishGithubPullRequestAction = ( } const pullRequestNumber = pr.number; + const pullRequest = { owner, repo, number: pullRequestNumber }; if (reviewers || teamReviewers) { - const pullRequest = { owner, repo, number: pullRequestNumber }; await requestReviewersOnPullRequest( pullRequest, reviewers, @@ -465,6 +476,21 @@ export const createPublishGithubPullRequestAction = ( ); } + if (assignees) { + if (assignees.length > 10) { + ctx.logger.warn( + 'Assignees list is too long, only the first 10 will be used.', + ); + } + await addAssigneesToPullRequest( + pullRequest, + assignees, + client, + ctx.logger, + ctx.checkpoint, + ); + } + const targetBranch = pr.base.ref; ctx.output('targetBranchName', targetBranch); ctx.output('remoteUrl', pr.html_url); @@ -475,6 +501,42 @@ export const createPublishGithubPullRequestAction = ( }, }); + async function addAssigneesToPullRequest( + pr: GithubPullRequest, + assignees: string[], + client: Octokit, + logger: LoggerService, + checkpoint: (opts: { + key: string; + fn: () => Promise | T; + }) => Promise, + ) { + try { + await checkpoint({ + key: `add.assignees.${pr.owner}.${pr.repo}.${pr.number}`, + fn: async () => { + const result = await client.rest.issues.addAssignees({ + owner: pr.owner, + repo: pr.repo, + issue_number: pr.number, + assignees, + }); + + const addedAssignees = result.data.assignees?.join(', ') ?? ''; + + logger.info( + `Added assignees [${addedAssignees}] to Pull request ${pr.number}`, + ); + }, + }); + } catch (e) { + logger.error( + `Failure when adding assignees to Pull request ${pr.number}`, + e, + ); + } + } + async function requestReviewersOnPullRequest( pr: GithubPullRequest, reviewers: string[] | undefined, diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index c3100d8259..0438922de3 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -339,6 +339,7 @@ export const createPublishGithubPullRequestAction: ( sourcePath?: string; token?: string; reviewers?: string[]; + assignees?: string[]; teamReviewers?: string[]; commitMessage?: string; update?: boolean;