diff --git a/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts b/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts index bb2050de..487a065c 100644 --- a/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts +++ b/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts @@ -263,6 +263,18 @@ test("PaC apply checks the Gitea repository before dry-run return and cluster mu } }); +test("PaC automatic webhook disable is target-scoped and does not touch Kubernetes", async () => { + const plan = await runPlatformInfraPipelinesAsCodeCommand({} as never, ["automatic-webhooks", "disable", "--target", "NC01"]); + expect(plan).toMatchObject({ ok: true, mutation: false, mode: "plan", target: { id: "NC01" }, plan: { automaticWebhookEnabled: false } }); + expect(Number((plan as any).plan.repositoryCount)).toBeGreaterThan(1); + const remote = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-remote.sh"), "utf8"); + const actionStart = remote.indexOf("disable_automatic_webhooks_action()"); + const action = remote.slice(actionStart, remote.indexOf("repository_manifest()", actionStart)); + expect(action).toContain("gitea_api DELETE"); + expect(action).not.toContain("kubectl"); + expect(action).not.toContain("ensure_webhook"); +}); + test("PaC release manifest renders YAML-owned watcher startup backlog probes", () => { const pac = readPacConfig({ consumerId: "hwlab-nc01-v03", selectDefault: true }); const source = readFileSync(resolve(import.meta.dir, "fixtures/pac/release-leading-comment-only.yaml"), "utf8"); diff --git a/scripts/src/platform-infra-pipelines-as-code-remote.sh b/scripts/src/platform-infra-pipelines-as-code-remote.sh index 3be242ed..5e412cd0 100644 --- a/scripts/src/platform-infra-pipelines-as-code-remote.sh +++ b/scripts/src/platform-infra-pipelines-as-code-remote.sh @@ -192,6 +192,37 @@ NODE done } +disable_automatic_webhooks_action() { + repositories=$(UNIDESK_PAC_WEBHOOK_REPOSITORIES_JSON="$UNIDESK_PAC_WEBHOOK_REPOSITORIES_JSON" node <<'NODE' +const data = JSON.parse(process.env.UNIDESK_PAC_WEBHOOK_REPOSITORIES_JSON || '[]'); +for (const item of data) console.log(`${item.id}\t${item.owner}\t${item.repo}`); +NODE +) + repository_count=0 + removed_count=0 + tab=$(printf '\t') + while IFS="$tab" read -r repository_id owner repo; do + [ -n "$repository_id" ] || continue + repository_count=$((repository_count + 1)) + UNIDESK_PAC_GITEA_OWNER=$owner + UNIDESK_PAC_GITEA_REPO=$repo + hooks=$(gitea_api GET "repos/$owner/$repo/hooks") + hook_ids=$(HOOKS_JSON="$hooks" node <<'NODE' +const data = JSON.parse(process.env.HOOKS_JSON || '[]'); +const url = process.env.UNIDESK_PAC_WEBHOOK_URL; +for (const item of data) if (item?.config?.url === url && item?.id) console.log(item.id); +NODE +) + for hook_id in $hook_ids; do + gitea_api DELETE "repos/$owner/$repo/hooks/$hook_id" >/dev/null + removed_count=$((removed_count + 1)) + done + done < [--confirm]", valuesPrinted: false }; + const options = parseApplyOptions(args.slice(2)); + return await disableAutomaticWebhooks(config, options); + } if (action === "apply") { const options = parseApplyOptions(args.slice(1)); const result = await apply(config, options); @@ -583,6 +588,7 @@ function help(scope: string | null): Record { configTruth: configLabel, usage: [ "bun scripts/cli.ts platform-infra pipelines-as-code plan --target JD01", + "bun scripts/cli.ts platform-infra pipelines-as-code automatic-webhooks disable --target [--confirm]", "bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 [--json|--full|--raw]", "bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 [--consumer hwlab-jd01-v03] [--limit 10]", "bun scripts/cli.ts platform-infra pipelines-as-code diagnose-regression --target NC01 --consumer agentrun-nc01-v02 [--limit 20] [--json|--full]", @@ -1517,6 +1523,54 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise> { + const pac = readPacConfig(); + const target = resolveTarget(pac, options.targetId); + const repositories = [...new Map(validPacConsumers(pac) + .filter((consumer) => consumer.node.toLowerCase() === target.id.toLowerCase()) + .map((consumer) => { + const repository = resolveRepository(pac, consumer.repositoryRef); + return [repository.id, repository] as const; + })).values()]; + const plan = { + mode: pac.deliveryTrigger.mode, + mechanism: pac.deliveryTrigger.mechanism, + configPath: `${configLabel}#deliveryTrigger`, + repositoryCount: repositories.length, + repositories: repositories.map((repository) => ({ id: repository.id, owner: repository.owner, repo: repository.repo })), + automaticWebhookEnabled: pac.gitea.webhook.enabled, + valuesPrinted: false, + }; + if (!options.confirm) { + return { + ok: true, + action: "platform-infra-pipelines-as-code-automatic-webhooks-disable", + mutation: false, + mode: "plan", + target: targetSummary(target), + plan, + next: `bun scripts/cli.ts platform-infra pipelines-as-code automatic-webhooks disable --target ${target.id} --confirm`, + valuesPrinted: false, + }; + } + const consumer = validPacConsumers(pac).find((item) => item.node.toLowerCase() === target.id.toLowerCase()); + if (consumer === undefined || repositories.length === 0) throw new Error(`target ${target.id} has no valid PaC repositories`); + const repository = resolveRepository(pac, consumer.repositoryRef); + const secrets = ensureSecrets(pac, false); + const result = await capture(config, target.route, ["sh"], remoteScript("disable-automatic-webhooks", pac, target, repository, consumer, options, secrets, "", [consumer], false, repositories)); + const remote = parseJsonOutput(result.stdout) ?? {}; + return { + ok: result.exitCode === 0 && remote.ok === true, + action: "platform-infra-pipelines-as-code-automatic-webhooks-disable", + mutation: remote.mutation === true, + mode: "confirmed", + target: targetSummary(target), + plan, + remote: { ...remote, exitCode: result.exitCode, stderrTail: result.stderr.slice(-1200) }, + valuesPrinted: false, + }; +} + async function bootstrap(config: UniDeskConfig, options: ApplyOptions): Promise> { const pac = readPacConfig({ consumerId: options.consumerId, selectDefault: true }); const target = resolveTarget(pac, options.targetId); @@ -2230,7 +2284,7 @@ export class PacReleaseManifestRenderError extends Error { } } -function remoteScript(action: "apply" | "status" | "history" | "diagnose-regression" | "debug-step", pac: PacConfig, target: PacTarget, repository: PacRepository, consumer: PacConsumer, options: ApplyOptions | HistoryOptions, secrets: SecretMaterial, releaseManifest: string, historyConsumers: PacConsumer[] = [consumer], collectArtifactLogs = false): string { +function remoteScript(action: "apply" | "status" | "history" | "diagnose-regression" | "debug-step" | "disable-automatic-webhooks", pac: PacConfig, target: PacTarget, repository: PacRepository, consumer: PacConsumer, options: ApplyOptions | HistoryOptions, secrets: SecretMaterial, releaseManifest: string, historyConsumers: PacConsumer[] = [consumer], collectArtifactLogs = false, webhookRepositories: PacRepository[] = [repository]): string { const webhookUrl = `${pac.gitea.internalBaseUrl.replace(/\/+$/u, "").replace(/gitea-http\.[^.]+\.svc\.cluster\.local:3000/u, `${pac.release.controllerServiceName}.${pac.release.namespace}.svc.cluster.local:${pac.release.controllerServicePort}`)}`; const admissionIdentity = pacAdmissionDesiredIdentity(target.id); const rbacIdentity = pacConsumerRbacDesiredIdentity(target.id); @@ -2256,6 +2310,7 @@ function remoteScript(action: "apply" | "status" | "history" | "diagnose-regress UNIDESK_PAC_GITEA_API_USERNAME: pac.gitea.admin.apiUsername, UNIDESK_PAC_GITEA_OWNER: repository.owner, UNIDESK_PAC_GITEA_REPO: repository.repo, + UNIDESK_PAC_WEBHOOK_REPOSITORIES_JSON: JSON.stringify(webhookRepositories.map((item) => ({ id: item.id, owner: item.owner, repo: item.repo }))), UNIDESK_PAC_WEBHOOK_URL: webhookUrl, UNIDESK_PAC_WEBHOOK_SECRET: secrets.webhookSecret, UNIDESK_PAC_REPOSITORY_NAME: repository.name,