Files
pikasTech-unidesk/scripts/src/platform-infra-pipelines-as-code-remote.sh
T
Codex bd8e844af6
Pipelines as Code CI / hwlab-web-probe-sentinel-jd01- Success
fix: expose pac sentinel cicd diagnosis
2026-07-06 16:56:48 +00:00

940 lines
39 KiB
Bash

#!/bin/sh
set -eu
json_escape() {
sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr '\n' ' '
}
json_string() {
printf '%s' "$1" | json_escape
}
now_epoch() {
date +%s
}
duration_seconds() {
start="$1"
end="$2"
if [ -n "$start" ] && [ -n "$end" ]; then
s=$(date -d "$start" +%s 2>/dev/null || echo "")
e=$(date -d "$end" +%s 2>/dev/null || echo "")
if [ -n "$s" ] && [ -n "$e" ]; then
echo $((e - s))
return
fi
fi
echo null
}
json_normalize() {
UNIDESK_JSON_NORMALIZE_INPUT="${1:-}" node <<'NODE'
const input = String(process.env.UNIDESK_JSON_NORMALIZE_INPUT || '').trim();
if (!input) {
process.stdout.write('{}');
process.exit(0);
}
for (let end = input.length; end > 0; end -= 1) {
try {
process.stdout.write(JSON.stringify(JSON.parse(input.slice(0, end))));
process.exit(0);
} catch {}
}
process.stdout.write('{}');
NODE
}
api_url() {
prepare_gitea_api_base
printf '%s/api/v1/%s' "$UNIDESK_PAC_GITEA_API_BASE_URL" "$1"
}
prepare_gitea_api_base() {
if [ -n "${UNIDESK_PAC_GITEA_API_BASE_URL:-}" ]; then
return
fi
base="$UNIDESK_PAC_GITEA_BASE_URL"
hostport=$(printf '%s' "$base" | sed -n 's#^http://\([^/]*\).*$#\1#p')
if printf '%s' "$hostport" | grep -q '\.svc\.cluster\.local'; then
host=${hostport%%:*}
port=${hostport##*:}
if [ "$port" = "$hostport" ]; then port=80; fi
service=${host%%.*}
rest=${host#*.}
namespace=${rest%%.*}
cluster_ip=$(kubectl -n "$namespace" get svc "$service" -o jsonpath='{.spec.clusterIP}')
UNIDESK_PAC_GITEA_API_BASE_URL="http://$cluster_ip:$port"
else
UNIDESK_PAC_GITEA_API_BASE_URL="$base"
fi
export UNIDESK_PAC_GITEA_API_BASE_URL
}
gitea_api() {
method="$1"
path="$2"
body="${3:-}"
if [ -n "$body" ]; then
curl -fsS -u "$UNIDESK_PAC_GITEA_ADMIN_USERNAME:$UNIDESK_PAC_GITEA_ADMIN_PASSWORD" \
-H 'Content-Type: application/json' \
-X "$method" \
--data "$body" \
"$(api_url "$path")"
else
curl -fsS -u "$UNIDESK_PAC_GITEA_ADMIN_USERNAME:$UNIDESK_PAC_GITEA_ADMIN_PASSWORD" \
-X "$method" \
"$(api_url "$path")"
fi
}
ensure_token() {
name="unidesk-pac-$(date +%Y%m%d%H%M%S)"
body=$(printf '{"name":"%s","scopes":["all"]}' "$name")
out=$(gitea_api POST "users/$UNIDESK_PAC_GITEA_API_USERNAME/tokens" "$body")
printf '%s' "$out" | sed -n 's/.*"sha1"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'
}
ensure_webhook() {
hooks=$(gitea_api GET "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks")
hook_ids=$(HOOKS_JSON="$hooks" node <<'NODE'
const data = JSON.parse(process.env.HOOKS_JSON || '[]');
const url = process.env.UNIDESK_PAC_WEBHOOK_URL;
for (const item of data) if (item?.config?.url === url && item?.id) console.log(item.id);
NODE
)
hook_id=$(printf '%s\n' "$hook_ids" | sed -n '1p')
body=$(printf '{"type":"gitea","config":{"url":"%s","content_type":"json","secret":"%s"},"events":["push"],"active":true}' "$UNIDESK_PAC_WEBHOOK_URL" "$UNIDESK_PAC_WEBHOOK_SECRET")
if [ -n "$hook_id" ]; then
gitea_api PATCH "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks/$hook_id" "$body" >/dev/null
printf '%s\n' "$hook_ids" | sed -n '2,$p' | while IFS= read -r duplicate_id; do
[ -n "$duplicate_id" ] || continue
gitea_api DELETE "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks/$duplicate_id" >/dev/null || true
done
else
created=$(gitea_api POST "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks" "$body")
hook_id=$(printf '%s' "$created" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p')
fi
printf '%s' "$hook_id"
}
repository_manifest() {
cat <<EOF
apiVersion: pipelinesascode.tekton.dev/v1alpha1
kind: Repository
metadata:
name: $UNIDESK_PAC_REPOSITORY_NAME
namespace: $UNIDESK_PAC_TARGET_NAMESPACE
labels:
app.kubernetes.io/managed-by: unidesk
app.kubernetes.io/part-of: $UNIDESK_PAC_PART_OF
unidesk.ai/spec: $UNIDESK_PAC_SPEC
spec:
url: $UNIDESK_PAC_REPOSITORY_URL
git_provider:
type: gitea
url: $UNIDESK_PAC_GITEA_BASE_URL
secret:
name: $UNIDESK_PAC_SECRET_NAME
key: $UNIDESK_PAC_TOKEN_KEY
webhook_secret:
name: $UNIDESK_PAC_SECRET_NAME
key: $UNIDESK_PAC_WEBHOOK_SECRET_KEY
concurrency_limit: $UNIDESK_PAC_CONCURRENCY_LIMIT
params:
$(printf '%s' "$UNIDESK_PAC_PARAMS_JSON" | node -e 'const fs=require("fs"); const p=JSON.parse(fs.readFileSync(0,"utf8")); for (const [name,value] of Object.entries(p)) console.log(` - name: ${name}\n value: ${JSON.stringify(String(value))}`);')
EOF
}
consumer_rbac_manifest() {
cat <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: unidesk-pac-consumer-runner
namespace: $UNIDESK_PAC_TARGET_NAMESPACE
labels:
app.kubernetes.io/managed-by: unidesk
app.kubernetes.io/part-of: $UNIDESK_PAC_PART_OF
unidesk.ai/spec: $UNIDESK_PAC_SPEC
rules:
- apiGroups: ["tekton.dev"]
resources: ["pipelineruns", "taskruns"]
verbs: ["get", "list", "watch", "create", "patch", "update"]
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: unidesk-pac-consumer-runner-default
namespace: $UNIDESK_PAC_TARGET_NAMESPACE
labels:
app.kubernetes.io/managed-by: unidesk
app.kubernetes.io/part-of: $UNIDESK_PAC_PART_OF
unidesk.ai/spec: $UNIDESK_PAC_SPEC
subjects:
- kind: ServiceAccount
name: default
namespace: $UNIDESK_PAC_TARGET_NAMESPACE
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: unidesk-pac-consumer-runner
EOF
}
apply_release() {
tmp=$(mktemp)
printf '%s' "$UNIDESK_PAC_RELEASE_MANIFEST_B64" | base64 -d > "$tmp"
kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER" -f "$tmp" >/dev/null
rm -f "$tmp"
}
wait_ready() {
timeout="${UNIDESK_PAC_WAIT_TIMEOUT_SECONDS:-55}"
end=$(( $(now_epoch) + timeout ))
while [ "$(now_epoch)" -lt "$end" ]; do
ready=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || true)
desired=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.spec.replicas}' 2>/dev/null || true)
if [ "${ready:-0}" = "${desired:-1}" ] && [ "${ready:-0}" != "0" ]; then
return 0
fi
sleep 2
done
return 1
}
apply_action() {
if [ "$UNIDESK_PAC_DRY_RUN" = "1" ]; then
printf '{"ok":true,"mode":"dry-run","mutation":false,"valuesPrinted":false}\n'
return
fi
apply_release
kubectl create ns "$UNIDESK_PAC_TARGET_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
consumer_rbac_manifest | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER" -f - >/dev/null
token=$(ensure_token)
test -n "$token"
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" create secret generic "$UNIDESK_PAC_SECRET_NAME" \
--from-literal="$UNIDESK_PAC_TOKEN_KEY=$token" \
--from-literal="$UNIDESK_PAC_WEBHOOK_SECRET_KEY=$UNIDESK_PAC_WEBHOOK_SECRET" \
--dry-run=client -o yaml | kubectl apply -f - >/dev/null
repository_manifest | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER" -f - >/dev/null
hook_id=$(ensure_webhook)
ready=false
if wait_ready; then ready=true; fi
printf '{"ok":%s,"mode":"confirmed","mutation":true,"controllerReady":%s,"webhook":{"present":true,"id":"%s","url":"%s"},"repository":"%s","namespace":"%s","valuesPrinted":false}\n' "$ready" "$ready" "$(json_string "$hook_id")" "$(json_string "$UNIDESK_PAC_WEBHOOK_URL")" "$(json_string "$UNIDESK_PAC_REPOSITORY_NAME")" "$(json_string "$UNIDESK_PAC_TARGET_NAMESPACE")"
}
condition_status() {
kubectl -n "$1" get "$2" "$3" -o jsonpath='{range .status.conditions[*]}{.type}={.status}:{.reason}{";"}{end}' 2>/dev/null || true
}
pipeline_rows() {
payload_file=$(mktemp)
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get pipelinerun -o json >"$payload_file" 2>/dev/null || printf '{"items":[]}' >"$payload_file"
node - "$payload_file" <<'NODE'
const fs = require('node:fs');
const input = fs.readFileSync(process.argv[2], 'utf8') || '{"items":[]}';
const data = input ? JSON.parse(input) : { items: [] };
function firstString(...values) {
for (const value of values) {
if (typeof value === 'string' && value.length > 0) return value;
}
return null;
}
function cond(item) {
const c = (item.status?.conditions || []).find((x) => x.type === 'Succeeded') || {};
return { status: c.status || '', reason: c.reason || '' };
}
function mapParams(params) {
const out = {};
for (const item of params || []) {
if (!item || typeof item.name !== 'string') continue;
if (typeof item.value === 'string') out[item.name] = item.value;
else if (item.value !== undefined && item.value !== null) out[item.name] = JSON.stringify(item.value);
}
return out;
}
function extractCommit(item, params) {
const labels = item.metadata?.labels || {};
const annotations = item.metadata?.annotations || {};
return firstString(
labels['pipelinesascode.tekton.dev/sha'],
labels['pipelinesascode.tekton.dev/commit'],
labels['agentrun.pikastech.local/source-commit'],
labels['unidesk.ai/source-commit'],
labels['hwlab.pikastech.local/source-commit'],
annotations['pipelinesascode.tekton.dev/sha'],
annotations['pipelinesascode.tekton.dev/commit'],
annotations['unidesk.ai/source-commit'],
params.revision,
params.source_revision,
params.sourceCommit,
params.source_commit,
params.git_sha,
params.sha,
params.commit
);
}
function durationSeconds(item) {
const start = item.status?.startTime;
const done = item.status?.completionTime;
if (!start) return null;
return Math.max(0, Math.round(((done ? Date.parse(done) : Date.now()) - Date.parse(start)) / 1000));
}
const prefix = process.env.UNIDESK_PAC_PIPELINE_RUN_PREFIX;
const pipeline = process.env.UNIDESK_PAC_PIPELINE_NAME;
const rows = (data.items || [])
.filter((item) => {
const labels = item.metadata?.labels || {};
const name = item.metadata?.name || '';
return name.startsWith(prefix)
|| labels['tekton.dev/pipeline'] === pipeline
|| labels['tekton.dev/pipelineName'] === pipeline;
})
.sort((a, b) => Date.parse(b.metadata?.creationTimestamp || 0) - Date.parse(a.metadata?.creationTimestamp || 0))
.slice(0, 8)
.map((item) => {
const c = cond(item);
const params = mapParams(item.spec?.params);
return {
name: item.metadata.name,
status: c.status,
reason: c.reason,
createdAt: item.metadata.creationTimestamp || null,
startTime: item.status?.startTime || null,
completionTime: item.status?.completionTime || null,
durationSeconds: durationSeconds(item),
sourceCommit: extractCommit(item, params),
};
});
process.stdout.write(JSON.stringify(rows));
NODE
rm -f "$payload_file"
}
history_rows() {
node <<'NODE'
const fs = require('node:fs');
const cp = require('node:child_process');
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'));
const displayTimeZone = process.env.UNIDESK_PAC_DISPLAY_TIME_ZONE || 'UTC';
const kubectlJsonErrors = [];
const displayTimeFormatter = new Intl.DateTimeFormat('sv-SE', {
timeZone: displayTimeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
function kubectlJson(args, fallback, context) {
const tmpDir = fs.mkdtempSync('/tmp/unidesk-pac-history-');
const outPath = `${tmpDir}/kubectl.json`;
let outFd = null;
try {
outFd = fs.openSync(outPath, 'w');
const result = cp.spawnSync('kubectl', args, {
stdio: ['ignore', outFd, 'pipe'],
encoding: 'utf8',
timeout: 30000,
maxBuffer: 1024 * 1024,
});
fs.closeSync(outFd);
outFd = null;
if (result.error || result.status !== 0) {
kubectlJsonErrors.push({
context: context || args.join(' '),
status: result.status,
error: result.error ? String(result.error.message || result.error) : null,
stderr: String(result.stderr || '').slice(0, 1000),
});
return fallback;
}
const out = fs.readFileSync(outPath, 'utf8');
return JSON.parse(out || JSON.stringify(fallback));
} catch (error) {
kubectlJsonErrors.push({
context: context || args.join(' '),
status: null,
error: String(error && error.message ? error.message : error).slice(0, 1000),
stderr: null,
});
return fallback;
} finally {
if (outFd !== null) {
try { fs.closeSync(outFd); } catch {}
}
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
}
}
function condition(item) {
const c = (item.status?.conditions || []).find((x) => x.type === 'Succeeded') || {};
return { status: c.status || '', reason: c.reason || '' };
}
function durationSeconds(start, done) {
if (!start) return null;
const startMs = Date.parse(start);
const doneMs = done ? Date.parse(done) : Date.now();
if (!Number.isFinite(startMs) || !Number.isFinite(doneMs)) return null;
return Math.max(0, Math.round((doneMs - startMs) / 1000));
}
function formatDisplayTime(value) {
if (!value) return null;
const date = new Date(value);
if (!Number.isFinite(date.getTime())) return null;
return displayTimeFormatter.format(date);
}
function mapParams(params) {
const out = {};
for (const item of params || []) {
if (!item || typeof item.name !== 'string') continue;
if (typeof item.value === 'string') out[item.name] = item.value;
else if (item.value !== undefined && item.value !== null) out[item.name] = JSON.stringify(item.value);
}
return out;
}
function firstString(...values) {
for (const value of values) {
if (typeof value === 'string' && value.length > 0) return value;
}
return null;
}
function extractCommit(item, params) {
const labels = item.metadata?.labels || {};
const annotations = item.metadata?.annotations || {};
return firstString(
labels['pipelinesascode.tekton.dev/sha'],
labels['pipelinesascode.tekton.dev/commit'],
labels['unidesk.ai/source-commit'],
labels['hwlab.pikastech.local/source-commit'],
labels['agentrun.pikastech.local/source-commit'],
annotations['pipelinesascode.tekton.dev/sha'],
annotations['pipelinesascode.tekton.dev/commit'],
annotations['unidesk.ai/source-commit'],
params.revision,
params.source_revision,
params.sourceCommit,
params.source_commit,
params.git_sha,
params.sha,
params.commit
);
}
function extractBranch(item, params) {
const labels = item.metadata?.labels || {};
const annotations = item.metadata?.annotations || {};
return firstString(
labels['pipelinesascode.tekton.dev/branch'],
annotations['pipelinesascode.tekton.dev/branch'],
params.source_branch,
params.branch,
params.git_branch
);
}
function recursiveEnvReuse(value, state) {
if (value === null || value === undefined) return;
if (Array.isArray(value)) {
for (const item of value) recursiveEnvReuse(item, state);
return;
}
if (typeof value !== 'object') return;
if (typeof value.buildSkippedCount === 'number') state.buildSkippedCount = value.buildSkippedCount;
if (typeof value.serviceReusedCount === 'number') state.serviceReusedCount = value.serviceReusedCount;
if (typeof value.reusedServiceCount === 'number') state.serviceReusedCount = value.reusedServiceCount;
if (typeof value.envReuse === 'string') state.status = value.envReuse;
if (typeof value.envReuseStatus === 'string') state.status = value.envReuseStatus;
if (value.envReuse && typeof value.envReuse === 'object') {
const env = value.envReuse;
if (typeof env.mode === 'string') state.status = env.mode;
if (typeof env.status === 'string') state.status = env.status;
if (typeof env.dependencyReuse === 'string') state.status = env.dependencyReuse;
if (typeof env.buildSkippedCount === 'number') state.buildSkippedCount = env.buildSkippedCount;
if (typeof env.serviceReusedCount === 'number') state.serviceReusedCount = env.serviceReusedCount;
}
if (typeof value.status === 'string' && /reus|skip|built|publish/u.test(value.status)) state.status = value.status;
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' };
let logs = '';
try {
logs = cp.execFileSync('kubectl', ['-n', namespace, 'logs', '-l', `tekton.dev/pipelineRun=${name}`, '--all-containers', '--tail=320'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 12000,
maxBuffer: 1024 * 1024,
});
} catch {
return { ...state, source: 'logs-unavailable' };
}
const lines = logs.split(/\r?\n/u);
const headerIndex = lines.findIndex((line) => /^ENV_REUSE\s+NODE_DEPS\s+/u.test(line.trim()));
if (headerIndex >= 0) {
const row = (lines.slice(headerIndex + 1).find((line) => line.trim()) || '').trim().split(/\s+/u);
if (row.length >= 5) {
state.status = row[0];
state.buildCache = row[4];
}
}
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue;
try {
recursiveEnvReuse(JSON.parse(trimmed), state);
} catch {}
}
return state;
}
function taskSummary(taskRuns, name) {
const rows = (taskRuns.items || [])
.filter((item) => item.metadata?.labels?.['tekton.dev/pipelineRun'] === name)
.map((item) => {
const c = condition(item);
return {
name: item.metadata?.name || '',
status: c.status || null,
reason: c.reason || null,
startTime: item.status?.startTime || null,
completionTime: item.status?.completionTime || null,
durationSeconds: durationSeconds(item.status?.startTime, item.status?.completionTime),
};
});
const longest = [...rows].sort((a, b) => (b.durationSeconds || 0) - (a.durationSeconds || 0))[0] || null;
return { count: rows.length, longest };
}
function rowFor(consumer, item, taskRuns) {
const params = mapParams(item.spec?.params);
const c = condition(item);
const tasks = taskSummary(taskRuns, item.metadata.name);
const labels = item.metadata?.labels || {};
const annotations = item.metadata?.annotations || {};
return {
id: item.metadata.name,
consumer: consumer.id,
repository: consumer.repository,
repo: consumer.repo,
repoUrl: consumer.repoUrl,
pipeline: consumer.pipeline,
pipelineRun: item.metadata.name,
triggeredAt: item.metadata?.creationTimestamp || item.status?.startTime || null,
startTime: item.status?.startTime || null,
completionTime: item.status?.completionTime || null,
displayTime: formatDisplayTime(item.metadata?.creationTimestamp || item.status?.startTime || null),
displayStartTime: formatDisplayTime(item.status?.startTime || null),
displayCompletionTime: formatDisplayTime(item.status?.completionTime || null),
displayTimeZone,
durationSeconds: durationSeconds(item.status?.startTime || item.metadata?.creationTimestamp, item.status?.completionTime),
status: c.status || null,
reason: c.reason || null,
commit: extractCommit(item, params),
branch: extractBranch(item, params),
eventType: firstString(labels['pipelinesascode.tekton.dev/event-type'], annotations['pipelinesascode.tekton.dev/event-type']),
sender: firstString(labels['pipelinesascode.tekton.dev/sender'], annotations['pipelinesascode.tekton.dev/sender']),
envReuse: envReuseForPipelineRun(consumer.namespace, item.metadata.name),
taskRuns: tasks,
source: 'gitea-pac-tekton-live',
valuesPrinted: false,
};
}
const rows = [];
const consumerSummaries = [];
for (const consumer of consumers) {
const pipelineRuns = kubectlJson(['-n', consumer.namespace, 'get', 'pipelinerun', '-o', 'json'], { items: [] }, `${consumer.id}:pipelinerun`);
const taskRuns = kubectlJson(['-n', consumer.namespace, 'get', 'taskrun', '-o', 'json'], { items: [] }, `${consumer.id}:taskrun`);
let matches = (pipelineRuns.items || []).filter((item) => {
const labels = item.metadata?.labels || {};
const name = item.metadata?.name || '';
if (detailId) return name === detailId;
return name.startsWith(consumer.pipelineRunPrefix)
|| labels['tekton.dev/pipeline'] === consumer.pipeline
|| labels['tekton.dev/pipelineName'] === consumer.pipeline;
});
matches = matches.sort((a, b) => Date.parse(b.metadata?.creationTimestamp || 0) - Date.parse(a.metadata?.creationTimestamp || 0));
if (!detailId) matches = matches.slice(0, limit);
for (const item of matches) rows.push(rowFor(consumer, item, taskRuns));
consumerSummaries.push({ id: consumer.id, namespace: consumer.namespace, repository: consumer.repository, pipelineRunPrefix: consumer.pipelineRunPrefix, totalPipelineRuns: (pipelineRuns.items || []).length, matched: matches.length });
}
rows.sort((a, b) => Date.parse(b.triggeredAt || 0) - Date.parse(a.triggeredAt || 0));
process.stdout.write(JSON.stringify({ rows, consumers: consumerSummaries, errors: kubectlJsonErrors }));
NODE
}
task_rows() {
payload_file=$(mktemp)
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get taskrun -o json >"$payload_file" 2>/dev/null || printf '{"items":[]}' >"$payload_file"
node - "$payload_file" <<'NODE'
const fs = require('node:fs');
const input = fs.readFileSync(process.argv[2], 'utf8') || '{"items":[]}';
const data = input ? JSON.parse(input) : { items: [] };
const pr = process.env.UNIDESK_PAC_TARGET_PIPELINERUN || '';
if (!pr) {
process.stdout.write('[]');
process.exit(0);
}
function cond(item) {
const c = (item.status?.conditions || []).find((x) => x.type === 'Succeeded') || {};
return { status: c.status || '', reason: c.reason || '' };
}
function durationSeconds(item) {
const start = item.status?.startTime;
const done = item.status?.completionTime;
if (!start) return null;
return Math.max(0, Math.round(((done ? Date.parse(done) : Date.now()) - Date.parse(start)) / 1000));
}
const rows = (data.items || [])
.filter((item) => item.metadata?.labels?.['tekton.dev/pipelineRun'] === pr)
.sort((a, b) => Date.parse(a.metadata?.creationTimestamp || 0) - Date.parse(b.metadata?.creationTimestamp || 0))
.slice(0, 20)
.map((item) => {
const c = cond(item);
return { name: item.metadata.name, pipelineRun: item.metadata.labels?.['tekton.dev/pipelineRun'] || null, status: c.status, reason: c.reason, startTime: item.status?.startTime || null, completionTime: item.status?.completionTime || null, durationSeconds: durationSeconds(item) };
});
process.stdout.write(JSON.stringify(rows));
NODE
rm -f "$payload_file"
}
artifact_summary() {
if [ -z "${UNIDESK_PAC_TARGET_PIPELINERUN:-}" ]; then
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'
const fs = require('node:fs');
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 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 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] };
}
}
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),
gitopsCommit: image.gitopsCommit || null,
sourceCommit: image.sourceCommit || null,
valuesPrinted: false,
};
} else if (humanEnv) {
out = {
imageStatus: 'published',
envIdentity: null,
envReuse: humanEnv.envReuse,
nodeDepsReuse: humanEnv.nodeDeps,
buildCache: humanEnv.cache,
buildPackage: humanEnv.buildPackage,
buildNetwork: humanEnv.buildNetwork,
digest: null,
gitopsCommit: null,
sourceCommit: null,
valuesPrinted: false,
};
}
process.stdout.write(JSON.stringify(out));
NODE
rm -f "$log_file"
}
runtime_summary() {
app_file=$(mktemp)
kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" get application "$UNIDESK_PAC_ARGO_APPLICATION" -o json >"$app_file" 2>/dev/null || printf '{}' >"$app_file"
target=$(node - "$app_file" <<'NODE'
const fs = require('node:fs');
const app = JSON.parse(fs.readFileSync(process.argv[2], 'utf8') || '{}');
const resource = (app.status?.resources || []).find((item) => item.kind === 'Deployment' && item.namespace && item.name);
if (resource) process.stdout.write(`${resource.namespace}\t${resource.name}`);
NODE
)
rm -f "$app_file"
if [ -z "$target" ]; then
printf '{}'
return
fi
namespace=$(printf '%s' "$target" | cut -f1)
name=$(printf '%s' "$target" | cut -f2)
deploy_file=$(mktemp)
kubectl -n "$namespace" get deploy "$name" -o json >"$deploy_file" 2>/dev/null || printf '{}' >"$deploy_file"
node - "$deploy_file" <<'NODE' || printf '{}'
const fs = require('node:fs');
const input = fs.readFileSync(process.argv[2], 'utf8').trim();
if (!input) {
process.stdout.write('{}');
process.exit(0);
}
const deploy = JSON.parse(input);
const image = deploy.spec?.template?.spec?.containers?.[0]?.image || null;
const digest = image && String(image).includes('@') ? String(image).split('@').slice(1).join('@') : null;
process.stdout.write(JSON.stringify({
namespace: deploy.metadata?.namespace || null,
deployment: deploy.metadata?.name || null,
readyReplicas: deploy.status?.readyReplicas ?? null,
replicas: deploy.spec?.replicas ?? null,
image,
digest,
valuesPrinted: false,
}));
NODE
rm -f "$deploy_file"
}
cicd_diagnostics() {
params_file=$(mktemp)
pipelines_file=$(mktemp)
artifact_file=$(mktemp)
argo_file=$(mktemp)
runtime_file=$(mktemp)
printf '%s' "$UNIDESK_PAC_PARAMS_JSON" >"$params_file"
printf '%s' "${1:-[]}" >"$pipelines_file"
printf '%s' "${2:-{}}" >"$artifact_file"
printf '%s' "${3:-{}}" >"$argo_file"
printf '%s' "${4:-{}}" >"$runtime_file"
probe_env=$(node - "$params_file" "$pipelines_file" <<'NODE'
const fs = require('node:fs');
const params = JSON.parse(fs.readFileSync(process.argv[2], 'utf8') || '{}');
const pipelines = JSON.parse(fs.readFileSync(process.argv[3], 'utf8') || '[]');
const latest = Array.isArray(pipelines) ? pipelines[0] || {} : {};
const sourceCommit = latest.sourceCommit || null;
const tag = sourceCommit ? String(sourceCommit).slice(0, 12) : '';
const imageRepository = typeof params.image_repository === 'string' ? params.image_repository : '';
const probeBase = typeof params.registry_probe_base === 'string' ? params.registry_probe_base.replace(/\/+$/u, '') : '';
let registryUrl = '';
if (imageRepository && tag) {
const firstSlash = imageRepository.indexOf('/');
const registry = firstSlash >= 0 ? imageRepository.slice(0, firstSlash) : imageRepository;
const path = firstSlash >= 0 ? imageRepository.slice(firstSlash + 1) : '';
const base = probeBase || (registry ? `http://${registry}` : '');
registryUrl = base && path ? `${base}/v2/${path}/manifests/${tag}` : '';
}
function line(key, value) {
process.stdout.write(`${key}=${Buffer.from(String(value || ''), 'utf8').toString('base64')}\n`);
}
line('imageRepository', imageRepository);
line('registryProbeBase', probeBase);
line('sourceCommit', sourceCommit || '');
line('tag', tag);
line('registryUrl', registryUrl);
line('sentinelId', params.sentinel_id || '');
line('gitopsBranch', params.gitops_branch || '');
line('gitopsManifestPath', params.gitops_manifest_path || '');
NODE
)
image_repository=$(printf '%s\n' "$probe_env" | sed -n 's/^imageRepository=//p' | base64 -d 2>/dev/null || true)
registry_probe_base=$(printf '%s\n' "$probe_env" | sed -n 's/^registryProbeBase=//p' | base64 -d 2>/dev/null || true)
source_commit=$(printf '%s\n' "$probe_env" | sed -n 's/^sourceCommit=//p' | base64 -d 2>/dev/null || true)
image_tag=$(printf '%s\n' "$probe_env" | sed -n 's/^tag=//p' | base64 -d 2>/dev/null || true)
registry_url=$(printf '%s\n' "$probe_env" | sed -n 's/^registryUrl=//p' | base64 -d 2>/dev/null || true)
sentinel_id=$(printf '%s\n' "$probe_env" | sed -n 's/^sentinelId=//p' | base64 -d 2>/dev/null || true)
gitops_branch=$(printf '%s\n' "$probe_env" | sed -n 's/^gitopsBranch=//p' | base64 -d 2>/dev/null || true)
gitops_manifest_path=$(printf '%s\n' "$probe_env" | sed -n 's/^gitopsManifestPath=//p' | base64 -d 2>/dev/null || true)
registry_present=false
registry_digest=""
if [ -n "$registry_url" ]; then
header_file=$(mktemp)
if curl -fsS -D "$header_file" -o /dev/null -H 'Accept: application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json' "$registry_url" >/dev/null 2>&1; then
registry_present=true
fi
headers=$(cat "$header_file" 2>/dev/null || true)
rm -f "$header_file"
if printf '%s' "$headers" | grep -qi '^HTTP/.* 200'; then registry_present=true; fi
registry_digest=$(printf '%s\n' "$headers" | awk 'BEGIN{IGNORECASE=1} /^Docker-Content-Digest:/ { sub(/\r$/,"",$2); print $2; exit }')
fi
registry_present_json=false
if [ "$registry_present" = true ]; then registry_present_json=true; fi
export UNIDESK_PAC_DIAG_IMAGE_REPOSITORY="$image_repository"
export UNIDESK_PAC_DIAG_REGISTRY_PROBE_BASE="$registry_probe_base"
export UNIDESK_PAC_DIAG_SOURCE_COMMIT="$source_commit"
export UNIDESK_PAC_DIAG_IMAGE_TAG="$image_tag"
export UNIDESK_PAC_DIAG_REGISTRY_URL="$registry_url"
export UNIDESK_PAC_DIAG_REGISTRY_PRESENT="$registry_present_json"
export UNIDESK_PAC_DIAG_REGISTRY_DIGEST="$registry_digest"
export UNIDESK_PAC_DIAG_SENTINEL_ID="$sentinel_id"
export UNIDESK_PAC_DIAG_GITOPS_BRANCH="$gitops_branch"
export UNIDESK_PAC_DIAG_GITOPS_MANIFEST_PATH="$gitops_manifest_path"
node - "$params_file" "$pipelines_file" "$artifact_file" "$argo_file" "$runtime_file" <<'NODE'
const fs = require('node:fs');
function parseLoose(path, fallback) {
const input = fs.readFileSync(path, 'utf8').trim();
if (!input) return fallback;
for (let end = input.length; end > 0; end -= 1) {
try { return JSON.parse(input.slice(0, end)); } catch {}
}
return fallback;
}
const params = parseLoose(process.argv[2], {});
const pipelines = parseLoose(process.argv[3], []);
const artifact = parseLoose(process.argv[4], {});
const argo = parseLoose(process.argv[5], {});
const runtime = parseLoose(process.argv[6], {});
const latest = Array.isArray(pipelines) ? pipelines[0] || {} : {};
const registryPresent = process.env.UNIDESK_PAC_DIAG_REGISTRY_PRESENT === 'true';
const sourceCommit = process.env.UNIDESK_PAC_DIAG_SOURCE_COMMIT || latest.sourceCommit || null;
const registryDigest = process.env.UNIDESK_PAC_DIAG_REGISTRY_DIGEST || null;
const expectedDigest = registryDigest || artifact.digest || null;
const runtimeImage = runtime.image || null;
const runtimeMatches = !expectedDigest || (typeof runtimeImage === 'string' && runtimeImage.includes(expectedDigest));
const gitopsReady = Boolean(artifact.gitopsCommit || argo.revision);
let code = 'pac-diagnostics-not-applicable';
let phase = 'not-applicable';
let ok = true;
let hint = 'consumer has no sentinel image_repository diagnostic config';
if (params.image_repository) {
if (!sourceCommit) {
ok = false; code = 'sentinel-pac-source-unknown'; phase = 'source-unknown'; hint = 'latest PaC PipelineRun did not expose a source commit';
} else if (!registryPresent) {
ok = false; code = 'sentinel-pac-registry-missing'; phase = 'source-ready-registry-missing'; hint = 'PaC source commit is known but the configured registry tag is missing';
} else if (!gitopsReady) {
ok = false; code = 'sentinel-pac-gitops-missing'; phase = 'registry-ready-gitops-missing'; hint = 'registry tag exists but GitOps revision is not visible in PaC artifact or Argo status';
} else if (argo.sync !== 'Synced' || argo.health !== 'Healthy') {
ok = false; code = 'sentinel-pac-argo-not-ready'; phase = 'gitops-ready-argo-pending'; hint = 'GitOps exists but Argo is not Synced/Healthy';
} else if (!runtimeMatches) {
ok = false; code = 'sentinel-pac-runtime-not-aligned'; phase = 'argo-ready-runtime-mismatch'; hint = 'runtime image does not match the registry digest observed for the source commit';
} else {
code = 'sentinel-pac-ready'; phase = 'ready'; hint = 'source, registry tag, GitOps, Argo and runtime are aligned';
}
}
process.stdout.write(JSON.stringify({
ok,
code,
phase,
hint,
sourceCommit,
imageRepository: process.env.UNIDESK_PAC_DIAG_IMAGE_REPOSITORY || null,
registryProbeBase: process.env.UNIDESK_PAC_DIAG_REGISTRY_PROBE_BASE || null,
imageTag: process.env.UNIDESK_PAC_DIAG_IMAGE_TAG || null,
registry: {
present: registryPresent,
digest: registryDigest,
url: process.env.UNIDESK_PAC_DIAG_REGISTRY_URL || null,
},
gitops: {
branch: process.env.UNIDESK_PAC_DIAG_GITOPS_BRANCH || null,
manifestPath: process.env.UNIDESK_PAC_DIAG_GITOPS_MANIFEST_PATH || null,
commit: artifact.gitopsCommit || argo.revision || null,
},
argo: { sync: argo.sync || null, health: argo.health || null, revision: argo.revision || null },
runtime: { image: runtimeImage, digest: runtime.digest || null, readyReplicas: runtime.readyReplicas ?? null, replicas: runtime.replicas ?? null },
pipelineRun: latest.name || null,
valuesPrinted: false,
}));
NODE
rm -f "$params_file" "$pipelines_file" "$artifact_file" "$argo_file" "$runtime_file"
}
hook_summary() {
hooks=$(gitea_api GET "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks" 2>/dev/null || echo '[]')
HOOKS_JSON="$hooks" node <<'NODE'
const data = JSON.parse(process.env.HOOKS_JSON || '[]');
const url = process.env.UNIDESK_PAC_WEBHOOK_URL;
const rows = data.filter((item) => item?.config?.url === url).map((item) => ({ id: item.id, active: item.active, type: item.type, events: item.events || [], url: item.config?.url || '' }));
process.stdout.write(JSON.stringify(rows));
NODE
}
status_action() {
crd=$(kubectl get crd repositories.pipelinesascode.tekton.dev -o name 2>/dev/null || true)
controller_ready=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.status.readyReplicas}/{.spec.replicas}' 2>/dev/null || echo "0/0")
repository_condition=$(condition_status "$UNIDESK_PAC_TARGET_NAMESPACE" repository "$UNIDESK_PAC_REPOSITORY_NAME")
pipelines=$(pipeline_rows)
latest=$(printf '%s' "$pipelines" | node -e 'const fs=require("fs"); const a=JSON.parse(fs.readFileSync(0,"utf8")||"[]"); process.stdout.write(a[0]?.name||"")')
export UNIDESK_PAC_TARGET_PIPELINERUN="$latest"
tasks=$(task_rows)
artifact=$(json_normalize "$(artifact_summary)")
hooks=$(hook_summary)
argo=$(kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" get application "$UNIDESK_PAC_ARGO_APPLICATION" -o json 2>/dev/null | node -e 'const fs=require("fs"); const s=fs.readFileSync(0,"utf8").trim(); if(!s){process.stdout.write("{}"); process.exit(0)} const a=JSON.parse(s); process.stdout.write(JSON.stringify({sync:a.status?.sync?.status||null, health:a.status?.health?.status||null, revision:a.status?.sync?.revision||null}))' || echo '{}')
argo=$(json_normalize "$argo")
runtime=$(json_normalize "$(runtime_summary)")
diagnostics=$(cicd_diagnostics "$pipelines" "$artifact" "$argo" "$runtime")
printf '{"ok":%s,"crdPresent":%s,"controllerReady":"%s","repositoryCondition":"%s","webhooks":%s,"pipelineRuns":%s,"taskRuns":%s,"artifact":%s,"argo":%s,"runtime":%s,"diagnostics":%s,"valuesPrinted":false}\n' \
"$( [ -n "$crd" ] && [ "$controller_ready" != "0/0" ] && echo true || echo false )" \
"$( [ -n "$crd" ] && echo true || echo false )" \
"$(json_string "$controller_ready")" \
"$(json_string "$repository_condition")" \
"$hooks" "$pipelines" "$tasks" "$artifact" "$argo" "$runtime" "$diagnostics"
}
history_action() {
crd=$(kubectl get crd repositories.pipelinesascode.tekton.dev -o name 2>/dev/null || true)
controller_ready=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.status.readyReplicas}/{.spec.replicas}' 2>/dev/null || echo "0/0")
repository_condition=$(condition_status "$UNIDESK_PAC_TARGET_NAMESPACE" repository "$UNIDESK_PAC_REPOSITORY_NAME")
hooks=$(hook_summary)
history=$(history_rows)
rows=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.rows||[]));')
consumers=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.consumers||[]));')
errors=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.errors||[]));')
printf '{"ok":%s,"crdPresent":%s,"controllerReady":"%s","repositoryCondition":"%s","repository":{"name":"%s","repo":"%s/%s","url":"%s"},"consumer":"%s","display":{"timeZone":"%s"},"consumerRows":%s,"rows":%s,"historyErrors":%s,"webhooks":%s,"source":"gitea-pac-tekton-live","historyStore":"none","valuesPrinted":false}\n' \
"$( [ -n "$crd" ] && [ "$controller_ready" != "0/0" ] && echo true || echo false )" \
"$( [ -n "$crd" ] && echo true || echo false )" \
"$(json_string "$controller_ready")" \
"$(json_string "$repository_condition")" \
"$(json_string "$UNIDESK_PAC_REPOSITORY_NAME")" \
"$(json_string "$UNIDESK_PAC_GITEA_OWNER")" \
"$(json_string "$UNIDESK_PAC_GITEA_REPO")" \
"$(json_string "$UNIDESK_PAC_REPOSITORY_URL")" \
"$(json_string "$UNIDESK_PAC_CONSUMER_ID")" \
"$(json_string "$UNIDESK_PAC_DISPLAY_TIME_ZONE")" \
"$consumers" "$rows" "$errors" "$hooks"
}
webhook_test_action() {
hooks=$(hook_summary)
hook_id=$(printf '%s' "$hooks" | node -e 'const fs=require("fs"); const a=JSON.parse(fs.readFileSync(0,"utf8")||"[]"); process.stdout.write(String(a[0]?.id||""))')
test -n "$hook_id"
gitea_api POST "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks/$hook_id/tests" '{}' >/dev/null
printf '{"ok":true,"mutation":true,"webhookTestSent":true,"hookId":"%s","valuesPrinted":false}\n' "$(json_string "$hook_id")"
}
case "$UNIDESK_PAC_ACTION" in
apply) apply_action ;;
status) status_action ;;
history) history_action ;;
webhook-test) webhook_test_action ;;
*) printf '{"ok":false,"error":"unsupported-action","valuesPrinted":false}\n'; exit 2 ;;
esac