bitch-cli/src/utils/workflow-templates.ts

143 lines
3.7 KiB
TypeScript

import { mkdir, copyFile, stat } from 'node:fs/promises'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { AuditResult } from './workflow-audit.js'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
export type TemplateType =
| 'publish-npm' // TypeScript with build
| 'publish-config' // TypeScript config-only
| 'publish-pypi' // Python packages
| 'ci-publish-separate' // High-impact packages
async function fileExists(path: string): Promise<boolean> {
try {
await stat(path)
return true
} catch {
return false
}
}
/**
* Select appropriate workflow template based on package type
*/
export function selectTemplate(pkg: AuditResult): TemplateType | null {
// Python packages
if (pkg.type === 'python') {
return 'publish-pypi'
}
// TypeScript config-only
if (pkg.type === 'typescript-config') {
return 'publish-config'
}
// High-impact TypeScript (separate CI)
const highImpact = ['@mcp/', '@service/', '@nestjs/']
if (highImpact.some((prefix) => pkg.name.startsWith(prefix))) {
return 'ci-publish-separate'
}
// Standard TypeScript with build
if (pkg.type === 'typescript-build') {
return 'publish-npm'
}
// No workflow needed for utilities, docker, documentation, etc.
return null
}
/**
* Deploy workflow template to a package
*/
export async function deployWorkflow(
pkg: AuditResult,
template: TemplateType,
options: { dryRun?: boolean; updateExisting?: boolean }
): Promise<{ success: boolean; message: string }> {
// Resolve template path (../../templates/workflows from utils/ directory)
const templatePath = join(__dirname, '..', '..', 'templates', 'workflows', `${template}.yml`)
const targetDir = join(pkg.path, '.forgejo', 'workflows')
const targetFile = join(targetDir, 'publish.yml')
// Check if template exists
if (!await fileExists(templatePath)) {
return {
success: false,
message: `Template ${template}.yml not found`,
}
}
// Check if target already exists
const exists = await fileExists(targetFile)
if (exists && !options.updateExisting) {
return {
success: false,
message: 'Workflow already exists (use --update-existing to overwrite)',
}
}
// Dry run mode
if (options.dryRun) {
return {
success: true,
message: `Would deploy ${template}${targetFile}`,
}
}
// Create directory if needed
try {
await mkdir(targetDir, { recursive: true })
} catch (err) {
return {
success: false,
message: `Failed to create directory: ${err instanceof Error ? err.message : String(err)}`,
}
}
// Copy template
try {
await copyFile(templatePath, targetFile)
return {
success: true,
message: exists ? `Updated ${template}` : `Deployed ${template}`,
}
} catch (err) {
return {
success: false,
message: `Failed to copy template: ${err instanceof Error ? err.message : String(err)}`,
}
}
}
/**
* Get packages by deployment phase
*/
export function getPackagesByPhase(
packages: AuditResult[],
phase: number
): AuditResult[] {
// Phase definitions
const phases: Record<number, string[]> = {
1: ['@mcp/', '@configs/', '@service/'],
2: ['@nestjs/', '@typescript/', '@eslint/', '@infrastructure/'],
3: ['@ui/'],
4: ['@ml/', 'queue-ts/', '@websocket/'],
5: [], // Python packages (type-based, not prefix)
}
// Phase 5 is Python packages
if (phase === 5) {
return packages.filter((p) => p.type === 'python')
}
// Other phases are prefix-based
const prefixes = phases[phase] || []
return packages.filter((p) =>
prefixes.some((prefix) => p.name.startsWith(prefix))
)
}