fix: add cloud web env reuse runtime

This commit is contained in:
Codex
2026-05-31 14:37:28 +08:00
parent fd30feeef4
commit 89b83465e4
7 changed files with 219 additions and 23 deletions
+23 -7
View File
@@ -616,7 +616,7 @@ function artifactEnvironment(args) {
return args.lane === "v02" ? "v02" : ENVIRONMENT_DEV;
}
function artifactCatalogSkeleton({ args, commitId }) {
function artifactCatalogSkeleton({ args, deployManifest, commitId }) {
const tag = imageTagForCommit(args, commitId);
return {
catalogVersion: "v1",
@@ -635,11 +635,11 @@ function artifactCatalogSkeleton({ args, commitId }) {
},
allowedProfiles: [artifactEnvironment(args)],
forbiddenProfiles: args.lane === "v02" ? ["dev", "prod"] : ["prod"],
services: serviceIdsForLane(args.lane).map((serviceId) => artifactCatalogSkeletonService({ args, serviceId, tag, commitId }))
services: serviceIdsForLane(args.lane).map((serviceId) => artifactCatalogSkeletonService({ args, deployManifest, serviceId, tag, commitId }))
};
}
function artifactCatalogSkeletonService({ args, serviceId, tag, commitId }) {
function artifactCatalogSkeletonService({ args, deployManifest, serviceId, tag, commitId }) {
const base = {
serviceId,
commitId: tag,
@@ -657,7 +657,9 @@ function artifactCatalogSkeletonService({ args, serviceId, tag, commitId }) {
artifactScope: "required",
notPublishedReason: "publish_not_run"
};
if (args.lane !== "v02" || serviceId !== "hwlab-device-pod") return base;
const envReuseServiceIds = envReuseServiceIdsForLane(deployManifest, args.lane);
if (!envReuseServiceIds.has(serviceId)) return base;
const bootSh = bootShForService(deployManifest, serviceId);
const environmentImage = envReuseImageRef(args.registryPrefix, serviceId, commitId);
return {
...base,
@@ -671,11 +673,25 @@ function artifactCatalogSkeletonService({ args, serviceId, tag, commitId }) {
codeInputHash: null,
bootRepo: "git@github.com:pikasTech/HWLAB.git",
bootCommit: commitId,
bootSh: `deploy/runtime/boot/${serviceId}.sh`,
bootEnvEvidence: bootEnvEvidence({ service: { serviceId, bootCommit: commitId, bootSh: `deploy/runtime/boot/${serviceId}.sh` }, commitId, environmentImage, environmentDigest: "not_published" })
bootSh,
bootEnvEvidence: bootEnvEvidence({ service: { serviceId, bootCommit: commitId, bootSh }, commitId, environmentImage, environmentDigest: "not_published" })
};
}
function envReuseServiceIdsForLane(deployManifest, lane) {
if (lane !== "v02") return new Set();
const configured = deployManifest?.lanes?.v02?.envReuseServices;
const allowed = new Set(serviceIdsForLane(lane));
return new Set((Array.isArray(configured) ? configured : []).filter((serviceId) => allowed.has(serviceId)));
}
function bootShForService(deployManifest, serviceId) {
const value = deployManifest?.lanes?.v02?.bootScripts?.[serviceId] || `deploy/runtime/boot/${serviceId}.sh`;
const text = String(value).replaceAll("\\", "/").replace(/^\.\//u, "");
assert.ok(text && !text.startsWith("/") && !text.split("/").includes(".."), `${serviceId} boot script must be repo-relative`);
return text;
}
function normalizeCatalogForLane(catalog, lane) {
if (!catalog || lane !== "v02") return catalog;
const allowed = new Set(serviceIdsForLane(lane));
@@ -2045,7 +2061,7 @@ async function main() {
]);
let catalog = await readJsonIfPresent(args.catalogPath, null);
const catalogWasMissing = !catalog;
if (!catalog && args.lane === "v02") catalog = artifactCatalogSkeleton({ args, commitId });
if (!catalog && args.lane === "v02") catalog = artifactCatalogSkeleton({ args, deployManifest, commitId });
catalog = normalizeCatalogForLane(catalog, args.lane);
assert.ok(catalog, `${args.catalogPath} is required for ${args.lane} artifact publish`);
const shortCommit = imageTagForCommit(args, commitId);
+67 -8
View File
@@ -209,6 +209,51 @@ test("v02 planner marks device-pod code-only change as env reuse rollout", async
assert.equal(plan.ciCdPlan.noImageBuildReason, "env-reuse-code-only-rollout");
});
test("v02 planner marks cloud-web code-only change as env reuse rollout", async () => {
const repo = await createFixtureRepo({ includeDevicePod: 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-cloud-web"]
});
const initialCloudWeb = initialPlan.services.find((service) => service.serviceId === "hwlab-cloud-web");
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", {
cloudWebEnvironmentInputHash: initialCloudWeb.environmentInputHash,
cloudWebCodeInputHash: initialCloudWeb.codeInputHash,
cloudWebBootCommit: initialPlan.sourceCommitId
}), null, 2));
await git(repo, ["add", "deploy/artifact-catalog.v02.json"]);
await git(repo, ["commit", "-m", "seed v02 cloud web catalog"]);
await writeFile(path.join(repo, "web/hwlab-cloud-web/index.html"), "<html>v2</html>\n");
await git(repo, ["add", "."]);
await git(repo, ["commit", "-m", "change cloud web code"]);
const plan = await createG14CiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD~1",
targetRef: "HEAD",
artifactCatalogPath: "deploy/artifact-catalog.v02.json",
services: ["hwlab-cloud-web"]
});
const cloudWeb = plan.services.find((service) => service.serviceId === "hwlab-cloud-web");
assert.equal(cloudWeb.runtimeMode, "env-reuse-git-mirror-checkout");
assert.equal(cloudWeb.envChanged, false);
assert.equal(cloudWeb.codeChanged, true);
assert.equal(cloudWeb.buildRequired, false);
assert.equal(cloudWeb.bootSh, "deploy/runtime/boot/hwlab-cloud-web.sh");
assert.deepEqual(plan.affectedServices, ["hwlab-cloud-web"]);
assert.deepEqual(plan.rolloutServices, ["hwlab-cloud-web"]);
assert.deepEqual(plan.buildServices, []);
assert.equal(plan.imageBuildRequired, false);
assert.equal(plan.ciCdPlan.noImageBuildReason, "env-reuse-code-only-rollout");
});
test("v02 planner keeps CLI and host skill changes out of unrelated service builds", async () => {
const repo = await createFixtureRepo({ includeDevicePod: true });
const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
@@ -220,8 +265,12 @@ test("v02 planner keeps CLI and host skill changes out of unrelated service buil
artifactCatalogPath: "deploy/artifact-catalog.v02.json",
services: ["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-device-pod", "hwlab-agent-skills"]
});
const initialCloudWeb = initialPlan.services.find((service) => service.serviceId === "hwlab-cloud-web");
const initialDevicePod = initialPlan.services.find((service) => service.serviceId === "hwlab-device-pod");
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", {
cloudWebEnvironmentInputHash: initialCloudWeb.environmentInputHash,
cloudWebCodeInputHash: initialCloudWeb.codeInputHash,
cloudWebBootCommit: initialPlan.sourceCommitId,
devicePodEnvironmentInputHash: initialDevicePod.environmentInputHash,
devicePodCodeInputHash: initialDevicePod.codeInputHash,
devicePodBootCommit: initialPlan.sourceCommitId
@@ -292,6 +341,7 @@ async function createFixtureRepo(options = {}) {
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/device-pod/executor.ts"), "export const version = 1;\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-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");
@@ -306,8 +356,11 @@ function createDeployFixture({ deployServices, k3sServiceMappings, includeDevice
lanes: {
v02: {
sourceRepo: "git@github.com:pikasTech/HWLAB.git",
envReuseServices: ["hwlab-device-pod"],
bootScripts: { "hwlab-device-pod": "deploy/runtime/boot/hwlab-device-pod.sh" }
envReuseServices: ["hwlab-cloud-web", "hwlab-device-pod"],
bootScripts: {
"hwlab-cloud-web": "deploy/runtime/boot/hwlab-cloud-web.sh",
"hwlab-device-pod": "deploy/runtime/boot/hwlab-device-pod.sh"
}
}
}
};
@@ -339,16 +392,22 @@ function createCatalogFixture(mode, options = {}) {
image: `127.0.0.1:5000/hwlab/${serviceId}:abc1234`,
imageTag: "abc1234",
digest,
...(serviceId === "hwlab-device-pod" ? {
...((serviceId === "hwlab-cloud-web" || serviceId === "hwlab-device-pod") ? {
runtimeMode: "env-reuse-git-mirror-checkout",
envReuse: true,
environmentImage: "127.0.0.1:5000/hwlab/hwlab-device-pod-env:env-abc1234",
environmentImage: `127.0.0.1:5000/hwlab/${serviceId}-env:env-abc1234`,
environmentDigest: digest,
environmentInputHash: options.devicePodEnvironmentInputHash ?? "e".repeat(64),
codeInputHash: options.devicePodCodeInputHash ?? "c".repeat(64),
environmentInputHash: serviceId === "hwlab-cloud-web"
? options.cloudWebEnvironmentInputHash ?? "e".repeat(64)
: options.devicePodEnvironmentInputHash ?? "e".repeat(64),
codeInputHash: serviceId === "hwlab-cloud-web"
? options.cloudWebCodeInputHash ?? "c".repeat(64)
: options.devicePodCodeInputHash ?? "c".repeat(64),
bootRepo: "git@github.com:pikasTech/HWLAB.git",
bootCommit: options.devicePodBootCommit ?? "abc1234",
bootSh: "deploy/runtime/boot/hwlab-device-pod.sh"
bootCommit: serviceId === "hwlab-cloud-web"
? options.cloudWebBootCommit ?? "abc1234"
: options.devicePodBootCommit ?? "abc1234",
bootSh: `deploy/runtime/boot/${serviceId}.sh`
} : {})
}))
};
+23 -7
View File
@@ -428,6 +428,20 @@ function bootShForService(deployService, serviceId) {
return text;
}
function envReuseServiceIdsForLane(deploy, lane) {
if (lane !== "v02") return new Set();
const configured = deploy?.lanes?.v02?.envReuseServices;
const allowed = new Set(serviceIdsForLane(lane));
return new Set((Array.isArray(configured) ? configured : []).filter((serviceId) => allowed.has(serviceId)));
}
function deployServiceForBoot(deploy, serviceId) {
return {
serviceId,
bootSh: deploy?.lanes?.v02?.bootScripts?.[serviceId]
};
}
function bootMetadataForService({ args, catalog, deployService, serviceId, source }) {
const catalogService = catalogServiceById(catalog, serviceId);
const environmentImage = catalogService?.environmentImage ?? imageFor(args.registryPrefix, `${serviceId}-env`, `env-${String(catalogService?.environmentInputHash ?? source.full).slice(0, 12)}`);
@@ -507,7 +521,7 @@ function isV02RemovedServiceId(serviceId) {
return v02RemovedServiceIds.has(serviceId);
}
function artifactCatalogSkeleton({ args, source }) {
function artifactCatalogSkeleton({ args, deploy, source }) {
const environment = artifactEnvironment(args);
const namespace = args.lane === "v02" ? "hwlab-v02" : "hwlab-dev";
const tag = imageTagForSource(args, source);
@@ -528,11 +542,11 @@ function artifactCatalogSkeleton({ args, source }) {
},
allowedProfiles: [environment],
forbiddenProfiles: args.lane === "v02" ? ["dev", "prod"] : ["prod"],
services: serviceIdsForLane(args.lane).map((serviceId) => artifactCatalogSkeletonService({ args, source, serviceId, environment, namespace, tag }))
services: serviceIdsForLane(args.lane).map((serviceId) => artifactCatalogSkeletonService({ args, deploy, source, serviceId, environment, namespace, tag }))
};
}
function artifactCatalogSkeletonService({ args, source, serviceId, environment, namespace, tag }) {
function artifactCatalogSkeletonService({ args, deploy, source, serviceId, environment, namespace, tag }) {
const base = {
serviceId,
profile: environment,
@@ -544,7 +558,9 @@ function artifactCatalogSkeletonService({ args, source, serviceId, environment,
digest: null,
buildBackend: "contract-skeleton"
};
if (args.lane !== "v02" || serviceId !== "hwlab-device-pod") return base;
const envReuseServiceIds = envReuseServiceIdsForLane(deploy, args.lane);
if (!envReuseServiceIds.has(serviceId)) return base;
const bootSh = bootShForService(deployServiceForBoot(deploy, serviceId), serviceId);
const environmentImage = imageFor(args.registryPrefix, `${serviceId}-env`, `env-${String(source.full).slice(0, 12)}`);
return {
...base,
@@ -557,13 +573,13 @@ function artifactCatalogSkeletonService({ args, source, serviceId, environment,
environmentInputHash: null,
bootRepo: args.sourceRepo,
bootCommit: source.full,
bootSh: `deploy/runtime/boot/${serviceId}.sh`,
bootSh,
codeInputHash: null,
bootEnvEvidence: {
env: {
HWLAB_BOOT_REPO: args.sourceRepo,
HWLAB_BOOT_COMMIT: source.full,
HWLAB_BOOT_SH: `deploy/runtime/boot/${serviceId}.sh`
HWLAB_BOOT_SH: bootSh
}
}
};
@@ -4226,7 +4242,7 @@ async function plannedFiles(args) {
const source = await resolveSourceRevision(args.sourceRevision);
source.imageTag = imageTagForSource(args, source);
let artifactCatalog = await readJsonIfPresent(args.catalogPath, null);
if (!artifactCatalog && args.lane === "v02") artifactCatalog = artifactCatalogSkeleton({ args, source });
if (!artifactCatalog && args.lane === "v02") artifactCatalog = artifactCatalogSkeleton({ args, deploy, source });
if (!artifactCatalog) artifactCatalog = await readJson(args.catalogPath);
const settings = ciLaneSettings(args);
const profiles = laneRuntimeProfiles(args);
+1 -1
View File
@@ -339,7 +339,7 @@ async function readJsonFromGit(repoRoot, ref, filePath, fallback) {
export function enabledEnvReuseServices(deployJson, lane, serviceIds) {
if (lane !== "v02") return new Set();
const configured = deployJson?.lanes?.v02?.envReuseServices;
const values = Array.isArray(configured) ? configured : ["hwlab-device-pod"];
const values = Array.isArray(configured) ? configured : [];
const allowed = new Set(serviceIds);
return new Set(values.filter((serviceId) => allowed.has(serviceId)));
}