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,138 @@
1
+ """`stackvitals ui owner/repo`: a repo's StackVitals pages on your machine, Explore included, from a local copy of its
2
+ stackvitals-data branch. Private repos and self-hosters get the full dashboard without Pages; nothing is served beyond
3
+ 127.0.0.1."""
4
+ import argparse
5
+ import base64
6
+ import functools
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ import webbrowser
11
+ from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
12
+ from pathlib import Path
13
+
14
+ from stackvitals.ci import cli as ci_cli
15
+ from stackvitals.ci.action import BRANCH
16
+
17
+
18
+ class _Quiet(SimpleHTTPRequestHandler):
19
+ def log_message(self, *args): # one line per file would bury the address
20
+ pass
21
+
22
+
23
+ def _git(args: list[str], tok: str | None, remote: str | None) -> subprocess.CompletedProcess:
24
+ # The token goes in a header for this one command, through its environment: never the copy's config or the process list.
25
+ env = None
26
+ if tok and not remote:
27
+ basic = base64.b64encode(f"x-access-token:{tok}".encode()).decode()
28
+ env = dict(os.environ)
29
+ try:
30
+ n = int(env.get("GIT_CONFIG_COUNT") or 0)
31
+ except ValueError:
32
+ n = 0
33
+ env.update({"GIT_CONFIG_COUNT": str(n + 1), f"GIT_CONFIG_KEY_{n}": "http.extraheader",
34
+ f"GIT_CONFIG_VALUE_{n}": f"AUTHORIZATION: basic {basic}"})
35
+ return subprocess.run(["git", *args], capture_output=True, text=True, env=env)
36
+
37
+
38
+ def fetch(repo: str, tok: str | None, folder: Path, remote: str | None = None) -> Path:
39
+ """A shallow copy of the data branch in `folder`, brought up to date (the branch may have been squashed and
40
+ force-pushed, so the copy follows it rather than merging)."""
41
+ folder = Path(folder)
42
+ url = remote or f"https://github.com/{repo}.git"
43
+ if (folder / ".git").is_dir():
44
+ done = _git(["-C", str(folder), "fetch", "--depth", "1", url, BRANCH], tok, remote)
45
+ if done.returncode == 0:
46
+ _git(["-C", str(folder), "reset", "--hard", "FETCH_HEAD"], None, None)
47
+ return folder
48
+ folder.parent.mkdir(parents=True, exist_ok=True)
49
+ done = _git(["clone", "--quiet", "--depth", "1", "--branch", BRANCH, "--single-branch", url, str(folder)], tok, remote)
50
+ if done.returncode != 0:
51
+ raise RuntimeError(f"no {BRANCH} branch to read in {repo} (does StackVitals run there, and can your token read it?)")
52
+ return folder
53
+
54
+
55
+ def serve(folder: Path, port: int = 0) -> ThreadingHTTPServer:
56
+ return ThreadingHTTPServer(("127.0.0.1", port), functools.partial(_Quiet, directory=str(folder)))
57
+
58
+
59
+ def first_page(docs: Path) -> str | None:
60
+ """The page to open: the app's home when there is one, else the first module page the branch has (Portal alone too)."""
61
+ from stackvitals import shell
62
+ return next((p for p in ("index.html", *(name for name, _ in shell.MODULES)) if (Path(docs) / p).is_file()), None)
63
+
64
+
65
+ def _run_job(argv: list[str], env: dict) -> int:
66
+ # The Action's own job, in this process, with the environment a GitHub run would have.
67
+ import os
68
+ from stackvitals.ci.cli import action_main
69
+ saved = dict(os.environ)
70
+ os.environ.update(env)
71
+ try:
72
+ return action_main(argv)
73
+ finally:
74
+ os.environ.clear()
75
+ os.environ.update(saved)
76
+
77
+
78
+ def collect_locally(repo: str, tok: str | None, home: Path, days: int = 90) -> str:
79
+ """Runs the Action's job on this machine into a local data branch (a bare repo under `home`), reading GitHub with the
80
+ given token and never writing to it. Returns the local remote's URL."""
81
+ import tempfile
82
+ bare = Path(home) / "local" / f"{repo.replace('/', '__')}.git"
83
+ if not (bare / "HEAD").exists():
84
+ bare.parent.mkdir(parents=True, exist_ok=True)
85
+ subprocess.run(["git", "init", "-q", "--bare", str(bare)], check=True)
86
+ env = {"GITHUB_REPOSITORY": repo, "STACKVITALS_TOKEN": tok or "", "GITHUB_TOKEN": tok or "", "STACKVITALS_REMOTE": str(bare),
87
+ "GITHUB_EVENT_NAME": "schedule", "STACKVITALS_READ_ONLY": "1", "RUNNER_TEMP": tempfile.mkdtemp(prefix="stackvitals-")}
88
+ code = _run_job(["--modules", "ci,flow", "--days", str(days), "--pr-comments", "false"], env)
89
+ if code:
90
+ raise RuntimeError(f"the local run ended with code {code}")
91
+ # A plain path, not a file:// URL: Git Bash setups that skip path conversion misread file:///C:/... addresses.
92
+ return str(bare)
93
+
94
+
95
+ def main(argv: list[str] | None = None, tok: str | None = None, home: Path | None = None, open_browser=webbrowser.open) -> int:
96
+ parser = argparse.ArgumentParser(prog="stackvitals ui", description="Open a repo's StackVitals pages from a local copy.")
97
+ parser.add_argument("repo", help="owner/name")
98
+ parser.add_argument("--port", type=int, default=0, help="port on 127.0.0.1 (default: any free one)")
99
+ parser.add_argument("--no-open", action="store_true", help="print the address instead of opening a browser")
100
+ parser.add_argument("--remote", help=argparse.SUPPRESS) # another place to clone from (tests, mirrors)
101
+ parser.add_argument("--collect", action="store_true",
102
+ help="no StackVitals in the repo? read its history on this machine first (no Action, no server; nothing is written to GitHub)")
103
+ parser.add_argument("--days", type=int, default=90, help="with --collect: days of history to read")
104
+ args = parser.parse_args(argv)
105
+ tok = tok if tok is not None else (None if args.remote else ci_cli.token())
106
+ folder = Path(home or ci_cli.home()) / "ui" / args.repo.replace("/", "__")
107
+ if args.collect and not args.remote:
108
+ print(f"Reading {args.repo}'s history on this machine (a few minutes; nothing is written to GitHub)...", flush=True)
109
+ try:
110
+ args.remote = collect_locally(args.repo, tok, Path(home or ci_cli.home()), args.days)
111
+ except Exception as exc: # noqa: BLE001 - say what went wrong instead of a traceback
112
+ print(f"Could not read {args.repo}: {exc}", file=sys.stderr)
113
+ return 1
114
+ try:
115
+ fetch(args.repo, tok, folder, remote=args.remote)
116
+ except RuntimeError as exc:
117
+ print(f"Could not open {args.repo}: {exc}", file=sys.stderr)
118
+ return 1
119
+ if (folder / "docs").is_dir():
120
+ # This install's app, not the copy the branch was last written with (a branch may lag a release or two).
121
+ from stackvitals.core.webapp import write_webapp
122
+ write_webapp(folder / "docs")
123
+ page = first_page(folder / "docs")
124
+ if page is None:
125
+ print(f"{args.repo}'s {BRANCH} branch has no pages yet.", file=sys.stderr)
126
+ return 1
127
+ server = serve(folder, args.port)
128
+ url = f"http://127.0.0.1:{server.server_port}/docs/{page}"
129
+ print(f"StackVitals for {args.repo}: {url} (Explore: /docs/explore.html). Ctrl+C to stop.", flush=True)
130
+ if not args.no_open:
131
+ open_browser(url)
132
+ try:
133
+ server.serve_forever()
134
+ except KeyboardInterrupt:
135
+ pass
136
+ finally:
137
+ server.server_close()
138
+ return 0
@@ -0,0 +1,79 @@
1
+ """The StackVitals app, one static page over every module, copied beside the pages at the end of every job: index.html is
2
+ the app (built from web/ into the package's web/index.html and web/app/; the CI page is ci.html), each file is written
3
+ only when it changed, so a quiet run commits nothing, and app files the package no longer carries are removed."""
4
+ import hashlib
5
+ import re
6
+ from importlib import resources
7
+ from pathlib import Path
8
+
9
+ APP_DIR = "app"
10
+ TEXT = (".js", ".mjs", ".css", ".html", ".md", ".json", ".txt", ".svg")
11
+ # Relative module imports in the app's scripts (static, side-effect and dynamic), and the page's links to its scripts and styles.
12
+ IMPORTS = re.compile(rb"""(\bfrom\s*["']|\bimport\s*\(?\s*["'])(\.{1,2}/[^"'?]+\.m?js)(?=["'])""")
13
+ LINKS = re.compile(rb"""((?:href|src)=")((?:app|site)/[^"?]+\.(?:m?js|css))(?=")""")
14
+ WEB = resources.files("stackvitals") / "web"
15
+ # The project site (landing page and forwards to the app) lives beside the app in the package but never goes to a repo's Pages.
16
+ SITE_SOURCE = Path(str(WEB / "site"))
17
+
18
+
19
+ def _walk(node, prefix: str, out: dict, skip=()) -> dict:
20
+ for child in node.iterdir():
21
+ if child.name.startswith((".", "__")) or child.name in skip:
22
+ continue
23
+ if child.is_dir():
24
+ _walk(child, f"{prefix}{child.name}/", out)
25
+ else:
26
+ out[prefix + child.name] = child.read_bytes()
27
+ return out
28
+
29
+
30
+ def files() -> dict[str, bytes]:
31
+ """The app's files from the package, by their path under docs/."""
32
+ return _walk(WEB, "", {}, skip=("site",))
33
+
34
+
35
+ def site_files() -> dict[str, bytes]:
36
+ """The project site's own files: its pages at the root, its styles under site/."""
37
+ return {(name if name.endswith(".html") else f"site/{name}"): data for name, data in _walk(WEB / "site", "", {}).items()}
38
+
39
+
40
+ def stamped(app: dict[str, bytes]) -> dict[str, bytes]:
41
+ """Every app URL gets `?v=` + a hash of all the files, so a browser never mixes a cached release with a new one."""
42
+ # Text files lose a Windows checkout's CRLF; fonts and other binaries are kept byte for byte.
43
+ norm = {k: v.replace(b"\r\n", b"\n") if k.endswith(TEXT) else v for k, v in app.items()}
44
+ digest = hashlib.sha256(b"".join(k.encode() + b"\0" + norm[k] + b"\0" for k in sorted(norm))).hexdigest()[:10]
45
+ tag = rb"\1\2?v=" + digest.encode()
46
+ out = {}
47
+ for name, data in norm.items():
48
+ if name.endswith((".js", ".mjs")):
49
+ data = IMPORTS.sub(tag, data)
50
+ elif name.endswith(".html"):
51
+ data = LINKS.sub(tag, data)
52
+ out[name] = data
53
+ return out
54
+
55
+
56
+ def write_webapp(docs: Path) -> list[str]:
57
+ """Copy the app into docs/ and return the paths that changed."""
58
+ docs = Path(docs)
59
+ wanted = stamped(files())
60
+ changed = []
61
+ same = lambda a, b: a.replace(b"\r\n", b"\n") == b.replace(b"\r\n", b"\n") # a Windows checkout turns LF into CRLF
62
+ for rel, data in sorted(wanted.items()):
63
+ target = docs / rel
64
+ if target.is_file() and same(target.read_bytes(), data):
65
+ continue
66
+ target.parent.mkdir(parents=True, exist_ok=True)
67
+ target.write_bytes(data)
68
+ changed.append(rel)
69
+ app = docs / APP_DIR
70
+ if app.is_dir():
71
+ # The earlier app's files (views/, vendor/, old fonts) go too, so an upgraded site carries only this release.
72
+ for path in sorted(app.rglob("*"), reverse=True):
73
+ rel = path.relative_to(docs).as_posix()
74
+ if path.is_file() and rel not in wanted:
75
+ path.unlink()
76
+ changed.append(rel)
77
+ elif path.is_dir() and not any(path.iterdir()):
78
+ path.rmdir()
79
+ return changed
@@ -0,0 +1,76 @@
1
+ """What changed before an incident: the deployments in the 72 hours before it began and the pull requests merged in
2
+ that time (each with the deployment that shipped it), config and dependency changes called out, ranked so the likeliest
3
+ cause comes first: the incident's own (or the last) deployment, a pull request a revert undid, then closeness in time."""
4
+ import re
5
+ from datetime import datetime, timedelta
6
+
7
+ from stackvitals.core.ai_outcomes import is_revert, revert_target
8
+
9
+ WINDOW_HOURS = 72
10
+ MAX_CANDIDATES = 15
11
+ DEPENDENCY = re.compile(r"(^|/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?|poetry\.lock|uv\.lock|Pipfile\.lock|"
12
+ r"requirements[^/]*\.txt|go\.(mod|sum)|Cargo\.(lock|toml)|Gemfile(\.lock)?|composer\.(json|lock)|"
13
+ r"packages\.lock\.json|gradle\.lockfile|pom\.xml|package\.json|pyproject\.toml)$")
14
+ CONFIG = re.compile(r"(^|/)(\.github/workflows/|Dockerfile|docker-compose[^/]*\.ya?ml$|[^/]+\.tf$|helm/|k8s/|kubernetes/|"
15
+ r"deploy/|config/|\.env\.example$|[^/]+\.(toml|ini|cfg|conf)$)")
16
+
17
+
18
+ def _t(text: str) -> datetime:
19
+ return datetime.fromisoformat(text.replace("Z", "+00:00"))
20
+
21
+
22
+ def _kind(paths: list[str]) -> str:
23
+ if any(DEPENDENCY.search(p) for p in paths):
24
+ return "dependency"
25
+ if any(CONFIG.search(p) for p in paths):
26
+ return "config"
27
+ return "pr"
28
+
29
+
30
+ def timeline(incident: dict, rows: list, deps: list[dict], hours: int = WINDOW_HOURS) -> dict:
31
+ """{"incident", "window_hours", "candidates": [{"kind", "ref", "at", "why", "deployment"}]}, likeliest first."""
32
+ start = _t(incident["created"])
33
+ since = start - timedelta(hours=hours)
34
+ within = lambda when: bool(when) and since <= _t(when) < start # noqa: E731
35
+ shipped = [d for d in deps if within(d.get("finished"))]
36
+ own = incident.get("deployment")
37
+ last = max(shipped, key=lambda d: d["finished"])["id"] if shipped else None
38
+ by_number = {p["n"]: p for p, _ph, _d in rows}
39
+ reverted = {n for p, _ph, _d in rows if is_revert(p) and (n := revert_target(p, by_number)) is not None}
40
+ near = lambda when: 1 - (start - _t(when)).total_seconds() / (hours * 3600) # noqa: E731
41
+ out = []
42
+ for d in shipped:
43
+ mine = d["id"] == own or (own is None and d["id"] == last)
44
+ out.append({"kind": "deployment", "ref": f"deployment {(d.get('sha') or '')[:7] or d['id']}", "at": d["finished"],
45
+ "deployment": d["id"],
46
+ "why": "the deployment this incident is tied to" if d["id"] == own else
47
+ "the last deployment before it began" if d["id"] == last else "deployed in the window",
48
+ "rank": 100 if mine else 40 + 20 * near(d["finished"])})
49
+ for p, _ph, d in rows:
50
+ if not within(p.get("mg")) or is_revert(p):
51
+ continue
52
+ kind = _kind(p.get("paths") or [])
53
+ why = []
54
+ rank = 30 * near(p["mg"])
55
+ if p["n"] in reverted:
56
+ why.append("later reverted")
57
+ rank += 60
58
+ if d and d["id"] in (own, last):
59
+ why.append("shipped in that deployment")
60
+ rank += 40
61
+ if kind != "pr":
62
+ why.append(f"changes {'dependencies' if kind == 'dependency' else 'configuration'}")
63
+ rank += 5
64
+ out.append({"kind": kind, "ref": f"#{p['n']}", "at": p["mg"], "deployment": d["id"] if d else None,
65
+ "why": "; ".join(why) or "merged in the window", "rank": rank, "title": (p.get("ti") or "")[:120]})
66
+ out.sort(key=lambda c: -c["rank"])
67
+ return {"incident": {k: incident.get(k) for k in ("source", "number", "created", "resolved", "deployment")},
68
+ "window_hours": hours, "candidates": [{k: v for k, v in c.items() if k != "rank"} for c in out[:MAX_CANDIDATES]]}
69
+
70
+
71
+ def recent(incidents: list[dict], rows: list, deps: list[dict], now: datetime, days: int = 90, most: int = 10) -> list[dict]:
72
+ """A timeline for each incident that began in the last `days`, newest first."""
73
+ floor = now - timedelta(days=days)
74
+ fresh = sorted((i for i in incidents if i.get("created") and _t(i["created"]) >= floor), key=lambda i: i["created"],
75
+ reverse=True)
76
+ return [timeline(i, rows, deps) for i in fresh[:most]]
@@ -0,0 +1,194 @@
1
+ """One checkout of the data branch per job: every module writes into it, there is one commit, and what must happen only
2
+ after the commit (re-runs, comments, issues, alerts) runs afterwards. A rejected push (another run got there first)
3
+ re-runs every step on the fresh data, so overlapping runs converge and nothing is lost."""
4
+ import os
5
+ import shutil
6
+ import stat
7
+ import tempfile
8
+ import time
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+
12
+ from stackvitals.ci.action import BRANCH, DataBranch, GitError
13
+ from stackvitals.core.store import DEFAULT_CAP_MB, SQUASH_EVERY, STREAMS, HistoryStore
14
+
15
+ RETRIES = 3
16
+
17
+
18
+ def _rmtree(path: Path) -> None:
19
+ # Git leaves read-only files on Windows; clear the bit and retry so no clone is left behind.
20
+ def onerror(fn, p, _exc):
21
+ try:
22
+ os.chmod(p, stat.S_IWRITE)
23
+ fn(p)
24
+ except OSError:
25
+ pass
26
+ shutil.rmtree(path, onerror=onerror)
27
+
28
+
29
+ class Workspace:
30
+ def __init__(self, env: dict, now: datetime | None = None):
31
+ self.env = env
32
+ self.now = now or datetime.now(timezone.utc)
33
+ self.repo = env["GITHUB_REPOSITORY"]
34
+ token = env.get("STACKVITALS_TOKEN") or env.get("GITHUB_TOKEN")
35
+ self.remote = env.get("STACKVITALS_REMOTE") or f"https://x-access-token:{token}@github.com/{self.repo}.git"
36
+ temp_root = env.get("RUNNER_TEMP")
37
+ if temp_root:
38
+ Path(temp_root).mkdir(parents=True, exist_ok=True)
39
+ self.work = Path(tempfile.mkdtemp(prefix="stackvitals-", dir=temp_root))
40
+ self.branch: DataBranch | None = None
41
+ self.hooks: list[tuple[str, object]] = []
42
+ self.notes: list[str] = []
43
+ # Side effects already done on GitHub in this job (a settings commit), kept when a rejected push retries the steps.
44
+ self.memo: dict = {}
45
+ self.results: dict = {}
46
+ # Data one step loaded and a later step may reuse, for this attempt only.
47
+ self.cache: dict = {}
48
+
49
+ @property
50
+ def path(self) -> Path:
51
+ return self.branch.path
52
+
53
+ def open(self, attempt: int = 0) -> None:
54
+ # Hooks registered during a rejected attempt are dropped: the steps run again and register them again.
55
+ self.hooks = []
56
+ self.cache = {}
57
+ from stackvitals.core.bucket import from_env
58
+ # A bucket when STACKVITALS_STORAGE names one (s3://...), else the stackvitals-data branch.
59
+ self.branch = from_env(self.env, self.work / f"data{attempt}") or DataBranch(self.remote, BRANCH, self.work / f"data{attempt}")
60
+ self.branch.open()
61
+
62
+ def after_commit(self, fn, label: str = "", first: bool = False) -> None:
63
+ # first: the job summary opens with it (what to fix first), whichever step registered it.
64
+ if first:
65
+ self.hooks.insert(0, (label, fn))
66
+ else:
67
+ self.hooks.append((label, fn))
68
+
69
+ def commit(self, message: str) -> str:
70
+ return self.branch.commit_push(message)
71
+
72
+ def run_hooks(self) -> None:
73
+ for label, fn in self.hooks:
74
+ try:
75
+ fn()
76
+ except Exception as exc: # noqa: BLE001
77
+ # The data is committed by now; a chat service or a comment refusal must not fail the run.
78
+ self.notes.append(f"{label}: {exc}")
79
+
80
+ def close(self) -> None:
81
+ _rmtree(self.work)
82
+
83
+
84
+ def housekeeping(ws: Workspace) -> dict:
85
+ """Retention per stream, the size cap, and the commit counter; all tunable through the Action's inputs."""
86
+ env, store = ws.env, HistoryStore(ws.path)
87
+ today = ws.now.date().isoformat()
88
+ days = {}
89
+ if env.get("STACKVITALS_RETENTION_DAYS"):
90
+ days.update({s: int(env["STACKVITALS_RETENTION_DAYS"]) for s in STREAMS if s != "ci/days"})
91
+ if env.get("STACKVITALS_CI_RETENTION_DAYS"):
92
+ days["ci/days"] = int(env["STACKVITALS_CI_RETENTION_DAYS"])
93
+ removed = store.retention(today, days)
94
+ cap_mb = float(env.get("STACKVITALS_SIZE_CAP_MB") or DEFAULT_CAP_MB)
95
+ dropped = store.cap(today, int(cap_mb * 1024 * 1024))
96
+ commits = store.bump_commits() if ws.branch.dirty() else int(store.meta().get("commits") or 0)
97
+ return {"removed": removed, "dropped": dropped, "commits": commits, "squashed": False}
98
+
99
+
100
+ # The steps that write summary/ files, which the Explore page reads.
101
+ SUMMARY_STEPS = ("CI Health", "Team Flow")
102
+
103
+
104
+ def _explore(ws, steps) -> None:
105
+ if not any(label in SUMMARY_STEPS for label, _ in steps) and not (ws.path / "summary" / "manifest.json").exists():
106
+ return # a Portal-only job has nothing to explore
107
+ try:
108
+ from stackvitals.core.explore import write_explore
109
+ write_explore(ws.path, ws.repo)
110
+ except Exception as exc: # noqa: BLE001 - an extra page must never cost the modules' data
111
+ ws.notes.append(f"Explore page skipped ({exc}).")
112
+
113
+
114
+ def run_job(env: dict, steps: list[tuple[str, object]], now: datetime | None = None, message: str | None = None,
115
+ catch: bool = True, prepare=None) -> dict:
116
+ """Open the data branch once, run every step on it, keep house, commit once, then run the hooks. Returns the
117
+ outcome (pushed, unchanged), each step's result, the errors of steps that failed, and the hooks' notes. `prepare`
118
+ runs first on each fresh checkout (the repo's settings, shared by every step)."""
119
+ ws = Workspace(env, now)
120
+ try:
121
+ for attempt in range(RETRIES):
122
+ if attempt:
123
+ # Two runs collided; a random, growing wait keeps them from colliding again in step.
124
+ from stackvitals.core.bucket import backoff
125
+ time.sleep(backoff(attempt))
126
+ ws.open(attempt)
127
+ if prepare is not None:
128
+ try:
129
+ prepare(ws)
130
+ except Exception as exc: # noqa: BLE001 - the steps still run, on the defaults
131
+ ws.notes.append(f"Settings were not read ({exc}); the defaults were used.")
132
+ try:
133
+ from stackvitals.core.webapp import write_webapp
134
+ write_webapp(ws.path / "docs") # the app at the site's front door, before the pages that link to it
135
+ except Exception as exc: # noqa: BLE001 - the app's files must never cost the modules' data
136
+ ws.notes.append(f"App files skipped ({exc}).")
137
+ _explore(ws, steps) # before the modules, so every page they write links to it from the first run
138
+ results, errors = {}, {}
139
+ ws.results = results
140
+ for label, step in steps:
141
+ if not catch:
142
+ results[label] = step(ws)
143
+ continue
144
+ try:
145
+ results[label] = step(ws)
146
+ except Exception as exc: # noqa: BLE001
147
+ # One step's trouble must not lose the others' work; the runner reports it.
148
+ errors[label] = str(exc)
149
+ _explore(ws, steps) # and after, so its own header lists every page the modules wrote
150
+ try:
151
+ from stackvitals.core.appdata import write_app_data
152
+ write_app_data(ws) # what the app reads: what ran, settings, decisions, the tables beside the pages
153
+ except Exception as exc: # noqa: BLE001 - the app's files must never cost the modules' data
154
+ ws.notes.append(f"App files skipped ({exc}).")
155
+ house = housekeeping(ws)
156
+ try:
157
+ from stackvitals.core.health import write_health
158
+ write_health(ws, results, errors, house) # the app's Data health page
159
+ except Exception as exc: # noqa: BLE001 - an extra page must never cost the modules' data
160
+ ws.notes.append(f"Data health skipped ({exc}).")
161
+ names = ", ".join(label for label, _ in steps)
162
+ outcome = ws.commit(message or f"stackvitals: {names} {ws.now:%Y-%m-%d %H:%M} UTC")
163
+ if outcome != "rejected":
164
+ break
165
+ else:
166
+ raise GitError("the data branch kept changing under us; the next run will catch up")
167
+ if (env.get("STACKVITALS_SQUASH") or "").strip().lower() == "true" and house["commits"] >= SQUASH_EVERY:
168
+ # Opt-in: the files stay byte for byte, only the git history behind them becomes one commit.
169
+ store = HistoryStore(ws.path)
170
+ store.save_meta({**store.meta(), "commits": 0})
171
+ house["squashed"] = ws.branch.squash(f"stackvitals: history squashed {ws.now:%Y-%m-%d}") == "pushed"
172
+ ws.run_hooks()
173
+ if (env.get("STACKVITALS_STORAGE") or "git") != "git":
174
+ ws.notes.append("History is kept in the bucket: stackvitals ui and sql, Explore, the MCP server, the Portal's org "
175
+ "view and quarantine in your CI read the stackvitals-data branch only, so they don't see it yet."
176
+ + ("" if env.get("STACKVITALS_PAGES_DIR") else " No branch publishes the app either: set pages to "
177
+ "deploy or artifact."))
178
+ # Pages from the Action (opt-in): the published folder goes to the deploy steps when this job changed it.
179
+ pages = bool(env.get("STACKVITALS_PAGES_DIR")) and outcome == "pushed" and (ws.path / "docs").is_dir()
180
+ if pages:
181
+ out = Path(env["STACKVITALS_PAGES_DIR"])
182
+ shutil.copytree(ws.path / "docs", out, dirs_exist_ok=True)
183
+ if env.get("STACKVITALS_PAGES_MODE") == "artifact":
184
+ # A download for people who can read the repo, so Explore gets the tables a private repo's Pages leave out.
185
+ if (ws.path / "summary").is_dir() and not (out / "summary").exists():
186
+ shutil.copytree(ws.path / "summary", out / "summary")
187
+ # The app reads its files over HTTP; bound to this machine so nobody else on the network can open it.
188
+ (out / "OPEN-ME.txt").write_text(
189
+ "Open the StackVitals app from this folder: run `python -m http.server 8000 --bind 127.0.0.1` here,\n"
190
+ "then visit http://127.0.0.1:8000/ in a browser. Opening index.html directly doesn't work: browsers block\n"
191
+ "its data files.\n", encoding="utf-8")
192
+ return {"outcome": outcome, "results": results, "errors": errors, "notes": ws.notes, "housekeeping": house, "pages": pages}
193
+ finally:
194
+ ws.close()
@@ -0,0 +1 @@
1
+ """Dependency Steward: should we add, keep, upgrade or replace a package? Evidence over time, never a "safe" stamp."""