mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2025-09-23 02:40:03 +00:00
231 lines
8.6 KiB
YAML
Generated
231 lines
8.6 KiB
YAML
Generated
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(", ")}`
|
||
});
|
||
}
|