fix: 修复 PaC reused delivery 终态证据
从目标 TaskRun condition/result 与结构化合同日志恢复 artifact catalog、digest 和 GitOps 证据,并为真实 PipelineRun 增加有界 debug-step。严格保留缺 artifact、缺 GitOps、runtime mismatch 与冲突证据的 fail-closed closeout。
This commit is contained in:
@@ -344,7 +344,7 @@ history_rows() {
|
||||
node <<'NODE'
|
||||
const fs = require('node:fs');
|
||||
const cp = require('node:child_process');
|
||||
const { extractPacSourceObservation } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
|
||||
const { extractPacArtifactEvidence, parsePacLogRecords, taskTerminalRecord } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
|
||||
const limit = Math.max(1, Math.min(50, Number.parseInt(process.env.UNIDESK_PAC_HISTORY_LIMIT || '5', 10) || 5));
|
||||
const detailId = process.env.UNIDESK_PAC_HISTORY_ID || '';
|
||||
const consumers = JSON.parse(Buffer.from(process.env.UNIDESK_PAC_HISTORY_CONSUMERS_B64 || 'W10=', 'base64').toString('utf8'));
|
||||
@@ -497,18 +497,41 @@ function recursiveEnvReuse(value, state) {
|
||||
for (const item of Object.values(value)) recursiveEnvReuse(item, state);
|
||||
}
|
||||
|
||||
function envReuseForPipelineRun(namespace, name) {
|
||||
const state = { status: null, buildSkippedCount: null, serviceReusedCount: null, buildCache: null, source: 'logs', sourceObservation: null };
|
||||
function envReuseForPipelineRun(namespace, name, taskRuns) {
|
||||
const state = { status: null, buildSkippedCount: null, serviceReusedCount: null, buildCache: null, source: 'pipeline-task-logs', sourceObservation: null, artifact: null, collector: null };
|
||||
const matching = (taskRuns.items || []).filter((item) => item.metadata?.labels?.['tekton.dev/pipelineRun'] === name);
|
||||
const contractTaskRuns = matching.filter((item) => taskTerminalRecord(item) !== null);
|
||||
const terminalRecords = contractTaskRuns.map(taskTerminalRecord).filter(Boolean);
|
||||
const logErrors = [];
|
||||
let logs = '';
|
||||
try {
|
||||
logs = cp.execFileSync('kubectl', ['-n', namespace, 'logs', '-l', `tekton.dev/pipelineRun=${name}`, '--all-containers', '--tail=320'], {
|
||||
for (const item of contractTaskRuns) {
|
||||
const podName = item.status?.podName;
|
||||
if (!podName) {
|
||||
logErrors.push({ taskRun: item.metadata?.name || null, reason: 'pod-name-missing' });
|
||||
continue;
|
||||
}
|
||||
const result = cp.spawnSync('kubectl', ['-n', namespace, 'logs', podName, '--all-containers=true'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
timeout: 12000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
maxBuffer: 4 * 1024 * 1024,
|
||||
});
|
||||
} catch {
|
||||
return { ...state, source: 'logs-unavailable' };
|
||||
if (result.error || result.status !== 0) {
|
||||
logErrors.push({ taskRun: item.metadata?.name || null, reason: result.error ? String(result.error.message || result.error) : String(result.stderr || '').slice(0, 240) });
|
||||
continue;
|
||||
}
|
||||
logs += `${result.stdout || ''}\n`;
|
||||
}
|
||||
if (contractTaskRuns.length === 0) {
|
||||
state.source = 'pipeline-run-log-fallback';
|
||||
const result = cp.spawnSync('kubectl', ['-n', namespace, 'logs', '-l', `tekton.dev/pipelineRun=${name}`, '--all-containers=true', '--tail=320'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 12000,
|
||||
maxBuffer: 2 * 1024 * 1024,
|
||||
});
|
||||
if (result.error || result.status !== 0) {
|
||||
return { ...state, source: 'logs-unavailable', collector: { mode: state.source, contractTaskCount: 0, terminalRecordCount: 0, logErrorCount: 1, valuesPrinted: false } };
|
||||
}
|
||||
logs = result.stdout || '';
|
||||
}
|
||||
const lines = logs.split(/\r?\n/u);
|
||||
const headerIndex = lines.findIndex((line) => /^ENV_REUSE\s+NODE_DEPS\s+/u.test(line.trim()));
|
||||
@@ -519,17 +542,18 @@ function envReuseForPipelineRun(namespace, name) {
|
||||
state.buildCache = row[4];
|
||||
}
|
||||
}
|
||||
const records = [];
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue;
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
records.push(parsed);
|
||||
recursiveEnvReuse(parsed, state);
|
||||
} catch {}
|
||||
}
|
||||
state.sourceObservation = extractPacSourceObservation(records);
|
||||
const records = [...parsePacLogRecords(logs), ...terminalRecords];
|
||||
for (const parsed of records) recursiveEnvReuse(parsed, state);
|
||||
state.artifact = extractPacArtifactEvidence(records, logs);
|
||||
state.sourceObservation = state.artifact.sourceObservation;
|
||||
state.collector = {
|
||||
mode: state.source,
|
||||
contractTaskCount: contractTaskRuns.length,
|
||||
terminalRecordCount: terminalRecords.length,
|
||||
logErrorCount: logErrors.length,
|
||||
logErrors,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -583,7 +607,7 @@ function rowFor(consumer, item, taskRuns) {
|
||||
const labels = item.metadata?.labels || {};
|
||||
const annotations = item.metadata?.annotations || {};
|
||||
const sourceCommit = extractCommit(item, params);
|
||||
const evidence = envReuseForPipelineRun(consumer.namespace, item.metadata.name);
|
||||
const evidence = envReuseForPipelineRun(consumer.namespace, item.metadata.name, taskRuns);
|
||||
const sourceObservation = evidence.sourceObservation === null ? null : {
|
||||
...evidence.sourceObservation,
|
||||
sourceMatched: sourceCommit !== null && evidence.sourceObservation.sourceCommit === sourceCommit,
|
||||
@@ -618,6 +642,8 @@ function rowFor(consumer, item, taskRuns) {
|
||||
source: evidence.source,
|
||||
},
|
||||
sourceObservation,
|
||||
artifact: evidence.artifact,
|
||||
collector: evidence.collector,
|
||||
taskRuns: tasks,
|
||||
source: 'gitea-pac-tekton-live',
|
||||
valuesPrinted: false,
|
||||
@@ -703,98 +729,61 @@ artifact_summary() {
|
||||
printf '{}'
|
||||
return
|
||||
fi
|
||||
log_file=$(mktemp)
|
||||
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" logs -l "tekton.dev/pipelineRun=$UNIDESK_PAC_TARGET_PIPELINERUN" --all-containers --tail=240 >"$log_file" 2>/dev/null || true
|
||||
node - "$log_file" <<'NODE'
|
||||
task_file=$(mktemp)
|
||||
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get taskrun -l "tekton.dev/pipelineRun=$UNIDESK_PAC_TARGET_PIPELINERUN" -o json >"$task_file" 2>/dev/null || printf '{"items":[]}' >"$task_file"
|
||||
node - "$task_file" <<'NODE'
|
||||
const fs = require('node:fs');
|
||||
const { extractPacSourceObservation } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
|
||||
const lines = fs.readFileSync(process.argv[2], 'utf8').split(/\r?\n/);
|
||||
const records = [];
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue;
|
||||
try { records.push(JSON.parse(trimmed)); } catch {}
|
||||
}
|
||||
const sourceObservation = extractPacSourceObservation(records);
|
||||
const publish = [...records].reverse().find((item) => item.phase === 'gitops-publish' || item.gitopsCommit);
|
||||
const image = publish || [...records].reverse().find((item) => item.imageStatus || item.status === 'reused' || item.status === 'built');
|
||||
const gitopsCommit = image?.gitopsCommit
|
||||
|| [...lines].reverse().map((line) => line.match(/^\[[^\]]+\s+([0-9a-f]{7,64})\]/u)?.[1] || null).find(Boolean)
|
||||
|| null;
|
||||
const loggedDigests = [...new Set(lines.flatMap((line) => [...line.matchAll(/"(?:digest|environmentDigest)"\s*:\s*"(sha256:[0-9a-f]{64})"/gu)].map((match) => match[1])))];
|
||||
const envHeaderIndex = lines.findIndex((line) => /^ENV_REUSE\s+NODE_DEPS\s+/u.test(line.trim()));
|
||||
let humanEnv = null;
|
||||
if (envHeaderIndex >= 0) {
|
||||
const row = (lines.slice(envHeaderIndex + 1).find((line) => line.trim()) || '').trim().split(/\s+/u);
|
||||
if (row.length >= 5) {
|
||||
humanEnv = { envReuse: row[0], nodeDeps: row[1], buildPackage: row[2], buildNetwork: row[3], cache: row[4] };
|
||||
const cp = require('node:child_process');
|
||||
const { extractPacArtifactEvidence, parsePacLogRecords, taskTerminalRecord } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
|
||||
const input = JSON.parse(fs.readFileSync(process.argv[2], 'utf8') || '{"items":[]}');
|
||||
const namespace = process.env.UNIDESK_PAC_TARGET_NAMESPACE;
|
||||
const pipelineRun = process.env.UNIDESK_PAC_TARGET_PIPELINERUN;
|
||||
const taskRuns = (input.items || []).filter((item) => item.metadata?.labels?.['tekton.dev/pipelineRun'] === pipelineRun);
|
||||
const contractTaskRuns = taskRuns.filter((item) => taskTerminalRecord(item) !== null);
|
||||
const terminalRecords = contractTaskRuns.map(taskTerminalRecord).filter(Boolean);
|
||||
const logErrors = [];
|
||||
let logText = '';
|
||||
for (const item of contractTaskRuns) {
|
||||
const podName = item.status?.podName;
|
||||
if (!podName) {
|
||||
logErrors.push({ taskRun: item.metadata?.name || null, reason: 'pod-name-missing' });
|
||||
continue;
|
||||
}
|
||||
const result = cp.spawnSync('kubectl', ['-n', namespace, 'logs', podName, '--all-containers=true'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 12000,
|
||||
maxBuffer: 4 * 1024 * 1024,
|
||||
});
|
||||
if (result.error || result.status !== 0) {
|
||||
logErrors.push({ taskRun: item.metadata?.name || null, reason: result.error ? String(result.error.message || result.error) : String(result.stderr || '').slice(0, 240) });
|
||||
continue;
|
||||
}
|
||||
logText += `${result.stdout || ''}\n`;
|
||||
}
|
||||
function digestOf(item) {
|
||||
if (item.digest) return item.digest;
|
||||
const ref = item.digestRef || '';
|
||||
const index = String(ref).indexOf('@');
|
||||
return index >= 0 ? String(ref).slice(index + 1) : null;
|
||||
}
|
||||
function envReuseOf(item) {
|
||||
const env = item.envReuse || {};
|
||||
return env.dependencyReuse || env.mode || item.envReuseStatus || null;
|
||||
}
|
||||
let out = { valuesPrinted: false };
|
||||
if (image) {
|
||||
out = {
|
||||
imageStatus: image.imageStatus || image.status || (image.digestRef ? 'built' : null),
|
||||
envIdentity: image.envIdentity || null,
|
||||
envReuse: envReuseOf(image),
|
||||
nodeDepsReuse: null,
|
||||
buildCache: null,
|
||||
digest: digestOf(image) || (loggedDigests.length === 1 ? loggedDigests[0] : null),
|
||||
digests: loggedDigests,
|
||||
gitopsCommit,
|
||||
sourceCommit: image.sourceCommit || null,
|
||||
runtimeSourceCommit: image.runtimeSourceCommit || null,
|
||||
baselineSourceCommit: image.baselineSourceCommit || null,
|
||||
action: image.action || null,
|
||||
reason: image.reason || null,
|
||||
sourceObservation,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
} else if (sourceObservation) {
|
||||
out = {
|
||||
imageStatus: sourceObservation.mode === 'no-runtime-change' && sourceObservation.valid === true ? 'retained' : null,
|
||||
envIdentity: null,
|
||||
envReuse: null,
|
||||
nodeDepsReuse: null,
|
||||
buildCache: null,
|
||||
digest: null,
|
||||
digests: [],
|
||||
gitopsCommit: null,
|
||||
sourceCommit: sourceObservation.sourceCommit || null,
|
||||
runtimeSourceCommit: null,
|
||||
action: sourceObservation.mode,
|
||||
reason: sourceObservation.reason,
|
||||
sourceObservation,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
} else if (humanEnv || gitopsCommit || loggedDigests.length > 0) {
|
||||
out = {
|
||||
imageStatus: humanEnv ? 'published' : null,
|
||||
envIdentity: null,
|
||||
envReuse: humanEnv?.envReuse || null,
|
||||
nodeDepsReuse: humanEnv?.nodeDeps || null,
|
||||
buildCache: humanEnv?.cache || null,
|
||||
buildPackage: humanEnv?.buildPackage || null,
|
||||
buildNetwork: humanEnv?.buildNetwork || null,
|
||||
digest: loggedDigests.length === 1 ? loggedDigests[0] : null,
|
||||
digests: loggedDigests,
|
||||
gitopsCommit,
|
||||
sourceCommit: null,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
let mode = 'pipeline-task-logs';
|
||||
if (contractTaskRuns.length === 0) {
|
||||
mode = 'pipeline-run-log-fallback';
|
||||
const fallback = cp.spawnSync('kubectl', ['-n', namespace, 'logs', '-l', `tekton.dev/pipelineRun=${pipelineRun}`, '--all-containers=true', '--tail=320'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 12000,
|
||||
maxBuffer: 2 * 1024 * 1024,
|
||||
});
|
||||
if (!fallback.error && fallback.status === 0) logText = fallback.stdout || '';
|
||||
else logErrors.push({ taskRun: null, reason: fallback.error ? String(fallback.error.message || fallback.error) : String(fallback.stderr || '').slice(0, 240) });
|
||||
}
|
||||
const records = [...parsePacLogRecords(logText), ...terminalRecords];
|
||||
const out = extractPacArtifactEvidence(records, logText);
|
||||
out.collector = {
|
||||
mode,
|
||||
contractTaskCount: contractTaskRuns.length,
|
||||
terminalRecordCount: terminalRecords.length,
|
||||
logErrorCount: logErrors.length,
|
||||
logErrors,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
NODE
|
||||
rm -f "$log_file"
|
||||
rm -f "$task_file"
|
||||
}
|
||||
|
||||
runtime_summary() {
|
||||
@@ -1206,11 +1195,124 @@ history_action() {
|
||||
}
|
||||
|
||||
debug_step_action() {
|
||||
node <<'NODE'
|
||||
fixtures=$(node <<'NODE'
|
||||
const { runPacStatusFixtureChecks } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
|
||||
const result = runPacStatusFixtureChecks();
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
if (!result.ok) process.exitCode = 1;
|
||||
process.stdout.write(JSON.stringify(result));
|
||||
NODE
|
||||
)
|
||||
if [ -z "$UNIDESK_PAC_HISTORY_ID" ]; then
|
||||
printf '%s\n' "$fixtures"
|
||||
FIXTURES_JSON="$fixtures" node -e 'const result=JSON.parse(process.env.FIXTURES_JSON||"{}"); if(!result.ok) process.exitCode=1'
|
||||
return
|
||||
fi
|
||||
history=$(history_rows)
|
||||
UNIDESK_PAC_DEBUG_FIXTURES_JSON="$fixtures" UNIDESK_PAC_DEBUG_HISTORY_JSON="$history" node <<'NODE'
|
||||
function record(value) {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
||||
}
|
||||
function terminalRow(sourceObservation, key, step) {
|
||||
const source = record(record(sourceObservation.terminalSources)[key]);
|
||||
return {
|
||||
step,
|
||||
present: source.present === true,
|
||||
status: source.status || 'missing',
|
||||
source: source.source || null,
|
||||
taskRun: source.taskRun || null,
|
||||
reason: source.reason || null,
|
||||
results: Array.isArray(source.results) ? source.results : [],
|
||||
markerPresent: source.markerPresent === true,
|
||||
markerSource: source.markerSource || null,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const fixtures = JSON.parse(process.env.UNIDESK_PAC_DEBUG_FIXTURES_JSON || '{}');
|
||||
const history = JSON.parse(process.env.UNIDESK_PAC_DEBUG_HISTORY_JSON || '{}');
|
||||
const rows = Array.isArray(history.rows) ? history.rows : [];
|
||||
const historyErrors = Array.isArray(history.errors) ? history.errors : [];
|
||||
const row = record(rows[0]);
|
||||
const sourceObservation = record(row.sourceObservation);
|
||||
const artifact = record(row.artifact);
|
||||
const catalog = record(artifact.catalog);
|
||||
const collector = record(row.collector);
|
||||
const terminal = [
|
||||
terminalRow(sourceObservation, 'planArtifacts', 'plan-artifacts'),
|
||||
terminalRow(sourceObservation, 'collectArtifacts', 'collect-artifacts'),
|
||||
terminalRow(sourceObservation, 'gitopsPromote', 'gitops-promote'),
|
||||
];
|
||||
let firstBreak = null;
|
||||
function breakAt(code, stage, reason) {
|
||||
if (firstBreak === null) firstBreak = { code, stage, reason, valuesPrinted: false };
|
||||
}
|
||||
if (historyErrors.length > 0) breakAt('target-read-failed', 'target-read', historyErrors[0]?.error || historyErrors[0]?.stderr || 'target-side history read failed');
|
||||
if (Object.keys(row).length === 0) breakAt('pipeline-run-not-found', 'pipeline-run', `PipelineRun ${process.env.UNIDESK_PAC_HISTORY_ID || '-'} was not found for the selected consumer`);
|
||||
if (Object.keys(row).length > 0 && row.status !== 'True') breakAt('pipeline-run-not-succeeded', 'pipeline-run', `PipelineRun terminal status is ${row.status || row.reason || 'unknown'}`);
|
||||
if (Object.keys(row).length > 0 && Object.keys(sourceObservation).length === 0) breakAt('source-observation-missing', 'plan-artifacts', 'g14-ci-plan structured evidence is missing');
|
||||
if (sourceObservation.sourceMatched !== true) breakAt('source-observation-mismatch', 'plan-artifacts', 'g14-ci-plan source commit does not match the selected PipelineRun');
|
||||
if (sourceObservation.valid !== true) breakAt('source-observation-invalid', 'plan-artifacts', sourceObservation.reason || 'delivery plan is incomplete or contradictory');
|
||||
for (const item of terminal) {
|
||||
if (item.present !== true || item.status !== 'succeeded') breakAt('terminal-evidence-incomplete', item.step, `${item.step} TaskRun terminal status is ${item.status}`);
|
||||
}
|
||||
if (Number(collector.logErrorCount || 0) > 0) breakAt('contract-log-read-failed', 'collector', 'one or more contract TaskRun logs could not be read');
|
||||
if (sourceObservation.mode === 'delivery') {
|
||||
if (catalog.present !== true || catalog.status !== 'published') breakAt('artifact-catalog-missing', 'collect-artifacts', 'published artifact catalog evidence is missing');
|
||||
if (catalog.registryVerified !== true) breakAt('artifact-catalog-unverified', 'collect-artifacts', 'artifact catalog registry verification is not true');
|
||||
if (!Array.isArray(artifact.digests) || artifact.digests.length === 0) breakAt('artifact-digest-missing', 'collect-artifacts', 'artifact catalog did not expose any sha256 digest');
|
||||
if (typeof artifact.gitopsCommit !== 'string' || artifact.gitopsCommit.length === 0) breakAt('gitops-commit-missing', 'gitops-promote', 'GitOps commit is not visible in the successful promotion TaskRun');
|
||||
}
|
||||
const realRun = {
|
||||
ok: firstBreak === null,
|
||||
found: Object.keys(row).length > 0,
|
||||
pipelineRun: Object.keys(row).length === 0 ? null : {
|
||||
id: row.id || row.pipelineRun || null,
|
||||
status: row.status || null,
|
||||
reason: row.reason || null,
|
||||
sourceCommit: row.commit || null,
|
||||
sourceMatched: sourceObservation.sourceMatched === true,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
mode: sourceObservation.mode || null,
|
||||
valid: sourceObservation.valid === true,
|
||||
terminal,
|
||||
artifact: {
|
||||
imageStatus: artifact.imageStatus || null,
|
||||
envReuse: artifact.envReuse || null,
|
||||
digests: Array.isArray(artifact.digests) ? artifact.digests : [],
|
||||
gitopsCommit: artifact.gitopsCommit || null,
|
||||
sourceCommit: artifact.sourceCommit || null,
|
||||
catalog: {
|
||||
present: catalog.present === true,
|
||||
status: catalog.status || null,
|
||||
sourceCommit: catalog.sourceCommit || null,
|
||||
registryVerified: catalog.registryVerified === true,
|
||||
publishedCount: catalog.publishedCount ?? null,
|
||||
reusedCount: catalog.reusedCount ?? null,
|
||||
requiredServiceCount: catalog.requiredServiceCount ?? null,
|
||||
digestCount: catalog.digestCount ?? null,
|
||||
source: catalog.source || null,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
},
|
||||
collector: {
|
||||
mode: collector.mode || null,
|
||||
contractTaskCount: collector.contractTaskCount ?? null,
|
||||
terminalRecordCount: collector.terminalRecordCount ?? null,
|
||||
logErrorCount: collector.logErrorCount ?? null,
|
||||
logErrors: Array.isArray(collector.logErrors) ? collector.logErrors : [],
|
||||
valuesPrinted: false,
|
||||
},
|
||||
firstBreak,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
const out = {
|
||||
ok: fixtures.ok === true && realRun.ok,
|
||||
checks: Array.isArray(fixtures.checks) ? fixtures.checks : [],
|
||||
realRun,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
process.stdout.write(`${JSON.stringify(out)}\n`);
|
||||
if (!out.ok) process.exitCode = 1;
|
||||
NODE
|
||||
}
|
||||
|
||||
|
||||
@@ -204,10 +204,10 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
|
||||
if (action === "history" || action === "runs") {
|
||||
const options = parseHistoryOptions(args.slice(1));
|
||||
const result = await history(config, options);
|
||||
return options.full || options.raw ? result : options.json ? compactHistoryJson(result) : renderHistory(result);
|
||||
return options.raw ? result : options.full ? compactHistoryJson(result, true) : options.json ? compactHistoryJson(result) : renderHistory(result);
|
||||
}
|
||||
if (action === "debug-step") {
|
||||
const options = parseCommonOptions(args.slice(1));
|
||||
const options = parseHistoryOptions(args.slice(1));
|
||||
const result = await debugStep(config, options);
|
||||
return options.full || options.raw ? result : options.json ? result : renderDebugStep(result);
|
||||
}
|
||||
@@ -231,7 +231,7 @@ function help(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code closeout --target JD01 --consumer <consumer> --source-commit <sha> --wait",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 [--consumer hwlab-jd01-v03] [--limit 10]",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --id <pipelinerun>",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 [--json]",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 [--consumer <consumer>] [--id <pipelinerun>] [--json]",
|
||||
],
|
||||
diagnostics: "webhook-test exists only for bounded connectivity diagnosis and must not be used as delivery evidence.",
|
||||
boundary: "Sole CI trigger path for GH-1552/GH-1607: GitHub PR merge -> GitHub webhook bridge -> Gitea mirror/snapshot -> Pipelines-as-Code -> Tekton -> GitOps/Argo/k8s runtime.",
|
||||
@@ -722,7 +722,7 @@ async function history(config: UniDeskConfig, options: HistoryOptions): Promise<
|
||||
};
|
||||
}
|
||||
|
||||
async function debugStep(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
|
||||
async function debugStep(config: UniDeskConfig, options: HistoryOptions): Promise<Record<string, unknown>> {
|
||||
const pac = readPacConfig();
|
||||
const target = resolveTarget(pac, options.targetId);
|
||||
const consumer = resolveConsumer(pac, options.consumerId);
|
||||
@@ -735,8 +735,10 @@ async function debugStep(config: UniDeskConfig, options: CommonOptions): Promise
|
||||
mutation: false,
|
||||
target: targetSummary(target),
|
||||
consumer: consumer.id,
|
||||
detailId: options.detailId,
|
||||
evaluator: "scripts/native/cicd/pac-status-evaluator.cjs",
|
||||
checks: parsed === null ? [] : arrayRecords(parsed.checks),
|
||||
realRun: parsed === null ? null : parsed.realRun ?? null,
|
||||
remote: parsed === null || options.raw ? parsed ?? compactCapture(result, { full: true }) : undefined,
|
||||
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
|
||||
valuesPrinted: false,
|
||||
@@ -1340,7 +1342,8 @@ function compactCloseoutJson(result: Record<string, unknown>): Record<string, un
|
||||
};
|
||||
}
|
||||
|
||||
function compactHistoryJson(result: Record<string, unknown>): Record<string, unknown> {
|
||||
function compactHistoryJson(result: Record<string, unknown>, includeTaskDetails = false): Record<string, unknown> {
|
||||
const detailRequested = stringValue(result.detailId) !== "-";
|
||||
return {
|
||||
ok: result.ok === true,
|
||||
action: result.action,
|
||||
@@ -1351,6 +1354,9 @@ function compactHistoryJson(result: Record<string, unknown>): Record<string, unk
|
||||
detailId: result.detailId,
|
||||
rows: arrayRecords(result.rows).map((row) => {
|
||||
const taskRuns = record(row.taskRuns);
|
||||
const artifact = record(row.artifact);
|
||||
const catalog = record(artifact.catalog);
|
||||
const collector = record(row.collector);
|
||||
return {
|
||||
id: row.id ?? row.pipelineRun,
|
||||
consumer: row.consumer,
|
||||
@@ -1368,10 +1374,42 @@ function compactHistoryJson(result: Record<string, unknown>): Record<string, unk
|
||||
branch: row.branch,
|
||||
envReuse: row.envReuse,
|
||||
sourceObservation: row.sourceObservation,
|
||||
artifact: detailRequested ? {
|
||||
imageStatus: artifact.imageStatus,
|
||||
envReuse: artifact.envReuse,
|
||||
digest: artifact.digest,
|
||||
digests: artifact.digests,
|
||||
gitopsCommit: artifact.gitopsCommit,
|
||||
sourceCommit: artifact.sourceCommit,
|
||||
action: artifact.action,
|
||||
reason: artifact.reason,
|
||||
catalog: {
|
||||
present: catalog.present === true,
|
||||
status: catalog.status,
|
||||
sourceCommit: catalog.sourceCommit,
|
||||
registryVerified: catalog.registryVerified === true,
|
||||
publishedCount: catalog.publishedCount,
|
||||
reusedCount: catalog.reusedCount,
|
||||
requiredServiceCount: catalog.requiredServiceCount,
|
||||
digestCount: catalog.digestCount,
|
||||
source: catalog.source,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
} : undefined,
|
||||
collector: detailRequested ? {
|
||||
mode: collector.mode,
|
||||
contractTaskCount: collector.contractTaskCount,
|
||||
terminalRecordCount: collector.terminalRecordCount,
|
||||
logErrorCount: collector.logErrorCount,
|
||||
logErrors: collector.logErrors,
|
||||
valuesPrinted: false,
|
||||
} : undefined,
|
||||
taskRuns: {
|
||||
count: taskRuns.count,
|
||||
longest: taskRuns.longest,
|
||||
failed: taskRuns.failed,
|
||||
details: includeTaskDetails ? taskRuns.details : undefined,
|
||||
logsCommand: taskRuns.logsCommand,
|
||||
},
|
||||
};
|
||||
@@ -1654,15 +1692,55 @@ function renderWebhookTest(result: Record<string, unknown>): RenderedCliResult {
|
||||
|
||||
function renderDebugStep(result: Record<string, unknown>): RenderedCliResult {
|
||||
const checks = arrayRecords(result.checks);
|
||||
const realRun = record(result.realRun);
|
||||
const pipelineRun = record(realRun.pipelineRun);
|
||||
const artifact = record(realRun.artifact);
|
||||
const catalog = record(artifact.catalog);
|
||||
const terminal = arrayRecords(realRun.terminal);
|
||||
const firstBreak = record(realRun.firstBreak);
|
||||
const fixturePassed = checks.filter((item) => item.ok === true).length;
|
||||
const lines = [
|
||||
"PLATFORM-INFRA PIPELINES-AS-CODE DEBUG STEP",
|
||||
...table(["TARGET", "CONSUMER", "OK", "CHECKS", "MUTATION"], [[
|
||||
...table(["TARGET", "CONSUMER", "PIPELINERUN", "OK", "FIXTURES", "MUTATION"], [[
|
||||
stringValue(record(result.target).id),
|
||||
stringValue(result.consumer),
|
||||
stringValue(result.detailId),
|
||||
boolText(result.ok),
|
||||
stringValue(checks.length),
|
||||
`${fixturePassed}/${checks.length}`,
|
||||
boolText(result.mutation),
|
||||
]]),
|
||||
...(Object.keys(realRun).length === 0 ? [] : [
|
||||
"",
|
||||
"REAL PIPELINERUN EVIDENCE",
|
||||
...table(["FOUND", "STATUS", "SOURCE", "MODE", "VALID", "FIRST_BREAK"], [[
|
||||
boolText(realRun.found),
|
||||
stringValue(pipelineRun.status ?? pipelineRun.reason),
|
||||
short(stringValue(pipelineRun.sourceCommit), 16),
|
||||
stringValue(realRun.mode),
|
||||
boolText(realRun.valid),
|
||||
stringValue(firstBreak.code),
|
||||
]]),
|
||||
"",
|
||||
"TERMINAL EVIDENCE",
|
||||
...(terminal.length === 0 ? ["-"] : table(["STEP", "PRESENT", "STATUS", "SOURCE", "MARKER", "TASKRUN", "RESULTS"], terminal.map((item) => [
|
||||
stringValue(item.step),
|
||||
boolText(item.present),
|
||||
stringValue(item.status),
|
||||
stringValue(item.source),
|
||||
item.markerPresent === true ? stringValue(item.markerSource) : "-",
|
||||
short(stringValue(item.taskRun), 52),
|
||||
compactResultNames(item.results),
|
||||
]))),
|
||||
"",
|
||||
"ARTIFACT / GITOPS",
|
||||
...table(["CATALOG", "VERIFIED", "REUSED", "DIGESTS", "GITOPS"], [[
|
||||
stringValue(catalog.status),
|
||||
boolText(catalog.registryVerified),
|
||||
stringValue(catalog.reusedCount),
|
||||
stringValue(catalog.digestCount),
|
||||
short(stringValue(artifact.gitopsCommit), 16),
|
||||
]]),
|
||||
]),
|
||||
"",
|
||||
"STATUS EVALUATOR FIXTURES",
|
||||
...(checks.length === 0 ? ["-"] : table(["CASE", "OK", "EXPECTED", "ACTUAL"], checks.map((item) => [
|
||||
@@ -1819,6 +1897,20 @@ function closeoutCiReady(value: Record<string, unknown>, sourceCommit: string |
|
||||
if (pipelineGate.ok !== true) return false;
|
||||
const observedSourceCommit = observedCloseoutSourceCommit(latest, diagnostics);
|
||||
if (sourceCommit !== null && observedSourceCommit !== sourceCommit) return false;
|
||||
const sourceObservation = record(summary.sourceObservation);
|
||||
if (Object.keys(sourceObservation).length > 0) {
|
||||
if (sourceObservation.valid !== true || stringValue(sourceObservation.sourceCommit) !== observedSourceCommit) return false;
|
||||
if (stringValue(sourceObservation.contract) === "hwlab-g14-ci-plan" && stringValue(sourceObservation.mode) === "delivery") {
|
||||
const deliveryArtifact = record(summary.artifact);
|
||||
const catalog = record(deliveryArtifact.catalog);
|
||||
const digests = Array.isArray(deliveryArtifact.digests) ? deliveryArtifact.digests : [];
|
||||
if (catalog.present !== true
|
||||
|| stringValue(catalog.status) !== "published"
|
||||
|| catalog.registryVerified !== true
|
||||
|| stringValue(catalog.sourceCommit) !== observedSourceCommit
|
||||
|| digests.length === 0) return false;
|
||||
}
|
||||
}
|
||||
if (stringValue(diagnostics.code) === "pac-ready-no-runtime-change") return true;
|
||||
const artifact = record(summary.artifact);
|
||||
if (stringValue(artifact.imageStatus) === "disabled") return true;
|
||||
@@ -2176,6 +2268,10 @@ function renderHistoryDetail(row: Record<string, unknown>): string[] {
|
||||
const envReuse = record(row.envReuse);
|
||||
const sourceObservation = record(row.sourceObservation);
|
||||
const deliveryPlan = record(sourceObservation.plan);
|
||||
const terminalSources = record(sourceObservation.terminalSources);
|
||||
const artifact = record(row.artifact);
|
||||
const catalog = record(artifact.catalog);
|
||||
const collector = record(row.collector);
|
||||
const taskRuns = record(row.taskRuns);
|
||||
const longest = record(taskRuns.longest);
|
||||
const failed = record(taskRuns.failed);
|
||||
@@ -2209,6 +2305,25 @@ function renderHistoryDetail(row: Record<string, unknown>): string[] {
|
||||
["reusedServices", stringValue(deliveryPlan.reusedServiceCount)],
|
||||
]),
|
||||
"",
|
||||
"TERMINAL EVIDENCE",
|
||||
...table(["STEP", "PRESENT", "STATUS", "SOURCE", "MARKER", "TASKRUN"], [
|
||||
["plan-artifacts", boolText(record(terminalSources.planArtifacts).present), stringValue(record(terminalSources.planArtifacts).status), stringValue(record(terminalSources.planArtifacts).source), record(terminalSources.planArtifacts).markerPresent === true ? stringValue(record(terminalSources.planArtifacts).markerSource) : "-", short(stringValue(record(terminalSources.planArtifacts).taskRun), 52)],
|
||||
["collect-artifacts", boolText(record(terminalSources.collectArtifacts).present), stringValue(record(terminalSources.collectArtifacts).status), stringValue(record(terminalSources.collectArtifacts).source), record(terminalSources.collectArtifacts).markerPresent === true ? stringValue(record(terminalSources.collectArtifacts).markerSource) : "-", short(stringValue(record(terminalSources.collectArtifacts).taskRun), 52)],
|
||||
["gitops-promote", boolText(record(terminalSources.gitopsPromote).present), stringValue(record(terminalSources.gitopsPromote).status), stringValue(record(terminalSources.gitopsPromote).source), record(terminalSources.gitopsPromote).markerPresent === true ? stringValue(record(terminalSources.gitopsPromote).markerSource) : "-", short(stringValue(record(terminalSources.gitopsPromote).taskRun), 52)],
|
||||
]),
|
||||
"",
|
||||
"ARTIFACT / COLLECTOR",
|
||||
...table(["CATALOG", "VERIFIED", "REUSED", "DIGESTS", "GITOPS", "TASKS", "TERMINALS", "LOG_ERRORS"], [[
|
||||
stringValue(catalog.status),
|
||||
boolText(catalog.registryVerified),
|
||||
stringValue(catalog.reusedCount),
|
||||
stringValue(catalog.digestCount),
|
||||
short(stringValue(artifact.gitopsCommit), 16),
|
||||
stringValue(collector.contractTaskCount),
|
||||
stringValue(collector.terminalRecordCount),
|
||||
stringValue(collector.logErrorCount),
|
||||
]]),
|
||||
"",
|
||||
"LOGS",
|
||||
` ${stringValue(taskRuns.logsCommand)}`,
|
||||
];
|
||||
@@ -2219,6 +2334,13 @@ function compactLine(value: string): string {
|
||||
return trimmed.length > 0 ? trimmed.slice(0, 120) : "-";
|
||||
}
|
||||
|
||||
function compactResultNames(value: unknown): string {
|
||||
if (!Array.isArray(value) || value.length === 0) return "-";
|
||||
const names = value.filter((item): item is string => typeof item === "string");
|
||||
if (names.length <= 2) return names.join(",");
|
||||
return `${names.length}:${names.slice(0, 2).join(",")},...`;
|
||||
}
|
||||
|
||||
function table(headers: string[], rows: string[][]): string[] {
|
||||
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length)));
|
||||
const format = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ");
|
||||
|
||||
@@ -471,7 +471,7 @@ export function platformInfraHelp(): unknown {
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 --consumer <consumer> [--json|--full|--raw]",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --consumer <consumer> --limit 10 [--json]",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --id <pipelinerun> [--json|--full]",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 --consumer <consumer> [--json]",
|
||||
"bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 --consumer <consumer> [--id <pipelinerun>] [--json]",
|
||||
],
|
||||
description: "Operate YAML-controlled platform-infra services such as Sub2API, LangBot, n8n, Web Terminal, WeChat archive workflows, OpenTelemetry tracing, the independent target-scoped secret plane, the target-scoped Kafka event bus, internal Gitea source authority, and Gitea + Pipelines-as-Code closeout for JD01 migrated consumers. Public services use PK01 Caddy/FRP or YAML-declared PK01 Caddy upstreams rather than Kubernetes Ingress, NodePort, or LoadBalancer.",
|
||||
target,
|
||||
|
||||
Reference in New Issue
Block a user