Skip to content

Auto-prefix & Label Issues #703

Auto-prefix & Label Issues

Auto-prefix & Label Issues #703

name: Auto-prefix & Label Issues
on:
issues:
types: [opened, edited]
schedule:
- cron: '0 0 * * *' # daily catch-up at midnight UTC
permissions:
issues: write
concurrency:
group: auto-prefix-issues-${{ github.event.issue.number || 'scheduled' }}
cancel-in-progress: true
jobs:
prefix_and_label:
runs-on: ubuntu-latest
steps:
- name: Ensure labels exist, then prefix titles & add labels
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
// ── 1. Ensure required labels exist ──────────────────────────
const required = [
{ name: 'bug', color: 'd73a4a', description: "Something isn't working" },
{ name: 'enhancement', color: 'a2eeef', description: 'New feature or request' }
];
const { data: existingLabels } = await github.rest.issues.listLabelsForRepo({
owner, repo, per_page: 100
});
const existingNames = new Set(existingLabels.map(l => l.name));
for (const lbl of required) {
if (!existingNames.has(lbl.name)) {
await github.rest.issues.createLabel({
owner, repo,
name: lbl.name,
color: lbl.color,
description: lbl.description
});
console.log(`Created label "${lbl.name}"`);
}
}
// ── 2. Keyword lists ─────────────────────────────────────────
// Bug keywords have higher priority when both match
const bugWords = [
'bug', 'error', 'crash', 'broken', 'fault', 'defect',
'regression', 'problem', 'fail', 'failed', 'failure',
'not working', "doesn't work", "won't work", 'exception',
'unexpected', 'incorrect'
];
const enhancementWords = [
'add', 'feature', 'improve', 'enhancement', 'request',
'support', 'implement', 'upgrade', 'extend', 'proposal',
'new', 'enhance', 'suggestion'
];
// Word-boundary match to avoid false positives
// e.g. "address" should not match "add"
function matchesKeyword(text, words) {
const lower = text.toLowerCase();
return words.some(w => {
if (w.includes(' ')) return lower.includes(w); // multi-word phrases: substring match
const re = new RegExp(`\\b${w}\\b`, 'i');
return re.test(text);
});
}
function classify(title, body) {
const text = `${title} ${body || ''}`;
const isBug = matchesKeyword(text, bugWords);
const isEnhancement = matchesKeyword(text, enhancementWords);
if (isBug && isEnhancement) {
// Tiebreak: check title first — if title clearly says bug, it's a bug
if (matchesKeyword(title, bugWords)) return 'bug';
if (matchesKeyword(title, enhancementWords)) return 'enhancement';
return 'bug'; // default tiebreak: bugs are more urgent
}
if (isBug) return 'bug';
if (isEnhancement) return 'enhancement';
return null;
}
// ── 3. Determine which issues to process ─────────────────────
let issuesToProcess = [];
if (context.eventName === 'issues') {
// Single issue event — process just that issue
issuesToProcess = [context.payload.issue];
} else {
// Scheduled run — process all open issues
const allIssues = await github.paginate(
github.rest.issues.listForRepo,
{ owner, repo, state: 'open', per_page: 100 }
);
// Filter out pull requests (GitHub returns PRs via the issues API)
issuesToProcess = allIssues.filter(i => !i.pull_request);
}
// ── 4. Process each issue ────────────────────────────────────
for (const issue of issuesToProcess) {
const origTitle = issue.title;
const body = issue.body || '';
const currentLabels = issue.labels.map(l => l.name);
// Skip if already prefixed
if (/^\[(bug|enhancement)\]/i.test(origTitle)) continue;
// Skip if already labeled with bug or enhancement
if (currentLabels.includes('bug') || currentLabels.includes('enhancement')) continue;
const kind = classify(origTitle, body);
if (!kind) continue;
const prefix = kind === 'bug' ? '[bug]' : '[enhancement]';
// Update title
await github.rest.issues.update({
owner, repo,
issue_number: issue.number,
title: `${prefix} ${origTitle}`
});
console.log(`#${issue.number}: prefixed with ${prefix}`);
// Add label
await github.rest.issues.addLabels({
owner, repo,
issue_number: issue.number,
labels: [kind]
});
console.log(`#${issue.number}: added label "${kind}"`);
}