import { createHash, randomBytes } from "node:crypto"; import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import type { UniDeskConfig } from "./config"; import { rootPath } from "./config"; import type { RenderedCliResult } from "./output"; import { capture, compactCapture, createYamlFieldReader, fingerprintValues, parseJsonOutput, readYamlRecord, shQuote, } from "./platform-infra-ops-library"; import { applyPk01CaddyBlock, escapeTomlString, prepareFrpcSecret, renderFrpcManifest } from "./platform-infra-public-service"; import type { PublicServiceExposure, PublicServiceTarget, FrpcSecretMaterial } from "./platform-infra-public-service"; import { applyPk01CaddySiteBlock, caddyManagedBlockMarkers } from "./pk01-caddy"; import { deriveGiteaWebhookAttemptBudgetMs, deriveGiteaWebhookResponseHeaderTimeoutMs, renderGiteaWebhookCaddyHandle, validateGiteaWebhookTiming } from "./platform-infra-gitea-caddy"; import type { GiteaWebhookIngressRetry } from "./platform-infra-gitea-caddy"; import { renderGiteaRemotePayloadMaterializer, summarizeGiteaRemotePayloads } from "./platform-infra-gitea-payload"; import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "./secrets"; import { PAC_AUTOMATIC_DELIVERY_REFERENCE } from "./cicd-delivery-authority"; import { renderPlatformInfraGiteaDesiredFragments } from "./platform-infra-gitea-desired-fragments"; 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 githubSyncServerFile = rootPath("scripts", "src", "platform-infra-gitea-github-sync-server.mjs"); const githubSyncEntrypointFile = rootPath("scripts", "src", "platform-infra-gitea-github-sync-entrypoint.mjs"); const statusEvaluatorFile = rootPath("scripts", "src", "platform_infra_gitea_status_evaluator.py"); 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; publicExposureEnabled: boolean; webhookSync: GiteaTargetWebhookSync | null; } interface GiteaTargetWebhookSync { publicPath: string; frpc: { proxyName: string; remotePort: number; }; } 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; }; sourceAuthority: { enabled: boolean; stage: string; statusAuthority: string; firstCiConsumer: string; credentials: { sourceRoot: string; admin: GiteaAdminCredential; github: GiteaGithubCredential; }; githubProxy: { enabled: boolean; url: string; noProxy: string[]; }; webhookSync: GiteaWebhookSync; responsibilities: GiteaSourceResponsibility[]; repositories: GiteaMirrorRepository[]; }; 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; }; publicExposure: PublicServiceExposure & { secretRoot: string }; database: { type: "sqlite3"; path: string; }; actions: { enabled: boolean; }; webhook: { allowedHostList: string; }; 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 GiteaAdminCredential { sourceRef: string; format: "line-pair"; usernameLine: number; passwordLine: number; requiredFor: string[]; } interface GiteaGithubCredential { transport: "https-token"; sourceRef: string; sourceKey: string; format: "env" | "raw-token"; requiredFor: string[]; } interface GiteaWebhookSync { enabled: boolean; direction: "github-to-gitea"; responseBudgetMs: number; publicPath: string; events: string[]; ingressRetry: GiteaWebhookIngressRetry; secret: { sourceRef: string; sourceKey: string; createIfMissing: boolean; }; bridge: { deploymentName: string; serviceName: string; secretName: string; configMapName: string; image: string; replicas: number; httpPort: number; serviceAccountName: string; shutdownGraceMs: number; candidateGate: { enabled: boolean; configMapName: string; jobName: string; activeDeadlineSeconds: number; ttlSecondsAfterFinished: number; healthTimeoutMs: number; pollIntervalMs: number; }; inbox: { claimName: string; path: string; storageSize: string; maxBytes: number; committedRetentionSeconds: number; cleanupIntervalMs: number; }; retry: { maxAttempts: number; initialDelayMs: number; maxDelayMs: number; terminalRetryDelayMs: number; attemptTimeoutMs: number; scanIntervalMs: number; }; }; gitOpsDelivery: GiteaWebhookGitOpsDelivery; frpc: { proxyName: string; remotePort: number; }; } export interface GiteaWebhookGitOpsDelivery { enabled: boolean; targetId: string; readUrl: string; writeUrl: string; branch: string; sourceSnapshotPrefix: string; desiredManifestPath: string; bootstrapApplicationPath: string; releaseStatePath: string; application: { name: string; namespace: string; project: string; repoUrl: string; targetRevision: string; path: string; destinationNamespace: string; automated: boolean; }; author: { name: string; email: string; }; cas: { maxAttempts: number; }; } interface GiteaSourceResponsibility { name: string; current: string; target: string; disposition: "replaced-by-gitea" | "retained-for-gitops-flush" | "migration-readonly"; } interface GiteaMirrorRepository { key: string; targetId: string; upstream: { repository: string; cloneUrl: string; branch: string; }; gitea: { owner: string; name: string; mirrorMode: "controlled-push"; publicRead: boolean; readUrl: string; }; gitops: { branch: string; flushDisposition: string; }; snapshot: { prefix: string; }; legacyGitMirror: { readUrl: string; configRef: string; disposition: "replaced-by-gitea" | "retained-for-gitops-flush" | "migration-readonly"; }; } interface CommonOptions { targetId: string | null; full: boolean; raw: boolean; } interface ApplyOptions extends CommonOptions { confirm: boolean; dryRun: boolean; wait: boolean; } interface MirrorOptions extends CommonOptions { confirm: boolean; repoKey: string | null; } interface MirrorSecrets { adminUsername: string; adminPassword: string; githubToken: string; webhookSecret: string; } interface MirrorRemoteParams { repos?: GiteaMirrorRepository[]; secrets?: MirrorSecrets; frpcSecret?: FrpcSecretMaterial; } export async function runPlatformInfraGiteaCommand(config: UniDeskConfig, args: string[]): Promise | RenderedCliResult> { const [action = "plan"] = args; if (action === "help" || action === "--help") return giteaHelp(args[1]); 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))); if (action === "mirror") return await mirrorCommand(config, args.slice(1)); return { ok: false, error: "unsupported-platform-infra-gitea-command", args, help: giteaHelp(), }; } export function giteaHelp(scope?: string): Record { if (scope === "platform-bootstrap") { return { command: "platform-infra gitea help platform-bootstrap", configTruth: configLabel, usage: [ "bun scripts/cli.ts platform-infra gitea apply --target --dry-run", "bun scripts/cli.ts platform-infra gitea apply --target --confirm", "bun scripts/cli.ts platform-infra gitea mirror bootstrap --target --confirm", "bun scripts/cli.ts platform-infra gitea mirror webhook apply --target --confirm", ], boundary: "这些命令只用于平台首次引导或独立平台维护,不能补齐 PR merge 后的 source delivery。", }; } return { command: "platform-infra gitea status|validate|mirror status|mirror webhook status", configTruth: configLabel, usage: [ "bun scripts/cli.ts platform-infra gitea status --target [--full|--raw]", "bun scripts/cli.ts platform-infra gitea validate --target [--full|--raw]", "bun scripts/cli.ts platform-infra gitea mirror status --target [--full|--raw]", "bun scripts/cli.ts platform-infra gitea mirror webhook status --target [--full|--raw]", ], scopedHelp: ["bun scripts/cli.ts platform-infra gitea help platform-bootstrap"], boundary: "PR merge 是唯一 delivery 触发;默认入口只读观察和校验,不能充当合并后的恢复动作。", }; } function readGiteaConfig(): GiteaConfig { const root = readYamlRecord>(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 sourceAuthority = y.objectField(root, "sourceAuthority", ""); const sourceCredentials = y.objectField(sourceAuthority, "credentials", "sourceAuthority"); const githubProxy = y.objectField(sourceAuthority, "githubProxy", "sourceAuthority"); const webhookSync = y.objectField(sourceAuthority, "webhookSync", "sourceAuthority"); 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 webhook = y.objectField(app, "webhook", "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 publicExposure = y.objectField(app, "publicExposure", "app"); 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"), }, sourceAuthority: { enabled: y.booleanField(sourceAuthority, "enabled", "sourceAuthority"), stage: y.stringField(sourceAuthority, "stage", "sourceAuthority"), statusAuthority: y.stringField(sourceAuthority, "statusAuthority", "sourceAuthority"), firstCiConsumer: y.stringField(sourceAuthority, "firstCiConsumer", "sourceAuthority"), credentials: { sourceRoot: y.absolutePathField(sourceCredentials, "sourceRoot", "sourceAuthority.credentials"), admin: parseAdminCredential(y.objectField(sourceCredentials, "admin", "sourceAuthority.credentials"), "sourceAuthority.credentials.admin"), github: parseGithubCredential(y.objectField(sourceCredentials, "github", "sourceAuthority.credentials"), "sourceAuthority.credentials.github"), }, githubProxy: { enabled: y.booleanField(githubProxy, "enabled", "sourceAuthority.githubProxy"), url: urlField(githubProxy, "url", "sourceAuthority.githubProxy"), noProxy: y.stringArrayField(githubProxy, "noProxy", "sourceAuthority.githubProxy"), }, webhookSync: parseWebhookSync(webhookSync, "sourceAuthority.webhookSync"), responsibilities: y.arrayOfRecords(sourceAuthority.responsibilities, "sourceAuthority.responsibilities").map(parseResponsibility), repositories: y.arrayOfRecords(sourceAuthority.repositories, "sourceAuthority.repositories").map(parseMirrorRepository), }, 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"), }, publicExposure: parsePublicExposure(publicExposure, "app.publicExposure"), 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"), }, webhook: { allowedHostList: y.stringField(webhook, "allowedHostList", "app.webhook"), }, 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 parseAdminCredential(record: Record, path: string): GiteaAdminCredential { return { sourceRef: secretSourceRefField(record, "sourceRef", path), format: literalField(record, "format", path, ["line-pair"]), usernameLine: positiveInteger(record, "usernameLine", path), passwordLine: positiveInteger(record, "passwordLine", path), requiredFor: y.stringArrayField(record, "requiredFor", path), }; } function parseGithubCredential(record: Record, path: string): GiteaGithubCredential { return { transport: y.enumField(record, "transport", path, ["https-token"] as const), sourceRef: secretSourceRefField(record, "sourceRef", path), sourceKey: y.envKeyField(record, "sourceKey", path), format: optionalLiteralField(record, "format", path, ["env", "raw-token"] as const) ?? "env", requiredFor: y.stringArrayField(record, "requiredFor", path), }; } function parseWebhookSync(record: Record, path: string): GiteaWebhookSync { const ingressRetry = y.objectField(record, "ingressRetry", path); const secret = y.objectField(record, "secret", path); const bridge = y.objectField(record, "bridge", path); const candidateGate = y.objectField(bridge, "candidateGate", `${path}.bridge`); const inbox = y.objectField(bridge, "inbox", `${path}.bridge`); const gitOpsDelivery = y.objectField(record, "gitOpsDelivery", path); const frpc = y.objectField(record, "frpc", path); return { enabled: y.booleanField(record, "enabled", path), direction: y.enumField(record, "direction", path, ["github-to-gitea"] as const), responseBudgetMs: positiveInteger(record, "responseBudgetMs", path), publicPath: y.apiPathField(record, "publicPath", path), events: y.stringArrayField(record, "events", path), ingressRetry: { enabled: y.booleanField(ingressRetry, "enabled", `${path}.ingressRetry`), maxBodyBytes: positiveInteger(ingressRetry, "maxBodyBytes", `${path}.ingressRetry`), maxRetries: positiveInteger(ingressRetry, "maxRetries", `${path}.ingressRetry`), tryIntervalMs: positiveInteger(ingressRetry, "tryIntervalMs", `${path}.ingressRetry`), dialTimeoutMs: positiveInteger(ingressRetry, "dialTimeoutMs", `${path}.ingressRetry`), writeTimeoutMs: positiveInteger(ingressRetry, "writeTimeoutMs", `${path}.ingressRetry`), retryStatusCodes: y.numberArrayField(ingressRetry, "retryStatusCodes", `${path}.ingressRetry`), }, secret: { sourceRef: secretSourceRefField(secret, "sourceRef", `${path}.secret`), sourceKey: y.envKeyField(secret, "sourceKey", `${path}.secret`), createIfMissing: y.booleanField(secret, "createIfMissing", `${path}.secret`), }, bridge: { deploymentName: y.kubernetesNameField(bridge, "deploymentName", `${path}.bridge`), serviceName: y.kubernetesNameField(bridge, "serviceName", `${path}.bridge`), secretName: y.kubernetesNameField(bridge, "secretName", `${path}.bridge`), configMapName: y.kubernetesNameField(bridge, "configMapName", `${path}.bridge`), image: y.stringField(bridge, "image", `${path}.bridge`), replicas: positiveInteger(bridge, "replicas", `${path}.bridge`), httpPort: y.portField(bridge, "httpPort", `${path}.bridge`), serviceAccountName: y.kubernetesNameField(bridge, "serviceAccountName", `${path}.bridge`), shutdownGraceMs: positiveInteger(bridge, "shutdownGraceMs", `${path}.bridge`), candidateGate: { enabled: y.booleanField(candidateGate, "enabled", `${path}.bridge.candidateGate`), configMapName: y.kubernetesNameField(candidateGate, "configMapName", `${path}.bridge.candidateGate`), jobName: y.kubernetesNameField(candidateGate, "jobName", `${path}.bridge.candidateGate`), activeDeadlineSeconds: positiveInteger(candidateGate, "activeDeadlineSeconds", `${path}.bridge.candidateGate`), ttlSecondsAfterFinished: positiveInteger(candidateGate, "ttlSecondsAfterFinished", `${path}.bridge.candidateGate`), healthTimeoutMs: positiveInteger(candidateGate, "healthTimeoutMs", `${path}.bridge.candidateGate`), pollIntervalMs: positiveInteger(candidateGate, "pollIntervalMs", `${path}.bridge.candidateGate`), }, inbox: { claimName: y.kubernetesNameField(inbox, "claimName", `${path}.bridge.inbox`), path: y.absolutePathField(inbox, "path", `${path}.bridge.inbox`), storageSize: quantity(inbox, "storageSize", `${path}.bridge.inbox`), maxBytes: positiveInteger(inbox, "maxBytes", `${path}.bridge.inbox`), committedRetentionSeconds: positiveInteger(inbox, "committedRetentionSeconds", `${path}.bridge.inbox`), cleanupIntervalMs: positiveInteger(inbox, "cleanupIntervalMs", `${path}.bridge.inbox`), }, retry: parseWebhookRetry(y.objectField(bridge, "retry", `${path}.bridge`), `${path}.bridge.retry`), }, gitOpsDelivery: parseWebhookGitOpsDelivery(gitOpsDelivery, `${path}.gitOpsDelivery`), frpc: { proxyName: y.stringField(frpc, "proxyName", `${path}.frpc`), remotePort: y.portField(frpc, "remotePort", `${path}.frpc`), }, }; } function parseWebhookGitOpsDelivery(record: Record, path: string): GiteaWebhookGitOpsDelivery { const application = y.objectField(record, "application", path); const author = y.objectField(record, "author", path); const cas = y.objectField(record, "cas", path); return { enabled: y.booleanField(record, "enabled", path), targetId: y.stringField(record, "targetId", path), readUrl: urlField(record, "readUrl", path), writeUrl: urlField(record, "writeUrl", path), branch: gitBranchField(record, "branch", path), sourceSnapshotPrefix: refPrefixField(record, "sourceSnapshotPrefix", path), desiredManifestPath: safeRelativePathField(record, "desiredManifestPath", path), bootstrapApplicationPath: safeRelativePathField(record, "bootstrapApplicationPath", path), releaseStatePath: safeRelativePathField(record, "releaseStatePath", path), application: { name: y.kubernetesNameField(application, "name", `${path}.application`), namespace: y.kubernetesNameField(application, "namespace", `${path}.application`), project: y.kubernetesNameField(application, "project", `${path}.application`), repoUrl: urlField(application, "repoUrl", `${path}.application`), targetRevision: gitBranchField(application, "targetRevision", `${path}.application`), path: safeRelativePathField(application, "path", `${path}.application`), destinationNamespace: y.kubernetesNameField(application, "destinationNamespace", `${path}.application`), automated: y.booleanField(application, "automated", `${path}.application`), }, author: { name: y.stringField(author, "name", `${path}.author`), email: y.stringField(author, "email", `${path}.author`), }, cas: { maxAttempts: positiveInteger(cas, "maxAttempts", `${path}.cas`), }, }; } function parseWebhookRetry(record: Record, path: string): GiteaWebhookSync["bridge"]["retry"] { return { maxAttempts: positiveInteger(record, "maxAttempts", path), initialDelayMs: positiveInteger(record, "initialDelayMs", path), maxDelayMs: positiveInteger(record, "maxDelayMs", path), terminalRetryDelayMs: positiveInteger(record, "terminalRetryDelayMs", path), attemptTimeoutMs: positiveInteger(record, "attemptTimeoutMs", path), scanIntervalMs: positiveInteger(record, "scanIntervalMs", path), }; } function parsePublicExposure(record: Record, path: string): PublicServiceExposure & { secretRoot: string } { const dns = y.objectField(record, "dns", path); const frpc = y.objectField(record, "frpc", path); const pk01 = y.objectField(record, "pk01", path); const publicBaseUrl = httpsUrlField(record, "publicBaseUrl", path); const hostname = y.hostField(dns, "hostname", `${path}.dns`); if (new URL(publicBaseUrl).hostname !== hostname) throw new Error(`${configLabel}.${path}.dns.hostname must match publicBaseUrl`); return { enabled: y.booleanField(record, "enabled", path), publicBaseUrl, secretRoot: y.absolutePathField(record, "secretRoot", path), dns: { hostname, expectedA: y.stringField(dns, "expectedA", `${path}.dns`), resolvers: y.stringArrayField(dns, "resolvers", `${path}.dns`), }, frpc: { deploymentName: y.kubernetesNameField(frpc, "deploymentName", `${path}.frpc`), secretName: y.kubernetesNameField(frpc, "secretName", `${path}.frpc`), secretKey: y.stringField(frpc, "secretKey", `${path}.frpc`), image: y.stringField(frpc, "image", `${path}.frpc`), serverAddr: y.hostField(frpc, "serverAddr", `${path}.frpc`), serverPort: y.portField(frpc, "serverPort", `${path}.frpc`), proxyName: y.stringField(frpc, "proxyName", `${path}.frpc`), remotePort: y.portField(frpc, "remotePort", `${path}.frpc`), localIP: y.hostField(frpc, "localIP", `${path}.frpc`), localPort: y.portField(frpc, "localPort", `${path}.frpc`), tokenSourceRef: secretSourceRefField(frpc, "tokenSourceRef", `${path}.frpc`), tokenSourceKey: y.envKeyField(frpc, "tokenSourceKey", `${path}.frpc`), }, pk01: { route: y.stringField(pk01, "route", `${path}.pk01`), caddyConfigPath: y.absolutePathField(pk01, "caddyConfigPath", `${path}.pk01`), caddyServiceName: y.stringField(pk01, "caddyServiceName", `${path}.pk01`), responseHeaderTimeoutSeconds: y.integerField(pk01, "responseHeaderTimeoutSeconds", `${path}.pk01`), }, }; } function parseResponsibility(record: Record, index: number): GiteaSourceResponsibility { const path = `sourceAuthority.responsibilities[${index}]`; return { name: y.stringField(record, "name", path), current: y.stringField(record, "current", path), target: y.stringField(record, "target", path), disposition: y.enumField(record, "disposition", path, ["replaced-by-gitea", "retained-for-gitops-flush", "migration-readonly"] as const), }; } function parseMirrorRepository(record: Record, index: number): GiteaMirrorRepository { const path = `sourceAuthority.repositories[${index}]`; const upstream = y.objectField(record, "upstream", path); const gitea = y.objectField(record, "gitea", path); const gitops = y.objectField(record, "gitops", path); const snapshot = y.objectField(record, "snapshot", path); const legacyGitMirror = y.objectField(record, "legacyGitMirror", path); return { key: y.stringField(record, "key", path), targetId: y.stringField(record, "targetId", path), upstream: { repository: repositoryField(upstream, "repository", `${path}.upstream`), cloneUrl: gitCloneUrlField(upstream, "cloneUrl", `${path}.upstream`), branch: y.stringField(upstream, "branch", `${path}.upstream`), }, gitea: { owner: y.stringField(gitea, "owner", `${path}.gitea`), name: giteaRepoNameField(gitea, "name", `${path}.gitea`), mirrorMode: y.enumField(gitea, "mirrorMode", `${path}.gitea`, ["controlled-push"] as const), publicRead: y.booleanField(gitea, "publicRead", `${path}.gitea`), readUrl: urlField(gitea, "readUrl", `${path}.gitea`), }, gitops: { branch: y.stringField(gitops, "branch", `${path}.gitops`), flushDisposition: y.stringField(gitops, "flushDisposition", `${path}.gitops`), }, snapshot: { prefix: refPrefixField(snapshot, "prefix", `${path}.snapshot`), }, legacyGitMirror: { readUrl: urlField(legacyGitMirror, "readUrl", `${path}.legacyGitMirror`), configRef: y.stringField(legacyGitMirror, "configRef", `${path}.legacyGitMirror`), disposition: y.enumField(legacyGitMirror, "disposition", `${path}.legacyGitMirror`, ["replaced-by-gitea", "retained-for-gitops-flush", "migration-readonly"] as const), }, }; } function parseTarget(record: Record, index: number): GiteaTarget { const path = `targets[${index}]`; const publicExposureRaw = record.publicExposure; if (publicExposureRaw !== undefined && (typeof publicExposureRaw !== "object" || publicExposureRaw === null || Array.isArray(publicExposureRaw))) { throw new Error(`${configLabel}.${path}.publicExposure must be an object`); } const webhookSyncRaw = record.webhookSync; if (webhookSyncRaw !== undefined && (typeof webhookSyncRaw !== "object" || webhookSyncRaw === null || Array.isArray(webhookSyncRaw))) { throw new Error(`${configLabel}.${path}.webhookSync must be an object`); } const publicExposure = publicExposureRaw as Record | undefined; 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), publicExposureEnabled: publicExposure === undefined ? true : y.booleanField(publicExposure, "enabled", `${path}.publicExposure`), webhookSync: webhookSyncRaw === undefined ? null : parseTargetWebhookSync(webhookSyncRaw as Record, `${path}.webhookSync`), }; } function parseTargetWebhookSync(record: Record, path: string): GiteaTargetWebhookSync { const frpc = y.objectField(record, "frpc", path); return { publicPath: y.apiPathField(record, "publicPath", path), frpc: { proxyName: y.stringField(frpc, "proxyName", `${path}.frpc`), remotePort: y.portField(frpc, "remotePort", `${path}.frpc`), }, }; } function validateConfig(gitea: GiteaConfig): void { resolveTarget(gitea, gitea.defaults.targetId); if (!gitea.sourceAuthority.enabled) throw new Error(`${configLabel}.sourceAuthority.enabled must be true for GH-1550`); if (gitea.sourceAuthority.repositories.length < 1) throw new Error(`${configLabel}.sourceAuthority.repositories must not be empty`); 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 stay explicit while Gitea is the source-authority service`); 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`); if (gitea.app.server.rootUrl !== gitea.app.publicExposure.publicBaseUrl.replace(/\/?$/u, "/")) throw new Error(`${configLabel}.app.server.rootUrl must match app.publicExposure.publicBaseUrl for the Web UI`); if (gitea.sourceAuthority.webhookSync.enabled) { const events = new Set(gitea.sourceAuthority.webhookSync.events); if (!events.has("push")) throw new Error(`${configLabel}.sourceAuthority.webhookSync.events must include push for GitHub -> Gitea source sync`); if (gitea.sourceAuthority.webhookSync.frpc.remotePort === gitea.app.publicExposure.frpc.remotePort) throw new Error(`${configLabel}.sourceAuthority.webhookSync.frpc.remotePort must differ from app.publicExposure.frpc.remotePort`); const ingressRetry = gitea.sourceAuthority.webhookSync.ingressRetry; const bridge = gitea.sourceAuthority.webhookSync.bridge; if (bridge.replicas !== 1) throw new Error(`${configLabel}.sourceAuthority.webhookSync.bridge.replicas must be 1 because the durable inbox uses a single-writer Recreate deployment`); if (bridge.retry.terminalRetryDelayMs < bridge.retry.maxDelayMs) throw new Error(`${configLabel}.sourceAuthority.webhookSync.bridge.retry.terminalRetryDelayMs must be >= maxDelayMs`); if (ingressRetry.retryStatusCodes.length < 1 || ingressRetry.retryStatusCodes.some((statusCode) => ![502, 503, 504].includes(statusCode))) { throw new Error(`${configLabel}.sourceAuthority.webhookSync.ingressRetry.retryStatusCodes may only contain 502, 503 and 504`); } if (ingressRetry.enabled) { try { validateGiteaWebhookTiming(gitea.sourceAuthority.webhookSync.responseBudgetMs, ingressRetry); } catch (error) { throw new Error(`${configLabel}.sourceAuthority.webhookSync timing invalid: ${String((error as Error).message || error)}`); } } const delivery = gitea.sourceAuthority.webhookSync.gitOpsDelivery; if (delivery.enabled) { const target = resolveTarget(gitea, delivery.targetId); if (target.id !== "NC01") throw new Error(`${configLabel}.sourceAuthority.webhookSync.gitOpsDelivery.targetId must be NC01`); if (delivery.application.targetRevision !== delivery.branch) throw new Error(`${configLabel}.sourceAuthority.webhookSync.gitOpsDelivery.application.targetRevision must match branch`); if (delivery.application.repoUrl !== delivery.readUrl) throw new Error(`${configLabel}.sourceAuthority.webhookSync.gitOpsDelivery.application.repoUrl must match readUrl`); if (dirname(delivery.desiredManifestPath) !== delivery.application.path) throw new Error(`${configLabel}.sourceAuthority.webhookSync.gitOpsDelivery.desiredManifestPath must be directly under application.path`); if (!delivery.bootstrapApplicationPath.startsWith("deploy/gitops/unidesk-host/")) throw new Error(`${configLabel}.sourceAuthority.webhookSync.gitOpsDelivery.bootstrapApplicationPath must be under the existing unidesk-host bootstrap path`); if (delivery.desiredManifestPath.startsWith("deploy/gitops/unidesk-host/")) throw new Error(`${configLabel}.sourceAuthority.webhookSync.gitOpsDelivery.desiredManifestPath must use an independent GitOps path`); if (!/^[^@\s]+@[^@\s]+$/u.test(delivery.author.email)) throw new Error(`${configLabel}.sourceAuthority.webhookSync.gitOpsDelivery.author.email must be an email address`); if (delivery.cas.maxAttempts > 5) throw new Error(`${configLabel}.sourceAuthority.webhookSync.gitOpsDelivery.cas.maxAttempts must be <= 5`); } } const webhookPaths = new Set(); const webhookPorts = new Set(); for (const route of webhookCaddyRoutes(gitea)) { if (webhookPaths.has(route.publicPath)) throw new Error(`${configLabel} webhook publicPath must be unique: ${route.publicPath}`); if (webhookPorts.has(route.remotePort)) throw new Error(`${configLabel} webhook frpc.remotePort must be unique: ${route.remotePort}`); if (route.remotePort === gitea.app.publicExposure.frpc.remotePort) throw new Error(`${configLabel} webhook frpc.remotePort must differ from app public exposure port: ${route.remotePort}`); webhookPaths.add(route.publicPath); webhookPorts.add(route.remotePort); } const repoKeys = new Set(); for (const repo of gitea.sourceAuthority.repositories) { if (repoKeys.has(repo.key)) throw new Error(`${configLabel}.sourceAuthority.repositories contains duplicate key ${repo.key}`); repoKeys.add(repo.key); resolveTarget(gitea, repo.targetId); const readUrl = new URL(repo.gitea.readUrl); if (readUrl.hostname !== `${gitea.app.serviceName}.${resolveTarget(gitea, repo.targetId).namespace}.svc.cluster.local`) throw new Error(`${configLabel}.sourceAuthority.repositories.${repo.key}.gitea.readUrl must use the internal k3s Gitea Service DNS`); if (readUrl.hostname === new URL(gitea.app.publicExposure.publicBaseUrl).hostname) throw new Error(`${configLabel}.sourceAuthority.repositories.${repo.key}.gitea.readUrl must not use the public Web UI hostname`); } } 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 { 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: giteaReadOnlyNextCommands(target.id), }; } async function apply(config: UniDeskConfig, options: ApplyOptions): Promise> { 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 frpcSecret = prepareGiteaFrpcSecret(gitea, target); const secrets = gitea.sourceAuthority.webhookSync.enabled && !options.dryRun ? ensureMirrorSecrets(gitea, true, true) : undefined; const remote = remoteScript("apply", gitea, target, manifest, options, { frpcSecret, secrets }); const result = await capture(config, target.route, ["sh"], remote.script); const parsed = parseJsonOutput(result.stdout); const caddy = !options.dryRun && result.exitCode === 0 && parsed?.ok === true && caddyExposureNeeded(gitea, target) ? await applyGiteaCaddyBlock(config, gitea) : { ok: true, skipped: true, reason: options.dryRun ? "dry-run" : "apply-not-ready" }; return { ok: result.exitCode === 0 && parsed?.ok === true && record(caddy).ok !== false, action: "platform-infra-gitea-apply", mode: options.dryRun ? "dry-run" : "confirmed", mutation: !options.dryRun, target: targetSummary(target), config: compactConfigSummary(gitea, target), policy, publicExposure: publicExposureSummary(gitea), payloadTransport: remote.payloadSummary, secrets: { frpc: frpcSecretSummary(frpcSecret), webhookSync: secrets === undefined ? { enabled: false } : webhookSecretSummary(gitea, target, secrets) }, pk01Caddy: caddy, remote: parsed ?? compactCapture(result, { full: true }), next: giteaReadOnlyNextCommands(target.id), }; } async function status(config: UniDeskConfig, options: CommonOptions): Promise> { 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 }).script); 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: giteaReadOnlyNextCommands(target.id), }; } async function validate(config: UniDeskConfig, options: CommonOptions): Promise> { 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 }).script); 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 }), }; } async function mirrorCommand(config: UniDeskConfig, args: string[]): Promise | RenderedCliResult> { const [action = "plan"] = args; if (action === "plan") { const options = parseCommonOptions(args.slice(1)); const result = mirrorPlan(options); return options.full || options.raw ? result : renderMirrorPlan(result); } if (action === "bootstrap") { const options = parseMirrorOptions(args.slice(1)); const result = await mirrorBootstrap(config, options); return options.full || options.raw ? result : renderMirrorBootstrap(result); } if (action === "sync" || action === "snapshot") { const options = parseMirrorOptions(args.slice(1)); const result = await mirrorSync(config, options); return options.full || options.raw ? result : renderMirrorSync(result); } if (action === "status") { const options = parseMirrorOptions(args.slice(1)); const result = await mirrorStatus(config, options); return options.full || options.raw ? result : renderMirrorStatus(result); } if (action === "webhook") { return await mirrorWebhookCommand(config, args.slice(1)); } return { ok: false, error: "unsupported-platform-infra-gitea-mirror-command", args, help: { usage: [ "bun scripts/cli.ts platform-infra gitea mirror status --target JD01 [--full|--raw]", "bun scripts/cli.ts platform-infra gitea mirror webhook status --target JD01 [--full|--raw]", ], }, }; } async function mirrorWebhookCommand(config: UniDeskConfig, args: string[]): Promise | RenderedCliResult> { const [action = "status"] = args; if (action === "apply") { const options = parseMirrorOptions(args.slice(1)); const result = await mirrorWebhookApply(config, options); return options.full || options.raw ? result : renderMirrorWebhookApply(result); } if (action === "status") { const options = parseMirrorOptions(args.slice(1)); const result = await mirrorWebhookStatus(config, options); return options.full || options.raw ? result : renderMirrorWebhookStatus(result); } return { ok: false, error: "unsupported-platform-infra-gitea-mirror-webhook-command", usage: "platform-infra gitea mirror webhook status --target [--repo ] [--full|--raw]", }; } function mirrorPlan(options: CommonOptions): Record { const gitea = readGiteaConfig(); const target = resolveTarget(gitea, options.targetId); return { ok: true, action: "platform-infra-gitea-mirror-plan", mutation: false, target: targetSummary(target), sourceAuthority: sourceAuthoritySummary(gitea, target), responsibilities: gitea.sourceAuthority.responsibilities.map(responsibilitySummary), repositories: repositoriesForTarget(gitea, target).map((repo) => repositorySummary(gitea, repo)), credentials: credentialSummaries(gitea), next: mirrorReadOnlyNextCommands(target.id), }; } async function mirrorStatus(config: UniDeskConfig, options: CommonOptions & { repoKey?: string | null }): Promise> { const gitea = readGiteaConfig(); const target = resolveTarget(gitea, options.targetId); const health = await validate(config, options); const healthValidation = record(health.validation); const credentials = credentialSummaries(gitea); const secrets = ensureMirrorSecrets(gitea, false, false); const selectedRepos = selectedRepositories(gitea, target, options.repoKey ?? null); const result = await capture(config, target.route, ["sh"], remoteScript("mirror-status", gitea, target, "", { ...options, confirm: false, dryRun: true, wait: false }, { repos: selectedRepos, secrets }).script); const parsed = parseJsonOutput(result.stdout); const repositories = arrayRecords(record(parsed).repositories); return { ok: health.ok === true && result.exitCode === 0 && parsed?.ok === true, action: "platform-infra-gitea-mirror-status", mutation: false, target: targetSummary(target), serviceHealth: { ok: health.ok === true, healthStatus: record(healthValidation.health).status, endpointsReady: healthValidation.endpointsReady === true, serviceProxyPath: healthValidation.serviceProxyPath, }, sourceAuthority: { ...sourceAuthoritySummary(gitea, target), mirrorReady: repositories.length > 0 && repositories.every((repo) => repo.sourceBundleReady === true), stageReady: health.ok === true, readinessDetail: "Gitea source-authority is ready when every configured repo has branch and snapshot refs in Gitea.", }, responsibilities: gitea.sourceAuthority.responsibilities.map(responsibilitySummary), repositories: repositories.length > 0 ? repositories : selectedRepos.map((repo) => repositorySummary(gitea, repo)), credentials, remote: parsed ?? compactCapture(result, { full: options.full || result.exitCode !== 0 }), next: mirrorReadOnlyNextCommands(target.id), }; } async function mirrorBootstrap(config: UniDeskConfig, options: MirrorOptions): Promise> { const gitea = readGiteaConfig(); const target = resolveTarget(gitea, options.targetId); if (!options.confirm) return { ok: false, action: "platform-infra-gitea-mirror-bootstrap", mutation: false, mode: "missing-confirm", error: "mirror bootstrap requires --confirm" }; const repos = selectedRepositories(gitea, target, options.repoKey); const secrets = ensureMirrorSecrets(gitea, true, true); const result = await capture(config, target.route, ["sh"], remoteScript("mirror-bootstrap", gitea, target, "", { ...options, dryRun: false, wait: false }, { repos, secrets }).script); const parsed = parseJsonOutput(result.stdout); return { ok: result.exitCode === 0 && parsed?.ok === true, action: "platform-infra-gitea-mirror-bootstrap", mutation: true, target: targetSummary(target), repositories: arrayRecords(record(parsed).repositories), credentials: credentialSummaries(gitea), remote: parsed ?? compactCapture(result, { full: true }), next: mirrorReadOnlyNextCommands(target.id), }; } async function mirrorSync(config: UniDeskConfig, options: MirrorOptions): Promise> { const gitea = readGiteaConfig(); const target = resolveTarget(gitea, options.targetId); if (gitea.sourceAuthority.webhookSync.enabled) { return { ok: false, action: "platform-infra-gitea-mirror-sync", mutation: false, mode: "automatic-source-authority-manual-sync-disabled", target: targetSummary(target), error: "manual mirror sync is disabled while GitHub webhook durable source authority is enabled; the durable controller owns automatic convergence", next: mirrorReadOnlyNextCommands(target.id), }; } if (!options.confirm) return { ok: false, action: "platform-infra-gitea-mirror-sync", mutation: false, mode: "missing-confirm", error: "mirror sync requires --confirm" }; const repos = selectedRepositories(gitea, target, options.repoKey); const secrets = ensureMirrorSecrets(gitea, false, false); const result = await capture(config, target.route, ["sh"], remoteScript("mirror-sync", gitea, target, "", { ...options, dryRun: false, wait: false }, { repos, secrets }).script); const parsed = parseJsonOutput(result.stdout); return { ok: result.exitCode === 0 && parsed?.ok === true, action: "platform-infra-gitea-mirror-sync", mutation: true, target: targetSummary(target), repositories: arrayRecords(record(parsed).repositories), sourceBundles: arrayRecords(record(parsed).sourceBundles), remote: parsed ?? compactCapture(result, { full: true }), next: mirrorReadOnlyNextCommands(target.id), }; } async function mirrorWebhookApply(config: UniDeskConfig, options: MirrorOptions): Promise> { const gitea = readGiteaConfig(); const target = resolveTarget(gitea, options.targetId); if (!gitea.sourceAuthority.webhookSync.enabled) return { ok: false, action: "platform-infra-gitea-mirror-webhook-apply", mutation: false, error: "sourceAuthority.webhookSync.enabled is false" }; if (!options.confirm) return { ok: false, action: "platform-infra-gitea-mirror-webhook-apply", mutation: false, mode: "missing-confirm", error: "mirror webhook apply requires --confirm" }; const repos = selectedRepositories(gitea, target, options.repoKey); const secrets = ensureMirrorSecrets(gitea, false, false); const result = await capture(config, target.route, ["sh"], remoteScript("mirror-webhook-apply", gitea, target, "", { ...options, dryRun: false, wait: false }, { repos, secrets }).script); const parsed = parseJsonOutput(result.stdout); return { ok: result.exitCode === 0 && parsed?.ok === true, action: "platform-infra-gitea-mirror-webhook-apply", mutation: true, target: targetSummary(target), webhook: webhookSyncSummary(gitea, target), repositories: arrayRecords(record(parsed).repositories), remote: parsed ?? compactCapture(result, { full: true }), next: mirrorReadOnlyNextCommands(target.id), }; } async function mirrorWebhookStatus(config: UniDeskConfig, options: MirrorOptions): Promise> { const gitea = readGiteaConfig(); const target = resolveTarget(gitea, options.targetId); const repos = selectedRepositories(gitea, target, options.repoKey); const secrets = ensureMirrorSecrets(gitea, false, false); const result = await capture(config, target.route, ["sh"], remoteScript("mirror-webhook-status", gitea, target, "", { ...options, confirm: false, dryRun: true, wait: false }, { repos, secrets }).script); const parsed = parseJsonOutput(result.stdout); const repositories = arrayRecords(record(parsed).repositories); return { ok: result.exitCode === 0 && parsed?.ok === true, action: "platform-infra-gitea-mirror-webhook-status", mutation: false, target: targetSummary(target), webhook: webhookSyncSummary(gitea, target), repositories, bridge: record(parsed).bridge, bridgeLogs: record(parsed).bridgeLogs, mirrorStatus: record(parsed).mirrorStatus, remote: parsed ?? compactCapture(result, { full: true }), next: mirrorReadOnlyNextCommands(target.id), }; } 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} ${renderFrpcManifest(publicServiceTarget(gitea, target))} ${renderGithubSyncManifest(gitea, target)}`; } function renderGithubSyncManifest(gitea: GiteaConfig, target: GiteaTarget): string { const sync = targetWebhookSync(gitea, target); if (!sync.enabled) return ""; const labels = ` app.kubernetes.io/name: ${sync.bridge.deploymentName} app.kubernetes.io/component: github-to-gitea-sync app.kubernetes.io/part-of: devops-infra app.kubernetes.io/managed-by: unidesk`; const reposJson = JSON.stringify(repositoriesForTarget(gitea, target).map(remoteRepoSpec), null, 2); const serverSource = readFileSync(githubSyncServerFile, "utf8"); const entrypointSource = readFileSync(githubSyncEntrypointFile, "utf8"); const configHash = createHash("sha256").update(reposJson).update("\0").update(serverSource).update("\0").update(entrypointSource).digest("hex").slice(0, 16); const gate = sync.bridge.candidateGate; const candidateManifest = gate.enabled ? `--- apiVersion: v1 kind: ConfigMap metadata: name: ${gate.configMapName} namespace: ${target.namespace} annotations: argocd.argoproj.io/hook: PreSync argocd.argoproj.io/hook-delete-policy: BeforeHookCreation argocd.argoproj.io/sync-wave: "-2" labels: ${labels} data: repos.json: | ${indentBlock(reposJson, 4)} server.mjs: | ${indentBlock(serverSource, 4)} entrypoint.mjs: | ${indentBlock(entrypointSource, 4)} --- apiVersion: batch/v1 kind: Job metadata: name: ${gate.jobName} namespace: ${target.namespace} annotations: argocd.argoproj.io/hook: PreSync argocd.argoproj.io/hook-delete-policy: BeforeHookCreation,HookSucceeded argocd.argoproj.io/sync-wave: "-1" labels: ${labels} spec: backoffLimit: 0 activeDeadlineSeconds: ${gate.activeDeadlineSeconds} ttlSecondsAfterFinished: ${gate.ttlSecondsAfterFinished} template: metadata: labels: app.kubernetes.io/name: ${sync.bridge.deploymentName} app.kubernetes.io/component: github-to-gitea-sync-candidate app.kubernetes.io/part-of: devops-infra app.kubernetes.io/managed-by: unidesk spec: restartPolicy: Never containers: - name: candidate-startup-gate image: ${sync.bridge.image} imagePullPolicy: IfNotPresent command: - /bin/sh - -eu - -c - | node /etc/gitea-github-sync-candidate/entrypoint.mjs & server_pid=$! trap 'kill "$server_pid" 2>/dev/null || true' EXIT INT TERM node --input-type=module - ${gate.healthTimeoutMs} ${gate.pollIntervalMs} <<'NODE' const timeoutMs = Number.parseInt(process.argv[2], 10); const pollIntervalMs = Number.parseInt(process.argv[3], 10); const deadline = Date.now() + timeoutMs; let lastError = "candidate-not-ready"; while (Date.now() < deadline) { try { const response = await fetch("http://127.0.0.1:${sync.bridge.httpPort}/healthz"); const body = await response.json(); if (response.ok && body.ok === true && body.durableInbox?.storageReady === true) { console.log(JSON.stringify({ ok: true, gate: "gitea-github-sync-candidate-startup", storageReady: true, valuesPrinted: false })); process.exit(0); } lastError = "health-status-" + response.status; } catch (error) { lastError = String(error?.cause?.code || error?.message || error); } await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); } console.error(JSON.stringify({ ok: false, gate: "gitea-github-sync-candidate-startup", errorType: "candidate-health-timeout", error: lastError, valuesPrinted: false })); process.exit(1); NODE env: - name: UNIDESK_GITEA_WEBHOOK_PORT value: ${yamlQuote(String(sync.bridge.httpPort))} - name: UNIDESK_GITEA_WEBHOOK_PATH value: ${yamlQuote(sync.publicPath)} - name: UNIDESK_GITEA_WEBHOOK_RESPONSE_BUDGET_MS value: ${yamlQuote(String(sync.responseBudgetMs))} - name: UNIDESK_GITEA_REPOS_PATH value: /etc/gitea-github-sync-candidate/repos.json - name: UNIDESK_GITEA_SERVICE_BASE_URL value: ${yamlQuote(`http://${gitea.app.serviceName}.${target.namespace}.svc.cluster.local:${gitea.app.service.httpPort}`)} - name: UNIDESK_GITEA_WEBHOOK_RETRY_MAX_ATTEMPTS value: ${yamlQuote(String(sync.bridge.retry.maxAttempts))} - name: UNIDESK_GITEA_WEBHOOK_RETRY_INITIAL_DELAY_MS value: ${yamlQuote(String(sync.bridge.retry.initialDelayMs))} - name: UNIDESK_GITEA_WEBHOOK_RETRY_MAX_DELAY_MS value: ${yamlQuote(String(sync.bridge.retry.maxDelayMs))} - name: UNIDESK_GITEA_WEBHOOK_RETRY_TERMINAL_DELAY_MS value: ${yamlQuote(String(sync.bridge.retry.terminalRetryDelayMs))} - name: UNIDESK_GITEA_WEBHOOK_WORKER_ATTEMPT_TIMEOUT_MS value: ${yamlQuote(String(sync.bridge.retry.attemptTimeoutMs))} - name: UNIDESK_GITEA_WEBHOOK_WORKER_SCAN_INTERVAL_MS value: ${yamlQuote(String(sync.bridge.retry.scanIntervalMs))} - name: UNIDESK_GITEA_WEBHOOK_MAX_BODY_BYTES value: ${yamlQuote(String(sync.ingressRetry.maxBodyBytes))} - name: UNIDESK_GITEA_WEBHOOK_INBOX_PATH value: ${yamlQuote(sync.bridge.inbox.path)} - name: UNIDESK_GITEA_WEBHOOK_INBOX_MAX_BYTES value: ${yamlQuote(String(sync.bridge.inbox.maxBytes))} - name: UNIDESK_GITEA_WEBHOOK_COMMITTED_RETENTION_SECONDS value: ${yamlQuote(String(sync.bridge.inbox.committedRetentionSeconds))} - name: UNIDESK_GITEA_WEBHOOK_CLEANUP_INTERVAL_MS value: ${yamlQuote(String(sync.bridge.inbox.cleanupIntervalMs))} - name: UNIDESK_GITEA_WEBHOOK_SHUTDOWN_GRACE_MS value: ${yamlQuote(String(sync.bridge.shutdownGraceMs))} - name: GITHUB_TOKEN valueFrom: secretKeyRef: name: ${sync.bridge.secretName} key: github-token - name: GITEA_USERNAME valueFrom: secretKeyRef: name: ${sync.bridge.secretName} key: gitea-username - name: GITEA_PASSWORD valueFrom: secretKeyRef: name: ${sync.bridge.secretName} key: gitea-password - name: GITHUB_WEBHOOK_SECRET valueFrom: secretKeyRef: name: ${sync.bridge.secretName} key: github-webhook-secret volumeMounts: - name: candidate-config mountPath: /etc/gitea-github-sync-candidate readOnly: true - name: candidate-inbox mountPath: ${sync.bridge.inbox.path} volumes: - name: candidate-config configMap: name: ${gate.configMapName} - name: candidate-inbox emptyDir: {} ` : ""; return `${candidateManifest}--- apiVersion: v1 kind: ServiceAccount metadata: name: ${sync.bridge.serviceAccountName} namespace: ${target.namespace} labels: ${labels} --- apiVersion: v1 kind: ConfigMap metadata: name: ${sync.bridge.configMapName} namespace: ${target.namespace} labels: ${labels} data: repos.json: | ${indentBlock(reposJson, 4)} server.mjs: | ${indentBlock(serverSource, 4)} entrypoint.mjs: | ${indentBlock(entrypointSource, 4)} --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: ${sync.bridge.inbox.claimName} namespace: ${target.namespace} labels: ${labels} spec: accessModes: - ReadWriteOnce storageClassName: ${target.storageClassName} resources: requests: storage: ${sync.bridge.inbox.storageSize} --- apiVersion: v1 kind: Service metadata: name: ${sync.bridge.serviceName} namespace: ${target.namespace} labels: ${labels} spec: type: ClusterIP selector: app.kubernetes.io/name: ${sync.bridge.deploymentName} app.kubernetes.io/component: github-to-gitea-sync ports: - name: http port: ${sync.bridge.httpPort} targetPort: http protocol: TCP --- apiVersion: apps/v1 kind: Deployment metadata: name: ${sync.bridge.deploymentName} namespace: ${target.namespace} labels: ${labels} annotations: unidesk.ai/sync-direction: ${yamlQuote(sync.direction)} unidesk.ai/public-path: ${yamlQuote(sync.publicPath)} spec: replicas: ${sync.bridge.replicas} strategy: type: Recreate selector: matchLabels: app.kubernetes.io/name: ${sync.bridge.deploymentName} app.kubernetes.io/component: github-to-gitea-sync template: metadata: labels: app.kubernetes.io/name: ${sync.bridge.deploymentName} app.kubernetes.io/component: github-to-gitea-sync app.kubernetes.io/part-of: devops-infra app.kubernetes.io/managed-by: unidesk annotations: unidesk.ai/github-sync-config-sha: ${yamlQuote(configHash)} spec: serviceAccountName: ${sync.bridge.serviceAccountName} terminationGracePeriodSeconds: ${Math.ceil(sync.bridge.shutdownGraceMs / 1000) + 5} containers: - name: github-to-gitea-sync image: ${sync.bridge.image} imagePullPolicy: IfNotPresent command: - node - /etc/gitea-github-sync/entrypoint.mjs ports: - name: http containerPort: ${sync.bridge.httpPort} env: - name: UNIDESK_GITEA_WEBHOOK_PORT value: ${yamlQuote(String(sync.bridge.httpPort))} - name: UNIDESK_GITEA_WEBHOOK_PATH value: ${yamlQuote(sync.publicPath)} - name: UNIDESK_GITEA_WEBHOOK_RESPONSE_BUDGET_MS value: ${yamlQuote(String(sync.responseBudgetMs))} - name: UNIDESK_GITEA_REPOS_PATH value: /etc/gitea-github-sync/repos.json - name: UNIDESK_GITEA_SERVICE_BASE_URL value: ${yamlQuote(`http://${gitea.app.serviceName}.${target.namespace}.svc.cluster.local:${gitea.app.service.httpPort}`)} - name: UNIDESK_GITEA_WEBHOOK_RETRY_MAX_ATTEMPTS value: ${yamlQuote(String(sync.bridge.retry.maxAttempts))} - name: UNIDESK_GITEA_WEBHOOK_RETRY_INITIAL_DELAY_MS value: ${yamlQuote(String(sync.bridge.retry.initialDelayMs))} - name: UNIDESK_GITEA_WEBHOOK_RETRY_MAX_DELAY_MS value: ${yamlQuote(String(sync.bridge.retry.maxDelayMs))} - name: UNIDESK_GITEA_WEBHOOK_RETRY_TERMINAL_DELAY_MS value: ${yamlQuote(String(sync.bridge.retry.terminalRetryDelayMs))} - name: UNIDESK_GITEA_WEBHOOK_WORKER_ATTEMPT_TIMEOUT_MS value: ${yamlQuote(String(sync.bridge.retry.attemptTimeoutMs))} - name: UNIDESK_GITEA_WEBHOOK_WORKER_SCAN_INTERVAL_MS value: ${yamlQuote(String(sync.bridge.retry.scanIntervalMs))} - name: UNIDESK_GITEA_WEBHOOK_MAX_BODY_BYTES value: ${yamlQuote(String(sync.ingressRetry.maxBodyBytes))} - name: UNIDESK_GITEA_WEBHOOK_INBOX_PATH value: ${yamlQuote(sync.bridge.inbox.path)} - name: UNIDESK_GITEA_WEBHOOK_INBOX_MAX_BYTES value: ${yamlQuote(String(sync.bridge.inbox.maxBytes))} - name: UNIDESK_GITEA_WEBHOOK_COMMITTED_RETENTION_SECONDS value: ${yamlQuote(String(sync.bridge.inbox.committedRetentionSeconds))} - name: UNIDESK_GITEA_WEBHOOK_CLEANUP_INTERVAL_MS value: ${yamlQuote(String(sync.bridge.inbox.cleanupIntervalMs))} - name: UNIDESK_GITEA_WEBHOOK_SHUTDOWN_GRACE_MS value: ${yamlQuote(String(sync.bridge.shutdownGraceMs))} - name: GITHUB_TOKEN valueFrom: secretKeyRef: name: ${sync.bridge.secretName} key: github-token - name: GITEA_USERNAME valueFrom: secretKeyRef: name: ${sync.bridge.secretName} key: gitea-username - name: GITEA_PASSWORD valueFrom: secretKeyRef: name: ${sync.bridge.secretName} key: gitea-password - name: GITHUB_WEBHOOK_SECRET valueFrom: secretKeyRef: name: ${sync.bridge.secretName} key: github-webhook-secret - name: HTTPS_PROXY value: ${yamlQuote(gitea.sourceAuthority.githubProxy.enabled ? gitea.sourceAuthority.githubProxy.url : "")} - name: HTTP_PROXY value: ${yamlQuote(gitea.sourceAuthority.githubProxy.enabled ? gitea.sourceAuthority.githubProxy.url : "")} - name: NO_PROXY value: ${yamlQuote(gitea.sourceAuthority.githubProxy.noProxy.join(","))} readinessProbe: httpGet: path: /healthz port: http periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 6 volumeMounts: - name: config mountPath: /etc/gitea-github-sync readOnly: true - name: durable-inbox mountPath: ${sync.bridge.inbox.path} volumes: - name: config configMap: name: ${sync.bridge.configMapName} - name: durable-inbox persistentVolumeClaim: claimName: ${sync.bridge.inbox.claimName} `; } export function renderGiteaWebhookSyncDesiredManifest(targetId: string): string { const gitea = readGiteaConfig(); const delivery = gitea.sourceAuthority.webhookSync.gitOpsDelivery; const target = resolveTarget(gitea, targetId); if (!delivery.enabled) throw new Error(`${configLabel}.sourceAuthority.webhookSync.gitOpsDelivery.enabled must be true`); if (target.id !== delivery.targetId) throw new Error(`GitOps delivery target ${delivery.targetId} does not match requested target ${target.id}`); return [ renderGithubSyncManifest(gitea, target).trim(), renderPlatformInfraGiteaDesiredFragments(target.id).trim(), ].filter((item) => item.length > 0).join("\n---\n") + "\n"; } export function readGiteaWebhookGitOpsDelivery(): GiteaWebhookGitOpsDelivery { return readGiteaConfig().sourceAuthority.webhookSync.gitOpsDelivery; } function envVars(gitea: GiteaConfig, target: GiteaTarget): string { const app = gitea.app; const values: Record = { 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__webhook__ALLOWED_HOST_LIST: app.webhook.allowedHostList, 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" | "mirror-bootstrap" | "mirror-sync" | "mirror-status" | "mirror-webhook-apply" | "mirror-webhook-status", gitea: GiteaConfig, target: GiteaTarget, manifest: string, options: ApplyOptions | MirrorOptions, params: MirrorRemoteParams = {}): { script: string; payloadSummary: Record } { const frpcExposure = targetFrpcExposure(gitea, target); const sync = targetWebhookSync(gitea, target); const env: Record = { 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_ROOT_URL: gitea.app.server.rootUrl, UNIDESK_GITEA_GITHUB_PROXY_ENABLED: gitea.sourceAuthority.githubProxy.enabled ? "1" : "0", UNIDESK_GITEA_GITHUB_PROXY_URL: gitea.sourceAuthority.githubProxy.url, UNIDESK_GITEA_NO_PROXY: gitea.sourceAuthority.githubProxy.noProxy.join(","), UNIDESK_GITEA_FRPC_ENABLED: frpcExposure === null ? "0" : "1", UNIDESK_GITEA_FRPC_DEPLOYMENT_NAME: frpcExposure?.frpc.deploymentName ?? gitea.app.publicExposure.frpc.deploymentName, UNIDESK_GITEA_FRPC_SECRET_NAME: frpcExposure?.frpc.secretName ?? gitea.app.publicExposure.frpc.secretName, UNIDESK_GITEA_FRPC_SECRET_KEY: frpcExposure?.frpc.secretKey ?? gitea.app.publicExposure.frpc.secretKey, UNIDESK_GITEA_WEBHOOK_SYNC_ENABLED: gitea.sourceAuthority.webhookSync.enabled ? "1" : "0", UNIDESK_GITEA_WEBHOOK_PUBLIC_URL: githubWebhookPublicUrl(gitea, target), UNIDESK_GITEA_WEBHOOK_PATH: sync.publicPath, UNIDESK_GITEA_WEBHOOK_EVENTS: sync.events.join(","), UNIDESK_GITEA_WEBHOOK_DEPLOYMENT: sync.bridge.deploymentName, UNIDESK_GITEA_WEBHOOK_SERVICE: sync.bridge.serviceName, UNIDESK_GITEA_WEBHOOK_SERVICE_PORT: String(sync.bridge.httpPort), UNIDESK_GITEA_WEBHOOK_SECRET_NAME: sync.bridge.secretName, }; if (params.frpcSecret !== undefined) { env.UNIDESK_GITEA_FRPC_SECRET_NAME = params.frpcSecret.secretName; env.UNIDESK_GITEA_FRPC_SECRET_KEY = params.frpcSecret.secretKey; } if (params.secrets !== undefined) { env.UNIDESK_GITEA_ADMIN_USERNAME = params.secrets.adminUsername; env.UNIDESK_GITEA_ADMIN_PASSWORD = params.secrets.adminPassword; env.UNIDESK_GITEA_GITHUB_TOKEN = params.secrets.githubToken; env.UNIDESK_GITEA_GITHUB_WEBHOOK_SECRET = params.secrets.webhookSecret; } const exports = Object.entries(env).map(([key, value]) => `export ${key}=${shQuote(value)}`).join("\n"); const payloads = { manifest, repositories: JSON.stringify((params.repos ?? []).map((repo) => remoteRepoSpec(repo))), statusEvaluator: readFileSync(statusEvaluatorFile, "utf8"), frpcToml: params.frpcSecret?.frpcToml ?? "", }; return { script: `${exports}\n${renderGiteaRemotePayloadMaterializer(payloads)}\n${readFileSync(remoteScriptFile, "utf8")}`, payloadSummary: summarizeGiteaRemotePayloads(payloads), }; } function policyChecks(gitea: GiteaConfig, target: GiteaTarget, manifest: string): Array> { const sync = targetWebhookSync(gitea, target); const delivery = sync.gitOpsDelivery; 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: "gitea-pac-service-contract", ok: target.namespace === "devops-infra" && gitea.app.serviceName === "gitea-http", detail: "The service matches the active Gitea source authority consumed by config/platform-infra/pipelines-as-code.yaml." }, { 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-not-trigger-authority", ok: gitea.app.actions.enabled, detail: "Gitea may expose its built-in Actions capability, but JD01 CI/CD trigger authority is Pipelines-as-Code, not act_runner." }, { 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." }, { name: "durable-inbox-single-writer", ok: sync.bridge.replicas === 1 && /strategy:\n\s+type: Recreate/u.test(manifest), detail: "The durable inbox has one bridge replica and Deployment strategy Recreate, so journal writes have one owner during rollout.", }, { name: "bridge-candidate-presync-gate", ok: sync.bridge.candidateGate.enabled && manifest.includes(`name: ${sync.bridge.candidateGate.configMapName}`) && manifest.includes(`name: ${sync.bridge.candidateGate.jobName}`) && manifest.includes("argocd.argoproj.io/hook: PreSync") && manifest.includes("app.kubernetes.io/component: github-to-gitea-sync-candidate") && manifest.includes("candidate-inbox\n emptyDir: {}"), detail: "Argo starts the exact ConfigMap-mounted bridge candidate with an isolated inbox before replacing the single-writer Deployment.", }, { name: "durable-inbox-pvc", ok: manifest.includes(`kind: PersistentVolumeClaim\nmetadata:\n name: ${sync.bridge.inbox.claimName}`) && manifest.includes(`claimName: ${sync.bridge.inbox.claimName}`) && manifest.includes(`mountPath: ${sync.bridge.inbox.path}`), detail: "The webhook bridge fsync inbox path, PVC claim, capacity and retention are rendered from the owning Gitea YAML.", }, { name: "durable-bridge-service-account", ok: manifest.includes(`kind: ServiceAccount\nmetadata:\n name: ${sync.bridge.serviceAccountName}`) && manifest.includes(`serviceAccountName: ${sync.bridge.serviceAccountName}`), detail: "The bridge ServiceAccount and Deployment binding are rendered together; the bootstrap PaC task continues to use the pre-existing default ServiceAccount.", }, { name: "independent-gitops-self-delivery", ok: delivery.targetId !== target.id || (delivery.enabled && existsSync(rootPath(".tekton", "platform-infra-gitea-nc01-pac.yaml")) && delivery.bootstrapApplicationPath.startsWith("deploy/gitops/unidesk-host/") && !delivery.desiredManifestPath.startsWith("deploy/gitops/unidesk-host/")), detail: "An independent PaC task publishes a child Argo Application into the existing unidesk-host bootstrap path and keeps bridge desired resources on an independent path.", }, ]; } function configSummary(gitea: GiteaConfig, target: GiteaTarget): Record { return { path: configLabel, metadata: gitea.metadata, migration: gitea.migration, target: targetSummary(target), app: appSummary(gitea), valuesPrinted: false, }; } function compactConfigSummary(gitea: GiteaConfig, target: GiteaTarget): Record { 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, publicBaseUrl: gitea.app.publicExposure.publicBaseUrl, actionsEnabled: gitea.app.actions.enabled, }, valuesPrinted: false, }; } function targetSummary(target: GiteaTarget): Record { return { id: target.id, route: target.route, namespace: target.namespace, role: target.role, createNamespace: target.createNamespace, storageClassName: target.storageClassName, publicExposureEnabled: target.publicExposureEnabled, webhookSync: target.webhookSync, }; } function appSummary(gitea: GiteaConfig): Record { 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, publicExposure: publicExposureSummary(gitea), actionsEnabled: gitea.app.actions.enabled, registrationDisabled: gitea.app.registration.disabled, storage: gitea.app.storage, healthPath: gitea.validation.healthPath, }; } function publicServiceTarget(gitea: GiteaConfig, target: GiteaTarget): PublicServiceTarget { return { id: target.id, route: target.route, namespace: target.namespace, replicas: 1, publicExposure: targetPublicExposure(gitea, target), }; } function publicExposureEnabled(gitea: GiteaConfig, target: GiteaTarget): boolean { return gitea.app.publicExposure.enabled && target.publicExposureEnabled; } function targetPublicExposure(gitea: GiteaConfig, target: GiteaTarget): GiteaConfig["app"]["publicExposure"] { const frpcExposure = targetFrpcExposure(gitea, target); return frpcExposure === null ? { ...gitea.app.publicExposure, enabled: false } : frpcExposure; } function prepareGiteaFrpcSecret(gitea: GiteaConfig, target: GiteaTarget): FrpcSecretMaterial | undefined { const exposure = targetFrpcExposure(gitea, target); if (exposure === null) return undefined; const base = prepareFrpcSecret({ secretRoot: gitea.app.publicExposure.secretRoot, exposure, sourcePathRedactor: redactRepoPath, parseEnvFile, requiredEnvValue, readTextFile, }); const sync = targetWebhookSync(gitea, target); if (!sync.enabled || !publicExposureEnabled(gitea, target)) return base; const extraProxy = [ "[[proxies]]", `name = "${escapeTomlString(sync.frpc.proxyName)}"`, 'type = "tcp"', `localIP = "${escapeTomlString(sync.bridge.serviceName)}.${escapeTomlString(target.namespace)}.svc.cluster.local"`, `localPort = ${sync.bridge.httpPort}`, `remotePort = ${sync.frpc.remotePort}`, "", ].join("\n"); const frpcToml = `${base.frpcToml.trim()}\n\n${extraProxy}`; return { ...base, frpcToml, fingerprint: fingerprintValues({ frpcToml }, ["frpcToml"]), }; } function targetFrpcExposure(gitea: GiteaConfig, target: GiteaTarget): GiteaConfig["app"]["publicExposure"] | null { if (publicExposureEnabled(gitea, target)) return gitea.app.publicExposure; if (target.webhookSync === null || !gitea.sourceAuthority.webhookSync.enabled) return null; const sync = targetWebhookSync(gitea, target); return { ...gitea.app.publicExposure, enabled: true, frpc: { ...gitea.app.publicExposure.frpc, proxyName: sync.frpc.proxyName, remotePort: sync.frpc.remotePort, localIP: `${sync.bridge.serviceName}.${target.namespace}.svc.cluster.local`, localPort: sync.bridge.httpPort, }, }; } function targetWebhookSync(gitea: GiteaConfig, target: GiteaTarget): GiteaWebhookSync { const base = gitea.sourceAuthority.webhookSync; if (target.webhookSync === null) return base; return { ...base, publicPath: target.webhookSync.publicPath, frpc: target.webhookSync.frpc, }; } function caddyExposureNeeded(gitea: GiteaConfig, target: GiteaTarget): boolean { return publicExposureEnabled(gitea, target) || (gitea.sourceAuthority.webhookSync.enabled && target.webhookSync !== null); } async function applyGiteaCaddyBlock(config: UniDeskConfig, gitea: GiteaConfig): Promise> { const exposure = gitea.app.publicExposure; const ingressRetry = gitea.sourceAuthority.webhookSync.ingressRetry; const webhookHandles = webhookCaddyRoutes(gitea) .sort((left, right) => right.publicPath.length - left.publicPath.length) .map((route) => renderGiteaWebhookCaddyHandle( route, ingressRetry, gitea.sourceAuthority.webhookSync.responseBudgetMs, exposure.pk01.responseHeaderTimeoutSeconds, )) .join("\n\n"); if (webhookHandles === "") return await applyPk01CaddyBlock(config, "gitea", exposure); const siteBlock = `${exposure.dns.hostname} { ${webhookHandles} handle { reverse_proxy 127.0.0.1:${exposure.frpc.remotePort} { transport http { response_header_timeout ${exposure.pk01.responseHeaderTimeoutSeconds}s } } } }`; return await applyPk01CaddySiteBlock(config, "gitea", exposure, siteBlock, caddyManagedBlockMarkers("gitea")); } function webhookCaddyRoutes(gitea: GiteaConfig): Array<{ publicPath: string; remotePort: number }> { const sync = gitea.sourceAuthority.webhookSync; if (!sync.enabled) return []; const routes = new Map(); routes.set(sync.publicPath, { publicPath: sync.publicPath, remotePort: sync.frpc.remotePort }); for (const target of gitea.targets) { if (!target.enabled || target.webhookSync === null) continue; const targetSync = targetWebhookSync(gitea, target); routes.set(targetSync.publicPath, { publicPath: targetSync.publicPath, remotePort: targetSync.frpc.remotePort }); } return [...routes.values()]; } function publicExposureSummary(gitea: GiteaConfig): Record { const exposure = gitea.app.publicExposure; return { enabled: exposure.enabled, publicBaseUrl: exposure.publicBaseUrl, internalReadUrlPolicy: "k3s-consumers-use-gitea-http-svc-not-public-url", dns: exposure.dns, frpc: { deploymentName: exposure.frpc.deploymentName, remotePort: exposure.frpc.remotePort, localIP: exposure.frpc.localIP, localPort: exposure.frpc.localPort, tokenSourceRef: exposure.frpc.tokenSourceRef, valuesPrinted: false, }, pk01: exposure.pk01, }; } function frpcSecretSummary(secret: FrpcSecretMaterial | undefined): Record { if (secret === undefined) return { enabled: false, skipped: true, reason: "target-public-exposure-disabled", valuesPrinted: false }; return { sourceRef: secret.sourceRef, sourcePath: secret.sourcePath, secretName: secret.secretName, secretKey: secret.secretKey, fingerprint: secret.fingerprint, valuesPrinted: false, }; } function webhookSecretSummary(gitea: GiteaConfig, target: GiteaTarget, secrets: MirrorSecrets): Record { const sync = targetWebhookSync(gitea, target); return { enabled: sync.enabled, publicUrl: githubWebhookPublicUrl(gitea, target), sourceRef: sync.secret.sourceRef, targetSecret: sync.bridge.secretName, keys: ["github-token", "gitea-username", "gitea-password", "github-webhook-secret"], fingerprint: fingerprintSecretParts([secrets.webhookSecret]), valuesPrinted: false, }; } function githubWebhookPublicUrl(gitea: GiteaConfig, target: GiteaTarget): string { const base = gitea.app.publicExposure.publicBaseUrl.replace(/\/+$/u, ""); return `${base}${targetWebhookSync(gitea, target).publicPath}`; } function statusSummary(payload: Record): Record { 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 sourceAuthoritySummary(gitea: GiteaConfig, target: GiteaTarget): Record { return { enabled: gitea.sourceAuthority.enabled, stage: gitea.sourceAuthority.stage, statusAuthority: gitea.sourceAuthority.statusAuthority, firstCiConsumer: gitea.sourceAuthority.firstCiConsumer, target: target.id, serviceBaseUrl: gitea.app.server.rootUrl, repositoryCount: repositoriesForTarget(gitea, target).length, }; } function responsibilitySummary(item: GiteaSourceResponsibility): Record { return { ...item }; } function repositoriesForTarget(gitea: GiteaConfig, target: GiteaTarget): GiteaMirrorRepository[] { return gitea.sourceAuthority.repositories.filter((repo) => repo.targetId.toLowerCase() === target.id.toLowerCase()); } function selectedRepositories(gitea: GiteaConfig, target: GiteaTarget, repoKey: string | null): GiteaMirrorRepository[] { const repos = repositoriesForTarget(gitea, target); if (repoKey === null) return repos; const repo = repos.find((item) => item.key === repoKey); if (repo === undefined) throw new Error(`unknown gitea mirror repo ${repoKey}; known repos: ${repos.map((item) => item.key).join(", ")}`); return [repo]; } function repositorySummary(gitea: GiteaConfig, repo: GiteaMirrorRepository): Record { return { key: repo.key, upstreamRepository: repo.upstream.repository, upstreamBranch: repo.upstream.branch, giteaOwner: repo.gitea.owner, giteaRepo: repo.gitea.name, mirrorMode: repo.gitea.mirrorMode, readUrl: repo.gitea.readUrl, snapshotPrefix: repo.snapshot.prefix, legacyReadUrl: repo.legacyGitMirror.readUrl, legacyDisposition: repo.legacyGitMirror.disposition, sourceAuthority: "gitea", rootUrlMatches: repo.gitea.readUrl.startsWith(gitea.app.server.rootUrl), }; } function remoteRepoSpec(repo: GiteaMirrorRepository): Record { return { key: repo.key, upstream: repo.upstream, gitea: repo.gitea, gitops: repo.gitops, snapshot: repo.snapshot, legacyGitMirror: repo.legacyGitMirror, }; } function mirrorReadOnlyNextCommands(targetId: string): Record { return { status: `bun scripts/cli.ts platform-infra gitea mirror status --target ${targetId}`, statusFull: `bun scripts/cli.ts platform-infra gitea mirror status --target ${targetId} --full`, webhookStatus: `bun scripts/cli.ts platform-infra gitea mirror webhook status --target ${targetId}`, webhookStatusFull: `bun scripts/cli.ts platform-infra gitea mirror webhook status --target ${targetId} --full`, fixAutomaticDelivery: PAC_AUTOMATIC_DELIVERY_REFERENCE, }; } function webhookSyncSummary(gitea: GiteaConfig, target: GiteaTarget): Record { const sync = targetWebhookSync(gitea, target); const ingressAttemptBudgetMs = sync.ingressRetry.enabled ? deriveGiteaWebhookAttemptBudgetMs(sync.responseBudgetMs, sync.ingressRetry) : null; const ingressResponseHeaderTimeoutMs = sync.ingressRetry.enabled ? deriveGiteaWebhookResponseHeaderTimeoutMs(sync.responseBudgetMs, sync.ingressRetry) : null; return { enabled: sync.enabled, direction: sync.direction, publicUrl: githubWebhookPublicUrl(gitea, target), events: sync.events, responseBudgetMs: sync.responseBudgetMs, ingressAttemptBudgetMs, ingressResponseHeaderTimeoutMs, ingressRetry: { ...sync.ingressRetry, method: "POST", bodyReplay: "request-body-size-limited-and-fully-buffered", }, bridge: sync.bridge, gitOpsDelivery: sync.gitOpsDelivery, frpc: sync.frpc, valuesPrinted: false, }; } function credentialSummaries(gitea: GiteaConfig): Array> { const adminPath = credentialPath(gitea, gitea.sourceAuthority.credentials.admin.sourceRef); const github = gitea.sourceAuthority.credentials.github; const githubPath = credentialPath(gitea, github.sourceRef); const adminLinePair = existsSync(adminPath) ? parseLinePairCredential(adminPath, gitea.sourceAuthority.credentials.admin) : { username: null, password: null }; const githubEnv = existsSync(githubPath) ? parseGithubCredentialFile(githubPath, github) : {}; return [ { id: "gitea-admin", format: gitea.sourceAuthority.credentials.admin.format, sourceRef: gitea.sourceAuthority.credentials.admin.sourceRef, sourcePath: adminPath, present: existsSync(adminPath), requiredKeysPresent: Boolean(adminLinePair.username) && Boolean(adminLinePair.password), fingerprint: fingerprintSecretParts([adminLinePair.username, adminLinePair.password]), valuesPrinted: false, }, { id: "github-upstream", transport: github.transport, sourceRef: github.sourceRef, sourcePath: githubPath, present: existsSync(githubPath), requiredKeysPresent: Boolean(githubEnv[github.sourceKey]), fingerprint: fingerprintKeys(githubEnv, [github.sourceKey]), valuesPrinted: false, }, ]; } function credentialBlockers(credentials: Array>): string[] { return credentials.filter((item) => item.present !== true || item.requiredKeysPresent !== true).map((item) => String(item.id)); } function ensureMirrorSecrets(gitea: GiteaConfig, createAdmin: boolean, createWebhookSecret: boolean): MirrorSecrets { const admin = gitea.sourceAuthority.credentials.admin; const adminPath = credentialPath(gitea, admin.sourceRef); if (!existsSync(adminPath)) { if (!createAdmin) throw new Error(`${admin.sourceRef} is missing; run mirror bootstrap --confirm first`); mkdirSync(dirname(adminPath), { recursive: true }); const username = "unidesk-admin"; const password = `udg_${randomBytes(32).toString("base64url")}`; writeFileSync(adminPath, `${username}\n${password}\n`, { encoding: "utf8", mode: 0o600 }); chmodSync(adminPath, 0o600); } const adminLinePair = parseLinePairCredential(adminPath, admin); const github = gitea.sourceAuthority.credentials.github; const githubPath = credentialPath(gitea, github.sourceRef); if (!existsSync(githubPath)) throw new Error(`${github.sourceRef} is missing; cannot sync GitHub upstream`); const githubEnv = parseGithubCredentialFile(githubPath, github); const webhookSecret = ensureWebhookSecret(gitea, createWebhookSecret); const adminUsername = adminLinePair.username; const adminPassword = adminLinePair.password; const githubToken = githubEnv[github.sourceKey]; if (!adminUsername || !adminPassword) throw new Error(`${admin.sourceRef} must contain username on line ${admin.usernameLine} and password on line ${admin.passwordLine}`); if (!githubToken) throw new Error(`${github.sourceRef} must contain ${github.sourceKey}`); return { adminUsername, adminPassword, githubToken, webhookSecret }; } function ensureWebhookSecret(gitea: GiteaConfig, createIfMissing: boolean): string { const sync = gitea.sourceAuthority.webhookSync; if (!sync.enabled) return ""; const path = credentialPath(gitea, sync.secret.sourceRef); if (!existsSync(path)) { if (!createIfMissing || !sync.secret.createIfMissing) throw new Error(`${sync.secret.sourceRef} is missing; run platform-infra gitea apply --target --confirm first`); mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, `${sync.secret.sourceKey}=ghs_${randomBytes(32).toString("base64url")}\n`, { encoding: "utf8", mode: 0o600 }); chmodSync(path, 0o600); } const values = parseEnvFileSafe(path); const value = values[sync.secret.sourceKey]; if (!value) throw new Error(`${sync.secret.sourceRef} must contain ${sync.secret.sourceKey}`); return value; } function credentialPath(gitea: GiteaConfig, sourceRef: string): string { return sourceRef.startsWith("/") ? sourceRef : join(gitea.sourceAuthority.credentials.sourceRoot, sourceRef); } function parseLinePairCredential(path: string, admin: GiteaAdminCredential): { username: string | null; password: string | null } { const lines = readFileSync(path, "utf8").split(/\r?\n/u); return { username: lines[admin.usernameLine - 1]?.trim() || null, password: lines[admin.passwordLine - 1]?.trim() || null, }; } function parseEnvFileSafe(path: string): Record { const values: Record = {}; for (const rawLine of readFileSync(path, "utf8").split(/\r?\n/u)) { let line = rawLine.trim(); if (line.length === 0 || line.startsWith("#")) continue; if (line.startsWith("export ")) line = line.slice("export ".length).trim(); const eq = line.indexOf("="); if (eq <= 0) continue; const key = line.slice(0, eq).trim(); if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) continue; values[key] = unquote(line.slice(eq + 1).trim()); } return values; } function parseGithubCredentialFile(path: string, github: GiteaGithubCredential): Record { const text = readFileSync(path, "utf8"); if (github.format === "raw-token") return { [github.sourceKey]: text.trim() }; return parseEnvTextSafe(text); } function parseEnvTextSafe(text: string): Record { const values: Record = {}; for (const rawLine of text.split(/\r?\n/u)) { let line = rawLine.trim(); if (line.length === 0 || line.startsWith("#")) continue; if (line.startsWith("export ")) line = line.slice("export ".length).trim(); const eq = line.indexOf("="); if (eq <= 0) continue; const key = line.slice(0, eq).trim(); if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) continue; values[key] = unquote(line.slice(eq + 1).trim()); } return values; } function unquote(value: string): string { if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1); return value; } function fingerprintKeys(values: Record, keys: string[]): string | null { if (keys.some((key) => !values[key])) return null; const hash = createHash("sha256"); for (const key of keys.slice().sort()) { hash.update(key); hash.update("\0"); hash.update(values[key]); hash.update("\0"); } return `sha256:${hash.digest("hex").slice(0, 16)}`; } function fingerprintSecretParts(parts: Array): string | null { if (parts.some((part) => !part)) return null; const hash = createHash("sha256"); for (const part of parts) { hash.update(part ?? ""); hash.update("\0"); } return `sha256:${hash.digest("hex").slice(0, 16)}`; } function renderMirrorPlan(result: Record): RenderedCliResult { const target = record(result.target); const source = record(result.sourceAuthority); const repos = arrayRecords(result.repositories).map((repo) => [stringValue(repo.key), stringValue(repo.upstreamRepository), stringValue(repo.upstreamBranch), stringValue(repo.readUrl), stringValue(repo.legacyDisposition)]); const responsibilities = arrayRecords(result.responsibilities).map((item) => [stringValue(item.name), stringValue(item.current), stringValue(item.target), stringValue(item.disposition)]); const next = record(result.next); return rendered(result, "platform-infra gitea mirror plan", [ "PLATFORM-INFRA GITEA MIRROR PLAN", ...table(["TARGET", "STAGE", "AUTHORITY", "REPOS"], [[stringValue(target.id), stringValue(source.stage), stringValue(source.statusAuthority), stringValue(source.repositoryCount)]]), "", "REPOSITORIES", ...(repos.length === 0 ? ["-"] : table(["KEY", "UPSTREAM", "BRANCH", "GITEA_READ_URL", "LEGACY"], repos)), "", "RESPONSIBILITIES", ...(responsibilities.length === 0 ? ["-"] : table(["NAME", "CURRENT", "TARGET", "DISPOSITION"], responsibilities)), "", "NEXT", ` status: ${stringValue(next.status)}`, ` full: ${stringValue(next.statusFull)}`, ` webhook-status: ${stringValue(next.webhookStatus)}`, ` fix-automatic-delivery: ${stringValue(next.fixAutomaticDelivery)}`, "", "Disclosure: credentials are summarized by presence/fingerprint only.", ]); } function renderMirrorBootstrap(result: Record): RenderedCliResult { const repos = arrayRecords(result.repositories).map((repo) => [stringValue(repo.key), stringValue(repo.owner), stringValue(repo.name), boolText(record(repo.create).ok)]); const next = record(result.next); return rendered(result, "platform-infra gitea mirror bootstrap", [ "PLATFORM-INFRA GITEA MIRROR BOOTSTRAP", ...table(["OK", "MUTATION", "TARGET"], [[boolText(result.ok), boolText(result.mutation), stringValue(record(result.target).id)]]), "", "REPOSITORIES", ...(repos.length === 0 ? ["-"] : table(["KEY", "OWNER", "REPO", "API_OK"], repos)), ...remoteErrorLines(result), "", `NEXT ${stringValue(next.webhookStatus)}`, ]); } function renderMirrorSync(result: Record): RenderedCliResult { const bundles = arrayRecords(result.sourceBundles).map((bundle) => [stringValue(bundle.key), stringValue(bundle.sourceCommit).slice(0, 12), stringValue(bundle.snapshotRef), stringValue(bundle.readUrl)]); const repos = arrayRecords(result.repositories).map((repo) => [ stringValue(repo.key), boolText(repo.syncOk), stringValue(repo.fetchRc), stringValue(repo.pushRc), stringValue(repo.gitopsPushRc), stringValue(repo.sourceCommit).slice(0, 12), compactTail(stringValue(repo.fetchTail) || stringValue(repo.pushTail) || stringValue(repo.revparseTail)), ]); const next = record(result.next); return rendered(result, "platform-infra gitea mirror sync", [ "PLATFORM-INFRA GITEA MIRROR SYNC", ...table(["OK", "MUTATION", "TARGET"], [[boolText(result.ok), boolText(result.mutation), stringValue(record(result.target).id)]]), "", "REPOSITORIES", ...(repos.length === 0 ? ["-"] : table(["KEY", "SYNC", "FETCH", "PUSH", "GITOPS", "COMMIT", "ERROR"], repos)), "", "SOURCE BUNDLES", ...(bundles.length === 0 ? ["-"] : table(["KEY", "COMMIT", "SNAPSHOT_REF", "READ_URL"], bundles)), ...remoteErrorLines(result), "", `NEXT ${stringValue(next.status)}`, ]); } function renderMirrorStatus(result: Record): RenderedCliResult { const source = record(result.sourceAuthority); const service = record(result.serviceHealth); const repos = arrayRecords(result.repositories).map((repo) => [stringValue(repo.key), stringValue(repo.mirrorState), stringValue(repo.branchCommit).slice(0, 12), stringValue(repo.snapshotCommit).slice(0, 12), stringValue(repo.snapshotRef)]); const next = record(result.next); return rendered(result, "platform-infra gitea mirror status", [ "PLATFORM-INFRA GITEA MIRROR STATUS", ...table(["TARGET", "SERVICE", "MIRROR_READY", "AUTHORITY"], [[stringValue(record(result.target).id), boolText(service.ok), boolText(source.mirrorReady), stringValue(source.statusAuthority)]]), "", "REPOSITORIES", ...(repos.length === 0 ? ["-"] : table(["KEY", "STATE", "BRANCH", "SNAPSHOT", "SNAPSHOT_REF"], repos)), ...remoteErrorLines(result), "", `NEXT ${stringValue(next.webhookStatus)}`, ]); } function renderMirrorWebhookApply(result: Record): RenderedCliResult { const repos = arrayRecords(result.repositories).map((repo) => [stringValue(repo.key), stringValue(repo.repository), boolText(repo.hookReady), stringValue(repo.hookId), compactTail(stringValue(repo.error))]); const next = record(result.next); return rendered(result, "platform-infra gitea mirror webhook apply", [ "PLATFORM-INFRA GITEA MIRROR WEBHOOK APPLY", ...table(["OK", "MUTATION", "TARGET", "URL"], [[boolText(result.ok), boolText(result.mutation), stringValue(record(result.target).id), stringValue(record(result.webhook).publicUrl)]]), "", "GITHUB HOOKS", ...(repos.length === 0 ? ["-"] : table(["KEY", "REPOSITORY", "READY", "HOOK_ID", "ERROR"], repos)), ...remoteErrorLines(result), "", `NEXT ${stringValue(next.webhookStatus)}`, ]); } function renderMirrorWebhookStatus(result: Record): RenderedCliResult { const repos = arrayRecords(result.repositories).map((repo) => { const latestPush = record(repo.latestPushDelivery); const latest = record(repo.latestDelivery); const delivery = latestPush.event !== "" ? latestPush : latest; const inbox = record(repo.durableInboxRecord); const inboxResult = record(inbox.result); const inboxState = stringValue(repo.durableInboxState, "missing"); const durableProof = repo.preDurableBootstrap === true ? "pre-durable-bootstrap" : repo.durableInboxCommitted === true ? `${stringValue(inboxResult.disposition)}:${stringValue(inboxResult.sourceCommit).slice(0, 12)}` : "-"; return [ stringValue(repo.key), boolText(repo.hookReady), stringValue(repo.githubHead).slice(0, 12), stringValue(repo.branchCommit).slice(0, 12), stringValue(repo.snapshotCommit).slice(0, 12), boolText(repo.authorityMatches), boolText(repo.bridgeEventStale), `${stringValue(repo.deliveryId)}:${stringValue(delivery.statusCode)}:${stringValue(delivery.status)}`, inboxState, durableProof, compactTail(stringValue(repo.error)), ]; }); const bridge = record(result.bridge); const bridgeLogs = record(result.bridgeLogs); const retry = record(record(record(result.webhook).bridge).retry); const retryText = `${stringValue(retry.maxAttempts)}x/${stringValue(retry.initialDelayMs)}-${stringValue(retry.maxDelayMs)}ms`; const next = record(result.next); return rendered(result, "platform-infra gitea mirror webhook status", [ "PLATFORM-INFRA GITEA MIRROR WEBHOOK STATUS", ...table(["TARGET", "BRIDGE", "READY", "RETRY", "URL", "LOGS"], [[stringValue(record(result.target).id), stringValue(bridge.deployment), boolText(bridge.ready), retryText, stringValue(record(result.webhook).publicUrl), `rc=${stringValue(bridgeLogs.exitCode)}`]]), "", "GITHUB HOOKS", ...(repos.length === 0 ? ["-"] : table(["KEY", "HOOK", "GH_HEAD", "GITEA", "SNAPSHOT", "MATCH", "STALE", "DELIVERY", "DURABLE", "PROOF", "ERROR"], repos)), ...remoteErrorLines(result), "", `NEXT ${stringValue(next.webhookStatusFull)}`, ]); } function renderPlan(result: Record): 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)], ["WEBUI", stringValue(record(app.publicExposure).publicBaseUrl), "internalGit", 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", ` status: ${stringValue(next.status)}`, ` validate: ${stringValue(next.validate)}`, ` mirror-status: ${stringValue(next.mirrorStatus)}`, ` webhook-status: ${stringValue(next.webhookStatus)}`, ` fix-automatic-delivery: ${stringValue(next.fixAutomaticDelivery)}`, "", "Boundary: Gitea is internal ClusterIP source authority for GH-1560; CI trigger authority is Pipelines-as-Code, not Gitea Actions/act_runner.", "Disclosure: Secret values are not printed; this stage does not create runner credentials.", ]); } function renderApply(result: Record): RenderedCliResult { const target = record(result.target); const payload = record(result.payloadTransport); const remote = record(result.remote); const steps = record(remote.steps); const serverDryRunStep = record(steps.serverDryRun); const frpcStep = record(steps.frpcSecret); const frpcCleanupStep = record(steps.frpcCleanup); const webhookSecretStep = record(steps.webhookSyncSecret); const applyStep = record(steps.apply); const rolloutStep = record(steps.rollout); const frpcRolloutStep = record(steps.frpcRollout); const webhookRolloutStep = record(steps.webhookSyncRollout); const caddy = record(result.pk01Caddy); 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)]]), ...table(["TRANSPORT", "BYTES", "MATERIALIZATION", "VALUES"], [[stringValue(payload.kind), stringValue(payload.totalBytes), stringValue(payload.materialization), stringValue(payload.valuesPrinted)]]), "", "STEPS", ...table(["STEP", "EXIT", "DETAIL"], [ ["serverDryRun", stringValue(serverDryRunStep.exitCode), compactLine(stringValue(serverDryRunStep.stderrTail, stringValue(serverDryRunStep.stdoutTail)))], ["frpc-secret", stringValue(frpcStep.exitCode), compactLine(stringValue(frpcStep.stderrTail, stringValue(frpcStep.stdoutTail)))], ["frpc-cleanup", stringValue(frpcCleanupStep.exitCode), compactLine(stringValue(frpcCleanupStep.stderrTail, stringValue(frpcCleanupStep.stdoutTail)))], ["webhook-secret", stringValue(webhookSecretStep.exitCode), compactLine(stringValue(webhookSecretStep.stderrTail, stringValue(webhookSecretStep.stdoutTail)))], ["apply", stringValue(applyStep.exitCode), compactLine(stringValue(applyStep.stderrTail, stringValue(applyStep.stdoutTail)))], ["rollout", stringValue(rolloutStep.exitCode), compactLine(stringValue(rolloutStep.stderrTail, stringValue(rolloutStep.stdoutTail)))], ["frpc-rollout", stringValue(frpcRolloutStep.exitCode), compactLine(stringValue(frpcRolloutStep.stderrTail, stringValue(frpcRolloutStep.stdoutTail)))], ["webhook-rollout", stringValue(webhookRolloutStep.exitCode), compactLine(stringValue(webhookRolloutStep.stderrTail, stringValue(webhookRolloutStep.stdoutTail)))], ["pk01-caddy", stringValue(caddy.exitCode ?? (caddy.ok === false ? 1 : 0)), compactLine(stringValue(caddy.error, stringValue(caddy.status, stringValue(caddy.reason))))], ]), ...remoteErrorLines(result), "", `NEXT bun scripts/cli.ts platform-infra gitea status --target ${stringValue(target.id)}`, ]); } function renderStatus(result: Record): 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[] { 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 compactTail(value: string): string { const line = value.split(/\r?\n/u).map((item) => item.trim()).filter(Boolean).at(-1) ?? ""; return line.length > 96 ? `${line.slice(0, 93)}...` : line; } function rendered(result: Record, 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 parseMirrorOptions(args: string[]): MirrorOptions { const commonArgs: string[] = []; let confirm = false; let repoKey: string | null = null; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--confirm") { confirm = true; } else if (arg === "--repo") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new Error("--repo requires a value"); if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error("--repo must be a simple repo key"); repoKey = value; index += 1; } else { commonArgs.push(arg); if (arg === "--target" || arg === "--node") { commonArgs.push(args[index + 1] ?? ""); index += 1; } } } return { ...parseCommonOptions(commonArgs), confirm, repoKey }; } 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 giteaReadOnlyNextCommands(targetId: string): Record { return { status: `bun scripts/cli.ts platform-infra gitea status --target ${targetId}`, validate: `bun scripts/cli.ts platform-infra gitea validate --target ${targetId}`, mirrorStatus: `bun scripts/cli.ts platform-infra gitea mirror status --target ${targetId}`, webhookStatus: `bun scripts/cli.ts platform-infra gitea mirror webhook status --target ${targetId}`, fixAutomaticDelivery: PAC_AUTOMATIC_DELIVERY_REFERENCE, }; } function manifestObjectSummary(yaml: string): Array> { const objects: Array> = []; 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, target: Record): string { const service = record(app.service); return `${stringValue(app.serviceName)}.${stringValue(target.namespace)}.svc.cluster.local:${stringValue(service.httpPort)}`; } function positiveInteger(obj: Record, 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, 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 literalField(obj: Record, key: string, path: string, allowed: T[]): T { const value = y.stringField(obj, key, path); if (!allowed.includes(value as T)) throw new Error(`${configLabel}.${path}.${key} must be one of ${allowed.join(", ")}`); return value as T; } function optionalLiteralField(obj: Record, key: string, path: string, allowed: readonly T[]): T | null { if (obj[key] === undefined || obj[key] === null) return null; const value = y.stringField(obj, key, path); if (!allowed.includes(value as T)) throw new Error(`${configLabel}.${path}.${key} must be one of ${allowed.join(", ")}`); return value as T; } function quantity(obj: Record, 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 secretSourceRefField(obj: Record, key: string, path: string): string { const value = y.stringField(obj, key, path); if (value.includes("..") || !/^[A-Za-z0-9_./-]+$/u.test(value)) throw new Error(`${configLabel}.${path}.${key} must be a source ref path without ..`); return value; } function repositoryField(obj: Record, key: string, path: string): string { const value = y.stringField(obj, key, path); if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(value)) throw new Error(`${configLabel}.${path}.${key} must be owner/repo`); return value; } function giteaRepoNameField(obj: Record, key: string, path: string): string { const value = y.stringField(obj, key, path); if (!/^[A-Za-z0-9_.-]+$/u.test(value) || value.includes("..")) throw new Error(`${configLabel}.${path}.${key} must be a Gitea repository name`); return value; } function gitCloneUrlField(obj: Record, key: string, path: string): string { const value = urlField(obj, key, path); if (!/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\.git$/u.test(value)) throw new Error(`${configLabel}.${path}.${key} must be an official GitHub HTTPS clone URL`); return value; } function refPrefixField(obj: Record, key: string, path: string): string { const value = y.stringField(obj, key, path); if (!/^refs\/[A-Za-z0-9._/-]+$/u.test(value) || value.includes("..")) throw new Error(`${configLabel}.${path}.${key} must be a git ref prefix`); return value.replace(/\/+$/u, ""); } function urlField(obj: Record, 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 gitBranchField(obj: Record, key: string, path: string): string { const value = y.stringField(obj, key, path); if (!/^[A-Za-z0-9._/-]+$/u.test(value) || value.startsWith("/") || value.endsWith("/") || value.includes("..") || value.includes("//")) { throw new Error(`${configLabel}.${path}.${key} must be a safe Git branch name`); } return value; } function safeRelativePathField(obj: Record, key: string, path: string): string { const value = y.stringField(obj, key, path); if (value.startsWith("/") || value.endsWith("/") || value.split("/").some((segment) => segment === "" || segment === "." || segment === "..") || !/^[A-Za-z0-9._/-]+$/u.test(value)) { throw new Error(`${configLabel}.${path}.${key} must be a safe relative path`); } return value; } function httpsUrlField(obj: Record, key: string, path: string): string { const value = urlField(obj, key, path); if (new URL(value).protocol !== "https:") throw new Error(`${configLabel}.${path}.${key} must be an https URL`); return value; } function yamlQuote(value: string): string { return JSON.stringify(value); } function indentBlock(value: string, spaces: number): string { const prefix = " ".repeat(spaces); return value.split(/\r?\n/u).map((line) => `${prefix}${line}`).join("\n"); } function record(value: unknown): Record { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : {}; } function arrayRecords(value: unknown): Record[] { return Array.isArray(value) ? value.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item)) as Record[] : []; } 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)]; }