fix: 修复 PaC 环境复用计划语义

This commit is contained in:
root
2026-07-10 22:31:16 +02:00
parent a39f8b9ede
commit beacbdd37f
8 changed files with 362 additions and 29 deletions
+4
View File
@@ -2331,10 +2331,14 @@ function ciPlanReportSummary(ciPlan) {
compatibility: ciPlan.compatibility,
changedPathSummary: ciPlan.changedPathSummary,
imageBuildRequired: ciPlan.imageBuildRequired,
artifactCatalog: ciPlan.artifactCatalog,
affectedServices: ciPlan.affectedServices,
rolloutServices: ciPlan.rolloutServices ?? ciPlan.affectedServices,
buildServices: ciPlan.buildServices ?? ciPlan.affectedServices,
rolloutWithoutImageBuildServices: ciPlan.rolloutWithoutImageBuildServices ?? [],
reusedServices: ciPlan.reusedServices,
serviceReusedCount: ciPlan.serviceReusedCount,
imageBuildSkippedServices: ciPlan.imageBuildSkippedServices,
buildSkippedCount: ciPlan.buildSkippedCount,
ciCdPlan: ciPlan.ciCdPlan
};
+28
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { readArtifactCatalogSummary } from "./src/artifact-catalog-authority.mjs";
const args = parseArgs(process.argv.slice(2));
const noDepsGitOpsOnlyPaths = Object.freeze([
@@ -25,6 +27,10 @@ if (args.help) {
const noDepsPlan = tryCreateNoDepsPlan(args);
if (noDepsPlan) {
const artifactCatalog = await noDepsArtifactCatalogSummary(args);
noDepsPlan.artifactCatalog = artifactCatalog;
noDepsPlan.compatibility.artifactCatalog = artifactCatalog.status === "missing" ? null : artifactCatalog.catalogPath;
noDepsPlan.compatibility.artifactCatalogAuthority = artifactCatalog.authority;
process.stdout.write(args.pretty ? `${JSON.stringify(noDepsPlan, null, 2)}\n` : `${JSON.stringify(noDepsPlan)}\n`);
process.exit(0);
}
@@ -73,7 +79,10 @@ function tryCreateNoDepsPlan(options) {
affectedServices: [],
rolloutServices: [],
buildServices: [],
rolloutWithoutImageBuildServices: [],
reusedServices: services,
serviceReusedCount: services.length,
imageBuildSkippedServices: services,
buildSkippedCount: services.length,
services: services.map((serviceId) => ({
serviceId,
@@ -89,6 +98,7 @@ function tryCreateNoDepsPlan(options) {
ciCdPlan: {
willBuild: [],
willRollout: [],
willRolloutWithoutImageBuild: [],
willReuse: services,
willRunGitopsPromote: summary.gitopsOnly,
noImageBuildReason: summary.summary
@@ -96,6 +106,24 @@ function tryCreateNoDepsPlan(options) {
};
}
async function noDepsArtifactCatalogSummary(options) {
const catalogPath = options.artifactCatalogPath;
if (!catalogPath) {
return {
status: "missing",
authority: "none",
catalogPath: null,
authorityPath: null,
gitopsBranch: null,
gitopsCommitId: null,
catalogSourceCommitId: null,
serviceCount: 0,
catalogSha256: null
};
}
return readArtifactCatalogSummary({ repoRoot: process.cwd(), catalogPath });
}
function classifyNoDepsGlobalChange(changedPaths) {
const relevant = changedPaths.filter(Boolean);
const testOnly = relevant.length > 0 && relevant.every(isNoDepsTestOnlyPath);
+113
View File
@@ -54,6 +54,117 @@ test("node CI planner treats docs-only changes as no image build", async () => {
assert.equal(plan.changedPathSummary.docsOnly, true);
});
test("v03 planner keeps affected code rollouts out of catalog reuse", async () => {
const selectedServices = ["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-gateway"];
const repo = await createFixtureRepo({ serviceIds: selectedServices });
const baseRef = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
await mkdir(path.join(repo, "internal/cloud"), { recursive: true });
await mkdir(path.join(repo, "tools/src/hwlab-cli"), { recursive: true });
await writeFile(path.join(repo, "internal/cloud/kafka-event-bridge.ts"), "export const bridge = 2;\n");
await writeFile(path.join(repo, "tools/src/hwlab-cli/trace-renderer.ts"), "export const renderer = 2;\n");
await writeFile(path.join(repo, "web/hwlab-cloud-web/index.html"), "<html>v2</html>\n");
await git(repo, ["add", "."]);
await git(repo, ["commit", "-m", "change shared cloud and web source"]);
const plan = await createCiPlan({
repoRoot: repo,
lane: "v03",
baseRef,
targetRef: "HEAD",
artifactCatalogPath: "deploy/artifact-catalog.v03.json",
services: selectedServices
});
assert.deepEqual(plan.affectedServices, ["hwlab-cloud-api", "hwlab-cloud-web"]);
assert.deepEqual(plan.buildServices, []);
assert.deepEqual(plan.rolloutWithoutImageBuildServices, ["hwlab-cloud-api", "hwlab-cloud-web"]);
assert.deepEqual(plan.reusedServices, ["hwlab-gateway"]);
assert.equal(plan.serviceReusedCount, 1);
assert.deepEqual(plan.imageBuildSkippedServices, selectedServices);
assert.equal(plan.buildSkippedCount, 3);
assert.equal(plan.ciCdPlan.noImageBuildReason, "env-reuse-code-only-rollout");
assert.equal(plan.artifactCatalog.authority, "workspace-file");
assert.equal(plan.artifactCatalog.serviceCount, 3);
});
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();
await git(repo, ["checkout", "-b", "fixture-gitops"]);
const catalogPath = path.join(repo, "deploy/artifact-catalog.v03.json");
const catalog = JSON.parse(await readFile(catalogPath, "utf8"));
catalog.commitId = "gitops-authoritative";
catalog.publish = { sourceCommitId: "a".repeat(40) };
await writeFile(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
await git(repo, ["add", "deploy/artifact-catalog.v03.json"]);
await git(repo, ["commit", "-m", "refresh gitops catalog"]);
const gitopsCommitId = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
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",
HWLAB_GIT_READ_URL: repo,
HWLAB_SERVICES: "hwlab-cloud-api,hwlab-cloud-web"
}
});
const restored = JSON.parse(await readFile(catalogPath, "utf8"));
const authority = JSON.parse(await readFile(`${catalogPath}.authority.json`, "utf8"));
assert.equal(restored.commitId, "gitops-authoritative");
assert.equal(authority.authority, "gitops-branch");
assert.equal(authority.gitopsCommitId, gitopsCommitId);
assert.equal(authority.catalogSourceCommitId, "a".repeat(40));
assert.match(restore.stderr, /"status":"hydrated"/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.status, "hydrated");
assert.equal(plan.artifactCatalog.authority, "gitops-branch");
assert.equal(plan.artifactCatalog.gitopsCommitId, gitopsCommitId);
assert.equal(plan.compatibility.artifactCatalogAuthority, "gitops-branch");
});
test("artifact catalog restore rejects a GitOps service-set mismatch", async () => {
const repo = await createFixtureRepo();
const sourceBranch = (await git(repo, ["branch", "--show-current"])).stdout.trim();
await git(repo, ["checkout", "-b", "fixture-gitops-mismatch"]);
const catalogPath = path.join(repo, "deploy/artifact-catalog.v03.json");
const catalog = JSON.parse(await readFile(catalogPath, "utf8"));
catalog.services = catalog.services.filter((service) => service.serviceId === "hwlab-cloud-api");
await writeFile(catalogPath, `${JSON.stringify(catalog, null, 2)}\n`);
await git(repo, ["add", "deploy/artifact-catalog.v03.json"]);
await git(repo, ["commit", "-m", "remove a service from gitops catalog"]);
await git(repo, ["checkout", sourceBranch]);
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_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);
return true;
}
);
});
test("node CI planner rolls service runtime config changes without rebuilding image", async () => {
const repo = await createFixtureRepo();
const deployPath = path.join(repo, "deploy/deploy.yaml");
@@ -127,6 +238,8 @@ test("v02 planner rolls cloud-api service code changes without rebuilding servic
assert.equal(cloudApi.buildRequired, false);
assert.deepEqual(cloudApi.reason, ["code-input-changed", "component-path-changed"]);
assert.deepEqual(plan.buildServices, []);
assert.deepEqual(plan.rolloutWithoutImageBuildServices, ["hwlab-cloud-api"]);
assert.deepEqual(plan.reusedServices, []);
});
test("v02 planner rebuilds env image when an env reuse catalog digest is missing", async () => {
+62 -24
View File
@@ -2,9 +2,14 @@
// Restore the node/lane artifact catalog from the GitOps branch before CI planning.
import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import {
artifactCatalogAuthorityPath,
createHydratedArtifactCatalogAuthority
} from "../src/artifact-catalog-authority.mjs";
const startedAt = Date.now();
function env(name) {
@@ -12,6 +17,10 @@ function env(name) {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function csvEnv(name) {
return (env(name) ?? "").split(",").map((value) => value.trim()).filter(Boolean);
}
function emit(payload) {
process.stderr.write(JSON.stringify({
event: "artifact-catalog-restore",
@@ -24,16 +33,6 @@ function emit(payload) {
}) + "\n");
}
function catalogHasServices(filePath) {
if (!existsSync(filePath)) return false;
try {
const parsed = JSON.parse(readFileSync(filePath, "utf8"));
return Array.isArray(parsed?.services) && parsed.services.length > 0;
} catch {
return false;
}
}
function git(args, options = {}) {
return execFileSync("git", args, {
cwd: process.cwd(),
@@ -46,32 +45,71 @@ function git(args, options = {}) {
const catalogPath = env("HWLAB_CATALOG_PATH");
const gitopsBranch = env("HWLAB_GITOPS_BRANCH");
const remote = env("HWLAB_GIT_READ_URL") ?? env("HWLAB_GIT_URL");
const selectedServices = csvEnv("HWLAB_SERVICES");
if (!catalogPath || !gitopsBranch || !remote) {
emit({ status: "skipped", reason: "required-input-missing", hasCatalogPath: Boolean(catalogPath), hasGitopsBranch: Boolean(gitopsBranch), hasRemote: Boolean(remote) });
process.exit(0);
}
if (catalogHasServices(catalogPath)) {
emit({ status: "source-present", reason: "catalog-already-present" });
process.exit(0);
if (!catalogPath || !gitopsBranch || !remote || selectedServices.length === 0) {
emit({
status: "failed",
reason: "required-input-missing",
hasCatalogPath: Boolean(catalogPath),
hasGitopsBranch: Boolean(gitopsBranch),
hasRemote: Boolean(remote),
selectedServiceCount: selectedServices.length
});
process.exit(2);
}
try {
git(["fetch", "--depth=1", remote, `refs/heads/${gitopsBranch}`], { capture: false, timeoutMs: 60_000 });
const gitopsCommitId = git(["rev-parse", "FETCH_HEAD"], { timeoutMs: 15_000 }).trim();
const content = git(["show", `FETCH_HEAD:${catalogPath}`], { timeoutMs: 30_000 });
const parsed = JSON.parse(content);
if (!Array.isArray(parsed?.services) || parsed.services.length === 0) {
emit({ status: "skipped", reason: "gitops-catalog-has-no-services" });
process.exit(0);
throw new Error("gitops catalog has no services");
}
const actualServices = parsed.services.map((service) => nonEmptyText(service?.serviceId)).filter(Boolean);
const expected = uniqueSorted(selectedServices);
const actual = uniqueSorted(actualServices);
const missing = expected.filter((serviceId) => !actual.includes(serviceId));
const extra = actual.filter((serviceId) => !expected.includes(serviceId));
const invalidEntryCount = parsed.services.length - actualServices.length;
const duplicateCount = actualServices.length - actual.length;
if (missing.length > 0 || extra.length > 0 || invalidEntryCount > 0 || duplicateCount > 0) {
const error = new Error(`gitops catalog service ids mismatch: missing=${missing.join(",") || "none"} extra=${extra.join(",") || "none"} invalid=${invalidEntryCount} duplicate=${duplicateCount}`);
error.code = "catalog-service-ids-mismatch";
throw error;
}
const authorityPath = artifactCatalogAuthorityPath(catalogPath);
const authority = createHydratedArtifactCatalogAuthority({ catalog: parsed, catalogPath, gitopsBranch, gitopsCommitId });
mkdirSync(dirname(catalogPath), { recursive: true });
writeFileSync(catalogPath, JSON.stringify(parsed, null, 2) + "\n");
emit({ status: "restored", reason: "gitops-catalog", serviceCount: parsed.services.length, bytes: Buffer.byteLength(content) });
writeFileSync(authorityPath, JSON.stringify(authority, null, 2) + "\n");
emit({
status: "hydrated",
reason: "gitops-catalog",
authority: authority.authority,
authorityPath,
gitopsCommitId,
catalogSourceCommitId: authority.catalogSourceCommitId,
catalogSha256: authority.catalogSha256,
serviceCount: parsed.services.length,
selectedServiceCount: selectedServices.length,
bytes: Buffer.byteLength(content)
});
} catch (error) {
emit({
status: "skipped",
reason: "gitops-catalog-unavailable",
status: "failed",
reason: error?.code === "catalog-service-ids-mismatch" ? "gitops-catalog-service-ids-mismatch" : "gitops-catalog-unavailable",
error: error instanceof Error ? error.message.slice(0, 500) : String(error).slice(0, 500)
});
process.exit(1);
}
function nonEmptyText(value) {
const result = typeof value === "string" ? value.trim() : "";
return result || null;
}
function uniqueSorted(values) {
return [...new Set(values)].sort();
}
+4
View File
@@ -155,6 +155,10 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
const planArtifactsScript = planArtifacts.taskSpec.steps[0].script;
assert.match(planArtifactsScript, /node scripts\/ci\/restore-artifact-catalog\.mjs/u);
assert.match(planArtifactsScript, /HWLAB_GIT_READ_URL="\$\(params\.git-read-url\)"/u);
assert.match(planArtifactsScript, /HWLAB_SERVICES="\$\(params\.services\)"/u);
assert.match(planArtifactsScript, /rolloutWithoutImageBuildServices/u);
assert.match(planArtifactsScript, /serviceReusedCount/u);
assert.match(planArtifactsScript, /artifactCatalog: plan\.artifactCatalog/u);
assert.ok(planArtifacts.taskSpec.params.some((param) => param.name === "git-read-url"));
assert.ok(planArtifacts.taskSpec.params.some((param) => param.name === "gitops-branch"));
assert.ok(planArtifacts.params.some((param) => param.name === "lane" && param.value === "$(params.lane)"));
+123
View File
@@ -0,0 +1,123 @@
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";
export function artifactCatalogAuthorityPath(catalogPath) {
return `${catalogPath}.authority.json`;
}
export function artifactCatalogSha256(catalog) {
return createHash("sha256").update(JSON.stringify(catalog)).digest("hex");
}
export function artifactCatalogSourceCommitId(catalog) {
const direct = text(catalog?.publish?.sourceCommitId) || text(catalog?.sourceCommitId) || text(catalog?.commitId);
if (direct) return direct;
const serviceCommits = unique((catalog?.services ?? []).map((service) => text(service?.sourceCommitId)).filter(Boolean));
return serviceCommits.length === 1 ? serviceCommits[0] : null;
}
export function createHydratedArtifactCatalogAuthority({ catalog, catalogPath, gitopsBranch, gitopsCommitId }) {
return {
schemaVersion: "v1",
status: "hydrated",
authority: "gitops-branch",
catalogPath: normalizeCatalogPath(catalogPath),
gitopsBranch: text(gitopsBranch),
gitopsCommitId: text(gitopsCommitId),
catalogSourceCommitId: artifactCatalogSourceCommitId(catalog),
serviceCount: Array.isArray(catalog?.services) ? catalog.services.length : 0,
catalogSha256: artifactCatalogSha256(catalog)
};
}
export async function readArtifactCatalogSummary({ repoRoot, catalogPath, authorityPath = artifactCatalogAuthorityPath(catalogPath), catalog = undefined }) {
const loadedCatalog = catalog === undefined ? await readJsonIfPresent(repoRoot, catalogPath) : catalog;
const authority = await readJsonIfPresent(repoRoot, authorityPath);
const normalizedCatalogPath = normalizeCatalogPath(catalogPath);
if (!loadedCatalog) {
if (authority) throw new Error(`artifact catalog authority exists without catalog ${normalizedCatalogPath}`);
return {
status: "missing",
authority: "none",
catalogPath: normalizedCatalogPath,
authorityPath: null,
gitopsBranch: null,
gitopsCommitId: null,
catalogSourceCommitId: null,
serviceCount: 0,
catalogSha256: null
};
}
const serviceCount = Array.isArray(loadedCatalog.services) ? loadedCatalog.services.length : 0;
const catalogSha256 = artifactCatalogSha256(loadedCatalog);
const catalogSourceCommitId = artifactCatalogSourceCommitId(loadedCatalog);
if (!authority) {
return {
status: "loaded",
authority: "workspace-file",
catalogPath: normalizedCatalogPath,
authorityPath: null,
gitopsBranch: null,
gitopsCommitId: null,
catalogSourceCommitId,
serviceCount,
catalogSha256
};
}
validateHydratedAuthority({ authority, catalogPath: normalizedCatalogPath, serviceCount, catalogSha256 });
return {
status: "hydrated",
authority: "gitops-branch",
catalogPath: normalizedCatalogPath,
authorityPath: normalizeCatalogPath(authorityPath),
gitopsBranch: text(authority.gitopsBranch),
gitopsCommitId: text(authority.gitopsCommitId),
catalogSourceCommitId: text(authority.catalogSourceCommitId) || catalogSourceCommitId,
serviceCount,
catalogSha256
};
}
function validateHydratedAuthority({ authority, catalogPath, serviceCount, catalogSha256 }) {
if (authority.schemaVersion !== "v1" || authority.status !== "hydrated" || authority.authority !== "gitops-branch") {
throw new Error(`artifact catalog authority for ${catalogPath} is not a hydrated gitops-branch record`);
}
if (normalizeCatalogPath(authority.catalogPath) !== catalogPath) {
throw new Error(`artifact catalog authority path mismatch: expected ${catalogPath}, got ${authority.catalogPath ?? "missing"}`);
}
if (authority.serviceCount !== serviceCount) {
throw new Error(`artifact catalog authority service count mismatch for ${catalogPath}: expected ${serviceCount}, got ${authority.serviceCount ?? "missing"}`);
}
if (authority.catalogSha256 !== catalogSha256) {
throw new Error(`artifact catalog authority fingerprint mismatch for ${catalogPath}`);
}
if (!text(authority.gitopsBranch) || !/^[a-f0-9]{40}$/u.test(text(authority.gitopsCommitId) ?? "")) {
throw new Error(`artifact catalog authority gitops provenance is incomplete for ${catalogPath}`);
}
}
async function readJsonIfPresent(repoRoot, filePath) {
const resolved = path.isAbsolute(filePath) ? filePath : path.join(repoRoot, filePath);
try {
return JSON.parse(await readFile(resolved, "utf8"));
} catch (error) {
if (error?.code === "ENOENT") return null;
throw error;
}
}
function normalizeCatalogPath(value) {
return String(value ?? "").replaceAll("\\", "/").replace(/^\.\//u, "").trim();
}
function text(value) {
const result = typeof value === "string" ? value.trim() : "";
return result || null;
}
function unique(values) {
return [...new Set(values)];
}
+22 -3
View File
@@ -6,6 +6,10 @@ import * as https from "node:https";
import path from "node:path";
import { promisify } from "node:util";
import {
artifactCatalogAuthorityPath,
readArtifactCatalogSummary
} from "./artifact-catalog-authority.mjs";
import { parseStructuredConfig, readStructuredFileIfPresent } from "./structured-config.mjs";
const execFileAsync = promisify(execFile);
@@ -86,6 +90,13 @@ export async function createCiPlan(options = {}) {
assertLaneEnvReuseConfig(laneConfig, lane);
const artifactCatalogPath = options.artifactCatalogPath ?? defaultArtifactCatalogPath(laneConfig, lane);
const artifactCatalog = await readStructuredFileIfPresent(repoRoot, artifactCatalogPath, null);
const catalogAuthorityPath = options.artifactCatalogAuthorityPath ?? artifactCatalogAuthorityPath(artifactCatalogPath);
const artifactCatalogSummary = await readArtifactCatalogSummary({
repoRoot,
catalogPath: artifactCatalogPath,
authorityPath: catalogAuthorityPath,
catalog: artifactCatalog
});
const runtimeReuseConfigPath = options.runtimeReuseConfigPath ?? "gitops/reuse.ymal";
const runtimeReuseConfig = await readStructuredFileIfPresent(repoRoot, runtimeReuseConfigPath, null);
const runtimeReuseByService = runtimeReuseServices(runtimeReuseConfig);
@@ -339,7 +350,9 @@ export async function createCiPlan(options = {}) {
const affectedServices = services.filter((service) => service.affected).map((service) => service.serviceId);
const buildServices = services.filter((service) => service.buildRequired).map((service) => service.serviceId);
const reusedServices = services.filter((service) => !service.buildRequired).map((service) => service.serviceId);
const rolloutWithoutImageBuildServices = services.filter((service) => service.affected && !service.buildRequired).map((service) => service.serviceId);
const reusedServices = services.filter((service) => !service.affected && !service.buildRequired).map((service) => service.serviceId);
const imageBuildSkippedServices = services.filter((service) => !service.buildRequired).map((service) => service.serviceId);
const gitopsRenderChanged = hasGitOpsRenderChange(normalizedChangedPaths);
return {
planVersion: CI_PLAN_VERSION,
@@ -349,7 +362,8 @@ export async function createCiPlan(options = {}) {
targetRef,
compatibility: {
deployConfig: deployConfigPath,
artifactCatalog: artifactCatalog ? artifactCatalogPath : null,
artifactCatalog: artifactCatalogSummary.status === "missing" ? null : artifactCatalogPath,
artifactCatalogAuthority: artifactCatalogSummary.authority,
lane,
ciContractSource: "scripts/gitops-render.mjs",
mode: "advisory-read-only",
@@ -373,17 +387,22 @@ export async function createCiPlan(options = {}) {
},
changedPaths: normalizedChangedPaths,
changedPathSummary: globalChange,
artifactCatalog: artifactCatalogSummary,
imageBuildRequired: buildServices.length > 0,
affectedServices,
rolloutServices: affectedServices,
buildServices,
rolloutWithoutImageBuildServices,
reusedServices,
buildSkippedCount: services.length - buildServices.length,
serviceReusedCount: reusedServices.length,
imageBuildSkippedServices,
buildSkippedCount: imageBuildSkippedServices.length,
envArtifactGroups: envArtifactGroupPlans,
services,
ciCdPlan: {
willBuild: buildServices,
willRollout: affectedServices,
willRolloutWithoutImageBuild: rolloutWithoutImageBuildServices,
willReuse: reusedServices,
envArtifactGroups: envArtifactGroupPlans,
willRunGitopsPromote: globalChange.gitopsOnly || affectedServices.length > 0 || gitopsRenderChanged,
@@ -9,7 +9,7 @@ if [ -d "$ci_node_deps/yaml" ] && [ ! -e node_modules/yaml ]; then
ln -s "$ci_node_deps/yaml" node_modules/yaml
fi
test "$(git rev-parse HEAD)" = "$(params.revision)"
HWLAB_CATALOG_PATH="$(params.catalog-path)" HWLAB_GIT_URL="$(params.git-url)" HWLAB_GIT_READ_URL="$(params.git-read-url)" HWLAB_GITOPS_BRANCH="$(params.gitops-branch)" node scripts/ci/restore-artifact-catalog.mjs
HWLAB_CATALOG_PATH="$(params.catalog-path)" HWLAB_GIT_URL="$(params.git-url)" HWLAB_GIT_READ_URL="$(params.git-read-url)" HWLAB_GITOPS_BRANCH="$(params.gitops-branch)" HWLAB_SERVICES="$(params.services)" node scripts/ci/restore-artifact-catalog.mjs
node scripts/ci-plan.mjs --lane "$(params.lane)" --target-ref HEAD --deploy-config deploy/deploy.yaml --artifact-catalog "$(params.catalog-path)" --registry-prefix "$(params.registry-prefix)" --services "$(params.services)" --verify-reuse-registry > /workspace/source/ci-plan.json
node - <<'NODE'
const fs = require("node:fs");
@@ -46,13 +46,17 @@ fs.writeFileSync("/workspace/source/affected-services.json", JSON.stringify({
affectedServices: plan.affectedServices || [],
rolloutServices: plan.rolloutServices || plan.affectedServices || [],
buildServices: plan.buildServices || entries.filter((entry) => entry.buildRequired).map((entry) => entry.serviceId),
rolloutWithoutImageBuildServices: plan.rolloutWithoutImageBuildServices || [],
reusedServices: plan.reusedServices || [],
serviceReusedCount: plan.serviceReusedCount || 0,
imageBuildSkippedServices: plan.imageBuildSkippedServices || [],
buildSkippedCount: plan.buildSkippedCount || 0,
artifactCatalog: plan.artifactCatalog || null,
envArtifactGroups: plan.envArtifactGroups || [],
changedPathSummary: plan.changedPathSummary || null,
ciCdPlan: plan.ciCdPlan || null,
services: plan.services || [],
entries
}, null, 2) + String.fromCharCode(10));
console.log(JSON.stringify({ event: "g14-ci-plan", sourceCommitId: plan.sourceCommitId, affectedServices: plan.affectedServices || [], rolloutServices: plan.rolloutServices || plan.affectedServices || [], buildServices: plan.buildServices || [], reusedServices: plan.reusedServices || [], buildSkippedCount: plan.buildSkippedCount || 0, envArtifactGroups: plan.envArtifactGroups || [] }));
console.log(JSON.stringify({ event: "g14-ci-plan", sourceCommitId: plan.sourceCommitId, affectedServices: plan.affectedServices || [], rolloutServices: plan.rolloutServices || plan.affectedServices || [], buildServices: plan.buildServices || [], rolloutWithoutImageBuildServices: plan.rolloutWithoutImageBuildServices || [], reusedServices: plan.reusedServices || [], serviceReusedCount: plan.serviceReusedCount || 0, imageBuildSkippedServices: plan.imageBuildSkippedServices || [], buildSkippedCount: plan.buildSkippedCount || 0, artifactCatalog: plan.artifactCatalog || null, noImageBuildReason: plan.ciCdPlan?.noImageBuildReason || null, envArtifactGroups: plan.envArtifactGroups || [] }));
NODE