Skip to content

Commit 265917f

Browse files
authored
Merge pull request #48 from exploreriii/link-test
test: auto close test
2 parents 3207c68 + 8450b45 commit 265917f

File tree

3 files changed

+171
-0
lines changed

3 files changed

+171
-0
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// A script to closes pull requests without a linked issue after 3 days automatically.
2+
3+
// dryRun env var: any case-insensitive 'true' value will enable dry-run
4+
const dryRun = (process.env.DRY_RUN || 'false').toString().toLowerCase() === 'true';
5+
// const daysBeforeClose = parseInt(process.env.DAYS_BEFORE_CLOSE || '3', 10);
6+
const daysBeforeClose = Number(process.env.DAYS_BEFORE_CLOSE || '0.0035');
7+
8+
const requireAuthorAssigned = (process.env.REQUIRE_AUTHOR_ASSIGNED || 'true').toLowerCase() === 'true';
9+
10+
// const getDaysOpen = (pr) =>
11+
// Math.floor((Date.now() - new Date(pr.created_at)) / (24 * 60 * 60 * 1000));
12+
13+
const getDaysOpen = (pr) =>
14+
(Date.now() - new Date(pr.created_at)) / (24 * 60 * 60 * 1000);
15+
16+
// Check if the PR author is assigned to the issue
17+
const isAuthorAssigned = (issue, login) => {
18+
if (!issue || issue.state?.toUpperCase() !== 'OPEN') return false;
19+
const assignees = issue.assignees?.nodes?.map(a => a.login) || [];
20+
return assignees.includes(login);
21+
};
22+
23+
const baseMessage = `Hi there! I'm the LinkedIssueBot.\nThis pull request has been automatically closed due to the following reason(s):
24+
`;
25+
const messageSuffix = `Please read - [Creating Issues](docs/sdk_developers/creating_issues.md) - [How To Link Issues](docs/sdk_developers/how_to_link_issues.md)\n\nThank you,
26+
From Python SDK team`
27+
28+
const messages = {
29+
no_issue: `${baseMessage} - Reason: This pull request is not linked to any issue. Please link it to an issue and reopen the pull request if this is an error.\n${messageSuffix}`,
30+
not_assigned: `${baseMessage} - Reason: You are not assigned to the linked issue. Please ensure you are assigned before reopening the pull request.\n${messageSuffix}`
31+
};
32+
33+
// Fetch linked issues using GraphQL
34+
async function getLinkedIssues(github, pr, owner, repo) {
35+
const query = `
36+
query($owner: String!, $repo: String!, $prNumber: Int!) {
37+
repository(owner: $owner, name: $repo) {
38+
pullRequest(number: $prNumber) {
39+
closingIssuesReferences(first: 100) {
40+
nodes {
41+
number
42+
state
43+
assignees(first: 100) {
44+
nodes { login }
45+
}
46+
}
47+
}
48+
}
49+
}
50+
}
51+
`;
52+
try {
53+
const result = await github.graphql(query, { owner, repo, prNumber: pr.number });
54+
const allIssues = result.repository.pullRequest.closingIssuesReferences.nodes || [];
55+
// Return only open issues
56+
return allIssues.filter(issue => issue.state === 'OPEN');
57+
} catch (err) {
58+
console.error(`GraphQL query failed for PR #${pr.number}:`, err.message);
59+
return null; // Signal error
60+
}
61+
}
62+
63+
// Validation
64+
async function validatePR(github, pr, owner, repo) {
65+
const issues = await getLinkedIssues(github, pr, owner, repo);
66+
67+
// Skip on API errors (fail-safe)
68+
if (issues === null) {
69+
console.log(`Skipping PR #${pr.number} due to API error`);
70+
return { valid: true };
71+
}
72+
73+
if (issues.length === 0) return { valid: false, reason: 'no_issue' };
74+
75+
if (requireAuthorAssigned) {
76+
const assigned = issues.some(issue => isAuthorAssigned(issue, pr.user.login));
77+
if (!assigned) return { valid: false, reason: 'not_assigned' };
78+
}
79+
return { valid: true };
80+
}
81+
82+
async function closePR(github, pr, owner, repo, reason) {
83+
if (dryRun) {
84+
console.log(`[DRY RUN] Would close PR #${pr.number} ${pr.html_url} (${reason})`);
85+
return true;
86+
}
87+
88+
try {
89+
await github.rest.issues.createComment({
90+
owner, repo, issue_number: pr.number,
91+
body: messages[reason]
92+
});
93+
await github.rest.pulls.update({
94+
owner, repo, pull_number: pr.number, state: 'closed'
95+
});
96+
console.log(`✓ Closed PR #${pr.number} (${reason}) link: ${pr.html_url}`);
97+
} catch (err) {
98+
console.error(`✗ Failed to close PR #${pr.number}:`, err.message);
99+
}
100+
}
101+
102+
module.exports = async ({ github, context }) => {
103+
try {
104+
const { owner, repo } = context.repo;
105+
const prs = await github.paginate(github.rest.pulls.list, {
106+
owner, repo, state: 'open', per_page: 100
107+
});
108+
109+
console.log(`Evaluating ${prs.length} open PRs\n`);
110+
111+
for (const pr of prs) {
112+
const days = getDaysOpen(pr);
113+
if (days < daysBeforeClose) {
114+
console.log(`PR #${pr.number} link: ${pr.html_url} is only ${days} days old. Skipping.`);
115+
continue;
116+
}
117+
118+
const { valid, reason } = await validatePR(github, pr, owner, repo);
119+
if (valid) {
120+
console.log(`PR #${pr.number} link: ${pr.html_url} is Valid ✓.`);
121+
} else {
122+
await closePR(github, pr, owner, repo, reason);
123+
}
124+
}
125+
} catch (err) {
126+
console.error('Unexpected error:', err.message);
127+
}
128+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# This workflow automatically closes pull requests without a linked issue after 3 days.
2+
3+
name: Linked Issue Enforcer
4+
5+
on:
6+
schedule:
7+
- cron: '45 11 * * 1,4'
8+
workflow_dispatch:
9+
inputs:
10+
dry_run:
11+
description: 'If true, do not post comments (dry run). Accepts "true" or "false". Default true for manual runs.'
12+
required: false
13+
default: 'true'
14+
15+
permissions:
16+
pull-requests: write
17+
contents: read
18+
19+
jobs:
20+
pr-linked-issue-checker:
21+
runs-on: ubuntu-latest
22+
env:
23+
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
24+
25+
steps:
26+
- name: Harden the runner
27+
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
28+
with:
29+
egress-policy: audit
30+
- name: Checkout repository
31+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
32+
- name: Enforce linked issues on PRs
33+
env:
34+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35+
DRY_RUN: ${{ env.DRY_RUN }}
36+
DAYS_BEFORE_CLOSE: '3'
37+
REQUIRE_AUTHOR_ASSIGNED: 'true'
38+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0
39+
with:
40+
script: |
41+
const script = require('./.github/scripts/linked_issue_enforce.js');
42+
await script({ github, context});

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.
5858
- Add new tests to `tests/unit/topic_info_query_test.py` (#1124)
5959
- Added `coding_token_transactions.md` for a high level overview training on how token transactions are created in the python sdk.
6060
- Added prompt for codeRabbit on how to review /examples ([#1180](https://github.com/hiero-ledger/hiero-sdk-python/issues/1180))
61+
- Add Linked Issue Enforcer to automatically close PRs without linked issues `.github/workflows/bot-linked-issue-enforcer.yml`.
6162

6263
### Changed
6364
- Updated Codecov coverage thresholds in 'codecov.yml' to require 90% of project coverage and 92% of patch coverage (#1157)

0 commit comments

Comments
 (0)