issue-creation-hygiene.ymlβ’8.94 kB
name: Issue Creation Hygiene
on:
issues:
types:
- opened
- edited
- reopened
permissions:
contents: read
issues: write
jobs:
enforce-labels:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Apply required labels from form selections
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
const owner = context.repo.owner;
const repo = context.repo.repo;
const body = (issue.body || "");
// Parse form selections and existing content
// GitHub forms output "P0 (Critical - System down, data loss)" format
const priorityMatch = body.match(/\b(P[0-5])\s*\([^)]*\)/i);
const priority = priorityMatch ? priorityMatch[1] : "";
// For type, check both direct labels and form dropdowns
let type = "";
const directTypeMatch = body.match(/\btype:(bug|feature|enhancement|documentation|test|refactor|chore|ci|dependencies)\b/i);
if (directTypeMatch) {
type = directTypeMatch[0].toLowerCase();
} else {
// Check issue template labels field
const labelsMatch = body.match(/labels:\s*\[([^\]]*)\]/);
if (labelsMatch) {
const labels = labelsMatch[1].split(',').map(l => l.trim().replace(/['"]/g, ''));
const typeLabel = labels.find(l => l.match(/^type:/i));
if (typeLabel) type = typeLabel.toLowerCase();
}
}
// For status, check form output and direct labels
const statusMatch = body.match(/\bstatus:(untriaged|ready|in-progress|blocked|needs-info|review)\b/i);
const status = statusMatch ? statusMatch[0].toLowerCase() : "";
// For areas, handle both form output and direct labels
let areas = [];
// First try direct area: labels
const directAreas = body.match(/\barea:[a-z0-9:-]+\b/gi);
if (directAreas) {
areas.push(...directAreas.map(s => s.toLowerCase()));
}
// Also check issue template labels field for area labels
const labelsMatch = body.match(/labels:\s*\[([^\]]*)\]/);
if (labelsMatch) {
const labels = labelsMatch[1].split(',').map(l => l.trim().replace(/['"]/g, ''));
const areaLabels = labels.filter(l => l.match(/^area:/i));
areas.push(...areaLabels.map(l => l.toLowerCase()));
}
// Deduplicate areas
areas = Array.from(new Set(areas));
console.log(`Parsed from issue body:
Priority: ${priority}
Type: ${type}
Status: ${status}
Areas: ${areas.join(', ')}`);
// Get existing labels (GitHub template labels are already applied)
const current = (issue.labels || []).map(l => l.name.toLowerCase());
const add = new Set(current);
// If we didn't find type in body, check if it's already in labels (from template)
if (!type) {
const existingType = current.find(l => /^type:/.test(l));
if (existingType) type = existingType;
}
// Ensure exactly one P* (Priority)
const existingP = current.find(l => /^p[0-5]$/.test(l));
if (existingP && priority) {
// Replace existing priority with parsed one
add.delete(existingP);
add.add(priority.toLowerCase());
} else if (priority) {
// Add new priority
add.add(priority.toLowerCase());
} else if (existingP) {
// Keep existing priority if no new one found in body
add.add(existingP);
}
// Ensure exactly one type:*
const existingType = current.find(l => /^type:/.test(l));
if (existingType && type) {
// Replace existing type with parsed one
add.delete(existingType);
add.add(type);
} else if (type) {
// Add new type
add.add(type);
} else if (existingType) {
// Keep existing type if no new one found in body
add.add(existingType);
}
// Ensure exactly one status:*
const existingStatus = current.find(l => /^status:/.test(l));
if (existingStatus && status) {
// Replace existing status with parsed one
add.delete(existingStatus);
add.add(status);
} else if (status) {
// Add new status
add.add(status);
} else if (existingStatus) {
// Keep existing status if no new one found in body
add.add(existingStatus);
} else {
// Default status if none found
add.add("status:untriaged");
}
// Ensure at least one area:*
const hasArea = Array.from(add).some(l => /^area:/.test(l));
if (!hasArea && areas.length) {
areas.forEach(a => add.add(a));
}
// Validate required categories
const labelsArr = Array.from(add);
const okPriority = labelsArr.some(l => /^p[0-5]$/.test(l));
const okType = labelsArr.some(l => /^type:/.test(l));
const okStatus = labelsArr.some(l => /^status:/.test(l));
const okArea = labelsArr.some(l => /^area:/.test(l));
console.log(`Label validation:
Priority: ${okPriority}
Type: ${okType}
Status: ${okStatus}
Area: ${okArea}
Final labels: ${labelsArr.join(', ')}`);
// Apply labels if we have any
if (labelsArr.length && labelsArr.join(',') !== current.join(',')) {
await github.rest.issues.setLabels({
owner, repo, issue_number: issue.number, labels: labelsArr
});
console.log(`Applied labels: ${labelsArr.join(', ')}`);
}
// Check for missing categories and provide helpful feedback
const missing = [];
if (!okPriority) missing.push("Priority (P0βP5)");
if (!okType) missing.push("Type (type:bug|feature|enhancement|documentation|test|refactor|chore|ci|dependencies)");
if (!okStatus) missing.push("Status (status:untriaged|ready|in-progress|blocked|needs-info|review)");
if (!okArea) missing.push("Area (area:core|api|build|documentation|testing|etc.)");
if (missing.length) {
const msg = [
'## β οΈ Issue Hygiene Check',
'',
`Missing required label categories: **${missing.join(", ")}**`,
'',
'Please add the missing labels manually or edit this issue to include them in the description.',
'',
'**Required label format:**',
'- **Priority**: P0 (Critical), P1 (High), P2 (Medium), P3 (Low), P4 (Minor), P5 (Trivial)',
'- **Type**: type:bug, type:feature, type:enhancement, type:documentation, type:test, type:refactor, type:chore, type:ci, type:dependencies',
'- **Status**: status:untriaged, status:ready, status:in-progress, status:blocked, status:needs-info, status:review',
'- **Area**: area:core, area:api, area:build, area:documentation, area:testing, area:performance, area:security, etc.',
'',
`See [Label Guide](https://github.com/${owner}/${repo}/blob/main/docs/tools/github-cli/issues.md) for complete list.`
].join('\n');
// Check if we already commented on this issue
const comments = await github.rest.issues.listComments({
owner, repo, issue_number: issue.number
});
const existingHygieneComment = comments.data.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('Issue Hygiene Check')
);
if (existingHygieneComment) {
await github.rest.issues.updateComment({
owner, repo, comment_id: existingHygieneComment.id, body: msg
});
} else {
await github.rest.issues.createComment({
owner, repo, issue_number: issue.number, body: msg
});
}
core.setFailed(`Issue hygiene: missing required label categories: ${missing.join(', ')}`);
} else {
console.log("β
All required label categories present");
}