refactor: model runtime lanes by node config

This commit is contained in:
Codex Agent
2026-06-08 17:44:25 +08:00
parent 9ce42e810f
commit 1ce1a40292
6 changed files with 102 additions and 24 deletions
+9 -2
View File
@@ -79,9 +79,15 @@ profiles:
enabled: false
namespace: hwlab-prod
notes: Reserved placeholder only. PROD deployment and acceptance are explicitly out of scope.
nodes:
G14:
gitopsRoot: deploy/gitops/g14
sourceRepo: git@github.com:pikasTech/HWLAB.git
publicHost: 74.48.78.17
lanes:
v02:
name: v0.2
node: G14
sourceBranch: v0.2
gitopsBranch: v0.2-gitops
namespace: hwlab-v02
@@ -92,7 +98,7 @@ lanes:
legacyFrontend: http://74.48.78.17:19666
legacyApi: http://74.48.78.17:19667
artifactCatalog: deploy/artifact-catalog.v02.json
runtimePath: deploy/gitops/g14/runtime-v02
runtimePath: runtime-v02
imageTagMode: full
envReuseServices:
- hwlab-cloud-api
@@ -232,6 +238,7 @@ lanes:
HWLAB_CODE_AGENT_AGENTRUN_REPO_URL: http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git
v03:
name: v0.3
node: G14
sourceBranch: v0.3
gitopsBranch: v0.3-gitops
namespace: hwlab-v03
@@ -242,7 +249,7 @@ lanes:
legacyFrontend: http://74.48.78.17:19766
legacyApi: http://74.48.78.17:19767
artifactCatalog: deploy/artifact-catalog.v03.json
runtimePath: deploy/gitops/g14/runtime-v03
runtimePath: runtime-v03
imageTagMode: full
envReuseServices:
- hwlab-cloud-api
+33 -15
View File
@@ -15,7 +15,7 @@ import {
runtimeLaneMirrorSpecs,
runtimeLaneConfig,
versionNameForRuntimeLane
} from "./src/g14-gitops-lane.ts";
} from "./src/runtime-lane.ts";
import { readStructuredFile } from "./src/structured-config.mjs";
const execFileAsync = promisify(execFile);
@@ -136,7 +136,7 @@ const forbiddenCiFragments = [
];
const scanTargets = [
"scripts/g14-gitops-render.mjs",
"scripts/src/g14-gitops-lane.ts",
"scripts/src/runtime-lane.ts",
"scripts/artifact-publish.mjs",
"scripts/g14-artifact-publish.mjs",
"scripts/src/g14-ci-plan-lib.mjs",
@@ -203,6 +203,7 @@ function proxyEnv() {
function parseArgs(argv) {
const args = {
lane: process.env.HWLAB_GITOPS_LANE || "g14",
nodeId: process.env.HWLAB_NODE_ID || "G14",
outDir: defaultOutDir,
catalogPath: defaultCatalogPath,
imageTagMode: process.env.HWLAB_ARTIFACT_IMAGE_TAG_MODE || "short",
@@ -225,6 +226,7 @@ function parseArgs(argv) {
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--lane") args.lane = readOption(argv, ++index, arg);
else if (arg === "--node") args.nodeId = readOption(argv, ++index, arg);
else if (arg === "--out") args.outDir = readOption(argv, ++index, arg);
else if (arg === "--catalog-path") args.catalogPath = readOption(argv, ++index, arg);
else if (arg === "--image-tag-mode") args.imageTagMode = readOption(argv, ++index, arg);
@@ -285,6 +287,7 @@ function usage() {
"",
"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"}`,
` --out DIR default: ${defaultOutDir}`,
` --catalog-path PATH default: ${defaultCatalogPath}`,
" --image-tag-mode MODE short or full; v02 uses full source commit IDs",
@@ -735,6 +738,18 @@ function gitopsTargetForProfile(profile) {
return isRuntimeLane(profile) ? profile : "g14";
}
function gitopsRootForArgs(args) {
return String(args.outDir || defaultOutDir).replace(/\/+$/u, "");
}
function gitopsPathForProfile(args, profile) {
return `${gitopsRootForArgs(args)}/${runtimePathForProfile(profile)}`;
}
function gitopsPathFor(args, relativePath) {
return `${gitopsRootForArgs(args)}/${String(relativePath).replace(/^\/+/, "")}`;
}
function profileEndpoint(args, profile) {
if (isRuntimeLane(profile)) return { runtimeEndpoint: args.runtimeEndpoint, webEndpoint: args.webEndpoint };
return profile === "prod"
@@ -757,7 +772,7 @@ function laneNames(args) {
serviceAccount: `${namespace}-tekton-runner`,
pipelineRunPrefix: `${namespace}-ci-poll-`,
runtimeNamespace: namespace,
runtimePath: `deploy/gitops/g14/runtime-${args.lane}`,
runtimePath: gitopsPathForProfile(args, args.lane),
argoApplications: [`argocd/hwlab-g14-${args.lane}`]
};
}
@@ -769,7 +784,7 @@ function laneNames(args) {
serviceAccount: "hwlab-tekton-runner",
pipelineRunPrefix: "hwlab-g14-ci-poll-",
runtimeNamespace: "hwlab-dev",
runtimePath: "deploy/gitops/g14/runtime-dev",
runtimePath: gitopsPathForProfile(args, "dev"),
argoApplications: ["argocd/hwlab-g14-dev", "argocd/hwlab-g14-prod"]
};
}
@@ -3071,7 +3086,7 @@ function imagePublishTaskSet(args = { lane: "g14" }, deploy = null) {
function tektonPipeline(args = { lane: "g14" }, deploy = null) {
const settings = ciLaneSettings(args);
const runtimePath = `deploy/gitops/g14/${runtimePathForProfile(settings.profile)}`;
const runtimePath = gitopsPathForProfile(args, settings.profile);
return {
apiVersion: "tekton.dev/v1",
kind: "Pipeline",
@@ -3196,7 +3211,7 @@ function tektonPipeline(args = { lane: "g14" }, deploy = null) {
function tektonPipelineRunTemplate({ source, args }) {
const settings = ciLaneSettings(args);
const runtimePath = `deploy/gitops/g14/${runtimePathForProfile(settings.profile)}`;
const runtimePath = gitopsPathForProfile(args, settings.profile);
return {
apiVersion: "tekton.dev/v1",
kind: "PipelineRun",
@@ -3293,7 +3308,7 @@ function tektonPollerCronJob(args, deploy) {
{ name: "LANE", value: settings.lane },
{ name: "CATALOG_PATH", value: args.catalogPath || settings.catalogPath },
{ name: "IMAGE_TAG_MODE", value: args.imageTagMode || settings.imageTagMode },
{ name: "RUNTIME_PATH", value: `deploy/gitops/g14/${runtimePathForProfile(settings.profile)}` },
{ name: "RUNTIME_PATH", value: gitopsPathForProfile(args, settings.profile) },
{ name: "GIT_URL", value: args.sourceRepo },
{ name: "GIT_READ_URL", value: args.gitReadUrl },
{ name: "SOURCE_BRANCH", value: args.sourceBranch },
@@ -3365,7 +3380,7 @@ function tektonControlPlaneReconcilerCronJob(args) {
{ name: "CATALOG_PATH", value: args.catalogPath || settings.catalogPath },
{ name: "IMAGE_TAG_MODE", value: args.imageTagMode || settings.imageTagMode },
{ name: "TEKTON_DIR", value: settings.tektonDir },
{ name: "ARGO_FILES", value: settings.lane === "v02" ? "deploy/gitops/g14/argocd/project.yaml,deploy/gitops/g14/argocd/application-v02.yaml" : "" },
{ name: "ARGO_FILES", value: settings.lane === "v02" ? `${gitopsPathFor(args, "argocd/project.yaml")},${gitopsPathFor(args, "argocd/application-v02.yaml")}` : "" },
{ name: "FIELD_MANAGER", value: settings.controlPlaneReconcilerName },
{ name: "RECONCILE_EVENT", value: `${settings.gitopsTarget}-control-plane-reconciled` },
{ name: "GIT_URL", value: args.sourceRepo },
@@ -3434,8 +3449,8 @@ function uniqueStrings(values) {
return Array.from(new Set(values.filter((value) => typeof value === "string" && value.length > 0)));
}
function devopsInfraGitMirrorManifest(_args, deploy) {
const mirrorSpecs = runtimeLaneMirrorSpecs(deploy);
function devopsInfraGitMirrorManifest(args, deploy) {
const mirrorSpecs = runtimeLaneMirrorSpecs(deploy, { defaultNodeId: args.nodeId, defaultGitopsRoot: defaultOutDir });
assert.ok(mirrorSpecs.length > 0, "runtime lane git mirror requires at least one deploy.lanes entry");
const sourceBranches = uniqueStrings(mirrorSpecs.map((spec) => spec.sourceBranch));
const gitopsBranches = uniqueStrings(mirrorSpecs.map((spec) => spec.gitopsBranch));
@@ -4146,7 +4161,7 @@ function argoApplication(args, profile = "dev") {
},
spec: {
project,
source: { repoURL, targetRevision: args.gitopsBranch, path: `deploy/gitops/g14/${runtimePath}` },
source: { repoURL, targetRevision: args.gitopsBranch, path: gitopsPathForProfile(args, profile) },
destination: { server: "https://kubernetes.default.svc", namespace },
syncPolicy: { automated: { prune: isRuntimeLane(profile), selfHeal: true }, syncOptions: ["CreateNamespace=true", "ApplyOutOfSyncOnly=true"] }
}
@@ -4789,6 +4804,7 @@ function v02OpenFgaManifest({ profile = "v02", source }) {
{ name: "OPENFGA_GRPC_ADDR", value: "0.0.0.0:8081" }
];
const templateLabels = { ...labels, ...selector };
const runtimeAnnotations = { ...annotations, "argocd.argoproj.io/sync-wave": "2" };
return {
apiVersion: "v1",
kind: "List",
@@ -4802,7 +4818,8 @@ function v02OpenFgaManifest({ profile = "v02", source }) {
labels,
annotations: {
...annotations,
"argocd.argoproj.io/hook": "PreSync",
"argocd.argoproj.io/hook": "Sync",
"argocd.argoproj.io/sync-wave": "1",
"argocd.argoproj.io/hook-delete-policy": "BeforeHookCreation,HookSucceeded"
}
},
@@ -4820,7 +4837,7 @@ function v02OpenFgaManifest({ profile = "v02", source }) {
{
apiVersion: "apps/v1",
kind: "Deployment",
metadata: { name, namespace, labels, annotations },
metadata: { name, namespace, labels, annotations: runtimeAnnotations },
spec: {
replicas: 1,
selector: { matchLabels: selector },
@@ -4845,7 +4862,7 @@ function v02OpenFgaManifest({ profile = "v02", source }) {
{
apiVersion: "v1",
kind: "Service",
metadata: { name, namespace, labels, annotations },
metadata: { name, namespace, labels, annotations: runtimeAnnotations },
spec: { type: "ClusterIP", selector, ports: [{ name: "http", port: 8080, targetPort: "http" }, { name: "grpc", port: 8081, targetPort: "grpc" }] }
}
]
@@ -4915,6 +4932,7 @@ async function plannedFiles(args) {
defaultBranch,
defaultGitopsBranch,
defaultCatalogPath,
defaultGitopsRoot: defaultOutDir,
defaultRuntimeEndpoint,
defaultWebEndpoint,
defaultV02RuntimeEndpoint,
@@ -4956,7 +4974,7 @@ async function plannedFiles(args) {
gitReadUrl: args.gitReadUrl,
gitWriteUrl: args.gitWriteUrl,
registryPrefix: args.registryPrefix,
runtimePaths: profiles.map((profile) => `deploy/gitops/g14/${runtimePathForProfile(profile)}`),
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"}`])),
tektonPipeline: `hwlab-ci/${settings.pipelineName}`,
+2 -1
View File
@@ -136,6 +136,7 @@ async function configureLane(deploy, options) {
const target = deepTransform(clone(source), replacements);
target.name = targetInfo.versionName;
target.node = target.node ?? source.node ?? "G14";
target.sourceBranch = targetInfo.sourceBranch;
target.gitopsBranch = targetInfo.gitopsBranch;
target.namespace = targetInfo.namespace;
@@ -148,7 +149,7 @@ async function configureLane(deploy, options) {
legacyApi: `http://${options.host}:${targetInfo.apiPort}`
};
target.artifactCatalog = `deploy/artifact-catalog.${options.lane}.json`;
target.runtimePath = `deploy/gitops/g14/runtime-${options.lane}`;
target.runtimePath = `runtime-${options.lane}`;
target.imageTagMode = target.imageTagMode ?? "full";
deploy.lanes[options.lane] = target;
+1 -1
View File
@@ -42,7 +42,7 @@ export const DEFAULT_GITOPS_ONLY_PATHS = Object.freeze([
"deploy/gitops/",
"deploy/frp/",
"scripts/g14-gitops-render.mjs",
"scripts/src/g14-gitops-lane.ts",
"scripts/src/runtime-lane.ts",
"tsconfig.gitops.json"
]);
@@ -1,4 +1,6 @@
type RuntimeLaneDefaults = {
defaultNodeId?: string;
defaultGitopsRoot?: string;
defaultBranch?: string;
defaultGitopsBranch?: string;
defaultCatalogPath?: string;
@@ -10,6 +12,8 @@ type RuntimeLaneDefaults = {
export type GitOpsRenderArgs = {
lane: string;
nodeId?: string;
outDir: string;
sourceBranch: string;
gitopsBranch: string;
catalogPath: string;
@@ -21,9 +25,11 @@ export type GitOpsRenderArgs = {
};
export type RuntimeLaneConfig = {
node?: string;
sourceBranch?: string;
gitopsBranch?: string;
artifactCatalog?: string;
runtimePath?: string;
imageTagMode?: string;
endpoint?: string;
publicEndpoints?: {
@@ -34,6 +40,12 @@ export type RuntimeLaneConfig = {
sourceRepo?: string;
};
export type NodeConfig = {
gitopsRoot?: string;
sourceRepo?: string;
publicHost?: string;
};
export type RuntimeLaneMirrorSpec = {
lane: string;
sourceBranch: string;
@@ -43,9 +55,12 @@ export type RuntimeLaneMirrorSpec = {
};
export type DeployConfig = {
nodes?: Record<string, NodeConfig | undefined>;
lanes?: Record<string, RuntimeLaneConfig | undefined>;
};
const defaultNodeId = "G14";
const defaultGitopsRoot = "deploy/gitops/g14";
const defaultPublicHost = "74.48.78.17";
export function isRuntimeLane(lane: unknown): lane is string {
@@ -71,6 +86,28 @@ export function runtimePathForRuntimeLane(lane: string): string {
return `runtime-${lane}`;
}
export function nodeConfig(deploy: DeployConfig, nodeId: string): NodeConfig {
const config = deploy.nodes?.[nodeId];
return isObject(config) ? config : {};
}
export function nodeIdForRuntimeLane(deploy: DeployConfig, lane: string, defaults: RuntimeLaneDefaults = {}): string {
const laneConfig = runtimeLaneConfig(deploy, lane);
return laneConfig.node ?? defaults.defaultNodeId ?? defaultNodeId;
}
export function gitopsRootForNode(deploy: DeployConfig, nodeId: string, defaults: RuntimeLaneDefaults = {}): string {
const config = nodeConfig(deploy, nodeId);
return normalizeRepoPath(config.gitopsRoot ?? defaults.defaultGitopsRoot ?? defaultGitopsRoot);
}
export function runtimeGitopsPathForRuntimeLane(deploy: DeployConfig, lane: string, defaults: RuntimeLaneDefaults = {}): string {
const laneConfig = runtimeLaneConfig(deploy, lane);
const runtimePath = normalizeRepoPath(laneConfig.runtimePath ?? runtimePathForRuntimeLane(lane));
if (runtimePath.startsWith("deploy/")) return runtimePath;
return joinRepoPath(gitopsRootForNode(deploy, nodeIdForRuntimeLane(deploy, lane, defaults), defaults), runtimePath);
}
export function defaultPortForRuntimeLane(lane: string): number {
return 19466 + runtimeLaneMinor(lane) * 100;
}
@@ -95,8 +132,12 @@ export function applyRuntimeLaneDeployConfig(args: GitOpsRenderArgs, deploy: Dep
if (!isRuntimeLane(args.lane)) return args;
const result: GitOpsRenderArgs = { ...args };
const laneConfig = runtimeLaneConfig(deploy, args.lane);
const nodeId = nodeIdForRuntimeLane(deploy, args.lane, defaults);
const node = nodeConfig(deploy, nodeId);
const versionName = versionNameForRuntimeLane(args.lane);
const defaultEndpointOptions = defaults.defaultV02RuntimeEndpoint ? { v02Endpoint: defaults.defaultV02RuntimeEndpoint } : {};
const defaultEndpointOptions: { host?: string; v02Endpoint?: string } = {};
if (node.publicHost) defaultEndpointOptions.host = node.publicHost;
if (defaults.defaultV02RuntimeEndpoint) defaultEndpointOptions.v02Endpoint = defaults.defaultV02RuntimeEndpoint;
const defaultEndpoint = defaultEndpointForRuntimeLane(args.lane, defaultEndpointOptions);
const configuredEndpoint = laneConfig.endpoint ?? defaultEndpoint;
const configuredWebEndpoint = laneConfig.publicEndpoints?.frontend ?? configuredEndpoint;
@@ -107,19 +148,22 @@ export function applyRuntimeLaneDeployConfig(args: GitOpsRenderArgs, deploy: Dep
const defaultRuntimeEndpoint = defaults.defaultRuntimeEndpoint ?? "http://74.48.78.17:17667";
const defaultWebEndpoint = defaults.defaultWebEndpoint ?? "http://74.48.78.17:17666";
const defaultSourceRepo = defaults.defaultSourceRepo ?? "git@github.com:pikasTech/HWLAB.git";
const defaultOutDir = defaults.defaultGitopsRoot ?? defaultGitopsRoot;
const defaultCatalog = `deploy/artifact-catalog.${args.lane}.json`;
if (!result.nodeId || result.nodeId === defaultNodeId) result.nodeId = nodeId;
if (result.outDir === defaultOutDir) result.outDir = gitopsRootForNode(deploy, nodeId, defaults);
if (result.sourceBranch === defaultBranch || result.sourceBranch === versionName) result.sourceBranch = laneConfig.sourceBranch ?? versionName;
if (result.gitopsBranch === defaultGitopsBranch || result.gitopsBranch === `${versionName}-gitops`) result.gitopsBranch = laneConfig.gitopsBranch ?? `${versionName}-gitops`;
if (result.catalogPath === defaultCatalogPath || result.catalogPath === defaultCatalog) result.catalogPath = laneConfig.artifactCatalog ?? defaultCatalog;
if (result.imageTagMode === "full") result.imageTagMode = laneConfig.imageTagMode ?? result.imageTagMode;
if (result.runtimeEndpoint === defaultRuntimeEndpoint || result.runtimeEndpoint === defaultEndpoint) result.runtimeEndpoint = configuredRuntimeEndpoint;
if (result.webEndpoint === defaultWebEndpoint || result.webEndpoint === defaultEndpoint) result.webEndpoint = configuredWebEndpoint;
if (result.sourceRepo === defaultSourceRepo && laneConfig.sourceRepo) result.sourceRepo = laneConfig.sourceRepo;
if (result.sourceRepo === defaultSourceRepo) result.sourceRepo = laneConfig.sourceRepo ?? node.sourceRepo ?? result.sourceRepo;
return result;
}
export function runtimeLaneMirrorSpecs(deploy: DeployConfig): RuntimeLaneMirrorSpec[] {
export function runtimeLaneMirrorSpecs(deploy: DeployConfig, defaults: RuntimeLaneDefaults = {}): RuntimeLaneMirrorSpec[] {
return Object.entries(deploy.lanes ?? {})
.filter(([lane, config]) => isRuntimeLane(lane) && isObject(config))
.sort(([left], [right]) => runtimeLaneMinor(left) - runtimeLaneMinor(right))
@@ -131,11 +175,19 @@ export function runtimeLaneMirrorSpecs(deploy: DeployConfig): RuntimeLaneMirrorS
sourceBranch: laneConfig.sourceBranch ?? versionName,
gitopsBranch: laneConfig.gitopsBranch ?? `${versionName}-gitops`,
artifactCatalog: laneConfig.artifactCatalog ?? `deploy/artifact-catalog.${lane}.json`,
runtimePath: `deploy/gitops/g14/${runtimePathForRuntimeLane(lane)}`,
runtimePath: runtimeGitopsPathForRuntimeLane(deploy, lane, defaults),
};
});
}
function normalizeRepoPath(value: string): string {
return value.replaceAll("\\\\", "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
}
function joinRepoPath(left: string, right: string): string {
return `${normalizeRepoPath(left)}/${normalizeRepoPath(right).replace(/^\/+/, "")}`;
}
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
+1 -1
View File
@@ -19,6 +19,6 @@
"useUnknownInCatchVariables": true
},
"include": [
"scripts/src/g14-gitops-lane.ts"
"scripts/src/runtime-lane.ts"
]
}