Skip to content

Commit eab297d

Browse files
authored
feat: add PR review reminder and draft conversion workflows (#1)
1 parent 2501a3e commit eab297d

2 files changed

Lines changed: 172 additions & 0 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Draft on Changes Requested
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
pr_number:
7+
required: true
8+
type: number
9+
secrets:
10+
token:
11+
required: true
12+
13+
jobs:
14+
convert-to-draft:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Convert PR to draft
18+
uses: actions/github-script@v7
19+
with:
20+
github-token: ${{ secrets.token }}
21+
script: |
22+
const prNumber = ${{ inputs.pr_number }};
23+
if (!Number.isInteger(prNumber) || prNumber <= 0) {
24+
core.setFailed(`Invalid PR number: ${prNumber}`);
25+
return;
26+
}
27+
28+
const { data: pr } = await github.rest.pulls.get({
29+
owner: context.repo.owner,
30+
repo: context.repo.repo,
31+
pull_number: prNumber,
32+
});
33+
34+
if (pr.draft) {
35+
core.info(`PR #${prNumber} is already a draft, skipping.`);
36+
return;
37+
}
38+
39+
await github.graphql(`
40+
mutation($id: ID!) {
41+
convertPullRequestToDraft(input: { pullRequestId: $id }) {
42+
pullRequest { isDraft }
43+
}
44+
}
45+
`, { id: pr.node_id });
46+
47+
core.info(`Converted PR #${prNumber} to draft.`);
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
name: PR Review Reminder
2+
3+
on:
4+
schedule:
5+
- cron: "0 7 * * 1-5"
6+
workflow_dispatch:
7+
8+
jobs:
9+
notify:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Fetch open PRs and notify Mattermost
13+
uses: actions/github-script@v7
14+
env:
15+
MATTERMOST_WEBHOOK: ${{ secrets.MATTERMOST_WEBHOOK }}
16+
with:
17+
script: |
18+
const repos = [
19+
{ owner: "DIRACGrid", name: "diracx" },
20+
{ owner: "DIRACGrid", name: "diracx-charts" },
21+
{ owner: "DIRACGrid", name: "diracx-web" },
22+
{ owner: "DIRACGrid", name: "mkdocs-diracx-plugin" },
23+
];
24+
25+
// Build a single GraphQL query with aliases for all repos
26+
const repoFragments = repos.map((r, i) =>
27+
`repo${i}: repository(owner: "${r.owner}", name: "${r.name}") {
28+
nameWithOwner
29+
pullRequests(states: OPEN, first: 100) {
30+
nodes {
31+
number
32+
title
33+
url
34+
isDraft
35+
createdAt
36+
author { login }
37+
reviews(last: 10) {
38+
nodes { state }
39+
}
40+
reviewRequests(first: 10) {
41+
totalCount
42+
nodes {
43+
requestedReviewer {
44+
... on User { login }
45+
... on Team { name }
46+
}
47+
}
48+
}
49+
}
50+
}
51+
}`
52+
).join("\n");
53+
54+
const query = `query { ${repoFragments} }`;
55+
const result = await github.graphql(query);
56+
57+
// Process results
58+
const sections = [];
59+
let totalPRs = 0;
60+
61+
for (let i = 0; i < repos.length; i++) {
62+
const data = result[`repo${i}`];
63+
const prs = data.pullRequests.nodes.filter(pr => !pr.isDraft);
64+
if (prs.length === 0) continue;
65+
totalPRs += prs.length;
66+
67+
const rows = prs.map(pr => {
68+
const age = Math.floor(
69+
(Date.now() - new Date(pr.createdAt).getTime()) / 86400000
70+
);
71+
const author = pr.author ? pr.author.login : "ghost";
72+
73+
// Determine review status
74+
let status;
75+
const reviewStates = pr.reviews.nodes.map(r => r.state);
76+
if (reviewStates.includes("CHANGES_REQUESTED")) {
77+
status = ":warning: Changes requested";
78+
} else if (reviewStates.includes("APPROVED")) {
79+
status = ":white_check_mark: Approved";
80+
} else if (pr.reviewRequests.totalCount > 0) {
81+
status = ":eyes: Review requested";
82+
} else {
83+
status = ":hourglass: Awaiting review";
84+
}
85+
86+
const reviewers = pr.reviewRequests.nodes
87+
.map(r => r.requestedReviewer?.login || r.requestedReviewer?.name || "")
88+
.filter(Boolean)
89+
.join(", ") || "-";
90+
91+
return `| [#${pr.number}](${pr.url}) | ${pr.title} | ${author} | ${reviewers} | ${age}d | ${status} |`;
92+
});
93+
94+
sections.push(
95+
`#### ${data.nameWithOwner}\n` +
96+
"| PR | Title | Author | Reviewer | Age | Status |\n" +
97+
"|:---|:------|:-------|:---------|:----|:-------|\n" +
98+
rows.join("\n")
99+
);
100+
}
101+
102+
if (totalPRs === 0) {
103+
core.info("No non-draft PRs found across any repos. Skipping notification.");
104+
return;
105+
}
106+
107+
const message = `### :mag: Open PR Review Summary\n\n${sections.join("\n\n")}`;
108+
109+
const webhookUrl = process.env.MATTERMOST_WEBHOOK;
110+
if (!webhookUrl) {
111+
core.setFailed("MATTERMOST_WEBHOOK secret is not set");
112+
return;
113+
}
114+
115+
const response = await fetch(webhookUrl, {
116+
method: "POST",
117+
headers: { "Content-Type": "application/json" },
118+
body: JSON.stringify({ text: message }),
119+
});
120+
121+
if (!response.ok) {
122+
core.setFailed(`Mattermost webhook failed: ${response.status} ${response.statusText}`);
123+
} else {
124+
core.info(`Posted summary of ${totalPRs} open PRs to Mattermost`);
125+
}

0 commit comments

Comments
 (0)