fix: 保留 catalog bootstrap 并同步 PaC pipeline
This commit is contained in:
+234
-4
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user