mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-30 14:16:38 +00:00
Move the dry-run check to the top of the loop so it applies to all decision types, not just AUTO_CLOSE. In dry-run mode the workflow now only logs what it would do without touching any issues.
155 lines
4.6 KiB
JavaScript
155 lines
4.6 KiB
JavaScript
import fs from "node:fs/promises";
|
|
|
|
const decisions = JSON.parse(await fs.readFile("decisions.json", "utf8"));
|
|
const dryRun = String(process.env.DRY_RUN).toLowerCase() === "true";
|
|
|
|
const headers = {
|
|
Authorization: `Bearer ${process.env.GH_TOKEN}`,
|
|
Accept: "application/vnd.github+json",
|
|
"X-GitHub-Api-Version": "2022-11-28",
|
|
};
|
|
|
|
async function rest(url, method = "GET", body) {
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers,
|
|
body: body ? JSON.stringify(body) : undefined
|
|
});
|
|
if (!res.ok) throw new Error(`${res.status} ${url}: ${await res.text()}`);
|
|
return res.status === 204 ? null : res.json();
|
|
}
|
|
|
|
async function graphql(query, variables) {
|
|
const res = await fetch("https://api.github.com/graphql", {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify({ query, variables })
|
|
});
|
|
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
|
|
const json = await res.json();
|
|
if (json.errors) throw new Error(JSON.stringify(json.errors));
|
|
return json.data;
|
|
}
|
|
|
|
async function addLabel(owner, repo, issueNumber, labels) {
|
|
return rest(
|
|
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/labels`,
|
|
"POST",
|
|
{ labels }
|
|
);
|
|
}
|
|
|
|
async function addComment(owner, repo, issueNumber, body) {
|
|
return rest(
|
|
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
|
|
"POST",
|
|
{ body }
|
|
);
|
|
}
|
|
|
|
async function closeIssue(owner, repo, issueNumber) {
|
|
return rest(
|
|
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`,
|
|
"PATCH",
|
|
{ state: "closed", state_reason: "completed" }
|
|
);
|
|
}
|
|
|
|
async function getIssueNodeId(owner, repo, issueNumber) {
|
|
const issue = await rest(`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`);
|
|
return issue.node_id;
|
|
}
|
|
|
|
async function addToProject(issueNodeId) {
|
|
const mutation = `
|
|
mutation($projectId: ID!, $contentId: ID!) {
|
|
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
|
|
item { id }
|
|
}
|
|
}
|
|
`;
|
|
|
|
try {
|
|
const data = await graphql(mutation, {
|
|
projectId: process.env.PROJECT_ID,
|
|
contentId: issueNodeId
|
|
});
|
|
return data.addProjectV2ItemById.item.id;
|
|
} catch (err) {
|
|
console.warn(`[WARN] Could not add to project (needs PAT with project scope): ${err.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function setTextField(itemId, fieldId, value) {
|
|
const mutation = `
|
|
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: String!) {
|
|
updateProjectV2ItemFieldValue(input: {
|
|
projectId: $projectId,
|
|
itemId: $itemId,
|
|
fieldId: $fieldId,
|
|
value: { text: $value }
|
|
}) {
|
|
projectV2Item { id }
|
|
}
|
|
}
|
|
`;
|
|
|
|
return graphql(mutation, {
|
|
projectId: process.env.PROJECT_ID,
|
|
itemId,
|
|
fieldId,
|
|
value
|
|
});
|
|
}
|
|
|
|
for (const d of decisions) {
|
|
const [owner, repo] = d.repository.split("/");
|
|
|
|
if (dryRun) {
|
|
console.log(`[DRY RUN] #${d.issue_number} → ${d.final_decision} (confidence: ${d.model.confidence}, reason: ${d.model.reason_code})`);
|
|
continue;
|
|
}
|
|
|
|
if (d.final_decision === "AUTO_CLOSE") {
|
|
await addLabel(owner, repo, d.issue_number, ["auto-closed-resolved"]);
|
|
await addComment(owner, repo, d.issue_number, d.model.close_comment);
|
|
await closeIssue(owner, repo, d.issue_number);
|
|
}
|
|
|
|
if (d.final_decision === "MANUAL_REVIEW") {
|
|
await addLabel(owner, repo, d.issue_number, ["resolution-candidate"]);
|
|
|
|
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(
|
|
owner,
|
|
repo,
|
|
d.issue_number,
|
|
d.model.manual_review_note ||
|
|
"This issue looks like a possible resolution candidate, but not with enough certainty for automatic closure. Added to the review queue."
|
|
);
|
|
}
|
|
} |