import fs from "node:fs/promises"; import path from "node:path"; import chalk from "chalk"; import { PACKAGE_ROOT } from "../../../../util"; import { type CommandResult, success, silentFailure } from "../../../lib/command-result"; export type GhActionsSyncSeverity = "error" | "warning"; export interface GhActionsSyncError { type: "missing" | "modified"; severity: GhActionsSyncSeverity; filePath: string; message: string; } export interface GhActionsSyncResult { exitCode: number; errors: GhActionsSyncError[]; summary: { filesChecked: number; errorCount: number; }; } const WORKFLOWS_SRC = path.join(PACKAGE_ROOT, "templates", "workflows"); const ACTIONS_SRC = path.join(PACKAGE_ROOT, "templates", "actions"); const WORKFLOWS_DEST_REL = path.join(".github", "workflows"); const ACTIONS_DEST_REL = path.join(".github", "actions"); interface TemplateFile { srcAbs: string; destRel: string; } async function listWorkflowTemplates(): Promise { let entries: string[]; try { entries = await fs.readdir(WORKFLOWS_SRC); } catch { return []; } return entries .filter((name) => name.endsWith(".yml") || name.endsWith(".yaml")) .map((name) => ({ srcAbs: path.join(WORKFLOWS_SRC, name), destRel: path.join(WORKFLOWS_DEST_REL, name), })); } async function listActionTemplates(): Promise { let entries: { name: string; isDirectory: () => boolean }[]; try { entries = await fs.readdir(ACTIONS_SRC, { withFileTypes: true }); } catch { return []; } const files: TemplateFile[] = []; for (const entry of entries) { if (!entry.isDirectory() || !entry.name.startsWith("erp-kit-")) continue; const actionDir = path.join(ACTIONS_SRC, entry.name); for (const file of await walkDir(actionDir)) { const relWithinAction = path.relative(actionDir, file); files.push({ srcAbs: file, destRel: path.join(ACTIONS_DEST_REL, entry.name, relWithinAction), }); } } return files; } async function walkDir(dir: string): Promise { const results: string[] = []; const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { results.push(...(await walkDir(full))); } else if (entry.isFile()) { results.push(full); } } return results; } async function readFileOrNull(absPath: string): Promise { try { return await fs.readFile(absPath, "utf-8"); } catch (e) { if ((e as NodeJS.ErrnoException).code === "ENOENT") return null; throw e; } } export async function collectGhActionsSyncResult(cwd: string): Promise { const templates = [...(await listWorkflowTemplates()), ...(await listActionTemplates())]; const errors: GhActionsSyncError[] = []; for (const { srcAbs, destRel } of templates) { const destAbs = path.join(cwd, destRel); const sourceContent = await fs.readFile(srcAbs, "utf-8"); const localContent = await readFileOrNull(destAbs); if (localContent === null) { errors.push({ type: "missing", severity: "error", filePath: destRel, message: `Framework template not found locally. Run \`pnpm erp-kit update\` to install it.`, }); continue; } if (localContent !== sourceContent) { errors.push({ type: "modified", severity: "error", filePath: destRel, message: `Framework template content differs from the version shipped with erp-kit. Run \`pnpm erp-kit update\` to refresh, or revert local edits.`, }); } } return { exitCode: errors.length > 0 ? 1 : 0, errors, summary: { filesChecked: templates.length, errorCount: errors.length, }, }; } export function formatGhActionsSyncReport(result: GhActionsSyncResult): string { const lines: string[] = []; lines.push(chalk.bold("gh-actions sync-check: Checking framework template sync...\n")); if (result.errors.length > 0) { lines.push(chalk.red.bold("Errors:\n")); for (const error of result.errors) { lines.push(` ${chalk.red(error.filePath)}`); lines.push(` ${error.message}`); lines.push(""); } } else { lines.push(chalk.green("All framework templates are in sync.\n")); } lines.push(chalk.bold("Summary:")); lines.push(` Files checked: ${result.summary.filesChecked}`); lines.push(` Errors: ${result.summary.errorCount}`); if (result.summary.errorCount > 0) { lines.push(""); lines.push( chalk.red.bold(`gh-actions sync-check failed with ${result.summary.errorCount} error(s).`), ); } else { lines.push(""); lines.push(chalk.green.bold("gh-actions sync-check passed.")); } return lines.join("\n"); } export async function runInternalGhActionsSyncCheck(cwd: string): Promise { const result = await collectGhActionsSyncResult(cwd); console.log(formatGhActionsSyncReport(result)); return result.exitCode === 0 ? success() : silentFailure(result.exitCode); }