mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-16 23:16:26 +00:00
Add SRS automation: scheduler, logger, weekly gates
This commit is contained in:
Executable
+146
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* One-shot (idempotent) SRS setup + PROJECT_PAT verification.
|
||||
*
|
||||
* 1. Proves the token can talk GraphQL (viewer login) — the acceptance test
|
||||
* for the PROJECT_PAT repo secret when dispatched as a workflow.
|
||||
* 2. Resolves the "Interview Prep" user project and its built-in Target Date
|
||||
* field (which the SRS reuses — no custom date field is ever created).
|
||||
* 3. Creates the `SRS Stage` and `First Attempt` single-selects when missing.
|
||||
* 4. Mirrors every laddered problem's state into the project fields.
|
||||
*
|
||||
* Prints every resolved ID so they are on record in the run log.
|
||||
*/
|
||||
import { $ } from "bun";
|
||||
|
||||
import { github } from "./github.ts";
|
||||
import { projectMirror, reportMirror } from "./srs-project.ts";
|
||||
import { loadState } from "./srs.ts";
|
||||
|
||||
const PROJECT_TITLE = "Interview Prep";
|
||||
|
||||
const token =
|
||||
process.env.PROJECT_PAT ||
|
||||
process.env.GH_TOKEN ||
|
||||
process.env.GITHUB_TOKEN ||
|
||||
(await $`gh auth token`.text()).trim();
|
||||
|
||||
async function graphql(query: string, variables: Record<string, unknown> = {}): Promise<unknown> {
|
||||
const res = await fetch("https://api.github.com/graphql", {
|
||||
method: "POST",
|
||||
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
||||
body: JSON.stringify({ query, variables }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`graphql -> ${res.status} ${await res.text()}`);
|
||||
const payload = (await res.json()) as { data?: unknown; errors?: { message: string }[] };
|
||||
if (payload.errors?.length) throw new Error(payload.errors.map((e) => e.message).join("; "));
|
||||
return payload.data;
|
||||
}
|
||||
|
||||
// 1. token check — hard fail here means PROJECT_PAT is missing or scopeless.
|
||||
const viewerData = (await graphql(`{ viewer { login } }`)) as { viewer: { login: string } };
|
||||
console.log(`token OK — authenticated as @${viewerData.viewer.login}`);
|
||||
|
||||
const gh = await github();
|
||||
const owner = gh.repo.split("/")[0]!;
|
||||
|
||||
// 2. resolve the project and its fields.
|
||||
interface FieldNode {
|
||||
id: string;
|
||||
name: string;
|
||||
dataType?: string;
|
||||
options?: { id: string; name: string }[];
|
||||
}
|
||||
const projectData = (await graphql(
|
||||
`query($owner: String!, $title: String!) {
|
||||
user(login: $owner) {
|
||||
projectsV2(first: 10, query: $title) {
|
||||
nodes {
|
||||
id number title
|
||||
fields(first: 30) {
|
||||
nodes {
|
||||
... on ProjectV2FieldCommon { id name dataType }
|
||||
... on ProjectV2SingleSelectField { id name options { id name } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{ owner, title: PROJECT_TITLE },
|
||||
)) as {
|
||||
user: {
|
||||
projectsV2: { nodes: { id: string; number: number; title: string; fields: { nodes: FieldNode[] } }[] };
|
||||
};
|
||||
};
|
||||
const project = projectData.user.projectsV2.nodes.find((n) => n.title === PROJECT_TITLE);
|
||||
if (!project) throw new Error(`project "${PROJECT_TITLE}" not found for @${owner}`);
|
||||
console.log(`project: "${project.title}" #${project.number} — ${project.id}`);
|
||||
|
||||
let fields = project.fields.nodes;
|
||||
const targetDate = fields.find((f) => f.name === "Target Date" && f.dataType === "DATE");
|
||||
if (!targetDate) {
|
||||
throw new Error('the built-in "Target Date" field is missing — add it in the project UI');
|
||||
}
|
||||
console.log(`Target Date field: ${targetDate.id} (reused, not created)`);
|
||||
|
||||
// 3. create the two SRS single-selects when absent.
|
||||
const WANTED: Record<string, { name: string; color: string; description: string }[]> = {
|
||||
"SRS Stage": [
|
||||
{ name: "new", color: "GRAY", description: "not yet on the ladder" },
|
||||
{ name: "+2", color: "YELLOW", description: "review due 2 days after last solve" },
|
||||
{ name: "+5", color: "ORANGE", description: "review due 5 days after last solve" },
|
||||
{ name: "+10", color: "BLUE", description: "review due 10 days after last solve" },
|
||||
{ name: "retired", color: "GREEN", description: "passed all three stages" },
|
||||
],
|
||||
"First Attempt": [
|
||||
{ name: "pass", color: "GREEN", description: "first timed solve passed" },
|
||||
{ name: "fail", color: "RED", description: "first timed solve failed" },
|
||||
],
|
||||
};
|
||||
|
||||
for (const [name, options] of Object.entries(WANTED)) {
|
||||
const existing = fields.find((f) => f.name === name);
|
||||
if (existing) {
|
||||
console.log(`${name} field: ${existing.id} (already exists)`);
|
||||
continue;
|
||||
}
|
||||
const optionsArg = options
|
||||
.map((o) => `{name:"${o.name}",color:${o.color},description:"${o.description}"}`)
|
||||
.join(",");
|
||||
const created = (await graphql(
|
||||
`mutation($project: ID!, $name: String!) {
|
||||
createProjectV2Field(input: {
|
||||
projectId: $project, dataType: SINGLE_SELECT, name: $name,
|
||||
singleSelectOptions: [${optionsArg}]
|
||||
}) {
|
||||
projectV2Field { ... on ProjectV2SingleSelectField { id name } }
|
||||
}
|
||||
}`,
|
||||
{ project: project.id, name },
|
||||
)) as { createProjectV2Field: { projectV2Field: { id: string } } };
|
||||
console.log(`${name} field: ${created.createProjectV2Field.projectV2Field.id} (created)`);
|
||||
fields = [...fields, { id: created.createProjectV2Field.projectV2Field.id, name }];
|
||||
}
|
||||
|
||||
// 4. mirror every laddered problem into the project.
|
||||
const state = await loadState();
|
||||
const mirror = await projectMirror(owner, gh.repo);
|
||||
let mirrored = 0;
|
||||
for (const [lc, p] of Object.entries(state.problems)) {
|
||||
await mirror.setStage(p.issue, p.stage);
|
||||
await mirror.setTargetDate(p.issue, p.stage === "retired" ? null : (p.next_review ?? null));
|
||||
const first = p.history[0];
|
||||
if (first) await mirror.setFirstAttempt(p.issue, first.result);
|
||||
mirrored++;
|
||||
if (mirrored % 10 === 0) console.log(` mirrored ${mirrored} problems… (last LC ${lc})`);
|
||||
}
|
||||
console.log(`mirrored ${mirrored} laddered problems into the project`);
|
||||
await reportMirror(mirror);
|
||||
|
||||
if (process.env.GITHUB_STEP_SUMMARY && mirror.warnings.length === 0) {
|
||||
await Bun.write(
|
||||
process.env.GITHUB_STEP_SUMMARY,
|
||||
`### srs-setup ✅\n\n- token: @${viewerData.viewer.login}\n- project: ${project.id} (#${project.number})\n- Target Date: ${targetDate.id}\n- mirrored: ${mirrored} problems\n`,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user