返回 DeepSeek-Reasonix
ci-timings.mjs
根目录 / scripts / ci-timings.mjs
1 #!/usr/bin/env node
2
3 import { readFileSync, appendFileSync, writeFileSync } from "node:fs";
4 import { fileURLToPath } from "node:url";
5 import path from "node:path";
6
7 function milliseconds(start, end) {
8 if (!start || !end) return null;
9 return Math.max(0, Date.parse(end) - Date.parse(start));
10 }
11
12 function format(ms) {
13 if (ms === null || !Number.isFinite(ms)) return "running";
14 const seconds = Math.round(ms / 1000);
15 return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
16 }
17
18 function stageKind(job, step) {
19 if (step === "Resolve reviewed source") return "release control preflight";
20 if (step === "Build each CLI binary once and package both surfaces") return "shared CLI/npm build";
21 if (step === "Build and package") return "Desktop build";
22 if (/^Finalize (amd64|arm64) in the shared Certum session$/.test(step)) return "Windows signing";
23 if (/Install, launch, migrate, restart, and preserve legacy session/.test(step)) return "Windows acceptance";
24 if (step === "Run universal DMG smoke on Intel hardware") return "macOS acceptance";
25 if (step === "Seal candidate record") return "candidate sealing";
26 if (step === "Verify payload provenance, bytes, source, and operation") return "candidate verification";
27 if (step === "Create or verify all implementation tags") return "release activation";
28 if (step === "Verify public artifacts") return "public verification";
29 if (step === "Dispatch and wait for the owned Pages deployment") return "site deployment";
30 if (/Install (frontend|memory) dependencies/.test(step)) return "dependency install";
31 if (step === "Install browser runtimes") return "browser setup";
32 if (/Build (stable|canary|memory) frontend/.test(step)) return "frontend build";
33 if (job.startsWith("desktop-browser-group") && step === "Test desktop browser group") return "browser group";
34 if (job === "desktop-windows-go" && step === "test (Windows desktop and update helper)") return "Go test";
35 if (job.startsWith("shard (") && step === "Run complete independent memory process") return "memory shard";
36 return null;
37 }
38
39 export function timingReport(run, jobsPayload, { now = new Date() } = {}) {
40 const jobs = (jobsPayload.jobs ?? []).filter(job => job.started_at && job.conclusion !== "skipped");
41 const endTimes = jobs.map(job => job.completed_at).filter(Boolean).map(Date.parse);
42 const workflowEnd = endTimes.length === jobs.length && jobs.length > 0 ? Math.max(...endTimes) : now.getTime();
43 const workflowStart = Date.parse(run.created_at ?? run.run_started_at);
44 const rows = [];
45 const stages = [];
46 let runnerSum = 0;
47 let queueSum = 0;
48 for (const job of jobs) {
49 const execution = milliseconds(job.started_at, job.completed_at);
50 const queue = milliseconds(job.created_at, job.started_at);
51 if (execution !== null) runnerSum += execution;
52 if (queue !== null) queueSum += queue;
53 rows.push({ name: job.name, queue, execution, conclusion: job.conclusion ?? "running" });
54 for (const step of job.steps ?? []) {
55 const kind = stageKind(job.name, step.name);
56 if (kind) stages.push({ kind, name: `${job.name} / ${step.name}`, duration: milliseconds(step.started_at, step.completed_at), conclusion: step.conclusion ?? "running" });
57 }
58 }
59 return {
60 workflowElapsed: Math.max(0, workflowEnd - workflowStart),
61 runnerSum,
62 queueSum,
63 rows: rows.sort((a, b) => a.name.localeCompare(b.name)),
64 stages: stages.sort((a, b) => a.name.localeCompare(b.name)),
65 };
66 }
67
68 export function timingMarkdown(report, title = "CI timing") {
69 const lines = [
70 `## ${title}`,
71 "",
72 `- Total workflow wait: **${format(report.workflowElapsed)}**`,
73 `- Sum of runner execution: **${format(report.runnerSum)}**`,
74 `- Sum of recorded job queue time: **${format(report.queueSum)}**`,
75 "",
76 "Step durations exclude job queue time. Total workflow wait is wall time from workflow creation through the latest completed job.",
77 ];
78 if (report.stages.length) {
79 lines.push("", "| Stage | Step | Execution | Result |", "| --- | --- | ---: | --- |");
80 for (const stage of report.stages) lines.push(`| ${stage.kind} | ${stage.name} | ${format(stage.duration)} | ${stage.conclusion} |`);
81 }
82 lines.push("", "<details><summary>Job timing</summary>", "", "| Job | Queue | Execution | Result |", "| --- | ---: | ---: | --- |");
83 for (const row of report.rows) lines.push(`| ${row.name} | ${format(row.queue)} | ${format(row.execution)} | ${row.conclusion} |`);
84 lines.push("", "</details>", "");
85 return lines.join("\n");
86 }
87
88 function args(argv) {
89 const out = {};
90 for (let i = 0; i < argv.length; i += 2) {
91 if (!argv[i]?.startsWith("--") || argv[i + 1] === undefined) throw new Error(`invalid argument ${argv[i] ?? ""}`);
92 out[argv[i].slice(2)] = argv[i + 1];
93 }
94 return out;
95 }
96
97 if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
98 try {
99 const options = args(process.argv.slice(2));
100 const run = JSON.parse(readFileSync(options.run, "utf8"));
101 const jobs = JSON.parse(readFileSync(options.jobs, "utf8"));
102 const report = timingReport(run, jobs);
103 const markdown = timingMarkdown(report, options.title);
104 if (options.output) writeFileSync(options.output, `${JSON.stringify(report, null, 2)}\n`);
105 if (options.summary) appendFileSync(options.summary, markdown);
106 else process.stdout.write(markdown);
107 } catch (error) {
108 console.error(`ci-timings: ${error.message}`);
109 process.exitCode = 1;
110 }
111 }
112
112 lines Plain Text