Skip to content

Commit

Permalink
Adding reviewers as input and pull request url as output (#104)
Browse files Browse the repository at this point in the history
* Add output for pull request URL in UpstreamToPr class

* Add output for pull request URL in action.yml

Fix output formatting for pull request URL in UpstreamToPr class

Add reviewers and team_reviewers inputs to action.yml and implement review requests in UpstreamToPr class

Log reviewers and team reviewers inputs in the run function

build lib

Log reviewer requests for pull requests in UpstreamToPr class

Refactor reviewer handling in createPR and requestReviewers methods

* Remove debug logs

* Add tests for reviewer requests

* Update README.md to include reviewer params and action output variable
  • Loading branch information
jordyantunes authored Jan 23, 2025
1 parent 1e939c6 commit c19156c
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 10 deletions.
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,69 @@ jobs:
upstream-tag: 'v1\..*'
```

#### Requesting reviewers or team reviewers

`reviewers` and `team_reviewers` are optional parameters that expect a comma separated string with usernames or teams respectively.

```yaml
name: upstream to PR
on:
schedule:
- cron: "0 12 * * *"
workflow_dispatch:
inputs: {}
jobs:
autoupdate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.PAT }}
- uses: fopina/upstream-to-pr@v1
with:
token: ${{ secrets.PAT }}
upstream-repository: https://github.com/surface-security/surface
upstream-tag: 'v1\.\d+\.\d+'
reviewers: person1,person2
team_reviewers: team1,team2
```

#### Getting PR API url

Upstream to PR outputs the API URL as the `pull-request-url` variable for the created Pull Request so you can use it in other steps.

```yaml
name: upstream to PR
on:
schedule:
- cron: "0 12 * * *"
workflow_dispatch:
inputs: {}
jobs:
autoupdate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.PAT }}
- uses: fopina/upstream-to-pr@v1
id: auto-pr
with:
token: ${{ secrets.PAT }}
upstream-repository: https://github.com/surface-security/surface
upstream-tag: 'v1\.\d+\.\d+'
reviewers: person1,person2
team_reviewers: team1,team2
- name: Display output
run: |
echo "Pull Request API URL: ${{ steps.auto-pr.outputs.pull-request-url }}"
```
95 changes: 89 additions & 6 deletions __tests__/upstream-to-pr.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import {UpstreamToPr, UpstreamToPrOptions} from '../src/upstream-to-pr'
import {
UpstreamToPr,
UpstreamToPrOptions,
PullRequest
} from '../src/upstream-to-pr'
import * as process from 'process'
import * as exec from '@actions/exec'
import * as core from '@actions/core'
Expand All @@ -16,7 +20,9 @@ const defaultOptions: UpstreamToPrOptions = {
token: 'xXx',
currentBranch: 'main',
upstreamTag: '',
keepOld: false
keepOld: false,
reviewers: [],
team_reviewers: []
}

function gitMock(opts: {branchList?: string; revParse?: string | null}) {
Expand Down Expand Up @@ -77,7 +83,8 @@ describe('test upstream-to-pr with branch', () => {
.spyOn(octoMock.rest.pulls, 'create')
.mockResolvedValue({
data: {
url: 'http://git.url/to/pr'
url: 'http://git.url/to/pr',
number: 123
}
} as any)
jest.spyOn(github, 'getOctokit').mockReturnValue(octoMock)
Expand Down Expand Up @@ -134,7 +141,8 @@ x`,
.spyOn(octoMock.rest.pulls, 'create')
.mockResolvedValue({
data: {
url: 'http://git.url/to/pr'
url: 'http://git.url/to/pr',
number: 123
}
} as any)
jest.spyOn(github, 'getOctokit').mockReturnValue(octoMock)
Expand Down Expand Up @@ -294,7 +302,8 @@ describe('test upstream-to-pr update-tag', () => {
.spyOn(octoMock.rest.pulls, 'create')
.mockResolvedValue({
data: {
url: 'http://git.url/to/pr'
url: 'http://git.url/to/pr',
number: 123
}
} as any)
await new UpstreamToPr({
Expand Down Expand Up @@ -342,7 +351,8 @@ describe('test upstream-to-pr createPR', () => {
.spyOn(octoMock.rest.pulls, 'create')
.mockResolvedValue({
data: {
url: 'http://git.url/to/pr'
url: 'http://git.url/to/pr',
number: 123
}
} as any)
const createPRArgs: [string, string, string] = [
Expand Down Expand Up @@ -411,4 +421,77 @@ Commit summary omitted as it exceeds maximum message size.`,
title: 'Upstream branch main (revision bababa)'
})
})
it('reviewers list', async () => {
const reviewers = ['reviewer1', 'reviewer2']
const upstreamMock = new UpstreamToPr({
...defaultOptions,
reviewers
})
const upstreamToPRRequestReviewerMock = jest.spyOn(
upstreamMock,
'requestReviewers'
)

await upstreamMock.createPR(...createPRArgs)
expect(mInfo).toBeCalledTimes(3)
expect(mInfo).toHaveBeenNthCalledWith(1, prLine)
expect(upstreamToPRRequestReviewerMock).toHaveBeenCalledWith({
number: 123,
url: 'http://git.url/to/pr'
})
})
})

describe('test upstream-to-pr requestReviewers', () => {
const prLine = 'Pull request created: http://git.url/to/pr.'
const octoMock = github.getOctokit('x')
jest.spyOn(github, 'getOctokit').mockReturnValue(octoMock)
const requestReviewerMock = jest
.spyOn(octoMock.rest.pulls, 'requestReviewers')
.mockResolvedValue({
data: {
url: 'http://git.url/to/pr',
number: 123
}
} as any)

const requestReviewersArgs: PullRequest = {
url: 'http://git.url/to/pr',
number: 123
}

it('reviewer list', async () => {
const reviewers = ['reviewer1', 'reviewer2']
const team_reviewers: string[] = []
const reviewersLine = `Reviewers requested for pull request: ${requestReviewersArgs.url}. Reviewers: ${reviewers}, team_reviewers: ${team_reviewers}`
await new UpstreamToPr({
...defaultOptions,
reviewers
}).requestReviewers(requestReviewersArgs)
expect(mInfo).toBeCalledTimes(1)
expect(mInfo).toHaveBeenNthCalledWith(1, reviewersLine)
expect(requestReviewerMock).toHaveBeenCalledWith({
owner: 'xxx',
repo: undefined,
pull_number: 123,
reviewers
})
})
it('team reviewer list', async () => {
const reviewers: string[] = []
const team_reviewers = ['team1', 'team2']
const reviewersLine = `Reviewers requested for pull request: ${requestReviewersArgs.url}. Reviewers: ${reviewers}, team_reviewers: ${team_reviewers}`
await new UpstreamToPr({
...defaultOptions,
team_reviewers
}).requestReviewers(requestReviewersArgs)
expect(mInfo).toBeCalledTimes(1)
expect(mInfo).toHaveBeenNthCalledWith(1, reviewersLine)
expect(requestReviewerMock).toHaveBeenCalledWith({
owner: 'xxx',
repo: undefined,
pull_number: 123,
team_reviewers
})
})
})
10 changes: 9 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ inputs:
keep-old:
description: 'delete older PRs (and branches) when a more recent PR is open'
default: 'false'

reviewers:
description: 'comma-separated list of reviewers for the PR'
required: false
team_reviewers:
description: 'comma-separated list of team reviewers for the PR'
required: false
outputs:
pull-request-url:
description: 'URL of the pull request created (if any)'
runs:
using: 'node20'
main: 'dist/index.js'
Expand Down
29 changes: 28 additions & 1 deletion dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ async function run(): Promise<void> {
const upstreamBranch: string = core.getInput('upstream-branch') || 'main'
const upstreamTag: string = core.getInput('upstream-tag')
const keepOld: boolean = core.getBooleanInput('keep-old')
const reviewers: string[] = core.getInput('reviewers').split(',')
const team_reviewers: string[] = core.getInput('team_reviewers').split(',')
// github.context does not expose REF_NAME nor HEAD_REF, just use env...
// try GITHUB_HEAD_REF (set if it is a PR) and fallback to GITHUB_REF_NAME

const currentBranch =
process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || ''

Expand All @@ -21,7 +24,9 @@ async function run(): Promise<void> {
token,
currentBranch,
upstreamTag,
keepOld
keepOld,
reviewers,
team_reviewers
}).run()
} catch (error) {
if (error instanceof Error) core.setFailed(error.message)
Expand Down
41 changes: 41 additions & 0 deletions src/upstream-to-pr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import * as exec from '@actions/exec'
import * as github from '@actions/github'
import * as io from '@actions/io'

export type PullRequest = {
url: string
number: number
}

const BRANCH_PREFIX = 'upstream-to-pr/rev-'
// GitHub PRs messages have a max body size limit of 65536
const PR_BODY_MAX_CHARACTERS = 60000
Expand All @@ -14,6 +19,8 @@ export interface UpstreamToPrOptions {
currentBranch: string
upstreamTag: string
keepOld: boolean
reviewers: string[]
team_reviewers: string[]
}

export class UpstreamToPr {
Expand Down Expand Up @@ -100,6 +107,40 @@ export class UpstreamToPr {
${changeList}`
})
core.info(`Pull request created: ${pullRequest.url}.`)
core.setOutput('pull-request-url', pullRequest.url)

if (
this.options.reviewers.length > 0 ||
this.options.team_reviewers.length > 0
) {
core.info(`Requesting reviewers for pull request: ${pullRequest.url}.`)
await this.requestReviewers(pullRequest as PullRequest)
}
}

async requestReviewers(pullRequest: PullRequest): Promise<void> {
const context = github.context
const octokit = github.getOctokit(this.options.token)
const reviewers = this.options.reviewers
const team_reviewers = this.options.team_reviewers

const review_payload: {reviewers?: string[]; team_reviewers?: string[]} = {}
if (reviewers.length > 0) {
review_payload['reviewers'] = reviewers
}
if (team_reviewers.length > 0) {
review_payload['team_reviewers'] = team_reviewers
}

await octokit.rest.pulls.requestReviewers({
...context.repo,
pull_number: pullRequest.number,
...review_payload
})

core.info(
`Reviewers requested for pull request: ${pullRequest.url}. Reviewers: ${reviewers}, team_reviewers: ${team_reviewers}`
)
}

async fetchHEAD(): Promise<string> {
Expand Down

0 comments on commit c19156c

Please sign in to comment.