delqhi/sin-github-issues
0
1import { execFile } from 'node:child_process';2import { promisify } from 'node:util';3import { runCommand, runJsonCommand } from './command.js';4 5const execFileAsync = promisify(execFile);6 7type WorkflowRunApi = {8 id: number;9 name?: string;10 display_title?: string;11 event?: string;12 head_branch?: string;13 head_sha?: string;14 status?: string;15 conclusion?: string | null;16 html_url?: string;17 created_at?: string;18 updated_at?: string;19};20 21type WorkflowRunsResponse = {22 workflow_runs?: WorkflowRunApi[];23};24 25type RunnerApi = {26 id: number;27 name: string;28 os: string;29 status: string;30 busy: boolean;31 labels?: Array<{ name: string }>;32};33 34type RunnersResponse = {35 total_count?: number;36 runners?: RunnerApi[];37};38 39const SUITE_COMMANDS: Record<string, string> = {40 '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',41 '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",42 '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',43};44 45export async function listWorkflowRuns(input: {46 repo: string;47 branch?: string;48 event?: string;49 status?: string;50 limit?: number;51}) {52 const params = new URLSearchParams();53 params.set('per_page', String(Math.max(1, Math.min(input.limit || 20, 100))));54 if (input.branch) params.set('branch', input.branch);55 if (input.event) params.set('event', input.event);56 if (input.status) params.set('status', input.status);57 const suffix = params.toString() ? `?${params.toString()}` : '';58 const payload = await runJsonCommand<WorkflowRunsResponse>('gh', ['api', `repos/${input.repo}/actions/runs${suffix}`]);59 return {60 ok: true,61 repo: input.repo,62 runs: (payload.workflow_runs || []).map((row) => ({63 id: row.id,64 workflowName: row.name || null,65 title: row.display_title || null,66 event: row.event || null,67 branch: row.head_branch || null,68 sha: row.head_sha || null,69 status: row.status || null,70 conclusion: row.conclusion || null,71 url: row.html_url || null,72 createdAt: row.created_at || null,73 updatedAt: row.updated_at || null,74 })),75 };76}77 78export async function rerunWorkflowRun(repo: string, runId: number) {79 await runCommand('gh', ['run', 'rerun', String(runId), '--repo', repo]);80 return { ok: true, repo, runId };81}82 83export async function cancelWorkflowRun(repo: string, runId: number) {84 await runCommand('gh', ['run', 'cancel', String(runId), '--repo', repo]);85 return { ok: true, repo, runId };86}87 88export async function listRunners(repo: string) {89 const payload = await runJsonCommand<RunnersResponse>('gh', ['api', `repos/${repo}/actions/runners`]);90 return {91 ok: true,92 repo,93 totalCount: payload.total_count || 0,94 runners: (payload.runners || []).map((row) => ({95 id: row.id,96 name: row.name,97 os: row.os,98 status: row.status,99 busy: row.busy,100 labels: (row.labels || []).map((label) => label.name),101 })),102 };103}104 105export async function runLocalSuite(input: {106 repoPath: string;107 suite: 'docs-and-architecture' | 'python-unit-311' | 'dashboard-quality';108 timeoutMs?: number;109}) {110 const command = SUITE_COMMANDS[input.suite];111 if (!command) {112 throw new Error(`unknown_ci_suite:${input.suite}`);113 }114 const startedAt = new Date().toISOString();115 const timeout = Math.max(60_000, Math.min(input.timeoutMs || 900_000, 3_600_000));116 try {117 const result = await execFileAsync('/bin/bash', ['-lc', command], {118 cwd: input.repoPath,119 env: process.env,120 maxBuffer: 1024 * 1024 * 16,121 timeout,122 });123 return {124 ok: true,125 suite: input.suite,126 startedAt,127 finishedAt: new Date().toISOString(),128 stdout: String(result.stdout || '').trim(),129 stderr: String(result.stderr || '').trim(),130 };131 } catch (error) {132 const failure = error as { stdout?: string; stderr?: string; message?: string; signal?: string; code?: number };133 return {134 ok: false,135 suite: input.suite,136 startedAt,137 finishedAt: new Date().toISOString(),138 stdout: String(failure.stdout || '').trim(),139 stderr: String(failure.stderr || '').trim(),140 signal: failure.signal || null,141 code: typeof failure.code === 'number' ? failure.code : null,142 error: String(failure.message || 'local_suite_failed'),143 };144 }145}146 