@desplega.ai/agent-swarm 1.103.0 → 1.105.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/{actions-7txktrdj.js → actions-45g8tvc8.js} +6 -6
- package/dist/{app-xm3xsdye.js → app-18x5k1cj.js} +3 -3
- package/dist/{assistant-qpv63wk6.js → assistant-ka281qe8.js} +9 -10
- package/dist/{boot-reembed-9cxqka3e.js → boot-reembed-f38vbxw8.js} +4 -3
- package/dist/{boot-reembed-chxhrwv8.js → boot-reembed-rp2gba79.js} +5 -4
- package/dist/{boot-scrub-logs-8qtfkcz8.js → boot-scrub-logs-nsnc9f1a.js} +2 -2
- package/dist/{cli-gzepjz54.js → cli-0e24r2b6.js} +900 -352
- package/dist/{cli-dz3d6zjg.js → cli-0n4xxmy0.js} +121 -2
- package/dist/{cli-jt9d9fjw.js → cli-0s37sw3k.js} +1 -1
- package/dist/{cli-yeemwevj.js → cli-2hg6bj31.js} +20 -16
- package/dist/{cli-wjyyv518.js → cli-3j01d33e.js} +9 -14
- package/dist/{cli-xmtsan9k.js → cli-4df7btj0.js} +1 -1
- package/dist/{cli-8w95v19d.js → cli-634jy7sa.js} +351 -24
- package/dist/{cli-m8f6qzck.js → cli-7esh9fda.js} +1 -1
- package/dist/{cli-4fnp2tc3.js → cli-87fj1jk4.js} +1 -1
- package/dist/{cli-ksq38x4n.js → cli-8y7y1vmp.js} +2 -2
- package/dist/{cli-rvae030h.js → cli-ba47wf2q.js} +3 -3
- package/dist/{cli-2qynerth.js → cli-c4h8m0m3.js} +1 -1
- package/dist/{cli-fygaw191.js → cli-dd6tegmp.js} +2 -2
- package/dist/{cli-85pf4z38.js → cli-ehcjr5vs.js} +5 -3
- package/dist/{cli-rttgde5f.js → cli-gq7gvvrn.js} +1 -1
- package/dist/{cli-kk51f3tf.js → cli-gzmv1371.js} +5 -5
- package/dist/{http-zts4haq7.js → cli-sdf2qwkt.js} +83970 -76419
- package/dist/{cli-krvxq8bc.js → cli-sm9yseh1.js} +23 -4
- package/dist/{cli-8hyv6c2v.js → cli-v2fb1fxf.js} +41 -23
- package/dist/{cli-fwxdt7kt.js → cli-vpwffend.js} +30 -5
- package/dist/{cli-fedn86nz.js → cli-wztagmpk.js} +8 -3
- package/dist/{cli-v9283nma.js → cli-yd29d4n4.js} +1 -1
- package/dist/{cli-yfwwjeft.js → cli-yfyqsqzj.js} +1 -1
- package/dist/{cli-ns0r7mkr.js → cli-yzr0qxx1.js} +3 -3
- package/dist/{cli-s14sb64w.js → cli-zmta7t3y.js} +5 -7
- package/dist/cli.js +12 -10
- package/dist/{commands-aqn63mcw.js → commands-tn8zj0an.js} +2 -2
- package/dist/{db-azrvwsnj.js → db-gtqymk7r.js} +2 -2
- package/dist/{handlers-hf61har3.js → handlers-5xdqk40d.js} +10 -12
- package/dist/{hook-306p3yz4.js → hook-gj6j6zwj.js} +7 -5
- package/dist/http-4pceba9h.js +17374 -0
- package/dist/{index-bfh9hgek.js → index-0rfp6wh2.js} +5 -5
- package/dist/{index-3c651yfk.js → index-as4evz9g.js} +14 -11
- package/dist/{index-h2yqjyk0.js → index-paaw4xt5.js} +9 -8
- package/dist/{index-29vg51x6.js → index-wf0q4k03.js} +23 -10
- package/dist/{keepalive-a0dg5pds.js → keepalive-akbzjt41.js} +4 -4
- package/dist/{lead-7mcxedkc.js → lead-g8v85nf4.js} +19 -19
- package/dist/{maintenance-mrzqd53t.js → maintenance-9yrzmrh8.js} +5 -4
- package/dist/{onboard-4dkpv9wa.js → onboard-v5bx0kpy.js} +2 -2
- package/dist/{otel-impl-jt7gpyp9.js → otel-impl-530442da.js} +4510 -4465
- package/dist/{pricing-refresh-xqnzh8dt.js → pricing-refresh-dt289v11.js} +4 -4
- package/dist/{seed-pricing-56tjm1fj.js → seed-pricing-183er9b3.js} +3 -3
- package/dist/{setup-tbwk8j10.js → setup-tjgsvznx.js} +2 -2
- package/dist/{worker-e03za92t.js → worker-nxcxk5pb.js} +19 -19
- package/openapi.json +90 -4
- package/package.json +5 -3
- package/src/be/memory/constants.ts +6 -0
- package/src/be/memory/providers/sqlite-store.ts +468 -71
- package/src/be/memory/raters/retrieval.ts +4 -2
- package/src/be/memory/reranker.ts +5 -2
- package/src/be/memory/retrieval-store.ts +2 -0
- package/src/be/memory/types.ts +46 -0
- package/src/be/migrations/099_memory_structured_key_versioning.sql +41 -0
- package/src/be/migrations/100_memory_retrieval_source.sql +8 -0
- package/src/be/migrations/101_script_connections.sql +67 -0
- package/src/be/script-connections.ts +744 -0
- package/src/be/script-credential-broker.ts +38 -0
- package/src/be/scripts/typecheck.ts +23 -2
- package/src/be/seed-scripts/catalog/boot-triage.inline.ts +16 -4
- package/src/be/seed-scripts/catalog/boot-triage.ts +16 -4
- package/src/commands/runner.ts +8 -6
- package/src/heartbeat/heartbeat.ts +42 -1
- package/src/heartbeat/index.ts +7 -1
- package/src/http/memory.ts +127 -14
- package/src/http/pages-public.ts +123 -2
- package/src/http/scripts.ts +15 -14
- package/src/http/session-data.ts +16 -0
- package/src/otel-impl.ts +73 -0
- package/src/otel.ts +12 -0
- package/src/scripts-runtime/api-client.ts +100 -0
- package/src/scripts-runtime/api-types.ts +45 -0
- package/src/scripts-runtime/credential-broker/README.md +40 -0
- package/src/scripts-runtime/credential-broker/broker.ts +51 -0
- package/src/scripts-runtime/credential-broker/default-bindings.ts +19 -0
- package/src/scripts-runtime/credential-broker/fetch-patch.ts +102 -0
- package/src/scripts-runtime/credential-broker/index.ts +17 -0
- package/src/scripts-runtime/credential-broker/store.ts +72 -0
- package/src/scripts-runtime/credential-broker/types.ts +54 -0
- package/src/scripts-runtime/ctx.ts +11 -1
- package/src/scripts-runtime/egress-secrets.ts +14 -76
- package/src/scripts-runtime/eval-harness.ts +1 -1
- package/src/scripts-runtime/executors/types.ts +5 -5
- package/src/scripts-runtime/loader.ts +6 -1
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +11 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +11 -0
- package/src/server.ts +6 -0
- package/src/tests/credential-broker.test.ts +281 -0
- package/src/tests/heartbeat.test.ts +239 -0
- package/src/tests/memory-edit.test.ts +177 -0
- package/src/tests/memory-hybrid.test.ts +212 -0
- package/src/tests/otel-impl-secret-scrubbing.test.ts +101 -3
- package/src/tests/otel-session-cost-metrics.test.ts +281 -0
- package/src/tests/pages-authed-mode.test.ts +50 -0
- package/src/tests/runner-repo-autostash.test.ts +39 -8
- package/src/tests/script-connections.test.ts +360 -0
- package/src/tests/scripts-http.test.ts +16 -10
- package/src/tests/scripts-runtime-secret-egress.test.ts +26 -5
- package/src/tests/swarm-config-reserved-keys.test.ts +23 -0
- package/src/tests/update-schedule-mcp-tool.test.ts +83 -4
- package/src/tools/credential-bindings/index.ts +1 -0
- package/src/tools/credential-bindings/tool.ts +238 -0
- package/src/tools/memory-edit.ts +148 -0
- package/src/tools/memory-search.ts +17 -10
- package/src/tools/schedules/delete-schedule.ts +18 -8
- package/src/tools/schedules/update-schedule.ts +4 -9
- package/src/tools/script-connections/index.ts +1 -0
- package/src/tools/script-connections/tool.ts +167 -0
- package/src/tools/swarm-config/delete-config.ts +5 -0
- package/src/tools/swarm-config/set-config.ts +23 -1
- package/src/tools/tool-config.ts +6 -1
- package/src/types.ts +6 -0
- package/src/workflows/executors/swarm-script.ts +2 -0
- package/templates/community/dora-metrics/PLAYBOOK.md +294 -0
- package/templates/community/dora-metrics/README.md +39 -0
- package/templates/community/dora-metrics/lead-prompt.md +56 -0
- package/templates/community/dora-metrics/report.mjs +393 -0
- package/templates/community/dora-metrics/run.sh +113 -0
- package/templates/schedules/weekly-dora-metrics/config.json +13 -0
- package/templates/schedules/weekly-dora-metrics/content.md +61 -0
- package/dist/cli-2307phk1.js +0 -24223
- package/dist/cli-2deg0cen.js +0 -120
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
const [outDir, repoDir, repoName, runDate, branch = "main", windowDaysArg = "90", hotfixWindowArg = "24", tagPattern = "v*"] = process.argv.slice(2);
|
|
6
|
+
if (!outDir || !repoDir || !repoName || !runDate) {
|
|
7
|
+
console.error("Usage: node report.mjs <outDir> <repoDir> <repoName> <runDate> [branch] [windowDays] [hotfixWindowHours] [tagPattern]");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const windowDays = Number(windowDaysArg);
|
|
12
|
+
const hotfixWindowHours = Number(hotfixWindowArg);
|
|
13
|
+
const now = new Date(`${runDate}T23:59:59Z`);
|
|
14
|
+
const start = new Date(now.getTime() - windowDays * 24 * 60 * 60 * 1000);
|
|
15
|
+
|
|
16
|
+
const CONFIG = {
|
|
17
|
+
deploymentFrequency: {
|
|
18
|
+
elitePerDay: 1,
|
|
19
|
+
highPerDay: 1 / 7,
|
|
20
|
+
mediumPerDay: 1 / 30,
|
|
21
|
+
},
|
|
22
|
+
leadTimeHours: {
|
|
23
|
+
eliteMax: 24,
|
|
24
|
+
highMax: 24 * 7,
|
|
25
|
+
mediumMax: 24 * 30,
|
|
26
|
+
},
|
|
27
|
+
changeFailureRate: {
|
|
28
|
+
eliteMax: 0.05,
|
|
29
|
+
highMax: 0.2,
|
|
30
|
+
mediumMax: 0.1,
|
|
31
|
+
},
|
|
32
|
+
failedDeploymentRecoveryHours: {
|
|
33
|
+
eliteMax: 1,
|
|
34
|
+
highMax: 24,
|
|
35
|
+
mediumMax: 24,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const read = (file) => fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
40
|
+
const esc = (value) => String(value ?? "")
|
|
41
|
+
.replaceAll("&", "&")
|
|
42
|
+
.replaceAll("<", "<")
|
|
43
|
+
.replaceAll(">", ">")
|
|
44
|
+
.replaceAll('"', """);
|
|
45
|
+
const hoursBetween = (a, b) => Math.max(0, (b.getTime() - a.getTime()) / 36e5);
|
|
46
|
+
const fmtHours = (hours) => {
|
|
47
|
+
if (!Number.isFinite(hours)) return "n/a";
|
|
48
|
+
if (hours < 48) return `${Math.round(hours * 10) / 10}h`;
|
|
49
|
+
return `${Math.round((hours / 24) * 10) / 10}d`;
|
|
50
|
+
};
|
|
51
|
+
const fmtPct = (value) => `${Math.round(value * 1000) / 10}%`;
|
|
52
|
+
const median = (values) => {
|
|
53
|
+
const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
|
|
54
|
+
if (!sorted.length) return null;
|
|
55
|
+
const mid = Math.floor(sorted.length / 2);
|
|
56
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
57
|
+
};
|
|
58
|
+
const git = (args) => execFileSync("git", ["-C", repoDir, ...args], { encoding: "utf8" }).trim();
|
|
59
|
+
|
|
60
|
+
function parseTsv(text, fields) {
|
|
61
|
+
return text.trim().split("\n").filter(Boolean).map((line) => {
|
|
62
|
+
const parts = line.split("\t");
|
|
63
|
+
return Object.fromEntries(fields.map((field, i) => [field, parts[i] ?? ""]));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function classifyDeploymentFrequency(perDay) {
|
|
68
|
+
if (perDay >= CONFIG.deploymentFrequency.elitePerDay) return "Elite";
|
|
69
|
+
if (perDay >= CONFIG.deploymentFrequency.highPerDay) return "High";
|
|
70
|
+
if (perDay >= CONFIG.deploymentFrequency.mediumPerDay) return "Medium";
|
|
71
|
+
return "Low";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function classifyLeadTime(hours) {
|
|
75
|
+
if (!Number.isFinite(hours)) return "Unknown";
|
|
76
|
+
if (hours < CONFIG.leadTimeHours.eliteMax) return "Elite";
|
|
77
|
+
if (hours <= CONFIG.leadTimeHours.highMax) return "High";
|
|
78
|
+
if (hours <= CONFIG.leadTimeHours.mediumMax) return "Medium";
|
|
79
|
+
return "Low";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function classifyChangeFailureRate(rate) {
|
|
83
|
+
if (!Number.isFinite(rate)) return "Unknown";
|
|
84
|
+
if (rate <= CONFIG.changeFailureRate.eliteMax) return "Elite";
|
|
85
|
+
if (rate <= CONFIG.changeFailureRate.mediumMax) return "Medium";
|
|
86
|
+
if (rate <= CONFIG.changeFailureRate.highMax) return "High";
|
|
87
|
+
return "Low";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function classifyRecovery(hours) {
|
|
91
|
+
if (!Number.isFinite(hours)) return "Unknown";
|
|
92
|
+
if (hours < CONFIG.failedDeploymentRecoveryHours.eliteMax) return "Elite";
|
|
93
|
+
if (hours <= CONFIG.failedDeploymentRecoveryHours.highMax) return "High";
|
|
94
|
+
if (hours <= CONFIG.failedDeploymentRecoveryHours.mediumMax) return "Medium";
|
|
95
|
+
return "Low";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function commitsBetween(fromRef, toRef) {
|
|
99
|
+
const range = fromRef ? `${fromRef}..${toRef}` : toRef;
|
|
100
|
+
const text = git(["log", range, "--format=%H%x09%ct%x09%s", "--no-merges"]);
|
|
101
|
+
return parseTsv(text, ["sha", "unix", "subject"]).map((row) => ({
|
|
102
|
+
...row,
|
|
103
|
+
date: new Date(Number(row.unix) * 1000),
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const allTags = parseTsv(read(path.join(outDir, "tags.tsv")), ["name", "sha", "date", "unix"])
|
|
108
|
+
.map((tag) => ({ ...tag, at: new Date(tag.date), unix: Number(tag.unix) }))
|
|
109
|
+
.filter((tag) => tag.name && Number.isFinite(tag.unix))
|
|
110
|
+
.sort((a, b) => a.unix - b.unix);
|
|
111
|
+
|
|
112
|
+
const deployments = allTags.filter((tag) => tag.at >= start && tag.at <= now);
|
|
113
|
+
const leadSamples = [];
|
|
114
|
+
const deploymentRows = [];
|
|
115
|
+
|
|
116
|
+
for (const tag of deployments) {
|
|
117
|
+
const idx = allTags.findIndex((candidate) => candidate.name === tag.name);
|
|
118
|
+
const previous = idx > 0 ? allTags[idx - 1] : null;
|
|
119
|
+
const commits = commitsBetween(previous?.name, tag.name);
|
|
120
|
+
const samples = commits
|
|
121
|
+
.map((commit) => hoursBetween(commit.date, tag.at))
|
|
122
|
+
.filter((hours) => Number.isFinite(hours) && hours >= 0);
|
|
123
|
+
leadSamples.push(...samples);
|
|
124
|
+
deploymentRows.push({
|
|
125
|
+
tag: tag.name,
|
|
126
|
+
date: tag.date,
|
|
127
|
+
commitCount: commits.length,
|
|
128
|
+
medianLeadTimeHours: median(samples),
|
|
129
|
+
previousTag: previous?.name ?? null,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const remediationCommits = parseTsv(read(path.join(outDir, "remediation-commits.tsv")), ["sha", "unix", "author", "subject"])
|
|
134
|
+
.map((row) => ({ ...row, at: new Date(Number(row.unix) * 1000), source: "commit" }));
|
|
135
|
+
|
|
136
|
+
const prs = JSON.parse(read(path.join(outDir, "prs.json")) || "[]");
|
|
137
|
+
const hotfixPrs = prs
|
|
138
|
+
.filter((pr) => pr?.mergedAt && /(revert|rollback|hotfix|fix-forward)/i.test(pr.title ?? ""))
|
|
139
|
+
.map((pr) => ({
|
|
140
|
+
sha: `PR #${pr.number}`,
|
|
141
|
+
at: new Date(pr.mergedAt),
|
|
142
|
+
author: pr.author?.login ?? "",
|
|
143
|
+
subject: pr.title,
|
|
144
|
+
url: pr.url,
|
|
145
|
+
source: "pull-request",
|
|
146
|
+
}))
|
|
147
|
+
.filter((signal) => signal.at >= start && signal.at <= now);
|
|
148
|
+
|
|
149
|
+
const remediationSignals = [...remediationCommits, ...hotfixPrs]
|
|
150
|
+
.filter((signal) => signal.at >= start && signal.at <= now)
|
|
151
|
+
.sort((a, b) => a.at - b.at);
|
|
152
|
+
|
|
153
|
+
const failureSignals = [];
|
|
154
|
+
const failedDeploymentNames = new Set();
|
|
155
|
+
for (const signal of remediationSignals) {
|
|
156
|
+
const fixing = deployments.find((tag) => tag.at >= signal.at) ?? deployments[deployments.length - 1];
|
|
157
|
+
const failing = [...deployments].reverse().find((tag) => {
|
|
158
|
+
const diff = (signal.at.getTime() - tag.at.getTime()) / 36e5;
|
|
159
|
+
return diff >= 0 && diff <= hotfixWindowHours;
|
|
160
|
+
});
|
|
161
|
+
if (!failing || !fixing) continue;
|
|
162
|
+
if (fixing.at < signal.at) continue;
|
|
163
|
+
if (fixing.name === failing.name) continue;
|
|
164
|
+
failedDeploymentNames.add(failing.name);
|
|
165
|
+
failureSignals.push({
|
|
166
|
+
source: signal.source,
|
|
167
|
+
subject: signal.subject,
|
|
168
|
+
sha: signal.sha,
|
|
169
|
+
url: signal.url,
|
|
170
|
+
signalAt: signal.at.toISOString(),
|
|
171
|
+
failingTag: failing.name,
|
|
172
|
+
failingAt: failing.at.toISOString(),
|
|
173
|
+
fixingTag: fixing.name,
|
|
174
|
+
fixingAt: fixing.at.toISOString(),
|
|
175
|
+
recoveryHours: hoursBetween(failing.at, fixing.at),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const deploymentCount = deployments.length;
|
|
180
|
+
const deploymentFrequencyPerDay = deploymentCount / windowDays;
|
|
181
|
+
const leadTimeMedianHours = median(leadSamples);
|
|
182
|
+
const changeFailureRate = deploymentCount ? failedDeploymentNames.size / deploymentCount : 0;
|
|
183
|
+
const recoveryMedianHours = median(failureSignals.map((signal) => signal.recoveryHours));
|
|
184
|
+
|
|
185
|
+
const metrics = {
|
|
186
|
+
deploymentFrequency: {
|
|
187
|
+
label: "Deployment Frequency",
|
|
188
|
+
quality: "EXACT",
|
|
189
|
+
value: deploymentFrequencyPerDay,
|
|
190
|
+
display: `${Math.round(deploymentFrequencyPerDay * 100) / 100}/day`,
|
|
191
|
+
band: classifyDeploymentFrequency(deploymentFrequencyPerDay),
|
|
192
|
+
detail: `${deploymentCount} ${tagPattern} deployments over ${windowDays} days`,
|
|
193
|
+
source: `${tagPattern} git tags mapped to production releases`,
|
|
194
|
+
},
|
|
195
|
+
leadTimeForChanges: {
|
|
196
|
+
label: "Lead Time for Changes",
|
|
197
|
+
quality: "EXACT",
|
|
198
|
+
valueHours: leadTimeMedianHours,
|
|
199
|
+
display: fmtHours(leadTimeMedianHours),
|
|
200
|
+
band: classifyLeadTime(leadTimeMedianHours),
|
|
201
|
+
detail: `Median across ${leadSamples.length} non-merge commits included in release tags`,
|
|
202
|
+
source: "Commit timestamp to containing release tag timestamp",
|
|
203
|
+
},
|
|
204
|
+
changeFailureRate: {
|
|
205
|
+
label: "Change Failure Rate",
|
|
206
|
+
quality: "PROXY",
|
|
207
|
+
value: changeFailureRate,
|
|
208
|
+
display: fmtPct(changeFailureRate),
|
|
209
|
+
band: classifyChangeFailureRate(changeFailureRate),
|
|
210
|
+
detail: `${failedDeploymentNames.size} failed-release proxies / ${deploymentCount} deployments`,
|
|
211
|
+
source: `Revert, rollback, hotfix, and fix-forward signals within ${hotfixWindowHours}h of a release`,
|
|
212
|
+
},
|
|
213
|
+
failedDeploymentRecoveryTime: {
|
|
214
|
+
label: "Failed Deployment Recovery Time",
|
|
215
|
+
quality: "PROXY",
|
|
216
|
+
valueHours: recoveryMedianHours,
|
|
217
|
+
display: fmtHours(recoveryMedianHours),
|
|
218
|
+
band: classifyRecovery(recoveryMedianHours),
|
|
219
|
+
detail: `${failureSignals.length} remediation signal(s) paired to release tags`,
|
|
220
|
+
source: "Time from failed-release proxy tag to fixing tag",
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const summary = {
|
|
225
|
+
repoName,
|
|
226
|
+
runDate,
|
|
227
|
+
generatedAt: new Date().toISOString(),
|
|
228
|
+
branch,
|
|
229
|
+
window: {
|
|
230
|
+
days: windowDays,
|
|
231
|
+
start: start.toISOString(),
|
|
232
|
+
end: now.toISOString(),
|
|
233
|
+
},
|
|
234
|
+
tagPattern,
|
|
235
|
+
hotfixWindowHours,
|
|
236
|
+
commit: read(path.join(outDir, "revision.txt")).trim(),
|
|
237
|
+
commitSummary: read(path.join(outDir, "revision-summary.txt")).trim(),
|
|
238
|
+
metrics,
|
|
239
|
+
deployments: deploymentRows,
|
|
240
|
+
failureSignals,
|
|
241
|
+
notes: [
|
|
242
|
+
"Deployment Frequency and Lead Time for Changes are exact only when the configured tag pattern maps 1:1 to production releases.",
|
|
243
|
+
"Change Failure Rate and Failed Deployment Recovery Time are proxy estimates. They can miss manual incidents and can include non-incident remediation work.",
|
|
244
|
+
"Use a formal incident tracker or production deployment event stream before treating CFR/MTTR as precise operational truth.",
|
|
245
|
+
],
|
|
246
|
+
references: {
|
|
247
|
+
doraMetrics: "https://dora.dev/guides/dora-metrics/",
|
|
248
|
+
dora2024Report: "https://dora.dev/research/2024/dora-report/",
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
fs.writeFileSync(path.join(outDir, "summary.json"), JSON.stringify(summary, null, 2));
|
|
253
|
+
fs.writeFileSync(path.join(outDir, "latest-pointer.json"), JSON.stringify({
|
|
254
|
+
repoName,
|
|
255
|
+
runDate,
|
|
256
|
+
path: outDir,
|
|
257
|
+
latestJson: path.join(path.dirname(outDir), "latest.json"),
|
|
258
|
+
latestHtml: path.join(path.dirname(outDir), "latest.html"),
|
|
259
|
+
generatedAt: summary.generatedAt,
|
|
260
|
+
}, null, 2));
|
|
261
|
+
|
|
262
|
+
const dataJson = JSON.stringify(summary).replaceAll("</script", "<\\/script");
|
|
263
|
+
const bandClass = (band) => String(band ?? "Unknown").toLowerCase();
|
|
264
|
+
const metricCards = Object.values(metrics).map((metric) => `
|
|
265
|
+
<div class="metric ${esc(bandClass(metric.band))} ${esc(String(metric.quality).toLowerCase())}">
|
|
266
|
+
<span>${esc(metric.label)} <em>${esc(metric.quality)}</em></span>
|
|
267
|
+
<strong>${esc(metric.display)}</strong>
|
|
268
|
+
<b>${esc(metric.band)}</b>
|
|
269
|
+
<p>${esc(metric.detail)}</p>
|
|
270
|
+
</div>`).join("");
|
|
271
|
+
|
|
272
|
+
const deploymentRowsHtml = deploymentRows.slice(-30).reverse().map((row) => `
|
|
273
|
+
<tr>
|
|
274
|
+
<td>${esc(row.tag)}</td>
|
|
275
|
+
<td>${esc(row.date)}</td>
|
|
276
|
+
<td>${esc(row.commitCount)}</td>
|
|
277
|
+
<td>${esc(fmtHours(row.medianLeadTimeHours))}</td>
|
|
278
|
+
</tr>`).join("");
|
|
279
|
+
|
|
280
|
+
const failureRowsHtml = failureSignals.map((signal) => `
|
|
281
|
+
<tr>
|
|
282
|
+
<td>${esc(signal.failingTag)}</td>
|
|
283
|
+
<td>${esc(signal.fixingTag)}</td>
|
|
284
|
+
<td>${esc(fmtHours(signal.recoveryHours))}</td>
|
|
285
|
+
<td>${esc(signal.source)}</td>
|
|
286
|
+
<td>${signal.url ? `<a href="${esc(signal.url)}">${esc(signal.subject)}</a>` : esc(signal.subject)}</td>
|
|
287
|
+
</tr>`).join("") || `<tr><td colspan="5">No proxy remediation signals detected in this window.</td></tr>`;
|
|
288
|
+
|
|
289
|
+
const html = `<!doctype html>
|
|
290
|
+
<html lang="en">
|
|
291
|
+
<head>
|
|
292
|
+
<meta charset="utf-8" />
|
|
293
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
294
|
+
<title>DORA Metrics - ${esc(repoName)}</title>
|
|
295
|
+
<style>
|
|
296
|
+
body { margin: 0; background: #f7f6f3; color: #181817; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.5; }
|
|
297
|
+
main { width: min(1180px, calc(100% - 32px)); margin: 0 auto; padding: 48px 0 80px; }
|
|
298
|
+
h1 { font-size: clamp(2rem, 5vw, 4rem); line-height: 1; margin: 0 0 12px; }
|
|
299
|
+
h2 { margin-top: 40px; }
|
|
300
|
+
p, .muted { color: #686660; }
|
|
301
|
+
.grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin: 28px 0; }
|
|
302
|
+
.metric, .card, .callout { background: #fff; border: 1px solid #e4ded3; border-radius: 8px; padding: 18px; }
|
|
303
|
+
.metric span { display: flex; justify-content: space-between; gap: 8px; color: #686660; font-size: 12px; font-weight: 700; text-transform: uppercase; }
|
|
304
|
+
.metric em { color: #181817; font-style: normal; }
|
|
305
|
+
.metric strong { display: block; margin-top: 8px; font-size: 30px; }
|
|
306
|
+
.metric b { display: inline-block; margin-top: 8px; padding: 2px 8px; border-radius: 999px; background: #f0ece5; font-size: 12px; text-transform: uppercase; }
|
|
307
|
+
.metric.proxy { border-color: #d4a84f; }
|
|
308
|
+
.charts { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
309
|
+
.chart { height: 320px; }
|
|
310
|
+
svg { width: 100%; height: 100%; display: block; }
|
|
311
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; background: #fff; border: 1px solid #e4ded3; border-radius: 8px; overflow: hidden; }
|
|
312
|
+
th, td { padding: 8px 10px; border-bottom: 1px solid #eee8dc; text-align: left; vertical-align: top; }
|
|
313
|
+
th { color: #686660; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }
|
|
314
|
+
td { max-width: 520px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
315
|
+
code { background: #fff; border: 1px solid #e4ded3; padding: 1px 5px; border-radius: 4px; }
|
|
316
|
+
a { color: #8b5a00; }
|
|
317
|
+
@media (max-width: 920px) { .grid, .charts { grid-template-columns: 1fr; } }
|
|
318
|
+
</style>
|
|
319
|
+
</head>
|
|
320
|
+
<body>
|
|
321
|
+
<main>
|
|
322
|
+
<p class="muted">Generated ${esc(summary.generatedAt)}</p>
|
|
323
|
+
<h1>DORA Metrics</h1>
|
|
324
|
+
<p><strong>${esc(repoName)}</strong> at <code>${esc(summary.commit.slice(0, 12))}</code>. Window: ${esc(summary.window.start.slice(0, 10))} to ${esc(summary.window.end.slice(0, 10))}. Release signal: <code>${esc(tagPattern)}</code> tags.</p>
|
|
325
|
+
|
|
326
|
+
<section class="grid">
|
|
327
|
+
${metricCards}
|
|
328
|
+
</section>
|
|
329
|
+
|
|
330
|
+
<section class="callout">
|
|
331
|
+
<h2>Data-source reality check</h2>
|
|
332
|
+
<p>Deployment Frequency and Lead Time for Changes are <strong>exact</strong> for this configuration because release tags are treated as production deployments. Change Failure Rate and Failed Deployment Recovery Time are <strong>proxy estimates</strong> from remediation signals. They are useful for trend watching, not a substitute for incident tracking.</p>
|
|
333
|
+
</section>
|
|
334
|
+
|
|
335
|
+
<section class="charts">
|
|
336
|
+
<div class="card">
|
|
337
|
+
<h2>Deployments by week</h2>
|
|
338
|
+
<div id="deployments" class="chart"></div>
|
|
339
|
+
</div>
|
|
340
|
+
<div class="card">
|
|
341
|
+
<h2>Lead time by release</h2>
|
|
342
|
+
<div id="leadtime" class="chart"></div>
|
|
343
|
+
</div>
|
|
344
|
+
</section>
|
|
345
|
+
|
|
346
|
+
<h2>Recent deployments</h2>
|
|
347
|
+
<table>
|
|
348
|
+
<thead><tr><th>Tag</th><th>Date</th><th>Commits</th><th>Median lead time</th></tr></thead>
|
|
349
|
+
<tbody>${deploymentRowsHtml}</tbody>
|
|
350
|
+
</table>
|
|
351
|
+
|
|
352
|
+
<h2>Proxy failure signals</h2>
|
|
353
|
+
<table>
|
|
354
|
+
<thead><tr><th>Failing tag</th><th>Fixing tag</th><th>Recovery</th><th>Source</th><th>Signal</th></tr></thead>
|
|
355
|
+
<tbody>${failureRowsHtml}</tbody>
|
|
356
|
+
</table>
|
|
357
|
+
|
|
358
|
+
<h2>References</h2>
|
|
359
|
+
<ul>
|
|
360
|
+
<li><a href="https://dora.dev/guides/dora-metrics/">DORA metrics guide</a></li>
|
|
361
|
+
<li><a href="https://dora.dev/research/2024/dora-report/">2024 Accelerate State of DevOps Report</a></li>
|
|
362
|
+
</ul>
|
|
363
|
+
</main>
|
|
364
|
+
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
|
|
365
|
+
<script id="report-data" type="application/json">${dataJson}</script>
|
|
366
|
+
<script>
|
|
367
|
+
const data = JSON.parse(document.getElementById("report-data").textContent);
|
|
368
|
+
const deployments = data.deployments.map((d) => ({ ...d, dateObj: new Date(d.date), lead: d.medianLeadTimeHours || 0 }));
|
|
369
|
+
function drawBars(id, values, getX, getY) {
|
|
370
|
+
const el = document.getElementById(id);
|
|
371
|
+
const width = el.clientWidth || 520;
|
|
372
|
+
const height = el.clientHeight || 320;
|
|
373
|
+
const margin = { top: 16, right: 16, bottom: 42, left: 46 };
|
|
374
|
+
const svg = d3.select(el).append("svg").attr("viewBox", [0, 0, width, height]);
|
|
375
|
+
const x = d3.scaleBand().domain(values.map(getX)).range([margin.left, width - margin.right]).padding(0.2);
|
|
376
|
+
const y = d3.scaleLinear().domain([0, d3.max(values, getY) || 1]).nice().range([height - margin.bottom, margin.top]);
|
|
377
|
+
svg.append("g").attr("transform", "translate(0," + (height - margin.bottom) + ")").call(d3.axisBottom(x).tickValues(x.domain().filter((_, i) => i % Math.ceil(x.domain().length / 6) === 0))).selectAll("text").attr("transform", "rotate(-35)").style("text-anchor", "end");
|
|
378
|
+
svg.append("g").attr("transform", "translate(" + margin.left + ",0)").call(d3.axisLeft(y).ticks(5));
|
|
379
|
+
svg.append("g").selectAll("rect").data(values).join("rect")
|
|
380
|
+
.attr("x", (d) => x(getX(d)))
|
|
381
|
+
.attr("y", (d) => y(getY(d)))
|
|
382
|
+
.attr("width", x.bandwidth())
|
|
383
|
+
.attr("height", (d) => y(0) - y(getY(d)))
|
|
384
|
+
.attr("fill", "#b58118");
|
|
385
|
+
}
|
|
386
|
+
const byWeek = Array.from(d3.rollup(deployments, (v) => v.length, (d) => d3.utcFormat("%Y-W%U")(d.dateObj)), ([week, count]) => ({ week, count }));
|
|
387
|
+
drawBars("deployments", byWeek, (d) => d.week, (d) => d.count);
|
|
388
|
+
drawBars("leadtime", deployments.slice(-20), (d) => d.tag, (d) => d.lead);
|
|
389
|
+
</script>
|
|
390
|
+
</body>
|
|
391
|
+
</html>`;
|
|
392
|
+
|
|
393
|
+
fs.writeFileSync(path.join(outDir, "report.html"), html);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# DORA metrics recurring report template.
|
|
5
|
+
#
|
|
6
|
+
# Lead kickoff prompt:
|
|
7
|
+
# See ./lead-prompt.md in this template. It is the canonical copy-paste prompt
|
|
8
|
+
# for asking an agent-swarm Lead to install this runner, run it once, publish a
|
|
9
|
+
# stable page, and schedule weekly refreshes.
|
|
10
|
+
#
|
|
11
|
+
# Default weekly cadence for the surrounding swarm schedule:
|
|
12
|
+
# cron: "0 22 * * 0"
|
|
13
|
+
# timezone: "UTC"
|
|
14
|
+
# Change the cron field to adjust when the report refreshes. Keep the same page
|
|
15
|
+
# ID when publishing so the report URL stays stable.
|
|
16
|
+
#
|
|
17
|
+
# Data-source note:
|
|
18
|
+
# Deployment Frequency and Lead Time for Changes are exact for repositories
|
|
19
|
+
# where v* tags map 1:1 to production releases. Change Failure Rate and Failed
|
|
20
|
+
# Deployment Recovery Time are proxy estimates from revert/hotfix signals.
|
|
21
|
+
|
|
22
|
+
BASE_DIR="${BASE_DIR:-/workspace/dora-metrics}"
|
|
23
|
+
REPO_NAME="${REPO_NAME:-my-repo}"
|
|
24
|
+
REPO_URL="${REPO_URL:-https://github.com/OWNER/REPO.git}"
|
|
25
|
+
BRANCH="${BRANCH:-main}"
|
|
26
|
+
LOCAL_SOURCE="${LOCAL_SOURCE:-}"
|
|
27
|
+
WINDOW_DAYS="${WINDOW_DAYS:-90}"
|
|
28
|
+
HOTFIX_WINDOW_HOURS="${HOTFIX_WINDOW_HOURS:-24}"
|
|
29
|
+
TAG_PATTERN="${TAG_PATTERN:-v*}"
|
|
30
|
+
|
|
31
|
+
REPO_DIR="$BASE_DIR/repos/$REPO_NAME"
|
|
32
|
+
OUT_ROOT="$BASE_DIR/out/$REPO_NAME"
|
|
33
|
+
RUN_DATE="${RUN_DATE:-$(date -u +%F)}"
|
|
34
|
+
OUT_DIR="$OUT_ROOT/$RUN_DATE"
|
|
35
|
+
|
|
36
|
+
mkdir -p "$BASE_DIR/repos" "$OUT_DIR"
|
|
37
|
+
|
|
38
|
+
need_cmd() {
|
|
39
|
+
command -v "$1" >/dev/null 2>&1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
ensure_cmd() {
|
|
43
|
+
local cmd="$1"
|
|
44
|
+
local pkg="${2:-$1}"
|
|
45
|
+
if ! need_cmd "$cmd"; then
|
|
46
|
+
sudo apt-get update
|
|
47
|
+
sudo apt-get install -y "$pkg"
|
|
48
|
+
fi
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
prepare_repo() {
|
|
52
|
+
if [ -d "$REPO_DIR/.git" ]; then
|
|
53
|
+
git -C "$REPO_DIR" remote set-url --push origin DISABLED >/dev/null 2>&1 || true
|
|
54
|
+
git -C "$REPO_DIR" fetch origin "$BRANCH" --prune
|
|
55
|
+
git -C "$REPO_DIR" fetch origin "refs/tags/$TAG_PATTERN:refs/tags/$TAG_PATTERN" --prune --force || true
|
|
56
|
+
git -C "$REPO_DIR" checkout -q "$BRANCH" || git -C "$REPO_DIR" checkout -q -B "$BRANCH" "origin/$BRANCH"
|
|
57
|
+
git -C "$REPO_DIR" reset --hard "origin/$BRANCH" >/dev/null
|
|
58
|
+
git -C "$REPO_DIR" clean -fdx >/dev/null
|
|
59
|
+
return
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if [ -n "$LOCAL_SOURCE" ] && [ -d "$LOCAL_SOURCE/.git" ]; then
|
|
63
|
+
git clone --no-hardlinks "$LOCAL_SOURCE" "$REPO_DIR"
|
|
64
|
+
git -C "$REPO_DIR" remote set-url origin "$REPO_URL" || true
|
|
65
|
+
git -C "$REPO_DIR" remote set-url --push origin DISABLED || true
|
|
66
|
+
git -C "$REPO_DIR" fetch origin "$BRANCH" --prune
|
|
67
|
+
git -C "$REPO_DIR" fetch origin "refs/tags/$TAG_PATTERN:refs/tags/$TAG_PATTERN" --prune --force || true
|
|
68
|
+
git -C "$REPO_DIR" checkout -q "$BRANCH" || git -C "$REPO_DIR" checkout -q -B "$BRANCH" "origin/$BRANCH"
|
|
69
|
+
git -C "$REPO_DIR" reset --hard "origin/$BRANCH" >/dev/null
|
|
70
|
+
else
|
|
71
|
+
git clone --branch "$BRANCH" "$REPO_URL" "$REPO_DIR"
|
|
72
|
+
git -C "$REPO_DIR" remote set-url --push origin DISABLED || true
|
|
73
|
+
git -C "$REPO_DIR" fetch origin "refs/tags/$TAG_PATTERN:refs/tags/$TAG_PATTERN" --prune --force || true
|
|
74
|
+
fi
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
write_pr_metadata() {
|
|
78
|
+
local slug
|
|
79
|
+
slug="$(printf '%s\n' "$REPO_URL" | sed -E 's#^git@github.com:##; s#^https://github.com/##; s#\.git$##')"
|
|
80
|
+
if need_cmd gh && gh auth status >/dev/null 2>&1 && printf '%s\n' "$slug" | grep -Eq '^[^/]+/[^/]+$'; then
|
|
81
|
+
gh pr list \
|
|
82
|
+
--repo "$slug" \
|
|
83
|
+
--state merged \
|
|
84
|
+
--base "$BRANCH" \
|
|
85
|
+
--limit 1000 \
|
|
86
|
+
--json number,title,mergedAt,url,author,headRefName \
|
|
87
|
+
> "$OUT_DIR/prs.json" || printf '[]\n' > "$OUT_DIR/prs.json"
|
|
88
|
+
else
|
|
89
|
+
printf '[]\n' > "$OUT_DIR/prs.json"
|
|
90
|
+
fi
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ensure_cmd git git
|
|
94
|
+
ensure_cmd jq jq
|
|
95
|
+
ensure_cmd node nodejs
|
|
96
|
+
prepare_repo
|
|
97
|
+
|
|
98
|
+
git -C "$REPO_DIR" rev-parse HEAD > "$OUT_DIR/revision.txt"
|
|
99
|
+
git -C "$REPO_DIR" log -1 --format='%h %ad %an %s' --date=short > "$OUT_DIR/revision-summary.txt"
|
|
100
|
+
git -C "$REPO_DIR" for-each-ref "refs/tags/$TAG_PATTERN" --sort=creatordate --format='%(refname:short)%09%(objectname)%09%(creatordate:iso-strict)%09%(creatordate:unix)' > "$OUT_DIR/tags.tsv"
|
|
101
|
+
git -C "$REPO_DIR" log "origin/$BRANCH" --since="$WINDOW_DAYS days ago" --format='%H%x09%ct%x09%an%x09%s' > "$OUT_DIR/recent-commits.tsv"
|
|
102
|
+
git -C "$REPO_DIR" log "origin/$BRANCH" --since="$WINDOW_DAYS days ago" --grep='revert' --grep='rollback' --grep='hotfix' --grep='fix-forward' --regexp-ignore-case --format='%H%x09%ct%x09%an%x09%s' > "$OUT_DIR/remediation-commits.tsv"
|
|
103
|
+
write_pr_metadata
|
|
104
|
+
|
|
105
|
+
node "$BASE_DIR/report.mjs" "$OUT_DIR" "$REPO_DIR" "$REPO_NAME" "$RUN_DATE" "$BRANCH" "$WINDOW_DAYS" "$HOTFIX_WINDOW_HOURS" "$TAG_PATTERN"
|
|
106
|
+
|
|
107
|
+
cp "$OUT_DIR/summary.json" "$OUT_ROOT/latest.json"
|
|
108
|
+
cp "$OUT_DIR/report.html" "$OUT_ROOT/latest.html"
|
|
109
|
+
cp "$OUT_DIR/latest-pointer.json" "$OUT_ROOT/latest-pointer.json"
|
|
110
|
+
|
|
111
|
+
echo "Wrote $OUT_DIR"
|
|
112
|
+
echo "Latest summary: $OUT_ROOT/latest.json"
|
|
113
|
+
echo "Latest page HTML: $OUT_ROOT/latest.html"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "schedule",
|
|
3
|
+
"name": "weekly-dora-metrics",
|
|
4
|
+
"displayName": "Weekly DORA Metrics",
|
|
5
|
+
"slug": "weekly-dora-metrics",
|
|
6
|
+
"title": "Weekly DORA Metrics",
|
|
7
|
+
"description": "Gallery entry point for the community DORA metrics report package. Run a weekly DORA report and update a stable page in place.",
|
|
8
|
+
"version": "1.0.0",
|
|
9
|
+
"category": "schedules",
|
|
10
|
+
"placeholders": ["REPO_URL", "BRANCH", "TAG_PATTERN", "REPORT_NAME", "PAGE_ID", "TIMEZONE"],
|
|
11
|
+
"runAllSeedersCandidate": false,
|
|
12
|
+
"tags": ["dora", "metrics", "engineering-productivity", "reporting", "community-template"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Weekly DORA Metrics
|
|
2
|
+
|
|
3
|
+
Run a recurring DORA metrics report for a repository and update the same stable report page in place.
|
|
4
|
+
|
|
5
|
+
This schedule is the templates gallery entry point for the community package in `templates/community/dora-metrics/`. The package itself is intentionally not an agent-template config; it is a runnable playbook bundle (`PLAYBOOK.md`, `run.sh`, `report.mjs`, and `lead-prompt.md`) used by this schedule.
|
|
6
|
+
|
|
7
|
+
## Schedule
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"cron": "0 22 * * 0",
|
|
12
|
+
"timezone": "UTC",
|
|
13
|
+
"agentRole": "worker",
|
|
14
|
+
"enabled": true
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Scheduled Task
|
|
19
|
+
|
|
20
|
+
This is a reusable starting prompt. Before enabling it, replace the repository, branch, release tag pattern, report name, page ID, timezone, and worker targeting details with values for your project.
|
|
21
|
+
|
|
22
|
+
Task Type: Weekly DORA Metrics
|
|
23
|
+
|
|
24
|
+
Repository: `https://github.com/OWNER/REPO.git`
|
|
25
|
+
Default branch: `main`
|
|
26
|
+
Release tag pattern: `v*`
|
|
27
|
+
Report name: `my-repo`
|
|
28
|
+
Stable page ID: `<PAGE_ID>`
|
|
29
|
+
|
|
30
|
+
## Instructions
|
|
31
|
+
|
|
32
|
+
1. Use the community template in `templates/community/dora-metrics/`. See the docs playbook at `https://docs.agent-swarm.dev/docs/playbooks/dora-metrics` for the full setup, metric-definition notes, and proxy caveats.
|
|
33
|
+
2. Install or update the runner workspace under `/workspace/dora-metrics`:
|
|
34
|
+
- `run.sh`
|
|
35
|
+
- `report.mjs`
|
|
36
|
+
- `lead-prompt.md`
|
|
37
|
+
3. Run the report with project-specific parameters:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
BASE_DIR=/workspace/dora-metrics \
|
|
41
|
+
REPO_NAME=my-repo \
|
|
42
|
+
REPO_URL=https://github.com/OWNER/REPO.git \
|
|
43
|
+
BRANCH=main \
|
|
44
|
+
TAG_PATTERN='v*' \
|
|
45
|
+
WINDOW_DAYS=90 \
|
|
46
|
+
bash /workspace/dora-metrics/run.sh
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
4. Publish `/workspace/dora-metrics/out/my-repo/latest.html` to the existing stable page ID. Do not create a new page for routine refreshes.
|
|
50
|
+
5. Verify the rendered page loads, D3 charts are not blank, and the exact/proxy labels are visible.
|
|
51
|
+
6. If the run fails because of a local runner, dependency, branch, tag-pattern, or report-generator issue, diagnose and fix it before reporting failure.
|
|
52
|
+
|
|
53
|
+
## Cadence
|
|
54
|
+
|
|
55
|
+
The default cron is `0 22 * * 0`, weekly on Sunday at 22:00 UTC. Change the `cron` field to adjust when the report refreshes, and change `timezone` if the cron should be interpreted in a different zone.
|
|
56
|
+
|
|
57
|
+
The cadence is separate from page identity. Keep updating the same page ID so the report URL remains stable.
|
|
58
|
+
|
|
59
|
+
## Completion
|
|
60
|
+
|
|
61
|
+
Call `store-progress` with status `completed` and an output summary that includes the stable page URL, report workspace path, commit analyzed, generated timestamp, and the data-source caveat: Deployment Frequency and Lead Time are exact from release tags/commits; Change Failure Rate and Failed Deployment Recovery Time are proxy estimates from revert/hotfix-style remediation signals.
|