From d84370db566ed8aff943ce0d7d5451f36f0ccd18 Mon Sep 17 00:00:00 2001 From: Lilith Date: Sun, 11 Jan 2026 06:35:33 -0800 Subject: [PATCH] =?UTF-8?q?fix(bitch-cli):=20=F0=9F=90=9B=20use=20HTML=20s?= =?UTF-8?q?craping=20for=20Forgejo=20Actions=20CI=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forgejo 11.0.8 doesn't expose working Actions API endpoints. Implemented HTML scraping to extract workflow run status from Actions page. - Scrape data-tooltip-content for run status (Success/Failure/Running/Waiting) - Extract run IDs from href patterns - Removed unused forgejoRequest function (API returns 404) - Maintained same WorkflowRun interface for compatibility Verified working with: bitch ci ml Fixes: CI status command now works correctly Co-Authored-By: Claude Sonnet 4.5 (1M context) --- src/utils/forgejo.ts | 91 +++++++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/src/utils/forgejo.ts b/src/utils/forgejo.ts index b55656a..27135ae 100644 --- a/src/utils/forgejo.ts +++ b/src/utils/forgejo.ts @@ -17,47 +17,66 @@ export interface CIStatus { status: 'success' | 'failure' | 'pending' | 'no-runs' | 'error' } -/** - * Get Forgejo API token from environment - */ -function getToken(): string | null { - return process.env.FORGEJO_TOKEN || null -} +// NOTE: Forgejo 11.0.8 doesn't expose working Actions API endpoints +// Using HTML scraping instead (see scrapeWorkflowRuns below) /** - * Make an authenticated request to the Forgejo API + * Scrape workflow runs from Forgejo Actions HTML page + * (Forgejo 11.0.8 doesn't expose Actions API endpoints) */ -async function forgejoRequest(endpoint: string): Promise { - const token = getToken() - - if (!token) { - throw new Error('FORGEJO_TOKEN environment variable not set') - } - - const url = `${DEFAULT_CONFIG.forgejo.api}${endpoint}` +async function scrapeWorkflowRuns(repoName: string): Promise { + const cleanName = repoName.replace('@lilith/', '').replace(/^lilith-/, '') + const url = `${DEFAULT_CONFIG.forgejo.url}/lilith/${cleanName}/actions` try { - const { statusCode, body } = await request(url, { - headers: { - Authorization: `token ${token}`, - Accept: 'application/json', - }, - }) - - if (statusCode === 404) { - return null - } + const { statusCode, body } = await request(url) if (statusCode !== 200) { - throw new Error(`Forgejo API returned ${statusCode}`) + return [] } - return (await body.json()) as T - } catch (error) { - if (error instanceof Error && error.message.includes('FORGEJO_TOKEN')) { - throw error + const html = await body.text() + const runs: WorkflowRun[] = [] + + // Extract run entries from HTML + // Structure: ...... + const runBlockPattern = /data-tooltip-content="(Success|Failure|Running|Waiting|Cancelled)"[\s\S]{1,1000}?href="\/lilith\/[^"]+\/actions\/runs\/(\d+)"/gi + const matches = html.matchAll(runBlockPattern) + + for (const match of matches) { + const statusText = match[1].toLowerCase() + const runId = parseInt(match[2]) + + let status: WorkflowRun['status'] = 'pending' + + if (statusText === 'success') { + status = 'success' + } else if (statusText === 'failure') { + status = 'failure' + } else if (statusText === 'running') { + status = 'running' + } else if (statusText === 'waiting') { + status = 'pending' + } else if (statusText === 'cancelled') { + status = 'cancelled' + } + + runs.push({ + id: runId, + status, + conclusion: status === 'success' || status === 'failure' ? status : null, + created_at: new Date().toISOString(), // Not available from HTML + updated_at: new Date().toISOString(), // Not available from HTML + head_branch: 'main', // Default, not easily extractable from HTML + event: 'push', // Default, not available from HTML + }) + + if (runs.length >= 5) break // Limit to 5 most recent } - return null + + return runs + } catch (error) { + return [] } } @@ -65,14 +84,8 @@ async function forgejoRequest(endpoint: string): Promise { * Get the latest workflow runs for a repository */ export async function getWorkflowRuns(repoName: string): Promise { - // Remove @lilith/ prefix if present - const cleanName = repoName.replace('@lilith/', '').replace(/^lilith-/, '') - - const response = await forgejoRequest<{ workflow_runs: WorkflowRun[] }>( - `/repos/lilith/${cleanName}/actions/runs?per_page=5` - ) - - return response?.workflow_runs || [] + // Use HTML scraping since Forgejo 11.0.8 doesn't expose Actions API + return await scrapeWorkflowRuns(repoName) } /**