fix: trigger branch followers natively

This commit is contained in:
Codex
2026-07-03 09:17:19 +00:00
parent 291e61e638
commit 0e75edd2c4
2 changed files with 410 additions and 23 deletions
+23 -19
View File
@@ -296,8 +296,30 @@ export async function runYamlLaneGitMirrorJob(config: UniDeskConfig, spec: Agent
}
export function yamlLanePipelineRunCreateScript(spec: AgentRunLaneSpec, sourceCommit: string, pipelineRun: string): string {
const manifest = yamlLanePipelineRunManifest(spec, sourceCommit, pipelineRun);
const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
return [
"set -eu",
`namespace=${shQuote(spec.ci.namespace)}`,
`pipeline_run=${shQuote(pipelineRun)}`,
`manifest_b64=${shQuote(manifestB64)}`,
"tmp=$(mktemp)",
"trap 'rm -f \"$tmp\"' EXIT",
"printf '%s' \"$manifest_b64\" | base64 -d > \"$tmp\"",
"existing_status=$(kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" -o 'jsonpath={.status.conditions[0].status}' 2>/dev/null || true)",
"if [ \"$existing_status\" = False ]; then kubectl -n \"$namespace\" delete pipelinerun \"$pipeline_run\" --ignore-not-found=true --wait=false >/dev/null 2>&1 || true; fi",
"if kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" >/dev/null 2>&1; then created=false; else kubectl create -f \"$tmp\"; created=true; fi",
"status=$(kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" -o 'jsonpath={.status.conditions[0].status}' 2>/dev/null || true)",
"CREATED=\"$created\" STATUS=\"$status\" PIPELINE_RUN=\"$pipeline_run\" python3 - <<'PY'",
"import json, os",
"print(json.dumps({'ok': True, 'pipelineRun': os.environ.get('PIPELINE_RUN'), 'created': os.environ.get('CREATED') == 'true', 'status': os.environ.get('STATUS') or None, 'valuesPrinted': False}, ensure_ascii=False))",
"PY",
].join("\n");
}
export function yamlLanePipelineRunManifest(spec: AgentRunLaneSpec, sourceCommit: string, pipelineRun: string): Record<string, unknown> {
const sourceStageRef = spec.source.statusMode === "k3s-git-mirror" ? agentRunSourceSnapshotRef(spec, sourceCommit) : null;
const manifest = {
return {
apiVersion: "tekton.dev/v1",
kind: "PipelineRun",
metadata: {
@@ -337,24 +359,6 @@ export function yamlLanePipelineRunCreateScript(spec: AgentRunLaneSpec, sourceCo
],
},
};
const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
return [
"set -eu",
`namespace=${shQuote(spec.ci.namespace)}`,
`pipeline_run=${shQuote(pipelineRun)}`,
`manifest_b64=${shQuote(manifestB64)}`,
"tmp=$(mktemp)",
"trap 'rm -f \"$tmp\"' EXIT",
"printf '%s' \"$manifest_b64\" | base64 -d > \"$tmp\"",
"existing_status=$(kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" -o 'jsonpath={.status.conditions[0].status}' 2>/dev/null || true)",
"if [ \"$existing_status\" = False ]; then kubectl -n \"$namespace\" delete pipelinerun \"$pipeline_run\" --ignore-not-found=true --wait=false >/dev/null 2>&1 || true; fi",
"if kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" >/dev/null 2>&1; then created=false; else kubectl create -f \"$tmp\"; created=true; fi",
"status=$(kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" -o 'jsonpath={.status.conditions[0].status}' 2>/dev/null || true)",
"CREATED=\"$created\" STATUS=\"$status\" PIPELINE_RUN=\"$pipeline_run\" python3 - <<'PY'",
"import json, os",
"print(json.dumps({'ok': True, 'pipelineRun': os.environ.get('PIPELINE_RUN'), 'created': os.environ.get('CREATED') == 'true', 'status': os.environ.get('STATUS') or None, 'valuesPrinted': False}, ensure_ascii=False))",
"PY",
].join("\n");
}
export function createYamlLaneJobScript(namespace: string, jobName: string, manifest: Record<string, unknown>): string {
+387 -4
View File
@@ -8,6 +8,12 @@ import { runCommand, type CommandResult } from "./command";
import { startJob } from "./jobs";
import type { RenderedCliResult } from "./output";
import { hwlabRuntimeLaneSpecForNode } from "./hwlab-node-lanes";
import { agentRunImageArtifact, renderAgentRunGitopsFiles } from "./agentrun-manifests";
import { agentRunPipelineRunName, resolveAgentRunLaneTarget } from "./agentrun-lanes";
import { yamlLaneGitMirrorJobManifest, yamlLaneGitopsPublishJobManifest, yamlLaneGitopsPublishPayloadFromProbe, yamlLanePipelineRunManifest } from "./agentrun/secrets";
import { yamlLaneK3sBuildImageJobManifest } from "./agentrun/yaml-lane";
import { nodeRuntimePipelineRunName } from "./hwlab-node/cleanup";
import { nodeRuntimePipelineRunManifest } from "./hwlab-node/web-probe";
import { loadSentinelCicdState } from "./hwlab-node-web-sentinel-cicd";
import { sentinelPublishPipelineRunManifest } from "./hwlab-node-web-sentinel-cicd-jobs";
import { sentinelPipelineRunName } from "./hwlab-node-web-sentinel-cicd-shared";
@@ -252,6 +258,24 @@ interface NativeCloseoutWaitResult {
parsedDownstreamCliOutput: false;
}
interface NativeK8sJobResult {
ok: boolean;
completed: boolean;
failed: boolean;
timedOut: boolean;
created: boolean;
reused: boolean;
jobName: string;
namespace: string;
polls: number;
elapsedMs: number;
logsTail: string | null;
conditionReason: string | null;
conditionMessage: string | null;
statusAuthority: "kubernetes-api-serviceaccount";
parsedDownstreamCliOutput: false;
}
interface FollowerState {
id: string;
adapter: string;
@@ -1104,6 +1128,12 @@ async function decideAndMaybeTrigger(
async function executeTrigger(registry: BranchFollowerRegistry, follower: FollowerSpec, observedSha: string | null, options: ParsedOptions): Promise<TriggerResult> {
const spec = follower.commands.trigger;
const timeoutSeconds = options.timeoutSeconds ?? spec.timeoutSeconds;
if (follower.adapter === "hwlab-node-runtime" && options.controller) {
return await executeNativeHwlabNodeTrigger(registry, follower, observedSha, options, timeoutSeconds);
}
if (follower.adapter === "agentrun-yaml-lane" && options.controller) {
return await executeNativeAgentRunTrigger(registry, follower, observedSha, options, timeoutSeconds);
}
if (follower.adapter === "web-probe-sentinel-cicd" && options.controller) {
return await executeNativeSentinelTrigger(registry, follower, observedSha, options, timeoutSeconds);
}
@@ -1132,6 +1162,197 @@ async function executeTrigger(registry: BranchFollowerRegistry, follower: Follow
};
}
async function executeNativeHwlabNodeTrigger(registry: BranchFollowerRegistry, follower: FollowerSpec, observedSha: string | null, options: ParsedOptions, timeoutSeconds: number): Promise<TriggerResult> {
if (observedSha === null) {
return nativeTriggerError(follower, "native HWLAB trigger requires observed source sha", "observed-sha-missing");
}
if (follower.nativeStatus.tekton === null) {
return nativeTriggerError(follower, "native HWLAB trigger requires Tekton nativeStatus", "tekton-native-status-missing");
}
const spec = hwlabRuntimeLaneSpecForNode(follower.target.lane, follower.target.node);
const pipelineRun = nodeRuntimePipelineRunName(spec, observedSha);
const namespace = follower.nativeStatus.tekton.namespace;
const manifest = nodeRuntimePipelineRunManifest(spec, observedSha, pipelineRun);
const startedAt = Date.now();
const result = runNativeTektonPipelineRun(namespace, pipelineRun, manifest, options.wait, timeoutSeconds);
const payload = parseJsonObject(result.stdout) ?? {};
const pipelineRunCompleted = payload.completed === true;
const failed = payload.failed === true || result.exitCode !== 0;
const closeout = !failed && options.wait && pipelineRunCompleted
? await waitNativeFollowerCloseout(registry, follower, observedSha, options, remainingSeconds(startedAt, timeoutSeconds))
: null;
return nativeTektonTriggerResult({
follower,
observedSha,
namespace,
pipelineRun,
stageRef: `${follower.source.snapshotPrefix.replace(/\/+$/u, "")}/${observedSha}`,
wait: options.wait,
result,
payload,
closeout,
successMessage: `native HWLAB PipelineRun ${pipelineRun} succeeded and runtime reached ${shortSha(observedSha)}`,
});
}
async function executeNativeAgentRunTrigger(registry: BranchFollowerRegistry, follower: FollowerSpec, observedSha: string | null, options: ParsedOptions, timeoutSeconds: number): Promise<TriggerResult> {
if (observedSha === null) {
return nativeTriggerError(follower, "native AgentRun trigger requires observed source sha", "observed-sha-missing");
}
if (follower.nativeStatus.tekton === null) {
return nativeTriggerError(follower, "native AgentRun trigger requires Tekton nativeStatus", "tekton-native-status-missing");
}
const { configPath, spec } = resolveAgentRunLaneTarget({ node: follower.target.node, lane: follower.target.lane });
const startedAt = Date.now();
const stageRef = `${follower.source.snapshotPrefix.replace(/\/+$/u, "")}/${observedSha}`;
const buildJob = `agentrun-bf-build-${spec.nodeId.toLowerCase()}-${observedSha.slice(0, 12)}-${Date.now().toString(36)}`.slice(0, 63);
const build = runNativeK8sJob(spec.ci.namespace, buildJob, yamlLaneK3sBuildImageJobManifest(spec, observedSha, buildJob), Math.min(remainingSeconds(startedAt, timeoutSeconds), Math.max(60, spec.deployment.manager.imageBuild.timeoutSeconds)), "buildkit");
const buildPayload = yamlLaneGitopsPublishPayloadFromProbe({ logsTail: stringOrNull(build.logsTail) ?? "" });
const digest = stringOrNull(buildPayload.digest);
const envIdentity = stringOrNull(buildPayload.envIdentity);
if (!build.ok || digest === null || envIdentity === null) {
return nativeAgentRunStageFailure(follower, observedSha, "image-build", buildJob, build, buildPayload, "native AgentRun image build failed");
}
const image = agentRunImageArtifact(spec, { sourceCommit: observedSha, envIdentity, digest, status: stringOrNull(buildPayload.status) ?? "built" });
const renderedFiles = renderAgentRunGitopsFiles(spec, { sourceCommit: observedSha, image });
const publishJob = `agentrun-bf-gitops-${spec.nodeId.toLowerCase()}-${observedSha.slice(0, 12)}-${Date.now().toString(36)}`.slice(0, 63);
const publish = runNativeK8sJob(spec.gitMirror.namespace, publishJob, yamlLaneGitopsPublishJobManifest(spec, renderedFiles, publishJob), remainingSeconds(startedAt, timeoutSeconds), "publish");
const publishPayload = yamlLaneGitopsPublishPayloadFromProbe({ logsTail: stringOrNull(publish.logsTail) ?? "" });
if (!publish.ok || publishPayload.ok === false || stringOrNull(publishPayload.gitopsCommit) === null) {
return nativeAgentRunStageFailure(follower, observedSha, "gitops-publish", publishJob, publish, publishPayload, "native AgentRun GitOps publish failed");
}
const flushJob = `${spec.gitMirror.flushJobPrefix}-${Date.now().toString(36)}-bf`.slice(0, 63);
const flush = runNativeK8sJob(spec.gitMirror.namespace, flushJob, yamlLaneGitMirrorJobManifest(spec, "flush", flushJob), remainingSeconds(startedAt, timeoutSeconds), "flush");
if (!flush.ok) {
return nativeAgentRunStageFailure(follower, observedSha, "git-mirror-flush", flushJob, flush, {}, "native AgentRun git-mirror flush failed");
}
const pipelineRun = agentRunPipelineRunName(spec, observedSha);
const tektonResult = runNativeTektonPipelineRun(follower.nativeStatus.tekton.namespace, pipelineRun, yamlLanePipelineRunManifest(spec, observedSha, pipelineRun), options.wait, remainingSeconds(startedAt, timeoutSeconds));
const tektonPayload = parseJsonObject(tektonResult.stdout) ?? {};
const pipelineRunCompleted = tektonPayload.completed === true;
const failed = tektonPayload.failed === true || tektonResult.exitCode !== 0;
const closeout = !failed && options.wait && pipelineRunCompleted
? await waitNativeFollowerCloseout(registry, follower, observedSha, options, remainingSeconds(startedAt, timeoutSeconds))
: null;
const trigger = nativeTektonTriggerResult({
follower,
observedSha,
namespace: follower.nativeStatus.tekton.namespace,
pipelineRun,
stageRef,
wait: options.wait,
result: tektonResult,
payload: {
...tektonPayload,
agentrun: {
configPath,
imageBuild: { jobName: buildJob, payload: buildPayload },
gitopsPublish: { jobName: publishJob, payload: publishPayload },
gitMirrorFlush: { jobName: flushJob, payload: flush },
},
},
closeout,
successMessage: `native AgentRun PipelineRun ${pipelineRun} succeeded and runtime reached ${shortSha(observedSha)}`,
});
return trigger;
}
function nativeTriggerError(follower: FollowerSpec, message: string, reason: string): TriggerResult {
return {
ok: false,
completed: false,
message,
jobId: null,
command: { mode: "k8s-native-tekton", adapter: follower.adapter, ok: false, reason, parsedDownstreamCliOutput: false },
};
}
function nativeAgentRunStageFailure(
follower: FollowerSpec,
observedSha: string,
phase: string,
jobName: string,
job: NativeK8sJobResult,
payload: Record<string, unknown>,
message: string,
): TriggerResult {
return {
ok: false,
completed: false,
message,
jobId: jobName,
command: {
mode: "k8s-native-job",
adapter: follower.adapter,
phase,
jobName,
sourceCommit: observedSha,
ok: false,
payload,
job,
statusAuthority: "kubernetes-api-serviceaccount",
parsedDownstreamCliOutput: false,
},
};
}
function nativeTektonTriggerResult(input: {
follower: FollowerSpec;
observedSha: string;
namespace: string;
pipelineRun: string;
stageRef: string;
wait: boolean;
result: CommandResult;
payload: Record<string, unknown>;
closeout: NativeCloseoutWaitResult | null;
successMessage: string;
}): TriggerResult {
const pipelineRunCompleted = input.payload.completed === true;
const failed = input.payload.failed === true || input.result.exitCode !== 0;
const stillRunning = input.payload.stillRunning === true || input.payload.timedOutWait === true;
const message = failed
? tailText(input.result.stderr || stringOrNull(input.payload.message) || input.result.stdout, 500)
: input.closeout?.completed === true
? input.successMessage
: input.closeout?.timedOut === true
? `native PipelineRun ${input.pipelineRun} succeeded but runtime closeout did not converge within budget`
: pipelineRunCompleted
? `native PipelineRun ${input.pipelineRun} succeeded; runtime closeout remains k8s-native`
: stillRunning
? `native PipelineRun ${input.pipelineRun} is still running; query status/events/logs for closeout`
: `native PipelineRun ${input.pipelineRun} submitted`;
const ok = !failed && (input.closeout === null || input.closeout.completed === true);
return {
ok,
completed: input.closeout?.completed === true,
message,
jobId: input.pipelineRun,
command: {
mode: "k8s-native-tekton",
adapter: input.follower.adapter,
namespace: input.namespace,
pipelineRun: input.pipelineRun,
sourceCommit: input.observedSha,
sourceStageRef: input.stageRef,
wait: input.wait,
pipelineRunCompleted,
stillRunning,
closeout: input.closeout,
statusAuthority: "kubernetes-api-serviceaccount",
parsedDownstreamCliOutput: false,
payload: input.payload,
exitCode: input.result.exitCode,
timedOut: input.result.timedOut,
stderrTail: failed ? redactText(tailText(input.result.stderr, 1000)) : "",
},
};
}
function remainingSeconds(startedAt: number, timeoutSeconds: number): number {
return Math.max(1, timeoutSeconds - Math.ceil((Date.now() - startedAt) / 1000));
}
async function executeNativeSentinelTrigger(registry: BranchFollowerRegistry, follower: FollowerSpec, observedSha: string | null, options: ParsedOptions, timeoutSeconds: number): Promise<TriggerResult> {
if (observedSha === null) {
return {
@@ -1255,6 +1476,16 @@ async function waitNativeSentinelCloseout(
};
}
async function waitNativeFollowerCloseout(
registry: BranchFollowerRegistry,
follower: FollowerSpec,
observedSha: string,
options: ParsedOptions,
timeoutSeconds: number,
): Promise<NativeCloseoutWaitResult> {
return await waitNativeSentinelCloseout(registry, follower, observedSha, options, timeoutSeconds);
}
function nativeCloseoutSummary(live: AdapterSummary): Record<string, unknown> {
const payload = asOptionalRecord(live.payload);
return {
@@ -1340,6 +1571,153 @@ function runNativeTektonPipelineRun(namespace: string, pipelineRun: string, mani
return runCommand(["sh", "-lc", script], repoRoot, { timeoutMs: Math.max(5, timeoutSeconds + 10) * 1000 });
}
function runNativeK8sJob(namespace: string, jobName: string, manifest: Record<string, unknown>, timeoutSeconds: number, logContainer: string): NativeK8sJobResult {
const manifestBase64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
const script = [
"set -eu",
"tmpdir=$(mktemp -d)",
"cleanup() { rm -rf \"$tmpdir\"; }",
"trap cleanup EXIT INT TERM",
"cat >\"$tmpdir/manifest.b64\" <<'UNIDESK_NATIVE_JOB_B64'",
manifestBase64,
"UNIDESK_NATIVE_JOB_B64",
`NAMESPACE=${shQuote(namespace)}`,
`JOB_NAME=${shQuote(jobName)}`,
`LOG_CONTAINER=${shQuote(logContainer)}`,
`TIMEOUT_SECONDS=${shQuote(String(timeoutSeconds))}`,
"export NAMESPACE JOB_NAME LOG_CONTAINER TIMEOUT_SECONDS",
"cat >\"$tmpdir/native-job.mjs\" <<'NODE_NATIVE_JOB'",
nativeK8sJobNodeScript(),
"NODE_NATIVE_JOB",
"node \"$tmpdir/native-job.mjs\" \"$tmpdir/manifest.b64\"",
].join("\n");
const result = runCommand(["sh", "-lc", script], repoRoot, { timeoutMs: Math.max(5, timeoutSeconds + 10) * 1000 });
const parsed = result.exitCode === 0 ? parseJsonObject(result.stdout) : null;
return {
ok: result.exitCode === 0 && parsed?.ok === true,
completed: parsed?.completed === true,
failed: parsed?.failed === true || result.exitCode !== 0,
timedOut: parsed?.timedOut === true || result.timedOut,
created: parsed?.created === true,
reused: parsed?.reused === true,
jobName,
namespace,
polls: numberOrNull(parsed?.polls) ?? 0,
elapsedMs: numberOrNull(parsed?.elapsedMs) ?? 0,
logsTail: stringOrNull(parsed?.logsTail),
conditionReason: stringOrNull(parsed?.conditionReason),
conditionMessage: stringOrNull(parsed?.conditionMessage) ?? (result.exitCode === 0 ? null : tailText(result.stderr || result.stdout, 500)),
statusAuthority: "kubernetes-api-serviceaccount",
parsedDownstreamCliOutput: false,
};
}
function nativeK8sJobNodeScript(): string {
return [
"import { readFileSync } from 'node:fs';",
"import https from 'node:https';",
"const manifestPath = process.argv[2];",
"const namespace = process.env.NAMESPACE || '';",
"const jobName = process.env.JOB_NAME || '';",
"const logContainer = process.env.LOG_CONTAINER || '';",
"const timeoutSeconds = Number(process.env.TIMEOUT_SECONDS || '60');",
"const host = process.env.KUBERNETES_SERVICE_HOST;",
"const port = Number(process.env.KUBERNETES_SERVICE_PORT || '443');",
"const token = readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/token', 'utf8').trim();",
"const ca = readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/ca.crt');",
"const manifest = JSON.parse(Buffer.from(readFileSync(manifestPath, 'utf8').replace(/\\s+/g, ''), 'base64').toString('utf8'));",
"function request(method, path, body, contentType = 'application/json') {",
" return new Promise((resolve, reject) => {",
" const headers = { authorization: `Bearer ${token}` };",
" const payload = body === undefined ? null : typeof body === 'string' ? body : JSON.stringify(body);",
" if (payload !== null) { headers['content-type'] = contentType; headers['content-length'] = Buffer.byteLength(payload); }",
" const req = https.request({ host, port, path, method, ca, headers }, (res) => {",
" let text = '';",
" res.setEncoding('utf8');",
" res.on('data', (chunk) => { text += chunk; });",
" res.on('end', () => resolve({ status: res.statusCode || 0, text }));",
" });",
" req.on('error', reject);",
" if (payload !== null) req.write(payload);",
" req.end();",
" });",
"}",
"function parse(text) { try { return text ? JSON.parse(text) : null; } catch { return null; } }",
"function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }",
"function condition(job, type) { return (Array.isArray(job?.status?.conditions) ? job.status.conditions : []).find((item) => item?.type === type && item?.status === 'True') || null; }",
"async function getJob() {",
" const result = await request('GET', `/apis/batch/v1/namespaces/${encodeURIComponent(namespace)}/jobs/${encodeURIComponent(jobName)}`);",
" if (result.status === 404) return null;",
" if (result.status < 200 || result.status >= 300) throw new Error(result.text || `kube api GET job status ${result.status}`);",
" return parse(result.text);",
"}",
"async function podNames() {",
" const selector = encodeURIComponent(`job-name=${jobName}`);",
" const result = await request('GET', `/api/v1/namespaces/${encodeURIComponent(namespace)}/pods?labelSelector=${selector}`);",
" if (result.status < 200 || result.status >= 300) return [];",
" const list = parse(result.text);",
" return (Array.isArray(list?.items) ? list.items : []).map((pod) => pod?.metadata?.name).filter(Boolean);",
"}",
"async function logsTail() {",
" const names = await podNames();",
" let combined = '';",
" for (const pod of names.slice(-2)) {",
" const container = logContainer ? `&container=${encodeURIComponent(logContainer)}` : '';",
" const result = await request('GET', `/api/v1/namespaces/${encodeURIComponent(namespace)}/pods/${encodeURIComponent(pod)}/log?tailLines=120${container}`);",
" if (result.status >= 200 && result.status < 300) combined += `${result.text}\\n`;",
" }",
" return combined.length > 6000 ? combined.slice(-6000) : combined;",
"}",
"let created = false;",
"let reused = false;",
"let existing = await getJob();",
"if (existing) {",
" reused = true;",
"} else {",
" const result = await request('POST', `/apis/batch/v1/namespaces/${encodeURIComponent(namespace)}/jobs`, manifest);",
" if (result.status === 409) reused = true;",
" else if (result.status >= 200 && result.status < 300) created = true;",
" else { process.stderr.write(result.text || `kube api POST job status ${result.status}`); process.exit(1); }",
"}",
"const startedAt = Date.now();",
"const deadline = startedAt + Math.max(1, timeoutSeconds) * 1000;",
"let polls = 0;",
"let latest = await getJob();",
"while (Date.now() <= deadline) {",
" const complete = condition(latest, 'Complete');",
" const failed = condition(latest, 'Failed');",
" if (complete || failed) break;",
" polls += 1;",
" await delay(2000);",
" latest = await getJob();",
"}",
"const complete = condition(latest, 'Complete');",
"const failed = condition(latest, 'Failed');",
"const logs = await logsTail();",
"const timedOut = !complete && !failed;",
"const output = {",
" ok: Boolean(complete) && !timedOut,",
" completed: Boolean(complete),",
" failed: Boolean(failed),",
" timedOut,",
" created,",
" reused,",
" jobName,",
" namespace,",
" polls,",
" elapsedMs: Date.now() - startedAt,",
" conditionReason: complete?.reason || failed?.reason || null,",
" conditionMessage: complete?.message || failed?.message || null,",
" logsTail: logs || null,",
" statusAuthority: 'kubernetes-api-serviceaccount',",
" parsedDownstreamCliOutput: false,",
" valuesRedacted: true,",
"};",
"process.stdout.write(JSON.stringify(output));",
"if (!output.ok) process.exit(1);",
].join("\n");
}
function nativeTektonPipelineRunNodeScript(): string {
return [
"import { readFileSync } from 'node:fs';",
@@ -1990,7 +2368,7 @@ function kubeConfigMapFollowerState(registry: BranchFollowerRegistry, options: P
function kubeConfigMapDataValue(registry: BranchFollowerRegistry, options: ParsedOptions, key: string): { ok: boolean; present: boolean; value: string | null; omitted: boolean; error: string } {
const template = `{{ with index .data ${JSON.stringify(key)} }}{{ . }}{{ end }}`;
const maxValueBytes = 4096;
const maxValueBytes = 6144;
const script = [
"set -eu",
"tmpdir=$(mktemp -d)",
@@ -2117,7 +2495,7 @@ function compactFollowerStateForConfigMap(state: FollowerState): Record<string,
decision: state.decision,
dryRun: state.dryRun,
updatedAt: state.updatedAt,
warnings: state.warnings.slice(0, 6),
warnings: compactStateWarnings(state.warnings),
stateFormat: "compact-v1",
command: compactStateCommand(state.command),
};
@@ -2157,6 +2535,10 @@ function compactStateCommand(command: Record<string, unknown> | undefined): Reco
};
}
function compactStateWarnings(warnings: string[]): string[] {
return warnings.slice(0, 4).map((item) => redactText(tailText(item, 400)));
}
function compactNativePayload(payload: Record<string, unknown> | null): Record<string, unknown> | null {
if (payload === null) return null;
return {
@@ -2391,7 +2773,7 @@ function renderControllerManifests(registry: BranchFollowerRegistry): Record<str
rules: [
{ apiGroups: [""], resources: ["configmaps", "pods", "events"], verbs: ["get", "list", "watch", "create", "update", "patch"] },
{ apiGroups: ["apps"], resources: ["deployments"], verbs: ["get", "list", "watch"] },
{ apiGroups: ["batch"], resources: ["jobs"], verbs: ["get", "list", "watch", "create", "update", "patch"] },
{ apiGroups: ["batch"], resources: ["jobs"], verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] },
{ apiGroups: ["coordination.k8s.io"], resources: ["leases"], verbs: ["get", "list", "watch", "create", "update", "patch"] },
],
},
@@ -2409,8 +2791,9 @@ function renderControllerManifests(registry: BranchFollowerRegistry): Record<str
rules: [
{ apiGroups: [""], resources: ["pods", "pods/log", "configmaps", "events"], verbs: ["get", "list", "watch"] },
{ apiGroups: [""], resources: ["pods/exec"], verbs: ["create"] },
{ apiGroups: ["batch"], resources: ["jobs"], verbs: ["get", "list", "watch", "create", "delete"] },
{ apiGroups: ["apps"], resources: ["deployments", "statefulsets"], verbs: ["get", "list", "watch"] },
{ apiGroups: ["tekton.dev"], resources: ["pipelineruns"], verbs: ["get", "list", "watch", "create"] },
{ apiGroups: ["tekton.dev"], resources: ["pipelineruns"], verbs: ["get", "list", "watch", "create", "patch", "delete"] },
{ apiGroups: ["tekton.dev"], resources: ["taskruns"], verbs: ["get", "list", "watch"] },
{ apiGroups: ["argoproj.io"], resources: ["applications"], verbs: ["get", "list", "watch", "patch"] },
],