feat: add gitea actions cicd poc plan
This commit is contained in:
@@ -0,0 +1,488 @@
|
||||
// SPEC: GH-1548 Gitea mirror/Actions visibility and controlled Docker builder POC.
|
||||
// Responsibility: read-only YAML-first plan/status for the proposed CI/CD governance split.
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { isAbsolute } from "node:path";
|
||||
import { rootPath, type UniDeskConfig } from "./config";
|
||||
import { renderMachine } from "./cicd-render";
|
||||
import type { RenderedCliResult } from "./output";
|
||||
|
||||
const DEFAULT_CONFIG_PATH = "config/cicd-gitea-actions-poc.yaml";
|
||||
|
||||
type OutputMode = "text" | "json" | "yaml";
|
||||
type Action = "plan" | "status" | "help";
|
||||
|
||||
interface Options {
|
||||
action: Action;
|
||||
configPath: string;
|
||||
output: OutputMode;
|
||||
targetId: string | null;
|
||||
}
|
||||
|
||||
interface LoadedPoc {
|
||||
configPath: string;
|
||||
root: Record<string, unknown>;
|
||||
spec: Record<string, unknown>;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export function cicdGiteaActionsPocHelp(): unknown {
|
||||
return {
|
||||
command: "cicd gitea-actions-poc plan|status",
|
||||
output: "text by default; use --json, --raw, or -o json|yaml for machine output",
|
||||
usage: [
|
||||
"bun scripts/cli.ts cicd gitea-actions-poc plan",
|
||||
"bun scripts/cli.ts cicd gitea-actions-poc status",
|
||||
"bun scripts/cli.ts cicd gitea-actions-poc plan --target agentrun-jd01-v02",
|
||||
],
|
||||
config: DEFAULT_CONFIG_PATH,
|
||||
issue: "https://github.com/pikasTech/unidesk/issues/1548",
|
||||
description: "Read-only P1/P2 plan for replacing branch-follower responsibilities with Gitea mirror, Gitea Actions visibility, controlled Docker/BuildKit builder plane, existing Tekton, existing Argo CD, and bounded UniDesk status while preserving env reuse.",
|
||||
};
|
||||
}
|
||||
|
||||
export async function runGiteaActionsPocCommand(_config: UniDeskConfig | null, args: string[], alias = "gitea-actions-poc"): Promise<RenderedCliResult> {
|
||||
const options = parseOptions(args);
|
||||
const command = `cicd ${alias}${options.action === "help" ? "" : ` ${options.action}`}`;
|
||||
if (options.action === "help") return renderMachine(command, cicdGiteaActionsPocHelp(), options.output === "yaml" ? "yaml" : "json");
|
||||
const loaded = loadPoc(options.configPath);
|
||||
const payload = options.action === "plan" ? buildPlan(loaded, options) : buildStatus(loaded, options);
|
||||
if (options.output === "json") return renderMachine(command, payload, "json", payload.ok !== false);
|
||||
if (options.output === "yaml") return renderMachine(command, payload, "yaml", payload.ok !== false);
|
||||
return {
|
||||
ok: payload.ok !== false,
|
||||
command,
|
||||
renderedText: options.action === "plan" ? renderPlanHuman(payload) : renderStatusHuman(payload),
|
||||
contentType: "text/plain",
|
||||
};
|
||||
}
|
||||
|
||||
function parseOptions(args: string[]): Options {
|
||||
const actionToken = args[0];
|
||||
const action: Action = actionToken === undefined || isHelpToken(actionToken) ? "help" : parseAction(actionToken);
|
||||
const options: Options = {
|
||||
action,
|
||||
configPath: DEFAULT_CONFIG_PATH,
|
||||
output: "text",
|
||||
targetId: null,
|
||||
};
|
||||
for (let index = action === "help" ? 0 : 1; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === undefined || isHelpToken(arg)) {
|
||||
options.action = "help";
|
||||
continue;
|
||||
}
|
||||
if (arg === "--json" || arg === "--raw") {
|
||||
options.output = "json";
|
||||
continue;
|
||||
}
|
||||
if (arg === "-o" || arg === "--output") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined) throw new Error(`${arg} requires text, json, or yaml`);
|
||||
options.output = parseOutput(value, arg);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("-o=")) {
|
||||
options.output = parseOutput(arg.slice(3), "-o");
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--output=")) {
|
||||
options.output = parseOutput(arg.slice("--output=".length), "--output");
|
||||
continue;
|
||||
}
|
||||
if (arg === "--config") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.length === 0) throw new Error("--config requires a path");
|
||||
options.configPath = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--config=")) {
|
||||
options.configPath = arg.slice("--config=".length);
|
||||
continue;
|
||||
}
|
||||
if (arg === "--target") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.length === 0) throw new Error("--target requires a target id");
|
||||
options.targetId = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--target=")) {
|
||||
options.targetId = arg.slice("--target=".length);
|
||||
continue;
|
||||
}
|
||||
if (arg === "--confirm") throw new Error("cicd gitea-actions-poc is read-only in GH-1548 P1/P2; --confirm is not accepted");
|
||||
if (arg === "--dry-run" || arg === "--all") continue;
|
||||
throw new Error(`unsupported cicd gitea-actions-poc option: ${arg}`);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function parseAction(value: string): Action {
|
||||
if (value === "plan" || value === "status") return value;
|
||||
if (isHelpToken(value)) return "help";
|
||||
if (value === "apply" || value === "run-once" || value === "trigger-current") {
|
||||
throw new Error(`cicd gitea-actions-poc ${value} is intentionally unavailable; GH-1548 first stage is read-only plan/status`);
|
||||
}
|
||||
throw new Error("cicd gitea-actions-poc usage: cicd gitea-actions-poc plan|status [--target <id>] [--config <path>]");
|
||||
}
|
||||
|
||||
function parseOutput(value: string, flag: string): OutputMode {
|
||||
if (value === "text" || value === "json" || value === "yaml") return value;
|
||||
throw new Error(`${flag} must be text, json, or yaml`);
|
||||
}
|
||||
|
||||
function isHelpToken(value: string): boolean {
|
||||
return value === "help" || value === "--help" || value === "-h";
|
||||
}
|
||||
|
||||
function loadPoc(configPath: string): LoadedPoc {
|
||||
const absolutePath = isAbsolute(configPath) ? configPath : rootPath(configPath);
|
||||
if (!existsSync(absolutePath)) throw new Error(`${configPath} does not exist`);
|
||||
const root = record(Bun.YAML.parse(readFileSync(absolutePath, "utf8")), configPath);
|
||||
const spec = record(root.spec, `${configPath}.spec`);
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
validateRoot(root, spec, configPath, errors, warnings);
|
||||
return { configPath, root, spec, errors, warnings };
|
||||
}
|
||||
|
||||
function validateRoot(root: Record<string, unknown>, spec: Record<string, unknown>, configPath: string, errors: string[], warnings: string[]): void {
|
||||
if (stringOrNull(root.kind) !== "CicdGiteaActionsPoc") errors.push(`${configPath}.kind must be CicdGiteaActionsPoc`);
|
||||
const scope = optionalRecord(spec.scope);
|
||||
if (scope?.productionFollowerReplacement !== false) errors.push(`${configPath}.spec.scope.productionFollowerReplacement must be false for first-stage POC`);
|
||||
if (scope?.rolloutEnabled !== false) errors.push(`${configPath}.spec.scope.rolloutEnabled must be false for first-stage POC`);
|
||||
const runtimePlane = optionalRecord(spec.runtimePlane);
|
||||
if (runtimePlane?.dockerAllowed !== false) errors.push(`${configPath}.spec.runtimePlane.dockerAllowed must be false`);
|
||||
if (runtimePlane?.buildAllowed !== false) errors.push(`${configPath}.spec.runtimePlane.buildAllowed must be false`);
|
||||
if (runtimePlane?.dockerSocketAllowed !== false) errors.push(`${configPath}.spec.runtimePlane.dockerSocketAllowed must be false`);
|
||||
const buildPlane = optionalRecord(spec.buildPlane);
|
||||
if (buildPlane?.dockerAllowed !== true) errors.push(`${configPath}.spec.buildPlane.dockerAllowed must be true for the controlled builder POC`);
|
||||
if (buildPlane?.forbidMasterServer !== true) errors.push(`${configPath}.spec.buildPlane.forbidMasterServer must be true`);
|
||||
if (buildPlane?.forbidRuntimeNode !== true) errors.push(`${configPath}.spec.buildPlane.forbidRuntimeNode must be true`);
|
||||
const reuse = optionalRecord(spec.reuse);
|
||||
if (reuse?.p0NoRegression !== true) errors.push(`${configPath}.spec.reuse.p0NoRegression must be true`);
|
||||
if (reuse?.ciConsumptionRequired !== true) errors.push(`${configPath}.spec.reuse.ciConsumptionRequired must be true`);
|
||||
const decisions = stringArray(reuse?.requiredDecisions);
|
||||
if (!decisions.includes("skipImageBuild")) errors.push(`${configPath}.spec.reuse.requiredDecisions must include skipImageBuild`);
|
||||
if (!decisions.includes("reuseEnvImage")) errors.push(`${configPath}.spec.reuse.requiredDecisions must include reuseEnvImage`);
|
||||
const targets = arrayRecords(spec.targets);
|
||||
if (targets.length === 0) errors.push(`${configPath}.spec.targets must declare at least one POC target`);
|
||||
const enabledTargets = targets.filter((target) => target.enabled !== false);
|
||||
if (enabledTargets.length === 0) warnings.push(`${configPath}.spec.targets has no enabled target`);
|
||||
if (arrayRecords(spec.componentSurvey).length === 0) warnings.push(`${configPath}.spec.componentSurvey is empty; component maturity evidence will be invisible`);
|
||||
}
|
||||
|
||||
function buildPlan(loaded: LoadedPoc, options: Options): Record<string, unknown> {
|
||||
const targets = selectedTargets(loaded, options);
|
||||
const scope = optionalRecord(loaded.spec.scope);
|
||||
return {
|
||||
ok: loaded.errors.length === 0 && targets.errors.length === 0,
|
||||
action: "plan",
|
||||
configPath: loaded.configPath,
|
||||
issue: stringOrNull(optionalRecord(loaded.root.metadata)?.issue),
|
||||
specRef: stringOrNull(optionalRecord(loaded.root.metadata)?.specRef),
|
||||
phase: stringOrNull(scope?.phase),
|
||||
rolloutEnabled: scope?.rolloutEnabled === true,
|
||||
productionFollowerReplacement: scope?.productionFollowerReplacement === true,
|
||||
sourceAuthority: compactSourceAuthority(optionalRecord(loaded.spec.sourceAuthority)),
|
||||
planes: [
|
||||
compactRuntimePlane(optionalRecord(loaded.spec.runtimePlane)),
|
||||
compactBuildPlane(optionalRecord(loaded.spec.buildPlane)),
|
||||
],
|
||||
reuse: compactReuse(optionalRecord(loaded.spec.reuse)),
|
||||
targets: targets.items.map(compactTarget),
|
||||
stages: arrayRecords(loaded.spec.stages).map(compactStage),
|
||||
componentSurvey: arrayRecords(loaded.spec.componentSurvey).map(compactComponent),
|
||||
statusProjection: compactStatusProjection(optionalRecord(loaded.spec.statusProjection)),
|
||||
budgets: loaded.spec.budgets ?? null,
|
||||
errors: [...loaded.errors, ...targets.errors],
|
||||
warnings: loaded.warnings,
|
||||
valuesRedacted: true,
|
||||
next: nextCommands(loaded, targets.items[0]),
|
||||
};
|
||||
}
|
||||
|
||||
function buildStatus(loaded: LoadedPoc, options: Options): Record<string, unknown> {
|
||||
const plan = buildPlan(loaded, options);
|
||||
const targets = arrayRecords(plan.targets);
|
||||
return {
|
||||
...plan,
|
||||
action: "status",
|
||||
statusSource: "config-only",
|
||||
statusMode: "declared-poc-not-applied",
|
||||
checks: targets.map((target) => ({
|
||||
target: target.id,
|
||||
source: "declared",
|
||||
giteaMirror: "not-applied",
|
||||
actionsRun: "not-applied",
|
||||
tekton: "existing-component",
|
||||
builderPlane: "declared-controlled-docker",
|
||||
argo: "existing-component",
|
||||
runtimePlane: "zero-docker-required",
|
||||
reuse: "p0-required-not-yet-proven-in-poc",
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function selectedTargets(loaded: LoadedPoc, options: Options): { items: Record<string, unknown>[]; errors: string[] } {
|
||||
const targets = arrayRecords(loaded.spec.targets);
|
||||
if (options.targetId === null) return { items: targets, errors: [] };
|
||||
const selected = targets.filter((target) => stringOrNull(target.id) === options.targetId);
|
||||
return selected.length > 0 ? { items: selected, errors: [] } : { items: [], errors: [`target ${options.targetId} not found in ${loaded.configPath}`] };
|
||||
}
|
||||
|
||||
function compactSourceAuthority(value: Record<string, unknown> | null): Record<string, unknown> {
|
||||
const mirror = optionalRecord(value?.giteaMirror);
|
||||
return {
|
||||
mode: stringOrNull(value?.mode),
|
||||
allowMutableBranchAsCiSource: value?.allowMutableBranchAsCiSource === true,
|
||||
allowHostWorktree: value?.allowHostWorktree === true,
|
||||
existingMirrorRef: stringOrNull(value?.existingMirrorRef),
|
||||
giteaMirrorEnabledForPoc: mirror?.enabledForPoc === true,
|
||||
giteaInternalBaseUrl: stringOrNull(mirror?.internalBaseUrl),
|
||||
snapshotRefPrefix: stringOrNull(mirror?.snapshotRefPrefix),
|
||||
repositories: arrayRecords(mirror?.repositories).map((repo) => `${repo.repository ?? "-"}@${repo.upstreamBranch ?? "-"}`),
|
||||
};
|
||||
}
|
||||
|
||||
function compactRuntimePlane(value: Record<string, unknown> | null): Record<string, unknown> {
|
||||
return {
|
||||
plane: "runtime",
|
||||
dockerAllowed: value?.dockerAllowed === true,
|
||||
buildAllowed: value?.buildAllowed === true,
|
||||
dockerSocketAllowed: value?.dockerSocketAllowed === true,
|
||||
hostWorktreeAllowed: value?.hostWorktreeAllowed === true,
|
||||
sourceAuthority: stringOrNull(value?.sourceAuthority),
|
||||
deployMode: stringOrNull(value?.deployMode),
|
||||
statusAuthority: stringArray(value?.statusAuthority),
|
||||
};
|
||||
}
|
||||
|
||||
function compactBuildPlane(value: Record<string, unknown> | null): Record<string, unknown> {
|
||||
return {
|
||||
plane: "ci-build",
|
||||
dockerAllowed: value?.dockerAllowed === true,
|
||||
buildAllowed: value?.buildAllowed === true,
|
||||
dockerScope: stringOrNull(value?.dockerScope),
|
||||
mode: stringOrNull(value?.mode),
|
||||
engineCandidates: stringArray(value?.engineCandidates),
|
||||
selectedEngineForPoc: stringOrNull(value?.selectedEngineForPoc),
|
||||
forbidMasterServer: value?.forbidMasterServer === true,
|
||||
forbidRuntimeNode: value?.forbidRuntimeNode === true,
|
||||
endpointRef: stringOrNull(optionalRecord(value?.endpoint)?.ref),
|
||||
registryRef: stringOrNull(optionalRecord(value?.registry)?.ref),
|
||||
provenanceRequired: optionalRecord(value?.provenance)?.required === true,
|
||||
provenanceFields: stringArray(optionalRecord(value?.provenance)?.fields),
|
||||
};
|
||||
}
|
||||
|
||||
function compactReuse(value: Record<string, unknown> | null): Record<string, unknown> {
|
||||
return {
|
||||
p0NoRegression: value?.p0NoRegression === true,
|
||||
sourceTruth: stringOrNull(value?.sourceTruth),
|
||||
sourceRead: stringOrNull(value?.sourceRead),
|
||||
existingParser: stringOrNull(value?.existingParser),
|
||||
existingAgentRunPlanner: stringOrNull(value?.existingAgentRunPlanner),
|
||||
ciConsumptionRequired: value?.ciConsumptionRequired === true,
|
||||
requiredDecisions: stringArray(value?.requiredDecisions),
|
||||
requiredArtifacts: stringArray(value?.requiredArtifacts),
|
||||
noRegressionChecks: stringArray(value?.noRegressionChecks),
|
||||
};
|
||||
}
|
||||
|
||||
function compactTarget(value: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
id: stringOrNull(value.id),
|
||||
enabled: value.enabled !== false,
|
||||
repository: stringOrNull(value.repository),
|
||||
branch: stringOrNull(value.branch),
|
||||
node: stringOrNull(value.node),
|
||||
lane: stringOrNull(value.lane),
|
||||
baselineSeconds: optionalRecord(value.baseline)?.currentBranchFollowerSeconds ?? null,
|
||||
budgetRef: stringOrNull(optionalRecord(value.baseline)?.budgetRef),
|
||||
snapshotRef: stringOrNull(optionalRecord(value.source)?.snapshotRef),
|
||||
mirrorReadUrlRef: stringOrNull(optionalRecord(value.source)?.currentMirrorReadUrlRef),
|
||||
workflowRef: stringOrNull(optionalRecord(value.actions)?.workflowRef),
|
||||
pipelineRef: stringOrNull(optionalRecord(value.tekton)?.pipelineRef),
|
||||
argoApplicationRef: stringOrNull(optionalRecord(value.argo)?.applicationRef),
|
||||
runtimeNamespaceRef: stringOrNull(optionalRecord(value.runtime)?.namespaceRef),
|
||||
workload: stringOrNull(optionalRecord(value.runtime)?.workload),
|
||||
healthPath: stringOrNull(optionalRecord(value.closeout)?.healthPath),
|
||||
requiredEvidence: stringArray(optionalRecord(value.closeout)?.requiredEvidence),
|
||||
};
|
||||
}
|
||||
|
||||
function compactStage(value: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
id: stringOrNull(value.id),
|
||||
owner: stringOrNull(value.owner),
|
||||
statusAuthority: stringOrNull(value.statusAuthority),
|
||||
output: stringOrNull(value.output),
|
||||
};
|
||||
}
|
||||
|
||||
function compactComponent(value: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
component: stringOrNull(value.component),
|
||||
role: stringOrNull(value.role),
|
||||
maturity: stringOrNull(value.maturity),
|
||||
directReuse: stringOrNull(value.directReuse),
|
||||
docs: stringOrNull(value.docs),
|
||||
risk: stringOrNull(value.risk),
|
||||
};
|
||||
}
|
||||
|
||||
function compactStatusProjection(value: Record<string, unknown> | null): Record<string, unknown> {
|
||||
return {
|
||||
mode: stringOrNull(value?.mode),
|
||||
defaultOutputMustNotDump: value?.defaultOutputMustNotDump === true,
|
||||
requiredFields: stringArray(value?.requiredFields),
|
||||
drillDown: optionalRecord(value?.drillDown) ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
function nextCommands(loaded: LoadedPoc, firstTarget: Record<string, unknown> | undefined): Record<string, unknown> {
|
||||
const declared = optionalRecord(loaded.spec.next);
|
||||
const targetId = stringOrNull(firstTarget?.id) ?? "agentrun-jd01-v02";
|
||||
return {
|
||||
plan: stringOrNull(declared?.plan) ?? "bun scripts/cli.ts cicd gitea-actions-poc plan",
|
||||
status: stringOrNull(declared?.status) ?? "bun scripts/cli.ts cicd gitea-actions-poc status",
|
||||
existingFollowerStatus: stringOrNull(declared?.existingFollowerStatus) ?? `bun scripts/cli.ts cicd branch-follower status --follower ${targetId}`,
|
||||
pocIssue: stringOrNull(declared?.pocIssue),
|
||||
};
|
||||
}
|
||||
|
||||
function renderPlanHuman(payload: Record<string, unknown>): string {
|
||||
const next = optionalRecord(payload.next);
|
||||
const planes = arrayRecords(payload.planes);
|
||||
const reuse = optionalRecord(payload.reuse);
|
||||
const statusProjection = optionalRecord(payload.statusProjection);
|
||||
const errors = stringArray(payload.errors);
|
||||
const warnings = stringArray(payload.warnings);
|
||||
return [
|
||||
`CI/CD GITEA-ACTIONS POC PLAN (${payload.ok === false ? "blocked" : "ok"})`,
|
||||
"",
|
||||
table(["TARGET", "SOURCE", "NODE/LANE", "BASELINE", "SNAPSHOT", "TEKTON", "ARGO", "RUNTIME"], arrayRecords(payload.targets).map((target) => [
|
||||
target.id,
|
||||
`${target.repository ?? "-"}@${target.branch ?? "-"}`,
|
||||
`${target.node ?? "-"}/${target.lane ?? "-"}`,
|
||||
target.baselineSeconds === null ? "-" : `${target.baselineSeconds}s`,
|
||||
target.snapshotRef,
|
||||
target.pipelineRef,
|
||||
target.argoApplicationRef,
|
||||
target.workload,
|
||||
])),
|
||||
"",
|
||||
"SOURCE AUTHORITY",
|
||||
sourceAuthorityLine(optionalRecord(payload.sourceAuthority)),
|
||||
"",
|
||||
table(["PLANE", "DOCKER", "BUILDS", "MODE", "ENGINE", "MASTER", "RUNTIME_NODE", "AUTHORITY"], planes.map((plane) => [
|
||||
plane.plane,
|
||||
boolText(plane.dockerAllowed),
|
||||
boolText(plane.buildAllowed),
|
||||
plane.mode ?? plane.deployMode ?? "-",
|
||||
plane.selectedEngineForPoc ?? "-",
|
||||
plane.forbidMasterServer === true ? "forbidden" : "-",
|
||||
plane.forbidRuntimeNode === true ? "forbidden" : "-",
|
||||
plane.sourceAuthority ?? plane.endpointRef ?? "-",
|
||||
])),
|
||||
"",
|
||||
"REUSE CONTRACT",
|
||||
`source=${reuse?.sourceTruth ?? "-"} p0=${boolText(reuse?.p0NoRegression)} ciConsumption=${boolText(reuse?.ciConsumptionRequired)} decisions=${stringArray(reuse?.requiredDecisions).join(",") || "-"}`,
|
||||
`artifacts=${stringArray(reuse?.requiredArtifacts).join(",") || "-"}`,
|
||||
"",
|
||||
table(["STAGE", "OWNER", "STATUS_AUTHORITY", "OUTPUT"], arrayRecords(payload.stages).map((stage) => [stage.id, stage.owner, stage.statusAuthority, stage.output])),
|
||||
"",
|
||||
table(["COMPONENT", "ROLE", "MATURITY", "DIRECT_REUSE", "RISK"], arrayRecords(payload.componentSurvey).map((component) => [component.component, component.role, component.maturity, component.directReuse, component.risk])),
|
||||
"",
|
||||
"STATUS FIELDS",
|
||||
stringArray(statusProjection?.requiredFields).join(", ") || "-",
|
||||
warnings.length === 0 ? "" : `\nWARNINGS\n${warnings.map((item) => `- ${item}`).join("\n")}`,
|
||||
errors.length === 0 ? "" : `\nERRORS\n${errors.map((item) => `- ${item}`).join("\n")}`,
|
||||
"",
|
||||
"NEXT",
|
||||
`status: ${next?.status ?? "-"}`,
|
||||
`existing-follower: ${next?.existingFollowerStatus ?? "-"}`,
|
||||
`issue: ${next?.pocIssue ?? payload.issue ?? "-"}`,
|
||||
"",
|
||||
].filter((line) => line !== "").join("\n");
|
||||
}
|
||||
|
||||
function renderStatusHuman(payload: Record<string, unknown>): string {
|
||||
const next = optionalRecord(payload.next);
|
||||
const errors = stringArray(payload.errors);
|
||||
return [
|
||||
`CI/CD GITEA-ACTIONS POC STATUS (${payload.ok === false ? "blocked" : "declared-only"})`,
|
||||
"",
|
||||
`statusSource=${payload.statusSource ?? "-"} mode=${payload.statusMode ?? "-"}`,
|
||||
"",
|
||||
table(["TARGET", "SOURCE", "GITEA", "ACTIONS", "TEKTON", "BUILDER", "ARGO", "RUNTIME", "REUSE"], arrayRecords(payload.checks).map((check) => [
|
||||
check.target,
|
||||
check.source,
|
||||
check.giteaMirror,
|
||||
check.actionsRun,
|
||||
check.tekton,
|
||||
check.builderPlane,
|
||||
check.argo,
|
||||
check.runtimePlane,
|
||||
check.reuse,
|
||||
])),
|
||||
errors.length === 0 ? "" : `\nERRORS\n${errors.map((item) => `- ${item}`).join("\n")}`,
|
||||
"",
|
||||
"NEXT",
|
||||
`plan: ${next?.plan ?? "-"}`,
|
||||
`existing-follower: ${next?.existingFollowerStatus ?? "-"}`,
|
||||
"",
|
||||
].filter((line) => line !== "").join("\n");
|
||||
}
|
||||
|
||||
function sourceAuthorityLine(value: Record<string, unknown> | null): string {
|
||||
return [
|
||||
`mode=${value?.mode ?? "-"}`,
|
||||
`mutableBranch=${boolText(value?.allowMutableBranchAsCiSource)}`,
|
||||
`hostWorktree=${boolText(value?.allowHostWorktree)}`,
|
||||
`giteaEnabled=${boolText(value?.giteaMirrorEnabledForPoc)}`,
|
||||
`snapshot=${value?.snapshotRefPrefix ?? "-"}`,
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function record(value: unknown, path: string): Record<string, unknown> {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be a YAML object`);
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function optionalRecord(value: unknown): Record<string, unknown> | null {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
function arrayRecords(value: unknown): Record<string, unknown>[] {
|
||||
return Array.isArray(value) ? value.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null && !Array.isArray(item)) : [];
|
||||
}
|
||||
|
||||
function stringArray(value: unknown): string[] {
|
||||
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.length > 0) : [];
|
||||
}
|
||||
|
||||
function stringOrNull(value: unknown): string | null {
|
||||
return typeof value === "string" && value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
function boolText(value: unknown): string {
|
||||
return value === true ? "true" : value === false ? "false" : "-";
|
||||
}
|
||||
|
||||
function table(headers: readonly string[], rows: readonly (readonly unknown[])[]): string {
|
||||
const normalized = rows.map((row) => headers.map((_, index) => cell(row[index])));
|
||||
const widths = headers.map((header, index) => Math.max(header.length, ...normalized.map((row) => row[index]?.length ?? 0)));
|
||||
const format = (row: readonly string[]) => row.map((value, index) => value.padEnd(widths[index] ?? 0)).join(" ").trimEnd();
|
||||
return [format(headers), format(headers.map((header) => "-".repeat(header.length))), ...normalized.map(format)].join("\n");
|
||||
}
|
||||
|
||||
function cell(value: unknown): string {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
const text = String(value).replace(/\s+/gu, " ");
|
||||
return text.length > 96 ? `${text.slice(0, 93)}...` : text;
|
||||
}
|
||||
+17
-4
@@ -1,13 +1,26 @@
|
||||
// SPEC: PJ2026-01060703 CI/CD branch follower draft-2026-07-03-p0-branch-follower.
|
||||
// Responsibility: thin CI/CD top-level route entry; branch-follower logic lives in responsibility modules.
|
||||
// SPEC: PJ2026-01060703 CI/CD branch follower and GH-1548 Gitea Actions POC.
|
||||
// Responsibility: thin CI/CD top-level route entry; subcommand logic lives in responsibility modules.
|
||||
import type { UniDeskConfig } from "./config";
|
||||
import { renderMachine } from "./cicd-render";
|
||||
import type { RenderedCliResult } from "./output";
|
||||
import { cicdHelp as branchFollowerHelp, runCicdCommand as runBranchFollowerCommand } from "./cicd-branch-follower";
|
||||
import { cicdGiteaActionsPocHelp, runGiteaActionsPocCommand } from "./cicd-gitea-actions-poc";
|
||||
|
||||
export function cicdHelp(): unknown {
|
||||
return branchFollowerHelp();
|
||||
return {
|
||||
command: "cicd branch-follower|gitea-actions-poc",
|
||||
output: "text by default for subcommands; top-level help is json",
|
||||
subcommands: [
|
||||
branchFollowerHelp(),
|
||||
cicdGiteaActionsPocHelp(),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export async function runCicdCommand(config: UniDeskConfig | null, args: string[]): Promise<RenderedCliResult> {
|
||||
return await runBranchFollowerCommand(config, args);
|
||||
const top = args[0];
|
||||
if (top === undefined || top === "help" || top === "--help" || top === "-h") return renderMachine("cicd", cicdHelp(), "json");
|
||||
if (top === "branch-follower") return await runBranchFollowerCommand(config, args);
|
||||
if (top === "gitea-actions-poc" || top === "gitea-builder-poc") return await runGiteaActionsPocCommand(config, args.slice(1), top);
|
||||
throw new Error("cicd usage: cicd branch-follower|gitea-actions-poc");
|
||||
}
|
||||
|
||||
+5
-2
@@ -59,6 +59,7 @@ export function rootHelp(): unknown {
|
||||
{ command: "decision show <id|docNo>", description: "Show one Decision Center record." },
|
||||
{ command: "deploy check|plan|apply [--file deploy.json|--env dev|prod] [--service id] [--commit full-sha] [--dry-run] [--force]", description: "Reconcile services from origin/master:deploy.json environments; --commit overrides one reviewed artifact consumer such as frontend for release/v1 validation or rollback. code-queue artifact consumption is dev-only." },
|
||||
{ command: "cicd branch-follower plan|apply|status|run-once|events|logs", description: "Deploy and inspect the YAML-first Kubernetes branch follower for HWLAB v0.3, AgentRun v0.2, and web-probe sentinel master without using host worktrees as source authority." },
|
||||
{ command: "cicd gitea-actions-poc plan|status", description: "Inspect the GH-1548 Gitea mirror/Actions visibility and controlled Docker/BuildKit builder-plane POC plan while keeping runtime plane 0 Docker and env reuse as a P0 no-regression contract." },
|
||||
{ command: "dev-env validate|prewarm-images", description: "Validate D601 unidesk-dev guardrails or prewarm dev foundation images into native k3s containerd through a bounded async job." },
|
||||
{ command: "artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service", description: "Manage the D601 host-managed CNCF Distribution registry and run pull-only artifact CD for supported services, including D601 direct, k3s-managed, and code-queue dev-only consumers." },
|
||||
{ command: "auth-broker contract|health --dry-run|credential-request --dry-run|pr-preflight --dry-run", description: "Inspect the P0 Rust auth broker and CLI adapter contract without reading token values, writing GitHub, or starting services." },
|
||||
@@ -734,15 +735,17 @@ function webProbeHelpSummary(): unknown {
|
||||
|
||||
function cicdHelpSummary(): unknown {
|
||||
return {
|
||||
command: "cicd branch-follower plan|apply|status|run-once|events|logs",
|
||||
command: "cicd branch-follower ... | gitea-actions-poc plan|status",
|
||||
output: "text by default; use --json, --raw, or -o json|yaml for machine output",
|
||||
usage: [
|
||||
"bun scripts/cli.ts cicd branch-follower plan",
|
||||
"bun scripts/cli.ts cicd branch-follower apply --confirm --wait",
|
||||
"bun scripts/cli.ts cicd branch-follower status",
|
||||
"bun scripts/cli.ts cicd branch-follower run-once --all --dry-run",
|
||||
"bun scripts/cli.ts cicd gitea-actions-poc plan",
|
||||
"bun scripts/cli.ts cicd gitea-actions-poc status",
|
||||
],
|
||||
description: "YAML-first Kubernetes branch follower for three CI/CD running planes, with K8s state and adapter drill-down visibility.",
|
||||
description: "YAML-first Kubernetes branch follower plus the GH-1548 read-only Gitea mirror/Actions and controlled builder-plane POC.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user