fix: smooth hwlab pr and rollout visibility
This commit is contained in:
+36
-9
@@ -814,6 +814,32 @@ function parsePositionalNumberForCommand(repo: string, args: string[], startInde
|
||||
return parseNumberForCommand(repo, targets[0], label);
|
||||
}
|
||||
|
||||
function resolvePositionalPrReference(args: string[], startIndex: number, label: string, options: GitHubOptions): GitHubResolvedNumberReference | GitHubCommandResult {
|
||||
const targets = positionalArgs(args.slice(startIndex));
|
||||
if (targets.length !== 1) {
|
||||
return validationError(label, options.repo, `${label} requires exactly one positive integer or owner/repo#number positional argument`, {
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh ${label} <number> --repo ${options.repo}`,
|
||||
`bun scripts/cli.ts gh ${label} ${options.repo}#<number>`,
|
||||
],
|
||||
});
|
||||
}
|
||||
const shorthand = parseOwnerRepoNumberShorthand(targets[0]);
|
||||
if (shorthand !== null) {
|
||||
const explicitRepo = optionValue(args, "--repo");
|
||||
if (explicitRepo !== undefined && explicitRepo !== shorthand.repo) {
|
||||
return validationError(label, explicitRepo, `${label} target ${shorthand.input} resolves to repo ${shorthand.repo}, but --repo ${explicitRepo} was also provided.`, {
|
||||
shorthand,
|
||||
explicitRepo,
|
||||
});
|
||||
}
|
||||
return { repo: shorthand.repo, number: shorthand.number, shorthand };
|
||||
}
|
||||
const number = parseNumberForCommand(options.repo, targets[0], label);
|
||||
if (typeof number !== "number") return number;
|
||||
return { repo: options.repo, number };
|
||||
}
|
||||
|
||||
function parseOwnerRepoNumberShorthand(raw: string | undefined): GitHubShorthandReference | null {
|
||||
if (raw === undefined) return null;
|
||||
const match = /^([^/#\s]+)\/([^/#\s]+)#([1-9]\d*)$/u.exec(raw);
|
||||
@@ -5911,14 +5937,14 @@ export function ghHelp(): unknown {
|
||||
"bun scripts/cli.ts gh issue board-row upsert <issueNumber> [--repo owner/name] --board-issue 20 --section open|closed [--category text] --branch <branch> --tasks <task> --summary <text> --focus <text> --validation <text> --progress <text> [--status OPEN|CLOSED] [--dry-run] [--expect-updated-at ts|--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue board-row move <issueNumber> [--repo owner/name] --board-issue 20 --to open|closed [--status OPEN|CLOSED] [--dry-run] [--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue board-row delete <issueNumber> [--repo owner/name] --board-issue 20 [--dry-run] [--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh preflight <prNumber> [--repo owner/name] [--full|--raw] [compatibility alias for gh pr preflight]",
|
||||
"bun scripts/cli.ts gh preflight <prNumber|owner/repo#number> [--repo owner/name] [--full|--raw] [compatibility alias for gh pr preflight]",
|
||||
"bun scripts/cli.ts gh pr list [owner/repo] [--repo owner/name] [--state open|closed|all] [--limit N] [--json number,title,state,url,updatedAt,createdAt,author,head,base,draft]",
|
||||
"bun scripts/cli.ts gh pr files <number> [--repo owner/name] [--limit N] [number may appear before or after options]",
|
||||
"bun scripts/cli.ts gh pr diff <number> --stat [--repo owner/name] [--limit N] [number may appear before or after options; compatibility alias for pr files; no raw diff]",
|
||||
"bun scripts/cli.ts gh pr read <number|owner/repo#number> [--repo owner/name] [--number N] [--json body,title,state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,draft,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh pr view <number|owner/repo#number> [--repo owner/name] [--raw|--full] [compatibility alias for pr read]",
|
||||
"bun scripts/cli.ts gh pr preflight <number> [--repo owner/name] [--full|--raw] [number may appear before or after options]",
|
||||
"bun scripts/cli.ts gh pr closeout <number> [--repo owner/name] [--full|--raw] [number may appear before or after options; compatibility alias for pr preflight]",
|
||||
"bun scripts/cli.ts gh pr preflight <number|owner/repo#number> [--repo owner/name] [--full|--raw] [number may appear before or after options]",
|
||||
"bun scripts/cli.ts gh pr closeout <number|owner/repo#number> [--repo owner/name] [--full|--raw] [number may appear before or after options; compatibility alias for pr preflight]",
|
||||
"bun scripts/cli.ts gh pr create --title <title> --body-file <file>|--body <text> --base <branch> --head <branch> [--repo owner/name] [--draft] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr edit <number> [--title title] [--body-file <file>|--body-file -|--body <text>] [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr update <number> --mode replace|append [--body-file <file>|--body-file -|--body <text>] [--title title] [--repo owner/name] [--dry-run]",
|
||||
@@ -5958,6 +5984,7 @@ export function ghHelp(): unknown {
|
||||
"PR files is the canonical compact changed-file/stat summary. It uses GitHub REST, returns bounded file rows, additions/deletions/changes when available, truncation metadata, and a next command for full details. Raw diff patches are not emitted by default; gh pr diff <number> --stat is a compatibility alias for the same JSON summary.",
|
||||
"PR edit/update PATCHes /repos/{owner}/{repo}/pulls/{number} through REST only, never GitHub Projects Classic GraphQL/projectCards, and returns low-noise JSON with repo, PR number, changedFields, url, and body size/SHA metadata instead of echoing the full body.",
|
||||
"PR read is the canonical read path; view remains a compatibility alias. PR read/view accept owner/repo#number shorthand and --number N as a compatibility alias for the positional PR number; shorthand derives --repo unless an explicit conflicting --repo is supplied, which fails structurally with suggested commands. PR read/view supports REST closeout fields stateDetail, closed, closedAt, merged, mergedAt, mergeCommit, headRefName, and baseRefName; mergeable, mergeStateStatus, and statusCheckRollup are fetched through GitHub GraphQL only when requested or when --raw/--full requests full disclosure, and closeoutMetadata makes GraphQL errors plus UNKNOWN/null metadata explicit.",
|
||||
"PR preflight/closeout accept the same owner/repo#number shorthand as PR read/view so merge readiness checks do not require repeating --repo after a PR URL has already been normalized.",
|
||||
"PR list does not fetch mergeability or statusCheckRollup; request those closeout fields with gh pr view <number> --json headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup.",
|
||||
"PR preflight is a low-noise read-only closeout helper. It combines redacted auth capability, PR branch/state metadata, mergeability, mergeStateStatus, compact status check counts, and the explicit UniDesk REST CLI no-merge policy. Use --full or --raw to include all fetched status contexts.",
|
||||
"PR merge is a guarded write operation: it first reads closeout metadata, refuses non-open/draft/conflicting/non-clean/failed/pending PRs, then uses GitHub REST merge. Use --dry-run to see the exact merge plan without writing.",
|
||||
@@ -6073,9 +6100,9 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
if (top === "auth" && sub === "status") return authStatus(options.repo);
|
||||
|
||||
if (top === "preflight") {
|
||||
const number = parsePositionalNumberForCommand(options.repo, args, 1, "preflight");
|
||||
if (typeof number !== "number") return number;
|
||||
return prPreflight(options.repo, number, "preflight", options.full || options.raw);
|
||||
const resolved = resolvePositionalPrReference(args, 1, "preflight", options);
|
||||
if (isGitHubCommandResult(resolved)) return resolved;
|
||||
return prPreflight(resolved.repo, resolved.number, "preflight", options.full || options.raw);
|
||||
}
|
||||
|
||||
if (top === "issue") {
|
||||
@@ -6197,9 +6224,9 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
}
|
||||
if (sub === "delete") return unsupportedCommand("pr delete", options.repo, "GitHub REST does not support hard-deleting pull requests; use gh pr close for lifecycle deletion semantics.");
|
||||
if (sub === "preflight" || sub === "closeout") {
|
||||
const number = parsePositionalNumberForCommand(options.repo, args, 2, `pr ${sub}`);
|
||||
if (typeof number !== "number") return number;
|
||||
return prPreflight(options.repo, number, sub === "closeout" ? "pr closeout" : "pr preflight", options.full || options.raw);
|
||||
const resolved = resolvePositionalPrReference(args, 2, `pr ${sub}`, options);
|
||||
if (isGitHubCommandResult(resolved)) return resolved;
|
||||
return prPreflight(resolved.repo, resolved.number, sub === "closeout" ? "pr closeout" : "pr preflight", options.full || options.raw);
|
||||
}
|
||||
if (sub === "comment" && third === "delete") {
|
||||
const commentId = parseNumberForCommand(options.repo, args[3], "pr comment delete");
|
||||
|
||||
+55
-51
@@ -414,6 +414,31 @@ function compactCommandResult(result: CommandJsonResult): Record<string, unknown
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -477,14 +502,6 @@ function getV02Head(): string | null {
|
||||
return match?.[0] ?? null;
|
||||
}
|
||||
|
||||
function getV02CachedHead(): string | null {
|
||||
const result = v02WorkspaceScript("git rev-parse origin/v0.2", 30_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)}`;
|
||||
}
|
||||
@@ -539,31 +556,6 @@ function secondsBetween(start: unknown, end: unknown): number | null {
|
||||
return Math.round((endMs - startMs) / 1000);
|
||||
}
|
||||
|
||||
function pipelineRunStatusRow(item: unknown, nowMs: number): Record<string, unknown> {
|
||||
const root = record(item);
|
||||
const metadata = record(root.metadata);
|
||||
const status = record(root.status);
|
||||
const labels = record(metadata.labels);
|
||||
const conditions = Array.isArray(status.conditions) ? status.conditions : [];
|
||||
const condition = record(conditions[0]);
|
||||
const startTime = status.startTime ?? null;
|
||||
const completionTime = status.completionTime ?? null;
|
||||
const startMs = timestampMs(startTime);
|
||||
const conditionStatus = typeof condition.status === "string" ? condition.status : null;
|
||||
return {
|
||||
name: metadata.name ?? null,
|
||||
sourceCommit: labels["hwlab.pikastech.local/source-commit"] ?? null,
|
||||
status: conditionStatus,
|
||||
reason: condition.reason ?? null,
|
||||
createdAt: metadata.creationTimestamp ?? null,
|
||||
startTime,
|
||||
completionTime,
|
||||
durationSeconds: secondsBetween(startTime, completionTime),
|
||||
elapsedSeconds: startMs === null || completionTime !== null ? null : Math.max(0, Math.round((nowMs - startMs) / 1000)),
|
||||
active: completionTime === null && conditionStatus !== "True" && conditionStatus !== "False",
|
||||
};
|
||||
}
|
||||
|
||||
function pipelineRunStatusRowFromLine(line: string, nowMs: number): Record<string, unknown> | null {
|
||||
const [
|
||||
name = "",
|
||||
@@ -592,8 +584,7 @@ function pipelineRunStatusRowFromLine(line: string, nowMs: number): Record<strin
|
||||
};
|
||||
}
|
||||
|
||||
function parsePipelineRunRows(text: string, limit: number): Record<string, unknown> {
|
||||
const nowMs = Date.now();
|
||||
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))
|
||||
@@ -698,9 +689,15 @@ function shellSectionOk(section: ShellSection | undefined): boolean {
|
||||
return section?.exitCode === 0;
|
||||
}
|
||||
|
||||
function v02ControlPlaneStatusBundle(pipelineRun: string | null): CommandJsonResult {
|
||||
function v02ControlPlaneStatusBundle(sourceCommit: string | null | undefined): CommandJsonResult {
|
||||
const sourceCommitLine = sourceCommit === undefined
|
||||
? `source_commit=$(git -C ${shellQuote(V02_WORKSPACE)} rev-parse origin/v0.2 2>/dev/null || true)`
|
||||
: `source_commit=${shellQuote(sourceCommit ?? "")}`;
|
||||
const script = [
|
||||
"set +e",
|
||||
sourceCommitLine,
|
||||
"pipeline_run=",
|
||||
`if [ -n "$source_commit" ]; then pipeline_run="${V02_PIPELINERUN_PREFIX}-$(printf '%s' "$source_commit" | cut -c1-12)"; fi`,
|
||||
"section() {",
|
||||
" name=\"$1\"",
|
||||
" shift",
|
||||
@@ -709,18 +706,18 @@ function v02ControlPlaneStatusBundle(pipelineRun: string | null): CommandJsonRes
|
||||
" code=$?",
|
||||
" printf '\\n__UNIDESK_SECTION_END__ %s exit=%s\\n' \"$name\" \"$code\"",
|
||||
"}",
|
||||
"section sourceCommit printf '%s\\n' \"$source_commit\"",
|
||||
"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"}'`,
|
||||
pipelineRun === null
|
||||
? "section pipelineRun sh -c 'true'"
|
||||
: `section pipelineRun kubectl get pipelinerun -n ${shellQuote(CI_NAMESPACE)} ${shellQuote(pipelineRun)} -o 'jsonpath={.status.conditions[0].status}{"\\n"}{.status.conditions[0].reason}{"\\n"}{.status.conditions[0].message}{"\\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`,
|
||||
`section recentPipelineRuns kubectl get pipelinerun -n ${shellQuote(CI_NAMESPACE)} -l hwlab.pikastech.local/gitops-target=v02 -o ${shellQuote(pipelineRunRowsJsonPath())}`,
|
||||
].join("\n");
|
||||
return g14K3s(["script", "--", script], 60_000);
|
||||
}
|
||||
|
||||
function listV02PipelineRunsCompactFromText(text: string, commandOk: boolean, command: string[] | string, exitCode: number | null, stderr: string, limit = 8): Record<string, unknown> {
|
||||
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,
|
||||
@@ -731,7 +728,7 @@ function listV02PipelineRunsCompactFromText(text: string, commandOk: boolean, co
|
||||
activeItems: [],
|
||||
};
|
||||
}
|
||||
return parsePipelineRunRows(text, limit);
|
||||
return parsePipelineRunRows(text, limit, nowMs);
|
||||
}
|
||||
|
||||
function pipelinePrefixesForLane(lane: "v02" | "g14" | "all"): string[] {
|
||||
@@ -1177,10 +1174,12 @@ function deleteV02PipelineRun(pipelineRun: string): CommandJsonResult {
|
||||
return g14K3s(["kubectl", "delete", "pipelinerun", "-n", CI_NAMESPACE, pipelineRun, "--ignore-not-found=true"], 60_000);
|
||||
}
|
||||
|
||||
function v02ControlPlaneStatus(sourceCommit: string | null = getV02CachedHead()): Record<string, unknown> {
|
||||
const pipelineRun = sourceCommit === null ? null : v02PipelineRunName(sourceCommit);
|
||||
const bundle = v02ControlPlaneStatusBundle(pipelineRun);
|
||||
function v02ControlPlaneStatus(sourceCommitInput?: string | null): Record<string, unknown> {
|
||||
const bundle = v02ControlPlaneStatusBundle(sourceCommitInput);
|
||||
const sections = parseShellSections(statusText(bundle));
|
||||
const sourceCommit = stringOrNull(sections.sourceCommit?.stdout) ?? null;
|
||||
const pipelineRun = sourceCommit === null ? null : v02PipelineRunName(sourceCommit);
|
||||
const queryNowMs = timestampMs(sections.queryNow?.stdout) ?? Date.now();
|
||||
const controlPlane = sections.controlPlane;
|
||||
const obsoleteCronJobs = sections.obsoleteCronJobs;
|
||||
const argo = sections.argo;
|
||||
@@ -1191,6 +1190,8 @@ function v02ControlPlaneStatus(sourceCommit: string | null = getV02CachedHead())
|
||||
"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);
|
||||
@@ -1252,12 +1253,12 @@ function v02ControlPlaneStatus(sourceCommit: string | null = getV02CachedHead())
|
||||
function runV02ControlPlane(options: G14ControlPlaneOptions): Record<string, unknown> {
|
||||
if (options.action === "cleanup-runs") return runControlPlaneCleanup(options);
|
||||
if (options.action === "cleanup-released-pvs") return runControlPlaneReleasedPvCleanup(options);
|
||||
const sourceCommit = options.action === "status" ? getV02CachedHead() : getV02Head();
|
||||
const sourceCommit = options.action === "status" ? undefined : getV02Head();
|
||||
if (sourceCommit === null) {
|
||||
return { ok: false, command: `hwlab g14 control-plane ${options.action} --lane v02`, degradedReason: "v02-head-unresolved", workspace: V02_WORKSPACE };
|
||||
}
|
||||
if (options.action === "runtime-migration") return runV02RuntimeMigration(options, sourceCommit);
|
||||
if (options.action === "status") return v02ControlPlaneStatus(sourceCommit);
|
||||
if (options.action === "status") return v02ControlPlaneStatus();
|
||||
if (options.action === "apply") {
|
||||
const render = runV02RenderToTemp(sourceCommit);
|
||||
if (!isCommandSuccess(render)) {
|
||||
@@ -1370,11 +1371,14 @@ function runV02ControlPlane(options: G14ControlPlaneOptions): Record<string, unk
|
||||
sourceCommit,
|
||||
pipelineRun: v02PipelineRunName(sourceCommit),
|
||||
before,
|
||||
controlPlaneRefresh,
|
||||
controlPlaneRefresh: compactControlPlaneRefresh(controlPlaneRefresh),
|
||||
gitMirrorPreSync,
|
||||
deletePipelineRun: compactCommandResult(deletePipelineRun),
|
||||
createPipelineRun: createPipelineRun === null ? null : compactCommandResult(createPipelineRun),
|
||||
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.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1543,8 +1547,8 @@ function compactGitMirrorSync(sync: Record<string, unknown>): Record<string, unk
|
||||
namespace: sync.namespace ?? GIT_MIRROR_NAMESPACE,
|
||||
jobName: sync.jobName ?? null,
|
||||
exitCode: nested(sync, ["result", "exitCode"]) ?? null,
|
||||
stdoutTail: tailText(nested(sync, ["result", "stdout"]), 3000),
|
||||
stderrTail: tailText(nested(sync, ["result", "stderr"]), 3000),
|
||||
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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user