fix: render pac history times from yaml timezone

This commit is contained in:
Codex
2026-07-06 00:53:33 +00:00
parent ed718f939a
commit cfc1b5bf9f
3 changed files with 52 additions and 5 deletions
@@ -14,6 +14,9 @@ defaults:
targetId: JD01
consumerId: agentrun-jd01-v02
display:
timeZone: Asia/Shanghai
release:
version: v0.48.0
manifestUrl: https://github.com/tektoncd/pipelines-as-code/releases/download/v0.48.0/release.k8s.yaml
@@ -265,6 +265,17 @@ 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 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) {
try {
@@ -293,6 +304,13 @@ function durationSeconds(start, done) {
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 || []) {
@@ -435,6 +453,10 @@ function rowFor(consumer, item, taskRuns) {
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,
@@ -650,7 +672,7 @@ history_action() {
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||[]));')
printf '{"ok":%s,"crdPresent":%s,"controllerReady":"%s","repositoryCondition":"%s","repository":{"name":"%s","repo":"%s/%s","url":"%s"},"consumer":"%s","consumerRows":%s,"rows":%s,"webhooks":%s,"source":"gitea-pac-tekton-live","historyStore":"none","valuesPrinted":false}\n' \
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,"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")" \
@@ -660,6 +682,7 @@ history_action() {
"$(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" "$hooks"
}
@@ -50,6 +50,9 @@ interface PacConfig {
targetId: string;
consumerId: string;
};
display: {
timeZone: string;
};
release: {
version: string;
manifestUrl: string;
@@ -192,6 +195,7 @@ function readPacConfig(): PacConfig {
const root = readYamlRecord<Record<string, unknown>>(configFile, "platform-infra-pipelines-as-code");
const release = y.objectField(root, "release", "");
const gitea = y.objectField(root, "gitea", "");
const display = y.objectField(root, "display", "");
const admin = y.objectField(gitea, "admin", "gitea");
const webhook = y.objectField(gitea, "webhook", "gitea");
const repositories = root.repositories === undefined
@@ -214,6 +218,9 @@ function readPacConfig(): PacConfig {
targetId: y.stringField(defaults, "targetId", "defaults"),
consumerId: typeof defaults.consumerId === "string" && defaults.consumerId.length > 0 ? defaults.consumerId : consumers[0]?.id ?? "default",
},
display: {
timeZone: y.stringField(display, "timeZone", "display"),
},
release: {
version: y.stringField(release, "version", "release"),
manifestUrl: urlField(release, "manifestUrl", "release"),
@@ -313,6 +320,7 @@ function validateConfig(config: PacConfig): void {
if (repository.namespace !== consumer.namespace) throw new Error(`${configLabel}.consumers.${consumer.id}.namespace must match repository namespace`);
}
if (!config.gitea.webhook.events.includes("push")) throw new Error(`${configLabel}.gitea.webhook.events must include push`);
validateTimeZone(config.display.timeZone, `${configLabel}.display.timeZone`);
}
function resolveTarget(config: PacConfig, targetId: string | null): PacTarget {
@@ -426,6 +434,7 @@ async function history(config: UniDeskConfig, options: HistoryOptions): Promise<
path: configLabel,
source: "Gitea Repository CR + Tekton PipelineRun/TaskRun live objects; no UniDesk-owned history store is written.",
limitPerConsumer: options.limit,
displayTimeZone: pac.display.timeZone,
valuesPrinted: false,
},
consumers: selectedConsumers.map((consumer) => consumer.id),
@@ -497,6 +506,7 @@ function remoteScript(action: "apply" | "status" | "history" | "webhook-test", p
UNIDESK_PAC_HISTORY_LIMIT: "limit" in options ? String(options.limit) : "5",
UNIDESK_PAC_HISTORY_ID: "detailId" in options && options.detailId !== null ? options.detailId : "",
UNIDESK_PAC_HISTORY_CONSUMERS_B64: Buffer.from(JSON.stringify(historyConsumers.map((item) => historyConsumerConfig(pac, item))), "utf8").toString("base64"),
UNIDESK_PAC_DISPLAY_TIME_ZONE: pac.display.timeZone,
UNIDESK_PAC_ARGO_NAMESPACE: consumer.argoNamespace,
UNIDESK_PAC_ARGO_APPLICATION: consumer.argoApplication,
UNIDESK_PAC_PART_OF: pacPartOf(consumer.id),
@@ -642,6 +652,7 @@ function configSummary(pac: PacConfig, consumer: PacConsumer, repository: PacRep
metadata: pac.metadata,
release: pac.release,
gitea: { configRef: pac.gitea.configRef, internalBaseUrl: pac.gitea.internalBaseUrl, webhookBranch: pac.gitea.webhook.branch },
display: pac.display,
repositories: pac.repositories.map(repositorySummary),
consumers: pac.consumers,
repository: repositorySummary(repository),
@@ -657,6 +668,7 @@ function compactConfigSummary(pac: PacConfig, consumer: PacConsumer, repository:
repository: repository.name,
providerType: repository.providerType,
sourceUrl: repository.cloneUrl,
displayTimeZone: pac.display.timeZone,
consumer: `${consumer.node}/${consumer.lane}`,
consumerId: consumer.id,
pipeline: consumer.pipeline,
@@ -775,14 +787,15 @@ function renderHistory(result: Record<string, unknown>): RenderedCliResult {
const rows = arrayRecords(result.rows);
const config = record(result.config);
const detailId = stringValue(result.detailId);
const timeHeader = `TIME(${stringValue(config.displayTimeZone)})`;
const lines = [
"PLATFORM-INFRA PIPELINES-AS-CODE HISTORY",
`DATA: ${stringValue(config.source)}`,
`ROWS: ${stringValue(rows.length)} LIMIT_PER_CONSUMER: ${stringValue(config.limitPerConsumer)}`,
"",
"TRIGGERS",
...(rows.length === 0 ? ["-"] : table(["TIME", "CONSUMER", "REPO", "STATUS", "DUR_S", "COMMIT", "ENV_REUSE", "ID"], rows.map((row) => [
isoShort(stringValue(row.triggeredAt)),
...(rows.length === 0 ? ["-"] : table([timeHeader, "CONSUMER", "REPO", "STATUS", "DUR_S", "COMMIT", "ENV_REUSE", "ID"], rows.map((row) => [
stringValue(row.displayTime) === "-" ? isoShort(stringValue(row.triggeredAt)) : stringValue(row.displayTime),
stringValue(row.consumer),
stringValue(row.repo),
statusText(row),
@@ -920,6 +933,14 @@ function urlField(obj: Record<string, unknown>, key: string, path: string): stri
return value.replace(/\/+$/u, "");
}
function validateTimeZone(value: string, path: string): void {
try {
new Intl.DateTimeFormat("en-US", { timeZone: value }).format(new Date(0));
} catch (error) {
throw new Error(`${path} must be a valid IANA time zone: ${(error as Error).message}`);
}
}
function stringRecord(obj: Record<string, unknown>, path: string): Record<string, string> {
const out: Record<string, string> = {};
for (const [key, value] of Object.entries(obj)) {
@@ -1001,8 +1022,8 @@ function renderHistoryDetail(row: Record<string, unknown>): string[] {
["pipeline", stringValue(row.pipeline)],
["branch", stringValue(row.branch)],
["commit", stringValue(row.commit)],
["started", stringValue(row.startTime)],
["completed", stringValue(row.completionTime)],
["started", stringValue(row.displayStartTime) === "-" ? stringValue(row.startTime) : stringValue(row.displayStartTime)],
["completed", stringValue(row.displayCompletionTime) === "-" ? stringValue(row.completionTime) : stringValue(row.displayCompletionTime)],
["taskRuns", stringValue(taskRuns.count)],
["longestTask", stringValue(longest.name)],
["longestTaskDurationS", stringValue(longest.durationSeconds)],