import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import path from "node:path"; import { promisify } from "node:util"; import { fileURLToPath } from "node:url"; const execFileAsync = promisify(execFile); const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const issue = "pikasTech/HWLAB#51"; const supports = ["pikasTech/HWLAB#7", "pikasTech/HWLAB#42", "pikasTech/HWLAB#39"]; const defaultTimeoutMs = 5000; const githubApiBase = "https://api.github.com"; function parseArgs(argv) { const args = { timeoutMs: defaultTimeoutMs, help: false }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "--timeout-ms") { args.timeoutMs = Number.parseInt(argv[++index], 10); } else if (arg === "--help") { args.help = true; } else { throw new Error(`unknown argument ${arg}`); } } assert.ok(Number.isInteger(args.timeoutMs) && args.timeoutMs > 0, "--timeout-ms must be positive"); return args; } function usage() { return "Usage: node scripts/runner-issue-visibility-preflight.mjs [--timeout-ms 5000]"; } function commandLine(command, args) { return [command, ...args].join(" "); } async function run(command, args = [], options = {}) { const commandText = commandLine(command, args); try { const result = await execFileAsync(command, args, { cwd: repoRoot, timeout: options.timeoutMs ?? defaultTimeoutMs, maxBuffer: 1024 * 1024 }); return { ok: true, command: commandText, exitCode: 0, stdout: result.stdout.trim(), stderr: result.stderr.trim() }; } catch (error) { return { ok: false, command: commandText, exitCode: typeof error.code === "number" ? error.code : 1, stdout: String(error.stdout ?? "").trim(), stderr: String(error.stderr ?? "").trim(), error: error instanceof Error ? error.message : String(error) }; } } async function readRemoteUrl() { return run("git", ["remote", "get-url", "origin"]); } async function readHeadCommit() { return run("git", ["rev-parse", "--short=12", "HEAD"]); } async function probeGitTransport(timeoutMs) { const result = await run("git", ["ls-remote", "origin", "main"], { timeoutMs }); const line = result.stdout.split("\n").find(Boolean) ?? ""; const [commitId, ref] = line.split("\t"); return { ...result, ref: ref ?? null, commitId: commitId ?? null, reachable: result.ok && Boolean(commitId) && Boolean(ref) }; } function parseGithubSlug(remoteUrl) { const patterns = [ /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/iu, /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/iu, /^ssh:\/\/git@github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/iu ]; for (const pattern of patterns) { const match = remoteUrl.match(pattern); if (match) { return `${match[1]}/${match[2]}`; } } return null; } async function probeGithubEndpoint(url, timeoutMs) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { method: "GET", headers: { accept: "application/vnd.github+json", "user-agent": "hwlab-runner-gh-visibility-preflight" }, signal: controller.signal }); const body = await response.text(); return { url, reachable: response.ok, ok: response.ok, status: response.status, statusText: response.statusText, bodyPreview: body.trim().slice(0, 240) }; } catch (error) { return { url, reachable: false, ok: false, error: error instanceof Error ? error.message : String(error) }; } finally { clearTimeout(timer); } } async function checkGh() { return run("gh", ["--version"]); } function tokenPresence(name) { return Boolean(process.env[name] && String(process.env[name]).trim().length > 0); } function blocker(type, scope, summary, nextTask) { const entry = { type, scope, status: "open", summary }; if (nextTask) { entry.nextTask = nextTask; } return entry; } function promptContract() { return { issueIsAuxiliaryOnly: true, selfContainedPromptRequired: true, requiredSections: [ "repository", "branch", "task", "background", "constraints", "acceptance", "validation", "final-response" ], mustInclude: [ "full task objective", "relevant background facts", "hard constraints", "acceptance criteria", "exact validation commands", "issue context pasted into the prompt when needed" ], mustNotAssume: [ "issue comments are readable in the runner", "GitHub REST issue visibility is available", "GitHub REST pull-request visibility is available", "gh is installed", "GH_TOKEN or GITHUB_TOKEN is present" ] }; } export async function runRunnerIssueVisibilityPreflight(argv) { const args = parseArgs(argv); if (args.help) { return { reportKind: "runner-gh-visibility-preflight", usage: usage() }; } const [remoteUrl, headCommit, gitTransport, ghResult] = await Promise.all([ readRemoteUrl(), readHeadCommit(), probeGitTransport(args.timeoutMs), checkGh() ]); const repoSlug = remoteUrl.ok ? parseGithubSlug(remoteUrl.stdout) : null; const tokenFlags = { GH_TOKEN: tokenPresence("GH_TOKEN"), GITHUB_TOKEN: tokenPresence("GITHUB_TOKEN") }; const github = { repo: null, issues: null, pulls: null }; if (repoSlug) { github.repo = await probeGithubEndpoint(`${githubApiBase}/repos/${repoSlug}`, args.timeoutMs); github.issues = await probeGithubEndpoint(`${githubApiBase}/repos/${repoSlug}/issues?per_page=1`, args.timeoutMs); github.pulls = await probeGithubEndpoint(`${githubApiBase}/repos/${repoSlug}/pulls?per_page=1`, args.timeoutMs); } const blockers = []; if (!remoteUrl.ok) { blockers.push( blocker( "environment_blocker", "git-origin", `Unable to read the origin remote URL: ${remoteUrl.stderr || remoteUrl.error || "unknown error"}.`, "Fix the local git remote before queueing a runner task." ) ); } if (!gitTransport.reachable) { blockers.push( blocker( "environment_blocker", "git-transport", `git ls-remote origin main is not reachable from this runner: ${gitTransport.stderr || gitTransport.error || "unknown error"}.`, "Restore git transport reachability before relying on runner-side GitHub visibility." ) ); } if (!repoSlug) { blockers.push( blocker( "observability_blocker", "github-remote-parse", `Could not parse a GitHub repo slug from origin URL ${JSON.stringify(remoteUrl.stdout || "")}.`, "Use a GitHub origin remote URL that can be mapped to owner/repo." ) ); } else { for (const [scope, result] of [ ["github-repo", github.repo], ["github-issues", github.issues], ["github-pulls", github.pulls] ]) { if (result && !result.reachable) { blockers.push( blocker( "observability_blocker", scope, `${scope} is not reachable from this runner${typeof result.status === "number" ? ` (HTTP ${result.status})` : `: ${result.error || "unknown error"}`}.`, "Do not assume issue or PR text is available; keep the prompt self-contained." ) ); } } } const conclusion = blockers.length === 0 ? "ready" : "blocked"; const upstreamRecommendations = []; if (ghResult.ok === false) { upstreamRecommendations.push("Add gh to the runner image or another supported GitHub visibility helper."); } if (!tokenFlags.GH_TOKEN && !tokenFlags.GITHUB_TOKEN) { upstreamRecommendations.push("Inject a read-only GH_TOKEN or GITHUB_TOKEN when GitHub issue or PR reads are required."); } if (blockers.some((item) => item.scope.startsWith("github-") || item.scope === "git-transport")) { upstreamRecommendations.push("Add a queue preflight that checks git transport, repo, issue, and PR visibility before dispatch."); upstreamRecommendations.push("Fail fast when GitHub visibility is missing and require the prompt to include the full task context."); } return { reportKind: "runner-gh-visibility-preflight", reportVersion: "v1", issue, supports, repoRoot, repoSlug, generatedFromCommit: headCommit.ok ? headCommit.stdout : null, conclusion, promptContract: promptContract(), checks: { remoteUrl, headCommit, gitTransport, gh: { available: ghResult.ok, command: ghResult.command, exitCode: ghResult.exitCode, stderr: ghResult.stderr, error: ghResult.error || null }, tokenPresence: tokenFlags, github }, blockers, upstreamRecommendations, validationCommands: [ "node --check scripts/runner-issue-visibility-preflight.mjs", "node --check scripts/src/runner-issue-visibility-preflight.mjs", "node scripts/runner-issue-visibility-preflight.mjs" ] }; }