fix(cicd): add target gitea webhook status

This commit is contained in:
Codex
2026-07-08 20:22:38 +02:00
parent 877d78e68e
commit 162486ca05
7 changed files with 267 additions and 44 deletions
+117 -4
View File
@@ -703,13 +703,28 @@ run_mirror_webhook_status() {
github_api_env
bridge_ready=$(kubectl -n "$UNIDESK_GITEA_NAMESPACE" get deploy "$UNIDESK_GITEA_WEBHOOK_DEPLOYMENT" -o jsonpath='{.status.readyReplicas}/{.spec.replicas}' 2>/dev/null || echo "0/0")
service_exists=$(kubectl -n "$UNIDESK_GITEA_NAMESPACE" get service "$UNIDESK_GITEA_WEBHOOK_SERVICE" -o name 2>/dev/null || true)
python3 - "$tmp/repos.json" "$bridge_ready" "$service_exists" <<'PY'
import json, os, sys, urllib.error, urllib.request
run_mirror_status >"$tmp/mirror-status.json" 2>"$tmp/mirror-status.err"
mirror_status_rc=$?
kubectl -n "$UNIDESK_GITEA_NAMESPACE" logs "deployment/$UNIDESK_GITEA_WEBHOOK_DEPLOYMENT" --tail=80 >"$tmp/bridge.log" 2>"$tmp/bridge.err"
bridge_logs_rc=$?
python3 - "$tmp/repos.json" "$bridge_ready" "$service_exists" "$mirror_status_rc" "$bridge_logs_rc" "$tmp/mirror-status.json" "$tmp/mirror-status.err" "$tmp/bridge.log" "$tmp/bridge.err" <<'PY'
import json, os, sys, urllib.error, urllib.parse, urllib.request
repos = json.load(open(sys.argv[1], encoding="utf-8"))
bridge_ready = sys.argv[2]
service_exists = bool(sys.argv[3])
mirror_status_rc = int(sys.argv[4])
bridge_logs_rc = int(sys.argv[5])
mirror_status_path = sys.argv[6]
mirror_status_err_path = sys.argv[7]
bridge_log_path = sys.argv[8]
bridge_log_err_path = sys.argv[9]
token = os.environ["UNIDESK_GITEA_GITHUB_TOKEN"]
url = os.environ["UNIDESK_GITEA_WEBHOOK_PUBLIC_URL"]
def text(path, limit=1600):
try:
return open(path, encoding="utf-8", errors="replace").read()[-limit:]
except FileNotFoundError:
return ""
def request(api_path):
req = urllib.request.Request("https://api.github.com" + api_path, method="GET")
req.add_header("Authorization", "Bearer " + token)
@@ -720,9 +735,60 @@ def request(api_path):
return {"ok": True, "status": resp.status, "body": resp.read().decode("utf-8", errors="replace")}
except urllib.error.HTTPError as exc:
return {"ok": False, "status": exc.code, "body": exc.read().decode("utf-8", errors="replace")[:1000]}
def github_head(repository, branch):
result = request(f"/repos/{repository}/git/ref/heads/{urllib.parse.quote(branch, safe='')}")
try:
payload = json.loads(result.get("body") or "{}")
except Exception:
payload = {}
return {"ok": result.get("ok"), "status": result.get("status"), "sha": payload.get("object", {}).get("sha"), "error": None if result.get("ok") else result.get("body", "")[:300]}
def hook_deliveries(repository, hook_id):
if not hook_id:
return {"ok": False, "status": None, "latest": None, "latestPush": None, "error": "hook not found"}
result = request(f"/repos/{repository}/hooks/{hook_id}/deliveries?per_page=8")
try:
rows = json.loads(result.get("body") or "[]")
except Exception:
rows = []
def compact(item):
if not isinstance(item, dict):
return None
return {
"id": item.get("id"),
"event": item.get("event"),
"status": item.get("status"),
"statusCode": item.get("status_code"),
"deliveredAt": item.get("delivered_at"),
"duration": item.get("duration"),
}
latest = compact(rows[0]) if rows else None
latest_push = next((compact(item) for item in rows if isinstance(item, dict) and item.get("event") == "push"), None)
return {"ok": result.get("ok"), "status": result.get("status"), "latest": latest, "latestPush": latest_push, "error": None if result.get("ok") else result.get("body", "")[:300]}
try:
mirror_status = json.load(open(mirror_status_path, encoding="utf-8"))
except Exception:
mirror_status = {}
mirror_by_key = {item.get("key"): item for item in mirror_status.get("repositories", []) if isinstance(item, dict)}
bridge_events = []
for line in text(bridge_log_path, 12000).splitlines():
try:
event = json.loads(line)
except Exception:
continue
if isinstance(event, dict) and str(event.get("event", "")).startswith("github-to-gitea"):
bridge_events.append({
"event": event.get("event"),
"repo": event.get("repo"),
"ok": event.get("ok"),
"sourceCommit": event.get("sourceCommit"),
"snapshotRef": event.get("snapshotRef"),
"error": event.get("error"),
"elapsedMs": event.get("elapsedMs"),
})
rows = []
for repo in repos:
repository = repo["upstream"]["repository"]
branch = repo["upstream"]["branch"]
result = request(f"/repos/{repository}/hooks")
hooks = []
try:
@@ -730,9 +796,56 @@ for repo in repos:
except Exception:
hooks = []
match = next((item for item in hooks if (item.get("config") or {}).get("url") == url), None)
rows.append({"key": repo["key"], "repository": repository, "hookId": match.get("id") if match else None, "hookReady": bool(match and match.get("active") is True), "active": match.get("active") if match else None, "status": result.get("status"), "error": None if result.get("ok") else result.get("body", "")[:500], "valuesPrinted": False})
hook_id = match.get("id") if match else None
head = github_head(repository, branch)
delivery = hook_deliveries(repository, hook_id)
mirror = mirror_by_key.get(repo["key"], {})
branch_commit = mirror.get("branchCommit")
snapshot_commit = mirror.get("snapshotCommit")
branch_matches_github = bool(head.get("sha") and branch_commit == head.get("sha"))
snapshot_matches_branch = bool(branch_commit and snapshot_commit == branch_commit)
repo_bridge_events = [event for event in bridge_events if event.get("repo") == repo["key"]]
last_bridge_event = repo_bridge_events[-1] if repo_bridge_events else None
error = None
if not result.get("ok"):
error = result.get("body", "")[:500]
elif not head.get("ok"):
error = head.get("error")
elif not delivery.get("ok"):
error = delivery.get("error")
elif not branch_matches_github:
error = "gitea branch does not match GitHub head"
elif not snapshot_matches_branch:
error = "gitea snapshot does not match branch"
rows.append({
"key": repo["key"],
"repository": repository,
"branch": branch,
"hookId": hook_id,
"hookReady": bool(match and match.get("active") is True),
"active": match.get("active") if match else None,
"status": result.get("status"),
"githubHead": head.get("sha"),
"branchCommit": branch_commit,
"snapshotCommit": snapshot_commit,
"branchMatchesGitHub": branch_matches_github,
"snapshotMatchesBranch": snapshot_matches_branch,
"latestDelivery": delivery.get("latest"),
"latestPushDelivery": delivery.get("latestPush"),
"lastBridgeEvent": last_bridge_event,
"error": error,
"valuesPrinted": False,
})
bridge = {"deployment": os.environ["UNIDESK_GITEA_WEBHOOK_DEPLOYMENT"], "ready": bridge_ready not in ("0/0", "/") and bridge_ready.split("/")[0] == bridge_ready.split("/")[-1], "readyReplicas": bridge_ready, "serviceExists": service_exists}
payload = {"ok": bridge["ready"] and service_exists and all(row["hookReady"] for row in rows), "bridge": bridge, "repositories": rows, "url": url, "valuesPrinted": False}
payload = {
"ok": bridge["ready"] and service_exists and mirror_status_rc == 0 and bridge_logs_rc == 0 and all(row["hookReady"] and row["branchMatchesGitHub"] and row["snapshotMatchesBranch"] for row in rows),
"bridge": bridge,
"repositories": rows,
"url": url,
"mirrorStatus": {"exitCode": mirror_status_rc, "errorTail": text(mirror_status_err_path)},
"bridgeLogs": {"exitCode": bridge_logs_rc, "events": bridge_events[-12:], "errorTail": text(bridge_log_err_path)},
"valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
+141 -37
View File
@@ -34,6 +34,15 @@ interface GiteaTarget {
createNamespace: boolean;
storageClassName: string;
publicExposureEnabled: boolean;
webhookSync: GiteaTargetWebhookSync | null;
}
interface GiteaTargetWebhookSync {
publicPath: string;
frpc: {
proxyName: string;
remotePort: number;
};
}
interface GiteaConfig {
@@ -576,6 +585,10 @@ function parseTarget(record: Record<string, unknown>, index: number): GiteaTarge
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),
@@ -586,6 +599,18 @@ function parseTarget(record: Record<string, unknown>, index: number): GiteaTarge
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`),
},
};
}
@@ -605,6 +630,15 @@ function validateConfig(gitea: GiteaConfig): void {
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 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}`);
@@ -653,7 +687,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
const secrets = gitea.sourceAuthority.webhookSync.enabled && !options.dryRun ? ensureMirrorSecrets(gitea, true, true) : undefined;
const result = await capture(config, target.route, ["sh"], remoteScript("apply", gitea, target, manifest, options, { frpcSecret, secrets }));
const parsed = parseJsonOutput(result.stdout);
const caddy = !options.dryRun && result.exitCode === 0 && parsed?.ok === true && publicExposureEnabled(gitea, target)
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 {
@@ -665,7 +699,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
config: compactConfigSummary(gitea, target),
policy,
publicExposure: publicExposureSummary(gitea),
secrets: { frpc: frpcSecretSummary(frpcSecret), webhookSync: secrets === undefined ? { enabled: false } : webhookSecretSummary(gitea, secrets) },
secrets: { frpc: frpcSecretSummary(frpcSecret), webhookSync: secrets === undefined ? { enabled: false } : webhookSecretSummary(gitea, target, secrets) },
pk01Caddy: caddy,
remote: parsed ?? compactCapture(result, { full: true }),
next: nextCommands(target.id),
@@ -882,7 +916,7 @@ async function mirrorWebhookApply(config: UniDeskConfig, options: MirrorOptions)
action: "platform-infra-gitea-mirror-webhook-apply",
mutation: true,
target: targetSummary(target),
webhook: webhookSyncSummary(gitea),
webhook: webhookSyncSummary(gitea, target),
repositories: arrayRecords(record(parsed).repositories),
remote: parsed ?? compactCapture(result, { full: true }),
next: mirrorNextCommands(target.id),
@@ -902,9 +936,11 @@ async function mirrorWebhookStatus(config: UniDeskConfig, options: MirrorOptions
action: "platform-infra-gitea-mirror-webhook-status",
mutation: false,
target: targetSummary(target),
webhook: webhookSyncSummary(gitea),
webhook: webhookSyncSummary(gitea, target),
repositories,
bridge: record(parsed).bridge,
bridgeLogs: record(parsed).bridgeLogs,
mirrorStatus: record(parsed).mirrorStatus,
remote: parsed ?? compactCapture(result, { full: true }),
next: mirrorNextCommands(target.id),
};
@@ -924,7 +960,7 @@ async function mirrorWebhookTest(config: UniDeskConfig, options: MirrorOptions):
action: "platform-infra-gitea-mirror-webhook-test",
mutation: true,
target: targetSummary(target),
webhook: webhookSyncSummary(gitea),
webhook: webhookSyncSummary(gitea, target),
repositories: arrayRecords(record(parsed).repositories),
remote: parsed ?? compactCapture(result, { full: true }),
next: mirrorNextCommands(target.id),
@@ -1095,7 +1131,7 @@ ${renderGithubSyncManifest(gitea, target)}`;
}
function renderGithubSyncManifest(gitea: GiteaConfig, target: GiteaTarget): string {
const sync = gitea.sourceAuthority.webhookSync;
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
@@ -1252,6 +1288,8 @@ function envVars(gitea: GiteaConfig, target: GiteaTarget): string {
}
function remoteScript(action: "apply" | "status" | "validate" | "mirror-bootstrap" | "mirror-sync" | "mirror-status" | "mirror-webhook-apply" | "mirror-webhook-status" | "mirror-webhook-test", gitea: GiteaConfig, target: GiteaTarget, manifest: string, options: ApplyOptions | MirrorOptions, params: MirrorRemoteParams = {}): string {
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,
@@ -1274,16 +1312,16 @@ function remoteScript(action: "apply" | "status" | "validate" | "mirror-bootstra
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: publicExposureEnabled(gitea, target) ? "1" : "0",
UNIDESK_GITEA_FRPC_DEPLOYMENT_NAME: gitea.app.publicExposure.frpc.deploymentName,
UNIDESK_GITEA_FRPC_ENABLED: frpcExposure === null ? "0" : "1",
UNIDESK_GITEA_FRPC_DEPLOYMENT_NAME: frpcExposure?.frpc.deploymentName ?? gitea.app.publicExposure.frpc.deploymentName,
UNIDESK_GITEA_WEBHOOK_SYNC_ENABLED: gitea.sourceAuthority.webhookSync.enabled ? "1" : "0",
UNIDESK_GITEA_WEBHOOK_PUBLIC_URL: githubWebhookPublicUrl(gitea),
UNIDESK_GITEA_WEBHOOK_PATH: gitea.sourceAuthority.webhookSync.publicPath,
UNIDESK_GITEA_WEBHOOK_EVENTS: gitea.sourceAuthority.webhookSync.events.join(","),
UNIDESK_GITEA_WEBHOOK_DEPLOYMENT: gitea.sourceAuthority.webhookSync.bridge.deploymentName,
UNIDESK_GITEA_WEBHOOK_SERVICE: gitea.sourceAuthority.webhookSync.bridge.serviceName,
UNIDESK_GITEA_WEBHOOK_SERVICE_PORT: String(gitea.sourceAuthority.webhookSync.bridge.httpPort),
UNIDESK_GITEA_WEBHOOK_SECRET_NAME: gitea.sourceAuthority.webhookSync.bridge.secretName,
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;
@@ -1347,6 +1385,7 @@ function targetSummary(target: GiteaTarget): Record<string, unknown> {
createNamespace: target.createNamespace,
storageClassName: target.storageClassName,
publicExposureEnabled: target.publicExposureEnabled,
webhookSync: target.webhookSync,
};
}
@@ -1382,21 +1421,23 @@ function publicExposureEnabled(gitea: GiteaConfig, target: GiteaTarget): boolean
}
function targetPublicExposure(gitea: GiteaConfig, target: GiteaTarget): GiteaConfig["app"]["publicExposure"] {
return publicExposureEnabled(gitea, target) ? gitea.app.publicExposure : { ...gitea.app.publicExposure, enabled: false };
const frpcExposure = targetFrpcExposure(gitea, target);
return frpcExposure === null ? { ...gitea.app.publicExposure, enabled: false } : frpcExposure;
}
function prepareGiteaFrpcSecret(gitea: GiteaConfig, target: GiteaTarget): FrpcSecretMaterial | undefined {
if (!publicExposureEnabled(gitea, target)) return undefined;
const exposure = targetFrpcExposure(gitea, target);
if (exposure === null) return undefined;
const base = prepareFrpcSecret({
secretRoot: gitea.app.publicExposure.secretRoot,
exposure: gitea.app.publicExposure,
exposure,
sourcePathRedactor: redactRepoPath,
parseEnvFile,
requiredEnvValue,
readTextFile,
});
const sync = gitea.sourceAuthority.webhookSync;
if (!sync.enabled) return base;
const sync = targetWebhookSync(gitea, target);
if (!sync.enabled || !publicExposureEnabled(gitea, target)) return base;
const extraProxy = [
"[[proxies]]",
`name = "${escapeTomlString(sync.frpc.proxyName)}"`,
@@ -1414,18 +1455,51 @@ function prepareGiteaFrpcSecret(gitea: GiteaConfig, target: GiteaTarget): FrpcSe
};
}
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 sync = gitea.sourceAuthority.webhookSync;
if (!sync.enabled) return await applyPk01CaddyBlock(config, "gitea", exposure);
const siteBlock = `${exposure.dns.hostname} {
handle ${sync.publicPath}* {
reverse_proxy 127.0.0.1:${sync.frpc.remotePort} {
const webhookHandles = webhookCaddyRoutes(gitea)
.sort((left, right) => right.publicPath.length - left.publicPath.length)
.map((route) => ` handle ${route.publicPath}* {
reverse_proxy 127.0.0.1:${route.remotePort} {
transport http {
response_header_timeout ${exposure.pk01.responseHeaderTimeoutSeconds}s
}
}
}
}`).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} {
@@ -1438,6 +1512,19 @@ async function applyGiteaCaddyBlock(config: UniDeskConfig, gitea: GiteaConfig):
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 {
@@ -1469,11 +1556,11 @@ function frpcSecretSummary(secret: FrpcSecretMaterial | undefined): Record<strin
};
}
function webhookSecretSummary(gitea: GiteaConfig, secrets: MirrorSecrets): Record<string, unknown> {
const sync = gitea.sourceAuthority.webhookSync;
function webhookSecretSummary(gitea: GiteaConfig, target: GiteaTarget, secrets: MirrorSecrets): Record<string, unknown> {
const sync = targetWebhookSync(gitea, target);
return {
enabled: sync.enabled,
publicUrl: githubWebhookPublicUrl(gitea),
publicUrl: githubWebhookPublicUrl(gitea, target),
sourceRef: sync.secret.sourceRef,
targetSecret: sync.bridge.secretName,
keys: ["github-token", "gitea-username", "gitea-password", "github-webhook-secret"],
@@ -1482,9 +1569,9 @@ function webhookSecretSummary(gitea: GiteaConfig, secrets: MirrorSecrets): Recor
};
}
function githubWebhookPublicUrl(gitea: GiteaConfig): string {
function githubWebhookPublicUrl(gitea: GiteaConfig, target: GiteaTarget): string {
const base = gitea.app.publicExposure.publicBaseUrl.replace(/\/+$/u, "");
return `${base}${gitea.sourceAuthority.webhookSync.publicPath}`;
return `${base}${targetWebhookSync(gitea, target).publicPath}`;
}
function statusSummary(payload: Record<string, unknown>): Record<string, unknown> {
@@ -1573,12 +1660,12 @@ function mirrorNextCommands(targetId: string): Record<string, string> {
};
}
function webhookSyncSummary(gitea: GiteaConfig): Record<string, unknown> {
const sync = gitea.sourceAuthority.webhookSync;
function webhookSyncSummary(gitea: GiteaConfig, target: GiteaTarget): Record<string, unknown> {
const sync = targetWebhookSync(gitea, target);
return {
enabled: sync.enabled,
direction: sync.direction,
publicUrl: githubWebhookPublicUrl(gitea),
publicUrl: githubWebhookPublicUrl(gitea, target),
events: sync.events,
bridge: sync.bridge,
frpc: sync.frpc,
@@ -1836,15 +1923,32 @@ function renderMirrorWebhookApply(result: Record<string, unknown>): RenderedCliR
}
function renderMirrorWebhookStatus(result: Record<string, unknown>): RenderedCliResult {
const repos = arrayRecords(result.repositories).map((repo) => [stringValue(repo.key), stringValue(repo.repository), boolText(repo.hookReady), stringValue(repo.hookId), stringValue(repo.active), compactTail(stringValue(repo.error))]);
const repos = arrayRecords(result.repositories).map((repo) => {
const latestPush = record(repo.latestPushDelivery);
const latest = record(repo.latestDelivery);
const delivery = latestPush.event !== "" ? latestPush : latest;
const bridge = record(repo.lastBridgeEvent);
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.branchMatchesGitHub),
`${stringValue(delivery.event)}:${stringValue(delivery.statusCode)}:${stringValue(delivery.status)}`,
`${stringValue(bridge.event)}:${boolText(bridge.ok)}:${stringValue(bridge.sourceCommit).slice(0, 12)}`,
compactTail(stringValue(repo.error)),
];
});
const bridge = record(result.bridge);
const bridgeLogs = record(result.bridgeLogs);
const next = record(result.next);
return rendered(result, "platform-infra gitea mirror webhook status", [
"PLATFORM-INFRA GITEA MIRROR WEBHOOK STATUS",
...table(["TARGET", "BRIDGE", "READY", "URL"], [[stringValue(record(result.target).id), stringValue(bridge.deployment), boolText(bridge.ready), stringValue(record(result.webhook).publicUrl)]]),
...table(["TARGET", "BRIDGE", "READY", "URL", "LOGS"], [[stringValue(record(result.target).id), stringValue(bridge.deployment), boolText(bridge.ready), stringValue(record(result.webhook).publicUrl), `rc=${stringValue(bridgeLogs.exitCode)}`]]),
"",
"GITHUB HOOKS",
...(repos.length === 0 ? ["-"] : table(["KEY", "REPOSITORY", "READY", "HOOK_ID", "ACTIVE", "ERROR"], repos)),
...(repos.length === 0 ? ["-"] : table(["KEY", "HOOK", "GH_HEAD", "GITEA", "SNAPSHOT", "MATCH", "DELIVERY", "BRIDGE_EVENT", "ERROR"], repos)),
...remoteErrorLines(result),
"",
`NEXT ${stringValue(next.webhookApply)}`,