fix: reuse agent worker env image in v02 ci

This commit is contained in:
Codex
2026-06-01 22:28:52 +08:00
parent d4c05d7963
commit 0fb8004896
8 changed files with 162 additions and 20 deletions
+107 -4
View File
@@ -354,6 +354,98 @@ 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 marks agent-worker code-only change as env reuse rollout", async () => {
const repo = await createFixtureRepo({ includeDevicePod: true, includeAgentWorker: true });
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: ["hwlab-agent-worker"]
});
const initialWorker = initialPlan.services.find((service) => service.serviceId === "hwlab-agent-worker");
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", {
agentWorkerEnvironmentInputHash: initialWorker.environmentInputHash,
agentWorkerCodeInputHash: initialWorker.codeInputHash,
agentWorkerBootCommit: initialPlan.sourceCommitId
}), null, 2));
await git(repo, ["add", "deploy/artifact-catalog.v02.json"]);
await git(repo, ["commit", "-m", "seed v02 worker catalog"]);
await writeFile(path.join(repo, "cmd/hwlab-agent-worker/main.ts"), "console.log('worker v2');\n");
await git(repo, ["add", "."]);
await git(repo, ["commit", "-m", "change agent worker code"]);
const plan = await createG14CiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD~1",
targetRef: "HEAD",
artifactCatalogPath: "deploy/artifact-catalog.v02.json",
services: ["hwlab-agent-worker"]
});
const worker = plan.services.find((service) => service.serviceId === "hwlab-agent-worker");
assert.equal(worker.runtimeMode, "env-reuse-git-mirror-checkout");
assert.equal(worker.envChanged, false);
assert.equal(worker.codeChanged, true);
assert.equal(worker.buildRequired, false);
assert.equal(worker.bootSh, "deploy/runtime/boot/hwlab-agent-worker.sh");
assert.deepEqual(plan.affectedServices, ["hwlab-agent-worker"]);
assert.deepEqual(plan.rolloutServices, ["hwlab-agent-worker"]);
assert.deepEqual(plan.buildServices, []);
assert.equal(plan.imageBuildRequired, false);
assert.equal(plan.ciCdPlan.noImageBuildReason, "env-reuse-code-only-rollout");
});
test("v02 planner rebuilds agent-worker env image only for runtime dependency changes", async () => {
const repo = await createFixtureRepo({ includeDevicePod: true, includeAgentWorker: true });
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: ["hwlab-agent-worker"]
});
const initialWorker = initialPlan.services.find((service) => service.serviceId === "hwlab-agent-worker");
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", {
agentWorkerEnvironmentInputHash: initialWorker.environmentInputHash,
agentWorkerCodeInputHash: initialWorker.codeInputHash,
agentWorkerBootCommit: initialPlan.sourceCommitId
}), null, 2));
await git(repo, ["add", "deploy/artifact-catalog.v02.json"]);
await git(repo, ["commit", "-m", "seed v02 worker catalog"]);
await writeFile(path.join(repo, "package.json"), JSON.stringify({
type: "module",
dependencies: {
"@openai/codex": "^0.129.0"
}
}, null, 2));
await git(repo, ["add", "package.json"]);
await git(repo, ["commit", "-m", "change worker env dependency"]);
const plan = await createG14CiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD~1",
targetRef: "HEAD",
artifactCatalogPath: "deploy/artifact-catalog.v02.json",
services: ["hwlab-agent-worker"]
});
const worker = plan.services.find((service) => service.serviceId === "hwlab-agent-worker");
assert.equal(worker.runtimeMode, "env-reuse-git-mirror-checkout");
assert.equal(worker.envChanged, true);
assert.equal(worker.buildRequired, true);
assert.deepEqual(plan.affectedServices, ["hwlab-agent-worker"]);
assert.deepEqual(plan.buildServices, ["hwlab-agent-worker"]);
assert.ok(worker.reason.includes("environment-input-changed"));
assert.ok(worker.reason.includes("runtime-deps-changed"));
});
test("v02 planner keeps hwlab-cli-only changes out of runtime service builds", async () => {
const repo = await createFixtureRepo({ includeDevicePod: true });
const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
@@ -458,9 +550,11 @@ async function createFixtureRepo(options = {}) {
const deployServices = options.deployServices !== false;
const k3sServiceMappings = options.k3sServiceMappings === true;
const includeDevicePod = options.includeDevicePod === true;
const includeAgentWorker = options.includeAgentWorker === true;
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-agent-worker"), { 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/device-pod"), { recursive: true });
@@ -479,6 +573,7 @@ async function createFixtureRepo(options = {}) {
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-agent-worker/main.ts"), "console.log('worker');\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");
@@ -493,6 +588,7 @@ async function createFixtureRepo(options = {}) {
await writeFile(path.join(repo, "skills/device-pod-cli/scripts/device-pod-cli.mjs"), "console.log('skill wrapper');\n");
await writeFile(path.join(repo, "skills/device-pod-cli/assets/device-host-cli.mjs"), "console.log('host cli');\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-agent-worker.sh"), "#!/bin/sh\nexec bun cmd/hwlab-agent-worker/main.ts \"$@\"\n");
await writeFile(path.join(repo, "deploy/runtime/boot/hwlab-device-pod.sh"), "#!/bin/sh\nexec bun cmd/hwlab-device-pod/main.ts\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");
@@ -506,7 +602,7 @@ async function createFixtureRepo(options = {}) {
baseRef: "HEAD",
targetRef: "HEAD",
artifactCatalogPath: "deploy/nonexistent-artifact-catalog.json",
services: ["hwlab-cloud-api", "hwlab-cloud-web", ...(includeDevicePod ? ["hwlab-device-pod"] : [])]
services: ["hwlab-cloud-api", "hwlab-cloud-web", ...(includeAgentWorker ? ["hwlab-agent-worker"] : []), ...(includeDevicePod ? ["hwlab-device-pod"] : [])]
});
const componentInputHashes = Object.fromEntries(initialPlan.services.map((service) => [service.serviceId, service.componentInputHash]));
const catalog = createCatalogFixture(catalogMode, { sourceCommitId: initialSha, componentInputHashes });
@@ -541,9 +637,10 @@ function createDeployFixture({ deployServices, k3sServiceMappings, includeDevice
lanes: {
v02: {
sourceRepo: "git@github.com:pikasTech/HWLAB.git",
envReuseServices: ["hwlab-cloud-web", "hwlab-device-pod"],
envReuseServices: ["hwlab-cloud-web", "hwlab-agent-worker", "hwlab-device-pod"],
bootScripts: {
"hwlab-cloud-web": "deploy/runtime/boot/hwlab-cloud-web.sh",
"hwlab-agent-worker": "deploy/runtime/boot/hwlab-agent-worker.sh",
"hwlab-device-pod": "deploy/runtime/boot/hwlab-device-pod.sh"
}
}
@@ -571,7 +668,7 @@ function createDeployFixture({ deployServices, k3sServiceMappings, includeDevice
function createCatalogFixture(mode, options = {}) {
const digest = mode === "ready" ? `sha256:${"1".repeat(64)}` : "not_published";
return {
services: ["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-device-pod"].map((serviceId) => ({
services: ["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-agent-worker", "hwlab-device-pod"].map((serviceId) => ({
serviceId,
commitId: "abc1234",
sourceCommitId: options.sourceCommitId ?? "abc1234",
@@ -581,20 +678,26 @@ function createCatalogFixture(mode, options = {}) {
...((options.componentInputHashes?.[serviceId] || (serviceId === "hwlab-cloud-api" && options.cloudApiComponentInputHash)) ? {
componentInputHash: options.cloudApiComponentInputHash ?? options.componentInputHashes?.[serviceId]
} : {}),
...((serviceId === "hwlab-cloud-web" || serviceId === "hwlab-device-pod") ? {
...((serviceId === "hwlab-cloud-web" || serviceId === "hwlab-agent-worker" || serviceId === "hwlab-device-pod") ? {
runtimeMode: "env-reuse-git-mirror-checkout",
envReuse: true,
environmentImage: `127.0.0.1:5000/hwlab/${serviceId}-env:env-abc1234`,
environmentDigest: digest,
environmentInputHash: serviceId === "hwlab-cloud-web"
? options.cloudWebEnvironmentInputHash ?? "e".repeat(64)
: serviceId === "hwlab-agent-worker"
? options.agentWorkerEnvironmentInputHash ?? "e".repeat(64)
: options.devicePodEnvironmentInputHash ?? "e".repeat(64),
codeInputHash: serviceId === "hwlab-cloud-web"
? options.cloudWebCodeInputHash ?? "c".repeat(64)
: serviceId === "hwlab-agent-worker"
? options.agentWorkerCodeInputHash ?? "c".repeat(64)
: options.devicePodCodeInputHash ?? "c".repeat(64),
bootRepo: "git@github.com:pikasTech/HWLAB.git",
bootCommit: serviceId === "hwlab-cloud-web"
? options.cloudWebBootCommit ?? "abc1234"
: serviceId === "hwlab-agent-worker"
? options.agentWorkerBootCommit ?? "abc1234"
: options.devicePodBootCommit ?? "abc1234",
bootSh: `deploy/runtime/boot/${serviceId}.sh`
} : {})
+12 -10
View File
@@ -416,7 +416,8 @@ function catalogServiceById(catalog, serviceId) {
return (catalog?.services ?? []).find((service) => service?.serviceId === serviceId) ?? null;
}
function v02EnvReuseEnabled(catalog, serviceId) {
function v02EnvReuseEnabled(catalog, serviceId, envReuseServiceIds = null) {
if (envReuseServiceIds?.has(serviceId)) return true;
const service = catalogServiceById(catalog, serviceId);
return service?.runtimeMode === v02EnvReuseRuntimeMode || service?.envReuse === true;
}
@@ -458,17 +459,17 @@ function bootMetadataForService({ args, catalog, deployService, serviceId, sourc
};
}
function runtimeArtifactForService({ catalog, serviceId, source, registryPrefix, useDeployImages = false, digestPin = false }) {
function runtimeArtifactForService({ catalog, deploy, serviceId, source, registryPrefix, useDeployImages = false, digestPin = false, envReuseServiceIds = null }) {
const catalogService = catalogServiceById(catalog, serviceId);
if (catalogService?.runtimeMode === v02EnvReuseRuntimeMode || catalogService?.envReuse === true) {
const environmentImage = catalogService.environmentImage ?? imageFor(registryPrefix, `${serviceId}-env`, `env-${String(catalogService.environmentInputHash ?? source.full).slice(0, 12)}`);
const runtimeImage = digestPin && useDeployImages ? digestPinnedImage(environmentImage, catalogService.environmentDigest ?? catalogService.digest) : environmentImage;
if (v02EnvReuseEnabled(catalog, serviceId, envReuseServiceIds)) {
const environmentImage = catalogService?.environmentImage ?? imageFor(registryPrefix, `${serviceId}-env`, `env-${String(catalogService?.environmentInputHash ?? source.full).slice(0, 12)}`);
const runtimeImage = digestPin && useDeployImages ? digestPinnedImage(environmentImage, catalogService?.environmentDigest ?? catalogService?.digest) : environmentImage;
return {
image: runtimeImage,
imageTag: imageTagFromReference(environmentImage) ?? source.imageTag ?? source.short,
commit: catalogService.bootCommit ?? source.full,
commit: catalogService?.bootCommit ?? source.full,
runtimeMode: v02EnvReuseRuntimeMode,
environmentDigest: catalogService.environmentDigest ?? catalogService.digest ?? null
environmentDigest: catalogService?.environmentDigest ?? catalogService?.digest ?? null
};
}
const fallbackImageTag = source.imageTag ?? source.short;
@@ -753,6 +754,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch =
const profileLabel = runtimeLabelForProfile(profile);
const gitopsTarget = gitopsTargetForProfile(profile);
const digestPin = profile === "v02";
const envReuseServiceIds = envReuseServiceIdsForLane(deploy, profile);
const stableRuntimeLabels = {
"app.kubernetes.io/part-of": "hwlab",
"hwlab.pikastech.local/environment": profileLabel,
@@ -784,7 +786,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch =
const podTemplate = item?.spec?.template;
if (!podTemplate?.spec?.containers) continue;
const templateServiceId = serviceIdForWorkload(item, podTemplate.spec.containers[0]);
const templateArtifact = runtimeArtifactForService({ catalog, serviceId: templateServiceId, source, registryPrefix, useDeployImages, digestPin });
const templateArtifact = runtimeArtifactForService({ catalog, deploy, serviceId: templateServiceId, source, registryPrefix, useDeployImages, digestPin, envReuseServiceIds });
const templateSourceCommit = profile === "v02" ? (templateArtifact.commit ?? source.full) : source.full;
const templateArtifactCommit = templateArtifact.commit;
label(podTemplate.metadata ??= {}, {
@@ -808,8 +810,8 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch =
const serviceId = serviceIdForWorkload(item, container);
const deployService = deployServices.get(serviceId);
if (!deployService) continue;
const artifact = runtimeArtifactForService({ catalog, serviceId, source, registryPrefix, useDeployImages, digestPin });
const envReuse = profile === "v02" && v02EnvReuseEnabled(catalog, serviceId);
const artifact = runtimeArtifactForService({ catalog, deploy, serviceId, source, registryPrefix, useDeployImages, digestPin, envReuseServiceIds });
const envReuse = profile === "v02" && v02EnvReuseEnabled(catalog, serviceId, envReuseServiceIds);
const bootMetadata = envReuse ? bootMetadataForService({ args: { sourceRepo: deploy?.lanes?.v02?.sourceRepo || defaultSourceRepo, registryPrefix }, catalog, deployService, serviceId, source }) : null;
const image = artifact.image;
const runtimeCommit = artifact.commit;
+8 -2
View File
@@ -229,9 +229,15 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
assert.ok(cloudApi, "expected v02 hwlab-cloud-api workload");
const cloudApiEnv = cloudApi.spec.template.spec.containers[0].env;
assert.ok(cloudApiEnv.some((entry) => entry.name === "HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT" && entry.value === sourceRevision));
const agentWorker = workloadsJson.items.find((item) => item.metadata?.name === "hwlab-agent-worker");
const agentWorker = workloadsJson.items.find((item) => item.metadata?.labels?.["hwlab.pikastech.local/service-id"] === "hwlab-agent-worker");
assert.ok(agentWorker, "expected v02 hwlab-agent-worker suspended Job template");
const agentWorkerEnv = agentWorker?.spec?.template?.spec?.containers?.[0]?.env ?? [];
assert.equal(agentWorkerEnv.some((entry) => entry.name === "HWLAB_BOOT_COMMIT"), false, "agent-worker must not be switched to env reuse by device-pod work");
assert.ok(agentWorkerEnv.some((entry) => entry.name === "HWLAB_RUNTIME_MODE" && entry.value === "env-reuse-git-mirror-checkout"));
assert.ok(agentWorkerEnv.some((entry) => entry.name === "HWLAB_BOOT_REPO" && entry.value === "git@github.com:pikasTech/HWLAB.git"));
assert.ok(agentWorkerEnv.some((entry) => entry.name === "HWLAB_BOOT_COMMIT" && entry.value === sourceRevision));
assert.ok(agentWorkerEnv.some((entry) => entry.name === "HWLAB_BOOT_SH" && entry.value === "deploy/runtime/boot/hwlab-agent-worker.sh"));
assert.equal(agentWorker?.spec?.template?.metadata?.annotations?.["hwlab.pikastech.local/runtime-mode"], "env-reuse-git-mirror-checkout");
assert.equal(agentWorker?.spec?.template?.metadata?.annotations?.["hwlab.pikastech.local/boot-commit"], sourceRevision);
const workloadTemplates = workloadsJson.items.filter((item) => item.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/source-commit"]);
assert.ok(workloadTemplates.length > 0, "expected rendered pod-template source labels");
assert.equal(devicePod.spec.template.metadata.labels["hwlab.pikastech.local/source-commit"], sourceRevision);
-2
View File
@@ -83,8 +83,6 @@ const serviceSpecificPaths = Object.freeze({
"hwlab-agent-worker": [
"cmd/hwlab-agent-worker/",
"internal/agent/",
"internal/db/",
"internal/audit/",
"skills/hwlab-agent-runtime/"
],
"hwlab-device-pod": ["cmd/hwlab-device-pod/", "internal/device-pod/"],