Merge pull request #623 from pikasTech/fix/v02-env-launcher-fetch-ref

fix: checkout env reuse runtime from source refs
This commit is contained in:
Lyon
2026-05-31 15:51:01 +08:00
committed by GitHub
3 changed files with 19 additions and 4 deletions
@@ -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();
+7
View File
@@ -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 <sha>` 依赖 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` 结果作为运行真相。
+5 -2
View File
@@ -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 }));