diff --git a/config/platform-infra/pipelines-as-code.yaml b/config/platform-infra/pipelines-as-code.yaml index 2c0843e4..fb4b255a 100644 --- a/config/platform-infra/pipelines-as-code.yaml +++ b/config/platform-infra/pipelines-as-code.yaml @@ -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 diff --git a/scripts/src/platform-infra-pipelines-as-code-remote.sh b/scripts/src/platform-infra-pipelines-as-code-remote.sh index faf9cc0c..6649b2d0 100644 --- a/scripts/src/platform-infra-pipelines-as-code-remote.sh +++ b/scripts/src/platform-infra-pipelines-as-code-remote.sh @@ -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" } diff --git a/scripts/src/platform-infra-pipelines-as-code.ts b/scripts/src/platform-infra-pipelines-as-code.ts index 61cd28d4..b35d0e17 100644 --- a/scripts/src/platform-infra-pipelines-as-code.ts +++ b/scripts/src/platform-infra-pipelines-as-code.ts @@ -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>(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): 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, 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, path: string): Record { const out: Record = {}; for (const [key, value] of Object.entries(obj)) { @@ -1001,8 +1022,8 @@ function renderHistoryDetail(row: Record): 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)],