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
stackvitals/ci/cli.py ADDED
@@ -0,0 +1,570 @@
1
+ import argparse
2
+ import json
3
+ import os
4
+ import re
5
+ import subprocess
6
+ import sys
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+
10
+ from stackvitals.ci.action import run_action, write_outputs
11
+ from stackvitals.ci.analyze import analyze
12
+ from stackvitals.ci.automation import run_quarantined
13
+ from stackvitals.ci.api import ActionsAPI, APIError, OutOfBudget
14
+ from stackvitals.ci.report import render_text
15
+ from stackvitals.ci.store import DataStore
16
+ from stackvitals.core import services
17
+ from stackvitals.flow.action import run_flow_action
18
+
19
+ ACTION_REF = "stackvitals/stackvitals@v0"
20
+ WORKFLOW = """name: StackVitals
21
+ on:
22
+ workflow_run:
23
+ workflows: {names} # your CI workflows; each finished run is read right away
24
+ types: [completed]
25
+ schedule:
26
+ - cron: "17 */3 * * *" # Team Flow every 3 hours, and a CI catch-up
27
+ repository_dispatch:
28
+ types: [stackvitals-deployment, stackvitals-incident] # sent by `stackvitals record` from any deploy tool
29
+ workflow_dispatch:
30
+ inputs:
31
+ request:
32
+ description: "A change sent from the StackVitals app (leave empty for a normal run)"
33
+ required: false
34
+ default: ""
35
+ push: # your settings file applies at once; adding this file starts the first run
36
+ paths: [".github/stackvitals.toml", ".github/workflows/stackvitals.yml"]
37
+ issues:
38
+ types: [opened, closed, reopened] # requests from the app; flaky-test issues closed as not planned, or reopened
39
+ issue_comment:
40
+ types: [created] # /stackvitals commands
41
+ pull_request: # the dependency check, only when a lockfile changes (never runs the PR's code)
42
+ paths: ["**/*.lock", "**/*lock.json", "**/*lock.yaml", "**/npm-shrinkwrap.json", "**/requirements*.txt", "**/go.mod",
43
+ "**/*.lockfile", "**/pom.xml", "**/pylock*.toml", "**/Package.resolved", "**/gems.locked",
44
+ "**/verification-metadata.xml", "**/*.freeze"]
45
+ permissions:
46
+ contents: write # saves history to the stackvitals-data branch; settings changes asked for from the app
47
+ actions: read # reads your CI runs, jobs and test reports (write: [automation] rerun_flaky re-runs known flakes)
48
+ checks: read # Team Flow: sees which PRs have failing checks (write: [automation] verdict_check)
49
+ statuses: read
50
+ deployments: read # Team Flow: delivery from deployments
51
+ issues: write # the weekly digest, answers to /stackvitals commands, flaky-test and alert issues
52
+ pull-requests: write # reads PRs for Team Flow; one comment on failed PR runs (set to read to turn comments off)
53
+ jobs:
54
+ vitals:
55
+ # Only what concerns StackVitals starts a runner: settings pushed to the default branch, requests and commands from
56
+ # people with access to the repo, and flaky-test issues being closed or reopened.
57
+ if: >-
58
+ (github.event_name != 'push' || github.ref_name == github.event.repository.default_branch) &&
59
+ (github.event_name != 'issues' ||
60
+ (github.event.action == 'opened' && contains(github.event.issue.body, '```stackvitals') &&
61
+ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association)) ||
62
+ (github.event.action != 'opened' && contains(github.event.issue.labels.*.name, 'flaky-test'))) &&
63
+ (github.event_name != 'issue_comment' ||
64
+ (contains(github.event.comment.body, '/stackvitals') &&
65
+ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association))) &&
66
+ (github.event_name != 'workflow_run' || !github.event.repository.private ||
67
+ github.event.workflow_run.conclusion != 'success' || vars.STACKVITALS_EVERY_CI_RUN == 'true')
68
+ # Private repos pay for Actions minutes: a CI run that passed starts no job (the 3-hourly run reads it), unless the
69
+ # repo variable STACKVITALS_EVERY_CI_RUN is "true" (the verdict check needs it). On the job, so events it skips never
70
+ # wait in (or cancel) the queue; PR checks and Test connection runs never wait for (or get dropped behind) data runs.
71
+ concurrency:
72
+ group: stackvitals-${{{{ github.event.pull_request.number || (contains(github.event.inputs.request, '"check"') && format('check-{{0}}', github.run_id)) || 'data' }}}}
73
+ cancel-in-progress: false
74
+ runs-on: ubuntu-latest
75
+ timeout-minutes: 20
76
+ steps:
77
+ - uses: {ref}
78
+ env:
79
+ STACKVITALS_EVERY_CI_RUN: ${{{{ vars.STACKVITALS_EVERY_CI_RUN }}}}
80
+ with:
81
+ modules: ci,flow,deps,community
82
+ jira-email: ${{{{ secrets.JIRA_EMAIL }}}} # optional: Jira Cloud (see Integrations); unused while unset
83
+ jira-token: ${{{{ secrets.JIRA_API_TOKEN }}}}
84
+ linear-token: ${{{{ secrets.LINEAR_API_KEY }}}} # optional: Linear
85
+ pagerduty-token: ${{{{ secrets.PAGERDUTY_API_KEY }}}} # optional: PagerDuty or incident.io incidents
86
+ incident-io-token: ${{{{ secrets.INCIDENT_IO_API_KEY }}}}
87
+ triage-key: ${{{{ secrets.GEMINI_API_KEY }}}} # optional: a model's view on undecided failures ([ci.triage])
88
+ # traffic-token: ${{{{ steps.app.outputs.token }}}} # optional: keep traffic past GitHub's 14 days (see docs/modules.md)
89
+ """
90
+
91
+
92
+ def _uncommented(line: str) -> str:
93
+ # The line without its YAML comment; a "#" inside quotes (a workflow name) stays.
94
+ quote = None
95
+ for i, ch in enumerate(line):
96
+ if ch in "\"'" and quote in (None, ch):
97
+ quote = None if quote else ch
98
+ elif ch == "#" and quote is None and (i == 0 or line[i - 1].isspace()):
99
+ return line[:i].rstrip()
100
+ return line
101
+
102
+
103
+ MODULES = ("ci", "flow", "deps", "community")
104
+
105
+
106
+ def _for_modules(text: str, modules: tuple[str, ...]) -> str:
107
+ """The workflow trimmed to the modules chosen: only their triggers and the permissions they use, each lowered to
108
+ read where nothing of theirs writes."""
109
+ has = set(modules)
110
+ lines, out, skip = text.splitlines(keepends=True), [], None
111
+ for line in lines:
112
+ key = line.strip().split(":", 1)[0]
113
+ indent = len(line) - len(line.lstrip())
114
+ if skip is not None:
115
+ if indent > skip or not line.strip():
116
+ continue
117
+ skip = None
118
+ # CI Health starts from your CI's finished runs; the dependency check from pull requests that change a lockfile.
119
+ if indent == 2 and ((key == "workflow_run" and "ci" not in has) or (key == "pull_request" and "deps" not in has)):
120
+ skip = indent
121
+ continue
122
+ if indent == 2 and key in ("actions", "checks", "statuses", "deployments", "pull-requests"):
123
+ needs = {"actions": "ci" in has, "checks": has & {"ci", "flow"}, "statuses": has & {"ci", "flow"},
124
+ "deployments": "flow" in has, "pull-requests": True}[key]
125
+ if not needs:
126
+ continue
127
+ if key == "pull-requests" and not has & {"ci", "flow"}:
128
+ line = " pull-requests: read # the dependency check reads the files a pull request changes\n"
129
+ if line.strip().startswith("modules:"):
130
+ line = line.split("modules:")[0] + "modules: " + ",".join(m for m in MODULES if m in has) + "\n"
131
+ out.append(line)
132
+ return "".join(out)
133
+
134
+
135
+ def workflow_template(ref: str = ACTION_REF, names: list[str] | None = None, compact: bool = False,
136
+ modules: tuple[str, ...] = MODULES) -> str:
137
+ # JSON strings are valid YAML, so any workflow name is quoted safely.
138
+ text = WORKFLOW.format(ref=ref, names="[" + ", ".join(json.dumps(n) for n in (names or ["CI"])) + "]")
139
+ if tuple(modules) != MODULES:
140
+ text = _for_modules(text, tuple(modules))
141
+ if not compact:
142
+ return text
143
+ # Without comments, for a prefilled GitHub link (GitHub refuses links much over 6,500 characters).
144
+ return "\n".join(kept for kept in (_uncommented(line) for line in text.splitlines()) if kept.strip()) + "\n"
145
+
146
+
147
+ _CI = re.compile(r"\b(test|tests|testing|ci|build|builds|lint|check|checks|pre-commit|e2e|unit|integration|verify|validate)\b", re.I)
148
+ _CHORE = re.compile(r"\b(lock|stale|label|labeler|greet|welcome|release|publish|deploy|pages|codeql|scorecard|zizmor|dependabot|triage|sync|review)\b", re.I)
149
+
150
+
151
+ def ci_guess(names: list[str]) -> list[str]:
152
+ # The workflows that look like CI (the app's Setup picks the same); none recognised means all of them.
153
+ picked = [n for n in names if _CI.search(n) and not _CHORE.search(n)]
154
+ return picked or list(names)
155
+
156
+
157
+ def workflow_names(api) -> list[str]:
158
+ body = api._get(f"/repos/{api.repo}/actions/workflows", {"per_page": 100})
159
+ return [w["name"] for w in body.get("workflows") or []
160
+ if w.get("state") == "active" and not (w.get("path") or "").startswith("dynamic/")
161
+ and not (w.get("path") or "").endswith("/stackvitals.yml")]
162
+
163
+
164
+ def home() -> Path:
165
+ root = Path(os.environ.get("STACKVITALS_HOME") or Path.home() / ".stackvitals")
166
+ root.mkdir(parents=True, exist_ok=True)
167
+ return root
168
+
169
+
170
+ def _gh_token() -> str | None:
171
+ try:
172
+ run = subprocess.run(["gh", "auth", "token"], capture_output=True, text=True, timeout=15)
173
+ except (OSError, subprocess.SubprocessError):
174
+ return None
175
+ return run.stdout.strip() or None if run.returncode == 0 else None
176
+
177
+
178
+ def token() -> str | None:
179
+ for name in ("STACKVITALS_TOKEN", "GITHUB_TOKEN", "GH_TOKEN", "MERGESCOUT_TOKEN"):
180
+ if os.environ.get(name, "").strip():
181
+ return os.environ[name].strip()
182
+ return _gh_token()
183
+
184
+
185
+ def _quarantine_set(path: str | None, make_api, tok: str | None) -> set | None:
186
+ # Any problem reading the list means nothing is quarantined: a broken list must never hide failures.
187
+ try:
188
+ if path:
189
+ raw = Path(path).read_bytes()
190
+ else:
191
+ repo = os.environ.get("GITHUB_REPOSITORY")
192
+ if not repo:
193
+ return None
194
+ raw = make_api(tok or token(), repo).file("ci/quarantine.json", "stackvitals-data")
195
+ items = json.loads(raw)
196
+ return {str(i) for i in items} if isinstance(items, list) else None
197
+ except (OSError, ValueError, APIError, OutOfBudget):
198
+ return None
199
+
200
+
201
+ def main(argv: list[str] | None = None, now: datetime | None = None, make_api=ActionsAPI, tok: str | None = None) -> int:
202
+ # These parse their own flags; argparse's REMAINDER would reject a leading flag such as --base or --help.
203
+ if argv and argv[0] in ("impact", "retry", "validate"):
204
+ return __import__(f"stackvitals.ci.{argv[0]}", fromlist=["main"]).main(argv[1:])
205
+ if argv and argv[0] == "gitlab-action":
206
+ from stackvitals.ci.gitlab_action import main as gitlab_main
207
+ return gitlab_main(argv[1:])
208
+ if argv and argv[0] == "upload":
209
+ import stackvitals.ci.upload as upload
210
+ return upload.main(argv[1:])
211
+ parser = argparse.ArgumentParser(prog="stackvitals ci", description="CI health: flaky and slow jobs.")
212
+ sub = parser.add_subparsers(dest="cmd", required=True)
213
+ r = sub.add_parser("report", help="Analyse any repo's CI from your machine (no install in the repo needed)")
214
+ r.add_argument("repo", help="owner/name")
215
+ r.add_argument("--days", type=int, default=14, help="days of history to read (GitHub keeps at most 90)")
216
+ r.add_argument("--window", type=int, default=14, help="days the report covers")
217
+ r.add_argument("--budget", type=int, default=3000, help="most API requests to spend")
218
+ r.add_argument("--html", help="also write the dashboard to this folder")
219
+ r.add_argument("--tests", action="store_true", help="also read test reports (JUnit, CTRF, Jest/Vitest, Go, TRX) from run artifacts")
220
+ r.add_argument("--json", action="store_true")
221
+ q = sub.add_parser("quarantine", help="Run your test command; pass if only quarantined (known flaky) tests failed")
222
+ q.add_argument("--reports", action="append", default=[], help="glob of test report files the command writes")
223
+ q.add_argument("--list", help="quarantine list file (default: read from the stackvitals-data branch)")
224
+ q.add_argument("--print", choices=("pytest", "jest", "vitest", "go", "playwright", "maven", "dotnet"),
225
+ help="print the flags that make that test runner skip (or, for pytest, tolerate) quarantined tests, and exit")
226
+ q.add_argument("command", nargs=argparse.REMAINDER, help="-- then your test command")
227
+ gl = sub.add_parser("gitlab", help="Analyse a GitLab project's CI the same way (read-only; GITLAB_TOKEN if private)")
228
+ gl.add_argument("project", help="group/project on gitlab.com")
229
+ gl.add_argument("--days", type=int, default=14, help="days of pipelines to read")
230
+ gl.add_argument("--budget", type=int, default=600, help="most GitLab requests to spend")
231
+ gl.add_argument("--base", default="https://gitlab.com/api/v4", help="a self-managed GitLab's API url")
232
+ gl.add_argument("--json", action="store_true")
233
+ b = sub.add_parser("burn-in", help="Run the tests a PR adds or changes several times; fail if one passes and fails")
234
+ b.add_argument("--base", default="origin/main", help="what the PR is compared with (git diff BASE...HEAD)")
235
+ b.add_argument("--times", type=int, default=5, help="how many times to run them")
236
+ b.add_argument("--reports", action="append", default=[], help="glob of test report files each run writes")
237
+ b.add_argument("--only-new", action="store_true", help="fail only when a test the default branch never ran flakes "
238
+ "(reads the stackvitals-data branch: git fetch origin stackvitals-data)")
239
+ b.add_argument("command", nargs=argparse.REMAINDER, help="-- then your test command, with {files} for the test files")
240
+ sh = sub.add_parser("shards", help="Split test files into N shards that take about the same time, by recorded times")
241
+ sh.add_argument("repo", nargs="?", help="owner/name: times from the newest green run on its default branch with test reports")
242
+ sh.add_argument("--n", type=int, required=True, help="how many shards")
243
+ sh.add_argument("--reports", action="append", default=[], help="glob of test report files on this machine to read instead")
244
+ sh.add_argument("--files", action="append", default=[], help="glob of test files to place even with no recorded time")
245
+ sh.add_argument("--index", type=int, help="print only shard INDEX (1 to N), space-separated, for a CI matrix")
246
+ sh.add_argument("--py-paths", action="store_true", help="turn pytest's dotted module names into file paths")
247
+ sh.add_argument("--json", action="store_true")
248
+ im = sub.add_parser("impact", add_help=False, help="The test files a change can affect (or all), from names and past failures")
249
+ im.add_argument("rest", nargs=argparse.REMAINDER)
250
+ rt = sub.add_parser("retry", add_help=False, help="Re-run only the failed tests in the same job, never known-broken ones")
251
+ rt.add_argument("rest", nargs=argparse.REMAINDER)
252
+ v = sub.add_parser("validate", add_help=False, help="Check test report files are read as you expect, before CI does")
253
+ v.add_argument("rest", nargs=argparse.REMAINDER)
254
+ i = sub.add_parser("init", help="Print the workflow file that installs StackVitals in a repo")
255
+ i.add_argument("repo", nargs="?", help="owner/name: fill in that repo's own workflow names")
256
+ i.add_argument("--ref", default=ACTION_REF)
257
+ i.add_argument("--modules", default=",".join(MODULES),
258
+ help="the modules to install, comma-separated (ci, flow, deps, community); the file asks only for what they use")
259
+ args = parser.parse_args(argv)
260
+ if args.cmd == "impact":
261
+ from stackvitals.ci.impact import main as impact_main
262
+ return impact_main(args.rest)
263
+ if args.cmd == "retry":
264
+ from stackvitals.ci.retry import main as retry_main
265
+ return retry_main(args.rest)
266
+ if args.cmd == "validate":
267
+ from stackvitals.ci.validate import main as validate_main
268
+ return validate_main(args.rest)
269
+ if args.cmd == "quarantine" and args.print:
270
+ from stackvitals.ci.quarantine_runtime import LIST_FILE, runtime_args
271
+ ids = _quarantine_set(args.list, make_api, tok) or set()
272
+ if args.print == "pytest" and ids:
273
+ # The pytest plugin reads the list from this file, so the flags alone are enough.
274
+ Path(LIST_FILE).write_text(json.dumps(sorted(ids)) + "\n", encoding="utf-8")
275
+ # One argument per line, for a shell array: mapfile -t args < <(stackvitals ci quarantine --print jest)
276
+ print("\n".join(runtime_args(ids, args.print)))
277
+ return 0
278
+ if args.cmd == "quarantine":
279
+ command = args.command[1:] if args.command[:1] == ["--"] else args.command
280
+ if not command:
281
+ print("Give the test command after --, e.g. stackvitals ci quarantine --reports 'out/*.xml' -- pytest",
282
+ file=sys.stderr)
283
+ return 2
284
+ return run_quarantined(command, args.reports, _quarantine_set(args.list, make_api, tok))
285
+ if args.cmd == "gitlab":
286
+ from stackvitals.ci import gitlab
287
+ now = now or datetime.now(timezone.utc)
288
+ store = DataStore(home() / "ci" / ("gitlab__" + args.project.replace("/", "__")))
289
+ lab = gitlab.GitLab(args.project, gitlab.token(), args.base, max_requests=args.budget)
290
+ try:
291
+ got = gitlab.collect(lab, store, now, args.days)
292
+ except APIError as exc:
293
+ print(f"GitLab refused: {exc}", file=sys.stderr)
294
+ return 1
295
+ result = analyze(store.load(), now, args.days, default_branch=store.state().get("default_branch"))
296
+ if args.json:
297
+ print(json.dumps({k: v for k, v in result.items() if k != "history"}, indent=1))
298
+ else:
299
+ print(render_text(result, args.project))
300
+ if got["stopped"]:
301
+ print(f"\n(Not all pipelines were read yet ({got['stopped']}); run the same command again to continue.)")
302
+ return 0
303
+ if args.cmd == "burn-in":
304
+ from stackvitals.ci.burnin import burn_in, changed_tests
305
+ command = args.command[1:] if args.command[:1] == ["--"] else args.command
306
+ if not command or not args.reports:
307
+ print("Give --reports and the test command after --, e.g. stackvitals ci burn-in --reports 'out/*.xml' -- "
308
+ "pytest {files}", file=sys.stderr)
309
+ return 2
310
+ from stackvitals.ci.burnin import flaky_rates, known_tests
311
+ code, out = burn_in(command, args.reports, max(1, min(args.times, 50)), changed_tests(args.base),
312
+ known=known_tests(), only_new=args.only_new, rates=flaky_rates())
313
+ for p in out.get("fix_proofs") or []:
314
+ print(f"{p['id']}: passed {p['passes']} of {p['passes']} after failing {p['rate']:.0%} of runs before; "
315
+ f"{p['chance']:.1%} chance of that without a fix, so the fix is "
316
+ f"{'shown' if p['shown'] else 'not shown yet (run more repeats with --times)'}.")
317
+ if out.get("new") is not None and out["runs"]:
318
+ print(f"{len(out['new'])} of the tests are new (the default branch never reported them).")
319
+ elif args.only_new and out["runs"]:
320
+ print("The stackvitals-data branch couldn't be read (git fetch origin stackvitals-data), so every flake counts.")
321
+ if out.get("problem"):
322
+ print(out["problem"], file=sys.stderr)
323
+ elif not out["runs"]:
324
+ print("No test files were added or changed, so nothing was run.")
325
+ else:
326
+ print(f"Ran {len(out['files'])} changed test file(s) {out['runs']} times: {out['tests']} tests.")
327
+ for tid in out["flaky"]:
328
+ print(f"flaky: {tid} passed and failed across the runs")
329
+ for tid in out["always_failed"]:
330
+ print(f"failed every time: {tid}")
331
+ return code
332
+ if args.cmd == "shards":
333
+ from stackvitals.ci import shards
334
+ return shards.run(args, make_api, None if args.reports else tok or token())
335
+ tok = tok or token()
336
+ if args.cmd == "init":
337
+ names = None
338
+ if args.repo and tok:
339
+ try:
340
+ names = ci_guess(workflow_names(make_api(tok, args.repo))) or None
341
+ except APIError as exc:
342
+ print(f"Couldn't read {args.repo}'s workflows ({str(exc)[:120]}); printing the template with [\"CI\"].",
343
+ file=sys.stderr)
344
+ chosen = tuple(m.strip() for m in args.modules.split(",") if m.strip())
345
+ if not chosen or set(chosen) - set(MODULES):
346
+ print(f"--modules takes some of {', '.join(MODULES)}; Portal has its own: stackvitals portal init", file=sys.stderr)
347
+ return 2
348
+ print(workflow_template(args.ref, names, modules=chosen), end="")
349
+ if not names:
350
+ print("# Replace [\"CI\"] with the names of your CI workflows (or run: stackvitals ci init owner/repo).",
351
+ file=sys.stderr)
352
+ return 0
353
+ if not tok:
354
+ print("A GitHub token is needed (GitHub allows only 60 requests an hour without one). "
355
+ "Set GITHUB_TOKEN, or run `gh auth login`.", file=sys.stderr)
356
+ return 1
357
+ now = now or datetime.now(timezone.utc)
358
+ try:
359
+ # The same reader as the MCP server and the brief; a budget stop keeps what was read and the report covers it.
360
+ result, _source = services.ci_result(args.repo, args.days, tok, home(), make_api=make_api, now=now, budget=args.budget,
361
+ tests=args.tests, window=args.window, owners=True,
362
+ log=lambda line: print(line, file=sys.stderr, flush=True))
363
+ except APIError as exc:
364
+ print(f"GitHub refused: {exc}", file=sys.stderr)
365
+ return 1
366
+ except OutOfBudget as exc:
367
+ print(f"Stopped early ({exc}); run again later to continue.", file=sys.stderr)
368
+ return 1
369
+ if args.html:
370
+ write_outputs(Path(args.html), result, args.repo, site=".", data_readme=False)
371
+ stopped = (result.get("collect") or {}).get("stopped")
372
+ if args.json:
373
+ # Scripts reading the JSON must see an early stop too, not just people reading the text.
374
+ print(json.dumps({k: v for k, v in result.items() if k != "history"}, indent=1))
375
+ if stopped:
376
+ print(f"Not all history was read yet ({stopped}); run the same command again to continue.", file=sys.stderr)
377
+ else:
378
+ print(render_text(result, args.repo))
379
+ if stopped:
380
+ print(f"\n(Not all history was read yet ({stopped}); run the same command again to continue.)")
381
+ return 0
382
+
383
+
384
+ SWITCHES = ("pr_comments", "rerun_flaky", "quarantine", "issues", "mention_owners", "preflight_comments", "deps_comments",
385
+ "label_stuck")
386
+
387
+
388
+ def switches(args, cfg: dict | None) -> dict[str, bool]:
389
+ """The Action's on/off switches: the settings file's [automation] wins over the workflow input when it is set."""
390
+ auto = (cfg or {}).get("automation") or {}
391
+ out = {k: auto[k] if auto.get(k) is not None else getattr(args, k).strip().lower() == "true" for k in SWITCHES}
392
+ out["assign_owners"] = bool(auto.get("assign_owners"))
393
+ out["verdict_check"] = bool(auto.get("verdict_check"))
394
+ return out
395
+
396
+
397
+ def _print_digest(found: dict) -> None:
398
+ d = found["Digest"]
399
+ if os.environ.get("STACKVITALS_READ_ONLY"):
400
+ print(f"Digest: week {d['week']} written to the local data, not sent (read-only local run).")
401
+ else:
402
+ print(f"Digest: week {d['week']} sent to {', '.join(d['channels'])}.")
403
+
404
+
405
+ def action_main(argv: list[str] | None = None) -> int:
406
+ parser = argparse.ArgumentParser(prog="stackvitals action", description="Run inside GitHub Actions.")
407
+ parser.add_argument("--modules", default="ci")
408
+ parser.add_argument("--days", type=int, default=90)
409
+ parser.add_argument("--budget", type=int, default=800)
410
+ parser.add_argument("--tests", default="true", help="read test reports from artifacts (true/false)")
411
+ parser.add_argument("--test-artifacts", default="", help="regex for artifact names holding test reports")
412
+ for flag, default in (("--pr-comments", "true"), ("--rerun-flaky", "false"), ("--quarantine", "false"),
413
+ ("--issues", "false"), ("--preflight-comments", "false"), ("--deps-comments", "false"),
414
+ ("--mention-owners", "false"), ("--label-stuck", "false")):
415
+ parser.add_argument(flag, default=default)
416
+ args = parser.parse_args(argv)
417
+ modules = {m.strip() for m in args.modules.split(",") if m.strip()}
418
+ unknown = modules - {"ci", "flow", "deps", "community", "portal"}
419
+ if unknown:
420
+ print(f"Unknown modules: {', '.join(sorted(unknown))}. Available now: ci, flow, deps, community, portal", file=sys.stderr)
421
+ return 2
422
+ if os.environ.get("GITHUB_EVENT_NAME") == "pull_request":
423
+ # On a pull request only the dependency check runs: the others write to the data branch, and a fork's token can't.
424
+ if "deps" in modules:
425
+ import stackvitals.deps.action as deps_action
426
+ from stackvitals.core.settings import read
427
+ token = os.environ.get("STACKVITALS_TOKEN") or os.environ.get("GITHUB_TOKEN")
428
+ cfg = read(ActionsAPI(token, os.environ.get("GITHUB_REPOSITORY", ""), max_requests=2))
429
+ try:
430
+ out = deps_action.run_deps_action(dict(os.environ), comments=switches(args, cfg)["deps_comments"],
431
+ policy=cfg["deps"]["policy"], outside=cfg["deps"]["outside_lookups"],
432
+ scan=True, deep=cfg["deps"]["guarddog"], deny=cfg["deps"]["licenses"]["deny"])
433
+ except Exception as exc: # noqa: BLE001
434
+ # The check informs; its own trouble (GitHub limits, an odd file) must never fail the PR.
435
+ print(f"::warning::The dependency check could not finish: {deps_action._annotation(str(exc))}")
436
+ return 0
437
+ print(f"Dependency check: {out['changed']} changed packages in {out['lockfiles']} lockfile"
438
+ f"{'' if out['lockfiles'] == 1 else 's'}, {out['investigate']} to investigate, {out['monitor']} to monitor.")
439
+ return 0
440
+ from stackvitals.core.workspace import run_job
441
+ from stackvitals.flow.action import FLOW_EVENTS
442
+ env, now = dict(os.environ), datetime.now(timezone.utc)
443
+ scheduled = env.get("GITHUB_EVENT_NAME") in FLOW_EVENTS
444
+ steps = []
445
+ # Switches are decided when each step runs, after the job has read the settings file.
446
+ sw = lambda ws: switches(args, ws.cache.get("settings"))
447
+ from stackvitals.core import requests as requests_mod
448
+ # First, on every run: a request or command (and any a cancelled run missed) changes what the modules then work on.
449
+ steps.append(("Requests", lambda ws: requests_mod.requests_step(ws, env, now)))
450
+ # A Test connection request from the app runs alone, so its answer comes back in seconds.
451
+ light = requests_mod.check_only(env)
452
+ if light:
453
+ modules, scheduled = set(), False
454
+ if env.get("GITHUB_EVENT_NAME") == "repository_dispatch":
455
+ from stackvitals.core.recorded import record_step
456
+ # A deployment or incident sent from another tool: kept alone, and counted by the next scheduled run.
457
+ steps.append(("Record", lambda ws: record_step(ws, env, now)))
458
+ modules, scheduled = set(), False
459
+ if "ci" in modules:
460
+ steps.append(("CI Health", lambda ws: run_action(
461
+ env, now=now, days=args.days, budget=args.budget, tests=args.tests.strip().lower() != "false",
462
+ artifact_pattern=args.test_artifacts or None, pr_comments=sw(ws)["pr_comments"],
463
+ rerun_flaky=sw(ws)["rerun_flaky"], quarantine=sw(ws)["quarantine"], issues=sw(ws)["issues"],
464
+ webhook=env.get("STACKVITALS_WEBHOOK") or None, mention_owners=sw(ws)["mention_owners"],
465
+ assign_owners=sw(ws)["assign_owners"], verdict_check=sw(ws)["verdict_check"], workspace=ws)))
466
+ for name, label in (("flow", "Team Flow"), ("community", "Community"), ("portal", "Portal")):
467
+ if name in modules and not scheduled:
468
+ print(f"{label}: runs on the schedule and manual runs only.")
469
+ if "flow" in modules and scheduled:
470
+ steps.append(("Team Flow", lambda ws: run_flow_action(env, now=now, preflight_comments=sw(ws)["preflight_comments"],
471
+ workspace=ws)))
472
+ if "community" in modules and scheduled:
473
+ import stackvitals.community.action as community_action
474
+ # Team Flow's PR history is reused when it ran earlier in this job; otherwise Community reads the PRs itself.
475
+ steps.append(("Community", lambda ws: community_action.run_community_action(env, now=now, workspace=ws)))
476
+ if "portal" in modules and scheduled:
477
+ import stackvitals.portal.action as portal_action
478
+ steps.append(("Portal", lambda ws: portal_action.run_portal_action(env, now=now, workspace=ws)))
479
+ from stackvitals import connectors
480
+ if scheduled:
481
+ # Jira, Linear: before Delivery, which joins their issues and incidents with the PRs and deployments.
482
+ steps.append(("Connectors", lambda ws: connectors.connectors_step(ws, env, now)))
483
+ if "deps" in modules and (scheduled or env.get("GITHUB_EVENT_NAME") == "workflow_run"):
484
+ from stackvitals.deps.vulns import vulns_step
485
+ # Every lockfile in every ecosystem: scanned when they change, and every 6 hours for new advisories.
486
+ steps.append(("Vulnerabilities", lambda ws: vulns_step(ws, env, now)))
487
+ if "deps" in modules and scheduled:
488
+ from stackvitals.deps.supply import supply_step
489
+ # Once a week: the SBOM, licences and Scorecards of what the repo depends on.
490
+ steps.append(("Supply chain", lambda ws: supply_step(ws, env, now)))
491
+ if "deps" in modules and scheduled:
492
+ from stackvitals.security.probes import security_step
493
+
494
+ def security(ws):
495
+ import httpx
496
+ # The badge lookup names the repo to bestpractices.dev, so private repos skip it.
497
+ public = not (ws.cache.get("repo_info") or {}).get("private")
498
+ return security_step(ws, env, now, get_json=(lambda u: httpx.get(u, timeout=20, follow_redirects=True).json()) if public else None)
499
+ # Once a week: the repo's own security checks (workflows, protection, policy, releases, binaries, fuzzing).
500
+ steps.append(("Security", security))
501
+ if "flow" in modules and scheduled:
502
+ from stackvitals.core.delivery_report import delivery_step
503
+ # After Team Flow and Community: joins their PRs and issues with CI Health's runs.
504
+ steps.append(("Delivery", lambda ws: delivery_step(ws, env, now, label_stuck=sw(ws)["label_stuck"])))
505
+ if steps:
506
+ from stackvitals.core.insights import insights_step
507
+ # Last: joins whatever CI runs and pull requests the checkout now holds.
508
+ steps.append(("Insights", lambda ws: insights_step(ws, env, now)))
509
+ if "flow" in modules and scheduled:
510
+ from stackvitals.core.alerts import alerts_step
511
+ # Thresholds on the delivery numbers, sent once per change of state.
512
+ steps.append(("Alerts", lambda ws: alerts_step(ws, env, now)))
513
+ if steps and env.get("GITHUB_EVENT_NAME") in ("schedule", "workflow_dispatch"):
514
+ from stackvitals.core.digest import digest_step
515
+ # Last: the weekly digest reads the pages every module just wrote.
516
+ steps.append(("Digest", lambda ws: digest_step(ws, env, now)))
517
+ if light:
518
+ steps = steps[:1]
519
+ if not steps:
520
+ return 0
521
+ # One checkout and one commit for every module; a module's trouble is reported and the others still run.
522
+ from stackvitals.core.settings import prepare
523
+ job = run_job(env, steps, now, prepare=prepare(env))
524
+ if job.get("pages") and env.get("GITHUB_OUTPUT"):
525
+ # The Action's deploy steps run only when this job changed what the monitor shows.
526
+ with open(env["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
527
+ f.write("pages=true\n")
528
+ for label, err in job["errors"].items():
529
+ print(f"::error::{label} could not finish: {err}")
530
+ saved, found = "saved" if job["outcome"] == "pushed" else "unchanged", job["results"]
531
+ if found.get("CI Health"):
532
+ out = found["CI Health"]
533
+ print(f"CI health: {out['collect']['runs']} runs read ({out['collect']['requests']} requests), "
534
+ f"{out['flaky']} flaky job(s), data {saved}.")
535
+ if found.get("Team Flow"):
536
+ flow = found["Team Flow"]
537
+ print(f"Team Flow: {flow['prs']} PRs read ({flow['points']} points), {flow['queue']} waiting, data {saved}.")
538
+ if found.get("Community"):
539
+ com = found["Community"]
540
+ score = f"{com['score']}/100" if com["score"] is not None else "not yet"
541
+ print(f"Community: {com['issues']} issues read ({com['points']} points), {com['waiting']} newcomer"
542
+ f"{'' if com['waiting'] == 1 else 's'} waiting, score {score}, data {saved}.")
543
+ for name, got in (found.get("Connectors") or {}).items():
544
+ print(f"{connectors.NAMES[name]}: " + (got["error"] if got["error"] else
545
+ f"{got['read']} updated issue(s) read{'' if got['complete'] else ' (more next run)'}, data {saved}."))
546
+ if found.get("Delivery"):
547
+ dl = found["Delivery"]
548
+ print(f"Delivery: {dl['prs']} merged PRs, {dl['deployments']} deployments, data {saved}.")
549
+ if found.get("Insights"):
550
+ ins = found["Insights"]
551
+ print(f"Insights: {ins.get('findings', 0)} findings; {ins['red_prs']} of {ins['prs']} merged PRs spent time with "
552
+ "failing checks.")
553
+ if found.get("Alerts"):
554
+ al = found["Alerts"]
555
+ levels = ", ".join(f"{k.split('_')[0]} {v}" for k, v in al["levels"].items())
556
+ print(f"Alerts: {al['events']} sent; {levels}.")
557
+ if found.get("Requests"):
558
+ req = found["Requests"]
559
+ print(f"Requests: {req.get('decisions', 0)} decision(s) recorded; settings "
560
+ f"{'changed' if req.get('settings') else 'unchanged'}.")
561
+ if req.get("checks") is not None:
562
+ print(f"Connection checks: {req.get('working', 0)} of {req['checks']} working (details in the job summary).")
563
+ if found.get("Digest"):
564
+ _print_digest(found)
565
+ if found.get("Portal"):
566
+ portal = found["Portal"]
567
+ print(f"Portal: {portal['repos']} repos ({portal['mode']} mode), data {saved}.")
568
+ for note in job["notes"]:
569
+ print(f"::warning::{note}")
570
+ return 1 if job["errors"] else 0