feat: add platform gitea install entry

This commit is contained in:
Codex
2026-07-05 07:04:02 +00:00
parent 2e00e749a5
commit 496b5fc729
6 changed files with 1246 additions and 4 deletions
+898
View File
@@ -0,0 +1,898 @@
import { readFileSync } from "node:fs";
import type { UniDeskConfig } from "./config";
import { rootPath } from "./config";
import type { RenderedCliResult } from "./output";
import {
capture,
compactCapture,
createYamlFieldReader,
parseJsonOutput,
readYamlRecord,
shQuote,
} from "./platform-infra-ops-library";
const configFile = rootPath("config", "platform-infra", "gitea.yaml");
const configLabel = "config/platform-infra/gitea.yaml";
const remoteScriptFile = rootPath("scripts", "src", "platform-infra-gitea-remote.sh");
const fieldManager = "unidesk-platform-infra-gitea";
const y = createYamlFieldReader(configLabel);
interface GiteaTarget {
id: string;
route: string;
namespace: string;
role: string;
enabled: boolean;
createNamespace: boolean;
storageClassName: string;
}
interface GiteaConfig {
version: number;
kind: "platform-infra-gitea";
metadata: {
id: string;
owner: string;
spec: string;
relatedIssues: number[];
};
defaults: {
targetId: string;
};
migration: {
role: string;
replaces: string;
parentConfigRef: string;
envReusePolicy: string;
buildPlane: string;
runtimePlane: string;
};
targets: GiteaTarget[];
app: {
name: string;
statefulSetName: string;
serviceName: string;
replicas: number;
image: {
repository: string;
tag: string;
pullPolicy: "Always" | "IfNotPresent" | "Never";
};
service: {
type: "ClusterIP";
httpPort: number;
sshPort: number;
};
server: {
domain: string;
rootUrl: string;
sshDomain: string;
protocol: "http" | "https";
startSshServer: boolean;
};
database: {
type: "sqlite3";
path: string;
};
actions: {
enabled: boolean;
};
registration: {
disabled: boolean;
};
storage: {
data: { size: string; mountPath: string };
config: { size: string; mountPath: string };
};
securityContext: {
runAsUser: number;
runAsGroup: number;
fsGroup: number;
};
resources: {
requests: { cpu: string; memory: string };
limits: { cpu: string; memory: string };
};
probes: {
healthPath: string;
initialDelaySeconds: number;
periodSeconds: number;
timeoutSeconds: number;
failureThreshold: number;
};
};
validation: {
waitTimeoutSeconds: number;
healthPath: string;
};
}
interface CommonOptions {
targetId: string | null;
full: boolean;
raw: boolean;
}
interface ApplyOptions extends CommonOptions {
confirm: boolean;
dryRun: boolean;
wait: boolean;
}
export async function runPlatformInfraGiteaCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
const [action = "plan"] = args;
if (action === "help" || action === "--help") return giteaHelp();
if (action === "plan") {
const options = parseCommonOptions(args.slice(1));
const result = plan(options);
return options.full || options.raw ? result : renderPlan(result);
}
if (action === "apply") {
const options = parseApplyOptions(args.slice(1));
const result = await apply(config, options);
return options.full || options.raw ? result : renderApply(result);
}
if (action === "status") {
const options = parseCommonOptions(args.slice(1));
const result = await status(config, options);
return options.full || options.raw ? result : renderStatus(result);
}
if (action === "validate") return await validate(config, parseCommonOptions(args.slice(1)));
return {
ok: false,
error: "unsupported-platform-infra-gitea-command",
args,
help: giteaHelp(),
};
}
export function giteaHelp(): Record<string, unknown> {
return {
command: "platform-infra gitea plan|apply|status|validate",
configTruth: configLabel,
usage: [
"bun scripts/cli.ts platform-infra gitea plan --target JD01",
"bun scripts/cli.ts platform-infra gitea apply --target JD01 --dry-run",
"bun scripts/cli.ts platform-infra gitea apply --target JD01 --confirm",
"bun scripts/cli.ts platform-infra gitea status --target JD01 [--full|--raw]",
"bun scripts/cli.ts platform-infra gitea validate --target JD01 [--full|--raw]",
],
boundary: "Gitea is installed as an internal ClusterIP source-authority service for GH-1548/GH-1549; runner registration and repository mirror bootstrap are later controlled stages.",
};
}
function readGiteaConfig(): GiteaConfig {
const root = readYamlRecord<Record<string, unknown>>(configFile, "platform-infra-gitea");
const version = y.integerField(root, "version", "");
if (version !== 1) throw new Error(`${configLabel}.version must be 1`);
const metadata = y.objectField(root, "metadata", "");
const defaults = y.objectField(root, "defaults", "");
const migration = y.objectField(root, "migration", "");
const app = y.objectField(root, "app", "");
const image = y.objectField(app, "image", "app");
const service = y.objectField(app, "service", "app");
const server = y.objectField(app, "server", "app");
const database = y.objectField(app, "database", "app");
const actions = y.objectField(app, "actions", "app");
const registration = y.objectField(app, "registration", "app");
const storage = y.objectField(app, "storage", "app");
const dataStorage = y.objectField(storage, "data", "app.storage");
const configStorage = y.objectField(storage, "config", "app.storage");
const securityContext = y.objectField(app, "securityContext", "app");
const resources = y.objectField(app, "resources", "app");
const requests = y.objectField(resources, "requests", "app.resources");
const limits = y.objectField(resources, "limits", "app.resources");
const probes = y.objectField(app, "probes", "app");
const validation = y.objectField(root, "validation", "");
const parsed: GiteaConfig = {
version,
kind: "platform-infra-gitea",
metadata: {
id: y.stringField(metadata, "id", "metadata"),
owner: y.stringField(metadata, "owner", "metadata"),
spec: y.stringField(metadata, "spec", "metadata"),
relatedIssues: y.numberArrayField(metadata, "relatedIssues", "metadata"),
},
defaults: {
targetId: y.stringField(defaults, "targetId", "defaults"),
},
migration: {
role: y.stringField(migration, "role", "migration"),
replaces: y.stringField(migration, "replaces", "migration"),
parentConfigRef: y.stringField(migration, "parentConfigRef", "migration"),
envReusePolicy: y.stringField(migration, "envReusePolicy", "migration"),
buildPlane: y.stringField(migration, "buildPlane", "migration"),
runtimePlane: y.stringField(migration, "runtimePlane", "migration"),
},
targets: y.arrayOfRecords(root.targets, "targets").map(parseTarget),
app: {
name: y.kubernetesNameField(app, "name", "app"),
statefulSetName: y.kubernetesNameField(app, "statefulSetName", "app"),
serviceName: y.kubernetesNameField(app, "serviceName", "app"),
replicas: positiveInteger(app, "replicas", "app"),
image: {
repository: y.stringField(image, "repository", "app.image"),
tag: y.stringField(image, "tag", "app.image"),
pullPolicy: y.enumField(image, "pullPolicy", "app.image", ["Always", "IfNotPresent", "Never"] as const),
},
service: {
type: y.enumField(service, "type", "app.service", ["ClusterIP"] as const),
httpPort: y.portField(service, "httpPort", "app.service"),
sshPort: y.portField(service, "sshPort", "app.service"),
},
server: {
domain: y.hostField(server, "domain", "app.server"),
rootUrl: urlField(server, "rootUrl", "app.server"),
sshDomain: y.hostField(server, "sshDomain", "app.server"),
protocol: y.enumField(server, "protocol", "app.server", ["http", "https"] as const),
startSshServer: y.booleanField(server, "startSshServer", "app.server"),
},
database: {
type: y.enumField(database, "type", "app.database", ["sqlite3"] as const),
path: y.absolutePathField(database, "path", "app.database"),
},
actions: {
enabled: y.booleanField(actions, "enabled", "app.actions"),
},
registration: {
disabled: y.booleanField(registration, "disabled", "app.registration"),
},
storage: {
data: { size: quantity(dataStorage, "size", "app.storage.data"), mountPath: y.absolutePathField(dataStorage, "mountPath", "app.storage.data") },
config: { size: quantity(configStorage, "size", "app.storage.config"), mountPath: y.absolutePathField(configStorage, "mountPath", "app.storage.config") },
},
securityContext: {
runAsUser: positiveInteger(securityContext, "runAsUser", "app.securityContext"),
runAsGroup: positiveInteger(securityContext, "runAsGroup", "app.securityContext"),
fsGroup: positiveInteger(securityContext, "fsGroup", "app.securityContext"),
},
resources: {
requests: { cpu: y.stringField(requests, "cpu", "app.resources.requests"), memory: quantity(requests, "memory", "app.resources.requests") },
limits: { cpu: y.stringField(limits, "cpu", "app.resources.limits"), memory: quantity(limits, "memory", "app.resources.limits") },
},
probes: {
healthPath: y.apiPathField(probes, "healthPath", "app.probes"),
initialDelaySeconds: positiveInteger(probes, "initialDelaySeconds", "app.probes"),
periodSeconds: positiveInteger(probes, "periodSeconds", "app.probes"),
timeoutSeconds: positiveInteger(probes, "timeoutSeconds", "app.probes"),
failureThreshold: positiveInteger(probes, "failureThreshold", "app.probes"),
},
},
validation: {
waitTimeoutSeconds: boundedTimeout(validation, "waitTimeoutSeconds", "validation"),
healthPath: y.apiPathField(validation, "healthPath", "validation"),
},
};
validateConfig(parsed);
return parsed;
}
function parseTarget(record: Record<string, unknown>, index: number): GiteaTarget {
const path = `targets[${index}]`;
return {
id: y.stringField(record, "id", path),
route: y.stringField(record, "route", path),
namespace: y.kubernetesNameField(record, "namespace", path),
role: y.stringField(record, "role", path),
enabled: y.booleanField(record, "enabled", path),
createNamespace: y.booleanField(record, "createNamespace", path),
storageClassName: y.stringField(record, "storageClassName", path),
};
}
function validateConfig(gitea: GiteaConfig): void {
resolveTarget(gitea, gitea.defaults.targetId);
if (!/^docker\.gitea\.com\/gitea$/u.test(gitea.app.image.repository)) throw new Error(`${configLabel}.app.image.repository must use the official Gitea image registry`);
if (!/-rootless$/u.test(gitea.app.image.tag)) throw new Error(`${configLabel}.app.image.tag must use a rootless Gitea image`);
if (gitea.app.service.type !== "ClusterIP") throw new Error(`${configLabel}.app.service.type must stay ClusterIP`);
if (!gitea.app.actions.enabled) throw new Error(`${configLabel}.app.actions.enabled must be true for GH-1548/GH-1549`);
if (!gitea.app.registration.disabled) throw new Error(`${configLabel}.app.registration.disabled must be true for the internal POC service`);
if (gitea.app.probes.healthPath !== gitea.validation.healthPath) throw new Error(`${configLabel}.app.probes.healthPath must match validation.healthPath`);
}
function resolveTarget(gitea: GiteaConfig, targetId: string | null): GiteaTarget {
const resolved = targetId ?? gitea.defaults.targetId;
const target = gitea.targets.find((item) => item.id.toLowerCase() === resolved.toLowerCase());
if (target === undefined) throw new Error(`unknown gitea target ${resolved}; known targets: ${gitea.targets.map((item) => item.id).join(", ")}`);
if (!target.enabled) throw new Error(`gitea target ${target.id} is disabled in ${configLabel}`);
return target;
}
function plan(options: CommonOptions): Record<string, unknown> {
const gitea = readGiteaConfig();
const target = resolveTarget(gitea, options.targetId);
const manifest = renderManifest(gitea, target);
const policy = policyChecks(gitea, target, manifest);
return {
ok: policy.every((check) => check.ok),
action: "platform-infra-gitea-plan",
mutation: false,
config: configSummary(gitea, target),
renderPlan: {
target: targetSummary(target),
objects: manifestObjectSummary(manifest),
},
policy,
next: nextCommands(target.id),
};
}
async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Record<string, unknown>> {
const gitea = readGiteaConfig();
const target = resolveTarget(gitea, options.targetId);
const manifest = renderManifest(gitea, target);
const policy = policyChecks(gitea, target, manifest);
if (!policy.every((check) => check.ok)) return { ok: false, action: "platform-infra-gitea-apply", mode: "policy-blocked", mutation: false, policy };
const result = await capture(config, target.route, ["sh"], remoteScript("apply", gitea, target, manifest, options));
const parsed = parseJsonOutput(result.stdout);
return {
ok: result.exitCode === 0 && parsed?.ok === true,
action: "platform-infra-gitea-apply",
mode: options.dryRun ? "dry-run" : "confirmed",
mutation: !options.dryRun,
target: targetSummary(target),
config: compactConfigSummary(gitea, target),
policy,
remote: parsed ?? compactCapture(result, { full: true }),
next: nextCommands(target.id),
};
}
async function status(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
const gitea = readGiteaConfig();
const target = resolveTarget(gitea, options.targetId);
const result = await capture(config, target.route, ["sh"], remoteScript("status", gitea, target, "", { ...options, confirm: false, dryRun: true, wait: false }));
const parsed = parseJsonOutput(result.stdout);
const summary = parsed === null ? null : statusSummary(parsed);
return {
ok: result.exitCode === 0 && summary?.ready === true,
action: "platform-infra-gitea-status",
mutation: false,
target: targetSummary(target),
config: configSummary(gitea, target),
summary,
remote: options.raw ? parsed : options.full ? parsed : summary ?? compactCapture(result, { full: true }),
next: {
apply: `bun scripts/cli.ts platform-infra gitea apply --target ${target.id} --confirm`,
validate: `bun scripts/cli.ts platform-infra gitea validate --target ${target.id}`,
},
};
}
async function validate(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
const gitea = readGiteaConfig();
const target = resolveTarget(gitea, options.targetId);
const result = await capture(config, target.route, ["sh"], remoteScript("validate", gitea, target, "", { ...options, confirm: false, dryRun: true, wait: false }));
const parsed = parseJsonOutput(result.stdout);
return {
ok: result.exitCode === 0 && parsed?.ok === true,
action: "platform-infra-gitea-validate",
mutation: false,
target: targetSummary(target),
config: compactConfigSummary(gitea, target),
validation: parsed ?? null,
remote: options.raw && parsed !== null ? parsed : compactCapture(result, { full: options.full || result.exitCode !== 0 }),
};
}
function renderManifest(gitea: GiteaConfig, target: GiteaTarget): string {
const app = gitea.app;
const image = `${app.image.repository}:${app.image.tag}`;
const labels = ` app.kubernetes.io/name: ${app.name}
app.kubernetes.io/component: gitea
app.kubernetes.io/part-of: devops-infra
app.kubernetes.io/managed-by: unidesk`;
return `apiVersion: v1
kind: Namespace
metadata:
name: ${target.namespace}
labels:
app.kubernetes.io/name: devops-infra
app.kubernetes.io/managed-by: unidesk
unidesk.ai/runtime-node: ${target.id}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-all
namespace: ${target.namespace}
labels:
${labels}
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- {}
egress:
- {}
---
apiVersion: v1
kind: Service
metadata:
name: ${app.serviceName}
namespace: ${target.namespace}
labels:
${labels}
annotations:
unidesk.ai/spec: ${yamlQuote(gitea.metadata.spec)}
unidesk.ai/parent-config-ref: ${yamlQuote(gitea.migration.parentConfigRef)}
spec:
type: ${app.service.type}
selector:
app.kubernetes.io/name: ${app.name}
app.kubernetes.io/component: gitea
ports:
- name: http
port: ${app.service.httpPort}
targetPort: http
protocol: TCP
- name: ssh
port: ${app.service.sshPort}
targetPort: ssh
protocol: TCP
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ${app.statefulSetName}
namespace: ${target.namespace}
labels:
${labels}
annotations:
unidesk.ai/spec: ${yamlQuote(gitea.metadata.spec)}
unidesk.ai/env-reuse-policy: ${yamlQuote(gitea.migration.envReusePolicy)}
spec:
serviceName: ${app.serviceName}
replicas: ${app.replicas}
selector:
matchLabels:
app.kubernetes.io/name: ${app.name}
app.kubernetes.io/component: gitea
template:
metadata:
labels:
app.kubernetes.io/name: ${app.name}
app.kubernetes.io/component: gitea
app.kubernetes.io/part-of: devops-infra
app.kubernetes.io/managed-by: unidesk
annotations:
unidesk.ai/runtime-plane: ${yamlQuote(gitea.migration.runtimePlane)}
unidesk.ai/build-plane: ${yamlQuote(gitea.migration.buildPlane)}
spec:
securityContext:
runAsUser: ${app.securityContext.runAsUser}
runAsGroup: ${app.securityContext.runAsGroup}
fsGroup: ${app.securityContext.fsGroup}
fsGroupChangePolicy: OnRootMismatch
containers:
- name: gitea
image: ${image}
imagePullPolicy: ${app.image.pullPolicy}
ports:
- name: http
containerPort: ${app.service.httpPort}
- name: ssh
containerPort: ${app.service.sshPort}
env:
${envVars(gitea, target)}
readinessProbe:
httpGet:
path: ${app.probes.healthPath}
port: http
initialDelaySeconds: ${app.probes.initialDelaySeconds}
periodSeconds: ${app.probes.periodSeconds}
timeoutSeconds: ${app.probes.timeoutSeconds}
failureThreshold: ${app.probes.failureThreshold}
livenessProbe:
httpGet:
path: ${app.probes.healthPath}
port: http
initialDelaySeconds: ${app.probes.initialDelaySeconds}
periodSeconds: ${app.probes.periodSeconds}
timeoutSeconds: ${app.probes.timeoutSeconds}
failureThreshold: ${app.probes.failureThreshold}
resources:
requests:
cpu: ${yamlQuote(app.resources.requests.cpu)}
memory: ${yamlQuote(app.resources.requests.memory)}
limits:
cpu: ${yamlQuote(app.resources.limits.cpu)}
memory: ${yamlQuote(app.resources.limits.memory)}
volumeMounts:
- name: data
mountPath: ${app.storage.data.mountPath}
- name: config
mountPath: ${app.storage.config.mountPath}
volumeClaimTemplates:
- metadata:
name: data
labels:
app.kubernetes.io/name: ${app.name}
app.kubernetes.io/component: gitea
app.kubernetes.io/part-of: devops-infra
app.kubernetes.io/managed-by: unidesk
spec:
accessModes:
- ReadWriteOnce
storageClassName: ${target.storageClassName}
resources:
requests:
storage: ${app.storage.data.size}
- metadata:
name: config
labels:
app.kubernetes.io/name: ${app.name}
app.kubernetes.io/component: gitea
app.kubernetes.io/part-of: devops-infra
app.kubernetes.io/managed-by: unidesk
spec:
accessModes:
- ReadWriteOnce
storageClassName: ${target.storageClassName}
resources:
requests:
storage: ${app.storage.config.size}
`;
}
function envVars(gitea: GiteaConfig, target: GiteaTarget): string {
const app = gitea.app;
const values: Record<string, string> = {
GITEA_WORK_DIR: app.storage.data.mountPath,
GITEA__security__INSTALL_LOCK: "true",
GITEA__server__PROTOCOL: app.server.protocol,
GITEA__server__DOMAIN: app.server.domain,
GITEA__server__ROOT_URL: app.server.rootUrl,
GITEA__server__HTTP_ADDR: "0.0.0.0",
GITEA__server__HTTP_PORT: String(app.service.httpPort),
GITEA__server__SSH_DOMAIN: app.server.sshDomain,
GITEA__server__SSH_PORT: String(app.service.sshPort),
GITEA__server__START_SSH_SERVER: app.server.startSshServer ? "true" : "false",
GITEA__database__DB_TYPE: app.database.type,
GITEA__database__PATH: app.database.path,
GITEA__repository__ROOT: `${app.storage.data.mountPath}/git/repositories`,
GITEA__actions__ENABLED: app.actions.enabled ? "true" : "false",
GITEA__service__DISABLE_REGISTRATION: app.registration.disabled ? "true" : "false",
GITEA__log__LEVEL: "Info",
UNIDESK_GITEA_TARGET: target.id,
};
return Object.entries(values).map(([name, value]) => ` - name: ${name}
value: ${yamlQuote(value)}`).join("\n");
}
function remoteScript(action: "apply" | "status" | "validate", gitea: GiteaConfig, target: GiteaTarget, manifest: string, options: ApplyOptions): string {
const env: Record<string, string> = {
UNIDESK_GITEA_ACTION: action,
UNIDESK_GITEA_TARGET_ID: target.id,
UNIDESK_GITEA_ROUTE: target.route,
UNIDESK_GITEA_NAMESPACE: target.namespace,
UNIDESK_GITEA_APP_NAME: gitea.app.name,
UNIDESK_GITEA_STATEFULSET_NAME: gitea.app.statefulSetName,
UNIDESK_GITEA_SERVICE_NAME: gitea.app.serviceName,
UNIDESK_GITEA_HTTP_PORT: String(gitea.app.service.httpPort),
UNIDESK_GITEA_HEALTH_PATH: gitea.validation.healthPath,
UNIDESK_GITEA_IMAGE: `${gitea.app.image.repository}:${gitea.app.image.tag}`,
UNIDESK_GITEA_FIELD_MANAGER: fieldManager,
UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS: String(gitea.validation.waitTimeoutSeconds),
UNIDESK_GITEA_DRY_RUN: options.dryRun ? "1" : "0",
UNIDESK_GITEA_WAIT: options.wait ? "1" : "0",
UNIDESK_GITEA_FULL: options.full ? "1" : "0",
UNIDESK_GITEA_MANIFEST_B64: Buffer.from(manifest, "utf8").toString("base64"),
};
const exports = Object.entries(env).map(([key, value]) => `export ${key}=${shQuote(value)}`).join("\n");
return `${exports}\n${readFileSync(remoteScriptFile, "utf8")}`;
}
function policyChecks(gitea: GiteaConfig, target: GiteaTarget, manifest: string): Array<Record<string, unknown>> {
return [
{ name: "yaml-source-of-truth", ok: true, detail: "Gitea target, namespace, image, storage, ports and probes are read from config/platform-infra/gitea.yaml." },
{ name: "gh-1548-service-contract", ok: target.namespace === "devops-infra" && gitea.app.serviceName === "gitea-http", detail: "The service matches config/cicd-gitea-actions-poc.yaml sourceAuthority.giteaMirror." },
{ name: "cluster-internal-only", ok: !/^\s*type:\s*(NodePort|LoadBalancer)\s*$/mu.test(manifest) && !/^\s*kind:\s*Ingress\s*$/mu.test(manifest), detail: "Gitea is ClusterIP-only for the POC." },
{ name: "runtime-zero-docker", ok: !manifest.includes("/var/run/docker.sock") && !/^\s*hostPath:\s*$/mu.test(manifest), detail: "Runtime Gitea does not mount Docker socket or hostPath." },
{ name: "rootless-image", ok: /-rootless$/u.test(gitea.app.image.tag), detail: "The runtime image is Gitea rootless." },
{ name: "actions-enabled", ok: gitea.app.actions.enabled, detail: "Gitea Actions is enabled; runner registration is a later controlled stage." },
{ name: "env-reuse-preserved", ok: gitea.migration.envReusePolicy === "preserve-existing-runtime-env-reuse", detail: "This install does not replace the existing env reuse path or runtime deployment." },
];
}
function configSummary(gitea: GiteaConfig, target: GiteaTarget): Record<string, unknown> {
return {
path: configLabel,
metadata: gitea.metadata,
migration: gitea.migration,
target: targetSummary(target),
app: appSummary(gitea),
valuesPrinted: false,
};
}
function compactConfigSummary(gitea: GiteaConfig, target: GiteaTarget): Record<string, unknown> {
return {
path: configLabel,
target: targetSummary(target),
app: {
image: `${gitea.app.image.repository}:${gitea.app.image.tag}`,
serviceDns: serviceDns(gitea, target),
rootUrl: gitea.app.server.rootUrl,
actionsEnabled: gitea.app.actions.enabled,
},
valuesPrinted: false,
};
}
function targetSummary(target: GiteaTarget): Record<string, unknown> {
return {
id: target.id,
route: target.route,
namespace: target.namespace,
role: target.role,
createNamespace: target.createNamespace,
storageClassName: target.storageClassName,
};
}
function appSummary(gitea: GiteaConfig): Record<string, unknown> {
return {
name: gitea.app.name,
statefulSetName: gitea.app.statefulSetName,
serviceName: gitea.app.serviceName,
image: `${gitea.app.image.repository}:${gitea.app.image.tag}`,
replicas: gitea.app.replicas,
service: gitea.app.service,
rootUrl: gitea.app.server.rootUrl,
actionsEnabled: gitea.app.actions.enabled,
registrationDisabled: gitea.app.registration.disabled,
storage: gitea.app.storage,
healthPath: gitea.validation.healthPath,
};
}
function statusSummary(payload: Record<string, unknown>): Record<string, unknown> {
return {
ready: payload.ready === true,
target: payload.target,
route: payload.route,
namespace: payload.namespace,
image: payload.image,
serviceDns: payload.serviceDns,
networkPolicy: payload.networkPolicy,
statefulSet: payload.statefulSet,
service: payload.service,
endpointsReady: payload.endpointsReady === true,
pods: Array.isArray(payload.pods) ? payload.pods : [],
pvcs: Array.isArray(payload.pvcs) ? payload.pvcs : [],
eventsTail: Array.isArray(payload.eventsTail) ? payload.eventsTail : [],
valuesPrinted: false,
};
}
function renderPlan(result: Record<string, unknown>): RenderedCliResult {
const config = record(result.config);
const target = record(config.target);
const app = record(config.app);
const migration = record(config.migration);
const policy = arrayRecords(result.policy);
const failed = policy.filter((item) => item.ok === false);
const next = record(result.next);
return rendered(result, "platform-infra gitea plan", [
"PLATFORM-INFRA GITEA PLAN",
...table(["FIELD", "VALUE", "DETAIL", "VALUE"], [
["TARGET", stringValue(target.id), "route", stringValue(target.route)],
["NAMESPACE", stringValue(target.namespace), "role", stringValue(target.role)],
["IMAGE", stringValue(app.image), "replicas", stringValue(app.replicas)],
["SERVICE", `${stringValue(app.serviceName)}:${stringValue(record(app.service).httpPort)}`, "dns", serviceDnsFromObjects(app, target)],
["ACTIONS", boolText(app.actionsEnabled), "registrationDisabled", boolText(app.registrationDisabled)],
["MIGRATION", stringValue(migration.role), "replaces", stringValue(migration.replaces)],
["POLICY", failed.length === 0 ? "ok" : `failed=${failed.length}`, "valuesPrinted", "false"],
]),
"",
"NEXT",
` dry-run: ${stringValue(next.dryRun)}`,
` apply: ${stringValue(next.apply)}`,
` status: ${stringValue(next.status)}`,
` validate: ${stringValue(next.validate)}`,
"",
"Boundary: Gitea is internal ClusterIP source authority for GH-1548/GH-1549; runner and mirror repo bootstrap are separate controlled stages.",
"Disclosure: Secret values are not printed; this stage does not create runner credentials.",
]);
}
function renderApply(result: Record<string, unknown>): RenderedCliResult {
const target = record(result.target);
const remote = record(result.remote);
const steps = record(remote.steps);
const applyStep = record(steps.apply);
const rolloutStep = record(steps.rollout);
return rendered(result, "platform-infra gitea apply", [
"PLATFORM-INFRA GITEA APPLY",
...table(["TARGET", "NAMESPACE", "MODE", "OK"], [[stringValue(target.id), stringValue(target.namespace), stringValue(result.mode), boolText(result.ok)]]),
"",
"STEPS",
...table(["STEP", "EXIT", "DETAIL"], [
["apply", stringValue(applyStep.exitCode), compactLine(stringValue(applyStep.stderrTail, stringValue(applyStep.stdoutTail)))],
["rollout", stringValue(rolloutStep.exitCode), compactLine(stringValue(rolloutStep.stderrTail, stringValue(rolloutStep.stdoutTail)))],
]),
...remoteErrorLines(result),
"",
`NEXT bun scripts/cli.ts platform-infra gitea status --target ${stringValue(target.id)}`,
]);
}
function renderStatus(result: Record<string, unknown>): RenderedCliResult {
const summary = record(result.summary);
const statefulSet = record(summary.statefulSet);
const service = record(summary.service);
const networkPolicy = record(summary.networkPolicy);
const pods = arrayRecords(summary.pods).map((pod) => [stringValue(pod.name), stringValue(pod.phase), boolText(pod.ready), stringValue(pod.restarts)]);
const pvcs = arrayRecords(summary.pvcs).map((pvc) => [stringValue(pvc.name), stringValue(pvc.phase), stringValue(pvc.capacity)]);
return rendered(result, "platform-infra gitea status", [
"PLATFORM-INFRA GITEA STATUS",
...table(["TARGET", "ROUTE", "NAMESPACE", "READY"], [[stringValue(summary.target), stringValue(summary.route), stringValue(summary.namespace), boolText(summary.ready)]]),
"",
"CONTROL",
...table(["CHECK", "VALUE", "DETAIL"], [
["statefulSet", `${stringValue(statefulSet.readyReplicas)}/${stringValue(statefulSet.desired)}`, stringValue(statefulSet.name)],
["service", stringValue(service.type), stringValue(summary.serviceDns)],
["endpoints", boolText(summary.endpointsReady), stringValue(service.clusterIP)],
["allow-all", boolText(networkPolicy.allowAllPresent), "NetworkPolicy"],
]),
"",
"PODS",
...(pods.length === 0 ? ["-"] : table(["POD", "PHASE", "READY", "RESTARTS"], pods)),
"",
"PVCS",
...(pvcs.length === 0 ? ["-"] : table(["PVC", "PHASE", "CAPACITY"], pvcs)),
...remoteErrorLines(result),
"",
`NEXT bun scripts/cli.ts platform-infra gitea validate --target ${stringValue(summary.target)}`,
]);
}
function remoteErrorLines(result: Record<string, unknown>): string[] {
if (result.ok !== false) return [];
const remote = record(result.remote);
const exitCode = stringValue(remote.exitCode);
const stderr = compactLine(stringValue(remote.stderrTail));
const stdout = compactLine(stringValue(remote.stdoutTail));
const detail = stderr !== "-" ? stderr : stdout;
return ["", "ERROR", ...table(["EXIT", "DETAIL"], [[exitCode, detail]])];
}
function rendered(result: Record<string, unknown>, command: string, lines: string[]): RenderedCliResult {
return { ok: result.ok !== false, command, renderedText: lines.join("\n"), contentType: "text/plain" };
}
function parseApplyOptions(args: string[]): ApplyOptions {
const commonArgs: string[] = [];
let confirm = false;
let dryRun = false;
let wait = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--confirm") confirm = true;
else if (arg === "--dry-run") dryRun = true;
else if (arg === "--wait") wait = true;
else {
commonArgs.push(arg);
if (arg === "--target" || arg === "--node") {
commonArgs.push(args[index + 1] ?? "");
index += 1;
}
}
}
if (confirm && dryRun) throw new Error("gitea apply accepts only one of --confirm or --dry-run");
return { ...parseCommonOptions(commonArgs), confirm, dryRun: dryRun || !confirm, wait };
}
function parseCommonOptions(args: string[]): CommonOptions {
let targetId: string | null = null;
let full = false;
let raw = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--target" || arg === "--node") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${arg} must be a simple target id`);
targetId = value;
index += 1;
} else if (arg === "--full") {
full = true;
} else if (arg === "--raw") {
raw = true;
full = true;
} else {
throw new Error(`unsupported gitea option: ${arg}`);
}
}
return { targetId, full, raw };
}
function nextCommands(targetId: string): Record<string, string> {
return {
dryRun: `bun scripts/cli.ts platform-infra gitea apply --target ${targetId} --dry-run`,
apply: `bun scripts/cli.ts platform-infra gitea apply --target ${targetId} --confirm`,
status: `bun scripts/cli.ts platform-infra gitea status --target ${targetId}`,
validate: `bun scripts/cli.ts platform-infra gitea validate --target ${targetId}`,
};
}
function manifestObjectSummary(yaml: string): Array<Record<string, unknown>> {
const objects: Array<Record<string, unknown>> = [];
for (const doc of yaml.split(/^---$/mu)) {
const kind = doc.match(/^\s*kind:\s*([A-Za-z0-9._-]+)\s*$/mu)?.[1];
const name = doc.match(/^\s*name:\s*([A-Za-z0-9._-]+)\s*$/mu)?.[1];
const namespace = doc.match(/^\s*namespace:\s*([A-Za-z0-9._-]+)\s*$/mu)?.[1] ?? null;
if (kind !== undefined && name !== undefined) objects.push({ kind, name, namespace });
}
return objects;
}
function serviceDns(gitea: GiteaConfig, target: GiteaTarget): string {
return `${gitea.app.serviceName}.${target.namespace}.svc.cluster.local:${gitea.app.service.httpPort}`;
}
function serviceDnsFromObjects(app: Record<string, unknown>, target: Record<string, unknown>): string {
const service = record(app.service);
return `${stringValue(app.serviceName)}.${stringValue(target.namespace)}.svc.cluster.local:${stringValue(service.httpPort)}`;
}
function positiveInteger(obj: Record<string, unknown>, key: string, path: string): number {
const value = y.integerField(obj, key, path);
if (value < 1) throw new Error(`${configLabel}.${path}.${key} must be positive`);
return value;
}
function boundedTimeout(obj: Record<string, unknown>, key: string, path: string): number {
const value = positiveInteger(obj, key, path);
if (value > 55) throw new Error(`${configLabel}.${path}.${key} must fit the 60s trans short-connection budget`);
return value;
}
function quantity(obj: Record<string, unknown>, key: string, path: string): string {
const value = y.stringField(obj, key, path);
if (!/^[0-9]+(?:m|Ki|Mi|Gi|Ti)?$/u.test(value)) throw new Error(`${configLabel}.${path}.${key} must be a Kubernetes quantity`);
return value;
}
function urlField(obj: Record<string, unknown>, key: string, path: string): string {
const value = y.stringField(obj, key, path);
const parsed = new URL(value);
if (!["http:", "https:"].includes(parsed.protocol) || parsed.search || parsed.hash) throw new Error(`${configLabel}.${path}.${key} must be an http(s) URL without query or hash`);
return value;
}
function yamlQuote(value: string): string {
return JSON.stringify(value);
}
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function arrayRecords(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item)) as Record<string, unknown>[] : [];
}
function stringValue(value: unknown, fallback = "-"): string {
if (typeof value === "string" && value.length > 0) return value;
if (typeof value === "number" || typeof value === "boolean") return String(value);
return fallback;
}
function boolText(value: unknown): string {
return value === true ? "true" : "false";
}
function compactLine(value: string): string {
const trimmed = value.replace(/\s+/gu, " ").trim();
return trimmed.length > 0 ? trimmed.slice(0, 220) : "-";
}
function table(headers: string[], rows: string[][]): string[] {
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length)));
const format = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ");
return [format(headers), format(headers.map((header, index) => "-".repeat(Math.max(header.length, widths[index])))), ...rows.map(format)];
}