feat: add hwlab v02 control-plane cli
This commit is contained in:
+1
-1
@@ -57,7 +57,7 @@ export function rootHelp(): unknown {
|
||||
{ command: "auth-broker contract|health --dry-run|credential-request --dry-run|pr-preflight --dry-run", description: "Inspect the P0 Rust auth broker and CLI adapter contract without reading token values, writing GitHub, or starting services." },
|
||||
{ command: "gh preflight|auth|issue|pr", description: "Run safe GitHub issue and PR CRUD/lifecycle operations through REST with body-file update replace/append, comment delete, token diagnostics, PR closeout preflight, hard delete unsupported, and guarded PR merge." },
|
||||
{ command: "commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run|prompt-lint --kind gpt55-pr", description: "Host Codex commander skeleton contract, no-daemon smoke plan, dry-run approval preview, and advisory GPT-5.5 PR prompt boundary lint without live bridges, message sends, or submit gating." },
|
||||
{ command: "hwlab g14 monitor-prs", description: "Start a fire-and-forget monitor that watches HWLAB PRs targeting G14, merges ready PRs through UniDesk gh, waits for G14 Tekton/GitOps/Argo DEV rollout, and appends the #7-indexed daily brief." },
|
||||
{ command: "hwlab g14 monitor-prs | hwlab g14 control-plane status|apply|rerun-current --lane v02 | hwlab g14 tools-image status|build", description: "Start the G14 PR monitor, run bounded v0.2 Tekton/Argo control-plane actions, or build/status fixed HWLAB CI tools images through UniDesk G14 routes." },
|
||||
{ command: "hwlab cd audit --env dev | hwlab cd status --env dev | hwlab cd apply --env dev --dry-run", description: "Legacy D601 HWLAB DEV CD wrapper kept for explicit old-path diagnostics; current HWLAB rollout uses G14 GitOps." },
|
||||
{ command: "code-agent-sandbox", description: "Independent Code Agent Sandbox service skeleton for adapter, mode, and credential-boundary diagnostics." },
|
||||
{ command: "schedule list|get|runs|run|retry-run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run <id> supports --wait-ms N and retry-run reuses the failed run's schedule." },
|
||||
|
||||
+496
-2
@@ -8,10 +8,20 @@ const HWLAB_REPO = "pikasTech/HWLAB";
|
||||
const G14_SOURCE_BRANCH = "G14";
|
||||
const G14_PROVIDER = "G14";
|
||||
const G14_WORKSPACE = "/root/hwlab";
|
||||
const V02_SOURCE_BRANCH = "v0.2";
|
||||
const V02_WORKSPACE = "/root/hwlab-v02";
|
||||
const DEV_NAMESPACE = "hwlab-dev";
|
||||
const CI_NAMESPACE = "hwlab-ci";
|
||||
const ARGO_NAMESPACE = "argocd";
|
||||
const DEV_APP = "hwlab-g14-dev";
|
||||
const V02_APP = "hwlab-g14-v02";
|
||||
const V02_PIPELINE = "hwlab-v02-ci-image-publish";
|
||||
const V02_POLLER = "hwlab-v02-branch-poller";
|
||||
const V02_RECONCILER = "hwlab-v02-control-plane-reconciler";
|
||||
const V02_PIPELINERUN_PREFIX = "hwlab-v02-ci-poll";
|
||||
const V02_CONTROL_PLANE_FIELD_MANAGER = "unidesk-hwlab-v02-control-plane";
|
||||
const G14_CI_TOOLS_IMAGE_REPO = "127.0.0.1:5000/hwlab/hwlab-ci-node-tools";
|
||||
const G14_CI_TOOLS_BASE_TAG = "node22-alpine-v1";
|
||||
const DEFAULT_INTERVAL_SECONDS = 600;
|
||||
const DEFAULT_MAX_CYCLES = 0;
|
||||
const DEFAULT_TIMEOUT_SECONDS = 1800;
|
||||
@@ -38,6 +48,24 @@ interface G14RecordRolloutOptions {
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
interface G14ControlPlaneOptions {
|
||||
action: "status" | "apply" | "rerun-current";
|
||||
lane: "v02";
|
||||
dryRun: boolean;
|
||||
confirm: boolean;
|
||||
timeoutSeconds: number;
|
||||
}
|
||||
|
||||
interface G14ToolsImageOptions {
|
||||
action: "status" | "build";
|
||||
name: "ci-node-tools";
|
||||
tag: string;
|
||||
dockerfile: string;
|
||||
dryRun: boolean;
|
||||
confirm: boolean;
|
||||
timeoutSeconds: number;
|
||||
}
|
||||
|
||||
interface CommandJsonResult {
|
||||
ok: boolean;
|
||||
command: string[];
|
||||
@@ -131,6 +159,50 @@ function parseRecordRolloutOptions(args: string[]): G14RecordRolloutOptions {
|
||||
};
|
||||
}
|
||||
|
||||
function parseControlPlaneOptions(args: string[]): G14ControlPlaneOptions {
|
||||
const [actionRaw] = args;
|
||||
if (actionRaw !== "status" && actionRaw !== "apply" && actionRaw !== "rerun-current") {
|
||||
throw new Error("control-plane usage: status|apply|rerun-current --lane v02 [--dry-run|--confirm]");
|
||||
}
|
||||
const lane = optionValue(args, "--lane");
|
||||
if (lane !== "v02") throw new Error("control-plane currently requires --lane v02");
|
||||
const confirm = args.includes("--confirm");
|
||||
const explicitDryRun = args.includes("--dry-run");
|
||||
if (confirm && explicitDryRun) throw new Error("control-plane accepts only one of --confirm or --dry-run");
|
||||
return {
|
||||
action: actionRaw,
|
||||
lane: "v02",
|
||||
confirm,
|
||||
dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm,
|
||||
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600),
|
||||
};
|
||||
}
|
||||
|
||||
function parseToolsImageOptions(args: string[]): G14ToolsImageOptions {
|
||||
const [actionRaw] = args;
|
||||
if (actionRaw !== "status" && actionRaw !== "build") {
|
||||
throw new Error("tools-image usage: status|build --name ci-node-tools --tag <tag> [--dockerfile path] [--dry-run|--confirm]");
|
||||
}
|
||||
const name = optionValue(args, "--name") ?? "ci-node-tools";
|
||||
if (name !== "ci-node-tools") throw new Error("tools-image currently supports --name ci-node-tools");
|
||||
const tag = optionValue(args, "--tag") ?? G14_CI_TOOLS_BASE_TAG;
|
||||
if (!/^[A-Za-z0-9_.-]+$/u.test(tag)) throw new Error("--tag may only contain letters, numbers, dot, underscore, and dash");
|
||||
const dockerfile = optionValue(args, "--dockerfile") ?? "deploy/ci/hwlab-ci-node-tools.Dockerfile";
|
||||
if (dockerfile.startsWith("/") || dockerfile.includes("..")) throw new Error("--dockerfile must be a repo-relative path without '..'");
|
||||
const confirm = args.includes("--confirm");
|
||||
const explicitDryRun = args.includes("--dry-run");
|
||||
if (confirm && explicitDryRun) throw new Error("tools-image accepts only one of --confirm or --dry-run");
|
||||
return {
|
||||
action: actionRaw,
|
||||
name,
|
||||
tag,
|
||||
dockerfile,
|
||||
confirm,
|
||||
dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm,
|
||||
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 600, 1800),
|
||||
};
|
||||
}
|
||||
|
||||
function positiveIntegerOption(args: string[], name: string, defaultValue: number, maxValue: number): number {
|
||||
const index = args.indexOf(name);
|
||||
if (index === -1) return defaultValue;
|
||||
@@ -140,6 +212,10 @@ function positiveIntegerOption(args: string[], name: string, defaultValue: numbe
|
||||
return Math.min(value, maxValue);
|
||||
}
|
||||
|
||||
function shellQuote(value: string): string {
|
||||
return `'${value.replace(/'/gu, `'"'"'`)}'`;
|
||||
}
|
||||
|
||||
function commandJson(command: string[], timeoutMs = 60_000): CommandJsonResult {
|
||||
const result = runCommand(command, repoRoot, { timeoutMs });
|
||||
let parsed: unknown | null = null;
|
||||
@@ -233,6 +309,407 @@ function precheckWorkspace(): CommandJsonResult {
|
||||
return cliJson(["ssh", `${G14_PROVIDER}:${G14_WORKSPACE}`, "script", "--", "pwd; git fetch origin G14 --prune; git status --short --branch; git remote -v | sed -n '1,4p'"], 120_000);
|
||||
}
|
||||
|
||||
function v02WorkspaceScript(script: string, timeoutMs = 120_000): CommandJsonResult {
|
||||
return cliJson(["ssh", `${G14_PROVIDER}:${V02_WORKSPACE}`, "script", "--", script], timeoutMs);
|
||||
}
|
||||
|
||||
function g14K3s(args: string[], timeoutMs = 60_000): CommandJsonResult {
|
||||
return cliJson(["ssh", `${G14_PROVIDER}:k3s`, ...args], timeoutMs);
|
||||
}
|
||||
|
||||
function getV02Head(): string | null {
|
||||
const result = v02WorkspaceScript("git fetch origin v0.2 --prune >/dev/null 2>&1; git rev-parse origin/v0.2", 120_000);
|
||||
if (!isCommandSuccess(result)) return null;
|
||||
const output = String(nested(result.parsed, ["data", "stdout"]) ?? result.stdout).trim();
|
||||
const match = /[0-9a-f]{40}/iu.exec(output);
|
||||
return match?.[0] ?? null;
|
||||
}
|
||||
|
||||
function v02PipelineRunName(sourceCommit: string): string {
|
||||
return `${V02_PIPELINERUN_PREFIX}-${shortSha(sourceCommit)}`;
|
||||
}
|
||||
|
||||
function v02ManualPollerJobName(sourceCommit: string): string {
|
||||
return `hwlab-v02-branch-poller-manual-${shortSha(sourceCommit)}`;
|
||||
}
|
||||
|
||||
function getPipelineRunCompact(name: string): Record<string, unknown> {
|
||||
const result = g14K3s([
|
||||
"kubectl",
|
||||
"get",
|
||||
"pipelinerun",
|
||||
"-n",
|
||||
CI_NAMESPACE,
|
||||
name,
|
||||
"-o",
|
||||
"jsonpath={.status.conditions[0].status}{\"\\n\"}{.status.conditions[0].reason}{\"\\n\"}{.status.conditions[0].message}{\"\\n\"}",
|
||||
], 60_000);
|
||||
const text = statusText(result);
|
||||
const [status = "", reason = "", message = ""] = text.split(/\r?\n/u);
|
||||
const notFound = !isCommandSuccess(result) && /not found/iu.test(`${result.stdout}\n${result.stderr}`);
|
||||
return {
|
||||
ok: isCommandSuccess(result),
|
||||
exists: isCommandSuccess(result) || !notFound,
|
||||
pipelineRun: name,
|
||||
status: status || null,
|
||||
reason: reason || null,
|
||||
message: message || null,
|
||||
command: result.command,
|
||||
exitCode: result.exitCode,
|
||||
stderr: result.stderr.trim().slice(0, 2000),
|
||||
};
|
||||
}
|
||||
|
||||
function runV02RenderCheck(sourceCommit: string): CommandJsonResult {
|
||||
const renderDir = v02RenderDir(sourceCommit);
|
||||
return v02WorkspaceScript([
|
||||
"set -eu",
|
||||
"git fetch origin v0.2 --prune",
|
||||
"git checkout v0.2 >/dev/null 2>&1 || true",
|
||||
"git merge --ff-only origin/v0.2",
|
||||
`test "$(git rev-parse HEAD)" = ${shellQuote(sourceCommit)}`,
|
||||
`rm -rf ${shellQuote(renderDir)}`,
|
||||
`mkdir -p ${shellQuote(renderDir)}`,
|
||||
`node scripts/g14-gitops-render.mjs --lane v02 --source-revision ${shellQuote(sourceCommit)} --out ${shellQuote(renderDir)}`,
|
||||
`node scripts/g14-gitops-render.mjs --lane v02 --source-revision ${shellQuote(sourceCommit)} --out ${shellQuote(renderDir)} --check`,
|
||||
].join("\n"), 180_000);
|
||||
}
|
||||
|
||||
function v02RenderDir(sourceCommit: string): string {
|
||||
return `/tmp/hwlab-v02-control-plane-${shortSha(sourceCommit)}`;
|
||||
}
|
||||
|
||||
function applyV02ControlPlaneFiles(sourceCommit: string, dryRun: boolean, timeoutSeconds: number): CommandJsonResult {
|
||||
const renderDir = v02RenderDir(sourceCommit);
|
||||
return g14K3s([
|
||||
"kubectl",
|
||||
"apply",
|
||||
"--server-side",
|
||||
"--force-conflicts",
|
||||
`--field-manager=${V02_CONTROL_PLANE_FIELD_MANAGER}`,
|
||||
...(dryRun ? ["--dry-run=server"] : []),
|
||||
"-f",
|
||||
`${renderDir}/tekton-v02/rbac.yaml`,
|
||||
"-f",
|
||||
`${renderDir}/tekton-v02/pipeline.yaml`,
|
||||
"-f",
|
||||
`${renderDir}/tekton-v02/poller.yaml`,
|
||||
"-f",
|
||||
`${renderDir}/tekton-v02/control-plane-reconciler.yaml`,
|
||||
"-f",
|
||||
`${renderDir}/argocd/project.yaml`,
|
||||
"-f",
|
||||
`${renderDir}/argocd/application-v02.yaml`,
|
||||
], timeoutSeconds * 1000);
|
||||
}
|
||||
|
||||
function getV02PollerCronJob(): CommandJsonResult {
|
||||
return g14K3s([
|
||||
"kubectl",
|
||||
"get",
|
||||
"cronjob",
|
||||
"-n",
|
||||
CI_NAMESPACE,
|
||||
V02_POLLER,
|
||||
"-o",
|
||||
"jsonpath={.metadata.name}{\"\\n\"}{.spec.schedule}{\"\\n\"}",
|
||||
], 60_000);
|
||||
}
|
||||
|
||||
function deleteV02PipelineRun(pipelineRun: string): CommandJsonResult {
|
||||
return g14K3s(["kubectl", "delete", "pipelinerun", "-n", CI_NAMESPACE, pipelineRun, "--ignore-not-found=true"], 60_000);
|
||||
}
|
||||
|
||||
function deleteV02ManualPollerJob(jobName: string): CommandJsonResult {
|
||||
return g14K3s(["kubectl", "delete", "job", "-n", CI_NAMESPACE, jobName, "--ignore-not-found=true"], 60_000);
|
||||
}
|
||||
|
||||
function createV02ManualPollerJob(jobName: string): CommandJsonResult {
|
||||
return g14K3s(["kubectl", "create", "job", "-n", CI_NAMESPACE, `--from=cronjob/${V02_POLLER}`, jobName], 60_000);
|
||||
}
|
||||
|
||||
function getV02ManualPollerJob(jobName: string): CommandJsonResult {
|
||||
return g14K3s([
|
||||
"kubectl",
|
||||
"get",
|
||||
"job",
|
||||
"-n",
|
||||
CI_NAMESPACE,
|
||||
jobName,
|
||||
"-o",
|
||||
"jsonpath={.metadata.name}{\"\\n\"}{.status.conditions[0].type}{\"\\n\"}{.status.conditions[0].status}{\"\\n\"}",
|
||||
], 60_000);
|
||||
}
|
||||
|
||||
function v02ControlPlaneStatus(sourceCommit: string | null = getV02Head()): Record<string, unknown> {
|
||||
const pipelineRun = sourceCommit === null ? null : v02PipelineRunName(sourceCommit);
|
||||
const controlPlane = g14K3s([
|
||||
"kubectl",
|
||||
"get",
|
||||
"cronjob,pipeline,role,rolebinding,serviceaccount",
|
||||
"-n",
|
||||
CI_NAMESPACE,
|
||||
"-l",
|
||||
"hwlab.pikastech.local/gitops-target=v02",
|
||||
"-o",
|
||||
"name",
|
||||
], 60_000);
|
||||
const argo = g14K3s([
|
||||
"kubectl",
|
||||
"get",
|
||||
"application",
|
||||
"-n",
|
||||
ARGO_NAMESPACE,
|
||||
V02_APP,
|
||||
"-o",
|
||||
"jsonpath={.spec.source.targetRevision}{\"\\n\"}{.spec.source.path}{\"\\n\"}{.status.sync.revision}{\"\\n\"}{.status.sync.status}{\"\\n\"}{.status.health.status}{\"\\n\"}",
|
||||
], 60_000);
|
||||
const [targetRevision = "", path = "", syncRevision = "", syncStatus = "", health = ""] = statusText(argo).split(/\r?\n/u);
|
||||
return {
|
||||
ok: sourceCommit !== null && isCommandSuccess(controlPlane) && isCommandSuccess(argo),
|
||||
command: "hwlab g14 control-plane status --lane v02",
|
||||
lane: "v02",
|
||||
sourceCommit,
|
||||
expected: {
|
||||
workspace: V02_WORKSPACE,
|
||||
branch: V02_SOURCE_BRANCH,
|
||||
namespace: CI_NAMESPACE,
|
||||
runtimeNamespace: "hwlab-v02",
|
||||
pipeline: V02_PIPELINE,
|
||||
poller: V02_POLLER,
|
||||
reconciler: V02_RECONCILER,
|
||||
argoApplication: V02_APP,
|
||||
},
|
||||
controlPlane: {
|
||||
ok: isCommandSuccess(controlPlane),
|
||||
names: statusText(controlPlane).split(/\r?\n/u).map((line) => line.trim()).filter(Boolean),
|
||||
stderr: controlPlane.stderr.trim().slice(0, 2000),
|
||||
},
|
||||
argo: {
|
||||
ok: isCommandSuccess(argo),
|
||||
raw: statusText(argo),
|
||||
fields: { targetRevision, path, syncRevision, syncStatus, health },
|
||||
stderr: argo.stderr.trim().slice(0, 2000),
|
||||
},
|
||||
pipelineRun: pipelineRun === null ? null : getPipelineRunCompact(pipelineRun),
|
||||
};
|
||||
}
|
||||
|
||||
function runV02ControlPlane(options: G14ControlPlaneOptions): Record<string, unknown> {
|
||||
const sourceCommit = getV02Head();
|
||||
if (sourceCommit === null) {
|
||||
return { ok: false, command: `hwlab g14 control-plane ${options.action} --lane v02`, degradedReason: "v02-head-unresolved", workspace: V02_WORKSPACE };
|
||||
}
|
||||
if (options.action === "status") return v02ControlPlaneStatus(sourceCommit);
|
||||
if (options.action === "apply") {
|
||||
const renderCheck = runV02RenderCheck(sourceCommit);
|
||||
if (!isCommandSuccess(renderCheck)) {
|
||||
return {
|
||||
ok: false,
|
||||
command: `hwlab g14 control-plane ${options.action} --lane v02`,
|
||||
phase: "source-render-check",
|
||||
sourceCommit,
|
||||
renderCheck,
|
||||
};
|
||||
}
|
||||
const apply = applyV02ControlPlaneFiles(sourceCommit, options.dryRun, options.timeoutSeconds);
|
||||
return {
|
||||
ok: isCommandSuccess(apply),
|
||||
command: "hwlab g14 control-plane apply --lane v02",
|
||||
lane: "v02",
|
||||
mode: options.dryRun ? "dry-run" : "confirmed-apply",
|
||||
sourceCommit,
|
||||
renderDir: v02RenderDir(sourceCommit),
|
||||
renderCheck: commandData(renderCheck),
|
||||
apply,
|
||||
status: v02ControlPlaneStatus(sourceCommit),
|
||||
next: options.dryRun
|
||||
? { apply: "bun scripts/cli.ts hwlab g14 control-plane apply --lane v02 --confirm" }
|
||||
: { rerunCurrent: "bun scripts/cli.ts hwlab g14 control-plane rerun-current --lane v02 --confirm" },
|
||||
};
|
||||
}
|
||||
const before = getPipelineRunCompact(v02PipelineRunName(sourceCommit));
|
||||
if (options.dryRun) {
|
||||
const poller = getV02PollerCronJob();
|
||||
return {
|
||||
ok: isCommandSuccess(poller),
|
||||
command: "hwlab g14 control-plane rerun-current --lane v02",
|
||||
lane: "v02",
|
||||
mode: "dry-run",
|
||||
sourceCommit,
|
||||
pipelineRun: v02PipelineRunName(sourceCommit),
|
||||
manualJob: v02ManualPollerJobName(sourceCommit),
|
||||
before,
|
||||
poller,
|
||||
next: { rerunCurrent: "bun scripts/cli.ts hwlab g14 control-plane rerun-current --lane v02 --confirm" },
|
||||
};
|
||||
}
|
||||
if (before.status === "True" || before.status === "Unknown") {
|
||||
return {
|
||||
ok: false,
|
||||
command: "hwlab g14 control-plane rerun-current --lane v02",
|
||||
lane: "v02",
|
||||
mode: "confirmed-rerun",
|
||||
sourceCommit,
|
||||
pipelineRun: v02PipelineRunName(sourceCommit),
|
||||
before,
|
||||
degradedReason: "refuse-active-or-successful-pipelinerun",
|
||||
};
|
||||
}
|
||||
const deletePipelineRun = deleteV02PipelineRun(v02PipelineRunName(sourceCommit));
|
||||
const deleteJob = deleteV02ManualPollerJob(v02ManualPollerJobName(sourceCommit));
|
||||
const createJob = isCommandSuccess(deletePipelineRun) && isCommandSuccess(deleteJob)
|
||||
? createV02ManualPollerJob(v02ManualPollerJobName(sourceCommit))
|
||||
: null;
|
||||
const job = createJob !== null && isCommandSuccess(createJob) ? getV02ManualPollerJob(v02ManualPollerJobName(sourceCommit)) : null;
|
||||
return {
|
||||
ok: isCommandSuccess(deletePipelineRun) && isCommandSuccess(deleteJob) && createJob !== null && isCommandSuccess(createJob),
|
||||
command: "hwlab g14 control-plane rerun-current --lane v02",
|
||||
lane: "v02",
|
||||
mode: "confirmed-rerun",
|
||||
sourceCommit,
|
||||
pipelineRun: v02PipelineRunName(sourceCommit),
|
||||
manualJob: v02ManualPollerJobName(sourceCommit),
|
||||
before,
|
||||
deletePipelineRun,
|
||||
deleteJob,
|
||||
createJob,
|
||||
job,
|
||||
after: getPipelineRunCompact(v02PipelineRunName(sourceCommit)),
|
||||
};
|
||||
}
|
||||
|
||||
function g14HostScript(script: string, timeoutMs = 120_000): CommandJsonResult {
|
||||
return cliJson(["ssh", G14_PROVIDER, "script", "--", script], timeoutMs);
|
||||
}
|
||||
|
||||
function g14CiToolsImage(tag: string): string {
|
||||
return `${G14_CI_TOOLS_IMAGE_REPO}:${tag}`;
|
||||
}
|
||||
|
||||
function runG14ToolsImageStatus(options: G14ToolsImageOptions): Record<string, unknown> {
|
||||
const image = g14CiToolsImage(options.tag);
|
||||
const script = [
|
||||
"set -u",
|
||||
`image=${shellQuote(image)}`,
|
||||
"exists=false",
|
||||
"image_id=",
|
||||
"created=",
|
||||
"size=",
|
||||
"node_version=",
|
||||
"npm_version=",
|
||||
"bun_version=",
|
||||
"git_version=",
|
||||
"docker_version=",
|
||||
"python_version=",
|
||||
"registry_tags=",
|
||||
"if docker image inspect \"$image\" >/tmp/hwlab-tools-image-inspect.json 2>/tmp/hwlab-tools-image-inspect.err; then",
|
||||
" exists=true",
|
||||
" image_id=$(docker image inspect \"$image\" --format '{{.Id}}' 2>/dev/null || true)",
|
||||
" created=$(docker image inspect \"$image\" --format '{{.Created}}' 2>/dev/null || true)",
|
||||
" size=$(docker image inspect \"$image\" --format '{{.Size}}' 2>/dev/null || true)",
|
||||
" node_version=$(docker run --rm \"$image\" node --version 2>/dev/null || true)",
|
||||
" npm_version=$(docker run --rm \"$image\" npm --version 2>/dev/null || true)",
|
||||
" bun_version=$(docker run --rm \"$image\" bun --version 2>/dev/null || true)",
|
||||
" git_version=$(docker run --rm \"$image\" git --version 2>/dev/null || true)",
|
||||
" docker_version=$(docker run --rm \"$image\" docker --version 2>/dev/null || true)",
|
||||
" python_version=$(docker run --rm \"$image\" python3 --version 2>/dev/null || true)",
|
||||
"fi",
|
||||
`registry_tags=$(curl -fsS --max-time 10 http://127.0.0.1:5000/v2/hwlab/hwlab-ci-node-tools/tags/list 2>/dev/null || true)`,
|
||||
"export image exists image_id created size node_version npm_version bun_version git_version docker_version python_version registry_tags",
|
||||
"node - <<'NODE'",
|
||||
"const fs = require('node:fs');",
|
||||
"const env = process.env;",
|
||||
"const payload = {",
|
||||
" image: env.image,",
|
||||
" exists: env.exists === 'true',",
|
||||
" imageId: env.image_id || null,",
|
||||
" created: env.created || null,",
|
||||
" sizeBytes: env.size ? Number(env.size) : null,",
|
||||
" tools: {",
|
||||
" node: env.node_version || null,",
|
||||
" npm: env.npm_version || null,",
|
||||
" bun: env.bun_version || null,",
|
||||
" git: env.git_version || null,",
|
||||
" docker: env.docker_version || null,",
|
||||
" python: env.python_version || null",
|
||||
" },",
|
||||
" registryTagsRaw: env.registry_tags || null",
|
||||
"};",
|
||||
"console.log(JSON.stringify(payload, null, 2));",
|
||||
"NODE",
|
||||
].join("\n");
|
||||
const result = g14HostScript(script, 180_000);
|
||||
let parsedStatus: unknown = null;
|
||||
const text = statusText(result);
|
||||
if (text.length > 0) {
|
||||
try {
|
||||
parsedStatus = JSON.parse(text) as unknown;
|
||||
} catch {
|
||||
parsedStatus = null;
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: isCommandSuccess(result) && record(parsedStatus).exists === true,
|
||||
command: "hwlab g14 tools-image status --name ci-node-tools",
|
||||
name: options.name,
|
||||
image,
|
||||
status: parsedStatus ?? text,
|
||||
result,
|
||||
};
|
||||
}
|
||||
|
||||
function runG14ToolsImageBuild(options: G14ToolsImageOptions): Record<string, unknown> {
|
||||
const image = g14CiToolsImage(options.tag);
|
||||
const script = [
|
||||
"set -eu",
|
||||
`cd ${shellQuote(V02_WORKSPACE)}`,
|
||||
"git fetch origin v0.2 --prune",
|
||||
"git checkout v0.2 >/dev/null 2>&1 || true",
|
||||
"git merge --ff-only origin/v0.2",
|
||||
`test -f ${shellQuote(options.dockerfile)}`,
|
||||
`image=${shellQuote(image)}`,
|
||||
`dockerfile=${shellQuote(options.dockerfile)}`,
|
||||
"echo \"{\\\"phase\\\":\\\"preflight\\\",\\\"image\\\":\\\"$image\\\",\\\"dockerfile\\\":\\\"$dockerfile\\\",\\\"commit\\\":\\\"$(git rev-parse HEAD)\\\"}\"",
|
||||
"export HTTP_PROXY=http://127.0.0.1:10808 HTTPS_PROXY=http://127.0.0.1:10808 http_proxy=http://127.0.0.1:10808 https_proxy=http://127.0.0.1:10808",
|
||||
"export NO_PROXY=localhost,127.0.0.1,::1,host.docker.internal,74.48.78.17,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,10.42.0.0/16,10.43.0.0/16,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,127.0.0.1:5000,localhost:5000",
|
||||
"export no_proxy=$NO_PROXY",
|
||||
"docker build --pull --build-arg HTTP_PROXY --build-arg HTTPS_PROXY --build-arg http_proxy --build-arg https_proxy --build-arg NO_PROXY --build-arg no_proxy -f \"$dockerfile\" -t \"$image\" .",
|
||||
"docker run --rm \"$image\" sh -lc 'node --version && npm --version && bun --version && git --version && python3 --version && docker --version && ssh -V'",
|
||||
"docker push \"$image\"",
|
||||
"digest=$(docker image inspect \"$image\" --format '{{index .RepoDigests 0}}' 2>/dev/null || true)",
|
||||
"echo \"{\\\"phase\\\":\\\"published\\\",\\\"image\\\":\\\"$image\\\",\\\"digest\\\":\\\"$digest\\\"}\"",
|
||||
].join("\n");
|
||||
if (options.dryRun) {
|
||||
return {
|
||||
ok: true,
|
||||
command: "hwlab g14 tools-image build --name ci-node-tools",
|
||||
mode: "dry-run",
|
||||
image,
|
||||
workspace: V02_WORKSPACE,
|
||||
dockerfile: options.dockerfile,
|
||||
buildScriptPreview: script.split(/\n/u),
|
||||
next: { build: `bun scripts/cli.ts hwlab g14 tools-image build --name ci-node-tools --tag ${options.tag} --confirm` },
|
||||
};
|
||||
}
|
||||
const result = g14HostScript(script, options.timeoutSeconds * 1000);
|
||||
return {
|
||||
ok: isCommandSuccess(result),
|
||||
command: "hwlab g14 tools-image build --name ci-node-tools",
|
||||
mode: "confirmed-build",
|
||||
image,
|
||||
workspace: V02_WORKSPACE,
|
||||
dockerfile: options.dockerfile,
|
||||
result,
|
||||
status: runG14ToolsImageStatus({ ...options, action: "status", dryRun: true, confirm: false }),
|
||||
};
|
||||
}
|
||||
|
||||
function runG14ToolsImage(options: G14ToolsImageOptions): Record<string, unknown> {
|
||||
if (options.action === "status") return runG14ToolsImageStatus(options);
|
||||
return runG14ToolsImageBuild(options);
|
||||
}
|
||||
|
||||
function listOpenG14PullRequests(): CommandJsonResult {
|
||||
return cliJson(["gh", "pr", "list", "--repo", HWLAB_REPO, "--state", "open", "--limit", "30", "--json", "number,title,state,url,head,base,draft,headRefName,baseRefName"], 60_000);
|
||||
}
|
||||
@@ -949,16 +1426,25 @@ export function hwlabG14Help(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts hwlab g14 monitor-prs",
|
||||
"bun scripts/cli.ts hwlab g14 monitor-prs --once --dry-run",
|
||||
"bun scripts/cli.ts hwlab g14 record-rollout --pr <number> [--source-commit sha]",
|
||||
"bun scripts/cli.ts hwlab g14 control-plane status --lane v02",
|
||||
"bun scripts/cli.ts hwlab g14 control-plane apply --lane v02 --dry-run",
|
||||
"bun scripts/cli.ts hwlab g14 control-plane apply --lane v02 --confirm",
|
||||
"bun scripts/cli.ts hwlab g14 control-plane rerun-current --lane v02 --confirm",
|
||||
"bun scripts/cli.ts hwlab g14 tools-image status --name ci-node-tools --tag node22-alpine-bun-v1",
|
||||
"bun scripts/cli.ts hwlab g14 tools-image build --name ci-node-tools --tag node22-alpine-bun-v1 --confirm",
|
||||
"bun scripts/cli.ts job status <jobId> --tail-bytes 30000",
|
||||
],
|
||||
description: "G14 HWLAB PR monitor and DEV rollout command. The public command starts a fire-and-forget job; the worker uses UniDesk gh and ssh routes for every GitHub and k3s operation, then appends the rollout record to the #7-indexed daily brief.",
|
||||
description: "G14 HWLAB PR monitor, DEV rollout command, bounded v0.2 control-plane bootstrap helper, and controlled CI tools image build/status entry. The public monitor starts a fire-and-forget job; control-plane status/apply/rerun-current uses UniDesk G14:k3s routes for v0.2 Tekton/Argo control resources only.",
|
||||
defaults: {
|
||||
repo: HWLAB_REPO,
|
||||
base: G14_SOURCE_BRANCH,
|
||||
provider: G14_PROVIDER,
|
||||
workspace: G14_WORKSPACE,
|
||||
v02Workspace: V02_WORKSPACE,
|
||||
ciToolsImageRepo: G14_CI_TOOLS_IMAGE_REPO,
|
||||
intervalSeconds: DEFAULT_INTERVAL_SECONDS,
|
||||
devApplication: DEV_APP,
|
||||
v02Application: V02_APP,
|
||||
briefIndexIssue: G14_BRIEF_INDEX_ISSUE,
|
||||
},
|
||||
stateFiles: {
|
||||
@@ -977,8 +1463,16 @@ export async function runHwlabG14Command(_config: Config, args: string[]): Promi
|
||||
const options = parseRecordRolloutOptions(args.slice(1));
|
||||
return appendRolloutBrief(options);
|
||||
}
|
||||
if (action === "control-plane") {
|
||||
const options = parseControlPlaneOptions(args.slice(1));
|
||||
return runV02ControlPlane(options);
|
||||
}
|
||||
if (action === "tools-image") {
|
||||
const options = parseToolsImageOptions(args.slice(1));
|
||||
return runG14ToolsImage(options);
|
||||
}
|
||||
if (action !== "monitor-prs") {
|
||||
return { ok: false, command: `hwlab g14 ${action ?? ""}`.trim(), degradedReason: "unsupported-command", message: "supported commands: hwlab g14 monitor-prs, hwlab g14 record-rollout" };
|
||||
return { ok: false, command: `hwlab g14 ${action ?? ""}`.trim(), degradedReason: "unsupported-command", message: "supported commands: hwlab g14 monitor-prs, hwlab g14 record-rollout, hwlab g14 control-plane, hwlab g14 tools-image" };
|
||||
}
|
||||
const options = parseOptions(args.slice(1));
|
||||
if (options.worker) return runMonitorWorker(options);
|
||||
|
||||
Reference in New Issue
Block a user