fix(bitch-cli): 🐛 use HTML scraping for Forgejo Actions CI status

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) <noreply@anthropic.com>
This commit is contained in:
Lilith 2026-01-11 06:35:33 -08:00
parent 7662f41158
commit d84370db56

View file

@ -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<T>(endpoint: string): Promise<T | null> {
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<WorkflowRun[]> {
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: <span data-tooltip-content="Status">...</span>...<a href="/lilith/REPO/actions/runs/ID">
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<T>(endpoint: string): Promise<T | null> {
* Get the latest workflow runs for a repository
*/
export async function getWorkflowRuns(repoName: string): Promise<WorkflowRun[]> {
// 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)
}
/**