bitch-cli/src/commands/ci.ts
2026-03-08 19:39:00 -07:00

155 lines
5.2 KiB
TypeScript

import { Command } from 'commander'
import ora from 'ora'
import { findPackages } from '../utils/package-json.js'
import { getCIStatus, formatDate, type CIStatus } from '../utils/forgejo.js'
import { colors, printTable, logInfo, logWarning, logError } from '../utils/output.js'
export function createCICommand(): Command {
return new Command('ci')
.description('Check Forgejo Actions CI status')
.argument('[package]', 'Specific package to check (optional)')
.option('-a, --all', 'Check all packages in workspace')
.option('-f, --failed', 'Show only failed packages')
.option('-v, --verbose', 'Show detailed failure information')
.option('--path <path>', 'Path to workspace (default: current directory)')
.action(async (packageName: string | undefined, options: {
all?: boolean
failed?: boolean
verbose?: boolean
path?: string
}) => {
const basePath = options.path || process.cwd()
// Check for FORGEJO_TOKEN
if (!process.env.FORGEJO_TOKEN) {
logError('FORGEJO_TOKEN environment variable not set')
console.log()
console.log('Set it with:')
console.log(colors.dim(' export FORGEJO_TOKEN="your-token-here"'))
process.exit(1)
}
const spinner = ora('Checking CI status...').start()
try {
let packagesToCheck: string[] = []
if (packageName) {
packagesToCheck = [packageName]
} else if (options.all) {
const packages = await findPackages(basePath)
packagesToCheck = packages
.filter(p => p.shouldPublish)
.map(p => p.name)
} else {
// Check current directory's package
const packages = await findPackages(basePath)
const currentPkg = packages.find(p => p.path === basePath)
if (currentPkg) {
packagesToCheck = [currentPkg.name]
} else {
spinner.stop()
logWarning('No package found in current directory. Use --all to check all packages.')
return
}
}
if (packagesToCheck.length === 0) {
spinner.stop()
logWarning('No packages to check')
return
}
spinner.text = `Checking ${packagesToCheck.length} package(s)...`
const statuses: CIStatus[] = []
for (const pkg of packagesToCheck) {
const status = await getCIStatus(pkg)
statuses.push(status)
}
spinner.stop()
// Filter if --failed flag is set
const displayStatuses = options.failed
? statuses.filter(s => s.status === 'failure')
: statuses
if (displayStatuses.length === 0 && options.failed) {
console.log()
console.log(colors.success('✓ No failing packages'))
return
}
console.log()
logInfo(`CI status for ${displayStatuses.length} package(s)${options.failed ? ' (failures only)' : ''}`)
console.log()
const headers = ['Package', 'Status', 'Branch', 'Last Run']
const rows = displayStatuses.map(s => [
s.repo,
formatStatus(s.status),
s.lastRun?.head_branch || colors.dim('-'),
s.lastRun ? formatDate(s.lastRun.updated_at) : colors.dim('never'),
])
printTable(headers, rows)
console.log()
// Show verbose failure details
if (options.verbose) {
const failures = displayStatuses.filter(s => s.status === 'failure')
if (failures.length > 0) {
console.log(colors.error('Failure Details:'))
console.log()
for (const fail of failures) {
console.log(colors.error(`${fail.repo}`))
if (fail.lastRun) {
const url = `http://forge.black.local/lilith/${fail.repo}/actions`
console.log(` ${colors.dim(url)}`)
}
console.log()
}
}
}
// Summary
const success = statuses.filter(s => s.status === 'success').length
const failure = statuses.filter(s => s.status === 'failure').length
const pending = statuses.filter(s => s.status === 'pending').length
const noRuns = statuses.filter(s => s.status === 'no-runs').length
if (failure > 0) {
logError(`${failure} package(s) have failing CI`)
}
if (pending > 0) {
logWarning(`${pending} package(s) have pending CI`)
}
if (success > 0) {
console.log(colors.success(`${success} package(s) have passing CI`))
}
if (noRuns > 0) {
console.log(colors.dim(`${noRuns} package(s) have no CI runs`))
}
} catch (error) {
spinner.fail('Failed to check CI status')
console.error(error instanceof Error ? error.message : error)
process.exit(1)
}
})
}
function formatStatus(status: CIStatus['status']): string {
switch (status) {
case 'success':
return colors.success('✓ passing')
case 'failure':
return colors.error('✗ failing')
case 'pending':
return colors.warning('● pending')
case 'no-runs':
return colors.dim('○ no runs')
case 'error':
return colors.error('? error')
}
}