fix(cicd): add target gitea webhook status
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)}`,
|
||||
|
||||
Reference in New Issue
Block a user