#!/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 = {}): Promise { 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 = { "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`, ); }