fix: 保留 catalog bootstrap 并同步 PaC pipeline

This commit is contained in:
root
2026-07-10 23:02:42 +02:00
parent beacbdd37f
commit 6f2a7c39bc
6 changed files with 491 additions and 46 deletions
+234 -4
View File
@@ -87,6 +87,40 @@ test("v03 planner keeps affected code rollouts out of catalog reuse", async () =
assert.equal(plan.artifactCatalog.serviceCount, 3);
});
test("tracked PaC remote pipelines pass selected services to catalog restore", async () => {
const targets = [
{
node: "NC01",
pipelineName: "hwlab-nc01-v03-ci-image-publish",
pipelinePath: "ci/pipelines/hwlab-nc01-v03-ci-image-publish.yaml",
pacPath: ".tekton/hwlab-nc01-v03-pac.yaml"
},
{
node: "JD01",
pipelineName: "hwlab-jd01-v03-ci-image-publish",
pipelinePath: "ci/pipelines/hwlab-jd01-v03-ci-image-publish.yaml",
pacPath: ".tekton/hwlab-jd01-v03-pac.yaml"
}
];
for (const target of targets) {
const pipeline = JSON.parse(await readFile(target.pipelinePath, "utf8"));
assert.equal(pipeline.metadata?.name, target.pipelineName, `${target.node} tracked Pipeline name`);
const planArtifacts = pipeline.spec?.tasks?.find((task) => task.name === "plan-artifacts");
assert.ok(planArtifacts, `${target.node} tracked Pipeline plan-artifacts task`);
assert.ok(planArtifacts.taskSpec?.params?.some((param) => param.name === "services"), `${target.node} plan-artifacts services param`);
assert.ok(planArtifacts.params?.some((param) => param.name === "services" && param.value === "$(params.services)"), `${target.node} plan-artifacts services binding`);
const script = planArtifacts.taskSpec?.steps?.[0]?.script ?? "";
assert.match(script, /HWLAB_SERVICES="\$\(params\.services\)" node scripts\/ci\/restore-artifact-catalog\.mjs/u);
assert.match(script, /rolloutWithoutImageBuildServices/u);
assert.match(script, /artifactCatalog: plan\.artifactCatalog/u);
const pac = await readFile(target.pacPath, "utf8");
assert.ok(pac.includes(`pipelinesascode.tekton.dev/pipeline: "${target.pipelinePath}"`), `${target.node} PaC remote Pipeline path`);
assert.ok(pac.includes(`pipelineRef:\n name: ${target.pipelineName}`), `${target.node} PaC PipelineRef`);
}
});
test("artifact catalog restore refreshes GitOps authority even when a source file exists", async () => {
const repo = await createFixtureRepo();
const sourceBranch = (await git(repo, ["branch", "--show-current"])).stdout.trim();
@@ -117,6 +151,7 @@ test("artifact catalog restore refreshes GitOps authority even when a source fil
assert.equal(authority.authority, "gitops-branch");
assert.equal(authority.gitopsCommitId, gitopsCommitId);
assert.equal(authority.catalogSourceCommitId, "a".repeat(40));
assert.equal(authority.coverage.status, "exact");
assert.match(restore.stderr, /"status":"hydrated"/u);
const plan = await createCiPlan({
@@ -130,10 +165,21 @@ test("artifact catalog restore refreshes GitOps authority even when a source fil
assert.equal(plan.artifactCatalog.status, "hydrated");
assert.equal(plan.artifactCatalog.authority, "gitops-branch");
assert.equal(plan.artifactCatalog.gitopsCommitId, gitopsCommitId);
assert.equal(plan.artifactCatalog.coverage.status, "exact");
assert.equal(plan.compatibility.artifactCatalogAuthority, "gitops-branch");
const fastPlan = await runCiPlanCli(repo, [
"--lane", "v03",
"--base-ref", "HEAD",
"--target-ref", "HEAD",
"--artifact-catalog", "deploy/artifact-catalog.v03.json",
"--services", "hwlab-cloud-api,hwlab-cloud-web"
]);
assert.equal(fastPlan.compatibility.mode, "no-deps-global-change-fast-path");
assert.deepEqual(sortedServiceIds(fastPlan.reusedServices), ["hwlab-cloud-api", "hwlab-cloud-web"]);
});
test("artifact catalog restore rejects a GitOps service-set mismatch", async () => {
test("artifact catalog restore hydrates partial GitOps coverage and builds missing services", async () => {
const repo = await createFixtureRepo();
const sourceBranch = (await git(repo, ["branch", "--show-current"])).stdout.trim();
await git(repo, ["checkout", "-b", "fixture-gitops-mismatch"]);
@@ -145,21 +191,200 @@ test("artifact catalog restore rejects a GitOps service-set mismatch", async ()
await git(repo, ["commit", "-m", "remove a service from gitops catalog"]);
await git(repo, ["checkout", sourceBranch]);
const restore = await execFileAsync(process.execPath, [path.resolve("scripts/ci/restore-artifact-catalog.mjs")], {
cwd: repo,
env: {
...process.env,
HWLAB_CATALOG_PATH: "deploy/artifact-catalog.v03.json",
HWLAB_GITOPS_BRANCH: "fixture-gitops-mismatch",
HWLAB_GIT_READ_URL: repo,
HWLAB_SERVICES: "hwlab-cloud-api,hwlab-cloud-web"
}
});
assert.match(restore.stderr, /"status":"partial"/u);
assert.match(restore.stderr, /"missingSelectedServices":\["hwlab-cloud-web"\]/u);
const plan = await createCiPlan({
repoRoot: repo,
lane: "v03",
baseRef: "HEAD",
targetRef: "HEAD",
artifactCatalogPath: "deploy/artifact-catalog.v03.json",
services: ["hwlab-cloud-api", "hwlab-cloud-web"]
});
assert.equal(plan.artifactCatalog.authority, "gitops-branch");
assert.equal(plan.artifactCatalog.coverage.status, "partial");
assert.deepEqual(plan.artifactCatalog.coverage.missingSelectedServices, ["hwlab-cloud-web"]);
assert.deepEqual(sortedServiceIds(plan.buildServices), ["hwlab-cloud-web"]);
const cliPlan = await runCiPlanCli(repo, [
"--lane", "v03",
"--base-ref", "HEAD",
"--target-ref", "HEAD",
"--artifact-catalog", "deploy/artifact-catalog.v03.json",
"--services", "hwlab-cloud-api,hwlab-cloud-web"
]);
assert.equal(cliPlan.changedPathSummary.summary, "no-source-diff");
assert.equal(cliPlan.compatibility.mode, "advisory-read-only");
assert.deepEqual(cliPlan.artifactCatalog.coverage.missingSelectedServices, ["hwlab-cloud-web"]);
assert.deepEqual(sortedServiceIds(cliPlan.buildServices), ["hwlab-cloud-web"]);
});
test("v03 planner builds all services when the GitOps branch is absent", async () => {
const repo = await createFixtureRepo({ catalog: false });
const serviceIds = ["hwlab-cloud-api", "hwlab-cloud-web"];
const restore = await execFileAsync(process.execPath, [path.resolve("scripts/ci/restore-artifact-catalog.mjs")], {
cwd: repo,
env: {
...process.env,
HWLAB_CATALOG_PATH: "deploy/artifact-catalog.v03.json",
HWLAB_GITOPS_BRANCH: "fixture-gitops-missing",
HWLAB_GIT_READ_URL: repo,
HWLAB_SERVICES: serviceIds.join(",")
}
});
assert.match(restore.stderr, /"status":"missing"/u);
assert.match(restore.stderr, /"reason":"gitops-branch-missing"/u);
const plan = await createCiPlan({
repoRoot: repo,
lane: "v03",
baseRef: "HEAD",
targetRef: "HEAD",
artifactCatalogPath: "deploy/artifact-catalog.v03.json",
services: serviceIds
});
assert.equal(plan.artifactCatalog.authority, "none");
assert.deepEqual(sortedServiceIds(plan.buildServices), sortedServiceIds(serviceIds));
});
test("ci-plan CLI bypasses no-deps reuse when the v03 artifact catalog is missing", async () => {
const repo = await createFixtureRepo({ catalog: false });
const serviceIds = ["hwlab-cloud-api", "hwlab-cloud-web"];
const commonArgs = [
"--lane", "v03",
"--target-ref", "HEAD",
"--artifact-catalog", "deploy/artifact-catalog.v03.json",
"--services", serviceIds.join(",")
];
const initialPlan = await runCiPlanCli(repo, commonArgs);
assert.equal(initialPlan.baseRef, "HEAD");
assert.equal(initialPlan.changedPathSummary.summary, "no-source-diff");
assert.equal(initialPlan.compatibility.mode, "advisory-read-only");
assert.equal(initialPlan.artifactCatalog.authority, "none");
assert.deepEqual(sortedServiceIds(initialPlan.buildServices), sortedServiceIds(serviceIds));
assert.deepEqual(initialPlan.reusedServices, []);
await mkdir(path.join(repo, "docs/reference"), { recursive: true });
await writeFile(path.join(repo, "docs/reference/bootstrap.md"), "bootstrap docs\n");
await git(repo, ["add", "docs/reference/bootstrap.md"]);
await git(repo, ["commit", "-m", "add bootstrap docs"]);
const docsPlan = await runCiPlanCli(repo, ["--base-ref", "HEAD~1", ...commonArgs]);
assert.equal(docsPlan.changedPathSummary.docsOnly, true);
assert.equal(docsPlan.compatibility.mode, "advisory-read-only");
assert.deepEqual(sortedServiceIds(docsPlan.buildServices), sortedServiceIds(serviceIds));
assert.deepEqual(docsPlan.reusedServices, []);
});
test("v03 planner builds all services when the GitOps branch has no catalog", async () => {
const repo = await createFixtureRepo({ catalog: false });
const sourceBranch = (await git(repo, ["branch", "--show-current"])).stdout.trim();
const serviceIds = ["hwlab-cloud-api", "hwlab-cloud-web"];
const restore = await execFileAsync(process.execPath, [path.resolve("scripts/ci/restore-artifact-catalog.mjs")], {
cwd: repo,
env: {
...process.env,
HWLAB_CATALOG_PATH: "deploy/artifact-catalog.v03.json",
HWLAB_GITOPS_BRANCH: sourceBranch,
HWLAB_GIT_READ_URL: repo,
HWLAB_SERVICES: serviceIds.join(",")
}
});
assert.match(restore.stderr, /"status":"missing"/u);
assert.match(restore.stderr, /"reason":"gitops-catalog-missing"/u);
const plan = await createCiPlan({
repoRoot: repo,
lane: "v03",
baseRef: "HEAD",
targetRef: "HEAD",
artifactCatalogPath: "deploy/artifact-catalog.v03.json",
services: serviceIds
});
assert.equal(plan.artifactCatalog.authority, "none");
assert.deepEqual(sortedServiceIds(plan.buildServices), sortedServiceIds(serviceIds));
});
test("v02 planner accepts a current contract skeleton when the GitOps catalog is absent", async () => {
const repo = await createFixtureRepo({ catalog: false });
const sourceBranch = (await git(repo, ["branch", "--show-current"])).stdout.trim();
const sourceCommitId = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
const catalogPath = path.join(repo, "deploy/artifact-catalog.v02.json");
const serviceIds = ["hwlab-cloud-api", "hwlab-cloud-web"];
const skeleton = {
catalogVersion: "v1",
kind: "hwlab-artifact-catalog",
environment: "v02",
profile: "v02",
namespace: "hwlab-v02",
commitId: sourceCommitId,
artifactState: "contract-skeleton",
publish: { sourceCommitId },
services: serviceIds.map((serviceId) => ({
serviceId,
sourceCommitId,
digest: null,
buildBackend: "contract-skeleton"
}))
};
await writeFile(catalogPath, `${JSON.stringify(skeleton, null, 2)}\n`);
const restore = await execFileAsync(process.execPath, [path.resolve("scripts/ci/restore-artifact-catalog.mjs")], {
cwd: repo,
env: {
...process.env,
HWLAB_CATALOG_PATH: "deploy/artifact-catalog.v02.json",
HWLAB_GITOPS_BRANCH: sourceBranch,
HWLAB_GIT_READ_URL: repo,
HWLAB_SERVICES: serviceIds.join(",")
}
});
assert.match(restore.stderr, /"status":"bootstrap"/u);
assert.match(restore.stderr, /"authority":"source-bootstrap"/u);
const plan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD",
targetRef: "HEAD",
artifactCatalogPath: "deploy/artifact-catalog.v02.json",
services: serviceIds
});
assert.equal(plan.artifactCatalog.status, "bootstrap");
assert.equal(plan.artifactCatalog.authority, "source-bootstrap");
assert.equal(plan.artifactCatalog.sourceCommitId, sourceCommitId);
assert.equal(plan.artifactCatalog.coverage.status, "exact");
assert.deepEqual(sortedServiceIds(plan.buildServices), sortedServiceIds(serviceIds));
});
test("artifact catalog restore rejects a non-bootstrap source catalog when GitOps is absent", async () => {
const repo = await createFixtureRepo();
await assert.rejects(
execFileAsync(process.execPath, [path.resolve("scripts/ci/restore-artifact-catalog.mjs")], {
cwd: repo,
env: {
...process.env,
HWLAB_CATALOG_PATH: "deploy/artifact-catalog.v03.json",
HWLAB_GITOPS_BRANCH: "fixture-gitops-mismatch",
HWLAB_GITOPS_BRANCH: "fixture-gitops-missing",
HWLAB_GIT_READ_URL: repo,
HWLAB_SERVICES: "hwlab-cloud-api,hwlab-cloud-web"
}
}),
(error) => {
assert.equal(error.code, 1);
assert.match(error.stderr, /"reason":"gitops-catalog-service-ids-mismatch"/u);
assert.match(error.stderr, /missing=hwlab-cloud-web/u);
assert.match(error.stderr, /existing source catalog is not an allowed v02 contract skeleton/u);
return true;
}
);
@@ -1743,6 +1968,11 @@ function uniquePreserveOrder(values) {
return result;
}
async function runCiPlanCli(repo, args) {
const { stdout } = await execFileAsync(process.execPath, [path.resolve("scripts/ci-plan.mjs"), ...args], { cwd: repo });
return JSON.parse(stdout);
}
async function git(cwd, args) {
return execFileAsync("git", ["-c", "user.name=HWLAB Test", "-c", "user.email=hwlab-test@example.invalid", ...args], { cwd });
}