143 lines
3.7 KiB
TypeScript
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))
|
|
)
|
|
}
|