Files
leetcode/scripts/srs-setup.ts
T

147 lines
5.8 KiB
TypeScript
Executable File

#!/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`,
);
}