mirror of
https://github.com/github/awesome-copilot.git
synced 2026-10-15 16:21:51 +00:00
Exclude manually overridden submissions from first-party fast-track until a fresh intake pass clears the override label. Disable individual approval controls while intake is pending. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 821f4796-505e-4fb8-96cf-0b5bbf80a636
358 lines
13 KiB
JavaScript
358 lines
13 KiB
JavaScript
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
import { randomBytes } from "node:crypto";
|
|
import path from "node:path";
|
|
|
|
export const RECOMMENDATIONS = ["straight-reject", "probably-reject", "needs-review", "accept"];
|
|
|
|
export const COLUMNS = [
|
|
{ id: "unreviewed", title: "Unreviewed", droppable: true },
|
|
{ id: "reviewing", title: "Reviewing", droppable: false },
|
|
{ id: "straight-reject", title: "Straight reject", droppable: true },
|
|
{ id: "probably-reject", title: "Probably reject", droppable: true },
|
|
{ id: "needs-review", title: "Needs review", droppable: true },
|
|
{ id: "accept", title: "Accept", droppable: true },
|
|
{ id: "actioned", title: "Actioned", droppable: false },
|
|
];
|
|
|
|
const HISTORY_LIMIT = 200;
|
|
export const FIRST_PARTY_LABEL = "first party";
|
|
const MANUAL_INTAKE_OVERRIDE_LABEL = "intake-manually-overridden";
|
|
const PENDING_INTAKE_LABELS = ["awaiting-review", "requires-submitter-fixes"];
|
|
|
|
export function isReadyForApproval(labels) {
|
|
return labels.includes("ready-for-review") && !labels.some((label) => PENDING_INTAKE_LABELS.includes(label));
|
|
}
|
|
|
|
// Validated submissions from first-party organizations skip AI triage and land in Accept. Only
|
|
// cards without an AI review, queued review, or manual bucket are fast-tracked so maintainer
|
|
// choices are kept; dragging a card back to Unreviewed opts it out so it gets a normal AI review.
|
|
function applyFirstPartyFastTrack(item) {
|
|
if (
|
|
!item.labels?.includes(FIRST_PARTY_LABEL) ||
|
|
!item.labels?.includes("ready-for-review") ||
|
|
item.labels?.some((label) => PENDING_INTAKE_LABELS.includes(label)) ||
|
|
item.labels?.includes(MANUAL_INTAKE_OVERRIDE_LABEL) ||
|
|
item.fastTrackDismissed ||
|
|
item.decision ||
|
|
item.review ||
|
|
item.manualColumn ||
|
|
item.reviewStatus === "queued"
|
|
) {
|
|
return false;
|
|
}
|
|
item.review = {
|
|
recommendation: "accept",
|
|
fastTracked: true,
|
|
rationale:
|
|
"First-party submission (repository owned by a Microsoft/GitHub organization) that passed automated intake validation; fast-tracked to approval.",
|
|
suggestedComment: "/approve",
|
|
reviewedAt: new Date().toISOString(),
|
|
};
|
|
item.reviewStatus = "reviewed";
|
|
return true;
|
|
}
|
|
|
|
function clearFirstPartyFastTrack(item) {
|
|
if (
|
|
(item.labels?.includes(FIRST_PARTY_LABEL) &&
|
|
item.labels?.includes("ready-for-review") &&
|
|
!item.labels?.includes(MANUAL_INTAKE_OVERRIDE_LABEL) &&
|
|
!item.labels?.some((label) => PENDING_INTAKE_LABELS.includes(label))) ||
|
|
!item.review?.fastTracked ||
|
|
item.reviewStatus === "queued"
|
|
) {
|
|
return;
|
|
}
|
|
item.review = null;
|
|
if (!item.decision && !item.manualColumn) item.reviewStatus = "unreviewed";
|
|
}
|
|
|
|
function newQueueId() {
|
|
return randomBytes(6).toString("base64url");
|
|
}
|
|
|
|
function emptyState(repo) {
|
|
return { version: 1, repo, lastRefreshedAt: null, items: {}, history: [] };
|
|
}
|
|
|
|
export function effectiveColumn(item) {
|
|
if (item.decision) return "actioned";
|
|
if (item.manualColumn) return item.manualColumn;
|
|
if (item.reviewStatus === "queued") return "reviewing";
|
|
if (item.review?.recommendation) return item.review.recommendation;
|
|
return "unreviewed";
|
|
}
|
|
|
|
export class BoardStore {
|
|
constructor(filePath, repo) {
|
|
this.filePath = filePath;
|
|
this.repo = repo;
|
|
this.state = emptyState(repo);
|
|
this.listeners = new Set();
|
|
this.writeChain = Promise.resolve();
|
|
}
|
|
|
|
async load() {
|
|
try {
|
|
const raw = await readFile(this.filePath, "utf8");
|
|
const parsed = JSON.parse(raw);
|
|
this.state = { ...emptyState(this.repo), ...parsed, repo: parsed.repo ?? this.repo };
|
|
} catch (error) {
|
|
if (error.code !== "ENOENT") throw error;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
onChange(listener) {
|
|
this.listeners.add(listener);
|
|
return () => this.listeners.delete(listener);
|
|
}
|
|
|
|
async commit() {
|
|
const data = JSON.stringify(this.state, null, 2) + "\n";
|
|
this.writeChain = this.writeChain
|
|
.catch(() => {})
|
|
.then(async () => {
|
|
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
const tmp = `${this.filePath}.tmp`;
|
|
await writeFile(tmp, data, "utf8");
|
|
await rename(tmp, this.filePath);
|
|
});
|
|
await this.writeChain;
|
|
const snapshot = this.snapshot();
|
|
for (const listener of this.listeners) {
|
|
try {
|
|
listener(snapshot);
|
|
} catch {
|
|
// listeners are best-effort UI pushes
|
|
}
|
|
}
|
|
}
|
|
|
|
getItem(number) {
|
|
const item = this.state.items[String(number)];
|
|
if (!item) throw new Error(`Issue #${number} is not on the board. Refresh first.`);
|
|
return item;
|
|
}
|
|
|
|
snapshot() {
|
|
const items = Object.values(this.state.items)
|
|
.map((item) => ({ ...item, column: effectiveColumn(item) }))
|
|
.sort((a, b) => b.number - a.number);
|
|
return {
|
|
repo: this.state.repo,
|
|
lastRefreshedAt: this.state.lastRefreshedAt,
|
|
columns: COLUMNS,
|
|
items: items.map((item) => ({
|
|
...item,
|
|
reviewEligible: isReadyForApproval(item.labels ?? []),
|
|
})),
|
|
history: this.state.history.slice(0, 50),
|
|
};
|
|
}
|
|
|
|
async syncIssues(issues) {
|
|
const incoming = new Map(issues.map((issue) => [String(issue.number), issue]));
|
|
const added = [];
|
|
const removed = [];
|
|
const fastTracked = [];
|
|
for (const [key, issue] of incoming) {
|
|
const existing = this.state.items[key];
|
|
if (existing) {
|
|
Object.assign(existing, {
|
|
title: issue.title,
|
|
author: issue.author,
|
|
url: issue.url,
|
|
labels: issue.labels,
|
|
createdAt: issue.createdAt,
|
|
updatedAt: issue.updatedAt,
|
|
});
|
|
} else {
|
|
this.state.items[key] = {
|
|
...issue,
|
|
reviewStatus: "unreviewed",
|
|
review: null,
|
|
manualColumn: null,
|
|
decision: null,
|
|
addedAt: new Date().toISOString(),
|
|
};
|
|
added.push(issue.number);
|
|
}
|
|
const item = this.state.items[key];
|
|
if (applyFirstPartyFastTrack(item)) fastTracked.push(issue.number);
|
|
else clearFirstPartyFastTrack(item);
|
|
}
|
|
for (const key of Object.keys(this.state.items)) {
|
|
if (!incoming.has(key)) {
|
|
removed.push(Number(key));
|
|
delete this.state.items[key];
|
|
}
|
|
}
|
|
this.state.lastRefreshedAt = new Date().toISOString();
|
|
await this.commit();
|
|
return { added, removed, fastTracked, total: incoming.size };
|
|
}
|
|
|
|
pendingReview(numbers) {
|
|
const all = Object.values(this.state.items).filter(
|
|
(item) =>
|
|
isReadyForApproval(item.labels ?? []) &&
|
|
!item.decision &&
|
|
!item.pendingDecision &&
|
|
item.reviewStatus !== "queued",
|
|
);
|
|
if (numbers?.length) {
|
|
const wanted = new Set(numbers.map(Number));
|
|
return all.filter((item) => wanted.has(item.number));
|
|
}
|
|
return all.filter((item) => item.reviewStatus !== "reviewed");
|
|
}
|
|
|
|
async markQueued(numbers, { rereviewGuidance } = {}) {
|
|
const now = new Date().toISOString();
|
|
const snapshots = [];
|
|
for (const number of numbers) {
|
|
const item = this.getItem(number);
|
|
snapshots.push({
|
|
number,
|
|
previous: {
|
|
reviewStatus: item.reviewStatus,
|
|
queuedAt: item.queuedAt,
|
|
queueId: item.queueId,
|
|
rereviewGuidance: item.rereviewGuidance,
|
|
},
|
|
});
|
|
item.reviewStatus = "queued";
|
|
item.queuedAt = now;
|
|
item.queueId = newQueueId();
|
|
if (rereviewGuidance !== undefined) item.rereviewGuidance = rereviewGuidance;
|
|
else delete item.rereviewGuidance;
|
|
}
|
|
await this.commit();
|
|
return snapshots;
|
|
}
|
|
|
|
async restoreQueueState(snapshots) {
|
|
for (const snapshot of snapshots) {
|
|
const item = this.state.items[String(snapshot.number)];
|
|
if (!item) continue;
|
|
const { previous } = snapshot;
|
|
item.reviewStatus = previous.reviewStatus;
|
|
if (previous.queuedAt === undefined) delete item.queuedAt;
|
|
else item.queuedAt = previous.queuedAt;
|
|
if (previous.queueId === undefined) delete item.queueId;
|
|
else item.queueId = previous.queueId;
|
|
if (previous.rereviewGuidance === undefined) delete item.rereviewGuidance;
|
|
else item.rereviewGuidance = previous.rereviewGuidance;
|
|
}
|
|
await this.commit();
|
|
}
|
|
|
|
async recordReviews(reviews) {
|
|
const recorded = [];
|
|
const skipped = [];
|
|
for (const review of reviews) {
|
|
const item = this.state.items[String(review.number)];
|
|
if (!item) {
|
|
skipped.push({ number: review.number, reason: "not on board" });
|
|
continue;
|
|
}
|
|
if (!RECOMMENDATIONS.includes(review.recommendation)) {
|
|
skipped.push({ number: review.number, reason: `invalid recommendation ${review.recommendation}` });
|
|
continue;
|
|
}
|
|
if (item.queueId && review.queueId !== item.queueId) {
|
|
skipped.push({ number: review.number, reason: "stale review result" });
|
|
continue;
|
|
}
|
|
const { number, ...rest } = review;
|
|
item.review = { ...rest, reviewedAt: new Date().toISOString() };
|
|
item.reviewStatus = "reviewed";
|
|
item.manualColumn = null;
|
|
delete item.queuedAt;
|
|
delete item.queueId;
|
|
delete item.rereviewGuidance;
|
|
recorded.push(number);
|
|
}
|
|
await this.commit();
|
|
return { recorded, skipped };
|
|
}
|
|
|
|
async moveItem(number, column) {
|
|
const item = this.getItem(number);
|
|
if (item.decision) throw new Error(`#${number} has already been actioned.`);
|
|
if (column === "unreviewed") {
|
|
if (item.review?.fastTracked || item.labels?.includes(FIRST_PARTY_LABEL)) item.fastTrackDismissed = true;
|
|
item.review = null;
|
|
item.reviewStatus = "unreviewed";
|
|
item.manualColumn = null;
|
|
delete item.queuedAt;
|
|
delete item.queueId;
|
|
} else if (RECOMMENDATIONS.includes(column)) {
|
|
item.manualColumn = item.review?.recommendation === column ? null : column;
|
|
if (item.reviewStatus === "queued") item.reviewStatus = item.review ? "reviewed" : "unreviewed";
|
|
delete item.queueId;
|
|
delete item.rereviewGuidance;
|
|
} else {
|
|
throw new Error(`Items cannot be moved to "${column}".`);
|
|
}
|
|
await this.commit();
|
|
return { number, column: effectiveColumn(item) };
|
|
}
|
|
|
|
async recordPendingDecision(number, pendingDecision) {
|
|
const item = this.getItem(number);
|
|
item.pendingDecision = pendingDecision;
|
|
await this.commit();
|
|
}
|
|
|
|
async resolvePendingDecision(number, status) {
|
|
const item = this.state.items[String(number)];
|
|
if (!item?.pendingDecision || status.state !== "CLOSED") return null;
|
|
const labels = new Set(status.labels ?? []);
|
|
const kind = labels.has("approved") ? "approve" : labels.has("rejected") ? "reject" : null;
|
|
if (!kind) return null;
|
|
const now = new Date().toISOString();
|
|
const pendingKind = item.pendingDecision.kind;
|
|
const decision =
|
|
pendingKind && pendingKind !== kind
|
|
? {
|
|
kind,
|
|
external: true,
|
|
comment: "",
|
|
body: null,
|
|
commentUrl: null,
|
|
at: now,
|
|
resolvedAt: now,
|
|
supersededPending: pendingKind,
|
|
}
|
|
: { ...item.pendingDecision, kind, resolvedAt: now };
|
|
await this.recordDecision(number, decision);
|
|
return decision;
|
|
}
|
|
|
|
async pendingDecisionNumbers() {
|
|
return Object.values(this.state.items)
|
|
.filter((item) => item.pendingDecision && !item.decision)
|
|
.map((item) => item.number);
|
|
}
|
|
|
|
async recordDecision(number, decision) {
|
|
const item = this.getItem(number);
|
|
item.decision = decision;
|
|
delete item.pendingDecision;
|
|
this.state.history.unshift({
|
|
number,
|
|
title: item.title,
|
|
author: item.author,
|
|
kind: decision.kind,
|
|
comment: decision.comment ?? "",
|
|
aiRecommendation: item.review?.recommendation ?? null,
|
|
boardColumn: item.manualColumn ?? item.review?.recommendation ?? null,
|
|
at: decision.at,
|
|
});
|
|
this.state.history = this.state.history.slice(0, HISTORY_LIMIT);
|
|
await this.commit();
|
|
}
|
|
}
|