refactor: split control-plane cli modules
This commit is contained in:
@@ -0,0 +1,712 @@
|
||||
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. status module for scripts/src/hwlab-node-impl.ts.
|
||||
|
||||
// Moved mechanically from scripts/src/hwlab-node-impl.ts:3281-3999 for #903.
|
||||
|
||||
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
|
||||
// Responsibility: YAML-first node/lane operations, including Workbench observability control commands.
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { repoRoot, rootPath, type Config } from "../config";
|
||||
import { runCommand, type CommandResult } from "../command";
|
||||
import { startJob } from "../jobs";
|
||||
import { classifySshTcpPoolFailure } from "../ssh";
|
||||
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "../hwlab-node-control-plane";
|
||||
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, hwlabRuntimeLaneSpecForNode, hwlabRuntimeNodeIds, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeObservabilityRecordingRuleSpec, type HwlabRuntimeObservabilitySpec, type HwlabRuntimeObservabilityWarningAlertSpec, type HwlabRuntimePublicExposureSpec, type HwlabRuntimeWebProbeAlertThresholdsSpec, type HwlabRuntimeWebProbeProjectManagementSpec } from "../hwlab-node-lanes";
|
||||
import { nodeWebProbeScriptRunnerSource } from "../hwlab-node-web-probe-runner-source";
|
||||
import { nodeWebObserveAnalyzerSource } from "../hwlab-node-web-observe-analyzer-source";
|
||||
import { nodeWebObserveRunnerSource } from "../hwlab-node-web-observe-runner-source";
|
||||
import { nodeWebObserveCollectViewNodeScript, parseNodeWebProbeObserveCollectView, type NodeWebProbeObserveCollectView } from "../hwlab-node-web-observe-collect";
|
||||
import { withWebObserveCollectRendered, withWebObserveCommandRendered, withWebObserveStatusRendered } from "../hwlab-node-web-observe-render";
|
||||
import { buildWebObserveWrapperForObserveOptions, webObserveWrapperStateDirFromStatus } from "../hwlab-node-web-observe-wrapper";
|
||||
import { renderWebObserveWrapperContract } from "../hwlab-node-web-observe-wrapper-render";
|
||||
import { runWebProbeSentinelCommand, type WebProbeSentinelOptions } from "../hwlab-node-web-sentinel-cicd";
|
||||
import { hwlabNodeHelp, hwlabNodeObservabilityHelp, hwlabNodeWebProbeHelp } from "../hwlab-node-help";
|
||||
import { compactWebProbeResult, compactWebProbeScriptResult } from "../hwlab-node-web-probe-summary";
|
||||
import { nodeObservabilityRecordingRuleExpression, nodeObservabilityRecordingRuleSummaries, nodeObservabilityWarningAlertExpression, nodeObservabilityWarningAlertSummaries } from "../hwlab-node-observability-promql";
|
||||
import { runDelegatedHwlabNodeCommand, type DelegatedNodeDomain } from "../hwlab-node-transport";
|
||||
import type { RenderedCliResult } from "../output";
|
||||
|
||||
import type { NodeRuntimeGitMirrorTargetSpec } from "./entry";
|
||||
import { isCommandSuccess, runNodeK3sArgs, runNodeK3sScript, shortValue } from "./cleanup";
|
||||
import { parseNodeScopedDelegatedOptions } from "./plan";
|
||||
import { nodeRuntimeGitMirrorGithubTransportSummary, nodeRuntimeGitMirrorJobManifest, nodeRuntimeGitMirrorJobName } from "./render";
|
||||
import { compactRuntimeCommand, nodeRuntimeGitMirrorFlushPartialSuccess, nodeRuntimeGitMirrorRefSources, nodeRuntimeUnsupportedAction } from "./runtime-common";
|
||||
import { parseJsonObject, record, shellQuote, statusText } from "./utils";
|
||||
import { webObserveTable } from "./web-observe-render";
|
||||
import { nodeRuntimeGitMirrorTarget, printNodeRuntimeTriggerProgress, sleepSync } from "./web-probe";
|
||||
import { webObserveShort, webObserveText } from "./web-probe-observe";
|
||||
|
||||
export function nodeRuntimeExternalPostgresSecretRows(secrets: Record<string, unknown>): unknown[][] {
|
||||
if (secrets.required === false) return [["not-required", "-", "-", "-", "false"]];
|
||||
if (secrets.required === true && secrets.ready !== true && secrets.degradedReason !== undefined) {
|
||||
return [[webObserveText(secrets.degradedReason), "-", "-", "false", "false"]];
|
||||
}
|
||||
const cloudApi = record(secrets.cloudApi);
|
||||
const openfga = record(secrets.openfga);
|
||||
const datastoreUri = record(openfga.datastoreUri);
|
||||
const authnPresharedKey = record(openfga.authnPresharedKey);
|
||||
return [
|
||||
["cloudApi", webObserveText(cloudApi.secret), webObserveText(cloudApi.key), webObserveText(cloudApi.present), webObserveText(cloudApi.valuesPrinted ?? false)],
|
||||
["openfga.datastoreUri", webObserveText(datastoreUri.secret), webObserveText(datastoreUri.key), webObserveText(datastoreUri.present), webObserveText(datastoreUri.valuesPrinted ?? false)],
|
||||
["openfga.authnPresharedKey", webObserveText(authnPresharedKey.secret), webObserveText(authnPresharedKey.key), webObserveText(authnPresharedKey.present), webObserveText(authnPresharedKey.valuesPrinted ?? false)],
|
||||
];
|
||||
}
|
||||
|
||||
export function nodeRuntimeGitMirrorStatus(scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>): Record<string, unknown> {
|
||||
const spec = scoped.spec;
|
||||
const mirror = nodeRuntimeGitMirrorTarget(spec);
|
||||
const script = [
|
||||
"set +e",
|
||||
`namespace=${shellQuote(mirror.namespace)}`,
|
||||
`read_deploy=${shellQuote(mirror.serviceReadName)}`,
|
||||
`write_deploy=${shellQuote(mirror.serviceWriteName)}`,
|
||||
`read_svc=${shellQuote(mirror.serviceReadName)}`,
|
||||
`write_svc=${shellQuote(mirror.serviceWriteName)}`,
|
||||
`cache_pvc=${shellQuote(mirror.cachePvcName)}`,
|
||||
`cache_host_path=${shellQuote(mirror.cacheHostPath ?? "")}`,
|
||||
`github_transport_mode=${shellQuote(mirror.githubTransport.mode)}`,
|
||||
`github_token_secret=${shellQuote(mirror.githubTransport.mode === "https" ? mirror.githubTransport.tokenSecretName : "")}`,
|
||||
`github_token_key=${shellQuote(mirror.githubTransport.mode === "https" ? mirror.githubTransport.tokenSecretKey : "")}`,
|
||||
`github_token_source_ref=${shellQuote(mirror.githubTransport.mode === "https" ? mirror.githubTransport.tokenSourceRef : "")}`,
|
||||
`github_token_source_key=${shellQuote(mirror.githubTransport.mode === "https" ? mirror.githubTransport.tokenSourceKey : "")}`,
|
||||
"deploy_ready() { desired=$(kubectl -n \"$1\" get deploy \"$2\" -o 'jsonpath={.spec.replicas}' 2>/dev/null || true); ready=$(kubectl -n \"$1\" get deploy \"$2\" -o 'jsonpath={.status.readyReplicas}' 2>/dev/null || true); [ -n \"$desired\" ] && [ \"$desired\" -gt 0 ] 2>/dev/null && [ \"${ready:-0}\" = \"$desired\" ] && printf true || printf false; }",
|
||||
"exists_res() { kubectl -n \"$1\" get \"$2\" \"$3\" >/dev/null 2>&1 && printf true || printf false; }",
|
||||
"endpoint_ready() { endpoints=$(kubectl -n \"$1\" get endpoints \"$2\" -o 'jsonpath={.subsets[*].addresses[*].ip}' 2>/dev/null || true); [ -n \"$endpoints\" ] && printf true || printf false; }",
|
||||
"github_transport_json=$(python3 - \"$github_transport_mode\" \"$namespace\" \"$github_token_secret\" \"$github_token_key\" \"$github_token_source_ref\" \"$github_token_source_key\" <<'PY'",
|
||||
"import hashlib, json, subprocess, sys",
|
||||
"mode, namespace, secret_name, secret_key, source_ref, source_key = sys.argv[1:7]",
|
||||
"if mode != 'https':",
|
||||
" print(json.dumps({'mode': mode, 'required': False, 'ready': True, 'valuesPrinted': False}))",
|
||||
" raise SystemExit(0)",
|
||||
"proc = subprocess.run(['kubectl', '-n', namespace, 'get', 'secret', secret_name, '-o', 'json'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)",
|
||||
"exists = proc.returncode == 0",
|
||||
"data = {}",
|
||||
"if exists:",
|
||||
" try:",
|
||||
" data = json.loads(proc.stdout).get('data') or {}",
|
||||
" except Exception:",
|
||||
" data = {}",
|
||||
"encoded = data.get(secret_key) if isinstance(data, dict) else None",
|
||||
"present = isinstance(encoded, str) and len(encoded) > 0",
|
||||
"fingerprint = 'sha256:' + hashlib.sha256(encoded.encode()).hexdigest()[:16] if present else None",
|
||||
"print(json.dumps({'mode': mode, 'required': True, 'ready': exists and present, 'tokenSecretName': secret_name, 'tokenSecretKey': secret_key, 'tokenSourceRef': source_ref, 'tokenSourceKey': source_key, 'tokenSecretExists': exists, 'tokenKeyPresent': present, 'tokenKeyBytes': len(encoded) if present else 0, 'tokenFingerprint': fingerprint, 'valuesPrinted': False}))",
|
||||
"PY",
|
||||
")",
|
||||
"summary_json=$(kubectl -n \"$namespace\" exec deploy/\"$read_deploy\" -- sh -lc '/etc/git-mirror/status.sh' 2>/tmp/hwlab-node-gitmirror-status.err || true)",
|
||||
"if [ -z \"$summary_json\" ]; then summary_json='{}'; fi",
|
||||
"read_deployment_ready=$(deploy_ready \"$namespace\" \"$read_deploy\")",
|
||||
"write_deployment_ready=$(deploy_ready \"$namespace\" \"$write_deploy\")",
|
||||
"read_service_exists=$(exists_res \"$namespace\" service \"$read_svc\")",
|
||||
"write_service_exists=$(exists_res \"$namespace\" service \"$write_svc\")",
|
||||
"read_endpoints_ready=$(endpoint_ready \"$namespace\" \"$read_svc\")",
|
||||
"write_endpoints_ready=$(endpoint_ready \"$namespace\" \"$write_svc\")",
|
||||
"cache_pvc_exists=$(exists_res \"$namespace\" pvc \"$cache_pvc\")",
|
||||
"cache_host_path_exists=false",
|
||||
"if [ -n \"$cache_host_path\" ] && kubectl -n \"$namespace\" exec deploy/\"$read_deploy\" -- sh -lc 'test -d /cache' >/dev/null 2>&1; then cache_host_path_exists=true; fi",
|
||||
"SUMMARY_JSON=\"$summary_json\" GITHUB_TRANSPORT_JSON=\"$github_transport_json\" read_deployment_ready=\"$read_deployment_ready\" write_deployment_ready=\"$write_deployment_ready\" read_service_exists=\"$read_service_exists\" write_service_exists=\"$write_service_exists\" read_endpoints_ready=\"$read_endpoints_ready\" write_endpoints_ready=\"$write_endpoints_ready\" cache_pvc_exists=\"$cache_pvc_exists\" cache_host_path=\"$cache_host_path\" cache_host_path_exists=\"$cache_host_path_exists\" node <<'NODE'",
|
||||
"const summary = (() => { try { return JSON.parse(process.env.SUMMARY_JSON || '{}'); } catch { return {}; } })();",
|
||||
"const githubTransport = (() => { try { return JSON.parse(process.env.GITHUB_TRANSPORT_JSON || '{}'); } catch { return {}; } })();",
|
||||
"const env = process.env;",
|
||||
"const ok = env.read_deployment_ready === 'true' && env.write_deployment_ready === 'true' && env.read_service_exists === 'true' && env.write_service_exists === 'true' && env.read_endpoints_ready === 'true' && env.write_endpoints_ready === 'true' && (env.cache_pvc_exists === 'true' || env.cache_host_path_exists === 'true') && githubTransport.ready !== false && summary.localSource;",
|
||||
"console.log(JSON.stringify({",
|
||||
" ok: Boolean(ok),",
|
||||
" resources: {",
|
||||
" readDeploymentReady: env.read_deployment_ready === 'true',",
|
||||
" writeDeploymentReady: env.write_deployment_ready === 'true',",
|
||||
" readServiceExists: env.read_service_exists === 'true',",
|
||||
" writeServiceExists: env.write_service_exists === 'true',",
|
||||
" readEndpointsReady: env.read_endpoints_ready === 'true',",
|
||||
" writeEndpointsReady: env.write_endpoints_ready === 'true',",
|
||||
" cachePvcExists: env.cache_pvc_exists === 'true',",
|
||||
" cacheHostPathConfigured: Boolean(env.cache_host_path),",
|
||||
" cacheHostPathExists: env.cache_host_path_exists === 'true'",
|
||||
" },",
|
||||
" githubTransport,",
|
||||
" summary,",
|
||||
" valuesPrinted: false",
|
||||
"}));",
|
||||
"NODE",
|
||||
].join("\n");
|
||||
const result = runNodeK3sScript(spec, script, scoped.timeoutSeconds);
|
||||
const parsed = record(parseJsonObject(statusText(result)));
|
||||
const summary = record(parsed.summary);
|
||||
const resources = record(parsed.resources);
|
||||
const githubTransport = record(parsed.githubTransport);
|
||||
const ok = result.exitCode === 0 && parsed.ok === true;
|
||||
const remoteText = `${result.stderr ?? ""}\n${result.stdout ?? ""}`;
|
||||
const remoteUnavailable = /provider is not online|UNIDESK_SSH_PROVIDER_OFFLINE|ssh.*unreachable/iu.test(remoteText);
|
||||
return {
|
||||
ok,
|
||||
command: `hwlab nodes git-mirror status --node ${scoped.node} --lane ${scoped.lane}`,
|
||||
node: scoped.node,
|
||||
lane: scoped.lane,
|
||||
mode: "status",
|
||||
mutation: false,
|
||||
namespace: mirror.namespace,
|
||||
readUrl: spec.gitReadUrl,
|
||||
writeUrl: spec.gitWriteUrl,
|
||||
sourceBranch: mirror.sourceBranch,
|
||||
gitopsBranch: mirror.gitopsBranch,
|
||||
resources,
|
||||
githubTransport,
|
||||
summary,
|
||||
refSources: nodeRuntimeGitMirrorRefSources(scoped, mirror),
|
||||
result: compactRuntimeCommand(result),
|
||||
degradedReason: ok ? undefined : remoteUnavailable ? "node-provider-unavailable" : "node-runtime-git-mirror-not-ready",
|
||||
valuesPrinted: false,
|
||||
next: {
|
||||
sync: `bun scripts/cli.ts hwlab nodes git-mirror sync --node ${scoped.node} --lane ${scoped.lane} --confirm`,
|
||||
flush: `bun scripts/cli.ts hwlab nodes git-mirror flush --node ${scoped.node} --lane ${scoped.lane} --confirm`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function nodeRuntimeGitMirrorRun(scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>): Record<string, unknown> {
|
||||
if (scoped.action !== "sync" && scoped.action !== "flush") return nodeRuntimeUnsupportedAction(scoped);
|
||||
if (!scoped.confirm && !scoped.dryRun) throw new Error(`git-mirror ${scoped.action} requires --dry-run or --confirm`);
|
||||
const spec = scoped.spec;
|
||||
const mirror = nodeRuntimeGitMirrorTarget(spec);
|
||||
const retryMaxAttempts = !scoped.dryRun && (scoped.action === "sync" || scoped.action === "flush") ? 5 : 1;
|
||||
const attempts: Record<string, unknown>[] = [];
|
||||
let finalAttempt: {
|
||||
attempt: number;
|
||||
retryLabel: string;
|
||||
jobName: string;
|
||||
manifest: Record<string, unknown>;
|
||||
result: CommandResult;
|
||||
partialSuccess: Record<string, unknown> | null;
|
||||
retryableFailure: Record<string, unknown> | null;
|
||||
actionSucceeded: boolean;
|
||||
} | null = null;
|
||||
|
||||
for (let attempt = 1; attempt <= retryMaxAttempts; attempt += 1) {
|
||||
const retryLabel = `${attempt}/${retryMaxAttempts}`;
|
||||
const jobName = nodeRuntimeGitMirrorJobName(mirror, scoped.action);
|
||||
const manifest = nodeRuntimeGitMirrorJobManifest(mirror, scoped.action, jobName);
|
||||
const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
|
||||
const waitTimeoutSeconds = Math.max(5, Math.min(45, Math.max(5, scoped.timeoutSeconds - 10)));
|
||||
const script = [
|
||||
"set -eu",
|
||||
`namespace=${shellQuote(mirror.namespace)}`,
|
||||
`job=${shellQuote(jobName)}`,
|
||||
`manifest_b64=${shellQuote(manifestB64)}`,
|
||||
"manifest_path=\"/tmp/$job.json\"",
|
||||
"printf '%s' \"$manifest_b64\" | base64 -d > \"$manifest_path\"",
|
||||
scoped.dryRun
|
||||
? "kubectl create --dry-run=server -f \"$manifest_path\" -o name"
|
||||
: [
|
||||
"kubectl delete job -n \"$namespace\" \"$job\" --ignore-not-found=true >/dev/null",
|
||||
"kubectl create -f \"$manifest_path\"",
|
||||
`deadline=$(( $(date +%s) + ${waitTimeoutSeconds} ))`,
|
||||
"while :; do",
|
||||
" status=$(kubectl get job -n \"$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 \"$namespace\" \"job/$job\" --tail=200 || true; exit 44; fi",
|
||||
" if [ \"$(date +%s)\" -ge \"$deadline\" ]; then kubectl get job,pod -n \"$namespace\" -l job-name=\"$job\" -o wide || true; exit 45; fi",
|
||||
" sleep 2",
|
||||
"done",
|
||||
"kubectl logs -n \"$namespace\" \"job/$job\" --tail=200 || true",
|
||||
].join("\n"),
|
||||
].join("\n");
|
||||
const result = runNodeK3sScript(spec, script, scoped.timeoutSeconds);
|
||||
const partialSuccess = nodeRuntimeGitMirrorFlushPartialSuccess(scoped, mirror, result);
|
||||
const actionSucceeded = isCommandSuccess(result);
|
||||
const retryableFailure = !actionSucceeded
|
||||
? nodeRuntimeGitMirrorRetryableFailure(scoped, mirror, result, partialSuccess, attempt, retryMaxAttempts)
|
||||
: null;
|
||||
const retryable = retryableFailure?.retryable === true;
|
||||
const exhausted = retryable && attempt >= retryMaxAttempts;
|
||||
attempts.push({
|
||||
attempt,
|
||||
retryLabel,
|
||||
jobName,
|
||||
ok: actionSucceeded,
|
||||
exitCode: result.exitCode,
|
||||
retryable,
|
||||
exhausted,
|
||||
partialSuccess: partialSuccess?.partialSuccess ?? null,
|
||||
degradedReason: partialSuccess !== null
|
||||
? "node-runtime-git-mirror-flush-post-push-fetch-failed"
|
||||
: actionSucceeded ? null : `node-runtime-git-mirror-${scoped.action}-failed`,
|
||||
result: compactRuntimeCommand(result),
|
||||
valuesPrinted: false,
|
||||
});
|
||||
finalAttempt = { attempt, retryLabel, jobName, manifest, result, partialSuccess, retryableFailure, actionSucceeded };
|
||||
if (actionSucceeded || scoped.dryRun || !retryable || exhausted) break;
|
||||
const backoffMs = nodeRuntimeGitMirrorRetryDelayMs(attempt);
|
||||
printNodeRuntimeTriggerProgress(spec, {
|
||||
stage: `git-mirror-${scoped.action}-retry`,
|
||||
status: "waiting",
|
||||
retryLabel,
|
||||
nextRetryLabel: `${attempt + 1}/${retryMaxAttempts}`,
|
||||
backoffMs,
|
||||
jobName,
|
||||
});
|
||||
sleepSync(backoffMs);
|
||||
}
|
||||
|
||||
if (finalAttempt === null) throw new Error("git-mirror run produced no attempts");
|
||||
const { jobName, manifest, result, partialSuccess, retryableFailure, actionSucceeded } = finalAttempt;
|
||||
const status = scoped.dryRun || !actionSucceeded
|
||||
? undefined
|
||||
: nodeRuntimeGitMirrorStatus({ ...scoped, action: "status", dryRun: true, confirm: false });
|
||||
const retryExhausted = retryableFailure?.retryable === true && attempts.length >= retryMaxAttempts;
|
||||
const stopped = !actionSucceeded && (retryExhausted || retryableFailure?.stopped === true || retryableFailure === null);
|
||||
return {
|
||||
ok: actionSucceeded && (status === undefined || status.ok === true),
|
||||
command: `hwlab nodes git-mirror ${scoped.action} --node ${scoped.node} --lane ${scoped.lane}`,
|
||||
node: scoped.node,
|
||||
lane: scoped.lane,
|
||||
mode: scoped.dryRun ? "dry-run" : `confirmed-${scoped.action}`,
|
||||
mutation: !scoped.dryRun && actionSucceeded,
|
||||
namespace: mirror.namespace,
|
||||
githubTransport: nodeRuntimeGitMirrorGithubTransportSummary(mirror),
|
||||
jobName,
|
||||
manifest: scoped.dryRun ? manifest : undefined,
|
||||
result: compactRuntimeCommand(result),
|
||||
status,
|
||||
retry: retryMaxAttempts > 1 ? {
|
||||
policy: "exponential-backoff",
|
||||
maxAttempts: retryMaxAttempts,
|
||||
attempts,
|
||||
exhausted: retryExhausted,
|
||||
stopped,
|
||||
valuesPrinted: false,
|
||||
} : undefined,
|
||||
retryableFailure: retryableFailure ?? undefined,
|
||||
partialSuccess: partialSuccess?.partialSuccess ?? undefined,
|
||||
recovery: partialSuccess ?? undefined,
|
||||
degradedReason: partialSuccess !== null
|
||||
? "node-runtime-git-mirror-flush-post-push-fetch-failed"
|
||||
: actionSucceeded ? status?.ok === false ? "node-runtime-git-mirror-status-failed-after-action" : undefined : `node-runtime-git-mirror-${scoped.action}-failed`,
|
||||
next: partialSuccess !== null
|
||||
? partialSuccess.next
|
||||
: scoped.dryRun
|
||||
? { run: `bun scripts/cli.ts hwlab nodes git-mirror ${scoped.action} --node ${scoped.node} --lane ${scoped.lane} --confirm` }
|
||||
: retryableFailure?.next ?? { status: `bun scripts/cli.ts hwlab nodes git-mirror status --node ${scoped.node} --lane ${scoped.lane}` },
|
||||
};
|
||||
}
|
||||
|
||||
export function nodeRuntimeGitMirrorRetryableFailure(
|
||||
scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>,
|
||||
mirror: NodeRuntimeGitMirrorTargetSpec,
|
||||
result: CommandResult,
|
||||
partialSuccess: Record<string, unknown> | null,
|
||||
attempt: number,
|
||||
retryMaxAttempts: number,
|
||||
): Record<string, unknown> | null {
|
||||
const text = `${result.stdout}\n${result.stderr}`;
|
||||
const proxyConnectFailure = /hwlab git-mirror proxy-connect:/iu.test(text);
|
||||
const sshProxyBannerFailure = mirror.egressProxy.mode !== "direct" && /Connection timed out during banner exchange|Connection to UNKNOWN port 65535 timed out|kex_exchange_identification|Connection closed by remote host/iu.test(text);
|
||||
const waitTimeoutFailure = result.exitCode === 45
|
||||
|| result.exitCode === 124
|
||||
|| /UNIDESK_SSH_RUNTIME_TIMEOUT|ssh-runtime-timeout|ssh\/tran operation exceeded|job,pod/iu.test(text);
|
||||
const scriptTransportMismatch =
|
||||
(mirror.githubTransport.mode === "ssh" && /transport=https[\s\S]*https auth: missing GITHUB_TOKEN/iu.test(text))
|
||||
|| (mirror.githubTransport.mode === "https" && /transport=ssh\b/iu.test(text));
|
||||
if (scriptTransportMismatch) {
|
||||
return {
|
||||
retryable: false,
|
||||
retryAttempt: attempt,
|
||||
retryMaxAttempts,
|
||||
retryLabel: `${attempt}/${retryMaxAttempts}`,
|
||||
retryExhausted: false,
|
||||
stopped: true,
|
||||
reason: `Git mirror script transport does not match YAML githubTransport.mode=${mirror.githubTransport.mode}; apply the node control-plane so the git-mirror ConfigMap is updated before retrying.`,
|
||||
refSources: nodeRuntimeGitMirrorRefSources(scoped, mirror),
|
||||
githubTransport: nodeRuntimeGitMirrorGithubTransportSummary(mirror),
|
||||
next: {
|
||||
applyControlPlane: `bun scripts/cli.ts hwlab nodes control-plane apply --node ${scoped.node} --lane ${scoped.lane} --confirm`,
|
||||
status: `bun scripts/cli.ts hwlab nodes git-mirror status --node ${scoped.node} --lane ${scoped.lane}`,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const authFailure = /Authentication failed|Invalid username or password|Repository not found|could not read Username|could not read Password|terminal prompts disabled|https auth: missing GITHUB_TOKEN/iu.test(text);
|
||||
if (authFailure) {
|
||||
const authFix = mirror.githubTransport.mode === "ssh"
|
||||
? {
|
||||
fixSecret: `apply the YAML-declared SSH private key Secret ${mirror.secretName} through the node control-plane infra apply path`,
|
||||
status: `bun scripts/cli.ts hwlab nodes git-mirror status --node ${scoped.node} --lane ${scoped.lane}`,
|
||||
}
|
||||
: {
|
||||
fixSecret: "apply the YAML-declared githubTransport token source through the node control-plane infra apply path",
|
||||
status: `bun scripts/cli.ts hwlab nodes git-mirror status --node ${scoped.node} --lane ${scoped.lane}`,
|
||||
};
|
||||
return {
|
||||
retryable: false,
|
||||
retryAttempt: attempt,
|
||||
retryMaxAttempts,
|
||||
retryLabel: `${attempt}/${retryMaxAttempts}`,
|
||||
retryExhausted: false,
|
||||
stopped: true,
|
||||
reason: `Git mirror GitHub ${mirror.githubTransport.mode} authentication failed. This is not retryable; fix the YAML-declared ${mirror.githubTransport.mode === "ssh" ? "SSH key Secret" : "token source/Secret"} instead of consuming retry budget.`,
|
||||
refSources: nodeRuntimeGitMirrorRefSources(scoped, mirror),
|
||||
githubTransport: nodeRuntimeGitMirrorGithubTransportSummary(mirror),
|
||||
next: authFix,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const retryable = partialSuccess !== null
|
||||
|| proxyConnectFailure
|
||||
|| sshProxyBannerFailure
|
||||
|| waitTimeoutFailure
|
||||
|| /kex_exchange_identification|Connection closed by remote host|Could not read from remote repository|ssh.github.com|github.com|fetch-pack|early EOF/iu.test(text);
|
||||
if (!retryable) return null;
|
||||
const retryLabel = `${attempt}/${retryMaxAttempts}`;
|
||||
const exhausted = attempt >= retryMaxAttempts;
|
||||
return {
|
||||
retryable: true,
|
||||
retryAttempt: attempt,
|
||||
retryMaxAttempts,
|
||||
retryLabel,
|
||||
retryExhausted: exhausted,
|
||||
stopped: exhausted,
|
||||
backoffPolicy: "exponential",
|
||||
nextRetryDelayMs: exhausted ? null : nodeRuntimeGitMirrorRetryDelayMs(attempt),
|
||||
reason: partialSuccess !== null
|
||||
? "GitOps push appears to have succeeded, but the post-push fetch/recheck failed. Standard git-mirror stops without host workspace fallback."
|
||||
: waitTimeoutFailure
|
||||
? "Git mirror job wait exceeded the controlled short-connection budget. Standard git-mirror retries with exponential backoff and stops when retry budget is exhausted."
|
||||
: proxyConnectFailure || sshProxyBannerFailure
|
||||
? "Git mirror job hit a retryable YAML-first SSH-over-proxy failure. Standard git-mirror keeps using the configured node-global proxy and stops when retry budget is exhausted."
|
||||
: `Git mirror job hit a retryable upstream GitHub ${mirror.githubTransport.mode} transport/fetch failure. Standard git-mirror stops without host workspace fallback.`,
|
||||
refSources: nodeRuntimeGitMirrorRefSources(scoped, mirror),
|
||||
githubTransport: nodeRuntimeGitMirrorGithubTransportSummary(mirror),
|
||||
next: exhausted
|
||||
? {
|
||||
status: `bun scripts/cli.ts hwlab nodes git-mirror status --node ${scoped.node} --lane ${scoped.lane}`,
|
||||
stopped: "retry budget exhausted; standard git-mirror stopped instead of silently continuing",
|
||||
}
|
||||
: {
|
||||
retry: `bun scripts/cli.ts hwlab nodes git-mirror ${scoped.action} --node ${scoped.node} --lane ${scoped.lane} --confirm --wait`,
|
||||
status: `bun scripts/cli.ts hwlab nodes git-mirror status --node ${scoped.node} --lane ${scoped.lane}`,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function nodeRuntimeGitMirrorRetryDelayMs(attempt: number): number {
|
||||
return Math.min(15_000, 1000 * (2 ** Math.max(0, attempt - 1)));
|
||||
}
|
||||
|
||||
export function nodeRuntimeGitMirrorHostWriteUrl(spec: HwlabRuntimeLaneSpec, mirror: NodeRuntimeGitMirrorTargetSpec): string | null {
|
||||
const result = runNodeK3sArgs(spec, ["kubectl", "-n", mirror.namespace, "get", "service", mirror.serviceWriteName, "-o", "jsonpath={.spec.clusterIP}{\":\"}{.spec.ports[0].port}"], 60);
|
||||
const value = result.stdout.trim();
|
||||
if (result.exitCode !== 0 || value.length === 0 || value.startsWith(":")) return null;
|
||||
return `http://${value}/${mirror.sourceRepository}.git`;
|
||||
}
|
||||
|
||||
export function nodeRuntimeEnsureGitMirrorSourceCurrent(scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>, sourceCommit: string, pipelineRun: string): Record<string, unknown> {
|
||||
const full = nodeScopedFullOutput(scoped);
|
||||
const before = nodeRuntimeGitMirrorStatus({ ...scoped, action: "status", dryRun: true, confirm: false });
|
||||
const beforeSummary = compactNodeRuntimeGitMirrorStatus(before);
|
||||
const beforeGithubTransport = record(before.githubTransport);
|
||||
if (beforeGithubTransport.required === true && beforeGithubTransport.ready === false) {
|
||||
return {
|
||||
ok: false,
|
||||
mode: "github-transport-auth-secret-not-ready",
|
||||
sourceCommit,
|
||||
beforeSummary,
|
||||
before: full ? before : undefined,
|
||||
githubTransport: beforeGithubTransport,
|
||||
degradedReason: "node-runtime-git-mirror-auth-secret-not-ready",
|
||||
next: {
|
||||
applyBootstrap: `bun scripts/cli.ts hwlab nodes control-plane infra apply --node ${scoped.node} --lane ${scoped.lane} --confirm`,
|
||||
status: `bun scripts/cli.ts hwlab nodes git-mirror status --node ${scoped.node} --lane ${scoped.lane}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (before.ok === true && beforeSummary.localSource === sourceCommit && beforeSummary.githubSource === sourceCommit) {
|
||||
const flush = nodeRuntimeEnsureGitMirrorFlushed(scoped, "pre", sourceCommit, pipelineRun, before);
|
||||
return {
|
||||
ok: flush.ok === true,
|
||||
mode: "already-current",
|
||||
sourceCommit,
|
||||
beforeSummary,
|
||||
before: full ? before : undefined,
|
||||
flush,
|
||||
degradedReason: flush.ok === true ? undefined : "node-runtime-git-mirror-pre-flush-failed",
|
||||
};
|
||||
}
|
||||
const sync = nodeRuntimeGitMirrorRun({ ...scoped, domain: "git-mirror", action: "sync", confirm: true, dryRun: false, wait: true });
|
||||
const after = record(sync.status);
|
||||
const afterSummary = Object.keys(after).length > 0 ? compactNodeRuntimeGitMirrorStatus(after) : {};
|
||||
const sourceOk = sync.ok === true && afterSummary.localSource === sourceCommit && afterSummary.githubSource === sourceCommit;
|
||||
const flush = sourceOk ? nodeRuntimeEnsureGitMirrorFlushed(scoped, "pre", sourceCommit, pipelineRun, after) : null;
|
||||
const ok = sourceOk && (flush === null || flush.ok === true);
|
||||
return {
|
||||
ok,
|
||||
mode: "synced-before-trigger",
|
||||
sourceCommit,
|
||||
beforeSummary,
|
||||
before: full ? before : undefined,
|
||||
sync: full ? sync : compactNodeRuntimeGitMirrorRun(sync),
|
||||
afterSummary: Object.keys(afterSummary).length > 0 ? afterSummary : null,
|
||||
after: full ? sync.status ?? null : undefined,
|
||||
flush,
|
||||
degradedReason: ok
|
||||
? undefined
|
||||
: sourceOk
|
||||
? "node-runtime-git-mirror-pre-flush-failed"
|
||||
: "node-runtime-git-mirror-local-source-not-current-after-sync",
|
||||
};
|
||||
}
|
||||
|
||||
export function nodeRuntimeEnsureGitMirrorFlushed(
|
||||
scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>,
|
||||
phase: "pre" | "post" | "parallel",
|
||||
sourceCommit: string,
|
||||
pipelineRun: string | null,
|
||||
statusInput: Record<string, unknown> | null = null,
|
||||
): Record<string, unknown> {
|
||||
const stage = `git-mirror-${phase}-flush`;
|
||||
const full = nodeScopedFullOutput(scoped);
|
||||
const before = statusInput ?? nodeRuntimeGitMirrorStatus({ ...scoped, action: "status", dryRun: true, confirm: false });
|
||||
const beforeSummary = compactNodeRuntimeGitMirrorStatus(before);
|
||||
if (before.ok !== true) {
|
||||
printNodeRuntimeTriggerProgress(scoped.spec, { stage, status: "failed", sourceCommit, pipelineRun, reason: "git-mirror-status-failed", ...beforeSummary });
|
||||
return {
|
||||
ok: false,
|
||||
phase,
|
||||
mode: "status-failed",
|
||||
executed: false,
|
||||
before: full ? before : undefined,
|
||||
beforeSummary,
|
||||
degradedReason: `node-runtime-git-mirror-${phase}-status-failed`,
|
||||
next: { status: `bun scripts/cli.ts hwlab nodes git-mirror status --node ${scoped.node} --lane ${scoped.lane}` },
|
||||
};
|
||||
}
|
||||
const flushNeeded = nodeRuntimeGitMirrorNeedsFlush(before);
|
||||
if (!flushNeeded) {
|
||||
printNodeRuntimeTriggerProgress(scoped.spec, { stage, status: "skipped", sourceCommit, pipelineRun, flushNeeded: false, ...beforeSummary });
|
||||
return {
|
||||
ok: true,
|
||||
phase,
|
||||
mode: "already-flushed",
|
||||
executed: false,
|
||||
before: full ? before : undefined,
|
||||
beforeSummary,
|
||||
after: full ? before : undefined,
|
||||
afterSummary: beforeSummary,
|
||||
};
|
||||
}
|
||||
printNodeRuntimeTriggerProgress(scoped.spec, { stage, status: "started", sourceCommit, pipelineRun, flushNeeded: true, ...beforeSummary });
|
||||
const flush = nodeRuntimeGitMirrorRun({ ...scoped, domain: "git-mirror", action: "flush", confirm: true, dryRun: false, wait: true });
|
||||
const after = record(flush.status);
|
||||
const afterSummary = Object.keys(after).length > 0 ? compactNodeRuntimeGitMirrorStatus(after) : {};
|
||||
const ok = flush.ok === true && Object.keys(after).length > 0 && !nodeRuntimeGitMirrorNeedsFlush(after);
|
||||
printNodeRuntimeTriggerProgress(scoped.spec, {
|
||||
stage,
|
||||
status: ok ? "succeeded" : "failed",
|
||||
sourceCommit,
|
||||
pipelineRun,
|
||||
flushNeeded: true,
|
||||
jobName: flush.jobName ?? null,
|
||||
...afterSummary,
|
||||
});
|
||||
return {
|
||||
ok,
|
||||
phase,
|
||||
mode: "flushed",
|
||||
executed: true,
|
||||
before: full ? before : undefined,
|
||||
beforeSummary,
|
||||
flush: full ? flush : compactNodeRuntimeGitMirrorRun(flush),
|
||||
jobName: flush.jobName ?? null,
|
||||
after: full ? (Object.keys(after).length > 0 ? after : null) : undefined,
|
||||
afterSummary: Object.keys(afterSummary).length > 0 ? afterSummary : null,
|
||||
degradedReason: ok ? undefined : `node-runtime-git-mirror-${phase}-flush-failed`,
|
||||
next: ok ? undefined : flush.next ?? { flush: `bun scripts/cli.ts hwlab nodes git-mirror flush --node ${scoped.node} --lane ${scoped.lane} --confirm --wait` },
|
||||
};
|
||||
}
|
||||
|
||||
export function nodeRuntimeOpportunisticGitMirrorSync(
|
||||
scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>,
|
||||
sourceCommit: string,
|
||||
pipelineRun: string,
|
||||
): Record<string, unknown> | null {
|
||||
const stage = "git-mirror-parallel-sync";
|
||||
const full = nodeScopedFullOutput(scoped);
|
||||
printNodeRuntimeTriggerProgress(scoped.spec, { stage, status: "started", sourceCommit, pipelineRun });
|
||||
const sync = nodeRuntimeGitMirrorRun({ ...scoped, domain: "git-mirror", action: "sync", confirm: true, dryRun: false, wait: true });
|
||||
const after = record(sync.status);
|
||||
const afterSummary = Object.keys(after).length > 0 ? compactNodeRuntimeGitMirrorStatus(after) : {};
|
||||
const ok = sync.ok === true;
|
||||
printNodeRuntimeTriggerProgress(scoped.spec, {
|
||||
stage,
|
||||
status: ok ? "succeeded" : "failed",
|
||||
sourceCommit,
|
||||
pipelineRun,
|
||||
jobName: sync.jobName ?? null,
|
||||
...afterSummary,
|
||||
});
|
||||
return {
|
||||
ok,
|
||||
phase: "parallel",
|
||||
mode: "synced-during-pipelinerun-wait",
|
||||
executed: true,
|
||||
sourceCommit,
|
||||
pipelineRun,
|
||||
sync: full ? sync : compactNodeRuntimeGitMirrorRun(sync),
|
||||
jobName: sync.jobName ?? null,
|
||||
after: full ? (Object.keys(after).length > 0 ? after : null) : undefined,
|
||||
afterSummary: Object.keys(afterSummary).length > 0 ? afterSummary : null,
|
||||
degradedReason: ok ? undefined : "node-runtime-git-mirror-parallel-sync-failed",
|
||||
next: ok ? undefined : sync.next ?? { sync: `bun scripts/cli.ts hwlab nodes git-mirror sync --node ${scoped.node} --lane ${scoped.lane} --confirm --wait` },
|
||||
};
|
||||
}
|
||||
|
||||
export function nodeRuntimeOpportunisticGitMirrorFlush(
|
||||
scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>,
|
||||
sourceCommit: string,
|
||||
pipelineRun: string,
|
||||
): Record<string, unknown> | null {
|
||||
const status = nodeRuntimeGitMirrorStatus({ ...scoped, action: "status", dryRun: true, confirm: false });
|
||||
if (status.ok !== true || !nodeRuntimeGitMirrorNeedsFlush(status)) return null;
|
||||
return nodeRuntimeEnsureGitMirrorFlushed(scoped, "parallel", sourceCommit, pipelineRun, status);
|
||||
}
|
||||
|
||||
export function nodeRuntimeGitMirrorNeedsFlush(status: Record<string, unknown>): boolean {
|
||||
const summary = record(status.summary);
|
||||
const localGitops = typeof summary.localGitops === "string" ? summary.localGitops : null;
|
||||
const githubGitops = typeof summary.githubGitops === "string" ? summary.githubGitops : null;
|
||||
return summary.pendingFlush === true
|
||||
|| summary.flushNeeded === true
|
||||
|| summary.githubInSync === false
|
||||
|| (localGitops !== null && githubGitops !== null && localGitops !== githubGitops);
|
||||
}
|
||||
|
||||
export function nodeScopedFullOutput(scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>): boolean {
|
||||
return scoped.originalArgs.includes("--full") || scoped.originalArgs.includes("--raw");
|
||||
}
|
||||
|
||||
export function compactNodeRuntimeGitMirrorObservation(status: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
ok: status.ok === true,
|
||||
mode: status.mode ?? null,
|
||||
mutation: status.mutation === true,
|
||||
summary: compactNodeRuntimeGitMirrorStatus(status),
|
||||
degradedReason: status.degradedReason ?? null,
|
||||
next: status.next ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function compactNodeRuntimeGitMirrorRun(result: Record<string, unknown>): Record<string, unknown> {
|
||||
const status = record(result.status);
|
||||
return {
|
||||
ok: result.ok === true,
|
||||
action: result.action ?? null,
|
||||
mode: result.mode ?? null,
|
||||
mutation: result.mutation === true,
|
||||
jobName: result.jobName ?? null,
|
||||
partialSuccess: result.partialSuccess ?? null,
|
||||
fallback: result.fallback ?? null,
|
||||
retry: result.retry ?? null,
|
||||
githubTransport: result.githubTransport ?? null,
|
||||
retryableFailure: result.retryableFailure ?? null,
|
||||
degradedReason: result.degradedReason ?? null,
|
||||
statusSummary: Object.keys(status).length > 0 ? compactNodeRuntimeGitMirrorStatus(status) : null,
|
||||
next: result.next ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function withNodeRuntimeGitMirrorRendered(result: Record<string, unknown>, scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>): RenderedCliResult {
|
||||
const status = record(result.status);
|
||||
const summary = scoped.action === "status" ? record(result.summary) : record(status.summary);
|
||||
const retry = record(result.retry);
|
||||
const retryAttempts = Array.isArray(retry.attempts) ? retry.attempts.map(record).filter((item): item is Record<string, unknown> => item !== null) : [];
|
||||
const retryText = typeof retry.maxAttempts === "number"
|
||||
? `${retryAttempts.length}/${retry.maxAttempts}${retry.exhausted === true ? " exhausted" : ""}`
|
||||
: "-";
|
||||
const transport = record(result.githubTransport);
|
||||
const commandResult = record(result.result);
|
||||
const retryableFailure = record(result.retryableFailure);
|
||||
const next = record(result.next);
|
||||
const resultStatus = result.ok === true ? "ok" : result.ok === false ? "failed" : "-";
|
||||
const lines = [
|
||||
`hwlab nodes git-mirror ${scoped.action}`,
|
||||
"",
|
||||
webObserveTable(
|
||||
["NODE", "LANE", "ACTION", "STATUS", "MODE", "JOB", "RETRY"],
|
||||
[[
|
||||
scoped.node,
|
||||
scoped.lane,
|
||||
scoped.action,
|
||||
resultStatus,
|
||||
webObserveText(result.mode),
|
||||
webObserveShort(webObserveText(result.jobName), 36),
|
||||
retryText,
|
||||
]],
|
||||
),
|
||||
"",
|
||||
Object.keys(summary).length === 0
|
||||
? "REFS\n-"
|
||||
: webObserveTable(
|
||||
["LOCAL_SOURCE", "GITHUB_SOURCE", "LOCAL_GITOPS", "GITHUB_GITOPS", "PENDING", "IN_SYNC"],
|
||||
[[
|
||||
shortValue(summary.localSource),
|
||||
shortValue(summary.githubSource),
|
||||
shortValue(summary.localGitops),
|
||||
shortValue(summary.githubGitops),
|
||||
webObserveText(summary.pendingFlush),
|
||||
webObserveText(summary.githubInSync),
|
||||
]],
|
||||
),
|
||||
"",
|
||||
Object.keys(transport).length === 0
|
||||
? "TRANSPORT\n-"
|
||||
: webObserveTable(
|
||||
["MODE", "READY", "REQUIRED"],
|
||||
[[webObserveText(transport.mode), webObserveText(transport.ready), webObserveText(transport.required)]],
|
||||
),
|
||||
"",
|
||||
Object.keys(commandResult).length === 0
|
||||
? "RESULT\n-"
|
||||
: webObserveTable(
|
||||
["EXIT", "TIMED_OUT", "STDOUT_BYTES", "STDERR_BYTES"],
|
||||
[[
|
||||
webObserveText(commandResult.exitCode),
|
||||
webObserveText(commandResult.timedOut),
|
||||
webObserveText(commandResult.stdoutBytes),
|
||||
webObserveText(commandResult.stderrBytes),
|
||||
]],
|
||||
),
|
||||
"",
|
||||
Object.keys(retryableFailure).length === 0
|
||||
? "RETRYABLE\n-"
|
||||
: webObserveTable(
|
||||
["RETRYABLE", "EXHAUSTED", "REASON"],
|
||||
[[
|
||||
webObserveText(retryableFailure.retryable),
|
||||
webObserveText(retryableFailure.exhausted),
|
||||
webObserveShort(webObserveText(retryableFailure.reason ?? retryableFailure.degradedReason), 100),
|
||||
]],
|
||||
),
|
||||
"",
|
||||
"NEXT",
|
||||
` ${next.status ?? next.sync ?? next.flush ?? next.retry ?? next.run ?? `bun scripts/cli.ts hwlab nodes git-mirror status --node ${scoped.node} --lane ${scoped.lane}`}`,
|
||||
"",
|
||||
"Disclosure:",
|
||||
" default view is a bounded git-mirror summary; use --full or --raw for the complete JSON payload.",
|
||||
];
|
||||
return { ...result, renderedText: lines.join("\n"), contentType: "text/plain" };
|
||||
}
|
||||
|
||||
export function compactNodeRuntimeGitMirrorStatus(status: Record<string, unknown>): Record<string, unknown> {
|
||||
const summary = record(status.summary);
|
||||
return {
|
||||
ok: status.ok === true,
|
||||
localSource: summary.localSource ?? null,
|
||||
githubSource: summary.githubSource ?? null,
|
||||
localGitops: summary.localGitops ?? null,
|
||||
githubGitops: summary.githubGitops ?? null,
|
||||
pendingFlush: summary.pendingFlush === true,
|
||||
flushNeeded: summary.flushNeeded === true,
|
||||
githubInSync: summary.githubInSync === true,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user