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 { CliInputError, type RenderedCliResult } from "./output"; import { capture, compactCapture, fingerprintValues, parseJsonOutput, shQuote, } from "./platform-infra-ops-library"; import { applyPk01CaddyBlock, escapeTomlString, prepareFrpcSecret, renderFrpcManifest } from "./platform-infra-public-service"; import type { PublicServiceTarget, FrpcSecretMaterial } from "./platform-infra-public-service"; import { applyPk01CaddySiteBlock, caddyManagedBlockMarkers } from "./pk01-caddy"; import { deriveGiteaWebhookAttemptBudgetMs, deriveGiteaWebhookResponseHeaderTimeoutMs, renderGiteaWebhookCaddyHandle } 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"; import { buildMirrorWebhookStatusDisclosure, rawMirrorWebhookStatus, } from "./platform-infra-gitea-disclosure"; import { renderApply, renderMirrorBootstrap, renderMirrorPlan, renderMirrorStatus, renderMirrorSync, renderMirrorWebhookApply, renderMirrorWebhookCredentialBlocked, renderMirrorWebhookStatus, renderPlan, renderStatus, } from "./platform-infra-gitea-render"; import { GITEA_CONFIG_LABEL, githubTokenEnvNameForSecretKey, readGiteaConfig, resolveTarget, targetWebhookSync, webhookCaddyRoutes, type GiteaAdminCredential, type GiteaConfig, type GiteaGithubCredential, type GiteaMirrorRepository, type GiteaSourceResponsibility, type GiteaTarget, type GiteaWebhookGitOpsDelivery, type GiteaWebhookSync, } from "./platform-infra-gitea-config"; const configLabel = GITEA_CONFIG_LABEL; 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 githubHookReconcilerFile = rootPath("scripts", "src", "platform_infra_gitea_hook_reconciler.py"); const statusEvaluatorFile = rootPath("scripts", "src", "platform_infra_gitea_status_evaluator.py"); const fieldManager = "unidesk-platform-infra-gitea"; 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; dryRun: boolean; wait: boolean; repoKey: string | null; } interface MirrorWebhookStatusOptions extends MirrorOptions { deliveryId: string | null; } interface MirrorSecrets { adminUsername: string; adminPassword: string; githubTokens: Record; 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 --dry-run", "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 [--repo ] [--delivery-id ] [--full|--raw]", ], scopedHelp: ["bun scripts/cli.ts platform-infra gitea help platform-bootstrap"], boundary: "PR merge 是唯一 delivery 触发;默认入口只读观察和校验,不能充当合并后的恢复动作。", }; } function resolveCommandTarget(gitea: GiteaConfig, targetId: string | null): GiteaTarget { if (targetId !== null && !gitea.targets.some((target) => target.id.toLowerCase() === targetId.toLowerCase())) { throw new CliInputError(`Unknown Gitea target: ${targetId}`, { code: "unknown-target", argument: targetId, supported: gitea.targets.map((target) => target.id), usage: "bun scripts/cli.ts platform-infra gitea --target ", }); } return resolveTarget(gitea, targetId); } function plan(options: CommonOptions): Record { const gitea = readGiteaConfig(); const target = resolveCommandTarget(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 = resolveCommandTarget(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 targetRepos = repositoriesForTarget(gitea, target); const secrets = gitea.sourceAuthority.webhookSync.enabled && !options.dryRun ? ensureMirrorSecrets(gitea, target, targetRepos, true, 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 = resolveCommandTarget(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 = resolveCommandTarget(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 [--repo ] [--delivery-id ] [--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 = parseMirrorWebhookStatusOptions(args.slice(1)); const result = await mirrorWebhookStatus(config, options); if (result.credentialBlocked === true) return options.full || options.raw ? result : renderMirrorWebhookCredentialBlocked(result); if (options.raw) return rawMirrorWebhookStatus(result); const disclosure = buildMirrorWebhookStatusDisclosure(result, options); return options.full ? disclosure : renderMirrorWebhookStatus(result, disclosure); } return { ok: false, error: "unsupported-platform-infra-gitea-mirror-webhook-command", usage: "platform-infra gitea mirror webhook status --target [--repo ] [--delivery-id ] [--full|--raw]", }; } function mirrorPlan(options: CommonOptions): Record { const gitea = readGiteaConfig(); const target = resolveCommandTarget(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, targetRepos), next: mirrorReadOnlyNextCommands(target.id), }; } async function mirrorStatus(config: UniDeskConfig, options: CommonOptions & { repoKey?: string | null }): Promise> { const gitea = readGiteaConfig(); const target = resolveCommandTarget(gitea, options.targetId); const selectedRepos = selectedRepositories(gitea, target, options.repoKey ?? null); const credentials = credentialSummaries(gitea, selectedRepos); const blockers = credentialBlockers(credentials); if (blockers.length > 0) { return { ok: false, action: "platform-infra-gitea-mirror-status", mutation: false, target: targetSummary(target), serviceHealth: { ok: false }, sourceAuthority: { ...sourceAuthoritySummary(gitea, target), mirrorReady: false, stageReady: false }, repositories: selectedRepos.map((repo) => repositorySummary(gitea, repo)), credentials, error: { code: "github-credential-unavailable", blockers, valuesPrinted: false }, next: mirrorReadOnlyNextCommands(target.id), }; } const health = await validate(config, options); const healthValidation = record(health.validation); const secrets = ensureMirrorSecrets(gitea, target, selectedRepos, false, false, false); 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 = resolveCommandTarget(gitea, options.targetId); const repos = selectedRepositories(gitea, target, options.repoKey); if (!options.confirm) { const credentials = credentialSummaries(gitea, repos); const blockers = credentialBlockers(credentials); const repoFlag = options.repoKey === null ? "" : ` --repo ${options.repoKey}`; return { ok: blockers.length === 0, action: "platform-infra-gitea-mirror-bootstrap", mutation: false, mode: "dry-run", target: targetSummary(target), repositories: repos.map((repo) => repositorySummary(gitea, repo)), credentials, blockers, next: { ...mirrorReadOnlyNextCommands(target.id), confirm: `bun scripts/cli.ts platform-infra gitea mirror bootstrap --target ${target.id}${repoFlag} --confirm`, }, }; } const secrets = ensureMirrorSecrets(gitea, target, repos, false, 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, mode: "confirmed", target: targetSummary(target), repositories: arrayRecords(record(parsed).repositories), credentials: credentialSummaries(gitea, repos), remote: parsed ?? compactCapture(result, { full: true }), next: mirrorReadOnlyNextCommands(target.id), }; } async function mirrorSync(config: UniDeskConfig, options: MirrorOptions): Promise> { const gitea = readGiteaConfig(); const target = resolveCommandTarget(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, target, repos, false, 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 = resolveCommandTarget(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, target, repos, false, 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: MirrorWebhookStatusOptions): Promise> { const gitea = readGiteaConfig(); const target = resolveCommandTarget(gitea, options.targetId); const repos = selectedRepositories(gitea, target, options.repoKey); const credentials = credentialSummaries(gitea, repos); const blockers = credentialBlockers(credentials); if (blockers.length > 0) return buildMirrorWebhookCredentialBlockedResult(gitea, target, repos, credentials, blockers); const secrets = ensureMirrorSecrets(gitea, target, repos, false, 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), }; } export function buildMirrorWebhookCredentialBlockedResult( gitea: GiteaConfig, target: GiteaTarget, repos: GiteaMirrorRepository[], credentials: Array>, blockers: string[], ): Record { return { ok: false, action: "platform-infra-gitea-mirror-webhook-status", mutation: false, credentialBlocked: true, target: targetSummary(target), webhook: webhookSyncSummary(gitea, target), repositories: repos.map((repo) => repositorySummary(gitea, repo)), credentials, error: { code: "github-credential-unavailable", blockers, valuesPrinted: false }, next: mirrorReadOnlyNextCommands(target.id), }; } export 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 ownsHookReconcile = sync.hookReconcile.enabled && sync.hookReconcile.ownerTargetId.toLowerCase() === target.id.toLowerCase(); const hookTopologyJson = ownsHookReconcile ? JSON.stringify(githubHookTopology(gitea), null, 2) : ""; const hookReconcilerSource = ownsHookReconcile ? readFileSync(githubHookReconcilerFile, "utf8") : ""; const bridgeTokenEnv = githubTokenSecretEnv(gitea, target, false); const hookTokenEnv = githubTokenSecretEnv(gitea, target, true); const hookReconcilerConfig = ownsHookReconcile ? ` hook-topology.json: | ${indentBlock(hookTopologyJson, 4)} platform_infra_gitea_hook_reconciler.py: | ${indentBlock(hookReconcilerSource, 4)} ` : ""; const hookRecoveryStateEnv = ownsHookReconcile && sync.hookReconcile.failedDeliveryRecovery.enabled ? ` - name: UNIDESK_GITEA_HOOK_RECOVERY_STATE_PATH value: ${yamlQuote(sync.hookReconcile.failedDeliveryRecovery.statePath)} ` : ""; const hookReconcilerContainer = ownsHookReconcile ? ` - name: github-hook-topology-reconciler image: ${sync.bridge.image} imagePullPolicy: IfNotPresent command: - python3 - /etc/gitea-github-sync/platform_infra_gitea_hook_reconciler.py - /etc/gitea-github-sync/hook-topology.json env: - name: UNIDESK_GITEA_TARGET_ID value: ${yamlQuote(target.id)} - name: UNIDESK_GITEA_GITHUB_DEFAULT_TOKEN_ENV value: ${yamlQuote(githubTokenEnvName(gitea.sourceAuthority.credentials.github.gitFetchCredential.secretRef.key))} ${hookTokenEnv} - 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(","))} volumeMounts: - name: config mountPath: /etc/gitea-github-sync readOnly: true - name: durable-inbox mountPath: ${sync.bridge.inbox.path}` : ""; const configHash = createHash("sha256") .update(reposJson) .update("\0") .update(serverSource) .update("\0") .update(entrypointSource) .update("\0") .update(hookTopologyJson) .update("\0") .update(hookReconcilerSource) .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: UNIDESK_GITEA_GITHUB_DEFAULT_TOKEN_ENV value: ${yamlQuote(githubTokenEnvName(gitea.sourceAuthority.credentials.github.gitFetchCredential.secretRef.key))} ${bridgeTokenEnv} - 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)} ${hookReconcilerConfig} --- 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)} ${hookRecoveryStateEnv} - 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: UNIDESK_GITEA_GITHUB_DEFAULT_TOKEN_ENV value: ${yamlQuote(githubTokenEnvName(gitea.sourceAuthority.credentials.github.gitFetchCredential.secretRef.key))} ${bridgeTokenEnv} - 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} ${hookReconcilerContainer} 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_GITHUB_SECRET_KEY: gitea.sourceAuthority.credentials.github.gitFetchCredential.secretRef.key, UNIDESK_GITEA_GITHUB_DEFAULT_TOKEN_ENV: githubTokenEnvName(gitea.sourceAuthority.credentials.github.gitFetchCredential.secretRef.key), UNIDESK_GITEA_GITHUB_SECRET_ENV_MAP: githubCredentialsForConsumers(gitea, target, repositoriesForTarget(gitea, target), true) .map((credential) => `${credential.gitFetchCredential.secretRef.key}=${githubTokenEnvName(credential.gitFetchCredential.secretRef.key)}`) .join(","), 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; for (const [key, token] of Object.entries(params.secrets.githubTokens)) env[githubTokenEnvName(key)] = token; 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))), hookTopology: JSON.stringify(githubHookTopology(gitea)), 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: "github-hook-topology-single-writer", ok: !sync.hookReconcile.enabled || ( sync.hookReconcile.ownerTargetId.toLowerCase() === target.id.toLowerCase() ? (manifest.match(/name: github-hook-topology-reconciler/gu) ?? []).length === 1 && manifest.includes("platform_infra_gitea_hook_reconciler.py") && manifest.includes("hook-topology.json") : !manifest.includes("name: github-hook-topology-reconciler") ), detail: "The owning YAML selects one target-side reconciler for the complete GitHub hook topology; candidate gates and non-owner targets never write hooks.", }, { 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 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 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: [gitea.sourceAuthority.credentials.github.gitFetchCredential.secretRef.key, "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 githubHookTopology(gitea: GiteaConfig): Record { const sync = gitea.sourceAuthority.webhookSync; const base = gitea.app.publicExposure.publicBaseUrl.replace(/\/+$/u, ""); const desiredByRepository = new Map; branches: Set }>>(); for (const repo of gitea.sourceAuthority.repositories) { const target = resolveTarget(gitea, repo.targetId); const desired = desiredByRepository.get(repo.upstream.repository) ?? new Map(); const key = target.id.toLowerCase(); const hook = desired.get(key) ?? { targetId: target.id, url: githubWebhookPublicUrl(gitea, target), repoKeys: new Set(), branches: new Set(), }; hook.repoKeys.add(repo.key); hook.branches.add(repo.upstream.branch); desired.set(key, hook); desiredByRepository.set(repo.upstream.repository, desired); } return { version: 1, kind: "GiteaGithubHookTopology", ownerTargetId: sync.hookReconcile.ownerTargetId, managedUrlPrefix: `${base}${sync.publicPath}`, bridgeStatusUrl: `http://127.0.0.1:${sync.bridge.httpPort}/status`, events: sync.events, reconcile: { intervalMs: sync.hookReconcile.intervalMs, requestTimeoutMs: sync.hookReconcile.requestTimeoutMs, listMaxPages: sync.hookReconcile.listMaxPages, failedDeliveryRecovery: sync.hookReconcile.failedDeliveryRecovery, }, repositories: [...desiredByRepository.entries()] .sort(([left], [right]) => left.localeCompare(right)) .map(([repository, desired]) => { const desiredHooks = [...desired.values()] .sort((left, right) => left.targetId.localeCompare(right.targetId)) .map((item) => ({ targetId: item.targetId, url: item.url, repoKeys: [...item.repoKeys].sort(), branches: [...item.branches].sort(), })); const repo = gitea.sourceAuthority.repositories.find((item) => item.upstream.repository === repository); const credential = repo === undefined ? gitea.sourceAuthority.credentials.github : githubCredentialForRepo(gitea, repo); return { repository, githubTokenEnv: githubTokenEnvName(credential.gitFetchCredential.secretRef.key), desiredUrls: desiredHooks.map((item) => item.url).sort(), desiredHooks, }; }), }; } 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 CliInputError(`Unknown Gitea mirror repository key: ${repoKey}`, { code: "unknown-repository-key", argument: repoKey, supported: repos.map((item) => item.key), usage: "bun scripts/cli.ts platform-infra gitea mirror webhook status --target --repo ", }); return [repo]; } function repositorySummary(gitea: GiteaConfig, repo: GiteaMirrorRepository): Record { return { key: repo.key, upstreamRepository: repo.upstream.repository, upstreamBranch: repo.upstream.branch, upstreamVisibility: repo.upstream.visibility, giteaOwner: repo.gitea.owner, giteaRepo: repo.gitea.name, mirrorMode: repo.gitea.mirrorMode, readUrl: repo.gitea.readUrl, snapshotPrefix: repo.snapshot.prefix, legacyReadUrl: repo.legacyGitMirror?.readUrl ?? null, legacyDisposition: repo.legacyGitMirror?.disposition ?? "native-gitea-only", sourceAuthority: "gitea", rootUrlMatches: repo.gitea.readUrl.startsWith(gitea.app.server.rootUrl), }; } function remoteRepoSpec(repo: GiteaMirrorRepository): Record { const github = repo.credentialOverride?.github; return { key: repo.key, upstream: repo.upstream, gitea: repo.gitea, gitops: repo.gitops, snapshot: repo.snapshot, legacyGitMirror: repo.legacyGitMirror, githubCredential: github === undefined ? null : { secretKey: github.gitFetchCredential.secretRef.key, tokenEnv: githubTokenEnvName(github.gitFetchCredential.secretRef.key), }, }; } 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 topology = githubHookTopology(gitea); const targetUrl = githubWebhookPublicUrl(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: targetUrl, hookReconcile: { ...sync.hookReconcile, runningOnTarget: sync.hookReconcile.enabled && sync.hookReconcile.ownerTargetId.toLowerCase() === target.id.toLowerCase(), desiredUrlSha256: `sha256:${createHash("sha256").update(targetUrl).digest("hex").slice(0, 16)}`, topologySha256: `sha256:${createHash("sha256").update(JSON.stringify(topology)).digest("hex").slice(0, 16)}`, }, 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, repositories = gitea.sourceAuthority.repositories): 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) : {}; const summaries: Array> = [ { id: "gitea-admin", sourceRef: gitea.sourceAuthority.credentials.admin.sourceRef, present: existsSync(adminPath), requiredKeysPresent: Boolean(adminLinePair.username) && Boolean(adminLinePair.password), fingerprint: fingerprintSecretParts([adminLinePair.username, adminLinePair.password]), permissionResult: { status: "not-applicable", error: null }, valuesPrinted: false, }, { id: "github-upstream", sourceRef: github.sourceRef, present: existsSync(githubPath), requiredKeysPresent: Boolean(githubEnv[github.sourceKey]), fingerprint: fingerprintKeys(githubEnv, [github.sourceKey]), permissionResult: existsSync(githubPath) && Boolean(githubEnv[github.sourceKey]) ? { status: "not-probed", error: null } : { status: "blocked-missing-credential", error: "credential material is absent" }, valuesPrinted: false, }, ]; for (const repo of repositories) { const githubOverride = repo.credentialOverride?.github; if (githubOverride === undefined) continue; const sourcePath = credentialPath(gitea, githubOverride.sourceRef); const values = existsSync(sourcePath) ? parseGithubCredentialFile(sourcePath, githubOverride) : {}; summaries.push({ id: `github-upstream:${repo.key}`, repository: repo.upstream.repository, sourceRef: githubOverride.sourceRef, present: existsSync(sourcePath), requiredKeysPresent: Boolean(values[githubOverride.sourceKey]), fingerprint: fingerprintKeys(values, [githubOverride.sourceKey]), permissionResult: existsSync(sourcePath) && Boolean(values[githubOverride.sourceKey]) ? { status: "not-probed", permissions: githubOverride.permissions, error: null } : { status: "blocked-missing-credential", permissions: githubOverride.permissions, error: "credential material is absent" }, valuesPrinted: false, }); } return summaries; } function credentialBlockers(credentials: Array>): string[] { return credentials.filter((item) => item.present !== true || item.requiredKeysPresent !== true).map((item) => String(item.id)); } function ensureMirrorSecrets( gitea: GiteaConfig, target: GiteaTarget, repositories: GiteaMirrorRepository[], includeHookOwnerCredentials: boolean, 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 githubTokens: Record = {}; for (const github of githubCredentialsForConsumers(gitea, target, repositories, includeHookOwnerCredentials)) { 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 githubToken = githubEnv[github.sourceKey]; if (!githubToken) throw new Error(`${github.sourceRef} must contain ${github.sourceKey}`); githubTokens[github.gitFetchCredential.secretRef.key] = githubToken; } const webhookSecret = ensureWebhookSecret(gitea, createWebhookSecret); const adminUsername = adminLinePair.username; const adminPassword = adminLinePair.password; if (!adminUsername || !adminPassword) throw new Error(`${admin.sourceRef} must contain username on line ${admin.usernameLine} and password on line ${admin.passwordLine}`); return { adminUsername, adminPassword, githubTokens, webhookSecret }; } function githubCredentialForRepo(gitea: GiteaConfig, repo: GiteaMirrorRepository): GiteaGithubCredential { return repo.credentialOverride?.github ?? gitea.sourceAuthority.credentials.github; } function githubCredentialsForTarget(gitea: GiteaConfig, target: GiteaTarget): GiteaGithubCredential[] { const credentials = [gitea.sourceAuthority.credentials.github, ...repositoriesForTarget(gitea, target).flatMap((repo) => repo.credentialOverride?.github ?? [])]; return [...new Map(credentials.map((credential) => [credential.gitFetchCredential.secretRef.key, credential])).values()]; } function githubCredentialsForConsumers( gitea: GiteaConfig, target: GiteaTarget, repositories: GiteaMirrorRepository[], includeHookOwnerCredentials: boolean, ): GiteaGithubCredential[] { const credentials = [gitea.sourceAuthority.credentials.github, ...repositories.flatMap((repo) => repo.credentialOverride?.github ?? [])]; if (includeHookOwnerCredentials && gitea.sourceAuthority.webhookSync.hookReconcile.ownerTargetId.toLowerCase() === target.id.toLowerCase()) { credentials.push(...gitea.sourceAuthority.repositories.flatMap((repo) => { const github = repo.credentialOverride?.github; return github !== undefined && github.requiredFor.some((item) => item.startsWith("github-hooks-")) ? [github] : []; })); } return [...new Map(credentials.map((credential) => [credential.gitFetchCredential.secretRef.key, credential])).values()]; } function githubTokenEnvName(secretKey: string): string { return githubTokenEnvNameForSecretKey(secretKey); } function githubTokenSecretEnv(gitea: GiteaConfig, target: GiteaTarget, includeHookOwnerCredentials: boolean): string { const secretName = targetWebhookSync(gitea, target).bridge.secretName; return githubCredentialsForConsumers(gitea, target, repositoriesForTarget(gitea, target), includeHookOwnerCredentials).map((credential) => ` - name: ${githubTokenEnvName(credential.gitFetchCredential.secretRef.key)} valueFrom: secretKeyRef: name: ${secretName} key: ${credential.gitFetchCredential.secretRef.key}`).join("\n"); } 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 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 CliInputError("Gitea apply accepts only one of --confirm or --dry-run", { code: "mutually-exclusive-options", argument: "--confirm --dry-run", usage: ["bun scripts/cli.ts platform-infra gitea apply --dry-run", "bun scripts/cli.ts platform-infra gitea apply --confirm"], }); return { ...parseCommonOptions(commonArgs), confirm, dryRun: dryRun || !confirm, wait }; } function parseMirrorWebhookStatusOptions(args: string[]): MirrorWebhookStatusOptions { const mirrorArgs: string[] = []; let deliveryId: string | null = null; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg !== "--delivery-id") { mirrorArgs.push(arg); continue; } const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new CliInputError("--delivery-id requires a value", { code: "missing-option-value", argument: "--delivery-id", usage: "bun scripts/cli.ts platform-infra gitea mirror webhook status --target [--repo ] --delivery-id ", }); if (!/^[A-Za-z0-9._:-]{1,160}$/u.test(value)) throw new CliInputError("--delivery-id must be a bounded GitHub delivery identifier", { code: "invalid-option-value", argument: value, usage: "bun scripts/cli.ts platform-infra gitea mirror webhook status --target [--repo ] --delivery-id ", }); deliveryId = value; index += 1; } return { ...parseMirrorOptions(mirrorArgs), deliveryId }; } function parseMirrorOptions(args: string[]): MirrorOptions { const commonArgs: string[] = []; let confirm = false; let dryRun = 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 === "--dry-run") { dryRun = true; } else if (arg === "--repo") { const value = args[index + 1]; if (value === undefined || value.startsWith("--")) throw new CliInputError("--repo requires a value", { code: "missing-option-value", argument: "--repo", usage: "bun scripts/cli.ts platform-infra gitea mirror webhook status --target --repo ", }); if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new CliInputError("--repo must be a simple repository key", { code: "invalid-option-value", argument: value, usage: "bun scripts/cli.ts platform-infra gitea mirror webhook status --target --repo ", }); repoKey = value; index += 1; } else { commonArgs.push(arg); if (arg === "--target" || arg === "--node") { commonArgs.push(args[index + 1] ?? ""); index += 1; } } } if (confirm && dryRun) throw new CliInputError("Gitea mirror bootstrap accepts only one of --confirm or --dry-run", { code: "mutually-exclusive-options", argument: "--confirm --dry-run", usage: [ "bun scripts/cli.ts platform-infra gitea mirror bootstrap --target --dry-run", "bun scripts/cli.ts platform-infra gitea mirror bootstrap --target --confirm", ], }); return { ...parseCommonOptions(commonArgs), confirm, dryRun: dryRun || !confirm, wait: false, 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 CliInputError(`${arg} requires a value`, { code: "missing-option-value", argument: arg, usage: "bun scripts/cli.ts platform-infra gitea --target ", }); if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new CliInputError(`${arg} must be a simple target id`, { code: "invalid-option-value", argument: value, usage: "bun scripts/cli.ts platform-infra gitea --target ", }); targetId = value; index += 1; } else if (arg === "--full") { full = true; } else if (arg === "--raw") { raw = true; full = true; } else { throw new CliInputError(`Unsupported Gitea option: ${arg}`, { code: "unsupported-option", argument: arg, supported: ["--target", "--node", "--repo", "--delivery-id", "--full", "--raw"], usage: "bun scripts/cli.ts platform-infra gitea help", }); } } 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 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)]; }