mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-17 07:26:27 +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
|
#!/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
|
* Reconciles state instead of reacting to a push diff: any `problem`-labelled
|
||||||
* issue whose LC number has an *implemented* solution file in work/ gets
|
* 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
|
* file existence alone means nothing — a stub must not close its issue. See
|
||||||
* isImplemented().
|
* 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
|
* bun apps/cli/close-solved.ts --dry-run # report only, touch nothing
|
||||||
*
|
*
|
||||||
* Auth: GH_TOKEN / GITHUB_TOKEN, else falls back to `gh auth token`.
|
* 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";
|
import { basename, join } from "node:path";
|
||||||
|
|
||||||
@@ -193,24 +196,82 @@ for (const num of [...work.keys()].sort((a, b) => a - b)) {
|
|||||||
closed++;
|
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 ───────────────────────────────────────────────────────
|
// ── report ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
const rows = [["lc", "issue", "set", "status"], ...report];
|
const rows = [["lc", "issue", "set", "status"], ...report];
|
||||||
printTable(rows);
|
printTable(rows);
|
||||||
|
|
||||||
const implemented = [...work.values()].filter((e) => e.implemented).length;
|
|
||||||
const actionable = report.filter((r) => r[3] !== "no open issue");
|
const actionable = report.filter((r) => r[3] !== "no open issue");
|
||||||
console.log(
|
console.log(
|
||||||
`\n${work.size} in work/ · ${implemented} implemented · ` +
|
`\n${work.size} in work/ · ${solved.length} implemented · ` +
|
||||||
`${actionable.length} matched an open issue · ` +
|
`${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(
|
await writeStepSummary(
|
||||||
`### close-solved${DRY ? " (dry run)" : ""}\n\n` +
|
`### 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} matched an open issue.\n\n` +
|
||||||
(actionable.length
|
(actionable.length
|
||||||
? `${markdownTable([rows[0]!, ...actionable])}\n`
|
? `${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