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
+85
View File
@@ -0,0 +1,85 @@
version: 1
kind: platform-infra-gitea
metadata:
id: gitea-internal-mirror
owner: unidesk
spec: GH-1548
relatedIssues:
- 1548
- 1549
defaults:
targetId: JD01
migration:
role: gitea-actions-driven-cicd-source-authority
replaces: branch-follower-self-maintained-branch-observer
parentConfigRef: config/cicd-gitea-actions-poc.yaml#spec.sourceAuthority.giteaMirror
envReusePolicy: preserve-existing-runtime-env-reuse
buildPlane: controlled-docker-or-buildkit-outside-runtime
runtimePlane: k3s-gitea-service-zero-docker
targets:
- id: JD01
route: JD01:k3s
namespace: devops-infra
role: active-poc
enabled: true
createNamespace: true
storageClassName: local-path
app:
name: gitea
statefulSetName: gitea
serviceName: gitea-http
replicas: 1
image:
repository: docker.gitea.com/gitea
tag: 1.26.4-rootless
pullPolicy: IfNotPresent
service:
type: ClusterIP
httpPort: 3000
sshPort: 2222
server:
domain: gitea-http.devops-infra.svc.cluster.local
rootUrl: http://gitea-http.devops-infra.svc.cluster.local:3000/
sshDomain: gitea-http.devops-infra.svc.cluster.local
protocol: http
startSshServer: true
database:
type: sqlite3
path: /var/lib/gitea/gitea.db
actions:
enabled: true
registration:
disabled: true
storage:
data:
size: 8Gi
mountPath: /var/lib/gitea
config:
size: 1Gi
mountPath: /etc/gitea
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
probes:
healthPath: /api/healthz
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 12
validation:
waitTimeoutSeconds: 55
healthPath: /api/healthz
+6 -2
View File
@@ -661,7 +661,7 @@ function agentRunHelpSummary(): unknown {
function platformInfraHelpSummary(): unknown { function platformInfraHelpSummary(): unknown {
return { return {
command: "platform-infra sub2api|egress-proxy|langbot|n8n|wechat-archive ...", command: "platform-infra sub2api|egress-proxy|langbot|n8n|wechat-archive|gitea ...",
output: "json", output: "json",
usage: [ usage: [
"bun scripts/cli.ts platform-infra sub2api plan", "bun scripts/cli.ts platform-infra sub2api plan",
@@ -689,8 +689,12 @@ function platformInfraHelpSummary(): unknown {
"bun scripts/cli.ts platform-infra wechat-archive pull --remote-path /UniDesk/WeChatArchive/...", "bun scripts/cli.ts platform-infra wechat-archive pull --remote-path /UniDesk/WeChatArchive/...",
"bun scripts/cli.ts platform-infra wechat-archive wcf-host-status --full", "bun scripts/cli.ts platform-infra wechat-archive wcf-host-status --full",
"bun scripts/cli.ts platform-infra wechat-archive collector-status --full", "bun scripts/cli.ts platform-infra wechat-archive collector-status --full",
"bun scripts/cli.ts platform-infra gitea plan --target JD01",
"bun scripts/cli.ts platform-infra gitea apply --target JD01 --confirm",
"bun scripts/cli.ts platform-infra gitea status --target JD01",
"bun scripts/cli.ts platform-infra gitea validate --target JD01",
], ],
description: "Operate G14 platform-infra services such as Sub2API, shared egress-proxy benchmarks, LangBot, n8n, WeChat archive workflows, and the YAML-controlled Codex pool.", description: "Operate platform-infra services such as Sub2API, shared egress-proxy benchmarks, LangBot, n8n, WeChat archive workflows, the YAML-controlled Codex pool, and internal Gitea for the GH-1548/GH-1549 CI/CD migration.",
}; };
} }
+246
View File
@@ -0,0 +1,246 @@
#!/bin/sh
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
json_tail() {
name="$1"
limit="${2:-2000}"
python3 - "$tmp/$name" "$limit" <<'PY'
import json, sys
path, limit = sys.argv[1], int(sys.argv[2])
try:
data = open(path, encoding="utf-8", errors="replace").read()[-limit:]
except FileNotFoundError:
data = ""
print(json.dumps(data))
PY
}
capture_json() {
name="$1"
shift
"$@" -o json >"$tmp/$name.json" 2>"$tmp/$name.err"
rc=$?
printf '%s' "$rc" >"$tmp/$name.rc"
}
run_apply() {
manifest="$tmp/gitea.k8s.yaml"
printf '%s' "$UNIDESK_GITEA_MANIFEST_B64" | base64 -d >"$manifest"
dry_arg=""
if [ "$UNIDESK_GITEA_DRY_RUN" = "1" ]; then
dry_arg="--dry-run=server"
fi
timeout "$UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS" kubectl apply --server-side $dry_arg --force-conflicts --field-manager="$UNIDESK_GITEA_FIELD_MANAGER" -f "$manifest" >"$tmp/apply.out" 2>"$tmp/apply.err"
apply_rc=$?
if [ "$apply_rc" -eq 0 ] && [ "$UNIDESK_GITEA_WAIT" = "1" ] && [ "$UNIDESK_GITEA_DRY_RUN" != "1" ]; then
timeout "$UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS" kubectl -n "$UNIDESK_GITEA_NAMESPACE" rollout status "statefulset/$UNIDESK_GITEA_STATEFULSET_NAME" --timeout="${UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS}s" >"$tmp/rollout.out" 2>"$tmp/rollout.err"
rollout_rc=$?
else
rollout_rc=0
: >"$tmp/rollout.out"
if [ "$UNIDESK_GITEA_DRY_RUN" = "1" ]; then
printf '%s\n' "dry-run: rollout wait skipped" >"$tmp/rollout.err"
else
printf '%s\n' "rollout wait not requested" >"$tmp/rollout.err"
fi
fi
python3 - "$apply_rc" "$rollout_rc" "$tmp/apply.out" "$tmp/apply.err" "$tmp/rollout.out" "$tmp/rollout.err" <<'PY'
import json, os, sys
apply_rc, rollout_rc = int(sys.argv[1]), int(sys.argv[2])
def text(path, limit=3000):
try:
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
except FileNotFoundError:
return ""
dry = os.environ.get("UNIDESK_GITEA_DRY_RUN") == "1"
payload = {
"ok": apply_rc == 0 and rollout_rc == 0,
"target": os.environ.get("UNIDESK_GITEA_TARGET_ID"),
"route": os.environ.get("UNIDESK_GITEA_ROUTE"),
"namespace": os.environ.get("UNIDESK_GITEA_NAMESPACE"),
"mode": "dry-run" if dry else "confirmed",
"mutation": not dry,
"image": os.environ.get("UNIDESK_GITEA_IMAGE"),
"objects": {
"statefulSet": os.environ.get("UNIDESK_GITEA_STATEFULSET_NAME"),
"service": os.environ.get("UNIDESK_GITEA_SERVICE_NAME"),
"networkPolicy": "allow-all",
},
"steps": {
"apply": {"exitCode": apply_rc, "stdoutTail": text(sys.argv[3]), "stderrTail": text(sys.argv[4])},
"rollout": {"exitCode": rollout_rc, "stdoutTail": text(sys.argv[5]), "stderrTail": text(sys.argv[6])},
},
"valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
}
run_status() {
selector="app.kubernetes.io/name=$UNIDESK_GITEA_APP_NAME,app.kubernetes.io/component=gitea"
capture_json namespace kubectl get namespace "$UNIDESK_GITEA_NAMESPACE"
capture_json networkpolicy kubectl -n "$UNIDESK_GITEA_NAMESPACE" get networkpolicy allow-all
capture_json statefulset kubectl -n "$UNIDESK_GITEA_NAMESPACE" get statefulset "$UNIDESK_GITEA_STATEFULSET_NAME"
capture_json service kubectl -n "$UNIDESK_GITEA_NAMESPACE" get service "$UNIDESK_GITEA_SERVICE_NAME"
capture_json endpoints kubectl -n "$UNIDESK_GITEA_NAMESPACE" get endpoints "$UNIDESK_GITEA_SERVICE_NAME"
capture_json pods kubectl -n "$UNIDESK_GITEA_NAMESPACE" get pods -l "$selector"
capture_json pvc kubectl -n "$UNIDESK_GITEA_NAMESPACE" get pvc -l "$selector"
capture_json events kubectl -n "$UNIDESK_GITEA_NAMESPACE" get events --sort-by=.lastTimestamp
python3 - "$tmp" <<'PY'
import json, os, sys
tmp = sys.argv[1]
def rc(name):
try:
return int(open(f"{tmp}/{name}.rc", encoding="utf-8").read() or "1")
except FileNotFoundError:
return 1
def load(name):
try:
return json.load(open(f"{tmp}/{name}.json", encoding="utf-8"))
except Exception:
return None
def items(name):
data = load(name)
if isinstance(data, dict) and isinstance(data.get("items"), list):
return data["items"]
if isinstance(data, dict) and data.get("kind") != "Status":
return [data]
return []
def meta_name(item):
return ((item or {}).get("metadata") or {}).get("name")
def pod_ready(item):
status = (item or {}).get("status") or {}
return any(c.get("type") == "Ready" and c.get("status") == "True" for c in status.get("conditions", []))
def pod_summary(item):
status = (item or {}).get("status") or {}
containers = status.get("containerStatuses") or []
return {
"name": meta_name(item),
"phase": status.get("phase"),
"ready": pod_ready(item),
"restarts": sum(int(c.get("restartCount") or 0) for c in containers if isinstance(c, dict)),
}
def service_summary(item):
spec = (item or {}).get("spec") or {}
return {
"name": meta_name(item),
"type": spec.get("type"),
"clusterIP": spec.get("clusterIP"),
"ports": [{"name": p.get("name"), "port": p.get("port"), "targetPort": p.get("targetPort")} for p in spec.get("ports", [])],
}
def endpoint_ready(item):
subsets = ((item or {}).get("subsets") or [])
return any(s.get("addresses") for s in subsets if isinstance(s, dict))
def pvc_summary(item):
spec = (item or {}).get("spec") or {}
status = (item or {}).get("status") or {}
return {
"name": meta_name(item),
"phase": status.get("phase"),
"storageClassName": spec.get("storageClassName"),
"capacity": (status.get("capacity") or {}).get("storage"),
}
def event_tail(limit=8):
result = []
for item in items("events")[-limit:]:
result.append({
"type": item.get("type"),
"reason": item.get("reason"),
"object": f"{((item.get('involvedObject') or {}).get('kind') or '-')}/{((item.get('involvedObject') or {}).get('name') or '-')}",
"message": (item.get("message") or "")[:300],
"lastTimestamp": item.get("lastTimestamp") or item.get("eventTime"),
})
return result
sts = load("statefulset") or {}
sts_spec = sts.get("spec") or {}
sts_status = sts.get("status") or {}
desired = int(sts_spec.get("replicas") or 0)
ready_replicas = int(sts_status.get("readyReplicas") or 0)
pods = [pod_summary(item) for item in items("pods")]
service = service_summary(load("service") or {})
endpoint_ok = endpoint_ready(load("endpoints") or {})
ready = rc("namespace") == 0 and rc("networkpolicy") == 0 and rc("service") == 0 and desired > 0 and ready_replicas >= desired and endpoint_ok
payload = {
"ok": True,
"ready": ready,
"target": os.environ.get("UNIDESK_GITEA_TARGET_ID"),
"route": os.environ.get("UNIDESK_GITEA_ROUTE"),
"namespace": os.environ.get("UNIDESK_GITEA_NAMESPACE"),
"image": os.environ.get("UNIDESK_GITEA_IMAGE"),
"serviceDns": f"{os.environ.get('UNIDESK_GITEA_SERVICE_NAME')}.{os.environ.get('UNIDESK_GITEA_NAMESPACE')}.svc.cluster.local:{os.environ.get('UNIDESK_GITEA_HTTP_PORT')}",
"networkPolicy": {"allowAllPresent": rc("networkpolicy") == 0},
"statefulSet": {"name": meta_name(sts), "desired": desired, "readyReplicas": ready_replicas, "updatedReplicas": sts_status.get("updatedReplicas")},
"service": service,
"endpointsReady": endpoint_ok,
"pods": pods,
"pvcs": [pvc_summary(item) for item in items("pvc")],
"eventsTail": event_tail(),
"valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0)
PY
}
run_validate() {
proxy_path="/api/v1/namespaces/$UNIDESK_GITEA_NAMESPACE/services/http:$UNIDESK_GITEA_SERVICE_NAME:$UNIDESK_GITEA_HTTP_PORT/proxy$UNIDESK_GITEA_HEALTH_PATH"
kubectl get --raw "$proxy_path" >"$tmp/health.out" 2>"$tmp/health.err"
health_rc=$?
capture_json endpoints kubectl -n "$UNIDESK_GITEA_NAMESPACE" get endpoints "$UNIDESK_GITEA_SERVICE_NAME"
python3 - "$health_rc" "$tmp/health.out" "$tmp/health.err" "$tmp/endpoints.json" "$tmp/endpoints.rc" <<'PY'
import json, os, sys
health_rc = int(sys.argv[1])
def text(path, limit=2000):
try:
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
except FileNotFoundError:
return ""
body = text(sys.argv[2], 4000)
try:
parsed = json.loads(body)
except Exception:
parsed = None
try:
endpoint_rc = int(open(sys.argv[5], encoding="utf-8").read() or "1")
except Exception:
endpoint_rc = 1
try:
endpoints = json.load(open(sys.argv[4], encoding="utf-8"))
except Exception:
endpoints = {}
subsets = endpoints.get("subsets") if isinstance(endpoints, dict) else []
endpoints_ready = any(isinstance(s, dict) and s.get("addresses") for s in (subsets or []))
health_status = parsed.get("status") if isinstance(parsed, dict) else None
ok = health_rc == 0 and endpoint_rc == 0 and endpoints_ready and health_status == "pass"
payload = {
"ok": ok,
"target": os.environ.get("UNIDESK_GITEA_TARGET_ID"),
"route": os.environ.get("UNIDESK_GITEA_ROUTE"),
"namespace": os.environ.get("UNIDESK_GITEA_NAMESPACE"),
"service": os.environ.get("UNIDESK_GITEA_SERVICE_NAME"),
"serviceProxyPath": f"/api/v1/namespaces/{os.environ.get('UNIDESK_GITEA_NAMESPACE')}/services/http:{os.environ.get('UNIDESK_GITEA_SERVICE_NAME')}:{os.environ.get('UNIDESK_GITEA_HTTP_PORT')}/proxy{os.environ.get('UNIDESK_GITEA_HEALTH_PATH')}",
"health": {
"exitCode": health_rc,
"status": health_status,
"description": parsed.get("description") if isinstance(parsed, dict) else None,
"bodyBytes": len(body.encode("utf-8")),
"stderrTail": text(sys.argv[3]),
},
"endpointsReady": endpoints_ready,
"valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if ok else 1)
PY
}
case "$UNIDESK_GITEA_ACTION" in
apply) run_apply ;;
status) run_status ;;
validate) run_validate ;;
*) printf '{"ok":false,"error":"unsupported-gitea-remote-action"}\n'; exit 2 ;;
esac
+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)];
}
+7 -2
View File
@@ -369,7 +369,7 @@ export interface ManagedResourceCleanupPlan {
export function platformInfraHelp(): unknown { export function platformInfraHelp(): unknown {
const target = sub2ApiHelpTargetSummary(); const target = sub2ApiHelpTargetSummary();
return { return {
command: "platform-infra sub2api|langbot|n8n|wechat-archive|observability|secret-plane|kafka ...", command: "platform-infra sub2api|langbot|n8n|wechat-archive|observability|secret-plane|kafka|gitea ...",
output: "json", output: "json",
usage: [ usage: [
"bun scripts/cli.ts platform-infra sub2api plan [--target G14|D601]", "bun scripts/cli.ts platform-infra sub2api plan [--target G14|D601]",
@@ -430,8 +430,13 @@ export function platformInfraHelp(): unknown {
"bun scripts/cli.ts platform-infra kafka offsets --node D518 --topic hwlab.agentrun.command.v1", "bun scripts/cli.ts platform-infra kafka offsets --node D518 --topic hwlab.agentrun.command.v1",
"bun scripts/cli.ts platform-infra kafka tail --node D518 --topic hwlab.agentrun.command.v1 --limit 5", "bun scripts/cli.ts platform-infra kafka tail --node D518 --topic hwlab.agentrun.command.v1 --limit 5",
"bun scripts/cli.ts platform-infra kafka produce --node D518 --topic hwlab.agentrun.command.v1 --key <trace-or-session>", "bun scripts/cli.ts platform-infra kafka produce --node D518 --topic hwlab.agentrun.command.v1 --key <trace-or-session>",
"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",
"bun scripts/cli.ts platform-infra gitea validate --target JD01",
], ],
description: "Operate YAML-controlled platform-infra services such as Sub2API, LangBot, n8n, WeChat archive workflows, OpenTelemetry tracing, the independent target-scoped secret plane, and the D518 Kafka event bus. Public services use PK01 Caddy+FRP rather than Kubernetes Ingress, NodePort, or LoadBalancer.", description: "Operate YAML-controlled platform-infra services such as Sub2API, LangBot, n8n, WeChat archive workflows, OpenTelemetry tracing, the independent target-scoped secret plane, the D518 Kafka event bus, and the internal Gitea source-authority service for GH-1548/GH-1549. Public services use PK01 Caddy+FRP rather than Kubernetes Ingress, NodePort, or LoadBalancer.",
target, target,
codexPool: { codexPool: {
usage: [ usage: [
+4
View File
@@ -65,6 +65,10 @@ export async function runPlatformInfraCommand(config: UniDeskConfig, args: strin
const { runPlatformInfraKafkaCommand } = await import("../platform-infra-kafka"); const { runPlatformInfraKafkaCommand } = await import("../platform-infra-kafka");
return await runPlatformInfraKafkaCommand(config, args.slice(1)); return await runPlatformInfraKafkaCommand(config, args.slice(1));
} }
if (target === "gitea") {
const { runPlatformInfraGiteaCommand } = await import("../platform-infra-gitea");
return await runPlatformInfraGiteaCommand(config, args.slice(1));
}
if (target !== "sub2api") return unsupported(args); if (target !== "sub2api") return unsupported(args);
if (action === "plan" || action === undefined) { if (action === "plan" || action === undefined) {
const planArgs = args.slice(2); const planArgs = args.slice(2);