feat: add g14 cicd timing output
This commit is contained in:
@@ -22,6 +22,14 @@ G14 CI/CD 耗时必须按阶段测量,不能只看一个 PipelineRun 总时长
|
||||
- `multi-component-build`:多个但不是全部服务镜像重建。
|
||||
- `full-rebuild`:共享输入或 build-system 输入变化导致全部服务镜像重建。
|
||||
|
||||
## Timing 输出合同
|
||||
|
||||
G14 Tekton 日志中的分段耗时统一使用 JSON 行事件:`event="g14-cicd-timing"`、`schemaVersion="v1"`、`stage`、`status`、`durationMs`,并尽量携带 `pipelineRun`、`taskRun`、`task`、`revision` 和 `serviceId`。后续性能对比只消费这些稳定字段,不从自然语言日志里反推耗时。
|
||||
|
||||
当前稳定 stage 名称:`source-clone`、`catalog-fetch`、`npm-ci`、`buildkit-context-transfer`、`dependency-install`、`cache-import`、`cache-export`、`gitops-render`、`gitops-clone`、`gitops-commit`、`gitops-push`、`argo-refresh`、`workload-ready`。
|
||||
|
||||
`argo-refresh` 以 `hwlab-dev` workload 的 `hwlab.pikastech.local/source-commit` 元数据收敛作为观测点,不要求 Tekton 跨 namespace patch Argo Application。`workload-ready` 以 `hwlab-dev` 的 Deployment/StatefulSet observed generation、ready replicas 和 updated replicas 为准;权限不足或超时必须用 `status` 明确输出,不得静默吞掉。
|
||||
|
||||
## 当前基线
|
||||
|
||||
当前 G14 pipeline 已具备关键架构优化:一分钟 source polling、组件级 lazy build、per-service fan-out、G14 本地 registry、本地 base image、BuildKit-only publish、GitOps/Argo 分离。剩余耗时主要集中在固定开销和镜像构建机制上。
|
||||
|
||||
@@ -215,6 +215,109 @@ function emit(event, payload = {}) {
|
||||
process.stderr.write(`${JSON.stringify({ event, at: new Date().toISOString(), ...payload })}\n`);
|
||||
}
|
||||
|
||||
function ciTimingEnabled(env = process.env) {
|
||||
return env.HWLAB_G14_CICD_TIMING === "1" || env.HWLAB_CI_TIMING === "1";
|
||||
}
|
||||
|
||||
function emitCiTiming(payload) {
|
||||
emit("g14-cicd-timing", {
|
||||
schemaVersion: "v1",
|
||||
source: "scripts/artifact-publish.mjs",
|
||||
pipelineRun: process.env.HWLAB_TEKTON_PIPELINERUN ?? null,
|
||||
taskRun: process.env.HWLAB_TEKTON_TASKRUN ?? null,
|
||||
task: process.env.HWLAB_TEKTON_TASK ?? null,
|
||||
revision: process.env.HWLAB_SOURCE_REVISION ?? null,
|
||||
...payload
|
||||
});
|
||||
}
|
||||
|
||||
function parseDurationMs(value) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (!text) return null;
|
||||
let total = 0;
|
||||
let matched = false;
|
||||
for (const match of text.matchAll(/(\d+(?:\.\d+)?)(ms|s|m|h)/gu)) {
|
||||
matched = true;
|
||||
const amount = Number(match[1]);
|
||||
const unit = match[2];
|
||||
if (unit === "ms") total += amount;
|
||||
else if (unit === "s") total += amount * 1000;
|
||||
else if (unit === "m") total += amount * 60 * 1000;
|
||||
else if (unit === "h") total += amount * 60 * 60 * 1000;
|
||||
}
|
||||
return matched ? Math.round(total) : null;
|
||||
}
|
||||
|
||||
function parseBuildkitPlainTimings(text) {
|
||||
const idToStage = new Map();
|
||||
const timings = new Map();
|
||||
const stageMatchers = [
|
||||
{ stage: "buildkit-context-transfer", pattern: /load build context|transferring context/iu },
|
||||
{ stage: "cache-import", pattern: /importing cache manifest/iu },
|
||||
{ stage: "cache-export", pattern: /exporting cache/iu }
|
||||
];
|
||||
for (const rawLine of String(text ?? "").split("\n")) {
|
||||
const line = rawLine.replace(/\u001b\[[0-9;]*m/gu, "").trim();
|
||||
const match = line.match(/^#(\d+)\s+(.*)$/u);
|
||||
if (!match) continue;
|
||||
const [, id, body] = match;
|
||||
const identified = stageMatchers.find((item) => item.pattern.test(body));
|
||||
if (identified) idToStage.set(id, identified.stage);
|
||||
const stage = idToStage.get(id);
|
||||
if (!stage || timings.has(stage)) continue;
|
||||
const durationMatch = body.match(/(?:DONE|done)\s+([0-9.]+(?:ms|s|m|h)(?:[0-9.]+(?:ms|s|m|h))*)/iu) ??
|
||||
body.match(/([0-9.]+(?:ms|s|m|h)(?:[0-9.]+(?:ms|s|m|h))*)\s+done/iu);
|
||||
const durationMs = parseDurationMs(durationMatch?.[1]);
|
||||
if (durationMs !== null) timings.set(stage, { stage, status: "succeeded", durationMs, source: "buildkit-plain-progress" });
|
||||
}
|
||||
return timings;
|
||||
}
|
||||
|
||||
function parseDockerfileStepTimings(text) {
|
||||
const timings = [];
|
||||
for (const rawLine of String(text ?? "").split("\n")) {
|
||||
const line = rawLine.replace(/\u001b\[[0-9;]*m/gu, "");
|
||||
const start = line.indexOf("{");
|
||||
const end = line.lastIndexOf("}");
|
||||
if (start < 0 || end <= start) continue;
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(line.slice(start, end + 1));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (parsed?.event !== "g14-cicd-build-step-timing") continue;
|
||||
timings.push({
|
||||
stage: parsed.stage,
|
||||
status: parsed.status ?? "unknown",
|
||||
durationMs: Number.isFinite(parsed.durationMs) ? parsed.durationMs : null,
|
||||
source: parsed.source ?? "dockerfile-run"
|
||||
});
|
||||
}
|
||||
return timings;
|
||||
}
|
||||
|
||||
function emitBuildkitTimingEvents({ service, result, timingEnabled }) {
|
||||
if (!timingEnabled) return;
|
||||
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
||||
const buildkitTimings = parseBuildkitPlainTimings(output);
|
||||
for (const stage of ["buildkit-context-transfer", "cache-import", "cache-export"]) {
|
||||
const timing = buildkitTimings.get(stage) ?? { stage, status: "not-observed", durationMs: null, source: "buildkit-plain-progress" };
|
||||
emitCiTiming({
|
||||
...timing,
|
||||
serviceId: service.serviceId,
|
||||
buildkitDurationMs: result.durationMs
|
||||
});
|
||||
}
|
||||
for (const timing of parseDockerfileStepTimings(output)) {
|
||||
emitCiTiming({
|
||||
...timing,
|
||||
serviceId: service.serviceId,
|
||||
buildkitDurationMs: result.durationMs
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function blocker({ type, scope, summary, next }) {
|
||||
return {
|
||||
type,
|
||||
@@ -934,6 +1037,7 @@ if (runtimeKind === "node-command" && entrypoint) {
|
||||
}
|
||||
|
||||
function dockerfile(baseImage, port, labels = []) {
|
||||
const dependencyTimingScript = `started_at=$(node -e "console.log(Date.now())"); status=succeeded; if [ -d /opt/hwlab-node-runtime-base/node_modules ]; then cp -a /opt/hwlab-node-runtime-base/node_modules ./node_modules; fi && if [ -f package-lock.json ]; then npm install --omit=dev --include=optional --ignore-scripts; else npm install --omit=dev --include=optional --ignore-scripts; fi && codex_version="$(node -p "require('./node_modules/@openai/codex/package.json').version")" && case "$(uname -m)" in x86_64|amd64) test -e node_modules/@openai/codex-linux-x64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-x64@npm:@openai/codex@\${codex_version}-linux-x64" ;; aarch64|arm64) test -e node_modules/@openai/codex-linux-arm64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-arm64@npm:@openai/codex@\${codex_version}-linux-arm64" ;; esac || status=failed; finished_at=$(node -e "console.log(Date.now())"); node -e "console.log(JSON.stringify({event:'g14-cicd-build-step-timing',stage:'dependency-install',status:process.argv[1],durationMs:Number(process.argv[3])-Number(process.argv[2]),source:'dockerfile-run'}))" "$status" "$started_at" "$finished_at"; test "$status" = succeeded`;
|
||||
return [
|
||||
`FROM ${baseImage}`,
|
||||
"WORKDIR /app",
|
||||
@@ -950,7 +1054,7 @@ function dockerfile(baseImage, port, labels = []) {
|
||||
"ENV HWLAB_CODE_AGENT_SKILLS_DIRS=/app/skills",
|
||||
"COPY package.json ./package.json",
|
||||
"COPY package-lock.json* ./",
|
||||
"RUN if [ -d /opt/hwlab-node-runtime-base/node_modules ]; then cp -a /opt/hwlab-node-runtime-base/node_modules ./node_modules; fi && if [ -f package-lock.json ]; then npm install --omit=dev --include=optional --ignore-scripts; else npm install --omit=dev --include=optional --ignore-scripts; fi && codex_version=\"$(node -p \"require('./node_modules/@openai/codex/package.json').version\")\" && case \"$(uname -m)\" in x86_64|amd64) test -e node_modules/@openai/codex-linux-x64 || npm install --omit=dev --include=optional --ignore-scripts \"@openai/codex-linux-x64@npm:@openai/codex@${codex_version}-linux-x64\" ;; aarch64|arm64) test -e node_modules/@openai/codex-linux-arm64 || npm install --omit=dev --include=optional --ignore-scripts \"@openai/codex-linux-arm64@npm:@openai/codex@${codex_version}-linux-arm64\" ;; esac",
|
||||
`RUN ${dependencyTimingScript}`,
|
||||
"COPY internal ./internal",
|
||||
"COPY cmd ./cmd",
|
||||
"COPY scripts ./scripts",
|
||||
@@ -1243,7 +1347,7 @@ async function buildServiceWithBuildkit({ args, service, ref, tag, buildCreatedA
|
||||
"--export-cache", `type=registry,ref=${cacheRef},mode=max${registryOption}`,
|
||||
"--output", `type=image,name=${ref},push=true${registryOption}`
|
||||
];
|
||||
if (!args.quietBuild) argsList.push("--progress", "plain");
|
||||
if (!args.quietBuild || ciTimingEnabled()) argsList.push("--progress", "plain");
|
||||
for (const [name, value] of envs) argsList.push("--opt", `build-arg:${name}=${value}`);
|
||||
|
||||
const buildEnv = { ...process.env };
|
||||
@@ -1253,6 +1357,7 @@ async function buildServiceWithBuildkit({ args, service, ref, tag, buildCreatedA
|
||||
await mkdir(buildEnv.XDG_RUNTIME_DIR, { recursive: true });
|
||||
}
|
||||
const result = await run(args.buildkitCommand, argsList, { env: buildEnv });
|
||||
emitBuildkitTimingEvents({ service, result, timingEnabled: ciTimingEnabled(buildEnv) });
|
||||
if (result.code !== 0) {
|
||||
return {
|
||||
...service,
|
||||
|
||||
@@ -590,6 +590,23 @@ function shellSingleQuote(value) {
|
||||
return `'${String(value).replace(/'/g, `'"'"'`)}'`;
|
||||
}
|
||||
|
||||
function ciTimingShellFunction() {
|
||||
return `ci_now_ms() {
|
||||
node -e 'console.log(Date.now())'
|
||||
}
|
||||
|
||||
ci_timing_emit() {
|
||||
stage="$1"
|
||||
status="$2"
|
||||
started_ms="$3"
|
||||
finished_ms="$(ci_now_ms)"
|
||||
duration_ms="$((finished_ms - started_ms))"
|
||||
service_id="\${HWLAB_TIMING_SERVICE_ID:-}"
|
||||
node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:"g14-cicd-timing",schemaVersion:"v1",stage,status,durationMs:Number(durationMs),pipelineRun:process.env.HWLAB_TEKTON_PIPELINERUN||null,taskRun:process.env.HWLAB_TEKTON_TASKRUN||null,task:process.env.HWLAB_TEKTON_TASK||null,revision:process.env.HWLAB_SOURCE_REVISION||null,serviceId:serviceId||null,source:"scripts/g14-gitops-render.mjs",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' "$stage" "$status" "$duration_ms" "$service_id"
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function dependencyProxyProbeScript(phase, targetUrl) {
|
||||
return `HWLAB_PROXY_PROBE_PHASE=${shellSingleQuote(phase)} HWLAB_PROXY_PROBE_URL=${shellSingleQuote(targetUrl)} node <<'NODE' || echo '{"event":"dependency-proxy-probe-nonblocking","phase":"${phase}","ok":false,"reason":"probe-failed-but-continuing"}'
|
||||
const net = require("node:net");
|
||||
@@ -714,6 +731,11 @@ function prepareSourceScript() {
|
||||
const lines = [
|
||||
"#!/bin/sh",
|
||||
"set -eu",
|
||||
ciTimingShellFunction(),
|
||||
"export HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"",
|
||||
"export HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"",
|
||||
"export HWLAB_TEKTON_TASK=\"prepare-source\"",
|
||||
"export HWLAB_SOURCE_REVISION=\"$(params.revision)\"",
|
||||
dependencyProxyProbeScript("prepare-source-proxy-preflight", "http://deb.debian.org/debian/dists/bookworm/InRelease"),
|
||||
curlDownloadProbeFunction(),
|
||||
`echo '{"event":"ci-base-image","phase":"prepare-source","image":"${ciToolsRunnerImage}","policy":"no-runtime-apk"}'`,
|
||||
@@ -725,11 +747,14 @@ function prepareSourceScript() {
|
||||
"chmod 600 /root/.ssh/id_rsa",
|
||||
"ssh-keyscan github.com >> /root/.ssh/known_hosts 2>/dev/null",
|
||||
"rm -rf /workspace/source/repo",
|
||||
"source_clone_started_ms=\"$(ci_now_ms)\"",
|
||||
"git clone --branch \"$(params.source-branch)\" \"$(params.git-url)\" /workspace/source/repo",
|
||||
"cd /workspace/source/repo",
|
||||
"git config --global --add safe.directory /workspace/source/repo",
|
||||
"git checkout \"$(params.revision)\"",
|
||||
"test \"$(git rev-parse HEAD)\" = \"$(params.revision)\"",
|
||||
"ci_timing_emit source-clone succeeded \"$source_clone_started_ms\"",
|
||||
"catalog_fetch_started_ms=\"$(ci_now_ms)\"",
|
||||
"if git ls-remote --exit-code --heads origin \"$(params.gitops-branch)\" >/dev/null 2>&1; then",
|
||||
" git fetch --depth 1 origin \"$(params.gitops-branch)\" >/dev/null 2>&1 || true",
|
||||
" if git cat-file -e FETCH_HEAD:deploy/artifact-catalog.dev.json 2>/dev/null; then",
|
||||
@@ -746,7 +771,10 @@ function prepareSourceScript() {
|
||||
"else",
|
||||
" echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"seed\",\"reason\":\"gitops-branch-missing\"}'",
|
||||
"fi",
|
||||
"ci_timing_emit catalog-fetch succeeded \"$catalog_fetch_started_ms\"",
|
||||
"npm_ci_started_ms=\"$(ci_now_ms)\"",
|
||||
"npm ci --ignore-scripts",
|
||||
"ci_timing_emit npm-ci succeeded \"$npm_ci_started_ms\"",
|
||||
`echo ${shellSingleQuote(`G14 prepare-source complete; validation task count=${primitiveValidationTasks.length}`)}`
|
||||
];
|
||||
return `${lines.join("\n")}\n`;
|
||||
@@ -816,6 +844,12 @@ NODE
|
||||
function perServicePublishScript() {
|
||||
return `#!/bin/sh
|
||||
set -eu
|
||||
${ciTimingShellFunction()}
|
||||
export HWLAB_TEKTON_PIPELINERUN="$(context.pipelineRun.name)"
|
||||
export HWLAB_TEKTON_TASKRUN="$(context.taskRun.name)"
|
||||
export HWLAB_TEKTON_TASK="build-$(params.service-id)"
|
||||
export HWLAB_SOURCE_REVISION="$(params.revision)"
|
||||
export HWLAB_TIMING_SERVICE_ID="$(params.service-id)"
|
||||
${dependencyProxyProbeScript("service-image-publish-proxy-preflight", "http://deb.debian.org/debian/dists/bookworm/InRelease")}
|
||||
echo '{"event":"ci-base-image","phase":"service-image-publish","serviceId":"'"$(params.service-id)"'","image":"${ciToolsRunnerImage}","policy":"no-runtime-apk"}'
|
||||
for tool in node npm git python3 ssh curl; do command -v "$tool" >/dev/null 2>&1 || { echo '{"event":"ci-base-image","phase":"service-image-publish","ok":false,"reason":"missing-tool","tool":"'"$tool"'"}'; exit 31; }; done
|
||||
@@ -858,6 +892,11 @@ export HWLAB_CI_ARTIFACT_RUN_ID="$(context.pipelineRun.name)-$(params.service-id
|
||||
export HWLAB_CI_ARTIFACT_RUN_OWNER="tekton"
|
||||
export HWLAB_DEV_REGISTRY_PREFIX="$(params.registry-prefix)"
|
||||
export HWLAB_DEV_REGISTRY_K8S_NATIVE=1
|
||||
export HWLAB_G14_CICD_TIMING=1
|
||||
export HWLAB_TEKTON_PIPELINERUN
|
||||
export HWLAB_TEKTON_TASKRUN
|
||||
export HWLAB_TEKTON_TASK
|
||||
export HWLAB_SOURCE_REVISION
|
||||
export PATH="/workspace/buildkit-bin:$PATH"
|
||||
export HWLAB_BUILDKIT_ADDR="unix:///workspace/buildkit-run/buildkitd.sock"
|
||||
if [ -n "$(params.base-image)" ]; then export HWLAB_DEV_BASE_IMAGE="$(params.base-image)"; fi
|
||||
@@ -905,6 +944,11 @@ node scripts/refresh-artifact-catalog.mjs --target-ref HEAD --publish-report /wo
|
||||
function gitopsPromoteScript() {
|
||||
return `#!/bin/sh
|
||||
set -eu
|
||||
${ciTimingShellFunction()}
|
||||
export HWLAB_TEKTON_PIPELINERUN="$(context.pipelineRun.name)"
|
||||
export HWLAB_TEKTON_TASKRUN="$(context.taskRun.name)"
|
||||
export HWLAB_TEKTON_TASK="gitops-promote"
|
||||
export HWLAB_SOURCE_REVISION="$(params.revision)"
|
||||
${dependencyProxyProbeScript("gitops-promote-proxy-preflight", "http://deb.debian.org/debian/dists/bookworm/InRelease")}
|
||||
echo '{"event":"ci-base-image","phase":"gitops-promote","image":"${ciToolsRunnerImage}","policy":"no-runtime-apt"}'
|
||||
for tool in node npm git ssh curl; do command -v "$tool" >/dev/null 2>&1 || { echo '{"event":"ci-base-image","phase":"gitops-promote","ok":false,"reason":"missing-tool","tool":"'"$tool"'"}'; exit 31; }; done
|
||||
@@ -936,10 +980,13 @@ git config --global user.email "hwlab-g14-gitops-bot@users.noreply.github.com"
|
||||
if [ -s /workspace/source/dev-artifacts.json ]; then
|
||||
node scripts/refresh-artifact-catalog.mjs --target-ref "$(params.revision)" --publish-report /workspace/source/dev-artifacts.json --write
|
||||
fi
|
||||
gitops_render_started_ms="$(ci_now_ms)"
|
||||
node scripts/g14-gitops-render.mjs --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)" --use-deploy-images
|
||||
node scripts/g14-gitops-render.mjs --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)" --use-deploy-images --check
|
||||
ci_timing_emit gitops-render succeeded "$gitops_render_started_ms"
|
||||
check_source_head before-push
|
||||
workdir="$(mktemp -d)"
|
||||
gitops_clone_started_ms="$(ci_now_ms)"
|
||||
git clone --no-checkout "$(params.git-url)" "$workdir/gitops"
|
||||
cd "$workdir/gitops"
|
||||
if git ls-remote --exit-code --heads origin "$(params.gitops-branch)" >/dev/null 2>&1; then
|
||||
@@ -950,17 +997,24 @@ else
|
||||
git checkout --orphan "$(params.gitops-branch)"
|
||||
git rm -rf . >/dev/null 2>&1 || true
|
||||
fi
|
||||
ci_timing_emit gitops-clone succeeded "$gitops_clone_started_ms"
|
||||
mkdir -p deploy/gitops
|
||||
cp -a /workspace/source/repo/deploy/gitops/g14 deploy/gitops/g14
|
||||
cp /workspace/source/repo/deploy/artifact-catalog.dev.json deploy/artifact-catalog.dev.json
|
||||
git add deploy/artifact-catalog.dev.json deploy/gitops/g14
|
||||
if git diff --cached --quiet; then
|
||||
echo '{"status":"unchanged","gitopsBranch":"'"$(params.gitops-branch)"'","sourceRevision":"'"$(params.revision)"'"}'
|
||||
ci_timing_emit gitops-commit unchanged "$(ci_now_ms)"
|
||||
ci_timing_emit gitops-push unchanged "$(ci_now_ms)"
|
||||
exit 0
|
||||
fi
|
||||
short="$(printf '%.7s' "$(params.revision)")"
|
||||
gitops_commit_started_ms="$(ci_now_ms)"
|
||||
git commit -m "chore: promote G14 GitOps source $short"
|
||||
ci_timing_emit gitops-commit succeeded "$gitops_commit_started_ms"
|
||||
gitops_push_started_ms="$(ci_now_ms)"
|
||||
git push origin "HEAD:$(params.gitops-branch)"
|
||||
ci_timing_emit gitops-push succeeded "$gitops_push_started_ms"
|
||||
echo '{"status":"pushed","gitopsBranch":"'"$(params.gitops-branch)"'","sourceRevision":"'"$(params.revision)"'"}'
|
||||
`;
|
||||
}
|
||||
@@ -1453,6 +1507,180 @@ function collectArtifactsTask() {
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeReadyScript() {
|
||||
return `#!/bin/sh
|
||||
set -eu
|
||||
${ciTimingShellFunction()}
|
||||
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";
|
||||
const revision = process.env.HWLAB_SOURCE_REVISION || "";
|
||||
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 = 5000;
|
||||
|
||||
function emit(payload) {
|
||||
console.log(JSON.stringify({
|
||||
event: "g14-cicd-timing",
|
||||
schemaVersion: "v1",
|
||||
pipelineRun,
|
||||
taskRun,
|
||||
task,
|
||||
revision,
|
||||
source: "scripts/g14-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: "skipped", reason: "kubernetes-service-host-missing", durationMs: 0 });
|
||||
emit({ stage: "workload-ready", status: "skipped", reason: "kubernetes-service-host-missing", durationMs: 0 });
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
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", "hwlab-dev", "deployments")),
|
||||
request("GET", listPath("apps", "v1", "hwlab-dev", "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"] === "g14");
|
||||
return { status: "ok", items };
|
||||
}
|
||||
|
||||
function runtimeSourceCommit(item) {
|
||||
return item.metadata?.labels?.["hwlab.pikastech.local/source-commit"] || item.metadata?.annotations?.["hwlab.pikastech.local/source-commit"] || null;
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
async function waitForArgoRefresh() {
|
||||
const startedAt = Date.now();
|
||||
for (;;) {
|
||||
const runtime = await runtimeItems();
|
||||
if (runtime.status === "rbac-denied") {
|
||||
emit({ stage: "argo-refresh", status: "skipped", reason: "runtime-observer-rbac-denied", durationMs: Date.now() - startedAt });
|
||||
return { observed: false, skipped: true };
|
||||
}
|
||||
const pending = runtime.items
|
||||
.map((item) => ({ kind: item.kind, name: item.metadata?.name, sourceCommit: runtimeSourceCommit(item) }))
|
||||
.filter((item) => item.sourceCommit !== revision);
|
||||
if (runtime.items.length > 0 && pending.length === 0) {
|
||||
emit({ stage: "argo-refresh", status: "succeeded", durationMs: Date.now() - startedAt, workloadCount: runtime.items.length });
|
||||
return { observed: true, skipped: false };
|
||||
}
|
||||
if (Date.now() - startedAt >= timeoutMs) {
|
||||
emit({ stage: "argo-refresh", status: "timeout", durationMs: Date.now() - startedAt, workloadCount: runtime.items.length, pending: pending.slice(0, 8) });
|
||||
return { observed: false, skipped: false };
|
||||
}
|
||||
await sleep(pollMs);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForWorkloads() {
|
||||
const startedAt = Date.now();
|
||||
for (;;) {
|
||||
const runtime = await runtimeItems();
|
||||
if (runtime.status === "rbac-denied") {
|
||||
emit({ stage: "workload-ready", status: "skipped", reason: "runtime-observer-rbac-denied", durationMs: Date.now() - startedAt });
|
||||
return;
|
||||
}
|
||||
const blocked = runtime.items.map((item) => ({ kind: item.kind, name: item.metadata?.name, ...readyWorkload(item) })).filter((item) => !item.ready);
|
||||
if (runtime.items.length > 0 && blocked.length === 0) {
|
||||
emit({ stage: "workload-ready", status: "succeeded", durationMs: Date.now() - startedAt, workloadCount: runtime.items.length });
|
||||
return;
|
||||
}
|
||||
if (Date.now() - startedAt >= timeoutMs) {
|
||||
emit({ stage: "workload-ready", status: "timeout", durationMs: Date.now() - startedAt, workloadCount: runtime.items.length, blocked: blocked.slice(0, 8) });
|
||||
return;
|
||||
}
|
||||
await sleep(pollMs);
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await waitForArgoRefresh();
|
||||
await waitForWorkloads();
|
||||
})().catch((error) => {
|
||||
emit({ stage: "runtime-ready", status: "failed", reason: error.message, durationMs: 0 });
|
||||
});
|
||||
NODE
|
||||
`;
|
||||
}
|
||||
|
||||
function runtimeReadyTask() {
|
||||
return {
|
||||
name: "runtime-ready",
|
||||
runAfter: ["gitops-promote"],
|
||||
taskSpec: {
|
||||
params: [{ name: "revision" }],
|
||||
steps: [{ name: "runtime-ready", image: ciToolsRunnerImage, env: proxyEnv(), script: runtimeReadyScript() }]
|
||||
},
|
||||
params: [{ name: "revision", value: "$(params.revision)" }]
|
||||
};
|
||||
}
|
||||
|
||||
function imagePublishTaskSet() {
|
||||
return [
|
||||
planArtifactsTask(),
|
||||
@@ -1543,7 +1771,8 @@ function tektonPipeline() {
|
||||
{ name: "revision", value: "$(params.revision)" },
|
||||
{ name: "registry-prefix", value: "$(params.registry-prefix)" }
|
||||
]
|
||||
}
|
||||
},
|
||||
runtimeReadyTask()
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user