Files
pikasTech-unidesk/scripts/src/platform-infra-gitea.ts
T

2756 lines
123 KiB
TypeScript

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<Record<string, unknown> | 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<string, unknown> {
if (scope === "platform-bootstrap") {
return {
command: "platform-infra gitea help platform-bootstrap",
configTruth: configLabel,
usage: [
"bun scripts/cli.ts platform-infra gitea apply --target <NODE> --dry-run",
"bun scripts/cli.ts platform-infra gitea apply --target <NODE> --confirm",
"bun scripts/cli.ts platform-infra gitea mirror bootstrap --target <NODE> --confirm",
"bun scripts/cli.ts platform-infra gitea mirror webhook apply --target <NODE> --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 <NODE> [--full|--raw]",
"bun scripts/cli.ts platform-infra gitea validate --target <NODE> [--full|--raw]",
"bun scripts/cli.ts platform-infra gitea mirror status --target <NODE> [--full|--raw]",
"bun scripts/cli.ts platform-infra gitea mirror webhook status --target <NODE> [--full|--raw]",
],
scopedHelp: ["bun scripts/cli.ts platform-infra gitea help platform-bootstrap"],
boundary: "PR merge 是唯一 delivery 触发;默认入口只读观察和校验,不能充当合并后的恢复动作。",
};
}
function readGiteaConfig(): GiteaConfig {
const root = readYamlRecord<Record<string, unknown>>(configFile, "platform-infra-gitea");
const version = y.integerField(root, "version", "");
if (version !== 1) throw new Error(`${configLabel}.version must be 1`);
const metadata = y.objectField(root, "metadata", "");
const defaults = y.objectField(root, "defaults", "");
const migration = y.objectField(root, "migration", "");
const 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown> | 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<string, unknown>, `${path}.webhookSync`),
};
}
function parseTargetWebhookSync(record: Record<string, unknown>, 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<string>();
const webhookPorts = new Set<number>();
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<string>();
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<string, unknown> {
const gitea = readGiteaConfig();
const target = resolveTarget(gitea, options.targetId);
const manifest = renderManifest(gitea, target);
const policy = policyChecks(gitea, target, manifest);
return {
ok: policy.every((check) => check.ok),
action: "platform-infra-gitea-plan",
mutation: false,
config: configSummary(gitea, target),
renderPlan: {
target: targetSummary(target),
objects: manifestObjectSummary(manifest),
},
policy,
next: giteaReadOnlyNextCommands(target.id),
};
}
async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Record<string, unknown>> {
const gitea = readGiteaConfig();
const target = resolveTarget(gitea, options.targetId);
const manifest = renderManifest(gitea, target);
const policy = policyChecks(gitea, target, manifest);
if (!policy.every((check) => check.ok)) return { ok: false, action: "platform-infra-gitea-apply", mode: "policy-blocked", mutation: false, policy };
const 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<Record<string, unknown>> {
const gitea = readGiteaConfig();
const target = resolveTarget(gitea, options.targetId);
const result = await capture(config, target.route, ["sh"], remoteScript("status", gitea, target, "", { ...options, confirm: false, dryRun: true, wait: false }).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<Record<string, unknown>> {
const gitea = readGiteaConfig();
const target = resolveTarget(gitea, options.targetId);
const result = await capture(config, target.route, ["sh"], remoteScript("validate", gitea, target, "", { ...options, confirm: false, dryRun: true, wait: false }).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<Record<string, unknown> | 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<Record<string, unknown> | 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 <node> [--repo <key>] [--full|--raw]",
};
}
function mirrorPlan(options: CommonOptions): Record<string, unknown> {
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<Record<string, unknown>> {
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<Record<string, unknown>> {
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<Record<string, unknown>> {
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<Record<string, unknown>> {
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<Record<string, unknown>> {
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<string, string> = {
GITEA_WORK_DIR: app.storage.data.mountPath,
GITEA__security__INSTALL_LOCK: "true",
GITEA__server__PROTOCOL: app.server.protocol,
GITEA__server__DOMAIN: app.server.domain,
GITEA__server__ROOT_URL: app.server.rootUrl,
GITEA__server__HTTP_ADDR: "0.0.0.0",
GITEA__server__HTTP_PORT: String(app.service.httpPort),
GITEA__server__SSH_DOMAIN: app.server.sshDomain,
GITEA__server__SSH_PORT: String(app.service.sshPort),
GITEA__server__START_SSH_SERVER: app.server.startSshServer ? "true" : "false",
GITEA__database__DB_TYPE: app.database.type,
GITEA__database__PATH: app.database.path,
GITEA__repository__ROOT: `${app.storage.data.mountPath}/git/repositories`,
GITEA__actions__ENABLED: app.actions.enabled ? "true" : "false",
GITEA__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<string, unknown> } {
const frpcExposure = targetFrpcExposure(gitea, target);
const sync = targetWebhookSync(gitea, target);
const env: Record<string, string> = {
UNIDESK_GITEA_ACTION: action,
UNIDESK_GITEA_TARGET_ID: target.id,
UNIDESK_GITEA_ROUTE: target.route,
UNIDESK_GITEA_NAMESPACE: target.namespace,
UNIDESK_GITEA_APP_NAME: gitea.app.name,
UNIDESK_GITEA_STATEFULSET_NAME: gitea.app.statefulSetName,
UNIDESK_GITEA_SERVICE_NAME: gitea.app.serviceName,
UNIDESK_GITEA_HTTP_PORT: String(gitea.app.service.httpPort),
UNIDESK_GITEA_HEALTH_PATH: gitea.validation.healthPath,
UNIDESK_GITEA_IMAGE: `${gitea.app.image.repository}:${gitea.app.image.tag}`,
UNIDESK_GITEA_FIELD_MANAGER: fieldManager,
UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS: String(gitea.validation.waitTimeoutSeconds),
UNIDESK_GITEA_DRY_RUN: options.dryRun ? "1" : "0",
UNIDESK_GITEA_WAIT: options.wait ? "1" : "0",
UNIDESK_GITEA_FULL: options.full ? "1" : "0",
UNIDESK_GITEA_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<Record<string, unknown>> {
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<string, unknown> {
return {
path: configLabel,
metadata: gitea.metadata,
migration: gitea.migration,
target: targetSummary(target),
app: appSummary(gitea),
valuesPrinted: false,
};
}
function compactConfigSummary(gitea: GiteaConfig, target: GiteaTarget): Record<string, unknown> {
return {
path: configLabel,
target: targetSummary(target),
app: {
image: `${gitea.app.image.repository}:${gitea.app.image.tag}`,
serviceDns: serviceDns(gitea, target),
rootUrl: gitea.app.server.rootUrl,
publicBaseUrl: gitea.app.publicExposure.publicBaseUrl,
actionsEnabled: gitea.app.actions.enabled,
},
valuesPrinted: false,
};
}
function targetSummary(target: GiteaTarget): Record<string, unknown> {
return {
id: target.id,
route: target.route,
namespace: target.namespace,
role: target.role,
createNamespace: target.createNamespace,
storageClassName: target.storageClassName,
publicExposureEnabled: target.publicExposureEnabled,
webhookSync: target.webhookSync,
};
}
function appSummary(gitea: GiteaConfig): Record<string, unknown> {
return {
name: gitea.app.name,
statefulSetName: gitea.app.statefulSetName,
serviceName: gitea.app.serviceName,
image: `${gitea.app.image.repository}:${gitea.app.image.tag}`,
replicas: gitea.app.replicas,
service: gitea.app.service,
rootUrl: gitea.app.server.rootUrl,
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<Record<string, unknown>> {
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<string, { publicPath: string; remotePort: number }>();
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<string, unknown> {
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<string, unknown> {
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<string, unknown> {
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<string, unknown>): Record<string, unknown> {
return {
ready: payload.ready === true,
target: payload.target,
route: payload.route,
namespace: payload.namespace,
image: payload.image,
serviceDns: payload.serviceDns,
networkPolicy: payload.networkPolicy,
statefulSet: payload.statefulSet,
service: payload.service,
endpointsReady: payload.endpointsReady === true,
pods: Array.isArray(payload.pods) ? payload.pods : [],
pvcs: Array.isArray(payload.pvcs) ? payload.pvcs : [],
eventsTail: Array.isArray(payload.eventsTail) ? payload.eventsTail : [],
valuesPrinted: false,
};
}
function sourceAuthoritySummary(gitea: GiteaConfig, target: GiteaTarget): Record<string, unknown> {
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<string, unknown> {
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<string, unknown> {
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<string, unknown> {
return {
key: repo.key,
upstream: repo.upstream,
gitea: repo.gitea,
gitops: repo.gitops,
snapshot: repo.snapshot,
legacyGitMirror: repo.legacyGitMirror,
};
}
function mirrorReadOnlyNextCommands(targetId: string): Record<string, string> {
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<string, unknown> {
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<Record<string, unknown>> {
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<Record<string, unknown>>): 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 <node> --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<string, string> {
const values: Record<string, string> = {};
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<string, string> {
const text = readFileSync(path, "utf8");
if (github.format === "raw-token") return { [github.sourceKey]: text.trim() };
return parseEnvTextSafe(text);
}
function parseEnvTextSafe(text: string): Record<string, string> {
const values: Record<string, string> = {};
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<string, string>, 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>): 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<string, unknown>): 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<string, unknown>): 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<string, unknown>): 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<string, unknown>): 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<string, unknown>): 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<string, unknown>): 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<string, unknown>): RenderedCliResult {
const config = record(result.config);
const target = record(config.target);
const app = record(config.app);
const migration = record(config.migration);
const policy = arrayRecords(result.policy);
const failed = policy.filter((item) => item.ok === false);
const next = record(result.next);
return rendered(result, "platform-infra gitea plan", [
"PLATFORM-INFRA GITEA PLAN",
...table(["FIELD", "VALUE", "DETAIL", "VALUE"], [
["TARGET", stringValue(target.id), "route", stringValue(target.route)],
["NAMESPACE", stringValue(target.namespace), "role", stringValue(target.role)],
["IMAGE", stringValue(app.image), "replicas", stringValue(app.replicas)],
["SERVICE", `${stringValue(app.serviceName)}:${stringValue(record(app.service).httpPort)}`, "dns", serviceDnsFromObjects(app, target)],
["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<string, unknown>): 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<string, unknown>): RenderedCliResult {
const summary = record(result.summary);
const statefulSet = record(summary.statefulSet);
const service = record(summary.service);
const networkPolicy = record(summary.networkPolicy);
const pods = arrayRecords(summary.pods).map((pod) => [stringValue(pod.name), stringValue(pod.phase), boolText(pod.ready), stringValue(pod.restarts)]);
const pvcs = arrayRecords(summary.pvcs).map((pvc) => [stringValue(pvc.name), stringValue(pvc.phase), stringValue(pvc.capacity)]);
return rendered(result, "platform-infra gitea status", [
"PLATFORM-INFRA GITEA STATUS",
...table(["TARGET", "ROUTE", "NAMESPACE", "READY"], [[stringValue(summary.target), stringValue(summary.route), stringValue(summary.namespace), boolText(summary.ready)]]),
"",
"CONTROL",
...table(["CHECK", "VALUE", "DETAIL"], [
["statefulSet", `${stringValue(statefulSet.readyReplicas)}/${stringValue(statefulSet.desired)}`, stringValue(statefulSet.name)],
["service", stringValue(service.type), stringValue(summary.serviceDns)],
["endpoints", boolText(summary.endpointsReady), stringValue(service.clusterIP)],
["allow-all", boolText(networkPolicy.allowAllPresent), "NetworkPolicy"],
]),
"",
"PODS",
...(pods.length === 0 ? ["-"] : table(["POD", "PHASE", "READY", "RESTARTS"], pods)),
"",
"PVCS",
...(pvcs.length === 0 ? ["-"] : table(["PVC", "PHASE", "CAPACITY"], pvcs)),
...remoteErrorLines(result),
"",
`NEXT bun scripts/cli.ts platform-infra gitea validate --target ${stringValue(summary.target)}`,
]);
}
function remoteErrorLines(result: Record<string, unknown>): string[] {
if (result.ok !== false) return [];
const remote = record(result.remote);
const exitCode = stringValue(remote.exitCode);
const stderr = compactLine(stringValue(remote.stderrTail));
const stdout = compactLine(stringValue(remote.stdoutTail));
const detail = stderr !== "-" ? stderr : stdout;
return ["", "ERROR", ...table(["EXIT", "DETAIL"], [[exitCode, detail]])];
}
function 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<string, unknown>, command: string, lines: string[]): RenderedCliResult {
return { ok: result.ok !== false, command, renderedText: lines.join("\n"), contentType: "text/plain" };
}
function parseApplyOptions(args: string[]): ApplyOptions {
const commonArgs: string[] = [];
let confirm = false;
let dryRun = false;
let wait = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--confirm") confirm = true;
else if (arg === "--dry-run") dryRun = true;
else if (arg === "--wait") wait = true;
else {
commonArgs.push(arg);
if (arg === "--target" || arg === "--node") {
commonArgs.push(args[index + 1] ?? "");
index += 1;
}
}
}
if (confirm && dryRun) throw new Error("gitea apply accepts only one of --confirm or --dry-run");
return { ...parseCommonOptions(commonArgs), confirm, dryRun: dryRun || !confirm, wait };
}
function 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<string, string> {
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<Record<string, unknown>> {
const objects: Array<Record<string, unknown>> = [];
for (const doc of yaml.split(/^---$/mu)) {
const kind = doc.match(/^\s*kind:\s*([A-Za-z0-9._-]+)\s*$/mu)?.[1];
const name = doc.match(/^\s*name:\s*([A-Za-z0-9._-]+)\s*$/mu)?.[1];
const namespace = doc.match(/^\s*namespace:\s*([A-Za-z0-9._-]+)\s*$/mu)?.[1] ?? null;
if (kind !== undefined && name !== undefined) objects.push({ kind, name, namespace });
}
return objects;
}
function serviceDns(gitea: GiteaConfig, target: GiteaTarget): string {
return `${gitea.app.serviceName}.${target.namespace}.svc.cluster.local:${gitea.app.service.httpPort}`;
}
function serviceDnsFromObjects(app: Record<string, unknown>, target: Record<string, unknown>): string {
const service = record(app.service);
return `${stringValue(app.serviceName)}.${stringValue(target.namespace)}.svc.cluster.local:${stringValue(service.httpPort)}`;
}
function positiveInteger(obj: Record<string, unknown>, key: string, path: string): number {
const value = y.integerField(obj, key, path);
if (value < 1) throw new Error(`${configLabel}.${path}.${key} must be positive`);
return value;
}
function boundedTimeout(obj: Record<string, unknown>, key: string, path: string): number {
const value = positiveInteger(obj, key, path);
if (value > 55) throw new Error(`${configLabel}.${path}.${key} must fit the 60s trans short-connection budget`);
return value;
}
function literalField<T extends string>(obj: Record<string, unknown>, 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<T extends string>(obj: Record<string, unknown>, 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<string, unknown>, key: string, path: string): string {
const value = y.stringField(obj, key, path);
if (!/^[0-9]+(?:m|Ki|Mi|Gi|Ti)?$/u.test(value)) throw new Error(`${configLabel}.${path}.${key} must be a Kubernetes quantity`);
return value;
}
function secretSourceRefField(obj: Record<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, key: string, path: string): string {
const value = y.stringField(obj, key, path);
const parsed = new URL(value);
if (!["http:", "https:"].includes(parsed.protocol) || parsed.search || parsed.hash) throw new Error(`${configLabel}.${path}.${key} must be an http(s) URL without query or hash`);
return value;
}
function gitBranchField(obj: Record<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function arrayRecords(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item)) as Record<string, unknown>[] : [];
}
function stringValue(value: unknown, fallback = "-"): string {
if (typeof value === "string" && value.length > 0) return value;
if (typeof value === "number" || typeof value === "boolean") return String(value);
return fallback;
}
function boolText(value: unknown): string {
return value === true ? "true" : "false";
}
function compactLine(value: string): string {
const trimmed = value.replace(/\s+/gu, " ").trim();
return trimmed.length > 0 ? trimmed.slice(0, 220) : "-";
}
function table(headers: string[], rows: string[][]): string[] {
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length)));
const format = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ");
return [format(headers), format(headers.map((header, index) => "-".repeat(Math.max(header.length, widths[index])))), ...rows.map(format)];
}