fix: harden v02 bun runtime rollout

This commit is contained in:
Codex
2026-05-29 04:28:34 +08:00
parent 6813e6e5cf
commit ddde5b8d37
6 changed files with 229 additions and 60 deletions
+32 -5
View File
@@ -73,6 +73,22 @@ const servicePorts = new Map([
["hwlab-agent-skills", 7430]
]);
const v02RuntimeServiceIds = Object.freeze([
"hwlab-cloud-api",
"hwlab-cloud-web",
"hwlab-agent-mgr",
"hwlab-agent-worker",
"hwlab-device-pod",
"hwlab-gateway",
"hwlab-edge-proxy",
"hwlab-cli",
"hwlab-agent-skills"
]);
function serviceIdsForLane(lane) {
return lane === "v02" ? v02RuntimeServiceIds : SERVICE_IDS;
}
function parseArgs(argv) {
const args = {
mode: "preflight",
@@ -128,6 +144,7 @@ function parseArgs(argv) {
assert.ok(["g14", "v02"].includes(args.lane), `unknown artifact lane ${args.lane}`);
assert.ok(["short", "full"].includes(args.imageTagMode), `unknown image tag mode ${args.imageTagMode}`);
if (!args.servicesExplicit) args.services = [...serviceIdsForLane(args.lane)];
return args;
}
@@ -574,7 +591,7 @@ function assertV02Contracts(catalog, deployManifest) {
assert.deepEqual(catalog.allowedProfiles, ["v02"], "catalog.allowedProfiles must only allow v02");
assert.deepEqual(catalog.forbiddenProfiles, ["dev", "prod"], "catalog.forbiddenProfiles must forbid dev/prod");
const catalogIds = (catalog.services ?? []).map((service) => service.serviceId);
assert.deepEqual(catalogIds, SERVICE_IDS, "v02 catalog services must match frozen service IDs");
assert.deepEqual(catalogIds, serviceIdsForLane("v02"), "v02 catalog services must match retained service IDs");
for (const service of catalog.services ?? []) {
assert.equal(service.profile, "v02", `${service.serviceId}.profile must be v02`);
assert.equal(service.namespace, "hwlab-v02", `${service.serviceId}.namespace must be hwlab-v02`);
@@ -614,7 +631,7 @@ function artifactCatalogSkeleton({ args, commitId }) {
},
allowedProfiles: [artifactEnvironment(args)],
forbiddenProfiles: args.lane === "v02" ? ["dev", "prod"] : ["prod"],
services: SERVICE_IDS.map((serviceId) => ({
services: serviceIdsForLane(args.lane).map((serviceId) => ({
serviceId,
commitId: tag,
sourceCommitId: commitId,
@@ -634,6 +651,15 @@ function artifactCatalogSkeleton({ args, commitId }) {
};
}
function normalizeCatalogForLane(catalog, lane) {
if (!catalog || lane !== "v02") return catalog;
const allowed = new Set(serviceIdsForLane(lane));
return {
...catalog,
services: (catalog.services ?? []).filter((service) => allowed.has(service.serviceId))
};
}
function parseRegistryPrefix(prefix) {
const normalized = prefix.replace(/\/+$/u, "");
const [hostPort, ...pathParts] = normalized.split("/");
@@ -1184,7 +1210,7 @@ if (runtimeKind === "node-command" && entrypoint) {
}
function dockerfile(baseImage, port, labels = []) {
const dependencyTimingScript = `started_at=$(node -e "console.log(Date.now())"); status=succeeded; if [ -d /opt/hwlab-node-runtime-base/node_modules ]; then cp -a /opt/hwlab-node-runtime-base/node_modules ./node_modules; fi && if [ -f package-lock.json ]; then npm install --omit=dev --include=optional --ignore-scripts; else npm install --omit=dev --include=optional --ignore-scripts; fi && if [ "\${HWLAB_ARTIFACT_KIND:-}" = "bun-command" ]; then npm install --no-save --omit=dev --include=optional --ignore-scripts bun@1.3.13; fi && codex_version="$(node -p "require('./node_modules/@openai/codex/package.json').version")" && case "$(uname -m)" in x86_64|amd64) test -e node_modules/@openai/codex-linux-x64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-x64@npm:@openai/codex@\${codex_version}-linux-x64" ;; aarch64|arm64) test -e node_modules/@openai/codex-linux-arm64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-arm64@npm:@openai/codex@\${codex_version}-linux-arm64" ;; esac || status=failed; finished_at=$(node -e "console.log(Date.now())"); node -e "console.log(JSON.stringify({event:'g14-cicd-build-step-timing',stage:'dependency-install',status:process.argv[1],durationMs:Number(process.argv[3])-Number(process.argv[2]),source:'dockerfile-run'}))" "$status" "$started_at" "$finished_at"; test "$status" = succeeded`;
const dependencyTimingScript = `started_at=$(node -e "console.log(Date.now())"); status=succeeded; if [ -d /opt/hwlab-node-runtime-base/node_modules ]; then cp -a /opt/hwlab-node-runtime-base/node_modules ./node_modules; fi && if [ -f package-lock.json ]; then npm install --omit=dev --include=optional --ignore-scripts; else npm install --omit=dev --include=optional --ignore-scripts; fi && if [ "\${HWLAB_ARTIFACT_KIND:-}" = "bun-command" ]; then npm install --no-save --omit=dev --include=optional --ignore-scripts bun@1.3.13 && test -x /app/node_modules/.bin/bun && ln -sf /app/node_modules/.bin/bun /usr/local/bin/bun; fi && codex_version="$(node -p "require('./node_modules/@openai/codex/package.json').version")" && case "$(uname -m)" in x86_64|amd64) test -e node_modules/@openai/codex-linux-x64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-x64@npm:@openai/codex@\${codex_version}-linux-x64" ;; aarch64|arm64) test -e node_modules/@openai/codex-linux-arm64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-arm64@npm:@openai/codex@\${codex_version}-linux-arm64" ;; esac || status=failed; finished_at=$(node -e "console.log(Date.now())"); node -e "console.log(JSON.stringify({event:'g14-cicd-build-step-timing',stage:'dependency-install',status:process.argv[1],durationMs:Number(process.argv[3])-Number(process.argv[2]),source:'dockerfile-run'}))" "$status" "$started_at" "$finished_at"; test "$status" = succeeded`;
return [
`FROM ${baseImage}`,
"WORKDIR /app",
@@ -1210,7 +1236,7 @@ function dockerfile(baseImage, port, labels = []) {
"COPY tools ./tools",
"COPY skills ./skills",
"COPY deploy ./deploy",
"RUN mkdir -p /workspace /codex-home && rm -rf /workspace/hwlab && ln -s /app /workspace/hwlab && chmod -R a+rwX /app /workspace /codex-home && test -x /app/node_modules/.bin/codex && /app/node_modules/.bin/codex --version >/tmp/hwlab-codex-version.txt && (test -x /app/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex || test -x /app/node_modules/@openai/codex-linux-arm64/vendor/aarch64-unknown-linux-musl/codex/codex)",
"RUN mkdir -p /workspace /codex-home && rm -rf /workspace/hwlab && ln -s /app /workspace/hwlab && if [ \"${HWLAB_ARTIFACT_KIND:-}\" = \"bun-command\" ]; then test -x /usr/local/bin/bun && /usr/local/bin/bun --version >/tmp/hwlab-bun-version.txt; fi && chmod -R a+rwX /app /workspace /codex-home && test -x /app/node_modules/.bin/codex && /app/node_modules/.bin/codex --version >/tmp/hwlab-codex-version.txt && (test -x /app/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex || test -x /app/node_modules/@openai/codex-linux-arm64/vendor/aarch64-unknown-linux-musl/codex/codex)",
`RUN node -e "const fs=require('node:fs'); fs.writeFileSync('/usr/local/bin/hwlab-dev-artifact-runtime.mjs', Buffer.from('${runtimeScriptBase64()}', 'base64')); fs.chmodSync('/usr/local/bin/hwlab-dev-artifact-runtime.mjs', 0o755);"`,
"ARG HWLAB_ENVIRONMENT",
"ARG HWLAB_SERVICE_ID",
@@ -2200,6 +2226,7 @@ async function main() {
let catalog = await readJsonIfPresent(args.catalogPath, null);
const catalogWasMissing = !catalog;
if (!catalog && args.lane === "v02") catalog = artifactCatalogSkeleton({ args, commitId });
catalog = normalizeCatalogForLane(catalog, args.lane);
assert.ok(catalog, `${args.catalogPath} is required for ${args.lane} artifact publish`);
const shortCommit = imageTagForCommit(args, commitId);
let effectiveCatalogPath = args.catalogPath;
@@ -2217,7 +2244,7 @@ async function main() {
artifactCatalogPath: effectiveCatalogPath,
registryPrefix: args.registryPrefix,
baseImage: args.baseImage ?? undefined,
...(args.servicesExplicit ? { services: args.services } : {})
services: args.services
});
const services = enrichServicesWithCiPlan(await resolveServices(args.services, catalog), ciPlan);
const catalogByServiceId = new Map((catalog?.services ?? []).map((service) => [service.serviceId, service]));
+116 -41
View File
@@ -43,7 +43,29 @@ const defaultServiceIds = Object.freeze([
"hwlab-cli",
"hwlab-agent-skills"
]);
const defaultServicesParam = defaultServiceIds.join(",");
const v02RuntimeServiceIds = Object.freeze([
"hwlab-cloud-api",
"hwlab-cloud-web",
"hwlab-agent-mgr",
"hwlab-agent-worker",
"hwlab-device-pod",
"hwlab-gateway",
"hwlab-edge-proxy",
"hwlab-cli",
"hwlab-agent-skills"
]);
const v02RemovedServiceIds = new Set([
"hwlab-router",
"hwlab-tunnel-client",
"hwlab-gateway-simu",
"hwlab-box-simu",
"hwlab-patch-panel"
]);
const v02RemovedServiceEnvNames = new Set([
"HWLAB_M3_GATEWAY_SIMU_1_URL",
"HWLAB_M3_GATEWAY_SIMU_2_URL",
"HWLAB_M3_PATCH_PANEL_URL"
]);
const moonBridgeSourceRef = process.env.HWLAB_G14_MOONBRIDGE_REF || "1b99888d3dae889b79ee602cb875c7907f7e76f2";
const moonBridgeImage = process.env.HWLAB_G14_MOONBRIDGE_IMAGE || `${defaultRegistryPrefix}/moonbridge:${moonBridgeSourceRef.slice(0, 12)}`;
const deepSeekProfileModel = process.env.HWLAB_G14_DEEPSEEK_PROFILE_MODEL || "deepseek-chat";
@@ -414,6 +436,22 @@ function artifactEndpoint(args) {
return args.lane === "v02" ? defaultV02RuntimeEndpoint : defaultRuntimeEndpoint;
}
function serviceIdsForLane(lane) {
return lane === "v02" ? v02RuntimeServiceIds : defaultServiceIds;
}
function serviceIdsForProfile(profile) {
return profile === "v02" ? v02RuntimeServiceIds : defaultServiceIds;
}
function servicesParamForLane(lane) {
return serviceIdsForLane(lane).join(",");
}
function isV02RemovedServiceId(serviceId) {
return v02RemovedServiceIds.has(serviceId);
}
function artifactCatalogSkeleton({ args, source }) {
const environment = artifactEnvironment(args);
const namespace = args.lane === "v02" ? "hwlab-v02" : "hwlab-dev";
@@ -435,7 +473,7 @@ function artifactCatalogSkeleton({ args, source }) {
},
allowedProfiles: [environment],
forbiddenProfiles: args.lane === "v02" ? ["dev", "prod"] : ["prod"],
services: defaultServiceIds.map((serviceId) => ({
services: serviceIdsForLane(args.lane).map((serviceId) => ({
serviceId,
profile: environment,
namespace,
@@ -585,8 +623,16 @@ function applyDeployEnv(envList, deployService, namespace, profile = "dev") {
}
}
function removeV02LegacySimulatorEnv(envList) {
if (!Array.isArray(envList)) return envList;
return envList.filter((entry) => !v02RemovedServiceEnvNames.has(entry?.name));
}
function deployServicesForProfile(deploy, profile) {
const services = new Map((deploy.services ?? []).map((service) => [service.serviceId, cloneJson(service)]));
const allowed = new Set(serviceIdsForProfile(profile));
const services = new Map((deploy.services ?? [])
.filter((service) => allowed.has(service.serviceId))
.map((service) => [service.serviceId, cloneJson(service)]));
const laneServices = profile === "v02" && Array.isArray(deploy.lanes?.v02?.services)
? deploy.lanes.v02.services
: [];
@@ -613,6 +659,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, registryPrefix
"hwlab.pikastech.local/environment": profileLabel,
"hwlab.pikastech.local/profile": profileLabel
};
result.items = asItems(result, "workloads").filter((item) => !(profile === "v02" && isV02RemovedServiceId(serviceIdForWorkload(item, null))));
for (const item of asItems(result, "workloads")) {
item.metadata.namespace = namespace;
label(item.metadata, {
@@ -674,6 +721,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, registryPrefix
if (Object.hasOwn(entry, "value")) entry.value = namespaceScopedHost(entry.value, namespace);
}
applyDeployEnv(container.env, deployService, namespace, profile);
if (profile === "v02") container.env = removeV02LegacySimulatorEnv(container.env);
upsertEnv(container.env, "HWLAB_ENVIRONMENT", profileLabel);
upsertEnv(container.env, "HWLAB_COMMIT_ID", runtimeCommit);
upsertEnv(container.env, "HWLAB_IMAGE", image);
@@ -689,6 +737,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, registryPrefix
}
if (serviceId === "hwlab-cloud-api") {
syncCodeAgentWorkspaceInitContainer(podTemplate.spec.initContainers, { image, runtimeCommit, runtimeImageTag });
if (profile === "v02") upsertEnv(container.env, "HWLAB_M3_IO_CONTROL_ENABLED", "false");
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE", "deepseek");
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEEPSEEK_MODEL", deepSeekProfileModel);
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL", deepSeekBaseUrl(namespace));
@@ -759,6 +808,14 @@ function transformListNamespace(value, namespace, labels, annotations) {
return result;
}
function transformServices({ services, namespace, labels, annotations, profile = "dev" }) {
const result = transformListNamespace(services, namespace, labels, annotations);
if (result.kind === "List") {
result.items = (result.items ?? []).filter((item) => !(profile === "v02" && isV02RemovedServiceId(serviceIdForWorkload(item, null))));
}
return result;
}
function shellSingleQuote(value) {
return `'${String(value).replace(/'/g, `'"'"'`)}'`;
}
@@ -934,7 +991,7 @@ function prepareSourceScript() {
" git fetch --depth 1 origin \"$(params.gitops-branch)\" >/dev/null 2>&1 || true",
" if git cat-file -e FETCH_HEAD:\"$catalog_path\" 2>/dev/null; then",
" git show FETCH_HEAD:\"$catalog_path\" > /tmp/hwlab-gitops-artifact-catalog.json",
" if node -e 'const fs=require(\"fs\"); const ids=(doc)=>(doc.services||[]).map((s)=>s.serviceId); const gitops=JSON.parse(fs.readFileSync(process.argv[1],\"utf8\")); const deploy=JSON.parse(fs.readFileSync(process.argv[2],\"utf8\")); if (JSON.stringify(ids(gitops)) !== JSON.stringify(ids(deploy))) process.exit(42);' /tmp/hwlab-gitops-artifact-catalog.json deploy/deploy.json; then",
" if node -e 'const fs=require(\"fs\"); const ids=(doc)=>(doc.services||[]).map((s)=>s.serviceId); const selected=(process.argv[3]||\"\").split(\",\").filter(Boolean); const gitops=JSON.parse(fs.readFileSync(process.argv[1],\"utf8\")); const deploy=JSON.parse(fs.readFileSync(process.argv[2],\"utf8\")); const expected=selected.length ? selected : ids(deploy); if (JSON.stringify(ids(gitops)) !== JSON.stringify(expected)) process.exit(42);' /tmp/hwlab-gitops-artifact-catalog.json deploy/deploy.json \"$(params.services)\"; then",
" cp /tmp/hwlab-gitops-artifact-catalog.json \"$catalog_path\"",
" echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"loaded\",\"branch\":\"'\"$(params.gitops-branch)\"'\"}'",
" else",
@@ -947,14 +1004,16 @@ function prepareSourceScript() {
" echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"seed\",\"reason\":\"gitops-branch-missing\"}'",
"fi",
"if [ ! -s \"$catalog_path\" ] && [ \"$(params.lane)\" = \"v02\" ]; then",
" node - \"$catalog_path\" \"$(params.revision)\" \"$(params.registry-prefix)\" \"$(params.image-tag-mode)\" <<'NODE'",
" node - \"$catalog_path\" \"$(params.revision)\" \"$(params.registry-prefix)\" \"$(params.image-tag-mode)\" \"$(params.services)\" <<'NODE'",
"const fs = require('node:fs');",
"const path = require('node:path');",
"const [catalogPath, revision, registryPrefix, imageTagMode] = process.argv.slice(2);",
"const [catalogPath, revision, registryPrefix, imageTagMode, selectedServices] = process.argv.slice(2);",
"const deploy = JSON.parse(fs.readFileSync('deploy/deploy.json', 'utf8'));",
"const tag = imageTagMode === 'full' ? revision : revision.slice(0, 7);",
"const namespace = 'hwlab-v02';",
"const services = (deploy.services || []).map((service) => ({ serviceId: service.serviceId, profile: 'v02', namespace, commitId: tag, sourceCommitId: revision, image: `${registryPrefix}/${service.serviceId}:${tag}`, imageTag: tag, digest: null, buildBackend: 'contract-skeleton' }));",
"const selected = (selectedServices || '').split(',').filter(Boolean);",
"const serviceIds = selected.length ? selected : (deploy.services || []).map((service) => service.serviceId);",
"const services = serviceIds.map((serviceId) => ({ serviceId, profile: 'v02', namespace, commitId: tag, sourceCommitId: revision, image: `${registryPrefix}/${serviceId}:${tag}`, imageTag: tag, digest: null, buildBackend: 'contract-skeleton' }));",
"fs.mkdirSync(path.dirname(catalogPath), { recursive: true });",
"fs.writeFileSync(catalogPath, JSON.stringify({ catalogVersion: 'v1', kind: 'hwlab-artifact-catalog', environment: 'v02', profile: 'v02', namespace, endpoint: 'http://74.48.78.17:19667', commitId: tag, artifactState: 'contract-skeleton', publish: { registryPrefix, sourceCommitId: revision, imageTag: tag, publishedAt: null }, allowedProfiles: ['v02'], forbiddenProfiles: ['dev', 'prod'], services }, null, 2) + '\\n');",
"NODE",
@@ -1025,7 +1084,7 @@ node - <<'NODE'
const fs = require("node:fs");
const plan = JSON.parse(fs.readFileSync("/workspace/source/ci-plan.json", "utf8"));
const selected = String(process.env.HWLAB_SELECTED_SERVICES || "").split(",").map((item) => item.trim()).filter(Boolean);
const allServices = ${JSON.stringify(defaultServiceIds)};
const allServices = selected.length > 0 ? selected : ${JSON.stringify(defaultServiceIds)};
const affected = new Set(plan.affectedServices || []);
const selectedSet = new Set(selected);
const entries = allServices.map((serviceId) => ({ serviceId, selected: selectedSet.has(serviceId), affected: selectedSet.has(serviceId) && affected.has(serviceId) }));
@@ -1714,19 +1773,19 @@ function serviceResultEnvName(serviceId, field) {
return `HWLAB_SERVICE_RESULT_${serviceId.toUpperCase().replace(/[^A-Z0-9]+/gu, "_")}_${field.toUpperCase().replace(/[^A-Z0-9]+/gu, "_")}`;
}
function serviceResultParams() {
return defaultServiceIds.flatMap((serviceId) => serviceResultFields.map((field) => ({ name: serviceResultParamName(serviceId, field) })));
function serviceResultParams(serviceIds = defaultServiceIds) {
return serviceIds.flatMap((serviceId) => serviceResultFields.map((field) => ({ name: serviceResultParamName(serviceId, field), default: "" })));
}
function serviceResultParamValues() {
return defaultServiceIds.flatMap((serviceId) => serviceResultFields.map((field) => ({
function serviceResultParamValues(serviceIds = defaultServiceIds) {
return serviceIds.flatMap((serviceId) => serviceResultFields.map((field) => ({
name: serviceResultParamName(serviceId, field),
value: `$(tasks.${buildTaskName(serviceId)}.results.${field})`
})));
}
function serviceResultEnv() {
return defaultServiceIds.flatMap((serviceId) => serviceResultFields.map((field) => ({
function serviceResultEnv(serviceIds = defaultServiceIds) {
return serviceIds.flatMap((serviceId) => serviceResultFields.map((field) => ({
name: serviceResultEnvName(serviceId, field),
value: `$(params.${serviceResultParamName(serviceId, field)})`
})));
@@ -1776,7 +1835,7 @@ function buildkitSidecar() {
};
}
function planArtifactsTask() {
function planArtifactsTask({ serviceIds = defaultServiceIds } = {}) {
return {
name: "plan-artifacts",
runAfter: primitiveValidationTaskNames(),
@@ -1788,7 +1847,7 @@ function planArtifactsTask() {
{ name: "registry-prefix" },
{ name: "services" }
],
results: defaultServiceIds.map((serviceId) => ({ name: affectedResultName(serviceId), description: `${serviceId} changed according to g14-ci-plan` })),
results: serviceIds.map((serviceId) => ({ name: affectedResultName(serviceId), description: `${serviceId} changed according to g14-ci-plan` })),
workspaces: [{ name: "source" }],
steps: [{
name: "plan",
@@ -1849,10 +1908,10 @@ function perServiceBuildTask(serviceId) {
};
}
function collectArtifactsTask() {
function collectArtifactsTask({ serviceIds = defaultServiceIds } = {}) {
return {
name: "collect-artifacts",
runAfter: defaultServiceIds.map(buildTaskName),
runAfter: serviceIds.map(buildTaskName),
workspaces: [{ name: "source", workspace: "source" }],
taskSpec: {
params: [
@@ -1863,10 +1922,10 @@ function collectArtifactsTask() {
{ name: "registry-prefix" },
{ name: "services" },
{ name: "base-image" },
...serviceResultParams()
...serviceResultParams(serviceIds)
],
workspaces: [{ name: "source" }],
steps: [{ name: "collect", image: ciToolsRunnerImage, env: [...proxyEnv(), ...serviceResultEnv()], script: collectArtifactsScript() }]
steps: [{ name: "collect", image: ciToolsRunnerImage, env: [...proxyEnv(), ...serviceResultEnv(serviceIds)], script: collectArtifactsScript() }]
},
params: [
{ name: "revision", value: "$(params.revision)" },
@@ -1876,7 +1935,7 @@ function collectArtifactsTask() {
{ name: "registry-prefix", value: "$(params.registry-prefix)" },
{ name: "services", value: "$(params.services)" },
{ name: "base-image", value: "$(params.base-image)" },
...serviceResultParamValues()
...serviceResultParamValues(serviceIds)
]
};
}
@@ -1982,7 +2041,11 @@ async function runtimeItems() {
}
function runtimeSourceCommit(item) {
return item.metadata?.labels?.["hwlab.pikastech.local/source-commit"] || item.metadata?.annotations?.["hwlab.pikastech.local/source-commit"] || null;
return item.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/source-commit"] ||
item.spec?.template?.metadata?.annotations?.["hwlab.pikastech.local/source-commit"] ||
item.metadata?.labels?.["hwlab.pikastech.local/source-commit"] ||
item.metadata?.annotations?.["hwlab.pikastech.local/source-commit"] ||
null;
}
function readyWorkload(item) {
@@ -2014,6 +2077,7 @@ async function waitForArgoRefresh() {
}
if (Date.now() - startedAt >= timeoutMs) {
emit({ stage: "argo-refresh", status: "timeout", durationMs: Date.now() - startedAt, workloadCount: runtime.items.length, pending: pending.slice(0, 8) });
process.exitCode = 1;
return { observed: false, skipped: false };
}
await sleep(pollMs);
@@ -2035,6 +2099,7 @@ async function waitForWorkloads() {
}
if (Date.now() - startedAt >= timeoutMs) {
emit({ stage: "workload-ready", status: "timeout", durationMs: Date.now() - startedAt, workloadCount: runtime.items.length, blocked: blocked.slice(0, 8) });
process.exitCode = 1;
return;
}
await sleep(pollMs);
@@ -2046,6 +2111,7 @@ async function waitForWorkloads() {
await waitForWorkloads();
})().catch((error) => {
emit({ stage: "runtime-ready", status: "failed", reason: error.message, durationMs: 0 });
process.exitCode = 1;
});
NODE
`;
@@ -2076,11 +2142,12 @@ function runtimeReadyTask({ profile = "dev" } = {}) {
};
}
function imagePublishTaskSet() {
function imagePublishTaskSet(args = { lane: "g14" }) {
const serviceIds = serviceIdsForLane(args.lane);
return [
planArtifactsTask(),
...defaultServiceIds.map(perServiceBuildTask),
collectArtifactsTask()
planArtifactsTask({ serviceIds }),
...serviceIds.map(perServiceBuildTask),
collectArtifactsTask({ serviceIds })
];
}
@@ -2114,7 +2181,7 @@ function tektonPipeline(args = { lane: "g14" }) {
{ name: "runtime-path", type: "string", default: runtimePath },
{ name: "revision", type: "string", description: "Full git commit SHA; the only source truth for image tags." },
{ name: "registry-prefix", type: "string", default: defaultRegistryPrefix },
{ name: "services", type: "string", default: defaultServicesParam },
{ name: "services", type: "string", default: servicesParamForLane(args.lane) },
{ name: "base-image", type: "string", default: defaultDevBaseImage }
],
workspaces: [
@@ -2137,6 +2204,7 @@ function tektonPipeline(args = { lane: "g14" }) {
{ name: "catalog-path" },
{ name: "image-tag-mode" },
{ name: "registry-prefix" },
{ name: "services" },
{ name: "revision" }
],
workspaces: [{ name: "source" }, { name: "git-ssh" }],
@@ -2150,11 +2218,12 @@ function tektonPipeline(args = { lane: "g14" }) {
{ name: "catalog-path", value: "$(params.catalog-path)" },
{ name: "image-tag-mode", value: "$(params.image-tag-mode)" },
{ name: "registry-prefix", value: "$(params.registry-prefix)" },
{ name: "services", value: "$(params.services)" },
{ name: "revision", value: "$(params.revision)" }
]
},
...primitiveValidationTasks.map(primitiveValidationTask),
...imagePublishTaskSet(),
...imagePublishTaskSet(args),
{
name: "gitops-promote",
runAfter: ["collect-artifacts"],
@@ -2296,7 +2365,7 @@ function tektonPollerCronJob(args) {
{ name: "SOURCE_BRANCH", value: args.sourceBranch },
{ name: "GITOPS_BRANCH", value: args.gitopsBranch },
{ name: "REGISTRY_PREFIX", value: args.registryPrefix },
{ name: "SERVICES", value: defaultServicesParam },
{ name: "SERVICES", value: servicesParamForLane(args.lane) },
{ name: "BASE_IMAGE", value: defaultDevBaseImage }
],
volumeMounts: [{ name: "git-ssh", mountPath: "/workspace/git-ssh", readOnly: true }],
@@ -2474,12 +2543,12 @@ function argoApplication(args, profile = "dev") {
project,
source: { repoURL: args.sourceRepo, targetRevision: args.gitopsBranch, path: `deploy/gitops/g14/${runtimePath}` },
destination: { server: "https://kubernetes.default.svc", namespace },
syncPolicy: { automated: { prune: false, selfHeal: true }, syncOptions: ["CreateNamespace=true", "ApplyOutOfSyncOnly=true"] }
syncPolicy: { automated: { prune: profile === "v02", selfHeal: true }, syncOptions: ["CreateNamespace=true", "ApplyOutOfSyncOnly=true"] }
}
};
}
function g14FrpcManifest({ profile = "dev" } = {}) {
function g14FrpcManifest({ profile = "dev", source = null } = {}) {
const namespace = namespaceNameForProfile(profile);
const profileLabel = runtimeLabelForProfile(profile);
const deploymentName = profile === "v02" ? "hwlab-v02-frpc" : profile === "prod" ? "hwlab-g14-prod-frpc" : "hwlab-g14-frpc";
@@ -2494,6 +2563,8 @@ function g14FrpcManifest({ profile = "dev" } = {}) {
"hwlab.pikastech.local/gitops-target": gitopsTargetForProfile(profile),
"hwlab.pikastech.local/profile": profileLabel
};
const sourceCommitLabel = source?.full ? { "hwlab.pikastech.local/source-commit": source.full } : {};
const renderedLabels = { ...labels, ...sourceCommitLabel };
return {
apiVersion: "v1",
kind: "List",
@@ -2504,7 +2575,7 @@ function g14FrpcManifest({ profile = "dev" } = {}) {
metadata: {
name: configName,
namespace,
labels
labels: renderedLabels
},
data: {
"frpc.toml": `serverAddr = "74.48.78.17"
@@ -2533,13 +2604,13 @@ remotePort = ${edgeRemotePort}
metadata: {
name: deploymentName,
namespace,
labels
labels: renderedLabels
},
spec: {
replicas: 1,
selector: { matchLabels: { "app.kubernetes.io/name": deploymentName } },
template: {
metadata: { labels },
metadata: { labels: renderedLabels, annotations: sourceCommitLabel },
spec: {
containers: [{
name: "frpc",
@@ -2569,10 +2640,12 @@ function deepSeekProxyManifest({ profile = "dev", source, registryPrefix = defau
"app.kubernetes.io/part-of": "hwlab",
"hwlab.pikastech.local/environment": runtimeLabelForProfile(profile),
"hwlab.pikastech.local/gitops-target": gitopsTargetForProfile(profile),
"hwlab.pikastech.local/service-id": "hwlab-deepseek-proxy"
"hwlab.pikastech.local/service-id": "hwlab-deepseek-proxy",
"hwlab.pikastech.local/source-commit": source.full
};
const templateAnnotations = {
"hwlab.pikastech.local/bridge-service-id": bridgeServiceId,
"hwlab.pikastech.local/source-commit": source.full,
"hwlab.pikastech.local/bridge-source-commit": bridgeCommit,
"hwlab.pikastech.local/bridge-image-tag": bridgeImageTag,
"hwlab.pikastech.local/bridge-image": bridgeImage
@@ -2940,7 +3013,7 @@ function deviceAgent71FreqManifest({ profile = "dev", source, registryPrefix, ca
};
}
function v02PostgresManifest({ migrationSql }) {
function v02PostgresManifest({ migrationSql, source }) {
const namespace = "hwlab-v02";
const name = "hwlab-v02-postgres";
const labels = {
@@ -2948,8 +3021,10 @@ function v02PostgresManifest({ migrationSql }) {
"app.kubernetes.io/part-of": "hwlab",
"hwlab.pikastech.local/environment": "v02",
"hwlab.pikastech.local/gitops-target": "v02",
"hwlab.pikastech.local/profile": "v02"
"hwlab.pikastech.local/profile": "v02",
"hwlab.pikastech.local/source-commit": source.full
};
const templateAnnotations = { "hwlab.pikastech.local/source-commit": source.full };
return {
apiVersion: "v1",
kind: "List",
@@ -2975,7 +3050,7 @@ function v02PostgresManifest({ migrationSql }) {
replicas: 1,
selector: { matchLabels: { "app.kubernetes.io/name": name } },
template: {
metadata: { labels },
metadata: { labels, annotations: templateAnnotations },
spec: {
containers: [{
name: "postgres",
@@ -3129,13 +3204,13 @@ async function plannedFiles(args) {
putJson(`${runtimePath}/kustomization.yaml`, runtimeKustomization({ profile, includeDeviceAgent }));
putJson(`${runtimePath}/namespace.yaml`, runtimeNamespace);
putJson(`${runtimePath}/code-agent-codex-config.yaml`, transformListNamespace(config, namespace, profileLabels, annotations));
putJson(`${runtimePath}/services.yaml`, transformListNamespace(services, namespace, profileLabels, annotations));
putJson(`${runtimePath}/services.yaml`, transformServices({ services, namespace, labels: profileLabels, annotations, profile }));
putJson(`${runtimePath}/health-contract.yaml`, transformHealthContract(health, namespace, profileLabels, annotations, endpoints.runtimeEndpoint, endpoints.webEndpoint, profile));
putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, catalog: artifactCatalog, source, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile, useDeployImages: args.useDeployImages }));
putJson(`${runtimePath}/deepseek-proxy.yaml`, deepSeekProxyManifest({ profile, source, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages }));
if (profile === "v02") putJson(`${runtimePath}/postgres.yaml`, v02PostgresManifest({ migrationSql }));
if (profile === "v02") putJson(`${runtimePath}/postgres.yaml`, v02PostgresManifest({ migrationSql, source }));
if (includeDeviceAgent) putJson(`${runtimePath}/device-agent-71-freq.yaml`, deviceAgent71FreqManifest({ profile, source, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages }));
putJson(`${runtimePath}/g14-frpc.yaml`, g14FrpcManifest({ profile }));
putJson(`${runtimePath}/g14-frpc.yaml`, g14FrpcManifest({ profile, source }));
}
return { files, source };
}
+31
View File
@@ -24,6 +24,37 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
assert.match(pipeline, /127\.0\.0\.1:5000\/hwlab\/hwlab-ci-node-tools:node22-alpine-bun-v1/u);
assert.match(pipeline, /node scripts\/run-bun\.mjs test cmd\/hwlab-codex-api-responses-forwarder\/main\.test\.ts/u);
assert.doesNotMatch(pipeline, /cmd\/hwlab-codex-api-responses-forwarder\/main\.mjs/u);
assert.match(pipeline, /process\.exitCode = 1/u);
assert.match(pipeline, /item\.spec\?\.template\?\.metadata\?\.labels\?\.\[\\"hwlab\.pikastech\.local\/source-commit\\"\]/u);
const removedServiceIds = [
"hwlab-router",
"hwlab-tunnel-client",
"hwlab-gateway-simu",
"hwlab-box-simu",
"hwlab-patch-panel"
];
for (const serviceId of removedServiceIds) {
assert.doesNotMatch(pipeline, new RegExp(`build-${serviceId}`, "u"));
}
const workloads = await readFile(path.join(outDir, "runtime-v02", "workloads.yaml"), "utf8");
const services = await readFile(path.join(outDir, "runtime-v02", "services.yaml"), "utf8");
for (const serviceId of removedServiceIds) {
assert.doesNotMatch(workloads, new RegExp(serviceId, "u"));
assert.doesNotMatch(services, new RegExp(serviceId, "u"));
}
assert.doesNotMatch(pipeline, /hwlab-gateway-simu-status/u);
assert.match(pipeline, /hwlab-cloud-api-status/u);
const application = await readFile(path.join(outDir, "argocd", "application-v02.yaml"), "utf8");
assert.match(application, /"prune": true/u);
for (const generatedFile of ["deepseek-proxy.yaml", "postgres.yaml", "g14-frpc.yaml"]) {
const content = await readFile(path.join(outDir, "runtime-v02", generatedFile), "utf8");
assert.match(content, /"hwlab\.pikastech\.local\/source-commit"/u);
}
const reconciler = await readFile(path.join(outDir, "tekton-v02", "control-plane-reconciler.yaml"), "utf8");
assert.match(reconciler, /cross-namespace-rbac-bootstrap-managed/u);
+45 -12
View File
@@ -25,6 +25,22 @@ const commitPattern = /^[a-f0-9]{7,40}$/;
const publishReadyStatuses = new Set(["published", "reused"]);
const v02RuntimeServiceIds = Object.freeze([
"hwlab-cloud-api",
"hwlab-cloud-web",
"hwlab-agent-mgr",
"hwlab-agent-worker",
"hwlab-device-pod",
"hwlab-gateway",
"hwlab-edge-proxy",
"hwlab-cli",
"hwlab-agent-skills"
]);
function serviceIdsForLane(lane) {
return lane === "v02" ? v02RuntimeServiceIds : SERVICE_IDS;
}
function parseArgs(argv) {
const args = {
lane: defaultLane,
@@ -186,7 +202,7 @@ function artifactCatalogSkeleton({ args, target, registryPrefix = defaultRegistr
},
allowedProfiles: [environment],
forbiddenProfiles: args.lane === "v02" ? ["dev", "prod"] : ["prod"],
services: SERVICE_IDS.map((serviceId) => ({
services: serviceIdsForLane(args.lane).map((serviceId) => ({
serviceId,
commitId: target.imageTag,
sourceCommitId: target.commitId,
@@ -253,7 +269,16 @@ function assertV02DeployAndCatalog(deploy, catalog) {
assert.equal(catalog.endpoint, "http://74.48.78.17:19667", "catalog endpoint must be the v02 API endpoint");
assert.deepEqual(catalog.allowedProfiles, ["v02"], "catalog allowedProfiles must only allow v02");
assert.deepEqual(catalog.forbiddenProfiles, ["dev", "prod"], "catalog forbiddenProfiles must forbid dev/prod");
assert.deepEqual((catalog.services ?? []).map((service) => service.serviceId), SERVICE_IDS, "catalog services must cover frozen service IDs in order");
assert.deepEqual((catalog.services ?? []).map((service) => service.serviceId), serviceIdsForLane("v02"), "catalog services must cover retained v0.2 service IDs in order");
}
function normalizeCatalogForLane(catalog, lane) {
if (!catalog || lane !== "v02") return catalog;
const allowed = new Set(serviceIdsForLane(lane));
return {
...catalog,
services: (catalog.services ?? []).filter((service) => allowed.has(service.serviceId))
};
}
function assertLaneDeployAndCatalog(args, deploy, catalog) {
@@ -261,12 +286,12 @@ function assertLaneDeployAndCatalog(args, deploy, catalog) {
return assertDevOnlyDeployAndCatalog(deploy, catalog);
}
function publishRecordsFromReport(report, target) {
function publishRecordsFromReport(report, target, serviceIds = SERVICE_IDS) {
const artifactPublish = report.artifactPublish;
assert.ok(artifactPublish && typeof artifactPublish === "object", "publish report missing artifactPublish");
assert.equal(artifactPublish.status, "published", "publish report artifactPublish.status must be published");
assert.ok(commitMatchesTarget(artifactPublish.sourceCommitId, target), "publish report sourceCommitId must match target ref");
assert.equal(artifactPublish.serviceCount, SERVICE_IDS.length, "publish report must cover every frozen service");
assert.equal(artifactPublish.serviceCount, serviceIds.length, "publish report must cover every service for the selected lane");
assert.ok(artifactPublish.publishPlan && typeof artifactPublish.publishPlan === "object", "publish report missing artifactPublish.publishPlan");
assert.equal(artifactPublish.publishPlan.version, "v2", "publish report plan version must be v2");
@@ -278,8 +303,8 @@ function publishRecordsFromReport(report, target) {
.map((service) => service.serviceId);
assert.deepEqual(
[...requiredServiceIds, ...disabledServiceIds].sort(),
[...SERVICE_IDS].sort(),
"publish plan must cover every frozen service"
[...serviceIds].sort(),
"publish plan must cover every service for the selected lane"
);
assert.equal(
(artifactPublish.publishedCount ?? 0) + (artifactPublish.reusedCount ?? 0),
@@ -288,8 +313,9 @@ function publishRecordsFromReport(report, target) {
);
const records = new Map();
const allowed = new Set(serviceIds);
for (const service of artifactPublish.services ?? []) {
assert.ok(SERVICE_IDS.includes(service.serviceId), `unknown service ${service.serviceId} in publish report`);
assert.ok(allowed.has(service.serviceId), `unknown service ${service.serviceId} in publish report`);
const image = parseTaggedImage(service.image, service.serviceId);
if (!requiredServiceIds.includes(service.serviceId)) {
assert.equal(service.artifactRequired, false, `${service.serviceId} disabled service must not be required`);
@@ -332,7 +358,7 @@ function publishRecordsFromReport(report, target) {
return { records, requiredServiceIds, disabledServiceIds };
}
function refreshDocuments({ deploy, catalog, target, publishRecords, serviceInventory, provenancePath, registryPrefix }) {
function refreshDocuments({ deploy, catalog, target, publishRecords, serviceInventory, provenancePath, registryPrefix, serviceIds = SERVICE_IDS }) {
const published = Boolean(publishRecords);
const records = publishRecords?.records ?? null;
const requiredIds = new Set(publishRecords?.requiredServiceIds ?? serviceInventory.requiredServiceIds);
@@ -340,6 +366,7 @@ function refreshDocuments({ deploy, catalog, target, publishRecords, serviceInve
const deployByService = new Map(deploy.services.map((service) => [service.serviceId, service]));
const inventoryByService = new Map(serviceInventory.services.map((service) => [service.serviceId, service]));
const refreshedServices = [];
const nextCatalogServices = [];
catalog.commitId = target.imageTag;
catalog.artifactState = published ? "published" : "contract-skeleton";
@@ -354,7 +381,7 @@ function refreshDocuments({ deploy, catalog, target, publishRecords, serviceInve
: "Artifact identity was refreshed to this source commit, but no publish report proved registry digests."
};
for (const serviceId of SERVICE_IDS) {
for (const serviceId of serviceIds) {
const deployService = deployByService.get(serviceId);
const catalogService = catalogByService.get(serviceId);
assert.ok(deployService, `${serviceId} missing from deploy manifest`);
@@ -406,8 +433,11 @@ function refreshDocuments({ deploy, catalog, target, publishRecords, serviceInve
artifactRequired: catalogService.artifactRequired,
notPublishedReason: catalogService.notPublishedReason
});
nextCatalogServices.push(catalogService);
}
catalog.services = nextCatalogServices;
return refreshedServices;
}
@@ -431,12 +461,14 @@ async function main() {
]);
let catalog = await readJsonIfPresent(args.catalogPath, null);
if (!catalog && args.lane === "v02") catalog = artifactCatalogSkeleton({ args, target, registryPrefix: publishReport?.artifactPublish?.registryPrefix ?? defaultRegistryPrefix });
catalog = normalizeCatalogForLane(catalog, args.lane);
assert.ok(catalog, `${args.catalogPath} is required for ${args.lane} catalog refresh`);
assertLaneDeployAndCatalog(args, deploy, catalog);
const publishRecords = publishReport ? publishRecordsFromReport(publishReport, target) : null;
const serviceIds = serviceIdsForLane(args.lane);
const publishRecords = publishReport ? publishRecordsFromReport(publishReport, target, serviceIds) : null;
const serviceInventory = publishReport?.artifactPublish?.serviceInventory ?? serviceInventoryFromServices(
await resolveDevArtifactServices(repoRoot, SERVICE_IDS, SERVICE_IDS)
await resolveDevArtifactServices(repoRoot, serviceIds, SERVICE_IDS)
);
const services = refreshDocuments({
deploy,
@@ -445,7 +477,8 @@ async function main() {
publishRecords,
serviceInventory,
provenancePath: publishReportProvenance(publishReport, args.publishReportPath ?? defaultPublishReportPath),
registryPrefix: publishReport?.artifactPublish?.registryPrefix ?? defaultRegistryPrefix
registryPrefix: publishReport?.artifactPublish?.registryPrefix ?? defaultRegistryPrefix,
serviceIds
});
if (args.write) {