Skip to content

Commit ae70406

Browse files
authored
fix(review): skip redundant GitHub PATCHes for unchanged upstream drift issues (#4503) (#4566)
fileUpstreamDriftIssues resolves each open drift report's GitHub issue and always PATCHes it, unconditionally, every 6-hour cycle -- githubDriftIssuePayload has no always-changing field (no timestamp), so an unresolved report with unchanged content produces a byte-identical payload across cycles. This wastes a GitHub call and spuriously bumps the issue's "updated" timestamp, notifying assignees/watchers with no real change. Extends validateRecordedGitHubIssue and findGitHubIssueForFingerprint (which already fetch the full issue) to also surface body/labels/assignees, then compares the freshly-built payload against that live state before PATCHing -- skipping the write (a new `unchanged` counter, distinct from the existing API-failure `skipped` count) when nothing would actually change. Labels and assignees compare as case-insensitive sets, not ordered arrays, since GitHub does not guarantee either matches what was last sent.
1 parent e063f55 commit ae70406

2 files changed

Lines changed: 249 additions & 10 deletions

File tree

src/upstream/ruleset.ts

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,22 @@ export async function fileUpstreamDriftIssues(env: Env): Promise<Record<string,
291291
let created = 0;
292292
let updated = 0;
293293
let skipped = 0;
294+
let unchanged = 0;
294295
for (const report of reports) {
295296
const existing = (await validateRecordedGitHubIssue(repo, token, report)) ?? (await findGitHubIssueForFingerprint(repo, token, report.fingerprint));
296297
if (existing) {
298+
// Keep the recorded issue reference correct even when the content below turns out unchanged -- this is a
299+
// local D1 write (no GitHub API cost), and it is what lets validateRecordedGitHubIssue's fast path replace
300+
// the slower findGitHubIssueForFingerprint list-search on every later cycle once a report is discovered.
301+
await updateUpstreamDriftReportIssue(env, report.fingerprint, existing);
302+
// #4503: skip the PATCH entirely when it would be a no-op -- githubDriftIssuePayload has no always-changing
303+
// field (no timestamp), so an unresolved report with unchanged content produces a byte-identical payload
304+
// across cycles; PATCHing it anyway wastes a GitHub call and spuriously bumps the issue's "updated" time,
305+
// notifying assignees/watchers with no real change.
306+
if (driftIssueUnchanged(existing, report, assignees)) {
307+
unchanged += 1;
308+
continue;
309+
}
297310
const issue = await updateGitHubDriftIssue(repo, token, existing.number, report, assignees);
298311
if (!issue) {
299312
skipped += 1;
@@ -314,9 +327,9 @@ export async function fileUpstreamDriftIssues(env: Env): Promise<Record<string,
314327
await recordAuditEvent(env, {
315328
eventType: "upstream.drift_issues_filed",
316329
outcome: "completed",
317-
metadata: { created, updated, skipped, repo },
330+
metadata: { created, updated, skipped, unchanged, repo },
318331
});
319-
return { status: "completed", created, updated, skipped };
332+
return { status: "completed", created, updated, skipped, unchanged };
320333
}
321334

322335
export async function buildUpstreamDriftReport(current: UpstreamRulesetSnapshotRecord, previous: UpstreamRulesetSnapshotRecord | null): Promise<UpstreamDriftReportRecord | null> {
@@ -1013,17 +1026,40 @@ function publicDriftReport(report: UpstreamDriftReportRecord): Record<string, Js
10131026
};
10141027
}
10151028

1016-
async function findGitHubIssueForFingerprint(repo: string, token: string, fingerprint: string): Promise<{ number: number; url: string } | null> {
1029+
/** The subset of a resolved existing GitHub drift issue needed to detect whether a fresh PATCH would be a no-op
1030+
* (#4503) -- body/labels/assignees are exactly the three fields updateGitHubDriftIssue's payload writes. */
1031+
type ExistingDriftIssue = { number: number; url: string; body: string | null; labels: string[]; assignees: string[] };
1032+
1033+
function githubIssueLabelNames(labels: Array<string | { name?: string }> | undefined): string[] {
1034+
return (labels ?? []).map((label) => (typeof label === "string" ? label : (label.name ?? ""))).filter((name) => name.length > 0);
1035+
}
1036+
1037+
async function findGitHubIssueForFingerprint(repo: string, token: string, fingerprint: string): Promise<ExistingDriftIssue | null> {
10171038
const [owner, name] = repo.split("/");
10181039
if (!owner || !name) return null;
10191040
try {
10201041
for (let page = 1; ; page += 1) {
10211042
const url = `https://api.github.com/repos/${owner}/${name}/issues?state=open&labels=signals&per_page=100&page=${page}`;
10221043
const response = await timeoutFetch(url, { headers: githubHeaders(token, "application/vnd.github+json") });
10231044
if (!response.ok) return null;
1024-
const issues = (await response.json()) as Array<{ number?: number; html_url?: string; body?: string | null }>;
1045+
const issues = (await response.json()) as Array<{
1046+
number?: number;
1047+
html_url?: string;
1048+
body?: string | null;
1049+
labels?: Array<string | { name?: string }>;
1050+
assignees?: Array<{ login?: string }>;
1051+
}>;
10251052
const match = issues.find((issue) => issue.body?.includes(`gittensory-upstream-drift:${fingerprint}`));
1026-
if (match?.number && match.html_url) return { number: match.number, url: match.html_url };
1053+
if (match?.number && match.html_url)
1054+
return {
1055+
number: match.number,
1056+
url: match.html_url,
1057+
/* v8 ignore next -- unreachable: `match` only exists when `issue.body?.includes(...)` was truthy above,
1058+
* which already requires match.body to be a defined, non-empty string. */
1059+
body: match.body ?? null,
1060+
labels: githubIssueLabelNames(match.labels),
1061+
assignees: (match.assignees ?? []).map((assignee) => assignee.login ?? "").filter((login) => login.length > 0),
1062+
};
10271063
if (!response.headers.get("link")?.includes('rel="next"')) return null;
10281064
}
10291065
} catch {
@@ -1057,7 +1093,7 @@ async function updateGitHubDriftIssue(repo: string, token: string, issueNumber:
10571093
return payload.number && payload.html_url ? { number: payload.number, url: payload.html_url } : null;
10581094
}
10591095

1060-
async function validateRecordedGitHubIssue(repo: string, token: string, report: UpstreamDriftReportRecord): Promise<{ number: number; url: string } | null> {
1096+
async function validateRecordedGitHubIssue(repo: string, token: string, report: UpstreamDriftReportRecord): Promise<ExistingDriftIssue | null> {
10611097
if (!Number.isInteger(report.issueNumber) || !report.issueNumber || report.issueNumber <= 0 || !report.issueUrl) return null;
10621098
const parsedUrl = parseGitHubIssueUrl(report.issueUrl);
10631099
const [owner, name] = repo.split("/");
@@ -1066,14 +1102,29 @@ async function validateRecordedGitHubIssue(repo: string, token: string, report:
10661102
try {
10671103
const response = await timeoutFetch(`https://api.github.com/repos/${owner}/${name}/issues/${report.issueNumber}`, { headers: githubHeaders(token, "application/vnd.github+json") });
10681104
if (!response.ok) return null;
1069-
const issue = (await response.json()) as { number?: number; html_url?: string; state?: string; body?: string | null; labels?: Array<string | { name?: string }> };
1105+
const issue = (await response.json()) as {
1106+
number?: number;
1107+
html_url?: string;
1108+
state?: string;
1109+
body?: string | null;
1110+
labels?: Array<string | { name?: string }>;
1111+
assignees?: Array<{ login?: string }>;
1112+
};
10701113
if (issue.number !== report.issueNumber || !issue.html_url || issue.state !== "open") return null;
10711114
if (!issue.body?.includes(`gittensory-upstream-drift:${report.fingerprint}`)) return null;
10721115
if (!issue.labels?.some((label) => (typeof label === "string" ? label : label.name)?.toLowerCase() === "signals")) return null;
10731116
const issueUrl = parseGitHubIssueUrl(issue.html_url);
10741117
if (!issueUrl || issueUrl.number !== report.issueNumber) return null;
10751118
if (issueUrl.owner.toLowerCase() !== owner.toLowerCase() || issueUrl.name.toLowerCase() !== name.toLowerCase()) return null;
1076-
return { number: report.issueNumber, url: issue.html_url };
1119+
return {
1120+
number: report.issueNumber,
1121+
url: issue.html_url,
1122+
/* v8 ignore next -- unreachable: `issue.body?.includes(...)` above already required issue.body to be a
1123+
* defined, non-empty string, or this function would have returned null before reaching here. */
1124+
body: issue.body ?? null,
1125+
labels: githubIssueLabelNames(issue.labels),
1126+
assignees: (issue.assignees ?? []).map((assignee) => assignee.login ?? "").filter((login) => login.length > 0),
1127+
};
10771128
} catch {
10781129
return null;
10791130
}
@@ -1112,15 +1163,36 @@ export function resolveDriftAssignees(env: Env): string[] {
11121163
.filter((login) => login.length > 0);
11131164
}
11141165

1166+
function githubDriftIssueLabels(report: UpstreamDriftReportRecord): string[] {
1167+
return ["signals", "scoring", "data", report.severity === "high" || report.severity === "blocking" ? "high-impact" : "backend"];
1168+
}
1169+
11151170
function githubDriftIssuePayload(report: UpstreamDriftReportRecord, assignees: string[]): Record<string, JsonValue> {
11161171
return {
11171172
title: githubDriftIssueTitle(report),
11181173
body: githubDriftIssueBody(report),
1119-
labels: ["signals", "scoring", "data", report.severity === "high" || report.severity === "blocking" ? "high-impact" : "backend"],
1174+
labels: githubDriftIssueLabels(report),
11201175
assignees,
11211176
};
11221177
}
11231178

1179+
/** Would a fresh PATCH of `existing` with `report`/`assignees` change anything on GitHub? (#4503) Compares
1180+
* against the issue's LIVE state (already fetched by validateRecordedGitHubIssue / findGitHubIssueForFingerprint)
1181+
* rather than any locally-stored copy — ground truth, no extra fetch or DB column needed. Body is an exact
1182+
* string match: githubDriftIssueBody has no always-changing field (no timestamp), so an unresolved report with
1183+
* unchanged content produces a byte-identical body every cycle. Labels/assignees are compared as
1184+
* case-insensitive SETS, not ordered arrays — GitHub does not guarantee either the order or the casing it
1185+
* echoes back matches what we last sent. */
1186+
function driftIssueUnchanged(existing: ExistingDriftIssue, report: UpstreamDriftReportRecord, assignees: string[]): boolean {
1187+
if (existing.body !== githubDriftIssueBody(report)) return false;
1188+
const sameSet = (a: string[], b: string[]): boolean => {
1189+
const normalizedA = new Set(a.map((value) => value.toLowerCase()));
1190+
const normalizedB = new Set(b.map((value) => value.toLowerCase()));
1191+
return normalizedA.size === normalizedB.size && [...normalizedA].every((value) => normalizedB.has(value));
1192+
};
1193+
return sameSet(existing.labels, githubDriftIssueLabels(report)) && sameSet(existing.assignees, assignees);
1194+
}
1195+
11241196
function githubDriftIssueBody(report: UpstreamDriftReportRecord): string {
11251197
return [
11261198
`<!-- gittensory-upstream-drift:${report.fingerprint} -->`,

0 commit comments

Comments
 (0)