import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { runCommand, runJsonCommand } from './command.js'; const execFileAsync = promisify(execFile); type WorkflowRunApi = { id: number; name?: string; display_title?: string; event?: string; head_branch?: string; head_sha?: string; status?: string; conclusion?: string | null; html_url?: string; created_at?: string; updated_at?: string; }; type WorkflowRunsResponse = { workflow_runs?: WorkflowRunApi[]; }; type RunnerApi = { id: number; name: string; os: string; status: string; busy: boolean; labels?: Array<{ name: string }>; }; type RunnersResponse = { total_count?: number; runners?: RunnerApi[]; }; const SUITE_COMMANDS: Record = { 'docs-and-architecture': 'npm ci && npm run test:docs:root && npm run test:contracts:active && npm run test:dashboard:boundary && npm run test:guardrails && npm run test:cycles', 'python-unit-311': "python3.11 -m venv .venv311 && . .venv311/bin/activate && python -m pip install --upgrade pip && python -m pip install -r requirements.txt -r requirements.runtime.txt && python -m pip install -e '.[dev]' && pytest tests/unit -q", 'dashboard-quality': 'npm --prefix dashboard-enterprise ci && npm --prefix dashboard-enterprise run lint && npm --prefix dashboard-enterprise run type-check && npm run ops:dashboard:build:ci && npm run ops:dashboard:interaction-smoke:ci', }; export async function listWorkflowRuns(input: { repo: string; branch?: string; event?: string; status?: string; limit?: number; }) { const params = new URLSearchParams(); params.set('per_page', String(Math.max(1, Math.min(input.limit || 20, 100)))); if (input.branch) params.set('branch', input.branch); if (input.event) params.set('event', input.event); if (input.status) params.set('status', input.status); const suffix = params.toString() ? `?${params.toString()}` : ''; const payload = await runJsonCommand('gh', ['api', `repos/${input.repo}/actions/runs${suffix}`]); return { ok: true, repo: input.repo, runs: (payload.workflow_runs || []).map((row) => ({ id: row.id, workflowName: row.name || null, title: row.display_title || null, event: row.event || null, branch: row.head_branch || null, sha: row.head_sha || null, status: row.status || null, conclusion: row.conclusion || null, url: row.html_url || null, createdAt: row.created_at || null, updatedAt: row.updated_at || null, })), }; } export async function rerunWorkflowRun(repo: string, runId: number) { await runCommand('gh', ['run', 'rerun', String(runId), '--repo', repo]); return { ok: true, repo, runId }; } export async function cancelWorkflowRun(repo: string, runId: number) { await runCommand('gh', ['run', 'cancel', String(runId), '--repo', repo]); return { ok: true, repo, runId }; } export async function listRunners(repo: string) { const payload = await runJsonCommand('gh', ['api', `repos/${repo}/actions/runners`]); return { ok: true, repo, totalCount: payload.total_count || 0, runners: (payload.runners || []).map((row) => ({ id: row.id, name: row.name, os: row.os, status: row.status, busy: row.busy, labels: (row.labels || []).map((label) => label.name), })), }; } export async function runLocalSuite(input: { repoPath: string; suite: 'docs-and-architecture' | 'python-unit-311' | 'dashboard-quality'; timeoutMs?: number; }) { const command = SUITE_COMMANDS[input.suite]; if (!command) { throw new Error(`unknown_ci_suite:${input.suite}`); } const startedAt = new Date().toISOString(); const timeout = Math.max(60_000, Math.min(input.timeoutMs || 900_000, 3_600_000)); try { const result = await execFileAsync('/bin/bash', ['-lc', command], { cwd: input.repoPath, env: process.env, maxBuffer: 1024 * 1024 * 16, timeout, }); return { ok: true, suite: input.suite, startedAt, finishedAt: new Date().toISOString(), stdout: String(result.stdout || '').trim(), stderr: String(result.stderr || '').trim(), }; } catch (error) { const failure = error as { stdout?: string; stderr?: string; message?: string; signal?: string; code?: number }; return { ok: false, suite: input.suite, startedAt, finishedAt: new Date().toISOString(), stdout: String(failure.stdout || '').trim(), stderr: String(failure.stderr || '').trim(), signal: failure.signal || null, code: typeof failure.code === 'number' ? failure.code : null, error: String(failure.message || 'local_suite_failed'), }; } }