Merge pull request #1510 from pikasTech/fix/1499-refresh-render-evidence

fix: persist HWLAB refresh render/apply evidence
This commit is contained in:
Lyon
2026-07-04 11:50:18 +08:00
committed by GitHub
7 changed files with 391 additions and 7 deletions
@@ -34,6 +34,8 @@ Do not debug the same state/read/write problem by repeatedly pushing empty or ti
When a branch-follower issue remains ambiguous after a debug step or drill-down, split the CLI into a smaller single-step probe before any new end-to-end run. Add or use a focused `debug-step`, follower-scoped drill-down, or bounded target-side diagnostic for the exact missing edge, such as PipelineRun -> Pipeline spec, controller refresh apply object, state write, closeout re-read, or log/timing extraction. Do not use another source PR, merge, or full automatic follower loop as the next diagnostic action until the narrower step can show the needed evidence.
For HWLAB native `control-plane-refresh`, the bounded evidence chain must preserve both the rendered Pipeline summary and the applied cluster object summary for the same source commit: rendered Pipeline name, bounded `runtime-ready` task/when summary, source commit/stage ref, applied Pipeline name, resourceVersion, and a short annotation/label subset proving which object was patched. If the Job TTL has already removed the original Job, status/events/logs must show `-` or a bounded missing reason from stored state instead of inferring the missing edge.
CI/CD validation must be decomposable into ordered single-step gates before a full rollout observation is accepted: first validate the reuse plan, then CI parallelism/TaskRun plan, then CD rollout plan, then post-deploy monitoring/health evidence. Each gate must have a CLI/debug-step/drill-down entry that can be run and fixed independently on the target side. Do not use issue comments, repeated PR merges, or end-to-end follower loops as substitutes for a missing single-step validator; add the missing bounded CLI step first.
When a repeated runtime pitfall or visibility defect is found during branch-follower work, update this reference or the skill entry first, then continue with the narrow debug step. Do not proceed to `run-once`, controller loop observation, automatic follower validation, or source-commit-driven integration until the relevant `state-read`, `status-read`, `decide`, and `state-write` debug steps pass for the affected follower.
@@ -26,8 +26,8 @@ try {
prepareYamlDependency();
applyDeployOverlay();
renderControlPlane();
await applyPipeline();
emit({ ok: true, status: "applied" });
const evidence = await applyPipeline();
emit({ ok: true, status: "applied", ...evidence });
} finally {
rmSync(workDir, { recursive: true, force: true });
}
@@ -143,16 +143,21 @@ async function applyPipeline() {
if (typeof renderedPipelineName !== "string" || renderedPipelineName.length === 0) {
throw new Error(`rendered Pipeline metadata.name missing: ${pipelinePath}`);
}
const render = summarizeRenderedPipeline(pipeline, renderedPipelineName);
const pipelineName = requiredOverlayString("pipelineName");
pipeline.metadata = pipeline.metadata && typeof pipeline.metadata === "object" ? pipeline.metadata : {};
pipeline.metadata.name = pipelineName;
const pipelineText = YAML.stringify(pipeline);
await kubeRequest(
const applyText = await kubeRequest(
"PATCH",
`/apis/tekton.dev/v1/namespaces/${encodeURIComponent(tektonNamespace)}/pipelines/${encodeURIComponent(pipelineName)}?fieldManager=${encodeURIComponent(fieldManager)}&force=true`,
pipelineText,
"application/apply-patch+yaml",
);
return {
render,
apply: summarizeAppliedPipeline(parseJsonObject(applyText), pipelineName, tektonNamespace),
};
}
function yamlModule() {
@@ -281,6 +286,102 @@ function requiredNonNegativeNumber(name) {
return Math.floor(value);
}
function summarizeRenderedPipeline(pipeline, pipelineName) {
const tasks = Array.isArray(pipeline?.spec?.tasks) ? pipeline.spec.tasks : [];
const runtimeReady = tasks.find((task) => recordOrNull(task)?.name === "runtime-ready");
return {
pipelineName,
taskCount: tasks.length,
runtimeReadyTask: summarizeRuntimeReadyTask(runtimeReady),
};
}
function summarizeRuntimeReadyTask(value) {
const task = recordOrNull(value);
if (task === null) return { present: false, name: null, runAfter: [], when: [] };
return {
present: true,
name: stringOrNull(task.name),
runAfter: compactStringArray(task.runAfter, 4),
when: compactWhenList(task.when, 4),
};
}
function summarizeAppliedPipeline(value, pipelineName, namespace) {
const metadata = recordOrNull(value?.metadata);
return {
pipelineName: stringOrNull(metadata?.name) || pipelineName,
namespace: stringOrNull(metadata?.namespace) || namespace,
resourceVersion: stringOrNull(metadata?.resourceVersion),
annotations: compactMetadataMap(metadata?.annotations, [
"sourceConfig",
"ciContract",
"policy",
"hwlab.pikastech.local/source-commit",
"tekton.dev/pipelines.minVersion",
], 6),
labels: compactMetadataMap(metadata?.labels, [
"hwlab.pikastech.local/source-commit",
"app.kubernetes.io/name",
"app.kubernetes.io/part-of",
"app.kubernetes.io/component",
], 6),
degradedReason: metadata === null ? "apply-response-metadata-missing" : null,
};
}
function compactMetadataMap(value, preferredKeys, limit) {
const record = recordOrNull(value);
if (record === null) return null;
const output = {};
for (const key of preferredKeys) {
const item = stringOrNull(record[key]);
if (item === null || output[key] !== undefined) continue;
output[key] = item;
if (Object.keys(output).length >= limit) return output;
}
for (const key of Object.keys(record).sort()) {
const item = stringOrNull(record[key]);
if (item === null || output[key] !== undefined) continue;
output[key] = item;
if (Object.keys(output).length >= limit) break;
}
return Object.keys(output).length === 0 ? null : output;
}
function compactStringArray(value, limit) {
return Array.isArray(value)
? value.map((item) => stringOrNull(item)).filter(Boolean).slice(0, limit)
: [];
}
function compactWhenList(value, limit) {
return Array.isArray(value)
? value.map((item) => recordOrNull(item)).filter(Boolean).slice(0, limit).map((item) => ({
input: stringOrNull(item.input),
operator: stringOrNull(item.operator),
values: compactStringArray(item.values, 4),
}))
: [];
}
function parseJsonObject(text) {
try {
const parsed = JSON.parse(text);
return recordOrNull(parsed);
} catch {
return null;
}
}
function recordOrNull(value) {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
}
function stringOrNull(value) {
return typeof value === "string" && value.length > 0 ? value : null;
}
function emit(extra) {
process.stdout.write(`${JSON.stringify({
...extra,
+62 -1
View File
@@ -292,16 +292,69 @@ function compactRefreshEvidence(refresh) {
jobName: stringOrNull(value.jobName) ?? stringOrNull(summary.jobName),
namespace: stringOrNull(value.namespace) ?? stringOrNull(summary.namespace),
status: stringOrNull(summary.status),
pipeline: stringOrNull(summary.pipeline),
pipeline: stringOrNull(summary.pipeline) || stringOrNull(recordOrNull(summary.apply)?.pipelineName) || stringOrNull(recordOrNull(summary.render)?.pipelineName),
sourceCommit: stringOrNull(summary.sourceCommit),
sourceStageRef: stringOrNull(summary.sourceStageRef),
elapsedMs: numberOrNull(summary.elapsedMs),
render: compactRefreshRender(recordOrNull(summary.render)),
apply: compactRefreshApply(recordOrNull(summary.apply)),
sourceAuthority: stringOrNull(summary.sourceAuthority),
statusAuthority: stringOrNull(summary.statusAuthority),
parsedDownstreamCliOutput: false,
};
}
function compactRefreshRender(value) {
if (value === null) return null;
return {
pipelineName: stringOrNull(value.pipelineName),
taskCount: numberOrNull(value.taskCount),
runtimeReadyTask: compactRefreshRuntimeReady(recordOrNull(value.runtimeReadyTask)),
};
}
function compactRefreshApply(value) {
if (value === null) return null;
return {
pipelineName: stringOrNull(value.pipelineName),
namespace: stringOrNull(value.namespace),
resourceVersion: stringOrNull(value.resourceVersion),
annotations: compactStringMap(recordOrNull(value.annotations)),
labels: compactStringMap(recordOrNull(value.labels)),
degradedReason: stringOrNull(value.degradedReason),
};
}
function compactRefreshRuntimeReady(value) {
if (value === null) return null;
return {
present: booleanOrNull(value.present),
name: stringOrNull(value.name),
runAfter: compactStringArray(value.runAfter, 4),
when: compactWhenList(value.when, 4),
};
}
function compactWhenList(value, limit) {
return Array.isArray(value)
? value.map((item) => recordOrNull(item)).filter(Boolean).slice(0, limit).map((item) => ({
input: stringOrNull(item.input),
operator: stringOrNull(item.operator),
values: compactStringArray(item.values, 4),
}))
: [];
}
function compactStringMap(value) {
if (value === null) return null;
const output = {};
for (const [key, item] of Object.entries(value).slice(0, 8)) {
const text = stringOrNull(item);
if (text !== null) output[key] = text;
}
return Object.keys(output).length === 0 ? null : output;
}
function compactArgo(argo) {
const value = recordOrNull(argo);
if (value === null) return null;
@@ -492,6 +545,14 @@ function numberOrNull(value) {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function booleanOrNull(value) {
return value === true ? true : value === false ? false : null;
}
function compactStringArray(value, limit) {
return Array.isArray(value) ? value.map((item) => stringOrNull(item)).filter(Boolean).slice(0, limit) : [];
}
const result = await readConfigMap();
const errors = [];
const stateByFollower = {};
+66 -1
View File
@@ -457,16 +457,73 @@ function compactRefreshEvidence(value: unknown): Record<string, unknown> | null
jobName: stringOrNull(refresh.jobName),
namespace: stringOrNull(refresh.namespace),
status: stringOrNull(refresh.status),
pipeline: stringOrNull(refresh.pipeline),
pipeline: stringOrNull(refresh.pipeline) ?? stringOrNull(asOptionalRecord(refresh.apply)?.pipelineName) ?? stringOrNull(asOptionalRecord(refresh.render)?.pipelineName),
sourceCommit: stringOrNull(refresh.sourceCommit),
sourceStageRef: stringOrNull(refresh.sourceStageRef),
elapsedMs: numberOrNull(refresh.elapsedMs),
render: compactRefreshRender(asOptionalRecord(refresh.render)),
apply: compactRefreshApply(asOptionalRecord(refresh.apply)),
sourceAuthority: stringOrNull(refresh.sourceAuthority),
statusAuthority: stringOrNull(refresh.statusAuthority),
parsedDownstreamCliOutput: false,
};
}
function compactRefreshRender(value: Record<string, unknown> | null): Record<string, unknown> | null {
if (value === null) return null;
return {
pipelineName: stringOrNull(value.pipelineName),
taskCount: numberOrNull(value.taskCount),
runtimeReadyTask: compactRefreshRuntimeReady(asOptionalRecord(value.runtimeReadyTask)),
};
}
function compactRefreshApply(value: Record<string, unknown> | null): Record<string, unknown> | null {
if (value === null) return null;
return {
pipelineName: stringOrNull(value.pipelineName),
namespace: stringOrNull(value.namespace),
resourceVersion: stringOrNull(value.resourceVersion),
annotations: compactStringMap(asOptionalRecord(value.annotations)),
labels: compactStringMap(asOptionalRecord(value.labels)),
degradedReason: stringOrNull(value.degradedReason),
};
}
function compactRefreshRuntimeReady(value: Record<string, unknown> | null): Record<string, unknown> | null {
if (value === null) return null;
return {
present: booleanOrNull(value.present),
name: stringOrNull(value.name),
runAfter: compactStringArray(value.runAfter, 4),
when: compactWhenList(value.when, 4),
};
}
function compactWhenList(value: unknown, limit: number): Array<Record<string, unknown>> {
return Array.isArray(value)
? value
.map((item) => asOptionalRecord(item))
.filter((item): item is Record<string, unknown> => item !== null)
.slice(0, limit)
.map((item) => ({
input: stringOrNull(item.input),
operator: stringOrNull(item.operator),
values: compactStringArray(item.values, 4),
}))
: [];
}
function compactStringMap(value: Record<string, unknown> | null): Record<string, string> | null {
if (value === null) return null;
const output: Record<string, string> = {};
for (const [key, item] of Object.entries(value).slice(0, 8)) {
const text = stringOrNull(item);
if (text !== null) output[key] = text;
}
return Object.keys(output).length === 0 ? null : output;
}
function arrayRecords(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null && !Array.isArray(item)) : [];
}
@@ -555,6 +612,14 @@ function numberOrNull(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function booleanOrNull(value: unknown): boolean | null {
return value === true ? true : value === false ? false : null;
}
function compactStringArray(value: unknown, limit: number): string[] {
return Array.isArray(value) ? value.map((item) => stringOrNull(item)).filter((item): item is string => item !== null).slice(0, limit) : [];
}
function shortSha(value: string | null): string {
if (value === null) return "-";
return value.length > 12 ? value.slice(0, 12) : value;
+46
View File
@@ -37,6 +37,7 @@ function renderJobHuman(payload: Record<string, unknown>): string {
const pods = arrayRecords(result?.pods);
const logs = arrayRecords(result?.logs);
const errors = arrayRecords(result?.errors);
const summaryEvidence = refreshEvidenceRows(asOptionalRecord(result?.summary));
const command = asOptionalRecord(payload.command);
const identity = asOptionalRecord(command?.identity);
return [
@@ -59,6 +60,7 @@ function renderJobHuman(payload: Record<string, unknown>): string {
pods.length === 0 ? "" : `\nPODS\n${table(["POD", "PHASE", "READY", "START", "CONTAINERS", "REASON"], pods.map(jobPodRow))}`,
logs.length === 0 ? "" : `\nLOG TAILS\n${table(["POD", "CONTAINER", "STATUS", "REASON", "LINES", "BYTES", "TIMING", "MESSAGE"], logs.map(logRow))}`,
errors.length === 0 ? "" : `\nERRORS\n${table(["POD", "CONTAINER", "REASON", "MESSAGE"], errors.map((item) => [item.pod, item.container, item.degradedReason, item.message]))}`,
summaryEvidence.length === 0 ? "" : `\nEVIDENCE\n${table(["TYPE", "STATUS", "DETAIL", "OBJECT"], summaryEvidence)}`,
command === null ? "" : `\nTARGET COMMAND\n${table(["ROUTE", "SCRIPT", "EXIT", "PARSE_ERROR"], [[identity?.route ?? "-", identity?.script ?? "-", command.exitCode ?? "-", command.parseError ?? "-"]])}`,
command?.stdoutTail ? `\nSTDOUT_TAIL\n${command.stdoutTail}` : "",
command?.stderrTail ? `\nSTDERR_TAIL\n${command.stderrTail}` : "",
@@ -287,11 +289,37 @@ function nativeGateRows(native: Record<string, unknown> | null): unknown[][] {
`${shortSha(stringOrNull(refresh.sourceCommit))}/${stringOrNull(refresh.pipeline) ?? "-"}`,
stringOrNull(refresh.jobName) ?? "-",
]);
rows.push(...refreshEvidenceRows(refresh));
}
for (const error of arrayTextItems(native.errors).slice(0, 5)) rows.push(["error", "present", error, "-"]);
return rows;
}
function refreshEvidenceRows(value: Record<string, unknown> | null): unknown[][] {
if (value === null) return [];
const rows: unknown[][] = [];
const render = asOptionalRecord(value.render);
const renderRuntimeReady = asOptionalRecord(render?.runtimeReadyTask);
if (render !== null) {
rows.push([
"control-plane-render",
renderRuntimeReady?.present === true ? "runtime-ready-present" : renderRuntimeReady?.present === false ? "runtime-ready-absent" : "-",
whenSummary(arrayRecords(renderRuntimeReady?.when)[0]),
stringOrNull(render.pipelineName) ?? "-",
]);
}
const apply = asOptionalRecord(value.apply);
if (apply !== null) {
rows.push([
"control-plane-apply",
stringOrNull(apply.resourceVersion) ?? stringOrNull(apply.degradedReason) ?? "-",
applyMetadataSummary(apply),
stringOrNull(apply.pipelineName) ?? "-",
]);
}
return rows;
}
function taskRunDetail(item: Record<string, unknown> | undefined): string {
if (item === undefined) return "-";
const duration = numberOrNull(item.durationSeconds);
@@ -308,6 +336,24 @@ function argoDetail(argo: Record<string, unknown>): string {
?? shortSha(stringOrNull(argo.revision));
}
function whenSummary(value: Record<string, unknown> | undefined): string {
if (value === undefined) return "-";
const values = arrayTextItems(value.values).join(",");
return `${stringOrNull(value.input) ?? "-"} ${stringOrNull(value.operator) ?? "-"} ${values || "-"}`;
}
function applyMetadataSummary(value: Record<string, unknown>): string {
const annotations = asOptionalRecord(value.annotations);
const labels = asOptionalRecord(value.labels);
return `ann:${firstEntry(annotations)} label:${firstEntry(labels)}`;
}
function firstEntry(value: Record<string, unknown> | null): string {
if (value === null) return "-";
const [key, item] = Object.entries(value)[0] ?? [];
return key === undefined ? "-" : `${key}=${stringOrNull(item) ?? "-"}`;
}
function asOptionalRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
+71 -2
View File
@@ -9,10 +9,12 @@ export function compactRefreshEvidence(value: Record<string, unknown> | null): R
jobName: stringOrNull(value.jobName) ?? stringOrNull(summary.jobName),
namespace: stringOrNull(value.namespace) ?? stringOrNull(summary.namespace),
status: stringOrNull(summary.status),
pipeline: stringOrNull(summary.pipeline),
pipeline: stringOrNull(summary.pipeline) ?? stringOrNull(asOptionalRecord(summary.apply)?.pipelineName) ?? stringOrNull(asOptionalRecord(summary.render)?.pipelineName),
sourceCommit: stringOrNull(summary.sourceCommit),
sourceStageRef: stringOrNull(summary.sourceStageRef),
elapsedMs: numberOrNull(summary.elapsedMs),
render: compactRefreshRender(asOptionalRecord(summary.render)),
apply: compactRefreshApply(asOptionalRecord(summary.apply)),
sourceAuthority: stringOrNull(summary.sourceAuthority),
statusAuthority: stringOrNull(summary.statusAuthority),
parsedDownstreamCliOutput: false,
@@ -34,7 +36,10 @@ export function followerEvidenceSummary(input: {
if (tekton === null && pipeline === null && refresh === null) return null;
const pipelineRefName = stringOrNull(tekton?.pipelineRefName);
const pipelineName = stringOrNull(asOptionalRecord(pipeline?.metadata)?.name);
const refreshPipeline = stringOrNull(refresh?.pipeline);
const refreshRender = asOptionalRecord(refresh?.render);
const refreshApply = asOptionalRecord(refresh?.apply);
const refreshRenderedPipeline = stringOrNull(refreshRender?.pipelineName);
const refreshPipeline = stringOrNull(refreshApply?.pipelineName) ?? stringOrNull(refresh?.pipeline);
const refreshSourceCommit = stringOrNull(refresh?.sourceCommit);
return {
pipelineRunRefName: pipelineRefName,
@@ -46,11 +51,67 @@ export function followerEvidenceSummary(input: {
...refresh,
pipelineRefMatches: pipelineRefName === null || refreshPipeline === null ? null : pipelineRefName === refreshPipeline,
pipelineSpecMatches: pipelineName === null || refreshPipeline === null ? null : pipelineName === refreshPipeline,
renderedPipelineMatchesApplied: refreshRenderedPipeline === null || refreshPipeline === null ? null : refreshRenderedPipeline === refreshPipeline,
sourceCommitMatches: input.observedSha === null || refreshSourceCommit === null ? null : input.observedSha === refreshSourceCommit,
},
};
}
function compactRefreshRender(value: Record<string, unknown> | null): Record<string, unknown> | null {
if (value === null) return null;
return {
pipelineName: stringOrNull(value.pipelineName),
taskCount: numberOrNull(value.taskCount),
runtimeReadyTask: compactRefreshRuntimeReady(asOptionalRecord(value.runtimeReadyTask)),
};
}
function compactRefreshApply(value: Record<string, unknown> | null): Record<string, unknown> | null {
if (value === null) return null;
return {
pipelineName: stringOrNull(value.pipelineName),
namespace: stringOrNull(value.namespace),
resourceVersion: stringOrNull(value.resourceVersion),
annotations: compactStringMap(asOptionalRecord(value.annotations)),
labels: compactStringMap(asOptionalRecord(value.labels)),
degradedReason: stringOrNull(value.degradedReason),
};
}
function compactRefreshRuntimeReady(value: Record<string, unknown> | null): Record<string, unknown> | null {
if (value === null) return null;
return {
present: booleanOrNull(value.present),
name: stringOrNull(value.name),
runAfter: compactStringArray(value.runAfter, 4),
when: compactWhenList(value.when, 4),
};
}
function compactWhenList(value: unknown, limit: number): Array<Record<string, unknown>> {
return Array.isArray(value)
? value
.map((item) => asOptionalRecord(item))
.filter((item): item is Record<string, unknown> => item !== null)
.slice(0, limit)
.map((item) => ({
input: stringOrNull(item.input),
operator: stringOrNull(item.operator),
values: compactStringArray(item.values, 4),
}))
: [];
}
function compactStringMap(value: Record<string, unknown> | null): Record<string, string> | null {
if (value === null) return null;
const output: Record<string, string> = {};
for (const [key, item] of Object.entries(value).slice(0, 8)) {
const text = stringOrNull(item);
if (text !== null) output[key] = text;
}
return Object.keys(output).length === 0 ? null : output;
}
function firstRecord(...values: Array<Record<string, unknown> | null>): Record<string, unknown> | null {
for (const value of values) {
if (value !== null) return value;
@@ -69,3 +130,11 @@ function stringOrNull(value: unknown): string | null {
function numberOrNull(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function booleanOrNull(value: unknown): boolean | null {
return value === true ? true : value === false ? false : null;
}
function compactStringArray(value: unknown, limit: number): string[] {
return Array.isArray(value) ? value.map((item) => stringOrNull(item)).filter((item): item is string => item !== null).slice(0, limit) : [];
}
+40
View File
@@ -264,6 +264,27 @@ function evidenceRowsForFollower(item: Record<string, unknown>): unknown[][] {
: `${shortSha(stringOrNull(refresh.sourceCommit))}/${boolMatch(refresh.pipelineRefMatches)}/${boolMatch(refresh.pipelineSpecMatches)}`,
refresh === null ? "-" : stringOrNull(refresh.pipeline) ?? "-",
]);
const refreshRender = asOptionalRecord(refresh?.render);
const refreshRenderRuntimeReady = asOptionalRecord(refreshRender?.runtimeReadyTask);
if (refreshRender !== null) {
rows.push([
item.id,
"refresh-render",
refreshRenderRuntimeReady?.present === true ? "runtime-ready-present" : refreshRenderRuntimeReady?.present === false ? "runtime-ready-absent" : "-",
whenSummary(arrayRecords(refreshRenderRuntimeReady?.when)[0]),
stringOrNull(refreshRender.pipelineName) ?? "-",
]);
}
const refreshApply = asOptionalRecord(refresh?.apply);
if (refreshApply !== null) {
rows.push([
item.id,
"refresh-apply",
stringOrNull(refreshApply.resourceVersion) ?? stringOrNull(refreshApply.degradedReason) ?? "-",
applyMetadataSummary(refreshApply),
stringOrNull(refreshApply.pipelineName) ?? "-",
]);
}
return rows;
}
@@ -313,6 +334,25 @@ function boolMatch(value: unknown): string {
return value === true ? "match" : value === false ? "mismatch" : "-";
}
function whenSummary(value: Record<string, unknown> | undefined): string {
if (value === undefined) return "-";
const values = arrayText(value.values);
return `${stringOrNull(value.input) ?? "-"} ${stringOrNull(value.operator) ?? "-"} ${values || "-"}`;
}
function applyMetadataSummary(value: Record<string, unknown>): string {
const annotations = asOptionalRecord(value.annotations);
const labels = asOptionalRecord(value.labels);
const annotation = annotations === null ? "-" : `${firstEntry(annotations)}`;
const label = labels === null ? "-" : `${firstEntry(labels)}`;
return `ann:${annotation} label:${label}`;
}
function firstEntry(value: Record<string, unknown>): string {
const [key, item] = Object.entries(value)[0] ?? [];
return key === undefined ? "-" : `${key}=${stringOrNull(item) ?? "-"}`;
}
function asOptionalRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}