-
Notifications
You must be signed in to change notification settings - Fork 601
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2088 from newrelic/issue_closer
add 'PR Closed' workflow and 'issue_closer' action
- Loading branch information
Showing
5 changed files
with
220 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# Issue Closer JavaScript GitHub Action | ||
|
||
GitHub will automatically close an issue when a PR containing a "resolves #1234" | ||
type comment is merged, but only if the PR is merged into the default branch. | ||
|
||
This action will close any issue referenced by a merged PR regardless of the | ||
branch the PR was merged into. | ||
|
||
## Inputs | ||
|
||
token: A GitHub token with permission to read PR body text, read PR comments, | ||
and close referenced GitHub issues. | ||
|
||
## Outputs | ||
|
||
(none) | ||
|
||
## Example usage | ||
|
||
```yaml | ||
# Example GitHub Workflow | ||
name: PR Closed | ||
|
||
on: | ||
pull_request: | ||
types: | ||
- closed | ||
|
||
jobs: | ||
issue_closer: | ||
if: github.event.pull_request.merged == true | ||
runs-on: ubuntu-latest | ||
permissions: write-all | ||
steps: | ||
uses: actions/issue-closer | ||
with: | ||
token: ${{ secrets.GITHUB_TOKEN }} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
name: 'Issue Closer' | ||
description: 'Close GitHub Issues on PR merges to non-default branches' | ||
inputs: | ||
token: | ||
description: 'A GitHub token with PR read and Issue close permissions' | ||
required: true | ||
runs: | ||
using: 'node16' | ||
main: 'index.js' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
const core = require('@actions/core'); | ||
const github = require('@actions/github'); | ||
const octokit_graphql = require('@octokit/graphql'); | ||
|
||
const DEFAULT_BRANCH = 'dev'; | ||
const COMMENT_COUNT = 100; | ||
const RESPONSE_SUCCESS = 200; | ||
|
||
async function prComments(owner, repo, number, token) { | ||
query = `query { | ||
repository(owner: "${owner}", name: "${repo}") { | ||
pullRequest(number: ${number}) { | ||
comments(last: ${COMMENT_COUNT}) { | ||
edges { | ||
node { | ||
bodyText | ||
} | ||
} | ||
}, | ||
body | ||
} | ||
} | ||
}` | ||
|
||
const results = await octokit_graphql.graphql({ | ||
query: query, | ||
headers: { | ||
authorization: `token ${token}` | ||
} | ||
}); | ||
|
||
const pr = results.repository.pullRequest; | ||
const combined = [pr.body]; // treat the PR body and comments as equals | ||
const comments = pr.comments.edges; | ||
let i = 0; | ||
while (i < comments.length) { | ||
combined.push(comments[i].node.bodyText); | ||
i++; | ||
} | ||
|
||
return combined; | ||
} | ||
|
||
async function issueNumbersFromComment(comment) { | ||
const pattern = /(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s+#(\d+)(?:(?:\s|,)+#(\d+))*/gi; | ||
const matches = pattern.exec(comment); | ||
|
||
if (matches) { | ||
matches.shift(); // $0 holds the entire match | ||
return matches.filter(ele => { return ele !== undefined; }) | ||
} else { | ||
return; | ||
} | ||
} | ||
|
||
async function issueNumbersFromPRComments(comments) { | ||
let issueNumbers = []; | ||
let i = 0; | ||
while (i < comments.length) { | ||
numbers = await issueNumbersFromComment(comments[i]); | ||
if (numbers) { | ||
issueNumbers = issueNumbers.concat(numbers); | ||
} | ||
i++; | ||
} | ||
return [...new Set(issueNumbers)]; // de-dupe | ||
} | ||
|
||
async function closeIssues(issueNumbers, owner, repo, token) { | ||
const octokit = github.getOctokit(token); | ||
|
||
let i = 0; | ||
while (i < issueNumbers.length) { | ||
console.log(`Using Octokit to close Issue #${issueNumbers[0]}...`); | ||
const response = await octokit.rest.issues.update({ | ||
owner: owner, | ||
repo: repo, | ||
issue_number: issueNumbers[0], | ||
state: 'closed' | ||
}); | ||
if (response.status != RESPONSE_SUCCESS) { | ||
throw `REST call to update issue ${issueNumbers[0]} failed - ${JSON.stringify(response)}` | ||
} | ||
i++; | ||
} | ||
} | ||
|
||
async function run() { | ||
try { | ||
const token = core.getInput('token'); | ||
if (!token) { | ||
throw "Action input 'token' is not set!"; | ||
} | ||
|
||
const payload = github.context.payload; | ||
|
||
const action = payload.action; | ||
if (action != "closed") { | ||
throw `Received invalid action of '${action}'. Expected 'closed'. Is a Workflow condition missing?`; | ||
} | ||
|
||
const number = payload.number; | ||
console.log(`The PR number is ${number}`); | ||
|
||
const base = payload.pull_request.base; | ||
|
||
const ref = base.ref; | ||
console.log(`The PR ref is ${ref}`); | ||
if (ref == DEFAULT_BRANCH) { | ||
console.log(`PR ${number} targeted branch ${ref}. Exiting.`); | ||
return | ||
} | ||
|
||
const fullName = base.repo.full_name; | ||
console.log(`The repo full name is ${fullName}`); | ||
const repoElements = fullName.split('/'); | ||
const owner = repoElements[0]; | ||
const repo = repoElements[1]; | ||
console.log(`PR repo owner = ${owner}, repo = ${repo}`); | ||
|
||
const comments = await prComments(owner, repo, number, token); | ||
const issueNumbers = await issueNumbersFromPRComments(comments); | ||
|
||
if (issueNumbers.length == 0) { | ||
console.log('No comments found with issue closing syntax'); | ||
return; | ||
} | ||
|
||
console.log(`Issue ids in need of closing: ${issueNumbers}`); | ||
closeIssues(issueNumbers, owner, repo, token); | ||
console.log('Done'); | ||
} catch (error) { | ||
core.setFailed(error.message); | ||
} | ||
} | ||
|
||
run(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"name": "issue_closer", | ||
"version": "1.0.0", | ||
"description": "Close GitHub Issues on PR merges to non-default branches", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "echo \"TODO: add tests\" && exit 1" | ||
}, | ||
"keywords": [], | ||
"author": "New Relic Ruby agent team", | ||
"license": "Apache 2", | ||
"dependencies": { | ||
"@actions/core": "^1.10.0", | ||
"@actions/github": "^5.1.1", | ||
"@octokit/graphql": "^5.0.6" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
name: PR Closed | ||
|
||
on: | ||
pull_request: | ||
types: | ||
- closed | ||
|
||
jobs: | ||
issue_closer: | ||
if: github.event.pull_request.merged == true | ||
runs-on: ubuntu-latest | ||
permissions: write-all | ||
steps: | ||
- name: Clone the repo | ||
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # tag v3.5.0 | ||
- name: issue_closer Action | ||
uses: ./.github/actions/issue_closer # no tag - locally sourced | ||
with: | ||
token: ${{ secrets.GITHUB_TOKEN }} # permission needed to read PR comments and close issues |