Skip to content

Commit e679c7d

Browse files
committed
Add voting system also for new pipelines
1 parent 9fd8097 commit e679c7d

File tree

1 file changed

+166
-0
lines changed

1 file changed

+166
-0
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
name: Pipeline approval automation
2+
3+
on:
4+
issues:
5+
types: [opened, closed]
6+
issue_comment:
7+
types: [created, edited]
8+
9+
jobs:
10+
pipeline_approval:
11+
# Only run for pipeline proposal issues
12+
if: startsWith(github.event.issue.title, 'New Pipeline')
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Handle Pipeline approval logic
17+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #v7.0.1
18+
with:
19+
github-token: ${{ secrets.nf_core_bot_auth_token }}
20+
script: |
21+
const issueNumber = context.issue.number;
22+
const org = context.repo.owner;
23+
const repo = context.repo.repo;
24+
25+
// Ignore comments on closed issues
26+
if (context.eventName === 'issue_comment' && context.payload.issue.state === 'closed') {
27+
console.log('Comment event on closed issue, ignoring.');
28+
return;
29+
}
30+
31+
// ---------------------------------------------
32+
// Fetch members of the core team
33+
// ---------------------------------------------
34+
async function getTeamMembers() {
35+
try {
36+
const res = await github.request('GET /orgs/{org}/teams/{team_slug}/members', {
37+
org,
38+
team_slug: 'core',
39+
per_page: 100
40+
});
41+
console.log(`Fetched ${res.data.length} core team members.`);
42+
return res.data.map(m => m.login);
43+
} catch (err) {
44+
console.error('Failed to fetch core team members:', err);
45+
throw err;
46+
}
47+
}
48+
49+
const teamMembers = await getTeamMembers();
50+
console.log('Core team members:', teamMembers);
51+
const quorum = Math.ceil(teamMembers.length / 2);
52+
console.log(`Quorum set to ${quorum}.`);
53+
54+
// Helper for list formatting and complete status body
55+
function formatUserList(users) {
56+
return users.length ? users.map(u => `[@${u}](https://github.com/${u})`).join(', ') : '-';
57+
}
58+
59+
function generateStatusBody(status, approvalsSet, rejectionsSet, awaitingArr) {
60+
const approvers = [...approvalsSet];
61+
const rejecters = [...rejectionsSet];
62+
const awaiting = awaitingArr;
63+
let body = `## Pipeline approval status: ${status}\n\nPipeline has approvals from ${approvalsSet.size}/${quorum} required @core-team quorum.\n\n`;
64+
if (approvers.length > 0 || rejecters.length > 0 || awaiting.length > 0) {
65+
body += `|Review Status|Core Team members|\n|--|--|\n`;
66+
if (approvers.length > 0) {
67+
body += `| ✅ Approved | ${formatUserList(approvers)} |\n`;
68+
}
69+
if (rejecters.length > 0) {
70+
body += `| ❌ Rejected | ${formatUserList(rejecters)} |\n`;
71+
}
72+
if (awaiting.length > 0) {
73+
body += `| 🕐 Pending | ${formatUserList(awaiting)} |\n`;
74+
}
75+
}
76+
return body;
77+
}
78+
79+
// -------------------------------------------------
80+
// If this workflow was triggered by issue creation
81+
// -------------------------------------------------
82+
if (context.eventName === 'issues' && context.payload.action === 'opened') {
83+
const body = generateStatusBody('🕐 Pending', new Set(), new Set(), teamMembers);
84+
console.log('Creating initial comment for review status');
85+
86+
await github.rest.issues.createComment({
87+
owner: org,
88+
repo,
89+
issue_number: issueNumber,
90+
body
91+
});
92+
return; // Nothing more to do for newly-opened issues
93+
}
94+
95+
// ---------------------------------------------------------------------
96+
// Collect comments and compute votes (shared for comment & closed events)
97+
// ---------------------------------------------------------------------
98+
99+
// Collect all comments on the issue
100+
const comments = await github.paginate(github.rest.issues.listComments, {
101+
owner: org,
102+
repo,
103+
issue_number: issueNumber,
104+
per_page: 100
105+
});
106+
107+
const approvals = new Set();
108+
const rejections = new Set();
109+
110+
for (const comment of comments) {
111+
const commenter = comment.user.login;
112+
if (!teamMembers.includes(commenter)) continue; // Only core team members count
113+
114+
// Count approvals / rejections based on line starting with /approve or /reject
115+
const lines = comment.body.split(/\r?\n/);
116+
for (const rawLine of lines) {
117+
const line = rawLine.trim();
118+
if (/^\/approve\b/i.test(line)) {
119+
approvals.add(commenter);
120+
} else if (/^\/reject\b/i.test(line)) {
121+
rejections.add(commenter);
122+
}
123+
}
124+
}
125+
126+
console.log(`Approvals (${approvals.size}):`, [...approvals]);
127+
console.log(`Rejections (${rejections.size}):`, [...rejections]);
128+
129+
const awaiting = teamMembers.filter(u => !approvals.has(u) && !rejections.has(u));
130+
131+
// Determine status
132+
let status = '🕐 Pending';
133+
134+
if (context.eventName === 'issues' && context.payload.action === 'closed' && context.payload.issue.state_reason === 'not_planned' && rejections.size > 0) {
135+
status = '❌ Rejected';
136+
} else if (approvals.size >= quorum && rejections.size === 0) {
137+
status = '✅ Approved';
138+
}
139+
140+
const statusBody = generateStatusBody(status, approvals, rejections, awaiting);
141+
console.log('New status body to post:\n', statusBody);
142+
143+
// Try to locate the existing status comment (starts with our header)
144+
let statusComment = comments.find(c => c.body.startsWith('## Pipeline approval status:'));
145+
146+
if (statusComment) {
147+
if (statusComment.body.trim() === statusBody.trim()) {
148+
console.log('Status comment already up to date - no update required.');
149+
} else {
150+
console.log('Updating existing status comment.');
151+
await github.rest.issues.updateComment({
152+
owner: org,
153+
repo,
154+
comment_id: statusComment.id,
155+
body: statusBody
156+
});
157+
}
158+
} else {
159+
// Fallback: create a new status comment if missing (shouldn't normally happen)
160+
await github.rest.issues.createComment({
161+
owner: org,
162+
repo,
163+
issue_number: issueNumber,
164+
body: statusBody
165+
});
166+
}

0 commit comments

Comments
 (0)