Skip to content

Commit 3d10ead

Browse files
authored
feat: add label community contribution (#594)
1 parent aad19b8 commit 3d10ead

File tree

1 file changed

+201
-0
lines changed
  • actions/auto/label-community-contribution

1 file changed

+201
-0
lines changed
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
name: Auto label community contribution
2+
description: Add community contribution label to merged fork PR and linked issues
3+
4+
inputs:
5+
github-token:
6+
description: GitHub token
7+
required: true
8+
pr-number:
9+
description: Pull request number
10+
required: true
11+
label-name:
12+
description: Label to apply
13+
required: false
14+
default: community contribution
15+
16+
runs:
17+
using: composite
18+
steps:
19+
- name: Label fork contribution
20+
uses: actions/github-script@v7
21+
env:
22+
PR_NUMBER: ${{ inputs.pr-number }}
23+
LABEL_NAME: ${{ inputs.label-name }}
24+
with:
25+
github-token: ${{ inputs.github-token }}
26+
script: |
27+
const prNumber = Number(process.env.PR_NUMBER);
28+
const label = process.env.LABEL_NAME || 'community contribution';
29+
30+
if (!prNumber) {
31+
throw new Error('PR_NUMBER is required');
32+
}
33+
34+
const owner = context.repo.owner;
35+
const repo = context.repo.repo;
36+
37+
async function ensureLabelExists() {
38+
try {
39+
await github.rest.issues.getLabel({
40+
owner,
41+
repo,
42+
name: label,
43+
});
44+
} catch (error) {
45+
if (error.status !== 404) {
46+
throw error;
47+
}
48+
49+
await github.rest.issues.createLabel({
50+
owner,
51+
repo,
52+
name: label,
53+
color: '0e8a16',
54+
description: 'Merged contribution from fork',
55+
});
56+
}
57+
}
58+
59+
async function addLabel(issue_number) {
60+
await github.rest.issues.addLabels({
61+
owner,
62+
repo,
63+
issue_number,
64+
labels: [label],
65+
});
66+
}
67+
68+
async function getPullRequest() {
69+
const {data} = await github.rest.pulls.get({
70+
owner,
71+
repo,
72+
pull_number: prNumber,
73+
});
74+
75+
return data;
76+
}
77+
78+
async function getClosingIssuesFromGraphQL() {
79+
const query = `
80+
query($owner: String!, $repo: String!, $number: Int!) {
81+
repository(owner: $owner, name: $repo) {
82+
pullRequest(number: $number) {
83+
closingIssuesReferences(first: 50) {
84+
nodes {
85+
number
86+
}
87+
}
88+
}
89+
}
90+
}
91+
`;
92+
93+
const result = await github.graphql(query, {
94+
owner,
95+
repo,
96+
number: prNumber,
97+
});
98+
99+
return result.repository?.pullRequest?.closingIssuesReferences?.nodes ?? [];
100+
}
101+
102+
function getClosingIssuesFromBody(body) {
103+
if (!body) {
104+
return [];
105+
}
106+
107+
const keywords = '(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)';
108+
109+
const sameRepoPattern = new RegExp(
110+
`\\b${keywords}\\s*:?\\s*#(\\d+)\\b`,
111+
'gi',
112+
);
113+
114+
const crossRepoPattern = new RegExp(
115+
`\\b${keywords}\\s*:?\\s*([\\w.-]+)\\/([\\w.-]+)#(\\d+)\\b`,
116+
'gi',
117+
);
118+
119+
const numbers = new Set();
120+
121+
for (const match of body.matchAll(sameRepoPattern)) {
122+
numbers.add(Number(match[1]));
123+
}
124+
125+
for (const match of body.matchAll(crossRepoPattern)) {
126+
const matchOwner = match[1];
127+
const matchRepo = match[2];
128+
const matchNumber = Number(match[3]);
129+
130+
if (matchOwner === owner && matchRepo === repo) {
131+
numbers.add(matchNumber);
132+
}
133+
}
134+
135+
return [...numbers].map(number => ({number}));
136+
}
137+
138+
async function getClosingIssues(pr) {
139+
const referenced = await getClosingIssuesFromGraphQL();
140+
141+
if (referenced.length > 0) {
142+
core.info(
143+
`Found linked issues from closingIssuesReferences: ${referenced
144+
.map(({number}) => `#${number}`)
145+
.join(', ')}`
146+
);
147+
148+
return referenced;
149+
}
150+
151+
const parsed = getClosingIssuesFromBody(pr.body);
152+
153+
core.info(
154+
`closingIssuesReferences is empty, parsed from PR body: ${
155+
parsed.map(({number}) => `#${number}`).join(', ') || 'none'
156+
}`
157+
);
158+
159+
return parsed;
160+
}
161+
162+
const pr = await getPullRequest();
163+
164+
core.info(
165+
JSON.stringify(
166+
{
167+
number: pr.number,
168+
state: pr.state,
169+
merged: pr.merged,
170+
author: pr.user?.login,
171+
fork: pr.head?.repo?.fork,
172+
headRepo: pr.head?.repo?.full_name,
173+
baseRepo: pr.base?.repo?.full_name,
174+
title: pr.title,
175+
},
176+
null,
177+
2,
178+
),
179+
);
180+
181+
if (!pr.head?.repo?.fork) {
182+
core.info('PR is not from a fork. Exiting without changes.');
183+
return;
184+
}
185+
186+
await ensureLabelExists();
187+
await addLabel(pr.number);
188+
189+
const issues = await getClosingIssues(pr);
190+
191+
for (const issue of issues) {
192+
if (issue?.number) {
193+
await addLabel(issue.number);
194+
}
195+
}
196+
197+
core.info(
198+
`Done. PR #${pr.number}, issues: ${
199+
issues.map(({number}) => `#${number}`).join(', ') || 'none'
200+
}`,
201+
);

0 commit comments

Comments
 (0)