diff --git a/ci/pipelines/hwlab-jd01-v03-ci-image-publish.yaml b/ci/pipelines/hwlab-jd01-v03-ci-image-publish.yaml index 6f50feec..f141d69f 100644 --- a/ci/pipelines/hwlab-jd01-v03-ci-image-publish.yaml +++ b/ci/pipelines/hwlab-jd01-v03-ci-image-publish.yaml @@ -718,7 +718,7 @@ "value": "hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local" } ], - "script": "#!/bin/sh\nset -eu\necho '{\"event\":\"ci-base-image\",\"phase\":\"plan-artifacts\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\ncd /workspace/source/repo\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps/yaml\" ] && [ ! -e node_modules/yaml ]; then\n mkdir -p node_modules\n ln -s \"$ci_node_deps/yaml\" node_modules/yaml\nfi\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nHWLAB_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\nnode 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\nnode - <<'NODE'\nconst fs = require(\"node:fs\");\nconst plan = JSON.parse(fs.readFileSync(\"/workspace/source/ci-plan.json\", \"utf8\"));\nconst selected = String(process.env.HWLAB_SELECTED_SERVICES || \"\").split(\",\").map((item) => item.trim()).filter(Boolean);\nconst allServices = selected.length > 0 ? selected : [\"hwlab-cloud-api\",\"hwlab-cloud-web\",\"hwlab-gateway\",\"hwlab-edge-proxy\",\"hwlab-agent-skills\"];\nconst affected = new Set(plan.affectedServices || []);\nconst plannedBuildServices = Array.isArray(plan.buildServices) ? new Set(plan.buildServices) : null;\nconst selectedSet = new Set(selected);\nconst byService = new Map((plan.services || []).map((service) => [service.serviceId, service]));\nconst entries = allServices.map((serviceId) => {\n const service = byService.get(serviceId) || {};\n const serviceSelected = selectedSet.has(serviceId);\n const rolloutAffected = serviceSelected && affected.has(serviceId);\n const envReuse = service.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.envReuse === true;\n const buildRequired = serviceSelected && (plannedBuildServices ? plannedBuildServices.has(serviceId) : (envReuse ? service.envChanged === true : rolloutAffected));\n return {\n serviceId,\n selected: serviceSelected,\n affected: rolloutAffected,\n buildRequired,\n rolloutAffected,\n runtimeMode: service.runtimeMode || \"service-image\",\n envChanged: service.envChanged ?? null,\n codeChanged: service.codeChanged ?? null\n };\n});\nfor (const entry of entries) {\n fs.writeFileSync(\"/tekton/results/affected-\" + entry.serviceId, entry.affected ? \"true\" : \"false\");\n fs.writeFileSync(\"/tekton/results/build-\" + entry.serviceId, entry.buildRequired ? \"true\" : \"false\");\n}\nfs.writeFileSync(\"/workspace/source/affected-services.json\", JSON.stringify({\n sourceCommitId: plan.sourceCommitId,\n affectedServices: plan.affectedServices || [],\n rolloutServices: plan.rolloutServices || plan.affectedServices || [],\n buildServices: plan.buildServices || entries.filter((entry) => entry.buildRequired).map((entry) => entry.serviceId),\n reusedServices: plan.reusedServices || [],\n buildSkippedCount: plan.buildSkippedCount || 0,\n envArtifactGroups: plan.envArtifactGroups || [],\n changedPathSummary: plan.changedPathSummary || null,\n ciCdPlan: plan.ciCdPlan || null,\n services: plan.services || [],\n entries\n}, null, 2) + String.fromCharCode(10));\nconsole.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 || [] }));\nNODE\n" + "script": "#!/bin/sh\nset -eu\necho '{\"event\":\"ci-base-image\",\"phase\":\"plan-artifacts\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\ncd /workspace/source/repo\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps/yaml\" ] && [ ! -e node_modules/yaml ]; then\n mkdir -p node_modules\n ln -s \"$ci_node_deps/yaml\" node_modules/yaml\nfi\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nHWLAB_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\nnode 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\nnode - <<'NODE'\nconst fs = require(\"node:fs\");\nconst plan = JSON.parse(fs.readFileSync(\"/workspace/source/ci-plan.json\", \"utf8\"));\nconst selected = String(process.env.HWLAB_SELECTED_SERVICES || \"\").split(\",\").map((item) => item.trim()).filter(Boolean);\nconst allServices = selected.length > 0 ? selected : [\"hwlab-cloud-api\",\"hwlab-cloud-web\",\"hwlab-gateway\",\"hwlab-edge-proxy\",\"hwlab-agent-skills\"];\nconst affected = new Set(plan.affectedServices || []);\nconst plannedBuildServices = Array.isArray(plan.buildServices) ? new Set(plan.buildServices) : null;\nconst selectedSet = new Set(selected);\nconst byService = new Map((plan.services || []).map((service) => [service.serviceId, service]));\nconst entries = allServices.map((serviceId) => {\n const service = byService.get(serviceId) || {};\n const serviceSelected = selectedSet.has(serviceId);\n const rolloutAffected = serviceSelected && affected.has(serviceId);\n const envReuse = service.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.envReuse === true;\n const buildRequired = serviceSelected && (plannedBuildServices ? plannedBuildServices.has(serviceId) : (envReuse ? service.envChanged === true : rolloutAffected));\n return {\n serviceId,\n selected: serviceSelected,\n affected: rolloutAffected,\n buildRequired,\n rolloutAffected,\n runtimeMode: service.runtimeMode || \"service-image\",\n envChanged: service.envChanged ?? null,\n codeChanged: service.codeChanged ?? null\n };\n});\nfor (const entry of entries) {\n fs.writeFileSync(\"/tekton/results/affected-\" + entry.serviceId, entry.affected ? \"true\" : \"false\");\n fs.writeFileSync(\"/tekton/results/build-\" + entry.serviceId, entry.buildRequired ? \"true\" : \"false\");\n}\nfs.writeFileSync(\"/workspace/source/affected-services.json\", JSON.stringify({\n sourceCommitId: plan.sourceCommitId,\n affectedServices: plan.affectedServices || [],\n rolloutServices: plan.rolloutServices || plan.affectedServices || [],\n buildServices: plan.buildServices || entries.filter((entry) => entry.buildRequired).map((entry) => entry.serviceId),\n rolloutWithoutImageBuildServices: plan.rolloutWithoutImageBuildServices || [],\n reusedServices: plan.reusedServices || [],\n serviceReusedCount: plan.serviceReusedCount || 0,\n imageBuildSkippedServices: plan.imageBuildSkippedServices || [],\n buildSkippedCount: plan.buildSkippedCount || 0,\n artifactCatalog: plan.artifactCatalog || null,\n envArtifactGroups: plan.envArtifactGroups || [],\n changedPathSummary: plan.changedPathSummary || null,\n ciCdPlan: plan.ciCdPlan || null,\n services: plan.services || [],\n entries\n}, null, 2) + String.fromCharCode(10));\nconsole.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 || [] }));\nNODE\n" } ] }, diff --git a/ci/pipelines/hwlab-nc01-v03-ci-image-publish.yaml b/ci/pipelines/hwlab-nc01-v03-ci-image-publish.yaml index 4849c2f1..0ac0b538 100644 --- a/ci/pipelines/hwlab-nc01-v03-ci-image-publish.yaml +++ b/ci/pipelines/hwlab-nc01-v03-ci-image-publish.yaml @@ -1 +1 @@ -{"apiVersion":"tekton.dev/v1","kind":"Pipeline","metadata":{"name":"hwlab-nc01-v03-ci-image-publish","namespace":"hwlab-ci","labels":{"app.kubernetes.io/part-of":"hwlab","hwlab.pikastech.local/gitops-target":"v03"},"annotations":{"hwlab.pikastech.local/source-config":"scripts/gitops-render.mjs#tekton-native-primitive-ci","hwlab.pikastech.local/ci-contract":"tekton-native-primitive-tasks","hwlab.pikastech.local/policy":"native-per-service-taskrun-image-publish"}},"spec":{"params":[{"name":"git-url","type":"string","default":"git@github.com:pikasTech/HWLAB.git"},{"name":"git-read-url","type":"string","default":"http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git"},{"name":"git-write-url","type":"string","default":"http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/HWLAB.git"},{"name":"source-branch","type":"string","default":"v0.3"},{"name":"gitops-branch","type":"string","default":"v0.3-gitops"},{"name":"lane","type":"string","default":"v03"},{"name":"catalog-path","type":"string","default":"deploy/artifact-catalog.v03.json"},{"name":"image-tag-mode","type":"string","default":"full"},{"name":"runtime-path","type":"string","default":"deploy/gitops/node/runtime-v03"},{"name":"revision","type":"string","description":"Full git commit SHA; the only source truth for image tags."},{"name":"registry-prefix","type":"string","default":"127.0.0.1:5000/hwlab"},{"name":"services","type":"string","default":"hwlab-cloud-api,hwlab-workbench-runtime,hwlab-user-billing,hwlab-project-management,hwlab-cloud-web,hwlab-gateway,hwlab-edge-proxy,hwlab-agent-skills"},{"name":"base-image","type":"string","default":"127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim"},{"name":"build-cache-mode","type":"string","default":"registry"},{"name":"runtime-ready-timeout-ms","type":"string","default":"60000"}],"workspaces":[{"name":"source"},{"name":"git-ssh"}],"tasks":[{"name":"prepare-source","workspaces":[{"name":"source","workspace":"source"},{"name":"git-ssh","workspace":"git-ssh"}],"taskSpec":{"params":[{"name":"git-url"},{"name":"git-read-url"},{"name":"source-branch"},{"name":"gitops-branch"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"services"},{"name":"revision"}],"workspaces":[{"name":"source"},{"name":"git-ssh"}],"steps":[{"name":"prepare-source","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"prepare-source\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\necho '{\"event\":\"ci-base-image\",\"phase\":\"prepare-source\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor tool in node git timeout; do command -v \"$tool\" >/dev/null 2>&1 || { echo '{\"event\":\"ci-base-image\",\"ok\":false,\"reason\":\"missing-tool\",\"tool\":\"'\"$tool\"'\"}'; exit 31; }; done\nnode -e 'console.log(JSON.stringify({event:\"ci-base-image\",phase:\"prepare-source\",ok:true,node:process.version}))'\ngit_ssh_setup() {\n mkdir -p /root/.ssh\n cp /workspace/git-ssh/ssh-privatekey /root/.ssh/id_rsa\n chmod 600 /root/.ssh/id_rsa\n timeout 10 ssh-keyscan github.com >> /root/.ssh/known_hosts 2>/dev/null || true\n timeout 10 ssh-keyscan -p 443 ssh.github.com >> /root/.ssh/known_hosts 2>/dev/null || true\n write_github_proxy_command\n export GIT_SSH_COMMAND=\"ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o ServerAliveInterval=10 -o ServerAliveCountMax=2 -o ProxyCommand='node /tmp/hwlab-github-proxy-connect.mjs 127.0.0.1 10808 %h %p'\"\n git config --global url.\"ssh://git@ssh.github.com:443/\".insteadOf \"git@github.com:\"\n git config --global url.\"ssh://git@ssh.github.com:443/\".insteadOf \"ssh://git@github.com/\"\n}\n\nwrite_github_proxy_command() {\n cat > /tmp/hwlab-github-proxy-connect.mjs <<'NODE_PROXY'\n#!/usr/bin/env node\nimport net from \"node:net\";\n\nconst [proxyHost, proxyPortRaw, targetHost, targetPortRaw] = process.argv.slice(2);\nconst proxyPort = Number.parseInt(proxyPortRaw || \"\", 10);\nconst targetPort = Number.parseInt(targetPortRaw || \"\", 10);\nif (!proxyHost || !Number.isInteger(proxyPort) || !targetHost || !Number.isInteger(targetPort)) {\n console.error(\"usage: hwlab-github-proxy-connect \");\n process.exit(64);\n}\n\nconst socket = net.createConnection({ host: proxyHost, port: proxyPort });\nlet buffer = Buffer.alloc(0);\n\nsocket.setTimeout(10000, () => {\n console.error(\"proxy connect timeout \" + proxyHost + \":\" + proxyPort + \" -> \" + targetHost + \":\" + targetPort);\n socket.destroy();\n process.exit(65);\n});\n\nsocket.on(\"connect\", () => {\n socket.write(\"CONNECT \" + targetHost + \":\" + targetPort + \" HTTP/1.1\\r\\nHost: \" + targetHost + \":\" + targetPort + \"\\r\\nProxy-Connection: Keep-Alive\\r\\n\\r\\n\");\n});\n\nsocket.on(\"error\", (error) => {\n console.error(\"proxy connect failed: \" + error.message);\n process.exit(66);\n});\n\nfunction onData(chunk) {\n buffer = Buffer.concat([buffer, chunk]);\n const headerEnd = buffer.indexOf(\"\\r\\n\\r\\n\");\n if (headerEnd === -1 && buffer.length < 8192) return;\n const head = buffer.slice(0, headerEnd + 4).toString(\"latin1\");\n const statusLine = head.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number.parseInt(statusLine.split(\" \")[1] || \"\", 10);\n if (!statusLine.startsWith(\"HTTP/1.\") || !Number.isInteger(statusCode) || statusCode < 200 || statusCode > 299) {\n console.error(\"proxy CONNECT rejected: \" + (statusLine || \"missing-status\"));\n socket.destroy();\n process.exit(67);\n }\n socket.off(\"data\", onData);\n socket.setTimeout(0);\n const rest = buffer.slice(headerEnd + 4);\n if (rest.length) process.stdout.write(rest);\n process.stdin.pipe(socket);\n socket.pipe(process.stdout);\n}\n\nsocket.on(\"data\", onData);\nsocket.on(\"close\", () => process.exit(0));\nNODE_PROXY\n chmod 0700 /tmp/hwlab-github-proxy-connect.mjs\n}\n\ngit_now_ms() {\n node -e 'console.log(Date.now())' 2>/dev/null || date +%s000\n}\n\ngit_timed() {\n phase=\"$1\"\n timeout_seconds=\"$2\"\n shift 2\n started_ms=\"$(git_now_ms)\"\n echo '{\"event\":\"git-operation\",\"phase\":\"'\"$phase\"'\",\"status\":\"started\",\"timeoutSeconds\":'\"$timeout_seconds\"'}' >&2\n set +e\n timeout \"$timeout_seconds\" \"$@\"\n status=\"$?\"\n set -e\n finished_ms=\"$(git_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n if [ \"$status\" -eq 0 ]; then\n echo '{\"event\":\"git-operation\",\"phase\":\"'\"$phase\"'\",\"status\":\"succeeded\",\"durationMs\":'\"$duration_ms\"'}' >&2\n return 0\n fi\n if [ \"$status\" -eq 124 ] || [ \"$status\" -eq 137 ]; then\n echo '{\"event\":\"git-operation\",\"phase\":\"'\"$phase\"'\",\"status\":\"failed\",\"reason\":\"timeout\",\"durationMs\":'\"$duration_ms\"',\"timeoutSeconds\":'\"$timeout_seconds\"'}' >&2\n else\n echo '{\"event\":\"git-operation\",\"phase\":\"'\"$phase\"'\",\"status\":\"failed\",\"exitCode\":'\"$status\"',\"durationMs\":'\"$duration_ms\"'}' >&2\n fi\n return \"$status\"\n}\n\ngit_url_requires_ssh() { case \"$1\" in git@*|ssh://*) return 0 ;; *) return 1 ;; esac; }\ngit_read_url=\"$(params.git-read-url)\"\nif git_url_requires_ssh \"$git_read_url\"; then\n for tool in ssh ssh-keyscan; do command -v \"$tool\" >/dev/null 2>&1 || { echo '{\"event\":\"ci-base-image\",\"ok\":false,\"reason\":\"missing-tool\",\"tool\":\"'\"$tool\"'\"}'; exit 31; }; done\n git_ssh_setup\nelse\n echo '{\"event\":\"git-ssh-setup\",\"phase\":\"prepare-source\",\"status\":\"skipped\",\"reason\":\"non-ssh-git-read-url\"}'\nfi\nrm -rf /workspace/source/repo\nsource_clone_started_ms=\"$(ci_now_ms)\"\ngit_timed source-clone 180 git clone --branch \"$(params.source-branch)\" \"$git_read_url\" /workspace/source/repo\ncd /workspace/source/repo\ngit config --global --add safe.directory /workspace/source/repo\ngit remote set-url origin \"$git_read_url\"\ngit checkout \"$(params.revision)\"\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\ngit merge-base --is-ancestor \"$(params.revision)\" \"origin/$(params.source-branch)\" || { echo '{\"event\":\"source-ancestry\",\"status\":\"failed\",\"revision\":\"'\"$(params.revision)\"'\",\"sourceBranch\":\"'\"$(params.source-branch)\"'\"}'; exit 32; }\nci_timing_emit source-clone succeeded \"$source_clone_started_ms\"\nprepare_source_dependencies_started_ms=\"$(ci_now_ms)\"\necho '{\"event\":\"prepare-source-dependencies\",\"status\":\"skipped\",\"reason\":\"renderer-dependency-install-disabled\",\"dependency\":\"yaml\"}'\nci_timing_emit prepare-source-dependencies succeeded \"$prepare_source_dependencies_started_ms\"\ncatalog_path=\"$(params.catalog-path)\"\nmkdir -p \"$(dirname \"$catalog_path\")\"\ncatalog_fetch_started_ms=\"$(ci_now_ms)\"\nif git_timed catalog-ls-remote 45 git ls-remote --exit-code --heads \"$git_read_url\" \"$(params.gitops-branch)\" >/dev/null; then\n git remote set-url gitops-catalog \"$git_read_url\" 2>/dev/null || git remote add gitops-catalog \"$git_read_url\"\n git_timed catalog-fetch 90 git fetch --depth 1 gitops-catalog \"$(params.gitops-branch)\" >/dev/null || true\n if git cat-file -e FETCH_HEAD:\"$catalog_path\" 2>/dev/null; then\n git show FETCH_HEAD:\"$catalog_path\" > /tmp/hwlab-gitops-artifact-catalog.json\n if node --input-type=module - /tmp/hwlab-gitops-artifact-catalog.json deploy/deploy.yaml \"$(params.services)\" <<'NODE'\nimport fs from \"node:fs\";\nconst [catalogPath, deployPath, selectedServices] = process.argv.slice(2);\nconst ids = (doc) => (doc.services || []).map((service) => service.serviceId).filter(Boolean);\nconst topLevelServiceIds = (text) => {\n const values = [];\n let inServices = false;\n for (const line of text.split(/\\r?\\n/u)) {\n if (/^services:\\s*$/u.test(line)) { inServices = true; continue; }\n if (!inServices) continue;\n if (/^\\S/u.test(line)) break;\n const match = line.match(/^\\s*-\\s+serviceId:\\s*['\"]?([^'\"#\\s]+)['\"]?/u);\n if (match) values.push(match[1]);\n }\n return values;\n};\nconst selected = (selectedServices || \"\").split(\",\").map((item) => item.trim()).filter(Boolean);\nconst uniqueSorted = (items) => [...new Set(items)].sort();\nconst gitops = JSON.parse(fs.readFileSync(catalogPath, \"utf8\"));\nconst expected = selected.length ? selected : topLevelServiceIds(fs.readFileSync(deployPath, \"utf8\"));\nconst actual = ids(gitops);\nconst expectedSet = uniqueSorted(expected);\nconst actualSet = uniqueSorted(actual);\nconst sameServices = expectedSet.length === actualSet.length && expectedSet.every((item, index) => item === actualSet[index]);\nif (!sameServices) {\n const missing = expectedSet.filter((item) => !actualSet.includes(item));\n const extra = actualSet.filter((item) => !expectedSet.includes(item));\n console.error(JSON.stringify({ event: \"gitops-artifact-catalog\", phase: \"prepare-source\", status: \"ignored-stale\", reason: \"service-ids-mismatch\", expected, actual, missing, extra }));\n process.exit(42);\n}\nNODE\n then\n cp /tmp/hwlab-gitops-artifact-catalog.json \"$catalog_path\"\n echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"loaded\",\"branch\":\"'\"$(params.gitops-branch)\"'\"}'\n else\n echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"ignored-stale\",\"reason\":\"service-ids-mismatch\",\"branch\":\"'\"$(params.gitops-branch)\"'\"}'\n fi\n else\n echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"seed\",\"reason\":\"missing-on-gitops-branch\"}'\n fi\nelse\n echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"seed\",\"reason\":\"gitops-branch-missing\"}'\nfi\nif [ ! -s \"$catalog_path\" ] && [ \"$(params.lane)\" = \"v02\" ]; then\n node --input-type=module - \"$catalog_path\" \"$(params.revision)\" \"$(params.registry-prefix)\" \"$(params.image-tag-mode)\" \"$(params.services)\" <<'NODE'\nimport fs from 'node:fs';\nimport path from 'node:path';\nconst [catalogPath, revision, registryPrefix, imageTagMode, selectedServices] = process.argv.slice(2);\nconst topLevelServiceIds = (text) => {\n const values = [];\n let inServices = false;\n for (const line of text.split(/\\r?\\n/u)) {\n if (/^services:\\s*$/u.test(line)) { inServices = true; continue; }\n if (!inServices) continue;\n if (/^\\S/u.test(line)) break;\n const match = line.match(/^\\s*-\\s+serviceId:\\s*['\"]?([^'\"#\\s]+)['\"]?/u);\n if (match) values.push(match[1]);\n }\n return values;\n};\nconst tag = imageTagMode === 'full' ? revision : revision.slice(0, 7);\nconst namespace = 'hwlab-v02';\nconst selected = (selectedServices || '').split(',').filter(Boolean);\nconst serviceIds = selected.length ? selected : topLevelServiceIds(fs.readFileSync('deploy/deploy.yaml', 'utf8'));\nconst services = serviceIds.map((serviceId) => {\n const service = { serviceId, profile: 'v02', namespace, commitId: tag, sourceCommitId: revision, image: `${registryPrefix}/${serviceId}:${tag}`, imageTag: tag, digest: null, buildBackend: 'contract-skeleton' };\n return service;\n});\nfs.mkdirSync(path.dirname(catalogPath), { recursive: true });\nfs.writeFileSync(catalogPath, JSON.stringify({ catalogVersion: 'v1', kind: 'hwlab-artifact-catalog', environment: 'v02', profile: 'v02', namespace, endpoint: \"https://hwlab.74-48-78-17.nip.io\", commitId: tag, artifactState: 'contract-skeleton', publish: { registryPrefix, sourceCommitId: revision, imageTag: tag, publishedAt: null }, allowedProfiles: ['v02'], forbiddenProfiles: ['dev', 'prod'], services }, null, 2) + '\\n');\nNODE\n echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"seeded-v02-skeleton\"}'\nfi\nci_timing_emit catalog-fetch succeeded \"$catalog_fetch_started_ms\"\necho 'node prepare-source complete; validation task count=3'\n"}]},"params":[{"name":"git-url","value":"$(params.git-url)"},{"name":"git-read-url","value":"$(params.git-read-url)"},{"name":"source-branch","value":"$(params.source-branch)"},{"name":"gitops-branch","value":"$(params.gitops-branch)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"services","value":"$(params.services)"},{"name":"revision","value":"$(params.revision)"}]},{"name":"repo-reports-guard","runAfter":["prepare-source"],"workspaces":[{"name":"source","workspace":"source"}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"source-branch"},{"name":"gitops-branch"}],"workspaces":[{"name":"source"}],"steps":[{"name":"repo-reports-guard","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"script":"#!/bin/sh\nset -eu\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nnode scripts/repo-reports-guard.mjs\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"source-branch","value":"$(params.source-branch)"},{"name":"gitops-branch","value":"$(params.gitops-branch)"}]},{"name":"node-contract-check","runAfter":["prepare-source"],"workspaces":[{"name":"source","workspace":"source"}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"source-branch"},{"name":"gitops-branch"}],"workspaces":[{"name":"source"}],"steps":[{"name":"node-contract-check","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"script":"#!/bin/sh\nset -eu\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nnode --check scripts/gitops-render.mjs\nnode --input-type=module <<'NODE'\nimport { readdirSync, readFileSync, statSync } from \"node:fs\";\nimport path from \"node:path\";\n\nconst root = process.cwd();\nconst ciFileName = \"CI\" + \".json\";\nconst forbiddenCiFragments = [\n \"ci\" + \"-json\",\n \"HWLAB_NODE_TEKTON\" + \"_SINGLE_IMAGE_PUBLISH\",\n \"single\" + \"-dind\",\n \"docker\" + \":29\",\n \"DOCKER\" + \"_HOST\",\n \"Docker\" + \"-in-Docker\",\n \"D\" + \"IND\",\n \"docker\" + \" push\",\n \"--build\" + \"-backend\",\n \"HWLAB_ARTIFACT\" + \"_BUILD_BACKEND\",\n \"--legacy\" + \"-source-images\",\n \"HWLAB_NODE_USE_DEPLOY_IMAGES\" + \"=0\"\n];\nconst scanTargets = [\n \"scripts/gitops-render.mjs\",\n \"scripts/src/runtime-lane.ts\",\n \"scripts/artifact-publish.mjs\",\n \"scripts/src/ci-plan-lib.mjs\",\n \"deploy/gitops/node/tekton\"\n];\nconst skipDirs = new Set([\".git\", \"node_modules\", \".worktree\"]);\n\nfunction walkFiles(relativePath) {\n const absolutePath = path.join(root, relativePath);\n let stat;\n try {\n stat = statSync(absolutePath);\n } catch {\n return [];\n }\n if (stat.isFile()) return [relativePath];\n if (!stat.isDirectory()) return [];\n const files = [];\n for (const entry of readdirSync(absolutePath, { withFileTypes: true })) {\n if (entry.isDirectory() && skipDirs.has(entry.name)) continue;\n files.push(...walkFiles(path.join(relativePath, entry.name)));\n }\n return files;\n}\n\nconst ciFiles = walkFiles(\".\").filter((filePath) => path.basename(filePath) === ciFileName);\nconst violations = [];\nfor (const filePath of scanTargets.flatMap(walkFiles)) {\n const text = readFileSync(path.join(root, filePath), \"utf8\");\n for (const fragment of forbiddenCiFragments) {\n if (text.includes(fragment)) violations.push({ filePath, fragment });\n }\n}\nif (ciFiles.length || violations.length) {\n console.error(JSON.stringify({ status: \"failed\", ciFiles, violations }, null, 2));\n process.exit(1);\n}\nNODE\nnode --check scripts/artifact-publish.mjs\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"source-branch","value":"$(params.source-branch)"},{"name":"gitops-branch","value":"$(params.gitops-branch)"}]},{"name":"codex-api-forwarder-check","runAfter":["prepare-source"],"workspaces":[{"name":"source","workspace":"source"}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"source-branch"},{"name":"gitops-branch"}],"workspaces":[{"name":"source"}],"steps":[{"name":"codex-api-forwarder-check","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"script":"#!/bin/sh\nset -eu\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nnode scripts/run-bun.mjs test cmd/hwlab-codex-api-responses-forwarder/main.test.ts\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"source-branch","value":"$(params.source-branch)"},{"name":"gitops-branch","value":"$(params.gitops-branch)"}]},{"name":"plan-artifacts","runAfter":["repo-reports-guard","node-contract-check","codex-api-forwarder-check"],"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"},{"name":"services"}],"results":[{"name":"affected-hwlab-cloud-api","description":"hwlab-cloud-api rollout affected according to ci-plan"},{"name":"build-hwlab-cloud-api","description":"hwlab-cloud-api image build required according to ci-plan"},{"name":"affected-hwlab-workbench-runtime","description":"hwlab-workbench-runtime rollout affected according to ci-plan"},{"name":"build-hwlab-workbench-runtime","description":"hwlab-workbench-runtime image build required according to ci-plan"},{"name":"affected-hwlab-user-billing","description":"hwlab-user-billing rollout affected according to ci-plan"},{"name":"build-hwlab-user-billing","description":"hwlab-user-billing image build required according to ci-plan"},{"name":"affected-hwlab-project-management","description":"hwlab-project-management rollout affected according to ci-plan"},{"name":"build-hwlab-project-management","description":"hwlab-project-management image build required according to ci-plan"},{"name":"affected-hwlab-cloud-web","description":"hwlab-cloud-web rollout affected according to ci-plan"},{"name":"build-hwlab-cloud-web","description":"hwlab-cloud-web image build required according to ci-plan"},{"name":"affected-hwlab-gateway","description":"hwlab-gateway rollout affected according to ci-plan"},{"name":"build-hwlab-gateway","description":"hwlab-gateway image build required according to ci-plan"},{"name":"affected-hwlab-edge-proxy","description":"hwlab-edge-proxy rollout affected according to ci-plan"},{"name":"build-hwlab-edge-proxy","description":"hwlab-edge-proxy image build required according to ci-plan"},{"name":"affected-hwlab-agent-skills","description":"hwlab-agent-skills rollout affected according to ci-plan"},{"name":"build-hwlab-agent-skills","description":"hwlab-agent-skills image build required according to ci-plan"}],"workspaces":[{"name":"source"}],"steps":[{"name":"plan","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","env":[{"name":"HWLAB_SELECTED_SERVICES","value":"$(params.services)"},{"name":"HWLAB_DEV_REGISTRY_K8S_NATIVE","value":"1"},{"name":"HWLAB_TEKTON_PIPELINERUN","value":"$(context.pipelineRun.name)"},{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"script":"#!/bin/sh\nset -eu\necho '{\"event\":\"ci-base-image\",\"phase\":\"plan-artifacts\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\ncd /workspace/source/repo\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps/yaml\" ] && [ ! -e node_modules/yaml ]; then\n mkdir -p node_modules\n ln -s \"$ci_node_deps/yaml\" node_modules/yaml\nfi\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nHWLAB_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\nnode 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\nnode - <<'NODE'\nconst fs = require(\"node:fs\");\nconst plan = JSON.parse(fs.readFileSync(\"/workspace/source/ci-plan.json\", \"utf8\"));\nconst selected = String(process.env.HWLAB_SELECTED_SERVICES || \"\").split(\",\").map((item) => item.trim()).filter(Boolean);\nconst allServices = selected.length > 0 ? selected : [\"hwlab-cloud-api\",\"hwlab-cloud-web\",\"hwlab-gateway\",\"hwlab-edge-proxy\",\"hwlab-agent-skills\"];\nconst affected = new Set(plan.affectedServices || []);\nconst plannedBuildServices = Array.isArray(plan.buildServices) ? new Set(plan.buildServices) : null;\nconst selectedSet = new Set(selected);\nconst byService = new Map((plan.services || []).map((service) => [service.serviceId, service]));\nconst entries = allServices.map((serviceId) => {\n const service = byService.get(serviceId) || {};\n const serviceSelected = selectedSet.has(serviceId);\n const rolloutAffected = serviceSelected && affected.has(serviceId);\n const envReuse = service.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.envReuse === true;\n const buildRequired = serviceSelected && (plannedBuildServices ? plannedBuildServices.has(serviceId) : (envReuse ? service.envChanged === true : rolloutAffected));\n return {\n serviceId,\n selected: serviceSelected,\n affected: rolloutAffected,\n buildRequired,\n rolloutAffected,\n runtimeMode: service.runtimeMode || \"service-image\",\n envChanged: service.envChanged ?? null,\n codeChanged: service.codeChanged ?? null\n };\n});\nfor (const entry of entries) {\n fs.writeFileSync(\"/tekton/results/affected-\" + entry.serviceId, entry.affected ? \"true\" : \"false\");\n fs.writeFileSync(\"/tekton/results/build-\" + entry.serviceId, entry.buildRequired ? \"true\" : \"false\");\n}\nfs.writeFileSync(\"/workspace/source/affected-services.json\", JSON.stringify({\n sourceCommitId: plan.sourceCommitId,\n affectedServices: plan.affectedServices || [],\n rolloutServices: plan.rolloutServices || plan.affectedServices || [],\n buildServices: plan.buildServices || entries.filter((entry) => entry.buildRequired).map((entry) => entry.serviceId),\n reusedServices: plan.reusedServices || [],\n buildSkippedCount: plan.buildSkippedCount || 0,\n envArtifactGroups: plan.envArtifactGroups || [],\n changedPathSummary: plan.changedPathSummary || null,\n ciCdPlan: plan.ciCdPlan || null,\n services: plan.services || [],\n entries\n}, null, 2) + String.fromCharCode(10));\nconsole.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 || [] }));\nNODE\n"}]},"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)"},{"name":"services","value":"$(params.services)"}]},{"name":"build-hwlab-cloud-api","runAfter":["plan-artifacts"],"workspaces":[{"name":"source","workspace":"source"}],"when":[{"input":"$(tasks.plan-artifacts.results.build-hwlab-cloud-api)","operator":"in","values":["true"]}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"service-id"},{"name":"base-image"},{"name":"build-cache-mode"}],"results":[{"name":"status","description":"hwlab-cloud-api status"},{"name":"service-id","description":"hwlab-cloud-api service-id"},{"name":"image","description":"hwlab-cloud-api image"},{"name":"image-tag","description":"hwlab-cloud-api image-tag"},{"name":"digest","description":"hwlab-cloud-api digest"},{"name":"repository-digest","description":"hwlab-cloud-api repository-digest"},{"name":"source-commit-id","description":"hwlab-cloud-api source-commit-id"},{"name":"component-input-hash","description":"hwlab-cloud-api component-input-hash"},{"name":"environment-input-hash","description":"hwlab-cloud-api environment-input-hash"},{"name":"code-input-hash","description":"hwlab-cloud-api code-input-hash"},{"name":"runtime-mode","description":"hwlab-cloud-api runtime-mode"},{"name":"boot-repo","description":"hwlab-cloud-api boot-repo"},{"name":"boot-commit","description":"hwlab-cloud-api boot-commit"},{"name":"boot-sh","description":"hwlab-cloud-api boot-sh"},{"name":"build-created-at","description":"hwlab-cloud-api build-created-at"},{"name":"build-backend","description":"hwlab-cloud-api build-backend"},{"name":"reused-from","description":"hwlab-cloud-api reused-from"}],"workspaces":[{"name":"source"}],"volumes":[{"name":"service-work","emptyDir":{}},{"name":"buildkit-bin","emptyDir":{}},{"name":"buildkit-run","emptyDir":{}}],"sidecars":[{"name":"buildkitd","image":"moby/buildkit:rootless","args":["--addr","unix:///workspace/buildkit-run/buildkitd.sock","--oci-worker-no-process-sandbox"],"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"securityContext":{"runAsUser":1000,"runAsGroup":1000,"allowPrivilegeEscalation":true,"appArmorProfile":{"type":"Unconfined"},"seccompProfile":{"type":"Unconfined"}}}],"steps":[{"name":"prepare-buildkit-client","image":"moby/buildkit:rootless","securityContext":{"runAsUser":0,"runAsGroup":0},"volumeMounts":[{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"}],"script":"#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"},{"name":"publish","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","securityContext":{"runAsUser":1000,"runAsGroup":1000},"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"service-work","mountPath":"/workspace/service-work"},{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"},{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -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.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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\" --build-cache-mode \"$(params.build-cache-mode)\"\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"service-id","value":"hwlab-cloud-api"},{"name":"base-image","value":"$(params.base-image)"},{"name":"build-cache-mode","value":"$(params.build-cache-mode)"}]},{"name":"build-hwlab-workbench-runtime","runAfter":["plan-artifacts"],"workspaces":[{"name":"source","workspace":"source"}],"when":[{"input":"$(tasks.plan-artifacts.results.build-hwlab-workbench-runtime)","operator":"in","values":["true"]}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"service-id"},{"name":"base-image"},{"name":"build-cache-mode"}],"results":[{"name":"status","description":"hwlab-workbench-runtime status"},{"name":"service-id","description":"hwlab-workbench-runtime service-id"},{"name":"image","description":"hwlab-workbench-runtime image"},{"name":"image-tag","description":"hwlab-workbench-runtime image-tag"},{"name":"digest","description":"hwlab-workbench-runtime digest"},{"name":"repository-digest","description":"hwlab-workbench-runtime repository-digest"},{"name":"source-commit-id","description":"hwlab-workbench-runtime source-commit-id"},{"name":"component-input-hash","description":"hwlab-workbench-runtime component-input-hash"},{"name":"environment-input-hash","description":"hwlab-workbench-runtime environment-input-hash"},{"name":"code-input-hash","description":"hwlab-workbench-runtime code-input-hash"},{"name":"runtime-mode","description":"hwlab-workbench-runtime runtime-mode"},{"name":"boot-repo","description":"hwlab-workbench-runtime boot-repo"},{"name":"boot-commit","description":"hwlab-workbench-runtime boot-commit"},{"name":"boot-sh","description":"hwlab-workbench-runtime boot-sh"},{"name":"build-created-at","description":"hwlab-workbench-runtime build-created-at"},{"name":"build-backend","description":"hwlab-workbench-runtime build-backend"},{"name":"reused-from","description":"hwlab-workbench-runtime reused-from"}],"workspaces":[{"name":"source"}],"volumes":[{"name":"service-work","emptyDir":{}},{"name":"buildkit-bin","emptyDir":{}},{"name":"buildkit-run","emptyDir":{}}],"sidecars":[{"name":"buildkitd","image":"moby/buildkit:rootless","args":["--addr","unix:///workspace/buildkit-run/buildkitd.sock","--oci-worker-no-process-sandbox"],"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"securityContext":{"runAsUser":1000,"runAsGroup":1000,"allowPrivilegeEscalation":true,"appArmorProfile":{"type":"Unconfined"},"seccompProfile":{"type":"Unconfined"}}}],"steps":[{"name":"prepare-buildkit-client","image":"moby/buildkit:rootless","securityContext":{"runAsUser":0,"runAsGroup":0},"volumeMounts":[{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"}],"script":"#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"},{"name":"publish","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","securityContext":{"runAsUser":1000,"runAsGroup":1000},"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"service-work","mountPath":"/workspace/service-work"},{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"},{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -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.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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\" --build-cache-mode \"$(params.build-cache-mode)\"\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"service-id","value":"hwlab-workbench-runtime"},{"name":"base-image","value":"$(params.base-image)"},{"name":"build-cache-mode","value":"$(params.build-cache-mode)"}]},{"name":"build-hwlab-user-billing","runAfter":["plan-artifacts"],"workspaces":[{"name":"source","workspace":"source"}],"when":[{"input":"$(tasks.plan-artifacts.results.build-hwlab-user-billing)","operator":"in","values":["true"]}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"service-id"},{"name":"base-image"},{"name":"build-cache-mode"}],"results":[{"name":"status","description":"hwlab-user-billing status"},{"name":"service-id","description":"hwlab-user-billing service-id"},{"name":"image","description":"hwlab-user-billing image"},{"name":"image-tag","description":"hwlab-user-billing image-tag"},{"name":"digest","description":"hwlab-user-billing digest"},{"name":"repository-digest","description":"hwlab-user-billing repository-digest"},{"name":"source-commit-id","description":"hwlab-user-billing source-commit-id"},{"name":"component-input-hash","description":"hwlab-user-billing component-input-hash"},{"name":"environment-input-hash","description":"hwlab-user-billing environment-input-hash"},{"name":"code-input-hash","description":"hwlab-user-billing code-input-hash"},{"name":"runtime-mode","description":"hwlab-user-billing runtime-mode"},{"name":"boot-repo","description":"hwlab-user-billing boot-repo"},{"name":"boot-commit","description":"hwlab-user-billing boot-commit"},{"name":"boot-sh","description":"hwlab-user-billing boot-sh"},{"name":"build-created-at","description":"hwlab-user-billing build-created-at"},{"name":"build-backend","description":"hwlab-user-billing build-backend"},{"name":"reused-from","description":"hwlab-user-billing reused-from"}],"workspaces":[{"name":"source"}],"volumes":[{"name":"service-work","emptyDir":{}},{"name":"buildkit-bin","emptyDir":{}},{"name":"buildkit-run","emptyDir":{}}],"sidecars":[{"name":"buildkitd","image":"moby/buildkit:rootless","args":["--addr","unix:///workspace/buildkit-run/buildkitd.sock","--oci-worker-no-process-sandbox"],"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"securityContext":{"runAsUser":1000,"runAsGroup":1000,"allowPrivilegeEscalation":true,"appArmorProfile":{"type":"Unconfined"},"seccompProfile":{"type":"Unconfined"}}}],"steps":[{"name":"prepare-buildkit-client","image":"moby/buildkit:rootless","securityContext":{"runAsUser":0,"runAsGroup":0},"volumeMounts":[{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"}],"script":"#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"},{"name":"publish","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","securityContext":{"runAsUser":1000,"runAsGroup":1000},"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"service-work","mountPath":"/workspace/service-work"},{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"},{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -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.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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\" --build-cache-mode \"$(params.build-cache-mode)\"\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"service-id","value":"hwlab-user-billing"},{"name":"base-image","value":"$(params.base-image)"},{"name":"build-cache-mode","value":"$(params.build-cache-mode)"}]},{"name":"build-hwlab-project-management","runAfter":["plan-artifacts"],"workspaces":[{"name":"source","workspace":"source"}],"when":[{"input":"$(tasks.plan-artifacts.results.build-hwlab-project-management)","operator":"in","values":["true"]}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"service-id"},{"name":"base-image"},{"name":"build-cache-mode"}],"results":[{"name":"status","description":"hwlab-project-management status"},{"name":"service-id","description":"hwlab-project-management service-id"},{"name":"image","description":"hwlab-project-management image"},{"name":"image-tag","description":"hwlab-project-management image-tag"},{"name":"digest","description":"hwlab-project-management digest"},{"name":"repository-digest","description":"hwlab-project-management repository-digest"},{"name":"source-commit-id","description":"hwlab-project-management source-commit-id"},{"name":"component-input-hash","description":"hwlab-project-management component-input-hash"},{"name":"environment-input-hash","description":"hwlab-project-management environment-input-hash"},{"name":"code-input-hash","description":"hwlab-project-management code-input-hash"},{"name":"runtime-mode","description":"hwlab-project-management runtime-mode"},{"name":"boot-repo","description":"hwlab-project-management boot-repo"},{"name":"boot-commit","description":"hwlab-project-management boot-commit"},{"name":"boot-sh","description":"hwlab-project-management boot-sh"},{"name":"build-created-at","description":"hwlab-project-management build-created-at"},{"name":"build-backend","description":"hwlab-project-management build-backend"},{"name":"reused-from","description":"hwlab-project-management reused-from"}],"workspaces":[{"name":"source"}],"volumes":[{"name":"service-work","emptyDir":{}},{"name":"buildkit-bin","emptyDir":{}},{"name":"buildkit-run","emptyDir":{}}],"sidecars":[{"name":"buildkitd","image":"moby/buildkit:rootless","args":["--addr","unix:///workspace/buildkit-run/buildkitd.sock","--oci-worker-no-process-sandbox"],"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"securityContext":{"runAsUser":1000,"runAsGroup":1000,"allowPrivilegeEscalation":true,"appArmorProfile":{"type":"Unconfined"},"seccompProfile":{"type":"Unconfined"}}}],"steps":[{"name":"prepare-buildkit-client","image":"moby/buildkit:rootless","securityContext":{"runAsUser":0,"runAsGroup":0},"volumeMounts":[{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"}],"script":"#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"},{"name":"publish","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","securityContext":{"runAsUser":1000,"runAsGroup":1000},"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"service-work","mountPath":"/workspace/service-work"},{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"},{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -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.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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\" --build-cache-mode \"$(params.build-cache-mode)\"\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"service-id","value":"hwlab-project-management"},{"name":"base-image","value":"$(params.base-image)"},{"name":"build-cache-mode","value":"$(params.build-cache-mode)"}]},{"name":"build-hwlab-cloud-web","runAfter":["plan-artifacts"],"workspaces":[{"name":"source","workspace":"source"}],"when":[{"input":"$(tasks.plan-artifacts.results.build-hwlab-cloud-web)","operator":"in","values":["true"]}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"service-id"},{"name":"base-image"},{"name":"build-cache-mode"}],"results":[{"name":"status","description":"hwlab-cloud-web status"},{"name":"service-id","description":"hwlab-cloud-web service-id"},{"name":"image","description":"hwlab-cloud-web image"},{"name":"image-tag","description":"hwlab-cloud-web image-tag"},{"name":"digest","description":"hwlab-cloud-web digest"},{"name":"repository-digest","description":"hwlab-cloud-web repository-digest"},{"name":"source-commit-id","description":"hwlab-cloud-web source-commit-id"},{"name":"component-input-hash","description":"hwlab-cloud-web component-input-hash"},{"name":"environment-input-hash","description":"hwlab-cloud-web environment-input-hash"},{"name":"code-input-hash","description":"hwlab-cloud-web code-input-hash"},{"name":"runtime-mode","description":"hwlab-cloud-web runtime-mode"},{"name":"boot-repo","description":"hwlab-cloud-web boot-repo"},{"name":"boot-commit","description":"hwlab-cloud-web boot-commit"},{"name":"boot-sh","description":"hwlab-cloud-web boot-sh"},{"name":"build-created-at","description":"hwlab-cloud-web build-created-at"},{"name":"build-backend","description":"hwlab-cloud-web build-backend"},{"name":"reused-from","description":"hwlab-cloud-web reused-from"}],"workspaces":[{"name":"source"}],"volumes":[{"name":"service-work","emptyDir":{}},{"name":"buildkit-bin","emptyDir":{}},{"name":"buildkit-run","emptyDir":{}}],"sidecars":[{"name":"buildkitd","image":"moby/buildkit:rootless","args":["--addr","unix:///workspace/buildkit-run/buildkitd.sock","--oci-worker-no-process-sandbox"],"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"securityContext":{"runAsUser":1000,"runAsGroup":1000,"allowPrivilegeEscalation":true,"appArmorProfile":{"type":"Unconfined"},"seccompProfile":{"type":"Unconfined"}}}],"steps":[{"name":"prepare-buildkit-client","image":"moby/buildkit:rootless","securityContext":{"runAsUser":0,"runAsGroup":0},"volumeMounts":[{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"}],"script":"#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"},{"name":"publish","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","securityContext":{"runAsUser":1000,"runAsGroup":1000},"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"service-work","mountPath":"/workspace/service-work"},{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"},{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -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.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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\" --build-cache-mode \"$(params.build-cache-mode)\"\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"service-id","value":"hwlab-cloud-web"},{"name":"base-image","value":"$(params.base-image)"},{"name":"build-cache-mode","value":"$(params.build-cache-mode)"}]},{"name":"build-hwlab-gateway","runAfter":["plan-artifacts"],"workspaces":[{"name":"source","workspace":"source"}],"when":[{"input":"$(tasks.plan-artifacts.results.build-hwlab-gateway)","operator":"in","values":["true"]}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"service-id"},{"name":"base-image"},{"name":"build-cache-mode"}],"results":[{"name":"status","description":"hwlab-gateway status"},{"name":"service-id","description":"hwlab-gateway service-id"},{"name":"image","description":"hwlab-gateway image"},{"name":"image-tag","description":"hwlab-gateway image-tag"},{"name":"digest","description":"hwlab-gateway digest"},{"name":"repository-digest","description":"hwlab-gateway repository-digest"},{"name":"source-commit-id","description":"hwlab-gateway source-commit-id"},{"name":"component-input-hash","description":"hwlab-gateway component-input-hash"},{"name":"environment-input-hash","description":"hwlab-gateway environment-input-hash"},{"name":"code-input-hash","description":"hwlab-gateway code-input-hash"},{"name":"runtime-mode","description":"hwlab-gateway runtime-mode"},{"name":"boot-repo","description":"hwlab-gateway boot-repo"},{"name":"boot-commit","description":"hwlab-gateway boot-commit"},{"name":"boot-sh","description":"hwlab-gateway boot-sh"},{"name":"build-created-at","description":"hwlab-gateway build-created-at"},{"name":"build-backend","description":"hwlab-gateway build-backend"},{"name":"reused-from","description":"hwlab-gateway reused-from"}],"workspaces":[{"name":"source"}],"volumes":[{"name":"service-work","emptyDir":{}},{"name":"buildkit-bin","emptyDir":{}},{"name":"buildkit-run","emptyDir":{}}],"sidecars":[{"name":"buildkitd","image":"moby/buildkit:rootless","args":["--addr","unix:///workspace/buildkit-run/buildkitd.sock","--oci-worker-no-process-sandbox"],"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"securityContext":{"runAsUser":1000,"runAsGroup":1000,"allowPrivilegeEscalation":true,"appArmorProfile":{"type":"Unconfined"},"seccompProfile":{"type":"Unconfined"}}}],"steps":[{"name":"prepare-buildkit-client","image":"moby/buildkit:rootless","securityContext":{"runAsUser":0,"runAsGroup":0},"volumeMounts":[{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"}],"script":"#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"},{"name":"publish","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","securityContext":{"runAsUser":1000,"runAsGroup":1000},"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"service-work","mountPath":"/workspace/service-work"},{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"},{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -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.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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\" --build-cache-mode \"$(params.build-cache-mode)\"\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"service-id","value":"hwlab-gateway"},{"name":"base-image","value":"$(params.base-image)"},{"name":"build-cache-mode","value":"$(params.build-cache-mode)"}]},{"name":"build-hwlab-edge-proxy","runAfter":["plan-artifacts"],"workspaces":[{"name":"source","workspace":"source"}],"when":[{"input":"$(tasks.plan-artifacts.results.build-hwlab-edge-proxy)","operator":"in","values":["true"]}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"service-id"},{"name":"base-image"},{"name":"build-cache-mode"}],"results":[{"name":"status","description":"hwlab-edge-proxy status"},{"name":"service-id","description":"hwlab-edge-proxy service-id"},{"name":"image","description":"hwlab-edge-proxy image"},{"name":"image-tag","description":"hwlab-edge-proxy image-tag"},{"name":"digest","description":"hwlab-edge-proxy digest"},{"name":"repository-digest","description":"hwlab-edge-proxy repository-digest"},{"name":"source-commit-id","description":"hwlab-edge-proxy source-commit-id"},{"name":"component-input-hash","description":"hwlab-edge-proxy component-input-hash"},{"name":"environment-input-hash","description":"hwlab-edge-proxy environment-input-hash"},{"name":"code-input-hash","description":"hwlab-edge-proxy code-input-hash"},{"name":"runtime-mode","description":"hwlab-edge-proxy runtime-mode"},{"name":"boot-repo","description":"hwlab-edge-proxy boot-repo"},{"name":"boot-commit","description":"hwlab-edge-proxy boot-commit"},{"name":"boot-sh","description":"hwlab-edge-proxy boot-sh"},{"name":"build-created-at","description":"hwlab-edge-proxy build-created-at"},{"name":"build-backend","description":"hwlab-edge-proxy build-backend"},{"name":"reused-from","description":"hwlab-edge-proxy reused-from"}],"workspaces":[{"name":"source"}],"volumes":[{"name":"service-work","emptyDir":{}},{"name":"buildkit-bin","emptyDir":{}},{"name":"buildkit-run","emptyDir":{}}],"sidecars":[{"name":"buildkitd","image":"moby/buildkit:rootless","args":["--addr","unix:///workspace/buildkit-run/buildkitd.sock","--oci-worker-no-process-sandbox"],"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"securityContext":{"runAsUser":1000,"runAsGroup":1000,"allowPrivilegeEscalation":true,"appArmorProfile":{"type":"Unconfined"},"seccompProfile":{"type":"Unconfined"}}}],"steps":[{"name":"prepare-buildkit-client","image":"moby/buildkit:rootless","securityContext":{"runAsUser":0,"runAsGroup":0},"volumeMounts":[{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"}],"script":"#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"},{"name":"publish","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","securityContext":{"runAsUser":1000,"runAsGroup":1000},"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"service-work","mountPath":"/workspace/service-work"},{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"},{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -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.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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\" --build-cache-mode \"$(params.build-cache-mode)\"\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"service-id","value":"hwlab-edge-proxy"},{"name":"base-image","value":"$(params.base-image)"},{"name":"build-cache-mode","value":"$(params.build-cache-mode)"}]},{"name":"build-hwlab-agent-skills","runAfter":["plan-artifacts"],"workspaces":[{"name":"source","workspace":"source"}],"when":[{"input":"$(tasks.plan-artifacts.results.build-hwlab-agent-skills)","operator":"in","values":["true"]}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"service-id"},{"name":"base-image"},{"name":"build-cache-mode"}],"results":[{"name":"status","description":"hwlab-agent-skills status"},{"name":"service-id","description":"hwlab-agent-skills service-id"},{"name":"image","description":"hwlab-agent-skills image"},{"name":"image-tag","description":"hwlab-agent-skills image-tag"},{"name":"digest","description":"hwlab-agent-skills digest"},{"name":"repository-digest","description":"hwlab-agent-skills repository-digest"},{"name":"source-commit-id","description":"hwlab-agent-skills source-commit-id"},{"name":"component-input-hash","description":"hwlab-agent-skills component-input-hash"},{"name":"environment-input-hash","description":"hwlab-agent-skills environment-input-hash"},{"name":"code-input-hash","description":"hwlab-agent-skills code-input-hash"},{"name":"runtime-mode","description":"hwlab-agent-skills runtime-mode"},{"name":"boot-repo","description":"hwlab-agent-skills boot-repo"},{"name":"boot-commit","description":"hwlab-agent-skills boot-commit"},{"name":"boot-sh","description":"hwlab-agent-skills boot-sh"},{"name":"build-created-at","description":"hwlab-agent-skills build-created-at"},{"name":"build-backend","description":"hwlab-agent-skills build-backend"},{"name":"reused-from","description":"hwlab-agent-skills reused-from"}],"workspaces":[{"name":"source"}],"volumes":[{"name":"service-work","emptyDir":{}},{"name":"buildkit-bin","emptyDir":{}},{"name":"buildkit-run","emptyDir":{}}],"sidecars":[{"name":"buildkitd","image":"moby/buildkit:rootless","args":["--addr","unix:///workspace/buildkit-run/buildkitd.sock","--oci-worker-no-process-sandbox"],"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"securityContext":{"runAsUser":1000,"runAsGroup":1000,"allowPrivilegeEscalation":true,"appArmorProfile":{"type":"Unconfined"},"seccompProfile":{"type":"Unconfined"}}}],"steps":[{"name":"prepare-buildkit-client","image":"moby/buildkit:rootless","securityContext":{"runAsUser":0,"runAsGroup":0},"volumeMounts":[{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"}],"script":"#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"},{"name":"publish","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","securityContext":{"runAsUser":1000,"runAsGroup":1000},"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"service-work","mountPath":"/workspace/service-work"},{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"},{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -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.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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\" --build-cache-mode \"$(params.build-cache-mode)\"\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"service-id","value":"hwlab-agent-skills"},{"name":"base-image","value":"$(params.base-image)"},{"name":"build-cache-mode","value":"$(params.build-cache-mode)"}]},{"name":"collect-artifacts","runAfter":["build-hwlab-cloud-api","build-hwlab-workbench-runtime","build-hwlab-user-billing","build-hwlab-project-management","build-hwlab-cloud-web","build-hwlab-gateway","build-hwlab-edge-proxy","build-hwlab-agent-skills"],"workspaces":[{"name":"source","workspace":"source"}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"services"},{"name":"base-image"}],"workspaces":[{"name":"source"}],"steps":[{"name":"collect","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"script":"#!/bin/sh\nset -eu\necho '{\"event\":\"ci-base-image\",\"phase\":\"collect-artifacts\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor tool in node git; do command -v \"$tool\" >/dev/null 2>&1 || { echo '{\"event\":\"ci-base-image\",\"phase\":\"collect-artifacts\",\"ok\":false,\"reason\":\"missing-tool\",\"tool\":\"'\"$tool\"'\"}'; exit 31; }; done\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-collect\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nif [ -s /workspace/source/affected-services.json ] && node - /workspace/source/affected-services.json <<'NODE'\nconst fs = require(\"node:fs\");\nconst plan = JSON.parse(fs.readFileSync(process.argv[2], \"utf8\"));\nconst buildServices = Array.isArray(plan.buildServices) ? plan.buildServices : [];\nconst affectedServices = Array.isArray(plan.affectedServices) ? plan.affectedServices : [];\nconst willRunGitopsPromote = plan.ciCdPlan && plan.ciCdPlan.willRunGitopsPromote === true;\nprocess.exit(!willRunGitopsPromote && buildServices.length === 0 && affectedServices.length === 0 ? 0 : 1);\nNODE\nthen\n rm -f /workspace/source/dev-artifacts.json\n echo '{\"event\":\"collect-artifacts\",\"status\":\"skipped\",\"reason\":\"no-build-no-rollout-plan\"}'\n exit 0\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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 --ci-plan-path /workspace/source/affected-services.json\nnode scripts/refresh-artifact-catalog.mjs --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --image-tag-mode \"$(params.image-tag-mode)\" --target-ref HEAD --publish-report /workspace/source/dev-artifacts.json --no-write\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"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)"}]},{"name":"gitops-promote","runAfter":["collect-artifacts"],"workspaces":[{"name":"source","workspace":"source"},{"name":"git-ssh","workspace":"git-ssh"}],"taskSpec":{"params":[{"name":"git-url"},{"name":"git-read-url"},{"name":"git-write-url"},{"name":"source-branch"},{"name":"gitops-branch"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"runtime-path"},{"name":"revision"},{"name":"registry-prefix"}],"results":[{"name":"runtime-ready-required","description":"true when GitOps promotion changed runtime desired state and runtime readiness must be observed"}],"workspaces":[{"name":"source"},{"name":"git-ssh"}],"steps":[{"name":"promote","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"gitops-promote\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\necho '{\"event\":\"ci-base-image\",\"phase\":\"gitops-promote\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apt\"}'\nfor tool in node git timeout; 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\ngit_ssh_setup() {\n mkdir -p /root/.ssh\n cp /workspace/git-ssh/ssh-privatekey /root/.ssh/id_rsa\n chmod 600 /root/.ssh/id_rsa\n timeout 10 ssh-keyscan github.com >> /root/.ssh/known_hosts 2>/dev/null || true\n timeout 10 ssh-keyscan -p 443 ssh.github.com >> /root/.ssh/known_hosts 2>/dev/null || true\n write_github_proxy_command\n export GIT_SSH_COMMAND=\"ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o ServerAliveInterval=10 -o ServerAliveCountMax=2 -o ProxyCommand='node /tmp/hwlab-github-proxy-connect.mjs 127.0.0.1 10808 %h %p'\"\n git config --global url.\"ssh://git@ssh.github.com:443/\".insteadOf \"git@github.com:\"\n git config --global url.\"ssh://git@ssh.github.com:443/\".insteadOf \"ssh://git@github.com/\"\n}\n\nwrite_github_proxy_command() {\n cat > /tmp/hwlab-github-proxy-connect.mjs <<'NODE_PROXY'\n#!/usr/bin/env node\nimport net from \"node:net\";\n\nconst [proxyHost, proxyPortRaw, targetHost, targetPortRaw] = process.argv.slice(2);\nconst proxyPort = Number.parseInt(proxyPortRaw || \"\", 10);\nconst targetPort = Number.parseInt(targetPortRaw || \"\", 10);\nif (!proxyHost || !Number.isInteger(proxyPort) || !targetHost || !Number.isInteger(targetPort)) {\n console.error(\"usage: hwlab-github-proxy-connect \");\n process.exit(64);\n}\n\nconst socket = net.createConnection({ host: proxyHost, port: proxyPort });\nlet buffer = Buffer.alloc(0);\n\nsocket.setTimeout(10000, () => {\n console.error(\"proxy connect timeout \" + proxyHost + \":\" + proxyPort + \" -> \" + targetHost + \":\" + targetPort);\n socket.destroy();\n process.exit(65);\n});\n\nsocket.on(\"connect\", () => {\n socket.write(\"CONNECT \" + targetHost + \":\" + targetPort + \" HTTP/1.1\\r\\nHost: \" + targetHost + \":\" + targetPort + \"\\r\\nProxy-Connection: Keep-Alive\\r\\n\\r\\n\");\n});\n\nsocket.on(\"error\", (error) => {\n console.error(\"proxy connect failed: \" + error.message);\n process.exit(66);\n});\n\nfunction onData(chunk) {\n buffer = Buffer.concat([buffer, chunk]);\n const headerEnd = buffer.indexOf(\"\\r\\n\\r\\n\");\n if (headerEnd === -1 && buffer.length < 8192) return;\n const head = buffer.slice(0, headerEnd + 4).toString(\"latin1\");\n const statusLine = head.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number.parseInt(statusLine.split(\" \")[1] || \"\", 10);\n if (!statusLine.startsWith(\"HTTP/1.\") || !Number.isInteger(statusCode) || statusCode < 200 || statusCode > 299) {\n console.error(\"proxy CONNECT rejected: \" + (statusLine || \"missing-status\"));\n socket.destroy();\n process.exit(67);\n }\n socket.off(\"data\", onData);\n socket.setTimeout(0);\n const rest = buffer.slice(headerEnd + 4);\n if (rest.length) process.stdout.write(rest);\n process.stdin.pipe(socket);\n socket.pipe(process.stdout);\n}\n\nsocket.on(\"data\", onData);\nsocket.on(\"close\", () => process.exit(0));\nNODE_PROXY\n chmod 0700 /tmp/hwlab-github-proxy-connect.mjs\n}\n\ngit_now_ms() {\n node -e 'console.log(Date.now())' 2>/dev/null || date +%s000\n}\n\ngit_timed() {\n phase=\"$1\"\n timeout_seconds=\"$2\"\n shift 2\n started_ms=\"$(git_now_ms)\"\n echo '{\"event\":\"git-operation\",\"phase\":\"'\"$phase\"'\",\"status\":\"started\",\"timeoutSeconds\":'\"$timeout_seconds\"'}' >&2\n set +e\n timeout \"$timeout_seconds\" \"$@\"\n status=\"$?\"\n set -e\n finished_ms=\"$(git_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n if [ \"$status\" -eq 0 ]; then\n echo '{\"event\":\"git-operation\",\"phase\":\"'\"$phase\"'\",\"status\":\"succeeded\",\"durationMs\":'\"$duration_ms\"'}' >&2\n return 0\n fi\n if [ \"$status\" -eq 124 ] || [ \"$status\" -eq 137 ]; then\n echo '{\"event\":\"git-operation\",\"phase\":\"'\"$phase\"'\",\"status\":\"failed\",\"reason\":\"timeout\",\"durationMs\":'\"$duration_ms\"',\"timeoutSeconds\":'\"$timeout_seconds\"'}' >&2\n else\n echo '{\"event\":\"git-operation\",\"phase\":\"'\"$phase\"'\",\"status\":\"failed\",\"exitCode\":'\"$status\"',\"durationMs\":'\"$duration_ms\"'}' >&2\n fi\n return \"$status\"\n}\n\ngit_url_requires_ssh() { case \"$1\" in git@*|ssh://*) return 0 ;; *) return 1 ;; esac; }\nlane=\"$(params.lane)\"\ncase \"$lane\" in\n v[0-9][0-9]*) runtime_lane=true ;;\n *) runtime_lane=false ;;\nesac\nif [ \"$runtime_lane\" = \"true\" ]; then\n printf 'false' > /tekton/results/runtime-ready-required\nelse\n printf 'true' > /tekton/results/runtime-ready-required\nfi\nsource_head_url_for_setup=\"$(params.git-url)\"\ngitops_read_url_for_setup=\"$(params.git-url)\"\nif [ \"$runtime_lane\" = \"true\" ]; then\n source_head_url_for_setup=\"$(params.git-read-url)\"\n gitops_read_url_for_setup=\"$(params.git-read-url)\"\nfi\ngitops_write_url_for_setup=\"$(params.git-write-url)\"\nif git_url_requires_ssh \"$source_head_url_for_setup\" || git_url_requires_ssh \"$gitops_read_url_for_setup\" || git_url_requires_ssh \"$gitops_write_url_for_setup\"; then\n for tool in ssh ssh-keyscan; 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\n git_ssh_setup\nelse\n echo '{\"event\":\"git-ssh-setup\",\"phase\":\"gitops-promote\",\"status\":\"skipped\",\"reason\":\"non-ssh-git-url\"}'\nfi\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\n\ncheck_source_head() {\n phase=\"$1\"\n expected=\"${2:-$(params.revision)}\"\n source_head_url=\"$(params.git-url)\"\n if [ \"$runtime_lane\" = \"true\" ]; then\n source_head_url=\"$(params.git-read-url)\"\n fi\n latest_file=\"$(mktemp)\"\n if ! git_timed \"source-head-$phase\" 45 git ls-remote \"$source_head_url\" \"refs/heads/$(params.source-branch)\" > \"$latest_file\"; then\n echo '{\"status\":\"failed\",\"reason\":\"source-head-check-failed\",\"phase\":\"'\"$phase\"'\",\"sourceBranch\":\"'\"$(params.source-branch)\"'\",\"sourceRevision\":\"'\"$(params.revision)\"'\"}'\n exit 1\n fi\n latest=\"$(cut -f1 \"$latest_file\" | head -n 1)\"\n if [ -z \"$latest\" ]; then\n echo '{\"status\":\"failed\",\"reason\":\"source-head-unresolved\",\"phase\":\"'\"$phase\"'\",\"sourceBranch\":\"'\"$(params.source-branch)\"'\",\"sourceRevision\":\"'\"$(params.revision)\"'\"}'\n exit 1\n fi\n if [ \"$latest\" != \"$expected\" ]; then\n printf 'false' > /tekton/results/runtime-ready-required || true\n echo '{\"status\":\"skipped-stale-source\",\"verdict\":\"superseded\",\"phase\":\"'\"$phase\"'\",\"sourceBranch\":\"'\"$(params.source-branch)\"'\",\"sourceRevision\":\"'\"$(params.revision)\"'\",\"expectedRevision\":\"'\"$expected\"'\",\"latestRevision\":\"'\"$latest\"'\"}'\n exit 0\n fi\n}\n\ncheck_source_head before-render\nif [ -s /workspace/source/affected-services.json ] && node - /workspace/source/affected-services.json <<'NODE'\nconst fs = require(\"node:fs\");\nconst plan = JSON.parse(fs.readFileSync(process.argv[2], \"utf8\"));\nconst buildServices = Array.isArray(plan.buildServices) ? plan.buildServices : [];\nconst affectedServices = Array.isArray(plan.affectedServices) ? plan.affectedServices : [];\nconst willRunGitopsPromote = plan.ciCdPlan && plan.ciCdPlan.willRunGitopsPromote === true;\nprocess.exit(!willRunGitopsPromote && buildServices.length === 0 && affectedServices.length === 0 ? 0 : 1);\nNODE\nthen\n printf 'false' > /tekton/results/runtime-ready-required || true\n echo '{\"event\":\"gitops-promote\",\"status\":\"skipped\",\"reason\":\"no-build-no-rollout-plan\"}'\n exit 0\nfi\ngit config --global user.name \"HWLAB node GitOps Bot\"\ngit config --global user.email \"hwlab-node-gitops-bot@users.noreply.github.com\"\ncatalog_path=\"$(params.catalog-path)\"\nruntime_path=\"$(params.runtime-path)\"\ngitops_root_from_runtime_path() {\n path=\"$1\"\n case \"$path\" in\n */*) printf '%s\n' \"${path%/*}\" ;;\n *) printf '.\n' ;;\n esac\n}\ngitops_root=\"$(gitops_root_from_runtime_path \"$runtime_path\")\"\n\nargo_hard_refresh_runtime_lane() {\n if [ \"$runtime_lane\" != \"true\" ]; then return 0; fi\n ARGO_APPLICATION=\"hwlab-node-$lane\" ARGO_FIELD_MANAGER=\"hwlab-$lane-gitops-promote\" node <<'NODE'\nconst fs = require(\"node:fs\");\nconst https = require(\"node:https\");\n\nconst host = process.env.KUBERNETES_SERVICE_HOST;\nconst port = process.env.KUBERNETES_SERVICE_PORT || \"443\";\nconst startedAt = Date.now();\nconst application = process.env.ARGO_APPLICATION || \"hwlab-node-v02\";\nconst fieldManager = process.env.ARGO_FIELD_MANAGER || \"hwlab-v02-gitops-promote\";\nconst pipelineRun = process.env.HWLAB_TEKTON_PIPELINERUN || null;\nconst taskRun = process.env.HWLAB_TEKTON_TASKRUN || null;\nconst task = process.env.HWLAB_TEKTON_TASK || null;\nconst revision = process.env.HWLAB_SOURCE_REVISION || null;\n\nfunction emit(payload) {\n console.log(JSON.stringify({\n event: \"node-cicd-timing\",\n schemaVersion: \"v1\",\n stage: \"argo-hard-refresh\",\n pipelineRun,\n taskRun,\n task,\n revision,\n application,\n source: \"scripts/gitops-render.mjs\",\n at: new Date().toISOString(),\n ...payload\n }));\n}\n\nfunction safeJson(text) {\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n\nfunction request(method, path, body, contentType = \"application/merge-patch+json\") {\n const token = fs.readFileSync(\"/var/run/secrets/kubernetes.io/serviceaccount/token\", \"utf8\");\n const ca = fs.readFileSync(\"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\");\n const payload = body ? JSON.stringify(body) : \"\";\n const headers = { Authorization: \"Bearer \" + token };\n if (payload) {\n headers[\"Content-Type\"] = contentType;\n headers[\"Content-Length\"] = Buffer.byteLength(payload);\n }\n return new Promise((resolve, reject) => {\n const req = https.request({ host, port, method, path, ca, headers }, (res) => {\n let data = \"\";\n res.setEncoding(\"utf8\");\n res.on(\"data\", (chunk) => { data += chunk; });\n res.on(\"end\", () => resolve({ statusCode: res.statusCode || 0, body: data, json: safeJson(data) }));\n });\n req.on(\"error\", reject);\n if (payload) req.write(payload);\n req.end();\n });\n}\n\n(async () => {\n if (!host) {\n emit({ status: \"skipped\", reason: \"kubernetes-service-host-missing\", durationMs: Date.now() - startedAt });\n return;\n }\n const response = await request(\n \"PATCH\",\n \"/apis/argoproj.io/v1alpha1/namespaces/argocd/applications/\" + encodeURIComponent(application) + \"?fieldManager=\" + encodeURIComponent(fieldManager),\n { metadata: { annotations: { \"argocd.argoproj.io/refresh\": \"hard\" } } }\n );\n if (response.statusCode >= 200 && response.statusCode < 300) {\n emit({ status: \"succeeded\", durationMs: Date.now() - startedAt, statusCode: response.statusCode });\n return;\n }\n emit({ status: \"degraded\", reason: \"argo-refresh-patch-failed\", durationMs: Date.now() - startedAt, statusCode: response.statusCode, body: response.body.slice(0, 1000) });\n})().catch((error) => {\n emit({ status: \"degraded\", reason: \"argo-refresh-patch-error\", durationMs: Date.now() - startedAt, error: error.message });\n});\nNODE\n}\n\nif [ -s /workspace/source/dev-artifacts.json ]; then\n node scripts/refresh-artifact-catalog.mjs --lane \"$lane\" --catalog-path \"$catalog_path\" --deploy-config deploy/deploy.yaml --image-tag-mode \"$(params.image-tag-mode)\" --target-ref \"$(params.revision)\" --publish-report /workspace/source/dev-artifacts.json --write\nfi\ngitops_render_started_ms=\"$(ci_now_ms)\"\nnode scripts/run-bun.mjs scripts/gitops-render.mjs --lane \"$lane\" --catalog-path \"$catalog_path\" --image-tag-mode \"$(params.image-tag-mode)\" --source-revision \"$(params.revision)\" --source-repo \"$(params.git-url)\" --source-branch \"$(params.source-branch)\" --gitops-branch \"$(params.gitops-branch)\" --gitops-root \"$gitops_root\" --out \"$gitops_root\" --registry-prefix \"$(params.registry-prefix)\" --use-deploy-images\nnode scripts/run-bun.mjs scripts/gitops-render.mjs --lane \"$lane\" --catalog-path \"$catalog_path\" --image-tag-mode \"$(params.image-tag-mode)\" --source-revision \"$(params.revision)\" --source-repo \"$(params.git-url)\" --source-branch \"$(params.source-branch)\" --gitops-branch \"$(params.gitops-branch)\" --gitops-root \"$gitops_root\" --out \"$gitops_root\" --registry-prefix \"$(params.registry-prefix)\" --use-deploy-images --check\nci_timing_emit gitops-render succeeded \"$gitops_render_started_ms\"\ncheck_source_head before-push\nworkdir=\"$(mktemp -d)\"\ngitops_read_url=\"$(params.git-url)\"\ngitops_write_url=\"$(params.git-write-url)\"\nif [ \"$runtime_lane\" = \"true\" ]; then\n gitops_read_url=\"$(params.git-read-url)\"\nfi\ngitops_clone_started_ms=\"$(ci_now_ms)\"\ngit_timed gitops-clone 180 git clone --no-checkout \"$gitops_read_url\" \"$workdir/gitops\"\ncd \"$workdir/gitops\"\ngit remote set-url origin \"$gitops_write_url\"\nold_runtime_snapshot=\"$workdir/old-runtime-snapshot\"\nif git_timed gitops-branch-ls 45 git ls-remote --exit-code --heads origin \"$(params.gitops-branch)\" >/dev/null; then\n git_timed gitops-fetch 90 git fetch origin \"$(params.gitops-branch)\"\n git checkout -B \"$(params.gitops-branch)\" \"origin/$(params.gitops-branch)\"\n if [ \"$runtime_lane\" = \"true\" ] && [ -d \"$runtime_path\" ]; then\n mkdir -p \"$old_runtime_snapshot\"\n cp -a \"$runtime_path\"/. \"$old_runtime_snapshot\"/\n fi\n if [ \"$runtime_lane\" = \"true\" ]; then rm -rf \"$runtime_path\" \"$catalog_path\"; else rm -rf deploy/gitops/node \"$catalog_path\"; fi\nelse\n git checkout --orphan \"$(params.gitops-branch)\"\n git rm -rf . >/dev/null 2>&1 || true\nfi\nci_timing_emit gitops-clone succeeded \"$gitops_clone_started_ms\"\nmkdir -p \"$(dirname \"$runtime_path\")\" \"$(dirname \"$catalog_path\")\"\nif [ \"$runtime_lane\" = \"true\" ]; then\n cp -a \"/workspace/source/repo/$runtime_path\" \"$runtime_path\"\n cp \"/workspace/source/repo/$catalog_path\" \"$catalog_path\"\n git add \"$catalog_path\" \"$runtime_path\"\nelse\n mkdir -p deploy/gitops\n cp -a /workspace/source/repo/deploy/gitops/node deploy/gitops/node\n cp \"/workspace/source/repo/$catalog_path\" \"$catalog_path\"\n git add \"$catalog_path\" deploy/gitops/node\nfi\nif [ \"$runtime_lane\" = \"true\" ] && [ -s /workspace/source/affected-services.json ]; then\n runtime_noop_decision=\"$(node - \"$runtime_path\" \"$old_runtime_snapshot\" /workspace/source/affected-services.json <<'NODE'\nconst fs = require(\"node:fs\");\nconst path = require(\"node:path\");\n\nconst [runtimePath, oldRuntimePath, planPath] = process.argv.slice(2);\n\nfunction readJson(filePath) {\n return JSON.parse(fs.readFileSync(filePath, \"utf8\"));\n}\n\nfunction stable(value) {\n if (Array.isArray(value)) return \"[\" + value.map(stable).join(\",\") + \"]\";\n if (value && typeof value === \"object\") {\n return \"{\" + Object.keys(value).sort().map((key) => JSON.stringify(key) + \":\" + stable(value[key])).join(\",\") + \"}\";\n }\n return JSON.stringify(value);\n}\n\nfunction scrub(value) {\n if (Array.isArray(value)) return value.map(scrub);\n if (!value || typeof value !== \"object\") return value;\n const result = {};\n for (const [key, child] of Object.entries(value)) {\n if (key === \"hwlab.pikastech.local/source-commit\" ||\n key === \"hwlab.pikastech.local/artifact-source-commit\" ||\n key === \"hwlab.pikastech.local/boot-commit\" ||\n key === \"sourceCommitId\" ||\n key === \"bootCommit\") {\n result[key] = \"\";\n continue;\n }\n const envName = String(value.name || \"\");\n if (key === \"value\" && /^HWLAB_.*COMMIT/.test(envName)) {\n result[key] = \"\";\n continue;\n }\n result[key] = scrub(child);\n }\n return result;\n}\n\nfunction listFiles(rootDir) {\n if (!rootDir || !fs.existsSync(rootDir)) return null;\n const files = [];\n function walk(current) {\n for (const entry of fs.readdirSync(current, { withFileTypes: true })) {\n const fullPath = path.join(current, entry.name);\n if (entry.isDirectory()) walk(fullPath);\n else if (entry.isFile()) files.push(path.relative(rootDir, fullPath).replaceAll(path.sep, \"/\"));\n }\n }\n walk(rootDir);\n return files.sort();\n}\n\nfunction normalizedTree(rootDir) {\n const files = listFiles(rootDir);\n if (!files) return null;\n const entries = {};\n for (const relativePath of files) {\n const filePath = path.join(rootDir, relativePath);\n const text = fs.readFileSync(filePath, \"utf8\");\n try {\n entries[relativePath] = stable(scrub(JSON.parse(text)));\n } catch {\n entries[relativePath] = text.replace(/[a-f0-9]{40}/gu, \"\");\n }\n }\n return stable(entries);\n}\n\nconst plan = readJson(planPath);\nconst buildServices = Array.isArray(plan.buildServices) ? plan.buildServices : [];\nconst rolloutServices = Array.isArray(plan.rolloutServices) ? plan.rolloutServices : [];\nif (buildServices.length > 0 || rolloutServices.length > 0) {\n process.stdout.write(\"runtime-required\");\n process.exit(0);\n}\n\nconst oldTree = normalizedTree(oldRuntimePath);\nconst newTree = normalizedTree(runtimePath);\nprocess.stdout.write(oldTree && newTree && oldTree === newTree ? \"runtime-identity-only\" : \"runtime-required\");\nNODE\n)\"\n if [ \"$runtime_noop_decision\" = \"runtime-identity-only\" ]; then\n printf 'false' > /tekton/results/runtime-ready-required\n echo '{\"status\":\"skipped-runtime-unchanged\",\"reason\":\"runtime-identity-only\",\"gitopsBranch\":\"'\"$(params.gitops-branch)\"'\",\"sourceRevision\":\"'\"$(params.revision)\"'\"}'\n ci_timing_emit gitops-commit skipped \"$(ci_now_ms)\"\n ci_timing_emit gitops-push skipped \"$(ci_now_ms)\"\n exit 0\n fi\nfi\nif git diff --cached --quiet; then\n printf 'false' > /tekton/results/runtime-ready-required\n echo '{\"status\":\"unchanged\",\"gitopsBranch\":\"'\"$(params.gitops-branch)\"'\",\"sourceRevision\":\"'\"$(params.revision)\"'\"}'\n ci_timing_emit gitops-commit unchanged \"$(ci_now_ms)\"\n ci_timing_emit gitops-push unchanged \"$(ci_now_ms)\"\n exit 0\nfi\nshort=\"$(printf '%.7s' \"$(params.revision)\")\"\ngitops_commit_started_ms=\"$(ci_now_ms)\"\ngit commit -m \"chore: promote node GitOps source $short\"\nci_timing_emit gitops-commit succeeded \"$gitops_commit_started_ms\"\ngitops_push_started_ms=\"$(ci_now_ms)\"\ngit_timed gitops-push 120 git push origin \"HEAD:$(params.gitops-branch)\"\nci_timing_emit gitops-push succeeded \"$gitops_push_started_ms\"\nif [ \"$runtime_lane\" = \"true\" ]; then\n printf 'false' > /tekton/results/runtime-ready-required\n echo '{\"event\":\"runtime-ready\",\"phase\":\"gitops-promote\",\"status\":\"delegated-post-flush-closeout\",\"gitopsBranch\":\"'\"$(params.gitops-branch)\"'\",\"sourceRevision\":\"'\"$(params.revision)\"'\"}'\nfi\nargo_hard_refresh_runtime_lane\necho '{\"status\":\"pushed\",\"gitopsBranch\":\"'\"$(params.gitops-branch)\"'\",\"sourceRevision\":\"'\"$(params.revision)\"'\",\"gitopsWriteUrl\":\"'\"$gitops_write_url\"'\"}'\n"}]},"params":[{"name":"git-url","value":"$(params.git-url)"},{"name":"git-read-url","value":"$(params.git-read-url)"},{"name":"git-write-url","value":"$(params.git-write-url)"},{"name":"source-branch","value":"$(params.source-branch)"},{"name":"gitops-branch","value":"$(params.gitops-branch)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"runtime-path","value":"$(params.runtime-path)"},{"name":"revision","value":"$(params.revision)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"}]}]}} +{"apiVersion":"tekton.dev/v1","kind":"Pipeline","metadata":{"name":"hwlab-nc01-v03-ci-image-publish","namespace":"hwlab-ci","labels":{"app.kubernetes.io/part-of":"hwlab","hwlab.pikastech.local/gitops-target":"v03"},"annotations":{"hwlab.pikastech.local/source-config":"scripts/gitops-render.mjs#tekton-native-primitive-ci","hwlab.pikastech.local/ci-contract":"tekton-native-primitive-tasks","hwlab.pikastech.local/policy":"native-per-service-taskrun-image-publish"}},"spec":{"params":[{"name":"git-url","type":"string","default":"git@github.com:pikasTech/HWLAB.git"},{"name":"git-read-url","type":"string","default":"http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git"},{"name":"git-write-url","type":"string","default":"http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/HWLAB.git"},{"name":"source-branch","type":"string","default":"v0.3"},{"name":"gitops-branch","type":"string","default":"v0.3-gitops"},{"name":"lane","type":"string","default":"v03"},{"name":"catalog-path","type":"string","default":"deploy/artifact-catalog.v03.json"},{"name":"image-tag-mode","type":"string","default":"full"},{"name":"runtime-path","type":"string","default":"deploy/gitops/node/runtime-v03"},{"name":"revision","type":"string","description":"Full git commit SHA; the only source truth for image tags."},{"name":"registry-prefix","type":"string","default":"127.0.0.1:5000/hwlab"},{"name":"services","type":"string","default":"hwlab-cloud-api,hwlab-workbench-runtime,hwlab-user-billing,hwlab-project-management,hwlab-cloud-web,hwlab-gateway,hwlab-edge-proxy,hwlab-agent-skills"},{"name":"base-image","type":"string","default":"127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim"},{"name":"build-cache-mode","type":"string","default":"registry"},{"name":"runtime-ready-timeout-ms","type":"string","default":"60000"}],"workspaces":[{"name":"source"},{"name":"git-ssh"}],"tasks":[{"name":"prepare-source","workspaces":[{"name":"source","workspace":"source"},{"name":"git-ssh","workspace":"git-ssh"}],"taskSpec":{"params":[{"name":"git-url"},{"name":"git-read-url"},{"name":"source-branch"},{"name":"gitops-branch"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"services"},{"name":"revision"}],"workspaces":[{"name":"source"},{"name":"git-ssh"}],"steps":[{"name":"prepare-source","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"prepare-source\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\necho '{\"event\":\"ci-base-image\",\"phase\":\"prepare-source\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor tool in node git timeout; do command -v \"$tool\" >/dev/null 2>&1 || { echo '{\"event\":\"ci-base-image\",\"ok\":false,\"reason\":\"missing-tool\",\"tool\":\"'\"$tool\"'\"}'; exit 31; }; done\nnode -e 'console.log(JSON.stringify({event:\"ci-base-image\",phase:\"prepare-source\",ok:true,node:process.version}))'\ngit_ssh_setup() {\n mkdir -p /root/.ssh\n cp /workspace/git-ssh/ssh-privatekey /root/.ssh/id_rsa\n chmod 600 /root/.ssh/id_rsa\n timeout 10 ssh-keyscan github.com >> /root/.ssh/known_hosts 2>/dev/null || true\n timeout 10 ssh-keyscan -p 443 ssh.github.com >> /root/.ssh/known_hosts 2>/dev/null || true\n write_github_proxy_command\n export GIT_SSH_COMMAND=\"ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o ServerAliveInterval=10 -o ServerAliveCountMax=2 -o ProxyCommand='node /tmp/hwlab-github-proxy-connect.mjs 127.0.0.1 10808 %h %p'\"\n git config --global url.\"ssh://git@ssh.github.com:443/\".insteadOf \"git@github.com:\"\n git config --global url.\"ssh://git@ssh.github.com:443/\".insteadOf \"ssh://git@github.com/\"\n}\n\nwrite_github_proxy_command() {\n cat > /tmp/hwlab-github-proxy-connect.mjs <<'NODE_PROXY'\n#!/usr/bin/env node\nimport net from \"node:net\";\n\nconst [proxyHost, proxyPortRaw, targetHost, targetPortRaw] = process.argv.slice(2);\nconst proxyPort = Number.parseInt(proxyPortRaw || \"\", 10);\nconst targetPort = Number.parseInt(targetPortRaw || \"\", 10);\nif (!proxyHost || !Number.isInteger(proxyPort) || !targetHost || !Number.isInteger(targetPort)) {\n console.error(\"usage: hwlab-github-proxy-connect \");\n process.exit(64);\n}\n\nconst socket = net.createConnection({ host: proxyHost, port: proxyPort });\nlet buffer = Buffer.alloc(0);\n\nsocket.setTimeout(10000, () => {\n console.error(\"proxy connect timeout \" + proxyHost + \":\" + proxyPort + \" -> \" + targetHost + \":\" + targetPort);\n socket.destroy();\n process.exit(65);\n});\n\nsocket.on(\"connect\", () => {\n socket.write(\"CONNECT \" + targetHost + \":\" + targetPort + \" HTTP/1.1\\r\\nHost: \" + targetHost + \":\" + targetPort + \"\\r\\nProxy-Connection: Keep-Alive\\r\\n\\r\\n\");\n});\n\nsocket.on(\"error\", (error) => {\n console.error(\"proxy connect failed: \" + error.message);\n process.exit(66);\n});\n\nfunction onData(chunk) {\n buffer = Buffer.concat([buffer, chunk]);\n const headerEnd = buffer.indexOf(\"\\r\\n\\r\\n\");\n if (headerEnd === -1 && buffer.length < 8192) return;\n const head = buffer.slice(0, headerEnd + 4).toString(\"latin1\");\n const statusLine = head.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number.parseInt(statusLine.split(\" \")[1] || \"\", 10);\n if (!statusLine.startsWith(\"HTTP/1.\") || !Number.isInteger(statusCode) || statusCode < 200 || statusCode > 299) {\n console.error(\"proxy CONNECT rejected: \" + (statusLine || \"missing-status\"));\n socket.destroy();\n process.exit(67);\n }\n socket.off(\"data\", onData);\n socket.setTimeout(0);\n const rest = buffer.slice(headerEnd + 4);\n if (rest.length) process.stdout.write(rest);\n process.stdin.pipe(socket);\n socket.pipe(process.stdout);\n}\n\nsocket.on(\"data\", onData);\nsocket.on(\"close\", () => process.exit(0));\nNODE_PROXY\n chmod 0700 /tmp/hwlab-github-proxy-connect.mjs\n}\n\ngit_now_ms() {\n node -e 'console.log(Date.now())' 2>/dev/null || date +%s000\n}\n\ngit_timed() {\n phase=\"$1\"\n timeout_seconds=\"$2\"\n shift 2\n started_ms=\"$(git_now_ms)\"\n echo '{\"event\":\"git-operation\",\"phase\":\"'\"$phase\"'\",\"status\":\"started\",\"timeoutSeconds\":'\"$timeout_seconds\"'}' >&2\n set +e\n timeout \"$timeout_seconds\" \"$@\"\n status=\"$?\"\n set -e\n finished_ms=\"$(git_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n if [ \"$status\" -eq 0 ]; then\n echo '{\"event\":\"git-operation\",\"phase\":\"'\"$phase\"'\",\"status\":\"succeeded\",\"durationMs\":'\"$duration_ms\"'}' >&2\n return 0\n fi\n if [ \"$status\" -eq 124 ] || [ \"$status\" -eq 137 ]; then\n echo '{\"event\":\"git-operation\",\"phase\":\"'\"$phase\"'\",\"status\":\"failed\",\"reason\":\"timeout\",\"durationMs\":'\"$duration_ms\"',\"timeoutSeconds\":'\"$timeout_seconds\"'}' >&2\n else\n echo '{\"event\":\"git-operation\",\"phase\":\"'\"$phase\"'\",\"status\":\"failed\",\"exitCode\":'\"$status\"',\"durationMs\":'\"$duration_ms\"'}' >&2\n fi\n return \"$status\"\n}\n\ngit_url_requires_ssh() { case \"$1\" in git@*|ssh://*) return 0 ;; *) return 1 ;; esac; }\ngit_read_url=\"$(params.git-read-url)\"\nif git_url_requires_ssh \"$git_read_url\"; then\n for tool in ssh ssh-keyscan; do command -v \"$tool\" >/dev/null 2>&1 || { echo '{\"event\":\"ci-base-image\",\"ok\":false,\"reason\":\"missing-tool\",\"tool\":\"'\"$tool\"'\"}'; exit 31; }; done\n git_ssh_setup\nelse\n echo '{\"event\":\"git-ssh-setup\",\"phase\":\"prepare-source\",\"status\":\"skipped\",\"reason\":\"non-ssh-git-read-url\"}'\nfi\nrm -rf /workspace/source/repo\nsource_clone_started_ms=\"$(ci_now_ms)\"\ngit_timed source-clone 180 git clone --branch \"$(params.source-branch)\" \"$git_read_url\" /workspace/source/repo\ncd /workspace/source/repo\ngit config --global --add safe.directory /workspace/source/repo\ngit remote set-url origin \"$git_read_url\"\ngit checkout \"$(params.revision)\"\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\ngit merge-base --is-ancestor \"$(params.revision)\" \"origin/$(params.source-branch)\" || { echo '{\"event\":\"source-ancestry\",\"status\":\"failed\",\"revision\":\"'\"$(params.revision)\"'\",\"sourceBranch\":\"'\"$(params.source-branch)\"'\"}'; exit 32; }\nci_timing_emit source-clone succeeded \"$source_clone_started_ms\"\nprepare_source_dependencies_started_ms=\"$(ci_now_ms)\"\necho '{\"event\":\"prepare-source-dependencies\",\"status\":\"skipped\",\"reason\":\"renderer-dependency-install-disabled\",\"dependency\":\"yaml\"}'\nci_timing_emit prepare-source-dependencies succeeded \"$prepare_source_dependencies_started_ms\"\ncatalog_path=\"$(params.catalog-path)\"\nmkdir -p \"$(dirname \"$catalog_path\")\"\ncatalog_fetch_started_ms=\"$(ci_now_ms)\"\nif git_timed catalog-ls-remote 45 git ls-remote --exit-code --heads \"$git_read_url\" \"$(params.gitops-branch)\" >/dev/null; then\n git remote set-url gitops-catalog \"$git_read_url\" 2>/dev/null || git remote add gitops-catalog \"$git_read_url\"\n git_timed catalog-fetch 90 git fetch --depth 1 gitops-catalog \"$(params.gitops-branch)\" >/dev/null || true\n if git cat-file -e FETCH_HEAD:\"$catalog_path\" 2>/dev/null; then\n git show FETCH_HEAD:\"$catalog_path\" > /tmp/hwlab-gitops-artifact-catalog.json\n if node --input-type=module - /tmp/hwlab-gitops-artifact-catalog.json deploy/deploy.yaml \"$(params.services)\" <<'NODE'\nimport fs from \"node:fs\";\nconst [catalogPath, deployPath, selectedServices] = process.argv.slice(2);\nconst ids = (doc) => (doc.services || []).map((service) => service.serviceId).filter(Boolean);\nconst topLevelServiceIds = (text) => {\n const values = [];\n let inServices = false;\n for (const line of text.split(/\\r?\\n/u)) {\n if (/^services:\\s*$/u.test(line)) { inServices = true; continue; }\n if (!inServices) continue;\n if (/^\\S/u.test(line)) break;\n const match = line.match(/^\\s*-\\s+serviceId:\\s*['\"]?([^'\"#\\s]+)['\"]?/u);\n if (match) values.push(match[1]);\n }\n return values;\n};\nconst selected = (selectedServices || \"\").split(\",\").map((item) => item.trim()).filter(Boolean);\nconst uniqueSorted = (items) => [...new Set(items)].sort();\nconst gitops = JSON.parse(fs.readFileSync(catalogPath, \"utf8\"));\nconst expected = selected.length ? selected : topLevelServiceIds(fs.readFileSync(deployPath, \"utf8\"));\nconst actual = ids(gitops);\nconst expectedSet = uniqueSorted(expected);\nconst actualSet = uniqueSorted(actual);\nconst sameServices = expectedSet.length === actualSet.length && expectedSet.every((item, index) => item === actualSet[index]);\nif (!sameServices) {\n const missing = expectedSet.filter((item) => !actualSet.includes(item));\n const extra = actualSet.filter((item) => !expectedSet.includes(item));\n console.error(JSON.stringify({ event: \"gitops-artifact-catalog\", phase: \"prepare-source\", status: \"ignored-stale\", reason: \"service-ids-mismatch\", expected, actual, missing, extra }));\n process.exit(42);\n}\nNODE\n then\n cp /tmp/hwlab-gitops-artifact-catalog.json \"$catalog_path\"\n echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"loaded\",\"branch\":\"'\"$(params.gitops-branch)\"'\"}'\n else\n echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"ignored-stale\",\"reason\":\"service-ids-mismatch\",\"branch\":\"'\"$(params.gitops-branch)\"'\"}'\n fi\n else\n echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"seed\",\"reason\":\"missing-on-gitops-branch\"}'\n fi\nelse\n echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"seed\",\"reason\":\"gitops-branch-missing\"}'\nfi\nif [ ! -s \"$catalog_path\" ] && [ \"$(params.lane)\" = \"v02\" ]; then\n node --input-type=module - \"$catalog_path\" \"$(params.revision)\" \"$(params.registry-prefix)\" \"$(params.image-tag-mode)\" \"$(params.services)\" <<'NODE'\nimport fs from 'node:fs';\nimport path from 'node:path';\nconst [catalogPath, revision, registryPrefix, imageTagMode, selectedServices] = process.argv.slice(2);\nconst topLevelServiceIds = (text) => {\n const values = [];\n let inServices = false;\n for (const line of text.split(/\\r?\\n/u)) {\n if (/^services:\\s*$/u.test(line)) { inServices = true; continue; }\n if (!inServices) continue;\n if (/^\\S/u.test(line)) break;\n const match = line.match(/^\\s*-\\s+serviceId:\\s*['\"]?([^'\"#\\s]+)['\"]?/u);\n if (match) values.push(match[1]);\n }\n return values;\n};\nconst tag = imageTagMode === 'full' ? revision : revision.slice(0, 7);\nconst namespace = 'hwlab-v02';\nconst selected = (selectedServices || '').split(',').filter(Boolean);\nconst serviceIds = selected.length ? selected : topLevelServiceIds(fs.readFileSync('deploy/deploy.yaml', 'utf8'));\nconst services = serviceIds.map((serviceId) => {\n const service = { serviceId, profile: 'v02', namespace, commitId: tag, sourceCommitId: revision, image: `${registryPrefix}/${serviceId}:${tag}`, imageTag: tag, digest: null, buildBackend: 'contract-skeleton' };\n return service;\n});\nfs.mkdirSync(path.dirname(catalogPath), { recursive: true });\nfs.writeFileSync(catalogPath, JSON.stringify({ catalogVersion: 'v1', kind: 'hwlab-artifact-catalog', environment: 'v02', profile: 'v02', namespace, endpoint: \"https://hwlab.74-48-78-17.nip.io\", commitId: tag, artifactState: 'contract-skeleton', publish: { registryPrefix, sourceCommitId: revision, imageTag: tag, publishedAt: null }, allowedProfiles: ['v02'], forbiddenProfiles: ['dev', 'prod'], services }, null, 2) + '\\n');\nNODE\n echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"seeded-v02-skeleton\"}'\nfi\nci_timing_emit catalog-fetch succeeded \"$catalog_fetch_started_ms\"\necho 'node prepare-source complete; validation task count=3'\n"}]},"params":[{"name":"git-url","value":"$(params.git-url)"},{"name":"git-read-url","value":"$(params.git-read-url)"},{"name":"source-branch","value":"$(params.source-branch)"},{"name":"gitops-branch","value":"$(params.gitops-branch)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"services","value":"$(params.services)"},{"name":"revision","value":"$(params.revision)"}]},{"name":"repo-reports-guard","runAfter":["prepare-source"],"workspaces":[{"name":"source","workspace":"source"}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"source-branch"},{"name":"gitops-branch"}],"workspaces":[{"name":"source"}],"steps":[{"name":"repo-reports-guard","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"script":"#!/bin/sh\nset -eu\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nnode scripts/repo-reports-guard.mjs\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"source-branch","value":"$(params.source-branch)"},{"name":"gitops-branch","value":"$(params.gitops-branch)"}]},{"name":"node-contract-check","runAfter":["prepare-source"],"workspaces":[{"name":"source","workspace":"source"}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"source-branch"},{"name":"gitops-branch"}],"workspaces":[{"name":"source"}],"steps":[{"name":"node-contract-check","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"script":"#!/bin/sh\nset -eu\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nnode --check scripts/gitops-render.mjs\nnode --input-type=module <<'NODE'\nimport { readdirSync, readFileSync, statSync } from \"node:fs\";\nimport path from \"node:path\";\n\nconst root = process.cwd();\nconst ciFileName = \"CI\" + \".json\";\nconst forbiddenCiFragments = [\n \"ci\" + \"-json\",\n \"HWLAB_NODE_TEKTON\" + \"_SINGLE_IMAGE_PUBLISH\",\n \"single\" + \"-dind\",\n \"docker\" + \":29\",\n \"DOCKER\" + \"_HOST\",\n \"Docker\" + \"-in-Docker\",\n \"D\" + \"IND\",\n \"docker\" + \" push\",\n \"--build\" + \"-backend\",\n \"HWLAB_ARTIFACT\" + \"_BUILD_BACKEND\",\n \"--legacy\" + \"-source-images\",\n \"HWLAB_NODE_USE_DEPLOY_IMAGES\" + \"=0\"\n];\nconst scanTargets = [\n \"scripts/gitops-render.mjs\",\n \"scripts/src/runtime-lane.ts\",\n \"scripts/artifact-publish.mjs\",\n \"scripts/src/ci-plan-lib.mjs\",\n \"deploy/gitops/node/tekton\"\n];\nconst skipDirs = new Set([\".git\", \"node_modules\", \".worktree\"]);\n\nfunction walkFiles(relativePath) {\n const absolutePath = path.join(root, relativePath);\n let stat;\n try {\n stat = statSync(absolutePath);\n } catch {\n return [];\n }\n if (stat.isFile()) return [relativePath];\n if (!stat.isDirectory()) return [];\n const files = [];\n for (const entry of readdirSync(absolutePath, { withFileTypes: true })) {\n if (entry.isDirectory() && skipDirs.has(entry.name)) continue;\n files.push(...walkFiles(path.join(relativePath, entry.name)));\n }\n return files;\n}\n\nconst ciFiles = walkFiles(\".\").filter((filePath) => path.basename(filePath) === ciFileName);\nconst violations = [];\nfor (const filePath of scanTargets.flatMap(walkFiles)) {\n const text = readFileSync(path.join(root, filePath), \"utf8\");\n for (const fragment of forbiddenCiFragments) {\n if (text.includes(fragment)) violations.push({ filePath, fragment });\n }\n}\nif (ciFiles.length || violations.length) {\n console.error(JSON.stringify({ status: \"failed\", ciFiles, violations }, null, 2));\n process.exit(1);\n}\nNODE\nnode --check scripts/artifact-publish.mjs\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"source-branch","value":"$(params.source-branch)"},{"name":"gitops-branch","value":"$(params.gitops-branch)"}]},{"name":"codex-api-forwarder-check","runAfter":["prepare-source"],"workspaces":[{"name":"source","workspace":"source"}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"source-branch"},{"name":"gitops-branch"}],"workspaces":[{"name":"source"}],"steps":[{"name":"codex-api-forwarder-check","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"script":"#!/bin/sh\nset -eu\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nnode scripts/run-bun.mjs test cmd/hwlab-codex-api-responses-forwarder/main.test.ts\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"source-branch","value":"$(params.source-branch)"},{"name":"gitops-branch","value":"$(params.gitops-branch)"}]},{"name":"plan-artifacts","runAfter":["repo-reports-guard","node-contract-check","codex-api-forwarder-check"],"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"},{"name":"services"}],"results":[{"name":"affected-hwlab-cloud-api","description":"hwlab-cloud-api rollout affected according to ci-plan"},{"name":"build-hwlab-cloud-api","description":"hwlab-cloud-api image build required according to ci-plan"},{"name":"affected-hwlab-workbench-runtime","description":"hwlab-workbench-runtime rollout affected according to ci-plan"},{"name":"build-hwlab-workbench-runtime","description":"hwlab-workbench-runtime image build required according to ci-plan"},{"name":"affected-hwlab-user-billing","description":"hwlab-user-billing rollout affected according to ci-plan"},{"name":"build-hwlab-user-billing","description":"hwlab-user-billing image build required according to ci-plan"},{"name":"affected-hwlab-project-management","description":"hwlab-project-management rollout affected according to ci-plan"},{"name":"build-hwlab-project-management","description":"hwlab-project-management image build required according to ci-plan"},{"name":"affected-hwlab-cloud-web","description":"hwlab-cloud-web rollout affected according to ci-plan"},{"name":"build-hwlab-cloud-web","description":"hwlab-cloud-web image build required according to ci-plan"},{"name":"affected-hwlab-gateway","description":"hwlab-gateway rollout affected according to ci-plan"},{"name":"build-hwlab-gateway","description":"hwlab-gateway image build required according to ci-plan"},{"name":"affected-hwlab-edge-proxy","description":"hwlab-edge-proxy rollout affected according to ci-plan"},{"name":"build-hwlab-edge-proxy","description":"hwlab-edge-proxy image build required according to ci-plan"},{"name":"affected-hwlab-agent-skills","description":"hwlab-agent-skills rollout affected according to ci-plan"},{"name":"build-hwlab-agent-skills","description":"hwlab-agent-skills image build required according to ci-plan"}],"workspaces":[{"name":"source"}],"steps":[{"name":"plan","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","env":[{"name":"HWLAB_SELECTED_SERVICES","value":"$(params.services)"},{"name":"HWLAB_DEV_REGISTRY_K8S_NATIVE","value":"1"},{"name":"HWLAB_TEKTON_PIPELINERUN","value":"$(context.pipelineRun.name)"},{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"script":"#!/bin/sh\nset -eu\necho '{\"event\":\"ci-base-image\",\"phase\":\"plan-artifacts\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\ncd /workspace/source/repo\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps/yaml\" ] && [ ! -e node_modules/yaml ]; then\n mkdir -p node_modules\n ln -s \"$ci_node_deps/yaml\" node_modules/yaml\nfi\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nHWLAB_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\nnode 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\nnode - <<'NODE'\nconst fs = require(\"node:fs\");\nconst plan = JSON.parse(fs.readFileSync(\"/workspace/source/ci-plan.json\", \"utf8\"));\nconst selected = String(process.env.HWLAB_SELECTED_SERVICES || \"\").split(\",\").map((item) => item.trim()).filter(Boolean);\nconst allServices = selected.length > 0 ? selected : [\"hwlab-cloud-api\",\"hwlab-cloud-web\",\"hwlab-gateway\",\"hwlab-edge-proxy\",\"hwlab-agent-skills\"];\nconst affected = new Set(plan.affectedServices || []);\nconst plannedBuildServices = Array.isArray(plan.buildServices) ? new Set(plan.buildServices) : null;\nconst selectedSet = new Set(selected);\nconst byService = new Map((plan.services || []).map((service) => [service.serviceId, service]));\nconst entries = allServices.map((serviceId) => {\n const service = byService.get(serviceId) || {};\n const serviceSelected = selectedSet.has(serviceId);\n const rolloutAffected = serviceSelected && affected.has(serviceId);\n const envReuse = service.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.envReuse === true;\n const buildRequired = serviceSelected && (plannedBuildServices ? plannedBuildServices.has(serviceId) : (envReuse ? service.envChanged === true : rolloutAffected));\n return {\n serviceId,\n selected: serviceSelected,\n affected: rolloutAffected,\n buildRequired,\n rolloutAffected,\n runtimeMode: service.runtimeMode || \"service-image\",\n envChanged: service.envChanged ?? null,\n codeChanged: service.codeChanged ?? null\n };\n});\nfor (const entry of entries) {\n fs.writeFileSync(\"/tekton/results/affected-\" + entry.serviceId, entry.affected ? \"true\" : \"false\");\n fs.writeFileSync(\"/tekton/results/build-\" + entry.serviceId, entry.buildRequired ? \"true\" : \"false\");\n}\nfs.writeFileSync(\"/workspace/source/affected-services.json\", JSON.stringify({\n sourceCommitId: plan.sourceCommitId,\n affectedServices: plan.affectedServices || [],\n rolloutServices: plan.rolloutServices || plan.affectedServices || [],\n buildServices: plan.buildServices || entries.filter((entry) => entry.buildRequired).map((entry) => entry.serviceId),\n rolloutWithoutImageBuildServices: plan.rolloutWithoutImageBuildServices || [],\n reusedServices: plan.reusedServices || [],\n serviceReusedCount: plan.serviceReusedCount || 0,\n imageBuildSkippedServices: plan.imageBuildSkippedServices || [],\n buildSkippedCount: plan.buildSkippedCount || 0,\n artifactCatalog: plan.artifactCatalog || null,\n envArtifactGroups: plan.envArtifactGroups || [],\n changedPathSummary: plan.changedPathSummary || null,\n ciCdPlan: plan.ciCdPlan || null,\n services: plan.services || [],\n entries\n}, null, 2) + String.fromCharCode(10));\nconsole.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 || [] }));\nNODE\n"}]},"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)"},{"name":"services","value":"$(params.services)"}]},{"name":"build-hwlab-cloud-api","runAfter":["plan-artifacts"],"workspaces":[{"name":"source","workspace":"source"}],"when":[{"input":"$(tasks.plan-artifacts.results.build-hwlab-cloud-api)","operator":"in","values":["true"]}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"service-id"},{"name":"base-image"},{"name":"build-cache-mode"}],"results":[{"name":"status","description":"hwlab-cloud-api status"},{"name":"service-id","description":"hwlab-cloud-api service-id"},{"name":"image","description":"hwlab-cloud-api image"},{"name":"image-tag","description":"hwlab-cloud-api image-tag"},{"name":"digest","description":"hwlab-cloud-api digest"},{"name":"repository-digest","description":"hwlab-cloud-api repository-digest"},{"name":"source-commit-id","description":"hwlab-cloud-api source-commit-id"},{"name":"component-input-hash","description":"hwlab-cloud-api component-input-hash"},{"name":"environment-input-hash","description":"hwlab-cloud-api environment-input-hash"},{"name":"code-input-hash","description":"hwlab-cloud-api code-input-hash"},{"name":"runtime-mode","description":"hwlab-cloud-api runtime-mode"},{"name":"boot-repo","description":"hwlab-cloud-api boot-repo"},{"name":"boot-commit","description":"hwlab-cloud-api boot-commit"},{"name":"boot-sh","description":"hwlab-cloud-api boot-sh"},{"name":"build-created-at","description":"hwlab-cloud-api build-created-at"},{"name":"build-backend","description":"hwlab-cloud-api build-backend"},{"name":"reused-from","description":"hwlab-cloud-api reused-from"}],"workspaces":[{"name":"source"}],"volumes":[{"name":"service-work","emptyDir":{}},{"name":"buildkit-bin","emptyDir":{}},{"name":"buildkit-run","emptyDir":{}}],"sidecars":[{"name":"buildkitd","image":"moby/buildkit:rootless","args":["--addr","unix:///workspace/buildkit-run/buildkitd.sock","--oci-worker-no-process-sandbox"],"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"securityContext":{"runAsUser":1000,"runAsGroup":1000,"allowPrivilegeEscalation":true,"appArmorProfile":{"type":"Unconfined"},"seccompProfile":{"type":"Unconfined"}}}],"steps":[{"name":"prepare-buildkit-client","image":"moby/buildkit:rootless","securityContext":{"runAsUser":0,"runAsGroup":0},"volumeMounts":[{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"}],"script":"#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"},{"name":"publish","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","securityContext":{"runAsUser":1000,"runAsGroup":1000},"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"service-work","mountPath":"/workspace/service-work"},{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"},{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -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.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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\" --build-cache-mode \"$(params.build-cache-mode)\"\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"service-id","value":"hwlab-cloud-api"},{"name":"base-image","value":"$(params.base-image)"},{"name":"build-cache-mode","value":"$(params.build-cache-mode)"}]},{"name":"build-hwlab-workbench-runtime","runAfter":["plan-artifacts"],"workspaces":[{"name":"source","workspace":"source"}],"when":[{"input":"$(tasks.plan-artifacts.results.build-hwlab-workbench-runtime)","operator":"in","values":["true"]}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"service-id"},{"name":"base-image"},{"name":"build-cache-mode"}],"results":[{"name":"status","description":"hwlab-workbench-runtime status"},{"name":"service-id","description":"hwlab-workbench-runtime service-id"},{"name":"image","description":"hwlab-workbench-runtime image"},{"name":"image-tag","description":"hwlab-workbench-runtime image-tag"},{"name":"digest","description":"hwlab-workbench-runtime digest"},{"name":"repository-digest","description":"hwlab-workbench-runtime repository-digest"},{"name":"source-commit-id","description":"hwlab-workbench-runtime source-commit-id"},{"name":"component-input-hash","description":"hwlab-workbench-runtime component-input-hash"},{"name":"environment-input-hash","description":"hwlab-workbench-runtime environment-input-hash"},{"name":"code-input-hash","description":"hwlab-workbench-runtime code-input-hash"},{"name":"runtime-mode","description":"hwlab-workbench-runtime runtime-mode"},{"name":"boot-repo","description":"hwlab-workbench-runtime boot-repo"},{"name":"boot-commit","description":"hwlab-workbench-runtime boot-commit"},{"name":"boot-sh","description":"hwlab-workbench-runtime boot-sh"},{"name":"build-created-at","description":"hwlab-workbench-runtime build-created-at"},{"name":"build-backend","description":"hwlab-workbench-runtime build-backend"},{"name":"reused-from","description":"hwlab-workbench-runtime reused-from"}],"workspaces":[{"name":"source"}],"volumes":[{"name":"service-work","emptyDir":{}},{"name":"buildkit-bin","emptyDir":{}},{"name":"buildkit-run","emptyDir":{}}],"sidecars":[{"name":"buildkitd","image":"moby/buildkit:rootless","args":["--addr","unix:///workspace/buildkit-run/buildkitd.sock","--oci-worker-no-process-sandbox"],"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"securityContext":{"runAsUser":1000,"runAsGroup":1000,"allowPrivilegeEscalation":true,"appArmorProfile":{"type":"Unconfined"},"seccompProfile":{"type":"Unconfined"}}}],"steps":[{"name":"prepare-buildkit-client","image":"moby/buildkit:rootless","securityContext":{"runAsUser":0,"runAsGroup":0},"volumeMounts":[{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"}],"script":"#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"},{"name":"publish","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","securityContext":{"runAsUser":1000,"runAsGroup":1000},"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"service-work","mountPath":"/workspace/service-work"},{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"},{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -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.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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\" --build-cache-mode \"$(params.build-cache-mode)\"\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"service-id","value":"hwlab-workbench-runtime"},{"name":"base-image","value":"$(params.base-image)"},{"name":"build-cache-mode","value":"$(params.build-cache-mode)"}]},{"name":"build-hwlab-user-billing","runAfter":["plan-artifacts"],"workspaces":[{"name":"source","workspace":"source"}],"when":[{"input":"$(tasks.plan-artifacts.results.build-hwlab-user-billing)","operator":"in","values":["true"]}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"service-id"},{"name":"base-image"},{"name":"build-cache-mode"}],"results":[{"name":"status","description":"hwlab-user-billing status"},{"name":"service-id","description":"hwlab-user-billing service-id"},{"name":"image","description":"hwlab-user-billing image"},{"name":"image-tag","description":"hwlab-user-billing image-tag"},{"name":"digest","description":"hwlab-user-billing digest"},{"name":"repository-digest","description":"hwlab-user-billing repository-digest"},{"name":"source-commit-id","description":"hwlab-user-billing source-commit-id"},{"name":"component-input-hash","description":"hwlab-user-billing component-input-hash"},{"name":"environment-input-hash","description":"hwlab-user-billing environment-input-hash"},{"name":"code-input-hash","description":"hwlab-user-billing code-input-hash"},{"name":"runtime-mode","description":"hwlab-user-billing runtime-mode"},{"name":"boot-repo","description":"hwlab-user-billing boot-repo"},{"name":"boot-commit","description":"hwlab-user-billing boot-commit"},{"name":"boot-sh","description":"hwlab-user-billing boot-sh"},{"name":"build-created-at","description":"hwlab-user-billing build-created-at"},{"name":"build-backend","description":"hwlab-user-billing build-backend"},{"name":"reused-from","description":"hwlab-user-billing reused-from"}],"workspaces":[{"name":"source"}],"volumes":[{"name":"service-work","emptyDir":{}},{"name":"buildkit-bin","emptyDir":{}},{"name":"buildkit-run","emptyDir":{}}],"sidecars":[{"name":"buildkitd","image":"moby/buildkit:rootless","args":["--addr","unix:///workspace/buildkit-run/buildkitd.sock","--oci-worker-no-process-sandbox"],"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"securityContext":{"runAsUser":1000,"runAsGroup":1000,"allowPrivilegeEscalation":true,"appArmorProfile":{"type":"Unconfined"},"seccompProfile":{"type":"Unconfined"}}}],"steps":[{"name":"prepare-buildkit-client","image":"moby/buildkit:rootless","securityContext":{"runAsUser":0,"runAsGroup":0},"volumeMounts":[{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"}],"script":"#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"},{"name":"publish","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","securityContext":{"runAsUser":1000,"runAsGroup":1000},"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"service-work","mountPath":"/workspace/service-work"},{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"},{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -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.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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\" --build-cache-mode \"$(params.build-cache-mode)\"\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"service-id","value":"hwlab-user-billing"},{"name":"base-image","value":"$(params.base-image)"},{"name":"build-cache-mode","value":"$(params.build-cache-mode)"}]},{"name":"build-hwlab-project-management","runAfter":["plan-artifacts"],"workspaces":[{"name":"source","workspace":"source"}],"when":[{"input":"$(tasks.plan-artifacts.results.build-hwlab-project-management)","operator":"in","values":["true"]}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"service-id"},{"name":"base-image"},{"name":"build-cache-mode"}],"results":[{"name":"status","description":"hwlab-project-management status"},{"name":"service-id","description":"hwlab-project-management service-id"},{"name":"image","description":"hwlab-project-management image"},{"name":"image-tag","description":"hwlab-project-management image-tag"},{"name":"digest","description":"hwlab-project-management digest"},{"name":"repository-digest","description":"hwlab-project-management repository-digest"},{"name":"source-commit-id","description":"hwlab-project-management source-commit-id"},{"name":"component-input-hash","description":"hwlab-project-management component-input-hash"},{"name":"environment-input-hash","description":"hwlab-project-management environment-input-hash"},{"name":"code-input-hash","description":"hwlab-project-management code-input-hash"},{"name":"runtime-mode","description":"hwlab-project-management runtime-mode"},{"name":"boot-repo","description":"hwlab-project-management boot-repo"},{"name":"boot-commit","description":"hwlab-project-management boot-commit"},{"name":"boot-sh","description":"hwlab-project-management boot-sh"},{"name":"build-created-at","description":"hwlab-project-management build-created-at"},{"name":"build-backend","description":"hwlab-project-management build-backend"},{"name":"reused-from","description":"hwlab-project-management reused-from"}],"workspaces":[{"name":"source"}],"volumes":[{"name":"service-work","emptyDir":{}},{"name":"buildkit-bin","emptyDir":{}},{"name":"buildkit-run","emptyDir":{}}],"sidecars":[{"name":"buildkitd","image":"moby/buildkit:rootless","args":["--addr","unix:///workspace/buildkit-run/buildkitd.sock","--oci-worker-no-process-sandbox"],"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"securityContext":{"runAsUser":1000,"runAsGroup":1000,"allowPrivilegeEscalation":true,"appArmorProfile":{"type":"Unconfined"},"seccompProfile":{"type":"Unconfined"}}}],"steps":[{"name":"prepare-buildkit-client","image":"moby/buildkit:rootless","securityContext":{"runAsUser":0,"runAsGroup":0},"volumeMounts":[{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"}],"script":"#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"},{"name":"publish","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","securityContext":{"runAsUser":1000,"runAsGroup":1000},"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"service-work","mountPath":"/workspace/service-work"},{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"},{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -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.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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\" --build-cache-mode \"$(params.build-cache-mode)\"\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"service-id","value":"hwlab-project-management"},{"name":"base-image","value":"$(params.base-image)"},{"name":"build-cache-mode","value":"$(params.build-cache-mode)"}]},{"name":"build-hwlab-cloud-web","runAfter":["plan-artifacts"],"workspaces":[{"name":"source","workspace":"source"}],"when":[{"input":"$(tasks.plan-artifacts.results.build-hwlab-cloud-web)","operator":"in","values":["true"]}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"service-id"},{"name":"base-image"},{"name":"build-cache-mode"}],"results":[{"name":"status","description":"hwlab-cloud-web status"},{"name":"service-id","description":"hwlab-cloud-web service-id"},{"name":"image","description":"hwlab-cloud-web image"},{"name":"image-tag","description":"hwlab-cloud-web image-tag"},{"name":"digest","description":"hwlab-cloud-web digest"},{"name":"repository-digest","description":"hwlab-cloud-web repository-digest"},{"name":"source-commit-id","description":"hwlab-cloud-web source-commit-id"},{"name":"component-input-hash","description":"hwlab-cloud-web component-input-hash"},{"name":"environment-input-hash","description":"hwlab-cloud-web environment-input-hash"},{"name":"code-input-hash","description":"hwlab-cloud-web code-input-hash"},{"name":"runtime-mode","description":"hwlab-cloud-web runtime-mode"},{"name":"boot-repo","description":"hwlab-cloud-web boot-repo"},{"name":"boot-commit","description":"hwlab-cloud-web boot-commit"},{"name":"boot-sh","description":"hwlab-cloud-web boot-sh"},{"name":"build-created-at","description":"hwlab-cloud-web build-created-at"},{"name":"build-backend","description":"hwlab-cloud-web build-backend"},{"name":"reused-from","description":"hwlab-cloud-web reused-from"}],"workspaces":[{"name":"source"}],"volumes":[{"name":"service-work","emptyDir":{}},{"name":"buildkit-bin","emptyDir":{}},{"name":"buildkit-run","emptyDir":{}}],"sidecars":[{"name":"buildkitd","image":"moby/buildkit:rootless","args":["--addr","unix:///workspace/buildkit-run/buildkitd.sock","--oci-worker-no-process-sandbox"],"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"securityContext":{"runAsUser":1000,"runAsGroup":1000,"allowPrivilegeEscalation":true,"appArmorProfile":{"type":"Unconfined"},"seccompProfile":{"type":"Unconfined"}}}],"steps":[{"name":"prepare-buildkit-client","image":"moby/buildkit:rootless","securityContext":{"runAsUser":0,"runAsGroup":0},"volumeMounts":[{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"}],"script":"#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"},{"name":"publish","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","securityContext":{"runAsUser":1000,"runAsGroup":1000},"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"service-work","mountPath":"/workspace/service-work"},{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"},{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -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.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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\" --build-cache-mode \"$(params.build-cache-mode)\"\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"service-id","value":"hwlab-cloud-web"},{"name":"base-image","value":"$(params.base-image)"},{"name":"build-cache-mode","value":"$(params.build-cache-mode)"}]},{"name":"build-hwlab-gateway","runAfter":["plan-artifacts"],"workspaces":[{"name":"source","workspace":"source"}],"when":[{"input":"$(tasks.plan-artifacts.results.build-hwlab-gateway)","operator":"in","values":["true"]}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"service-id"},{"name":"base-image"},{"name":"build-cache-mode"}],"results":[{"name":"status","description":"hwlab-gateway status"},{"name":"service-id","description":"hwlab-gateway service-id"},{"name":"image","description":"hwlab-gateway image"},{"name":"image-tag","description":"hwlab-gateway image-tag"},{"name":"digest","description":"hwlab-gateway digest"},{"name":"repository-digest","description":"hwlab-gateway repository-digest"},{"name":"source-commit-id","description":"hwlab-gateway source-commit-id"},{"name":"component-input-hash","description":"hwlab-gateway component-input-hash"},{"name":"environment-input-hash","description":"hwlab-gateway environment-input-hash"},{"name":"code-input-hash","description":"hwlab-gateway code-input-hash"},{"name":"runtime-mode","description":"hwlab-gateway runtime-mode"},{"name":"boot-repo","description":"hwlab-gateway boot-repo"},{"name":"boot-commit","description":"hwlab-gateway boot-commit"},{"name":"boot-sh","description":"hwlab-gateway boot-sh"},{"name":"build-created-at","description":"hwlab-gateway build-created-at"},{"name":"build-backend","description":"hwlab-gateway build-backend"},{"name":"reused-from","description":"hwlab-gateway reused-from"}],"workspaces":[{"name":"source"}],"volumes":[{"name":"service-work","emptyDir":{}},{"name":"buildkit-bin","emptyDir":{}},{"name":"buildkit-run","emptyDir":{}}],"sidecars":[{"name":"buildkitd","image":"moby/buildkit:rootless","args":["--addr","unix:///workspace/buildkit-run/buildkitd.sock","--oci-worker-no-process-sandbox"],"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"securityContext":{"runAsUser":1000,"runAsGroup":1000,"allowPrivilegeEscalation":true,"appArmorProfile":{"type":"Unconfined"},"seccompProfile":{"type":"Unconfined"}}}],"steps":[{"name":"prepare-buildkit-client","image":"moby/buildkit:rootless","securityContext":{"runAsUser":0,"runAsGroup":0},"volumeMounts":[{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"}],"script":"#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"},{"name":"publish","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","securityContext":{"runAsUser":1000,"runAsGroup":1000},"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"service-work","mountPath":"/workspace/service-work"},{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"},{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -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.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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\" --build-cache-mode \"$(params.build-cache-mode)\"\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"service-id","value":"hwlab-gateway"},{"name":"base-image","value":"$(params.base-image)"},{"name":"build-cache-mode","value":"$(params.build-cache-mode)"}]},{"name":"build-hwlab-edge-proxy","runAfter":["plan-artifacts"],"workspaces":[{"name":"source","workspace":"source"}],"when":[{"input":"$(tasks.plan-artifacts.results.build-hwlab-edge-proxy)","operator":"in","values":["true"]}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"service-id"},{"name":"base-image"},{"name":"build-cache-mode"}],"results":[{"name":"status","description":"hwlab-edge-proxy status"},{"name":"service-id","description":"hwlab-edge-proxy service-id"},{"name":"image","description":"hwlab-edge-proxy image"},{"name":"image-tag","description":"hwlab-edge-proxy image-tag"},{"name":"digest","description":"hwlab-edge-proxy digest"},{"name":"repository-digest","description":"hwlab-edge-proxy repository-digest"},{"name":"source-commit-id","description":"hwlab-edge-proxy source-commit-id"},{"name":"component-input-hash","description":"hwlab-edge-proxy component-input-hash"},{"name":"environment-input-hash","description":"hwlab-edge-proxy environment-input-hash"},{"name":"code-input-hash","description":"hwlab-edge-proxy code-input-hash"},{"name":"runtime-mode","description":"hwlab-edge-proxy runtime-mode"},{"name":"boot-repo","description":"hwlab-edge-proxy boot-repo"},{"name":"boot-commit","description":"hwlab-edge-proxy boot-commit"},{"name":"boot-sh","description":"hwlab-edge-proxy boot-sh"},{"name":"build-created-at","description":"hwlab-edge-proxy build-created-at"},{"name":"build-backend","description":"hwlab-edge-proxy build-backend"},{"name":"reused-from","description":"hwlab-edge-proxy reused-from"}],"workspaces":[{"name":"source"}],"volumes":[{"name":"service-work","emptyDir":{}},{"name":"buildkit-bin","emptyDir":{}},{"name":"buildkit-run","emptyDir":{}}],"sidecars":[{"name":"buildkitd","image":"moby/buildkit:rootless","args":["--addr","unix:///workspace/buildkit-run/buildkitd.sock","--oci-worker-no-process-sandbox"],"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"securityContext":{"runAsUser":1000,"runAsGroup":1000,"allowPrivilegeEscalation":true,"appArmorProfile":{"type":"Unconfined"},"seccompProfile":{"type":"Unconfined"}}}],"steps":[{"name":"prepare-buildkit-client","image":"moby/buildkit:rootless","securityContext":{"runAsUser":0,"runAsGroup":0},"volumeMounts":[{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"}],"script":"#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"},{"name":"publish","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","securityContext":{"runAsUser":1000,"runAsGroup":1000},"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"service-work","mountPath":"/workspace/service-work"},{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"},{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -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.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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\" --build-cache-mode \"$(params.build-cache-mode)\"\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"service-id","value":"hwlab-edge-proxy"},{"name":"base-image","value":"$(params.base-image)"},{"name":"build-cache-mode","value":"$(params.build-cache-mode)"}]},{"name":"build-hwlab-agent-skills","runAfter":["plan-artifacts"],"workspaces":[{"name":"source","workspace":"source"}],"when":[{"input":"$(tasks.plan-artifacts.results.build-hwlab-agent-skills)","operator":"in","values":["true"]}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"service-id"},{"name":"base-image"},{"name":"build-cache-mode"}],"results":[{"name":"status","description":"hwlab-agent-skills status"},{"name":"service-id","description":"hwlab-agent-skills service-id"},{"name":"image","description":"hwlab-agent-skills image"},{"name":"image-tag","description":"hwlab-agent-skills image-tag"},{"name":"digest","description":"hwlab-agent-skills digest"},{"name":"repository-digest","description":"hwlab-agent-skills repository-digest"},{"name":"source-commit-id","description":"hwlab-agent-skills source-commit-id"},{"name":"component-input-hash","description":"hwlab-agent-skills component-input-hash"},{"name":"environment-input-hash","description":"hwlab-agent-skills environment-input-hash"},{"name":"code-input-hash","description":"hwlab-agent-skills code-input-hash"},{"name":"runtime-mode","description":"hwlab-agent-skills runtime-mode"},{"name":"boot-repo","description":"hwlab-agent-skills boot-repo"},{"name":"boot-commit","description":"hwlab-agent-skills boot-commit"},{"name":"boot-sh","description":"hwlab-agent-skills boot-sh"},{"name":"build-created-at","description":"hwlab-agent-skills build-created-at"},{"name":"build-backend","description":"hwlab-agent-skills build-backend"},{"name":"reused-from","description":"hwlab-agent-skills reused-from"}],"workspaces":[{"name":"source"}],"volumes":[{"name":"service-work","emptyDir":{}},{"name":"buildkit-bin","emptyDir":{}},{"name":"buildkit-run","emptyDir":{}}],"sidecars":[{"name":"buildkitd","image":"moby/buildkit:rootless","args":["--addr","unix:///workspace/buildkit-run/buildkitd.sock","--oci-worker-no-process-sandbox"],"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"securityContext":{"runAsUser":1000,"runAsGroup":1000,"allowPrivilegeEscalation":true,"appArmorProfile":{"type":"Unconfined"},"seccompProfile":{"type":"Unconfined"}}}],"steps":[{"name":"prepare-buildkit-client","image":"moby/buildkit:rootless","securityContext":{"runAsUser":0,"runAsGroup":0},"volumeMounts":[{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"}],"script":"#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"},{"name":"publish","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","securityContext":{"runAsUser":1000,"runAsGroup":1000},"env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"volumeMounts":[{"name":"service-work","mountPath":"/workspace/service-work"},{"name":"buildkit-bin","mountPath":"/workspace/buildkit-bin"},{"name":"buildkit-run","mountPath":"/workspace/buildkit-run"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"build-$(params.service-id)\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$(params.service-id)\"\nHWLAB_PROXY_PROBE_PHASE='service-image-publish-proxy-preflight' HWLAB_PROXY_PROBE_URL='http://deb.debian.org/debian/dists/bookworm/InRelease' node <<'NODE' || echo '{\"event\":\"dependency-proxy-probe-nonblocking\",\"phase\":\"service-image-publish-proxy-preflight\",\"ok\":false,\"reason\":\"probe-failed-but-continuing\"}'\nconst net = require(\"node:net\");\n\nconst phase = process.env.HWLAB_PROXY_PROBE_PHASE || \"unknown\";\nconst target = new URL(process.env.HWLAB_PROXY_PROBE_URL || \"http://deb.debian.org/debian/dists/bookworm/InRelease\");\nconst proxyRaw = process.env.HTTP_PROXY || process.env.http_proxy || \"\";\n\nfunction redactProxy(value) {\n if (!value) return \"\";\n try {\n const parsed = new URL(value);\n if (parsed.username || parsed.password) {\n parsed.username = \"***\";\n parsed.password = \"\";\n }\n return parsed.toString();\n } catch {\n return \"\";\n }\n}\n\nfunction emit(payload, exitCode = 0) {\n console.log(JSON.stringify({\n event: \"dependency-proxy-probe\",\n phase,\n target: target.href,\n proxy: redactProxy(proxyRaw),\n ...payload\n }));\n if (exitCode) process.exit(exitCode);\n}\n\nif (!proxyRaw) emit({ ok: false, reason: \"proxy-env-missing\" }, 21);\n\nlet proxy;\ntry {\n proxy = new URL(proxyRaw);\n} catch (error) {\n emit({ ok: false, reason: \"proxy-url-invalid\", error: error.message }, 22);\n}\n\nif (proxy.protocol !== \"http:\" && proxy.protocol !== \"https:\") {\n emit({ ok: false, reason: \"http-proxy-required\", protocol: proxy.protocol }, 23);\n}\n\nconst startedAt = Date.now();\nconst socket = net.createConnection({ host: proxy.hostname, port: Number(proxy.port || (proxy.protocol === \"https:\" ? 443 : 80)) });\nlet bytes = 0;\nlet firstByteMs = null;\nlet responseHead = \"\";\nlet finished = false;\n\nconst timeout = setTimeout(() => {\n if (finished) return;\n finished = true;\n socket.destroy();\n emit({ ok: false, reason: \"timeout\", timeoutMs: 15000 }, 24);\n}, 15000);\n\nsocket.on(\"connect\", () => {\n socket.write(\"GET \" + target.href + \" HTTP/1.1\\r\\nHost: \" + target.host + \"\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n\");\n});\n\nsocket.on(\"data\", (chunk) => {\n if (firstByteMs === null) firstByteMs = Date.now() - startedAt;\n bytes += chunk.length;\n if (responseHead.length < 240) responseHead += chunk.toString(\"utf8\", 0, Math.min(chunk.length, 240 - responseHead.length));\n});\n\nsocket.on(\"end\", () => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n const totalMs = Date.now() - startedAt;\n const statusLine = responseHead.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number(statusLine.split(\" \")[1] || 0);\n const ok = statusLine.startsWith(\"HTTP/\") && statusCode >= 200 && statusCode < 400;\n emit({\n ok,\n reason: ok ? null : \"bad-http-status\",\n statusLine,\n statusCode,\n firstByteMs,\n totalMs,\n bytes,\n speedBytesPerSecond: totalMs > 0 ? Math.round(bytes * 1000 / totalMs) : 0\n }, ok ? 0 : 25);\n});\n\nsocket.on(\"error\", (error) => {\n if (finished) return;\n finished = true;\n clearTimeout(timeout);\n emit({ ok: false, reason: \"proxy-connect-failed\", error: error.message, totalMs: Date.now() - startedAt }, 26);\n});\nNODE\necho '{\"event\":\"ci-base-image\",\"phase\":\"service-image-publish\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor 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\nmkdir -p /workspace/service-work/home\nexport HOME=/workspace/service-work/home\ngit config --global --add safe.directory /workspace/source/repo\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nmkdir -p /workspace/source/service-results\nif [ -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.buildRequired ? 0 : 1);' \"$(params.service-id)\"; then\n node - \"$(params.service-id)\" <<'NODE'\nconst fs = require(\"node:fs\");\nconst serviceId = process.argv[2];\nconst catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PATH, \"utf8\"));\nconst plan = fs.existsSync(\"/workspace/source/affected-services.json\") ? JSON.parse(fs.readFileSync(\"/workspace/source/affected-services.json\", \"utf8\")) : {};\nconst service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};\nconst planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};\nconst envReuse = planned.runtimeMode === \"env-reuse-git-mirror-checkout\" || service.runtimeMode === \"env-reuse-git-mirror-checkout\" || planned.envReuse === true || service.envReuse === true;\nconst image = envReuse ? (service.environmentImage || service.image || \"\") : (service.image || \"\");\nconst digest = envReuse ? (service.environmentDigest || service.digest || \"not_published\") : (service.digest || \"not_published\");\nconst imageTag = service.imageTag || (image.includes(\":\") ? image.slice(image.lastIndexOf(\":\") + 1) : \"\");\nconst repository = image.includes(\":\") ? image.slice(0, image.lastIndexOf(\":\")) : image;\nconst revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;\nconst componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || \"\") : (service.componentInputHash || \"\");\nconst environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || \"\") : (service.environmentInputHash || \"\");\nconst codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || \"\") : (service.codeInputHash || \"\");\nconst values = {\n \"service-id\": serviceId,\n status: /^sha256:[a-f0-9]{64}$/.test(digest) ? \"reused\" : \"blocked_reuse_unavailable\",\n image,\n \"image-tag\": imageTag,\n digest,\n \"repository-digest\": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + \"@\" + digest : \"\",\n \"source-commit-id\": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),\n \"component-input-hash\": componentInputHash,\n \"environment-input-hash\": environmentInputHash,\n \"code-input-hash\": codeInputHash,\n \"runtime-mode\": envReuse ? \"env-reuse-git-mirror-checkout\" : \"service-image\",\n \"boot-repo\": planned.bootRepo || service.bootRepo || \"\",\n \"boot-commit\": envReuse ? revision : (service.bootCommit || \"\"),\n \"boot-sh\": planned.bootSh || service.bootSh || \"\",\n \"build-created-at\": service.buildCreatedAt || \"\",\n \"build-backend\": envReuse ? \"reused-env-catalog\" : \"reused-catalog\",\n \"reused-from\": (envReuse ? service.environmentInputHash : service.componentInputHash) || service.commitId || imageTag || \"catalog\"\n};\nfor (const [name, value] of Object.entries(values)) fs.writeFileSync(\"/tekton/results/\" + name, String(value || \"\"));\nNODE\n echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"reason\":\"component-inputs-unchanged\"}'\n exit 0\nfi\nrm -rf /workspace/service-work/repo\nmkdir -p /workspace/service-work/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/repo -xo -f -\ncd /workspace/service-work/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$(params.service-id)\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nexport HWLAB_NODE_CICD_TIMING=1\nexport HWLAB_TEKTON_PIPELINERUN\nexport HWLAB_TEKTON_TASKRUN\nexport HWLAB_TEKTON_TASK\nexport HWLAB_SOURCE_REVISION\nexport PATH=\"/workspace/buildkit-bin:$PATH\"\nexport HWLAB_BUILDKIT_ADDR=\"unix:///workspace/buildkit-run/buildkitd.sock\"\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n echo '{\"event\":\"dependency-local-registry-probe-start\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"target\":\"http://127.0.0.1:5000/v2/\"}'\n registry_started_at=\"$(date +%s)\"\n curl -fsS --max-time 10 http://127.0.0.1:5000/v2/ >/dev/null\n registry_finished_at=\"$(date +%s)\"\n echo '{\"event\":\"dependency-local-registry-probe\",\"phase\":\"service-image-publish-pre-base-image\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"ok\":true,\"durationSeconds\":'\"$((registry_finished_at - registry_started_at))\"'}'\nfi\nbuildkit_ready=0\nfor attempt in $(seq 1 60); do\n if /workspace/buildkit-bin/buildctl --addr \"$HWLAB_BUILDKIT_ADDR\" debug workers >/dev/null 2>&1; then\n buildkit_ready=1\n break\n fi\n sleep 1\ndone\nif [ \"$buildkit_ready\" != \"1\" ]; then\n echo '{\"event\":\"buildkit-sidecar-not-ready\",\"serviceId\":\"'\"$(params.service-id)\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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\" --build-cache-mode \"$(params.build-cache-mode)\"\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"},{"name":"service-id","value":"hwlab-agent-skills"},{"name":"base-image","value":"$(params.base-image)"},{"name":"build-cache-mode","value":"$(params.build-cache-mode)"}]},{"name":"collect-artifacts","runAfter":["build-hwlab-cloud-api","build-hwlab-workbench-runtime","build-hwlab-user-billing","build-hwlab-project-management","build-hwlab-cloud-web","build-hwlab-gateway","build-hwlab-edge-proxy","build-hwlab-agent-skills"],"workspaces":[{"name":"source","workspace":"source"}],"taskSpec":{"params":[{"name":"revision"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"registry-prefix"},{"name":"services"},{"name":"base-image"}],"workspaces":[{"name":"source"}],"steps":[{"name":"collect","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"script":"#!/bin/sh\nset -eu\necho '{\"event\":\"ci-base-image\",\"phase\":\"collect-artifacts\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apk\"}'\nfor tool in node git; do command -v \"$tool\" >/dev/null 2>&1 || { echo '{\"event\":\"ci-base-image\",\"phase\":\"collect-artifacts\",\"ok\":false,\"reason\":\"missing-tool\",\"tool\":\"'\"$tool\"'\"}'; exit 31; }; done\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-collect\"\nexport HWLAB_CI_ARTIFACT_RUN_OWNER=\"tekton\"\nexport HWLAB_DEV_REGISTRY_PREFIX=\"$(params.registry-prefix)\"\nexport HWLAB_DEV_REGISTRY_K8S_NATIVE=1\nif [ -n \"$(params.base-image)\" ]; then export HWLAB_DEV_BASE_IMAGE=\"$(params.base-image)\"; fi\nexport HWLAB_ARTIFACT_LANE=\"$(params.lane)\"\nexport HWLAB_ARTIFACT_CATALOG_PATH=\"$(params.catalog-path)\"\nexport HWLAB_DEPLOY_MANIFEST_PATH=\"deploy/deploy.yaml\"\nexport HWLAB_ARTIFACT_IMAGE_TAG_MODE=\"$(params.image-tag-mode)\"\nif [ -s /workspace/source/affected-services.json ] && node - /workspace/source/affected-services.json <<'NODE'\nconst fs = require(\"node:fs\");\nconst plan = JSON.parse(fs.readFileSync(process.argv[2], \"utf8\"));\nconst buildServices = Array.isArray(plan.buildServices) ? plan.buildServices : [];\nconst affectedServices = Array.isArray(plan.affectedServices) ? plan.affectedServices : [];\nconst willRunGitopsPromote = plan.ciCdPlan && plan.ciCdPlan.willRunGitopsPromote === true;\nprocess.exit(!willRunGitopsPromote && buildServices.length === 0 && affectedServices.length === 0 ? 0 : 1);\nNODE\nthen\n rm -f /workspace/source/dev-artifacts.json\n echo '{\"event\":\"collect-artifacts\",\"status\":\"skipped\",\"reason\":\"no-build-no-rollout-plan\"}'\n exit 0\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node scripts/artifact-publish.mjs --publish --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --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 --ci-plan-path /workspace/source/affected-services.json\nnode scripts/refresh-artifact-catalog.mjs --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --deploy-config deploy/deploy.yaml --image-tag-mode \"$(params.image-tag-mode)\" --target-ref HEAD --publish-report /workspace/source/dev-artifacts.json --no-write\n"}]},"params":[{"name":"revision","value":"$(params.revision)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"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)"}]},{"name":"gitops-promote","runAfter":["collect-artifacts"],"workspaces":[{"name":"source","workspace":"source"},{"name":"git-ssh","workspace":"git-ssh"}],"taskSpec":{"params":[{"name":"git-url"},{"name":"git-read-url"},{"name":"git-write-url"},{"name":"source-branch"},{"name":"gitops-branch"},{"name":"lane"},{"name":"catalog-path"},{"name":"image-tag-mode"},{"name":"runtime-path"},{"name":"revision"},{"name":"registry-prefix"}],"results":[{"name":"runtime-ready-required","description":"true when GitOps promotion changed runtime desired state and runtime readiness must be observed"}],"workspaces":[{"name":"source"},{"name":"git-ssh"}],"steps":[{"name":"promote","image":"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1","env":[{"name":"HTTP_PROXY","value":"http://127.0.0.1:10808"},{"name":"HTTPS_PROXY","value":"http://127.0.0.1:10808"},{"name":"ALL_PROXY","value":"socks5h://127.0.0.1:10808"},{"name":"NO_PROXY","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"},{"name":"http_proxy","value":"http://127.0.0.1:10808"},{"name":"https_proxy","value":"http://127.0.0.1:10808"},{"name":"all_proxy","value":"socks5h://127.0.0.1:10808"},{"name":"no_proxy","value":"hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local"}],"script":"#!/bin/sh\nset -eu\nci_now_ms() {\n node -e 'console.log(Date.now())'\n}\n\nci_timing_emit() {\n stage=\"$1\"\n status=\"$2\"\n started_ms=\"$3\"\n finished_ms=\"$(ci_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n service_id=\"${HWLAB_TIMING_SERVICE_ID:-}\"\n node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:\"node-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/gitops-render.mjs\",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' \"$stage\" \"$status\" \"$duration_ms\" \"$service_id\"\n}\n\nexport HWLAB_TEKTON_PIPELINERUN=\"$(context.pipelineRun.name)\"\nexport HWLAB_TEKTON_TASKRUN=\"$(context.taskRun.name)\"\nexport HWLAB_TEKTON_TASK=\"gitops-promote\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\necho '{\"event\":\"ci-base-image\",\"phase\":\"gitops-promote\",\"image\":\"127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1\",\"policy\":\"no-runtime-apt\"}'\nfor tool in node git timeout; 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\ngit_ssh_setup() {\n mkdir -p /root/.ssh\n cp /workspace/git-ssh/ssh-privatekey /root/.ssh/id_rsa\n chmod 600 /root/.ssh/id_rsa\n timeout 10 ssh-keyscan github.com >> /root/.ssh/known_hosts 2>/dev/null || true\n timeout 10 ssh-keyscan -p 443 ssh.github.com >> /root/.ssh/known_hosts 2>/dev/null || true\n write_github_proxy_command\n export GIT_SSH_COMMAND=\"ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o ServerAliveInterval=10 -o ServerAliveCountMax=2 -o ProxyCommand='node /tmp/hwlab-github-proxy-connect.mjs 127.0.0.1 10808 %h %p'\"\n git config --global url.\"ssh://git@ssh.github.com:443/\".insteadOf \"git@github.com:\"\n git config --global url.\"ssh://git@ssh.github.com:443/\".insteadOf \"ssh://git@github.com/\"\n}\n\nwrite_github_proxy_command() {\n cat > /tmp/hwlab-github-proxy-connect.mjs <<'NODE_PROXY'\n#!/usr/bin/env node\nimport net from \"node:net\";\n\nconst [proxyHost, proxyPortRaw, targetHost, targetPortRaw] = process.argv.slice(2);\nconst proxyPort = Number.parseInt(proxyPortRaw || \"\", 10);\nconst targetPort = Number.parseInt(targetPortRaw || \"\", 10);\nif (!proxyHost || !Number.isInteger(proxyPort) || !targetHost || !Number.isInteger(targetPort)) {\n console.error(\"usage: hwlab-github-proxy-connect \");\n process.exit(64);\n}\n\nconst socket = net.createConnection({ host: proxyHost, port: proxyPort });\nlet buffer = Buffer.alloc(0);\n\nsocket.setTimeout(10000, () => {\n console.error(\"proxy connect timeout \" + proxyHost + \":\" + proxyPort + \" -> \" + targetHost + \":\" + targetPort);\n socket.destroy();\n process.exit(65);\n});\n\nsocket.on(\"connect\", () => {\n socket.write(\"CONNECT \" + targetHost + \":\" + targetPort + \" HTTP/1.1\\r\\nHost: \" + targetHost + \":\" + targetPort + \"\\r\\nProxy-Connection: Keep-Alive\\r\\n\\r\\n\");\n});\n\nsocket.on(\"error\", (error) => {\n console.error(\"proxy connect failed: \" + error.message);\n process.exit(66);\n});\n\nfunction onData(chunk) {\n buffer = Buffer.concat([buffer, chunk]);\n const headerEnd = buffer.indexOf(\"\\r\\n\\r\\n\");\n if (headerEnd === -1 && buffer.length < 8192) return;\n const head = buffer.slice(0, headerEnd + 4).toString(\"latin1\");\n const statusLine = head.split(\"\\r\\n\", 1)[0] || \"\";\n const statusCode = Number.parseInt(statusLine.split(\" \")[1] || \"\", 10);\n if (!statusLine.startsWith(\"HTTP/1.\") || !Number.isInteger(statusCode) || statusCode < 200 || statusCode > 299) {\n console.error(\"proxy CONNECT rejected: \" + (statusLine || \"missing-status\"));\n socket.destroy();\n process.exit(67);\n }\n socket.off(\"data\", onData);\n socket.setTimeout(0);\n const rest = buffer.slice(headerEnd + 4);\n if (rest.length) process.stdout.write(rest);\n process.stdin.pipe(socket);\n socket.pipe(process.stdout);\n}\n\nsocket.on(\"data\", onData);\nsocket.on(\"close\", () => process.exit(0));\nNODE_PROXY\n chmod 0700 /tmp/hwlab-github-proxy-connect.mjs\n}\n\ngit_now_ms() {\n node -e 'console.log(Date.now())' 2>/dev/null || date +%s000\n}\n\ngit_timed() {\n phase=\"$1\"\n timeout_seconds=\"$2\"\n shift 2\n started_ms=\"$(git_now_ms)\"\n echo '{\"event\":\"git-operation\",\"phase\":\"'\"$phase\"'\",\"status\":\"started\",\"timeoutSeconds\":'\"$timeout_seconds\"'}' >&2\n set +e\n timeout \"$timeout_seconds\" \"$@\"\n status=\"$?\"\n set -e\n finished_ms=\"$(git_now_ms)\"\n duration_ms=\"$((finished_ms - started_ms))\"\n if [ \"$status\" -eq 0 ]; then\n echo '{\"event\":\"git-operation\",\"phase\":\"'\"$phase\"'\",\"status\":\"succeeded\",\"durationMs\":'\"$duration_ms\"'}' >&2\n return 0\n fi\n if [ \"$status\" -eq 124 ] || [ \"$status\" -eq 137 ]; then\n echo '{\"event\":\"git-operation\",\"phase\":\"'\"$phase\"'\",\"status\":\"failed\",\"reason\":\"timeout\",\"durationMs\":'\"$duration_ms\"',\"timeoutSeconds\":'\"$timeout_seconds\"'}' >&2\n else\n echo '{\"event\":\"git-operation\",\"phase\":\"'\"$phase\"'\",\"status\":\"failed\",\"exitCode\":'\"$status\"',\"durationMs\":'\"$duration_ms\"'}' >&2\n fi\n return \"$status\"\n}\n\ngit_url_requires_ssh() { case \"$1\" in git@*|ssh://*) return 0 ;; *) return 1 ;; esac; }\nlane=\"$(params.lane)\"\ncase \"$lane\" in\n v[0-9][0-9]*) runtime_lane=true ;;\n *) runtime_lane=false ;;\nesac\nif [ \"$runtime_lane\" = \"true\" ]; then\n printf 'false' > /tekton/results/runtime-ready-required\nelse\n printf 'true' > /tekton/results/runtime-ready-required\nfi\nsource_head_url_for_setup=\"$(params.git-url)\"\ngitops_read_url_for_setup=\"$(params.git-url)\"\nif [ \"$runtime_lane\" = \"true\" ]; then\n source_head_url_for_setup=\"$(params.git-read-url)\"\n gitops_read_url_for_setup=\"$(params.git-read-url)\"\nfi\ngitops_write_url_for_setup=\"$(params.git-write-url)\"\nif git_url_requires_ssh \"$source_head_url_for_setup\" || git_url_requires_ssh \"$gitops_read_url_for_setup\" || git_url_requires_ssh \"$gitops_write_url_for_setup\"; then\n for tool in ssh ssh-keyscan; 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\n git_ssh_setup\nelse\n echo '{\"event\":\"git-ssh-setup\",\"phase\":\"gitops-promote\",\"status\":\"skipped\",\"reason\":\"non-ssh-git-url\"}'\nfi\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\n\ncheck_source_head() {\n phase=\"$1\"\n expected=\"${2:-$(params.revision)}\"\n source_head_url=\"$(params.git-url)\"\n if [ \"$runtime_lane\" = \"true\" ]; then\n source_head_url=\"$(params.git-read-url)\"\n fi\n latest_file=\"$(mktemp)\"\n if ! git_timed \"source-head-$phase\" 45 git ls-remote \"$source_head_url\" \"refs/heads/$(params.source-branch)\" > \"$latest_file\"; then\n echo '{\"status\":\"failed\",\"reason\":\"source-head-check-failed\",\"phase\":\"'\"$phase\"'\",\"sourceBranch\":\"'\"$(params.source-branch)\"'\",\"sourceRevision\":\"'\"$(params.revision)\"'\"}'\n exit 1\n fi\n latest=\"$(cut -f1 \"$latest_file\" | head -n 1)\"\n if [ -z \"$latest\" ]; then\n echo '{\"status\":\"failed\",\"reason\":\"source-head-unresolved\",\"phase\":\"'\"$phase\"'\",\"sourceBranch\":\"'\"$(params.source-branch)\"'\",\"sourceRevision\":\"'\"$(params.revision)\"'\"}'\n exit 1\n fi\n if [ \"$latest\" != \"$expected\" ]; then\n printf 'false' > /tekton/results/runtime-ready-required || true\n echo '{\"status\":\"skipped-stale-source\",\"verdict\":\"superseded\",\"phase\":\"'\"$phase\"'\",\"sourceBranch\":\"'\"$(params.source-branch)\"'\",\"sourceRevision\":\"'\"$(params.revision)\"'\",\"expectedRevision\":\"'\"$expected\"'\",\"latestRevision\":\"'\"$latest\"'\"}'\n exit 0\n fi\n}\n\ncheck_source_head before-render\nif [ -s /workspace/source/affected-services.json ] && node - /workspace/source/affected-services.json <<'NODE'\nconst fs = require(\"node:fs\");\nconst plan = JSON.parse(fs.readFileSync(process.argv[2], \"utf8\"));\nconst buildServices = Array.isArray(plan.buildServices) ? plan.buildServices : [];\nconst affectedServices = Array.isArray(plan.affectedServices) ? plan.affectedServices : [];\nconst willRunGitopsPromote = plan.ciCdPlan && plan.ciCdPlan.willRunGitopsPromote === true;\nprocess.exit(!willRunGitopsPromote && buildServices.length === 0 && affectedServices.length === 0 ? 0 : 1);\nNODE\nthen\n printf 'false' > /tekton/results/runtime-ready-required || true\n echo '{\"event\":\"gitops-promote\",\"status\":\"skipped\",\"reason\":\"no-build-no-rollout-plan\"}'\n exit 0\nfi\ngit config --global user.name \"HWLAB node GitOps Bot\"\ngit config --global user.email \"hwlab-node-gitops-bot@users.noreply.github.com\"\ncatalog_path=\"$(params.catalog-path)\"\nruntime_path=\"$(params.runtime-path)\"\ngitops_root_from_runtime_path() {\n path=\"$1\"\n case \"$path\" in\n */*) printf '%s\n' \"${path%/*}\" ;;\n *) printf '.\n' ;;\n esac\n}\ngitops_root=\"$(gitops_root_from_runtime_path \"$runtime_path\")\"\n\nargo_hard_refresh_runtime_lane() {\n if [ \"$runtime_lane\" != \"true\" ]; then return 0; fi\n ARGO_APPLICATION=\"hwlab-node-$lane\" ARGO_FIELD_MANAGER=\"hwlab-$lane-gitops-promote\" node <<'NODE'\nconst fs = require(\"node:fs\");\nconst https = require(\"node:https\");\n\nconst host = process.env.KUBERNETES_SERVICE_HOST;\nconst port = process.env.KUBERNETES_SERVICE_PORT || \"443\";\nconst startedAt = Date.now();\nconst application = process.env.ARGO_APPLICATION || \"hwlab-node-v02\";\nconst fieldManager = process.env.ARGO_FIELD_MANAGER || \"hwlab-v02-gitops-promote\";\nconst pipelineRun = process.env.HWLAB_TEKTON_PIPELINERUN || null;\nconst taskRun = process.env.HWLAB_TEKTON_TASKRUN || null;\nconst task = process.env.HWLAB_TEKTON_TASK || null;\nconst revision = process.env.HWLAB_SOURCE_REVISION || null;\n\nfunction emit(payload) {\n console.log(JSON.stringify({\n event: \"node-cicd-timing\",\n schemaVersion: \"v1\",\n stage: \"argo-hard-refresh\",\n pipelineRun,\n taskRun,\n task,\n revision,\n application,\n source: \"scripts/gitops-render.mjs\",\n at: new Date().toISOString(),\n ...payload\n }));\n}\n\nfunction safeJson(text) {\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n\nfunction request(method, path, body, contentType = \"application/merge-patch+json\") {\n const token = fs.readFileSync(\"/var/run/secrets/kubernetes.io/serviceaccount/token\", \"utf8\");\n const ca = fs.readFileSync(\"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt\");\n const payload = body ? JSON.stringify(body) : \"\";\n const headers = { Authorization: \"Bearer \" + token };\n if (payload) {\n headers[\"Content-Type\"] = contentType;\n headers[\"Content-Length\"] = Buffer.byteLength(payload);\n }\n return new Promise((resolve, reject) => {\n const req = https.request({ host, port, method, path, ca, headers }, (res) => {\n let data = \"\";\n res.setEncoding(\"utf8\");\n res.on(\"data\", (chunk) => { data += chunk; });\n res.on(\"end\", () => resolve({ statusCode: res.statusCode || 0, body: data, json: safeJson(data) }));\n });\n req.on(\"error\", reject);\n if (payload) req.write(payload);\n req.end();\n });\n}\n\n(async () => {\n if (!host) {\n emit({ status: \"skipped\", reason: \"kubernetes-service-host-missing\", durationMs: Date.now() - startedAt });\n return;\n }\n const response = await request(\n \"PATCH\",\n \"/apis/argoproj.io/v1alpha1/namespaces/argocd/applications/\" + encodeURIComponent(application) + \"?fieldManager=\" + encodeURIComponent(fieldManager),\n { metadata: { annotations: { \"argocd.argoproj.io/refresh\": \"hard\" } } }\n );\n if (response.statusCode >= 200 && response.statusCode < 300) {\n emit({ status: \"succeeded\", durationMs: Date.now() - startedAt, statusCode: response.statusCode });\n return;\n }\n emit({ status: \"degraded\", reason: \"argo-refresh-patch-failed\", durationMs: Date.now() - startedAt, statusCode: response.statusCode, body: response.body.slice(0, 1000) });\n})().catch((error) => {\n emit({ status: \"degraded\", reason: \"argo-refresh-patch-error\", durationMs: Date.now() - startedAt, error: error.message });\n});\nNODE\n}\n\nif [ -s /workspace/source/dev-artifacts.json ]; then\n node scripts/refresh-artifact-catalog.mjs --lane \"$lane\" --catalog-path \"$catalog_path\" --deploy-config deploy/deploy.yaml --image-tag-mode \"$(params.image-tag-mode)\" --target-ref \"$(params.revision)\" --publish-report /workspace/source/dev-artifacts.json --write\nfi\ngitops_render_started_ms=\"$(ci_now_ms)\"\nnode scripts/run-bun.mjs scripts/gitops-render.mjs --lane \"$lane\" --catalog-path \"$catalog_path\" --image-tag-mode \"$(params.image-tag-mode)\" --source-revision \"$(params.revision)\" --source-repo \"$(params.git-url)\" --source-branch \"$(params.source-branch)\" --gitops-branch \"$(params.gitops-branch)\" --gitops-root \"$gitops_root\" --out \"$gitops_root\" --registry-prefix \"$(params.registry-prefix)\" --use-deploy-images\nnode scripts/run-bun.mjs scripts/gitops-render.mjs --lane \"$lane\" --catalog-path \"$catalog_path\" --image-tag-mode \"$(params.image-tag-mode)\" --source-revision \"$(params.revision)\" --source-repo \"$(params.git-url)\" --source-branch \"$(params.source-branch)\" --gitops-branch \"$(params.gitops-branch)\" --gitops-root \"$gitops_root\" --out \"$gitops_root\" --registry-prefix \"$(params.registry-prefix)\" --use-deploy-images --check\nci_timing_emit gitops-render succeeded \"$gitops_render_started_ms\"\ncheck_source_head before-push\nworkdir=\"$(mktemp -d)\"\ngitops_read_url=\"$(params.git-url)\"\ngitops_write_url=\"$(params.git-write-url)\"\nif [ \"$runtime_lane\" = \"true\" ]; then\n gitops_read_url=\"$(params.git-read-url)\"\nfi\ngitops_clone_started_ms=\"$(ci_now_ms)\"\ngit_timed gitops-clone 180 git clone --no-checkout \"$gitops_read_url\" \"$workdir/gitops\"\ncd \"$workdir/gitops\"\ngit remote set-url origin \"$gitops_write_url\"\nold_runtime_snapshot=\"$workdir/old-runtime-snapshot\"\nif git_timed gitops-branch-ls 45 git ls-remote --exit-code --heads origin \"$(params.gitops-branch)\" >/dev/null; then\n git_timed gitops-fetch 90 git fetch origin \"$(params.gitops-branch)\"\n git checkout -B \"$(params.gitops-branch)\" \"origin/$(params.gitops-branch)\"\n if [ \"$runtime_lane\" = \"true\" ] && [ -d \"$runtime_path\" ]; then\n mkdir -p \"$old_runtime_snapshot\"\n cp -a \"$runtime_path\"/. \"$old_runtime_snapshot\"/\n fi\n if [ \"$runtime_lane\" = \"true\" ]; then rm -rf \"$runtime_path\" \"$catalog_path\"; else rm -rf deploy/gitops/node \"$catalog_path\"; fi\nelse\n git checkout --orphan \"$(params.gitops-branch)\"\n git rm -rf . >/dev/null 2>&1 || true\nfi\nci_timing_emit gitops-clone succeeded \"$gitops_clone_started_ms\"\nmkdir -p \"$(dirname \"$runtime_path\")\" \"$(dirname \"$catalog_path\")\"\nif [ \"$runtime_lane\" = \"true\" ]; then\n cp -a \"/workspace/source/repo/$runtime_path\" \"$runtime_path\"\n cp \"/workspace/source/repo/$catalog_path\" \"$catalog_path\"\n git add \"$catalog_path\" \"$runtime_path\"\nelse\n mkdir -p deploy/gitops\n cp -a /workspace/source/repo/deploy/gitops/node deploy/gitops/node\n cp \"/workspace/source/repo/$catalog_path\" \"$catalog_path\"\n git add \"$catalog_path\" deploy/gitops/node\nfi\nif [ \"$runtime_lane\" = \"true\" ] && [ -s /workspace/source/affected-services.json ]; then\n runtime_noop_decision=\"$(node - \"$runtime_path\" \"$old_runtime_snapshot\" /workspace/source/affected-services.json <<'NODE'\nconst fs = require(\"node:fs\");\nconst path = require(\"node:path\");\n\nconst [runtimePath, oldRuntimePath, planPath] = process.argv.slice(2);\n\nfunction readJson(filePath) {\n return JSON.parse(fs.readFileSync(filePath, \"utf8\"));\n}\n\nfunction stable(value) {\n if (Array.isArray(value)) return \"[\" + value.map(stable).join(\",\") + \"]\";\n if (value && typeof value === \"object\") {\n return \"{\" + Object.keys(value).sort().map((key) => JSON.stringify(key) + \":\" + stable(value[key])).join(\",\") + \"}\";\n }\n return JSON.stringify(value);\n}\n\nfunction scrub(value) {\n if (Array.isArray(value)) return value.map(scrub);\n if (!value || typeof value !== \"object\") return value;\n const result = {};\n for (const [key, child] of Object.entries(value)) {\n if (key === \"hwlab.pikastech.local/source-commit\" ||\n key === \"hwlab.pikastech.local/artifact-source-commit\" ||\n key === \"hwlab.pikastech.local/boot-commit\" ||\n key === \"sourceCommitId\" ||\n key === \"bootCommit\") {\n result[key] = \"\";\n continue;\n }\n const envName = String(value.name || \"\");\n if (key === \"value\" && /^HWLAB_.*COMMIT/.test(envName)) {\n result[key] = \"\";\n continue;\n }\n result[key] = scrub(child);\n }\n return result;\n}\n\nfunction listFiles(rootDir) {\n if (!rootDir || !fs.existsSync(rootDir)) return null;\n const files = [];\n function walk(current) {\n for (const entry of fs.readdirSync(current, { withFileTypes: true })) {\n const fullPath = path.join(current, entry.name);\n if (entry.isDirectory()) walk(fullPath);\n else if (entry.isFile()) files.push(path.relative(rootDir, fullPath).replaceAll(path.sep, \"/\"));\n }\n }\n walk(rootDir);\n return files.sort();\n}\n\nfunction normalizedTree(rootDir) {\n const files = listFiles(rootDir);\n if (!files) return null;\n const entries = {};\n for (const relativePath of files) {\n const filePath = path.join(rootDir, relativePath);\n const text = fs.readFileSync(filePath, \"utf8\");\n try {\n entries[relativePath] = stable(scrub(JSON.parse(text)));\n } catch {\n entries[relativePath] = text.replace(/[a-f0-9]{40}/gu, \"\");\n }\n }\n return stable(entries);\n}\n\nconst plan = readJson(planPath);\nconst buildServices = Array.isArray(plan.buildServices) ? plan.buildServices : [];\nconst rolloutServices = Array.isArray(plan.rolloutServices) ? plan.rolloutServices : [];\nif (buildServices.length > 0 || rolloutServices.length > 0) {\n process.stdout.write(\"runtime-required\");\n process.exit(0);\n}\n\nconst oldTree = normalizedTree(oldRuntimePath);\nconst newTree = normalizedTree(runtimePath);\nprocess.stdout.write(oldTree && newTree && oldTree === newTree ? \"runtime-identity-only\" : \"runtime-required\");\nNODE\n)\"\n if [ \"$runtime_noop_decision\" = \"runtime-identity-only\" ]; then\n printf 'false' > /tekton/results/runtime-ready-required\n echo '{\"status\":\"skipped-runtime-unchanged\",\"reason\":\"runtime-identity-only\",\"gitopsBranch\":\"'\"$(params.gitops-branch)\"'\",\"sourceRevision\":\"'\"$(params.revision)\"'\"}'\n ci_timing_emit gitops-commit skipped \"$(ci_now_ms)\"\n ci_timing_emit gitops-push skipped \"$(ci_now_ms)\"\n exit 0\n fi\nfi\nif git diff --cached --quiet; then\n printf 'false' > /tekton/results/runtime-ready-required\n echo '{\"status\":\"unchanged\",\"gitopsBranch\":\"'\"$(params.gitops-branch)\"'\",\"sourceRevision\":\"'\"$(params.revision)\"'\"}'\n ci_timing_emit gitops-commit unchanged \"$(ci_now_ms)\"\n ci_timing_emit gitops-push unchanged \"$(ci_now_ms)\"\n exit 0\nfi\nshort=\"$(printf '%.7s' \"$(params.revision)\")\"\ngitops_commit_started_ms=\"$(ci_now_ms)\"\ngit commit -m \"chore: promote node GitOps source $short\"\nci_timing_emit gitops-commit succeeded \"$gitops_commit_started_ms\"\ngitops_push_started_ms=\"$(ci_now_ms)\"\ngit_timed gitops-push 120 git push origin \"HEAD:$(params.gitops-branch)\"\nci_timing_emit gitops-push succeeded \"$gitops_push_started_ms\"\nif [ \"$runtime_lane\" = \"true\" ]; then\n printf 'false' > /tekton/results/runtime-ready-required\n echo '{\"event\":\"runtime-ready\",\"phase\":\"gitops-promote\",\"status\":\"delegated-post-flush-closeout\",\"gitopsBranch\":\"'\"$(params.gitops-branch)\"'\",\"sourceRevision\":\"'\"$(params.revision)\"'\"}'\nfi\nargo_hard_refresh_runtime_lane\necho '{\"status\":\"pushed\",\"gitopsBranch\":\"'\"$(params.gitops-branch)\"'\",\"sourceRevision\":\"'\"$(params.revision)\"'\",\"gitopsWriteUrl\":\"'\"$gitops_write_url\"'\"}'\n"}]},"params":[{"name":"git-url","value":"$(params.git-url)"},{"name":"git-read-url","value":"$(params.git-read-url)"},{"name":"git-write-url","value":"$(params.git-write-url)"},{"name":"source-branch","value":"$(params.source-branch)"},{"name":"gitops-branch","value":"$(params.gitops-branch)"},{"name":"lane","value":"$(params.lane)"},{"name":"catalog-path","value":"$(params.catalog-path)"},{"name":"image-tag-mode","value":"$(params.image-tag-mode)"},{"name":"runtime-path","value":"$(params.runtime-path)"},{"name":"revision","value":"$(params.revision)"},{"name":"registry-prefix","value":"$(params.registry-prefix)"}]}]}} diff --git a/scripts/ci-plan.mjs b/scripts/ci-plan.mjs index ad9ac663..de9e88eb 100644 --- a/scripts/ci-plan.mjs +++ b/scripts/ci-plan.mjs @@ -28,11 +28,13 @@ 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); + if (catalogAllowsNoDepsReuse(artifactCatalog, args.services)) { + noDepsPlan.artifactCatalog = artifactCatalog; + noDepsPlan.compatibility.artifactCatalog = 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); + } } const { createCiPlan } = await import("./src/ci-plan-lib.mjs"); @@ -43,7 +45,8 @@ function tryCreateNoDepsPlan(options) { const services = Array.isArray(options.services) ? options.services.filter(Boolean) : []; if (services.length === 0) return null; const targetRef = options.targetRef ?? "HEAD"; - const baseRef = options.baseRef ?? (gitValue(["rev-parse", `${targetRef}^`]) || targetRef); + const inferredBaseRef = options.baseRef ?? tryGitValue(["rev-parse", `${targetRef}^`]); + const baseRef = inferredBaseRef || targetRef; const changedPaths = gitValue(["diff", "--name-only", baseRef, targetRef]).split("\n").map((line) => line.trim()).filter(Boolean); const summary = classifyNoDepsGlobalChange(changedPaths); if (!summary.gitopsOnly && !summary.docsOnly && !summary.testOnly && summary.summary !== "no-source-diff") return null; @@ -118,12 +121,21 @@ async function noDepsArtifactCatalogSummary(options) { gitopsCommitId: null, catalogSourceCommitId: null, serviceCount: 0, - catalogSha256: null + catalogSha256: null, + coverage: null, + bootstrapReason: null }; } return readArtifactCatalogSummary({ repoRoot: process.cwd(), catalogPath }); } +function catalogAllowsNoDepsReuse(artifactCatalog, selectedServices) { + if (artifactCatalog?.status !== "hydrated" || artifactCatalog.authority !== "gitops-branch") return false; + const catalogServiceIds = artifactCatalog.coverage?.catalogServiceIds; + if (!Array.isArray(catalogServiceIds)) return false; + return selectedServices.every((serviceId) => catalogServiceIds.includes(serviceId)); +} + function classifyNoDepsGlobalChange(changedPaths) { const relevant = changedPaths.filter(Boolean); const testOnly = relevant.length > 0 && relevant.every(isNoDepsTestOnlyPath); @@ -178,6 +190,14 @@ function gitValue(args) { return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 15000 }).trim(); } +function tryGitValue(args) { + try { + return gitValue(args); + } catch { + return ""; + } +} + function booleanEnv(value) { return ["1", "true", "yes", "on"].includes(String(value ?? "").trim().toLowerCase()); } diff --git a/scripts/ci-plan.test.mjs b/scripts/ci-plan.test.mjs index bda40fd8..e47f8719 100644 --- a/scripts/ci-plan.test.mjs +++ b/scripts/ci-plan.test.mjs @@ -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 }); } diff --git a/scripts/ci/restore-artifact-catalog.mjs b/scripts/ci/restore-artifact-catalog.mjs index 904285a3..8e4e6ae0 100644 --- a/scripts/ci/restore-artifact-catalog.mjs +++ b/scripts/ci/restore-artifact-catalog.mjs @@ -1,12 +1,14 @@ #!/usr/bin/env node // Restore the node/lane artifact catalog from the GitOps branch before CI planning. -import { execFileSync } from "node:child_process"; -import { mkdirSync, writeFileSync } from "node:fs"; +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname } from "node:path"; import { artifactCatalogAuthorityPath, + artifactCatalogServiceCoverage, + createBootstrapArtifactCatalogAuthority, createHydratedArtifactCatalogAuthority } from "../src/artifact-catalog-authority.mjs"; @@ -42,6 +44,21 @@ function git(args, options = {}) { }); } +function gitProbe(args, options = {}) { + const result = spawnSync("git", args, { + cwd: process.cwd(), + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + timeout: options.timeoutMs ?? 45_000 + }); + if (result.error) throw result.error; + return { + status: result.status ?? 1, + stdout: result.stdout ?? "", + stderr: result.stderr ?? "" + }; +} + const catalogPath = env("HWLAB_CATALOG_PATH"); const gitopsBranch = env("HWLAB_GITOPS_BRANCH"); const remote = env("HWLAB_GIT_READ_URL") ?? env("HWLAB_GIT_URL"); @@ -60,27 +77,42 @@ if (!catalogPath || !gitopsBranch || !remote || selectedServices.length === 0) { } try { + const selectedServiceCoverageInput = uniqueSorted(selectedServices); + if (selectedServiceCoverageInput.length !== selectedServices.length) { + throw new Error("selected services contain duplicate service ids"); + } + const branchProbe = gitProbe(["ls-remote", "--exit-code", "--heads", remote, `refs/heads/${gitopsBranch}`], { timeoutMs: 45_000 }); + if (branchProbe.status === 2) { + restoreBootstrapOrMissing({ reason: "gitops-branch-missing", selectedServices: selectedServiceCoverageInput }); + process.exit(0); + } + if (branchProbe.status !== 0) { + throw new Error(`gitops branch probe failed: ${boundedError(branchProbe.stderr)}`); + } 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 catalogProbe = gitProbe(["ls-tree", "-z", "--full-tree", "--name-only", "FETCH_HEAD", "--", catalogPath], { timeoutMs: 15_000 }); + if (catalogProbe.status !== 0) { + throw new Error(`gitops catalog tree probe failed: ${boundedError(catalogProbe.stderr)}`); + } + if (catalogProbe.stdout.length === 0) { + restoreBootstrapOrMissing({ reason: "gitops-catalog-missing", selectedServices: selectedServiceCoverageInput }); + process.exit(0); + } const content = git(["show", `FETCH_HEAD:${catalogPath}`], { timeoutMs: 30_000 }); const parsed = JSON.parse(content); if (!Array.isArray(parsed?.services) || parsed.services.length === 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 coverage = artifactCatalogServiceCoverage(parsed, selectedServiceCoverageInput); const authorityPath = artifactCatalogAuthorityPath(catalogPath); - const authority = createHydratedArtifactCatalogAuthority({ catalog: parsed, catalogPath, gitopsBranch, gitopsCommitId }); + const authority = createHydratedArtifactCatalogAuthority({ + catalog: parsed, + catalogPath, + gitopsBranch, + gitopsCommitId, + selectedServices: selectedServiceCoverageInput + }); mkdirSync(dirname(catalogPath), { recursive: true }); writeFileSync(catalogPath, JSON.stringify(parsed, null, 2) + "\n"); writeFileSync(authorityPath, JSON.stringify(authority, null, 2) + "\n"); @@ -94,22 +126,78 @@ try { catalogSha256: authority.catalogSha256, serviceCount: parsed.services.length, selectedServiceCount: selectedServices.length, + coverage, bytes: Buffer.byteLength(content) }); } catch (error) { emit({ status: "failed", - reason: error?.code === "catalog-service-ids-mismatch" ? "gitops-catalog-service-ids-mismatch" : "gitops-catalog-unavailable", + reason: "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 restoreBootstrapOrMissing({ reason, selectedServices }) { + const authorityPath = artifactCatalogAuthorityPath(catalogPath); + if (!existsSync(catalogPath)) { + if (existsSync(authorityPath)) throw new Error(`artifact catalog authority exists without catalog ${catalogPath}`); + emit({ + status: "missing", + reason, + authority: "none", + selectedServiceCount: selectedServices.length + }); + return; + } + const catalog = JSON.parse(readFileSync(catalogPath, "utf8")); + const sourceCommitId = git(["rev-parse", "HEAD"], { timeoutMs: 15_000 }).trim(); + validateBootstrapCatalog({ catalog, sourceCommitId, selectedServices }); + const authority = createBootstrapArtifactCatalogAuthority({ + catalog, + catalogPath, + sourceCommitId, + reason, + selectedServices + }); + writeFileSync(authorityPath, JSON.stringify(authority, null, 2) + "\n"); + emit({ + status: "bootstrap", + reason, + authority: authority.authority, + authorityPath, + sourceCommitId, + serviceCount: authority.serviceCount, + catalogSha256: authority.catalogSha256, + coverage: authority.coverage + }); } function uniqueSorted(values) { return [...new Set(values)].sort(); } + +function validateBootstrapCatalog({ catalog, sourceCommitId, selectedServices }) { + if (catalog?.catalogVersion !== "v1" || catalog?.kind !== "hwlab-artifact-catalog" || catalog?.artifactState !== "contract-skeleton" || catalog?.environment !== "v02" || catalog?.profile !== "v02") { + throw new Error(`existing source catalog is not an allowed v02 contract skeleton for ${catalogPath}`); + } + if (catalog?.publish?.sourceCommitId !== sourceCommitId) { + throw new Error(`v02 contract skeleton source commit does not match HEAD for ${catalogPath}`); + } + const coverage = artifactCatalogServiceCoverage(catalog, selectedServices); + if (coverage.status !== "exact") { + throw new Error(`v02 contract skeleton service coverage is ${coverage.status} for ${catalogPath}`); + } + const unsafe = catalog.services.filter((service) => ( + service?.sourceCommitId !== sourceCommitId + || service?.buildBackend !== "contract-skeleton" + || service?.digest !== null + || (service?.environmentDigest !== undefined && service?.environmentDigest !== null) + )); + if (unsafe.length > 0) throw new Error(`v02 contract skeleton contains reusable artifacts for ${catalogPath}`); +} + +function boundedError(value) { + const result = String(value ?? "").replace(/\s+/gu, " ").trim(); + return result ? result.slice(0, 300) : "exit-without-stderr"; +} diff --git a/scripts/src/artifact-catalog-authority.mjs b/scripts/src/artifact-catalog-authority.mjs index 3b17f85b..ef4514ae 100644 --- a/scripts/src/artifact-catalog-authority.mjs +++ b/scripts/src/artifact-catalog-authority.mjs @@ -17,7 +17,27 @@ export function artifactCatalogSourceCommitId(catalog) { return serviceCommits.length === 1 ? serviceCommits[0] : null; } -export function createHydratedArtifactCatalogAuthority({ catalog, catalogPath, gitopsBranch, gitopsCommitId }) { +export function artifactCatalogServiceCoverage(catalog, selectedServices) { + if (!Array.isArray(catalog?.services)) throw new Error("artifact catalog services must be an array"); + const selectedServiceIds = normalizedServiceIds(selectedServices, "selected services"); + const catalogServiceIds = normalizedServiceIds(catalog.services.map((service) => service?.serviceId), "artifact catalog services"); + const missingSelectedServices = selectedServiceIds.filter((serviceId) => !catalogServiceIds.includes(serviceId)); + const extraCatalogServices = catalogServiceIds.filter((serviceId) => !selectedServiceIds.includes(serviceId)); + return { + status: missingSelectedServices.length > 0 + ? (extraCatalogServices.length > 0 ? "divergent" : "partial") + : (extraCatalogServices.length > 0 ? "superset" : "exact"), + selectedServiceCount: selectedServiceIds.length, + selectedServicePresentCount: selectedServiceIds.length - missingSelectedServices.length, + catalogServiceCount: catalogServiceIds.length, + selectedServiceIds, + catalogServiceIds, + missingSelectedServices, + extraCatalogServices + }; +} + +export function createHydratedArtifactCatalogAuthority({ catalog, catalogPath, gitopsBranch, gitopsCommitId, selectedServices }) { return { schemaVersion: "v1", status: "hydrated", @@ -27,7 +47,23 @@ export function createHydratedArtifactCatalogAuthority({ catalog, catalogPath, g gitopsCommitId: text(gitopsCommitId), catalogSourceCommitId: artifactCatalogSourceCommitId(catalog), serviceCount: Array.isArray(catalog?.services) ? catalog.services.length : 0, - catalogSha256: artifactCatalogSha256(catalog) + catalogSha256: artifactCatalogSha256(catalog), + coverage: artifactCatalogServiceCoverage(catalog, selectedServices) + }; +} + +export function createBootstrapArtifactCatalogAuthority({ catalog, catalogPath, sourceCommitId, reason, selectedServices }) { + return { + schemaVersion: "v1", + status: "bootstrap", + authority: "source-bootstrap", + catalogPath: normalizeCatalogPath(catalogPath), + sourceCommitId: text(sourceCommitId), + bootstrapReason: text(reason), + catalogSourceCommitId: artifactCatalogSourceCommitId(catalog), + serviceCount: Array.isArray(catalog?.services) ? catalog.services.length : 0, + catalogSha256: artifactCatalogSha256(catalog), + coverage: artifactCatalogServiceCoverage(catalog, selectedServices) }; } @@ -46,7 +82,9 @@ export async function readArtifactCatalogSummary({ repoRoot, catalogPath, author gitopsCommitId: null, catalogSourceCommitId: null, serviceCount: 0, - catalogSha256: null + catalogSha256: null, + coverage: null, + bootstrapReason: null }; } @@ -63,27 +101,49 @@ export async function readArtifactCatalogSummary({ repoRoot, catalogPath, author gitopsCommitId: null, catalogSourceCommitId, serviceCount, - catalogSha256 + catalogSha256, + coverage: null, + bootstrapReason: null }; } - validateHydratedAuthority({ authority, catalogPath: normalizedCatalogPath, serviceCount, catalogSha256 }); + const coverage = validateCatalogAuthority({ authority, catalog: loadedCatalog, catalogPath: normalizedCatalogPath, serviceCount, catalogSha256 }); + if (authority.authority === "gitops-branch") { + 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, + coverage, + bootstrapReason: null + }; + } return { - status: "hydrated", - authority: "gitops-branch", + status: "bootstrap", + authority: "source-bootstrap", catalogPath: normalizedCatalogPath, authorityPath: normalizeCatalogPath(authorityPath), - gitopsBranch: text(authority.gitopsBranch), - gitopsCommitId: text(authority.gitopsCommitId), + gitopsBranch: null, + gitopsCommitId: null, + sourceCommitId: text(authority.sourceCommitId), catalogSourceCommitId: text(authority.catalogSourceCommitId) || catalogSourceCommitId, serviceCount, - catalogSha256 + catalogSha256, + coverage, + bootstrapReason: text(authority.bootstrapReason) }; } -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`); +function validateCatalogAuthority({ authority, catalog, catalogPath, serviceCount, catalogSha256 }) { + const gitopsAuthority = authority.schemaVersion === "v1" && authority.status === "hydrated" && authority.authority === "gitops-branch"; + const bootstrapAuthority = authority.schemaVersion === "v1" && authority.status === "bootstrap" && authority.authority === "source-bootstrap"; + if (!gitopsAuthority && !bootstrapAuthority) { + throw new Error(`artifact catalog authority for ${catalogPath} is not a supported authority record`); } if (normalizeCatalogPath(authority.catalogPath) !== catalogPath) { throw new Error(`artifact catalog authority path mismatch: expected ${catalogPath}, got ${authority.catalogPath ?? "missing"}`); @@ -94,9 +154,21 @@ function validateHydratedAuthority({ authority, catalogPath, serviceCount, catal 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) ?? "")) { + const coverage = artifactCatalogServiceCoverage(catalog, authority.coverage?.selectedServiceIds); + validateCoverageRecord(authority.coverage, coverage, catalogPath); + if (gitopsAuthority && (!text(authority.gitopsBranch) || !/^[a-f0-9]{40}$/u.test(text(authority.gitopsCommitId) ?? ""))) { throw new Error(`artifact catalog authority gitops provenance is incomplete for ${catalogPath}`); } + if (bootstrapAuthority) { + const sourceCommitId = text(authority.sourceCommitId); + if (!/^[a-f0-9]{40}$/u.test(sourceCommitId ?? "") || !text(authority.bootstrapReason)) { + throw new Error(`artifact catalog bootstrap authority is incomplete for ${catalogPath}`); + } + if (sourceCommitId !== artifactCatalogSourceCommitId(catalog)) { + throw new Error(`artifact catalog bootstrap source commit mismatch for ${catalogPath}`); + } + } + return coverage; } async function readJsonIfPresent(repoRoot, filePath) { @@ -121,3 +193,38 @@ function text(value) { function unique(values) { return [...new Set(values)]; } + +function normalizedServiceIds(values, label) { + if (!Array.isArray(values) || values.length === 0) throw new Error(`${label} must be a non-empty array`); + const normalized = values.map(text); + if (normalized.some((value) => value === null)) throw new Error(`${label} contains an empty service id`); + const deduplicated = unique(normalized); + if (deduplicated.length !== normalized.length) throw new Error(`${label} contains duplicate service ids`); + return deduplicated.sort(); +} + +function validateCoverageRecord(actual, expected, catalogPath) { + const scalarFields = ["status", "selectedServiceCount", "selectedServicePresentCount", "catalogServiceCount"]; + const arrayFields = ["selectedServiceIds", "catalogServiceIds", "missingSelectedServices", "extraCatalogServices"]; + const expectedFields = [...scalarFields, ...arrayFields].sort(); + const actualFields = actual && typeof actual === "object" && !Array.isArray(actual) + ? Object.keys(actual).sort() + : []; + if (!sameArray(actualFields, expectedFields)) { + throw new Error(`artifact catalog authority coverage fields mismatch for ${catalogPath}`); + } + for (const field of scalarFields) { + if (actual[field] !== expected[field]) { + throw new Error(`artifact catalog authority coverage ${field} mismatch for ${catalogPath}`); + } + } + for (const field of arrayFields) { + if (!sameArray(actual[field], expected[field])) { + throw new Error(`artifact catalog authority coverage ${field} mismatch for ${catalogPath}`); + } + } +} + +function sameArray(left, right) { + return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => value === right[index]); +}