Skip to content

Commit 8251ad9

Browse files
Reopen Issues Without Linked PR (#8438)
* Create workflow to check linked PR * update workflow in response to review * Add delay to allow other GH Action changes * Remove test file
1 parent b1ef5f5 commit 8251ad9

File tree

3 files changed

+221
-0
lines changed

3 files changed

+221
-0
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Check Closed Issue for Linked PR
2+
3+
on:
4+
issues:
5+
types: [closed]
6+
7+
jobs:
8+
check-for-linked-issue:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Check Out Repository
12+
uses: actions/checkout@v6
13+
14+
- name: Check Issue Labels And Linked PRs
15+
uses: actions/github-script@v8
16+
id: check-issue-labels-and-linked-prs
17+
with:
18+
script: |
19+
const script = require(
20+
'./github-actions'
21+
+ '/check-closed-issue-for-linked-pr'
22+
+ '/check-for-linked-issue'
23+
+ '/check-issue-labels-and-linked-prs.js'
24+
);
25+
const isValidClose = await script({github, context});
26+
core.setOutput('isValidClose', isValidClose);
27+
28+
# Sleep to allow other GitHub Actions to change project status.
29+
- name: Sleep
30+
id: sleep
31+
shell: bash
32+
run: sleep 30s
33+
34+
- name: Reopen Issue
35+
if: steps.check-issue-labels-and-linked-prs.outputs.isValidClose == 'false'
36+
uses: actions/github-script@v8
37+
id: reopen-issue
38+
with:
39+
github-token: ${{ secrets.HACKFORLA_GRAPHQL_TOKEN }}
40+
script: |
41+
const script = require(
42+
'./github-actions'
43+
+ '/check-closed-issue-for-linked-pr'
44+
+ '/check-for-linked-issue'
45+
+ '/reopen-issue.js'
46+
);
47+
await script({github, context});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
const retrieveLabelDirectory = require('../../utils/retrieve-label-directory');
2+
3+
// Use labelKeys to retrieve current labelNames from directory
4+
const [
5+
nonPrContribution
6+
] = [
7+
'NEW-nonPrContribution'
8+
].map(retrieveLabelDirectory);
9+
10+
// ==================================================
11+
12+
/**
13+
* Checks whether a closed issue has a linked PR or one of the labels to excuse
14+
* this GitHub Actions workflow.
15+
*
16+
* @param {{github: object, context: object}} actionsGithubScriptArgs - GitHub
17+
* objects from actions/github-script
18+
* @returns {boolean} False if the issue does not have a linked PR, a "non-PR
19+
* contribution" label, or an "Ignore..." label.
20+
*/
21+
async function hasLinkedPrOrExcusableLabel({ github, context }) {
22+
const repoOwner = context.repo.owner;
23+
const repoName = context.repo.repo;
24+
const issueNumber = context.payload.issue.number;
25+
26+
const labels = context.payload.issue.labels.map((label) => label.name);
27+
28+
const consoleMessageAllowClose =
29+
`Issue #${issueNumber} is allowed to be closed.`;
30+
31+
// --------------------------------------------------
32+
33+
// Check if the issue has the labels that will avoid re-opening it.
34+
if (
35+
labels.some(
36+
(label) =>
37+
label === nonPrContribution || label.toLowerCase().includes('ignore')
38+
)
39+
) {
40+
console.info(consoleMessageAllowClose);
41+
return true;
42+
}
43+
44+
console.info(
45+
`Issue #${issueNumber} does not have ` +
46+
`the necessary labels to excuse reopening it.`
47+
);
48+
49+
// Use GitHub's GraphQL's closedByPullRequestsReferences to more reliably
50+
// determine if there is a linked PR.
51+
const query = `query($owner: String!, $repo: String!, $issue: Int!) {
52+
repository(owner: $owner, name: $repo) {
53+
issue(number: $issue) {
54+
closedByPullRequestsReferences(includeClosedPrs: true, first: 1) {
55+
totalCount
56+
}
57+
}
58+
}
59+
}`;
60+
61+
const variables = {
62+
owner: repoOwner,
63+
repo: repoName,
64+
issue: issueNumber,
65+
};
66+
67+
// Determine if there is a linked PR.
68+
try {
69+
const response = await github.graphql(query, variables);
70+
71+
const numLinkedPrs =
72+
response.repository.issue.closedByPullRequestsReferences.totalCount;
73+
74+
console.debug(`Number of linked PRs found: ${numLinkedPrs}.`);
75+
76+
if (numLinkedPrs > 0) {
77+
console.info(consoleMessageAllowClose);
78+
return true;
79+
}
80+
} catch (err) {
81+
throw new Error(
82+
`Can not find issue #${issueNumber} or its PR count; error = ${err}`
83+
);
84+
}
85+
console.info(`Issue #${issueNumber} does not have a linked PR.`);
86+
87+
// If the issue does not have a linked PR or any of the excusable labels.
88+
console.info(`Issue #${issueNumber} is not allowed to be closed.`);
89+
return false;
90+
}
91+
92+
// ==================================================
93+
94+
module.exports = hasLinkedPrOrExcusableLabel;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const queryIssueInfo = require('../../utils/query-issue-info');
2+
const mutateIssueStatus = require('../../utils/mutate-issue-status');
3+
const postComment = require('../../utils/post-issue-comment');
4+
5+
const statusFieldIds = require('../../utils/_data/status-field-ids');
6+
const labelDirectory = require('../../utils/_data/label-directory.json');
7+
8+
// ==================================================
9+
10+
/**
11+
* Reopens an issue that does not have a linked PR or excusable labels. Adds a
12+
* "ready for product" label, sets the project status to Questions / In Review",
13+
* and posts a comment to the issue.
14+
*
15+
* @param {{github: object, context: object}} actionsGithubScriptArgs -
16+
* GitHub objects from actions/github-script
17+
*/
18+
async function reopenIssue({ github, context }) {
19+
const repoOwner = context.repo.owner;
20+
const repoName = context.repo.repo;
21+
const issueNumber = context.payload.issue.number;
22+
23+
const labelsToAdd = [labelDirectory.readyForPM[0]];
24+
25+
const newStatusFieldId = statusFieldIds('Questions_In_Review');
26+
27+
const comment =
28+
'This issue was reopened because ' +
29+
`it did not have any of the following:
30+
- A linked PR,
31+
- An \`Ignore\` label
32+
- A \`non-PR contribution\` label`;
33+
34+
// --------------------------------------------------
35+
36+
// Add the "ready for product" label.
37+
try {
38+
await github.rest.issues.addLabels({
39+
owner: repoOwner,
40+
repo: repoName,
41+
issue_number: issueNumber,
42+
labels: labelsToAdd,
43+
});
44+
} catch (err) {
45+
throw new Error(
46+
`Unable to add "ready for product" label to issue #${issueNumber}; ` +
47+
`error = ${err}`
48+
);
49+
}
50+
console.info(`Added "ready for product" label to issue #${issueNumber}.`);
51+
52+
// Change the project status of the issue to "Questions / In Review".
53+
const issueInfo = await queryIssueInfo(github, context, issueNumber);
54+
await mutateIssueStatus(github, context, issueInfo.id, newStatusFieldId);
55+
console.info(
56+
`Changed project status to ` +
57+
`"Questions / In Review" in issue #${issueNumber}.`
58+
);
59+
60+
// Post comment to the issue.
61+
await postComment(issueNumber, comment, github, context);
62+
console.info(`Posted comment to issue #${issueNumber}.`);
63+
64+
// Re-opening the issue.
65+
try {
66+
await github.rest.issues.update({
67+
owner: repoOwner,
68+
repo: repoName,
69+
issue_number: issueNumber,
70+
state: 'open',
71+
});
72+
} catch (err) {
73+
throw new Error(`Unable to reopen issue #${issueNumber}; error = ${err}`);
74+
}
75+
console.info(`Reopened issue #${issueNumber}.`);
76+
}
77+
78+
// ==================================================
79+
80+
module.exports = reopenIssue;

0 commit comments

Comments
 (0)