fix: restore cicd reuse catalog before planning

This commit is contained in:
root
2026-07-03 12:45:32 +00:00
parent b5bb1a86b4
commit 7e4ec0a225
4 changed files with 362 additions and 280 deletions
+21 -265
View File
@@ -2061,6 +2061,11 @@ echo '{"event":"ci-base-image","phase":"plan-artifacts","image":"${ciToolsRunner
for tool in node git; do command -v "$tool" >/dev/null 2>&1 || { echo '{"event":"ci-base-image","phase":"plan-artifacts","ok":false,"reason":"missing-tool","tool":"'"$tool"'"}'; exit 31; }; done
cd /workspace/source/repo
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
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");
@@ -3096,6 +3101,10 @@ function planArtifactsTask({ serviceIds = defaultServiceIds } = {}) {
workspaces: [{ name: "source", workspace: "source" }],
taskSpec: {
params: [
{ name: "git-url" },
{ name: "git-read-url" },
{ name: "gitops-branch" },
{ name: "lane" },
{ name: "revision" },
{ name: "catalog-path" },
{ name: "registry-prefix" },
@@ -3116,6 +3125,10 @@ function planArtifactsTask({ serviceIds = defaultServiceIds } = {}) {
}]
},
params: [
{ name: "git-url", value: "$(params.git-url)" },
{ name: "git-read-url", value: "$(params.git-read-url)" },
{ name: "gitops-branch", value: "$(params.gitops-branch)" },
{ name: "lane", value: "$(params.lane)" },
{ name: "revision", value: "$(params.revision)" },
{ name: "catalog-path", value: "$(params.catalog-path)" },
{ name: "registry-prefix", value: "$(params.registry-prefix)" },
@@ -3208,268 +3221,9 @@ export HWLAB_TEKTON_PIPELINERUN="$(context.pipelineRun.name)"
export HWLAB_TEKTON_TASKRUN="$(context.taskRun.name)"
export HWLAB_TEKTON_TASK="runtime-ready"
export HWLAB_SOURCE_REVISION="$(params.revision)"
node <<'NODE'
const fs = require("node:fs");
const https = require("node:https");
const host = process.env.KUBERNETES_SERVICE_HOST;
const port = process.env.KUBERNETES_SERVICE_PORT || "443";
function requiredEnv(name) {
const value = process.env[name];
if (!value) throw new Error(name + " is required");
return value;
}
const revision = requiredEnv("HWLAB_SOURCE_REVISION");
const runtimeNamespace = requiredEnv("HWLAB_RUNTIME_NAMESPACE");
const gitopsTarget = requiredEnv("HWLAB_GITOPS_TARGET");
const argoApplication = requiredEnv("HWLAB_ARGO_APPLICATION");
const pipelineRun = process.env.HWLAB_TEKTON_PIPELINERUN || null;
const taskRun = process.env.HWLAB_TEKTON_TASKRUN || null;
const task = process.env.HWLAB_TEKTON_TASK || null;
const timeoutMs = 240000;
const pollMs = 1000;
const progressEveryMs = 10000;
function emit(payload) {
console.log(JSON.stringify({
event: "node-cicd-timing",
schemaVersion: "v1",
pipelineRun,
taskRun,
task,
revision,
source: "scripts/gitops-render.mjs",
at: new Date().toISOString(),
...payload
}));
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function safeJson(text) {
try {
return JSON.parse(text);
} catch {
return null;
}
}
if (!host) {
emit({ stage: "argo-refresh", status: "failed", reason: "kubernetes-service-host-missing", durationMs: 0 });
process.exit(1);
}
const token = fs.readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8");
const ca = fs.readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt");
function request(method, path, body, contentType = "application/json") {
const payload = body ? JSON.stringify(body) : "";
const headers = { Authorization: "Bearer " + token };
if (payload) {
headers["Content-Type"] = contentType;
headers["Content-Length"] = Buffer.byteLength(payload);
}
return new Promise((resolve, reject) => {
const req = https.request({ host, port, method, path, ca, headers }, (res) => {
let data = "";
res.setEncoding("utf8");
res.on("data", (chunk) => { data += chunk; });
res.on("end", () => resolve({ statusCode: res.statusCode || 0, body: data, json: safeJson(data) }));
});
req.on("error", reject);
if (payload) req.write(payload);
req.end();
});
}
function listPath(group, version, namespace, resource) {
if (group) return "/apis/" + group + "/" + version + "/namespaces/" + encodeURIComponent(namespace) + "/" + resource;
return "/api/" + version + "/namespaces/" + encodeURIComponent(namespace) + "/" + resource;
}
async function runtimeItems() {
const [deployments, statefulsets] = await Promise.all([
request("GET", listPath("apps", "v1", runtimeNamespace, "deployments")),
request("GET", listPath("apps", "v1", runtimeNamespace, "statefulsets"))
]);
if (deployments.statusCode === 403 || statefulsets.statusCode === 403) return { status: "rbac-denied", items: [] };
const items = [...(deployments.json?.items || []), ...(statefulsets.json?.items || [])]
.filter((item) => item.metadata?.labels?.["hwlab.pikastech.local/gitops-target"] === gitopsTarget);
return { status: "ok", items };
}
function runtimeSourceCommit(item) {
return item.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/gitops-render-source-commit"] ||
item.spec?.template?.metadata?.annotations?.["hwlab.pikastech.local/gitops-render-source-commit"] ||
item.metadata?.labels?.["hwlab.pikastech.local/gitops-render-source-commit"] ||
item.metadata?.annotations?.["hwlab.pikastech.local/gitops-render-source-commit"] ||
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"] ||
item.metadata?.annotations?.["hwlab.pikastech.local/source-commit"] ||
null;
}
function serviceIdForRuntimeItem(item) {
return item.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
item.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
item.metadata?.labels?.["app.kubernetes.io/name"] ||
item.metadata?.name ||
null;
}
function readObservedServiceIds() {
try {
const plan = JSON.parse(fs.readFileSync("/workspace/source/affected-services.json", "utf8"));
const rolloutServices = Array.isArray(plan.rolloutServices) ? plan.rolloutServices.filter(Boolean) : [];
if (rolloutServices.length === 0) return null;
return new Set(rolloutServices);
} catch {
return null;
}
}
const observedServiceIds = readObservedServiceIds();
const readinessMode = observedServiceIds ? "service-rollout" : "infra-runtime-change";
function observedRuntimeItems(items) {
if (!observedServiceIds) return items;
return items.filter((item) => observedServiceIds.has(serviceIdForRuntimeItem(item)));
}
function readyWorkload(item) {
const specReplicas = item.spec?.replicas ?? 1;
if (specReplicas === 0) return { ready: true, skipped: true };
const status = item.status || {};
const desired = specReplicas;
const ready = status.readyReplicas ?? 0;
const updated = status.updatedReplicas ?? 0;
const observedGeneration = status.observedGeneration ?? 0;
const generation = item.metadata?.generation ?? 0;
return { ready: observedGeneration >= generation && ready >= desired && updated >= desired, desired, readyReplicas: ready, updatedReplicas: updated };
}
function maybeEmitProgress(state, stage, startedAt, payload) {
const now = Date.now();
if (now - state.lastProgressAt < progressEveryMs) return;
state.lastProgressAt = now;
emit({ stage, status: "progress", durationMs: now - startedAt, readinessMode, ...payload });
}
async function argoApplicationState() {
const path = "/apis/argoproj.io/v1alpha1/namespaces/argocd/applications/" + encodeURIComponent(argoApplication);
const response = await request("GET", path);
if (response.statusCode === 403) return { status: "rbac-denied", item: null, statusCode: response.statusCode };
if (response.statusCode < 200 || response.statusCode > 299) return { status: "error", item: null, statusCode: response.statusCode, body: response.body.slice(0, 500) };
return { status: "ok", item: response.json, statusCode: response.statusCode };
}
async function waitForArgoSyncedHealthy() {
const startedAt = Date.now();
const progressState = { lastProgressAt: 0 };
for (;;) {
const application = await argoApplicationState();
if (application.status === "rbac-denied") {
emit({ stage: "argo-sync-health", status: "failed", reason: "argocd-application-rbac-denied", durationMs: Date.now() - startedAt, readinessMode });
process.exitCode = 1;
return { observed: false };
}
if (application.status !== "ok") {
emit({ stage: "argo-sync-health", status: "failed", reason: "argocd-application-read-failed", statusCode: application.statusCode, body: application.body || null, durationMs: Date.now() - startedAt, readinessMode });
process.exitCode = 1;
return { observed: false };
}
const syncStatus = application.item?.status?.sync?.status || null;
const healthStatus = application.item?.status?.health?.status || null;
const operationPhase = application.item?.status?.operationState?.phase || null;
const operationMessage = application.item?.status?.operationState?.message || null;
if (syncStatus === "Synced" && healthStatus === "Healthy") {
emit({ stage: "argo-sync-health", status: "succeeded", durationMs: Date.now() - startedAt, readinessMode, syncStatus, healthStatus, operationPhase });
return { observed: true };
}
if (Date.now() - startedAt >= timeoutMs) {
emit({ stage: "argo-sync-health", status: "timeout", durationMs: Date.now() - startedAt, readinessMode, syncStatus, healthStatus, operationPhase, operationMessage });
process.exitCode = 1;
return { observed: false };
}
maybeEmitProgress(progressState, "argo-sync-health", startedAt, { syncStatus, healthStatus, operationPhase, operationMessage });
await sleep(pollMs);
}
}
async function waitForArgoRefresh() {
const startedAt = Date.now();
const progressState = { lastProgressAt: 0 };
for (;;) {
const runtime = await runtimeItems();
if (runtime.status === "rbac-denied") {
emit({ stage: "argo-refresh", status: "failed", reason: "runtime-observer-rbac-denied", durationMs: Date.now() - startedAt });
process.exitCode = 1;
return { observed: false, skipped: true };
}
const observed = observedRuntimeItems(runtime.items);
const pending = observed
.map((item) => ({ kind: item.kind, name: item.metadata?.name, sourceCommit: runtimeSourceCommit(item) }))
.filter((item) => item.sourceCommit !== revision);
if (observed.length > 0 && pending.length === 0) {
emit({ stage: "argo-refresh", status: "succeeded", durationMs: Date.now() - startedAt, workloadCount: runtime.items.length, observedCount: observed.length });
return { observed: true, skipped: false };
}
if (Date.now() - startedAt >= timeoutMs) {
emit({ stage: "argo-refresh", status: "timeout", reason: "source-commit-refresh-timeout", durationMs: Date.now() - startedAt, workloadCount: runtime.items.length, observedCount: observed.length, pending: pending.slice(0, 8) });
process.exitCode = 1;
return { observed: false, skipped: false };
}
maybeEmitProgress(progressState, "argo-refresh", startedAt, { workloadCount: runtime.items.length, observedCount: observed.length, pending: pending.slice(0, 8) });
await sleep(pollMs);
}
}
async function waitForWorkloads() {
const startedAt = Date.now();
const progressState = { lastProgressAt: 0 };
for (;;) {
const runtime = await runtimeItems();
if (runtime.status === "rbac-denied") {
emit({ stage: "workload-ready", status: "failed", reason: "runtime-observer-rbac-denied", durationMs: Date.now() - startedAt });
process.exitCode = 1;
return;
}
const observed = observedRuntimeItems(runtime.items);
const blocked = observed.map((item) => ({ kind: item.kind, name: item.metadata?.name, ...readyWorkload(item) })).filter((item) => !item.ready);
if (observed.length > 0 && blocked.length === 0) {
emit({ stage: "workload-ready", status: "succeeded", durationMs: Date.now() - startedAt, workloadCount: runtime.items.length, observedCount: observed.length });
return;
}
if (Date.now() - startedAt >= timeoutMs) {
emit({ stage: "workload-ready", status: "timeout", durationMs: Date.now() - startedAt, workloadCount: runtime.items.length, observedCount: observed.length, blocked: blocked.slice(0, 8) });
process.exitCode = 1;
return;
}
maybeEmitProgress(progressState, "workload-ready", startedAt, { workloadCount: runtime.items.length, observedCount: observed.length, blocked: blocked.slice(0, 8) });
await sleep(pollMs);
}
}
(async () => {
emit({ stage: "runtime-ready", status: "started", readinessMode, observedServiceCount: observedServiceIds ? observedServiceIds.size : 0 });
if (!observedServiceIds) {
const argo = await waitForArgoSyncedHealthy();
if (!argo.observed) return;
await waitForWorkloads();
return;
}
const refresh = await waitForArgoRefresh();
if (!refresh.observed) return;
await waitForWorkloads();
})().catch((error) => {
emit({ stage: "runtime-ready", status: "failed", reason: error.message, durationMs: 0 });
process.exitCode = 1;
});
NODE
export HWLAB_RUNTIME_READY_TIMEOUT_MS="$(params.timeout-ms)"
cd /workspace/source/repo
node scripts/ci/runtime-ready.mjs
`;
}
@@ -3479,7 +3233,7 @@ function runtimeReadyTask({ profile = "dev" } = {}) {
runAfter: ["gitops-promote"],
workspaces: [{ name: "source", workspace: "source" }],
taskSpec: {
params: [{ name: "revision" }, { name: "runtime-namespace" }, { name: "gitops-target" }, { name: "argo-application" }],
params: [{ name: "revision" }, { name: "runtime-namespace" }, { name: "gitops-target" }, { name: "argo-application" }, { name: "timeout-ms" }],
workspaces: [{ name: "source" }],
steps: [{
name: "runtime-ready",
@@ -3497,7 +3251,8 @@ function runtimeReadyTask({ profile = "dev" } = {}) {
{ name: "revision", value: "$(params.revision)" },
{ name: "runtime-namespace", value: namespaceNameForProfile(profile) },
{ name: "gitops-target", value: gitopsTargetForProfile(profile) },
{ name: "argo-application", value: argoApplicationName(profile) }
{ name: "argo-application", value: argoApplicationName(profile) },
{ name: "timeout-ms", value: "$(params.runtime-ready-timeout-ms)" }
]
};
}
@@ -3548,7 +3303,8 @@ function tektonPipeline(args = { lane: "node" }, deploy = null) {
{ name: "registry-prefix", type: "string", default: defaultRegistryPrefix },
{ name: "services", type: "string", default: servicesParamForLane(args.lane, deploy) },
{ name: "base-image", type: "string", default: defaultDevBaseImage },
{ name: "build-cache-mode", type: "string", default: "registry" }
{ name: "build-cache-mode", type: "string", default: "registry" },
{ name: "runtime-ready-timeout-ms", type: "string", default: "60000" }
],
workspaces: [
{ name: "source" },