fix: reuse agent worker env image in v02 ci
This commit is contained in:
@@ -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`
|
||||
} : {})
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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/"],
|
||||
|
||||
Reference in New Issue
Block a user