diff --git a/deploy/runtime/launcher/hwlab-env-reuse-launcher.ts b/deploy/runtime/launcher/hwlab-env-reuse-launcher.ts index 04ffc123..ca7da95a 100644 --- a/deploy/runtime/launcher/hwlab-env-reuse-launcher.ts +++ b/deploy/runtime/launcher/hwlab-env-reuse-launcher.ts @@ -5,18 +5,23 @@ import path from "node:path"; const bootRepo = requiredEnv("HWLAB_BOOT_REPO"); const bootCommit = requiredEnv("HWLAB_BOOT_COMMIT"); const bootSh = requiredEnv("HWLAB_BOOT_SH"); +const bootRef = requiredEnv("HWLAB_BOOT_REF"); const checkoutDir = process.env.HWLAB_BOOT_CHECKOUT_DIR || "/workspace/hwlab-boot/repo"; const runtimeNodeModules = process.env.HWLAB_RUNTIME_NODE_MODULES || "/opt/hwlab-env/node_modules"; if (!/^[a-f0-9]{40}$/u.test(bootCommit)) fail("HWLAB_BOOT_COMMIT must be a full 40-char SHA"); if (path.isAbsolute(bootSh) || bootSh.split("/").includes("..")) fail("HWLAB_BOOT_SH must be repo-relative"); +if ( + !/^[A-Za-z0-9._/-]+$/u.test(bootRef) || + bootRef.startsWith("-") || + bootRef.includes("..") +) fail("HWLAB_BOOT_REF must be a safe branch or ref name"); const readUrl = process.env.HWLAB_BOOT_READ_URL || mirrorReadUrl(bootRepo); process.env.HWLAB_BUN_COMMAND ||= process.execPath; mkdirSync(path.dirname(checkoutDir), { recursive: true }); rmSync(checkoutDir, { recursive: true, force: true }); -run("git", ["clone", "--no-checkout", readUrl, checkoutDir], "/"); -run("git", ["fetch", "--depth", "1", "origin", bootCommit], checkoutDir); +run("git", ["clone", "--no-checkout", "--single-branch", "--branch", bootRef, readUrl, checkoutDir], "/"); run("git", ["checkout", "--detach", bootCommit], checkoutDir); linkNodeModules(); diff --git a/docs/reference/spec-v02-cicd.md b/docs/reference/spec-v02-cicd.md index 5d229dab..5740311d 100644 --- a/docs/reference/spec-v02-cicd.md +++ b/docs/reference/spec-v02-cicd.md @@ -420,6 +420,13 @@ code boot metadata 固定映射到三个启动环境变量: CI/CD 必须把三变量同时写入 `deploy/artifact-catalog.v02.json`、rendered workload Pod template env/annotation 和 runtime health identity。三变量是由 lane 自动推导的发布事实,不是人工 OPS 参数;如果自动推导缺失或无法证明 `HWLAB_BOOT_COMMIT` 属于 `v0.2` 允许 ancestry,本轮 promotion 必须失败。 +`HWLAB_BOOT_REF` 是由 lane 的 source branch 派生的 mirror transport hint, +不属于发布身份三变量,也不得作为人工 OPS 参数。env-reuse launcher 必须先通过 +`HWLAB_BOOT_REF` 读取 mirror 中可广告的分支/ref,再 checkout `HWLAB_BOOT_COMMIT`; +不得直接 `git fetch ` 依赖 smart HTTP 对 unadvertised object 的支持。 +这样可以稳定支持历史 commit rollout、Argo 回滚和 env image 复用, +同时保持发布身份仍由 `HWLAB_BOOT_REPO`、`HWLAB_BOOT_COMMIT`、`HWLAB_BOOT_SH` 三变量表达。 + 推荐启动形态是 initContainer + `emptyDir` + generic env image。initContainer 或 env image 内的 launcher 读取三变量,通过 resolver 把 `HWLAB_BOOT_REPO` 的只读 fetch/checkout 自动分流到 `devops-infra` git mirror/cache,按 `HWLAB_BOOT_COMMIT` checkout 到 Pod 私有 `emptyDir`;main container 复用同一 env image,并执行 checkout 后代码目录里的 `$HWLAB_BOOT_SH`。env image 只承载系统依赖、runtime、launcher、git client 和通用工具,不把业务代码或旧 boot script 当作运行真相。 code-only 变更时,planner 必须输出 `envChanged=false`、`codeChanged=true`,跳过 BuildKit image publish,复用上一版 env image digest,只更新 catalog 和 workload Pod template 中的 `HWLAB_BOOT_COMMIT`、code identity annotation 与相关 health metadata。Kubernetes 原生 Deployment/StatefulSet rolling update 仍由 Pod template hash 变化触发;不需要在容器内长期驻留 watcher,也不把 `git pull` 结果作为运行真相。 diff --git a/scripts/g14-gitops-render.mjs b/scripts/g14-gitops-render.mjs index 1aadc531..fcc0ed20 100644 --- a/scripts/g14-gitops-render.mjs +++ b/scripts/g14-gitops-render.mjs @@ -746,7 +746,7 @@ function deployServicesForProfile(deploy, profile) { return services; } -function transformWorkloads({ workloads, deploy, catalog, source, registryPrefix, runtimeEndpoint, webEndpoint, profile = "dev", useDeployImages = false }) { +function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch = defaultBranch, registryPrefix, runtimeEndpoint, webEndpoint, profile = "dev", useDeployImages = false }) { const result = cloneJson(workloads); const deployServices = deployServicesForProfile(deploy, profile); const namespace = namespaceNameForProfile(profile); @@ -836,6 +836,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, registryPrefix upsertEnv(container.env, "HWLAB_RUNTIME_MODE", bootMetadata.runtimeMode); upsertEnv(container.env, "HWLAB_BOOT_REPO", bootMetadata.bootRepo); upsertEnv(container.env, "HWLAB_BOOT_COMMIT", bootMetadata.bootCommit); + upsertEnv(container.env, "HWLAB_BOOT_REF", sourceBranch); upsertEnv(container.env, "HWLAB_BOOT_SH", bootMetadata.bootSh); upsertEnv(container.env, "HWLAB_BOOT_READ_URL", defaultV02GitReadUrl); upsertEnv(container.env, "HWLAB_ENVIRONMENT_IMAGE", bootMetadata.environmentImage ?? ""); @@ -3177,6 +3178,8 @@ else fi /script/install-hooks.sh git -C "$repo_path" config uploadpack.hideRefs refs/mirror-stage +git -C "$repo_path" config uploadpack.allowReachableSHA1InWant true +git -C "$repo_path" config uploadpack.allowAnySHA1InWant false git -C "$repo_path" config --unset-all remote.origin.fetch 2>/dev/null || true git -C "$repo_path" config --add remote.origin.fetch '+refs/heads/v0.2:refs/mirror-stage/heads/v0.2' git -C "$repo_path" config --add remote.origin.fetch '+refs/heads/v0.2-gitops:refs/mirror-stage/heads/v0.2-gitops' @@ -4433,7 +4436,7 @@ async function plannedFiles(args) { putJson(`${runtimePath}/code-agent-codex-config.yaml`, transformListNamespace(config, namespace, profileLabels, annotations)); putJson(`${runtimePath}/services.yaml`, transformServices({ services, namespace, labels: profileLabels, annotations, profile })); putJson(`${runtimePath}/health-contract.yaml`, transformHealthContract(health, namespace, profileLabels, annotations, endpoints.runtimeEndpoint, endpoints.webEndpoint, profile)); - putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, catalog: artifactCatalog, source, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile, useDeployImages: args.useDeployImages })); + putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, catalog: artifactCatalog, source, sourceBranch: args.sourceBranch, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile, useDeployImages: args.useDeployImages })); putJson(`${runtimePath}/deepseek-proxy.yaml`, deepSeekProxyManifest({ profile, source, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages })); if (profile === "v02") putJson(`${runtimePath}/postgres.yaml`, v02PostgresManifest({ migrationSql, source })); if (includeDeviceAgent) putJson(`${runtimePath}/device-agent-71-freq.yaml`, deviceAgent71FreqManifest({ profile, source, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages }));