mirror of
https://github.com/github/awesome-copilot.git
synced 2026-10-13 07:14:24 +00:00
Add external plugin review board canvas for maintainers (#4174)
* feat: add external plugin review board canvas extension Project-scoped Copilot canvas for maintainers to triage external plugin submissions: kanban buckets, AI review, guided re-review via sub-session, refresh, issue/comments view, and quick /approve or /reject. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix: harden review board per PR feedback - Deny framing and hide stack traces from API error responses - Track review queue ids, restore queue state on send failure, skip queued items - Require reject reasons; treat posted decisions as pending until the workflow closes the issue - Invalidate browser issue cache on refresh - Clarify paid-services guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix: keep review decisions UI-only and record external outcomes - Remove the agent-facing post_decision action to block prompt-injected approvals/rejections - Record opposite terminal labels as external GitHub decisions instead of reusing the pending command - State visibly that a reject reason is required Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot App
parent
e7c25a4e9c
commit
e62be9667a
@@ -0,0 +1,99 @@
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
function runGh(args, { input } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn("gh", args, { windowsHide: true, env: process.env });
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => (stdout += chunk));
|
||||
child.stderr.on("data", (chunk) => (stderr += chunk));
|
||||
child.on("error", (error) => {
|
||||
reject(
|
||||
new Error(
|
||||
error.code === "ENOENT"
|
||||
? "GitHub CLI (gh) was not found on PATH. Install it and run `gh auth login`."
|
||||
: error.message,
|
||||
),
|
||||
);
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve(stdout);
|
||||
} else {
|
||||
reject(new Error(stderr.trim() || `gh exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
child.stdin.end(input ?? undefined);
|
||||
});
|
||||
}
|
||||
|
||||
export async function listReadyIssues(repo) {
|
||||
const out = await runGh([
|
||||
"issue",
|
||||
"list",
|
||||
"-R",
|
||||
repo,
|
||||
"--label",
|
||||
"external-plugin",
|
||||
"--label",
|
||||
"ready-for-review",
|
||||
"--state",
|
||||
"open",
|
||||
"--limit",
|
||||
"500",
|
||||
"--json",
|
||||
"number,title,author,createdAt,updatedAt,labels,url",
|
||||
]);
|
||||
return JSON.parse(out).map((issue) => ({
|
||||
number: issue.number,
|
||||
title: issue.title,
|
||||
author: issue.author?.login ?? "unknown",
|
||||
createdAt: issue.createdAt,
|
||||
updatedAt: issue.updatedAt,
|
||||
url: issue.url,
|
||||
labels: (issue.labels ?? []).map((label) => label.name),
|
||||
}));
|
||||
}
|
||||
|
||||
const FULL_JSON = ["-H", "Accept: application/vnd.github.full+json"];
|
||||
|
||||
export async function getIssueDetail(repo, number) {
|
||||
const [issueRaw, commentsRaw] = await Promise.all([
|
||||
runGh(["api", `repos/${repo}/issues/${number}`, ...FULL_JSON]),
|
||||
runGh(["api", `repos/${repo}/issues/${number}/comments?per_page=100`, "--paginate", "--slurp", ...FULL_JSON]),
|
||||
]);
|
||||
const issue = JSON.parse(issueRaw);
|
||||
const comments = JSON.parse(commentsRaw).flat();
|
||||
return {
|
||||
number: issue.number,
|
||||
title: issue.title,
|
||||
state: issue.state,
|
||||
url: issue.html_url,
|
||||
author: issue.user?.login,
|
||||
createdAt: issue.created_at,
|
||||
labels: (issue.labels ?? []).map((label) => label.name),
|
||||
bodyHtml: issue.body_html ?? "",
|
||||
comments: comments.map((comment) => ({
|
||||
id: comment.id,
|
||||
author: comment.user?.login,
|
||||
authorType: comment.user?.type,
|
||||
association: comment.author_association,
|
||||
createdAt: comment.created_at,
|
||||
url: comment.html_url,
|
||||
bodyHtml: comment.body_html ?? "",
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getIssueStatus(repo, number) {
|
||||
const out = await runGh(["issue", "view", String(number), "-R", repo, "--json", "state,labels"]);
|
||||
const data = JSON.parse(out);
|
||||
return { state: data.state, labels: (data.labels ?? []).map((label) => label.name) };
|
||||
}
|
||||
|
||||
export async function postComment(repo, number, body) {
|
||||
const out = await runGh(["issue", "comment", String(number), "-R", repo, "--body-file", "-"], { input: body });
|
||||
return out.trim();
|
||||
}
|
||||
Reference in New Issue
Block a user