feat: split G14 GitOps promotion branch

This commit is contained in:
Codex
2026-05-25 08:59:01 +08:00
parent 9d6dd2a579
commit 2fe40c637a
17 changed files with 311 additions and 2800 deletions
+3
View File
@@ -36,3 +36,6 @@ yarn-error.log*
# Deploy/runtime state
*.local.json
.state/
# G14 generated GitOps desired state is stored on the G14-gitops branch.
/deploy/gitops/g14/
-10
View File
@@ -1,10 +0,0 @@
# HWLAB G14 GitOps CI/CD
This directory is generated from `CI.json`, `deploy/deploy.json`, and `deploy/k8s/*` by `scripts/g14-gitops-render.mjs`.
- Target: G14 k3s only.
- CI: Tekton Pipeline `hwlab-ci/hwlab-g14-ci-image-publish`.
- Artifact contract: images are tagged by source git commit `bec4109` and pushed to `127.0.0.1:5000/hwlab`.
- CD: Argo CD Application `argocd/hwlab-g14-dev` consumes `deploy/gitops/g14/runtime`.
- D601 boundary: this path does not target D601, does not use UniDesk Code Queue, and does not change D601 production traffic.
- Concurrency: CI builds are per source commit and do not acquire the legacy DEV CD Lease; Argo CD reconciles the Git desired state.
-34
View File
@@ -1,34 +0,0 @@
{
"apiVersion": "argoproj.io/v1alpha1",
"kind": "Application",
"metadata": {
"name": "hwlab-g14-dev",
"namespace": "argocd",
"labels": {
"app.kubernetes.io/part-of": "hwlab",
"hwlab.pikastech.local/gitops-target": "g14"
}
},
"spec": {
"project": "hwlab-g14",
"source": {
"repoURL": "git@github.com:pikasTech/HWLAB.git",
"targetRevision": "G14",
"path": "deploy/gitops/g14/runtime"
},
"destination": {
"server": "https://kubernetes.default.svc",
"namespace": "hwlab-dev"
},
"syncPolicy": {
"automated": {
"prune": false,
"selfHeal": true
},
"syncOptions": [
"CreateNamespace=true",
"ApplyOutOfSyncOnly=true"
]
}
}
}
-32
View File
@@ -1,32 +0,0 @@
{
"apiVersion": "argoproj.io/v1alpha1",
"kind": "AppProject",
"metadata": {
"name": "hwlab-g14",
"namespace": "argocd"
},
"spec": {
"description": "HWLAB G14 GitOps project; D601 remains outside this project.",
"sourceRepos": [
"git@github.com:pikasTech/HWLAB.git"
],
"destinations": [
{
"server": "https://kubernetes.default.svc",
"namespace": "hwlab-dev"
}
],
"clusterResourceWhitelist": [
{
"group": "",
"kind": "Namespace"
}
],
"namespaceResourceWhitelist": [
{
"group": "*",
"kind": "*"
}
]
}
}
-113
View File
@@ -1,113 +0,0 @@
{
"apiVersion": "v1",
"kind": "List",
"items": [
{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": {
"name": "hwlab-ci"
}
},
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "hwlab-registry",
"namespace": "hwlab-ci",
"labels": {
"app.kubernetes.io/name": "hwlab-registry"
}
},
"spec": {
"replicas": 1,
"selector": {
"matchLabels": {
"app.kubernetes.io/name": "hwlab-registry"
}
},
"template": {
"metadata": {
"labels": {
"app.kubernetes.io/name": "hwlab-registry"
}
},
"spec": {
"hostNetwork": true,
"dnsPolicy": "ClusterFirstWithHostNet",
"containers": [
{
"name": "registry",
"image": "registry:2.8.3",
"imagePullPolicy": "IfNotPresent",
"ports": [
{
"name": "registry",
"containerPort": 5000,
"hostPort": 5000
}
],
"env": [
{
"name": "REGISTRY_STORAGE_DELETE_ENABLED",
"value": "true"
}
],
"volumeMounts": [
{
"name": "storage",
"mountPath": "/var/lib/registry"
}
],
"readinessProbe": {
"httpGet": {
"path": "/v2/",
"port": "registry"
}
},
"livenessProbe": {
"httpGet": {
"path": "/v2/",
"port": "registry"
}
}
}
],
"volumes": [
{
"name": "storage",
"hostPath": {
"path": "/var/lib/hwlab/registry",
"type": "DirectoryOrCreate"
}
}
]
}
}
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-registry",
"namespace": "hwlab-ci",
"labels": {
"app.kubernetes.io/name": "hwlab-registry"
}
},
"spec": {
"selector": {
"app.kubernetes.io/name": "hwlab-registry"
},
"ports": [
{
"name": "registry",
"port": 5000,
"targetPort": "registry"
}
]
}
}
]
}
@@ -1,20 +0,0 @@
{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "hwlab-code-agent-codex-config",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-code-agent-codex-config",
"hwlab.pikastech.local/service-id": "hwlab-cloud-api",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"data": {
"config.toml": "model_provider = \"OpenAI\"\nmodel = \"gpt-5.4\"\nreview_model = \"gpt-5.4\"\nmodel_reasoning_effort = \"xhigh\"\ndisable_response_storage = true\nnetwork_access = \"enabled\"\nmodel_context_window = 1000000\nmodel_auto_compact_token_limit = 900000\n\n[model_providers.OpenAI]\nname = \"OpenAI\"\nbase_url = \"https://hyueapi.com\"\nwire_api = \"responses\"\nrequires_openai_auth = true\n\n[model_providers.crs]\nrequest_max_retries = 1000\nstream_max_retries = 1000\nname = \"crs\"\nbase_url = \"https://api.gptclubapi.xyz/openai\"\nwire_api = \"responses\"\nrequires_openai_auth = true\nenv_key = \"CRS_OAI_KEY\"\n"
}
}
@@ -1,30 +0,0 @@
{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "hwlab-dev-health-contract",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-dev-health-contract",
"hwlab.pikastech.local/profile": "dev",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"data": {
"endpoint": "http://hwlab-edge-proxy.hwlab-dev.svc.cluster.local:6667",
"cloud-api": "GET /health/live through http://hwlab-edge-proxy.hwlab-dev.svc.cluster.local:6667",
"cloud-api-db": "DEV DB config gate requires HWLAB_CLOUD_DB_URL from Secret hwlab-cloud-api-dev-db/database-url and HWLAB_CLOUD_DB_SSL_MODE=disable. Runtime readiness dials the redacted host parsed from the Secret URL and reports endpointSource=secret-url-host. cloud-api-db is an optional desired alias only until this repo owns Service plus Endpoint or EndpointSlice manifests and rollout/apply contract; health reports env presence, env injection, redacted connection attempt/result, and liveConnected without exposing secret values",
"cloud-web": "GET /health/live on http://hwlab-cloud-web.hwlab-dev.svc.cluster.local:8080; consumes cloud-api only",
"agent": "hwlab-agent-mgr readiness gates worker template creation; hwlab-agent-worker is suspended until a session is scheduled",
"sim": "hwlab-gateway-simu:7101 and hwlab-box-simu:7201 expose /health/live for DEV smoke",
"patch-panel": "hwlab-patch-panel:7301 exposes /health/live and owns patch panel state",
"router": "hwlab-router:7401 exposes /health/live and owns edge reachability routing",
"tunnel-client": "disabled for G14 GitOps; no FRP or production traffic tunnel is required",
"edge-proxy": "hwlab-edge-proxy:6667 exposes /health/live and proxies DEV endpoint traffic to hwlab-cloud-api",
"runtime-substitute-policy": "Do not replace HWLAB runtime with UniDesk backend, provider-gateway, or microservice proxy"
}
}
@@ -1,12 +0,0 @@
{
"apiVersion": "kustomize.config.k8s.io/v1beta1",
"kind": "Kustomization",
"namespace": "hwlab-dev",
"resources": [
"namespace.yaml",
"code-agent-codex-config.yaml",
"services.yaml",
"health-contract.yaml",
"workloads.yaml"
]
}
-16
View File
@@ -1,16 +0,0 @@
{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": {
"name": "hwlab-dev",
"labels": {
"app.kubernetes.io/part-of": "hwlab",
"hwlab.pikastech.local/profile": "dev",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
}
}
-492
View File
@@ -1,492 +0,0 @@
{
"apiVersion": "v1",
"kind": "List",
"items": [
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-cloud-api",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-cloud-api",
"hwlab.pikastech.local/service-id": "hwlab-cloud-api",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"app.kubernetes.io/name": "hwlab-cloud-api"
},
"ports": [
{
"name": "http",
"port": 6667,
"targetPort": "http"
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-gateway-simu-1",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-gateway-simu",
"hwlab.pikastech.local/service-id": "hwlab-gateway-simu",
"hwlab.pikastech.local/instance-id": "gwsimu_1",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"statefulset.kubernetes.io/pod-name": "hwlab-gateway-simu-0"
},
"ports": [
{
"name": "http",
"port": 7101,
"targetPort": "http"
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-gateway-simu-2",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-gateway-simu",
"hwlab.pikastech.local/service-id": "hwlab-gateway-simu",
"hwlab.pikastech.local/instance-id": "gwsimu_2",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"statefulset.kubernetes.io/pod-name": "hwlab-gateway-simu-1"
},
"ports": [
{
"name": "http",
"port": 7101,
"targetPort": "http"
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-cloud-web",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-cloud-web",
"hwlab.pikastech.local/service-id": "hwlab-cloud-web",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"app.kubernetes.io/name": "hwlab-cloud-web"
},
"ports": [
{
"name": "http",
"port": 8080,
"targetPort": "http"
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-box-simu-1",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-box-simu",
"hwlab.pikastech.local/service-id": "hwlab-box-simu",
"hwlab.pikastech.local/instance-id": "res_boxsimu_1",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"statefulset.kubernetes.io/pod-name": "hwlab-box-simu-0"
},
"ports": [
{
"name": "http",
"port": 7201,
"targetPort": "http"
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-box-simu-2",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-box-simu",
"hwlab.pikastech.local/service-id": "hwlab-box-simu",
"hwlab.pikastech.local/instance-id": "res_boxsimu_2",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"statefulset.kubernetes.io/pod-name": "hwlab-box-simu-1"
},
"ports": [
{
"name": "http",
"port": 7201,
"targetPort": "http"
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-gateway",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-gateway",
"hwlab.pikastech.local/service-id": "hwlab-gateway",
"hwlab.pikastech.local/activation": "manual",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"app.kubernetes.io/name": "hwlab-gateway"
},
"ports": [
{
"name": "http",
"port": 7001,
"targetPort": "http"
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-cli",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-cli",
"hwlab.pikastech.local/service-id": "hwlab-cli",
"hwlab.pikastech.local/activation": "manual",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"app.kubernetes.io/name": "hwlab-cli"
},
"ports": [
{
"name": "http",
"port": 7501,
"targetPort": "http"
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-agent-skills",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-agent-skills",
"hwlab.pikastech.local/service-id": "hwlab-agent-skills",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"app.kubernetes.io/name": "hwlab-agent-skills"
},
"ports": [
{
"name": "http",
"port": 7430,
"targetPort": "http"
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-agent-mgr",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-agent-mgr",
"hwlab.pikastech.local/service-id": "hwlab-agent-mgr",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"app.kubernetes.io/name": "hwlab-agent-mgr"
},
"ports": [
{
"name": "http",
"port": 7410,
"targetPort": "http"
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-gateway-simu",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-gateway-simu",
"hwlab.pikastech.local/service-id": "hwlab-gateway-simu",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"app.kubernetes.io/name": "hwlab-gateway-simu"
},
"ports": [
{
"name": "http",
"port": 7101,
"targetPort": "http"
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-box-simu",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-box-simu",
"hwlab.pikastech.local/service-id": "hwlab-box-simu",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"app.kubernetes.io/name": "hwlab-box-simu"
},
"ports": [
{
"name": "http",
"port": 7201,
"targetPort": "http"
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-patch-panel",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-patch-panel",
"hwlab.pikastech.local/service-id": "hwlab-patch-panel",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"app.kubernetes.io/name": "hwlab-patch-panel"
},
"ports": [
{
"name": "http",
"port": 7301,
"targetPort": "http"
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-router",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-router",
"hwlab.pikastech.local/service-id": "hwlab-router",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"app.kubernetes.io/name": "hwlab-router"
},
"ports": [
{
"name": "http",
"port": 7401,
"targetPort": "http"
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-tunnel-client",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-tunnel-client",
"hwlab.pikastech.local/service-id": "hwlab-tunnel-client",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"app.kubernetes.io/name": "hwlab-tunnel-client"
},
"ports": [
{
"name": "http",
"port": 7402,
"targetPort": "http"
}
]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-edge-proxy",
"namespace": "hwlab-dev",
"labels": {
"app.kubernetes.io/name": "hwlab-edge-proxy",
"hwlab.pikastech.local/service-id": "hwlab-edge-proxy",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
},
"annotations": {
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
}
},
"spec": {
"type": "ClusterIP",
"selector": {
"app.kubernetes.io/name": "hwlab-edge-proxy"
},
"ports": [
{
"name": "http",
"port": 6667,
"targetPort": "http"
}
]
}
}
]
}
File diff suppressed because it is too large Load Diff
-11
View File
@@ -1,11 +0,0 @@
{
"kind": "hwlab-g14-gitops-source",
"sourceCommit": "bec4109af1bddd606683821c60186c0c89f72584",
"sourceShortCommit": "bec4109",
"sourceBranch": "G14",
"sourceRepo": "git@github.com:pikasTech/HWLAB.git",
"registryPrefix": "127.0.0.1:5000/hwlab",
"runtimePath": "deploy/gitops/g14/runtime",
"tektonPipeline": "hwlab-ci/hwlab-g14-ci-image-publish",
"argoApplication": "argocd/hwlab-g14-dev"
}
-326
View File
@@ -1,326 +0,0 @@
{
"apiVersion": "tekton.dev/v1",
"kind": "Pipeline",
"metadata": {
"name": "hwlab-g14-ci-image-publish",
"namespace": "hwlab-ci",
"labels": {
"app.kubernetes.io/part-of": "hwlab",
"hwlab.pikastech.local/gitops-target": "g14"
},
"annotations": {
"hwlab.pikastech.local/source-config": "CI.json",
"hwlab.pikastech.local/policy": "parallel-no-lock-ci-image-publish"
}
},
"spec": {
"params": [
{
"name": "git-url",
"type": "string",
"default": "git@github.com:pikasTech/HWLAB.git"
},
{
"name": "source-branch",
"type": "string",
"default": "G14"
},
{
"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-cloud-web,hwlab-agent-mgr,hwlab-agent-worker,hwlab-gateway,hwlab-gateway-simu,hwlab-box-simu,hwlab-patch-panel,hwlab-router,hwlab-tunnel-client,hwlab-edge-proxy,hwlab-cli,hwlab-agent-skills"
},
{
"name": "base-image",
"type": "string",
"default": "node:20-bookworm-slim"
},
{
"name": "concurrency",
"type": "string",
"default": "4"
}
],
"workspaces": [
{
"name": "source"
},
{
"name": "git-ssh"
}
],
"tasks": [
{
"name": "ci-json",
"workspaces": [
{
"name": "source",
"workspace": "source"
},
{
"name": "git-ssh",
"workspace": "git-ssh"
}
],
"taskSpec": {
"params": [
{
"name": "git-url"
},
{
"name": "source-branch"
},
{
"name": "revision"
}
],
"workspaces": [
{
"name": "source"
},
{
"name": "git-ssh"
}
],
"steps": [
{
"name": "ci-json",
"image": "node:22-bookworm-slim",
"env": [
{
"name": "HTTP_PROXY",
"value": "http://127.0.0.1:10808"
},
{
"name": "HTTPS_PROXY",
"value": "http://127.0.0.1:10808"
},
{
"name": "ALL_PROXY",
"value": "socks5h://127.0.0.1:10808"
},
{
"name": "NO_PROXY",
"value": "127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc"
},
{
"name": "http_proxy",
"value": "http://127.0.0.1:10808"
},
{
"name": "https_proxy",
"value": "http://127.0.0.1:10808"
},
{
"name": "all_proxy",
"value": "socks5h://127.0.0.1:10808"
},
{
"name": "no_proxy",
"value": "127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc"
}
],
"script": "#!/bin/sh\nset -eu\nif ! command -v git >/dev/null 2>&1 || ! command -v python3 >/dev/null 2>&1 || ! command -v ssh >/dev/null 2>&1; then\n export DEBIAN_FRONTEND=noninteractive\n apt-get update\n apt-get install -y --no-install-recommends git python3 openssh-client ca-certificates\n rm -rf /var/lib/apt/lists/*\nfi\nmkdir -p /root/.ssh\ncp /workspace/git-ssh/ssh-privatekey /root/.ssh/id_rsa\nchmod 600 /root/.ssh/id_rsa\nssh-keyscan github.com >> /root/.ssh/known_hosts 2>/dev/null\nrm -rf /workspace/source/repo\ngit clone --branch \"$(params.source-branch)\" \"$(params.git-url)\" /workspace/source/repo\ncd /workspace/source/repo\ngit checkout \"$(params.revision)\"\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nnpm ci --ignore-scripts\nnpx playwright install --with-deps chromium\necho 'CI.json hwlab-deploy-skeleton-ci command count=6'\necho 'CI.json command: repo-reports-guard'\nnode scripts/repo-reports-guard.mjs\necho 'CI.json command: contract-check'\nnpm run check\necho 'CI.json command: dev-artifact-publish-static-check'\nnode --check scripts/dev-artifact-publish.mjs\necho 'CI.json command: parse-deploy-json'\nnode -e \"JSON.parse(require('node:fs').readFileSync('deploy/deploy.json','utf8'))\"\necho 'CI.json command: parse-k8s-json-yaml'\nnode -e \"const fs=require('node:fs'); for (const f of fs.readdirSync('deploy/k8s/base').concat(fs.readdirSync('deploy/k8s/dev').map(x=>'../dev/'+x), fs.readdirSync('deploy/k8s/prod').map(x=>'../prod/'+x))) { if (!f.endsWith('.yaml')) continue; const p=f.startsWith('..')?'deploy/k8s/'+f.replace('../',''):'deploy/k8s/base/'+f; JSON.parse(fs.readFileSync(p,'utf8')); }\"\necho 'CI.json command: parse-frp-toml'\npython3 - <<'PY'\nimport pathlib, tomllib\nfor path in pathlib.Path('deploy/frp').glob('*.toml'):\n tomllib.loads(path.read_text())\nPY\n"
}
]
},
"params": [
{
"name": "git-url",
"value": "$(params.git-url)"
},
{
"name": "source-branch",
"value": "$(params.source-branch)"
},
{
"name": "revision",
"value": "$(params.revision)"
}
]
},
{
"name": "image-publish",
"runAfter": [
"ci-json"
],
"workspaces": [
{
"name": "source",
"workspace": "source"
}
],
"taskSpec": {
"params": [
{
"name": "revision"
},
{
"name": "registry-prefix"
},
{
"name": "services"
},
{
"name": "base-image"
},
{
"name": "concurrency"
}
],
"workspaces": [
{
"name": "source"
}
],
"volumes": [
{
"name": "docker-run",
"emptyDir": {}
}
],
"sidecars": [
{
"name": "docker",
"image": "docker:29-dind",
"args": [
"--host=unix:///var/run/docker.sock",
"--storage-driver=vfs"
],
"env": [
{
"name": "DOCKER_TLS_CERTDIR",
"value": ""
},
{
"name": "HTTP_PROXY",
"value": "http://127.0.0.1:10808"
},
{
"name": "HTTPS_PROXY",
"value": "http://127.0.0.1:10808"
},
{
"name": "ALL_PROXY",
"value": "socks5h://127.0.0.1:10808"
},
{
"name": "NO_PROXY",
"value": "127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc"
},
{
"name": "http_proxy",
"value": "http://127.0.0.1:10808"
},
{
"name": "https_proxy",
"value": "http://127.0.0.1:10808"
},
{
"name": "all_proxy",
"value": "socks5h://127.0.0.1:10808"
},
{
"name": "no_proxy",
"value": "127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc"
}
],
"securityContext": {
"privileged": true
},
"volumeMounts": [
{
"name": "docker-run",
"mountPath": "/var/run"
}
]
}
],
"steps": [
{
"name": "publish",
"image": "node:22-alpine",
"env": [
{
"name": "DOCKER_HOST",
"value": "unix:///var/run/docker.sock"
},
{
"name": "HTTP_PROXY",
"value": "http://127.0.0.1:10808"
},
{
"name": "HTTPS_PROXY",
"value": "http://127.0.0.1:10808"
},
{
"name": "ALL_PROXY",
"value": "socks5h://127.0.0.1:10808"
},
{
"name": "NO_PROXY",
"value": "127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc"
},
{
"name": "http_proxy",
"value": "http://127.0.0.1:10808"
},
{
"name": "https_proxy",
"value": "http://127.0.0.1:10808"
},
{
"name": "all_proxy",
"value": "socks5h://127.0.0.1:10808"
},
{
"name": "no_proxy",
"value": "127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc"
}
],
"volumeMounts": [
{
"name": "docker-run",
"mountPath": "/var/run"
}
],
"script": "#!/bin/sh\nset -eu\napk add --no-cache docker-cli git openssh-client python3\nexport DOCKER_HOST=unix:///var/run/docker.sock\nexport HWLAB_CI_ARTIFACT_RUN_ID=\"$(context.pipelineRun.name)\"\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\nfor i in $(seq 1 90); do docker info >/dev/null 2>&1 && break; sleep 2; done\ndocker info >/dev/null\nif [ -n \"${HWLAB_DEV_BASE_IMAGE:-}\" ]; then\n docker pull \"$HWLAB_DEV_BASE_IMAGE\"\nfi\ncd /workspace/source/repo\ntest \"$(git rev-parse HEAD)\" = \"$(params.revision)\"\nnode scripts/dev-artifact-publish.mjs --publish --registry-prefix \"$(params.registry-prefix)\" --report /workspace/source/dev-artifacts.json --quiet-build --concurrency \"$(params.concurrency)\" --services \"$(params.services)\"\nnode scripts/refresh-artifact-catalog.mjs --target-ref HEAD --publish-report /workspace/source/dev-artifacts.json --no-write\n"
}
]
},
"params": [
{
"name": "revision",
"value": "$(params.revision)"
},
{
"name": "registry-prefix",
"value": "$(params.registry-prefix)"
},
{
"name": "services",
"value": "$(params.services)"
},
{
"name": "base-image",
"value": "$(params.base-image)"
},
{
"name": "concurrency",
"value": "$(params.concurrency)"
}
]
}
]
}
}
@@ -1,74 +0,0 @@
{
"apiVersion": "tekton.dev/v1",
"kind": "PipelineRun",
"metadata": {
"generateName": "hwlab-g14-ci-",
"namespace": "hwlab-ci",
"labels": {
"app.kubernetes.io/part-of": "hwlab",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": "bec4109af1bddd606683821c60186c0c89f72584"
}
},
"spec": {
"pipelineRef": {
"name": "hwlab-g14-ci-image-publish"
},
"taskRunTemplate": {
"serviceAccountName": "hwlab-tekton-runner",
"podTemplate": {
"hostNetwork": true,
"dnsPolicy": "ClusterFirstWithHostNet"
}
},
"params": [
{
"name": "git-url",
"value": "git@github.com:pikasTech/HWLAB.git"
},
{
"name": "source-branch",
"value": "G14"
},
{
"name": "revision",
"value": "bec4109af1bddd606683821c60186c0c89f72584"
},
{
"name": "registry-prefix",
"value": "127.0.0.1:5000/hwlab"
},
{
"name": "base-image",
"value": "node:20-bookworm-slim"
},
{
"name": "concurrency",
"value": "4"
}
],
"workspaces": [
{
"name": "source",
"volumeClaimTemplate": {
"spec": {
"accessModes": [
"ReadWriteOnce"
],
"resources": {
"requests": {
"storage": "8Gi"
}
}
}
}
},
{
"name": "git-ssh",
"secret": {
"secretName": "hwlab-git-ssh"
}
}
]
}
}
-93
View File
@@ -1,93 +0,0 @@
{
"apiVersion": "v1",
"kind": "List",
"items": [
{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": {
"name": "hwlab-ci"
}
},
{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"name": "hwlab-tekton-runner",
"namespace": "hwlab-ci"
}
},
{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "Role",
"metadata": {
"name": "hwlab-tekton-runtime-observer",
"namespace": "hwlab-dev"
},
"rules": [
{
"apiGroups": [
""
],
"resources": [
"pods",
"services",
"configmaps"
],
"verbs": [
"get",
"list",
"watch"
]
},
{
"apiGroups": [
"apps"
],
"resources": [
"deployments",
"statefulsets"
],
"verbs": [
"get",
"list",
"watch"
]
},
{
"apiGroups": [
"batch"
],
"resources": [
"jobs"
],
"verbs": [
"get",
"list",
"watch"
]
}
]
},
{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "RoleBinding",
"metadata": {
"name": "hwlab-tekton-runtime-observer",
"namespace": "hwlab-dev"
},
"subjects": [
{
"kind": "ServiceAccount",
"name": "hwlab-tekton-runner",
"namespace": "hwlab-ci"
}
],
"roleRef": {
"apiGroup": "rbac.authorization.k8s.io",
"kind": "Role",
"name": "hwlab-tekton-runtime-observer"
}
}
]
}
+13 -2
View File
@@ -7,7 +7,8 @@ G14 是 HWLAB 的旁路 DEV 集群目标。G14 CI/CD 必须只作用于 G14 k3s
- Source of truth:业务版本以 Git source commit 为唯一身份;镜像 tag、OCI labels、runtime annotation 和 Argo CD desired state 都必须记录同一个 source commit。
- CITekton 在 G14 k3s 内运行 `hwlab-g14-ci-image-publish` Pipeline,读取 `CI.json` 命令,按 source commit 构建镜像并推送到 G14 registry。
- Artifact:镜像使用 commit tag,例如 `127.0.0.1:5000/hwlab/hwlab-cloud-api:<shortCommit>`digest 由 registry 返回,CI report 只作为审计证据,不作为 CD 真相。
- CDArgo CD 只消费 `deploy/gitops/g14/runtime` 的 Git desired state,不重新构建镜像,不读取 D601 状态,不获取 legacy DEV CD Lease。
- Branch split`G14` 是源码监控分支;`G14-gitops` 是 Tekton promotion 写入的生成分支,保存 `deploy/gitops/g14/**` desired state。
- CDArgo CD 只消费 `G14-gitops:deploy/gitops/g14/runtime` 的 Git desired state,不重新构建镜像,不读取 D601 状态,不获取 legacy DEV CD Lease。
- 并行性:不同 source commit 的 CI build 不共享发布锁;并行安全由 immutable commit tag/digest 和 Git desired state 保证。最终运行版本由 Argo CD 当前同步的 Git revision 决定。
## 生成入口
@@ -16,7 +17,9 @@ G14 是 HWLAB 的旁路 DEV 集群目标。G14 CI/CD 必须只作用于 G14 k3s
- 读取 `CI.json`,生成 Tekton Pipeline 和 PipelineRun 样板。
- 读取 `deploy/deploy.json``deploy/k8s/*`,生成 Argo CD 可消费的 G14 runtime Kustomize path。
- 生成 `hwlab-g14-branch-poller` CronJob:它使用 G14 集群内的 Git SSH Secret 轮询 `G14` 分支,按 source commit 创建确定命名的 Tekton PipelineRun。
- 默认输出到 `deploy/gitops/g14/`
- 默认 GitOps 生成分支是 `G14-gitops`Pipeline 成功后把本次 source commit 对应的 `deploy/gitops/g14/**` 推送到该分支,避免把生成提交继续写回 `G14`
- 默认 registry prefix 是 `127.0.0.1:5000/hwlab`,用于 G14 单节点 k3s 的 node-local registry。
- 默认 CI/CD proxy 是 G14 本机 `http://127.0.0.1:10808` / `socks5h://127.0.0.1:10808`。Tekton CI step、Docker-in-Docker sidecar 和 publish step 都注入 proxy/no_proxypublish step 必须先在 DIND 内 `docker pull $HWLAB_DEV_BASE_IMAGE`,再进入 DEV base-image preflight、build 和 registry push,保证 npm、Playwright Chromium 安装、base image 拉取和 registry push 走 G14 本机代理边界。CI step 固定使用 Node 22 Debian 镜像,避免 Playwright 官方镜像内 Node 版本漂移影响 `node --test`
@@ -27,9 +30,17 @@ npm run g14:gitops:render -- --source-revision <sourceCommit>
npm run g14:gitops:check
```
## Polling 触发
G14 不要求 GitHub webhook 或 GitHub Actions 配置。`hwlab-ci/hwlab-g14-branch-poller` CronJob 每 5 分钟通过 Git SSH 拉取 `G14` HEAD,并用 source commit 的前 12 位生成 PipelineRun 名称 `hwlab-g14-ci-poll-<short12>`
如果同名 PipelineRun 已存在,poller 直接跳过;如果不存在,则创建新的 PipelineRun。这样可以用 Kubernetes 原生 CronJob、ServiceAccount、RBAC 和 Tekton API 实现无 GitHub webhook 的分支监控,同时避免同一个 commit 被反复派单。历史上的 `chore: promote G14 GitOps source ...` 生成提交会被 poller 跳过。
Pipeline 的标准路径是:`G14` source commit -> `CI.json` Tekton 测试 -> commit-tagged image push 到 G14 registry -> render `deploy/gitops/g14/**` -> push `G14-gitops` -> Argo CD 同步 runtime。CD 只消费已经构建好的镜像和 Git desired state,不在 Argo CD 内构建镜像。
## 集群资源
- `hwlab-ci`registry、Tekton runner RBAC、Pipeline/PipelineRun 所在 namespace。
- `hwlab-ci`registry、Tekton runner RBAC、Polling CronJob、Pipeline/PipelineRun 所在 namespace。
- `argocd`Argo CD 控制面和 `hwlab-g14-dev` Application 所在 namespace。
- `hwlab-dev`HWLAB runtime namespace,由 Argo CD 应用 `deploy/gitops/g14/runtime`
+295 -2
View File
@@ -12,6 +12,7 @@ const defaultOutDir = "deploy/gitops/g14";
const defaultRegistryPrefix = process.env.HWLAB_G14_REGISTRY_PREFIX || "127.0.0.1:5000/hwlab";
const defaultSourceRepo = process.env.HWLAB_G14_GIT_URL || "git@github.com:pikasTech/HWLAB.git";
const defaultBranch = process.env.HWLAB_G14_BRANCH || "G14";
const defaultGitopsBranch = process.env.HWLAB_G14_GITOPS_BRANCH || "G14-gitops";
const defaultRuntimeEndpoint = "http://hwlab-edge-proxy.hwlab-dev.svc.cluster.local:6667";
const defaultWebEndpoint = "http://hwlab-cloud-web.hwlab-dev.svc.cluster.local:8080";
const defaultProxyUrl = process.env.HWLAB_G14_PROXY_URL || "http://127.0.0.1:10808";
@@ -38,6 +39,7 @@ function parseArgs(argv) {
registryPrefix: defaultRegistryPrefix,
sourceRevision: null,
sourceBranch: defaultBranch,
gitopsBranch: defaultGitopsBranch,
sourceRepo: defaultSourceRepo,
runtimeEndpoint: defaultRuntimeEndpoint,
webEndpoint: defaultWebEndpoint,
@@ -51,6 +53,7 @@ function parseArgs(argv) {
else if (arg === "--registry-prefix") args.registryPrefix = readOption(argv, ++index, arg).replace(/\/+$/u, "");
else if (arg === "--source-revision") args.sourceRevision = readOption(argv, ++index, arg);
else if (arg === "--source-branch") args.sourceBranch = readOption(argv, ++index, arg);
else if (arg === "--gitops-branch") args.gitopsBranch = readOption(argv, ++index, arg);
else if (arg === "--source-repo") args.sourceRepo = readOption(argv, ++index, arg);
else if (arg === "--runtime-endpoint") args.runtimeEndpoint = readOption(argv, ++index, arg);
else if (arg === "--web-endpoint") args.webEndpoint = readOption(argv, ++index, arg);
@@ -80,6 +83,7 @@ function usage() {
` --out DIR default: ${defaultOutDir}`,
` --source-repo URL default: ${defaultSourceRepo}`,
` --source-branch BRANCH default: ${defaultBranch}`,
` --gitops-branch BRANCH default: ${defaultGitopsBranch}`,
" --source-revision SHA source commit used for image tags and runtime annotations; default: git rev-parse HEAD",
` --registry-prefix PREFIX default: ${defaultRegistryPrefix}`,
` --runtime-endpoint URL default: ${defaultRuntimeEndpoint}`,
@@ -344,6 +348,184 @@ node scripts/refresh-artifact-catalog.mjs --target-ref HEAD --publish-report /wo
`;
}
function gitopsPromoteScript() {
return `#!/bin/sh
set -eu
if ! command -v git >/dev/null 2>&1 || ! command -v ssh >/dev/null 2>&1; then
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends git openssh-client ca-certificates
rm -rf /var/lib/apt/lists/*
fi
mkdir -p /root/.ssh
cp /workspace/git-ssh/ssh-privatekey /root/.ssh/id_rsa
chmod 600 /root/.ssh/id_rsa
ssh-keyscan github.com >> /root/.ssh/known_hosts 2>/dev/null || true
export GIT_SSH_COMMAND="ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"
cd /workspace/source/repo
test "$(git rev-parse HEAD)" = "$(params.revision)"
node scripts/g14-gitops-render.mjs --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)"
node scripts/g14-gitops-render.mjs --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)" --check
git config --global user.name "HWLAB G14 GitOps Bot"
git config --global user.email "hwlab-g14-gitops-bot@users.noreply.github.com"
workdir="$(mktemp -d)"
git clone --no-checkout "$(params.git-url)" "$workdir/gitops"
cd "$workdir/gitops"
if git ls-remote --exit-code --heads origin "$(params.gitops-branch)" >/dev/null 2>&1; then
git fetch origin "$(params.gitops-branch)"
git checkout -B "$(params.gitops-branch)" "origin/$(params.gitops-branch)"
rm -rf deploy/gitops/g14
else
git checkout --orphan "$(params.gitops-branch)"
git rm -rf . >/dev/null 2>&1 || true
fi
mkdir -p deploy/gitops
cp -a /workspace/source/repo/deploy/gitops/g14 deploy/gitops/g14
git add deploy/gitops/g14
if git diff --cached --quiet; then
echo '{"status":"unchanged","gitopsBranch":"'"$(params.gitops-branch)"'","sourceRevision":"'"$(params.revision)"'"}'
exit 0
fi
short="$(printf '%.7s' "$(params.revision)")"
git commit -m "chore: promote G14 GitOps source $short"
git push origin "HEAD:$(params.gitops-branch)"
echo '{"status":"pushed","gitopsBranch":"'"$(params.gitops-branch)"'","sourceRevision":"'"$(params.revision)"'"}'
`;
}
function pollerScript() {
return `#!/bin/sh
set -eu
apk add --no-cache git openssh-client ca-certificates
mkdir -p /root/.ssh
cp /workspace/git-ssh/ssh-privatekey /root/.ssh/id_rsa
chmod 600 /root/.ssh/id_rsa
ssh-keyscan github.com >> /root/.ssh/known_hosts 2>/dev/null || true
export GIT_SSH_COMMAND="ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"
workdir="$(mktemp -d)"
git clone --depth 1 --branch "$SOURCE_BRANCH" "$GIT_URL" "$workdir/repo"
cd "$workdir/repo"
revision="$(git rev-parse HEAD)"
subject="$(git log -1 --pretty=%s)"
case "$subject" in
"chore: promote G14 GitOps source "*)
echo "Skipping generated GitOps promotion commit $revision"
exit 0
;;
esac
short="$(printf '%.12s' "$revision")"
name="hwlab-g14-ci-poll-$short"
export POLLER_REVISION="$revision"
export POLLER_PIPELINERUN="$name"
export POLLER_SOURCE_SUBJECT="$subject"
echo "Checking G14 source $revision with PipelineRun $name"
node <<'NODE'
const fs = require("node:fs");
const https = require("node:https");
function env(name, fallback) {
return process.env[name] || fallback;
}
const namespace = env("POD_NAMESPACE", "hwlab-ci");
const name = env("POLLER_PIPELINERUN", "");
const revision = env("POLLER_REVISION", "");
const host = process.env.KUBERNETES_SERVICE_HOST;
const port = process.env.KUBERNETES_SERVICE_PORT || "443";
if (!name || !revision) throw new Error("poller did not resolve a PipelineRun name and revision");
if (!host) throw new Error("KUBERNETES_SERVICE_HOST is not available");
const token = fs.readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8");
const ca = fs.readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt");
function request(method, path, body) {
const payload = body ? JSON.stringify(body) : "";
const headers = { Authorization: "Bearer " + token };
if (payload) {
headers["Content-Type"] = "application/json";
headers["Content-Length"] = Buffer.byteLength(payload);
}
return new Promise((resolve, reject) => {
const req = https.request({ host, port, method, path, ca, headers }, (res) => {
let data = "";
res.setEncoding("utf8");
res.on("data", (chunk) => { data += chunk; });
res.on("end", () => resolve({ statusCode: res.statusCode || 0, body: data }));
});
req.on("error", reject);
if (payload) req.write(payload);
req.end();
});
}
const labels = {
"app.kubernetes.io/part-of": "hwlab",
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/source-commit": revision,
"hwlab.pikastech.local/trigger": "polling"
};
const pipelineRun = {
apiVersion: "tekton.dev/v1",
kind: "PipelineRun",
metadata: {
name,
namespace,
labels,
annotations: {
"hwlab.pikastech.local/source-subject": env("POLLER_SOURCE_SUBJECT", ""),
"hwlab.pikastech.local/source-branch": env("SOURCE_BRANCH", "G14"),
"hwlab.pikastech.local/gitops-branch": env("GITOPS_BRANCH", "G14-gitops")
}
},
spec: {
pipelineRef: { name: env("PIPELINE_NAME", "hwlab-g14-ci-image-publish") },
taskRunTemplate: {
serviceAccountName: "hwlab-tekton-runner",
podTemplate: { hostNetwork: true, dnsPolicy: "ClusterFirstWithHostNet" }
},
params: [
{ name: "git-url", value: env("GIT_URL", "git@github.com:pikasTech/HWLAB.git") },
{ name: "source-branch", value: env("SOURCE_BRANCH", "G14") },
{ name: "gitops-branch", value: env("GITOPS_BRANCH", "G14-gitops") },
{ name: "revision", value: revision },
{ name: "registry-prefix", value: env("REGISTRY_PREFIX", "127.0.0.1:5000/hwlab") },
{ name: "services", value: env("SERVICES", "hwlab-cloud-api,hwlab-cloud-web,hwlab-agent-mgr,hwlab-agent-worker,hwlab-gateway,hwlab-gateway-simu,hwlab-box-simu,hwlab-patch-panel,hwlab-router,hwlab-tunnel-client,hwlab-edge-proxy,hwlab-cli,hwlab-agent-skills") },
{ name: "base-image", value: env("BASE_IMAGE", "node:20-bookworm-slim") },
{ name: "concurrency", value: env("CONCURRENCY", "4") }
],
workspaces: [
{ name: "source", volumeClaimTemplate: { spec: { accessModes: ["ReadWriteOnce"], resources: { requests: { storage: "8Gi" } } } } },
{ name: "git-ssh", secret: { secretName: "hwlab-git-ssh" } }
]
}
};
const base = "/apis/tekton.dev/v1/namespaces/" + encodeURIComponent(namespace) + "/pipelineruns";
const item = base + "/" + encodeURIComponent(name);
(async () => {
const existing = await request("GET", item);
if (existing.statusCode === 200) {
console.log(JSON.stringify({ status: "exists", pipelineRun: name, revision }));
return;
}
if (existing.statusCode !== 404) {
throw new Error("unexpected PipelineRun lookup status " + existing.statusCode + ": " + existing.body.slice(0, 500));
}
const created = await request("POST", base, pipelineRun);
if (created.statusCode < 200 || created.statusCode > 299) {
throw new Error("PipelineRun create failed with status " + created.statusCode + ": " + created.body.slice(0, 1000));
}
console.log(JSON.stringify({ status: "created", pipelineRun: name, revision }));
})().catch((error) => {
console.error(error.stack || error.message);
process.exitCode = 1;
});
NODE
`;
}
function tektonRbac() {
return {
apiVersion: "v1",
@@ -367,6 +549,21 @@ function tektonRbac() {
metadata: { name: "hwlab-tekton-runtime-observer", namespace: "hwlab-dev" },
subjects: [{ kind: "ServiceAccount", name: "hwlab-tekton-runner", namespace: "hwlab-ci" }],
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: "hwlab-tekton-runtime-observer" }
},
{
apiVersion: "rbac.authorization.k8s.io/v1",
kind: "Role",
metadata: { name: "hwlab-tekton-pipelinerun-writer", namespace: "hwlab-ci" },
rules: [
{ apiGroups: ["tekton.dev"], resources: ["pipelineruns"], verbs: ["get", "list", "create"] }
]
},
{
apiVersion: "rbac.authorization.k8s.io/v1",
kind: "RoleBinding",
metadata: { name: "hwlab-tekton-pipelinerun-writer", namespace: "hwlab-ci" },
subjects: [{ kind: "ServiceAccount", name: "hwlab-tekton-runner", namespace: "hwlab-ci" }],
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: "hwlab-tekton-pipelinerun-writer" }
}
]
};
@@ -392,6 +589,7 @@ function tektonPipeline(ci) {
params: [
{ name: "git-url", type: "string", default: defaultSourceRepo },
{ name: "source-branch", type: "string", default: defaultBranch },
{ name: "gitops-branch", type: "string", default: defaultGitopsBranch },
{ name: "revision", type: "string", description: "Full git commit SHA; the only source truth for image tags." },
{ name: "registry-prefix", type: "string", default: defaultRegistryPrefix },
{ name: "services", type: "string", default: "hwlab-cloud-api,hwlab-cloud-web,hwlab-agent-mgr,hwlab-agent-worker,hwlab-gateway,hwlab-gateway-simu,hwlab-box-simu,hwlab-patch-panel,hwlab-router,hwlab-tunnel-client,hwlab-edge-proxy,hwlab-cli,hwlab-agent-skills" },
@@ -461,6 +659,32 @@ function tektonPipeline(ci) {
{ name: "base-image", value: "$(params.base-image)" },
{ name: "concurrency", value: "$(params.concurrency)" }
]
},
{
name: "gitops-promote",
runAfter: ["image-publish"],
workspaces: [
{ name: "source", workspace: "source" },
{ name: "git-ssh", workspace: "git-ssh" }
],
taskSpec: {
params: [
{ name: "git-url" },
{ name: "source-branch" },
{ name: "gitops-branch" },
{ name: "revision" },
{ name: "registry-prefix" }
],
workspaces: [{ name: "source" }, { name: "git-ssh" }],
steps: [{ name: "promote", image: "node:22-bookworm-slim", env: proxyEnv(), script: gitopsPromoteScript() }]
},
params: [
{ name: "git-url", value: "$(params.git-url)" },
{ name: "source-branch", value: "$(params.source-branch)" },
{ name: "gitops-branch", value: "$(params.gitops-branch)" },
{ name: "revision", value: "$(params.revision)" },
{ name: "registry-prefix", value: "$(params.registry-prefix)" }
]
}
]
}
@@ -492,6 +716,7 @@ function tektonPipelineRunTemplate({ source, args }) {
params: [
{ name: "git-url", value: args.sourceRepo },
{ name: "source-branch", value: args.sourceBranch },
{ name: "gitops-branch", value: args.gitopsBranch },
{ name: "revision", value: source.full },
{ name: "registry-prefix", value: args.registryPrefix },
{ name: "base-image", value: "node:20-bookworm-slim" },
@@ -505,6 +730,71 @@ function tektonPipelineRunTemplate({ source, args }) {
};
}
function tektonPollerCronJob(args) {
return {
apiVersion: "batch/v1",
kind: "CronJob",
metadata: {
name: "hwlab-g14-branch-poller",
namespace: "hwlab-ci",
labels: {
"app.kubernetes.io/part-of": "hwlab",
"hwlab.pikastech.local/gitops-target": "g14"
},
annotations: {
"hwlab.pikastech.local/source-branch": args.sourceBranch,
"hwlab.pikastech.local/gitops-branch": args.gitopsBranch
}
},
spec: {
schedule: "*/5 * * * *",
concurrencyPolicy: "Forbid",
successfulJobsHistoryLimit: 3,
failedJobsHistoryLimit: 3,
jobTemplate: {
spec: {
backoffLimit: 1,
template: {
metadata: {
labels: {
"app.kubernetes.io/part-of": "hwlab",
"hwlab.pikastech.local/gitops-target": "g14"
}
},
spec: {
serviceAccountName: "hwlab-tekton-runner",
restartPolicy: "Never",
hostNetwork: true,
dnsPolicy: "ClusterFirstWithHostNet",
containers: [{
name: "poll",
image: "node:22-alpine",
imagePullPolicy: "IfNotPresent",
env: [
...proxyEnv(),
{ name: "POD_NAMESPACE", valueFrom: { fieldRef: { fieldPath: "metadata.namespace" } } },
{ name: "PIPELINE_NAME", value: "hwlab-g14-ci-image-publish" },
{ name: "GIT_URL", value: args.sourceRepo },
{ name: "SOURCE_BRANCH", value: args.sourceBranch },
{ name: "GITOPS_BRANCH", value: args.gitopsBranch },
{ name: "REGISTRY_PREFIX", value: args.registryPrefix },
{ name: "SERVICES", value: "hwlab-cloud-api,hwlab-cloud-web,hwlab-agent-mgr,hwlab-agent-worker,hwlab-gateway,hwlab-gateway-simu,hwlab-box-simu,hwlab-patch-panel,hwlab-router,hwlab-tunnel-client,hwlab-edge-proxy,hwlab-cli,hwlab-agent-skills" },
{ name: "BASE_IMAGE", value: "node:20-bookworm-slim" },
{ name: "CONCURRENCY", value: "4" }
],
volumeMounts: [{ name: "git-ssh", mountPath: "/workspace/git-ssh", readOnly: true }],
command: ["/bin/sh", "-c"],
args: [pollerScript()]
}],
volumes: [{ name: "git-ssh", secret: { secretName: "hwlab-git-ssh" } }]
}
}
}
}
}
};
}
function registryManifest() {
return {
apiVersion: "v1",
@@ -574,7 +864,7 @@ function argoApplication(args) {
},
spec: {
project: "hwlab-g14",
source: { repoURL: args.sourceRepo, targetRevision: args.sourceBranch, path: "deploy/gitops/g14/runtime" },
source: { repoURL: args.sourceRepo, targetRevision: args.gitopsBranch, path: "deploy/gitops/g14/runtime" },
destination: { server: "https://kubernetes.default.svc", namespace: "hwlab-dev" },
syncPolicy: { automated: { prune: false, selfHeal: true }, syncOptions: ["CreateNamespace=true", "ApplyOutOfSyncOnly=true"] }
}
@@ -598,7 +888,8 @@ This directory is generated from \`CI.json\`, \`deploy/deploy.json\`, and \`depl
- Target: G14 k3s only.
- CI: Tekton Pipeline \`hwlab-ci/hwlab-g14-ci-image-publish\`.
- Artifact contract: images are tagged by source git commit \`${source.short}\` and pushed to \`${args.registryPrefix}\`.
- CD: Argo CD Application \`argocd/hwlab-g14-dev\` consumes \`deploy/gitops/g14/runtime\`.
- Branch split: source branch \`${args.sourceBranch}\` remains the watched code branch; generated desired state is promoted to \`${args.gitopsBranch}\`.
- CD: Argo CD Application \`argocd/hwlab-g14-dev\` consumes \`${args.gitopsBranch}:deploy/gitops/g14/runtime\`.
- D601 boundary: this path does not target D601, does not use UniDesk Code Queue, and does not change D601 production traffic.
- Concurrency: CI builds are per source commit and do not acquire the legacy DEV CD Lease; Argo CD reconciles the Git desired state.
`;
@@ -630,6 +921,7 @@ async function plannedFiles(args) {
sourceCommit: source.full,
sourceShortCommit: source.short,
sourceBranch: args.sourceBranch,
gitopsBranch: args.gitopsBranch,
sourceRepo: args.sourceRepo,
registryPrefix: args.registryPrefix,
runtimePath: "deploy/gitops/g14/runtime",
@@ -640,6 +932,7 @@ async function plannedFiles(args) {
putJson("registry/registry.yaml", registryManifest());
putJson("tekton/rbac.yaml", tektonRbac());
putJson("tekton/pipeline.yaml", tektonPipeline(ci));
putJson("tekton/poller.yaml", tektonPollerCronJob(args));
putJson("tekton/pipelinerun.sample.yaml", tektonPipelineRunTemplate({ source, args }));
putJson("argocd/project.yaml", argoProject());
putJson("argocd/application.yaml", argoApplication(args));