Files
pikasTech-unidesk/scripts/src/hwlab-g14.ts
T
2026-06-03 01:44:46 +00:00

4060 lines
177 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { createHash } from "node:crypto";
import { repoRoot, rootPath, type Config } from "./config";
import { runCommand } from "./command";
import { startJob } from "./jobs";
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 V02_CICD_REPO = "/root/hwlab-v02-cicd.git";
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 V02_SECRET_FIELD_MANAGER = "unidesk-hwlab-v02-secret";
const V02_GIT_URL = "git@github.com:pikasTech/HWLAB.git";
const V02_GIT_READ_URL = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git";
const V02_GIT_WRITE_URL = "http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/HWLAB.git";
const V02_GITOPS_BRANCH = "v0.2-gitops";
const V02_CATALOG_PATH = "deploy/artifact-catalog.v02.json";
const V02_RUNTIME_PATH = "deploy/gitops/g14/runtime-v02";
const V02_RUNTIME_NAMESPACE = "hwlab-v02";
const V02_DEVICE_POD_API_KEY_SECRET = "hwlab-v02-device-pod-api-key";
const V02_DEVICE_POD_API_KEY_SECRET_KEY = "api-key";
const V02_REGISTRY_PREFIX = "127.0.0.1:5000/hwlab";
const V02_BASE_IMAGE = "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim";
const GIT_MIRROR_NAMESPACE = "devops-infra";
const GIT_MIRROR_MANIFEST_FIELD_MANAGER = "unidesk-hwlab-git-mirror";
const GIT_MIRROR_SYNC_JOB_PREFIX = "git-mirror-hwlab-sync-manual";
const GIT_MIRROR_LEGACY_CRONJOB = "git-mirror-hwlab-sync";
const V02_SERVICE_IDS = [
"hwlab-cloud-api",
"hwlab-cloud-web",
"hwlab-agent-mgr",
"hwlab-agent-worker",
"hwlab-device-pod",
"hwlab-gateway",
"hwlab-edge-proxy",
"hwlab-agent-skills",
];
const V02_CLOUD_WEB_URL = "http://74.48.78.17:19666";
const V02_CLOUD_API_URL = "http://74.48.78.17:19667";
export function v02PipelineServiceIds(): string[] {
return [...V02_SERVICE_IDS];
}
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;
const G14_BRIEF_INDEX_ISSUE = 7;
const BEIJING_OFFSET_MS = 8 * 60 * 60 * 1000;
const V02_BUILD_TASKRUN_WARNING_SECONDS = 120;
const V02_BUILD_TASKRUN_CRITICAL_SECONDS = 180;
interface G14MonitorOptions {
intervalSeconds: number;
maxCycles: number;
once: boolean;
dryRun: boolean;
worker: boolean;
timeoutSeconds: number;
}
interface G14RecordRolloutOptions {
prNumber: number;
sourceCommit?: string;
pipelineRun?: string;
gitopsRevision?: string;
mergedAt?: string;
pipelineSucceededAt?: string;
finishedAt?: string;
dryRun: boolean;
}
interface G14ControlPlaneOptions {
action: "status" | "apply" | "trigger-current" | "cleanup-runs" | "cleanup-released-pvs" | "runtime-migration";
lane: "v02" | "g14" | "all";
dryRun: boolean;
confirm: boolean;
wait: boolean;
allowLiveDbRead: boolean;
timeoutSeconds: number;
minAgeMinutes: number;
limit: number;
sourceCommit?: string;
pipelineRun?: string;
}
type V02StatusTargetMode = "latest-source-head" | "source-commit" | "pipeline-run";
interface V02ControlPlaneStatusTarget {
sourceCommit?: string | null;
pipelineRun?: string | null;
mode?: V02StatusTargetMode;
}
interface G14ToolsImageOptions {
action: "status" | "build";
name: "ci-node-tools";
tag: string;
dockerfile: string;
dryRun: boolean;
confirm: boolean;
timeoutSeconds: number;
}
interface G14GitMirrorOptions {
action: "status" | "apply" | "sync" | "flush";
dryRun: boolean;
confirm: boolean;
wait: boolean;
timeoutSeconds: number;
}
interface G14SecretOptions {
action: "status" | "ensure";
lane: "v02";
dryRun: boolean;
confirm: boolean;
name: typeof V02_DEVICE_POD_API_KEY_SECRET;
key: typeof V02_DEVICE_POD_API_KEY_SECRET_KEY;
timeoutSeconds: number;
}
interface CommandJsonResult {
ok: boolean;
command: string[];
exitCode: number | null;
stdout: string;
stderr: string;
parsed: unknown | null;
}
interface ShellSection {
stdout: string;
exitCode: number | null;
}
interface OpenPullRequest {
number: number;
title?: string;
url?: string;
baseRefName?: string;
headRefName?: string;
mergeable?: string | null;
mergeStateStatus?: string | null;
draft?: boolean;
}
interface CiServiceMetric {
taskRun: string;
serviceId: string;
status: string;
durationSeconds: number | null;
buildBackend: string | null;
reusedFrom: string | null;
imageTag: string | null;
sourceCommitId: string | null;
}
interface CiPipelineMetrics {
ok: boolean;
pipelineRun: string;
serviceTotal: number;
reusedCount: number;
rebuildCount: number;
reusedServices: string[];
rebuildServices: string[];
services: CiServiceMetric[];
degradedReason?: string;
rawText?: string;
}
export function hwlabG14MonitorStateFileName(options: Pick<G14MonitorOptions, "once" | "dryRun">): string {
if (options.once && options.dryRun) return "latest-once-dry-run-job.json";
if (options.once) return "latest-once-job.json";
if (options.dryRun) return "latest-dry-run-job.json";
return "latest-monitor-job.json";
}
function hwlabG14MonitorStateRole(options: Pick<G14MonitorOptions, "once" | "dryRun">): string {
if (options.once && options.dryRun) return "once-dry-run";
if (options.once) return "once";
if (options.dryRun) return "dry-run-monitor";
return "monitor";
}
function parseOptions(args: string[]): G14MonitorOptions {
return {
intervalSeconds: positiveIntegerOption(args, "--interval-seconds", DEFAULT_INTERVAL_SECONDS, 86400),
maxCycles: positiveIntegerOption(args, "--max-cycles", DEFAULT_MAX_CYCLES, 100000),
once: args.includes("--once"),
dryRun: args.includes("--dry-run"),
worker: args.includes("--worker"),
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", DEFAULT_TIMEOUT_SECONDS, 86400),
};
}
function optionValue(args: string[], name: string): string | undefined {
const index = args.indexOf(name);
if (index === -1) return undefined;
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
return value;
}
function validateFullShaOption(value: string, name: string): string {
if (!/^[0-9a-f]{40}$/iu.test(value)) throw new Error(`${name} must be a full 40-character git SHA`);
return value.toLowerCase();
}
function validateV02PipelineRunOption(value: string): string {
if (!/^hwlab-v02-ci-poll-[0-9a-f]{7,40}$/iu.test(value)) throw new Error("--pipeline-run must be a hwlab-v02-ci-poll-<sha> PipelineRun name");
return value.toLowerCase();
}
function parseRecordRolloutOptions(args: string[]): G14RecordRolloutOptions {
const prRaw = optionValue(args, "--pr") ?? optionValue(args, "--number");
const prNumber = Number(prRaw);
if (!Number.isInteger(prNumber) || prNumber <= 0) throw new Error("record-rollout requires --pr <number>");
return {
prNumber,
sourceCommit: optionValue(args, "--source-commit"),
pipelineRun: optionValue(args, "--pipeline-run"),
gitopsRevision: optionValue(args, "--gitops-revision"),
mergedAt: optionValue(args, "--merged-at"),
pipelineSucceededAt: optionValue(args, "--pipeline-succeeded-at"),
finishedAt: optionValue(args, "--finished-at"),
dryRun: args.includes("--dry-run"),
};
}
function parseControlPlaneOptions(args: string[]): G14ControlPlaneOptions {
const [rawAction] = args;
const actionRaw = rawAction === "rerun-current" ? "trigger-current" : rawAction;
if (
actionRaw !== "status" &&
actionRaw !== "apply" &&
actionRaw !== "trigger-current" &&
actionRaw !== "cleanup-runs" &&
actionRaw !== "cleanup-released-pvs" &&
actionRaw !== "runtime-migration"
) {
throw new Error("control-plane usage: status|apply|trigger-current|runtime-migration --lane v02 | cleanup-runs --lane v02|g14|all | cleanup-released-pvs --lane all [--dry-run|--confirm]");
}
const lane = optionValue(args, "--lane") ?? (actionRaw === "cleanup-released-pvs" ? "all" : "v02");
if (actionRaw === "cleanup-runs") {
if (lane !== "v02" && lane !== "g14" && lane !== "all") throw new Error("control-plane cleanup-runs requires --lane v02|g14|all");
} else if (actionRaw === "cleanup-released-pvs") {
if (lane !== "all") throw new Error("control-plane cleanup-released-pvs requires --lane all because released PVs no longer preserve the v02/g14 PipelineRun lane");
} else if (lane !== "v02") {
throw new Error("control-plane status/apply/trigger-current/runtime-migration 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");
const allowLiveDbRead = args.includes("--allow-live-db-read");
if (allowLiveDbRead && confirm) throw new Error("control-plane runtime-migration accepts --allow-live-db-read only with dry-run/source-check mode, not --confirm");
if (allowLiveDbRead && actionRaw !== "runtime-migration") throw new Error("--allow-live-db-read is only valid for control-plane runtime-migration");
const sourceCommitRaw = optionValue(args, "--source-commit");
const pipelineRunRaw = optionValue(args, "--pipeline-run");
if ((sourceCommitRaw !== undefined || pipelineRunRaw !== undefined) && actionRaw !== "status") {
throw new Error("--source-commit and --pipeline-run are only valid for control-plane status");
}
if (sourceCommitRaw !== undefined && pipelineRunRaw !== undefined) throw new Error("control-plane status accepts only one of --source-commit or --pipeline-run");
return {
action: actionRaw,
lane,
confirm,
wait: args.includes("--wait"),
allowLiveDbRead,
dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm,
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600),
minAgeMinutes: positiveIntegerOption(args, "--min-age-minutes", 60, 10080),
limit: positiveIntegerOption(args, "--limit", 20, 200),
sourceCommit: sourceCommitRaw === undefined ? undefined : validateFullShaOption(sourceCommitRaw, "--source-commit"),
pipelineRun: pipelineRunRaw === undefined ? undefined : validateV02PipelineRunOption(pipelineRunRaw),
};
}
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 parseGitMirrorOptions(args: string[]): G14GitMirrorOptions {
const [actionRaw] = args;
if (actionRaw !== "status" && actionRaw !== "apply" && actionRaw !== "sync" && actionRaw !== "flush") {
throw new Error("git-mirror usage: status|apply|sync|flush [--dry-run|--confirm]");
}
const confirm = args.includes("--confirm");
const explicitDryRun = args.includes("--dry-run");
if (confirm && explicitDryRun) throw new Error("git-mirror accepts only one of --confirm or --dry-run");
return {
action: actionRaw,
confirm,
wait: args.includes("--wait"),
dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm,
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", actionRaw === "sync" || actionRaw === "flush" ? 300 : 120, 900),
};
}
function parseSecretOptions(args: string[]): G14SecretOptions {
const [actionRaw] = args;
if (actionRaw !== "status" && actionRaw !== "ensure") {
throw new Error("secret usage: status|ensure --lane v02 --name hwlab-v02-device-pod-api-key --key api-key [--dry-run|--confirm]");
}
const lane = optionValue(args, "--lane") ?? "v02";
if (lane !== "v02") throw new Error("secret currently supports --lane v02");
const name = optionValue(args, "--name") ?? V02_DEVICE_POD_API_KEY_SECRET;
if (name !== V02_DEVICE_POD_API_KEY_SECRET) throw new Error(`secret currently supports --name ${V02_DEVICE_POD_API_KEY_SECRET}`);
const key = optionValue(args, "--key") ?? V02_DEVICE_POD_API_KEY_SECRET_KEY;
if (key !== V02_DEVICE_POD_API_KEY_SECRET_KEY) throw new Error(`secret currently supports --key ${V02_DEVICE_POD_API_KEY_SECRET_KEY}`);
const confirm = args.includes("--confirm");
const explicitDryRun = args.includes("--dry-run");
if (confirm && explicitDryRun) throw new Error("secret accepts only one of --confirm or --dry-run");
return {
action: actionRaw,
lane,
confirm,
dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm,
name,
key,
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600),
};
}
function positiveIntegerOption(args: string[], name: string, defaultValue: number, maxValue: number): number {
const index = args.indexOf(name);
if (index === -1) return defaultValue;
const raw = args[index + 1];
const value = Number(raw);
if (!Number.isInteger(value) || value < 0) throw new Error(`${name} must be a non-negative integer`);
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;
if (result.stdout.trim().length > 0) {
try {
parsed = JSON.parse(result.stdout) as unknown;
} catch {
parsed = null;
}
}
return {
ok: result.exitCode === 0,
command,
exitCode: result.exitCode,
stdout: result.stdout,
stderr: result.stderr,
parsed,
};
}
function cliJson(args: string[], timeoutMs = 60_000): CommandJsonResult {
return commandJson(["bun", "scripts/cli.ts", ...args], timeoutMs);
}
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function stringOrNull(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function nested(value: unknown, keys: string[]): unknown {
let current = value;
for (const key of keys) current = record(current)[key];
return current;
}
function expandedParsedRoot(result: CommandJsonResult): Record<string, unknown> {
const dumpPath = nested(result.parsed, ["data", "dump", "path"]);
if (typeof dumpPath === "string" && existsSync(dumpPath)) {
try {
return record(JSON.parse(readFileSync(dumpPath, "utf8")) as unknown);
} catch {
return record(result.parsed);
}
}
return record(result.parsed);
}
function commandData(result: CommandJsonResult): Record<string, unknown> {
return record(sanitizeCliOutput(record(expandedParsedRoot(result).data)));
}
function textHash(value: string): string {
return createHash("sha256").update(value).digest("hex").slice(0, 12);
}
function redactLargePayloads(value: string): string {
return value
.replace(/manifest_b64='([^']{128,})'/gu, (_match, payload: string) => {
return `manifest_b64='<omitted base64 chars=${payload.length} sha256=${textHash(payload)}>'`;
})
.replace(/manifest_b64="([^"]{128,})"/gu, (_match, payload: string) => {
return `manifest_b64="<omitted base64 chars=${payload.length} sha256=${textHash(payload)}>"`;
})
.replace(/manifest_b64=([A-Za-z0-9+/=]{128,})/gu, (_match, payload: string) => {
return `manifest_b64=<omitted base64 chars=${payload.length} sha256=${textHash(payload)}>`;
})
.replace(/[A-Za-z0-9+/=]{1024,}/gu, (payload: string) => {
return `<omitted base64 chars=${payload.length} sha256=${textHash(payload)}>`;
});
}
function compactInlineText(value: string, maxChars = 4000): string {
const redacted = redactLargePayloads(value);
if (redacted.length <= maxChars) return redacted;
const headChars = Math.floor(maxChars * 0.35);
const tailChars = maxChars - headChars;
const omittedChars = redacted.length - headChars - tailChars;
return [
redacted.slice(0, headChars),
`<truncated chars=${omittedChars} sha256=${textHash(redacted)}>`,
redacted.slice(-tailChars),
].join("\n");
}
function sanitizeCliOutput(value: unknown): unknown {
if (typeof value === "string") return compactInlineText(value);
if (Array.isArray(value)) return value.map((item) => sanitizeCliOutput(item));
if (typeof value !== "object" || value === null) return value;
const output: Record<string, unknown> = {};
for (const [key, item] of Object.entries(value)) output[key] = sanitizeCliOutput(item);
return output;
}
function redactedCommand(command: string[]): string[] {
return command.map((arg) => compactInlineText(arg, 1200));
}
function compactCommandResult(result: CommandJsonResult): Record<string, unknown> {
const data = record(expandedParsedRoot(result).data);
const stdout = String(data.stdout ?? result.stdout ?? "");
const stderr = String(data.stderr ?? result.stderr ?? "");
return {
ok: isCommandSuccess(result),
exitCode: result.exitCode,
command: redactedCommand(result.command),
stdout: compactInlineText(stdout.trim(), 4000),
stderr: compactInlineText(stderr.trim(), 4000),
stdoutBytes: Buffer.byteLength(stdout),
stderrBytes: Buffer.byteLength(stderr),
};
}
function compactControlPlaneRefresh(value: Record<string, unknown>): Record<string, unknown> {
return {
ok: value.ok === true,
phase: value.phase ?? null,
sourceCommit: value.sourceCommit ?? null,
renderDir: value.renderDir ?? null,
applyOk: nested(value, ["apply", "ok"]) ?? null,
cleanupOk: nested(value, ["cleanupObsoleteCronJobs", "ok"]) ?? null,
applyStdout: tailText(nested(value, ["apply", "stdout"]), 1200),
degradedReason: value.degradedReason ?? null,
};
}
function compactTriggerCommandResult(value: Record<string, unknown> | null): Record<string, unknown> | null {
if (value === null) return null;
return {
ok: value.ok ?? null,
exitCode: value.exitCode ?? null,
stdout: tailText(value.stdout, 1200),
stderr: tailText(value.stderr, 1200),
stdoutBytes: value.stdoutBytes ?? null,
stderrBytes: value.stderrBytes ?? null,
};
}
function isCommandSuccess(result: CommandJsonResult): boolean {
if (!result.ok) return false;
const topOk = record(result.parsed).ok;
if (topOk === false) return false;
const dataOk = nested(result.parsed, ["data", "ok"]);
return dataOk !== false;
}
function extractPullRequests(result: CommandJsonResult): OpenPullRequest[] {
const prs = nested(result.parsed, ["data", "pullRequests"]);
if (!Array.isArray(prs)) return [];
return prs
.map((item) => record(item))
.filter((item) => item.baseRefName === G14_SOURCE_BRANCH || record(item.base).ref === G14_SOURCE_BRANCH)
.map((item) => ({
number: Number(item.number),
title: typeof item.title === "string" ? item.title : undefined,
url: typeof item.url === "string" ? item.url : undefined,
baseRefName: typeof item.baseRefName === "string" ? item.baseRefName : typeof record(item.base).ref === "string" ? record(item.base).ref as string : undefined,
headRefName: typeof item.headRefName === "string" ? item.headRefName : typeof record(item.head).ref === "string" ? record(item.head).ref as string : undefined,
mergeable: typeof item.mergeable === "string" ? item.mergeable : null,
mergeStateStatus: typeof item.mergeStateStatus === "string" ? item.mergeStateStatus : null,
draft: item.draft === true,
}))
.filter((item) => Number.isInteger(item.number) && item.number > 0);
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function printEvent(event: string, data: Record<string, unknown> = {}): void {
process.stdout.write(`${JSON.stringify({ event, at: new Date().toISOString(), ...data })}\n`);
}
function printProgressEvent(event: string, data: Record<string, unknown> = {}): void {
process.stderr.write(`${JSON.stringify({ event, at: new Date().toISOString(), ...data })}\n`);
}
function shortSha(sha: string): string {
return sha.slice(0, 12);
}
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 g14HostScript(script: string, timeoutMs = 120_000): CommandJsonResult {
return cliJson(["ssh", G14_PROVIDER, "script", "--", script], timeoutMs);
}
function g14K3s(args: string[], timeoutMs = 60_000): CommandJsonResult {
return cliJson(["ssh", `${G14_PROVIDER}:k3s`, ...args], timeoutMs);
}
function v02CicdRepoEnsureScript(): string {
return [
`cicd_repo=${shellQuote(V02_CICD_REPO)}`,
`cicd_url=${shellQuote(V02_GIT_URL)}`,
"mkdir -p \"$(dirname \"$cicd_repo\")\"",
"if [ -d \"$cicd_repo/objects\" ] && [ -f \"$cicd_repo/HEAD\" ]; then",
" :",
"elif [ -e \"$cicd_repo\" ]; then",
" echo \"v0.2 CI/CD repo path exists but is not a bare git repo: $cicd_repo\" >&2",
" exit 41",
"else",
" git clone --bare \"$cicd_url\" \"$cicd_repo\"",
"fi",
"git --git-dir=\"$cicd_repo\" remote set-url origin \"$cicd_url\" 2>/dev/null || git --git-dir=\"$cicd_repo\" remote add origin \"$cicd_url\"",
"git --git-dir=\"$cicd_repo\" config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'",
"git --git-dir=\"$cicd_repo\" fetch origin '+refs/heads/v0.2:refs/remotes/origin/v0.2' --prune",
].join("\n");
}
function getV02Head(): string | null {
const result = g14HostScript([
"set -eu",
v02CicdRepoEnsureScript(),
"git --git-dir=\"$cicd_repo\" rev-parse refs/remotes/origin/v0.2",
].join("\n"), 180_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 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);
return pipelineRunCompactFromText(name, statusText(result), isCommandSuccess(result), result.command, result.exitCode, result.stderr);
}
function pipelineRunCompactFromText(
name: string,
text: string,
commandOk: boolean,
command: string[] | string,
exitCode: number | null,
stderr: string,
): Record<string, unknown> {
const [status = "", reason = "", message = ""] = text.split(/\r?\n/u);
const notFound = !commandOk && /not found/iu.test(`${text}\n${stderr}`);
return {
ok: commandOk,
exists: commandOk || !notFound,
pipelineRun: name,
status: status || null,
reason: reason || null,
message: message || null,
command: Array.isArray(command) ? redactedCommand(command) : command,
exitCode,
stderr: stderr.trim().slice(0, 2000),
};
}
function timestampMs(value: unknown): number | null {
if (typeof value !== "string" || value.trim().length === 0) return null;
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : null;
}
function secondsBetween(start: unknown, end: unknown): number | null {
const startMs = timestampMs(start);
const endMs = timestampMs(end);
if (startMs === null || endMs === null || endMs < startMs) return null;
return Math.round((endMs - startMs) / 1000);
}
function pipelineRunStatusRowFromLine(line: string, nowMs: number): Record<string, unknown> | null {
const [
name = "",
sourceCommit = "",
createdAt = "",
startTime = "",
completionTime = "",
status = "",
reason = "",
] = line.split("\t");
if (!name.startsWith(V02_PIPELINERUN_PREFIX)) return null;
const startMs = timestampMs(startTime);
const completion = completionTime.trim().length > 0 ? completionTime : null;
const conditionStatus = status.trim().length > 0 ? status : null;
return {
name,
sourceCommit: sourceCommit || null,
status: conditionStatus,
reason: reason || null,
createdAt: createdAt || null,
startTime: startTime || null,
completionTime: completion,
durationSeconds: secondsBetween(startTime, completion),
elapsedSeconds: startMs === null || completion !== null ? null : Math.max(0, Math.round((nowMs - startMs) / 1000)),
active: completion === null && conditionStatus !== "True" && conditionStatus !== "False",
};
}
function parsePipelineRunRows(text: string, limit: number, nowMs = Date.now()): Record<string, unknown> {
const rows = text
.split(/\r?\n/u)
.map((line) => pipelineRunStatusRowFromLine(line.trim(), nowMs))
.filter((item): item is Record<string, unknown> => item !== null)
.sort((left, right) => {
return (timestampMs(right.createdAt) ?? 0) - (timestampMs(left.createdAt) ?? 0);
});
const activeItems = rows.filter((item) => item.active === true);
return {
ok: true,
count: rows.length,
activeCount: activeItems.length,
items: rows.slice(0, limit),
activeItems: activeItems.slice(0, limit),
disclosure: rows.length > limit ? `showing newest ${limit} of ${rows.length}` : "complete",
};
}
function pipelineRunRowsJsonPath(): string {
return [
"jsonpath={range .items[*]}",
"{.metadata.name}",
"{\"\\t\"}",
"{.metadata.labels.hwlab\\.pikastech\\.local/source-commit}",
"{\"\\t\"}",
"{.metadata.creationTimestamp}",
"{\"\\t\"}",
"{.status.startTime}",
"{\"\\t\"}",
"{.status.completionTime}",
"{\"\\t\"}",
"{.status.conditions[0].status}",
"{\"\\t\"}",
"{.status.conditions[0].reason}",
"{\"\\n\"}",
"{end}",
].join("");
}
function listV02PipelineRunsCompact(limit = 8): Record<string, unknown> {
const result = g14K3s([
"kubectl",
"get",
"pipelinerun",
"-n",
CI_NAMESPACE,
"-l",
"hwlab.pikastech.local/gitops-target=v02",
"-o",
pipelineRunRowsJsonPath(),
], 60_000);
if (!isCommandSuccess(result)) {
return {
ok: false,
command: redactedCommand(result.command),
exitCode: result.exitCode,
stderr: commandErrorSummary(result),
items: [],
activeItems: [],
};
}
return parsePipelineRunRows(statusText(result), limit);
}
function parseShellSections(output: string): Record<string, ShellSection> {
const sections: Record<string, ShellSection> = {};
let currentName: string | null = null;
let currentLines: string[] = [];
let currentExitCode: number | null = null;
const flush = (): void => {
if (currentName === null) return;
sections[currentName] = {
stdout: currentLines.join("\n").trim(),
exitCode: currentExitCode,
};
};
for (const line of output.split(/\r?\n/u)) {
const begin = /^__UNIDESK_SECTION_BEGIN__ ([A-Za-z0-9_-]+)$/u.exec(line);
if (begin !== null) {
flush();
currentName = begin[1] ?? null;
currentLines = [];
currentExitCode = null;
continue;
}
const end = /^__UNIDESK_SECTION_END__ ([A-Za-z0-9_-]+) exit=([0-9]+)$/u.exec(line);
if (end !== null && currentName === end[1]) {
currentExitCode = Number(end[2]);
flush();
currentName = null;
currentLines = [];
currentExitCode = null;
continue;
}
if (currentName !== null) currentLines.push(line);
}
flush();
return sections;
}
function shellSectionOk(section: ShellSection | undefined): boolean {
return section?.exitCode === 0;
}
function v02ControlPlaneStatusBundle(target: V02ControlPlaneStatusTarget = {}): CommandJsonResult {
const targetMode: V02StatusTargetMode = target.mode
?? (target.pipelineRun !== undefined && target.pipelineRun !== null ? "pipeline-run" : target.sourceCommit !== undefined ? "source-commit" : "latest-source-head");
const targetInit = target.pipelineRun !== undefined && target.pipelineRun !== null
? [
`pipeline_run=${shellQuote(target.pipelineRun)}`,
`source_commit=$(kubectl get pipelinerun -n ${shellQuote(CI_NAMESPACE)} "$pipeline_run" -o 'jsonpath={.metadata.labels.hwlab\\.pikastech\\.local/source-commit}' 2>/dev/null || true)`,
`if [ -z "$source_commit" ]; then source_commit=$(printf '%s' "$pipeline_run" | cut -d- -f5-); fi`,
].join("\n")
: [
target.sourceCommit === undefined
? `source_commit=$(git --git-dir=${shellQuote(V02_CICD_REPO)} rev-parse refs/remotes/origin/v0.2 2>/dev/null || true)`
: `source_commit=${shellQuote(target.sourceCommit ?? "")}`,
"pipeline_run=",
].join("\n");
const script = [
"set +e",
targetInit,
`target_mode=${shellQuote(targetMode)}`,
`if [ -z "$pipeline_run" ] && [ -n "$source_commit" ]; then pipeline_run="${V02_PIPELINERUN_PREFIX}-$(printf '%s' "$source_commit" | cut -c1-12)"; fi`,
"section() {",
" name=\"$1\"",
" shift",
" printf '__UNIDESK_SECTION_BEGIN__ %s\\n' \"$name\"",
" \"$@\"",
" code=$?",
" printf '\\n__UNIDESK_SECTION_END__ %s exit=%s\\n' \"$name\" \"$code\"",
"}",
"section statusTarget printf 'mode\\t%s\\nsourceCommit\\t%s\\npipelineRun\\t%s\\n' \"$target_mode\" \"$source_commit\" \"$pipeline_run\"",
"section sourceCommit printf '%s\\n' \"$source_commit\"",
"section pipelineRunName printf '%s\\n' \"$pipeline_run\"",
`section sourceHeads sh -c ${shellQuote(v02SourceHeadsProbeScript())}`,
"section queryNow date -u +%Y-%m-%dT%H:%M:%SZ",
`section controlPlane kubectl get pipeline,role,rolebinding,serviceaccount -n ${shellQuote(CI_NAMESPACE)} -l hwlab.pikastech.local/gitops-target=v02 -o name`,
`section obsoleteCronJobs kubectl get cronjob -n ${shellQuote(CI_NAMESPACE)} ${shellQuote(V02_POLLER)} ${shellQuote(V02_RECONCILER)} --ignore-not-found -o name`,
`section argo kubectl get application -n ${shellQuote(ARGO_NAMESPACE)} ${shellQuote(V02_APP)} -o 'jsonpath={.spec.source.targetRevision}{"\\n"}{.spec.source.path}{"\\n"}{.status.sync.revision}{"\\n"}{.status.sync.status}{"\\n"}{.status.health.status}{"\\n"}'`,
`if [ -n "$pipeline_run" ]; then section pipelineRun kubectl get pipelinerun -n ${shellQuote(CI_NAMESPACE)} "$pipeline_run" -o 'jsonpath={.status.conditions[0].status}{"\\n"}{.status.conditions[0].reason}{"\\n"}{.status.conditions[0].message}{"\\n"}'; else section pipelineRun sh -c 'true'; fi`,
`if [ -n "$pipeline_run" ]; then section taskRuns kubectl get taskrun -n ${shellQuote(CI_NAMESPACE)} -l "tekton.dev/pipelineRun=$pipeline_run" -o 'jsonpath={range .items[*]}{.metadata.name}{"\\t"}{.status.conditions[0].status}{"\\t"}{.status.conditions[0].reason}{"\\t"}{.status.startTime}{"\\t"}{.status.completionTime}{"\\n"}{end}'; else section taskRuns sh -c 'true'; fi`,
`if [ -n "$pipeline_run" ]; then section planArtifacts sh -c ${shellQuote(v02PlanArtifactsLogScript())} plan-artifacts "$pipeline_run"; else section planArtifacts sh -c 'true'; fi`,
`section runtimeWorkloads kubectl get deploy,statefulset,job -n hwlab-v02 -l hwlab.pikastech.local/gitops-target=v02 -o ${shellQuote(v02RuntimeWorkloadsColumns())} --no-headers`,
`section recentPipelineRuns kubectl get pipelinerun -n ${shellQuote(CI_NAMESPACE)} -l hwlab.pikastech.local/gitops-target=v02 -o ${shellQuote(pipelineRunRowsJsonPath())}`,
`section gitMirrorCache kubectl exec -n ${shellQuote(GIT_MIRROR_NAMESPACE)} deploy/git-mirror-http -- sh -lc ${shellQuote(gitMirrorCacheProbeScript())}`,
`section webAssets sh -c ${shellQuote(v02WebAssetsProbeScript())}`,
].join("\n");
return g14K3s(["script", "--", script], 60_000);
}
function v02SourceHeadsProbeScript(): string {
return [
"set +e",
`cicd_repo=${shellQuote(V02_CICD_REPO)}`,
`workspace=${shellQuote(V02_WORKSPACE)}`,
"rev_cicd() { git --git-dir=\"$cicd_repo\" rev-parse \"$1\" 2>/dev/null || true; }",
"rev_workspace() { git -C \"$workspace\" rev-parse \"$1\" 2>/dev/null || true; }",
"printf 'cicdRepo\\t%s\\n' \"$cicd_repo\"",
"printf 'cicdRepoExists\\t%s\\n' \"$([ -d \"$cicd_repo/objects\" ] && printf yes || printf no)\"",
"printf 'cicdSourceHead\\t%s\\n' \"$(rev_cicd refs/remotes/origin/v0.2)\"",
"printf 'originHead\\t%s\\n' \"$(rev_cicd refs/remotes/origin/v0.2)\"",
"printf 'workspacePath\\t%s\\n' \"$workspace\"",
"printf 'workspaceBranch\\t%s\\n' \"$(git -C \"$workspace\" rev-parse --abbrev-ref HEAD 2>/dev/null || true)\"",
"printf 'workspaceHead\\t%s\\n' \"$(rev_workspace HEAD)\"",
"printf 'workspaceOriginHead\\t%s\\n' \"$(rev_workspace refs/remotes/origin/v0.2)\"",
"printf 'workspaceDirtyCount\\t%s\\n' \"$(git -C \"$workspace\" status --porcelain=v1 2>/dev/null | wc -l | tr -d ' ')\"",
].join("\n");
}
function v02PlanArtifactsLogScript(): string {
return [
"pipeline_run=\"$1\"",
`namespace=${shellQuote(CI_NAMESPACE)}`,
"[ -n \"$pipeline_run\" ] || exit 0",
"pods=$(kubectl -n \"$namespace\" get pods -l \"tekton.dev/pipelineRun=$pipeline_run,tekton.dev/pipelineTask=plan-artifacts\" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null || true)",
"for pod in $pods; do",
" printf '__POD__\\t%s\\n' \"$pod\"",
" kubectl -n \"$namespace\" logs \"$pod\" --all-containers --tail=200 2>/dev/null || true",
"done",
].join("\n");
}
function v02RuntimeWorkloadsColumns(): string {
return [
"custom-columns=KIND:.kind",
"NAME:.metadata.name",
"SERVICE:.metadata.labels.hwlab\\.pikastech\\.local/service-id",
"ARTIFACT:.spec.template.metadata.annotations.hwlab\\.pikastech\\.local/artifact-source-commit",
"SOURCE:.spec.template.metadata.annotations.hwlab\\.pikastech\\.local/source-commit",
"IMAGE:.spec.template.spec.containers[0].image",
"READY:.status.readyReplicas",
"REPLICAS:.status.replicas",
].join(",");
}
function v02WebAssetsProbeScript(): string {
return [
"set +e",
`base=${shellQuote(V02_CLOUD_WEB_URL)}`,
`api=${shellQuote(V02_CLOUD_API_URL)}`,
"fetch_url() {",
" if command -v curl >/dev/null 2>&1; then",
" curl -fsS --connect-timeout 2 --max-time 5 \"$1\"",
" elif command -v wget >/dev/null 2>&1; then",
" wget -q -T 5 -O - \"$1\"",
" else",
" return 127",
" fi",
"}",
"printf 'baseUrl\\t%s\\n' \"$base\"",
"printf 'apiUrl\\t%s\\n' \"$api\"",
"html=$(fetch_url \"$base/\" 2>/dev/null)",
"html_code=$?",
"printf 'htmlOk\\t%s\\n' \"$html_code\"",
"printf 'readonlyNote\\t%s\\n' \"$(printf '%s' \"$html\" | grep -Eiq 'readonly-rpc|复核入口'; printf '%s' \"$?\")\"",
"css=$(fetch_url \"$base/styles.css\" 2>/dev/null)",
"css_code=$?",
"printf 'cssOk\\t%s\\n' \"$css_code\"",
"app_js=$(fetch_url \"$base/app.js\" 2>/dev/null)",
"app_js_code=$?",
"printf 'appJsOk\\t%s\\n' \"$app_js_code\"",
"printf 'appJsBytes\\t%s\\n' \"$(printf '%s' \"$app_js\" | wc -c | tr -d ' ')\"",
"printf 'sidebarFitCss\\t%s\\n' \"$(printf '%s' \"$css\" | grep -Eq 'grid-template-rows:[[:space:]]*auto[[:space:]]+auto[[:space:]]+auto[[:space:]]+auto[[:space:]]+minmax\\(0,[[:space:]]*1fr\\)'; printf '%s' \"$?\")\"",
"printf 'workspaceFitCss\\t%s\\n' \"$(printf '%s' \"$css\" | grep -Eq 'grid-template-rows:[[:space:]]*auto[[:space:]]+minmax\\(0,[[:space:]]*1fr\\)'; printf '%s' \"$?\")\"",
"printf 'eventPanelFitCss\\t%s\\n' \"$(printf '%s' \"$css\" | grep -Eq 'grid-template-rows:[[:space:]]*auto[[:space:]]+minmax\\(132px,[[:space:]]*1fr\\)'; printf '%s' \"$?\")\"",
"health=$(fetch_url \"$api/health/live\" 2>/dev/null)",
"health_code=$?",
"printf 'apiHealthOk\\t%s\\n' \"$health_code\"",
"printf 'apiRevision\\t%s\\n' \"$(printf '%s' \"$health\" | sed -n 's/.*\"revision\"[[:space:]]*:[[:space:]]*\"\\([0-9A-Za-z._-]*\\)\".*/\\1/p' | head -1)\"",
].join("\n");
}
function taskRunsCompactFromText(text: string, commandOk: boolean, pipelineRun: string | null, exitCode: number | null, stderr: string): Record<string, unknown> {
if (!commandOk) {
return {
ok: false,
pipelineRun,
exitCode,
stderr: stderr.trim().slice(0, 2000),
counts: { succeeded: 0, failed: 0, running: 0, unknown: 0 },
items: [],
performance: v02TaskRunPerformanceSummary([]),
};
}
const items = text
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const [name = "", status = "", reason = "", startTime = "", completionTime = ""] = line.split("\t");
return {
name,
status: status || null,
reason: reason || null,
startTime: startTime || null,
completionTime: completionTime || null,
durationSeconds: secondsBetween(startTime, completionTime),
};
});
const counts = {
succeeded: items.filter((item) => item.status === "True").length,
failed: items.filter((item) => item.status === "False").length,
running: items.filter((item) => item.status === "Unknown").length,
unknown: items.filter((item) => item.status !== "True" && item.status !== "False" && item.status !== "Unknown").length,
};
const performance = v02TaskRunPerformanceSummary(items);
const performanceWarning = performance.ok === false ? `; ${String(performance.summary ?? "")}` : "";
return {
ok: true,
pipelineRun,
counts,
items,
performance,
summary: `taskruns succeeded=${counts.succeeded} failed=${counts.failed} running=${counts.running} unknown=${counts.unknown}${performanceWarning}`,
disclosure: items.length > 0 ? "complete taskrun condition summary" : "no taskruns observed yet",
};
}
function v02TaskRunPipelineTaskName(name: string): string | null {
const buildIndex = name.lastIndexOf("-build-");
if (buildIndex >= 0) return name.slice(buildIndex + 1);
const knownSuffixes = [
"prepare-source",
"plan-artifacts",
"publish-artifact-catalog",
"gitops-render",
"gitops-promote",
"runtime-ready",
"collect-artifacts",
];
return knownSuffixes.find((suffix) => name.endsWith(`-${suffix}`)) ?? null;
}
export function v02TaskRunPerformanceSummary(taskRuns: unknown[]): Record<string, unknown> {
const slowTaskRuns: Record<string, unknown>[] = [];
for (const itemRaw of taskRuns) {
const item = record(itemRaw);
const name = stringOrNull(item.name) ?? "";
const pipelineTask = stringOrNull(item.pipelineTask) ?? v02TaskRunPipelineTaskName(name);
const durationSeconds = typeof item.durationSeconds === "number" ? item.durationSeconds : null;
if (!pipelineTask?.startsWith("build-") || durationSeconds === null || durationSeconds <= V02_BUILD_TASKRUN_WARNING_SECONDS) continue;
const serviceId = pipelineTask.slice("build-".length) || null;
slowTaskRuns.push({
name,
pipelineTask,
serviceId,
status: stringOrNull(item.status),
reason: stringOrNull(item.reason),
durationSeconds,
budgetSeconds: V02_BUILD_TASKRUN_WARNING_SECONDS,
severity: durationSeconds >= V02_BUILD_TASKRUN_CRITICAL_SECONDS ? "critical" : "warning",
message: `${pipelineTask} took ${durationSeconds}s, above v0.2 build TaskRun warning budget ${V02_BUILD_TASKRUN_WARNING_SECONDS}s`,
});
}
slowTaskRuns.sort((left, right) => Number(right.durationSeconds ?? 0) - Number(left.durationSeconds ?? 0));
const worst = slowTaskRuns[0];
return {
ok: slowTaskRuns.length === 0,
warningCount: slowTaskRuns.length,
thresholds: {
buildTaskRunWarningSeconds: V02_BUILD_TASKRUN_WARNING_SECONDS,
buildTaskRunCriticalSeconds: V02_BUILD_TASKRUN_CRITICAL_SECONDS,
},
slowTaskRuns,
summary: worst
? `slow build taskruns=${slowTaskRuns.length}; worst=${String(worst.pipelineTask)} ${String(worst.durationSeconds)}s budget=${V02_BUILD_TASKRUN_WARNING_SECONDS}s`
: `no build taskrun over ${V02_BUILD_TASKRUN_WARNING_SECONDS}s`,
};
}
function v02WebAssetsFromText(
text: string,
commandOk: boolean,
sourceCommit: string | null,
argoSyncRevision: string | null,
exitCode: number | null,
stderr: string,
activePipelineRuns: unknown[] = [],
): Record<string, unknown> {
const fields: Record<string, string> = {};
for (const line of text.split(/\r?\n/u)) {
const [key = "", ...rest] = line.split("\t");
if (key.length > 0) fields[key] = rest.join("\t");
}
const htmlOk = fields.htmlOk === "0";
const cssOk = fields.cssOk === "0";
const appJsOk = fields.appJsOk === "0";
const apiHealthOk = fields.apiHealthOk === "0";
const readonlyNoteAbsent = fields.readonlyNote === "1";
const sidebarFitCss = fields.sidebarFitCss === "0";
const workspaceFitCss = fields.workspaceFitCss === "0";
const eventPanelFitCss = fields.eventPanelFitCss === "0";
const apiRevision = fields.apiRevision || null;
const webChecksPass = htmlOk && cssOk && appJsOk && readonlyNoteAbsent && sidebarFitCss && workspaceFitCss && eventPanelFitCss && apiHealthOk;
const failedChecks = Object.entries({
htmlOk,
cssOk,
appJsOk,
readonlyNoteAbsent,
sidebarFitCss,
workspaceFitCss,
eventPanelFitCss,
apiHealthOk,
}).filter(([, ok]) => !ok).map(([name]) => name);
return {
ok: commandOk && webChecksPass,
summary: commandOk && webChecksPass ? "19666/19667 probes passed" : `19666/19667 probe issues: ${failedChecks.join(", ") || "command failed"}`,
baseUrl: fields.baseUrl || V02_CLOUD_WEB_URL,
apiUrl: fields.apiUrl || V02_CLOUD_API_URL,
sourceCommit,
argoSyncRevision: argoSyncRevision || null,
checks: {
htmlOk,
cssOk,
appJsOk,
readonlyNoteAbsent,
sidebarFitCss,
workspaceFitCss,
eventPanelFitCss,
apiHealthOk,
},
probeExitCodes: {
html: numericField(fields.htmlOk),
css: numericField(fields.cssOk),
appJs: numericField(fields.appJsOk),
readonlyNoteGrep: numericField(fields.readonlyNote),
sidebarFitCssGrep: numericField(fields.sidebarFitCss),
workspaceFitCssGrep: numericField(fields.workspaceFitCss),
eventPanelFitCssGrep: numericField(fields.eventPanelFitCss),
apiHealth: numericField(fields.apiHealthOk),
},
assetBytes: {
appJs: numericField(fields.appJsBytes),
},
apiRevision,
note: webAssetsRevisionNote(apiRevision, sourceCommit, activePipelineRuns),
exitCode,
stderr: commandOk ? "" : stderr.trim().slice(0, 2000),
};
}
function v02PlanArtifactsFromText(text: string, commandOk: boolean, pipelineRun: string | null, exitCode: number | null, stderr: string): Record<string, unknown> {
const events: Record<string, unknown>[] = [];
const pods: string[] = [];
for (const rawLine of text.split(/\r?\n/u)) {
const line = rawLine.trim();
if (line.startsWith("__POD__")) {
const [, pod = ""] = line.split("\t");
if (pod.length > 0) pods.push(pod);
continue;
}
if (!line.startsWith("{")) continue;
try {
const event = record(JSON.parse(line) as unknown);
if (event.event === "g14-ci-plan") events.push(event);
} catch {
// Ignore non-JSON log lines; the raw log remains visible through pod logs.
}
}
const latest = events.at(-1) ?? null;
if (!commandOk || latest === null) {
return {
ok: false,
pipelineRun,
pods,
eventFound: latest !== null,
degradedReason: commandOk ? "g14-ci-plan-event-not-found" : "plan-artifacts-log-query-failed",
exitCode,
stderr: commandOk ? "" : stderr.trim().slice(0, 2000),
};
}
const audit = record(latest.artifactProvenanceAudit);
const unsafeReuseServices = Array.isArray(audit.unsafeReuseServices) ? audit.unsafeReuseServices : [];
const provenanceRebuildServices = Array.isArray(audit.provenanceRebuildServices) ? audit.provenanceRebuildServices : [];
return {
ok: true,
pipelineRun,
pods,
sourceCommitId: stringOrNull(latest.sourceCommitId),
affectedServices: stringArray(latest.affectedServices),
rolloutServices: stringArray(latest.rolloutServices),
buildServices: stringArray(latest.buildServices),
reusedServices: stringArray(latest.reusedServices),
buildSkippedCount: numericValue(latest.buildSkippedCount),
artifactProvenanceAudit: Object.keys(audit).length > 0 ? audit : null,
summary: `build=${stringArray(latest.buildServices).length} reuse=${stringArray(latest.reusedServices).length} unsafeReuse=${unsafeReuseServices.length} provenanceRebuild=${provenanceRebuildServices.length}`,
disclosure: "parsed from plan-artifacts g14-ci-plan log event",
};
}
function v02RuntimeWorkloadsFromText(text: string, commandOk: boolean, exitCode: number | null, stderr: string): Record<string, unknown> {
if (!commandOk) {
return {
ok: false,
items: [],
exitCode,
stderr: stderr.trim().slice(0, 2000),
};
}
const items = text
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const [kind = "", name = "", serviceId = "", artifactSourceCommit = "", sourceCommit = "", image = "", ready = "", replicas = ""] = line.split(/\s+/u);
return {
kind,
name,
serviceId: serviceId === "<none>" ? null : serviceId,
artifactSourceCommit: artifactSourceCommit === "<none>" ? null : artifactSourceCommit,
sourceCommit: sourceCommit === "<none>" ? null : sourceCommit,
image: image === "<none>" ? null : image,
readyReplicas: numericField(ready),
replicas: numericField(replicas),
};
});
return {
ok: true,
items,
summary: `runtime deployments=${items.length}`,
};
}
function keyValueLinesFromText(text: string): Record<string, string> {
const fields: Record<string, string> = {};
for (const line of text.split(/\r?\n/u)) {
const [key = "", ...rest] = line.split("\t");
if (key.trim().length > 0) fields[key.trim()] = rest.join("\t").trim();
}
return fields;
}
function v02SourceHeadsFromText(text: string, commandOk: boolean, exitCode: number | null, stderr: string): Record<string, unknown> {
const fields = keyValueLinesFromText(text);
const dirtyCount = numericField(fields.workspaceDirtyCount);
return {
ok: commandOk,
cicdRepo: fields.cicdRepo || V02_CICD_REPO,
cicdRepoExists: fields.cicdRepoExists === "yes",
cicdSourceHead: fields.cicdSourceHead || null,
originHead: fields.originHead || null,
workspace: {
path: fields.workspacePath || V02_WORKSPACE,
branch: fields.workspaceBranch || null,
head: fields.workspaceHead || null,
originHead: fields.workspaceOriginHead || null,
dirtyCount,
dirty: typeof dirtyCount === "number" ? dirtyCount > 0 : null,
isolatedFromCicd: true,
},
exitCode,
stderr: commandOk ? "" : stderr.trim().slice(0, 2000),
};
}
export function v02FalseGreenGuard(input: {
sourceCommit: string | null;
pipelineRun: Record<string, unknown> | null;
taskRuns: Record<string, unknown>;
planArtifacts: Record<string, unknown>;
runtimeWorkloads: Record<string, unknown>;
}): Record<string, unknown> {
const status = stringOrNull(input.pipelineRun?.status);
if (input.sourceCommit === null || input.pipelineRun === null || input.pipelineRun.exists === false) {
return { ok: null, state: "not-started", summary: "current source commit has no PipelineRun yet" };
}
if (status !== "True") {
return { ok: null, state: "pipeline-not-final", summary: `PipelineRun status=${status ?? "unknown"}` };
}
if (input.planArtifacts.ok !== true) {
return {
ok: false,
state: "missing-plan-artifacts-evidence",
summary: "PipelineRun succeeded but plan-artifacts g14-ci-plan evidence was not visible",
planArtifacts: input.planArtifacts,
};
}
const sourceCommit = input.sourceCommit;
const buildServices = stringArray(input.planArtifacts.buildServices);
const reusedServices = stringArray(input.planArtifacts.reusedServices);
const taskItems = Array.isArray(input.taskRuns.items) ? input.taskRuns.items.map((item) => record(item)) : [];
const workloadItems = Array.isArray(input.runtimeWorkloads.items) ? input.runtimeWorkloads.items.map((item) => record(item)) : [];
const workloadByService = new Map(workloadItems.map((item) => [String(item.serviceId ?? ""), item]));
const missingBuildTaskRuns = buildServices.filter((serviceId) => !taskItems.some((item) => String(item.name ?? "").endsWith(`build-${serviceId}`) && item.status === "True"));
const runtimeMismatches = buildServices
.map((serviceId) => {
const workload = workloadByService.get(serviceId);
if (!workload) return { serviceId, reason: "runtime-workload-missing" };
if (workload.artifactSourceCommit !== sourceCommit) {
return {
serviceId,
reason: "artifact-source-commit-mismatch",
runtimeArtifactSourceCommit: workload.artifactSourceCommit ?? null,
expectedSourceCommit: sourceCommit,
image: workload.image ?? null,
};
}
return null;
})
.filter((item): item is Record<string, unknown> => item !== null);
const audit = record(input.planArtifacts.artifactProvenanceAudit);
const unsafeReuseServices = Array.isArray(audit.unsafeReuseServices) ? audit.unsafeReuseServices : [];
const auditPresent = input.planArtifacts.artifactProvenanceAudit !== null;
const provenanceWarning = !auditPresent && reusedServices.length > 0
? { reason: "artifact-provenance-audit-missing-for-reuse", reusedServices }
: null;
const provenanceOk = auditPresent ? audit.ok !== false && unsafeReuseServices.length === 0 : true;
const failures = [
...(input.runtimeWorkloads.ok === true ? [] : [{ reason: "runtime-workload-query-failed" }]),
...(provenanceOk ? [] : [{ reason: "artifact-provenance-audit-failed", unsafeReuseServices }]),
...missingBuildTaskRuns.map((serviceId) => ({ serviceId, reason: "expected-build-taskrun-not-succeeded" })),
...runtimeMismatches,
];
return {
ok: failures.length === 0,
state: failures.length === 0 ? "passed" : "failed",
summary: failures.length === 0
? `artifact provenance and built runtime services align with ${shortSha(sourceCommit)}`
: `false-green risk: ${failures.length} invariant violation(s)`,
sourceCommit,
buildServices,
reusedServices,
provenanceOk,
provenanceAuditPresent: auditPresent,
provenanceWarnings: provenanceWarning ? [provenanceWarning] : [],
missingBuildTaskRuns,
runtimeMismatches,
failures,
};
}
function v02TargetValidation(input: {
targetMode: V02StatusTargetMode;
sourceCommit: string | null;
pipelineRun: Record<string, unknown> | null;
argo: Record<string, unknown>;
planArtifacts: Record<string, unknown>;
runtimeWorkloads: Record<string, unknown>;
webAssets: Record<string, unknown>;
gitMirror: Record<string, unknown>;
recentPipelineRuns: Record<string, unknown>;
}): Record<string, unknown> {
if (input.targetMode === "latest-source-head") {
return {
applicable: false,
state: "latest-source-head",
summary: "default status uses strict latest source head alignment; use --pipeline-run or --source-commit for historical run validation",
};
}
const sourceCommit = input.sourceCommit;
const argoFields = record(input.argo.fields);
const gitMirrorSummary = record(input.gitMirror.summary);
const workloadItems = Array.isArray(input.runtimeWorkloads.items)
? input.runtimeWorkloads.items.map((item) => record(item))
: [];
const serviceSourceCommits = Object.fromEntries(workloadItems
.filter((item) => item.serviceId === "hwlab-cloud-api" || item.serviceId === "hwlab-cloud-web")
.map((item) => [String(item.serviceId), stringOrNull(item.sourceCommit) ?? stringOrNull(item.artifactSourceCommit)]));
const rolloutServices = stringArray(input.planArtifacts.rolloutServices)
.filter((serviceId) => serviceId === "hwlab-cloud-api" || serviceId === "hwlab-cloud-web");
const requiredRuntimeServices = rolloutServices;
const recentItems = Array.isArray(input.recentPipelineRuns.items)
? input.recentPipelineRuns.items.map((item) => record(item))
: [];
const targetPipelineRunName = stringOrNull(input.pipelineRun?.pipelineRun) ?? stringOrNull(input.pipelineRun?.name);
const targetRecentIndex = recentItems.findIndex((item) => (
(targetPipelineRunName !== null && item.name === targetPipelineRunName)
|| (sourceCommit !== null && item.sourceCommit === sourceCommit)
));
const newerCandidates = targetRecentIndex >= 0 ? recentItems.slice(0, targetRecentIndex) : recentItems;
const newerSucceededRuns = sourceCommit === null
? []
: newerCandidates.filter((item) => item.sourceCommit !== sourceCommit && item.status === "True");
const failures: Record<string, unknown>[] = [];
const supersededRuntimeServices: Record<string, unknown>[] = [];
if (sourceCommit === null) failures.push({ reason: "source-commit-unresolved" });
if (input.pipelineRun === null || input.pipelineRun.exists === false) {
failures.push({ reason: "pipeline-run-missing" });
} else if (input.pipelineRun.status !== "True") {
failures.push({ reason: "pipeline-run-not-succeeded", status: input.pipelineRun.status ?? null, reasonDetail: input.pipelineRun.reason ?? null });
}
if (input.argo.ok !== true || argoFields.syncStatus !== "Synced" || argoFields.health !== "Healthy") {
failures.push({ reason: "argo-not-synced-healthy", syncStatus: argoFields.syncStatus ?? null, health: argoFields.health ?? null });
}
if (input.webAssets.ok !== true) failures.push({ reason: "web-assets-probe-failed", summary: input.webAssets.summary ?? null });
if (gitMirrorSummary.pendingFlush !== false || gitMirrorSummary.githubInSync !== true) {
failures.push({
reason: "git-mirror-not-flushed",
pendingFlush: gitMirrorSummary.pendingFlush ?? null,
githubInSync: gitMirrorSummary.githubInSync ?? null,
});
}
for (const serviceId of requiredRuntimeServices) {
const serviceCommit = serviceSourceCommits[serviceId];
if (sourceCommit !== null && serviceCommit !== sourceCommit) {
const mismatch = { reason: "runtime-service-source-mismatch", serviceId, expectedSourceCommit: sourceCommit, actualSourceCommit: serviceCommit ?? null };
if (newerSucceededRuns.length > 0) supersededRuntimeServices.push(mismatch);
else failures.push(mismatch);
}
}
const superseded = failures.length === 0 && supersededRuntimeServices.length > 0;
return {
applicable: true,
ok: failures.length === 0,
state: failures.length === 0 ? (superseded ? "superseded" : "passed") : "failed",
summary: failures.length === 0
? superseded
? `target ${input.targetMode} completed for ${shortSha(sourceCommit ?? "")}; runtime now reflects newer v0.2 PipelineRun`
: requiredRuntimeServices.length === 0
? `target ${input.targetMode} validation passed for ${shortSha(sourceCommit ?? "")}; no runtime service rollout required`
: `target ${input.targetMode} validation passed for ${shortSha(sourceCommit ?? "")}`
: `target ${input.targetMode} validation failed with ${failures.length} issue(s)`,
sourceCommit,
pipelineRun: input.pipelineRun === null
? null
: { name: input.pipelineRun.pipelineRun ?? null, status: input.pipelineRun.status ?? null, reason: input.pipelineRun.reason ?? null },
argo: { syncRevision: argoFields.syncRevision ?? null, syncStatus: argoFields.syncStatus ?? null, health: argoFields.health ?? null },
runtimeServices: serviceSourceCommits,
requiredRuntimeServices,
rolloutServices: stringArray(input.planArtifacts.rolloutServices),
reusedServices: stringArray(input.planArtifacts.reusedServices),
superseded,
supersededRuntimeServices,
newerSucceededRuns: newerSucceededRuns.slice(0, 3).map((item) => ({
name: item.name ?? null,
sourceCommit: item.sourceCommit ?? null,
createdAt: item.createdAt ?? null,
durationSeconds: item.durationSeconds ?? null,
})),
webAssets: { ok: input.webAssets.ok ?? null, summary: input.webAssets.summary ?? null, appJsBytes: record(input.webAssets.assetBytes).appJs ?? null },
gitMirror: { pendingFlush: gitMirrorSummary.pendingFlush ?? null, githubInSync: gitMirrorSummary.githubInSync ?? null },
failures,
};
}
export function v02CommitAlignment(input: {
expectedSourceHead: string | null;
sourceHeads: Record<string, unknown>;
gitMirrorSummary: Record<string, unknown>;
pipelineRun: Record<string, unknown> | null;
recentPipelineRuns: Record<string, unknown>;
planArtifacts?: Record<string, unknown>;
runtimeWorkloads: Record<string, unknown>;
webAssets: Record<string, unknown>;
}): Record<string, unknown> {
const expectedSourceHead = input.expectedSourceHead;
const cicdSourceHead = stringOrNull(input.sourceHeads.cicdSourceHead);
const originHead = stringOrNull(input.sourceHeads.originHead) ?? expectedSourceHead;
const workspace = record(input.sourceHeads.workspace);
const workspaceHead = stringOrNull(workspace.head);
const workspaceOriginHead = stringOrNull(workspace.originHead);
const mirrorSourceHead = stringOrNull(input.gitMirrorSummary.localV02);
const mirrorGithubSourceHead = stringOrNull(input.gitMirrorSummary.githubV02);
const recentItems = Array.isArray(input.recentPipelineRuns.items)
? input.recentPipelineRuns.items.map((item) => record(item))
: [];
const latestPipelineRun = recentItems[0] ?? null;
const latestPipelineSourceCommit = latestPipelineRun === null ? null : stringOrNull(latestPipelineRun.sourceCommit);
const currentPipelineStatus = stringOrNull(input.pipelineRun?.status);
const apiRevision = stringOrNull(input.webAssets.apiRevision);
const planArtifacts = record(input.planArtifacts);
const planSourceCommit = stringOrNull(planArtifacts.sourceCommitId);
const rolloutServices = stringArray(planArtifacts.rolloutServices);
const apiRevisionRequired = expectedSourceHead !== null && planSourceCommit === expectedSourceHead && rolloutServices.includes("hwlab-cloud-api");
const workloadItems = Array.isArray(input.runtimeWorkloads.items)
? input.runtimeWorkloads.items.map((item) => record(item))
: [];
const serviceSourceCommits = Object.fromEntries(workloadItems
.filter((item) => typeof item.serviceId === "string" && item.serviceId.length > 0)
.map((item) => [String(item.serviceId), stringOrNull(item.sourceCommit) ?? stringOrNull(item.artifactSourceCommit)]));
const staleReasons: string[] = [];
if (expectedSourceHead === null) staleReasons.push("expected-source-head-unresolved");
if (expectedSourceHead !== null && originHead !== null && originHead !== expectedSourceHead) staleReasons.push("origin-head-mismatch");
if (expectedSourceHead !== null && cicdSourceHead !== null && cicdSourceHead !== expectedSourceHead) staleReasons.push("cicd-source-repo-stale");
if (expectedSourceHead !== null && mirrorSourceHead !== expectedSourceHead) staleReasons.push("mirror-source-stale");
if (expectedSourceHead !== null && latestPipelineSourceCommit !== expectedSourceHead) staleReasons.push("latest-pipelinerun-not-current");
if (apiRevisionRequired && apiRevision !== expectedSourceHead) staleReasons.push("runtime-api-revision-stale");
const workspaceWarnings: string[] = [];
if (expectedSourceHead !== null && workspaceHead !== null && workspaceHead !== expectedSourceHead) workspaceWarnings.push("workspace-head-differs-from-latest-source-but-isolated");
if (expectedSourceHead !== null && workspaceOriginHead !== null && workspaceOriginHead !== expectedSourceHead) workspaceWarnings.push("workspace-origin-ref-stale-but-isolated");
if (workspace.dirty === true) workspaceWarnings.push("workspace-dirty-but-isolated-from-cicd");
const runtimeWarnings: string[] = [];
if (!apiRevisionRequired && expectedSourceHead !== null && apiRevision !== null && apiRevision !== expectedSourceHead) {
runtimeWarnings.push("api-revision-differs-without-current-cloud-api-rollout");
}
const latestPipelineSucceeded = latestPipelineRun !== null && latestPipelineRun.status === "True";
const aligned = staleReasons.length === 0;
const state = aligned
? "aligned"
: latestPipelineSucceeded
? "stale-success"
: currentPipelineStatus === "Unknown"
? "in-progress"
: "stale";
return {
aligned,
state,
expectedSourceHead,
originHead,
cicdSourceHead,
cicdRepo: input.sourceHeads.cicdRepo ?? V02_CICD_REPO,
mirrorSourceHead,
mirrorGithubSourceHead,
latestPipelineRun: latestPipelineRun === null
? null
: {
name: latestPipelineRun.name ?? null,
sourceCommit: latestPipelineSourceCommit,
status: latestPipelineRun.status ?? null,
reason: latestPipelineRun.reason ?? null,
createdAt: latestPipelineRun.createdAt ?? null,
durationSeconds: latestPipelineRun.durationSeconds ?? null,
},
latestPipelineSourceCommit,
currentPipelineRun: input.pipelineRun === null
? null
: {
name: input.pipelineRun.pipelineRun ?? null,
status: input.pipelineRun.status ?? null,
reason: input.pipelineRun.reason ?? null,
},
runtimeSourceCommit: apiRevision,
apiRevision,
apiRevisionRequired,
planSourceCommit,
rolloutServices,
serviceSourceCommits,
sourceInSync: input.gitMirrorSummary.sourceInSync ?? null,
gitopsInSync: input.gitMirrorSummary.gitopsInSync ?? null,
staleReasons,
workspace: {
path: workspace.path ?? V02_WORKSPACE,
branch: workspace.branch ?? null,
head: workspaceHead,
originHead: workspaceOriginHead,
dirty: workspace.dirty ?? null,
dirtyCount: workspace.dirtyCount ?? null,
isolatedFromCicd: true,
},
workspaceWarnings,
runtimeWarnings,
summary: aligned
? apiRevisionRequired
? `v0.2 CI/CD source, mirror, PipelineRun, and cloud-api runtime align with ${shortSha(expectedSourceHead ?? "")}`
: `v0.2 CI/CD source, mirror, and PipelineRun align with ${shortSha(expectedSourceHead ?? "")}; cloud-api runtime rollout not required`
: `v0.2 CI/CD alignment state=${state}; staleReasons=${staleReasons.join(",")}`,
};
}
function webAssetsRevisionNote(apiRevision: string | null, sourceCommit: string | null, activePipelineRuns: unknown[]): string | null {
if (!apiRevision || !sourceCommit || apiRevision === sourceCommit) return null;
const activeItems = activePipelineRuns.map((item) => record(item));
const sourceIsActive = activeItems.some((item) => item.sourceCommit === sourceCommit || String(item.name ?? "").includes(shortSha(sourceCommit)));
if (sourceIsActive) {
return `runtime rollout still in progress; cloud-api apiRevision=${apiRevision} has not reached sourceCommit=${sourceCommit} yet`;
}
if (activeItems.length > 0) {
return `runtime apiRevision=${apiRevision} differs from sourceCommit=${sourceCommit}; active v02 PipelineRun observed, wait for rollout before judging runtime revision`;
}
return `cloud-api apiRevision=${apiRevision} is service-scoped and differs from sourceCommit=${sourceCommit}; when web asset probes pass, this is not proof that Cloud Web is stale`;
}
function numericField(value: string | undefined): number | null {
if (value === undefined || value.trim().length === 0) return null;
if (value === "<none>") return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
function numericValue(value: unknown): number | null {
const parsed = typeof value === "number" ? value : Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
function stringArray(value: unknown): string[] {
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.length > 0) : [];
}
function listV02PipelineRunsCompactFromText(text: string, commandOk: boolean, command: string[] | string, exitCode: number | null, stderr: string, limit = 8, nowMs = Date.now()): Record<string, unknown> {
if (!commandOk) {
return {
ok: false,
command: Array.isArray(command) ? redactedCommand(command) : command,
exitCode,
stderr: stderr.trim().slice(0, 4000),
items: [],
activeItems: [],
};
}
return parsePipelineRunRows(text, limit, nowMs);
}
function pipelinePrefixesForLane(lane: "v02" | "g14" | "all"): string[] {
if (lane === "v02") return ["hwlab-v02-ci-poll-"];
if (lane === "g14") return ["hwlab-g14-ci-poll-"];
return ["hwlab-v02-ci-poll-", "hwlab-g14-ci-poll-"];
}
function commandErrorSummary(result: CommandJsonResult): string {
const remoteStderr = String(nested(result.parsed, ["data", "stderr"]) ?? "").trim();
const remoteStdout = String(nested(result.parsed, ["data", "stdout"]) ?? "").trim();
const text = [result.stderr.trim(), remoteStderr, remoteStdout, result.stdout.trim()].find((item) => item.length > 0) ?? "";
return text.slice(0, 4000);
}
function tailText(value: unknown, maxBytes = 2000): string {
const text = String(value ?? "").trim();
return text.length <= maxBytes ? text : text.slice(-maxBytes);
}
function listCleanupPipelineRuns(options: G14ControlPlaneOptions): Record<string, unknown>[] {
const result = g14K3s([
"kubectl",
"get",
"pipelinerun",
"-n",
CI_NAMESPACE,
"-o",
'jsonpath={range .items[*]}{.metadata.name}{"\\t"}{.metadata.creationTimestamp}{"\\t"}{.status.conditions[0].status}{"\\t"}{.status.conditions[0].reason}{"\\n"}{end}',
], 60_000);
if (!isCommandSuccess(result)) {
throw new Error(`failed to list hwlab-ci PipelineRuns: ${commandErrorSummary(result)}`);
}
const prefixes = pipelinePrefixesForLane(options.lane);
const now = Date.now();
return statusText(result)
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const [name = "", createdAt = "", status = "", reason = ""] = line.split("\t");
const createdMs = Date.parse(createdAt);
const ageMinutes = Number.isFinite(createdMs) ? Math.floor((now - createdMs) / 60000) : null;
return { name, createdAt, ageMinutes, status: status || null, reason: reason || null };
})
.filter((item) => item.name.length > 0 && prefixes.some((prefix) => item.name.startsWith(prefix)))
.filter((item) => item.status === "True" || item.status === "False")
.filter((item) => typeof item.ageMinutes === "number" && item.ageMinutes >= options.minAgeMinutes)
.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)))
.slice(0, options.limit);
}
function listOwnedWorkspacePvcs(pipelineRunNames: string[]): Record<string, unknown>[] {
if (pipelineRunNames.length === 0) {
return [];
}
const result = g14K3s([
"kubectl",
"get",
"pvc",
"-n",
CI_NAMESPACE,
"-o",
'jsonpath={range .items[*]}{.metadata.name}{"\\t"}{.spec.volumeName}{"\\t"}{.status.phase}{"\\t"}{.metadata.ownerReferences[0].kind}{"\\t"}{.metadata.ownerReferences[0].name}{"\\n"}{end}',
], 60_000);
if (!isCommandSuccess(result)) {
throw new Error(`failed to list hwlab-ci PipelineRun PVCs: ${commandErrorSummary(result)}`);
}
const wanted = new Set(pipelineRunNames);
return statusText(result)
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const [name = "", volume = "", phase = "", ownerKind = "", owner = ""] = line.split("\t");
return {
name,
volume: volume || null,
phase: phase || null,
ownerKind: ownerKind || null,
owner: owner || null,
};
})
.filter((item) => item.ownerKind === "PipelineRun" && typeof item.owner === "string" && wanted.has(item.owner));
}
function deletePipelineRuns(names: string[], timeoutMs: number): CommandJsonResult {
if (names.length === 0) {
return {
ok: true,
command: [],
exitCode: 0,
stdout: "no candidates",
stderr: "",
parsed: null,
};
}
return g14K3s(["kubectl", "delete", "pipelinerun", "-n", CI_NAMESPACE, ...names, "--ignore-not-found=true"], timeoutMs);
}
function listReleasedCiWorkspacePvs(options: G14ControlPlaneOptions): Record<string, unknown>[] {
const result = g14K3s([
"kubectl",
"get",
"pv",
"-o",
'jsonpath={range .items[*]}{.metadata.name}{"\\t"}{.metadata.creationTimestamp}{"\\t"}{.status.phase}{"\\t"}{.spec.storageClassName}{"\\t"}{.spec.persistentVolumeReclaimPolicy}{"\\t"}{.spec.claimRef.namespace}{"\\t"}{.spec.claimRef.name}{"\\t"}{.spec.capacity.storage}{"\\n"}{end}',
], 60_000);
if (!isCommandSuccess(result)) {
throw new Error(`failed to list released hwlab-ci PVs: ${commandErrorSummary(result)}`);
}
return statusText(result)
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const [name = "", createdAt = "", phase = "", storageClass = "", reclaimPolicy = "", claimNamespace = "", claimName = "", capacity = ""] = line.split("\t");
return {
name,
createdAt,
phase,
storageClass,
reclaimPolicy,
claimNamespace,
claimName,
capacity,
hostPath: claimNamespace === CI_NAMESPACE && claimName.length > 0 ? `/var/lib/rancher/k3s/storage/${name}_${claimNamespace}_${claimName}` : null,
};
})
.filter((item) => item.phase === "Released")
.filter((item) => item.storageClass === "local-path" && item.reclaimPolicy === "Delete")
.filter((item) => item.claimNamespace === CI_NAMESPACE && typeof item.claimName === "string" && /^pvc-[a-z0-9]+$/u.test(item.claimName))
.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)))
.slice(0, options.limit);
}
function deletePersistentVolumes(names: string[], timeoutMs: number): CommandJsonResult {
if (names.length === 0) {
return {
ok: true,
command: [],
exitCode: 0,
stdout: "no candidates",
stderr: "",
parsed: null,
};
}
return g14K3s(["kubectl", "delete", "pv", ...names, "--ignore-not-found=true"], timeoutMs);
}
function runControlPlaneReleasedPvCleanup(options: G14ControlPlaneOptions): Record<string, unknown> {
const candidates = listReleasedCiWorkspacePvs(options);
const candidateNames = candidates.map((item) => String(item.name));
if (options.dryRun) {
return {
ok: true,
command: "hwlab g14 control-plane cleanup-released-pvs",
mode: "dry-run",
lane: options.lane,
limit: options.limit,
candidates,
candidateCount: candidates.length,
mutation: false,
next: { confirm: `bun scripts/cli.ts hwlab g14 control-plane cleanup-released-pvs --lane all --limit ${options.limit} --confirm` },
};
}
const deletion = deletePersistentVolumes(candidateNames, options.timeoutSeconds * 1000);
return {
ok: isCommandSuccess(deletion),
command: "hwlab g14 control-plane cleanup-released-pvs",
mode: "confirmed-cleanup",
lane: options.lane,
limit: options.limit,
deletedPersistentVolumes: candidateNames,
deletedPersistentVolumeCount: candidateNames.length,
candidatesBefore: candidates,
deletion,
followUp: {
diskPressure: "trans G14:k3s kubectl get node ubuntu-rog-zephyrus-g14-ga401iv-ga401iv -o jsonpath='{range .status.conditions[*]}{.type}{\"=\"}{.status}{\" \"}{.reason}{\"\\n\"}{end}'",
storage: "trans G14 script -- 'df -h /; sudo du -xh -d 1 /var/lib/rancher/k3s/storage 2>/dev/null | sort -h | tail -20'",
},
};
}
function runV02RuntimeMigration(options: G14ControlPlaneOptions, sourceCommit: string): Record<string, unknown> {
const reportPath = `/tmp/hwlab-v02-runtime-migration-${shortSha(sourceCommit)}.json`;
const migrationArgs = options.dryRun
? [
...(options.allowLiveDbRead ? ["--dry-run", "--allow-live-db-read", "--confirm-dev"] : ["--check"]),
"--report",
reportPath,
]
: [
"--apply",
"--confirm-dev",
"--confirmed-non-production",
"--report",
reportPath,
];
const command = [
"exec",
"-n",
"hwlab-v02",
"deployment/hwlab-cloud-api",
"-c",
"hwlab-cloud-api",
"--",
"bun",
"cmd/hwlab-cloud-api/migrate.ts",
...migrationArgs,
];
const result = g14K3s(["kubectl", ...command], options.timeoutSeconds * 1000);
const ok = isCommandSuccess(result);
return {
ok,
command: "hwlab g14 control-plane runtime-migration --lane v02",
lane: "v02",
mode: options.dryRun ? options.allowLiveDbRead ? "live-read-dry-run" : "source-check" : "confirmed-apply",
sourceCommit,
runtimeNamespace: "hwlab-v02",
target: "deployment/hwlab-cloud-api -c hwlab-cloud-api",
migrationCommand: ["bun", "cmd/hwlab-cloud-api/migrate.ts", ...migrationArgs],
reportPath,
mutation: !options.dryRun,
result,
next: options.dryRun
? {
liveReadDryRun: "bun scripts/cli.ts hwlab g14 control-plane runtime-migration --lane v02 --allow-live-db-read --dry-run",
apply: "bun scripts/cli.ts hwlab g14 control-plane runtime-migration --lane v02 --confirm",
}
: {
health: "curl -fsS --max-time 20 http://74.48.78.17:19667/health/live",
triggerCurrent: "bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm",
},
};
}
function runControlPlaneCleanup(options: G14ControlPlaneOptions): Record<string, unknown> {
const candidates = listCleanupPipelineRuns(options);
const candidateNames = candidates.map((item) => String(item.name));
const ownedPvcs = listOwnedWorkspacePvcs(candidateNames);
if (options.dryRun) {
return {
ok: true,
command: "hwlab g14 control-plane cleanup-runs",
mode: "dry-run",
lane: options.lane,
minAgeMinutes: options.minAgeMinutes,
limit: options.limit,
candidates,
candidateCount: candidates.length,
ownedPvcs,
ownedPvcCount: ownedPvcs.length,
mutation: false,
next: { confirm: `bun scripts/cli.ts hwlab g14 control-plane cleanup-runs --lane ${options.lane} --min-age-minutes ${options.minAgeMinutes} --limit ${options.limit} --confirm` },
};
}
const deletion = deletePipelineRuns(candidateNames, options.timeoutSeconds * 1000);
return {
ok: isCommandSuccess(deletion),
command: "hwlab g14 control-plane cleanup-runs",
mode: "confirmed-cleanup",
lane: options.lane,
minAgeMinutes: options.minAgeMinutes,
limit: options.limit,
deletedPipelineRuns: candidateNames,
deletedPipelineRunCount: candidateNames.length,
ownedPvcsBefore: ownedPvcs,
ownedPvcCountBefore: ownedPvcs.length,
deletion,
followUp: {
status: "bun scripts/cli.ts hwlab g14 control-plane status --lane v02",
diskPressure: "trans G14:k3s kubectl get node ubuntu-rog-zephyrus-g14-ga401iv-ga401iv -o jsonpath='{.spec.taints}{\"\\n\"}{range .status.conditions[*]}{.type}{\"=\"}{.status}{\" \"}{.reason}{\"\\n\"}{end}'",
},
};
}
export function v02ControlPlaneRenderScript(sourceCommit: string): string {
const renderDir = v02RenderDir(sourceCommit);
const worktreeDir = v02RenderWorktreeDir(sourceCommit);
return [
"set -eu",
v02CicdRepoEnsureScript(),
`render_dir=${shellQuote(renderDir)}`,
`worktree_dir=${shellQuote(worktreeDir)}`,
"cleanup_render_worktree() { git --git-dir=\"$cicd_repo\" worktree remove --force \"$worktree_dir\" >/dev/null 2>&1 || rm -rf \"$worktree_dir\"; }",
"trap cleanup_render_worktree EXIT",
`test "$(git --git-dir="$cicd_repo" rev-parse refs/remotes/origin/v0.2)" = ${shellQuote(sourceCommit)}`,
"cleanup_render_worktree",
"git --git-dir=\"$cicd_repo\" worktree prune >/dev/null 2>&1 || true",
"rm -rf \"$render_dir\"",
"mkdir -p \"$render_dir\" \"$(dirname \"$worktree_dir\")\"",
`git --git-dir="$cicd_repo" worktree add --detach "$worktree_dir" ${shellQuote(sourceCommit)}`,
"cd \"$worktree_dir\"",
`node scripts/g14-gitops-render.mjs --lane v02 --source-revision ${shellQuote(sourceCommit)} --out "$render_dir"`,
].join("\n");
}
function runV02RenderToTemp(sourceCommit: string): CommandJsonResult {
return g14HostScript(v02ControlPlaneRenderScript(sourceCommit), 180_000);
}
function v02RenderDir(sourceCommit: string): string {
return `/tmp/hwlab-v02-control-plane-${shortSha(sourceCommit)}`;
}
function v02RenderWorktreeDir(sourceCommit: string): string {
return `/tmp/hwlab-v02-control-plane-source-${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}/argocd/project.yaml`,
"-f",
`${renderDir}/argocd/application-v02.yaml`,
], timeoutSeconds * 1000);
}
function refreshV02ControlPlaneBeforeTrigger(sourceCommit: string, timeoutSeconds: number): Record<string, unknown> {
const render = runV02RenderToTemp(sourceCommit);
if (!isCommandSuccess(render)) {
return {
ok: false,
phase: "source-render",
sourceCommit,
renderDir: v02RenderDir(sourceCommit),
render,
degradedReason: "control-plane-render-failed",
};
}
const apply = applyV02ControlPlaneFiles(sourceCommit, false, timeoutSeconds);
const cleanupObsoleteCronJobs = isCommandSuccess(apply) ? deleteV02ObsoleteCronJobs(false) : null;
return {
ok: isCommandSuccess(apply) && (cleanupObsoleteCronJobs === null || isCommandSuccess(cleanupObsoleteCronJobs)),
phase: "control-plane-refresh",
sourceCommit,
renderDir: v02RenderDir(sourceCommit),
render: commandData(render),
apply: compactCommandResult(apply),
cleanupObsoleteCronJobs: cleanupObsoleteCronJobs === null ? null : compactCommandResult(cleanupObsoleteCronJobs),
degradedReason: !isCommandSuccess(apply)
? "control-plane-apply-failed"
: cleanupObsoleteCronJobs !== null && !isCommandSuccess(cleanupObsoleteCronJobs)
? "obsolete-cronjob-cleanup-failed"
: undefined,
};
}
function getV02ObsoleteCronJobs(): CommandJsonResult {
return g14K3s([
"kubectl",
"get",
"cronjob",
"-n",
CI_NAMESPACE,
V02_POLLER,
V02_RECONCILER,
"--ignore-not-found",
"-o",
"name",
], 60_000);
}
function deleteV02ObsoleteCronJobs(dryRun: boolean): CommandJsonResult {
return g14K3s([
"kubectl",
"delete",
"cronjob",
"-n",
CI_NAMESPACE,
V02_POLLER,
V02_RECONCILER,
"--ignore-not-found=true",
...(dryRun ? ["--dry-run=server", "-o", "name"] : []),
], 60_000);
}
function v02PipelineRunManifest(sourceCommit: string): Record<string, unknown> {
const pipelineRun = v02PipelineRunName(sourceCommit);
return {
apiVersion: "tekton.dev/v1",
kind: "PipelineRun",
metadata: {
name: pipelineRun,
namespace: CI_NAMESPACE,
labels: {
"app.kubernetes.io/part-of": "hwlab",
"hwlab.pikastech.local/gitops-target": "v02",
"hwlab.pikastech.local/source-commit": sourceCommit,
"hwlab.pikastech.local/trigger": "manual-cli",
},
annotations: {
"hwlab.pikastech.local/source-branch": V02_SOURCE_BRANCH,
"hwlab.pikastech.local/gitops-branch": V02_GITOPS_BRANCH,
"hwlab.pikastech.local/triggered-by": "unidesk-cli",
},
},
spec: {
pipelineRef: { name: V02_PIPELINE },
taskRunTemplate: {
serviceAccountName: "hwlab-v02-tekton-runner",
podTemplate: {
hostNetwork: true,
dnsPolicy: "ClusterFirstWithHostNet",
securityContext: { fsGroup: 1000 },
},
},
params: [
{ name: "git-url", value: V02_GIT_URL },
{ name: "git-read-url", value: V02_GIT_READ_URL },
{ name: "source-branch", value: V02_SOURCE_BRANCH },
{ name: "gitops-branch", value: V02_GITOPS_BRANCH },
{ name: "lane", value: "v02" },
{ name: "catalog-path", value: V02_CATALOG_PATH },
{ name: "image-tag-mode", value: "full" },
{ name: "runtime-path", value: V02_RUNTIME_PATH },
{ name: "revision", value: sourceCommit },
{ name: "registry-prefix", value: V02_REGISTRY_PREFIX },
{ name: "services", value: V02_SERVICE_IDS.join(",") },
{ name: "base-image", value: V02_BASE_IMAGE },
],
workspaces: [
{ name: "source", volumeClaimTemplate: { spec: { accessModes: ["ReadWriteOnce"], resources: { requests: { storage: "8Gi" } } } } },
{ name: "git-ssh", secret: { secretName: "hwlab-git-ssh" } },
],
},
};
}
function createV02PipelineRun(sourceCommit: string, timeoutSeconds: number): CommandJsonResult {
const manifest = JSON.stringify(v02PipelineRunManifest(sourceCommit));
const manifestB64 = Buffer.from(manifest, "utf8").toString("base64");
const pipelineRun = v02PipelineRunName(sourceCommit);
const script = [
"set -eu",
`manifest_b64=${shellQuote(manifestB64)}`,
`manifest_path=/tmp/${pipelineRun}.json`,
"printf '%s' \"$manifest_b64\" | base64 -d > \"$manifest_path\"",
"kubectl create -f \"$manifest_path\"",
`kubectl get pipelinerun -n ${shellQuote(CI_NAMESPACE)} ${shellQuote(pipelineRun)} -o jsonpath='{.metadata.name}{\"\\n\"}{.metadata.labels.hwlab\\.pikastech\\.local/source-commit}{\"\\n\"}{.status.conditions[0].status}{\"\\n\"}{.status.conditions[0].reason}{\"\\n\"}'`,
].join("\n");
return g14K3s(["script", "--", script], timeoutSeconds * 1000);
}
function deleteV02PipelineRun(pipelineRun: string): CommandJsonResult {
return g14K3s(["kubectl", "delete", "pipelinerun", "-n", CI_NAMESPACE, pipelineRun, "--ignore-not-found=true"], 60_000);
}
function v02ControlPlaneStatus(target: V02ControlPlaneStatusTarget = {}): Record<string, unknown> {
const targetMode: V02StatusTargetMode = target.mode
?? (target.pipelineRun !== undefined && target.pipelineRun !== null ? "pipeline-run" : target.sourceCommit !== undefined ? "source-commit" : "latest-source-head");
const strictHeadAlignment = targetMode === "latest-source-head";
const bundle = v02ControlPlaneStatusBundle({ ...target, mode: targetMode });
const sections = parseShellSections(statusText(bundle));
const sourceCommit = stringOrNull(sections.sourceCommit?.stdout) ?? null;
const pipelineRun = stringOrNull(sections.pipelineRunName?.stdout) ?? (sourceCommit === null ? null : v02PipelineRunName(sourceCommit));
const statusTargetFields = keyValueLinesFromText(sections.statusTarget?.stdout ?? "");
const queryNowMs = timestampMs(sections.queryNow?.stdout) ?? Date.now();
const sourceHeadsSection = sections.sourceHeads;
const controlPlane = sections.controlPlane;
const obsoleteCronJobs = sections.obsoleteCronJobs;
const argo = sections.argo;
const pipelineRunSection = sections.pipelineRun;
const taskRunsSection = sections.taskRuns;
const planArtifactsSection = sections.planArtifacts;
const runtimeWorkloadsSection = sections.runtimeWorkloads;
const gitMirrorCacheSection = sections.gitMirrorCache;
const webAssetsSection = sections.webAssets;
const recentPipelineRuns = listV02PipelineRunsCompactFromText(
sections.recentPipelineRuns?.stdout ?? "",
shellSectionOk(sections.recentPipelineRuns),
"kubectl get pipelinerun -n hwlab-ci -l hwlab.pikastech.local/gitops-target=v02 -o jsonpath=<summary-fields>",
sections.recentPipelineRuns?.exitCode ?? null,
bundle.stderr,
8,
queryNowMs,
);
const activePipelineRuns = Array.isArray(recentPipelineRuns.activeItems) ? recentPipelineRuns.activeItems : [];
const [targetRevision = "", path = "", syncRevision = "", syncStatus = "", health = ""] = String(argo?.stdout ?? "").split(/\r?\n/u);
const sourceHeads = v02SourceHeadsFromText(
sourceHeadsSection?.stdout ?? "",
shellSectionOk(sourceHeadsSection),
sourceHeadsSection?.exitCode ?? null,
bundle.stderr,
);
const pipelineRunInfo = pipelineRun === null
? null
: pipelineRunCompactFromText(
pipelineRun,
pipelineRunSection?.stdout ?? "",
shellSectionOk(pipelineRunSection),
`kubectl get pipelinerun -n hwlab-ci ${pipelineRun}`,
pipelineRunSection?.exitCode ?? null,
bundle.stderr,
);
const taskRuns = taskRunsCompactFromText(
taskRunsSection?.stdout ?? "",
shellSectionOk(taskRunsSection),
pipelineRun,
taskRunsSection?.exitCode ?? null,
bundle.stderr,
);
const planArtifacts = v02PlanArtifactsFromText(
planArtifactsSection?.stdout ?? "",
shellSectionOk(planArtifactsSection),
pipelineRun,
planArtifactsSection?.exitCode ?? null,
bundle.stderr,
);
const runtimeWorkloads = v02RuntimeWorkloadsFromText(
runtimeWorkloadsSection?.stdout ?? "",
shellSectionOk(runtimeWorkloadsSection),
runtimeWorkloadsSection?.exitCode ?? null,
bundle.stderr,
);
const webAssets = v02WebAssetsFromText(
webAssetsSection?.stdout ?? "",
shellSectionOk(webAssetsSection),
sourceCommit,
syncRevision,
webAssetsSection?.exitCode ?? null,
bundle.stderr,
activePipelineRuns,
);
const gitMirror = {
ok: shellSectionOk(gitMirrorCacheSection),
summary: gitMirrorStatusSummary(String(gitMirrorCacheSection?.stdout ?? "").trim()),
raw: String(gitMirrorCacheSection?.stdout ?? "").trim(),
exitCode: gitMirrorCacheSection?.exitCode ?? null,
stderr: shellSectionOk(gitMirrorCacheSection) ? "" : bundle.stderr.trim().slice(0, 2000),
};
const commitAlignment = v02CommitAlignment({
expectedSourceHead: sourceCommit,
sourceHeads,
gitMirrorSummary: record(gitMirror.summary),
pipelineRun: pipelineRunInfo,
recentPipelineRuns,
planArtifacts,
runtimeWorkloads,
webAssets,
});
const targetValidation = v02TargetValidation({
targetMode,
sourceCommit,
pipelineRun: pipelineRunInfo,
argo: {
ok: shellSectionOk(argo),
fields: { targetRevision, path, syncRevision, syncStatus, health },
},
planArtifacts,
runtimeWorkloads,
webAssets,
gitMirror,
recentPipelineRuns,
});
const falseGreenGuard = targetValidation.state === "superseded"
? {
ok: null,
state: "superseded",
summary: "target PipelineRun succeeded but runtime now reflects a newer v0.2 PipelineRun; current-runtime false-green guard is not applicable to this historical target",
sourceCommit,
targetValidationState: targetValidation.state,
newerSucceededRuns: targetValidation.newerSucceededRuns ?? [],
supersededRuntimeServices: targetValidation.supersededRuntimeServices ?? [],
}
: v02FalseGreenGuard({ sourceCommit, pipelineRun: pipelineRunInfo, taskRuns, planArtifacts, runtimeWorkloads });
const baseOk = sourceCommit !== null && isCommandSuccess(bundle) && shellSectionOk(controlPlane) && shellSectionOk(argo);
const targetPipelineRunOk = strictHeadAlignment
? true
: pipelineRunInfo !== null && pipelineRunInfo.ok === true && pipelineRunInfo.exists !== false;
const targetDegradedReason = !strictHeadAlignment && !targetPipelineRunOk ? "target-pipelinerun-not-found-or-unreadable" : undefined;
const statusCommand = targetMode === "pipeline-run" && pipelineRun !== null
? `hwlab g14 control-plane status --lane v02 --pipeline-run ${pipelineRun}`
: targetMode === "source-commit" && sourceCommit !== null
? `hwlab g14 control-plane status --lane v02 --source-commit ${sourceCommit}`
: "hwlab g14 control-plane status --lane v02";
const state = strictHeadAlignment ? commitAlignment.state : `target-${targetMode}`;
const sourceCommitSource = targetMode === "pipeline-run"
? "PipelineRun metadata label hwlab.pikastech.local/source-commit"
: targetMode === "source-commit"
? "explicit --source-commit"
: "G14 CI/CD dedicated bare repo refs/remotes/origin/v0.2; workspace checkout is observable but isolated";
return {
ok: baseOk && targetPipelineRunOk && (!strictHeadAlignment || commitAlignment.aligned !== false),
command: statusCommand,
lane: "v02",
state,
degradedReason: strictHeadAlignment && commitAlignment.aligned === false ? commitAlignment.state : targetDegradedReason,
statusTarget: {
mode: statusTargetFields.mode || targetMode,
sourceCommit,
pipelineRun,
strictHeadAlignment,
note: strictHeadAlignment
? "default status validates the latest v0.2 source head alignment"
: "targeted status inspects the requested PipelineRun/source commit without failing merely because origin/v0.2 advanced later",
},
sourceCommit,
sourceCommitSource,
expected: {
sourceRepo: V02_CICD_REPO,
workspace: V02_WORKSPACE,
workspaceIsolation: "workspace dirty/stale state must not select CI/CD source commits",
branch: V02_SOURCE_BRANCH,
namespace: CI_NAMESPACE,
runtimeNamespace: "hwlab-v02",
pipeline: V02_PIPELINE,
trigger: "manual UniDesk CLI PipelineRun creation",
argoApplication: V02_APP,
},
commitAlignment,
targetValidation,
sourceHeads,
gitMirror,
controlPlane: {
ok: shellSectionOk(controlPlane),
names: String(controlPlane?.stdout ?? "").split(/\r?\n/u).map((line) => line.trim()).filter(Boolean),
exitCode: controlPlane?.exitCode ?? null,
},
obsoleteCronJobs: {
ok: shellSectionOk(obsoleteCronJobs),
names: String(obsoleteCronJobs?.stdout ?? "").split(/\r?\n/u).map((line) => line.trim()).filter(Boolean),
exitCode: obsoleteCronJobs?.exitCode ?? null,
cleanupCommand: "bun scripts/cli.ts hwlab g14 control-plane apply --lane v02 --confirm",
},
argo: {
ok: shellSectionOk(argo),
raw: argo?.stdout ?? "",
fields: { targetRevision, path, syncRevision, syncStatus, health },
exitCode: argo?.exitCode ?? null,
},
pipelineRun: pipelineRunInfo,
taskRuns,
planArtifacts,
runtimeWorkloads,
falseGreenGuard,
webAssets,
activePipelineRuns,
recentPipelineRuns,
query: {
ok: isCommandSuccess(bundle),
exitCode: bundle.exitCode,
stderr: bundle.stderr.trim().slice(0, 2000),
},
visibilityHint: activePipelineRuns.length > 0
? "activePipelineRuns shows running v02 CI even when origin/v0.2 advanced after a previous trigger"
: "no active v02 PipelineRun observed",
};
}
function runV02ControlPlane(options: G14ControlPlaneOptions): Record<string, unknown> {
if (options.action === "cleanup-runs") return runControlPlaneCleanup(options);
if (options.action === "cleanup-released-pvs") return runControlPlaneReleasedPvCleanup(options);
if (options.action === "status" && options.pipelineRun !== undefined) return v02ControlPlaneStatus({ pipelineRun: options.pipelineRun, mode: "pipeline-run" });
if (options.action === "status" && options.sourceCommit !== undefined) return v02ControlPlaneStatus({ sourceCommit: options.sourceCommit, mode: "source-commit" });
const sourceCommit = getV02Head();
if (sourceCommit === null) {
return { ok: false, command: `hwlab g14 control-plane ${options.action} --lane v02`, degradedReason: "v02-head-unresolved", sourceRepo: V02_CICD_REPO, workspace: V02_WORKSPACE };
}
if (options.action === "runtime-migration") return runV02RuntimeMigration(options, sourceCommit);
if (options.action === "status") return v02ControlPlaneStatus({ sourceCommit, mode: "latest-source-head" });
if (options.action === "apply") {
const render = runV02RenderToTemp(sourceCommit);
if (!isCommandSuccess(render)) {
return {
ok: false,
command: `hwlab g14 control-plane ${options.action} --lane v02`,
phase: "source-render",
sourceCommit,
render: compactCommandResult(render),
};
}
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),
render: commandData(render),
apply: compactCommandResult(apply),
cleanupObsoleteCronJobs: compactCommandResult(options.dryRun ? deleteV02ObsoleteCronJobs(true) : deleteV02ObsoleteCronJobs(false)),
status: v02ControlPlaneStatus({ sourceCommit, mode: "latest-source-head" }),
next: options.dryRun
? { apply: "bun scripts/cli.ts hwlab g14 control-plane apply --lane v02 --confirm" }
: { triggerCurrent: "bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm" },
};
}
const before = getPipelineRunCompact(v02PipelineRunName(sourceCommit));
if (options.dryRun) {
const gitMirrorPreSync = preSyncV02GitMirror(sourceCommit, options);
return {
ok: true,
command: "hwlab g14 control-plane trigger-current --lane v02",
lane: "v02",
mode: "dry-run",
sourceCommit,
pipelineRun: v02PipelineRunName(sourceCommit),
before,
gitMirrorPreSync,
controlPlaneRefresh: {
mode: "skipped-dry-run",
wouldRefreshBeforeCreate: true,
resources: ["tekton-v02/rbac.yaml", "tekton-v02/pipeline.yaml", "argocd/project.yaml", "argocd/application-v02.yaml"],
},
manifest: v02PipelineRunManifest(sourceCommit),
next: { triggerCurrent: "bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm" },
};
}
if (before.status === "True" || before.status === "Unknown") {
return {
ok: false,
command: "hwlab g14 control-plane trigger-current --lane v02",
lane: "v02",
mode: "confirmed-trigger",
sourceCommit,
pipelineRun: v02PipelineRunName(sourceCommit),
before,
degradedReason: "refuse-active-or-successful-pipelinerun",
};
}
printProgressEvent("hwlab.v02.trigger.progress", { stage: "control-plane-refresh", status: "started", sourceCommit, pipelineRun: v02PipelineRunName(sourceCommit) });
const controlPlaneRefresh = refreshV02ControlPlaneBeforeTrigger(sourceCommit, options.timeoutSeconds);
printProgressEvent("hwlab.v02.trigger.progress", { stage: "control-plane-refresh", status: controlPlaneRefresh.ok === true ? "succeeded" : "failed", sourceCommit, pipelineRun: v02PipelineRunName(sourceCommit), degradedReason: record(controlPlaneRefresh).degradedReason ?? null });
if (controlPlaneRefresh.ok !== true) {
return {
ok: false,
command: "hwlab g14 control-plane trigger-current --lane v02",
lane: "v02",
mode: "confirmed-trigger",
phase: "control-plane-refresh",
sourceCommit,
pipelineRun: v02PipelineRunName(sourceCommit),
before,
controlPlaneRefresh,
degradedReason: record(controlPlaneRefresh).degradedReason ?? "control-plane-refresh-failed",
};
}
printProgressEvent("hwlab.v02.trigger.progress", {
stage: "git-mirror-pre-sync",
status: "started",
sourceCommit,
pipelineRun: v02PipelineRunName(sourceCommit),
reason: "ensure local git mirror has source commit before creating PipelineRun",
});
const gitMirrorPreSync = preSyncV02GitMirror(sourceCommit, options);
printProgressEvent("hwlab.v02.trigger.progress", {
stage: "git-mirror-pre-sync",
status: gitMirrorPreSync.ok === true ? "succeeded" : "failed",
sourceCommit,
pipelineRun: v02PipelineRunName(sourceCommit),
mode: record(gitMirrorPreSync).mode ?? null,
required: nested(gitMirrorPreSync, ["before", "required"]) ?? null,
localV02: nested(gitMirrorPreSync, ["before", "localV02"]) ?? null,
pendingFlush: nested(gitMirrorPreSync, ["before", "pendingFlush"]) ?? null,
reason: nested(gitMirrorPreSync, ["before", "reason"]) ?? null,
syncElapsedMs: nested(gitMirrorPreSync, ["sync", "elapsedMs"]) ?? null,
degradedReason: record(gitMirrorPreSync).degradedReason ?? null,
});
if (gitMirrorPreSync.ok !== true) {
return {
ok: false,
command: "hwlab g14 control-plane trigger-current --lane v02",
lane: "v02",
mode: "confirmed-trigger",
phase: "git-mirror-pre-sync",
sourceCommit,
pipelineRun: v02PipelineRunName(sourceCommit),
before,
controlPlaneRefresh,
gitMirrorPreSync,
degradedReason: "git-mirror-pre-sync-failed",
};
}
printProgressEvent("hwlab.v02.trigger.progress", { stage: "delete-existing-pipelinerun", status: "started", sourceCommit, pipelineRun: v02PipelineRunName(sourceCommit) });
const deletePipelineRun = deleteV02PipelineRun(v02PipelineRunName(sourceCommit));
printProgressEvent("hwlab.v02.trigger.progress", { stage: "delete-existing-pipelinerun", status: isCommandSuccess(deletePipelineRun) ? "succeeded" : "failed", sourceCommit, pipelineRun: v02PipelineRunName(sourceCommit), exitCode: deletePipelineRun.exitCode });
printProgressEvent("hwlab.v02.trigger.progress", { stage: "create-pipelinerun", status: "started", sourceCommit, pipelineRun: v02PipelineRunName(sourceCommit) });
const createPipelineRun = isCommandSuccess(deletePipelineRun)
? createV02PipelineRun(sourceCommit, options.timeoutSeconds)
: null;
printProgressEvent("hwlab.v02.trigger.progress", { stage: "create-pipelinerun", status: createPipelineRun !== null && isCommandSuccess(createPipelineRun) ? "succeeded" : "failed", sourceCommit, pipelineRun: v02PipelineRunName(sourceCommit), exitCode: createPipelineRun?.exitCode ?? null });
return {
ok: isCommandSuccess(deletePipelineRun) && createPipelineRun !== null && isCommandSuccess(createPipelineRun),
command: "hwlab g14 control-plane trigger-current --lane v02",
lane: "v02",
mode: "confirmed-trigger",
sourceCommit,
pipelineRun: v02PipelineRunName(sourceCommit),
before,
controlPlaneRefresh: compactControlPlaneRefresh(controlPlaneRefresh),
gitMirrorPreSync,
deletePipelineRun: compactTriggerCommandResult(compactCommandResult(deletePipelineRun)),
createPipelineRun: createPipelineRun === null ? null : compactTriggerCommandResult(compactCommandResult(createPipelineRun)),
after: getPipelineRunCompact(v02PipelineRunName(sourceCommit)),
disclosure: {
fullTriggerOutput: "Use the async job stdout/stderr files from job status for full command details.",
},
};
}
function v02SecretScript(options: G14SecretOptions): string {
const emitAfterStatus = [
"after_exists=$(secret_exists_flag)",
"after_b64=$(secret_b64)",
"after_key_present=$([ -n \"$after_b64\" ] && printf yes || printf no)",
"after_value_bytes=$(decoded_length \"$after_b64\")",
"printf 'namespace\\t%s\\n' \"$namespace\"",
"printf 'secret\\t%s\\n' \"$name\"",
"printf 'key\\t%s\\n' \"$key\"",
"printf 'action\\t%s\\n' \"$action\"",
"printf 'dryRun\\t%s\\n' \"$dry_run\"",
"printf 'mutation\\t%s\\n' \"$mutation\"",
"printf 'beforeExists\\t%s\\n' \"$before_exists\"",
"printf 'beforeKeyPresent\\t%s\\n' \"$before_key_present\"",
"printf 'beforeValueBytes\\t%s\\n' \"$before_value_bytes\"",
"printf 'afterExists\\t%s\\n' \"$after_exists\"",
"printf 'afterKeyPresent\\t%s\\n' \"$after_key_present\"",
"printf 'afterValueBytes\\t%s\\n' \"$after_value_bytes\"",
"printf 'applyExitCode\\t%s\\n' \"$apply_exit\"",
].join("\n");
return [
"set +e",
`namespace=${shellQuote(V02_RUNTIME_NAMESPACE)}`,
`name=${shellQuote(options.name)}`,
`key=${shellQuote(options.key)}`,
`action_request=${shellQuote(options.action)}`,
`dry_run=${shellQuote(options.dryRun ? "true" : "false")}`,
`field_manager=${shellQuote(V02_SECRET_FIELD_MANAGER)}`,
"secret_exists_flag() { kubectl -n \"$namespace\" get secret \"$name\" >/dev/null 2>&1 && printf yes || printf no; }",
"secret_b64() { kubectl -n \"$namespace\" get secret \"$name\" -o \"go-template={{ index .data \\\"$key\\\" }}\" 2>/dev/null || true; }",
"decoded_length() { if [ -n \"$1\" ]; then printf '%s' \"$1\" | base64 -d 2>/dev/null | wc -c | tr -d ' '; else printf '0'; fi; }",
"before_exists=$(secret_exists_flag)",
"before_b64=$(secret_b64)",
"before_key_present=$([ -n \"$before_b64\" ] && printf yes || printf no)",
"before_value_bytes=$(decoded_length \"$before_b64\")",
"action=observed",
"mutation=false",
"apply_exit=",
"if [ \"$action_request\" = ensure ]; then",
" if [ \"$dry_run\" = true ]; then",
" if [ \"$before_key_present\" = yes ] && [ \"$before_value_bytes\" -gt 0 ]; then action=kept; else action=would-create; fi",
" elif [ \"$before_key_present\" = yes ] && [ \"$before_value_bytes\" -gt 0 ]; then",
" action=kept",
" else",
" if ! command -v openssl >/dev/null 2>&1; then",
" action=openssl-missing",
" apply_exit=127",
" else",
" generated_key=$(openssl rand -base64 48)",
" kubectl -n \"$namespace\" create secret generic \"$name\" --from-literal=\"$key=$generated_key\" --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=\"$field_manager\" -f -",
" apply_exit=$?",
" generated_key=",
" if [ \"$apply_exit\" -eq 0 ]; then action=ensured; mutation=true; else action=apply-failed; fi",
" fi",
" fi",
"fi",
emitAfterStatus,
"if [ -n \"$apply_exit\" ] && [ \"$apply_exit\" != 0 ]; then exit \"$apply_exit\"; fi",
].join("\n");
}
function v02SecretStatusFromText(text: string, commandOk: boolean, exitCode: number | null, stderr: string): Record<string, unknown> {
const fields = keyValueLinesFromText(text);
const afterExists = fields.afterExists === "yes";
const afterKeyPresent = fields.afterKeyPresent === "yes";
const afterValueBytes = numericField(fields.afterValueBytes);
const healthy = afterExists && afterKeyPresent && typeof afterValueBytes === "number" && afterValueBytes > 0;
return {
ok: commandOk && healthy,
namespace: fields.namespace || V02_RUNTIME_NAMESPACE,
secret: fields.secret || V02_DEVICE_POD_API_KEY_SECRET,
key: fields.key || V02_DEVICE_POD_API_KEY_SECRET_KEY,
action: fields.action || null,
dryRun: fields.dryRun === "true",
mutation: fields.mutation === "true",
before: {
exists: fields.beforeExists === "yes",
keyPresent: fields.beforeKeyPresent === "yes",
valueBytes: numericField(fields.beforeValueBytes),
},
after: {
exists: afterExists,
keyPresent: afterKeyPresent,
valueBytes: afterValueBytes,
},
applyExitCode: numericField(fields.applyExitCode),
exitCode,
stderr: commandOk ? "" : stderr.trim().slice(0, 2000),
valuesRedacted: true,
summary: healthy
? `${fields.secret || V02_DEVICE_POD_API_KEY_SECRET}/${fields.key || V02_DEVICE_POD_API_KEY_SECRET_KEY} exists`
: `${fields.secret || V02_DEVICE_POD_API_KEY_SECRET}/${fields.key || V02_DEVICE_POD_API_KEY_SECRET_KEY} missing or empty`,
};
}
function runG14Secret(options: G14SecretOptions): Record<string, unknown> {
const script = v02SecretScript(options);
const result = g14K3s(["script", "--", script], options.timeoutSeconds * 1000);
const status = v02SecretStatusFromText(statusText(result), isCommandSuccess(result), result.exitCode, result.stderr);
const dryRunOk = options.action === "ensure" && options.dryRun && isCommandSuccess(result);
const ok = dryRunOk ? true : status.ok === true;
return {
ok,
command: `hwlab g14 secret ${options.action} --lane v02`,
lane: options.lane,
namespace: V02_RUNTIME_NAMESPACE,
secret: options.name,
key: options.key,
mode: options.action === "status" ? "status" : options.dryRun ? "dry-run" : "confirmed-ensure",
status,
mutation: status.mutation === true,
result: compactCommandResult(result),
valuesRedacted: true,
next: ok && options.action === "status"
? undefined
: { ensure: `bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name ${options.name} --key ${options.key} --confirm` },
};
}
function deleteLegacyGitMirrorCronJob(dryRun: boolean): CommandJsonResult {
return g14K3s([
"kubectl",
"delete",
"cronjob",
"-n",
GIT_MIRROR_NAMESPACE,
GIT_MIRROR_LEGACY_CRONJOB,
"--ignore-not-found=true",
...(dryRun ? ["--dry-run=server", "-o", "name"] : []),
], 60_000);
}
function applyGitMirrorManifestFile(sourceCommit: string, dryRun: boolean, timeoutSeconds: number): CommandJsonResult {
const renderDir = v02RenderDir(sourceCommit);
return g14K3s([
"kubectl",
"apply",
"--server-side",
"--force-conflicts",
`--field-manager=${GIT_MIRROR_MANIFEST_FIELD_MANAGER}`,
...(dryRun ? ["--dry-run=server"] : []),
"-f",
`${renderDir}/devops-infra/git-mirror.yaml`,
], timeoutSeconds * 1000);
}
function gitMirrorSyncJobName(): string {
return `${GIT_MIRROR_SYNC_JOB_PREFIX}-${Date.now().toString(36)}`.slice(0, 63);
}
function gitMirrorFlushJobName(): string {
return `git-mirror-hwlab-flush-manual-${Date.now().toString(36)}`.slice(0, 63);
}
export function gitMirrorSyncJobManifest(name: string): Record<string, unknown> {
return {
apiVersion: "batch/v1",
kind: "Job",
metadata: {
name,
namespace: GIT_MIRROR_NAMESPACE,
labels: {
"app.kubernetes.io/name": "git-mirror",
"app.kubernetes.io/part-of": "devops-infra",
"app.kubernetes.io/component": "sync-controller",
"hwlab.pikastech.local/trigger": "manual-cli",
},
},
spec: {
backoffLimit: 0,
activeDeadlineSeconds: 300,
ttlSecondsAfterFinished: 3600,
template: {
metadata: {
labels: {
"app.kubernetes.io/name": "git-mirror",
"app.kubernetes.io/part-of": "devops-infra",
"app.kubernetes.io/component": "sync-controller",
"hwlab.pikastech.local/trigger": "manual-cli",
},
},
spec: {
restartPolicy: "Never",
hostNetwork: true,
dnsPolicy: "ClusterFirstWithHostNet",
volumes: [
{ name: "cache", persistentVolumeClaim: { claimName: "git-mirror-cache" } },
{ name: "git-ssh", secret: { secretName: "git-mirror-github-ssh", defaultMode: 0o400 } },
{ name: "script", configMap: { name: "git-mirror-sync-script", defaultMode: 0o755 } },
],
containers: [{
name: "sync",
image: "127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1",
imagePullPolicy: "IfNotPresent",
command: ["/script/sync.sh"],
volumeMounts: [
{ name: "cache", mountPath: "/cache" },
{ name: "git-ssh", mountPath: "/git-ssh", readOnly: true },
{ name: "script", mountPath: "/script", readOnly: true },
],
}],
},
},
},
};
}
export function gitMirrorFlushJobManifest(name: string): Record<string, unknown> {
const manifest = gitMirrorSyncJobManifest(name);
record(record(manifest.metadata).labels)["app.kubernetes.io/component"] = "flush-controller";
const template = record(record(manifest.spec).template);
record(record(template.metadata).labels)["app.kubernetes.io/component"] = "flush-controller";
const podSpec = record(template.spec);
const containers = Array.isArray(podSpec.containers) ? podSpec.containers : [];
const first = record(containers[0]);
first.name = "flush";
first.command = ["/script/flush.sh"];
podSpec.containers = [first];
return manifest;
}
export function parseGitMirrorStatusRefs(raw: string): { refs: Record<string, string | null>; pendingFlush: boolean | null } {
const line = raw.split(/\r?\n/u).find((item) => item.startsWith("refs="));
if (line === undefined) return { refs: {}, pendingFlush: null };
try {
const parsed = record(JSON.parse(line.slice("refs=".length)) as unknown);
const refs = record(parsed.refs);
return {
refs: {
localV02: stringOrNull(refs.localV02),
githubV02: stringOrNull(refs.githubV02),
localG14: stringOrNull(refs.localG14),
githubG14: stringOrNull(refs.githubG14),
localGitops: stringOrNull(refs.localGitops),
githubGitops: stringOrNull(refs.githubGitops),
},
pendingFlush: typeof parsed.pendingFlush === "boolean" ? parsed.pendingFlush : null,
};
} catch {
return { refs: {}, pendingFlush: null };
}
}
function parseLabeledJsonLine(raw: string, label: string): Record<string, unknown> {
const line = raw.split(/\r?\n/u).find((item) => item.startsWith(`${label}=`));
if (line === undefined) return {};
const value = line.slice(label.length + 1).trim();
if (value.length === 0) return {};
try {
return record(JSON.parse(value) as unknown);
} catch {
return {};
}
}
export function gitMirrorStatusSummary(raw: string): Record<string, unknown> {
const refs = parseGitMirrorStatusRefs(raw);
const lastSync = parseLabeledJsonLine(raw, "lastSync");
const lastWrite = parseLabeledJsonLine(raw, "lastWrite");
const lastFlush = parseLabeledJsonLine(raw, "lastFlush");
const localV02 = refs.refs.localV02 ?? null;
const githubV02 = refs.refs.githubV02 ?? null;
const localGitops = refs.refs.localGitops ?? null;
const githubGitops = refs.refs.githubGitops ?? null;
const sourceInSync = Boolean(localV02 && githubV02 && localV02 === githubV02);
const gitopsInSync = Boolean(localGitops && githubGitops && localGitops === githubGitops);
return {
localV02,
githubV02,
localG14: refs.refs.localG14 ?? null,
githubG14: refs.refs.githubG14 ?? null,
localGitops,
githubGitops,
pendingFlush: refs.pendingFlush,
lastSync: {
status: stringOrNull(lastSync.status) ?? null,
at: stringOrNull(lastSync.publishedAt) ?? stringOrNull(lastSync.syncedAt) ?? null,
pendingFlush: typeof lastSync.pendingFlush === "boolean" ? lastSync.pendingFlush : null,
},
lastWrite: {
status: stringOrNull(lastWrite.status) ?? null,
at: stringOrNull(lastWrite.writtenAt) ?? null,
pendingFlush: typeof lastWrite.pendingFlush === "boolean" ? lastWrite.pendingFlush : null,
},
lastFlush: {
status: stringOrNull(lastFlush.status) ?? null,
at: stringOrNull(lastFlush.flushedAt) ?? null,
pendingFlush: typeof lastFlush.pendingFlush === "boolean" ? lastFlush.pendingFlush : null,
},
flushNeeded: refs.pendingFlush === true,
flushCommand: refs.pendingFlush === true ? "bun scripts/cli.ts hwlab g14 git-mirror flush --confirm" : null,
sourceInSync,
gitopsInSync,
githubInSync: sourceInSync && gitopsInSync,
};
}
export function gitMirrorV02SyncRequirement(sourceCommit: string, rawStatus: string): Record<string, unknown> {
const parsed = parseGitMirrorStatusRefs(rawStatus);
const localV02 = parsed.refs.localV02 ?? null;
return {
required: localV02 !== sourceCommit,
sourceCommit,
localV02,
pendingFlush: parsed.pendingFlush,
refs: parsed.refs,
reason: localV02 === sourceCommit ? "local-v02-current" : localV02 === null ? "local-v02-unresolved" : "local-v02-stale",
};
}
function gitMirrorStatusCacheRaw(status: Record<string, unknown>): string {
return String(nested(status, ["cache", "raw"]) ?? "");
}
function compactGitMirrorStatus(status: Record<string, unknown>, sourceCommit: string): Record<string, unknown> {
const requirement = gitMirrorV02SyncRequirement(sourceCommit, gitMirrorStatusCacheRaw(status));
return {
ok: status.ok === true,
readUrl: status.readUrl ?? V02_GIT_READ_URL,
writeUrl: status.writeUrl ?? V02_GIT_WRITE_URL,
elapsedMs: status.elapsedMs ?? null,
legacyCronJobExists: nested(status, ["legacyCronJob", "exists"]) === true,
required: requirement.required,
sourceCommit,
localV02: requirement.localV02 ?? null,
githubV02: requirement.refs.githubV02 ?? null,
pendingFlush: requirement.pendingFlush ?? null,
sourceInSync: Boolean(requirement.refs.localV02 && requirement.refs.githubV02 && requirement.refs.localV02 === requirement.refs.githubV02),
reason: requirement.reason,
cacheOk: nested(status, ["cache", "ok"]) === true,
cacheStderr: tailText(nested(status, ["cache", "stderr"]), 1000),
resourcesOk: nested(status, ["resources", "ok"]) === true,
};
}
function compactGitMirrorSync(sync: Record<string, unknown>): Record<string, unknown> {
return {
ok: sync.ok === true,
command: sync.command ?? "hwlab g14 git-mirror sync",
mode: sync.mode ?? null,
namespace: sync.namespace ?? GIT_MIRROR_NAMESPACE,
jobName: sync.jobName ?? null,
elapsedMs: sync.elapsedMs ?? null,
exitCode: nested(sync, ["result", "exitCode"]) ?? null,
stdoutTail: tailText(nested(sync, ["result", "stdout"]), 1600),
stderrTail: tailText(nested(sync, ["result", "stderr"]), 1200),
stdoutBytes: nested(sync, ["result", "stdoutBytes"]) ?? null,
stderrBytes: nested(sync, ["result", "stderrBytes"]) ?? null,
};
}
function gitMirrorCacheProbeScript(): string {
return [
"printf 'lastSync='; cat /cache/HWLAB.last-sync.json 2>/dev/null || true; printf '\\n'",
"printf 'lastWrite='; cat /cache/HWLAB.last-write.json 2>/dev/null || true; printf '\\n'",
"printf 'lastFlush='; cat /cache/HWLAB.last-flush.json 2>/dev/null || true; printf '\\n'",
"printf 'refs='",
"node - <<'NODE' 2>/dev/null || true",
"const { execFileSync } = require('node:child_process');",
"const repo = '/cache/pikasTech/HWLAB.git';",
"function rev(ref) {",
" try {",
" return execFileSync('git', ['--git-dir=' + repo, 'rev-parse', ref], { encoding: 'utf8' }).trim();",
" } catch {",
" return null;",
" }",
"}",
"const localGitops = rev('refs/heads/v0.2-gitops');",
"const githubGitops = rev('refs/mirror-stage/heads/v0.2-gitops');",
"console.log(JSON.stringify({",
" refs: {",
" localV02: rev('refs/heads/v0.2'),",
" githubV02: rev('refs/mirror-stage/heads/v0.2'),",
" localG14: rev('refs/heads/G14'),",
" githubG14: rev('refs/mirror-stage/heads/G14'),",
" localGitops,",
" githubGitops,",
" },",
" pendingFlush: Boolean(localGitops && githubGitops && localGitops !== githubGitops),",
"}));",
"NODE",
].join("\n");
}
function preSyncV02GitMirror(sourceCommit: string, options: Pick<G14ControlPlaneOptions, "dryRun" | "timeoutSeconds">): Record<string, unknown> {
const statusStartMs = Date.now();
printProgressEvent("hwlab.v02.trigger.progress", { stage: "git-mirror-pre-sync-status", status: "started", sourceCommit });
const before = runGitMirrorStatus();
const beforeSummary = compactGitMirrorStatus(before, sourceCommit);
printProgressEvent("hwlab.v02.trigger.progress", {
stage: "git-mirror-pre-sync-status",
status: beforeSummary.ok === true ? "succeeded" : "failed",
sourceCommit,
durationMs: Date.now() - statusStartMs,
required: beforeSummary.required,
localV02: beforeSummary.localV02,
pendingFlush: beforeSummary.pendingFlush,
reason: beforeSummary.reason,
});
if (options.dryRun) {
return {
ok: true,
mode: "dry-run",
sourceCommit,
before: beforeSummary,
action: beforeSummary.required === true ? "would-sync-before-trigger" : "already-current",
next: beforeSummary.required === true ? { syncAndTrigger: "bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm" } : undefined,
};
}
if (beforeSummary.required !== true) {
return {
ok: true,
mode: "already-current",
sourceCommit,
before: beforeSummary,
};
}
printProgressEvent("hwlab.v02.trigger.progress", {
stage: "git-mirror-pre-sync-sync",
status: "started",
sourceCommit,
reason: beforeSummary.reason,
localV02: beforeSummary.localV02,
pendingFlush: beforeSummary.pendingFlush,
});
const sync = runGitMirrorSync({
action: "sync",
confirm: true,
dryRun: false,
timeoutSeconds: options.timeoutSeconds,
});
printProgressEvent("hwlab.v02.trigger.progress", {
stage: "git-mirror-pre-sync-sync",
status: sync.ok === true ? "succeeded" : "failed",
sourceCommit,
durationMs: sync.elapsedMs ?? null,
jobName: sync.jobName ?? null,
});
const syncStatus = record(sync.status);
const after = syncStatus.ok === true ? syncStatus : runGitMirrorStatus();
const afterSummary = compactGitMirrorStatus(after, sourceCommit);
const ok = sync.ok === true && afterSummary.required === false;
return {
ok,
mode: "auto-sync-before-trigger",
sourceCommit,
before: beforeSummary,
sync: compactGitMirrorSync(sync),
after: afterSummary,
degradedReason: ok ? undefined : "git-mirror-local-v02-not-current-after-sync",
};
}
function runGitMirrorStatus(): Record<string, unknown> {
const startedAtMs = Date.now();
const script = [
"set +e",
"section() {",
" name=\"$1\"",
" shift",
" printf '__UNIDESK_SECTION_BEGIN__ %s\\n' \"$name\"",
" \"$@\"",
" code=$?",
" printf '\\n__UNIDESK_SECTION_END__ %s exit=%s\\n' \"$name\" \"$code\"",
"}",
[
"section resources kubectl get deploy,svc,pvc,cm",
`-n ${shellQuote(GIT_MIRROR_NAMESPACE)}`,
"-l app.kubernetes.io/name=git-mirror",
"-o name",
].join(" "),
[
"section legacyCronJob kubectl get cronjob",
`-n ${shellQuote(GIT_MIRROR_NAMESPACE)}`,
shellQuote(GIT_MIRROR_LEGACY_CRONJOB),
"--ignore-not-found",
"-o name",
].join(" "),
[
`section jobs kubectl get job -n ${shellQuote(GIT_MIRROR_NAMESPACE)}`,
"-l app.kubernetes.io/name=git-mirror",
"--sort-by=.metadata.creationTimestamp",
"-o custom-columns=NAME:.metadata.name,SUCCEEDED:.status.succeeded,FAILED:.status.failed,START:.status.startTime,COMPLETION:.status.completionTime",
"--no-headers",
].join(" "),
[
"section cache kubectl exec",
`-n ${shellQuote(GIT_MIRROR_NAMESPACE)}`,
"deploy/git-mirror-http",
"-- sh -lc",
shellQuote(gitMirrorCacheProbeScript()),
].join(" "),
].join("\n");
const bundle = g14K3s(["script", "--", script], 60_000);
const sections = parseShellSections(statusText(bundle));
const resources = sections.resources;
const legacyCronJob = sections.legacyCronJob;
const jobs = sections.jobs;
const cache = sections.cache;
const bundleStderr = bundle.stderr.trim().slice(0, 2000);
return {
ok: isCommandSuccess(bundle) && shellSectionOk(resources),
command: "hwlab g14 git-mirror status",
namespace: GIT_MIRROR_NAMESPACE,
readUrl: V02_GIT_READ_URL,
writeUrl: V02_GIT_WRITE_URL,
elapsedMs: Date.now() - startedAtMs,
resources: {
ok: shellSectionOk(resources),
names: String(resources?.stdout ?? "").split(/\r?\n/u).map((line) => line.trim()).filter(Boolean),
stderr: shellSectionOk(resources) ? "" : bundleStderr,
},
legacyCronJob: {
ok: shellSectionOk(legacyCronJob),
exists: String(legacyCronJob?.stdout ?? "").trim().length > 0,
name: String(legacyCronJob?.stdout ?? "").trim() || null,
cleanupCommand: "bun scripts/cli.ts hwlab g14 git-mirror apply --confirm",
},
jobs: {
ok: shellSectionOk(jobs),
raw: String(jobs?.stdout ?? "").trim(),
stderr: shellSectionOk(jobs) ? "" : bundleStderr,
},
cache: {
ok: shellSectionOk(cache),
summary: gitMirrorStatusSummary(String(cache?.stdout ?? "").trim()),
raw: String(cache?.stdout ?? "").trim(),
stderr: shellSectionOk(cache) ? "" : bundleStderr,
},
next: {
apply: "bun scripts/cli.ts hwlab g14 git-mirror apply --confirm",
sync: "bun scripts/cli.ts hwlab g14 git-mirror sync --confirm",
flush: "bun scripts/cli.ts hwlab g14 git-mirror flush --confirm",
},
};
}
function runGitMirrorApply(options: G14GitMirrorOptions): Record<string, unknown> {
const sourceCommit = getV02Head();
if (sourceCommit === null) {
return { ok: false, command: "hwlab g14 git-mirror apply", degradedReason: "v02-head-unresolved", sourceRepo: V02_CICD_REPO, workspace: V02_WORKSPACE };
}
const render = runV02RenderToTemp(sourceCommit);
if (!isCommandSuccess(render)) {
return {
ok: false,
command: "hwlab g14 git-mirror apply",
phase: "source-render",
sourceCommit,
render: compactCommandResult(render),
};
}
const apply = applyGitMirrorManifestFile(sourceCommit, options.dryRun, options.timeoutSeconds);
const cleanupLegacyCronJob = isCommandSuccess(apply)
? deleteLegacyGitMirrorCronJob(options.dryRun)
: {
ok: false,
command: [],
exitCode: null,
stdout: "",
stderr: "skipped because apply failed",
parsed: null,
};
return {
ok: isCommandSuccess(apply) && isCommandSuccess(cleanupLegacyCronJob),
command: "hwlab g14 git-mirror apply",
mode: options.dryRun ? "dry-run" : "confirmed-apply",
sourceCommit,
manifest: `${v02RenderDir(sourceCommit)}/devops-infra/git-mirror.yaml`,
render: commandData(render),
apply: compactCommandResult(apply),
cleanupLegacyCronJob: compactCommandResult(cleanupLegacyCronJob),
status: runGitMirrorStatus(),
next: options.dryRun
? { apply: "bun scripts/cli.ts hwlab g14 git-mirror apply --confirm" }
: { sync: "bun scripts/cli.ts hwlab g14 git-mirror sync --confirm" },
};
}
function runGitMirrorSync(options: G14GitMirrorOptions): Record<string, unknown> {
const startedAtMs = Date.now();
const jobName = gitMirrorSyncJobName();
const manifest = gitMirrorSyncJobManifest(jobName);
const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
const script = [
"set -eu",
`job=${shellQuote(jobName)}`,
`manifest_b64=${shellQuote(manifestB64)}`,
"manifest_path=\"/tmp/$job.json\"",
"printf '%s' \"$manifest_b64\" | base64 -d > \"$manifest_path\"",
options.dryRun
? "kubectl create --dry-run=server -f \"$manifest_path\" -o name"
: [
`kubectl delete job -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "$job" --ignore-not-found=true >/dev/null`,
"kubectl create -f \"$manifest_path\"",
`deadline=$(( $(date +%s) + ${options.timeoutSeconds} ))`,
"while :; do",
` status=$(kubectl get job -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "$job" -o jsonpath='succeeded={.status.succeeded} failed={.status.failed}' 2>/dev/null || true)`,
" succeeded=$(printf '%s\\n' \"$status\" | awk '{for (i = 1; i <= NF; i++) { split($i, a, \"=\"); if (a[1] == \"succeeded\") print a[2]; }}')",
" failed=$(printf '%s\\n' \"$status\" | awk '{for (i = 1; i <= NF; i++) { split($i, a, \"=\"); if (a[1] == \"failed\") print a[2]; }}')",
" if [ \"${succeeded:-0}\" = \"1\" ]; then break; fi",
" if [ \"${failed:-0}\" != \"\" ] && [ \"${failed:-0}\" != \"0\" ]; then",
` kubectl logs -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "job/$job" --tail=200 || true`,
" exit 44",
" fi",
" if [ \"$(date +%s)\" -ge \"$deadline\" ]; then",
` kubectl get job,pod -n ${shellQuote(GIT_MIRROR_NAMESPACE)} -l job-name="$job" -o wide || true`,
" exit 45",
" fi",
" sleep 2",
"done",
`kubectl logs -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "job/$job" --tail=200 || true`,
`kubectl exec -n ${shellQuote(GIT_MIRROR_NAMESPACE)} deploy/git-mirror-http -- sh -lc 'cat /cache/HWLAB.last-sync.json 2>/dev/null || true; printf "\\n"; git --git-dir=/cache/pikasTech/HWLAB.git rev-parse refs/heads/v0.2 refs/heads/v0.2-gitops refs/heads/G14 2>/dev/null || true'`,
].join("\n"),
].join("\n");
const result = g14K3s(["script", "--", script], options.timeoutSeconds * 1000 + 30_000);
return {
ok: isCommandSuccess(result),
command: "hwlab g14 git-mirror sync",
mode: options.dryRun ? "dry-run" : "confirmed-sync",
namespace: GIT_MIRROR_NAMESPACE,
jobName,
elapsedMs: Date.now() - startedAtMs,
manifest: options.dryRun ? manifest : undefined,
result: compactCommandResult(result),
status: options.dryRun ? undefined : runGitMirrorStatus(),
next: options.dryRun ? { sync: "bun scripts/cli.ts hwlab g14 git-mirror sync --confirm" } : { triggerCurrent: "bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm" },
};
}
function runGitMirrorFlush(options: G14GitMirrorOptions): Record<string, unknown> {
const jobName = gitMirrorFlushJobName();
const manifest = gitMirrorFlushJobManifest(jobName);
const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
const script = [
"set -eu",
`job=${shellQuote(jobName)}`,
`manifest_b64=${shellQuote(manifestB64)}`,
"manifest_path=\"/tmp/$job.json\"",
"printf '%s' \"$manifest_b64\" | base64 -d > \"$manifest_path\"",
options.dryRun
? "kubectl create --dry-run=server -f \"$manifest_path\" -o name"
: [
`kubectl delete job -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "$job" --ignore-not-found=true >/dev/null`,
"kubectl create -f \"$manifest_path\"",
`deadline=$(( $(date +%s) + ${options.timeoutSeconds} ))`,
"while :; do",
` status=$(kubectl get job -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "$job" -o jsonpath='succeeded={.status.succeeded} failed={.status.failed}' 2>/dev/null || true)`,
" succeeded=$(printf '%s\n' \"$status\" | awk '{for (i = 1; i <= NF; i++) { split($i, a, \"=\"); if (a[1] == \"succeeded\") print a[2]; }}')",
" failed=$(printf '%s\n' \"$status\" | awk '{for (i = 1; i <= NF; i++) { split($i, a, \"=\"); if (a[1] == \"failed\") print a[2]; }}')",
" if [ \"${succeeded:-0}\" = \"1\" ]; then break; fi",
" if [ \"${failed:-0}\" != \"\" ] && [ \"${failed:-0}\" != \"0\" ]; then",
` kubectl logs -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "job/$job" --tail=200 || true`,
" exit 54",
" fi",
" if [ \"$(date +%s)\" -ge \"$deadline\" ]; then",
` kubectl get job,pod -n ${shellQuote(GIT_MIRROR_NAMESPACE)} -l job-name="$job" -o wide || true`,
" exit 55",
" fi",
" sleep 2",
"done",
`kubectl logs -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "job/$job" --tail=200 || true`,
`kubectl exec -n ${shellQuote(GIT_MIRROR_NAMESPACE)} deploy/git-mirror-http -- sh -lc 'cat /cache/HWLAB.last-flush.json 2>/dev/null || true; printf "\\n"; git --git-dir=/cache/pikasTech/HWLAB.git rev-parse refs/heads/v0.2-gitops refs/mirror-stage/heads/v0.2-gitops 2>/dev/null || true'`,
].join("\n"),
].join("\n");
const result = g14K3s(["script", "--", script], options.timeoutSeconds * 1000 + 30_000);
return {
ok: isCommandSuccess(result),
command: "hwlab g14 git-mirror flush",
mode: options.dryRun ? "dry-run" : "confirmed-flush",
namespace: GIT_MIRROR_NAMESPACE,
jobName,
manifest: options.dryRun ? manifest : undefined,
result: compactCommandResult(result),
status: options.dryRun ? undefined : runGitMirrorStatus(),
next: options.dryRun ? { flush: "bun scripts/cli.ts hwlab g14 git-mirror flush --confirm" } : { status: "bun scripts/cli.ts hwlab g14 git-mirror status" },
};
}
function runG14GitMirror(options: G14GitMirrorOptions): Record<string, unknown> {
if (options.action === "status") return runGitMirrorStatus();
if (options.action === "apply") return runGitMirrorApply(options);
if (options.action === "flush") return runGitMirrorFlush(options);
return runGitMirrorSync(options);
}
function startAsyncHwlabG14Job(name: string, command: string[], note: string): Record<string, unknown> {
const job = startJob(name, command, note);
const statusCommand = `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`;
return {
ok: true,
mode: "async-job",
job,
statusCommand,
tailCommand: `tail -f ${job.stdoutFile}`,
next: {
status: statusCommand,
tail: `tail -f ${job.stdoutFile}`,
},
};
}
function startControlPlaneTriggerJob(options: G14ControlPlaneOptions): Record<string, unknown> {
const command = [
"bun",
"scripts/cli.ts",
"hwlab",
"g14",
"control-plane",
"trigger-current",
"--lane",
options.lane,
"--confirm",
"--timeout-seconds",
String(options.timeoutSeconds),
"--wait",
];
return {
command: "hwlab g14 control-plane trigger-current --lane v02",
lane: options.lane,
reason: "confirmed trigger can spend tens of seconds syncing git mirror and creating PipelineRun; default is fire-and-forget to avoid silent blocking",
waitCommand: command.join(" "),
...startAsyncHwlabG14Job(
"hwlab_g14_v02_trigger_current",
command,
"Trigger HWLAB v0.2 current commit PipelineRun with git mirror pre-sync through G14 control-plane",
),
};
}
function startGitMirrorJob(options: G14GitMirrorOptions): Record<string, unknown> {
const command = [
"bun",
"scripts/cli.ts",
"hwlab",
"g14",
"git-mirror",
options.action,
"--confirm",
"--timeout-seconds",
String(options.timeoutSeconds),
"--wait",
];
return {
command: `hwlab g14 git-mirror ${options.action}`,
namespace: GIT_MIRROR_NAMESPACE,
reason: "manual git mirror sync/flush waits for a Kubernetes Job; default is fire-and-forget to keep CLI output immediately visible",
waitCommand: command.join(" "),
...startAsyncHwlabG14Job(
`hwlab_g14_git_mirror_${options.action}`,
command,
`Run HWLAB devops-infra git mirror ${options.action} through a bounded manual Kubernetes Job`,
),
};
}
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);
}
function preflightPullRequest(number: number): CommandJsonResult {
return cliJson(["gh", "pr", "preflight", String(number), "--repo", HWLAB_REPO], 80_000);
}
function mergePullRequest(number: number, dryRun: boolean): CommandJsonResult {
return cliJson(["gh", "pr", "merge", String(number), "--repo", HWLAB_REPO, "--merge", "--delete-branch", ...(dryRun ? ["--dry-run"] : [])], 100_000);
}
function getG14Head(): string | null {
const result = cliJson(["ssh", `${G14_PROVIDER}:${G14_WORKSPACE}`, "script", "--", "git fetch origin G14 --prune >/dev/null 2>&1; git rev-parse origin/G14"], 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 refreshArgoDev(): void {
cliJson(["ssh", `${G14_PROVIDER}:k3s`, "kubectl", "annotate", "application", "-n", ARGO_NAMESPACE, DEV_APP, "argocd.argoproj.io/refresh=hard", "--overwrite"], 60_000);
}
function getPipelineStatus(sourceCommit: string): CommandJsonResult {
return cliJson(["ssh", `${G14_PROVIDER}:k3s`, "kubectl", "get", "pipelinerun", "-n", CI_NAMESPACE, `hwlab-g14-ci-poll-${shortSha(sourceCommit)}`, "-o", "jsonpath={.status.conditions[0].status}{\"\\n\"}{.status.conditions[0].reason}{\"\\n\"}{.status.conditions[0].message}{\"\\n\"}"], 60_000);
}
function getArgoStatus(): CommandJsonResult {
return cliJson(["ssh", `${G14_PROVIDER}:k3s`, "kubectl", "get", "application", "-n", ARGO_NAMESPACE, DEV_APP, "-o", "jsonpath={.status.sync.revision}{\"\\n\"}{.status.sync.status}{\"\\n\"}{.status.health.status}{\"\\n\"}{.status.operationState.phase}{\"\\n\"}{.status.operationState.syncResult.revision}{\"\\n\"}"], 60_000);
}
function getDevWorkloads(): CommandJsonResult {
return cliJson(["ssh", `${G14_PROVIDER}:k3s`, "kubectl", "get", "deploy,statefulset,pod", "-n", DEV_NAMESPACE, "-o", "wide"], 60_000);
}
function getLiveHealth(): CommandJsonResult {
return commandJson(["curl", "-fsS", "--max-time", "20", "http://74.48.78.17:17667/health/live"], 30_000);
}
function statusText(result: CommandJsonResult): string {
return String(nested(result.parsed, ["data", "stdout"]) ?? result.stdout).trim();
}
function workloadReadiness(workloadsText: string, commandOk: boolean): Record<string, unknown> {
const blockers: string[] = [];
const ignoredPods: string[] = [];
const readyWorkloads: string[] = [];
const allowedZeroDeployments = new Set(["deployment.apps/hwlab-gateway", "deployment.apps/hwlab-tunnel-client"]);
for (const line of workloadsText.split(/\r?\n/u)) {
const trimmed = line.trim();
if (trimmed.length === 0 || trimmed.startsWith("NAME ")) continue;
const fields = trimmed.split(/\s+/u);
const name = fields[0] ?? "";
const ready = fields[1] ?? "";
if (name.startsWith("deployment.apps/") || name.startsWith("statefulset.apps/")) {
if (ready === "0/0" && allowedZeroDeployments.has(name)) {
readyWorkloads.push(`${name}:${ready}:scaled-zero`);
continue;
}
const match = /^(\d+)\/(\d+)$/u.exec(ready);
if (match === null) {
blockers.push(`${name}:unparseable-ready:${ready}`);
continue;
}
const readyCount = Number(match[1]);
const desiredCount = Number(match[2]);
if (desiredCount > 0 && readyCount < desiredCount) blockers.push(`${name}:not-ready:${ready}`);
else readyWorkloads.push(`${name}:${ready}`);
continue;
}
if (name.startsWith("pod/")) {
const status = fields[2] ?? "";
if (status === "Completed" || status === "Succeeded") ignoredPods.push(`${name}:${status}`);
if (/^(CrashLoopBackOff|ImagePullBackOff|ErrImagePull|CreateContainerConfigError|CreateContainerError|RunContainerError)$/u.test(status)) {
blockers.push(`${name}:pod-status:${status}`);
}
}
}
return {
ready: commandOk && blockers.length === 0,
blockers,
readyWorkloads: readyWorkloads.slice(0, 20),
ignoredPods: ignoredPods.slice(0, 20),
};
}
function beijingParts(date = new Date()): { date: string; time: string; iso: string } {
const shifted = new Date(date.getTime() + BEIJING_OFFSET_MS).toISOString();
return { date: shifted.slice(0, 10), time: shifted.slice(11, 19), iso: shifted };
}
function durationSeconds(start?: string | null, end?: string | null): number | null {
if (start === undefined || start === null || end === undefined || end === null) return null;
const startMs = Date.parse(start);
const endMs = Date.parse(end);
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs < startMs) return null;
return Math.round((endMs - startMs) / 1000);
}
function formatDuration(seconds: number | null): string {
if (seconds === null) return "n/a";
const minutes = Math.floor(seconds / 60);
const rest = seconds % 60;
if (minutes === 0) return `${rest}s`;
return `${minutes}m${String(rest).padStart(2, "0")}s`;
}
function pipelineTaskRunResultsJsonPath(): string {
return [
"jsonpath=",
"{range .items[*]}",
"{.metadata.name}{\"\\t\"}",
"{.metadata.labels.tekton\\.dev/pipelineTask}{\"\\t\"}",
"{.status.startTime}{\"\\t\"}",
"{.status.completionTime}{\"\\t\"}",
"{range .status.results[*]}{.name}{\"=\"}{.value}{\";\"}{end}",
"{\"\\n\"}",
"{end}",
].join("");
}
function getPipelineTaskRunResults(pipelineRun: string): CommandJsonResult {
return cliJson([
"ssh",
`${G14_PROVIDER}:k3s`,
"kubectl",
"get",
"taskrun",
"-n",
CI_NAMESPACE,
"-l",
`tekton.dev/pipelineRun=${pipelineRun}`,
"-o",
pipelineTaskRunResultsJsonPath(),
], 80_000);
}
function parseResultMap(raw: string): Record<string, string> {
const values: Record<string, string> = {};
for (const item of raw.split(";")) {
if (item.length === 0) continue;
const separator = item.indexOf("=");
if (separator <= 0) continue;
values[item.slice(0, separator)] = item.slice(separator + 1);
}
return values;
}
function serviceMetricSort(left: CiServiceMetric, right: CiServiceMetric): number {
return left.serviceId.localeCompare(right.serviceId);
}
export function parsePipelineTaskRunMetrics(pipelineRun: string, text: string): CiPipelineMetrics {
const services: CiServiceMetric[] = [];
for (const line of text.split(/\r?\n/u)) {
const trimmed = line.trimEnd();
if (trimmed.length === 0) continue;
const [taskRunName = "", pipelineTask = "", startTime = "", completionTime = "", resultsRaw = ""] = trimmed.split("\t");
if (!pipelineTask.startsWith("build-")) continue;
const results = parseResultMap(resultsRaw);
const serviceId = results["service-id"] ?? pipelineTask.replace(/^build-/u, "");
if (serviceId.length === 0) continue;
services.push({
taskRun: taskRunName,
serviceId,
status: results.status ?? "unknown",
durationSeconds: durationSeconds(startTime, completionTime),
buildBackend: results["build-backend"] ?? null,
reusedFrom: results["reused-from"] ?? null,
imageTag: results["image-tag"] ?? null,
sourceCommitId: results["source-commit-id"] ?? null,
});
}
services.sort(serviceMetricSort);
const reusedServices = services.filter((service) => service.status === "reused").map((service) => service.serviceId);
const rebuildServices = services.filter((service) => service.status !== "reused").map((service) => service.serviceId);
return {
ok: services.length > 0,
pipelineRun,
serviceTotal: services.length,
reusedCount: reusedServices.length,
rebuildCount: rebuildServices.length,
reusedServices,
rebuildServices,
services,
...(services.length === 0 ? { degradedReason: "no-build-task-results", rawText: text.slice(0, 2000) } : {}),
};
}
function ciMetricsFromValue(value: unknown): CiPipelineMetrics | null {
const data = record(value);
if (data.ok !== true || typeof data.pipelineRun !== "string") return null;
const services = Array.isArray(data.services) ? data.services.map((item) => {
const service = record(item);
return {
taskRun: String(service.taskRun ?? ""),
serviceId: String(service.serviceId ?? ""),
status: String(service.status ?? "unknown"),
durationSeconds: typeof service.durationSeconds === "number" ? service.durationSeconds : null,
buildBackend: typeof service.buildBackend === "string" ? service.buildBackend : null,
reusedFrom: typeof service.reusedFrom === "string" ? service.reusedFrom : null,
imageTag: typeof service.imageTag === "string" ? service.imageTag : null,
sourceCommitId: typeof service.sourceCommitId === "string" ? service.sourceCommitId : null,
};
}).filter((service) => service.serviceId.length > 0).sort(serviceMetricSort) : [];
const reusedServices = Array.isArray(data.reusedServices) ? data.reusedServices.map(String) : services.filter((service) => service.status === "reused").map((service) => service.serviceId);
const rebuildServices = Array.isArray(data.rebuildServices) ? data.rebuildServices.map(String) : services.filter((service) => service.status !== "reused").map((service) => service.serviceId);
return {
ok: true,
pipelineRun: data.pipelineRun,
serviceTotal: typeof data.serviceTotal === "number" ? data.serviceTotal : services.length,
reusedCount: typeof data.reusedCount === "number" ? data.reusedCount : reusedServices.length,
rebuildCount: typeof data.rebuildCount === "number" ? data.rebuildCount : rebuildServices.length,
reusedServices,
rebuildServices,
services,
};
}
function collectPipelineMetrics(pipelineRun: string): CiPipelineMetrics {
const result = getPipelineTaskRunResults(pipelineRun);
if (!isCommandSuccess(result)) {
return {
ok: false,
pipelineRun,
serviceTotal: 0,
reusedCount: 0,
rebuildCount: 0,
reusedServices: [],
rebuildServices: [],
services: [],
degradedReason: "taskrun-query-failed",
rawText: `${result.stderr}\n${result.stdout}`.trim().slice(0, 2000),
};
}
return parsePipelineTaskRunMetrics(pipelineRun, statusText(result));
}
function ciMetricLines(metrics: CiPipelineMetrics | null): string[] {
if (metrics === null) {
return [
"- CI/CD 关键指标:",
" - lazy build reused: n/a",
" - reused services: n/a",
" - rebuild services: n/a",
" - service durations: n/a",
];
}
if (metrics.ok !== true) {
return [
"- CI/CD 关键指标:",
` - lazy build reused: unavailable (${metrics.degradedReason ?? "unknown"})`,
" - reused services: n/a",
" - rebuild services: n/a",
" - service durations: n/a",
];
}
const lines = [
"- CI/CD 关键指标:",
` - lazy build reused: ${metrics.reusedCount}/${metrics.serviceTotal}`,
` - reused services: ${metrics.reusedServices.length > 0 ? metrics.reusedServices.join(", ") : "none"}`,
` - rebuild services: ${metrics.rebuildServices.length > 0 ? metrics.rebuildServices.join(", ") : "none"}`,
" - service durations:",
];
for (const service of metrics.services) {
const backend = service.buildBackend === null ? "" : `, backend=${service.buildBackend}`;
lines.push(` - ${service.serviceId}: ${service.status}, ${formatDuration(service.durationSeconds)}${backend}`);
}
return lines;
}
function stripMarkdownInline(value: string): string {
return value
.replace(/\[([^\]]+)\]\([^)]+\)/gu, "$1")
.replace(/\*\*([^*]+)\*\*/gu, "$1")
.replace(/\*([^*]+)\*/gu, "$1")
.replace(/__([^_]+)__/gu, "$1")
.replace(/_([^_]+)_/gu, "$1")
.replace(/\s+/gu, " ")
.trim();
}
function markdownSection(body: string, headings: string[]): string {
const normalizedHeadings = new Set(headings.map((heading) => heading.toLowerCase()));
const lines = body.split(/\r?\n/u);
let capturing = false;
const captured: string[] = [];
for (const line of lines) {
const heading = /^(#{1,6})\s+(.+?)\s*$/u.exec(line);
if (heading !== null) {
const title = stripMarkdownInline(heading[2] ?? "").replace(/[:]\s*$/u, "").toLowerCase();
if (capturing) break;
capturing = normalizedHeadings.has(title);
continue;
}
if (capturing) captured.push(line);
}
return captured.join("\n").trim();
}
function firstParagraph(section: string): string | null {
for (const paragraph of section.split(/\n\s*\n/u)) {
const trimmed = stripMarkdownInline(paragraph.replace(/^[-*]\s+/gmu, ""));
if (trimmed.length > 0) return trimmed;
}
return null;
}
function topLevelBullets(section: string, limit = 8): string[] {
const bullets: string[] = [];
for (const line of section.split(/\r?\n/u)) {
const match = /^[-*]\s+(.+)$/u.exec(line);
if (match === null) continue;
const text = stripMarkdownInline(match[1] ?? "");
if (text.length > 0) bullets.push(text);
if (bullets.length >= limit) break;
}
return bullets;
}
function conventionalTitleSummary(title: string): string {
const match = /^(feat|fix|docs|test|refactor|perf|ci|build|chore)(?:\([^)]+\))?:\s*(.+)$/iu.exec(title.trim());
if (match === null) return stripMarkdownInline(title);
const kind = String(match[1] ?? "").toLowerCase();
const subject = stripMarkdownInline(match[2] ?? title);
const labels: Record<string, string> = {
feat: "功能",
fix: "修复",
docs: "文档",
test: "测试",
refactor: "重构",
perf: "性能",
ci: "CI/CD",
build: "构建",
chore: "维护",
};
return `${labels[kind] ?? "变更"}${subject}`;
}
function fileHeuristicBullets(fileSummary: Record<string, unknown>): string[] {
const keyFiles = Array.isArray(fileSummary.keyFiles) ? fileSummary.keyFiles.map(String) : [];
const bullets: string[] = [];
if (keyFiles.some((file) => file.includes("web/hwlab-cloud-web/"))) {
bullets.push("前端:更新 HWLAB Cloud Web / Workbench 交互行为。");
}
if (keyFiles.some((file) => /\.(test|spec)\./u.test(file) || file.includes("/test"))) {
bullets.push("验证:补充或更新回归测试,覆盖本次变更路径。");
}
if (keyFiles.some((file) => file.includes("deploy/") || file.includes("k8s") || file.includes("gitops"))) {
bullets.push("部署:更新 G14 Kubernetes/GitOps 发布声明或发布计划。");
}
if (keyFiles.some((file) => file.includes("internal/cloud/") || file.includes("code-agent"))) {
bullets.push("运行时:调整 Cloud API / Code Agent 运行链路。");
}
return bullets;
}
export function semanticChangelogBullets(title: string, body: string, fileSummary: Record<string, unknown>): string[] {
const bullets: string[] = [];
const background = firstParagraph(markdownSection(body, ["背景", "问题", "problem", "background", "context"]));
if (background !== null && /^(fix|perf)(?:\([^)]+\))?:/iu.test(title)) {
bullets.push(`修复目标:${background}`);
}
const changeSection = markdownSection(body, ["上线 changelog", "changelog", "摘要", "概述", "总结", "修改", "变更", "改动", "changes", "what changed", "summary"]);
bullets.push(...topLevelBullets(changeSection));
if (bullets.length === 0) bullets.push(conventionalTitleSummary(title));
for (const bullet of fileHeuristicBullets(fileSummary)) {
if (bullets.length >= 8) break;
if (!bullets.includes(bullet)) bullets.push(bullet);
}
return bullets.slice(0, 8);
}
function readIssue(issueNumber: number): CommandJsonResult {
return cliJson(["gh", "issue", "read", String(issueNumber), "--repo", HWLAB_REPO, "--json", "title,body,state,updatedAt", "--raw"], 80_000);
}
function issueFromRead(result: CommandJsonResult): Record<string, unknown> {
return record(commandData(result).issue);
}
function parseBriefIssueFromIndex(indexBody: string, date: string): { number: number; url: string } | null {
const escapedDate = date.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
const linePattern = new RegExp(`^\\|\\s*${escapedDate}\\s*\\|([^\\n]+)$`, "mu");
const line = linePattern.exec(indexBody)?.[0];
if (line === undefined) return null;
const match = /\[#(\d+)\]\((https:\/\/github\.com\/pikasTech\/HWLAB\/issues\/\d+)\)/u.exec(line);
if (match === null) return null;
return { number: Number(match[1]), url: match[2] };
}
function dailyBriefTitle(date: string): string {
return `${date} 指挥简报(北京时间)`;
}
function writeStateFile(name: string, content: string): string {
const dir = rootPath(".state", "hwlab-g14");
mkdirSync(dir, { recursive: true });
const path = join(dir, name);
writeFileSync(path, content, "utf8");
return path;
}
function listIssues(): CommandJsonResult {
return cliJson(["gh", "issue", "list", "--repo", HWLAB_REPO, "--state", "open", "--limit", "100", "--json", "number,title,state,url"], 80_000);
}
function findBriefIssueInList(date: string): { number: number; url: string } | null {
const listed = listIssues();
if (!isCommandSuccess(listed)) return null;
const issues = commandData(listed).issues;
if (!Array.isArray(issues)) return null;
const title = dailyBriefTitle(date);
const match = issues.map((item) => record(item)).find((issue) => issue.title === title);
if (match === undefined) return null;
return { number: Number(match.number), url: String(match.url) };
}
function newDailyBriefBody(date: string): string {
return [
`# ${dailyBriefTitle(date)}`,
"",
"## 当日更新",
"",
"## 常驻观察与长期建议",
"",
"- G14 HWLAB source truth 固定为 G14 `/root/hwlab` 与 `origin/G14`DEV rollout 只接受 G14 Tekton/GitOps/Argo 和公网 health 证据。",
"- GitHub issue/PR 写操作必须走 UniDesk `gh` 子命令;G14 k3s 操作必须走 UniDesk `trans G14:k3s`。",
].join("\n");
}
function insertBriefIndexRow(indexBody: string, date: string, brief: { number: number; url: string }): string {
if (parseBriefIssueFromIndex(indexBody, date) !== null) return indexBody;
const row = `| ${date} | [#${brief.number}](${brief.url}) | 已创建当日 G14 DEV rollout 自动简报入口;后续每次 DEV 上线记录 CI/CD 耗时和上线 changelog。 | 继续监控 HWLAB base=G14 PRready 后自动合并并滚动到 G14 DEV。 |`;
const marker = "### 指挥简报索引";
const markerIndex = indexBody.indexOf(marker);
if (markerIndex === -1) return `${indexBody.trimEnd()}\n\n${marker}\n\n| 日期(北京时间) | 指挥简报 issue | 当日推进 | 下一步计划 |\n| --- | --- | --- | --- |\n${row}\n`;
const afterMarker = indexBody.slice(markerIndex);
const separator = "| --- | --- | --- | --- |";
const separatorInBody = afterMarker.indexOf(separator);
if (separatorInBody === -1) return `${indexBody.slice(0, markerIndex + marker.length)}\n\n| 日期(北京时间) | 指挥简报 issue | 当日推进 | 下一步计划 |\n| --- | --- | --- | --- |\n${row}\n${indexBody.slice(markerIndex + marker.length)}`;
const insertAt = markerIndex + separatorInBody + separator.length;
return `${indexBody.slice(0, insertAt)}\n${row}${indexBody.slice(insertAt)}`;
}
function ensureDailyBriefIssue(date: string, dryRun: boolean): Record<string, unknown> {
const indexRead = readIssue(G14_BRIEF_INDEX_ISSUE);
if (!isCommandSuccess(indexRead)) return { ok: false, phase: "read-index", indexRead };
const indexIssue = issueFromRead(indexRead);
const indexBody = String(indexIssue.body ?? "");
let brief = parseBriefIssueFromIndex(indexBody, date) ?? findBriefIssueInList(date);
const actions: Record<string, unknown>[] = [];
if (brief === null) {
const bodyPath = writeStateFile(`daily-brief-${date}.md`, `${newDailyBriefBody(date)}\n`);
if (dryRun) {
brief = { number: 0, url: `https://github.com/pikasTech/HWLAB/issues/new?title=${encodeURIComponent(dailyBriefTitle(date))}` };
actions.push({ action: "would-create-daily-brief", title: dailyBriefTitle(date), bodyPath });
} else {
const created = cliJson(["gh", "issue", "create", "--repo", HWLAB_REPO, "--title", dailyBriefTitle(date), "--body-file", bodyPath], 80_000);
if (!isCommandSuccess(created)) return { ok: false, phase: "create-daily-brief", created };
const createdIssue = record(commandData(created).issue);
brief = { number: Number(createdIssue.number), url: String(createdIssue.url) };
actions.push({ action: "created-daily-brief", issue: brief, bodyPath });
}
}
if (brief.number > 0 && parseBriefIssueFromIndex(indexBody, date) === null) {
const nextBody = insertBriefIndexRow(indexBody, date, brief);
const bodyPath = writeStateFile(`issue-${G14_BRIEF_INDEX_ISSUE}-brief-index-${date}.md`, `${nextBody.trimEnd()}\n`);
if (dryRun) {
actions.push({ action: "would-update-brief-index", issue: G14_BRIEF_INDEX_ISSUE, bodyPath });
} else {
const bodySha = String(indexIssue.bodySha ?? "");
const update = cliJson(["gh", "issue", "update", String(G14_BRIEF_INDEX_ISSUE), "--repo", HWLAB_REPO, "--mode", "replace", "--body-file", bodyPath, ...(bodySha.length > 0 ? ["--expect-body-sha", bodySha] : [])], 100_000);
if (!isCommandSuccess(update)) return { ok: false, phase: "update-brief-index", update, bodyPath };
actions.push({ action: "updated-brief-index", issue: G14_BRIEF_INDEX_ISSUE, bodyPath });
}
}
return { ok: true, date, brief, actions };
}
function readPullRequest(number: number): CommandJsonResult {
return cliJson(["gh", "pr", "read", String(number), "--repo", HWLAB_REPO, "--json", "title,body,url,stateDetail,merged,mergedAt,mergeCommit,headRefName,baseRefName"], 80_000);
}
function readPullRequestFiles(number: number): CommandJsonResult {
return cliJson(["gh", "pr", "files", String(number), "--repo", HWLAB_REPO, "--limit", "30"], 80_000);
}
function summarizePrFiles(filesResult: CommandJsonResult): Record<string, unknown> {
if (!isCommandSuccess(filesResult)) return { ok: false, filesResult };
const data = commandData(filesResult);
const files = Array.isArray(data.files) ? data.files.map((item) => record(item)) : [];
return {
ok: true,
summary: data.summary ?? null,
keyFiles: files.slice(0, 8).map((file) => `${file.status ?? "changed"} ${file.filename ?? ""}`),
};
}
function mergeCommitFromPr(prData: Record<string, unknown>): string | null {
const mergeCommit = record(record(prData.json).mergeCommit);
const fromJson = mergeCommit.oid;
if (typeof fromJson === "string") return fromJson;
const fromSummary = record(record(prData.pullRequest).mergeCommit).oid;
return typeof fromSummary === "string" ? fromSummary : null;
}
function currentGitopsRevision(): string | null {
const argo = getArgoStatus();
if (!isCommandSuccess(argo)) return null;
return statusText(argo).split(/\r?\n/u)[0] ?? null;
}
export function rolloutRecordBody(input: {
pr: OpenPullRequest;
prData: Record<string, unknown>;
fileSummary: Record<string, unknown>;
sourceCommit: string;
pipelineRun: string;
gitopsRevision: string | null;
mergedAt: string | null;
pipelineSucceededAt: string | null;
finishedAt: string | null;
rollout: Record<string, unknown>;
ciMetrics?: CiPipelineMetrics | null;
}): string {
const now = beijingParts();
const title = String(record(input.prData.json).title ?? input.pr.title ?? `PR #${input.pr.number}`);
const prBody = String(record(input.prData.json).body ?? record(input.prData.pullRequest).body ?? "");
const url = String(record(input.prData.json).url ?? input.pr.url ?? `https://github.com/pikasTech/HWLAB/pull/${input.pr.number}`);
const summary = record(input.fileSummary.summary);
const keyFiles = Array.isArray(input.fileSummary.keyFiles) ? input.fileSummary.keyFiles.map(String) : [];
const semanticBullets = semanticChangelogBullets(title, prBody, input.fileSummary);
const mergeToPipeline = durationSeconds(input.mergedAt, input.pipelineSucceededAt);
const pipelineToDev = durationSeconds(input.pipelineSucceededAt, input.finishedAt);
const mergeToDev = durationSeconds(input.mergedAt, input.finishedAt);
const metrics = input.ciMetrics ?? ciMetricsFromValue(input.rollout.ciMetrics);
return [
"",
"",
`## 更新 ${now.date} ${now.time.slice(0, 5)} 北京时间`,
"",
`### G14 DEV rolloutPR #${input.pr.number}`,
"",
`- PR: [#${input.pr.number} ${title}](${url})`,
`- 合并 commit: \`${input.sourceCommit}\``,
`- GitOps revision: \`${input.gitopsRevision ?? "unknown"}\``,
`- PipelineRun: \`${input.pipelineRun}\``,
"- CI/CD 耗时:",
` - merge -> pipeline succeeded: ${formatDuration(mergeToPipeline)}`,
` - pipeline succeeded -> DEV Healthy: ${formatDuration(pipelineToDev)}`,
` - merge -> DEV Healthy: ${formatDuration(mergeToDev)}`,
...ciMetricLines(metrics),
"- 上线 changelog(语义化):",
...semanticBullets.map((bullet) => ` - ${bullet}`),
"- 自动 diff 摘要:",
` - changed files: ${String(summary.files ?? "n/a")}; +${String(summary.additions ?? "n/a")} / -${String(summary.deletions ?? "n/a")}; commits: ${String(summary.commits ?? "n/a")}`,
...keyFiles.map((file) => ` - ${file}`),
"- DEV 验证:",
` - Tekton: ${String(input.rollout.pipelineText ?? "Succeeded").split(/\r?\n/u).join(" / ")}`,
` - Argo: ${String(input.rollout.argoText ?? "Synced / Healthy").split(/\r?\n/u).slice(0, 5).join(" / ")}`,
` - health/live: ${input.rollout.healthOk === false ? "not-ok" : "ok"}`,
" - workload readiness: Deployment/StatefulSet ready;历史 Completed smoke/debug pod 不作为 DEV rollout blocker。",
].join("\n");
}
function appendRolloutBrief(options: G14RecordRolloutOptions, rollout: Record<string, unknown> = {}): Record<string, unknown> {
const now = beijingParts();
const ensured = ensureDailyBriefIssue(now.date, options.dryRun);
if (record(ensured).ok !== true) return ensured;
const brief = record(record(ensured).brief);
const prRead = readPullRequest(options.prNumber);
if (!isCommandSuccess(prRead)) return { ok: false, phase: "read-pr", prRead };
const prData = commandData(prRead);
const sourceCommit = options.sourceCommit ?? mergeCommitFromPr(prData);
if (sourceCommit === null || sourceCommit === undefined) return { ok: false, phase: "source-commit", message: "source commit unavailable", prData };
const pipelineRun = options.pipelineRun ?? `hwlab-g14-ci-poll-${shortSha(sourceCommit)}`;
const files = summarizePrFiles(readPullRequestFiles(options.prNumber));
const rolloutGitopsRevision = typeof rollout.gitopsRevision === "string" && rollout.gitopsRevision.length > 0 ? rollout.gitopsRevision : null;
const gitopsRevision = options.gitopsRevision ?? rolloutGitopsRevision ?? currentGitopsRevision();
const prJson = record(prData.json);
const prSummary = record(prData.pullRequest);
const prMergedAt = typeof prJson.mergedAt === "string" && prJson.mergedAt.length > 0
? prJson.mergedAt
: typeof prSummary.mergedAt === "string" && prSummary.mergedAt.length > 0
? prSummary.mergedAt
: null;
const rolloutPipelineSucceededAt = typeof rollout.pipelineSucceededAt === "string" && rollout.pipelineSucceededAt.length > 0 ? rollout.pipelineSucceededAt : null;
const rolloutFinishedAt = typeof rollout.finishedAt === "string" && rollout.finishedAt.length > 0 ? rollout.finishedAt : null;
const mergedAt = options.mergedAt ?? prMergedAt;
const pipelineSucceededAt = options.pipelineSucceededAt ?? rolloutPipelineSucceededAt;
const finishedAt = options.finishedAt ?? rolloutFinishedAt;
const ciMetrics = ciMetricsFromValue(rollout.ciMetrics) ?? collectPipelineMetrics(pipelineRun);
const pr: OpenPullRequest = {
number: options.prNumber,
title: String(record(prData.json).title ?? ""),
url: String(record(prData.json).url ?? ""),
baseRefName: String(record(prData.json).baseRefName ?? ""),
headRefName: String(record(prData.json).headRefName ?? ""),
};
const body = rolloutRecordBody({ pr, prData, fileSummary: files, sourceCommit, pipelineRun, gitopsRevision, mergedAt, pipelineSucceededAt, finishedAt, rollout, ciMetrics });
if (Number(brief.number) <= 0 || options.dryRun) {
return { ok: true, dryRun: true, date: now.date, brief, wouldAppend: { bodyPreview: body.slice(0, 1000), bodyChars: body.length }, ensured };
}
const currentBrief = readIssue(Number(brief.number));
if (!isCommandSuccess(currentBrief)) return { ok: false, phase: "read-daily-brief", currentBrief, brief };
const currentBody = String(issueFromRead(currentBrief).body ?? "");
if (currentBody.includes(`PR #${options.prNumber}`) && currentBody.includes(sourceCommit)) {
return { ok: true, skipped: true, reason: "rollout-brief-already-recorded", brief, sourceCommit, ensured };
}
const bodyPath = writeStateFile(`rollout-pr-${options.prNumber}-${shortSha(sourceCommit)}.md`, `${body}\n`);
const update = cliJson(["gh", "issue", "update", String(brief.number), "--repo", HWLAB_REPO, "--mode", "append", "--body-file", bodyPath, "--body-profile", "commander-brief"], 100_000);
if (!isCommandSuccess(update)) return { ok: false, phase: "append-rollout-brief", update, bodyPath, brief, ensured };
return { ok: true, date: now.date, brief, sourceCommit, pipelineRun, gitopsRevision, ciMetrics, bodyPath, update: commandData(update), ensured };
}
async function waitForG14Dev(sourceCommit: string, timeoutSeconds: number): Promise<Record<string, unknown>> {
const started = Date.now();
const startedAt = new Date(started).toISOString();
let pipelineText = "";
let argoText = "";
let healthOk = false;
let workloadsReady: Record<string, unknown> = { ready: false };
let pipelineSucceededAt: string | null = null;
while (Date.now() - started < timeoutSeconds * 1000) {
const pipeline = getPipelineStatus(sourceCommit);
pipelineText = statusText(pipeline);
printEvent("g14.pipeline.status", { sourceCommit, pipelineRun: `hwlab-g14-ci-poll-${shortSha(sourceCommit)}`, text: pipelineText.slice(0, 500) });
if (!pipelineText.startsWith("True\nSucceeded")) {
if (pipelineText.startsWith("False\n")) return { ok: false, phase: "pipeline", sourceCommit, pipelineText };
await sleep(30_000);
continue;
}
pipelineSucceededAt ??= new Date().toISOString();
refreshArgoDev();
const argo = getArgoStatus();
argoText = statusText(argo);
printEvent("g14.argo.status", { text: argoText.slice(0, 500) });
const argoLines = argoText.split(/\r?\n/u);
const synced = argoLines[1] === "Synced";
const healthy = argoLines[2] === "Healthy";
const gitopsRevision = argoLines[0] ?? null;
const workloads = getDevWorkloads();
const workloadsText = statusText(workloads);
workloadsReady = workloadReadiness(workloadsText, isCommandSuccess(workloads));
const health = getLiveHealth();
healthOk = health.ok && /"status"\s*:\s*"ok"/u.test(health.stdout);
if (synced && healthy && record(workloadsReady).ready === true && healthOk) {
const finishedAt = new Date().toISOString();
const pipelineRun = `hwlab-g14-ci-poll-${shortSha(sourceCommit)}`;
const ciMetrics = collectPipelineMetrics(pipelineRun);
return { ok: true, sourceCommit, gitopsRevision, pipelineRun, pipelineText, argoText, startedAt, pipelineSucceededAt, finishedAt, workloadsReady, healthOk, ciMetrics };
}
await sleep(30_000);
}
return { ok: false, phase: "timeout", sourceCommit, pipelineText, argoText, startedAt, pipelineSucceededAt, workloadsReady, healthOk, timeoutSeconds };
}
async function monitorCycle(options: G14MonitorOptions, cycle: number): Promise<Record<string, unknown>> {
printEvent("g14.monitor.cycle.start", { cycle, dryRun: options.dryRun });
const precheck = precheckWorkspace();
if (!isCommandSuccess(precheck)) return { ok: false, cycle, phase: "workspace-precheck", precheck };
const listed = listOpenG14PullRequests();
if (!isCommandSuccess(listed)) return { ok: false, cycle, phase: "list-prs", listed };
const prs = extractPullRequests(listed);
printEvent("g14.monitor.prs", { cycle, count: prs.length, pullRequests: prs });
if (prs.length === 0) return { ok: true, cycle, action: "none", pullRequests: [] };
const merged: unknown[] = [];
for (const pr of prs) {
const preflight = preflightPullRequest(pr.number);
printEvent("g14.pr.preflight", { cycle, number: pr.number, ok: isCommandSuccess(preflight) });
if (!isCommandSuccess(preflight) || nested(preflight.parsed, ["data", "mergeability", "readyForCommanderMerge"]) !== true) {
return { ok: false, cycle, phase: "pr-preflight", pullRequest: pr, preflight };
}
const merge = mergePullRequest(pr.number, options.dryRun);
printEvent("g14.pr.merge", { cycle, number: pr.number, dryRun: options.dryRun, ok: isCommandSuccess(merge) });
if (!isCommandSuccess(merge)) return { ok: false, cycle, phase: "pr-merge", pullRequest: pr, merge };
const sourceCommit = getG14Head();
const rollout = options.dryRun || sourceCommit === null ? { skipped: true, dryRun: options.dryRun, sourceCommit } : await waitForG14Dev(sourceCommit, options.timeoutSeconds);
const brief = options.dryRun || sourceCommit === null || record(rollout).ok !== true
? { skipped: true, dryRun: options.dryRun }
: appendRolloutBrief({ prNumber: pr.number, sourceCommit, dryRun: false }, rollout);
printEvent("g14.rollout.brief", { cycle, number: pr.number, ok: record(brief).ok, skipped: record(brief).skipped ?? false, brief: record(brief).brief ?? null });
merged.push({ pullRequest: pr, merge: commandData(merge), sourceCommit, rollout, brief });
if (record(rollout).ok === false) return { ok: false, cycle, phase: "rollout", pullRequest: pr, sourceCommit, rollout, merged };
if (record(brief).ok === false) return { ok: false, cycle, phase: "rollout-brief", pullRequest: pr, sourceCommit, rollout, brief, merged };
}
return { ok: true, cycle, action: options.dryRun ? "dry-run-merge" : "merged-and-rolled-dev", merged };
}
async function runMonitorWorker(options: G14MonitorOptions): Promise<Record<string, unknown>> {
const maxCycles = options.once ? 1 : options.maxCycles;
let cycle = 0;
const results: unknown[] = [];
while (maxCycles === 0 || cycle < maxCycles) {
cycle += 1;
const result = await monitorCycle(options, cycle);
results.push(result);
printEvent("g14.monitor.cycle.done", { cycle, ok: record(result).ok, action: record(result).action ?? null, phase: record(result).phase ?? null });
if (record(result).ok !== true) return { ok: false, cycles: cycle, lastResult: result, results };
if (options.once || record(result).action !== "none") return { ok: true, cycles: cycle, lastResult: result, results };
printEvent("g14.monitor.sleep", { cycle, intervalSeconds: options.intervalSeconds });
await sleep(options.intervalSeconds * 1000);
}
return { ok: true, cycles: cycle, results };
}
export function hwlabG14Help(): Record<string, unknown> {
return {
command: "hwlab g14",
output: "json",
usage: [
"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 status --lane v02 --pipeline-run hwlab-v02-ci-poll-<short-sha>",
"bun scripts/cli.ts hwlab g14 control-plane status --lane v02 --source-commit <full-sha>",
"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 trigger-current --lane v02 --confirm",
"bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm --wait",
"bun scripts/cli.ts hwlab g14 control-plane cleanup-runs --lane v02 --min-age-minutes 30 --limit 20 --dry-run",
"bun scripts/cli.ts hwlab g14 control-plane cleanup-runs --lane v02 --min-age-minutes 30 --limit 20 --confirm",
"bun scripts/cli.ts hwlab g14 control-plane cleanup-released-pvs --lane all --limit 20 --dry-run",
"bun scripts/cli.ts hwlab g14 control-plane cleanup-released-pvs --lane all --limit 20 --confirm",
"bun scripts/cli.ts hwlab g14 control-plane runtime-migration --lane v02 --dry-run",
"bun scripts/cli.ts hwlab g14 control-plane runtime-migration --lane v02 --allow-live-db-read --dry-run",
"bun scripts/cli.ts hwlab g14 control-plane runtime-migration --lane v02 --confirm",
"bun scripts/cli.ts hwlab g14 secret status --lane v02 --name hwlab-v02-device-pod-api-key --key api-key",
"bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name hwlab-v02-device-pod-api-key --key api-key --dry-run",
"bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name hwlab-v02-device-pod-api-key --key api-key --confirm",
"bun scripts/cli.ts hwlab g14 git-mirror status",
"bun scripts/cli.ts hwlab g14 git-mirror apply --confirm",
"bun scripts/cli.ts hwlab g14 git-mirror sync --confirm",
"bun scripts/cli.ts hwlab g14 git-mirror flush --confirm",
"bun scripts/cli.ts hwlab g14 git-mirror sync --confirm --wait",
"bun scripts/cli.ts hwlab g14 git-mirror flush --confirm --wait",
"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, DEV rollout command, bounded v0.2 control-plane bootstrap/cleanup/runtime-migration helper, v0.2 runtime SecretRef bootstrap, devops-infra git mirror maintenance, and controlled CI tools image build/status entry. The public monitor starts a fire-and-forget job; confirmed control-plane trigger-current and git-mirror sync/flush also return async jobs by default, with --wait reserved for explicit synchronous debugging. control-plane status/apply/cleanup-runs/cleanup-released-pvs/runtime-migration uses UniDesk G14:k3s routes for v0.2 Tekton/Argo control resources, runtime migration, and completed CI workspace retention only. secret status/ensure is the standard v0.2 runtime SecretRef bootstrap path; it never reads or prints secret values. git-mirror status/apply/sync/flush is the manual devops-infra mirror/relay control path and does not install a CronJob.",
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: {
monitor: ".state/hwlab-g14/latest-monitor-job.json",
once: ".state/hwlab-g14/latest-once-job.json",
dryRun: ".state/hwlab-g14/latest-dry-run-job.json",
onceDryRun: ".state/hwlab-g14/latest-once-dry-run-job.json",
},
};
}
export async function runHwlabG14Command(_config: Config, args: string[]): Promise<Record<string, unknown>> {
if (args.length === 0 || args.includes("--help") || args.includes("-h")) return { ok: true, ...hwlabG14Help() };
const [action] = args;
if (action === "record-rollout") {
const options = parseRecordRolloutOptions(args.slice(1));
return appendRolloutBrief(options);
}
if (action === "control-plane") {
const options = parseControlPlaneOptions(args.slice(1));
if (options.action === "trigger-current" && options.confirm && !options.dryRun && !options.wait) {
return startControlPlaneTriggerJob(options);
}
return runV02ControlPlane(options);
}
if (action === "secret") {
const options = parseSecretOptions(args.slice(1));
return runG14Secret(options);
}
if (action === "tools-image") {
const options = parseToolsImageOptions(args.slice(1));
return runG14ToolsImage(options);
}
if (action === "git-mirror") {
const options = parseGitMirrorOptions(args.slice(1));
if ((options.action === "sync" || options.action === "flush") && options.confirm && !options.dryRun && !options.wait) {
return startGitMirrorJob(options);
}
return runG14GitMirror(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, hwlab g14 control-plane, hwlab g14 secret, hwlab g14 git-mirror, hwlab g14 tools-image" };
}
const options = parseOptions(args.slice(1));
if (options.worker) return runMonitorWorker(options);
const command = ["bun", "scripts/cli.ts", "hwlab", "g14", "monitor-prs", "--worker", "--interval-seconds", String(options.intervalSeconds), "--timeout-seconds", String(options.timeoutSeconds), ...(options.once ? ["--once"] : []), ...(options.dryRun ? ["--dry-run"] : []), ...(options.maxCycles > 0 ? ["--max-cycles", String(options.maxCycles)] : [])];
const job = startJob("hwlab_g14_pr_monitor", command, `Monitor ${HWLAB_REPO} PRs targeting ${G14_SOURCE_BRANCH} and roll merged changes to G14 DEV`);
const statusCommand = `bun scripts/cli.ts job status ${job.id} --tail-bytes 30000`;
const stateDir = rootPath(".state", "hwlab-g14");
mkdirSync(stateDir, { recursive: true });
const stateFileName = hwlabG14MonitorStateFileName(options);
const stateFileRole = hwlabG14MonitorStateRole(options);
const latestPath = join(stateDir, stateFileName);
const previousLatest = existsSync(latestPath) ? readFileSync(latestPath, "utf8") : null;
writeFileSync(latestPath, `${JSON.stringify({ jobId: job.id, createdAt: job.createdAt, statusCommand, role: stateFileRole }, null, 2)}\n`, "utf8");
return {
ok: true,
command: "hwlab g14 monitor-prs",
mode: "async-job",
job,
statusCommand,
latestPath,
stateFileName,
stateFileRole,
previousLatest,
next: {
status: statusCommand,
tail: `tail -f ${job.stdoutFile}`,
},
};
}