@@ -154,6 +154,124 @@ jobs:
154154 configuration-path : .github/labeler.yml
155155 sync-labels : true
156156
157+ pr-keyword-labels :
158+ name : Label PRs From Title And Body
159+ if : github.event_name == 'pull_request_target'
160+ needs : ensure-labels
161+ runs-on : ubuntu-latest
162+
163+ steps :
164+ - name : Apply labels from PR keywords
165+ uses : actions/github-script@v7
166+ with :
167+ script : |
168+ const pullRequest = context.payload.pull_request;
169+ const text = `${pullRequest.title || ""}\n${pullRequest.body || ""}`.toLowerCase();
170+ const managedLabels = [
171+ "bug",
172+ "feature",
173+ "documentation",
174+ "question",
175+ "security",
176+ "kind: bug",
177+ "kind: feature",
178+ "kind: documentation",
179+ "kind: question",
180+ "kind: security",
181+ ];
182+ const rules = [
183+ {
184+ label: "security",
185+ patterns: [
186+ /\bsecurity\b/,
187+ /\bvulnerability\b/,
188+ /\bxss\b/,
189+ /\bcsrf\b/,
190+ /\binjection\b/,
191+ /\bmalware\b/,
192+ /\bcve\b/,
193+ ],
194+ },
195+ {
196+ label: "bug",
197+ patterns: [
198+ /\bbug\b/,
199+ /\bbugfix\b/,
200+ /\bfix\b/,
201+ /\bfixes\b/,
202+ /\bfixed\b/,
203+ /\bregression\b/,
204+ /\berror\b/,
205+ /\bcrash\b/,
206+ /\bbroken\b/,
207+ /\bfailing\b/,
208+ /\bfailed\b/,
209+ ],
210+ },
211+ {
212+ label: "documentation",
213+ patterns: [
214+ /\bdocs?\b/,
215+ /\breadme\b/,
216+ /\bdocumentation\b/,
217+ /\bguide\b/,
218+ ],
219+ },
220+ {
221+ label: "feature",
222+ patterns: [
223+ /\bfeature\b/,
224+ /\benhancement\b/,
225+ /\brequest\b/,
226+ /\bidea\b/,
227+ /\bproposal\b/,
228+ /\badd\b/,
229+ /\bintroduce\b/,
230+ /\bimplement\b/,
231+ ],
232+ },
233+ {
234+ label: "question",
235+ patterns: [
236+ /\bquestion\b/,
237+ /\bhow do i\b/,
238+ /\bhow to\b/,
239+ /\bhelp\b/,
240+ ],
241+ },
242+ ];
243+
244+ const expected = rules
245+ .filter((rule) => rule.patterns.some((pattern) => pattern.test(text)))
246+ .map((rule) => rule.label);
247+ const existing = (pullRequest.labels || [])
248+ .map((label) => typeof label === "string" ? label : label.name)
249+ .filter(Boolean);
250+ const toRemove = existing.filter((label) => managedLabels.includes(label) && !expected.includes(label));
251+ const toAdd = expected.filter((label) => !existing.includes(label));
252+
253+ for (const label of toRemove) {
254+ await github.rest.issues.removeLabel({
255+ owner: context.repo.owner,
256+ repo: context.repo.repo,
257+ issue_number: pullRequest.number,
258+ name: label,
259+ });
260+ core.info(`PR #${pullRequest.number}: removed ${label}`);
261+ }
262+
263+ if (toAdd.length > 0) {
264+ await github.rest.issues.addLabels({
265+ owner: context.repo.owner,
266+ repo: context.repo.repo,
267+ issue_number: pullRequest.number,
268+ labels: toAdd,
269+ });
270+ core.notice(`PR #${pullRequest.number}: added ${toAdd.join(", ")}`);
271+ } else if (toRemove.length === 0) {
272+ core.info(`PR #${pullRequest.number}: no keyword label changes needed`);
273+ }
274+
157275 issue-keyword-labels :
158276 name : Label Issues
159277 if : github.event_name == 'issues'
0 commit comments