scripts/check-readiness.mjs
scripts/check-readiness.mjsBrowse 6 files
1,896 tokens
7,921 bytes
Token encoding: o200k_base
Snapshot 5b913e7
← Back to SKILL.md
1#!/usr/bin/env node2import { pathToFileURL } from "node:url";3import {4 ghJson,5 isTerminalIssue,6 normalizeCheck,7 normalizeRepository,8 parseArgs,9 readJson,10 reason,11 summarizePullRequestBody,12 writeJson,13} from "./lib.mjs";14 15function assessChecks(contexts) {16 const checks = contexts.map(normalizeCheck);17 return {18 checks,19 pending: checks.filter((check) => check.pending),20 failing: checks.filter((check) => !check.pending && !check.green),21 allGreen: checks.length > 0 && checks.every((check) => check.green),22 };23}24 25function assessGreptile(checkRuns) {26 const runs = checkRuns.filter((run) => /greptile/i.test(run.name));27 const completed = runs.filter((run) => run.status === "completed");28 const clean = completed.filter((run) => run.conclusion === "success" || run.conclusion === "neutral");29 const blocking = completed.filter((run) => run.conclusion !== "success" && run.conclusion !== "neutral");30 return {31 present: runs.length > 0,32 pending: runs.some((run) => run.status !== "completed"),33 clean: clean.length > 0 && blocking.length === 0,34 runs: runs.map((run) => ({35 name: run.name,36 status: run.status,37 conclusion: run.conclusion,38 detailsUrl: run.details_url ?? null,39 })),40 };41}42 43function fetchCheckRuns(repository, headSha) {44 const runs = [];45 for (let page = 1; page <= 100; page += 1) {46 const response = ghJson([47 "api",48 `repos/${repository}/commits/${headSha}/check-runs?per_page=100&page=${page}`,49 ]);50 const pageRuns = response.check_runs ?? [];51 runs.push(...pageRuns);52 if (pageRuns.length < 100) return runs;53 }54 throw new Error(`Check-run pagination exceeded 100 pages for ${headSha}`);55}56 57export function readinessVerdict({ pullRequest, checks, greptile, behindBy, originatingIssue }) {58 const reasons = [];59 if (pullRequest.state !== "OPEN") reasons.push(reason("pr_not_open", `PR is ${pullRequest.state.toLowerCase()}`));60 const mergeable = pullRequest.mergeable ?? "UNKNOWN";61 if (mergeable === "CONFLICTING") reasons.push(reason("merge_conflict", "GitHub reports merge conflicts"));62 if (mergeable === "UNKNOWN") reasons.push(reason("mergeability_unknown", "GitHub has not resolved mergeability"));63 if (checks.pending.length > 0) {64 reasons.push(reason("checks_pending", `${checks.pending.length} check(s) are pending`, "blocking", { names: checks.pending.map((check) => check.name) }));65 }66 if (checks.failing.length > 0) {67 reasons.push(reason("checks_failing", `${checks.failing.length} check(s) are not green`, "blocking", { names: checks.failing.map((check) => check.name) }));68 }69 if (checks.checks.length === 0) reasons.push(reason("checks_missing", "No status checks were found at the current head"));70 if (!greptile.present) reasons.push(reason("greptile_missing", "No Greptile check-run exists at the current head"));71 else if (greptile.pending) reasons.push(reason("greptile_pending", "Greptile has not completed at the current head"));72 else if (!greptile.clean) reasons.push(reason("greptile_not_clean", "Greptile did not conclude success or neutral at the current head"));73 if (pullRequest.reviewDecision === "CHANGES_REQUESTED") reasons.push(reason("changes_requested", "A review requests changes"));74 if (pullRequest.reviewDecision === "REVIEW_REQUIRED") reasons.push(reason("review_required", "Required review approval is missing"));75 if (behindBy > 0) reasons.push(reason("base_behind", `Head is ${behindBy} commit(s) behind base`, "blocking", { behindBy }));76 if (!originatingIssue) reasons.push(reason("originating_issue_missing", "No originating Paperclip issue was identified", "reporting"));77 else if (!isTerminalIssue(originatingIssue.status)) {78 reasons.push(reason("originating_issue_active", `Originating issue ${originatingIssue.identifier ?? originatingIssue.issueId} is ${originatingIssue.status}`, "reporting"));79 }80 81 if (pullRequest.isDraft) return { verdict: "report_only", reasons };82 return { verdict: reasons.some((entry) => entry.severity === "blocking") ? "needs_gardening" : "ready", reasons };83}84 85export function confidenceFor(entry) {86 if (entry.verdict === "report_only") return "low";87 const codes = new Set(entry.reasons.map((entryReason) => entryReason.code));88 const lowConfidenceCodes = [89 "originating_issue_missing",90 "greptile_missing",91 "greptile_pending",92 "greptile_not_clean",93 "checks_missing",94 "checks_failing",95 "checks_pending",96 "merge_conflict",97 "mergeability_unknown",98 "changes_requested",99 ];100 if (lowConfidenceCodes.some((code) => codes.has(code))) {101 return "low";102 }103 if (entry.verdict === "ready" && !codes.has("originating_issue_active")) return "high";104 return "medium";105}106 107export async function checkReadiness(candidatesDocument, options = {}) {108 const repository = normalizeRepository(options.repo ?? candidatesDocument.repository);109 const results = [];110 for (const candidate of candidatesDocument.candidates) {111 const pullRequest = ghJson([112 "pr",113 "view",114 String(candidate.number),115 "--repo",116 repository,117 "--json",118 "number,url,title,author,body,state,isDraft,headRefOid,baseRefName,headRefName,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,updatedAt",119 ]);120 const checkRuns = fetchCheckRuns(repository, pullRequest.headRefOid);121 const comparison = ghJson([122 "api",123 `repos/${repository}/compare/${encodeURIComponent(pullRequest.baseRefName)}...${encodeURIComponent(pullRequest.headRefOid)}`,124 ]);125 const checks = assessChecks(pullRequest.statusCheckRollup ?? []);126 const greptile = assessGreptile(checkRuns);127 const assessment = readinessVerdict({128 pullRequest,129 checks,130 greptile,131 behindBy: comparison.behind_by ?? 0,132 originatingIssue: candidate.originatingIssue,133 });134 const entry = {135 number: pullRequest.number,136 url: pullRequest.url,137 title: pullRequest.title,138 author: pullRequest.author?.login ?? candidate.author ?? null,139 purpose: summarizePullRequestBody(pullRequest.body),140 state: pullRequest.state.toLowerCase(),141 isDraft: pullRequest.isDraft,142 headSha: pullRequest.headRefOid,143 baseRefName: pullRequest.baseRefName,144 headRefName: pullRequest.headRefName,145 mergeable: (pullRequest.mergeable ?? "UNKNOWN").toLowerCase(),146 mergeStateStatus: (pullRequest.mergeStateStatus ?? "UNKNOWN").toLowerCase(),147 reviewDecision: pullRequest.reviewDecision || null,148 behindBy: comparison.behind_by ?? 0,149 checks,150 greptile,151 originatingIssue: candidate.originatingIssue,152 sourceIssues: candidate.sourceIssues,153 ...assessment,154 };155 entry.confidence = confidenceFor(entry);156 results.push(entry);157 }158 return {159 schemaVersion: 1,160 generatedAt: new Date().toISOString(),161 repository,162 windowDays: candidatesDocument.windowDays ?? null,163 authors: candidatesDocument.query?.authors ?? null,164 truncatedIssues: candidatesDocument.source?.truncatedIssues ?? [],165 candidatesGeneratedAt: candidatesDocument.generatedAt,166 dryRun: Boolean(options.dry_run ?? candidatesDocument.dryRun),167 summary: {168 total: results.length,169 ready: results.filter((entry) => entry.verdict === "ready").length,170 needsGardening: results.filter((entry) => entry.verdict === "needs_gardening").length,171 reportOnly: results.filter((entry) => entry.verdict === "report_only").length,172 },173 pullRequests: results,174 };175}176 177async function main() {178 const options = parseArgs(process.argv.slice(2), { input: "candidates.json", output: "readiness.json" });179 writeJson(options.output, await checkReadiness(readJson(options.input), options));180}181 182if (import.meta.url === pathToFileURL(process.argv[1]).href) {183 main().catch((error) => {184 console.error(error.message);185 process.exitCode = 1;186 });187}188