mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-16 23:16:26 +00:00
147 lines
5.8 KiB
TypeScript
Executable File
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`,
|
|
);
|
|
}
|