feat: share go env reuse artifact
This commit is contained in:
@@ -5,6 +5,15 @@ metadata:
|
||||
ownerRepository: pikasTech/HWLAB
|
||||
spec:
|
||||
envArtifactGroups:
|
||||
hwlab-go-runtime-env:
|
||||
enabled: true
|
||||
mode: env-reuse-git-mirror-checkout
|
||||
buildService: hwlab-user-billing
|
||||
imageRepository: hwlab-go-runtime-env
|
||||
cacheRepository: cache/hwlab-go-runtime-env
|
||||
services:
|
||||
- hwlab-user-billing
|
||||
- hwlab-workbench-runtime
|
||||
hwlab-ts-runtime-env:
|
||||
enabled: true
|
||||
mode: env-reuse-git-mirror-checkout
|
||||
|
||||
+157
-1
@@ -1145,6 +1145,79 @@ test("v03 planner reads env reuse declarations and catalog from v03 lane config"
|
||||
}
|
||||
});
|
||||
|
||||
test("v03 planner builds one shared Go env artifact for compatible Go services", async () => {
|
||||
const repo = await createFixtureRepo({ catalog: false });
|
||||
await addGoEnvReuseFixture(repo);
|
||||
await writeFile(path.join(repo, "go.mod"), "module github.com/pikasTech/HWLAB\n\ngo 1.22\n\nrequire example.com/direct v0.0.2\n");
|
||||
await git(repo, ["add", "go.mod"]);
|
||||
await git(repo, ["commit", "-m", "change go env dependency"]);
|
||||
|
||||
const plan = await createCiPlan({
|
||||
repoRoot: repo,
|
||||
lane: "v03",
|
||||
baseRef: "HEAD~1",
|
||||
targetRef: "HEAD",
|
||||
artifactCatalogPath: "deploy/nonexistent-artifact-catalog.json",
|
||||
services: ["hwlab-user-billing", "hwlab-workbench-runtime"]
|
||||
});
|
||||
|
||||
const group = plan.envArtifactGroups.find((item) => item.id === "hwlab-go-runtime-env");
|
||||
assert.equal(group.buildRequired, true);
|
||||
assert.deepEqual(group.buildServices, ["hwlab-user-billing"]);
|
||||
assert.deepEqual(group.consumerServices, ["hwlab-workbench-runtime"]);
|
||||
assert.deepEqual(plan.buildServices, ["hwlab-user-billing"]);
|
||||
const userBilling = plan.services.find((service) => service.serviceId === "hwlab-user-billing");
|
||||
const workbenchRuntime = plan.services.find((service) => service.serviceId === "hwlab-workbench-runtime");
|
||||
assert.equal(userBilling.envArtifactCacheRef, "127.0.0.1:5000/hwlab/cache/hwlab-go-runtime-env");
|
||||
assert.equal(workbenchRuntime.envArtifactCacheRef, "127.0.0.1:5000/hwlab/cache/hwlab-go-runtime-env");
|
||||
assert.equal(userBilling.environmentImage, `127.0.0.1:5000/hwlab/hwlab-go-runtime-env:env-${userBilling.environmentInputHash.slice(0, 12)}`);
|
||||
assert.equal(workbenchRuntime.environmentImage, userBilling.environmentImage);
|
||||
assert.equal(workbenchRuntime.sharedEnvBuildSkipped, true);
|
||||
assert.equal(workbenchRuntime.buildRequired, false);
|
||||
});
|
||||
|
||||
test("v03 planner ignores Go launcher comment-only changes for env identity", async () => {
|
||||
const repo = await createFixtureRepo({ catalog: false });
|
||||
await addGoEnvReuseFixture(repo);
|
||||
const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
|
||||
const initialPlan = await createCiPlan({
|
||||
repoRoot: repo,
|
||||
lane: "v03",
|
||||
baseRef: "HEAD",
|
||||
targetRef: initialSha,
|
||||
artifactCatalogPath: "deploy/nonexistent-artifact-catalog.json",
|
||||
services: ["hwlab-user-billing", "hwlab-workbench-runtime"]
|
||||
});
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v03.json"), JSON.stringify(createCatalogFixture("ready", catalogOptionsFromPlan(initialPlan, {
|
||||
sourceCommitId: initialSha,
|
||||
serviceIds: ["hwlab-user-billing", "hwlab-workbench-runtime"]
|
||||
})), null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.v03.json"]);
|
||||
await git(repo, ["commit", "-m", "seed go env reuse catalog"]);
|
||||
|
||||
await writeFile(path.join(repo, "deploy/runtime/launcher/hwlab-go-env-reuse-launcher.mjs"), "// changed explanatory comment\nexport const goEnvLauncher = true;\n");
|
||||
await git(repo, ["add", "deploy/runtime/launcher/hwlab-go-env-reuse-launcher.mjs"]);
|
||||
await git(repo, ["commit", "-m", "change go launcher comment only"]);
|
||||
|
||||
const plan = await createCiPlan({
|
||||
repoRoot: repo,
|
||||
lane: "v03",
|
||||
baseRef: "HEAD~1",
|
||||
targetRef: "HEAD",
|
||||
artifactCatalogPath: "deploy/artifact-catalog.v03.json",
|
||||
services: ["hwlab-user-billing", "hwlab-workbench-runtime"]
|
||||
});
|
||||
|
||||
assert.deepEqual(plan.affectedServices, []);
|
||||
assert.deepEqual(plan.buildServices, []);
|
||||
assert.equal(plan.imageBuildRequired, false);
|
||||
for (const service of plan.services) {
|
||||
assert.equal(service.runtimeReuseDecision.envIdentityHit, true, service.serviceId);
|
||||
assert.equal(service.environmentInputChanged, false, service.serviceId);
|
||||
assert.equal(service.buildRequired, false, service.serviceId);
|
||||
}
|
||||
});
|
||||
|
||||
async function createFixtureRepo(options = {}) {
|
||||
const serviceIds = options.serviceIds ?? ["hwlab-cloud-api", "hwlab-cloud-web"];
|
||||
const laneServiceIds = options.laneServiceIds ?? V02_RUNTIME_SERVICE_IDS;
|
||||
@@ -1255,6 +1328,84 @@ function planV02CoreServices(repo, options) {
|
||||
});
|
||||
}
|
||||
|
||||
async function addGoEnvReuseFixture(repo) {
|
||||
await mkdir(path.join(repo, "cmd/hwlab-user-billing"), { recursive: true });
|
||||
await mkdir(path.join(repo, "cmd/hwlab-workbench-runtime"), { recursive: true });
|
||||
await mkdir(path.join(repo, "internal/userbilling"), { recursive: true });
|
||||
await mkdir(path.join(repo, "internal/workbenchruntime"), { recursive: true });
|
||||
await mkdir(path.join(repo, "gitops"), { recursive: true });
|
||||
await writeFile(path.join(repo, "go.mod"), "module github.com/pikasTech/HWLAB\n\ngo 1.22\n\nrequire example.com/direct v0.0.1\n");
|
||||
await writeFile(path.join(repo, "go.sum"), "example.com/direct v0.0.1 h1:fixture\n");
|
||||
await writeFile(path.join(repo, "cmd/hwlab-user-billing/main.go"), "package main\n\nfunc main() {}\n");
|
||||
await writeFile(path.join(repo, "cmd/hwlab-workbench-runtime/main.go"), "package main\n\nfunc main() {}\n");
|
||||
await writeFile(path.join(repo, "internal/userbilling/billing.go"), "package userbilling\n");
|
||||
await writeFile(path.join(repo, "internal/workbenchruntime/runtime.go"), "package workbenchruntime\n");
|
||||
await writeFile(path.join(repo, "deploy/runtime/boot/hwlab-user-billing.sh"), "#!/bin/sh\nexec /app/hwlab-user-billing\n");
|
||||
await writeFile(path.join(repo, "deploy/runtime/boot/hwlab-workbench-runtime.sh"), "#!/bin/sh\nexec /app/hwlab-workbench-runtime\n");
|
||||
await writeFile(path.join(repo, "deploy/runtime/launcher/hwlab-go-env-reuse-launcher.mjs"), "// initial explanatory comment\nexport const goEnvLauncher = true;\n");
|
||||
const deployPath = path.join(repo, "deploy/deploy.yaml");
|
||||
const deploy = await readStructuredFile(repo, deployPath);
|
||||
deploy.lanes.v03.envReuseServices = uniquePreserveOrder([
|
||||
...deploy.lanes.v03.envReuseServices,
|
||||
"hwlab-user-billing",
|
||||
"hwlab-workbench-runtime"
|
||||
]);
|
||||
deploy.lanes.v03.bootScripts["hwlab-user-billing"] = "deploy/runtime/boot/hwlab-user-billing.sh";
|
||||
deploy.lanes.v03.bootScripts["hwlab-workbench-runtime"] = "deploy/runtime/boot/hwlab-workbench-runtime.sh";
|
||||
deploy.lanes.v03.serviceDeclarations["hwlab-user-billing"] = {
|
||||
runtimeKind: "go-service",
|
||||
entrypoint: "cmd/hwlab-user-billing/main.go",
|
||||
artifactKind: "go-service",
|
||||
healthPath: "/health/live",
|
||||
healthPort: 7101,
|
||||
componentPaths: ["cmd/hwlab-user-billing/", "internal/userbilling/"],
|
||||
env: {},
|
||||
observable: true
|
||||
};
|
||||
deploy.lanes.v03.serviceDeclarations["hwlab-workbench-runtime"] = {
|
||||
runtimeKind: "go-service",
|
||||
entrypoint: "cmd/hwlab-workbench-runtime/main.go",
|
||||
artifactKind: "go-service",
|
||||
healthPath: "/health/live",
|
||||
healthPort: 7102,
|
||||
componentPaths: ["cmd/hwlab-workbench-runtime/", "internal/workbenchruntime/"],
|
||||
env: {},
|
||||
observable: true
|
||||
};
|
||||
await writeStructuredFile(repo, deployPath, deploy);
|
||||
await writeStructuredFile(repo, "gitops/reuse.ymal", {
|
||||
apiVersion: "unidesk.pikapython.com/v1alpha1",
|
||||
kind: "RuntimeReuseConfig",
|
||||
metadata: { name: "hwlab-v03-reuse", ownerRepository: "pikasTech/HWLAB" },
|
||||
spec: {
|
||||
envArtifactGroups: {
|
||||
"hwlab-go-runtime-env": {
|
||||
enabled: true,
|
||||
mode: "env-reuse-git-mirror-checkout",
|
||||
buildService: "hwlab-user-billing",
|
||||
imageRepository: "hwlab-go-runtime-env",
|
||||
cacheRepository: "cache/hwlab-go-runtime-env",
|
||||
services: ["hwlab-user-billing", "hwlab-workbench-runtime"]
|
||||
}
|
||||
},
|
||||
services: Object.fromEntries(["hwlab-user-billing", "hwlab-workbench-runtime"].map((serviceId) => [serviceId, {
|
||||
runtimeReuse: {
|
||||
enabled: true,
|
||||
codeIdentity: { paths: [serviceId === "hwlab-user-billing" ? "cmd/hwlab-user-billing" : "cmd/hwlab-workbench-runtime"] },
|
||||
envIdentity: { paths: ["go.mod", "go.sum", "deploy/runtime/launcher/hwlab-go-env-reuse-launcher.mjs"] }
|
||||
},
|
||||
envReuse: {
|
||||
enabled: true,
|
||||
mode: "env-reuse-git-mirror-checkout",
|
||||
envIdentityFiles: ["go.mod", "go.sum", "deploy/runtime/launcher/hwlab-go-env-reuse-launcher.mjs"]
|
||||
}
|
||||
}]))
|
||||
}
|
||||
});
|
||||
await git(repo, ["add", "."]);
|
||||
await git(repo, ["commit", "-m", "add go env reuse fixture"]);
|
||||
}
|
||||
|
||||
function createDeployFixture({ serviceIds = V02_RUNTIME_SERVICE_IDS } = {}) {
|
||||
const v02 = {
|
||||
sourceRepo: "git@github.com:pikasTech/HWLAB.git",
|
||||
@@ -1380,6 +1531,11 @@ function createDeployFixture({ serviceIds = V02_RUNTIME_SERVICE_IDS } = {}) {
|
||||
|
||||
function createCatalogFixture(mode, options = {}) {
|
||||
const digest = mode === "ready" ? `sha256:${"1".repeat(64)}` : "not_published";
|
||||
const envReuseCatalogServiceIds = new Set([
|
||||
...V02_RUNTIME_SERVICE_IDS,
|
||||
...Object.keys(options.environmentInputHashes ?? {}),
|
||||
...Object.keys(options.codeInputHashes ?? {})
|
||||
]);
|
||||
const serviceIds = options.serviceIds ?? uniquePreserveOrder([
|
||||
"hwlab-cloud-api",
|
||||
"hwlab-cloud-web",
|
||||
@@ -1398,7 +1554,7 @@ function createCatalogFixture(mode, options = {}) {
|
||||
...((options.componentInputHashes?.[serviceId] || (serviceId === "hwlab-cloud-api" && options.cloudApiComponentInputHash)) ? {
|
||||
componentInputHash: options.cloudApiComponentInputHash ?? options.componentInputHashes?.[serviceId]
|
||||
} : {}),
|
||||
...(V02_RUNTIME_SERVICE_IDS.includes(serviceId) ? {
|
||||
...(envReuseCatalogServiceIds.has(serviceId) ? {
|
||||
runtimeMode: "env-reuse-git-mirror-checkout",
|
||||
envReuse: true,
|
||||
environmentImage: `127.0.0.1:5000/hwlab/${serviceId}-env:env-abc1234`,
|
||||
|
||||
@@ -25,6 +25,10 @@ export const GO_RUNTIME_DEP_PATHS = Object.freeze([
|
||||
"deploy/runtime/launcher/hwlab-go-env-reuse-launcher.mjs"
|
||||
]);
|
||||
|
||||
const COMMENT_INSENSITIVE_ENV_IDENTITY_PATHS = new Set([
|
||||
"deploy/runtime/launcher/hwlab-go-env-reuse-launcher.mjs"
|
||||
]);
|
||||
|
||||
export const DEFAULT_RUNTIME_PACKAGE_FIELDS = Object.freeze([
|
||||
"dependencies",
|
||||
"optionalDependencies",
|
||||
@@ -629,12 +633,25 @@ export async function runtimeDependencyChangedPaths(repoRoot, baseRef, targetRef
|
||||
result.add(filePath);
|
||||
continue;
|
||||
}
|
||||
if (COMMENT_INSENSITIVE_ENV_IDENTITY_PATHS.has(filePath)) {
|
||||
if (await envIdentityTextChanged(repoRoot, baseRef, targetRef, filePath)) result.add(filePath);
|
||||
continue;
|
||||
}
|
||||
if (filePath !== "package.json") continue;
|
||||
if (await packageRuntimeFieldsChanged(repoRoot, baseRef, targetRef, filePath)) result.add(filePath);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function envIdentityTextChanged(repoRoot, baseRef, targetRef, filePath) {
|
||||
const [before, after] = await Promise.all([
|
||||
readTextFromGit(repoRoot, baseRef, filePath, null),
|
||||
readTextFromGit(repoRoot, targetRef, filePath, null)
|
||||
]);
|
||||
if (before === null || after === null) return true;
|
||||
return stableJson(normalizeEnvIdentityText(filePath, before)) !== stableJson(normalizeEnvIdentityText(filePath, after));
|
||||
}
|
||||
|
||||
async function serviceRuntimeConfigChanged(repoRoot, deployConfigPath, baseRef, targetRef, lane, serviceId) {
|
||||
const [before, after] = await Promise.all([
|
||||
readDeployConfigFromGit(repoRoot, baseRef, deployConfigPath),
|
||||
@@ -690,13 +707,21 @@ function pickRuntimePackageJson(packageJson) {
|
||||
|
||||
async function readJsonFromGit(repoRoot, ref, filePath, fallback) {
|
||||
try {
|
||||
const value = await gitValue(repoRoot, ["show", `${ref}:${filePath}`]);
|
||||
const value = await readTextFromGit(repoRoot, ref, filePath, "");
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
async function readTextFromGit(repoRoot, ref, filePath, fallback) {
|
||||
try {
|
||||
return await gitValue(repoRoot, ["show", `${ref}:${filePath}`]);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export function enabledEnvReuseServices(deployJson, lane, serviceIds) {
|
||||
const configured = deployJson?.lanes?.[lane]?.envReuseServices;
|
||||
const values = Array.isArray(configured) ? configured : [];
|
||||
@@ -925,6 +950,10 @@ export async function hashGitPaths(repoRoot, targetRef, paths, options = {}) {
|
||||
const packageJson = await readJsonFromGit(repoRoot, targetRef, filePath, null);
|
||||
return `100644 blob ${stableHash(pickRuntimePackageJson(packageJson))}\t${filePath}`;
|
||||
}
|
||||
if (COMMENT_INSENSITIVE_ENV_IDENTITY_PATHS.has(filePath)) {
|
||||
const source = await readTextFromGit(repoRoot, targetRef, filePath, "");
|
||||
return `100644 blob ${stableHash(normalizeEnvIdentityText(filePath, source))}\t${filePath}`;
|
||||
}
|
||||
return line;
|
||||
})))
|
||||
.filter(Boolean)
|
||||
@@ -932,6 +961,19 @@ export async function hashGitPaths(repoRoot, targetRef, paths, options = {}) {
|
||||
return stableHash({ entries: lines });
|
||||
}
|
||||
|
||||
function normalizeEnvIdentityText(filePath, source) {
|
||||
const normalizedPath = normalizeRepoPath(filePath);
|
||||
const text = String(source ?? "").replace(/\r\n?/gu, "\n");
|
||||
if (!COMMENT_INSENSITIVE_ENV_IDENTITY_PATHS.has(normalizedPath)) return text;
|
||||
return text
|
||||
.split("\n")
|
||||
.filter((line) => {
|
||||
const trimmed = line.trim();
|
||||
return trimmed && !trimmed.startsWith("//");
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
async function hashEnvReuseInputs(repoRoot, targetRef, paths, recipe, options = {}) {
|
||||
return stableHash({
|
||||
gitPaths: await hashGitPaths(repoRoot, targetRef, paths, options),
|
||||
|
||||
Reference in New Issue
Block a user