fix: enable v02 env reuse for runtime services
This commit is contained in:
+10
-2
@@ -123,10 +123,18 @@
|
||||
"runtimePath": "deploy/gitops/g14/runtime-v02",
|
||||
"imageTagMode": "full",
|
||||
"envReuseServices": [
|
||||
"hwlab-cloud-web"
|
||||
"hwlab-cloud-api",
|
||||
"hwlab-cloud-web",
|
||||
"hwlab-gateway",
|
||||
"hwlab-edge-proxy",
|
||||
"hwlab-agent-skills"
|
||||
],
|
||||
"bootScripts": {
|
||||
"hwlab-cloud-web": "deploy/runtime/boot/hwlab-cloud-web.sh"
|
||||
"hwlab-cloud-api": "deploy/runtime/boot/hwlab-cloud-api.sh",
|
||||
"hwlab-cloud-web": "deploy/runtime/boot/hwlab-cloud-web.sh",
|
||||
"hwlab-gateway": "deploy/runtime/boot/hwlab-gateway.sh",
|
||||
"hwlab-edge-proxy": "deploy/runtime/boot/hwlab-edge-proxy.sh",
|
||||
"hwlab-agent-skills": "deploy/runtime/boot/hwlab-agent-skills.sh"
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
export HWLAB_SERVICE_ID="hwlab-agent-skills"
|
||||
export HWLAB_ARTIFACT_KIND="skills-bundle"
|
||||
export HWLAB_SERVICE_ENTRYPOINT="skills/hwlab-agent-runtime/SKILL.md"
|
||||
export HWLAB_RUNTIME_MODE="${HWLAB_RUNTIME_MODE:-env-reuse-git-mirror-checkout}"
|
||||
export HWLAB_COMMIT_ID="${HWLAB_BOOT_COMMIT:?HWLAB_BOOT_COMMIT is required}"
|
||||
export HWLAB_REVISION="${HWLAB_BOOT_COMMIT}"
|
||||
export HWLAB_SKILLS_COMMIT_ID="${HWLAB_SKILLS_COMMIT_ID:-${HWLAB_BOOT_COMMIT}}"
|
||||
export HWLAB_IMAGE="${HWLAB_ENVIRONMENT_IMAGE:-${HWLAB_IMAGE:-}}"
|
||||
export HWLAB_IMAGE_DIGEST="${HWLAB_ENVIRONMENT_DIGEST:-${HWLAB_IMAGE_DIGEST:-unknown}}"
|
||||
export PORT="${PORT:-${HWLAB_PORT:-7430}}"
|
||||
export HWLAB_PORT="$PORT"
|
||||
|
||||
bun_bin="${HWLAB_BUN_COMMAND:-}"
|
||||
if [ -z "$bun_bin" ]; then
|
||||
if command -v bun >/dev/null 2>&1; then
|
||||
bun_bin="$(command -v bun)"
|
||||
elif [ -x /usr/local/bin/bun ]; then
|
||||
bun_bin="/usr/local/bin/bun"
|
||||
elif [ -x ./node_modules/.bin/bun ]; then
|
||||
bun_bin="./node_modules/.bin/bun"
|
||||
else
|
||||
echo "hwlab-agent-skills boot failed: bun not found" >&2
|
||||
exit 127
|
||||
fi
|
||||
fi
|
||||
|
||||
cat > .hwlab-agent-skills-runtime.mjs <<'NODE'
|
||||
import { Buffer } from "node:buffer";
|
||||
import { createServer } from "node:http";
|
||||
import { readdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const serviceId = process.env.HWLAB_SERVICE_ID || "hwlab-agent-skills";
|
||||
const environment = process.env.HWLAB_ENVIRONMENT || "v02";
|
||||
const port = Number.parseInt(process.env.PORT || process.env.HWLAB_PORT || "7430", 10);
|
||||
const processStartedAt = new Date();
|
||||
|
||||
function sendJson(response, statusCode, body) {
|
||||
const payload = JSON.stringify(body, null, 2);
|
||||
response.writeHead(statusCode, {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-length": Buffer.byteLength(payload)
|
||||
});
|
||||
response.end(payload);
|
||||
}
|
||||
|
||||
function imageTagFromReference(imageReference) {
|
||||
const source = String(imageReference || "").trim();
|
||||
const slashIndex = source.lastIndexOf("/");
|
||||
const colonIndex = source.lastIndexOf(":");
|
||||
if (colonIndex <= slashIndex) return null;
|
||||
return source.slice(colonIndex + 1) || null;
|
||||
}
|
||||
|
||||
function shortRevision(value) {
|
||||
const source = String(value || "").trim();
|
||||
return source.length >= 12 ? source.slice(0, 12) : source || "unknown";
|
||||
}
|
||||
|
||||
function skillNames() {
|
||||
const root = path.join(process.cwd(), "skills");
|
||||
try {
|
||||
return readdirSync(root, { withFileTypes: true })
|
||||
.filter((item) => item.isDirectory())
|
||||
.map((item) => item.name)
|
||||
.sort();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function healthPayload() {
|
||||
const commitId = process.env.HWLAB_COMMIT_ID || process.env.HWLAB_BOOT_COMMIT || "unknown";
|
||||
const imageReference = process.env.HWLAB_IMAGE || process.env.HWLAB_ENVIRONMENT_IMAGE || "unknown";
|
||||
const names = skillNames();
|
||||
return {
|
||||
serviceId,
|
||||
environment,
|
||||
status: "ok",
|
||||
artifactKind: "skills-bundle",
|
||||
runtimeMode: process.env.HWLAB_RUNTIME_MODE || "env-reuse-git-mirror-checkout",
|
||||
revision: process.env.HWLAB_REVISION || commitId,
|
||||
commit: {
|
||||
id: commitId,
|
||||
source: process.env.HWLAB_BUILD_SOURCE || "git-mirror-runtime-checkout"
|
||||
},
|
||||
image: {
|
||||
reference: imageReference,
|
||||
tag: imageTagFromReference(imageReference) || shortRevision(commitId),
|
||||
digest: process.env.HWLAB_IMAGE_DIGEST || process.env.HWLAB_ENVIRONMENT_DIGEST || "unknown"
|
||||
},
|
||||
boot: {
|
||||
repo: process.env.HWLAB_BOOT_REPO || null,
|
||||
commit: process.env.HWLAB_BOOT_COMMIT || null,
|
||||
script: process.env.HWLAB_BOOT_SH || null
|
||||
},
|
||||
skills: {
|
||||
root: `${process.cwd()}/skills`,
|
||||
count: names.length,
|
||||
names,
|
||||
commitId: process.env.HWLAB_SKILLS_COMMIT_ID || commitId,
|
||||
entrypoint: process.env.HWLAB_SERVICE_ENTRYPOINT || "skills/hwlab-agent-runtime/SKILL.md"
|
||||
},
|
||||
process: {
|
||||
pid: process.pid,
|
||||
startedAt: processStartedAt.toISOString(),
|
||||
uptimeSeconds: Math.round((Date.now() - processStartedAt.getTime()) / 1000)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const server = createServer((request, response) => {
|
||||
const url = new URL(request.url || "/", "http://hwlab-agent-skills.local");
|
||||
if (url.pathname === "/health/live" || url.pathname === "/health") {
|
||||
sendJson(response, 200, healthPayload());
|
||||
return;
|
||||
}
|
||||
if (url.pathname === "/help") {
|
||||
sendJson(response, 200, { serviceId, commands: ["GET /health", "GET /health/live", "GET /help"] });
|
||||
return;
|
||||
}
|
||||
sendJson(response, 404, { error: "not_found", serviceId, path: url.pathname });
|
||||
});
|
||||
|
||||
server.listen(port, "0.0.0.0", () => {
|
||||
process.stdout.write(JSON.stringify({ serviceId, environment, status: "listening", port }) + "\n");
|
||||
});
|
||||
NODE
|
||||
|
||||
exec "$bun_bin" .hwlab-agent-skills-runtime.mjs
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
export HWLAB_SERVICE_ID="hwlab-cloud-api"
|
||||
export HWLAB_ARTIFACT_KIND="bun-command"
|
||||
export HWLAB_SERVICE_ENTRYPOINT="cmd/hwlab-cloud-api/main.ts"
|
||||
export HWLAB_RUNTIME_MODE="${HWLAB_RUNTIME_MODE:-env-reuse-git-mirror-checkout}"
|
||||
export HWLAB_COMMIT_ID="${HWLAB_BOOT_COMMIT:?HWLAB_BOOT_COMMIT is required}"
|
||||
export HWLAB_REVISION="${HWLAB_BOOT_COMMIT}"
|
||||
export HWLAB_IMAGE="${HWLAB_ENVIRONMENT_IMAGE:-${HWLAB_IMAGE:-}}"
|
||||
export HWLAB_IMAGE_DIGEST="${HWLAB_ENVIRONMENT_DIGEST:-${HWLAB_IMAGE_DIGEST:-unknown}}"
|
||||
export PORT="${PORT:-${HWLAB_CLOUD_API_PORT:-${HWLAB_PORT:-6667}}}"
|
||||
export HWLAB_PORT="$PORT"
|
||||
export HWLAB_CLOUD_API_PORT="$PORT"
|
||||
export HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT="${HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT:-${HWLAB_BOOT_COMMIT}}"
|
||||
export HWLAB_CODE_AGENT_AGENTRUN_REPO_URL="${HWLAB_CODE_AGENT_AGENTRUN_REPO_URL:-${HWLAB_BOOT_READ_URL:-http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git}}"
|
||||
export HWLAB_CODE_AGENT_WORKSPACE="${HWLAB_CODE_AGENT_WORKSPACE:-$PWD}"
|
||||
export HWLAB_CODE_AGENT_CODEX_WORKSPACE="${HWLAB_CODE_AGENT_CODEX_WORKSPACE:-$HWLAB_CODE_AGENT_WORKSPACE}"
|
||||
export CODEX_HOME="${CODEX_HOME:-/codex-home}"
|
||||
|
||||
if [ -z "${HWLAB_CODE_AGENT_CODEX_COMMAND:-}" ] || [ "${HWLAB_CODE_AGENT_CODEX_COMMAND:-}" = "/app/node_modules/.bin/codex" ]; then
|
||||
export HWLAB_CODE_AGENT_CODEX_COMMAND="$PWD/node_modules/.bin/codex"
|
||||
fi
|
||||
if [ -z "${HWLAB_PREINSTALLED_SKILLS_DIR:-}" ] || [ "${HWLAB_PREINSTALLED_SKILLS_DIR:-}" = "/app/skills" ]; then
|
||||
export HWLAB_PREINSTALLED_SKILLS_DIR="$PWD/skills"
|
||||
fi
|
||||
export HWLAB_USER_SKILLS_DIR="${HWLAB_USER_SKILLS_DIR:-/data/user-skills}"
|
||||
if [ -z "${HWLAB_CODE_AGENT_SKILLS_DIRS:-}" ] || [ "${HWLAB_CODE_AGENT_SKILLS_DIRS:-}" = "/app/skills:/data/user-skills" ]; then
|
||||
export HWLAB_CODE_AGENT_SKILLS_DIRS="$HWLAB_PREINSTALLED_SKILLS_DIR:$HWLAB_USER_SKILLS_DIR"
|
||||
fi
|
||||
|
||||
bun_bin="${HWLAB_BUN_COMMAND:-}"
|
||||
if [ -z "$bun_bin" ]; then
|
||||
if command -v bun >/dev/null 2>&1; then
|
||||
bun_bin="$(command -v bun)"
|
||||
elif [ -x /usr/local/bin/bun ]; then
|
||||
bun_bin="/usr/local/bin/bun"
|
||||
elif [ -x ./node_modules/.bin/bun ]; then
|
||||
bun_bin="./node_modules/.bin/bun"
|
||||
else
|
||||
echo "hwlab-cloud-api boot failed: bun not found" >&2
|
||||
exit 127
|
||||
fi
|
||||
fi
|
||||
|
||||
exec "$bun_bin" run cmd/hwlab-cloud-api/main.ts
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
export HWLAB_SERVICE_ID="hwlab-edge-proxy"
|
||||
export HWLAB_ARTIFACT_KIND="bun-command"
|
||||
export HWLAB_SERVICE_ENTRYPOINT="cmd/hwlab-edge-proxy/main.ts"
|
||||
export HWLAB_RUNTIME_MODE="${HWLAB_RUNTIME_MODE:-env-reuse-git-mirror-checkout}"
|
||||
export HWLAB_COMMIT_ID="${HWLAB_BOOT_COMMIT:?HWLAB_BOOT_COMMIT is required}"
|
||||
export HWLAB_REVISION="${HWLAB_BOOT_COMMIT}"
|
||||
export HWLAB_IMAGE="${HWLAB_ENVIRONMENT_IMAGE:-${HWLAB_IMAGE:-}}"
|
||||
export HWLAB_IMAGE_DIGEST="${HWLAB_ENVIRONMENT_DIGEST:-${HWLAB_IMAGE_DIGEST:-unknown}}"
|
||||
edge_port="${HWLAB_EDGE_PORT:-${PORT:-${HWLAB_PORT:-6667}}}"
|
||||
export PORT="${PORT:-$edge_port}"
|
||||
export HWLAB_PORT="$PORT"
|
||||
export HWLAB_EDGE_PORT="$edge_port"
|
||||
export HWLAB_EDGE_LISTEN="${HWLAB_EDGE_LISTEN:-0.0.0.0:$edge_port}"
|
||||
|
||||
bun_bin="${HWLAB_BUN_COMMAND:-}"
|
||||
if [ -z "$bun_bin" ]; then
|
||||
if command -v bun >/dev/null 2>&1; then
|
||||
bun_bin="$(command -v bun)"
|
||||
elif [ -x /usr/local/bin/bun ]; then
|
||||
bun_bin="/usr/local/bin/bun"
|
||||
elif [ -x ./node_modules/.bin/bun ]; then
|
||||
bun_bin="./node_modules/.bin/bun"
|
||||
else
|
||||
echo "hwlab-edge-proxy boot failed: bun not found" >&2
|
||||
exit 127
|
||||
fi
|
||||
fi
|
||||
|
||||
exec "$bun_bin" run cmd/hwlab-edge-proxy/main.ts
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
export HWLAB_SERVICE_ID="hwlab-gateway"
|
||||
export HWLAB_ARTIFACT_KIND="bun-command"
|
||||
export HWLAB_SERVICE_ENTRYPOINT="cmd/hwlab-gateway/main.ts"
|
||||
export HWLAB_RUNTIME_MODE="${HWLAB_RUNTIME_MODE:-env-reuse-git-mirror-checkout}"
|
||||
export HWLAB_COMMIT_ID="${HWLAB_BOOT_COMMIT:?HWLAB_BOOT_COMMIT is required}"
|
||||
export HWLAB_REVISION="${HWLAB_BOOT_COMMIT}"
|
||||
export HWLAB_IMAGE="${HWLAB_ENVIRONMENT_IMAGE:-${HWLAB_IMAGE:-}}"
|
||||
export HWLAB_IMAGE_DIGEST="${HWLAB_ENVIRONMENT_DIGEST:-${HWLAB_IMAGE_DIGEST:-unknown}}"
|
||||
export PORT="${PORT:-${HWLAB_PORT:-7001}}"
|
||||
export HWLAB_PORT="$PORT"
|
||||
export HWLAB_GATEWAY_ENDPOINT="${HWLAB_GATEWAY_ENDPOINT:-http://127.0.0.1:$PORT}"
|
||||
|
||||
bun_bin="${HWLAB_BUN_COMMAND:-}"
|
||||
if [ -z "$bun_bin" ]; then
|
||||
if command -v bun >/dev/null 2>&1; then
|
||||
bun_bin="$(command -v bun)"
|
||||
elif [ -x /usr/local/bin/bun ]; then
|
||||
bun_bin="/usr/local/bin/bun"
|
||||
elif [ -x ./node_modules/.bin/bun ]; then
|
||||
bun_bin="./node_modules/.bin/bun"
|
||||
else
|
||||
echo "hwlab-gateway boot failed: bun not found" >&2
|
||||
exit 127
|
||||
fi
|
||||
fi
|
||||
|
||||
exec "$bun_bin" run cmd/hwlab-gateway/main.ts
|
||||
@@ -126,10 +126,10 @@ devops-infra git mirror 仍是 PipelineRun 和 Argo CD 的集群内读写源。`
|
||||
7. affected service 通过 BuildKit 发布到 G14 本地 registry;reused service 复用 catalog digest。
|
||||
所有 selected service 的 build TaskRun 都只依赖 `plan-artifacts`,不按 service 串行排队,也不设置 8 并发或其他 Pipeline 级限流。
|
||||
实际并发由 Tekton controller、G14 节点资源、PVC I/O、BuildKit sidecar 和本地 registry 承载能力决定。
|
||||
`hwlab-cloud-web` 必须使用 `env-reuse-git-mirror-checkout`。
|
||||
只有 package/runtime/env 输入变化时才构建 `<service>-env` 镜像。
|
||||
纯前端源码、HWPOD workspace 工具或 boot code 变化只更新三变量和 GitOps desired state,
|
||||
不再重新构建业务镜像。
|
||||
v0.2 保留 runtime service(`hwlab-cloud-api`、`hwlab-cloud-web`、`hwlab-gateway`、`hwlab-edge-proxy`、`hwlab-agent-skills`)必须使用 `env-reuse-git-mirror-checkout`。
|
||||
只有 package/runtime/env、env image 构建定义、launcher 输入变化或缺少可复用 env digest 时才构建 `<service>-env` 镜像。
|
||||
service source 或 boot script 的 code-only 变化只更新三变量和 GitOps desired state,不再重新构建业务镜像。
|
||||
CLI、CaseRun、HWPOD workspace 工具、文档、测试和其他非 service 资产不进入 env-reuse service 触发面。
|
||||
8. promotion 刷新 `deploy/artifact-catalog.v02.json`,render `deploy/gitops/g14/runtime-v02/**`,只在本 PipelineRun 的 source commit 仍是当前 `origin/v0.2` head 时推送到 `devops-infra` mirror/relay 的 `v0.2-gitops`;若 source branch 已推进,本轮输出 superseded/no-op,写出 `runtime-ready-required=false`,不得回写旧 GitOps revision。
|
||||
9. `hwlab-g14-v02` 从本地 mirror/relay 的 `v0.2-gitops:deploy/gitops/g14/runtime-v02` 同步到 `hwlab-v02`。
|
||||
10. UniDesk CLI auto-CD 观察定点 `control-plane status --lane v02 --source-commit <merge-head>` 或 `--pipeline-run <name>`,确认 PipelineRun、Argo、runtime workload、public probes 和 planArtifacts build/reuse 摘要。历史 merge head 若因后续 `origin/v0.2` 推进而被 superseded,PR 评论以 superseded 收口;最新 head 仍必须继续收敛到 runtime。
|
||||
@@ -222,7 +222,7 @@ Git 读写都走 `devops-infra` 本地 mirror/relay。读路径用 HTTP mirror c
|
||||
|
||||
任何仍需要访问 GitHub canonical remote 的 SSH 操作都必须显式走 G14 proxy。Host SSH config 可以使用 OpenBSD `nc -X connect -x 127.0.0.1:10808 %h %p`;Tekton `GIT_SSH_COMMAND`、git-mirror `sync`/`flush` 和其他容器内 `git@github.com` 或 `ssh://git@ssh.github.com:443` 路径必须使用 repo-owned Node HTTP CONNECT ProxyCommand,并保留短 `ConnectTimeout` 与 `ServerAlive*`。只改 `HTTP_PROXY`/`HTTPS_PROXY` 对 OpenSSH 无效;BusyBox `nc` 不支持 `-X connect`,不得再把 `ssh.github.com:443` 当作等价 proxy 或让 GitHub SSH 直连反复 60s 超时。
|
||||
|
||||
env image 复用把系统依赖和业务代码身份分离。`environmentDigest` 表示可复用运行环境,`HWLAB_BOOT_REPO`、`HWLAB_BOOT_COMMIT` 和 `HWLAB_BOOT_SH` 表示本次代码启动身份。code-only 变更只更新 boot metadata 和 runtime identity,不发布新 env image;只有真实 env 输入变化或缺少可复用 env digest 时才进入 env rebuild。AgentRun v0.1 是 HWLAB 外部共享执行基础设施,`hwlab-agent-worker` 不再作为 v0.2 service image 或 suspended Job template 参与 planner、BuildKit、artifact catalog 或 GitOps render。`package.json` 只按 dependency/runtime 字段参与 env/runtime hash,`scripts` 只属于开发入口,不得因为清理旧门禁脚本而重建所有 service 或重建 env image。
|
||||
env image 复用把系统依赖和业务代码身份分离。`environmentDigest` 表示可复用运行环境,`HWLAB_BOOT_REPO`、`HWLAB_BOOT_COMMIT` 和 `HWLAB_BOOT_SH` 表示本次代码启动身份。service code-only 变更只更新 boot metadata 和 runtime identity,不发布新 env image;只有真实 env 输入、env image 构建定义、launcher 输入变化或缺少可复用 env digest 时才进入 env rebuild。AgentRun v0.1 是 HWLAB 外部共享执行基础设施,`hwlab-agent-worker` 不再作为 v0.2 service image 或 suspended Job template 参与 planner、BuildKit、artifact catalog 或 GitOps render。`package.json` 只按 dependency/runtime 字段参与 env/runtime hash,`scripts` 只属于开发入口,不得因为清理旧门禁脚本而重建所有 service 或重建 env image。CLI、CaseRun、HWPOD workspace 工具、文档和测试不属于 v0.2 service runtime 输入,不得因为 env reuse 机制被纳入 service rollout 或 build 范围;若某个工具文件确实被浏览器或服务源码直接 import,必须作为对应 service 的显式 component path 建模,而不能通过宽泛 shared path 扩散到所有服务。env image 构建定义只包含真正改变 `<service>-env` 镜像内容或启动器行为的文件,不能把 planner、测试、CLI 或 host helper 混入环境输入。
|
||||
|
||||
BuildKit publish 采用 service 级全并行 fan-out。
|
||||
每个 service build task 拥有独立 BuildKit sidecar、独立 service workdir 和独立 report 文件,均从同一 `plan-artifacts` 结果判断是否需要执行。
|
||||
@@ -467,7 +467,7 @@ CI/CD 必须把三变量同时写入 `deploy/artifact-catalog.v02.json`、render
|
||||
|
||||
推荐启动形态是 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` 结果作为运行真相。
|
||||
service 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。非 service 变更必须输出空 `affectedServices` 或只影响自身所属的非 runtime 流程,不得为了 env reuse 触发 runtime service rollout。Kubernetes 原生 Deployment/StatefulSet rolling update 仍由 Pod template hash 变化触发;不需要在容器内长期驻留 watcher,也不把 `git pull` 结果作为运行真相。
|
||||
|
||||
`devops-infra` git mirror/relay 是 allowlisted、PVC-backed 或等价持久化服务,负责缓存 GitHub 对象并承接 `v0.2-gitops` 本地写入。只有 mirror/relay sync/flush 边界持有 GitHub 远端凭证;`hwlab-v02`、AgentRun 和其他业务 namespace 只能访问只读 mirror/cache endpoint,不持有 GitHub deploy key。runtime checkout 失败、mirror miss 超过明确等待窗口、commit ancestry 不满足 lane 约束或 boot script 校验失败时,Pod 必须启动失败或 NotReady,不得回退到 env image 内旧代码,也不得在业务 namespace 直连 GitHub 拉取。
|
||||
|
||||
@@ -547,8 +547,9 @@ registry 与 git mirror/relay 分属不同基础设施边界。registry 保持
|
||||
- `gitops-promote` 推送本地 mirror/relay 的 `v0.2-gitops` 后应触发 `argocd/hwlab-g14-v02` hard refresh,减少 GitOps push 与 Argo 仓库发现之间的漂移窗口;refresh 触发失败只作为低噪声事件输出,最终通过仍由 `runtime-ready` 判定。
|
||||
- mirror/relay status 必须能显示本地 `v0.2-gitops` revision、GitHub 已 flush revision、pending flush revision 和最近一次 flush 错误;CI 通过不要求 GitHub flush 已完成,但 auto-CD 收口必须在 `pendingFlush=true` 时触发 flush 并最终观察到 `pendingFlush=false`。
|
||||
- `runtime-ready` 必须以 workload Pod template 的 source commit 判断 Argo 刷新,并在 Argo 刷新超时、observer RBAC 不足或 workload 未就绪时失败;不得把 CrashLoop、未刷新或公网不可用的 runtime 标成绿色。
|
||||
- 启用 env 容器复用的服务必须在 catalog、Pod template annotation/env 和 `/health/live` 或等价 runtime identity 中同时暴露 env image digest 与 `HWLAB_BOOT_REPO`/`HWLAB_BOOT_COMMIT`/`HWLAB_BOOT_SH`;只暴露镜像 digest 不能证明 code-only rollout 已生效。
|
||||
- 启用 env 容器复用的 runtime service 必须在 catalog、Pod template annotation/env 和 `/health/live` 或等价 runtime identity 中同时暴露 env image digest 与 `HWLAB_BOOT_REPO`/`HWLAB_BOOT_COMMIT`/`HWLAB_BOOT_SH`;只暴露镜像 digest 不能证明 service code-only rollout 已生效。
|
||||
- code-only rollout 的 PipelineRun 必须能证明没有发布新 env image、复用了上一版 env digest、只更新 boot commit/code identity,并由 Kubernetes Pod template hash 触发滚动。
|
||||
- 非 service 资产变更必须能证明没有因为 env reuse 触发 runtime service build 或 rollout;典型非 service 资产包括 `tools/hwlab-cli*`、CaseRun runner/helper、`.hwlab/` workspace spec、docs 和 tests。
|
||||
- `devops-infra` git mirror/cache miss、commit ancestry 拒绝或 boot script 校验失败必须导致本轮 rollout fail closed,不得隐式回退到 GitHub 或 env image 内旧代码。
|
||||
- `http://74.48.78.17:19666/` 返回 `v0.2` Cloud Web。
|
||||
- `http://74.48.78.17:19667/health/live` 返回 `v0.2` runtime health,payload 中的 namespace、revision 或 runtime identity 能与 `hwlab-v02`/`v0.2` 对齐。
|
||||
@@ -616,7 +617,8 @@ GitOps branch 已更新、source branch render 通过、PipelineRun 名称存在
|
||||
| Argo v02 Application | 已实现 | `hwlab-g14-v02` 指向 v02 GitOps path 和 namespace。 |
|
||||
| FRP `19666/19667` 入口 | 已实现 | 由 `hwlab-v02-frpc` 与 master frps allowlist 共同提供。 |
|
||||
| SecretRef 独立与 provider 验收 | 已实现/持续约束 | SecretRef 已独立;验收必须做真实短连接聊天。 |
|
||||
| env 容器复用三变量启动 | 已实现/持续约束 | env-reuse fast lane 已由 CI/CD 自动推导 `HWLAB_BOOT_REPO`、`HWLAB_BOOT_COMMIT`、`HWLAB_BOOT_SH`;code-only rollout 复用 env image digest,只更新代码身份。 |
|
||||
| env 容器复用三变量启动 | 已实现/持续约束 | 五个 v0.2 runtime service 均启用 env-reuse fast lane,并由 CI/CD 自动推导 `HWLAB_BOOT_REPO`、`HWLAB_BOOT_COMMIT`、`HWLAB_BOOT_SH`;service code-only rollout 复用 env image digest,只更新代码身份。 |
|
||||
| 非 service 资产不触发 env reuse | 已实现/持续约束 | CLI、CaseRun、HWPOD workspace 工具、`.hwlab/`、docs 和 tests 不进入 env-reuse service 触发面;被服务直接 import 的文件必须显式挂到对应 service component path。 |
|
||||
| `devops-infra` git mirror/relay 加速 | 已实现/持续约束 | source/catalog/runtime checkout 读路径和 GitOps promotion 写路径均使用独立基础设施集群 mirror/relay;Argo source 指向本地 mirror,GitHub flush 由 UniDesk CLI 手动触发,不设置 CronJob。runtime namespace 不持有 GitHub deploy key,registry 保持 G14 `hwlab-ci/hwlab-registry`。 |
|
||||
| `hwlab-cli` 不进 CI/CD service matrix | 已实现/持续约束 | CLI 是固定 repo 短连接 client,不发布镜像、不生成 artifact、不创建 `build-hwlab-cli` TaskRun;相关旧入口出现时直接删除。 |
|
||||
| CI/CD fast lane 性能预算 | 已实现/持续约束 | env-reuse no-op 目标约 40s;真实 runtime rollout 目标约 50s;不得恢复 `prepare-source` 依赖安装、GitHub 关键路径写入或 no-op runtime 等待。 |
|
||||
|
||||
+170
-107
@@ -16,6 +16,14 @@ import {
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const V02_RUNTIME_SERVICE_IDS = Object.freeze([
|
||||
"hwlab-cloud-api",
|
||||
"hwlab-cloud-web",
|
||||
"hwlab-gateway",
|
||||
"hwlab-edge-proxy",
|
||||
"hwlab-agent-skills"
|
||||
]);
|
||||
|
||||
test("G14 CI planner maps bridge changes to cloud-api only", async () => {
|
||||
const repo = await createFixtureRepo();
|
||||
await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.ts"), "console.log('bridge v2');\n");
|
||||
@@ -56,7 +64,7 @@ test("planner rebuilds when catalog digest is missing instead of blocking reuse"
|
||||
assert.equal(plan.services[0].reason.includes("catalog-digest-missing"), true);
|
||||
});
|
||||
|
||||
test("planner rebuilds service image when catalog component input hash is stale", async () => {
|
||||
test("v02 planner rolls cloud-api service code changes without rebuilding service image", async () => {
|
||||
const repo = await createFixtureRepo();
|
||||
const initialPlan = await createG14CiPlan({
|
||||
repoRoot: repo,
|
||||
@@ -67,21 +75,18 @@ test("planner rebuilds service image when catalog component input hash is stale"
|
||||
services: ["hwlab-cloud-api", "hwlab-cloud-web"]
|
||||
});
|
||||
const initialCloudApi = initialPlan.services.find((service) => service.serviceId === "hwlab-cloud-api");
|
||||
const staleHash = initialCloudApi.componentInputHash;
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", {
|
||||
sourceCommitId: initialPlan.sourceCommitId,
|
||||
cloudApiComponentInputHash: staleHash
|
||||
environmentInputHashes: { "hwlab-cloud-api": initialCloudApi.environmentInputHash },
|
||||
codeInputHashes: { "hwlab-cloud-api": initialCloudApi.codeInputHash },
|
||||
bootCommits: { "hwlab-cloud-api": initialPlan.sourceCommitId }
|
||||
}), null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.v02.json"]);
|
||||
await git(repo, ["commit", "-m", "seed catalog with current cloud-api hash"]);
|
||||
await git(repo, ["commit", "-m", "seed catalog with current cloud-api env reuse hashes"]);
|
||||
|
||||
await writeFile(path.join(repo, "tools/src/hwpod-harness-lib.ts"), "export const hwpod = true;\n");
|
||||
await git(repo, ["add", "tools/src/hwpod-harness-lib.ts"]);
|
||||
await git(repo, ["commit", "-m", "change shared hwpod runtime"]);
|
||||
await mkdir(path.join(repo, "docs/reference"), { recursive: true });
|
||||
await writeFile(path.join(repo, "docs/reference/g14-gitops-cicd.md"), "document planner reuse invariant\n");
|
||||
await git(repo, ["add", "docs/reference/g14-gitops-cicd.md"]);
|
||||
await git(repo, ["commit", "-m", "document planner reuse invariant"]);
|
||||
await writeFile(path.join(repo, "cmd/hwlab-cloud-api/main.ts"), "console.log('api v2');\n");
|
||||
await git(repo, ["add", "cmd/hwlab-cloud-api/main.ts"]);
|
||||
await git(repo, ["commit", "-m", "change cloud-api service code"]);
|
||||
|
||||
const plan = await createG14CiPlan({
|
||||
repoRoot: repo,
|
||||
@@ -93,41 +98,20 @@ test("planner rebuilds service image when catalog component input hash is stale"
|
||||
});
|
||||
const cloudApi = plan.services.find((service) => service.serviceId === "hwlab-cloud-api");
|
||||
assert.equal(cloudApi.affected, true);
|
||||
assert.equal(cloudApi.buildRequired, true);
|
||||
assert.equal(cloudApi.componentInputChanged, true);
|
||||
assert.equal(cloudApi.reuse.status, "component-input-mismatch");
|
||||
assert.deepEqual(cloudApi.reason, ["component-input-mismatch"]);
|
||||
assert.ok(plan.buildServices.includes("hwlab-cloud-api"));
|
||||
assert.equal(cloudApi.runtimeMode, "env-reuse-git-mirror-checkout");
|
||||
assert.equal(cloudApi.envChanged, false);
|
||||
assert.equal(cloudApi.codeChanged, true);
|
||||
assert.equal(cloudApi.buildRequired, false);
|
||||
assert.deepEqual(cloudApi.reason, ["code-input-changed", "component-path-changed"]);
|
||||
assert.deepEqual(plan.buildServices, []);
|
||||
});
|
||||
|
||||
test("planner rejects polluted catalog hash that does not match catalog source tree", async () => {
|
||||
const repo = await createFixtureRepo();
|
||||
const oldSourceCommit = (await git(repo, ["rev-parse", "HEAD~1"])).stdout.trim();
|
||||
|
||||
await writeFile(path.join(repo, "tools/src/hwpod-harness-lib.ts"), "export const hwpod = true;\n");
|
||||
await git(repo, ["add", "tools/src/hwpod-harness-lib.ts"]);
|
||||
await git(repo, ["commit", "-m", "change shared hwpod runtime"]);
|
||||
|
||||
const currentPlan = await createG14CiPlan({
|
||||
repoRoot: repo,
|
||||
lane: "v02",
|
||||
baseRef: "HEAD",
|
||||
targetRef: "HEAD",
|
||||
artifactCatalogPath: "deploy/nonexistent-artifact-catalog.json",
|
||||
services: ["hwlab-cloud-api"]
|
||||
});
|
||||
const currentCloudApi = currentPlan.services.find((service) => service.serviceId === "hwlab-cloud-api");
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", {
|
||||
sourceCommitId: oldSourceCommit,
|
||||
cloudApiComponentInputHash: currentCloudApi.componentInputHash
|
||||
}), null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.v02.json"]);
|
||||
await git(repo, ["commit", "-m", "pollute catalog provenance"]);
|
||||
|
||||
test("v02 planner rebuilds env image when an env reuse catalog digest is missing", async () => {
|
||||
const repo = await createFixtureRepo({ catalog: "missing-digest" });
|
||||
await mkdir(path.join(repo, "docs/reference"), { recursive: true });
|
||||
await writeFile(path.join(repo, "docs/reference/g14-gitops-cicd.md"), "docs only after provenance pollution\n");
|
||||
await writeFile(path.join(repo, "docs/reference/g14-gitops-cicd.md"), "docs only with missing env digest\n");
|
||||
await git(repo, ["add", "docs/reference/g14-gitops-cicd.md"]);
|
||||
await git(repo, ["commit", "-m", "docs after polluted catalog"]);
|
||||
await git(repo, ["commit", "-m", "docs after missing env digest"]);
|
||||
|
||||
const plan = await createG14CiPlan({
|
||||
repoRoot: repo,
|
||||
@@ -140,8 +124,9 @@ test("planner rejects polluted catalog hash that does not match catalog source t
|
||||
const cloudApi = plan.services.find((service) => service.serviceId === "hwlab-cloud-api");
|
||||
assert.equal(cloudApi.affected, true);
|
||||
assert.equal(cloudApi.buildRequired, true);
|
||||
assert.equal(cloudApi.reuse.status, "component-source-mismatch");
|
||||
assert.deepEqual(cloudApi.reason, ["component-source-mismatch"]);
|
||||
assert.equal(cloudApi.envChanged, true);
|
||||
assert.equal(cloudApi.reuse.status, "not-reused");
|
||||
assert.deepEqual(cloudApi.reason, ["environment-input-changed"]);
|
||||
});
|
||||
|
||||
test("artifact reuse paths preserve catalog provenance instead of current planner provenance", async () => {
|
||||
@@ -168,11 +153,11 @@ test("component model uses built-in service paths", () => {
|
||||
"internal/db/",
|
||||
"skills/hwlab-agent-runtime/"
|
||||
]);
|
||||
assert.equal(models[0].sharedPaths.includes(".hwlab/"), true);
|
||||
assert.deepEqual(models[0].sharedPaths, ["internal/build-metadata.mjs", "internal/protocol/"]);
|
||||
assert.deepEqual(matchingPaths(["cmd/hwlab-cloud-api/main.ts"], models[0].componentPaths), ["cmd/hwlab-cloud-api/main.ts"]);
|
||||
});
|
||||
|
||||
test("v02 planner treats preinstalled hwpod-spec changes as shared runtime inputs", async () => {
|
||||
test("v02 planner keeps hwpod-spec changes out of runtime service env reuse", async () => {
|
||||
const repo = await createFixtureRepo();
|
||||
await writeFile(path.join(repo, ".hwlab/hwpod-spec.yaml"), "kind: Hwpod\nmetadata:\n name: changed-preinstalled\nspec:\n nodeBinding:\n nodeId: node-changed\n workspace:\n path: F:\\\\Work\\\\D601-HWLAB\n targetDevice:\n board: D601-F103-V2\n debugProbe:\n type: daplink\n ioProbe:\n uart:\n port: COM9\n", "utf8");
|
||||
await git(repo, ["add", ".hwlab/hwpod-spec.yaml"]);
|
||||
@@ -186,10 +171,10 @@ test("v02 planner treats preinstalled hwpod-spec changes as shared runtime input
|
||||
artifactCatalogPath: "deploy/artifact-catalog.v02.json",
|
||||
services: ["hwlab-cloud-api", "hwlab-cloud-web"]
|
||||
});
|
||||
assert.deepEqual(plan.affectedServices, ["hwlab-cloud-api", "hwlab-cloud-web"]);
|
||||
assert.deepEqual(plan.buildServices, ["hwlab-cloud-api"]);
|
||||
assert.deepEqual(plan.services.find((service) => service.serviceId === "hwlab-cloud-api").reason, ["shared-runtime-changed"]);
|
||||
assert.equal(plan.services.find((service) => service.serviceId === "hwlab-cloud-web").codeChanged, true);
|
||||
assert.deepEqual(plan.affectedServices, []);
|
||||
assert.deepEqual(plan.buildServices, []);
|
||||
assert.equal(plan.services.find((service) => service.serviceId === "hwlab-cloud-api").affected, false);
|
||||
assert.equal(plan.services.find((service) => service.serviceId === "hwlab-cloud-web").affected, false);
|
||||
});
|
||||
|
||||
test("v02 planner treats AgentRun runtime prompt changes as cloud-api inputs", async () => {
|
||||
@@ -209,14 +194,11 @@ test("v02 planner treats AgentRun runtime prompt changes as cloud-api inputs", a
|
||||
services: ["hwlab-cloud-api", "hwlab-cloud-web"]
|
||||
});
|
||||
const initialCloudWeb = initialPlan.services.find((service) => service.serviceId === "hwlab-cloud-web");
|
||||
const initialHashes = Object.fromEntries(initialPlan.services.map((service) => [service.serviceId, service.componentInputHash]));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", {
|
||||
sourceCommitId: initialPlan.sourceCommitId,
|
||||
componentInputHashes: initialHashes,
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", catalogOptionsFromPlan(initialPlan, {
|
||||
cloudWebEnvironmentInputHash: initialCloudWeb.environmentInputHash,
|
||||
cloudWebCodeInputHash: initialCloudWeb.codeInputHash,
|
||||
cloudWebBootCommit: initialPlan.sourceCommitId
|
||||
}), null, 2));
|
||||
})), null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.v02.json"]);
|
||||
await git(repo, ["commit", "-m", "seed v02 catalog with agentrun prompt"]);
|
||||
|
||||
@@ -233,11 +215,13 @@ test("v02 planner treats AgentRun runtime prompt changes as cloud-api inputs", a
|
||||
services: ["hwlab-cloud-api", "hwlab-cloud-web"]
|
||||
});
|
||||
assert.deepEqual(plan.affectedServices, ["hwlab-cloud-api"]);
|
||||
assert.deepEqual(plan.buildServices, ["hwlab-cloud-api"]);
|
||||
assert.deepEqual(plan.buildServices, []);
|
||||
assert.deepEqual(plan.rolloutServices, ["hwlab-cloud-api"]);
|
||||
const cloudApi = plan.services.find((service) => service.serviceId === "hwlab-cloud-api");
|
||||
assert.deepEqual(cloudApi.changedPaths, ["internal/agent/prompts/hwlab-v02-runtime.md"]);
|
||||
assert.deepEqual(cloudApi.reason, ["component-path-changed"]);
|
||||
assert.equal(cloudApi.envChanged, false);
|
||||
assert.equal(cloudApi.codeChanged, true);
|
||||
assert.deepEqual(cloudApi.reason, ["code-input-changed", "component-path-changed"]);
|
||||
});
|
||||
|
||||
test("planner scopes cloud-web runtime changes to cloud-web", async () => {
|
||||
@@ -265,14 +249,11 @@ test("v02 planner treats shared trace renderer changes as cloud-web code rollout
|
||||
services: ["hwlab-cloud-api", "hwlab-cloud-web"]
|
||||
});
|
||||
const initialCloudWeb = initialPlan.services.find((service) => service.serviceId === "hwlab-cloud-web");
|
||||
const initialHashes = Object.fromEntries(initialPlan.services.map((service) => [service.serviceId, service.componentInputHash]));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", {
|
||||
sourceCommitId: initialPlan.sourceCommitId,
|
||||
componentInputHashes: initialHashes,
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", catalogOptionsFromPlan(initialPlan, {
|
||||
cloudWebEnvironmentInputHash: initialCloudWeb.environmentInputHash,
|
||||
cloudWebCodeInputHash: initialCloudWeb.codeInputHash,
|
||||
cloudWebBootCommit: initialPlan.sourceCommitId
|
||||
}), null, 2));
|
||||
})), null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.v02.json"]);
|
||||
await git(repo, ["commit", "-m", "seed v02 catalog with trace renderer"]);
|
||||
|
||||
@@ -425,6 +406,61 @@ test("v02 planner marks cloud-web code-only change as env reuse rollout", async
|
||||
assert.equal(plan.ciCdPlan.noImageBuildReason, "env-reuse-code-only-rollout");
|
||||
});
|
||||
|
||||
test("v02 planner enables env reuse for all retained runtime services", async () => {
|
||||
const repo = await createFixtureRepo({ serviceIds: V02_RUNTIME_SERVICE_IDS });
|
||||
const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
|
||||
const initialPlan = await createG14CiPlan({
|
||||
repoRoot: repo,
|
||||
lane: "v02",
|
||||
baseRef: "HEAD",
|
||||
targetRef: initialSha,
|
||||
artifactCatalogPath: "deploy/artifact-catalog.v02.json",
|
||||
services: V02_RUNTIME_SERVICE_IDS
|
||||
});
|
||||
const environmentInputHashes = Object.fromEntries(initialPlan.services.map((service) => [service.serviceId, service.environmentInputHash]));
|
||||
const codeInputHashes = Object.fromEntries(initialPlan.services.map((service) => [service.serviceId, service.codeInputHash]));
|
||||
const bootCommits = Object.fromEntries(initialPlan.services.map((service) => [service.serviceId, initialPlan.sourceCommitId]));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", {
|
||||
sourceCommitId: initialPlan.sourceCommitId,
|
||||
environmentInputHashes,
|
||||
codeInputHashes,
|
||||
bootCommits,
|
||||
serviceIds: V02_RUNTIME_SERVICE_IDS
|
||||
}), null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.v02.json"]);
|
||||
await git(repo, ["commit", "-m", "seed all v02 env reuse catalogs"]);
|
||||
|
||||
await writeFile(path.join(repo, "cmd/hwlab-cloud-api/main.ts"), "console.log('api v2');\n");
|
||||
await writeFile(path.join(repo, "web/hwlab-cloud-web/index.html"), "<html>v2</html>\n");
|
||||
await writeFile(path.join(repo, "cmd/hwlab-gateway/main.ts"), "console.log('gateway v2');\n");
|
||||
await writeFile(path.join(repo, "cmd/hwlab-edge-proxy/main.ts"), "console.log('edge v2');\n");
|
||||
await writeFile(path.join(repo, "skills/hwlab-agent-runtime/SKILL.md"), "agent runtime skill v2\n");
|
||||
await git(repo, ["add", "."]);
|
||||
await git(repo, ["commit", "-m", "change all v02 service code"]);
|
||||
|
||||
const plan = await createG14CiPlan({
|
||||
repoRoot: repo,
|
||||
lane: "v02",
|
||||
baseRef: "HEAD~1",
|
||||
targetRef: "HEAD",
|
||||
artifactCatalogPath: "deploy/artifact-catalog.v02.json",
|
||||
services: V02_RUNTIME_SERVICE_IDS
|
||||
});
|
||||
assert.deepEqual(plan.compatibility.v02EnvReuseServiceIds, V02_RUNTIME_SERVICE_IDS);
|
||||
assert.deepEqual(plan.affectedServices, sortedServiceIds(V02_RUNTIME_SERVICE_IDS));
|
||||
assert.deepEqual(plan.rolloutServices, sortedServiceIds(V02_RUNTIME_SERVICE_IDS));
|
||||
assert.deepEqual(plan.buildServices, []);
|
||||
assert.equal(plan.imageBuildRequired, false);
|
||||
for (const service of plan.services) {
|
||||
assert.equal(service.runtimeMode, "env-reuse-git-mirror-checkout", service.serviceId);
|
||||
assert.equal(service.envReuse, true, service.serviceId);
|
||||
assert.equal(service.envChanged, false, service.serviceId);
|
||||
assert.equal(service.codeChanged, true, service.serviceId);
|
||||
assert.equal(service.buildRequired, false, service.serviceId);
|
||||
assert.equal(service.bootSh, `deploy/runtime/boot/${service.serviceId}.sh`, service.serviceId);
|
||||
}
|
||||
});
|
||||
|
||||
test("v02 planner rejects removed HWLAB-owned agent worker service selections", async () => {
|
||||
const repo = await createFixtureRepo();
|
||||
await assert.rejects(
|
||||
@@ -451,15 +487,7 @@ test("v02 planner keeps hwlab-cli-only changes out of runtime service builds", a
|
||||
artifactCatalogPath: "deploy/artifact-catalog.v02.json",
|
||||
services: ["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-agent-skills"]
|
||||
});
|
||||
const initialCloudWeb = initialPlan.services.find((service) => service.serviceId === "hwlab-cloud-web");
|
||||
const initialHashes = Object.fromEntries(initialPlan.services.map((service) => [service.serviceId, service.componentInputHash]));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", {
|
||||
sourceCommitId: initialPlan.sourceCommitId,
|
||||
componentInputHashes: initialHashes,
|
||||
cloudWebEnvironmentInputHash: initialCloudWeb.environmentInputHash,
|
||||
cloudWebCodeInputHash: initialCloudWeb.codeInputHash,
|
||||
cloudWebBootCommit: initialPlan.sourceCommitId
|
||||
}), null, 2));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", catalogOptionsFromPlan(initialPlan)), null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.v02.json"]);
|
||||
await git(repo, ["commit", "-m", "seed v02 catalog"]);
|
||||
|
||||
@@ -488,7 +516,7 @@ test("v02 planner keeps hwlab-cli-only changes out of runtime service builds", a
|
||||
assert.equal(plan.services.find((service) => service.serviceId === "hwlab-cloud-api").affected, false);
|
||||
});
|
||||
|
||||
test("v02 planner treats hwpod runner CLI changes as runtime inputs", async () => {
|
||||
test("v02 planner scopes hwpod skill bundle changes to the skills service", async () => {
|
||||
const repo = await createFixtureRepo();
|
||||
const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
|
||||
const initialPlan = await createG14CiPlan({
|
||||
@@ -499,15 +527,7 @@ test("v02 planner treats hwpod runner CLI changes as runtime inputs", async () =
|
||||
artifactCatalogPath: "deploy/artifact-catalog.v02.json",
|
||||
services: ["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-agent-skills"]
|
||||
});
|
||||
const initialCloudWeb = initialPlan.services.find((service) => service.serviceId === "hwlab-cloud-web");
|
||||
const initialHashes = Object.fromEntries(initialPlan.services.map((service) => [service.serviceId, service.componentInputHash]));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", {
|
||||
sourceCommitId: initialPlan.sourceCommitId,
|
||||
componentInputHashes: initialHashes,
|
||||
cloudWebEnvironmentInputHash: initialCloudWeb.environmentInputHash,
|
||||
cloudWebCodeInputHash: initialCloudWeb.codeInputHash,
|
||||
cloudWebBootCommit: initialPlan.sourceCommitId
|
||||
}), null, 2));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", catalogOptionsFromPlan(initialPlan)), null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.v02.json"]);
|
||||
await git(repo, ["commit", "-m", "seed v02 catalog"]);
|
||||
|
||||
@@ -524,20 +544,28 @@ test("v02 planner treats hwpod runner CLI changes as runtime inputs", async () =
|
||||
artifactCatalogPath: "deploy/artifact-catalog.v02.json",
|
||||
services: ["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-agent-skills"]
|
||||
});
|
||||
assert.deepEqual(plan.affectedServices, ["hwlab-agent-skills", "hwlab-cloud-api", "hwlab-cloud-web"]);
|
||||
assert.deepEqual(plan.buildServices, ["hwlab-agent-skills", "hwlab-cloud-api"]);
|
||||
assert.equal(plan.services.find((service) => service.serviceId === "hwlab-cloud-web").codeChanged, true);
|
||||
assert.deepEqual(plan.services.find((service) => service.serviceId === "hwlab-cloud-api").reason, ["shared-runtime-changed"]);
|
||||
assert.deepEqual(plan.affectedServices, ["hwlab-agent-skills"]);
|
||||
assert.deepEqual(plan.buildServices, []);
|
||||
assert.equal(plan.services.find((service) => service.serviceId === "hwlab-cloud-api").affected, false);
|
||||
assert.equal(plan.services.find((service) => service.serviceId === "hwlab-cloud-web").affected, false);
|
||||
const skillsService = plan.services.find((service) => service.serviceId === "hwlab-agent-skills");
|
||||
assert.equal(skillsService.codeChanged, true);
|
||||
assert.equal(skillsService.envChanged, false);
|
||||
assert.deepEqual(skillsService.reason, ["code-input-changed", "component-path-changed"]);
|
||||
});
|
||||
|
||||
async function createFixtureRepo(options = {}) {
|
||||
const deployServices = options.deployServices !== false;
|
||||
const k3sServiceMappings = options.k3sServiceMappings === true;
|
||||
const serviceIds = options.serviceIds ?? ["hwlab-cloud-api", "hwlab-cloud-web"];
|
||||
const repo = await mkdtemp(path.join(os.tmpdir(), "hwlab-g14-ci-plan-"));
|
||||
await mkdir(path.join(repo, "cmd/hwlab-cloud-api"), { recursive: true });
|
||||
await mkdir(path.join(repo, "cmd/hwlab-deepseek-responses-bridge"), { recursive: true });
|
||||
await mkdir(path.join(repo, "cmd/hwlab-gateway"), { recursive: true });
|
||||
await mkdir(path.join(repo, "cmd/hwlab-edge-proxy"), { recursive: true });
|
||||
await mkdir(path.join(repo, "web/hwlab-cloud-web"), { recursive: true });
|
||||
await mkdir(path.join(repo, "internal/dev-entrypoint"), { recursive: true });
|
||||
await mkdir(path.join(repo, "internal/sim"), { recursive: true });
|
||||
await mkdir(path.join(repo, "tools/hwlab-cli"), { recursive: true });
|
||||
await mkdir(path.join(repo, "tools/src"), { recursive: true });
|
||||
await mkdir(path.join(repo, "skills/hwpod-cli"), { recursive: true });
|
||||
@@ -547,19 +575,22 @@ async function createFixtureRepo(options = {}) {
|
||||
await mkdir(path.join(repo, "deploy/runtime/boot"), { recursive: true });
|
||||
await mkdir(path.join(repo, "deploy/runtime/launcher"), { recursive: true });
|
||||
await mkdir(path.join(repo, "deploy"), { recursive: true });
|
||||
await writeFile(path.join(repo, "deploy/deploy.json"), JSON.stringify(createDeployFixture({ deployServices, k3sServiceMappings }), null, 2));
|
||||
await writeFile(path.join(repo, "deploy/deploy.json"), JSON.stringify(createDeployFixture({ deployServices, k3sServiceMappings, serviceIds }), null, 2));
|
||||
const catalogMode = options.catalog ?? "ready";
|
||||
await writeFile(path.join(repo, "package.json"), JSON.stringify({ type: "module" }, null, 2));
|
||||
await writeFile(path.join(repo, "package-lock.json"), JSON.stringify({ lockfileVersion: 3 }, null, 2));
|
||||
await writeFile(path.join(repo, "cmd/hwlab-cloud-api/main.ts"), "console.log('api');\n");
|
||||
await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.ts"), "console.log('bridge');\n");
|
||||
await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.test.ts"), "console.log('test');\n");
|
||||
await writeFile(path.join(repo, "cmd/hwlab-gateway/main.ts"), "console.log('gateway');\n");
|
||||
await writeFile(path.join(repo, "cmd/hwlab-edge-proxy/main.ts"), "console.log('edge');\n");
|
||||
await writeFile(path.join(repo, "web/hwlab-cloud-web/index.html"), "<html></html>\n");
|
||||
await writeFile(path.join(repo, "internal/dev-entrypoint/artifact-runtime.mjs"), "export const artifactRuntime = 1;\n");
|
||||
await writeFile(path.join(repo, "internal/dev-entrypoint/cloud-web-runtime.mjs"), "export const runtime = 1;\n");
|
||||
await writeFile(path.join(repo, "internal/dev-entrypoint/cloud-web-proxy.mjs"), "export const proxy = 1;\n");
|
||||
await writeFile(path.join(repo, "internal/dev-entrypoint/cloud-web-routes.mjs"), "export const routes = 1;\n");
|
||||
await writeFile(path.join(repo, "internal/dev-entrypoint/http.mjs"), "export const http = 1;\n");
|
||||
await writeFile(path.join(repo, "internal/sim/http.mjs"), "export const simHttp = 1;\n");
|
||||
await writeFile(path.join(repo, "tools/hwpod-cli.ts"), "console.log('hwpod cli');\n");
|
||||
await writeFile(path.join(repo, "tools/hwpod-ctl.ts"), "console.log('hwpod ctl');\n");
|
||||
await writeFile(path.join(repo, "tools/hwpod-compiler-cli.ts"), "console.log('hwpod compiler');\n");
|
||||
@@ -572,6 +603,10 @@ async function createFixtureRepo(options = {}) {
|
||||
await writeFile(path.join(repo, "skills/hwpod-ctl/SKILL.md"), "hwpod ctl skill\n");
|
||||
await writeFile(path.join(repo, ".hwlab/hwpod-spec.yaml"), "kind: Hwpod\nmetadata:\n name: d601-f103-v2\nspec:\n nodeBinding:\n nodeId: node-d601-f103-v2\n workspace:\n path: F:\\\\Work\\\\D601-HWLAB\n targetDevice:\n board: D601-F103-V2\n debugProbe:\n type: daplink\n ioProbe:\n uart:\n port: COM9\n");
|
||||
await writeFile(path.join(repo, "deploy/runtime/boot/hwlab-cloud-web.sh"), "#!/bin/sh\nbun run --cwd web/hwlab-cloud-web build\nexec bun .hwlab-cloud-web-runtime.mjs\n");
|
||||
await writeFile(path.join(repo, "deploy/runtime/boot/hwlab-cloud-api.sh"), "#!/bin/sh\nexec bun run cmd/hwlab-cloud-api/main.ts\n");
|
||||
await writeFile(path.join(repo, "deploy/runtime/boot/hwlab-gateway.sh"), "#!/bin/sh\nexec bun run cmd/hwlab-gateway/main.ts\n");
|
||||
await writeFile(path.join(repo, "deploy/runtime/boot/hwlab-edge-proxy.sh"), "#!/bin/sh\nexec bun run cmd/hwlab-edge-proxy/main.ts\n");
|
||||
await writeFile(path.join(repo, "deploy/runtime/boot/hwlab-agent-skills.sh"), "#!/bin/sh\nexec bun .hwlab-agent-skills-runtime.mjs\n");
|
||||
await writeFile(path.join(repo, "deploy/runtime/launcher/hwlab-env-reuse-launcher.ts"), "console.log('launcher');\n");
|
||||
await writeFile(path.join(repo, "skills/hwlab-agent-runtime/SKILL.md"), "agent runtime skill\n");
|
||||
await git(repo, ["init"]);
|
||||
@@ -581,13 +616,13 @@ async function createFixtureRepo(options = {}) {
|
||||
const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
|
||||
const initialPlan = await createG14CiPlan({
|
||||
repoRoot: repo,
|
||||
lane: "v02",
|
||||
baseRef: "HEAD",
|
||||
targetRef: "HEAD",
|
||||
artifactCatalogPath: "deploy/nonexistent-artifact-catalog.json",
|
||||
services: ["hwlab-cloud-api", "hwlab-cloud-web"]
|
||||
services: serviceIds
|
||||
});
|
||||
const componentInputHashes = Object.fromEntries(initialPlan.services.map((service) => [service.serviceId, service.componentInputHash]));
|
||||
const catalog = createCatalogFixture(catalogMode, { sourceCommitId: initialSha, componentInputHashes });
|
||||
const catalog = createCatalogFixture(catalogMode, catalogOptionsFromPlan(initialPlan, { sourceCommitId: initialSha, serviceIds }));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.dev.json"), JSON.stringify(catalog, null, 2));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(catalog, null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.dev.json", "deploy/artifact-catalog.v02.json"]);
|
||||
@@ -614,30 +649,28 @@ async function refreshFixtureCatalogForHead(repo) {
|
||||
await git(repo, ["commit", "-m", "refresh fixture artifact catalogs"]);
|
||||
}
|
||||
|
||||
function createDeployFixture({ deployServices, k3sServiceMappings }) {
|
||||
function createDeployFixture({ deployServices, k3sServiceMappings, serviceIds = ["hwlab-cloud-api", "hwlab-cloud-web"] }) {
|
||||
const deploy = {
|
||||
lanes: {
|
||||
v02: {
|
||||
sourceRepo: "git@github.com:pikasTech/HWLAB.git",
|
||||
envReuseServices: ["hwlab-cloud-web"],
|
||||
envReuseServices: [...V02_RUNTIME_SERVICE_IDS],
|
||||
bootScripts: {
|
||||
"hwlab-cloud-web": "deploy/runtime/boot/hwlab-cloud-web.sh"
|
||||
"hwlab-cloud-api": "deploy/runtime/boot/hwlab-cloud-api.sh",
|
||||
"hwlab-cloud-web": "deploy/runtime/boot/hwlab-cloud-web.sh",
|
||||
"hwlab-gateway": "deploy/runtime/boot/hwlab-gateway.sh",
|
||||
"hwlab-edge-proxy": "deploy/runtime/boot/hwlab-edge-proxy.sh",
|
||||
"hwlab-agent-skills": "deploy/runtime/boot/hwlab-agent-skills.sh"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (deployServices) {
|
||||
deploy.services = [
|
||||
{ serviceId: "hwlab-cloud-api" },
|
||||
{ serviceId: "hwlab-cloud-web" }
|
||||
];
|
||||
deploy.services = serviceIds.map((serviceId) => ({ serviceId }));
|
||||
}
|
||||
if (k3sServiceMappings) {
|
||||
deploy.k3s = {
|
||||
serviceMappings: [
|
||||
{ serviceId: "hwlab-cloud-api", name: "hwlab-cloud-api" },
|
||||
{ serviceId: "hwlab-cloud-web", name: "hwlab-cloud-web" }
|
||||
]
|
||||
serviceMappings: serviceIds.map((serviceId) => ({ serviceId, name: serviceId }))
|
||||
};
|
||||
}
|
||||
return deploy;
|
||||
@@ -645,11 +678,13 @@ function createDeployFixture({ deployServices, k3sServiceMappings }) {
|
||||
|
||||
function createCatalogFixture(mode, options = {}) {
|
||||
const digest = mode === "ready" ? `sha256:${"1".repeat(64)}` : "not_published";
|
||||
const serviceIds = [
|
||||
const serviceIds = options.serviceIds ?? uniquePreserveOrder([
|
||||
"hwlab-cloud-api",
|
||||
"hwlab-cloud-web",
|
||||
...(options.componentInputHashes?.["hwlab-agent-skills"] ? ["hwlab-agent-skills"] : [])
|
||||
];
|
||||
...Object.keys(options.componentInputHashes ?? {}),
|
||||
...Object.keys(options.environmentInputHashes ?? {}),
|
||||
...Object.keys(options.codeInputHashes ?? {})
|
||||
]);
|
||||
return {
|
||||
services: serviceIds.map((serviceId) => ({
|
||||
serviceId,
|
||||
@@ -661,21 +696,49 @@ function createCatalogFixture(mode, options = {}) {
|
||||
...((options.componentInputHashes?.[serviceId] || (serviceId === "hwlab-cloud-api" && options.cloudApiComponentInputHash)) ? {
|
||||
componentInputHash: options.cloudApiComponentInputHash ?? options.componentInputHashes?.[serviceId]
|
||||
} : {}),
|
||||
...(serviceId === "hwlab-cloud-web" ? {
|
||||
...(V02_RUNTIME_SERVICE_IDS.includes(serviceId) ? {
|
||||
runtimeMode: "env-reuse-git-mirror-checkout",
|
||||
envReuse: true,
|
||||
environmentImage: `127.0.0.1:5000/hwlab/${serviceId}-env:env-abc1234`,
|
||||
environmentDigest: digest,
|
||||
environmentInputHash: options.cloudWebEnvironmentInputHash ?? "e".repeat(64),
|
||||
codeInputHash: options.cloudWebCodeInputHash ?? "c".repeat(64),
|
||||
environmentInputHash: options.environmentInputHashes?.[serviceId] ?? (serviceId === "hwlab-cloud-web" ? options.cloudWebEnvironmentInputHash : null) ?? "e".repeat(64),
|
||||
codeInputHash: options.codeInputHashes?.[serviceId] ?? (serviceId === "hwlab-cloud-web" ? options.cloudWebCodeInputHash : null) ?? "c".repeat(64),
|
||||
bootRepo: "git@github.com:pikasTech/HWLAB.git",
|
||||
bootCommit: options.cloudWebBootCommit ?? "abc1234",
|
||||
bootCommit: options.bootCommits?.[serviceId] ?? (serviceId === "hwlab-cloud-web" ? options.cloudWebBootCommit : null) ?? "abc1234",
|
||||
bootSh: `deploy/runtime/boot/${serviceId}.sh`
|
||||
} : {})
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
function catalogOptionsFromPlan(plan, extra = {}) {
|
||||
const serviceIds = plan.services.map((service) => service.serviceId);
|
||||
return {
|
||||
sourceCommitId: plan.sourceCommitId,
|
||||
serviceIds,
|
||||
componentInputHashes: Object.fromEntries(plan.services.map((service) => [service.serviceId, service.componentInputHash])),
|
||||
environmentInputHashes: Object.fromEntries(plan.services.map((service) => [service.serviceId, service.environmentInputHash]).filter(([, value]) => value)),
|
||||
codeInputHashes: Object.fromEntries(plan.services.map((service) => [service.serviceId, service.codeInputHash]).filter(([, value]) => value)),
|
||||
bootCommits: Object.fromEntries(plan.services.map((service) => [service.serviceId, plan.sourceCommitId])),
|
||||
...extra
|
||||
};
|
||||
}
|
||||
|
||||
function sortedServiceIds(values) {
|
||||
return [...values].sort();
|
||||
}
|
||||
|
||||
function uniquePreserveOrder(values) {
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
for (const value of values) {
|
||||
if (!value || seen.has(value)) continue;
|
||||
seen.add(value);
|
||||
result.push(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function git(cwd, args) {
|
||||
return execFileAsync("git", ["-c", "user.name=HWLAB Test", "-c", "user.email=hwlab-test@example.invalid", ...args], { cwd });
|
||||
}
|
||||
|
||||
@@ -20,10 +20,7 @@ export const DEFAULT_BUILD_SYSTEM_PATHS = Object.freeze([
|
||||
|
||||
export const DEFAULT_ENV_REUSE_LAUNCHER_PATHS = Object.freeze([
|
||||
"deploy/runtime/launcher/",
|
||||
"scripts/artifact-publish.mjs",
|
||||
"scripts/g14-artifact-publish.mjs",
|
||||
"scripts/src/dev-artifact-services.mjs",
|
||||
"scripts/src/g14-ci-plan-lib.mjs"
|
||||
"scripts/artifact-publish.mjs"
|
||||
]);
|
||||
|
||||
export const DEFAULT_RUNTIME_DEP_PATHS = Object.freeze([
|
||||
@@ -43,19 +40,8 @@ export const DEFAULT_RUNTIME_PACKAGE_FIELDS = Object.freeze([
|
||||
]);
|
||||
|
||||
export const DEFAULT_SHARED_RUNTIME_PATHS = Object.freeze([
|
||||
".hwlab/",
|
||||
"internal/protocol/",
|
||||
"internal/build-metadata.mjs",
|
||||
"tools/hwpod-cli.ts",
|
||||
"tools/hwpod-ctl.ts",
|
||||
"tools/hwpod-compiler-cli.ts",
|
||||
"tools/hwpod-node.ts",
|
||||
"tools/src/hwpod-harness-lib.ts",
|
||||
"tools/src/hwpod-node-lib.ts",
|
||||
"tools/src/hwpod-node-ops-contract.ts",
|
||||
"tools/src/runtime-endpoint-resolver.ts",
|
||||
"skills/hwpod-cli/",
|
||||
"skills/hwpod-ctl/"
|
||||
"internal/build-metadata.mjs"
|
||||
]);
|
||||
|
||||
export const DEFAULT_DOCS_ONLY_PATHS = Object.freeze([
|
||||
@@ -89,7 +75,7 @@ const serviceSpecificPaths = Object.freeze({
|
||||
"internal/dev-entrypoint/cloud-web-routes.mjs",
|
||||
"tools/src/hwlab-cli/trace-renderer.ts"
|
||||
],
|
||||
"hwlab-gateway": ["cmd/hwlab-gateway/"],
|
||||
"hwlab-gateway": ["cmd/hwlab-gateway/", "internal/sim/"],
|
||||
"hwlab-edge-proxy": ["cmd/hwlab-edge-proxy/", "internal/dev-entrypoint/http.mjs"],
|
||||
"hwlab-agent-skills": ["skills/"]
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user