mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-16 23:16:26 +00:00
feat(cli): report solved set to SRS worker and log sync status
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Close the GitHub issue for every LeetCode problem actually solved under work/.
|
||||
* Close the GitHub issue for every LeetCode problem actually solved under work/,
|
||||
* then report the solved set to the SRS Worker so D1 agrees.
|
||||
*
|
||||
* Reconciles state instead of reacting to a push diff: any `problem`-labelled
|
||||
* issue whose LC number has an *implemented* solution file in work/ gets
|
||||
@@ -11,10 +12,12 @@
|
||||
* file existence alone means nothing — a stub must not close its issue. See
|
||||
* isImplemented().
|
||||
*
|
||||
* bun apps/cli/close-solved.ts # close matches
|
||||
* bun apps/cli/close-solved.ts # close matches, log them in D1
|
||||
* bun apps/cli/close-solved.ts --dry-run # report only, touch nothing
|
||||
*
|
||||
* Auth: GH_TOKEN / GITHUB_TOKEN, else falls back to `gh auth token`.
|
||||
* SRS_ADMIN_KEY for the D1 push (unset = skip it); SRS_API overrides
|
||||
* the Worker URL for local `wrangler dev` runs.
|
||||
*/
|
||||
import { basename, join } from "node:path";
|
||||
|
||||
@@ -193,24 +196,82 @@ for (const num of [...work.keys()].sort((a, b) => a - b)) {
|
||||
closed++;
|
||||
}
|
||||
|
||||
// ── push the solved set into D1 ───────────────────────────────────
|
||||
|
||||
// Closing an issue is invisible to the Worker: its catalog reconcile reads
|
||||
// GitHub for titles, labels and milestones and never looks at issue state,
|
||||
// and D1's SRS columns only ever move through logAttempt(). So a solution
|
||||
// pushed here — rather than tapped in the digest email — has to be reported,
|
||||
// or the charts, the digest's "already solved" ticks, and the drill/gate
|
||||
// pools all keep treating it as untouched.
|
||||
//
|
||||
// The whole implemented set goes over, not just this run's closes: the
|
||||
// Worker drops anything already on the ladder, so the call is a total
|
||||
// recompute and backfills whatever earlier runs missed.
|
||||
const SRS_API = process.env.SRS_API ?? "https://srs-api.prdlk.workers.dev";
|
||||
const solved = [...work.entries()]
|
||||
.filter(([, entry]) => entry.implemented)
|
||||
.map(([lc]) => lc)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
let syncNote: string;
|
||||
let syncFailed = false;
|
||||
if (!process.env.SRS_ADMIN_KEY) {
|
||||
syncNote = "SRS_ADMIN_KEY unset — D1 not touched";
|
||||
} else if (solved.length === 0) {
|
||||
syncNote = "nothing implemented — D1 not touched";
|
||||
} else {
|
||||
try {
|
||||
const res = await fetch(`${SRS_API}/admin/solved${DRY ? "?dry=1" : ""}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
authorization: `Bearer ${process.env.SRS_ADMIN_KEY}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ lc: solved }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
|
||||
// Narrow the report payload instead of casting it; a shape change should
|
||||
// read as "logged nothing", not throw inside the reconciler.
|
||||
const payload: unknown = await res.json();
|
||||
const fields = payload && typeof payload === "object" ? payload : {};
|
||||
const strings = (value: unknown): string[] => (Array.isArray(value) ? value.map(String) : []);
|
||||
const logged = strings("logged" in fields ? fields.logged : null);
|
||||
const skipped = strings("skipped" in fields ? fields.skipped : null);
|
||||
syncNote =
|
||||
`${DRY ? "would log" : "logged"} ${logged.length} of ${solved.length} ` +
|
||||
`(${skipped.length} already on the ladder or off-curriculum)`;
|
||||
for (const line of logged) console.log(` d1: ${line}`);
|
||||
} catch (err) {
|
||||
// The closes above already landed; surface the failure instead of
|
||||
// letting D1 drift silently until the next push.
|
||||
syncFailed = true;
|
||||
syncNote = `D1 push FAILED: ${err instanceof Error ? err.message : String(err)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── report ───────────────────────────────────────────────────────
|
||||
|
||||
const rows = [["lc", "issue", "set", "status"], ...report];
|
||||
printTable(rows);
|
||||
|
||||
const implemented = [...work.values()].filter((e) => e.implemented).length;
|
||||
const actionable = report.filter((r) => r[3] !== "no open issue");
|
||||
console.log(
|
||||
`\n${work.size} in work/ · ${implemented} implemented · ` +
|
||||
`\n${work.size} in work/ · ${solved.length} implemented · ` +
|
||||
`${actionable.length} matched an open issue · ` +
|
||||
`${DRY ? `would close ${actionable.filter((r) => r[3] === "would close").length}` : `closed ${closed}`}`,
|
||||
`${DRY ? `would close ${actionable.filter((r) => r[3] === "would close").length}` : `closed ${closed}`}` +
|
||||
`\nd1: ${syncNote}`,
|
||||
);
|
||||
|
||||
await writeStepSummary(
|
||||
`### close-solved${DRY ? " (dry run)" : ""}\n\n` +
|
||||
`${work.size} files in \`work/\`, ${implemented} implemented, ` +
|
||||
`${work.size} files in \`work/\`, ${solved.length} implemented, ` +
|
||||
`${actionable.length} matched an open issue.\n\n` +
|
||||
(actionable.length
|
||||
? `${markdownTable([rows[0]!, ...actionable])}\n`
|
||||
: "Nothing to close.\n"),
|
||||
: "Nothing to close.\n") +
|
||||
`\nD1: ${syncNote}\n`,
|
||||
);
|
||||
|
||||
// Non-zero only for the D1 push: the closes are already reported above.
|
||||
if (syncFailed) process.exitCode = 1;
|
||||
|
||||
Reference in New Issue
Block a user