fix: enable v02 env reuse for runtime services

This commit is contained in:
Codex Agent
2026-06-07 21:26:07 +08:00
parent 7a7dac50a8
commit 1b76fb3760
8 changed files with 434 additions and 134 deletions
+170 -107
View File
@@ -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 });
}
+3 -17
View File
@@ -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/"]
});