Merge pull request #2417 from pikasTech/issue-2416-go-shared-env

feat: share Go env reuse artifact
This commit is contained in:
Lyon
2026-07-07 02:02:31 +08:00
committed by GitHub
3 changed files with 209 additions and 2 deletions
+9
View File
@@ -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
View File
@@ -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`,
+43 -1
View File
@@ -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),