mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-14 17:39:12 +02:00
feat(ci): use PROJECT_PAT for project board, populate board in dry-run
- Add PROJECT_PAT secret for project board GraphQL calls, falling back to GITHUB_TOKEN if not set - Dry-run now populates the project board with decisions (confidence, reason, evidence fields) while still skipping labels/comments/closing - Extract addToProjectWithFields helper to reduce duplication
This commit is contained in:
@@ -3,16 +3,23 @@ import fs from "node:fs/promises";
|
|||||||
const decisions = JSON.parse(await fs.readFile("decisions.json", "utf8"));
|
const decisions = JSON.parse(await fs.readFile("decisions.json", "utf8"));
|
||||||
const dryRun = String(process.env.DRY_RUN).toLowerCase() === "true";
|
const dryRun = String(process.env.DRY_RUN).toLowerCase() === "true";
|
||||||
|
|
||||||
const headers = {
|
const ghHeaders = {
|
||||||
Authorization: `Bearer ${process.env.GH_TOKEN}`,
|
Authorization: `Bearer ${process.env.GH_TOKEN}`,
|
||||||
Accept: "application/vnd.github+json",
|
Accept: "application/vnd.github+json",
|
||||||
"X-GitHub-Api-Version": "2022-11-28",
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Use PROJECT_PAT for project board operations, fall back to GH_TOKEN
|
||||||
|
const projectHeaders = {
|
||||||
|
Authorization: `Bearer ${process.env.PROJECT_PAT || process.env.GH_TOKEN}`,
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
};
|
||||||
|
|
||||||
async function rest(url, method = "GET", body) {
|
async function rest(url, method = "GET", body) {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers: ghHeaders,
|
||||||
body: body ? JSON.stringify(body) : undefined
|
body: body ? JSON.stringify(body) : undefined
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`${res.status} ${url}: ${await res.text()}`);
|
if (!res.ok) throw new Error(`${res.status} ${url}: ${await res.text()}`);
|
||||||
@@ -22,7 +29,7 @@ async function rest(url, method = "GET", body) {
|
|||||||
async function graphql(query, variables) {
|
async function graphql(query, variables) {
|
||||||
const res = await fetch("https://api.github.com/graphql", {
|
const res = await fetch("https://api.github.com/graphql", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers: projectHeaders,
|
||||||
body: JSON.stringify({ query, variables })
|
body: JSON.stringify({ query, variables })
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
|
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
|
||||||
@@ -103,11 +110,47 @@ async function setTextField(itemId, fieldId, value) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addToProjectWithFields(owner, repo, d) {
|
||||||
|
const issueNodeId = await getIssueNodeId(owner, repo, d.issue_number);
|
||||||
|
const itemId = await addToProject(issueNodeId);
|
||||||
|
|
||||||
|
if (itemId) {
|
||||||
|
if (process.env.PROJECT_CONFIDENCE_FIELD_ID) {
|
||||||
|
await setTextField(itemId, process.env.PROJECT_CONFIDENCE_FIELD_ID, String(d.model.confidence));
|
||||||
|
}
|
||||||
|
if (process.env.PROJECT_REASON_FIELD_ID) {
|
||||||
|
await setTextField(itemId, process.env.PROJECT_REASON_FIELD_ID, d.model.reason_code);
|
||||||
|
}
|
||||||
|
if (process.env.PROJECT_EVIDENCE_FIELD_ID) {
|
||||||
|
await setTextField(itemId, process.env.PROJECT_EVIDENCE_FIELD_ID, d.issue_url);
|
||||||
|
}
|
||||||
|
if (process.env.PROJECT_LINKED_PR_FIELD_ID) {
|
||||||
|
const linked = (d.model.hard_signals || []).map(x => x.url).join(", ");
|
||||||
|
if (linked) {
|
||||||
|
await setTextField(itemId, process.env.PROJECT_LINKED_PR_FIELD_ID, linked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (process.env.PROJECT_REPO_FIELD_ID) {
|
||||||
|
await setTextField(itemId, process.env.PROJECT_REPO_FIELD_ID, d.repository);
|
||||||
|
}
|
||||||
|
console.log(` → Added to project board`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const d of decisions) {
|
for (const d of decisions) {
|
||||||
const [owner, repo] = d.repository.split("/");
|
const [owner, repo] = d.repository.split("/");
|
||||||
|
|
||||||
|
if (d.final_decision === "KEEP_OPEN") {
|
||||||
|
console.log(`#${d.issue_number} → KEEP_OPEN (confidence: ${d.model.confidence}, reason: ${d.model.reason_code})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (dryRun) {
|
if (dryRun) {
|
||||||
console.log(`[DRY RUN] #${d.issue_number} → ${d.final_decision} (confidence: ${d.model.confidence}, reason: ${d.model.reason_code})`);
|
console.log(`[DRY RUN] #${d.issue_number} → ${d.final_decision} (confidence: ${d.model.confidence}, reason: ${d.model.reason_code})`);
|
||||||
|
// In dry-run: populate project board but don't touch issues
|
||||||
|
if (d.final_decision === "MANUAL_REVIEW" || d.final_decision === "AUTO_CLOSE") {
|
||||||
|
await addToProjectWithFields(owner, repo, d);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,35 +158,12 @@ for (const d of decisions) {
|
|||||||
await addLabel(owner, repo, d.issue_number, ["auto-closed-resolved"]);
|
await addLabel(owner, repo, d.issue_number, ["auto-closed-resolved"]);
|
||||||
await addComment(owner, repo, d.issue_number, d.model.close_comment);
|
await addComment(owner, repo, d.issue_number, d.model.close_comment);
|
||||||
await closeIssue(owner, repo, d.issue_number);
|
await closeIssue(owner, repo, d.issue_number);
|
||||||
|
await addToProjectWithFields(owner, repo, d);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (d.final_decision === "MANUAL_REVIEW") {
|
if (d.final_decision === "MANUAL_REVIEW") {
|
||||||
await addLabel(owner, repo, d.issue_number, ["resolution-candidate"]);
|
await addLabel(owner, repo, d.issue_number, ["resolution-candidate"]);
|
||||||
|
await addToProjectWithFields(owner, repo, d);
|
||||||
const issueNodeId = await getIssueNodeId(owner, repo, d.issue_number);
|
|
||||||
const itemId = await addToProject(issueNodeId);
|
|
||||||
|
|
||||||
if (itemId) {
|
|
||||||
if (process.env.PROJECT_CONFIDENCE_FIELD_ID) {
|
|
||||||
await setTextField(itemId, process.env.PROJECT_CONFIDENCE_FIELD_ID, String(d.model.confidence));
|
|
||||||
}
|
|
||||||
if (process.env.PROJECT_REASON_FIELD_ID) {
|
|
||||||
await setTextField(itemId, process.env.PROJECT_REASON_FIELD_ID, d.model.reason_code);
|
|
||||||
}
|
|
||||||
if (process.env.PROJECT_EVIDENCE_FIELD_ID) {
|
|
||||||
await setTextField(itemId, process.env.PROJECT_EVIDENCE_FIELD_ID, d.issue_url);
|
|
||||||
}
|
|
||||||
if (process.env.PROJECT_LINKED_PR_FIELD_ID) {
|
|
||||||
const linked = (d.model.hard_signals || []).map(x => x.url).join(", ");
|
|
||||||
if (linked) {
|
|
||||||
await setTextField(itemId, process.env.PROJECT_LINKED_PR_FIELD_ID, linked);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (process.env.PROJECT_REPO_FIELD_ID) {
|
|
||||||
await setTextField(itemId, process.env.PROJECT_REPO_FIELD_ID, d.repository);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await addComment(
|
await addComment(
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PROJECT_PAT: ${{ secrets.PROJECT_PAT }}
|
||||||
DRY_RUN: "true"
|
DRY_RUN: "true"
|
||||||
MAX_ISSUES: "100"
|
MAX_ISSUES: "100"
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
|
|||||||
Reference in New Issue
Block a user