fix: harden v02 bun runtime rollout
This commit is contained in:
@@ -159,6 +159,7 @@ CI/CD 内部由 branch poller、PipelineRun、component planner、BuildKit publi
|
||||
- `v0.2-gitops` 中存在 `deploy/artifact-catalog.v02.json` 与 `deploy/gitops/g14/runtime-v02/**`。
|
||||
- `argocd/hwlab-g14-v02` 指向 `v0.2-gitops:deploy/gitops/g14/runtime-v02`,sync revision 与目标 GitOps revision 对齐。
|
||||
- `hwlab-v02` 中长驻 workload ready,没有把 DEV/PROD namespace 当成 `v0.2` 通过证据。
|
||||
- `runtime-ready` 必须以 workload Pod template 的 source commit 判断 Argo 刷新,并在 Argo 刷新超时或 workload 未就绪时失败;不得把 CrashLoop、未刷新或公网不可用的 runtime 标成绿色。
|
||||
- `http://74.48.78.17:19666/` 返回 `v0.2` Cloud Web。
|
||||
- `http://74.48.78.17:19667/health/live` 返回 `v0.2` runtime health,payload 中的 namespace、revision 或 runtime identity 能与 `hwlab-v02`/`v0.2` 对齐。
|
||||
|
||||
@@ -188,6 +189,8 @@ GitOps branch 已更新、source branch render 通过、PipelineRun 名称存在
|
||||
| --- | --- | --- |
|
||||
| v02 独立 source/GitOps/runtime lane | 已实现 | `v0.2`、`v0.2-gitops`、`hwlab-v02` 和 `runtime-v02` 已固定。 |
|
||||
| Tekton poller/pipeline/promotion | 已实现 | 通过 `hwlab-v02-*` 对象和 GitOps promotion 管理。 |
|
||||
| v02 runtime readiness fail-closed | 已实现 | `runtime-ready` 读取 workload Pod template source commit,timeout 或未就绪会让 PipelineRun 失败。 |
|
||||
| v02 裁撤服务不进入发布面 | 已实现 | v02 Tekton build service set、artifact catalog 和 runtime render 只包含保留服务;v02 Argo 开启 prune 清理旧 live 对象;裁撤服务仍可保留在 DEV legacy 源码中。 |
|
||||
| Argo v02 Application | 已实现 | `hwlab-g14-v02` 指向 v02 GitOps path 和 namespace。 |
|
||||
| FRP `19666/19667` 入口 | 已实现 | 由 `hwlab-v02-frpc` 与 master frps allowlist 共同提供。 |
|
||||
| SecretRef 独立与 provider 验收 | 已实现/持续约束 | SecretRef 已独立;验收必须做真实短连接聊天。 |
|
||||
|
||||
@@ -183,7 +183,7 @@ hwlab-patch-panel
|
||||
|
||||
## T2
|
||||
|
||||
阅读 docs/reference/spec-v02-services.md,然后用 cli 手动测试以下内容:从 `deploy/gitops/g14/runtime-v02` 读取 Deployment、StatefulSet、Service 和 Job 模板,确认每个保留服务和稳定外部服务都有对应 spec;裁撤对象即使 runtime 暂存,也不能作为新开发权威入口。
|
||||
阅读 docs/reference/spec-v02-services.md,然后用 cli 手动测试以下内容:从 `deploy/gitops/g14/runtime-v02` 读取 Deployment、StatefulSet、Service 和 Job 模板,确认每个保留服务和稳定外部服务都有对应 spec;确认 `hwlab-router`、`hwlab-tunnel-client`、`hwlab-gateway-simu`、`hwlab-box-simu` 和 `hwlab-patch-panel` 不再出现在 v0.2 runtime desired state 或 v0.2 artifact catalog 中。
|
||||
|
||||
## T3
|
||||
|
||||
@@ -200,7 +200,7 @@ hwlab-patch-panel
|
||||
| v0.2 总体依赖方向 | 已实现 | 本文定义浏览器、API、device、provider 和 CI/CD 链路。 |
|
||||
| 保留服务均有 spec | 已实现 | 本文服务总表列出当前保留服务和 spec 文件。 |
|
||||
| 稳定外部服务纳入 spec | 已实现 | Postgres、Codex API forwarder/hyueapi、DeepSeek/Moon Bridge、FRP 已独立成文或交叉引用权威规格。 |
|
||||
| `simu`、接线盘、router、tunnel-client 裁撤 | 已实现 | 本文记录裁撤口径,不建立对应单服务 spec。 |
|
||||
| `simu`、接线盘、router、tunnel-client 裁撤 | 已实现 | 本文记录裁撤口径;v0.2 render、artifact catalog、Tekton build service set 和 cloud-api 运行时 env 不再包含裁撤对象。 |
|
||||
| Bun + TypeScript 迁移边界 | 已实现 | 本文区分第一阶段、后续、暂不迁移和裁撤集合。 |
|
||||
| spec 作为开发和测试权威 | 已实现 | AGENTS.md 规格区提供顶级索引。 |
|
||||
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user