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
    hwlab.pikastech.local/download-profile: jd01-node-default
    hwlab.pikastech.local/network-profile: jd01-node-ci-egress
    unidesk.ai/ci-resource-governance-config-ref: "config/hwlab-node-lanes.yaml#lanes.v03.targets.NC01.ciResourceGovernance"
    unidesk.ai/ci-dedupe-identity: repository+lane+sourceCommit+pipelineIntent
    unidesk.ai/ci-build-queue-mode: single-taskrun-bounded-workers
    unidesk.ai/ci-build-service-count: "17"
    unidesk.ai/ci-build-batch-count: "9"
    unidesk.ai/ci-build-max-parallel: "2"
    unidesk.ai/owning-config-ref: "config/hwlab-node-lanes.yaml#lanes.v03.targets.NC01"
    unidesk.ai/effective-config-sha256: sha256:f42181ba9c079806968bb8374967a2bc710c7a87b56030d512cc00c004be126b
    unidesk.ai/source-artifact-renderer: hwlab-runtime-lane
    unidesk.ai/source-artifact-mode: remote-pipeline-annotation
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: gitops-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://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git
    - name: gitops-username
      type: string
      default: unidesk-admin
    - 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.nc01-v03.json
    - name: image-tag-mode
      type: string
      default: full
    - name: runtime-path
      type: string
      default: deploy/gitops/node/nc01/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-tasktree-api,hwlab-tasktree-worker,hwlab-harnessrl-api,hwlab-harnessrl-worker,hwlab-workbench-api,hwlab-workbench-worker,hwlab-hwpod-api,hwlab-hwpod-worker,hwlab-hwpod-web,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"
    - name: release-base-commit
      type: string
      description: Reviewed release plan base commit.
    - name: release-target
      type: string
      description: Reviewed release target.
    - name: release-consumer
      type: string
      description: Reviewed release consumer.
    - name: reviewed-plan
      type: string
      description: Reviewed release plan envelope supplied by the manual webhook.
  workspaces:
    - name: source
    - name: gitea-auth
  tasks:
    - name: prepare-source
      workspaces:
        - name: source
          workspace: source
      taskSpec:
        params:
          - name: git-url
          - name: git-read-url
          - name: gitops-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
        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: http://127.0.0.1:10808
              - name: NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: http_proxy
                value: http://127.0.0.1:10808
              - name: https_proxy
                value: http://127.0.0.1:10808
              - name: all_proxy
                value: http://127.0.0.1:10808
              - name: no_proxy
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: HWLAB_NODE_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_ALL_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com"
              - name: HOME
                value: /tekton/home
              - name: XDG_CONFIG_HOME
                value: /tekton/home/.config
            script: "# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n  if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n  mkdir -p \"$ci_node_deps_target\"\n  for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/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_read_url=\"$(params.git-read-url)\"\ngitops_read_url=\"$(params.gitops-read-url)\"\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 \"$gitops_read_url\" \"$(params.gitops-branch)\" >/dev/null; then\n  git remote set-url gitops-catalog \"$gitops_read_url\" 2>/dev/null || git remote add gitops-catalog \"$gitops_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"
            imagePullPolicy: Always
      params:
        - name: git-url
          value: $(params.git-url)
        - name: git-read-url
          value: $(params.git-read-url)
        - name: gitops-read-url
          value: $(params.gitops-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: http://127.0.0.1:10808
              - name: NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: http_proxy
                value: http://127.0.0.1:10808
              - name: https_proxy
                value: http://127.0.0.1:10808
              - name: all_proxy
                value: http://127.0.0.1:10808
              - name: no_proxy
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: HWLAB_NODE_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_ALL_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com"
              - name: HOME
                value: /tekton/home
              - name: XDG_CONFIG_HOME
                value: /tekton/home/.config
            script: "# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n  if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n  mkdir -p \"$ci_node_deps_target\"\n  for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/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"
            imagePullPolicy: Always
      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: http://127.0.0.1:10808
              - name: NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: http_proxy
                value: http://127.0.0.1:10808
              - name: https_proxy
                value: http://127.0.0.1:10808
              - name: all_proxy
                value: http://127.0.0.1:10808
              - name: no_proxy
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: HWLAB_NODE_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_ALL_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com"
              - name: HOME
                value: /tekton/home
              - name: XDG_CONFIG_HOME
                value: /tekton/home/.config
            script: "# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n  if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n  mkdir -p \"$ci_node_deps_target\"\n  for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/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"
            imagePullPolicy: Always
      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: http://127.0.0.1:10808
              - name: NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: http_proxy
                value: http://127.0.0.1:10808
              - name: https_proxy
                value: http://127.0.0.1:10808
              - name: all_proxy
                value: http://127.0.0.1:10808
              - name: no_proxy
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: HWLAB_NODE_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_ALL_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com"
              - name: HOME
                value: /tekton/home
              - name: XDG_CONFIG_HOME
                value: /tekton/home/.config
            script: "# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n  if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n  mkdir -p \"$ci_node_deps_target\"\n  for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/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"
            imagePullPolicy: Always
      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: gitops-read-url
          - name: gitops-branch
          - name: lane
          - name: revision
          - name: catalog-path
          - name: registry-prefix
          - name: services
          - name: release-base-commit
          - name: release-target
          - name: release-consumer
          - name: reviewed-plan
        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-tasktree-api
            description: hwlab-tasktree-api rollout affected according to ci-plan
          - name: build-hwlab-tasktree-api
            description: hwlab-tasktree-api image build required according to ci-plan
          - name: affected-hwlab-tasktree-worker
            description: hwlab-tasktree-worker rollout affected according to ci-plan
          - name: build-hwlab-tasktree-worker
            description: hwlab-tasktree-worker image build required according to ci-plan
          - name: affected-hwlab-harnessrl-api
            description: hwlab-harnessrl-api rollout affected according to ci-plan
          - name: build-hwlab-harnessrl-api
            description: hwlab-harnessrl-api image build required according to ci-plan
          - name: affected-hwlab-harnessrl-worker
            description: hwlab-harnessrl-worker rollout affected according to ci-plan
          - name: build-hwlab-harnessrl-worker
            description: hwlab-harnessrl-worker image build required according to ci-plan
          - name: affected-hwlab-workbench-api
            description: hwlab-workbench-api rollout affected according to ci-plan
          - name: build-hwlab-workbench-api
            description: hwlab-workbench-api image build required according to ci-plan
          - name: affected-hwlab-workbench-worker
            description: hwlab-workbench-worker rollout affected according to ci-plan
          - name: build-hwlab-workbench-worker
            description: hwlab-workbench-worker image build required according to ci-plan
          - name: affected-hwlab-hwpod-api
            description: hwlab-hwpod-api rollout affected according to ci-plan
          - name: build-hwlab-hwpod-api
            description: hwlab-hwpod-api image build required according to ci-plan
          - name: affected-hwlab-hwpod-worker
            description: hwlab-hwpod-worker rollout affected according to ci-plan
          - name: build-hwlab-hwpod-worker
            description: hwlab-hwpod-worker image build required according to ci-plan
          - name: affected-hwlab-hwpod-web
            description: hwlab-hwpod-web rollout affected according to ci-plan
          - name: build-hwlab-hwpod-web
            description: hwlab-hwpod-web 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: http://127.0.0.1:10808
              - name: NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: http_proxy
                value: http://127.0.0.1:10808
              - name: https_proxy
                value: http://127.0.0.1:10808
              - name: all_proxy
                value: http://127.0.0.1:10808
              - name: no_proxy
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: HWLAB_NODE_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_ALL_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com"
              - name: HOME
                value: /tekton/home
              - name: XDG_CONFIG_HOME
                value: /tekton/home/.config
            script: "# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n  if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n  mkdir -p \"$ci_node_deps_target\"\n  for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/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.gitops-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)\" --base-ref \"$(params.release-base-commit)\" --target-ref HEAD --deploy-config deploy/deploy.yaml --artifact-catalog \"$(params.catalog-path)\" --registry-prefix \"$(params.registry-prefix)\" --services \"$(params.services)\" --release-target \"$(params.release-target)\" --release-consumer \"$(params.release-consumer)\" --verify-reuse-registry > /workspace/source/ci-plan.json\nnode scripts/ci/verify-reviewed-plan.mjs --actual-plan /workspace/source/ci-plan.json --reviewed-plan \"$(params.reviewed-plan)\"\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-gitea-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  planIdentity: plan.planIdentity || null,\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, planIdentity: plan.planIdentity || null, 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"
            imagePullPolicy: Always
      params:
        - name: git-url
          value: $(params.git-url)
        - name: gitops-read-url
          value: $(params.gitops-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: release-base-commit
          value: $(params.release-base-commit)
        - name: release-target
          value: $(params.release-target)
        - name: release-consumer
          value: $(params.release-consumer)
        - name: reviewed-plan
          value: $(params.reviewed-plan)
    - name: build-services
      runAfter:
        - plan-artifacts
      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
          - name: build-cache-mode
        results:
          []
        workspaces:
          - name: source
        volumes:
          - name: service-work
            emptyDir:
              {}
          - name: buildkit-bin
            emptyDir:
              {}
          - name: buildkit-run
            emptyDir:
              {}
        sidecars:
          - name: buildkitd
            image: 127.0.0.1:5000/hwlab/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: http://127.0.0.1:10808
              - name: NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: http_proxy
                value: http://127.0.0.1:10808
              - name: https_proxy
                value: http://127.0.0.1:10808
              - name: all_proxy
                value: http://127.0.0.1:10808
              - name: no_proxy
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
            volumeMounts:
              - name: buildkit-run
                mountPath: /workspace/buildkit-run
            securityContext:
              runAsUser: 1000
              runAsGroup: 1000
              allowPrivilegeEscalation: true
              appArmorProfile:
                type: Unconfined
              seccompProfile:
                type: Unconfined
            computeResources:
              requests:
                cpu: 250m
                memory: 256Mi
              limits:
                cpu: 1500m
                memory: 1536Mi
        steps:
          - name: prepare-buildkit-client
            image: 127.0.0.1:5000/hwlab/buildkit:rootless
            securityContext:
              runAsUser: 0
              runAsGroup: 0
            volumeMounts:
              - name: buildkit-bin
                mountPath: /workspace/buildkit-bin
            script: "# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n  if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n  mkdir -p \"$ci_node_deps_target\"\n  for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/bin/sh\nset -eu\nmkdir -p /workspace/buildkit-bin\ncp /usr/bin/buildctl /workspace/buildkit-bin/\nchmod +x /workspace/buildkit-bin/*\n"
            computeResources:
              requests:
                cpu: 100m
                memory: 128Mi
              limits:
                cpu: 750m
                memory: 768Mi
            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: http://127.0.0.1:10808
              - name: NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: http_proxy
                value: http://127.0.0.1:10808
              - name: https_proxy
                value: http://127.0.0.1:10808
              - name: all_proxy
                value: http://127.0.0.1:10808
              - name: no_proxy
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: HWLAB_NODE_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_ALL_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com"
              - name: HOME
                value: /tekton/home
              - name: XDG_CONFIG_HOME
                value: /tekton/home/.config
          - 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: http://127.0.0.1:10808
              - name: NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: http_proxy
                value: http://127.0.0.1:10808
              - name: https_proxy
                value: http://127.0.0.1:10808
              - name: all_proxy
                value: http://127.0.0.1:10808
              - name: no_proxy
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: HWLAB_NODE_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_ALL_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com"
              - name: HOME
                value: /tekton/home
              - name: XDG_CONFIG_HOME
                value: /tekton/home/.config
            volumeMounts:
              - name: service-work
                mountPath: /workspace/service-work
              - name: buildkit-bin
                mountPath: /workspace/buildkit-bin
              - name: buildkit-run
                mountPath: /workspace/buildkit-run
            script: "set -eu\nbuild_service() {\n  service_id=\"$1\"\n  mkdir -p \"/workspace/service-work/$service_id\" \"/workspace/service-results-tekton/$service_id\"\n  export HWLAB_TEKTON_RESULTS_DIR=\"/workspace/service-results-tekton/$service_id\"\n# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n  if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n  mkdir -p \"$ci_node_deps_target\"\n  for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/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-$service_id\"\nexport HWLAB_SOURCE_REVISION=\"$(params.revision)\"\nexport HWLAB_TIMING_SERVICE_ID=\"$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 \"<invalid-proxy-url>\";\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\":\"'\"$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/$service_id/home\nexport HOME=/workspace/service-work/$service_id/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);' \"$service_id\"; then\n  node - \"$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-gitea-checkout\" || service.runtimeMode === \"env-reuse-gitea-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-gitea-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(process.env.HWLAB_TEKTON_RESULTS_DIR + \"/\" + name, String(value || \"\"));\nNODE\n  echo '{\"event\":\"service-build-skip\",\"serviceId\":\"'\"$service_id\"'\",\"reason\":\"component-inputs-unchanged\"}'\n  exit 0\nfi\nrm -rf /workspace/service-work/$service_id/repo\nmkdir -p /workspace/service-work/$service_id/repo\ntar -C /workspace/source/repo -cf - . | tar -C /workspace/service-work/$service_id/repo -xo -f -\ncd /workspace/service-work/$service_id/repo\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)-$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\":\"'\"$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\":\"'\"$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\":\"'\"$service_id\"'\",\"addr\":\"'\"$HWLAB_BUILDKIT_ADDR\"'\"}' >&2\n  exit 1\nfi\nHWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY=1 node - <<'NODE_UNIDESK_DEPLOY_YAML_OVERLAY'\nconst fs = require('fs');\nconst YAML = require('yaml');\nconst overlay = {\"nodeId\":\"NC01\",\"lane\":\"v03\",\"sourceBranch\":\"v0.3\",\"gitopsBranch\":\"v0.3-gitops\",\"gitopsRoot\":\"deploy/gitops/node/nc01\",\"runtimePath\":\"deploy/gitops/node/nc01/runtime-v03\",\"runtimeRenderDir\":\"runtime-v03\",\"runtimeNamespace\":\"hwlab-v03\",\"environment\":\"development\",\"catalogPath\":\"deploy/artifact-catalog.nc01-v03.json\",\"gitUrl\":\"git@github.com:pikasTech/HWLAB.git\",\"publicWebUrl\":\"https://lab-dev.hwpod.com\",\"publicApiUrl\":\"https://lab-dev.hwpod.com\",\"externalPostgres\":{\"enabled\":true,\"serviceName\":\"nc01-host-postgres\",\"endpointAddress\":\"10.42.0.1\",\"port\":5432,\"runtimeAccess\":{\"routeName\":\"nc01-host-postgres\",\"endpointAddress\":\"10.42.0.1\",\"port\":5432},\"sslmode\":\"require\"},\"runtimeStore\":{\"postgres\":{\"mode\":\"platform-service\",\"serviceName\":\"nc01-host-postgres\",\"poolMax\":16}},\"codeAgentRuntime\":{\"enabled\":true,\"adapter\":\"agentrun-v02\",\"managerUrl\":\"http://agentrun-mgr.agentrun-v02.svc.cluster.local:8080\",\"apiKeySecretName\":\"hwlab-v03-master-server-admin-api-key\",\"apiKeySecretKey\":\"api-key\",\"runnerNamespace\":\"agentrun-v02\",\"secretNamespace\":\"agentrun-v02\",\"providerSecretNames\":{\"codex\":\"agentrun-v01-provider-codex\",\"gpt-pika\":\"agentrun-v02-provider-gpt-pika\",\"grok\":\"agentrun-v02-provider-grok\",\"dsflash-go\":\"agentrun-v01-provider-dsflash-go\"},\"toolSecretNames\":{\"githubPr\":\"agentrun-v01-tool-github-pr\",\"unideskSsh\":\"agentrun-v01-tool-unidesk-ssh\"},\"repoUrlFrom\":\"runtimeGitReadUrl\",\"repoUrl\":\"http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git\",\"providerIdFrom\":\"runtimeNodeId\",\"providerId\":\"NC01\",\"defaultProviderProfile\":\"gpt.pika\",\"codexStdioSupervisor\":\"repo-owned\",\"kafkaShadowProducer\":null,\"kafkaEventBridge\":{\"enabled\":true,\"features\":{\"directPublish\":true,\"liveKafkaSse\":true,\"kafkaRefreshReplay\":true,\"transactionalProjector\":false,\"projectionOutboxRelay\":false,\"projectionRealtime\":false},\"refreshReplay\":{\"groupIdPrefix\":\"hwlab-v03-cloud-api-sse\",\"timeoutMs\":30000,\"scanLimit\":1000000,\"matchedEventLimit\":2000,\"liveBufferLimit\":2000},\"configRef\":\"config/platform-infra/kafka.yaml#clients.hwlab-v03-cloud-api\",\"bootstrapServers\":\"platform-infra-kafka-kafka-bootstrap.platform-infra.svc.cluster.local:9092\",\"stdioTopic\":\"codex-stdio.raw.v1\",\"agentRunEventTopic\":\"agentrun.event.v1\",\"hwlabEventTopic\":\"hwlab.event.v1\",\"clientId\":\"hwlab-v03-cloud-api\",\"directPublishConsumerGroupId\":\"hwlab-agentrun-event-direct-publish\",\"transactionalProjectorConsumerGroupId\":\"hwlab-v03-agentrun-event-projector\",\"hwlabEventConsumerGroupId\":\"hwlab-v03-workbench-live-sse\"},\"valuesPrinted\":false},\"observability\":{\"prometheusOperator\":false,\"webProbe\":{\"sentinels\":[{\"id\":\"nc01-web-probe-sentinel\",\"enabled\":true,\"configRef\":\"config/hwlab-web-probe-sentinel/profiles.yaml#nodes.NC01.sentinels.nc01-web-probe-sentinel.sentinel\"}],\"monitor\":{\"configRef\":\"config/hwlab-web-probe-monitor/runtime.yaml#monitor\"},\"monitorRoot\":{\"enabled\":true,\"sentinelId\":\"nc01-web-probe-sentinel\",\"publicBaseUrl\":\"https://monitor.pikapython.com\",\"routePrefix\":\"/\",\"caddyManagedBlockOwner\":\"hwlab-web-probe-sentinel-active-root\"}},\"recordingRules\":[],\"warningAlerts\":[]},\"runtimeImageRewrites\":[{\"source\":\"fatedier/frpc:v0.68.1\",\"target\":\"127.0.0.1:5000/hwlab/frpc:v0.68.1\"},{\"source\":\"openfga/openfga:v1.17.0\",\"target\":\"127.0.0.1:5000/hwlab/openfga:v1.17.0\"},{\"source\":\"ghcr.io/anomalyco/opencode:1.17.7\",\"target\":\"127.0.0.1:5000/hwlab/opencode:1.17.7\"}],\"dockerProxyHttp\":\"http://127.0.0.1:10808\",\"dockerProxyHttps\":\"http://127.0.0.1:10808\",\"dockerNoProxyList\":[\"localhost\",\"127.0.0.1\",\"::1\",\"127.0.0.1:5000\",\"localhost:5000\",\".svc\",\".svc.cluster.local\",\".cluster.local\",\"hyueapi.com\",\".hyueapi.com\"],\"npmRegistry\":\"https://registry.npmmirror.com/\",\"npmFetchTimeoutMs\":120000,\"npmRetries\":3};\nconst file = 'deploy/deploy.yaml';\nif (!fs.existsSync(file)) {\n  console.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: false, reason: 'deploy-yaml-missing', file }));\n  process.exit(45);\n}\nconst doc = YAML.parse(fs.readFileSync(file, 'utf8'));\ndoc.nodes = doc.nodes || {};\ndoc.nodes[overlay.nodeId] = { ...(doc.nodes[overlay.nodeId] || {}), gitopsRoot: overlay.gitopsRoot, sourceRepo: overlay.gitUrl };\ndoc.lanes = doc.lanes || {};\nconst lane = doc.lanes[overlay.lane] || {};\nconst envRecipe = lane.envRecipe || {};\nconst downloadStack = {\n  ...(envRecipe.downloadStack || {}),\n  httpProxy: overlay.dockerProxyHttp,\n  httpsProxy: overlay.dockerProxyHttps,\n  noProxy: overlay.dockerNoProxyList,\n};\nif (overlay.npmRegistry) downloadStack.npmRegistry = overlay.npmRegistry;\nif (overlay.npmFetchTimeoutMs) downloadStack.npmFetchTimeoutMs = overlay.npmFetchTimeoutMs;\ndoc.lanes[overlay.lane] = {\n  ...lane,\n  node: overlay.nodeId,\n  sourceBranch: overlay.sourceBranch,\n  gitopsBranch: overlay.gitopsBranch,\n  namespace: overlay.runtimeNamespace,\n  endpoint: overlay.publicApiUrl,\n  publicEndpoints: { frontend: overlay.publicWebUrl, api: overlay.publicApiUrl },\n  artifactCatalog: overlay.catalogPath,\n  runtimePath: overlay.runtimeRenderDir,\n  imageTagMode: 'full',\n  sourceRepo: overlay.gitUrl,\n  observability: overlay.observability,\n  envRecipe: { ...envRecipe, downloadStack },\n};\nif (overlay.externalPostgres === undefined || overlay.externalPostgres === null) delete doc.lanes[overlay.lane].externalPostgres;\nelse doc.lanes[overlay.lane].externalPostgres = overlay.externalPostgres;\nif (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore;\nif (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime;\nfs.writeFileSync(file, YAML.stringify(doc));\nconsole.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: true, lane: overlay.lane, httpProxy: overlay.dockerProxyHttp, noProxyCount: overlay.dockerNoProxyList.length }));\nNODE_UNIDESK_DEPLOY_YAML_OVERLAY\nnode 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/$service_id.json\" --tekton-results-dir \"$HWLAB_TEKTON_RESULTS_DIR\" --quiet-build --concurrency 1 --services \"$service_id\" --buildkit-command /workspace/buildkit-bin/buildctl --buildkit-addr \"$HWLAB_BUILDKIT_ADDR\" --build-cache-mode \"$(params.build-cache-mode)\"\n\n}\nmax_parallel=2\nservice_csv=\"$(params.services)\"\nold_ifs=\"$IFS\"\nIFS=','\nset -- $service_csv\nIFS=\"$old_ifs\"\nfailed=0\nwhile [ \"$#\" -gt 0 ]; do\n  pids=\"\"\n  launched=0\n  while [ \"$#\" -gt 0 ] && [ \"$launched\" -lt \"$max_parallel\" ]; do\n    service_id=\"$1\"\n    shift\n    (build_service \"$service_id\") &\n    pids=\"$pids $!\"\n    launched=$((launched + 1))\n  done\n  for pid in $pids; do if ! wait \"$pid\"; then failed=1; fi; done\n  if [ \"$failed\" -ne 0 ]; then exit \"$failed\"; fi\ndone"
            imagePullPolicy: Always
            computeResources:
              requests:
                cpu: 100m
                memory: 128Mi
              limits:
                cpu: 750m
                memory: 768Mi
      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: base-image
          value: $(params.base-image)
        - name: build-cache-mode
          value: $(params.build-cache-mode)
        - name: services
          value: $(params.services)
      timeout: 20m
    - name: collect-artifacts
      runAfter:
        - build-services
      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: http://127.0.0.1:10808
              - name: NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: http_proxy
                value: http://127.0.0.1:10808
              - name: https_proxy
                value: http://127.0.0.1:10808
              - name: all_proxy
                value: http://127.0.0.1:10808
              - name: no_proxy
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: HWLAB_NODE_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_ALL_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com"
              - name: HOME
                value: /tekton/home
              - name: XDG_CONFIG_HOME
                value: /tekton/home/.config
            script: "# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n  if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n  mkdir -p \"$ci_node_deps_target\"\n  for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/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 - <<'NODE_UNIDESK_DEPLOY_YAML_OVERLAY'\nconst fs = require('fs');\nconst YAML = require('yaml');\nconst overlay = {\"nodeId\":\"NC01\",\"lane\":\"v03\",\"sourceBranch\":\"v0.3\",\"gitopsBranch\":\"v0.3-gitops\",\"gitopsRoot\":\"deploy/gitops/node/nc01\",\"runtimePath\":\"deploy/gitops/node/nc01/runtime-v03\",\"runtimeRenderDir\":\"runtime-v03\",\"runtimeNamespace\":\"hwlab-v03\",\"environment\":\"development\",\"catalogPath\":\"deploy/artifact-catalog.nc01-v03.json\",\"gitUrl\":\"git@github.com:pikasTech/HWLAB.git\",\"publicWebUrl\":\"https://lab-dev.hwpod.com\",\"publicApiUrl\":\"https://lab-dev.hwpod.com\",\"externalPostgres\":{\"enabled\":true,\"serviceName\":\"nc01-host-postgres\",\"endpointAddress\":\"10.42.0.1\",\"port\":5432,\"runtimeAccess\":{\"routeName\":\"nc01-host-postgres\",\"endpointAddress\":\"10.42.0.1\",\"port\":5432},\"sslmode\":\"require\"},\"runtimeStore\":{\"postgres\":{\"mode\":\"platform-service\",\"serviceName\":\"nc01-host-postgres\",\"poolMax\":16}},\"codeAgentRuntime\":{\"enabled\":true,\"adapter\":\"agentrun-v02\",\"managerUrl\":\"http://agentrun-mgr.agentrun-v02.svc.cluster.local:8080\",\"apiKeySecretName\":\"hwlab-v03-master-server-admin-api-key\",\"apiKeySecretKey\":\"api-key\",\"runnerNamespace\":\"agentrun-v02\",\"secretNamespace\":\"agentrun-v02\",\"providerSecretNames\":{\"codex\":\"agentrun-v01-provider-codex\",\"gpt-pika\":\"agentrun-v02-provider-gpt-pika\",\"grok\":\"agentrun-v02-provider-grok\",\"dsflash-go\":\"agentrun-v01-provider-dsflash-go\"},\"toolSecretNames\":{\"githubPr\":\"agentrun-v01-tool-github-pr\",\"unideskSsh\":\"agentrun-v01-tool-unidesk-ssh\"},\"repoUrlFrom\":\"runtimeGitReadUrl\",\"repoUrl\":\"http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git\",\"providerIdFrom\":\"runtimeNodeId\",\"providerId\":\"NC01\",\"defaultProviderProfile\":\"gpt.pika\",\"codexStdioSupervisor\":\"repo-owned\",\"kafkaShadowProducer\":null,\"kafkaEventBridge\":{\"enabled\":true,\"features\":{\"directPublish\":true,\"liveKafkaSse\":true,\"kafkaRefreshReplay\":true,\"transactionalProjector\":false,\"projectionOutboxRelay\":false,\"projectionRealtime\":false},\"refreshReplay\":{\"groupIdPrefix\":\"hwlab-v03-cloud-api-sse\",\"timeoutMs\":30000,\"scanLimit\":1000000,\"matchedEventLimit\":2000,\"liveBufferLimit\":2000},\"configRef\":\"config/platform-infra/kafka.yaml#clients.hwlab-v03-cloud-api\",\"bootstrapServers\":\"platform-infra-kafka-kafka-bootstrap.platform-infra.svc.cluster.local:9092\",\"stdioTopic\":\"codex-stdio.raw.v1\",\"agentRunEventTopic\":\"agentrun.event.v1\",\"hwlabEventTopic\":\"hwlab.event.v1\",\"clientId\":\"hwlab-v03-cloud-api\",\"directPublishConsumerGroupId\":\"hwlab-agentrun-event-direct-publish\",\"transactionalProjectorConsumerGroupId\":\"hwlab-v03-agentrun-event-projector\",\"hwlabEventConsumerGroupId\":\"hwlab-v03-workbench-live-sse\"},\"valuesPrinted\":false},\"observability\":{\"prometheusOperator\":false,\"webProbe\":{\"sentinels\":[{\"id\":\"nc01-web-probe-sentinel\",\"enabled\":true,\"configRef\":\"config/hwlab-web-probe-sentinel/profiles.yaml#nodes.NC01.sentinels.nc01-web-probe-sentinel.sentinel\"}],\"monitor\":{\"configRef\":\"config/hwlab-web-probe-monitor/runtime.yaml#monitor\"},\"monitorRoot\":{\"enabled\":true,\"sentinelId\":\"nc01-web-probe-sentinel\",\"publicBaseUrl\":\"https://monitor.pikapython.com\",\"routePrefix\":\"/\",\"caddyManagedBlockOwner\":\"hwlab-web-probe-sentinel-active-root\"}},\"recordingRules\":[],\"warningAlerts\":[]},\"runtimeImageRewrites\":[{\"source\":\"fatedier/frpc:v0.68.1\",\"target\":\"127.0.0.1:5000/hwlab/frpc:v0.68.1\"},{\"source\":\"openfga/openfga:v1.17.0\",\"target\":\"127.0.0.1:5000/hwlab/openfga:v1.17.0\"},{\"source\":\"ghcr.io/anomalyco/opencode:1.17.7\",\"target\":\"127.0.0.1:5000/hwlab/opencode:1.17.7\"}],\"dockerProxyHttp\":\"http://127.0.0.1:10808\",\"dockerProxyHttps\":\"http://127.0.0.1:10808\",\"dockerNoProxyList\":[\"localhost\",\"127.0.0.1\",\"::1\",\"127.0.0.1:5000\",\"localhost:5000\",\".svc\",\".svc.cluster.local\",\".cluster.local\",\"hyueapi.com\",\".hyueapi.com\"],\"npmRegistry\":\"https://registry.npmmirror.com/\",\"npmFetchTimeoutMs\":120000,\"npmRetries\":3};\nconst file = 'deploy/deploy.yaml';\nif (!fs.existsSync(file)) {\n  console.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: false, reason: 'deploy-yaml-missing', file }));\n  process.exit(45);\n}\nconst doc = YAML.parse(fs.readFileSync(file, 'utf8'));\ndoc.nodes = doc.nodes || {};\ndoc.nodes[overlay.nodeId] = { ...(doc.nodes[overlay.nodeId] || {}), gitopsRoot: overlay.gitopsRoot, sourceRepo: overlay.gitUrl };\ndoc.lanes = doc.lanes || {};\nconst lane = doc.lanes[overlay.lane] || {};\nconst envRecipe = lane.envRecipe || {};\nconst downloadStack = {\n  ...(envRecipe.downloadStack || {}),\n  httpProxy: overlay.dockerProxyHttp,\n  httpsProxy: overlay.dockerProxyHttps,\n  noProxy: overlay.dockerNoProxyList,\n};\nif (overlay.npmRegistry) downloadStack.npmRegistry = overlay.npmRegistry;\nif (overlay.npmFetchTimeoutMs) downloadStack.npmFetchTimeoutMs = overlay.npmFetchTimeoutMs;\ndoc.lanes[overlay.lane] = {\n  ...lane,\n  node: overlay.nodeId,\n  sourceBranch: overlay.sourceBranch,\n  gitopsBranch: overlay.gitopsBranch,\n  namespace: overlay.runtimeNamespace,\n  endpoint: overlay.publicApiUrl,\n  publicEndpoints: { frontend: overlay.publicWebUrl, api: overlay.publicApiUrl },\n  artifactCatalog: overlay.catalogPath,\n  runtimePath: overlay.runtimeRenderDir,\n  imageTagMode: 'full',\n  sourceRepo: overlay.gitUrl,\n  observability: overlay.observability,\n  envRecipe: { ...envRecipe, downloadStack },\n};\nif (overlay.externalPostgres === undefined || overlay.externalPostgres === null) delete doc.lanes[overlay.lane].externalPostgres;\nelse doc.lanes[overlay.lane].externalPostgres = overlay.externalPostgres;\nif (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore;\nif (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime;\nfs.writeFileSync(file, YAML.stringify(doc));\nconsole.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: true, lane: overlay.lane, httpProxy: overlay.dockerProxyHttp, noProxyCount: overlay.dockerNoProxyList.length }));\nNODE_UNIDESK_DEPLOY_YAML_OVERLAY\nnode 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"
            imagePullPolicy: Always
      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: gitea-auth
          workspace: gitea-auth
      taskSpec:
        params:
          - name: git-url
          - name: git-read-url
          - name: gitops-read-url
          - name: git-write-url
          - name: gitops-username
          - 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: gitea-auth
        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: http://127.0.0.1:10808
              - name: NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: http_proxy
                value: http://127.0.0.1:10808
              - name: https_proxy
                value: http://127.0.0.1:10808
              - name: all_proxy
                value: http://127.0.0.1:10808
              - name: no_proxy
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.16.0.0/12,192.168.0.0/16,82.156.23.220,74.48.78.17,hyueapi.com,.hyueapi.com"
              - name: HWLAB_NODE_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_ALL_PROXY_URL
                value: http://127.0.0.1:10808
              - name: HWLAB_NODE_NO_PROXY
                value: "localhost,127.0.0.1,::1,127.0.0.1:5000,localhost:5000,.svc,.svc.cluster.local,.cluster.local,hyueapi.com,.hyueapi.com"
              - name: HOME
                value: /tekton/home
              - name: XDG_CONFIG_HOME
                value: /tekton/home/.config
            script: "# unidesk-step-env-bootstrap\nexport HOME='/tekton/home'\nexport XDG_CONFIG_HOME='/tekton/home/.config'\nmkdir -p \"$HOME\"\nmkdir -p \"$XDG_CONFIG_HOME\"\nci_node_deps=\"${HWLAB_CI_NODE_DEPS:-/opt/hwlab-ci-node-deps/node_modules}\"\nif [ -d \"$ci_node_deps\" ]; then\n  if [ -d /workspace/source/repo ]; then ci_node_deps_target=/workspace/source/repo/node_modules; else ci_node_deps_target=./node_modules; fi\n  mkdir -p \"$ci_node_deps_target\"\n  for ci_node_dep in yaml; do if [ -d \"$ci_node_deps/$ci_node_dep\" ] && [ ! -e \"$ci_node_deps_target/$ci_node_dep\" ]; then ln -s \"$ci_node_deps/$ci_node_dep\" \"$ci_node_deps_target/$ci_node_dep\"; fi; done\nfi\n#!/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\nlane=\"$(params.lane)\"\ncase \"$lane\" in\n  node) runtime_lane=false ;;\n  *) runtime_lane=true ;;\nesac\nif [ \"$runtime_lane\" = \"true\" ]; then\n  printf 'false' > /tekton/results/runtime-ready-required\nelse\n  printf 'true' > /tekton/results/runtime-ready-required\nfi\ntest -s /workspace/gitea-auth/token || { echo '{\"event\":\"gitea-auth\",\"phase\":\"gitops-promote\",\"status\":\"failed\",\"reason\":\"token-missing\"}'; exit 31; }\naskpass_dir=\"$(mktemp -d)\"\ncat > \"$askpass_dir/askpass.sh\" <<'SH'\n#!/bin/sh\ncase \"$1\" in\n  *sername*) printf '%s\\n' \"$GITEA_USERNAME\" ;;\n  *) cat /workspace/gitea-auth/token ;;\nesac\nSH\nchmod 0700 \"$askpass_dir/askpass.sh\"\nexport GITEA_USERNAME=\"$(params.gitops-username)\"\nexport GIT_ASKPASS=\"$askpass_dir/askpass.sh\"\nexport GIT_ASKPASS_REQUIRE=force\nexport GIT_TERMINAL_PROMPT=0\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-read-url)\"\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 : [];\nprocess.exit(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\":\"continuing\",\"reason\":\"no-build-no-rollout-plan-gitops-verify\"}' >&2\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)\"\nunidesk_legacy_runtime_path='deploy/gitops/node/runtime-v03'\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 - <<'NODE_UNIDESK_DEPLOY_YAML_OVERLAY'\nconst fs = require('fs');\nconst YAML = require('yaml');\nconst overlay = {\"nodeId\":\"NC01\",\"lane\":\"v03\",\"sourceBranch\":\"v0.3\",\"gitopsBranch\":\"v0.3-gitops\",\"gitopsRoot\":\"deploy/gitops/node/nc01\",\"runtimePath\":\"deploy/gitops/node/nc01/runtime-v03\",\"runtimeRenderDir\":\"runtime-v03\",\"runtimeNamespace\":\"hwlab-v03\",\"environment\":\"development\",\"catalogPath\":\"deploy/artifact-catalog.nc01-v03.json\",\"gitUrl\":\"git@github.com:pikasTech/HWLAB.git\",\"publicWebUrl\":\"https://lab-dev.hwpod.com\",\"publicApiUrl\":\"https://lab-dev.hwpod.com\",\"externalPostgres\":{\"enabled\":true,\"serviceName\":\"nc01-host-postgres\",\"endpointAddress\":\"10.42.0.1\",\"port\":5432,\"runtimeAccess\":{\"routeName\":\"nc01-host-postgres\",\"endpointAddress\":\"10.42.0.1\",\"port\":5432},\"sslmode\":\"require\"},\"runtimeStore\":{\"postgres\":{\"mode\":\"platform-service\",\"serviceName\":\"nc01-host-postgres\",\"poolMax\":16}},\"codeAgentRuntime\":{\"enabled\":true,\"adapter\":\"agentrun-v02\",\"managerUrl\":\"http://agentrun-mgr.agentrun-v02.svc.cluster.local:8080\",\"apiKeySecretName\":\"hwlab-v03-master-server-admin-api-key\",\"apiKeySecretKey\":\"api-key\",\"runnerNamespace\":\"agentrun-v02\",\"secretNamespace\":\"agentrun-v02\",\"providerSecretNames\":{\"codex\":\"agentrun-v01-provider-codex\",\"gpt-pika\":\"agentrun-v02-provider-gpt-pika\",\"grok\":\"agentrun-v02-provider-grok\",\"dsflash-go\":\"agentrun-v01-provider-dsflash-go\"},\"toolSecretNames\":{\"githubPr\":\"agentrun-v01-tool-github-pr\",\"unideskSsh\":\"agentrun-v01-tool-unidesk-ssh\"},\"repoUrlFrom\":\"runtimeGitReadUrl\",\"repoUrl\":\"http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git\",\"providerIdFrom\":\"runtimeNodeId\",\"providerId\":\"NC01\",\"defaultProviderProfile\":\"gpt.pika\",\"codexStdioSupervisor\":\"repo-owned\",\"kafkaShadowProducer\":null,\"kafkaEventBridge\":{\"enabled\":true,\"features\":{\"directPublish\":true,\"liveKafkaSse\":true,\"kafkaRefreshReplay\":true,\"transactionalProjector\":false,\"projectionOutboxRelay\":false,\"projectionRealtime\":false},\"refreshReplay\":{\"groupIdPrefix\":\"hwlab-v03-cloud-api-sse\",\"timeoutMs\":30000,\"scanLimit\":1000000,\"matchedEventLimit\":2000,\"liveBufferLimit\":2000},\"configRef\":\"config/platform-infra/kafka.yaml#clients.hwlab-v03-cloud-api\",\"bootstrapServers\":\"platform-infra-kafka-kafka-bootstrap.platform-infra.svc.cluster.local:9092\",\"stdioTopic\":\"codex-stdio.raw.v1\",\"agentRunEventTopic\":\"agentrun.event.v1\",\"hwlabEventTopic\":\"hwlab.event.v1\",\"clientId\":\"hwlab-v03-cloud-api\",\"directPublishConsumerGroupId\":\"hwlab-agentrun-event-direct-publish\",\"transactionalProjectorConsumerGroupId\":\"hwlab-v03-agentrun-event-projector\",\"hwlabEventConsumerGroupId\":\"hwlab-v03-workbench-live-sse\"},\"valuesPrinted\":false},\"observability\":{\"prometheusOperator\":false,\"webProbe\":{\"sentinels\":[{\"id\":\"nc01-web-probe-sentinel\",\"enabled\":true,\"configRef\":\"config/hwlab-web-probe-sentinel/profiles.yaml#nodes.NC01.sentinels.nc01-web-probe-sentinel.sentinel\"}],\"monitor\":{\"configRef\":\"config/hwlab-web-probe-monitor/runtime.yaml#monitor\"},\"monitorRoot\":{\"enabled\":true,\"sentinelId\":\"nc01-web-probe-sentinel\",\"publicBaseUrl\":\"https://monitor.pikapython.com\",\"routePrefix\":\"/\",\"caddyManagedBlockOwner\":\"hwlab-web-probe-sentinel-active-root\"}},\"recordingRules\":[],\"warningAlerts\":[]},\"runtimeImageRewrites\":[{\"source\":\"fatedier/frpc:v0.68.1\",\"target\":\"127.0.0.1:5000/hwlab/frpc:v0.68.1\"},{\"source\":\"openfga/openfga:v1.17.0\",\"target\":\"127.0.0.1:5000/hwlab/openfga:v1.17.0\"},{\"source\":\"ghcr.io/anomalyco/opencode:1.17.7\",\"target\":\"127.0.0.1:5000/hwlab/opencode:1.17.7\"}],\"dockerProxyHttp\":\"http://127.0.0.1:10808\",\"dockerProxyHttps\":\"http://127.0.0.1:10808\",\"dockerNoProxyList\":[\"localhost\",\"127.0.0.1\",\"::1\",\"127.0.0.1:5000\",\"localhost:5000\",\".svc\",\".svc.cluster.local\",\".cluster.local\",\"hyueapi.com\",\".hyueapi.com\"],\"npmRegistry\":\"https://registry.npmmirror.com/\",\"npmFetchTimeoutMs\":120000,\"npmRetries\":3};\nconst file = 'deploy/deploy.yaml';\nif (!fs.existsSync(file)) {\n  console.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: false, reason: 'deploy-yaml-missing', file }));\n  process.exit(45);\n}\nconst doc = YAML.parse(fs.readFileSync(file, 'utf8'));\ndoc.nodes = doc.nodes || {};\ndoc.nodes[overlay.nodeId] = { ...(doc.nodes[overlay.nodeId] || {}), gitopsRoot: overlay.gitopsRoot, sourceRepo: overlay.gitUrl };\ndoc.lanes = doc.lanes || {};\nconst lane = doc.lanes[overlay.lane] || {};\nconst envRecipe = lane.envRecipe || {};\nconst downloadStack = {\n  ...(envRecipe.downloadStack || {}),\n  httpProxy: overlay.dockerProxyHttp,\n  httpsProxy: overlay.dockerProxyHttps,\n  noProxy: overlay.dockerNoProxyList,\n};\nif (overlay.npmRegistry) downloadStack.npmRegistry = overlay.npmRegistry;\nif (overlay.npmFetchTimeoutMs) downloadStack.npmFetchTimeoutMs = overlay.npmFetchTimeoutMs;\ndoc.lanes[overlay.lane] = {\n  ...lane,\n  node: overlay.nodeId,\n  sourceBranch: overlay.sourceBranch,\n  gitopsBranch: overlay.gitopsBranch,\n  namespace: overlay.runtimeNamespace,\n  endpoint: overlay.publicApiUrl,\n  publicEndpoints: { frontend: overlay.publicWebUrl, api: overlay.publicApiUrl },\n  artifactCatalog: overlay.catalogPath,\n  runtimePath: overlay.runtimeRenderDir,\n  imageTagMode: 'full',\n  sourceRepo: overlay.gitUrl,\n  observability: overlay.observability,\n  envRecipe: { ...envRecipe, downloadStack },\n};\nif (overlay.externalPostgres === undefined || overlay.externalPostgres === null) delete doc.lanes[overlay.lane].externalPostgres;\nelse doc.lanes[overlay.lane].externalPostgres = overlay.externalPostgres;\nif (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore;\nif (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime;\nfs.writeFileSync(file, YAML.stringify(doc));\nconsole.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: true, lane: overlay.lane, httpProxy: overlay.dockerProxyHttp, noProxyCount: overlay.dockerNoProxyList.length }));\nNODE_UNIDESK_DEPLOY_YAML_OVERLAY\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 \"deploy/gitops/node/nc01\" --out \"deploy/gitops/node/nc01\" --registry-prefix \"$(params.registry-prefix)\" --use-deploy-images\nUNIDESK_RUNTIME_GITOPS_OVERLAY_FILE=/etc/unidesk-cicd-runtime-gitops/runtime-gitops-overlay.json node /etc/unidesk-cicd-runtime-gitops/runtime-gitops-postprocess.mjs\nfeature_config_schema_started_ms=\"$(date +%s%3N 2>/dev/null || date +%s000)\"\nexport UNIDESK_FEATURE_CONFIG_REPO_DIR=\"$PWD\"\nexport UNIDESK_FEATURE_CONFIG_RENDERED_ROOT=\"deploy/gitops/node/nc01/runtime-v03\"\nexport UNIDESK_PAC_CONSUMER='hwlab-nc01-v03'\nexport OTEL_EXPORTER_OTLP_TRACES_ENDPOINT='http://otel-collector.platform-infra.svc.cluster.local:4318/v1/traces'\nexport OTEL_SERVICE_NAME='unidesk-cicd'\nexport OTEL_TRACES_SAMPLER_ARG='1'\nexport OTEL_EXPORTER_TIMEOUT_MS='300'\nfeature_config_validator_base=''\nfeature_config_validator=''\nif feature_config_validator_base=\"$(mktemp \"${TMPDIR:-/tmp}/unidesk-feature-config-validator.XXXXXX\" 2>/dev/null)\"; then\n  feature_config_validator=\"${feature_config_validator_base}.mjs\"\n  if ! printf '%s' '[encoded data, 6160 characters]' | base64 -d | gzip -d >\"$feature_config_validator\"; then\n    rm -f \"$feature_config_validator_base\" \"$feature_config_validator\" || true\n    feature_config_validator=''\n  fi\n  rm -f \"$feature_config_validator_base\" || true\nfi\nexport UNIDESK_AJV2020_BUNDLE='/etc/unidesk-cicd-runtime-gitops/ajv2020.min.js'\nexport UNIDESK_FEATURE_CONFIG_VALIDATOR=\"${feature_config_validator:-${TMPDIR:-/tmp}/unidesk-feature-config-validator-unavailable.mjs}\"\nif ! node --input-type=module <<'NODE_UNIDESK_FEATURE_CONFIG_SCHEMA'\nimport { pathToFileURL } from 'node:url';\nconst validator = await import(pathToFileURL(process.env.UNIDESK_FEATURE_CONFIG_VALIDATOR).href);\nawait validator.runFeatureConfigSchemaValidation({ repoDir: process.env.UNIDESK_FEATURE_CONFIG_REPO_DIR, renderedRoot: process.env.UNIDESK_FEATURE_CONFIG_RENDERED_ROOT, consumer: process.env.UNIDESK_PAC_CONSUMER });\nNODE_UNIDESK_FEATURE_CONFIG_SCHEMA\nthen\n  printf '{\"event\":\"feature-config-schema-validation\",\"warning\":true,\"blocking\":false,\"code\":\"feature-config-validator-process-failure\",\"mutation\":false,\"valuesPrinted\":false}\\n' >&2\nfi\nif [ -n \"$feature_config_validator\" ]; then rm -f \"$feature_config_validator\" || true; fi\nif command -v ci_timing_emit >/dev/null 2>&1; then\n  ci_timing_emit feature-config-schema-validation succeeded \"$feature_config_schema_started_ms\"\nelse\n  printf '{\"event\":\"ci.stage.timing\",\"stage\":\"feature-config-schema-validation\",\"status\":\"succeeded\",\"blocking\":false}\\n' >&2\nfi\n\nUNIDESK_RUNTIME_GITOPS_OVERLAY_FILE=/etc/unidesk-cicd-runtime-gitops/runtime-gitops-overlay.json node /etc/unidesk-cicd-runtime-gitops/runtime-gitops-verify.mjs\nci_timing_emit gitops-render succeeded \"$gitops_render_started_ms\"\ncheck_source_head before-push\nworkdir=\"$(mktemp -d)\"\ngitops_read_url=\"$(params.gitops-read-url)\"\ngitops_write_url=\"$(params.git-write-url)\"\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\" \"$unidesk_legacy_runtime_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\"\n            if [ -n \"${unidesk_legacy_runtime_path:-}\" ]; then git add -A \"$unidesk_legacy_runtime_path\" || true; fi\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] = \"<runtime-source-identity>\";\n      continue;\n    }\n    const envName = String(value.name || \"\");\n    if (key === \"value\" && /^HWLAB_.*COMMIT/.test(envName)) {\n      result[key] = \"<runtime-source-identity>\";\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, \"<runtime-source-identity>\");\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"
            imagePullPolicy: Always
            volumeMounts:
              - name: unidesk-runtime-gitops-scripts
                mountPath: /etc/unidesk-cicd-runtime-gitops
                readOnly: true
        volumes:
          - name: unidesk-runtime-gitops-scripts
            configMap:
              name: hwlab-nc01-v03-ci-image-publish-runtime-gitops-scripts
              defaultMode: 493
      params:
        - name: git-url
          value: $(params.git-url)
        - name: git-read-url
          value: $(params.git-read-url)
        - name: gitops-read-url
          value: $(params.gitops-read-url)
        - name: git-write-url
          value: $(params.git-write-url)
        - name: gitops-username
          value: $(params.gitops-username)
        - 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)
