name: Auto Label Pull Requests on: workflow_dispatch: pull_request_target: branches: ["main"] types: [opened, synchronize, reopened, edited] jobs: autolabeler: if: github.repository == 'community-scripts/ProxmoxVE' runs-on: ubuntu-latest permissions: pull-requests: write env: CONFIG_PATH: .github/autolabeler-config.json steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install dependencies run: npm install minimatch - name: Label PR based on file changes and PR template uses: actions/github-script@v7 with: script: | const fs = require('fs').promises; const path = require('path'); const { minimatch } = require('minimatch'); const configPath = path.resolve(process.env.CONFIG_PATH); const fileContent = await fs.readFile(configPath, 'utf-8'); const autolabelerConfig = JSON.parse(fileContent); const prNumber = context.payload.pull_request.number; const prBody = context.payload.pull_request.body || ""; let labelsToAdd = new Set(); const prListFilesResponse = await github.rest.pulls.listFiles({ owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber, }); const prFiles = prListFilesResponse.data; for (const [label, rules] of Object.entries(autolabelerConfig)) { const shouldAddLabel = prFiles.some((prFile) => { return rules.some((rule) => { const isFileStatusMatch = rule.fileStatus ? rule.fileStatus === prFile.status : true; const isIncludeGlobMatch = rule.includeGlobs.some((glob) => minimatch(prFile.filename, glob)); const isExcludeGlobMatch = rule.excludeGlobs.some((glob) => minimatch(prFile.filename, glob)); return isFileStatusMatch && isIncludeGlobMatch && !isExcludeGlobMatch; }); }); if (shouldAddLabel) { labelsToAdd.add(label); if (label === "update script") { for (const prFile of prFiles) { const filename = prFile.filename; if (filename.startsWith("vm/")) labelsToAdd.add("vm"); if (filename.startsWith("tools/addon/")) labelsToAdd.add("addon"); if (filename.startsWith("tools/pve/")) labelsToAdd.add("pve-tool"); } } } } if (labelsToAdd.size < 2) { const templateLabelMappings = { "🐞 **Bug fix**": "bugfix", "✨ **New feature**": "feature", "💥 **Breaking change**": "breaking change", "🆕 **New script**": "new script", "🌍 **Website update**": "website", // handled special "🔧 **Refactoring / Code Cleanup**": "refactor", "📝 **Documentation update**": "documentation" // mapped to maintenance }; for (const [checkbox, label] of Object.entries(templateLabelMappings)) { const escapedCheckbox = checkbox.replace(/([.*+?^=!:${}()|[\]\/\\])/g, "\\$1"); const regex = new RegExp(`- \\[(x|X)\\]\\s*${escapedCheckbox}`, "i"); if (regex.test(prBody)) { if (label === "website") { const hasJson = prFiles.some((f) => f.filename.startsWith("frontend/public/json/")); const hasUpdateScript = labelsToAdd.has("update script"); const hasContentLabel = ["bugfix", "feature", "refactor"].some((l) => labelsToAdd.has(l)); if (!(hasUpdateScript && hasContentLabel)) { labelsToAdd.add(hasJson ? "json" : "website"); } } else if (label === "documentation") { labelsToAdd.add("maintenance"); } else { labelsToAdd.add(label); } } } } if (labelsToAdd.size === 0) { labelsToAdd.add("needs triage"); } if (labelsToAdd.size > 0) { await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, labels: Array.from(labelsToAdd), }); } ai-check: needs: autolabeler runs-on: ubuntu-latest permissions: pull-requests: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Load priority config run: | echo "PRIORITY_JSON=$(jq -c . .github/label-priority.json)" >> $GITHUB_ENV - name: Fetch PR metadata id: pr uses: actions/github-script@v7 with: script: | const pr = context.payload.pull_request; const files = await github.rest.pulls.listFiles({ owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number }); const prData = { title: pr.title || "", body: pr.body || "", files: files.data.map(f => f.filename) }; require('fs').writeFileSync(process.env.GITHUB_ENV, `PR_DATA=${JSON.stringify(prData)}\n`, {flag: 'a'}); - name: AI Label Review id: ai uses: actions/github-script@v7 env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} with: script: | const prData = JSON.parse(process.env.PR_DATA); const prompt = ` You are a GitHub labeling bot. Task: - Analyze PR title, body, and file list. - For each possible label, return a confidence score (0–1). - If both bugfix and refactor apply, prefer refactor. - Output JSON: {"labels":[{"name":"bugfix","score":0.9},{"name":"refactor","score":0.6}]} Valid labels: [new script, update script, delete script, bugfix, feature, maintenance, refactor, website, json, api, core, github, addon, pve-tool, vm]. PR data: ${JSON.stringify(prData)} `; const response = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer " + process.env.OPENAI_API_KEY, }, body: JSON.stringify({ model: "gpt-4.1-mini", messages: [{ role: "user", content: prompt }], temperature: 0 }) }); const data = await response.json(); const labels = JSON.parse(data.choices[0].message.content).labels; core.setOutput("labels", JSON.stringify(labels)); - name: Apply AI Labels uses: actions/github-script@v7 with: script: | const raw = JSON.parse('${{ steps.ai.outputs.labels }}'); const prNumber = context.payload.pull_request.number; const config = JSON.parse(process.env.PRIORITY_JSON); let toApply = []; let toSuggest = []; raw.forEach(l => { if (l.score >= 0.8) { const conflicts = config.conflicts[l.name] || []; const hasStrongerConflict = conflicts.some(c => raw.some(x => x.name === c && x.score >= 0.6 && (config.priorities[c] || 0) >= (config.priorities[l.name] || 0) ) ); if (!hasStrongerConflict) { toApply.push(l.name); } } else if (l.score >= 0.5) { toSuggest.push(`${l.name} (${Math.round(l.score*100)}%)`); } }); if (toApply.length > 0) { await github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, labels: toApply }); } if (toSuggest.length > 0) { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body: `🤖 AI suggests these possible labels (uncertain): ${toSuggest.join(", ")}` }); }