Files
pikasTech-HWLAB/scripts/src/runner-issue-visibility-preflight.mjs
T
2026-05-21 17:55:13 +00:00

323 lines
9.1 KiB
JavaScript

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"
]
};
}