Merge pull request #590 from pikasTech/fix/v02-env-reuse-code-only-fastlane

fix: enable v02 env reuse code-only rollout
This commit is contained in:
Lyon
2026-05-30 01:17:03 +08:00
committed by GitHub
5 changed files with 71 additions and 35 deletions
+16 -7
View File
@@ -1994,6 +1994,7 @@ function artifactRecordForCatalogReuse({ service, catalogService, commitId }) {
imageTag,
buildCreatedAt: catalogService?.buildCreatedAt ?? null,
buildSource: catalogService?.buildSource ?? null,
buildBackend: "reused-catalog",
digest,
repositoryDigest: repositoryDigestFor(image, digest),
reusedFrom: null,
@@ -2040,6 +2041,7 @@ function artifactRecordForEnvReuseCatalogReuse({ service, catalogService, commit
bootSh: service.bootSh ?? catalogService?.bootSh ?? `deploy/runtime/boot/${service.serviceId}.sh`,
buildCreatedAt: catalogService?.buildCreatedAt ?? null,
buildSource: catalogService?.buildSource ?? null,
buildBackend: "reused-env-catalog",
repositoryDigest: catalogService?.repositoryDigest ?? repositoryDigestFor(environmentImage, environmentDigest),
bootEnvEvidence: bootEnvEvidence({ service, commitId, environmentImage, environmentDigest })
};
@@ -2092,11 +2094,15 @@ async function readExternalServiceArtifacts({ dir, services, commitId }) {
const artifacts = new Map();
const blockers = [];
for (const service of services) {
const buildRequired = service.buildRequired === true || service.ciBuildRequired === true;
const reportPath = externalServiceReportPath(dir, service.serviceId);
let report;
try {
report = JSON.parse(await readFile(reportPath, "utf8"));
} catch (error) {
if (!buildRequired) {
continue;
}
blockers.push(blocker({
type: "environment_blocker",
scope: service.serviceId,
@@ -2119,11 +2125,11 @@ async function readExternalServiceArtifacts({ dir, services, commitId }) {
const artifact = artifactRecordFromExternalReport({ service, record, reportPath });
artifacts.set(service.serviceId, artifact);
if (artifact.status !== "published" || !shaDigestPattern.test(artifact.digest ?? "")) {
if (!publishReadyStatuses.has(artifact.status) || !shaDigestPattern.test(artifact.digest ?? "")) {
blockers.push(blocker({
type: "environment_blocker",
scope: service.serviceId,
summary: `per-service artifact report ${reportPath} is not a verified published image for ${service.serviceId}`,
summary: `per-service artifact report ${reportPath} is not a verified published/reused image for ${service.serviceId}`,
next: "Inspect the corresponding service TaskRun logs and rerun the failed service build."
}));
}
@@ -2280,6 +2286,7 @@ function plannerFieldsFor(servicePlan) {
bootRepo: servicePlan.bootRepo ?? null,
bootCommit: servicePlan.bootCommit ?? null,
bootSh: servicePlan.bootSh ?? null,
buildRequired: servicePlan.buildRequired ?? servicePlan.affected ?? false,
environmentImage: servicePlan.environmentImage ?? null,
environmentDigest: servicePlan.environmentDigest ?? null,
dockerfileHash: servicePlan.dockerfileHash ?? null,
@@ -2312,6 +2319,8 @@ function ciPlanReportSummary(ciPlan) {
changedPathSummary: ciPlan.changedPathSummary,
imageBuildRequired: ciPlan.imageBuildRequired,
affectedServices: ciPlan.affectedServices,
rolloutServices: ciPlan.rolloutServices ?? ciPlan.affectedServices,
buildServices: ciPlan.buildServices ?? ciPlan.affectedServices,
reusedServices: ciPlan.reusedServices,
buildSkippedCount: ciPlan.buildSkippedCount,
ciCdPlan: ciPlan.ciCdPlan
@@ -2569,6 +2578,7 @@ async function main() {
const producer = g14ArtifactProducer(process.env);
const ciPlan = await createG14CiPlan({
repoRoot,
lane: args.lane,
targetRef: "HEAD",
deployJsonPath: args.deployPath,
artifactCatalogPath: effectiveCatalogPath,
@@ -2579,11 +2589,10 @@ async function main() {
const services = enrichServicesWithCiPlan(await resolveServices(args.services, catalog), ciPlan);
const catalogByServiceId = new Map((catalog?.services ?? []).map((service) => [service.serviceId, service]));
const affectedServiceIds = new Set(ciPlan?.affectedServices ?? []);
const buildServiceIds = new Set(
services
.filter((service) => service.artifactRequired && (service.envReuse ? service.envChanged === true : affectedServiceIds.has(service.serviceId)))
.map((service) => service.serviceId)
);
const plannedBuildServices = Array.isArray(ciPlan?.buildServices) ? ciPlan.buildServices : null;
const buildServiceIds = new Set(plannedBuildServices ?? services
.filter((service) => service.artifactRequired && (service.envReuse ? service.envChanged === true : affectedServiceIds.has(service.serviceId)))
.map((service) => service.serviceId));
const preflightBuildServiceIds = args.externalServiceReportDir || args.externalServiceResultsEnv ? new Set() : buildServiceIds;
let blockers = await preflight({ args, services, catalog, deployManifest, baseImagePreflight, registryCapabilities, buildServiceIds: preflightBuildServiceIds });
+9 -1
View File
@@ -90,11 +90,12 @@ test("global change classifier distinguishes gitops-only and test-only", () => {
test("v02 planner marks device-pod code-only change as env reuse rollout", async () => {
const repo = await createFixtureRepo({ includeDevicePod: true });
const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
const initialPlan = await createG14CiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD",
targetRef: "HEAD",
targetRef: initialSha,
artifactCatalogPath: "deploy/artifact-catalog.v02.json",
services: ["hwlab-device-pod"]
});
@@ -123,8 +124,15 @@ test("v02 planner marks device-pod code-only change as env reuse rollout", async
assert.equal(devicePod.runtimeMode, "env-reuse-git-mirror-checkout");
assert.equal(devicePod.envChanged, false);
assert.equal(devicePod.codeChanged, true);
assert.equal(devicePod.buildRequired, false);
assert.equal(devicePod.bootSh, "deploy/runtime/boot/hwlab-device-pod.sh");
assert.deepEqual(plan.affectedServices, ["hwlab-device-pod"]);
assert.deepEqual(plan.rolloutServices, ["hwlab-device-pod"]);
assert.deepEqual(plan.buildServices, []);
assert.equal(plan.imageBuildRequired, false);
assert.deepEqual(plan.ciCdPlan.willBuild, []);
assert.deepEqual(plan.ciCdPlan.willRollout, ["hwlab-device-pod"]);
assert.equal(plan.ciCdPlan.noImageBuildReason, "env-reuse-code-only-rollout");
});
async function createFixtureRepo(options = {}) {
+18 -15
View File
@@ -235,7 +235,6 @@ function parseArgs(argv) {
if (args.lane === "v02") {
assert.equal(args.sourceBranch, "v0.2", "v02 source branch must be v0.2");
assert.equal(args.gitopsBranch, "v0.2-gitops", "v02 GitOps branch must be v0.2-gitops");
assert.equal(args.catalogPath, "deploy/artifact-catalog.v02.json", "v02 catalog path must be deploy/artifact-catalog.v02.json");
}
return args;
}
@@ -761,7 +760,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, registryPrefix
if (!podTemplate?.spec?.containers) continue;
const templateServiceId = serviceIdForWorkload(item, podTemplate.spec.containers[0]);
const templateArtifact = runtimeArtifactForService({ catalog, serviceId: templateServiceId, source, registryPrefix, useDeployImages, digestPin });
const templateSourceCommit = source.full;
const templateSourceCommit = profile === "v02" ? (templateArtifact.commit ?? source.full) : source.full;
const templateArtifactCommit = templateArtifact.commit;
label(podTemplate.metadata ??= {}, {
...stableRuntimeLabels,
@@ -807,7 +806,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, registryPrefix
}
upsertEnv(container.env, "HWLAB_GITOPS_TARGET", gitopsTarget);
upsertEnv(container.env, "HWLAB_GITOPS_PROFILE", profileLabel);
upsertEnv(container.env, "HWLAB_GITOPS_SOURCE_COMMIT", source.full);
upsertEnv(container.env, "HWLAB_GITOPS_SOURCE_COMMIT", profile === "v02" ? runtimeCommit : source.full);
if (bootMetadata) {
upsertEnv(container.env, "HWLAB_RUNTIME_MODE", bootMetadata.runtimeMode);
upsertEnv(container.env, "HWLAB_BOOT_REPO", bootMetadata.bootRepo);
@@ -1239,6 +1238,7 @@ const plan = JSON.parse(fs.readFileSync("/workspace/source/ci-plan.json", "utf8"
const selected = String(process.env.HWLAB_SELECTED_SERVICES || "").split(",").map((item) => item.trim()).filter(Boolean);
const allServices = selected.length > 0 ? selected : ${JSON.stringify(defaultServiceIds)};
const affected = new Set(plan.affectedServices || []);
const plannedBuildServices = Array.isArray(plan.buildServices) ? new Set(plan.buildServices) : null;
const selectedSet = new Set(selected);
const byService = new Map((plan.services || []).map((service) => [service.serviceId, service]));
const entries = allServices.map((serviceId) => {
@@ -1246,11 +1246,12 @@ const entries = allServices.map((serviceId) => {
const serviceSelected = selectedSet.has(serviceId);
const rolloutAffected = serviceSelected && affected.has(serviceId);
const envReuse = service.runtimeMode === "env-reuse-git-mirror-checkout" || service.envReuse === true;
const buildRequired = envReuse ? service.envChanged === true : rolloutAffected;
const buildRequired = serviceSelected && (plannedBuildServices ? plannedBuildServices.has(serviceId) : (envReuse ? service.envChanged === true : rolloutAffected));
return {
serviceId,
selected: serviceSelected,
affected: buildRequired,
buildRequired,
rolloutAffected,
runtimeMode: service.runtimeMode || "service-image",
envChanged: service.envChanged ?? null,
@@ -1263,12 +1264,14 @@ for (const entry of entries) {
fs.writeFileSync("/workspace/source/affected-services.json", JSON.stringify({
sourceCommitId: plan.sourceCommitId,
affectedServices: plan.affectedServices || [],
rolloutServices: plan.rolloutServices || plan.affectedServices || [],
buildServices: plan.buildServices || entries.filter((entry) => entry.buildRequired).map((entry) => entry.serviceId),
reusedServices: plan.reusedServices || [],
buildSkippedCount: plan.buildSkippedCount || 0,
services: plan.services || [],
entries
}, null, 2) + String.fromCharCode(10));
console.log(JSON.stringify({ event: "g14-ci-plan", sourceCommitId: plan.sourceCommitId, affectedServices: plan.affectedServices || [], reusedServices: plan.reusedServices || [], buildSkippedCount: plan.buildSkippedCount || 0 }));
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 }));
NODE
`;
}
@@ -1292,6 +1295,7 @@ export HWLAB_ARTIFACT_LANE="$(params.lane)"
export HWLAB_ARTIFACT_CATALOG_PATH="$(params.catalog-path)"
export HWLAB_DEPLOY_MANIFEST_PATH="deploy/deploy.json"
export HWLAB_ARTIFACT_IMAGE_TAG_MODE="$(params.image-tag-mode)"
mkdir -p /workspace/source/service-results
if [ -s /workspace/source/affected-services.json ] && ! node -e 'const fs=require("node:fs"); const p=JSON.parse(fs.readFileSync("/workspace/source/affected-services.json","utf8")); const service=process.argv[1]; const item=(p.entries||[]).find((entry)=>entry.serviceId===service); process.exit(item && item.affected ? 0 : 1);' "$(params.service-id)"; then
node - "$(params.service-id)" <<'NODE'
const fs = require("node:fs");
@@ -1365,7 +1369,7 @@ if [ "$buildkit_ready" != "1" ]; then
echo '{"event":"buildkit-sidecar-not-ready","serviceId":"'"$(params.service-id)"'","addr":"'"$HWLAB_BUILDKIT_ADDR"'"}' >&2
exit 1
fi
node scripts/g14-artifact-publish.mjs --publish --lane "$(params.lane)" --catalog-path "$(params.catalog-path)" --deploy-json deploy/deploy.json --image-tag-mode "$(params.image-tag-mode)" --registry-prefix "$(params.registry-prefix)" --no-report --tekton-results-dir /tekton/results --quiet-build --concurrency 1 --services "$(params.service-id)" --buildkit-command /workspace/buildkit-bin/buildctl --buildkit-addr "$HWLAB_BUILDKIT_ADDR"
node scripts/g14-artifact-publish.mjs --publish --lane "$(params.lane)" --catalog-path "$(params.catalog-path)" --deploy-json deploy/deploy.json --image-tag-mode "$(params.image-tag-mode)" --registry-prefix "$(params.registry-prefix)" --report "/workspace/source/service-results/$(params.service-id).json" --tekton-results-dir /tekton/results --quiet-build --concurrency 1 --services "$(params.service-id)" --buildkit-command /workspace/buildkit-bin/buildctl --buildkit-addr "$HWLAB_BUILDKIT_ADDR"
`;
}
@@ -1386,7 +1390,7 @@ export HWLAB_ARTIFACT_LANE="$(params.lane)"
export HWLAB_ARTIFACT_CATALOG_PATH="$(params.catalog-path)"
export HWLAB_DEPLOY_MANIFEST_PATH="deploy/deploy.json"
export HWLAB_ARTIFACT_IMAGE_TAG_MODE="$(params.image-tag-mode)"
node scripts/g14-artifact-publish.mjs --publish --lane "$(params.lane)" --catalog-path "$(params.catalog-path)" --deploy-json deploy/deploy.json --image-tag-mode "$(params.image-tag-mode)" --registry-prefix "$(params.registry-prefix)" --report /workspace/source/dev-artifacts.json --quiet-build --concurrency 1 --services "$(params.services)" --external-service-results-env
node scripts/g14-artifact-publish.mjs --publish --lane "$(params.lane)" --catalog-path "$(params.catalog-path)" --deploy-json deploy/deploy.json --image-tag-mode "$(params.image-tag-mode)" --registry-prefix "$(params.registry-prefix)" --report /workspace/source/dev-artifacts.json --quiet-build --concurrency 1 --services "$(params.services)" --external-service-report-dir /workspace/source/service-results
node scripts/refresh-artifact-catalog.mjs --lane "$(params.lane)" --catalog-path "$(params.catalog-path)" --deploy-json deploy/deploy.json --image-tag-mode "$(params.image-tag-mode)" --target-ref HEAD --publish-report /workspace/source/dev-artifacts.json --no-write
`;
}
@@ -2140,6 +2144,7 @@ function perServiceBuildTask(serviceId, { runAfter = ["plan-artifacts"] } = {})
name: buildTaskName(serviceId),
runAfter,
workspaces: [{ name: "source", workspace: "source" }],
when: [{ input: `$(tasks.plan-artifacts.results.${affectedResultName(serviceId)})`, operator: "in", values: ["true"] }],
taskSpec: {
params: [
{ name: "revision" },
@@ -2191,11 +2196,10 @@ function collectArtifactsTask({ serviceIds = defaultServiceIds } = {}) {
{ name: "image-tag-mode" },
{ name: "registry-prefix" },
{ name: "services" },
{ name: "base-image" },
...serviceResultParams(serviceIds)
{ name: "base-image" }
],
workspaces: [{ name: "source" }],
steps: [{ name: "collect", image: ciToolsRunnerImage, env: [...proxyEnv(), ...serviceResultEnv(serviceIds)], script: collectArtifactsScript() }]
steps: [{ name: "collect", image: ciToolsRunnerImage, env: proxyEnv(), script: collectArtifactsScript() }]
},
params: [
{ name: "revision", value: "$(params.revision)" },
@@ -2204,8 +2208,7 @@ function collectArtifactsTask({ serviceIds = defaultServiceIds } = {}) {
{ name: "image-tag-mode", value: "$(params.image-tag-mode)" },
{ name: "registry-prefix", value: "$(params.registry-prefix)" },
{ name: "services", value: "$(params.services)" },
{ name: "base-image", value: "$(params.base-image)" },
...serviceResultParamValues(serviceIds)
{ name: "base-image", value: "$(params.base-image)" }
]
};
}
@@ -2310,10 +2313,10 @@ async function runtimeItems() {
}
function runtimeSourceCommit(item) {
return item.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/source-commit"] ||
item.spec?.template?.metadata?.annotations?.["hwlab.pikastech.local/source-commit"] ||
item.metadata?.labels?.["hwlab.pikastech.local/source-commit"] ||
return item.metadata?.labels?.["hwlab.pikastech.local/source-commit"] ||
item.metadata?.annotations?.["hwlab.pikastech.local/source-commit"] ||
item.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/source-commit"] ||
item.spec?.template?.metadata?.annotations?.["hwlab.pikastech.local/source-commit"] ||
null;
}
+13 -6
View File
@@ -46,7 +46,7 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
assert.match(pipeline, /node scripts\/run-bun\.mjs test cmd\/hwlab-codex-api-responses-forwarder\/main\.test\.ts/u);
assert.doesNotMatch(pipeline, /cmd\/hwlab-codex-api-responses-forwarder\/main\.mjs/u);
assert.match(pipeline, /process\.exitCode = 1/u);
assert.match(pipeline, /item\.spec\?\.template\?\.metadata\?\.labels\?\.\[\\"hwlab\.pikastech\.local\/source-commit\\"\]/u);
assert.match(pipeline, /item\.metadata\?\.labels\?\.\[\\"hwlab\.pikastech\.local\/source-commit\\"\]/u);
const runtimeReady = taskByName(pipelineJson, "runtime-ready");
const runtimeReadyScript = runtimeReady.taskSpec.steps[0].script;
@@ -113,8 +113,16 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
const task = taskByName(pipelineJson, `build-${serviceId}`);
const expectedRunAfter = index === 0 ? ["plan-artifacts"] : [`build-${retainedServiceIds[index - 1]}`];
assert.deepEqual(task.runAfter, expectedRunAfter);
assert.deepEqual(task.when, [{ input: `$(tasks.plan-artifacts.results.affected-${serviceId})`, operator: "in", values: ["true"] }]);
}
const collectArtifacts = taskByName(pipelineJson, "collect-artifacts");
const collectScript = collectArtifacts.taskSpec.steps[0].script;
assert.match(collectScript, /--external-service-report-dir \/workspace\/source\/service-results/u);
assert.doesNotMatch(JSON.stringify(collectArtifacts), /tasks\.build-hwlab-device-pod\.results/u);
const devicePodBuildScript = taskByName(pipelineJson, "build-hwlab-device-pod").taskSpec.steps.find((step) => step.name === "publish")?.script ?? "";
assert.match(devicePodBuildScript, /--report "\/workspace\/source\/service-results\/\$\(params\.service-id\)\.json"/u);
const workloads = await readFile(path.join(outDir, "runtime-v02", "workloads.yaml"), "utf8");
const workloadsJson = JSON.parse(workloads);
const services = await readFile(path.join(outDir, "runtime-v02", "services.yaml"), "utf8");
@@ -140,10 +148,8 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
assert.equal(agentWorkerEnv.some((entry) => entry.name === "HWLAB_BOOT_COMMIT"), false, "agent-worker must not be switched to env reuse by device-pod work");
const workloadTemplates = workloadsJson.items.filter((item) => item.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/source-commit"]);
assert.ok(workloadTemplates.length > 0, "expected rendered pod-template source labels");
for (const item of workloadTemplates) {
assert.equal(item.spec.template.metadata.labels["hwlab.pikastech.local/source-commit"], sourceRevision);
assert.equal(item.spec.template.metadata.annotations["hwlab.pikastech.local/source-commit"], sourceRevision);
}
assert.equal(devicePod.spec.template.metadata.labels["hwlab.pikastech.local/source-commit"], sourceRevision);
assert.equal(devicePod.spec.template.metadata.annotations["hwlab.pikastech.local/source-commit"], sourceRevision);
assert.ok(workloadTemplates.some((item) => item.spec.template.metadata.labels["hwlab.pikastech.local/artifact-source-commit"]), "expected artifact source labels on pod templates");
assert.doesNotMatch(workloads, /HWLAB_GATEWAY_SIMU_URL/u);
assert.doesNotMatch(workloads, /HWLAB_BOX_SIMU_URL/u);
@@ -152,7 +158,8 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
assert.doesNotMatch(workloads, /HWLAB_TUNNEL_CLIENT_URL/u);
assert.doesNotMatch(pipeline, /hwlab-gateway-simu-status/u);
assert.match(pipeline, /hwlab-cloud-api-status/u);
assert.doesNotMatch(pipeline, /tasks\.build-hwlab-cloud-api\.results/u);
assert.match(pipeline, /--external-service-report-dir \/workspace\/source\/service-results/u);
const application = await readFile(path.join(outDir, "argocd", "application-v02.yaml"), "utf8");
assert.match(application, /"prune": true/u);
+15 -6
View File
@@ -145,11 +145,12 @@ export async function createG14CiPlan(options = {}) {
const environmentDigest = envReuse ? environmentDigestFromCatalog(catalogRecord) : null;
const environmentReady = envReuse && /^sha256:[a-f0-9]{64}$/u.test(environmentDigest ?? "") && Boolean(environmentImage);
const envChanged = envReuse ? relevantEnvMatches.length > 0 || !environmentReady || catalogRecord?.environmentInputHash !== environmentInputHash : null;
const codeChanged = envReuse ? relevantCodeMatches.length > 0 || catalogRecord?.codeInputHash !== codeInputHash || catalogRecord?.bootCommit !== sourceCommitId : null;
const codeChanged = envReuse ? relevantCodeMatches.length > 0 || catalogRecord?.codeInputHash !== codeInputHash : null;
const catalogReuse = envReuse ? envReuseCandidate(catalogRecord, envChanged) : reuseCandidate(catalogRecord, imageRelevantChangedPaths.length > 0);
const affected = envReuse
? Boolean(envChanged || codeChanged)
: imageRelevantChangedPaths.length > 0 || catalogReuse.status === "candidate-no-catalog" || catalogReuse.status === "candidate-unverified-digest";
const buildRequired = envReuse ? envChanged === true : affected;
const componentInputPaths = uniqueSorted([
...model.componentPaths,
...model.sharedPaths,
@@ -183,6 +184,7 @@ export async function createG14CiPlan(options = {}) {
buildArgsHash,
changedPaths: imageRelevantChangedPaths,
affected,
buildRequired,
runtimeMode: envReuse ? V02_ENV_REUSE_RUNTIME_MODE : "service-image",
envReuse,
envChanged,
@@ -204,6 +206,7 @@ export async function createG14CiPlan(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.affected).map((service) => service.serviceId);
return {
planVersion: G14_CI_PLAN_VERSION,
@@ -222,6 +225,7 @@ export async function createG14CiPlan(options = {}) {
serviceIdSource: serviceIdResolution.source,
v02EnvReuseServiceIds: [...envReuseServices],
defaultComponentLazyBuild: true,
envReuseCodeOnlyFastLane: true,
fullBuildFallback: false
},
inputs: {
@@ -231,16 +235,21 @@ export async function createG14CiPlan(options = {}) {
},
changedPaths: normalizedChangedPaths,
changedPathSummary: globalChange,
imageBuildRequired: affectedServices.length > 0,
imageBuildRequired: buildServices.length > 0,
affectedServices,
rolloutServices: affectedServices,
buildServices,
reusedServices,
buildSkippedCount: reusedServices.length,
buildSkippedCount: services.length - buildServices.length,
services,
ciCdPlan: {
willBuild: affectedServices,
willBuild: buildServices,
willRollout: affectedServices,
willReuse: reusedServices,
willRunGitopsPromote: globalChange.gitopsOnly || affectedServices.length > 0,
noImageBuildReason: affectedServices.length === 0 ? globalChange.summary : null
noImageBuildReason: buildServices.length === 0
? (affectedServices.length > 0 ? "env-reuse-code-only-rollout" : globalChange.summary)
: null
}
};
}
@@ -354,7 +363,7 @@ export async function hashGitPaths(repoRoot, targetRef, paths, options = {}) {
return !options.skipPath?.(filePath);
})
.sort();
return stableHash({ targetRef, entries: lines });
return stableHash({ entries: lines });
}
export async function lastCommitForPaths(repoRoot, targetRef, paths) {