refactor: make v0.3 runtime node-scoped

This commit is contained in:
Codex Agent
2026-06-08 22:19:30 +08:00
parent e8ab452b10
commit 187fb80a5d
38 changed files with 549 additions and 450 deletions
+29 -29
View File
@@ -14,7 +14,7 @@ import { fileURLToPath } from "node:url";
import { activeReportLifecycle } from "../internal/dev-report-lifecycle.mjs";
import { ENVIRONMENT_DEV, SERVICE_IDS } from "../internal/protocol/index.mjs";
import { createG14CiPlan } from "./src/g14-ci-plan-lib.mjs";
import { createCiPlan } from "./src/ci-plan-lib.mjs";
import {
BUILDABLE_IMPLEMENTATION_STATES,
resolveDevArtifactServices,
@@ -26,7 +26,7 @@ import { readStructuredFile } from "./src/structured-config.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const cliEntrypoint = process.env.HWLAB_ARTIFACT_PUBLISH_ENTRYPOINT || "scripts/artifact-publish.mjs";
const defaultLane = process.env.HWLAB_ARTIFACT_LANE || process.env.HWLAB_GITOPS_LANE || "g14";
const defaultLane = process.env.HWLAB_ARTIFACT_LANE || process.env.HWLAB_GITOPS_LANE || "node";
const defaultRegistryPrefix =
process.env.HWLAB_DEV_REGISTRY_PREFIX || process.env.HWLAB_DEV_REGISTRY || "127.0.0.1:5000/hwlab";
const defaultBaseImage = process.env.HWLAB_DEV_BASE_IMAGE || null;
@@ -121,7 +121,7 @@ function parseArgs(argv) {
else throw new Error(`unknown argument ${arg}`);
}
assert.ok(args.lane === "g14" || isRuntimeArtifactLane(args.lane), `unknown artifact lane ${args.lane}`);
assert.ok(args.lane === "node" || isRuntimeArtifactLane(args.lane), `unknown artifact lane ${args.lane}`);
assert.ok(["short", "full"].includes(args.imageTagMode), `unknown image tag mode ${args.imageTagMode}`);
return args;
@@ -146,12 +146,12 @@ function artifactIdentityValue(env, name) {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function g14ArtifactIdentityName(env = process.env) {
function artifactIdentityName(env = process.env) {
return ciArtifactIdentityEnvNames.find((name) => artifactIdentityValue(env, name));
}
function g14ArtifactProducer(env = process.env) {
const idName = g14ArtifactIdentityName(env);
function artifactProducer(env = process.env) {
const idName = artifactIdentityName(env);
if (!idName) {
return {
kind: "missing",
@@ -160,24 +160,24 @@ function g14ArtifactProducer(env = process.env) {
};
}
return {
kind: "g14-ci-artifact",
kind: "node-ci-artifact",
runId: artifactIdentityValue(env, idName),
owner: artifactIdentityValue(env, "HWLAB_CI_ARTIFACT_RUN_OWNER") ?? idName
};
}
function g14ArtifactPublishIdentityGuardFailure({ script, mode }) {
function artifactPublishIdentityGuardFailure({ script, mode }) {
return {
ok: false,
status: "blocked",
error: "g14-artifact-publish-identity-required",
code: "g14-artifact-publish-identity-required",
error: "artifact-publish-identity-required",
code: "artifact-publish-identity-required",
script,
mode,
requiredEnv: "HWLAB_CI_ARTIFACT_RUN_ID or standard CI run identity",
acceptedEnv: ciArtifactIdentityEnvNames,
entrypoint: `HWLAB_CI_ARTIFACT_RUN_ID=<run-id> HWLAB_CI_ARTIFACT_RUN_OWNER=<owner> node scripts/g14-artifact-publish.mjs --publish --report ${tempReportPath("dev-artifacts.json")}`,
summary: `${script} ${mode} is a G14 artifact publish side-effect step and must run under CI artifact identity. Legacy D601 CD transaction identity is not accepted.`,
entrypoint: `HWLAB_CI_ARTIFACT_RUN_ID=<run-id> HWLAB_CI_ARTIFACT_RUN_OWNER=<owner> node scripts/artifact-publish.mjs --publish --report ${tempReportPath("dev-artifacts.json")}`,
summary: `${script} ${mode} is a node artifact publish side-effect step and must run under CI artifact identity. Legacy D601 CD transaction identity is not accepted.`,
devOnly: true,
prodTouched: false,
cdLockRequired: false,
@@ -186,8 +186,8 @@ function g14ArtifactPublishIdentityGuardFailure({ script, mode }) {
};
}
function requireG14ArtifactPublishIdentity({ env = process.env, script, mode }) {
return g14ArtifactIdentityName(env) ? null : g14ArtifactPublishIdentityGuardFailure({ script, mode });
function requireArtifactPublishIdentity({ env = process.env, script, mode }) {
return artifactIdentityName(env) ? null : artifactPublishIdentityGuardFailure({ script, mode });
}
function printHelp() {
@@ -197,7 +197,7 @@ function printHelp() {
"Commit-pinned artifact build/publish helper for controlled CI execution.",
"",
"options:",
" --lane LANE g14 or runtime lane vNN; default: g14",
" --lane LANE node or runtime lane vNN; default: node",
` --catalog-path PATH default: ${defaultCatalogPath}`,
` --deploy-config PATH default: ${defaultDeployPath}`,
" --image-tag-mode MODE short or full; v02 uses full source commit IDs",
@@ -235,11 +235,11 @@ function emit(event, payload = {}) {
}
function ciTimingEnabled(env = process.env) {
return env.HWLAB_G14_CICD_TIMING === "1" || env.HWLAB_CI_TIMING === "1";
return env.HWLAB_NODE_CICD_TIMING === "1" || env.HWLAB_CI_TIMING === "1";
}
function emitCiTiming(payload) {
emit("g14-cicd-timing", {
emit("node-cicd-timing", {
schemaVersion: "v1",
source: "scripts/artifact-publish.mjs",
pipelineRun: process.env.HWLAB_TEKTON_PIPELINERUN ?? null,
@@ -305,7 +305,7 @@ function parseDockerfileStepTimings(text) {
} catch {
continue;
}
if (parsed?.event !== "g14-cicd-build-step-timing") continue;
if (parsed?.event !== "node-cicd-build-step-timing") continue;
timings.push({
stage: parsed.stage,
status: parsed.status ?? "unknown",
@@ -369,7 +369,7 @@ function buildkitBaseImagePreflight(baseImage) {
blockers: [],
nextSteps: [],
recommendation: {
source: "G14 k8s registry image used by rootless BuildKit.",
source: "node k8s registry image used by rootless BuildKit.",
envVar: "HWLAB_DEV_BASE_IMAGE",
recommendedEnvValue: requestedImage
},
@@ -638,7 +638,7 @@ function artifactCatalogSkeleton({ args, deployManifest, commitId }) {
ciPublished: false,
registryVerified: false,
provenance: "not_available_until_publish",
note: `${artifactEnvironment(args)} artifact catalog skeleton initialized without DEV/G14 fallback.`
note: `${artifactEnvironment(args)} artifact catalog skeleton initialized without DEV/node fallback.`
},
allowedProfiles: [artifactEnvironment(args)],
forbiddenProfiles: isRuntimeArtifactLane(args.lane) ? ["dev", "prod"] : ["prod"],
@@ -808,7 +808,7 @@ async function resolveServices(serviceIds, catalog) {
}
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 bun_pkg=""; case "$(uname -m)" in x86_64|amd64) bun_pkg="@oven/bun-linux-x64@1.3.13" ;; aarch64|arm64) bun_pkg="@oven/bun-linux-aarch64@1.3.13" ;; *) echo "unsupported bun-command architecture: $(uname -m)" >&2; false ;; esac && npm install --no-save --omit=dev --include=optional --ignore-scripts "$bun_pkg" && test -x "node_modules/\${bun_pkg%@*}/bin/bun" && ln -sf "/app/node_modules/\${bun_pkg%@*}/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`;
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 bun_pkg=""; case "$(uname -m)" in x86_64|amd64) bun_pkg="@oven/bun-linux-x64@1.3.13" ;; aarch64|arm64) bun_pkg="@oven/bun-linux-aarch64@1.3.13" ;; *) echo "unsupported bun-command architecture: $(uname -m)" >&2; false ;; esac && npm install --no-save --omit=dev --include=optional --ignore-scripts "$bun_pkg" && test -x "node_modules/\${bun_pkg%@*}/bin/bun" && ln -sf "/app/node_modules/\${bun_pkg%@*}/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:'node-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 runtimeReadinessScript = `set -eu; fail() { echo "hwlab-artifact-runtime-check-failed:$1" >&2; exit 1; }; 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 || fail bun-missing; /usr/local/bin/bun --version >/tmp/hwlab-bun-version.txt || fail bun-version; fi; chmod -R a+rwX /app /workspace /codex-home || fail chmod; test -x /app/node_modules/.bin/codex || fail codex-bin-missing; /app/node_modules/.bin/codex --version >/tmp/hwlab-codex-version.txt || fail codex-version; 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 || fail codex-native-missing`;
return [
`FROM ${baseImage}`,
@@ -1032,7 +1032,7 @@ async function preflight({ args, services, catalog, deployManifest, baseImagePre
type: "safety_blocker",
scope: "registry",
summary: error.message,
next: "Use the G14 local/internal registry prefix, for example 127.0.0.1:5000/hwlab."
next: "Use the node local/internal registry prefix, for example 127.0.0.1:5000/hwlab."
})
);
}
@@ -1072,7 +1072,7 @@ async function preflight({ args, services, catalog, deployManifest, baseImagePre
type: "environment_blocker",
scope: "buildkit-command",
summary: `BuildKit command ${args.buildkitCommand} is not available in the publish task image.`,
next: "Use the G14 Tekton BuildKit path that prepares buildctl/buildkitd before running --publish."
next: "Use the node Tekton BuildKit path that prepares buildctl/buildkitd before running --publish."
})
);
}
@@ -2038,14 +2038,14 @@ function createReport({ args, repo, commitId, shortCommit, mode, services, artif
documents: [
"docs/reference/MVP-e2e-acceptance.md",
"docs/reference/spec-v02-documentation-governance.md",
"docs/reference/g14-gitops-cicd.md",
"docs/reference/node-gitops-cicd.md",
"docs/reference/spec-v02-cicd.md"
],
summary: `${environment} artifact publish evidence; runtime acceptance stays separate from artifact publication.`
},
validationCommands: [
"node --check scripts/artifact-publish.mjs",
"node --check scripts/g14-artifact-publish.mjs",
"node --check scripts/artifact-publish.mjs",
"node --check scripts/src/dev-artifact-services.mjs",
"node --check scripts/src/registry-capabilities.mjs",
"node --check scripts/preflight-dev-base-image.mjs",
@@ -2053,7 +2053,7 @@ function createReport({ args, repo, commitId, shortCommit, mode, services, artif
"node --check scripts/dev-runtime-base-image.mjs",
"node --test scripts/refresh-artifact-catalog.test.mjs",
"node scripts/preflight-dev-base-image.mjs",
"node scripts/g14-artifact-publish.mjs --preflight --no-report",
"node scripts/artifact-publish.mjs --preflight --no-report",
"node --check scripts/validate-dev-gate-report.mjs",
"node scripts/validate-dev-gate-report.mjs"
],
@@ -2197,7 +2197,7 @@ async function main() {
return;
}
if (args.mode === "publish") {
const identityGuard = requireG14ArtifactPublishIdentity({
const identityGuard = requireArtifactPublishIdentity({
script: cliEntrypoint,
mode: "--publish"
});
@@ -2237,8 +2237,8 @@ async function main() {
}
const buildCreatedAt = new Date().toISOString();
const repo = repoLabelFromRemote(remoteUrl);
const producer = g14ArtifactProducer(process.env);
const ciPlan = await createG14CiPlan({
const producer = artifactProducer(process.env);
const ciPlan = await createCiPlan({
repoRoot,
lane: args.lane,
targetRef: "HEAD",
@@ -1,5 +1,5 @@
#!/usr/bin/env node
import { createG14CiPlan } from "./src/g14-ci-plan-lib.mjs";
import { createCiPlan } from "./src/ci-plan-lib.mjs";
const args = parseArgs(process.argv.slice(2));
@@ -8,7 +8,7 @@ if (args.help) {
process.exit(0);
}
const plan = await createG14CiPlan(args);
const plan = await createCiPlan(args);
process.stdout.write(args.pretty ? `${JSON.stringify(plan, null, 2)}\n` : `${JSON.stringify(plan)}\n`);
function parseArgs(argv) {
@@ -38,14 +38,14 @@ function readOption(argv, index, name) {
function usage() {
return [
"usage: node scripts/g14-ci-plan.mjs [--lane v02|v03] [--base-ref REF] [--target-ref REF] [--pretty]",
"usage: node scripts/ci-plan.mjs [--lane v02|v03] [--base-ref REF] [--target-ref REF] [--pretty]",
"",
"Plan G14 monorepo image builds without mutating CI/CD state.",
"Plan node monorepo image builds without mutating CI/CD state.",
"For runtime lane env reuse, service component boundaries and env recipe must come from deploy/deploy.yaml.",
"",
"examples:",
" node scripts/g14-ci-plan.mjs --lane v02 --base-ref HEAD~1 --target-ref HEAD --pretty",
" node scripts/g14-ci-plan.mjs --lane v03 --base-ref HEAD~1 --target-ref HEAD --pretty",
" node scripts/g14-ci-plan.mjs --services hwlab-cloud-api,hwlab-cloud-web --pretty"
" node scripts/ci-plan.mjs --lane v02 --base-ref HEAD~1 --target-ref HEAD --pretty",
" node scripts/ci-plan.mjs --lane v03 --base-ref HEAD~1 --target-ref HEAD --pretty",
" node scripts/ci-plan.mjs --services hwlab-cloud-api,hwlab-cloud-web --pretty"
].join("\n");
}
@@ -9,11 +9,11 @@ import { promisify } from "node:util";
import {
classifyGlobalChange,
componentModelsForServices,
createG14CiPlan,
createCiPlan,
envReuseRecipeForLane,
matchingPaths,
packageRuntimeFieldsChanged
} from "./src/g14-ci-plan-lib.mjs";
} from "./src/ci-plan-lib.mjs";
import { readStructuredFile, writeStructuredFile } from "./src/structured-config.mjs";
const execFileAsync = promisify(execFile);
@@ -26,7 +26,7 @@ const V02_RUNTIME_SERVICE_IDS = Object.freeze([
"hwlab-agent-skills"
]);
test("G14 CI planner maps bridge changes to cloud-api only", async () => {
test("node CI planner maps bridge changes to cloud-api only", async () => {
const repo = await createFixtureRepo();
await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.ts"), "console.log('bridge v2');\n");
await git(repo, ["add", "."]);
@@ -40,10 +40,10 @@ test("G14 CI planner maps bridge changes to cloud-api only", async () => {
assert.deepEqual(cloudApi.reason, ["code-input-changed", "component-path-changed"]);
});
test("G14 CI planner treats docs-only changes as no image build", async () => {
test("node CI planner treats docs-only changes as no image build", async () => {
const repo = await createFixtureRepo();
await mkdir(path.join(repo, "docs/reference"), { recursive: true });
await writeFile(path.join(repo, "docs/reference/g14-gitops-cicd.md"), "docs v2\n");
await writeFile(path.join(repo, "docs/reference/node-gitops-cicd.md"), "docs v2\n");
await git(repo, ["add", "."]);
await git(repo, ["commit", "-m", "change docs"]);
@@ -56,7 +56,7 @@ test("G14 CI planner treats docs-only changes as no image build", async () => {
test("planner rebuilds when catalog digest is missing instead of blocking reuse", async () => {
const repo = await createFixtureRepo({ catalog: "missing-digest" });
await mkdir(path.join(repo, "docs/reference"), { recursive: true });
await writeFile(path.join(repo, "docs/reference/g14-gitops-cicd.md"), "docs v2\n");
await writeFile(path.join(repo, "docs/reference/node-gitops-cicd.md"), "docs v2\n");
await git(repo, ["add", "."]);
await git(repo, ["commit", "-m", "change docs"]);
@@ -69,7 +69,7 @@ test("planner rebuilds when catalog digest is missing instead of blocking reuse"
test("v02 planner rolls cloud-api service code changes without rebuilding service image", async () => {
const repo = await createFixtureRepo();
const initialPlan = await createG14CiPlan({
const initialPlan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD",
@@ -91,7 +91,7 @@ test("v02 planner rolls cloud-api service code changes without rebuilding servic
await git(repo, ["add", "cmd/hwlab-cloud-api/main.ts"]);
await git(repo, ["commit", "-m", "change cloud-api service code"]);
const plan = await createG14CiPlan({
const plan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD~1",
@@ -112,11 +112,11 @@ test("v02 planner rolls cloud-api service code changes without rebuilding servic
test("v02 planner rebuilds env image when an env reuse catalog digest is missing", async () => {
const repo = await createFixtureRepo({ catalog: "missing-digest" });
await mkdir(path.join(repo, "docs/reference"), { recursive: true });
await writeFile(path.join(repo, "docs/reference/g14-gitops-cicd.md"), "docs only with missing env digest\n");
await git(repo, ["add", "docs/reference/g14-gitops-cicd.md"]);
await writeFile(path.join(repo, "docs/reference/node-gitops-cicd.md"), "docs only with missing env digest\n");
await git(repo, ["add", "docs/reference/node-gitops-cicd.md"]);
await git(repo, ["commit", "-m", "docs after missing env digest"]);
const plan = await createG14CiPlan({
const plan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD~1",
@@ -143,7 +143,7 @@ test("artifact reuse paths preserve catalog provenance instead of current planne
assert.match(artifactPublish, /envReuseRecipe: servicePlan\.envReuseRecipe \?\? null/u);
assert.match(artifactPublish, /componentInputHash: serviceResultValue\(env, service\.serviceId, "COMPONENT_INPUT_HASH"\) \?\? \(reusedServiceImage \? null : service\.componentInputHash \?\? null\)/u);
const gitopsRender = await readFile("scripts/g14-gitops-render.mjs", "utf8");
const gitopsRender = await readFile("scripts/gitops-render.mjs", "utf8");
assert.match(gitopsRender, /const componentInputHash = envReuse \? \(planned\.componentInputHash \|\| service\.componentInputHash \|\| ""\) : \(service\.componentInputHash \|\| ""\);/u);
assert.doesNotMatch(gitopsRender, /"component-input-hash": planned\.componentInputHash \|\| service\.componentInputHash \|\| ""/u);
});
@@ -178,7 +178,7 @@ test("v02 planner rejects missing service declarations instead of falling back t
await git(repo, ["commit", "-m", "remove v02 declarations"]);
await assert.rejects(
() => createG14CiPlan({
() => createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD~1",
@@ -200,7 +200,7 @@ test("v02 planner rejects missing boot script declarations instead of guessing p
await git(repo, ["commit", "-m", "remove v02 cloud-api boot script"]);
await assert.rejects(
() => createG14CiPlan({
() => createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD~1",
@@ -218,7 +218,7 @@ test("v02 planner keeps hwpod-spec changes out of runtime service env reuse", as
await git(repo, ["add", ".hwlab/hwpod-spec.yaml"]);
await git(repo, ["commit", "-m", "change preinstalled hwpod spec"]);
const plan = await createG14CiPlan({
const plan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD~1",
@@ -240,7 +240,7 @@ test("v02 planner treats AgentRun runtime prompt changes as cloud-api inputs", a
await git(repo, ["add", "."]);
await git(repo, ["commit", "-m", "seed agentrun prompt"]);
const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
const initialPlan = await createG14CiPlan({
const initialPlan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD",
@@ -261,7 +261,7 @@ test("v02 planner treats AgentRun runtime prompt changes as cloud-api inputs", a
await git(repo, ["add", "internal/agent/prompts/hwlab-v02-runtime.md"]);
await git(repo, ["commit", "-m", "change agentrun prompt"]);
const plan = await createG14CiPlan({
const plan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD~1",
@@ -295,7 +295,7 @@ test("planner scopes cloud-web runtime changes to cloud-web", async () => {
test("v02 planner treats shared trace renderer changes as cloud-web code rollout inputs", async () => {
const repo = await createFixtureRepo();
const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
const initialPlan = await createG14CiPlan({
const initialPlan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD",
@@ -317,7 +317,7 @@ test("v02 planner treats shared trace renderer changes as cloud-web code rollout
await git(repo, ["add", "tools/src/hwlab-cli/trace-renderer.ts"]);
await git(repo, ["commit", "-m", "change shared trace renderer"]);
const plan = await createG14CiPlan({
const plan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD~1",
@@ -399,14 +399,14 @@ test("planner treats dependency changes as runtime image inputs", async () => {
});
test("global change classifier distinguishes gitops-only and test-only", () => {
assert.equal(classifyGlobalChange(["deploy/gitops/g14/runtime-dev/workloads.yaml"]).gitopsOnly, true);
assert.equal(classifyGlobalChange(["deploy/gitops/node/runtime-dev/workloads.yaml"]).gitopsOnly, true);
assert.equal(classifyGlobalChange(["cmd/hwlab-cloud-api/runtime-options.test.ts"]).testOnly, true);
});
test("v02 planner marks cloud-web code-only change as env reuse rollout", async () => {
const repo = await createFixtureRepo();
const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
const initialPlan = await createG14CiPlan({
const initialPlan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD",
@@ -427,7 +427,7 @@ test("v02 planner marks cloud-web code-only change as env reuse rollout", async
await git(repo, ["add", "."]);
await git(repo, ["commit", "-m", "change cloud web code"]);
const plan = await createG14CiPlan({
const plan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD~1",
@@ -451,7 +451,7 @@ test("v02 planner marks cloud-web code-only change as env reuse rollout", async
test("v02 planner enables env reuse for all retained runtime services", async () => {
const repo = await createFixtureRepo({ serviceIds: V02_RUNTIME_SERVICE_IDS });
const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
const initialPlan = await createG14CiPlan({
const initialPlan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD",
@@ -480,7 +480,7 @@ test("v02 planner enables env reuse for all retained runtime services", async ()
await git(repo, ["add", "."]);
await git(repo, ["commit", "-m", "change all v02 service code"]);
const plan = await createG14CiPlan({
const plan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD~1",
@@ -506,7 +506,7 @@ test("v02 planner enables env reuse for all retained runtime services", async ()
test("v02 planner rejects removed HWLAB-owned agent worker service selections", async () => {
const repo = await createFixtureRepo();
await assert.rejects(
() => createG14CiPlan({
() => createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD",
@@ -514,14 +514,14 @@ test("v02 planner rejects removed HWLAB-owned agent worker service selections",
artifactCatalogPath: "deploy/artifact-catalog.v02.json",
services: ["hwlab-agent-worker"]
}),
/unknown service IDs for G14 CI plan: hwlab-agent-worker/u
/unknown service IDs for node CI plan: hwlab-agent-worker/u
);
});
test("v02 planner keeps hwlab-cli-only changes out of runtime service builds", async () => {
const repo = await createFixtureRepo();
const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
const initialPlan = await createG14CiPlan({
const initialPlan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD",
@@ -544,7 +544,7 @@ test("v02 planner keeps hwlab-cli-only changes out of runtime service builds", a
await git(repo, ["add", "."]);
await git(repo, ["commit", "-m", "change cli and host asset"]);
const plan = await createG14CiPlan({
const plan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD~1",
@@ -561,7 +561,7 @@ test("v02 planner keeps hwlab-cli-only changes out of runtime service builds", a
test("v02 planner scopes hwpod skill bundle changes to the skills service", async () => {
const repo = await createFixtureRepo();
const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
const initialPlan = await createG14CiPlan({
const initialPlan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD",
@@ -578,7 +578,7 @@ test("v02 planner scopes hwpod skill bundle changes to the skills service", asyn
await git(repo, ["add", "."]);
await git(repo, ["commit", "-m", "change hwpod runtime cli"]);
const plan = await createG14CiPlan({
const plan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD~1",
@@ -619,7 +619,7 @@ test("v02 planner reads env reuse declarations and recipe from deploy config", a
test("v03 planner reads env reuse declarations and catalog from v03 lane config", async () => {
const repo = await createFixtureRepo();
const plan = await createG14CiPlan({
const plan = await createCiPlan({
repoRoot: repo,
lane: "v03",
baseRef: "HEAD",
@@ -645,7 +645,7 @@ test("v03 planner reads env reuse declarations and catalog from v03 lane config"
async function createFixtureRepo(options = {}) {
const serviceIds = options.serviceIds ?? ["hwlab-cloud-api", "hwlab-cloud-web"];
const laneServiceIds = options.laneServiceIds ?? V02_RUNTIME_SERVICE_IDS;
const repo = await mkdtemp(path.join(os.tmpdir(), "hwlab-g14-ci-plan-"));
const repo = await mkdtemp(path.join(os.tmpdir(), "hwlab-ci-plan-"));
await mkdir(path.join(repo, "cmd/hwlab-cloud-api"), { recursive: true });
await mkdir(path.join(repo, "cmd/hwlab-codex-api-responses-forwarder"), { recursive: true });
await mkdir(path.join(repo, "cmd/hwlab-deepseek-responses-bridge"), { recursive: true });
@@ -705,7 +705,7 @@ async function createFixtureRepo(options = {}) {
await git(repo, ["commit", "-m", "initial"]);
if (catalogMode !== false) {
const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
const initialPlan = await createG14CiPlan({
const initialPlan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD",
@@ -726,7 +726,7 @@ async function createFixtureRepo(options = {}) {
async function refreshFixtureCatalogForHead(repo) {
const sourceCommitId = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
const services = ["hwlab-cloud-api", "hwlab-cloud-web"];
const plan = await createG14CiPlan({
const plan = await createCiPlan({
repoRoot: repo,
lane: "v02",
baseRef: "HEAD",
@@ -743,7 +743,7 @@ async function refreshFixtureCatalogForHead(repo) {
}
function planV02CoreServices(repo, options) {
return createG14CiPlan({
return createCiPlan({
repoRoot: repo,
lane: "v02",
artifactCatalogPath: "deploy/artifact-catalog.v02.json",
@@ -853,7 +853,6 @@ function createDeployFixture({ serviceIds = V02_RUNTIME_SERVICE_IDS } = {}) {
additionalEnvPaths: [
"internal/dev-entrypoint/artifact-runtime.mjs",
"scripts/artifact-publish.mjs",
"scripts/g14-artifact-publish.mjs",
"scripts/src/dev-artifact-services.mjs"
],
hwpodAliases: [
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env node
process.env.HWLAB_ARTIFACT_PUBLISH_ENTRYPOINT ||= "scripts/g14-artifact-publish.mjs";
process.env.HWLAB_ARTIFACT_PUBLISH_PROFILE ||= "g14";
await import("./artifact-publish.mjs");
@@ -20,29 +20,29 @@ import { readStructuredFile } from "./src/structured-config.mjs";
const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const defaultOutDir = "deploy/gitops/g14";
const defaultRegistryPrefix = process.env.HWLAB_G14_REGISTRY_PREFIX || "127.0.0.1:5000/hwlab";
const defaultSourceRepo = process.env.HWLAB_G14_GIT_URL || "git@github.com:pikasTech/HWLAB.git";
const defaultGitReadUrl = process.env.HWLAB_G14_GIT_READ_URL || null;
const defaultOutDir = "deploy/gitops/node";
const defaultRegistryPrefix = process.env.HWLAB_NODE_REGISTRY_PREFIX || "127.0.0.1:5000/hwlab";
const defaultSourceRepo = process.env.HWLAB_NODE_GIT_URL || "git@github.com:pikasTech/HWLAB.git";
const defaultGitReadUrl = process.env.HWLAB_NODE_GIT_READ_URL || null;
const defaultV02GitReadUrl = process.env.HWLAB_V02_GIT_READ_URL || "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git";
const defaultV02GitWriteUrl = process.env.HWLAB_V02_GIT_WRITE_URL || "http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/HWLAB.git";
const defaultRuntimeLaneGitReadUrl = process.env.HWLAB_RUNTIME_LANE_GIT_READ_URL || defaultV02GitReadUrl;
const defaultRuntimeLaneGitWriteUrl = process.env.HWLAB_RUNTIME_LANE_GIT_WRITE_URL || defaultV02GitWriteUrl;
const defaultBranch = process.env.HWLAB_G14_BRANCH || "G14";
const defaultGitopsBranch = process.env.HWLAB_G14_GITOPS_BRANCH || "G14-gitops";
const defaultBranch = process.env.HWLAB_NODE_BRANCH || "node";
const defaultGitopsBranch = process.env.HWLAB_NODE_GITOPS_BRANCH || "node-gitops";
const defaultCatalogPath = process.env.HWLAB_ARTIFACT_CATALOG_PATH || "deploy/artifact-catalog.dev.json";
const defaultRuntimeEndpoint = process.env.HWLAB_G14_RUNTIME_ENDPOINT || "http://74.48.78.17:17667";
const defaultWebEndpoint = process.env.HWLAB_G14_WEB_ENDPOINT || "http://74.48.78.17:17666";
const defaultProdRuntimeEndpoint = process.env.HWLAB_G14_PROD_RUNTIME_ENDPOINT || "http://74.48.78.17:18667";
const defaultProdWebEndpoint = process.env.HWLAB_G14_PROD_WEB_ENDPOINT || "http://74.48.78.17:18666";
const defaultRuntimeEndpoint = process.env.HWLAB_NODE_RUNTIME_ENDPOINT || "http://74.48.78.17:17667";
const defaultWebEndpoint = process.env.HWLAB_NODE_WEB_ENDPOINT || "http://74.48.78.17:17666";
const defaultProdRuntimeEndpoint = process.env.HWLAB_NODE_PROD_RUNTIME_ENDPOINT || "http://74.48.78.17:18667";
const defaultProdWebEndpoint = process.env.HWLAB_NODE_PROD_WEB_ENDPOINT || "http://74.48.78.17:18666";
const defaultV02RuntimeEndpoint = process.env.HWLAB_V02_RUNTIME_ENDPOINT || "https://hwlab.74-48-78-17.nip.io";
const defaultV02WebEndpoint = process.env.HWLAB_V02_WEB_ENDPOINT || "https://hwlab.74-48-78-17.nip.io";
const defaultProxyUrl = process.env.HWLAB_G14_PROXY_URL || "http://127.0.0.1:10808";
const defaultAllProxyUrl = process.env.HWLAB_G14_ALL_PROXY_URL || "socks5h://127.0.0.1:10808";
const defaultNoProxy = process.env.HWLAB_G14_NO_PROXY || "hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local";
const ciToolsRunnerImage = process.env.HWLAB_G14_CI_TOOLS_IMAGE || "127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1";
const buildkitRunnerImage = process.env.HWLAB_G14_BUILDKIT_IMAGE || "moby/buildkit:rootless";
const defaultDevBaseImage = process.env.HWLAB_G14_DEV_BASE_IMAGE || "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim";
const defaultProxyUrl = process.env.HWLAB_NODE_PROXY_URL || "http://127.0.0.1:10808";
const defaultAllProxyUrl = process.env.HWLAB_NODE_ALL_PROXY_URL || "socks5h://127.0.0.1:10808";
const defaultNoProxy = process.env.HWLAB_NODE_NO_PROXY || "hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local";
const ciToolsRunnerImage = process.env.HWLAB_NODE_CI_TOOLS_IMAGE || "127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1";
const buildkitRunnerImage = process.env.HWLAB_NODE_BUILDKIT_IMAGE || "moby/buildkit:rootless";
const defaultDevBaseImage = process.env.HWLAB_NODE_DEV_BASE_IMAGE || "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim";
const defaultServiceIds = Object.freeze([
"hwlab-cloud-api",
"hwlab-cloud-web",
@@ -86,13 +86,13 @@ const v02RemovedServiceEnvNames = new Set([
"HWLAB_ROUTER_URL",
"HWLAB_TUNNEL_CLIENT_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";
const codexApiProfileModel = process.env.HWLAB_G14_CODEX_API_PROFILE_MODEL || "gpt-5.5";
const codexApiProfileBaseUrl = process.env.HWLAB_G14_CODEX_API_PROFILE_BASE_URL || "http://127.0.0.1:49280/responses";
const codexApiForwarderPort = process.env.HWLAB_G14_CODEX_API_FORWARDER_PORT || "49280";
const codexApiForwarderUpstreamBaseUrl = process.env.HWLAB_G14_CODEX_API_UPSTREAM_BASE_URL || "https://hyueapi.com";
const moonBridgeSourceRef = process.env.HWLAB_NODE_MOONBRIDGE_REF || "1b99888d3dae889b79ee602cb875c7907f7e76f2";
const moonBridgeImage = process.env.HWLAB_NODE_MOONBRIDGE_IMAGE || `${defaultRegistryPrefix}/moonbridge:${moonBridgeSourceRef.slice(0, 12)}`;
const deepSeekProfileModel = process.env.HWLAB_NODE_DEEPSEEK_PROFILE_MODEL || "deepseek-chat";
const codexApiProfileModel = process.env.HWLAB_NODE_CODEX_API_PROFILE_MODEL || "gpt-5.5";
const codexApiProfileBaseUrl = process.env.HWLAB_NODE_CODEX_API_PROFILE_BASE_URL || "http://127.0.0.1:49280/responses";
const codexApiForwarderPort = process.env.HWLAB_NODE_CODEX_API_FORWARDER_PORT || "49280";
const codexApiForwarderUpstreamBaseUrl = process.env.HWLAB_NODE_CODEX_API_UPSTREAM_BASE_URL || "https://hyueapi.com";
const sourceCommitPattern = /^[a-f0-9]{7,40}$/u;
const v02EnvReuseRuntimeMode = "env-reuse-git-mirror-checkout";
@@ -106,14 +106,14 @@ const primitiveValidationTasks = Object.freeze([
commands: ["node scripts/repo-reports-guard.mjs"]
},
{
name: "g14-contract-check",
name: "node-contract-check",
commands: [
"npm run gitops:ts:check",
"node scripts/run-bun.mjs scripts/g14-gitops-render.mjs --help >/dev/null",
"node scripts/run-bun.mjs scripts/gitops-render.mjs --help >/dev/null",
"render_check_revision=$(git rev-parse HEAD)",
"render_check_dir=$(mktemp -d)",
"node scripts/run-bun.mjs scripts/g14-gitops-render.mjs --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --image-tag-mode \"$(params.image-tag-mode)\" --source-branch \"$(params.source-branch)\" --gitops-branch \"$(params.gitops-branch)\" --out \"$render_check_dir\" --source-revision \"$render_check_revision\"",
"node scripts/run-bun.mjs scripts/g14-gitops-render.mjs --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --image-tag-mode \"$(params.image-tag-mode)\" --source-branch \"$(params.source-branch)\" --gitops-branch \"$(params.gitops-branch)\" --out \"$render_check_dir\" --check --source-revision \"$render_check_revision\"",
"node scripts/run-bun.mjs scripts/gitops-render.mjs --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --image-tag-mode \"$(params.image-tag-mode)\" --source-branch \"$(params.source-branch)\" --gitops-branch \"$(params.gitops-branch)\" --out \"$render_check_dir\" --source-revision \"$render_check_revision\"",
"node scripts/run-bun.mjs scripts/gitops-render.mjs --lane \"$(params.lane)\" --catalog-path \"$(params.catalog-path)\" --image-tag-mode \"$(params.image-tag-mode)\" --source-branch \"$(params.source-branch)\" --gitops-branch \"$(params.gitops-branch)\" --out \"$render_check_dir\" --check --source-revision \"$render_check_revision\"",
`node --input-type=module <<'NODE'
import { readdirSync, readFileSync, statSync } from "node:fs";
import path from "node:path";
@@ -122,7 +122,7 @@ const root = process.cwd();
const ciFileName = "CI" + ".json";
const forbiddenCiFragments = [
"ci" + "-json",
"HWLAB_G14_TEKTON" + "_SINGLE_IMAGE_PUBLISH",
"HWLAB_NODE_TEKTON" + "_SINGLE_IMAGE_PUBLISH",
"single" + "-dind",
"docker" + ":29",
"DOCKER" + "_HOST",
@@ -132,15 +132,14 @@ const forbiddenCiFragments = [
"--build" + "-backend",
"HWLAB_ARTIFACT" + "_BUILD_BACKEND",
"--legacy" + "-source-images",
"HWLAB_G14_USE_DEPLOY_IMAGES" + "=0"
"HWLAB_NODE_USE_DEPLOY_IMAGES" + "=0"
];
const scanTargets = [
"scripts/g14-gitops-render.mjs",
"scripts/gitops-render.mjs",
"scripts/src/runtime-lane.ts",
"scripts/artifact-publish.mjs",
"scripts/g14-artifact-publish.mjs",
"scripts/src/g14-ci-plan-lib.mjs",
"deploy/gitops/g14/tekton"
"scripts/src/ci-plan-lib.mjs",
"deploy/gitops/node/tekton"
];
const skipDirs = new Set([".git", "node_modules", ".worktree"]);
@@ -175,8 +174,7 @@ if (ciFiles.length || violations.length) {
process.exit(1);
}
NODE`,
"node --check scripts/artifact-publish.mjs",
"node --check scripts/g14-artifact-publish.mjs"
"node --check scripts/artifact-publish.mjs"
]
},
{
@@ -202,8 +200,8 @@ function proxyEnv() {
function parseArgs(argv) {
const args = {
lane: process.env.HWLAB_GITOPS_LANE || "g14",
nodeId: process.env.HWLAB_NODE_ID || "G14",
lane: process.env.HWLAB_GITOPS_LANE || "node",
nodeId: process.env.HWLAB_NODE_ID || "node",
outDir: defaultOutDir,
gitopsRoot: defaultOutDir,
catalogPath: defaultCatalogPath,
@@ -265,7 +263,7 @@ function parseArgs(argv) {
}
if (!args.gitReadUrl) args.gitReadUrl = args.sourceRepo;
if (!args.gitWriteUrl) args.gitWriteUrl = args.sourceRepo;
assert.ok(args.lane === "g14" || isRuntimeLane(args.lane), `unknown lane ${args.lane}`);
assert.ok(args.lane === "node" || isRuntimeLane(args.lane), `unknown lane ${args.lane}`);
assert.ok(["short", "full"].includes(args.imageTagMode), `unknown image tag mode ${args.imageTagMode}`);
if (isRuntimeLane(args.lane)) {
const versionName = versionNameForRuntimeLane(args.lane);
@@ -283,13 +281,13 @@ function readOption(argv, index, name) {
function usage() {
return [
"usage: node scripts/run-bun.mjs scripts/g14-gitops-render.mjs [options]",
"usage: node scripts/run-bun.mjs scripts/gitops-render.mjs [options]",
"",
"Render G14-only Kubernetes-native CI/CD manifests from built-in Tekton CI primitives, deploy/deploy.yaml, and deploy/k8s/*.",
"Render node-scoped Kubernetes-native CI/CD manifests from built-in Tekton CI primitives, deploy/deploy.yaml, and deploy/k8s/*.",
"",
"options:",
" --lane LANE g14 or runtime lane such as v02/v03/v04; default: g14",
` --node NODE deployment node id from deploy.nodes; default: ${process.env.HWLAB_NODE_ID || "G14"}`,
" --lane LANE node or runtime lane such as v02/v03/v04; default: node",
` --node NODE deployment node id from deploy.nodes; runtime lanes default from deploy.lanes.<lane>.node`,
` --out DIR default: ${defaultOutDir}`,
` --gitops-root DIR path inside GitOps repo; default comes from deploy.nodes.<node>.gitopsRoot`,
` --catalog-path PATH default: ${defaultCatalogPath}`,
@@ -453,7 +451,7 @@ function v02EnvReuseEnabled(catalog, serviceId, envReuseServiceIds = null) {
}
function bootShForService(deployService, serviceId) {
assert.ok(deployService?.bootSh, `deploy.lanes.v02.bootScripts.${serviceId} is required`);
assert.ok(deployService?.bootSh, `deploy lane bootScripts.${serviceId} is required`);
const value = deployService.bootSh;
const text = String(value).replaceAll("\\", "/").replace(/^\.\//u, "");
assert.ok(text && !text.startsWith("/") && !text.split("/").includes(".."), `${serviceId} boot script must be repo-relative`);
@@ -461,16 +459,16 @@ function bootShForService(deployService, serviceId) {
}
function envReuseServiceIdsForLane(deploy, lane) {
if (lane !== "v02") return new Set();
const configured = deploy?.lanes?.v02?.envReuseServices;
if (!isRuntimeLane(lane)) return new Set();
const configured = deploy?.lanes?.[lane]?.envReuseServices;
const allowed = new Set(serviceIdsForLane(lane, deploy));
return new Set((Array.isArray(configured) ? configured : []).filter((serviceId) => allowed.has(serviceId)));
}
function deployServiceForBoot(deploy, serviceId) {
function deployServiceForBoot(deploy, serviceId, lane = "v02") {
return {
serviceId,
bootSh: deploy?.lanes?.v02?.bootScripts?.[serviceId]
bootSh: deploy?.lanes?.[lane]?.bootScripts?.[serviceId]
};
}
@@ -697,7 +695,7 @@ function artifactCatalogSkeletonService({ args, deploy, source, serviceId, envir
};
const envReuseServiceIds = envReuseServiceIdsForLane(deploy, args.lane);
if (!envReuseServiceIds.has(serviceId)) return base;
const bootSh = bootShForService(deployServiceForBoot(deploy, serviceId), serviceId);
const bootSh = bootShForService(deployServiceForBoot(deploy, serviceId, args.lane), serviceId);
const environmentImage = imageFor(args.registryPrefix, `${serviceId}-env`, `env-${String(source.full).slice(0, 12)}`);
return {
...base,
@@ -738,7 +736,7 @@ function runtimeLabelForProfile(profile) {
}
function gitopsTargetForProfile(profile) {
return isRuntimeLane(profile) ? profile : "g14";
return isRuntimeLane(profile) ? profile : "node";
}
function gitopsRootForArgs(args) {
@@ -776,24 +774,24 @@ function laneNames(args) {
pipelineRunPrefix: `${namespace}-ci-poll-`,
runtimeNamespace: namespace,
runtimePath: gitopsPathForProfile(args, args.lane),
argoApplications: [`argocd/hwlab-g14-${args.lane}`]
argoApplications: [`argocd/hwlab-node-${args.lane}`]
};
}
return {
gitopsTarget: "g14",
pipeline: "hwlab-g14-ci-image-publish",
poller: "hwlab-g14-branch-poller",
reconciler: "hwlab-g14-control-plane-reconciler",
gitopsTarget: "node",
pipeline: "hwlab-node-ci-image-publish",
poller: "hwlab-node-branch-poller",
reconciler: "hwlab-node-control-plane-reconciler",
serviceAccount: "hwlab-tekton-runner",
pipelineRunPrefix: "hwlab-g14-ci-poll-",
pipelineRunPrefix: "hwlab-node-ci-poll-",
runtimeNamespace: "hwlab-dev",
runtimePath: gitopsPathForProfile(args, "dev"),
argoApplications: ["argocd/hwlab-g14-dev", "argocd/hwlab-g14-prod"]
argoApplications: ["argocd/hwlab-node-dev", "argocd/hwlab-node-prod"]
};
}
function argoApplicationName(profile) {
return `hwlab-g14-${profile}`;
return `hwlab-node-${profile}`;
}
function imageTagForSource(args, source) {
@@ -830,16 +828,16 @@ function namespaceScopedHost(value, namespace) {
}
function secretNameForProfile(secretName, profile) {
if (profile !== "v02") return secretName;
if (secretName === "hwlab-cloud-api-dev-db") return "hwlab-cloud-api-v02-db";
if (secretName === "hwlab-code-agent-provider") return "hwlab-v02-code-agent-provider";
if (secretName === "hwlab-code-agent-codex-auth") return "hwlab-v02-code-agent-codex-auth";
if (secretName === "hwlab-bootstrap-admin") return "hwlab-v02-bootstrap-admin";
if (!isRuntimeLane(profile)) return secretName;
if (secretName === "hwlab-cloud-api-dev-db") return `hwlab-cloud-api-${profile}-db`;
if (secretName === "hwlab-code-agent-provider") return `hwlab-${profile}-code-agent-provider`;
if (secretName === "hwlab-code-agent-codex-auth") return `hwlab-${profile}-code-agent-codex-auth`;
if (secretName === "hwlab-bootstrap-admin") return `hwlab-${profile}-bootstrap-admin`;
return secretName;
}
function rewritePodSecretRefs(podSpec, profile) {
if (!podSpec || profile !== "v02") return;
if (!podSpec || !isRuntimeLane(profile)) return;
for (const volume of podSpec.volumes ?? []) {
if (volume?.secret?.secretName) volume.secret.secretName = secretNameForProfile(volume.secret.secretName, profile);
}
@@ -897,8 +895,8 @@ function deployServicesForProfile(deploy, 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
const laneServices = isRuntimeLane(profile) && Array.isArray(deploy.lanes?.[profile]?.services)
? deploy.lanes[profile].services
: [];
for (const override of laneServices) {
const existing = services.get(override.serviceId) ?? { serviceId: override.serviceId };
@@ -911,6 +909,12 @@ function deployServicesForProfile(deploy, profile) {
return services;
}
function runtimeNodeIdForProfile(deploy, profile, fallback = "node") {
if (!isRuntimeLane(profile)) return fallback;
const nodeId = deploy?.lanes?.[profile]?.node;
return typeof nodeId === "string" && nodeId.length > 0 ? nodeId : fallback;
}
function v02MetricsLabels(serviceId) {
return {
"hwlab.pikastech.local/monitoring": "enabled",
@@ -978,25 +982,26 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch =
const namespace = namespaceNameForProfile(profile);
const profileLabel = runtimeLabelForProfile(profile);
const gitopsTarget = gitopsTargetForProfile(profile);
const digestPin = profile === "v02";
const runtimeLane = isRuntimeLane(profile);
const digestPin = runtimeLane;
const envReuseServiceIds = envReuseServiceIdsForLane(deploy, profile);
const v02ObservableServiceIds = profile === "v02" ? v02ObservableServiceIdsForDeploy(deploy) : new Set();
const runtimeObservableServiceIds = runtimeLane ? runtimeLaneObservableServiceIdsForDeploy(deploy, profile) : new Set();
const stableRuntimeLabels = {
"app.kubernetes.io/part-of": "hwlab",
"hwlab.pikastech.local/environment": profileLabel,
"hwlab.pikastech.local/profile": profileLabel
};
result.items = asItems(result, "workloads").filter((item) => !(profile === "v02" && isV02RemovedServiceId(serviceIdForWorkload(item, null))));
result.items = asItems(result, "workloads").filter((item) => !(runtimeLane && isV02RemovedServiceId(serviceIdForWorkload(item, null))));
for (const item of asItems(result, "workloads")) {
item.metadata.namespace = namespace;
label(item.metadata, {
...stableRuntimeLabels,
"hwlab.pikastech.local/gitops-target": gitopsTarget,
"hwlab.pikastech.local/source-commit": source.full,
"unidesk.ai/g14-staging": profile === "dev" ? "true" : "false"
"unidesk.ai/node-staging": profile === "dev" ? "true" : "false"
});
annotate(item.metadata, {
"hwlab.pikastech.local/gitops-note": profile === "v02" ? "HWLAB v0.2 GitOps target." : profile === "prod" ? "G14 PROD GitOps target." : "G14 DEV GitOps target.",
"hwlab.pikastech.local/gitops-note": profile === "v02" ? "HWLAB v0.2 GitOps target." : profile === "prod" ? "node PROD GitOps target." : "node DEV GitOps target.",
"hwlab.pikastech.local/source-commit": source.full
});
if (item.kind === "Job") {
@@ -1013,7 +1018,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch =
if (!podTemplate?.spec?.containers) continue;
const templateServiceId = serviceIdForWorkload(item, podTemplate.spec.containers[0]);
const templateArtifact = runtimeArtifactForService({ catalog, deploy, serviceId: templateServiceId, source, registryPrefix, useDeployImages, digestPin, envReuseServiceIds });
const templateSourceCommit = profile === "v02" ? (templateArtifact.commit ?? source.full) : source.full;
const templateSourceCommit = runtimeLane ? (templateArtifact.commit ?? source.full) : source.full;
const templateArtifactCommit = templateArtifact.commit;
label(podTemplate.metadata ??= {}, {
...stableRuntimeLabels,
@@ -1021,7 +1026,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch =
"hwlab.pikastech.local/source-commit": templateSourceCommit,
"hwlab.pikastech.local/artifact-source-commit": templateArtifactCommit
});
if (profile === "v02" && v02ObservableServiceIds.has(templateServiceId)) {
if (runtimeLane && runtimeObservableServiceIds.has(templateServiceId)) {
label(item.metadata, v02MetricsLabels(templateServiceId));
label(podTemplate.metadata, v02MetricsLabels(templateServiceId));
annotate(podTemplate.metadata, v02MetricsSidecarAnnotations(metricsSidecarSha256));
@@ -1036,7 +1041,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch =
annotate(podTemplate.metadata, {
"hwlab.pikastech.local/source-commit": templateSourceCommit,
"hwlab.pikastech.local/artifact-source-commit": templateArtifactCommit,
"hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs"
"hwlab.pikastech.local/rendered-by": "scripts/gitops-render.mjs"
});
for (const container of podTemplate.spec.containers) {
if (container.name === "hwlab-metrics") continue;
@@ -1044,8 +1049,8 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch =
const deployService = deployServices.get(serviceId);
if (!deployService) continue;
const artifact = runtimeArtifactForService({ catalog, deploy, serviceId, source, registryPrefix, useDeployImages, digestPin, envReuseServiceIds });
const envReuse = profile === "v02" && v02EnvReuseEnabled(catalog, serviceId, envReuseServiceIds);
const bootMetadata = envReuse ? bootMetadataForService({ args: { sourceRepo: deploy?.lanes?.v02?.sourceRepo || defaultSourceRepo, registryPrefix }, catalog, deployService, serviceId, source }) : null;
const envReuse = runtimeLane && v02EnvReuseEnabled(catalog, serviceId, envReuseServiceIds);
const bootMetadata = envReuse ? bootMetadataForService({ args: { sourceRepo: deploy?.lanes?.[profile]?.sourceRepo || defaultSourceRepo, registryPrefix }, catalog, deployService, serviceId, source }) : null;
const image = artifact.image;
const runtimeCommit = artifact.commit;
const runtimeImageTag = artifact.imageTag;
@@ -1056,7 +1061,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch =
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);
if (runtimeLane) container.env = removeV02LegacySimulatorEnv(container.env);
upsertEnv(container.env, "HWLAB_ENVIRONMENT", profileLabel);
upsertEnv(container.env, "HWLAB_COMMIT_ID", runtimeCommit);
upsertEnv(container.env, "HWLAB_IMAGE", image);
@@ -1066,8 +1071,8 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch =
}
upsertEnv(container.env, "HWLAB_GITOPS_TARGET", gitopsTarget);
upsertEnv(container.env, "HWLAB_GITOPS_PROFILE", profileLabel);
upsertEnv(container.env, "HWLAB_GITOPS_SOURCE_COMMIT", profile === "v02" ? runtimeCommit : source.full);
if (profile === "v02" && serviceId === "hwlab-cloud-api") {
upsertEnv(container.env, "HWLAB_GITOPS_SOURCE_COMMIT", runtimeLane ? runtimeCommit : source.full);
if (runtimeLane && serviceId === "hwlab-cloud-api") {
upsertEnv(container.env, "HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT", runtimeCommit);
}
if (bootMetadata) {
@@ -1080,32 +1085,17 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch =
if (serviceId === "hwlab-cloud-api" || serviceId === "hwlab-edge-proxy") {
upsertEnv(container.env, "HWLAB_PUBLIC_ENDPOINT", runtimeEndpoint);
}
if (profile === "v02" && serviceId === "hwlab-edge-proxy") {
if (runtimeLane && serviceId === "hwlab-edge-proxy") {
useLocalHealthProbe(container, "/health");
}
if (serviceId === "hwlab-cloud-api") {
if (profile === "v02") {
if (runtimeLane) {
removeV02RepoOwnedCodeAgentPodConfig(podTemplate.spec);
upsertEnv(container.env, "HWLAB_M3_IO_CONTROL_ENABLED", "false");
upsertEnv(container.env, "HWLAB_ACCESS_CONTROL_REQUIRED", "1");
upsertEnv(container.env, "HWLAB_BOOTSTRAP_ADMIN_ID", "usr_v02_admin");
upsertEnv(container.env, "HWLAB_BOOTSTRAP_ADMIN_USERNAME", "admin");
upsertEnv(container.env, "HWLAB_BOOTSTRAP_ADMIN_DISPLAY_NAME", "HWLAB v0.2 Admin");
upsertEnvEntry(container.env, deployEnvEntry("HWLAB_BOOTSTRAP_ADMIN_PASSWORD_HASH", "secretRef:hwlab-v02-bootstrap-admin/password-hash", namespace, profile));
upsertEnv(container.env, "HWLAB_BOOTSTRAP_ADMIN_API_KEY_ID", "key_master_server_admin");
upsertEnvEntry(container.env, deployEnvEntry("HWLAB_BOOTSTRAP_ADMIN_API_KEY", "secretRef:hwlab-v02-master-server-admin-api-key/api-key", namespace, profile));
upsertEnv(container.env, "HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID", runtimeNodeIdForProfile(deploy, profile));
upsertEnv(container.env, "HWLAB_OPENFGA_MODE", "enforce");
upsertEnv(container.env, "HWLAB_OPENFGA_API_URL", "http://hwlab-openfga.hwlab-v02.svc.cluster.local:8080");
upsertEnvEntry(container.env, deployEnvEntry("HWLAB_OPENFGA_AUTHN_TOKEN", "secretRef:hwlab-v02-openfga/authn-preshared-key", namespace, profile));
upsertEnv(container.env, "HWLAB_KEYCLOAK_ISSUER", "https://auth.74-48-78-17.nip.io/realms/hwlab");
upsertEnv(container.env, "HWLAB_KEYCLOAK_CLIENT_ID", "hwlab-cloud-web");
upsertEnvEntry(container.env, deployEnvEntry("HWLAB_KEYCLOAK_CLIENT_SECRET", "secretRef:hwlab-cloud-web-client/client-secret", namespace, profile));
upsertEnv(container.env, "HWLAB_CODE_AGENT_ADAPTER", "agentrun-v01");
upsertEnv(container.env, "AGENTRUN_MGR_URL", "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080");
upsertEnv(container.env, "HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID", "G14");
upsertEnv(container.env, "HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE", "agentrun-v01");
upsertEnv(container.env, "HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE", "agentrun-v01");
upsertEnv(container.env, "HWLAB_CODE_AGENT_AGENTRUN_REPO_URL", "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git");
upsertEnv(container.env, "HWLAB_OPENFGA_API_URL", `http://hwlab-openfga.${namespace}.svc.cluster.local:8080`);
upsertEnvEntry(container.env, deployEnvEntry("HWLAB_OPENFGA_AUTHN_TOKEN", `secretRef:hwlab-${profile}-openfga/authn-preshared-key`, namespace, profile));
upsertEnv(container.env, "UNIDESK_MAIN_SERVER_IP", "http://74.48.78.17:18081");
} else {
syncCodeAgentWorkspaceInitContainer(podTemplate.spec.initContainers, { image, runtimeCommit, runtimeImageTag });
@@ -1150,7 +1140,7 @@ function transformHealthContract(value, namespace, labels, annotations, runtimeE
result.data["cloud-api"] = `GET /health/live through ${runtimeEndpoint}`;
}
if (Object.hasOwn(result.data, "tunnel-client")) {
result.data["tunnel-client"] = "disabled for G14 GitOps; no FRP or production traffic tunnel is required";
result.data["tunnel-client"] = "disabled for node GitOps; no FRP or production traffic tunnel is required";
}
}
return result;
@@ -1278,7 +1268,7 @@ ci_timing_emit() {
finished_ms="$(ci_now_ms)"
duration_ms="$((finished_ms - started_ms))"
service_id="\${HWLAB_TIMING_SERVICE_ID:-}"
node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:"g14-cicd-timing",schemaVersion:"v1",stage,status,durationMs:Number(durationMs),pipelineRun:process.env.HWLAB_TEKTON_PIPELINERUN||null,taskRun:process.env.HWLAB_TEKTON_TASKRUN||null,task:process.env.HWLAB_TEKTON_TASK||null,revision:process.env.HWLAB_SOURCE_REVISION||null,serviceId:serviceId||null,source:"scripts/g14-gitops-render.mjs",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' "$stage" "$status" "$duration_ms" "$service_id"
node -e 'const [stage,status,durationMs,serviceId]=process.argv.slice(1); const payload={event:"node-cicd-timing",schemaVersion:"v1",stage,status,durationMs:Number(durationMs),pipelineRun:process.env.HWLAB_TEKTON_PIPELINERUN||null,taskRun:process.env.HWLAB_TEKTON_TASKRUN||null,task:process.env.HWLAB_TEKTON_TASK||null,revision:process.env.HWLAB_SOURCE_REVISION||null,serviceId:serviceId||null,source:"scripts/gitops-render.mjs",at:new Date().toISOString()}; console.log(JSON.stringify(payload));' "$stage" "$status" "$duration_ms" "$service_id"
}
`;
}
@@ -1344,7 +1334,7 @@ const timeout = setTimeout(() => {
}, 15000);
socket.on("connect", () => {
socket.write("GET " + target.href + " HTTP/1.1\\r\\nHost: " + target.host + "\\r\\nUser-Agent: hwlab-g14-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n");
socket.write("GET " + target.href + " HTTP/1.1\\r\\nHost: " + target.host + "\\r\\nUser-Agent: hwlab-node-proxy-probe\\r\\nConnection: close\\r\\n\\r\\n");
});
socket.on("data", (chunk) => {
@@ -1601,7 +1591,7 @@ function prepareSourceScript() {
" echo '{\"event\":\"gitops-artifact-catalog\",\"phase\":\"prepare-source\",\"status\":\"seeded-v02-skeleton\"}'",
"fi",
"ci_timing_emit catalog-fetch succeeded \"$catalog_fetch_started_ms\"",
`echo ${shellSingleQuote(`G14 prepare-source complete; validation task count=${primitiveValidationTasks.length}`)}`
`echo ${shellSingleQuote(`node prepare-source complete; validation task count=${primitiveValidationTasks.length}`)}`
];
return `${lines.join("\n")}\n`;
}
@@ -1656,7 +1646,7 @@ echo '{"event":"ci-base-image","phase":"plan-artifacts","image":"${ciToolsRunner
for tool in node git; do command -v "$tool" >/dev/null 2>&1 || { echo '{"event":"ci-base-image","phase":"plan-artifacts","ok":false,"reason":"missing-tool","tool":"'"$tool"'"}'; exit 31; }; done
cd /workspace/source/repo
test "$(git rev-parse HEAD)" = "$(params.revision)"
node scripts/g14-ci-plan.mjs --lane "$(params.lane)" --target-ref HEAD --deploy-config deploy/deploy.yaml --artifact-catalog "$(params.catalog-path)" --registry-prefix "$(params.registry-prefix)" --services "$(params.services)" > /workspace/source/ci-plan.json
node scripts/ci-plan.mjs --lane "$(params.lane)" --target-ref HEAD --deploy-config deploy/deploy.yaml --artifact-catalog "$(params.catalog-path)" --registry-prefix "$(params.registry-prefix)" --services "$(params.services)" > /workspace/source/ci-plan.json
node - <<'NODE'
const fs = require("node:fs");
const plan = JSON.parse(fs.readFileSync("/workspace/source/ci-plan.json", "utf8"));
@@ -1698,7 +1688,7 @@ fs.writeFileSync("/workspace/source/affected-services.json", JSON.stringify({
services: plan.services || [],
entries
}, null, 2) + String.fromCharCode(10));
console.log(JSON.stringify({ event: "g14-ci-plan", sourceCommitId: plan.sourceCommitId, affectedServices: plan.affectedServices || [], rolloutServices: plan.rolloutServices || plan.affectedServices || [], buildServices: plan.buildServices || [], reusedServices: plan.reusedServices || [], buildSkippedCount: plan.buildSkippedCount || 0 }));
console.log(JSON.stringify({ event: "ci-plan", sourceCommitId: plan.sourceCommitId, affectedServices: plan.affectedServices || [], rolloutServices: plan.rolloutServices || plan.affectedServices || [], buildServices: plan.buildServices || [], reusedServices: plan.reusedServices || [], buildSkippedCount: plan.buildSkippedCount || 0 }));
NODE
`;
}
@@ -1772,7 +1762,7 @@ export HWLAB_CI_ARTIFACT_RUN_ID="$(context.pipelineRun.name)-$(params.service-id
export HWLAB_CI_ARTIFACT_RUN_OWNER="tekton"
export HWLAB_DEV_REGISTRY_PREFIX="$(params.registry-prefix)"
export HWLAB_DEV_REGISTRY_K8S_NATIVE=1
export HWLAB_G14_CICD_TIMING=1
export HWLAB_NODE_CICD_TIMING=1
export HWLAB_TEKTON_PIPELINERUN
export HWLAB_TEKTON_TASKRUN
export HWLAB_TEKTON_TASK
@@ -1799,7 +1789,7 @@ if [ "$buildkit_ready" != "1" ]; then
echo '{"event":"buildkit-sidecar-not-ready","serviceId":"'"$(params.service-id)"'","addr":"'"$HWLAB_BUILDKIT_ADDR"'"}' >&2
exit 1
fi
node scripts/g14-artifact-publish.mjs --publish --lane "$(params.lane)" --catalog-path "$(params.catalog-path)" --deploy-config deploy/deploy.yaml --image-tag-mode "$(params.image-tag-mode)" --registry-prefix "$(params.registry-prefix)" --report "/workspace/source/service-results/$(params.service-id).json" --tekton-results-dir /tekton/results --quiet-build --concurrency 1 --services "$(params.service-id)" --buildkit-command /workspace/buildkit-bin/buildctl --buildkit-addr "$HWLAB_BUILDKIT_ADDR"
node scripts/artifact-publish.mjs --publish --lane "$(params.lane)" --catalog-path "$(params.catalog-path)" --deploy-config deploy/deploy.yaml --image-tag-mode "$(params.image-tag-mode)" --registry-prefix "$(params.registry-prefix)" --report "/workspace/source/service-results/$(params.service-id).json" --tekton-results-dir /tekton/results --quiet-build --concurrency 1 --services "$(params.service-id)" --buildkit-command /workspace/buildkit-bin/buildctl --buildkit-addr "$HWLAB_BUILDKIT_ADDR"
`;
}
@@ -1819,7 +1809,7 @@ export HWLAB_ARTIFACT_LANE="$(params.lane)"
export HWLAB_ARTIFACT_CATALOG_PATH="$(params.catalog-path)"
export HWLAB_DEPLOY_MANIFEST_PATH="deploy/deploy.yaml"
export HWLAB_ARTIFACT_IMAGE_TAG_MODE="$(params.image-tag-mode)"
node scripts/g14-artifact-publish.mjs --publish --lane "$(params.lane)" --catalog-path "$(params.catalog-path)" --deploy-config deploy/deploy.yaml --image-tag-mode "$(params.image-tag-mode)" --registry-prefix "$(params.registry-prefix)" --report /workspace/source/dev-artifacts.json --quiet-build --concurrency 1 --services "$(params.services)" --external-service-report-dir /workspace/source/service-results
node scripts/artifact-publish.mjs --publish --lane "$(params.lane)" --catalog-path "$(params.catalog-path)" --deploy-config deploy/deploy.yaml --image-tag-mode "$(params.image-tag-mode)" --registry-prefix "$(params.registry-prefix)" --report /workspace/source/dev-artifacts.json --quiet-build --concurrency 1 --services "$(params.services)" --external-service-report-dir /workspace/source/service-results
node scripts/refresh-artifact-catalog.mjs --lane "$(params.lane)" --catalog-path "$(params.catalog-path)" --deploy-config deploy/deploy.yaml --image-tag-mode "$(params.image-tag-mode)" --target-ref HEAD --publish-report /workspace/source/dev-artifacts.json --no-write
`;
}
@@ -1883,21 +1873,21 @@ check_source_head() {
}
check_source_head before-render
git config --global user.name "HWLAB G14 GitOps Bot"
git config --global user.email "hwlab-g14-gitops-bot@users.noreply.github.com"
git config --global user.name "HWLAB node GitOps Bot"
git config --global user.email "hwlab-node-gitops-bot@users.noreply.github.com"
catalog_path="$(params.catalog-path)"
runtime_path="$(params.runtime-path)"
argo_hard_refresh_runtime_lane() {
if [ "$runtime_lane" != "true" ]; then return 0; fi
ARGO_APPLICATION="hwlab-g14-$lane" ARGO_FIELD_MANAGER="hwlab-$lane-gitops-promote" node <<'NODE'
ARGO_APPLICATION="hwlab-node-$lane" ARGO_FIELD_MANAGER="hwlab-$lane-gitops-promote" node <<'NODE'
const fs = require("node:fs");
const https = require("node:https");
const host = process.env.KUBERNETES_SERVICE_HOST;
const port = process.env.KUBERNETES_SERVICE_PORT || "443";
const startedAt = Date.now();
const application = process.env.ARGO_APPLICATION || "hwlab-g14-v02";
const application = process.env.ARGO_APPLICATION || "hwlab-node-v02";
const fieldManager = process.env.ARGO_FIELD_MANAGER || "hwlab-v02-gitops-promote";
const pipelineRun = process.env.HWLAB_TEKTON_PIPELINERUN || null;
const taskRun = process.env.HWLAB_TEKTON_TASKRUN || null;
@@ -1906,7 +1896,7 @@ const revision = process.env.HWLAB_SOURCE_REVISION || null;
function emit(payload) {
console.log(JSON.stringify({
event: "g14-cicd-timing",
event: "node-cicd-timing",
schemaVersion: "v1",
stage: "argo-hard-refresh",
pipelineRun,
@@ -1914,7 +1904,7 @@ function emit(payload) {
task,
revision,
application,
source: "scripts/g14-gitops-render.mjs",
source: "scripts/gitops-render.mjs",
at: new Date().toISOString(),
...payload
}));
@@ -1975,8 +1965,8 @@ if [ -s /workspace/source/dev-artifacts.json ]; then
node scripts/refresh-artifact-catalog.mjs --lane "$lane" --catalog-path "$catalog_path" --deploy-config deploy/deploy.yaml --image-tag-mode "$(params.image-tag-mode)" --target-ref "$(params.revision)" --publish-report /workspace/source/dev-artifacts.json --write
fi
gitops_render_started_ms="$(ci_now_ms)"
node scripts/run-bun.mjs scripts/g14-gitops-render.mjs --lane "$lane" --catalog-path "$catalog_path" --image-tag-mode "$(params.image-tag-mode)" --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)" --use-deploy-images
node scripts/run-bun.mjs scripts/g14-gitops-render.mjs --lane "$lane" --catalog-path "$catalog_path" --image-tag-mode "$(params.image-tag-mode)" --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)" --use-deploy-images --check
node scripts/run-bun.mjs scripts/gitops-render.mjs --lane "$lane" --catalog-path "$catalog_path" --image-tag-mode "$(params.image-tag-mode)" --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)" --use-deploy-images
node scripts/run-bun.mjs scripts/gitops-render.mjs --lane "$lane" --catalog-path "$catalog_path" --image-tag-mode "$(params.image-tag-mode)" --source-revision "$(params.revision)" --source-repo "$(params.git-url)" --source-branch "$(params.source-branch)" --gitops-branch "$(params.gitops-branch)" --registry-prefix "$(params.registry-prefix)" --use-deploy-images --check
ci_timing_emit gitops-render succeeded "$gitops_render_started_ms"
check_source_head before-push
workdir="$(mktemp -d)"
@@ -1997,7 +1987,7 @@ if git_timed gitops-branch-ls 45 git ls-remote --exit-code --heads origin "$(par
mkdir -p "$old_runtime_snapshot"
cp -a "$runtime_path"/. "$old_runtime_snapshot"/
fi
if [ "$runtime_lane" = "true" ]; then rm -rf "$runtime_path" "$catalog_path"; else rm -rf deploy/gitops/g14 "$catalog_path"; fi
if [ "$runtime_lane" = "true" ]; then rm -rf "$runtime_path" "$catalog_path"; else rm -rf deploy/gitops/node "$catalog_path"; fi
else
git checkout --orphan "$(params.gitops-branch)"
git rm -rf . >/dev/null 2>&1 || true
@@ -2010,9 +2000,9 @@ if [ "$runtime_lane" = "true" ]; then
git add "$catalog_path" "$runtime_path"
else
mkdir -p deploy/gitops
cp -a /workspace/source/repo/deploy/gitops/g14 deploy/gitops/g14
cp -a /workspace/source/repo/deploy/gitops/node deploy/gitops/node
cp "/workspace/source/repo/$catalog_path" "$catalog_path"
git add "$catalog_path" deploy/gitops/g14
git add "$catalog_path" deploy/gitops/node
fi
if [ "$runtime_lane" = "true" ] && [ -s /workspace/source/affected-services.json ]; then
runtime_noop_decision="$(node - "$runtime_path" "$old_runtime_snapshot" /workspace/source/affected-services.json <<'NODE'
@@ -2116,7 +2106,7 @@ if git diff --cached --quiet; then
fi
short="$(printf '%.7s' "$(params.revision)")"
gitops_commit_started_ms="$(ci_now_ms)"
git commit -m "chore: promote G14 GitOps source $short"
git commit -m "chore: promote node GitOps source $short"
ci_timing_emit gitops-commit succeeded "$gitops_commit_started_ms"
gitops_push_started_ms="$(ci_now_ms)"
git_timed gitops-push 120 git push origin "HEAD:$(params.gitops-branch)"
@@ -2142,8 +2132,8 @@ revision="$(git rev-parse HEAD)"
: "\${LANE:?LANE is required}"
: "\${CATALOG_PATH:?CATALOG_PATH is required}"
: "\${IMAGE_TAG_MODE:?IMAGE_TAG_MODE is required}"
node scripts/run-bun.mjs scripts/g14-gitops-render.mjs --lane "$LANE" --catalog-path "$CATALOG_PATH" --image-tag-mode "$IMAGE_TAG_MODE" --source-revision "$revision" --source-repo "$GIT_URL" --source-branch "$SOURCE_BRANCH" --gitops-branch "$GITOPS_BRANCH" --registry-prefix "$REGISTRY_PREFIX"
node scripts/run-bun.mjs scripts/g14-gitops-render.mjs --lane "$LANE" --catalog-path "$CATALOG_PATH" --image-tag-mode "$IMAGE_TAG_MODE" --source-revision "$revision" --source-repo "$GIT_URL" --source-branch "$SOURCE_BRANCH" --gitops-branch "$GITOPS_BRANCH" --registry-prefix "$REGISTRY_PREFIX" --check
node scripts/run-bun.mjs scripts/gitops-render.mjs --lane "$LANE" --catalog-path "$CATALOG_PATH" --image-tag-mode "$IMAGE_TAG_MODE" --source-revision "$revision" --source-repo "$GIT_URL" --source-branch "$SOURCE_BRANCH" --gitops-branch "$GITOPS_BRANCH" --registry-prefix "$REGISTRY_PREFIX"
node scripts/run-bun.mjs scripts/gitops-render.mjs --lane "$LANE" --catalog-path "$CATALOG_PATH" --image-tag-mode "$IMAGE_TAG_MODE" --source-revision "$revision" --source-repo "$GIT_URL" --source-branch "$SOURCE_BRANCH" --gitops-branch "$GITOPS_BRANCH" --registry-prefix "$REGISTRY_PREFIX" --check
node <<'NODE'
const fs = require("node:fs");
const https = require("node:https");
@@ -2166,11 +2156,11 @@ const argoFiles = String(process.env.ARGO_FILES || "").split(",").map((item) =>
const fieldManager = requiredEnv("FIELD_MANAGER");
const eventName = requiredEnv("RECONCILE_EVENT");
const manifestFiles = [
"deploy/gitops/g14/" + tektonDir + "/rbac.yaml",
"deploy/gitops/g14/" + tektonDir + "/pipeline.yaml",
"deploy/gitops/node/" + tektonDir + "/rbac.yaml",
"deploy/gitops/node/" + tektonDir + "/pipeline.yaml",
...(process.env.LANE === "v02" ? [] : [
"deploy/gitops/g14/" + tektonDir + "/poller.yaml",
"deploy/gitops/g14/" + tektonDir + "/control-plane-reconciler.yaml"
"deploy/gitops/node/" + tektonDir + "/poller.yaml",
"deploy/gitops/node/" + tektonDir + "/control-plane-reconciler.yaml"
]),
...argoFiles
];
@@ -2280,7 +2270,7 @@ git remote set-url origin "$GIT_READ_URL"
revision="$(git rev-parse HEAD)"
subject="$(git log -1 --pretty=%s)"
case "$subject" in
"chore: promote G14 GitOps source "*)
"chore: promote node GitOps source "*)
echo "Skipping generated GitOps promotion commit $revision"
exit 0
;;
@@ -2410,7 +2400,7 @@ NODE
`;
}
function ciLaneSettings(argsOrLane = "g14") {
function ciLaneSettings(argsOrLane = "node") {
const lane = typeof argsOrLane === "string" ? argsOrLane : argsOrLane.lane;
if (isRuntimeLane(lane)) {
const namespace = namespaceNameForProfile(lane);
@@ -2430,14 +2420,14 @@ function ciLaneSettings(argsOrLane = "g14") {
};
}
return {
lane: "g14",
lane: "node",
profile: "dev",
gitopsTarget: "g14",
pipelineName: "hwlab-g14-ci-image-publish",
pipelineRunPrefix: "hwlab-g14-ci-poll",
gitopsTarget: "node",
pipelineName: "hwlab-node-ci-image-publish",
pipelineRunPrefix: "hwlab-node-ci-poll",
serviceAccountName: "hwlab-tekton-runner",
pollerName: "hwlab-g14-branch-poller",
controlPlaneReconcilerName: "hwlab-g14-control-plane-reconciler",
pollerName: "hwlab-node-branch-poller",
controlPlaneReconcilerName: "hwlab-node-control-plane-reconciler",
tektonDir: "tekton",
catalogPath: "deploy/artifact-catalog.dev.json",
imageTagMode: "short",
@@ -2445,7 +2435,7 @@ function ciLaneSettings(argsOrLane = "g14") {
};
}
function tektonRbac(args = { lane: "g14" }) {
function tektonRbac(args = { lane: "node" }) {
const settings = ciLaneSettings(args);
if (isRuntimeLane(settings.lane)) {
const runtimeObserverName = `${settings.runtimeNamespace}-runtime-observer`;
@@ -2521,7 +2511,7 @@ function tektonRbac(args = { lane: "g14" }) {
{
apiVersion: "rbac.authorization.k8s.io/v1",
kind: "Role",
metadata: { name: "hwlab-g14-control-plane-reconciler", namespace: "hwlab-dev" },
metadata: { name: "hwlab-node-control-plane-reconciler", namespace: "hwlab-dev" },
rules: [
{ apiGroups: ["rbac.authorization.k8s.io"], resources: ["roles", "rolebindings"], verbs: ["get", "patch", "create"] }
]
@@ -2529,9 +2519,9 @@ function tektonRbac(args = { lane: "g14" }) {
{
apiVersion: "rbac.authorization.k8s.io/v1",
kind: "RoleBinding",
metadata: { name: "hwlab-g14-control-plane-reconciler", namespace: "hwlab-dev" },
metadata: { name: "hwlab-node-control-plane-reconciler", namespace: "hwlab-dev" },
subjects: [{ kind: "ServiceAccount", name: "hwlab-tekton-runner", namespace: "hwlab-ci" }],
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: "hwlab-g14-control-plane-reconciler" }
roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: "hwlab-node-control-plane-reconciler" }
},
{
apiVersion: "rbac.authorization.k8s.io/v1",
@@ -2683,7 +2673,7 @@ function planArtifactsTask({ serviceIds = defaultServiceIds } = {}) {
{ name: "registry-prefix" },
{ name: "services" }
],
results: serviceIds.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 ci-plan` })),
workspaces: [{ name: "source" }],
steps: [{
name: "plan",
@@ -2808,13 +2798,13 @@ const progressEveryMs = 10000;
function emit(payload) {
console.log(JSON.stringify({
event: "g14-cicd-timing",
event: "node-cicd-timing",
schemaVersion: "v1",
pipelineRun,
taskRun,
task,
revision,
source: "scripts/g14-gitops-render.mjs",
source: "scripts/gitops-render.mjs",
at: new Date().toISOString(),
...payload
}));
@@ -3075,7 +3065,7 @@ function runtimeReadyTask({ profile = "dev" } = {}) {
};
}
function imagePublishTaskSet(args = { lane: "g14" }, deploy = null) {
function imagePublishTaskSet(args = { lane: "node" }, deploy = null) {
const serviceIds = serviceIdsForLane(args.lane, deploy);
const buildTasks = serviceIds.map((serviceId) => perServiceBuildTask(serviceId, {
runAfter: ["plan-artifacts"]
@@ -3087,7 +3077,7 @@ function imagePublishTaskSet(args = { lane: "g14" }, deploy = null) {
];
}
function tektonPipeline(args = { lane: "g14" }, deploy = null) {
function tektonPipeline(args = { lane: "node" }, deploy = null) {
const settings = ciLaneSettings(args);
const runtimePath = gitopsPathForProfile(args, settings.profile);
return {
@@ -3101,7 +3091,7 @@ function tektonPipeline(args = { lane: "g14" }, deploy = null) {
"hwlab.pikastech.local/gitops-target": settings.gitopsTarget
},
annotations: {
"hwlab.pikastech.local/source-config": "scripts/g14-gitops-render.mjs#tekton-native-primitive-ci",
"hwlab.pikastech.local/source-config": "scripts/gitops-render.mjs#tekton-native-primitive-ci",
"hwlab.pikastech.local/ci-contract": "tekton-native-primitive-tasks",
"hwlab.pikastech.local/policy": "native-per-service-taskrun-image-publish"
}
@@ -3349,7 +3339,7 @@ function tektonControlPlaneReconcilerCronJob(args) {
annotations: {
"hwlab.pikastech.local/source-branch": args.sourceBranch,
"hwlab.pikastech.local/gitops-branch": args.gitopsBranch,
"hwlab.pikastech.local/purpose": "render-and-apply-g14-ci-control-plane"
"hwlab.pikastech.local/purpose": "render-and-apply-node-ci-control-plane"
}
},
spec: {
@@ -4115,7 +4105,7 @@ function contentType(filePath) {
};
}
function argoProject(args = { lane: "g14" }) {
function argoProject(args = { lane: "node" }) {
if (isRuntimeLane(args.lane)) {
const namespace = namespaceNameForProfile(args.lane);
return {
@@ -4123,7 +4113,7 @@ function argoProject(args = { lane: "g14" }) {
kind: "AppProject",
metadata: { name: namespace, namespace: "argocd" },
spec: {
description: `HWLAB ${versionNameForRuntimeLane(args.lane)} additive GitOps project on G14; DEV/PROD remain outside this lane.`,
description: `HWLAB ${versionNameForRuntimeLane(args.lane)} additive GitOps project on target node; DEV/PROD remain outside this lane.`,
sourceRepos: [args.gitReadUrl],
destinations: [{ server: "https://kubernetes.default.svc", namespace }],
clusterResourceWhitelist: [{ group: "", kind: "Namespace" }],
@@ -4134,9 +4124,9 @@ function argoProject(args = { lane: "g14" }) {
return {
apiVersion: "argoproj.io/v1alpha1",
kind: "AppProject",
metadata: { name: "hwlab-g14", namespace: "argocd" },
metadata: { name: "hwlab-node", namespace: "argocd" },
spec: {
description: "HWLAB G14 GitOps project; D601 remains outside this project.",
description: "HWLAB node GitOps project; D601 remains outside this project.",
sourceRepos: [defaultSourceRepo],
destinations: [
{ server: "https://kubernetes.default.svc", namespace: "hwlab-dev" },
@@ -4152,7 +4142,7 @@ function argoApplication(args, profile = "dev") {
const namespace = namespaceNameForProfile(profile);
const runtimePath = runtimePathForProfile(profile);
const gitopsTarget = gitopsTargetForProfile(profile);
const project = isRuntimeLane(profile) ? namespace : "hwlab-g14";
const project = isRuntimeLane(profile) ? namespace : "hwlab-node";
const repoURL = isRuntimeLane(profile) ? args.gitReadUrl : args.sourceRepo;
return {
apiVersion: "argoproj.io/v1alpha1",
@@ -4171,12 +4161,12 @@ function argoApplication(args, profile = "dev") {
};
}
function g14FrpcManifest({ profile = "dev", source = null } = {}) {
function nodeFrpcManifest({ profile = "dev", source = null } = {}) {
const namespace = namespaceNameForProfile(profile);
const profileLabel = runtimeLabelForProfile(profile);
const deploymentName = isRuntimeLane(profile) ? `hwlab-${profile}-frpc` : profile === "prod" ? "hwlab-g14-prod-frpc" : "hwlab-g14-frpc";
const deploymentName = isRuntimeLane(profile) ? `hwlab-${profile}-frpc` : profile === "prod" ? "hwlab-node-prod-frpc" : "hwlab-node-frpc";
const configName = `${deploymentName}-config`;
const proxyPrefix = isRuntimeLane(profile) ? `hwlab-${profile}` : profile === "prod" ? "hwlab-g14-prod" : "hwlab-g14";
const proxyPrefix = isRuntimeLane(profile) ? `hwlab-${profile}` : profile === "prod" ? "hwlab-node-prod" : "hwlab-node";
const webRemotePort = isRuntimeLane(profile) ? defaultPortForRuntimeLane(profile) : profile === "prod" ? 18666 : 17666;
const edgeRemotePort = isRuntimeLane(profile) ? webRemotePort + 1 : profile === "prod" ? 18667 : 17667;
const labels = {
@@ -4260,16 +4250,17 @@ remotePort = ${edgeRemotePort}
};
}
function deepSeekProxyManifest({ profile = "dev", source, sourceBranch = "G14", sourceRepo = defaultSourceRepo, deploy = null, registryPrefix = defaultRegistryPrefix, catalog = null, useDeployImages = false, metricsSidecarSha256 = null } = {}) {
function deepSeekProxyManifest({ profile = "dev", source, sourceBranch = defaultBranch, sourceRepo = defaultSourceRepo, deploy = null, registryPrefix = defaultRegistryPrefix, catalog = null, useDeployImages = false, metricsSidecarSha256 = null } = {}) {
const namespace = namespaceNameForProfile(profile);
const bridgeServiceId = "hwlab-cloud-api";
const digestPin = profile === "v02";
const envReuseServiceIds = profile === "v02" ? new Set([bridgeServiceId]) : null;
const runtimeLane = isRuntimeLane(profile);
const digestPin = runtimeLane;
const envReuseServiceIds = runtimeLane ? envReuseServiceIdsForLane(deploy, profile) : null;
const bridgeImage = runtimeImageForService({ catalog, deploy, serviceId: bridgeServiceId, source, registryPrefix, useDeployImages, digestPin, envReuseServiceIds });
const bridgeCommit = runtimeCommitForService({ catalog, deploy, serviceId: bridgeServiceId, source, registryPrefix, useDeployImages, digestPin, envReuseServiceIds });
const bridgeImageTag = runtimeImageTagForService({ catalog, deploy, serviceId: bridgeServiceId, source, registryPrefix, useDeployImages, digestPin, envReuseServiceIds });
const bridgeBootMetadata = profile === "v02" && v02EnvReuseEnabled(catalog, bridgeServiceId, envReuseServiceIds)
? bootMetadataForService({ args: { sourceRepo, registryPrefix }, catalog, deployService: deployServiceForBoot(deploy, bridgeServiceId), serviceId: bridgeServiceId, source })
const bridgeBootMetadata = runtimeLane && v02EnvReuseEnabled(catalog, bridgeServiceId, envReuseServiceIds)
? bootMetadataForService({ args: { sourceRepo: deploy?.lanes?.[profile]?.sourceRepo || sourceRepo, registryPrefix }, catalog, deployService: deployServiceForBoot(deploy, bridgeServiceId, profile), serviceId: bridgeServiceId, source })
: null;
const labels = {
"app.kubernetes.io/name": "hwlab-deepseek-proxy",
@@ -4279,7 +4270,7 @@ function deepSeekProxyManifest({ profile = "dev", source, sourceBranch = "G14",
"hwlab.pikastech.local/service-id": "hwlab-deepseek-proxy",
"hwlab.pikastech.local/source-commit": source.full
};
if (profile === "v02") Object.assign(labels, v02MetricsLabels("hwlab-deepseek-proxy"));
if (runtimeLane) Object.assign(labels, v02MetricsLabels("hwlab-deepseek-proxy"));
const templateLabels = {
...labels,
"hwlab.pikastech.local/source-commit": bridgeCommit,
@@ -4291,7 +4282,7 @@ function deepSeekProxyManifest({ profile = "dev", source, sourceBranch = "G14",
"hwlab.pikastech.local/bridge-source-commit": bridgeCommit,
"hwlab.pikastech.local/bridge-image-tag": bridgeImageTag,
"hwlab.pikastech.local/bridge-image": bridgeImage,
...v02MetricsSidecarAnnotations(profile === "v02" ? metricsSidecarSha256 : null)
...v02MetricsSidecarAnnotations(runtimeLane ? metricsSidecarSha256 : null)
};
const responsesBridgeContainer = {
name: "responses-bridge",
@@ -4372,12 +4363,12 @@ function deepSeekProxyManifest({ profile = "dev", source, sourceBranch = "G14",
{ name: "moonbridge-config", mountPath: "/config", readOnly: true },
{ name: "moonbridge-data", mountPath: "/data" }
]
}, ...(profile === "v02" ? [v02MetricsSidecarContainer({ deploy, serviceId: "hwlab-deepseek-proxy", namespace, gitopsTarget: gitopsTargetForProfile(profile) })] : [])],
}, ...(runtimeLane ? [v02MetricsSidecarContainer({ deploy, serviceId: "hwlab-deepseek-proxy", namespace, gitopsTarget: gitopsTargetForProfile(profile) })] : [])],
volumes: [
{ name: "moonbridge-scripts", configMap: { name: "hwlab-deepseek-proxy-config" } },
{ name: "moonbridge-config", emptyDir: {} },
{ name: "moonbridge-data", emptyDir: {} },
...(profile === "v02" ? [v02MetricsSidecarVolume()] : [])
...(runtimeLane ? [v02MetricsSidecarVolume()] : [])
]
}
}
@@ -4387,7 +4378,7 @@ function deepSeekProxyManifest({ profile = "dev", source, sourceBranch = "G14",
apiVersion: "v1",
kind: "Service",
metadata: { name: "hwlab-deepseek-proxy", namespace, labels },
spec: { type: "ClusterIP", selector: { "app.kubernetes.io/name": "hwlab-deepseek-proxy" }, ports: [{ name: "http", port: 4000, targetPort: "http" }, ...(profile === "v02" ? [{ name: "metrics", port: 9100, targetPort: "metrics" }] : [])] }
spec: { type: "ClusterIP", selector: { "app.kubernetes.io/name": "hwlab-deepseek-proxy" }, ports: [{ name: "http", port: 4000, targetPort: "http" }, ...(runtimeLane ? [{ name: "metrics", port: 9100, targetPort: "metrics" }] : [])] }
}
]
};
@@ -4620,12 +4611,12 @@ function deviceAgent71FreqManifest({ profile = "dev", source, registryPrefix, ca
"hwlab.pikastech.local/component": "device-agent",
"hwlab.pikastech.local/device-id": "71-freq",
"hwlab.pikastech.local/environment": runtimeLabelForProfile(profile),
"hwlab.pikastech.local/gitops-target": "g14",
"hwlab.pikastech.local/gitops-target": "node",
"hwlab.pikastech.local/profile": runtimeLabelForProfile(profile),
"hwlab.pikastech.local/service-id": name,
"hwlab.pikastech.local/source-commit": source.full
};
const annotations = { "hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs" };
const annotations = { "hwlab.pikastech.local/rendered-by": "scripts/gitops-render.mjs" };
const selector = { "app.kubernetes.io/name": name };
const script = deviceAgent71FreqServerScript();
const scriptSha256 = createHash("sha256").update(script).digest("hex");
@@ -4795,7 +4786,7 @@ function v02OpenFgaManifest({ profile = "v02", source }) {
"hwlab.pikastech.local/service-id": name,
"hwlab.pikastech.local/source-commit": source.full
};
const annotations = { "hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs" };
const annotations = { "hwlab.pikastech.local/rendered-by": "scripts/gitops-render.mjs" };
const selector = { "app.kubernetes.io/name": name };
const env = [
{ name: "OPENFGA_DATASTORE_ENGINE", value: "postgres" },
@@ -4877,7 +4868,7 @@ function runtimeKustomization({ profile = "dev", includeDeviceAgent = profile ==
if (!isRuntimeLane(profile)) resources.splice(1, 0, "code-agent-codex-config.yaml");
if (isRuntimeLane(profile)) resources.push("postgres.yaml", "openfga.yaml", "observability.yaml");
if (includeDeviceAgent) resources.push("device-agent-71-freq.yaml");
resources.push("g14-frpc.yaml");
resources.push("node-frpc.yaml");
return {
apiVersion: "kustomize.config.k8s.io/v1beta1",
kind: "Kustomization",
@@ -4890,33 +4881,33 @@ function readme({ source, args }) {
if (args.lane === "v02") {
return `# HWLAB v0.2 GitOps CI/CD
This directory is generated by \`scripts/g14-gitops-render.mjs --lane v02\` from source branch \`${args.sourceBranch}\`.
This directory is generated by \`scripts/gitops-render.mjs --lane v02\` from source branch \`${args.sourceBranch}\`.
- Target: additive G14 v0.2 lane only.
- Target: additive node-scoped v0.2 lane only.
- CI: Tekton Pipeline \`hwlab-ci/hwlab-v02-ci-image-publish\`.
- Git mirror/relay: v0.2 CI reads source/catalog from \`${args.gitReadUrl}\` and writes GitOps promotion to \`${args.gitWriteUrl}\`; canonical repo \`${args.sourceRepo}\` remains the source identity and manual flush upstream.
- Artifact contract: runtime images come from \`${args.catalogPath}\`; source branch \`v0.2\` does not track this generated catalog.
- Manual trigger: run \`bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm\` from UniDesk to create commit-pinned PipelineRun \`hwlab-v02-ci-poll-<short12>\` for the current \`origin/v0.2\` commit.
- Manual trigger: run \`bun scripts/cli.ts hwlab node control-plane trigger-current --lane v02 --confirm\` from UniDesk to create commit-pinned PipelineRun \`hwlab-v02-ci-poll-<short12>\` for the current \`origin/v0.2\` commit.
- Branch split: source branch \`${args.sourceBranch}\` remains human-authored source; generated desired state is promoted to local mirror branch \`${args.gitopsBranch}\`.
- CD: Argo CD Application \`argocd/hwlab-g14-v02\` consumes \`${args.gitReadUrl}:${args.gitopsBranch}:deploy/gitops/g14/runtime-v02\` and deploys only to namespace \`hwlab-v02\`.
- CD: Argo CD Application \`argocd/hwlab-node-v02\` consumes \`${args.gitReadUrl}:${args.gitopsBranch}:deploy/gitops/node/runtime-v02\` and deploys only to namespace \`hwlab-v02\`.
- Public preview: FRP exposes v0.2 web \`${args.webEndpoint}\` / edge \`${args.runtimeEndpoint}\` through \`hwlab-v02/hwlab-v02-frpc\`.
- G14 DEV/PROD boundary: this lane does not modify \`G14\`, \`G14-gitops\`, \`hwlab-dev\`, \`hwlab-prod\`, 17666/17667 or 18666/18667.
- node DEV/PROD boundary: this lane does not modify archived DEV/PROD branches, \`hwlab-dev\`, \`hwlab-prod\`, 17666/17667 or 18666/18667.
- CronJob policy: v0.2 does not generate \`hwlab-v02-branch-poller\` or \`hwlab-v02-control-plane-reconciler\`; obsolete live CronJobs should be deleted by the UniDesk control-plane apply command.
- Gate policy: old DEV/D601/main gates do not enter this lane; new checks stay limited to the fixed branch, namespace, catalog, runtime path and Argo boundaries.
`;
}
return `# HWLAB G14 GitOps CI/CD
return `# HWLAB node GitOps CI/CD
This directory is generated from built-in Tekton-native CI primitive declarations plus human-authored \`deploy/deploy.yaml\`, CI-authored \`deploy/artifact-catalog.dev.json\`, and \`deploy/k8s/*\` by \`scripts/g14-gitops-render.mjs\`.
This directory is generated from built-in Tekton-native CI primitive declarations plus human-authored \`deploy/deploy.yaml\`, CI-authored \`deploy/artifact-catalog.dev.json\`, and \`deploy/k8s/*\` by \`scripts/gitops-render.mjs\`.
- Target: G14 k3s only.
- CI: Tekton Pipeline \`hwlab-ci/hwlab-g14-ci-image-publish\`.
- Target: node k3s only.
- CI: Tekton Pipeline \`hwlab-ci/hwlab-node-ci-image-publish\`.
- Artifact contract: runtime images come from \`deploy/artifact-catalog.dev.json\`; \`deploy/deploy.yaml\` only carries human-authored config such as env and topology.
- Branch split: source branch \`${args.sourceBranch}\` remains the watched code branch; generated desired state is promoted to \`${args.gitopsBranch}\`.
- CD: Argo CD Applications \`argocd/hwlab-g14-dev\` and \`argocd/hwlab-g14-prod\` consume \`${args.gitopsBranch}:deploy/gitops/g14/runtime-dev\` and \`runtime-prod\`.
- Public preview: FRP exposes G14 DEV web \`${args.webEndpoint}\` / edge \`${args.runtimeEndpoint}\` through \`hwlab-dev/hwlab-g14-frpc\`, and G14 PROD web \`${args.prodWebEndpoint}\` / edge \`${args.prodRuntimeEndpoint}\` through \`hwlab-prod/hwlab-g14-prod-frpc\`; D601 keeps \`:16666/:16667\`.
- Manual polling: run \`kubectl -n hwlab-ci create job --from=cronjob/hwlab-g14-branch-poller hwlab-g14-branch-poller-manual-$(date -u +%Y%m%d%H%M%S)\`; this reuses the same poller path and creates a commit-pinned Tekton PipelineRun only when needed.
- Control-plane reconcile: CronJob \`hwlab-ci/hwlab-g14-control-plane-reconciler\` polls \`${args.sourceBranch}\`, runs \`scripts/g14-gitops-render.mjs\`, and server-side-applies the generated Tekton RBAC/Pipeline/Poller/Reconciler manifests so CI control-plane changes do not require a manual render/apply step.
- CD: Argo CD Applications \`argocd/hwlab-node-dev\` and \`argocd/hwlab-node-prod\` consume \`${args.gitopsBranch}:deploy/gitops/node/runtime-dev\` and \`runtime-prod\`.
- Public preview: FRP exposes node DEV web \`${args.webEndpoint}\` / edge \`${args.runtimeEndpoint}\` through \`hwlab-dev/hwlab-node-frpc\`, and node PROD web \`${args.prodWebEndpoint}\` / edge \`${args.prodRuntimeEndpoint}\` through \`hwlab-prod/hwlab-node-prod-frpc\`; D601 keeps \`:16666/:16667\`.
- Manual polling: run \`kubectl -n hwlab-ci create job --from=cronjob/hwlab-node-branch-poller hwlab-node-branch-poller-manual-$(date -u +%Y%m%d%H%M%S)\`; this reuses the same poller path and creates a commit-pinned Tekton PipelineRun only when needed.
- Control-plane reconcile: CronJob \`hwlab-ci/hwlab-node-control-plane-reconciler\` polls \`${args.sourceBranch}\`, runs \`scripts/gitops-render.mjs\`, and server-side-applies the generated Tekton RBAC/Pipeline/Poller/Reconciler manifests so CI control-plane changes do not require a manual render/apply step.
- D601 boundary: this path does not target D601, does not use UniDesk Code Queue, and does not change D601 production traffic.
- Concurrency: CI builds are per source commit and do not acquire the legacy DEV CD Lease; Argo CD reconciles the Git desired state.
`;
@@ -4956,7 +4947,7 @@ async function plannedFiles(args) {
: null;
const metricsSidecarSha256 = metricsSidecarScript ? createHash("sha256").update(metricsSidecarScript).digest("hex") : null;
const baseLabels = { "hwlab.pikastech.local/gitops-target": settings.gitopsTarget, "hwlab.pikastech.local/source-commit": source.full };
const annotations = { "hwlab.pikastech.local/rendered-by": "scripts/g14-gitops-render.mjs" };
const annotations = { "hwlab.pikastech.local/rendered-by": "scripts/gitops-render.mjs" };
const files = new Map();
const putJson = (relative, value) => files.set(path.join(args.outDir, relative), jsonManifest(value));
const putText = (relative, value) => files.set(path.join(args.outDir, relative), textFile(value));
@@ -4966,7 +4957,7 @@ async function plannedFiles(args) {
}));
const sourceDescriptor = {
kind: isRuntimeLane(args.lane) ? `hwlab-${args.lane}-gitops-source` : "hwlab-g14-gitops-source",
kind: isRuntimeLane(args.lane) ? `hwlab-${args.lane}-gitops-source` : "hwlab-node-gitops-source",
sourceCommit: source.full,
sourceShortCommit: source.short,
sourceImageTag: source.imageTag,
@@ -4979,7 +4970,7 @@ async function plannedFiles(args) {
registryPrefix: args.registryPrefix,
runtimePaths: profiles.map((profile) => gitopsPathForProfile(args, profile)),
publicEndpoints,
frpDeployments: Object.fromEntries(profiles.map((profile) => [profile, `${namespaceNameForProfile(profile)}/${isRuntimeLane(profile) ? `hwlab-${profile}-frpc` : profile === "prod" ? "hwlab-g14-prod-frpc" : "hwlab-g14-frpc"}`])),
frpDeployments: Object.fromEntries(profiles.map((profile) => [profile, `${namespaceNameForProfile(profile)}/${isRuntimeLane(profile) ? `hwlab-${profile}-frpc` : profile === "prod" ? "hwlab-node-prod-frpc" : "hwlab-node-frpc"}`])),
tektonPipeline: `hwlab-ci/${settings.pipelineName}`,
argoApplications: profiles.map((profile) => `argocd/${argoApplicationName(profile)}`)
};
@@ -5025,7 +5016,7 @@ async function plannedFiles(args) {
if (isRuntimeLane(profile)) putJson(`${runtimePath}/openfga.yaml`, v02OpenFgaManifest({ profile, source }));
if (isRuntimeLane(profile)) putJson(`${runtimePath}/observability.yaml`, observabilityManifest({ deploy, profile, namespace, labels: profileLabels, annotations, metricsSidecarScript }));
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, source }));
putJson(`${runtimePath}/node-frpc.yaml`, nodeFrpcManifest({ profile, source }));
}
return { files, source };
}
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
@@ -11,6 +11,28 @@ function taskByName(pipeline, name) {
return task;
}
async function collectGeneratedFiles(root) {
const files = [];
async function walk(dir) {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) await walk(fullPath);
else if (entry.isFile()) files.push(fullPath);
}
}
await walk(root);
return files.sort();
}
function collectContainersFromItem(item) {
const podSpec = item?.spec?.template?.spec ?? item?.spec?.jobTemplate?.spec?.template?.spec ?? item?.spec?.template?.spec;
return [
...(podSpec?.initContainers ?? []),
...(podSpec?.containers ?? [])
];
}
function extractRuntimeNoopDecisionScript(gitopsPromoteScript) {
const marker = `runtime_noop_decision="$(node - "$runtime_path" "$old_runtime_snapshot" /workspace/source/affected-services.json <<'NODE'\n`;
const start = gitopsPromoteScript.indexOf(marker);
@@ -91,7 +113,7 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
const render = spawnSync(process.execPath, [
"scripts/run-bun.mjs",
"scripts/g14-gitops-render.mjs",
"scripts/gitops-render.mjs",
"--lane", "v02",
"--catalog-path", "deploy/artifact-catalog.v02.json",
"--image-tag-mode", "full",
@@ -130,7 +152,7 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
assert.match(runtimeReadyScript, /if \(!observedServiceIds\) \{/u);
assert.match(runtimeReadyScript, /const refresh = await waitForArgoRefresh\(\);\n if \(!refresh\.observed\) return;/u);
assert.ok(runtimeReady.taskSpec.params.some((param) => param.name === "argo-application"));
assert.ok(runtimeReady.params.some((param) => param.name === "argo-application" && param.value === "hwlab-g14-v02"));
assert.ok(runtimeReady.params.some((param) => param.name === "argo-application" && param.value === "hwlab-node-v02"));
assert.ok(runtimeReady.workspaces?.some((workspace) => workspace.name === "source" && workspace.workspace === "source"));
assert.ok(runtimeReady.taskSpec.workspaces?.some((workspace) => workspace.name === "source"));
@@ -152,7 +174,7 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
}
}), "runtime-identity-only");
assert.match(gitopsPromoteScript, /argo_hard_refresh_runtime_lane\(\) \{/u);
assert.match(gitopsPromoteScript, /ARGO_APPLICATION="hwlab-g14-\$lane" ARGO_FIELD_MANAGER="hwlab-\$lane-gitops-promote"/u);
assert.match(gitopsPromoteScript, /ARGO_APPLICATION="hwlab-node-\$lane" ARGO_FIELD_MANAGER="hwlab-\$lane-gitops-promote"/u);
assert.match(gitopsPromoteScript, /argocd\.argoproj\.io\/refresh": "hard"/u);
assert.match(gitopsPromoteScript, /applications\/" \+ encodeURIComponent\(application\) \+ "\?fieldManager=" \+ encodeURIComponent\(fieldManager\)/u);
assert.match(gitopsPromoteScript, /BatchMode=yes/u);
@@ -231,7 +253,7 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
assert.match(gitMirrorScript, /refs\/mirror-stage\/heads\/v0\.3/u);
assert.match(gitMirrorScript, /for gitops_branch in 'v0\.2-gitops' 'v0\.3-gitops'/u);
assert.match(gitMirrorScript, /keeps local \$name ahead of GitHub/u);
assert.match(gitMirror, /deploy\/artifact-catalog\.v03\.json\|deploy\/gitops\/g14\/runtime-v03\/\*/u);
assert.match(gitMirror, /deploy\/artifact-catalog\.v03\.json\|deploy\/gitops\/node\/runtime-v03\/\*/u);
assert.ok((gitMirrorJson.items || []).every((item) => item.kind !== "Secret"));
assert.ok((gitMirrorJson.items || []).every((item) => item.kind !== "CronJob"));
assert.ok((gitMirrorJson.items || []).every((item) => !item.data?.["ssh-privatekey"]));
@@ -281,7 +303,7 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
const collectScript = collectArtifacts.taskSpec.steps[0].script;
assert.match(collectScript, /--external-service-report-dir \/workspace\/source\/service-results/u);
const cloudWebBuildScript = taskByName(pipelineJson, "build-hwlab-cloud-web").taskSpec.steps.find((step) => step.name === "publish")?.script ?? "";
assert.match(cloudWebBuildScript, /node scripts\/g14-artifact-publish\.mjs --publish/u);
assert.match(cloudWebBuildScript, /node scripts\/artifact-publish\.mjs --publish/u);
const artifactPublishSource = await readFile("scripts/artifact-publish.mjs", "utf8");
assert.match(artifactPublishSource, /\["run", "--cwd", "web\/hwlab-cloud-web", "check"\]/u);
@@ -426,11 +448,11 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
assert.match(application, /git-mirror-http\.devops-infra\.svc\.cluster\.local/u);
assert.doesNotMatch(application, /git@github\.com:pikasTech\/HWLAB\.git/u);
for (const generatedFile of ["deepseek-proxy.yaml", "postgres.yaml", "openfga.yaml", "g14-frpc.yaml"]) {
for (const generatedFile of ["deepseek-proxy.yaml", "postgres.yaml", "openfga.yaml", "node-frpc.yaml"]) {
const content = await readFile(path.join(outDir, "runtime-v02", generatedFile), "utf8");
assert.match(content, /"hwlab\.pikastech\.local\/source-commit"/u);
}
const frpc = JSON.parse(await readFile(path.join(outDir, "runtime-v02", "g14-frpc.yaml"), "utf8"));
const frpc = JSON.parse(await readFile(path.join(outDir, "runtime-v02", "node-frpc.yaml"), "utf8"));
const frpcDeployment = frpc.items.find((item) => item.kind === "Deployment");
const frpcConfigLabel = frpcDeployment.spec.template.metadata.labels["hwlab.pikastech.local/config-sha256"];
const frpcConfigAnnotation = frpcDeployment.spec.template.metadata.annotations["hwlab.pikastech.local/config-sha256"];
@@ -485,3 +507,49 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
await rm(outDir, { recursive: true, force: true });
}
});
test("v03 render keeps node identity as data instead of generated structure", async () => {
const outDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-v03-render-test-"));
try {
const render = spawnSync(process.execPath, [
"scripts/run-bun.mjs",
"scripts/gitops-render.mjs",
"--lane", "v03",
"--node", "G14",
"--catalog-path", "deploy/artifact-catalog.v03.json",
"--image-tag-mode", "full",
"--source-branch", "v0.3",
"--gitops-branch", "v0.3-gitops",
"--out", outDir,
"--source-revision", "HEAD"
], { cwd: process.cwd(), encoding: "utf8" });
assert.equal(render.status, 0, render.stderr || render.stdout);
const generatedFiles = await collectGeneratedFiles(outDir);
const generatedPaths = generatedFiles.map((filePath) => path.relative(outDir, filePath));
assert.ok(generatedPaths.includes("runtime-v03/node-frpc.yaml"));
assert.ok(generatedPaths.includes("argocd/application-v03.yaml"));
assert.ok(generatedPaths.includes("tekton-v03/pipeline.yaml"));
for (const relativePath of generatedPaths) {
assert.doesNotMatch(relativePath, /g14/i, `legacy node token leaked into generated path ${relativePath}`);
}
const workloads = await readFile(path.join(outDir, "runtime-v03", "workloads.yaml"), "utf8");
const workloadsJson = JSON.parse(workloads);
const agentRunNodeEnv = (workloadsJson.items ?? [])
.flatMap((item) => collectContainersFromItem(item))
.flatMap((container) => container.env ?? [])
.filter((entry) => entry.name === "HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID");
assert.ok(agentRunNodeEnv.length > 0, "expected AgentRun provider id env to carry configured node id");
assert.deepEqual([...new Set(agentRunNodeEnv.map((entry) => entry.value))], ["G14"]);
const allowedNodeValue = /"name":\s*"HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID"[\s\S]{0,120}"value":\s*"G14"/gu;
for (const filePath of generatedFiles) {
const text = await readFile(filePath, "utf8");
const sanitized = text.replace(allowedNodeValue, "__CONFIGURED_NODE_ID__");
assert.doesNotMatch(sanitized, /g14|G14/u, `legacy node token leaked outside node data in ${path.relative(outDir, filePath)}`);
}
} finally {
await rm(outDir, { recursive: true, force: true });
}
});
+5 -4
View File
@@ -38,9 +38,9 @@ process.stdout.write(`${JSON.stringify({
copiedFiles: result.copiedFiles.map((file) => ({ from: file.from, to: file.to })),
config: laneSummary(result.config),
next: {
render: `node scripts/run-bun.mjs scripts/g14-gitops-render.mjs --lane ${result.lane} --source-branch ${result.config.sourceBranch} --gitops-branch ${result.config.gitopsBranch} --catalog-path ${result.config.artifactCatalog} --image-tag-mode ${result.config.imageTagMode ?? "full"}`,
controlPlane: `bun scripts/cli.ts hwlab g14 control-plane apply --lane ${result.lane} --confirm`,
trigger: `bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane ${result.lane} --confirm`
render: `node scripts/run-bun.mjs scripts/gitops-render.mjs --lane ${result.lane} --source-branch ${result.config.sourceBranch} --gitops-branch ${result.config.gitopsBranch} --catalog-path ${result.config.artifactCatalog} --image-tag-mode ${result.config.imageTagMode ?? "full"}`,
controlPlane: `bun scripts/cli.ts hwlab node control-plane apply --lane ${result.lane} --confirm`,
trigger: `bun scripts/cli.ts hwlab node control-plane trigger-current --lane ${result.lane} --confirm`
}
}, null, 2)}\n`);
@@ -136,7 +136,8 @@ async function configureLane(deploy, options) {
const target = deepTransform(clone(source), replacements);
target.name = targetInfo.versionName;
target.node = target.node ?? source.node ?? "G14";
target.node = target.node ?? source.node;
assert.ok(target.node, "target lane node is required; set deploy.lanes.<lane>.node or source lane node");
target.sourceBranch = targetInfo.sourceBranch;
target.gitopsBranch = targetInfo.gitopsBranch;
target.namespace = targetInfo.namespace;
+5 -5
View File
@@ -16,7 +16,7 @@ import { readStructuredFile } from "./src/structured-config.mjs";
const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const defaultLane = process.env.HWLAB_ARTIFACT_LANE || process.env.HWLAB_GITOPS_LANE || "g14";
const defaultLane = process.env.HWLAB_ARTIFACT_LANE || process.env.HWLAB_GITOPS_LANE || "node";
const defaultCatalogPath = process.env.HWLAB_ARTIFACT_CATALOG_PATH || "deploy/artifact-catalog.dev.json";
const defaultDeployPath = process.env.HWLAB_DEPLOY_MANIFEST_PATH || "deploy/deploy.yaml";
const defaultPublishReportPath = tempReportPath("dev-artifacts.json");
@@ -99,7 +99,7 @@ function parseArgs(argv) {
}
if (!args.help) {
assert.ok(args.lane === "g14" || isRuntimeArtifactLane(args.lane), `unknown artifact lane ${args.lane}`);
assert.ok(args.lane === "node" || isRuntimeArtifactLane(args.lane), `unknown artifact lane ${args.lane}`);
assert.ok(["short", "full"].includes(args.imageTagMode), `unknown image tag mode ${args.imageTagMode}`);
assert.notEqual(args.blocked && Boolean(args.publishReportPath), true, "--blocked and --publish-report are mutually exclusive");
assert.ok(args.blocked || args.publishReportPath, `choose --blocked or --publish-report ${defaultPublishReportPath}`);
@@ -122,7 +122,7 @@ function usage() {
"usage: node scripts/refresh-artifact-catalog.mjs --target-ref REF (--blocked|--publish-report PATH) [--write|--no-write]",
"",
"Preview or explicitly write DEV artifact catalog identity without faking digest evidence.",
"Default mode is read-only. --write is reserved for G14 Tekton GitOps promotion and never writes deploy/deploy.yaml.",
"Default mode is read-only. --write is reserved for node Tekton GitOps promotion and never writes deploy/deploy.yaml.",
"",
"examples:",
" node scripts/refresh-artifact-catalog.mjs --target-ref HEAD --blocked --no-write",
@@ -225,7 +225,7 @@ function artifactCatalogSkeleton({ args, deploy, target, registryPrefix = defaul
ciPublished: false,
registryVerified: false,
provenance: "not_available_until_publish",
note: `${environment} artifact catalog skeleton initialized without DEV/G14 fallback.`
note: `${environment} artifact catalog skeleton initialized without DEV/node fallback.`
},
healthContract: {
method: "GET",
@@ -426,7 +426,7 @@ function publishRecordsFromReport(report, target, serviceIds = SERVICE_IDS) {
return { records, requiredServiceIds, disabledServiceIds };
}
function refreshDocuments({ deploy, catalog, target, publishRecords, serviceInventory, provenancePath, registryPrefix, lane = "g14", serviceIds = SERVICE_IDS }) {
function refreshDocuments({ deploy, catalog, target, publishRecords, serviceInventory, provenancePath, registryPrefix, lane = "node", serviceIds = SERVICE_IDS }) {
const published = Boolean(publishRecords);
const records = publishRecords?.records ?? null;
const requiredIds = new Set(publishRecords?.requiredServiceIds ?? serviceInventory.requiredServiceIds);
+3 -3
View File
@@ -43,7 +43,7 @@ test("refresh accepts an absolute publish report path", async () => {
const reportPath = path.join(tempDir, "dev-artifacts.json");
await writeFile(reportPath, `${JSON.stringify({
reportVersion: "v1",
taskId: "g14-artifact-publish",
taskId: "artifact-publish",
commitId: shortCommitId,
artifactPublish: {
status: "published",
@@ -112,7 +112,7 @@ test("refresh is read-only by default", async () => {
const reportPath = path.join(tempDir, "dev-artifacts.json");
await writeFile(reportPath, `${JSON.stringify({
reportVersion: "v1",
taskId: "g14-artifact-publish",
taskId: "artifact-publish",
commitId: shortCommitId,
artifactPublish: {
status: "published",
@@ -170,7 +170,7 @@ test("refresh keeps reused service image tags from the publish report", async ()
const reportPath = path.join(tempDir, "dev-artifacts.json");
await writeFile(reportPath, `${JSON.stringify({
reportVersion: "v1",
taskId: "g14-artifact-publish",
taskId: "artifact-publish",
commitId: shortCommitId,
artifactPublish: {
status: "published",
@@ -141,7 +141,7 @@ export function evaluateArtifactRuntimeReadiness({
pass: commitsMatch(catalog.commitId, target.commitId),
type: "observability_blocker",
summary: `Artifact catalog commit=${catalog.shortCommitId}; target=${target.shortCommitId}.`,
nextTask: "Do not refresh source desired-state. Let G14 Tekton promotion write artifact catalog identity to G14-gitops, then verify Argo/runtime revision."
nextTask: "Do not refresh source desired-state. Let node Tekton promotion write artifact catalog identity to the configured GitOps branch, then verify Argo/runtime revision."
});
addCheck(checks, {
id: "artifact-report-catalog-match",
@@ -162,7 +162,7 @@ export function evaluateArtifactRuntimeReadiness({
pass: desiredState.matchesTarget,
type: "observability_blocker",
summary: `Desired-state deploy=${desiredState.deployCommitId}; target=${target.shortCommitId}; convergence=${desiredState.targetConvergence.state}; pending=${desiredState.targetConvergence.pendingTargetFields}.`,
nextTask: `Run node scripts/deploy-desired-state-plan.mjs --promotion-commit ${target.shortCommitId} --check as a read-only review, then rely on G14 Tekton promotion for catalog writes.`
nextTask: `Run node scripts/deploy-desired-state-plan.mjs --promotion-commit ${target.shortCommitId} --check as a read-only review, then rely on node Tekton promotion for catalog writes.`
});
addCheck(checks, {
id: "rollout-read-access",
@@ -303,7 +303,7 @@ export function buildArtifactRuntimeReadinessReport({
sourceContract: {
status: "pass",
documents: [
"docs/reference/g14-gitops-cicd.md"
"docs/reference/node-gitops-cicd.md"
],
summary: "Target commit, artifact publish/catalog identity, desired-state convergence, API runtime identity, and Cloud Web served runtime identity are separate gates."
},
@@ -1220,7 +1220,7 @@ function nextCommands({ canPublish, canApply, blockedReasons }) {
return [
"bun run --cwd web/hwlab-cloud-web build",
"bun run --cwd web/hwlab-cloud-web check",
"node scripts/g14-artifact-publish.mjs --preflight --no-report",
"node scripts/artifact-publish.mjs --preflight --no-report",
"node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked --no-write",
guardCommand
];
@@ -1229,7 +1229,7 @@ function nextCommands({ canPublish, canApply, blockedReasons }) {
return [
"node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --pretty",
"node scripts/deploy-desired-state-plan.mjs --promotion-commit <origin-main-sha> --check",
"node scripts/g14-gitops-render.mjs --no-write",
"node scripts/gitops-render.mjs --no-write",
guardCommand
];
}
@@ -1241,7 +1241,7 @@ function nextCommands({ canPublish, canApply, blockedReasons }) {
];
}
return [
"node scripts/g14-gitops-render.mjs --no-write",
"node scripts/gitops-render.mjs --no-write",
"node scripts/artifact-runtime-readiness-guard.mjs --target-ref origin/main --check --live --no-report"
];
}
+2 -2
View File
@@ -101,8 +101,8 @@ export const checkProfiles = Object.freeze({
{ id: "check-090-deploy-refresh-artifact-catalog", group: "deploy", command: ["node","--check","scripts/refresh-artifact-catalog.mjs"] },
{ id: "check-091-deploy-refresh-artifact-catalog-test", group: "deploy", command: ["node","--check","scripts/refresh-artifact-catalog.test.mjs"] },
{ id: "check-092-artifact-artifact-publish", group: "artifact", command: ["node","--check","scripts/artifact-publish.mjs"] },
{ id: "check-093-artifact-g14-artifact-publish", group: "artifact", command: ["node","--check","scripts/g14-artifact-publish.mjs"] },
{ id: "check-093a-artifact-g14-gitops-render-test", group: "artifact", command: ["node","scripts/run-bun.mjs","test","scripts/g14-gitops-render.test.ts"] },
{ id: "check-093-artifact-artifact-publish", group: "artifact", command: ["node","--check","scripts/artifact-publish.mjs"] },
{ id: "check-093a-artifact-gitops-render-test", group: "artifact", command: ["node","scripts/run-bun.mjs","test","scripts/gitops-render.test.ts"] },
{ id: "check-094-artifact-dev-runtime-base-image", group: "artifact", command: ["node","--check","scripts/dev-runtime-base-image.mjs"] },
{ id: "check-095-artifact-dev-artifact-services", group: "artifact", command: ["node","--check","scripts/src/dev-artifact-services.mjs"] },
{ id: "check-096-artifact-registry-capabilities", group: "artifact", command: ["node","--check","scripts/src/registry-capabilities.mjs"] },
@@ -7,7 +7,7 @@ import { readStructuredFileIfPresent } from "./structured-config.mjs";
const execFileAsync = promisify(execFile);
export const G14_CI_PLAN_VERSION = "v1";
export const CI_PLAN_VERSION = "v1";
export const ENV_REUSE_RUNTIME_MODE = "env-reuse-git-mirror-checkout";
export const V02_ENV_REUSE_RUNTIME_MODE = ENV_REUSE_RUNTIME_MODE;
@@ -41,12 +41,12 @@ export const DEFAULT_DOCS_ONLY_PATHS = Object.freeze([
export const DEFAULT_GITOPS_ONLY_PATHS = Object.freeze([
"deploy/gitops/",
"deploy/frp/",
"scripts/g14-gitops-render.mjs",
"scripts/gitops-render.mjs",
"scripts/src/runtime-lane.ts",
"tsconfig.gitops.json"
]);
export async function createG14CiPlan(options = {}) {
export async function createCiPlan(options = {}) {
const repoRoot = options.repoRoot ?? process.cwd();
const deployConfigPath = options.deployConfigPath ?? "deploy/deploy.yaml";
const targetRef = options.targetRef ?? "HEAD";
@@ -218,7 +218,7 @@ export async function createG14CiPlan(options = {}) {
const buildServices = services.filter((service) => service.buildRequired).map((service) => service.serviceId);
const reusedServices = services.filter((service) => !service.affected).map((service) => service.serviceId);
return {
planVersion: G14_CI_PLAN_VERSION,
planVersion: CI_PLAN_VERSION,
sourceCommitId,
shortCommitId,
baseRef,
@@ -227,7 +227,7 @@ export async function createG14CiPlan(options = {}) {
deployConfig: deployConfigPath,
artifactCatalog: artifactCatalog ? artifactCatalogPath : null,
lane,
ciContractSource: "scripts/g14-gitops-render.mjs",
ciContractSource: "scripts/gitops-render.mjs",
mode: "advisory-read-only",
currentRenderCompatible: true,
plannerMutatesDeployJson: false,
@@ -595,7 +595,7 @@ function resolveServiceIds({ options, laneConfig, lane }) {
const serviceIds = uniqueSorted(options.services.map(String));
const unknownServiceIds = serviceIds.filter((serviceId) => !allowedServiceIds.has(serviceId));
if (unknownServiceIds.length > 0) {
throw new Error(`unknown service IDs for G14 CI plan: ${unknownServiceIds.join(", ")}`);
throw new Error(`unknown service IDs for node CI plan: ${unknownServiceIds.join(", ")}`);
}
return { source: "cli-services", serviceIds };
}
+1 -1
View File
@@ -974,7 +974,7 @@ export async function buildDesiredStatePlan(options = {}) {
humanAuthoredTruth: [deployPath, workloadsPath],
artifactIdentityTruth: [catalogPath],
nonAuthoritativeEvidence: [artifactReportPath],
note: "This planner is read-only. deploy.yaml is human-authored config; artifact-catalog carries generated image identity and is written by G14 Tekton promotion only to G14-gitops. It does not prove image existence, registry reachability, a real DEV apply, or M3 DEV-LIVE hardware-loop evidence."
note: "This planner is read-only. deploy.yaml is human-authored config; artifact-catalog carries generated image identity and is written by node Tekton promotion only to the configured GitOps branch. It does not prove image existence, registry reachability, a real DEV apply, or M3 DEV-LIVE hardware-loop evidence."
},
cloudApiDb: cloudApiDb ?? {
status: "blocked",
+1 -1
View File
@@ -67,7 +67,7 @@ function provisionPlan({ exactBlocker = null, envValue = recommendedNode20Image,
`${envVarName}=${recommendedNode20Image} node scripts/preflight-dev-base-image.mjs`
],
artifactPublishDryRun: [
`${envVarName}=${envValue} node scripts/g14-artifact-publish.mjs --preflight --no-report`
`${envVarName}=${envValue} node scripts/artifact-publish.mjs --preflight --no-report`
]
}
};
+1 -1
View File
@@ -168,7 +168,7 @@ async function createDevGateReport(edgeHealth) {
documents: [
"docs/reference/dev-runtime-boundary.md",
"docs/reference/spec-v02-documentation-governance.md",
"docs/reference/g14-gitops-cicd.md",
"docs/reference/node-gitops-cicd.md",
"deploy/frp/README.md",
"deploy/master-edge/README.md"
],
@@ -327,7 +327,7 @@ function buildEvidence() {
category: "repository-governance",
level: "SOURCE",
status: "pass",
sources: ["scripts/repo-reports-guard.mjs", "package.json", "scripts/g14-gitops-render.mjs"],
sources: ["scripts/repo-reports-guard.mjs", "package.json", "scripts/gitops-render.mjs"],
commands: ["node scripts/repo-reports-guard.mjs"],
summary: "Repository report files are removed and guarded against recurrence."
})
+6 -6
View File
@@ -70,7 +70,7 @@ const artifactBuildInputFiles = new Set([
"package.json",
"package-lock.json",
"npm-shrinkwrap.json",
"scripts/g14-artifact-publish.mjs",
"scripts/artifact-publish.mjs",
"scripts/src/dev-artifact-services.mjs"
]);
@@ -674,10 +674,10 @@ async function validateLocalContracts(reporter, contracts, optionalReports, targ
function validateArtifactPublishReport(reporter, artifactReport, artifactIdentity, targetShortCommit, targetCommit, targetRef) {
if (!artifactReport) {
reporter.check("g14-artifact-publish-report", "registry", "blocked", "No G14 artifact publish report is present.");
reporter.check("artifact-publish-report", "registry", "blocked", "No node artifact publish report is present.");
reporter.block({
type: "runtime_blocker",
scope: "g14-artifact-publish",
scope: "artifact-publish",
summary: `${defaultArtifactReportPath} is missing, so the preflight has no publish evidence for HWLAB runtime artifacts.`,
nextTask: `Run the DEV artifact publish workflow and keep its temporary JSON at ${defaultArtifactReportPath}; do not commit report output.`
});
@@ -721,7 +721,7 @@ function validateArtifactPublishReport(reporter, artifactReport, artifactIdentit
const summary = sourceMatchesTarget
? "DEV artifact publish report covers all required enabled service IDs with immutable digests for the target commit, and records disabled service reasons."
: `DEV artifact publish report covers artifact source ${artifactIdentity.artifactSource.shortCommitId}; target ${targetRef} ${targetShortCommit} has no artifact build input changes since that source.`;
reporter.check("g14-artifact-publish-report", "registry", "pass", summary, [artifactIdentity?.targetCoverage].filter(Boolean));
reporter.check("artifact-publish-report", "registry", "pass", summary, [artifactIdentity?.targetCoverage].filter(Boolean));
return;
}
@@ -731,14 +731,14 @@ function validateArtifactPublishReport(reporter, artifactReport, artifactIdentit
? `source states resolved: ${sourcePresentCount} source-present, ${intentionallyDisabledCount} intentionally-disabled`
: "source states are not fully resolved";
reporter.check(
"g14-artifact-publish-report",
"artifact-publish-report",
"registry",
"blocked",
`DEV artifact publish report status is ${artifactPublish?.status ?? "missing"} with ${publishedCount}/${requiredServices.length || serviceCount} required services published for source ${artifactPublish?.sourceCommitId ?? artifactReport.commitId ?? "unknown"}; ${sourceSummary}.`
);
reporter.block({
type: "runtime_blocker",
scope: "g14-artifact-publish",
scope: "artifact-publish",
summary: `${defaultArtifactReportPath} does not prove all required HWLAB service artifacts for ${targetRef} ${targetShortCommit}; current status is ${artifactPublish?.status ?? "missing"} with ${publishedCount}/${requiredServices.length || serviceCount} required services published and ${sourceSummary}.`,
nextTask: "Complete DEV artifact publishing for every enabled required HWLAB service at the artifact source commit, or prove the target commit has no artifact build input changes; keep disabled services marked not_published with reasons."
});
+4 -4
View File
@@ -60,8 +60,8 @@ export type DeployConfig = {
lanes?: Record<string, RuntimeLaneConfig | undefined>;
};
const defaultNodeId = "G14";
const defaultGitopsRoot = "deploy/gitops/g14";
const defaultNodeId = "node";
const defaultGitopsRoot = "deploy/gitops/node";
const defaultPublicHost = "74.48.78.17";
export function isRuntimeLane(lane: unknown): lane is string {
@@ -143,8 +143,8 @@ export function applyRuntimeLaneDeployConfig(args: GitOpsRenderArgs, deploy: Dep
const configuredEndpoint = laneConfig.endpoint ?? defaultEndpoint;
const configuredWebEndpoint = laneConfig.publicEndpoints?.frontend ?? configuredEndpoint;
const configuredRuntimeEndpoint = laneConfig.publicEndpoints?.api ?? configuredEndpoint;
const defaultBranch = defaults.defaultBranch ?? "G14";
const defaultGitopsBranch = defaults.defaultGitopsBranch ?? "G14-gitops";
const defaultBranch = defaults.defaultBranch ?? "node";
const defaultGitopsBranch = defaults.defaultGitopsBranch ?? "node-gitops";
const defaultCatalogPath = defaults.defaultCatalogPath ?? "deploy/artifact-catalog.dev.json";
const defaultRuntimeEndpoint = defaults.defaultRuntimeEndpoint ?? "http://74.48.78.17:17667";
const defaultWebEndpoint = defaults.defaultWebEndpoint ?? "http://74.48.78.17:17666";
+4 -4
View File
@@ -249,10 +249,10 @@ function assertCloudApiImageRuntimeEntrypoints({ packageJson, artifactPublisher,
assertIncludes(packageJson.scripts?.check, `bun build ${expected} --target=bun --packages=external`, `${label} package check`);
assertIncludes(packageJson.scripts?.check, `bun ${expected} --check`, `${label} package runtime check`);
}
for (const expected of ["scripts/g14-artifact-publish.mjs", "runtime-dev", "runtime-prod"]) {
assertIncludes(gitopsRenderSource, expected, `${label} G14 GitOps render source`);
for (const expected of ["scripts/artifact-publish.mjs", "runtime-dev", "runtime-prod"]) {
assertIncludes(gitopsRenderSource, expected, `${label} node GitOps render source`);
}
for (const expected of ["name: \"prepare-source\"", "name: \"repo-reports-guard\"", "name: \"g14-contract-check\"", "name: \"codex-api-forwarder-check\""]) {
for (const expected of ["name: \"prepare-source\"", "name: \"repo-reports-guard\"", "name: \"node-contract-check\"", "name: \"codex-api-forwarder-check\""]) {
assertIncludes(gitopsRenderSource, expected, `${label} Tekton primitive CI task`);
}
}
@@ -446,7 +446,7 @@ assertDurableRuntimeRunbook(await readText("docs/reference/dev-runtime-boundary.
assertCloudApiImageRuntimeEntrypoints({
packageJson: await readJSON("package.json"),
artifactPublisher: await readText("scripts/artifact-publish.mjs"),
gitopsRenderSource: await readText("scripts/g14-gitops-render.mjs")
gitopsRenderSource: await readText("scripts/gitops-render.mjs")
});
console.log(