stackvitals 0.1.0__py3-none-any.whl

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.
Files changed (465) hide show
  1. mergescout/__init__.py +1 -0
  2. mergescout/archive/__init__.py +0 -0
  3. mergescout/archive/__main__.py +30 -0
  4. mergescout/archive/convert.py +83 -0
  5. mergescout/archive/events.py +55 -0
  6. mergescout/archive/manifest.py +45 -0
  7. mergescout/archive/stream.py +83 -0
  8. mergescout/bench/__init__.py +1 -0
  9. mergescout/bench/__main__.py +135 -0
  10. mergescout/bench/ci.py +257 -0
  11. mergescout/bench/deploys.py +115 -0
  12. mergescout/bench/matched.py +22 -0
  13. mergescout/bench/prior.py +42 -0
  14. mergescout/bench/report.py +230 -0
  15. mergescout/bench/shared.py +112 -0
  16. mergescout/bench/stars.py +57 -0
  17. mergescout/cli.py +89 -0
  18. mergescout/config.py +35 -0
  19. mergescout/crawl/__init__.py +0 -0
  20. mergescout/crawl/__main__.py +97 -0
  21. mergescout/crawl/chain.py +34 -0
  22. mergescout/crawl/fetch.py +146 -0
  23. mergescout/crawl/outcomes.py +69 -0
  24. mergescout/crawl/queries.py +250 -0
  25. mergescout/crawl/runner.py +176 -0
  26. mergescout/crawl/seed.py +77 -0
  27. mergescout/crawl/state.py +169 -0
  28. mergescout/crawl/writer.py +87 -0
  29. mergescout/discover/__init__.py +0 -0
  30. mergescout/discover/__main__.py +58 -0
  31. mergescout/discover/sweep.py +286 -0
  32. mergescout/github/__init__.py +0 -0
  33. mergescout/github/graphql.py +202 -0
  34. mergescout/github/identities.py +35 -0
  35. mergescout/github/releases.py +137 -0
  36. mergescout/labels.py +20 -0
  37. mergescout/mcp_server.py +228 -0
  38. mergescout/measure/__init__.py +0 -0
  39. mergescout/measure/__main__.py +75 -0
  40. mergescout/measure/api_cost.py +91 -0
  41. mergescout/measure/count_repos.py +30 -0
  42. mergescout/measure/jsonl.py +14 -0
  43. mergescout/measure/report.py +56 -0
  44. mergescout/measure/sample.py +37 -0
  45. mergescout/owners.py +22 -0
  46. mergescout/setup_check.py +64 -0
  47. mergescout/signals/__init__.py +0 -0
  48. mergescout/signals/__main__.py +96 -0
  49. mergescout/signals/bump_ledger.py +258 -0
  50. mergescout/signals/bumps.py +104 -0
  51. mergescout/signals/check.py +141 -0
  52. mergescout/signals/community_bench.py +149 -0
  53. mergescout/signals/delivery_bench.py +103 -0
  54. mergescout/signals/deps_map.py +261 -0
  55. mergescout/signals/deps_trends.py +117 -0
  56. mergescout/signals/evidence.py +94 -0
  57. mergescout/signals/exploits.py +43 -0
  58. mergescout/signals/flow_bench.py +258 -0
  59. mergescout/signals/identity.py +73 -0
  60. mergescout/signals/issues.py +76 -0
  61. mergescout/signals/labels.py +79 -0
  62. mergescout/signals/osv.py +232 -0
  63. mergescout/signals/owners_watch.py +80 -0
  64. mergescout/signals/repo_stats.py +96 -0
  65. mergescout/signals/rules.py +70 -0
  66. mergescout/signals/silver.py +33 -0
  67. mergescout/signals/top_packages.py +26 -0
  68. mergescout/signals/triage.py +71 -0
  69. mergescout/signals/triage_math.py +120 -0
  70. mergescout/signals/walls.py +91 -0
  71. mergescout/snapshot/__init__.py +0 -0
  72. mergescout/snapshot/__main__.py +215 -0
  73. mergescout/snapshot/build.py +147 -0
  74. mergescout/snapshot/publish.py +70 -0
  75. mergescout/snapshot/schema.py +34 -0
  76. mergescout/snapshot/score.py +30 -0
  77. mergescout/store/__init__.py +0 -0
  78. mergescout/store/__main__.py +46 -0
  79. mergescout/store/sync.py +148 -0
  80. mergescout/timeutil.py +11 -0
  81. mergescout/tool/__init__.py +0 -0
  82. mergescout/tool/action_pack.py +62 -0
  83. mergescout/tool/live.py +201 -0
  84. mergescout/tool/queries.py +293 -0
  85. mergescout/tool/refs.py +31 -0
  86. mergescout/tool/store.py +129 -0
  87. mergescout/tool/tracking.py +83 -0
  88. stackvitals/__init__.py +2 -0
  89. stackvitals/__main__.py +4 -0
  90. stackvitals/ci/__init__.py +1 -0
  91. stackvitals/ci/action.py +574 -0
  92. stackvitals/ci/analyze.py +577 -0
  93. stackvitals/ci/api.py +218 -0
  94. stackvitals/ci/automation.py +212 -0
  95. stackvitals/ci/bench.py +195 -0
  96. stackvitals/ci/burnin.py +102 -0
  97. stackvitals/ci/cli.py +570 -0
  98. stackvitals/ci/collect.py +239 -0
  99. stackvitals/ci/cost.py +107 -0
  100. stackvitals/ci/coverage.py +155 -0
  101. stackvitals/ci/feedback.py +699 -0
  102. stackvitals/ci/fingerprint.py +220 -0
  103. stackvitals/ci/gitlab.py +121 -0
  104. stackvitals/ci/gitlab_action.py +125 -0
  105. stackvitals/ci/impact.py +125 -0
  106. stackvitals/ci/investigate.py +59 -0
  107. stackvitals/ci/kinds.py +52 -0
  108. stackvitals/ci/monitors.py +105 -0
  109. stackvitals/ci/owners.py +116 -0
  110. stackvitals/ci/quarantine_runtime.py +90 -0
  111. stackvitals/ci/report.py +435 -0
  112. stackvitals/ci/retry.py +67 -0
  113. stackvitals/ci/selfheal.py +102 -0
  114. stackvitals/ci/shards.py +120 -0
  115. stackvitals/ci/share.py +59 -0
  116. stackvitals/ci/store.py +93 -0
  117. stackvitals/ci/testreports.py +351 -0
  118. stackvitals/ci/tickets.py +106 -0
  119. stackvitals/ci/track_record.json +11 -0
  120. stackvitals/ci/triage.py +130 -0
  121. stackvitals/ci/triggers.py +147 -0
  122. stackvitals/ci/upload.py +85 -0
  123. stackvitals/ci/validate.py +91 -0
  124. stackvitals/cli.py +111 -0
  125. stackvitals/codeowners.py +69 -0
  126. stackvitals/community/__init__.py +0 -0
  127. stackvitals/community/action.py +140 -0
  128. stackvitals/community/affiliation.py +51 -0
  129. stackvitals/community/analyze.py +507 -0
  130. stackvitals/community/build.py +56 -0
  131. stackvitals/community/chaoss.py +70 -0
  132. stackvitals/community/cli.py +79 -0
  133. stackvitals/community/collect.py +240 -0
  134. stackvitals/community/context.py +54 -0
  135. stackvitals/community/report.py +250 -0
  136. stackvitals/connectors/__init__.py +244 -0
  137. stackvitals/connectors/ai_spend.py +190 -0
  138. stackvitals/connectors/base.py +82 -0
  139. stackvitals/connectors/check.py +686 -0
  140. stackvitals/connectors/copilot.py +104 -0
  141. stackvitals/connectors/discord.py +103 -0
  142. stackvitals/connectors/discourse.py +117 -0
  143. stackvitals/connectors/incident_io.py +106 -0
  144. stackvitals/connectors/jira.py +210 -0
  145. stackvitals/connectors/linear.py +140 -0
  146. stackvitals/connectors/pagerduty.py +96 -0
  147. stackvitals/connectors/slack.py +110 -0
  148. stackvitals/connectors/sonar.py +77 -0
  149. stackvitals/connectors/store.py +120 -0
  150. stackvitals/connectors/tracker.py +108 -0
  151. stackvitals/connectors/writeback.py +105 -0
  152. stackvitals/contribute/__init__.py +1 -0
  153. stackvitals/contribute/brief.py +297 -0
  154. stackvitals/contribute/cli.py +64 -0
  155. stackvitals/core/__init__.py +1 -0
  156. stackvitals/core/actors.py +103 -0
  157. stackvitals/core/agent_ledger.py +120 -0
  158. stackvitals/core/agreements.py +95 -0
  159. stackvitals/core/ai_outcomes.py +276 -0
  160. stackvitals/core/alerts.py +224 -0
  161. stackvitals/core/appdata.py +112 -0
  162. stackvitals/core/bucket.py +190 -0
  163. stackvitals/core/bugs.py +43 -0
  164. stackvitals/core/calendar.py +34 -0
  165. stackvitals/core/capex.py +57 -0
  166. stackvitals/core/change_risk.py +181 -0
  167. stackvitals/core/changes.py +133 -0
  168. stackvitals/core/chat.py +143 -0
  169. stackvitals/core/config.py +373 -0
  170. stackvitals/core/dataflow.py +168 -0
  171. stackvitals/core/decisions.py +275 -0
  172. stackvitals/core/delivery.py +954 -0
  173. stackvitals/core/delivery_peers.py +98 -0
  174. stackvitals/core/delivery_report.py +429 -0
  175. stackvitals/core/deliverycli.py +147 -0
  176. stackvitals/core/devlake_dora.sql +485 -0
  177. stackvitals/core/digest.py +333 -0
  178. stackvitals/core/explaincli.py +107 -0
  179. stackvitals/core/explore.py +296 -0
  180. stackvitals/core/export.py +342 -0
  181. stackvitals/core/exportcli.py +117 -0
  182. stackvitals/core/findings.py +350 -0
  183. stackvitals/core/health.py +94 -0
  184. stackvitals/core/identity.py +82 -0
  185. stackvitals/core/insights.py +365 -0
  186. stackvitals/core/insightscli.py +43 -0
  187. stackvitals/core/installdoc.py +69 -0
  188. stackvitals/core/investment.py +187 -0
  189. stackvitals/core/joins.py +43 -0
  190. stackvitals/core/llmsdoc.py +40 -0
  191. stackvitals/core/merge_queue.py +70 -0
  192. stackvitals/core/metrics.py +109 -0
  193. stackvitals/core/model.py +285 -0
  194. stackvitals/core/projectcli.py +66 -0
  195. stackvitals/core/propose.py +86 -0
  196. stackvitals/core/pulse.py +85 -0
  197. stackvitals/core/recorded.py +145 -0
  198. stackvitals/core/releases.py +39 -0
  199. stackvitals/core/requests.py +528 -0
  200. stackvitals/core/resync.py +35 -0
  201. stackvitals/core/schema.py +205 -0
  202. stackvitals/core/services.py +203 -0
  203. stackvitals/core/settings.py +96 -0
  204. stackvitals/core/sqlcli.py +111 -0
  205. stackvitals/core/stats.py +222 -0
  206. stackvitals/core/store.py +125 -0
  207. stackvitals/core/summary.py +104 -0
  208. stackvitals/core/time.py +29 -0
  209. stackvitals/core/tomledit.py +149 -0
  210. stackvitals/core/uicli.py +138 -0
  211. stackvitals/core/webapp.py +79 -0
  212. stackvitals/core/what_changed.py +76 -0
  213. stackvitals/core/workspace.py +194 -0
  214. stackvitals/deps/__init__.py +1 -0
  215. stackvitals/deps/action.py +299 -0
  216. stackvitals/deps/actions_vulns.py +89 -0
  217. stackvitals/deps/cli.py +77 -0
  218. stackvitals/deps/dossier.py +341 -0
  219. stackvitals/deps/evidence.py +44 -0
  220. stackvitals/deps/fixplan.py +40 -0
  221. stackvitals/deps/guarddog.py +32 -0
  222. stackvitals/deps/hygiene.py +47 -0
  223. stackvitals/deps/imports.py +113 -0
  224. stackvitals/deps/lockfiles.py +198 -0
  225. stackvitals/deps/lookalike.py +80 -0
  226. stackvitals/deps/reach.py +59 -0
  227. stackvitals/deps/release.py +110 -0
  228. stackvitals/deps/runtimes.py +161 -0
  229. stackvitals/deps/supply.py +454 -0
  230. stackvitals/deps/vulns.py +588 -0
  231. stackvitals/editors/__init__.py +40 -0
  232. stackvitals/editors/vscode/extension.js +101 -0
  233. stackvitals/editors/vscode/marks.js +92 -0
  234. stackvitals/editors/vscode/package.json +26 -0
  235. stackvitals/flow/__init__.py +1 -0
  236. stackvitals/flow/action.py +178 -0
  237. stackvitals/flow/analyze.py +418 -0
  238. stackvitals/flow/automate.py +72 -0
  239. stackvitals/flow/build.py +46 -0
  240. stackvitals/flow/cli.py +93 -0
  241. stackvitals/flow/code_hosts.py +146 -0
  242. stackvitals/flow/collect.py +451 -0
  243. stackvitals/flow/gitlab_mr.py +75 -0
  244. stackvitals/flow/knowledge.py +202 -0
  245. stackvitals/flow/peers.py +90 -0
  246. stackvitals/flow/preflight.py +210 -0
  247. stackvitals/flow/report.py +253 -0
  248. stackvitals/flow/rules.py +135 -0
  249. stackvitals/flow/store.py +108 -0
  250. stackvitals/flow/survival.py +84 -0
  251. stackvitals/flow/triage.py +62 -0
  252. stackvitals/mcp_server.py +670 -0
  253. stackvitals/portal/__init__.py +0 -0
  254. stackvitals/portal/action.py +205 -0
  255. stackvitals/portal/batch.py +80 -0
  256. stackvitals/portal/campaigns.py +104 -0
  257. stackvitals/portal/catalog.py +104 -0
  258. stackvitals/portal/checks.py +63 -0
  259. stackvitals/portal/cli.py +172 -0
  260. stackvitals/portal/descriptor.py +105 -0
  261. stackvitals/portal/export.py +43 -0
  262. stackvitals/portal/github.py +48 -0
  263. stackvitals/portal/golden.py +95 -0
  264. stackvitals/portal/history.py +30 -0
  265. stackvitals/portal/inventory.py +264 -0
  266. stackvitals/portal/owners.py +78 -0
  267. stackvitals/portal/pages.py +153 -0
  268. stackvitals/portal/rollup.py +32 -0
  269. stackvitals/portal/scorecard.py +366 -0
  270. stackvitals/security/__init__.py +2 -0
  271. stackvitals/security/probes.py +154 -0
  272. stackvitals/security/repo_checks.py +132 -0
  273. stackvitals/security/workflows.py +111 -0
  274. stackvitals/shell.py +107 -0
  275. stackvitals/site.py +51 -0
  276. stackvitals/web/app/LICENSES.md +428 -0
  277. stackvitals/web/app/boot.js +1 -0
  278. stackvitals/web/app/chunks/AddRepo-KRPZXWCY.js +1 -0
  279. stackvitals/web/app/chunks/SetupRunning-XWFQAKRZ.js +1 -0
  280. stackvitals/web/app/chunks/accuracy-data-OQTDXI7Y.js +1 -0
  281. stackvitals/web/app/chunks/agents-data-BAL6QAOF.js +11 -0
  282. stackvitals/web/app/chunks/chunk-24LFKFZ6.js +1 -0
  283. stackvitals/web/app/chunks/chunk-3G4SQNUC.js +1 -0
  284. stackvitals/web/app/chunks/chunk-3IXWEOIJ.js +154 -0
  285. stackvitals/web/app/chunks/chunk-3ROQFHPV.js +1 -0
  286. stackvitals/web/app/chunks/chunk-45PGAQRO.js +8 -0
  287. stackvitals/web/app/chunks/chunk-4H53OYLG.js +1 -0
  288. stackvitals/web/app/chunks/chunk-4LUI7O5P.js +1 -0
  289. stackvitals/web/app/chunks/chunk-4SQPQ5TB.js +1 -0
  290. stackvitals/web/app/chunks/chunk-4WKQTWUC.js +2 -0
  291. stackvitals/web/app/chunks/chunk-576YHDEK.js +8 -0
  292. stackvitals/web/app/chunks/chunk-6BDBXA6V.js +1 -0
  293. stackvitals/web/app/chunks/chunk-6RLVD2DT.js +1 -0
  294. stackvitals/web/app/chunks/chunk-763NH7PP.js +1 -0
  295. stackvitals/web/app/chunks/chunk-76WAVKUG.js +1 -0
  296. stackvitals/web/app/chunks/chunk-7II2BD5F.js +1 -0
  297. stackvitals/web/app/chunks/chunk-7TJCBOAY.js +1 -0
  298. stackvitals/web/app/chunks/chunk-ASYNLVOP.js +1 -0
  299. stackvitals/web/app/chunks/chunk-CTKDYZMG.js +1 -0
  300. stackvitals/web/app/chunks/chunk-CVB26NMC.js +1 -0
  301. stackvitals/web/app/chunks/chunk-G7NGB5UX.js +1 -0
  302. stackvitals/web/app/chunks/chunk-GPVLJRZD.js +1 -0
  303. stackvitals/web/app/chunks/chunk-GQYFN4PE.js +75 -0
  304. stackvitals/web/app/chunks/chunk-GT4EAKAO.js +2 -0
  305. stackvitals/web/app/chunks/chunk-IYMBFPCV.js +1 -0
  306. stackvitals/web/app/chunks/chunk-LAAGCPU3.js +1 -0
  307. stackvitals/web/app/chunks/chunk-MAPYGK3P.js +1 -0
  308. stackvitals/web/app/chunks/chunk-P5TXDD45.js +1 -0
  309. stackvitals/web/app/chunks/chunk-PABICGBL.js +1 -0
  310. stackvitals/web/app/chunks/chunk-RPXHAG6F.js +1 -0
  311. stackvitals/web/app/chunks/chunk-RQZPSJ2Y.js +1 -0
  312. stackvitals/web/app/chunks/chunk-S2ZBMEVA.js +1 -0
  313. stackvitals/web/app/chunks/chunk-S654BBEP.js +6 -0
  314. stackvitals/web/app/chunks/chunk-T7QVTNXF.js +1 -0
  315. stackvitals/web/app/chunks/chunk-THFUOCPC.js +273 -0
  316. stackvitals/web/app/chunks/chunk-TO4VVRZ7.js +1 -0
  317. stackvitals/web/app/chunks/chunk-VQ5YXO54.js +1 -0
  318. stackvitals/web/app/chunks/chunk-VWKSMFWJ.js +1 -0
  319. stackvitals/web/app/chunks/chunk-Y6LUXDM6.js +1 -0
  320. stackvitals/web/app/chunks/chunk-YTANGYYH.js +1 -0
  321. stackvitals/web/app/chunks/connect-KONYWYTT.js +1 -0
  322. stackvitals/web/app/chunks/connections-data-2DYS4UVW.js +32 -0
  323. stackvitals/web/app/chunks/logos-N5IINUAM.js +1 -0
  324. stackvitals/web/app/chunks/more-KMJMJQD3.js +10 -0
  325. stackvitals/web/app/chunks/more-PC6FVS3P.js +1 -0
  326. stackvitals/web/app/chunks/more-RCIVCUDE.js +2 -0
  327. stackvitals/web/app/chunks/more-ZKKRY6NO.css +1 -0
  328. stackvitals/web/app/chunks/sealbox-35YL4EUH.js +3 -0
  329. stackvitals/web/app/chunks/session-DYYTOE2K.js +1 -0
  330. stackvitals/web/app/chunks/settingsApply-2HRJEZYM.js +1 -0
  331. stackvitals/web/app/chunks/settingsReview-GX2FZQ4M.js +3 -0
  332. stackvitals/web/app/chunks/uPlot.esm-OIWT3AHJ.js +3 -0
  333. stackvitals/web/app/demo/ci/impact.json +1 -0
  334. stackvitals/web/app/demo/ci/quarantine.json +1 -0
  335. stackvitals/web/app/demo/ci/tickets.json +1 -0
  336. stackvitals/web/app/demo/mobile/alerts.json +1 -0
  337. stackvitals/web/app/demo/mobile/app.json +1 -0
  338. stackvitals/web/app/demo/mobile/community.json +1 -0
  339. stackvitals/web/app/demo/mobile/connectors.json +1 -0
  340. stackvitals/web/app/demo/mobile/data.json +1 -0
  341. stackvitals/web/app/demo/mobile/decisions.json +1 -0
  342. stackvitals/web/app/demo/mobile/delivery.json +1 -0
  343. stackvitals/web/app/demo/mobile/deps.json +1 -0
  344. stackvitals/web/app/demo/mobile/flow.json +1 -0
  345. stackvitals/web/app/demo/mobile/health.json +1 -0
  346. stackvitals/web/app/demo/mobile/insights.json +1 -0
  347. stackvitals/web/app/demo/mobile/pulse.json +1 -0
  348. stackvitals/web/app/demo/mobile/security.json +1 -0
  349. stackvitals/web/app/demo/mobile/summary/ci_jobs.jsonl +5 -0
  350. stackvitals/web/app/demo/mobile/summary/devlake_project_pr_metrics.jsonl +14 -0
  351. stackvitals/web/app/demo/mobile/summary/devlake_pull_requests.jsonl +14 -0
  352. stackvitals/web/app/demo/mobile/summary/manifest.json +1 -0
  353. stackvitals/web/app/demo/mobile/summary/prs.jsonl +19 -0
  354. stackvitals/web/app/demo/mobile/summary/tests.jsonl +1 -0
  355. stackvitals/web/app/demo/mobile/supply.json +1 -0
  356. stackvitals/web/app/demo/mobile/vulns.json +1 -0
  357. stackvitals/web/app/demo/payments-api/alerts.json +1 -0
  358. stackvitals/web/app/demo/payments-api/app.json +1 -0
  359. stackvitals/web/app/demo/payments-api/community.json +1 -0
  360. stackvitals/web/app/demo/payments-api/connectors.json +1 -0
  361. stackvitals/web/app/demo/payments-api/data.json +1 -0
  362. stackvitals/web/app/demo/payments-api/decisions.json +1 -0
  363. stackvitals/web/app/demo/payments-api/delivery.json +1 -0
  364. stackvitals/web/app/demo/payments-api/deps.json +1 -0
  365. stackvitals/web/app/demo/payments-api/flow.json +1 -0
  366. stackvitals/web/app/demo/payments-api/health.json +1 -0
  367. stackvitals/web/app/demo/payments-api/insights.json +1 -0
  368. stackvitals/web/app/demo/payments-api/pulse.json +1 -0
  369. stackvitals/web/app/demo/payments-api/security.json +1 -0
  370. stackvitals/web/app/demo/payments-api/summary/ci_jobs.jsonl +5 -0
  371. stackvitals/web/app/demo/payments-api/summary/devlake_project_pr_metrics.jsonl +14 -0
  372. stackvitals/web/app/demo/payments-api/summary/devlake_pull_requests.jsonl +14 -0
  373. stackvitals/web/app/demo/payments-api/summary/manifest.json +1 -0
  374. stackvitals/web/app/demo/payments-api/summary/prs.jsonl +22 -0
  375. stackvitals/web/app/demo/payments-api/summary/tests.jsonl +3 -0
  376. stackvitals/web/app/demo/payments-api/supply.json +1 -0
  377. stackvitals/web/app/demo/payments-api/vulns.json +1 -0
  378. stackvitals/web/app/demo/portal.json +1 -0
  379. stackvitals/web/app/demo/settings.json +1 -0
  380. stackvitals/web/app/demo/web-app/alerts.json +1 -0
  381. stackvitals/web/app/demo/web-app/app.json +1 -0
  382. stackvitals/web/app/demo/web-app/community.json +1 -0
  383. stackvitals/web/app/demo/web-app/connectors.json +1 -0
  384. stackvitals/web/app/demo/web-app/data.json +1 -0
  385. stackvitals/web/app/demo/web-app/decisions.json +1 -0
  386. stackvitals/web/app/demo/web-app/delivery.json +1 -0
  387. stackvitals/web/app/demo/web-app/deps.json +1 -0
  388. stackvitals/web/app/demo/web-app/flow.json +1 -0
  389. stackvitals/web/app/demo/web-app/health.json +1 -0
  390. stackvitals/web/app/demo/web-app/insights.json +1 -0
  391. stackvitals/web/app/demo/web-app/pulse.json +1 -0
  392. stackvitals/web/app/demo/web-app/security.json +1 -0
  393. stackvitals/web/app/demo/web-app/summary/ci_jobs.jsonl +5 -0
  394. stackvitals/web/app/demo/web-app/summary/devlake_project_pr_metrics.jsonl +14 -0
  395. stackvitals/web/app/demo/web-app/summary/devlake_pull_requests.jsonl +14 -0
  396. stackvitals/web/app/demo/web-app/summary/manifest.json +1 -0
  397. stackvitals/web/app/demo/web-app/summary/prs.jsonl +18 -0
  398. stackvitals/web/app/demo/web-app/summary/tests.jsonl +2 -0
  399. stackvitals/web/app/demo/web-app/supply.json +1 -0
  400. stackvitals/web/app/demo/web-app/vulns.json +1 -0
  401. stackvitals/web/app/fonts/plex-mono-400.woff2 +0 -0
  402. stackvitals/web/app/fonts/plex-sans-400.woff2 +0 -0
  403. stackvitals/web/app/fonts/plex-sans-600.woff2 +0 -0
  404. stackvitals/web/app/fonts/plex-sans-condensed-700.woff2 +0 -0
  405. stackvitals/web/app/help/accuracy.json +1 -0
  406. stackvitals/web/app/help/connectors.json +1 -0
  407. stackvitals/web/app/help/getting-started.json +1 -0
  408. stackvitals/web/app/help/index.json +1 -0
  409. stackvitals/web/app/help/modules.json +1 -0
  410. stackvitals/web/app/help/privacy-and-security.json +1 -0
  411. stackvitals/web/app/help/reference.json +1 -0
  412. stackvitals/web/app/main.css +1 -0
  413. stackvitals/web/app/main.js +1 -0
  414. stackvitals/web/app/pages/about.css +1 -0
  415. stackvitals/web/app/pages/about.js +1 -0
  416. stackvitals/web/app/pages/activity.css +1 -0
  417. stackvitals/web/app/pages/activity.js +1 -0
  418. stackvitals/web/app/pages/agents.css +1 -0
  419. stackvitals/web/app/pages/agents.js +2 -0
  420. stackvitals/web/app/pages/ci.css +1 -0
  421. stackvitals/web/app/pages/ci.js +1 -0
  422. stackvitals/web/app/pages/community.css +1 -0
  423. stackvitals/web/app/pages/community.js +1 -0
  424. stackvitals/web/app/pages/connections.css +1 -0
  425. stackvitals/web/app/pages/connections.js +6 -0
  426. stackvitals/web/app/pages/delivery.css +1 -0
  427. stackvitals/web/app/pages/delivery.js +45 -0
  428. stackvitals/web/app/pages/deps.css +1 -0
  429. stackvitals/web/app/pages/deps.js +7 -0
  430. stackvitals/web/app/pages/explore.css +1 -0
  431. stackvitals/web/app/pages/explore.js +7 -0
  432. stackvitals/web/app/pages/health.css +1 -0
  433. stackvitals/web/app/pages/health.js +1 -0
  434. stackvitals/web/app/pages/help.css +1 -0
  435. stackvitals/web/app/pages/help.js +6 -0
  436. stackvitals/web/app/pages/insights.css +1 -0
  437. stackvitals/web/app/pages/insights.js +1 -0
  438. stackvitals/web/app/pages/investment.css +1 -0
  439. stackvitals/web/app/pages/investment.js +3 -0
  440. stackvitals/web/app/pages/overview.css +1 -0
  441. stackvitals/web/app/pages/overview.js +1 -0
  442. stackvitals/web/app/pages/portal.css +1 -0
  443. stackvitals/web/app/pages/portal.js +51 -0
  444. stackvitals/web/app/pages/prs.css +1 -0
  445. stackvitals/web/app/pages/prs.js +4 -0
  446. stackvitals/web/app/pages/settings.css +1 -0
  447. stackvitals/web/app/pages/settings.js +1 -0
  448. stackvitals/web/app/pages/setup.css +1 -0
  449. stackvitals/web/app/pages/setup.js +2 -0
  450. stackvitals/web/app/pages/standing.css +1 -0
  451. stackvitals/web/app/pages/standing.js +1 -0
  452. stackvitals/web/app/pages/welcome-sheet.js +1 -0
  453. stackvitals/web/app/pages/welcome.js +1 -0
  454. stackvitals/web/app/settings-workflow.json +1 -0
  455. stackvitals/web/index.html +34 -0
  456. stackvitals/web/site/index.html +108 -0
  457. stackvitals/web/site/integrations.html +15 -0
  458. stackvitals/web/site/setup.html +15 -0
  459. stackvitals/web/site/site.css +68 -0
  460. stackvitals-0.1.0.dist-info/METADATA +134 -0
  461. stackvitals-0.1.0.dist-info/RECORD +465 -0
  462. stackvitals-0.1.0.dist-info/WHEEL +4 -0
  463. stackvitals-0.1.0.dist-info/entry_points.txt +3 -0
  464. stackvitals-0.1.0.dist-info/licenses/LICENSE +202 -0
  465. stackvitals-0.1.0.dist-info/licenses/NOTICE +16 -0
@@ -0,0 +1,147 @@
1
+ """`stackvitals delivery owner/repo`: cycle time phases, DORA, stuck pull requests and sizes for any repo, from the repo's
2
+ published file when it runs StackVitals, else from its pull requests and CI runs read with your token."""
3
+ import argparse
4
+ import json
5
+ import sys
6
+
7
+ import httpx
8
+
9
+ from mergescout.github.graphql import GraphQLError
10
+ from stackvitals.ci import cli as ci_cli
11
+ from stackvitals.ci.api import APIError, OutOfBudget
12
+ from stackvitals.core import services
13
+ from stackvitals.core.delivery_report import NAMES, _h
14
+
15
+
16
+ def main(argv: list[str] | None = None, tok: str | None = None) -> int:
17
+ parser = argparse.ArgumentParser(prog="stackvitals delivery", description="Cycle time, DORA and stuck pull requests.")
18
+ parser.add_argument("repo", help="owner/name")
19
+ parser.add_argument("--days", type=int, default=90, help="history to read and measure (max 90 for CI runs)")
20
+ parser.add_argument("--json", action="store_true")
21
+ parser.add_argument("--deploys", action="store_true",
22
+ help="the deploy dry run: which deployments count, which workflows and environments don't, and why")
23
+ parser.add_argument("--gitlab", action="store_true", help="a GitLab project (group/project): merge requests and deployments")
24
+ parser.add_argument("--bitbucket", action="store_true", help="a Bitbucket Cloud repo (workspace/repo): pull requests and deployments")
25
+ parser.add_argument("--azure", action="store_true", help="an Azure DevOps repo (org/project/repo): pull requests")
26
+ args = parser.parse_args(argv)
27
+ if args.gitlab or args.bitbucket or args.azure:
28
+ host = "gitlab" if args.gitlab else "bitbucket" if args.bitbucket else "azure"
29
+ out, source = (_gitlab if host == "gitlab" else _host)(args.repo, max(1, min(args.days, 90)), *([] if host == "gitlab" else [host]))
30
+ if out is None:
31
+ return 1
32
+ return _show(args, out, source)
33
+ tok = tok or ci_cli.token()
34
+ if not tok:
35
+ print("A GitHub token is needed. Set GITHUB_TOKEN, or run `gh auth login`.", file=sys.stderr)
36
+ return 1
37
+ try:
38
+ out, source = services.delivery(args.repo, max(1, min(args.days, 90)), tok, ci_cli.home(),
39
+ log=lambda line: print(line, file=sys.stderr, flush=True))
40
+ except (APIError, OutOfBudget, GraphQLError, httpx.HTTPError) as exc:
41
+ print(f"Could not read {args.repo}: {exc}", file=sys.stderr)
42
+ return 1
43
+ return _show(args, out, source)
44
+
45
+
46
+ def _gitlab(project: str, days: int):
47
+ from datetime import datetime, timezone
48
+
49
+ from stackvitals.ci.gitlab import GitLab, token
50
+ from stackvitals.core.config import parse
51
+ from stackvitals.core.delivery import build
52
+ from stackvitals.flow import gitlab_mr
53
+ now = datetime.now(timezone.utc)
54
+ try:
55
+ flow, repo = gitlab_mr.collect(GitLab(project, token()), now, days)
56
+ except (APIError, OutOfBudget, httpx.HTTPError) as exc:
57
+ print(f"Could not read {project} on GitLab: {exc}", file=sys.stderr)
58
+ return None, None
59
+ return build(flow, repo, {"runs": []}, [], now, days, parse("")[0]), "GitLab merge requests"
60
+
61
+
62
+ def _host(repo: str, days: int, host: str):
63
+ from datetime import datetime, timezone
64
+
65
+ from stackvitals.core.config import parse
66
+ from stackvitals.core.delivery import build
67
+ from stackvitals.flow import code_hosts
68
+ now = datetime.now(timezone.utc)
69
+ name = {"bitbucket": "Bitbucket", "azure": "Azure DevOps"}[host]
70
+ try:
71
+ flow, facts = getattr(code_hosts, host)(repo, now, days)
72
+ except (APIError, OutOfBudget, httpx.HTTPError, ValueError) as exc:
73
+ print(f"Could not read {repo} on {name}: {exc}", file=sys.stderr)
74
+ return None, None
75
+ return build(flow, facts, {"runs": []}, [], now, days, parse("")[0]), f"{name} pull requests"
76
+
77
+
78
+ def _show(args, out: dict, source: str) -> int:
79
+ if args.json:
80
+ print(json.dumps({"repo": args.repo, "source": source, "delivery": out}, indent=1))
81
+ return 0
82
+ if args.deploys:
83
+ print(deploy_dry_run(args.repo, out.get("deploy_check") or {}))
84
+ return 0
85
+ d = out["dora"]
86
+ print(f"Delivery in {args.repo}: {out['prs']} merged PRs in {out['window_days']} days ({source}).")
87
+ for name, label in NAMES.items():
88
+ s = out["phases"][name]
89
+ if not s.get("enough", True):
90
+ print(f" {label:20} not enough data (n={s['n']}, needs 10)")
91
+ continue
92
+ print(f" {label:20} P50 {_h(s['p50_h']):>10} P75 {_h(s['p75_h']):>10} n={s['n']}"
93
+ + (f" ({s['linearb_band']})" if s["linearb_band"] else ""))
94
+ def tier(value):
95
+ return value or "not enough data"
96
+ cfr = "-" if d["change_failure_rate"] is None else f"{d['change_failure_rate']:.0%}"
97
+ print(f"DORA {d['report']} (deployments from {out['deploy_source'] or 'none found'}, {d['deployments']} in the window): "
98
+ f"{d['deployment_days_per_week']} deployment days a week ({tier(d['frequency_tier'])}), lead time "
99
+ f"{_h(d['lead_time_hours'])} ({tier(d['lead_time_tier'])}), change failure rate {cfr} ({tier(d['cfr_tier'])}), "
100
+ f"recovery {_h(d['recovery_hours'])} ({tier(d['recovery_tier'])}).")
101
+ if out.get("deploy_confidence") == "low":
102
+ names = ", ".join(f"{x['name']} ({x['n']})" for x in out.get("deploy_names") or [])
103
+ print(f"Deployments are a guess from workflow names: {names}. If that is not how this repo ships, set "
104
+ "source or deploy_workflow under [delivery] in .github/stackvitals.toml.")
105
+ peers = out.get("peers")
106
+ if peers:
107
+ print(f"Compared with {peers['group']} ({peers['repos']} repos in the daily crawl, measured the crawl's way):")
108
+ for m in peers["metrics"]:
109
+ good = f"; {m['linearb']['good']:.0%} of them inside LinearB's good band" if m.get("linearb") else ""
110
+ print(f" {m['name']}: {_h(m['you'])}, typical {_h(m['typical'])} ({_h(m['low'])}-{_h(m['high'])}), "
111
+ f"{m['place']}{good}")
112
+ for key, items in out["stuck"].items():
113
+ if items:
114
+ print(f"Stuck, {key.replace('_', ' ')}: " + ", ".join(f"#{x['pr']}" for x in items[:15]))
115
+ return 0
116
+
117
+
118
+ def deploy_dry_run(repo: str, c: dict) -> str:
119
+ """The deploy dry run as plain lines."""
120
+ s = c.get("settings") or {}
121
+ out = [f"Deployments in {repo}: {c.get('deployments', 0)} from {c.get('source') or 'no source'} "
122
+ f"([delivery] source = \"{s.get('source')}\", deploy_workflow = \"{s.get('deploy_workflow')}\", "
123
+ f"production = {s.get('production')})."]
124
+ for x in c.get("considered") or []:
125
+ out.append(f" {'counted' if x['used'] else 'passed over':12} {x['source']}: {x['found']}")
126
+ if c.get("order_reason"):
127
+ out.append(f" ({c['order_reason']})")
128
+ flows = c.get("workflows") or []
129
+ if flows:
130
+ out.append("Workflows:")
131
+ for f in flows:
132
+ if not f["kind"]:
133
+ runs = ""
134
+ elif f.get("used"):
135
+ runs = f" ({f['counted']} runs counted, {f['skipped']} skipped: failed, or not a release or the default branch)"
136
+ else:
137
+ runs = f" (passed over: {c['source'] or 'nothing'} is the source used)"
138
+ out.append(f" {f['name']} ({f['path']}): {f['why']}{runs}")
139
+ envs = c.get("environments") or []
140
+ if envs:
141
+ out.append("Environments:")
142
+ out += [f" {e['name']}: {e['deployments']} deployments, {'counted' if e['counted'] else 'not counted'} ({e['as']})"
143
+ for e in envs]
144
+ r = c.get("releases") or {}
145
+ out.append(f"Releases: {r.get('published', 0)} published, {r.get('prereleases_skipped', 0)} prereleases skipped.")
146
+ out.append("To change what counts, set source, deploy_workflow or production under [delivery] in .github/stackvitals.toml.")
147
+ return "\n".join(out)
@@ -0,0 +1,485 @@
1
+ -- Copyright The Apache Software Foundation. Licensed under the Apache License, Version 2.0
2
+ -- (https://www.apache.org/licenses/LICENSE-2.0). From Apache DevLake (https://github.com/apache/incubator-devlake);
3
+ -- changed by StackVitals as described below. See NOTICE.
4
+ -- Apache DevLake's DORA dashboard (grafana/dashboards/DORA.json), its five main panels ported from MySQL to DuckDB.
5
+ -- The CTEs are DevLake's own; only the dialect changed: Grafana's time macros became {{FROM}} and {{TO}}, the project
6
+ -- variable {{PROJECT}}, the report variable {{REPORT}}; DATE() became CAST(... AS date), WEEKDAY() isodow() - 1,
7
+ -- date_format() strftime(), TIMESTAMPDIFF(MINUTE) whole elapsed minutes, double-quoted strings single-quoted.
8
+
9
+ -- panel: deployment_frequency
10
+ with last_few_calendar_months as(
11
+ -- construct the last few calendar months within the selected time period in the top-right corner
12
+ SELECT CAST((TIMESTAMP '{{TO}}' - INTERVAL (H+T+U) DAY) AS date) as day
13
+ FROM ( SELECT 0 H
14
+ UNION ALL SELECT 100 UNION ALL SELECT 200 UNION ALL SELECT 300
15
+ ) H CROSS JOIN ( SELECT 0 T
16
+ UNION ALL SELECT 10 UNION ALL SELECT 20 UNION ALL SELECT 30
17
+ UNION ALL SELECT 40 UNION ALL SELECT 50 UNION ALL SELECT 60
18
+ UNION ALL SELECT 70 UNION ALL SELECT 80 UNION ALL SELECT 90
19
+ ) T CROSS JOIN ( SELECT 0 U
20
+ UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
21
+ UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6
22
+ UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9
23
+ ) U
24
+ WHERE
25
+ (TIMESTAMP '{{TO}}' - INTERVAL (H+T+U) DAY) > TIMESTAMP '{{FROM}}'
26
+ ),
27
+
28
+ _production_deployment_days as(
29
+ SELECT
30
+ cdc.cicd_deployment_id as deployment_id,
31
+ max(CAST(cdc.finished_date AS date)) as day
32
+ FROM cicd_deployment_commits cdc
33
+ JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id and pm."table" = 'cicd_scopes'
34
+ WHERE
35
+ pm.project_name in ('{{PROJECT}}')
36
+ and cdc.result = 'SUCCESS'
37
+ and cdc.environment = 'PRODUCTION'
38
+ GROUP BY 1
39
+ ),
40
+
41
+ _days_weekly_deploy as(
42
+ SELECT
43
+ CAST(last_few_calendar_months.day - INTERVAL (isodow(last_few_calendar_months.day) - 1) DAY AS date) as week,
44
+ MAX(CASE WHEN _production_deployment_days.day is not null THEN 1 END) as weeks_deployed,
45
+ COUNT(distinct _production_deployment_days.day) as days_deployed
46
+ FROM
47
+ last_few_calendar_months
48
+ LEFT JOIN _production_deployment_days ON _production_deployment_days.day = last_few_calendar_months.day
49
+ GROUP BY week
50
+ ),
51
+
52
+ _days_monthly_deploy as(
53
+ SELECT
54
+ CAST(last_few_calendar_months.day - INTERVAL (day(last_few_calendar_months.day) - 1) DAY AS date) as month,
55
+ MAX(CASE WHEN _production_deployment_days.day is not null THEN 1 END) as months_deployed,
56
+ COUNT(distinct _production_deployment_days.day) as days_deployed
57
+ FROM
58
+ last_few_calendar_months
59
+ LEFT JOIN _production_deployment_days ON _production_deployment_days.day = last_few_calendar_months.day
60
+ GROUP BY month
61
+ ),
62
+
63
+ _days_six_months_deploy AS (
64
+ SELECT
65
+ month,
66
+ SUM(days_deployed) OVER (
67
+ ORDER BY month
68
+ ROWS BETWEEN 5 PRECEDING AND CURRENT ROW
69
+ ) AS days_deployed_per_six_months,
70
+ COUNT(months_deployed) OVER (
71
+ ORDER BY month
72
+ ROWS BETWEEN 5 PRECEDING AND CURRENT ROW
73
+ ) AS months_deployed_count,
74
+ ROW_NUMBER() OVER (
75
+ -- MySQL reads DATE_FORMAT(month, '%Y-%m') DIV 6 as the year DIV 6
76
+ PARTITION BY year(month) // 6
77
+ ORDER BY month DESC
78
+ ) AS rn
79
+ FROM _days_monthly_deploy
80
+ ),
81
+
82
+ _median_number_of_deployment_days_per_week_ranks as(
83
+ SELECT *, percent_rank() over(order by days_deployed) as ranks
84
+ FROM _days_weekly_deploy
85
+ ),
86
+
87
+ _median_number_of_deployment_days_per_week as(
88
+ SELECT max(days_deployed) as median_number_of_deployment_days_per_week
89
+ FROM _median_number_of_deployment_days_per_week_ranks
90
+ WHERE ranks <= 0.5
91
+ ),
92
+
93
+ _median_number_of_deployment_days_per_month_ranks as(
94
+ SELECT *, percent_rank() over(order by days_deployed) as ranks
95
+ FROM _days_monthly_deploy
96
+ ),
97
+
98
+ _median_number_of_deployment_days_per_month as(
99
+ SELECT max(days_deployed) as median_number_of_deployment_days_per_month
100
+ FROM _median_number_of_deployment_days_per_month_ranks
101
+ WHERE ranks <= 0.5
102
+ ),
103
+
104
+ _days_per_six_months_deploy_by_filter AS (
105
+ SELECT
106
+ month,
107
+ days_deployed_per_six_months,
108
+ months_deployed_count
109
+ FROM _days_six_months_deploy
110
+ WHERE rn%6 = 1
111
+ ),
112
+
113
+ _median_number_of_deployment_days_per_six_months_ranks as(
114
+ SELECT *, percent_rank() over(order by days_deployed_per_six_months) as ranks
115
+ FROM _days_per_six_months_deploy_by_filter
116
+ ),
117
+
118
+ _median_number_of_deployment_days_per_six_months as(
119
+ SELECT min(days_deployed_per_six_months) as median_number_of_deployment_days_per_six_months, min(months_deployed_count) as is_collected
120
+ FROM _median_number_of_deployment_days_per_six_months_ranks
121
+ WHERE ranks >= 0.5
122
+ )
123
+
124
+ SELECT
125
+ CASE
126
+ WHEN ('{{REPORT}}') = '2023' THEN
127
+ CASE
128
+ WHEN median_number_of_deployment_days_per_week >= 5 THEN CONCAT(median_number_of_deployment_days_per_week, ' deployment days per week(elite)')
129
+ WHEN median_number_of_deployment_days_per_week >= 1 THEN CONCAT(median_number_of_deployment_days_per_week, ' deployment days per week(high)')
130
+ WHEN median_number_of_deployment_days_per_month >= 1 THEN CONCAT(median_number_of_deployment_days_per_month, ' deployment days per month(medium)')
131
+ WHEN median_number_of_deployment_days_per_month < 1 and is_collected is not null THEN CONCAT(median_number_of_deployment_days_per_month, ' deployment days per month(low)')
132
+ ELSE 'N/A. Please check if you have collected deployments.' END
133
+ WHEN ('{{REPORT}}') = '2021' THEN
134
+ CASE
135
+ WHEN median_number_of_deployment_days_per_week >= 5 THEN CONCAT(median_number_of_deployment_days_per_week, ' deployment days per week(elite)')
136
+ WHEN median_number_of_deployment_days_per_month >= 1 THEN CONCAT(median_number_of_deployment_days_per_month, ' deployment days per month(high)')
137
+ WHEN median_number_of_deployment_days_per_six_months >= 1 THEN CONCAT(median_number_of_deployment_days_per_six_months, ' deployment days per six months(medium)')
138
+ WHEN median_number_of_deployment_days_per_six_months < 1 and is_collected is not null THEN CONCAT(median_number_of_deployment_days_per_six_months, ' deployment days per six months(low)')
139
+ ELSE 'N/A. Please check if you have collected deployments.' END
140
+ ELSE 'Invalid dora report'
141
+ END AS "Deployment Frequency"
142
+ FROM _median_number_of_deployment_days_per_week, _median_number_of_deployment_days_per_month, _median_number_of_deployment_days_per_six_months;
143
+
144
+ -- panel: lead_time
145
+ with _pr_stats as (
146
+ -- get the cycle time of PRs deployed by the deployments finished in the selected period
147
+ SELECT
148
+ distinct pr.id,
149
+ ppm.pr_cycle_time
150
+ FROM
151
+ pull_requests pr
152
+ join project_pr_metrics ppm on ppm.id = pr.id
153
+ join project_mapping pm on pr.base_repo_id = pm.row_id and pm."table" = 'repos'
154
+ join cicd_deployment_commits cdc on ppm.deployment_commit_id = cdc.id
155
+ WHERE
156
+ pm.project_name in ('{{PROJECT}}')
157
+ and pr.merged_date is not null
158
+ and ppm.pr_cycle_time is not null
159
+ and cdc.finished_date BETWEEN TIMESTAMP '{{FROM}}' AND TIMESTAMP '{{TO}}'
160
+ ),
161
+
162
+ _median_change_lead_time_ranks as(
163
+ SELECT *, percent_rank() over(order by pr_cycle_time) as ranks
164
+ FROM _pr_stats
165
+ ),
166
+
167
+ _median_change_lead_time as(
168
+ -- use median PR cycle time as the median change lead time
169
+ SELECT max(pr_cycle_time) as median_change_lead_time
170
+ FROM _median_change_lead_time_ranks
171
+ WHERE ranks <= 0.5
172
+ )
173
+
174
+ SELECT
175
+ CASE
176
+ WHEN ('{{REPORT}}') = '2023' THEN
177
+ CASE
178
+ WHEN median_change_lead_time < 24 * 60 THEN CONCAT(round(median_change_lead_time/60,1), '(elite)')
179
+ WHEN median_change_lead_time < 7 * 24 * 60 THEN CONCAT(round(median_change_lead_time/60,1), '(high)')
180
+ WHEN median_change_lead_time < 30 * 24 * 60 THEN CONCAT(round(median_change_lead_time/60,1), '(medium)')
181
+ WHEN median_change_lead_time >= 30 * 24 * 60 THEN CONCAT(round(median_change_lead_time/60,1), '(low)')
182
+ ELSE 'N/A. Please check if you have collected deployments/pull_requests.'
183
+ END
184
+ WHEN ('{{REPORT}}') = '2021' THEN
185
+ CASE
186
+ WHEN median_change_lead_time < 60 THEN CONCAT(round(median_change_lead_time/60,1), '(elite)')
187
+ WHEN median_change_lead_time < 7 * 24 * 60 THEN CONCAT(round(median_change_lead_time/60,1), '(high)')
188
+ WHEN median_change_lead_time < 180 * 24 * 60 THEN CONCAT(round(median_change_lead_time/60,1), '(medium)')
189
+ WHEN median_change_lead_time >= 180 * 24 * 60 THEN CONCAT(round(median_change_lead_time/60,1), '(low)')
190
+ ELSE 'N/A. Please check if you have collected deployments/pull_requests.'
191
+ END
192
+ ELSE 'Invalid dora report'
193
+ END AS median_change_lead_time
194
+ FROM _median_change_lead_time;
195
+
196
+ -- panel: change_failure_rate
197
+ with _deployments as (
198
+ SELECT
199
+ cdc.cicd_deployment_id as deployment_id,
200
+ max(cdc.finished_date) as deployment_finished_date
201
+ FROM
202
+ cicd_deployment_commits cdc
203
+ JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id
204
+ and pm."table" = 'cicd_scopes'
205
+ WHERE
206
+ pm.project_name in ('{{PROJECT}}')
207
+ and cdc.result = 'SUCCESS'
208
+ and cdc.environment = 'PRODUCTION'
209
+ GROUP BY
210
+ 1
211
+ HAVING
212
+ max(cdc.finished_date) BETWEEN TIMESTAMP '{{FROM}}' AND TIMESTAMP '{{TO}}'
213
+ ),
214
+ _failure_caused_by_deployments as (
215
+ -- calculate the number of incidents caused by each deployment
216
+ SELECT
217
+ d.deployment_id,
218
+ d.deployment_finished_date,
219
+ count(
220
+ distinct case
221
+ when i.id is not null then d.deployment_id
222
+ else null
223
+ end
224
+ ) as has_incident
225
+ FROM
226
+ _deployments d
227
+ left join project_incident_deployment_relationships pim on d.deployment_id = pim.deployment_id
228
+ left join incidents i on pim.id = i.id
229
+ GROUP BY
230
+ 1,
231
+ 2
232
+ ),
233
+ _change_failure_rate as (
234
+ SELECT
235
+ case
236
+ when count(deployment_id) is null then null
237
+ else sum(has_incident) / count(deployment_id)
238
+ end as change_failure_rate
239
+ FROM
240
+ _failure_caused_by_deployments
241
+ ),
242
+ _is_collected_data as(
243
+ SELECT
244
+ CASE
245
+ WHEN COUNT(i.id) = 0
246
+ AND COUNT(cdc.id) = 0 THEN 'No All'
247
+ WHEN COUNT(i.id) = 0 THEN 'No Incidents'
248
+ WHEN COUNT(cdc.id) = 0 THEN 'No Deployments'
249
+ END AS is_collected
250
+ FROM
251
+ (
252
+ SELECT
253
+ 1
254
+ ) AS dummy
255
+ LEFT JOIN incidents i ON 1 = 1
256
+ LEFT JOIN cicd_deployment_commits cdc ON 1 = 1
257
+ )
258
+ SELECT
259
+ CASE
260
+ WHEN ('{{REPORT}}') = '2023' THEN CASE
261
+ WHEN is_collected = 'No All' THEN 'N/A. Please check if you have collected deployments/incidents.'
262
+ WHEN is_collected = 'No Incidents' THEN 'N/A. Please check if you have collected incidents.'
263
+ WHEN is_collected = 'No Deployments' THEN 'N/A. Please check if you have collected deployments.'
264
+ WHEN change_failure_rate <=.05 THEN CONCAT(round(change_failure_rate * 100, 1), '%(elite)')
265
+ WHEN change_failure_rate <=.10 THEN CONCAT(round(change_failure_rate * 100, 1), '%(high)')
266
+ WHEN change_failure_rate <=.15 THEN CONCAT(round(change_failure_rate * 100, 1), '%(medium)')
267
+ WHEN change_failure_rate >.15 THEN CONCAT(round(change_failure_rate * 100, 1), '%(low)')
268
+ ELSE 'N/A. Please check if you have collected deployments/incidents.'
269
+ END
270
+ WHEN ('{{REPORT}}') = '2021' THEN CASE
271
+ WHEN is_collected = 'No All' THEN 'N/A. Please check if you have collected deployments/incidents.'
272
+ WHEN is_collected = 'No Incidents' THEN 'N/A. Please check if you have collected incidents.'
273
+ WHEN is_collected = 'No Deployments' THEN 'N/A. Please check if you have collected deployments.'
274
+ WHEN change_failure_rate <=.15 THEN CONCAT(round(change_failure_rate * 100, 1), '%(elite)')
275
+ WHEN change_failure_rate <=.20 THEN CONCAT(round(change_failure_rate * 100, 1), '%(high)')
276
+ WHEN change_failure_rate <=.30 THEN CONCAT(round(change_failure_rate * 100, 1), '%(medium)')
277
+ WHEN change_failure_rate >.30 THEN CONCAT(round(change_failure_rate * 100, 1), '%(low)')
278
+ ELSE 'N/A. Please check if you have collected deployments/incidents.'
279
+ END
280
+ ELSE 'No data'
281
+ END AS change_failure_rate
282
+ FROM
283
+ _change_failure_rate,
284
+ _is_collected_data;
285
+
286
+ -- panel: recovery
287
+ with _deployments as (
288
+ SELECT
289
+ cdc.cicd_deployment_id as deployment_id,
290
+ max(cdc.finished_date) as deployment_finished_date
291
+ FROM
292
+ cicd_deployment_commits cdc
293
+ JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id
294
+ and pm."table" = 'cicd_scopes'
295
+ WHERE
296
+ pm.project_name in ('{{PROJECT}}')
297
+ and cdc.result = 'SUCCESS'
298
+ and cdc.environment = 'PRODUCTION'
299
+ GROUP BY
300
+ 1
301
+ HAVING
302
+ max(cdc.finished_date) BETWEEN TIMESTAMP '{{FROM}}' AND TIMESTAMP '{{TO}}'
303
+ ),
304
+ _incidents_for_deployments as (
305
+ SELECT
306
+ i.id as incident_id,
307
+ i.created_date as incident_create_date,
308
+ i.resolution_date as incident_resolution_date,
309
+ fd.deployment_id as caused_by_deployment,
310
+ fd.deployment_finished_date,
311
+ strftime(fd.deployment_finished_date, '%y/%m') as deployment_finished_month
312
+ FROM
313
+ incidents i
314
+ left join project_incident_deployment_relationships pim on i.id = pim.id
315
+ join _deployments fd on pim.deployment_id = fd.deployment_id
316
+ WHERE
317
+ i.resolution_date BETWEEN TIMESTAMP '{{FROM}}' AND TIMESTAMP '{{TO}}'
318
+ ),
319
+ _recovery_time_ranks as (
320
+ SELECT
321
+ *,
322
+ percent_rank() over(
323
+ order by
324
+ trunc((epoch(incident_resolution_date) - epoch(deployment_finished_date)) / 60)
325
+ ) as ranks
326
+ FROM
327
+ _incidents_for_deployments
328
+ ),
329
+ _median_recovery_time as (
330
+ SELECT
331
+ max(
332
+ CAST(trunc((epoch(incident_resolution_date) - epoch(deployment_finished_date)) / 60) AS BIGINT)
333
+ ) as median_recovery_time
334
+ FROM
335
+ _recovery_time_ranks
336
+ WHERE
337
+ ranks <= 0.5
338
+ ),
339
+
340
+ _is_collected_data as(
341
+ SELECT
342
+ CASE
343
+ WHEN EXISTS(select COUNT(d.deployment_id) from _deployments) = 0 AND EXISTS(select COUNT(i.incident_id) FROM incidents) = 0 THEN 'No deployments and incidents'
344
+ WHEN EXISTS(select COUNT(d.deployment_id) from _deployments) = 0 THEN 'No Deployments'
345
+ WHEN EXISTS(select COUNT(i.incident_id) FROM incidents) = 0 THEN 'No Incidents'
346
+ Else 'No incidents are mapped to deployments'
347
+ END AS is_collected
348
+ FROM
349
+ _deployments d, _incidents_for_deployments i
350
+ ),
351
+
352
+ _metric_recovery_time_2023_report as(
353
+ SELECT
354
+ CASE
355
+ WHEN ('{{REPORT}}') = '2023' THEN CASE
356
+ WHEN is_collected = 'No deployments and incidents' THEN 'N/A. Please check if you have collected deployments and incidents.'
357
+ WHEN is_collected = 'No Deployments' THEN 'N/A. Please check if you have collected deployments.'
358
+ WHEN is_collected = 'No Incidents' THEN 'N/A. Please check if you have collected incidents.'
359
+ WHEN median_recovery_time < 60 THEN CONCAT(round(median_recovery_time / 60, 1), '(elite)')
360
+ WHEN median_recovery_time < 24 * 60 THEN CONCAT(round(median_recovery_time / 60, 1), '(high)')
361
+ WHEN median_recovery_time < 7 * 24 * 60 THEN CONCAT(round(median_recovery_time / 60, 1), '(medium)')
362
+ WHEN median_recovery_time >= 7 * 24 * 60 THEN CONCAT(round(median_recovery_time / 60, 1), '(low)')
363
+ ELSE 'No data'
364
+ END
365
+ END AS median_recovery_time
366
+ FROM
367
+ _median_recovery_time,
368
+ _is_collected_data
369
+ ),
370
+ _incidents as (
371
+ -- get the incidents created within the selected time period in the top-right corner
372
+ SELECT
373
+ distinct i.id,
374
+ cast(lead_time_minutes as BIGINT) as lead_time_minutes
375
+ FROM
376
+ incidents i
377
+ join project_mapping pm on i.scope_id = pm.row_id
378
+ and pm."table" = i."table"
379
+ WHERE
380
+ pm.project_name in ('{{PROJECT}}')
381
+ and i.resolution_date BETWEEN TIMESTAMP '{{FROM}}' AND TIMESTAMP '{{TO}}'
382
+ ),
383
+ _median_mttr_ranks as(
384
+ SELECT
385
+ *,
386
+ percent_rank() over(
387
+ order by
388
+ lead_time_minutes
389
+ ) as ranks
390
+ FROM
391
+ _incidents
392
+ ),
393
+ _median_mttr as(
394
+ SELECT
395
+ max(lead_time_minutes) as median_time_to_resolve
396
+ FROM
397
+ _median_mttr_ranks
398
+ WHERE
399
+ ranks <= 0.5
400
+ ),
401
+ _metric_mttr_2021_report as(
402
+ SELECT
403
+ CASE
404
+ WHEN ('{{REPORT}}') = '2021' THEN CASE
405
+ WHEN median_time_to_resolve < 60 THEN CONCAT(round(median_time_to_resolve / 60, 1), '(elite)')
406
+ WHEN median_time_to_resolve < 24 * 60 THEN CONCAT(round(median_time_to_resolve / 60, 1), '(high)')
407
+ WHEN median_time_to_resolve < 7 * 24 * 60 THEN CONCAT(
408
+ round(median_time_to_resolve / 60, 1),
409
+ '(medium)'
410
+ )
411
+ WHEN median_time_to_resolve >= 7 * 24 * 60 THEN CONCAT(round(median_time_to_resolve / 60, 1), '(low)')
412
+ ELSE 'N/A. Please check if you have collected incidents.'
413
+ END
414
+ END AS median_time_to_resolve
415
+ FROM
416
+ _median_mttr
417
+ )
418
+ SELECT
419
+ median_recovery_time AS median_time_in_hour
420
+ FROM
421
+ _metric_recovery_time_2023_report
422
+ WHERE
423
+ ('{{REPORT}}') = '2023'
424
+ UNION
425
+ SELECT
426
+ median_time_to_resolve AS median_time_to_resolve
427
+ FROM
428
+ _metric_mttr_2021_report
429
+ WHERE
430
+ ('{{REPORT}}') = '2021';
431
+
432
+ -- panel: monthly
433
+ with _deployments as(
434
+ SELECT
435
+ strftime(deployment_finished_date,'%y/%m') as month,
436
+ count(cicd_deployment_id) as deployment_count
437
+ FROM (
438
+ SELECT
439
+ cdc.cicd_deployment_id,
440
+ max(cdc.finished_date) as deployment_finished_date
441
+ FROM cicd_deployment_commits cdc
442
+ JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id and pm."table" = 'cicd_scopes'
443
+ WHERE
444
+ pm.project_name in ('{{PROJECT}}')
445
+ and cdc.result = 'SUCCESS'
446
+ and cdc.environment = 'PRODUCTION'
447
+ GROUP BY 1
448
+ HAVING max(cdc.finished_date) BETWEEN TIMESTAMP '{{FROM}}' AND TIMESTAMP '{{TO}}'
449
+ ) _production_deployments
450
+ GROUP BY 1
451
+ )
452
+
453
+ SELECT
454
+ cm.month,
455
+ case when d.deployment_count is null then 0 else d.deployment_count end as "Deployment Count"
456
+ FROM
457
+ calendar_months cm
458
+ LEFT JOIN _deployments d on cm.month = d.month
459
+ WHERE month_timestamp between CAST(date_trunc('month', TIMESTAMP '{{FROM}}') AS date) AND CAST(date_trunc('month', TIMESTAMP '{{TO}}') AS date)
460
+ ORDER BY cm.month_timestamp;
461
+
462
+ -- panel: pr_cycle_time
463
+ -- From DevLake's Engineering Throughput and Cycle Time dashboard ("PR Cycle Time [Merged PRs]", monthly interval).
464
+ with _prs as(
465
+ SELECT
466
+ pr.id,
467
+ pr.merged_date as pr_merged_date,
468
+ -- convert null to 0 if a PR has no cycle_time to make sure cycle_time equals the sum of the four metrics below
469
+ coalesce(prm.pr_cycle_time/60,0) as cycle_time
470
+ FROM pull_requests pr
471
+ join project_pr_metrics prm on pr.id = prm.id
472
+ join project_mapping pm on pr.base_repo_id = pm.row_id and pm."table" = 'repos'
473
+ WHERE
474
+ pr.merged_date BETWEEN TIMESTAMP '{{FROM}}' AND TIMESTAMP '{{TO}}'
475
+ and pr.created_date >= CAST(date_trunc('month', TIMESTAMP '{{FROM}}') AS date) + INTERVAL 1 MONTH
476
+ and pm.project_name in ('{{PROJECT}}')
477
+ GROUP BY 1,2,3
478
+ )
479
+
480
+ SELECT
481
+ CAST(CAST(pr_merged_date AS date) - INTERVAL (day(CAST(pr_merged_date AS date)) - 1) DAY AS date) as time,
482
+ avg(cycle_time) as "PR Cycle Time(h)"
483
+ FROM _prs
484
+ GROUP BY 1
485
+ ORDER BY 1;