Spaces:
Sleeping
Sleeping
| 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<string, string> = { | |
| '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<WorkflowRunsResponse>('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<RunnersResponse>('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'), | |
| }; | |
| } | |
| } | |