Skip to content

Commit

Permalink
Merge pull request #2088 from newrelic/issue_closer
Browse files Browse the repository at this point in the history
add 'PR Closed' workflow and 'issue_closer' action
  • Loading branch information
fallwith authored Jun 20, 2023
2 parents f6d5ad5 + 18a2ff1 commit 140ea80
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 0 deletions.
38 changes: 38 additions & 0 deletions .github/actions/issue_closer/README.md
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 }}
```
9 changes: 9 additions & 0 deletions .github/actions/issue_closer/action.yml
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'
137 changes: 137 additions & 0 deletions .github/actions/issue_closer/index.js
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();
17 changes: 17 additions & 0 deletions .github/actions/issue_closer/package.json
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"
}
}
19 changes: 19 additions & 0 deletions .github/workflows/pr_closed.yml
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

0 comments on commit 140ea80

Please sign in to comment.