Files
pikasTech-unidesk/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts
T
2026-07-16 03:27:42 +02:00

381 lines
23 KiB
TypeScript

import { expect, test } from "bun:test";
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import type { GiteaConfig, GiteaMirrorRepository } from "./platform-infra-gitea-config";
import { renderMirrorBootstrap } from "./platform-infra-gitea-render";
import { pacBootstrapYamlMatchFailure, projectPacBootstrapResult, renderPacBootstrap, resolvePacBootstrapMirrorRepository } from "./platform-infra-pipelines-as-code-bootstrap";
import { PacReleaseManifestRenderError, parsePacConfigDocument, readPacConfig, renderPacReleaseManifest, runPlatformInfraPipelinesAsCodeCommand, validatePacConfig, validPacConsumers } from "./platform-infra-pipelines-as-code";
import { materializeYamlComposition } from "./yaml-composition";
const mirror: GiteaMirrorRepository = {
key: "widgets-nc01",
targetId: "NC01",
upstream: { repository: "acme/widgets", cloneUrl: "https://github.com/acme/widgets.git", branch: "main", visibility: "public" },
gitea: { owner: "mirrors", name: "acme-widgets", mirrorMode: "controlled-push", publicRead: true, readUrl: "http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/acme-widgets.git" },
gitops: { branch: "main", flushDisposition: "not-a-gitops-branch" },
snapshot: { prefix: "refs/unidesk/snapshots/widgets-main-nc01" },
legacyGitMirror: null,
};
function fixtureConfig(repositories: GiteaMirrorRepository[]): GiteaConfig {
return { sourceAuthority: { repositories } } as unknown as GiteaConfig;
}
function pacConfigDocument(): Record<string, unknown> {
const source = Bun.YAML.parse(readFileSync(resolve(import.meta.dir, "../../config/platform-infra/pipelines-as-code.yaml"), "utf8")) as Record<string, unknown>;
return structuredClone(materializeYamlComposition(source, { label: "config/platform-infra/pipelines-as-code.yaml" }).value);
}
test("PaC bootstrap uses one exact YAML-owned Gitea repository", () => {
const selected = resolvePacBootstrapMirrorRepository(fixtureConfig([mirror]), "nc01", {
id: "widgets-nc01",
owner: "mirrors",
repo: "acme-widgets",
cloneUrl: `${mirror.gitea.readUrl}/`,
sourceBranch: "main",
});
expect(selected.key).toBe("widgets-nc01");
const releaseMirror = { ...mirror, key: "widgets-release-nc01", upstream: { ...mirror.upstream, branch: "release" } };
expect(resolvePacBootstrapMirrorRepository(fixtureConfig([mirror, releaseMirror]), "NC01", {
id: "widgets-release-nc01",
owner: "mirrors",
repo: "acme-widgets",
cloneUrl: mirror.gitea.readUrl,
sourceBranch: "release",
}).key).toBe("widgets-release-nc01");
expect(() => resolvePacBootstrapMirrorRepository(fixtureConfig([]), "NC01", {
id: "widgets-nc01",
owner: "mirrors",
repo: "acme-widgets",
cloneUrl: mirror.gitea.readUrl,
sourceBranch: "main",
})).toThrow("no exact match");
expect(() => resolvePacBootstrapMirrorRepository(fixtureConfig([mirror, { ...mirror }]), "NC01", {
id: "widgets-nc01",
owner: "mirrors",
repo: "acme-widgets",
cloneUrl: mirror.gitea.readUrl,
sourceBranch: "main",
})).toThrow("2 exact matches");
});
test("selfmedia consumers share one PaC Repository while keeping branch parameters isolated", () => {
const config = Bun.YAML.parse(readFileSync(resolve(import.meta.dir, "../../config/platform-infra/pipelines-as-code.yaml"), "utf8")) as Record<string, unknown>;
const repositories = config.repositories as Array<Record<string, unknown>>;
const consumers = config.consumers as Array<Record<string, unknown>>;
const selfmediaRepositories = repositories.filter((item) => item.url === "https://gitea.pikapython.com/mirrors/pikainc-selfmedia");
expect(selfmediaRepositories).toHaveLength(1);
const development = consumers.find((item) => item.id === "selfmedia-nc01");
const production = consumers.find((item) => item.id === "selfmedia-production-nc01");
expect(development?.repositoryRef).toBe("selfmedia-nc01");
expect(production?.repositoryRef).toBe("selfmedia-nc01");
expect(development?.namespace).toBe("selfmedia-ci");
expect(production?.namespace).toBe("selfmedia-ci");
expect((selfmediaRepositories[0]?.params as Record<string, unknown>).source_branch).toBe("master");
expect((production?.params as Record<string, unknown>).source_branch).toBe("release");
expect((production?.params as Record<string, unknown>).pipeline_name).toBe("selfmedia-production-nc01-pac");
});
test("HWLAB v0.3 and production share one NC01 PaC Repository while keeping lane parameters isolated", () => {
const config = pacConfigDocument();
const repositories = config.repositories as Array<Record<string, unknown>>;
const consumers = config.consumers as Array<Record<string, unknown>>;
const nc01Repositories = repositories.filter((item) => item.id === "hwlab-nc01-v03" || item.id === "hwlab-nc01-production");
expect(nc01Repositories.map((item) => item.id)).toEqual(["hwlab-nc01-v03"]);
const development = consumers.find((item) => item.id === "hwlab-nc01-v03");
const production = consumers.find((item) => item.id === "hwlab-nc01-production");
expect(development?.repositoryRef).toBe("hwlab-nc01-v03");
expect(production?.repositoryRef).toBe("hwlab-nc01-v03");
expect((nc01Repositories[0]?.params as Record<string, unknown>).source_branch).toBe("v0.3");
expect((production?.params as Record<string, unknown>).source_branch).toBe("release");
expect((production?.params as Record<string, unknown>).gitops_branch).toBe("release-gitops");
expect((production?.params as Record<string, unknown>).pipeline_name).toBe("hwlab-nc01-production-ci-image-publish");
expect(readPacConfig({ consumerId: "hwlab-nc01-production" }).validationWarnings).toEqual([]);
});
test("selected PaC consumer rejects duplicate URL authority conflicts before mutation", () => {
const conflictMutations: Array<[string, (repository: Record<string, unknown>) => void]> = [
["namespace", (repository) => { repository.namespace = "other-ci"; }],
["SecretRef", (repository) => { repository.secretName = "pac-gitea-hwlab-conflict"; }],
["providerType", (repository) => { repository.providerType = "github"; }],
];
for (const [field, mutate] of conflictMutations) {
const config = structuredClone(readPacConfig({ consumerId: "hwlab-nc01-production" }));
const sharedRepository = config.repositories.find((repository) => repository.id === "hwlab-nc01-v03");
const developmentConsumer = config.consumers.find((consumer) => consumer.id === "hwlab-nc01-v03");
if (sharedRepository === undefined || developmentConsumer === undefined) throw new Error("HWLAB NC01 fixtures are required");
const duplicateRepository = structuredClone(sharedRepository) as unknown as Record<string, unknown>;
duplicateRepository.id = "hwlab-nc01-conflict";
duplicateRepository.name = "hwlab-nc01-conflict";
mutate(duplicateRepository);
config.repositories.push(duplicateRepository as never);
config.consumers.push({ ...structuredClone(developmentConsumer), id: "hwlab-nc01-conflict", repositoryRef: "hwlab-nc01-conflict" });
expect(() => validatePacConfig(config, new Set(["hwlab-nc01-production"]))).toThrow(field);
}
});
test("unselected production validation is a non-blocking warning while selected production fails closed", () => {
const config = structuredClone(readPacConfig({ consumerId: "selfmedia-nc01" }));
const development = config.consumers.find((consumer) => consumer.id === "selfmedia-nc01");
const production = config.consumers.find((consumer) => consumer.id === "selfmedia-production-nc01");
expect(development?.deliveryProvenance).not.toBeNull();
expect(production?.deliveryProvenance).not.toBeNull();
if (development?.deliveryProvenance === null || development?.deliveryProvenance === undefined || production?.deliveryProvenance === null || production?.deliveryProvenance === undefined) {
throw new Error("selfmedia delivery provenance fixtures are required");
}
production.params.gitops_secret_name = "invalid-production-secret";
production.deliveryProvenance.markerValue = development.deliveryProvenance.markerValue;
config.consumers = [production, development, ...config.consumers.filter((consumer) => consumer !== production && consumer !== development)];
const warnings = validatePacConfig(config, new Set(["selfmedia-nc01"]));
const warning = warnings.find((item) => item.consumer === "selfmedia-production-nc01");
expect(warning).toEqual({
code: "pac-unselected-consumer-invalid",
object: { kind: "consumer", id: "selfmedia-production-nc01" },
consumer: "selfmedia-production-nc01",
configPath: "config/platform-infra/pipelines-as-code.yaml#consumers.selfmedia-production-nc01",
blocking: false,
evidence: expect.stringMatching(/^type=string,length=\d+,sha256=[0-9a-f]{64}$/u),
});
expect(() => validatePacConfig(config, new Set(["selfmedia-production-nc01"]))).toThrow();
});
test("unselected parse and repository errors remain warnings while selected objects fail closed", () => {
const invalidConsumerDocument = pacConfigDocument();
const invalidConsumer = (invalidConsumerDocument.consumers as Array<Record<string, unknown>>).find((item) => item.id === "selfmedia-production-nc01");
if (invalidConsumer === undefined) throw new Error("production consumer fixture is required");
invalidConsumer.namespace = 42;
const development = parsePacConfigDocument(invalidConsumerDocument, { consumerId: "selfmedia-nc01" });
expect(development.consumers.some((consumer) => consumer.id === "selfmedia-production-nc01")).toBe(false);
expect(development.validationWarnings).toContainEqual({
code: "pac-unselected-consumer-parse-invalid",
object: { kind: "consumer", id: "selfmedia-production-nc01" },
consumer: "selfmedia-production-nc01",
configPath: expect.stringMatching(/#consumers\[\d+\]$/u),
blocking: false,
evidence: expect.stringMatching(/^type=string,length=\d+,sha256=[0-9a-f]{64}$/u),
});
expect(() => parsePacConfigDocument(invalidConsumerDocument, { consumerId: "selfmedia-production-nc01" })).toThrow("namespace");
const invalidRepositoryDocument = pacConfigDocument();
const invalidRepository = (invalidRepositoryDocument.repositories as Array<Record<string, unknown>>).find((item) => item.id === "pikaoa-test-nc01");
if (invalidRepository === undefined) throw new Error("pikaoa repository fixture is required");
invalidRepository.cloneUrl = "https://gitea.example.invalid/mirrors/pikaoa.git";
const isolated = parsePacConfigDocument(invalidRepositoryDocument, { consumerId: "selfmedia-nc01" });
expect(isolated.validationWarnings).toContainEqual({
code: "pac-unselected-repository-invalid",
object: { kind: "repository", id: "pikaoa-test-nc01" },
consumer: null,
configPath: "config/platform-infra/pipelines-as-code.yaml#repositories.pikaoa-test-nc01",
blocking: false,
evidence: expect.stringMatching(/^type=string,length=\d+,sha256=[0-9a-f]{64}$/u),
});
expect(validPacConsumers(isolated).some((consumer) => consumer.repositoryRef === "pikaoa-test-nc01")).toBe(false);
expect(() => parsePacConfigDocument(invalidRepositoryDocument, { consumerId: "pikaoa-nc01" })).toThrow("cloneUrl");
const duplicateSelectedRepository = pacConfigDocument();
const repositories = duplicateSelectedRepository.repositories as Array<Record<string, unknown>>;
const selfmediaRepository = repositories.find((item) => item.id === "selfmedia-nc01");
if (selfmediaRepository === undefined) throw new Error("selfmedia repository fixture is required");
repositories.push(structuredClone(selfmediaRepository));
expect(() => parsePacConfigDocument(duplicateSelectedRepository, { consumerId: "selfmedia-nc01" })).toThrow("must match exactly one YAML object");
});
test("Gitea mirror bootstrap defaults to a visible dry-run instead of an empty refusal", () => {
const rendered = renderMirrorBootstrap({
ok: true,
action: "platform-infra-gitea-mirror-bootstrap",
mode: "dry-run",
mutation: false,
target: { id: "NC01" },
repositories: [{ key: "widgets-nc01", giteaOwner: "mirrors", giteaRepo: "acme-widgets" }],
blockers: [],
next: { confirm: "bun scripts/cli.ts platform-infra gitea mirror bootstrap --target NC01 --repo widgets-nc01 --confirm" },
}).renderedText;
expect(rendered).toContain("MODE MUTATION TARGET");
expect(rendered).toContain("widgets-nc01");
expect(rendered).toContain("--repo widgets-nc01 --confirm");
expect(rendered).not.toContain("missing-confirm");
});
test("PaC apply checks the Gitea repository before dry-run return and cluster mutation", () => {
const remote = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-remote.sh"), "utf8");
const action = remote.indexOf("apply_action() {");
const preflight = remote.indexOf("if ! repository_preflight", action);
const dryRun = remote.indexOf('if [ "$UNIDESK_PAC_DRY_RUN" = "1" ]', action);
const release = remote.indexOf(" apply_release", action);
const admission = remote.indexOf(" apply_admission_manifest", release);
const rbac = remote.indexOf(" rbac_tmp=$(mktemp)", admission);
expect(action).toBeGreaterThanOrEqual(0);
expect(preflight).toBeGreaterThan(action);
expect(dryRun).toBeGreaterThan(preflight);
expect(release).toBeGreaterThan(dryRun);
expect(admission).toBeGreaterThan(release);
expect(rbac).toBeGreaterThan(admission);
expect(remote).toContain('"admission":{"applied":%s,"ready":%s}');
expect(remote).toContain('"admissionProvenance":%s');
expect(remote).toContain('"error":"gitea-repository-missing"');
const statusAction = remote.slice(remote.indexOf("status_action() {"), remote.indexOf("history_action() {"));
const bootstrapReadyAssignment = statusAction.split("\n").find((line) => line.includes("bootstrap_ready=$("));
expect(bootstrapReadyAssignment).toBeDefined();
expect(remote).toContain("status_progress() {");
expect(remote).toContain('stepTimeoutSeconds');
expect(remote).toContain('pac_read kubectl');
expect(remote).toContain('export UNIDESK_PAC_ARGO_FILE="$argo_file"');
expect(remote).toContain('get pipelinerun -l "pipelinesascode.tekton.dev/repository=$UNIDESK_PAC_REPOSITORY_NAME"');
expect(statusAction).toContain("status_progress artifact-evidence");
expect(statusAction).toContain("status_progress diagnostics");
expect(statusAction).toContain("status_progress complete");
expect(statusAction).toContain("output.code === 'pac-gitops-missing'");
expect(statusAction).toContain("output.code = 'pac-artifact-log-evidence-deferred'");
expect(statusAction).toContain("blocking: false");
expect(statusAction).toContain('[ "$bootstrap_ready" = "true" ]');
expect(statusAction).toContain("output.code = 'pac-consumer-bootstrap-not-ready'");
expect(statusAction).toContain("output.ok = false");
expect(statusAction).toContain("code: 'pac-admission-provenance-not-ready'");
expect(statusAction).toContain("blocking: false");
for (const fixture of [
{ state: { configured: false, ready: null }, expected: true },
{ state: { configured: true, ready: true }, expected: true },
{ state: { configured: true, ready: false }, expected: false },
]) {
const smoke = spawnSync("sh", ["-eu", "-c", [
"consumer_bootstrap=$UNIDESK_PAC_BOOTSTRAP_STATE",
bootstrapReadyAssignment!.trim(),
`printf '{"ok":%s}\\n' "$( [ "$bootstrap_ready" = "true" ] && echo true || echo false )"`,
].join("\n")], {
encoding: "utf8",
env: { ...process.env, UNIDESK_PAC_BOOTSTRAP_STATE: JSON.stringify(fixture.state) },
});
expect(smoke).toMatchObject({ status: 0, stderr: "" });
expect(JSON.parse(smoke.stdout)).toEqual({ ok: fixture.expected });
}
});
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");
const manifest = renderPacReleaseManifest(pac, source);
const documents = manifest.split(/^---\s*$/mu).map((item) => item.trim()).filter(Boolean).map((item) => Bun.YAML.parse(item) as any);
expect(documents).toHaveLength(2);
const watcher = documents[0].spec.template.spec.containers[0];
expect(watcher.startupProbe).toEqual(pac.release.workloads.watcher.startupProbe);
expect(watcher.readinessProbe).toEqual(pac.release.workloads.watcher.readinessProbe);
expect(watcher.livenessProbe).toEqual(pac.release.workloads.watcher.livenessProbe);
expect(documents[1].kind).toBe("Service");
});
test("PaC release manifest rejects resources without typed object identity", () => {
const pac = readPacConfig({ consumerId: "hwlab-nc01-v03", selectDefault: true });
const source = readFileSync(resolve(import.meta.dir, "fixtures/pac/release-missing-object-identity.yaml"), "utf8");
try {
renderPacReleaseManifest(pac, source);
throw new Error("expected release manifest renderer to reject the invalid resource");
} catch (error) {
expect(error).toBeInstanceOf(PacReleaseManifestRenderError);
expect(error).toMatchObject({
code: "pac-release-manifest-object-identity-invalid",
documentIndex: 2,
missingFields: ["apiVersion", "kind"],
});
}
});
test("platform bootstrap help advertises the composite entry before low-level apply", async () => {
const help = await runPlatformInfraPipelinesAsCodeCommand({} as never, ["help", "platform-bootstrap"]);
const usage = (help as Record<string, unknown>).usage as string[];
expect(usage[0]).toContain("pipelines-as-code bootstrap");
expect(usage).toContain("bun scripts/cli.ts platform-infra pipelines-as-code bootstrap --target <NODE> --consumer <consumer> --confirm");
});
test("PaC bootstrap projects typed stages and compact JSON", () => {
const result = {
ok: true,
action: "platform-infra-pipelines-as-code-bootstrap",
mutation: false,
mode: "dry-run",
target: { id: "NC01" },
consumer: { id: "widgets", node: "NC01", lane: "v01" },
repository: { id: "widgets", namespace: "ci" },
mirrorRepository: { key: mirror.key, upstreamRepository: mirror.upstream.repository, upstreamBranch: mirror.upstream.branch, owner: mirror.gitea.owner, repo: mirror.gitea.name },
githubPreflight: { ok: true, mutation: false, repositories: [{ key: mirror.key, code: "github-upstream-available", ok: true }] },
giteaBootstrap: { ok: true, mutation: false, mode: "dry-run", blockers: [] },
pipelinesAsCodeApply: { ok: true, mutation: false, mode: "dry-run", remote: { ok: true, preflight: { repositoryExists: true } } },
};
const projection = projectPacBootstrapResult(result);
expect(projection.ok).toBe(true);
expect(JSON.stringify(projection)).not.toContain("pipelinesAsCodeApply");
expect((projection.stages as Record<string, unknown>[]).map((item) => item.id)).toEqual(["gitea-init", "pac-controller", "pac-admission", "tekton-consumer", "gitops-argo"]);
const rendered = renderPacBootstrap(result).renderedText;
expect(rendered).toContain("PRECONDITIONS");
expect(rendered).toContain("yaml-repository-exact-match");
expect(rendered).toContain("confirm:");
});
test("PaC bootstrap keeps admission drift as a non-blocking warning", () => {
const result = {
ok: true,
action: "platform-infra-pipelines-as-code-bootstrap",
mutation: true,
mode: "confirmed",
target: { id: "NC01" },
consumer: { id: "widgets", node: "NC01", lane: "v01" },
repository: { id: "widgets", namespace: "ci" },
mirrorRepository: { key: mirror.key, upstreamRepository: mirror.upstream.repository, upstreamBranch: mirror.upstream.branch, owner: mirror.gitea.owner, repo: mirror.gitea.name },
githubPreflight: { ok: true, mutation: false, repositories: [{ key: mirror.key, code: "github-upstream-available", ok: true }] },
giteaBootstrap: { ok: true, mutation: false, mode: "skipped", blockers: [] },
pipelinesAsCodeApply: {
ok: true,
mutation: true,
mode: "confirmed",
remote: {
ok: true,
admission: { applied: true, ready: false },
admissionProvenance: { policyName: "unidesk-pac-delivery-provenance-v2", reasons: ["policy-generation-not-observed"] },
},
},
};
const projection = projectPacBootstrapResult(result);
expect(projection.ok).toBe(true);
expect((projection.stages as Record<string, unknown>[]).find((item) => item.id === "pac-admission")).toMatchObject({ ok: true, state: "warning", blocking: false });
expect(projection.warnings).toEqual([expect.objectContaining({ code: "pac-admission-provenance-not-ready", blocking: false })]);
});
test("PaC bootstrap returns fix-yaml next for zero YAML matches", () => {
const result = pacBootstrapYamlMatchFailure("NC01", "widgets", new Error("cannot resolve repository: no exact match"));
expect(projectPacBootstrapResult(result)).toBe(result);
expect((result.blockers as Record<string, unknown>[])[0]?.code).toBe("yaml-repository-no-match");
expect(result.next).toEqual({
kind: "fix-yaml",
command: "检查 config/platform-infra/gitea.yaml 与 config/platform-infra/pipelines-as-code.yaml 的 target、owner/name 和 read URL 精确匹配",
});
});
test("PaC bootstrap returns fix-yaml next for multiple YAML matches", () => {
const result = pacBootstrapYamlMatchFailure("NC01", "widgets", new Error("cannot resolve repository: 2 exact matches"));
expect(result.mutation).toBe(false);
expect((result.blockers as Record<string, unknown>[])[0]?.code).toBe("yaml-repository-multiple-matches");
expect((result.next as Record<string, unknown>).kind).toBe("fix-yaml");
});
test("PaC bootstrap distinguishes GitHub access failure from Gitea and PaC stages", () => {
const projection = projectPacBootstrapResult({
ok: false,
action: "platform-infra-pipelines-as-code-bootstrap",
mutation: false,
mode: "dry-run",
target: { id: "NC01" },
consumer: { id: "widgets", node: "NC01", lane: "v01" },
repository: { id: "widgets", namespace: "ci" },
mirrorRepository: { key: mirror.key, upstreamRepository: mirror.upstream.repository, upstreamBranch: mirror.upstream.branch, owner: mirror.gitea.owner, repo: mirror.gitea.name },
githubPreflight: { ok: false, mutation: false, repositories: [{ key: mirror.key, code: "github-repository-not-found-or-no-access", errorTail: "repository unavailable" }] },
giteaBootstrap: { ok: false, mutation: false, mode: "skipped", githubPreflight: { ok: false, repositories: [{ code: "github-repository-not-found-or-no-access", errorTail: "repository unavailable" }] } },
pipelinesAsCodeApply: { ok: false, mutation: false, mode: "skipped" },
});
expect((projection.blockers as Record<string, unknown>[])).toEqual([{ code: "github-repository-not-found-or-no-access", stage: "github-upstream", reason: "repository unavailable" }]);
expect((projection.preconditions as Record<string, unknown>[])[2]).toMatchObject({ ok: null, code: "gitea-bootstrap-skipped" });
expect((projection.stages as Record<string, unknown>[]).map((item) => item.state)).toEqual(["skipped", "skipped", "skipped", "skipped", "skipped"]);
expect((projection.next as Record<string, unknown>).kind).toBe("read-only-status");
});