name: Auto Label Issues on: issues: types: [opened, edited] permissions: issues: write jobs: label: runs-on: ubuntu-latest steps: - name: Auto-label based on content uses: actions/github-script@v7 with: script: | const issue = context.payload.issue; const title = issue.title.toLowerCase(); const body = (issue.body || '').toLowerCase(); const content = title + ' ' + body; const labelsToAdd = []; // Type labels based on title prefix if (title.includes('[bug]')) { labelsToAdd.push('bug'); } else if (title.includes('[feature]') || title.includes('feat(') || title.includes('feat:')) { labelsToAdd.push('enhancement'); } else if (title.includes('[docs]') || title.includes('docs(') || title.includes('docs:')) { labelsToAdd.push('documentation'); } // Project labels based on content if (content.includes('core dev') || content.includes('core work') || content.includes('core commit') || content.includes('core push')) { labelsToAdd.push('project:core-cli'); } if (content.includes('core php') || content.includes('composer') || content.includes('pest') || content.includes('phpstan')) { labelsToAdd.push('project:core-php'); } if (content.includes('setup') || content.includes('install') || content.includes('makefile')) { labelsToAdd.push('project:workstation'); } // Priority detection if (content.includes('critical') || content.includes('urgent') || content.includes('breaking')) { labelsToAdd.push('priority:high'); } // Agent labels if (content.includes('agent') || content.includes('ai ') || content.includes('claude') || content.includes('agentic')) { labelsToAdd.push('agentic'); } // Complexity - from template dropdown or heuristics if (body.includes('small - quick fix')) { labelsToAdd.push('complexity:small'); labelsToAdd.push('good first issue'); } else if (body.includes('medium - multiple files')) { labelsToAdd.push('complexity:medium'); } else if (body.includes('large - significant')) { labelsToAdd.push('complexity:large'); } else if (!body.includes('unknown - not sure')) { // Heuristic complexity detection const checklistCount = (body.match(/- \[ \]/g) || []).length; const codeBlocks = (body.match(/```/g) || []).length / 2; const sections = (body.match(/^##/gm) || []).length; const fileRefs = (body.match(/\.(go|php|js|ts|yml|yaml|json|md)\b/g) || []).length; const complexKeywords = ['refactor', 'rewrite', 'migration', 'breaking change', 'across repos', 'architecture']; const simpleKeywords = ['simple', 'quick fix', 'typo', 'minor', 'trivial']; const hasComplexKeyword = complexKeywords.some(k => content.includes(k)); const hasSimpleKeyword = simpleKeywords.some(k => content.includes(k)); let score = checklistCount * 2 + codeBlocks + sections + fileRefs; score += hasComplexKeyword ? 5 : 0; score -= hasSimpleKeyword ? 3 : 0; if (hasSimpleKeyword || score <= 2) { labelsToAdd.push('complexity:small'); labelsToAdd.push('good first issue'); } else if (score <= 6) { labelsToAdd.push('complexity:medium'); } else { labelsToAdd.push('complexity:large'); } } // Apply labels if any detected if (labelsToAdd.length > 0) { // Filter to only existing labels const existingLabels = await github.rest.issues.listLabelsForRepo({ owner: context.repo.owner, repo: context.repo.repo, per_page: 100 }); const validLabels = existingLabels.data.map(l => l.name); const filteredLabels = labelsToAdd.filter(l => validLabels.includes(l)); if (filteredLabels.length > 0) { await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, labels: filteredLabels }); console.log(`Added labels: ${filteredLabels.join(', ')}`); } }