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,101 @@
1
+ // Marks flaky and broken tests in the editor, read through the user's own git from the stackvitals-data branch.
2
+ "use strict";
3
+
4
+ const vscode = require("vscode");
5
+ const { execFile } = require("child_process");
6
+ const path = require("path");
7
+ const { findMarks, describe } = require("./marks");
8
+
9
+ const TEST_FILE = /test|spec/i;
10
+ const RELOAD_MS = 30 * 60 * 1000;
11
+ const folders = new Map();
12
+ let diagnostics;
13
+
14
+ function git(cwd, args) {
15
+ return new Promise((resolve) => execFile("git", args, { cwd, maxBuffer: 64 << 20, windowsHide: true },
16
+ (err, out) => resolve(err ? null : out)));
17
+ }
18
+
19
+ function ref(folder) {
20
+ const given = vscode.workspace.getConfiguration("stackvitals", folder.uri).get("dataRef");
21
+ // A ref is passed to git, so anything that could read as an option is refused.
22
+ return typeof given === "string" && /^[\w.][\w./-]*$/.test(given) ? given : "origin/stackvitals-data";
23
+ }
24
+
25
+ async function load(folder) {
26
+ const raw = await git(folder.uri.fsPath, ["show", `${ref(folder)}:docs/data.json`]);
27
+ let data = null;
28
+ try {
29
+ data = raw ? JSON.parse(raw) : null;
30
+ } catch {
31
+ data = null; // a half-written or foreign file marks nothing
32
+ }
33
+ const remote = ((await git(folder.uri.fsPath, ["remote", "get-url", "origin"])) || "").trim();
34
+ const repo = (remote.match(/github\.com[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?$/) || [])[1] || null;
35
+ const tests = data && Array.isArray(data.tests) ? data.tests : [];
36
+ folders.set(folder.uri.toString(), { tests, repo });
37
+ return tests.filter((t) => t && (t.state === "flaky" || t.state === "broken")).length;
38
+ }
39
+
40
+ function mark(doc) {
41
+ if (doc.uri.scheme !== "file") return;
42
+ const folder = vscode.workspace.getWorkspaceFolder(doc.uri);
43
+ const known = folder && folders.get(folder.uri.toString());
44
+ const rel = folder ? path.relative(folder.uri.fsPath, doc.uri.fsPath).split(path.sep).join("/") : "";
45
+ if (!known || !TEST_FILE.test(rel)) {
46
+ diagnostics.delete(doc.uri);
47
+ return;
48
+ }
49
+ diagnostics.set(doc.uri, findMarks(doc.getText(), rel, known.tests).map((m) => {
50
+ const range = new vscode.Range(m.line, m.col, m.line, m.col + m.length);
51
+ const d = new vscode.Diagnostic(range, describe(m.test), vscode.DiagnosticSeverity.Warning);
52
+ d.source = "StackVitals";
53
+ const proof = (Array.isArray(m.test.evidence) ? m.test.evidence : []).slice(-1)[0];
54
+ d.code = known.repo && proof && Number.isInteger(proof.run)
55
+ ? { value: "latest evidence", target: vscode.Uri.parse(`https://github.com/${known.repo}/actions/runs/${proof.run}/attempts/${proof.att || 1}`) }
56
+ : m.test.state;
57
+ return d;
58
+ }));
59
+ }
60
+
61
+ async function reload(fetch) {
62
+ let count = 0;
63
+ for (const folder of vscode.workspace.workspaceFolders || []) {
64
+ if (fetch) {
65
+ const [remote, branch] = ref(folder).split(/\/(.+)/);
66
+ // "+": the data branch is squashed now and then, which a plain fetch would refuse.
67
+ await git(folder.uri.fsPath, ["fetch", "--quiet", remote, `+${branch}:refs/remotes/${remote}/${branch}`]);
68
+ }
69
+ count += await load(folder);
70
+ }
71
+ vscode.workspace.textDocuments.forEach(mark);
72
+ return count;
73
+ }
74
+
75
+ function activate(context) {
76
+ diagnostics = vscode.languages.createDiagnosticCollection("stackvitals");
77
+ const timers = new Map();
78
+ const later = (doc) => {
79
+ clearTimeout(timers.get(doc.uri.toString()));
80
+ timers.set(doc.uri.toString(), setTimeout(() => mark(doc), 300));
81
+ };
82
+ const every = setInterval(() => reload(false), RELOAD_MS);
83
+ context.subscriptions.push(
84
+ diagnostics,
85
+ { dispose: () => clearInterval(every) },
86
+ vscode.workspace.onDidOpenTextDocument(mark),
87
+ vscode.workspace.onDidChangeTextDocument((e) => later(e.document)),
88
+ vscode.workspace.onDidCloseTextDocument((doc) => diagnostics.delete(doc.uri)),
89
+ vscode.workspace.onDidChangeWorkspaceFolders(() => reload(false)),
90
+ vscode.commands.registerCommand("stackvitals.refresh", async () => {
91
+ const n = await reload(true);
92
+ vscode.window.showInformationMessage(n ? `StackVitals: ${n} flaky or broken test${n === 1 ? "" : "s"} marked.`
93
+ : "StackVitals: no flaky or broken tests found on the data branch.");
94
+ }),
95
+ );
96
+ reload(false);
97
+ }
98
+
99
+ function deactivate() {}
100
+
101
+ module.exports = { activate, deactivate };
@@ -0,0 +1,92 @@
1
+ // Where the repo's flaky and broken tests are declared in one file, from the data branch's docs/data.json.
2
+ "use strict";
3
+
4
+ const MARKED = new Set(["flaky", "broken"]);
5
+ // Test declarations by language; group 1 is the name (an identifier, or a title for JS and TS).
6
+ const DECLS = [
7
+ { re: /^[ \t]*(?:async[ \t]+)?def[ \t]+(test\w*)/gmd },
8
+ { re: /^func[ \t]+(?:\([^)]*\)[ \t]*)?((?:Test|Benchmark|Fuzz|Example)\w*)/gmd },
9
+ { re: /\b(?:void|fun|Task|fn)[ \t]+(\w+)[ \t]*[(<]/gd },
10
+ { re: /\b(?:it|test|describe|context)(?:\.\w+)*[ \t]*\([ \t]*(?:'((?:\\.|[^'\\\n])+)'|"((?:\\.|[^"\\\n])+)"|`((?:\\.|[^`\\\n])+)`)/gd, title: true },
11
+ ];
12
+
13
+ function parts(tid) {
14
+ // "file::Class::name", "pkg::TestX/sub" or "Ns.Class.Method": the scope it lives in and its own name.
15
+ const cut = tid.lastIndexOf("::");
16
+ let scope = cut >= 0 ? tid.slice(0, cut) : "";
17
+ let name = cut >= 0 ? tid.slice(cut + 2) : tid;
18
+ // Parameters ("test_x[1.5]") may hold dots, so only the part before them is split.
19
+ const bare = name.replace(/\[.*$/s, "");
20
+ if (!/\s/.test(bare) && bare.includes(".") && !bare.includes("(")) {
21
+ scope += "." + bare.slice(0, bare.lastIndexOf("."));
22
+ name = name.slice(bare.lastIndexOf(".") + 1);
23
+ }
24
+ return { scope, name };
25
+ }
26
+
27
+ function identifier(name) {
28
+ // test_x[param], method(args) and TestX/sub all live where their first part is declared.
29
+ return name.replace(/\[.*$/s, "").replace(/\(.*$/s, "").split("/")[0].trim();
30
+ }
31
+
32
+ function inFile(scope, rel) {
33
+ if (!scope) return true;
34
+ const file = rel.split("/").pop();
35
+ const stem = file.replace(/\.[^.]+$/, "");
36
+ if (stem && scope.includes(stem)) return true;
37
+ // Go names the package (its folder), not the file.
38
+ if (file.endsWith(".go")) {
39
+ const dirs = rel.split("/").slice(0, -1);
40
+ return dirs.length === 0 || scope.split("/").pop() === dirs[dirs.length - 1];
41
+ }
42
+ return false;
43
+ }
44
+
45
+ function findMarks(text, rel, tests) {
46
+ const wanted = (Array.isArray(tests) ? tests : [])
47
+ .filter((t) => t && typeof t.id === "string" && t.id && MARKED.has(t.state))
48
+ .map((t) => ({ t, ...parts(t.id) }))
49
+ .filter((w) => w.name && inFile(w.scope, rel));
50
+ if (!wanted.length) return [];
51
+ const starts = [0];
52
+ for (let i = 0; i < text.length; i++) if (text[i] === "\n") starts.push(i + 1);
53
+ const at = (offset) => {
54
+ let lo = 0, hi = starts.length - 1;
55
+ while (lo < hi) {
56
+ const mid = (lo + hi + 1) >> 1;
57
+ if (starts[mid] <= offset) lo = mid; else hi = mid - 1;
58
+ }
59
+ return { line: lo, col: offset - starts[lo] };
60
+ };
61
+ const out = [];
62
+ for (const decl of DECLS) {
63
+ decl.re.lastIndex = 0;
64
+ for (const m of text.matchAll(decl.re)) {
65
+ const g = [1, 2, 3].find((k) => m[k] !== undefined);
66
+ const found = m[g];
67
+ const hits = wanted.filter((w) => decl.title
68
+ ? w.name === found || [" ", " > ", " › "].some((sep) => w.name.endsWith(sep + found))
69
+ : identifier(w.name) === found);
70
+ if (!hits.length) continue;
71
+ // Parametrised cases share one declaration; the one that flaked most speaks for it.
72
+ hits.sort((a, b) => (b.t.flake_events || 0) - (a.t.flake_events || 0));
73
+ out.push({ ...at(m.indices[g][0]), length: found.length, test: hits[0].t, variants: hits.length });
74
+ }
75
+ }
76
+ return out.sort((a, b) => a.line - b.line || a.col - b.col);
77
+ }
78
+
79
+ function describe(t) {
80
+ const n = (v) => (Number.isFinite(v) ? v : "?");
81
+ if (t.state === "broken") {
82
+ return `Broken (StackVitals): failed in ${n(t.failures)} of the last ${n(t.runs)} runs.`
83
+ + (t.message ? ` Failure: ${String(t.message).slice(0, 200)}` : "");
84
+ }
85
+ const last = typeof t.last_flake === "string" ? `, last on ${t.last_flake.slice(0, 10)}` : "";
86
+ return `Flaky (StackVitals): failed and then passed with no code change ${n(t.flake_events)} times in ${n(t.runs)} runs`
87
+ + ` over the last 30 days${last}.`
88
+ + (t.flake ? ` Looks like a ${t.flake} flake.` : "")
89
+ + (t.message ? ` Usual failure: ${String(t.message).slice(0, 200)}` : "");
90
+ }
91
+
92
+ module.exports = { findMarks, describe };
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "stackvitals-flaky-tests",
3
+ "displayName": "StackVitals flaky tests",
4
+ "description": "Marks the tests your CI history shows are flaky or broken, read from the repo's stackvitals-data branch.",
5
+ "version": "0.1.0",
6
+ "publisher": "stackvitals",
7
+ "license": "Apache-2.0",
8
+ "engines": { "vscode": "^1.80.0" },
9
+ "main": "./extension.js",
10
+ "activationEvents": ["onStartupFinished"],
11
+ "contributes": {
12
+ "commands": [
13
+ { "command": "stackvitals.refresh", "title": "StackVitals: Refresh flaky tests" }
14
+ ],
15
+ "configuration": {
16
+ "title": "StackVitals",
17
+ "properties": {
18
+ "stackvitals.dataRef": {
19
+ "type": "string",
20
+ "default": "origin/stackvitals-data",
21
+ "description": "The git ref holding StackVitals' data branch; the refresh command fetches it."
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
@@ -0,0 +1 @@
1
+ """Team Flow: what is blocking a repo's pull requests, who acts next, and how its review flow compares."""
@@ -0,0 +1,178 @@
1
+ import json
2
+ from datetime import datetime, timezone
3
+ from pathlib import Path
4
+
5
+ from mergescout.github.graphql import GitHubGraphQL
6
+ from stackvitals.ci.action import BRANCH, DataBranch, GitError
7
+ from stackvitals.ci.api import ActionsAPI, APIError, OutOfBudget
8
+ from stackvitals.core.actors import People
9
+ from stackvitals.core.config import parse
10
+ from stackvitals.core.delivery import apply_identity
11
+ from stackvitals.core.store import published
12
+ from stackvitals.core.summary import write_pr_summary
13
+ from stackvitals.flow.report import _md
14
+ from stackvitals.flow import peers
15
+ from stackvitals.flow.build import build
16
+ from stackvitals.flow.collect import collect
17
+ from stackvitals.flow.preflight import Habits, collect_asks, gaps
18
+ from stackvitals.flow.report import render_html, render_markdown
19
+ from stackvitals.flow.store import FlowStore
20
+ from stackvitals import shell
21
+
22
+ # Team Flow runs on the schedule and manual runs (the user's choice: every 3 hours), when the settings file is pushed and
23
+ # when someone makes a decision on GitHub (so a change applies at once), never on each CI run.
24
+ # Comment commands run light (Requests and CI Health), so a burst of them finishes fast.
25
+ FLOW_EVENTS = {"schedule", "workflow_dispatch", "push", "issues"}
26
+ LABEL = "Team Flow"
27
+
28
+
29
+ def make_gql(token: str) -> GitHubGraphQL:
30
+ # Inside Actions nothing waits for the hourly reset; the next scheduled run continues.
31
+ return GitHubGraphQL(token, max_wait=0, reserve=50)
32
+
33
+
34
+ def write_flow_outputs(root: Path, result: dict, repo: str) -> None:
35
+ docs = root / "docs"
36
+ docs.mkdir(parents=True, exist_ok=True)
37
+ # The public page and file never carry people's answer times.
38
+ public = {k: v for k, v in result.items() if k != "responders"}
39
+ (docs / "flow.html").write_text(render_html(public, repo, shell.present(docs) | {"flow.html"}), encoding="utf-8")
40
+ (docs / "flow.json").write_text(json.dumps(published(public), indent=1), encoding="utf-8")
41
+ (docs / ".nojekyll").write_text("", encoding="utf-8")
42
+
43
+
44
+ PREFLIGHT_MARKER = "<!-- stackvitals:preflight -->"
45
+
46
+
47
+ def _quiet(text: str) -> str:
48
+ # Escaped, and with @ and www. broken by a zero-width space, so a note can never ping people or become a link.
49
+ return _md(text).replace("@", "@​").replace("www.", "www​.")
50
+
51
+
52
+ def _preflight_targets(data: dict, since: str, done: set[int], now: datetime) -> list[tuple[int, str]]:
53
+ """New PRs from outside contributors that differ from what merged PRs here usually include, with the note to post."""
54
+ habits_index = Habits(data["open"] + data["closed"], now)
55
+ people = People(data["open"] + data["closed"])
56
+ out = []
57
+ for p in data["open"]:
58
+ if p["cr"] <= since or p["n"] in done or p.get("draft") or not people.is_outside(p):
59
+ continue
60
+ habits = habits_index.norms(p.get("paths") or [])
61
+ found = gaps(p, habits)
62
+ if found:
63
+ lines = "\n".join(f"- {_quiet(g['why'])}{'' if g['what'] == 'size' else '; this PR has none.'}" for g in found)
64
+ out.append((p["n"], f"{PREFLIGHT_MARKER}\n### Before review: how this PR compares with merged PRs here\n{lines}\n\n"
65
+ f"<sub>From this repo's own history ({habits['merged_prs']} merged PRs). The maintainers turned "
66
+ f"this note on; it is information, not a rule.</sub>"))
67
+ return out
68
+
69
+
70
+ def flow_step(ws, env: dict, now: datetime, months: int = 12, make_gql=make_gql, make_api=ActionsAPI,
71
+ preflight_comments: bool = False) -> dict:
72
+ """Team Flow's part of a job: PRs changed since the last run into the shared checkout, the page, and (after the
73
+ commit) the pre-flight notes and the summary."""
74
+ repo, token = ws.repo, env.get("STACKVITALS_TOKEN") or env.get("GITHUB_TOKEN")
75
+ owner, name = repo.split("/")
76
+ store = FlowStore(ws.path)
77
+ previous_run = store.state().get("last_collect")
78
+ gql = make_gql(token)
79
+ stats = collect(gql, store, owner, name, now, months=months)
80
+ data = store.load()
81
+ ws.cache["flow"] = data
82
+ # Review comments of closed PRs, a few hundred per run (about 6 points per 20 PRs).
83
+ collect_asks(gql, store, owner, name, data["closed"], cap=500)
84
+ from stackvitals.flow import survival
85
+ # How much of each squash-merged PR's code is still there 30 and 90 days on (git blame, about 12 points a run).
86
+ survival.collect(gql, store.dir, owner, name, data["closed"], now)
87
+ # The repo's [identity] settings (bots, people, old logins) shape every number; the stored history stays as GitHub said.
88
+ seen = apply_identity(data, (ws.cache.get("settings") or parse("")[0]).get("identity"))
89
+ # Pull requests the team left out (a label or /stackvitals exclude) leave every number; the page says how many.
90
+ from stackvitals.core.decisions import visible
91
+ seen = visible(ws.path, seen, now.date())
92
+ # Comparisons come from a public file only (none before launch): no extra request otherwise.
93
+ repo_meta = store.repo()
94
+ # Public pages never name the person behind an area unless the repo asks for it.
95
+ names = bool(repo_meta.get("private")) or bool(((ws.cache.get("settings") or {}).get("identity") or {}).get("knowledge_names"))
96
+ result = build(seen, repo_meta, now, owner=owner, asks=store.asks(), bench=peers.load(None, ws.work / "bench"), names=names)
97
+ result["excluded"] = seen.get("excluded", 0)
98
+ to_comment = []
99
+ if preflight_comments and previous_run:
100
+ # Decided and remembered before the commit, so overlapping runs never comment twice.
101
+ done = store.state().get("preflight_commented", [])
102
+ to_comment = _preflight_targets(seen, previous_run, set(done), now)
103
+ if to_comment:
104
+ store.save_state({**store.state(), "preflight_commented": (done + [n for n, _ in to_comment])[-500:]})
105
+ # Team automation (on by default): planned and remembered before the commit, sent after it.
106
+ from stackvitals.flow.automate import plan, send
107
+ switches = (ws.cache.get("settings") or parse("")[0]).get("automation") or {}
108
+ acts, asked = plan(seen["open"], result["queue"], switches, store.state().get("automation") or {})
109
+ if acts:
110
+ store.save_state({**store.state(), "automation": asked})
111
+ # PR rules as code ([[rules]]): planned and remembered the same way, and replayed on past PRs for the app.
112
+ rule_list = (ws.cache.get("settings") or parse("")[0]).get("rules") or []
113
+ if rule_list:
114
+ from stackvitals.flow import rules as pr_rules
115
+ ctx = pr_rules.context(seen, rule_list)
116
+ rule_acts, rule_state, matched = pr_rules.plan(seen["open"], rule_list, ctx, store.state().get("rules") or {})
117
+ if rule_acts:
118
+ store.save_state({**store.state(), "rules": rule_state})
119
+ acts = acts + rule_acts
120
+ merged = sorted((p for p in seen["closed"] if p.get("mg")), key=lambda p: p["mg"])[-200:]
121
+ replayed = pr_rules.replay(rule_list, merged, ctx)
122
+ result["rules"] = [{"name": r["name"], "dry_run": r.get("dry_run", False), "open": matched[r["name"]],
123
+ "replay": replayed[r["name"]]} for r in rule_list]
124
+ write_flow_outputs(ws.path, result, repo)
125
+ # Dependency-update hygiene from the same pull requests (open updates by age, security ones past 48 h, a quiet bot).
126
+ from stackvitals.core.decisions import Store as Decisions
127
+ from stackvitals.core.store import write_if_changed
128
+ from stackvitals.deps.hygiene import hygiene
129
+ deps = hygiene(seen, Decisions(ws.path).state(now.date()), now)
130
+ if deps is not None:
131
+ write_if_changed(ws.path / "docs" / "deps.json", published(deps))
132
+ write_pr_summary(ws.path, seen, now, repo)
133
+ out = {"prs": stats["prs"], "points": stats["points"], "complete": stats["complete"], "queue": len(result["queue"])}
134
+
135
+ def after_commit():
136
+ notes = []
137
+ if acts:
138
+ done = send(make_api(token, repo, max_requests=len(acts) + 5), acts)
139
+ notes.append(f"Team automation: {done['review']} reviewer request(s), {done['label']} label(s) added, "
140
+ f"{done['unlabel']} removed, {done['comment']} rule comment(s). Turn any part off under [automation] "
141
+ "in .github/stackvitals.toml (review_requests, size_labels, docs_label), or change [[rules]].")
142
+ if done["refused"]:
143
+ notes.append("Some were refused (" + "; ".join(done["refused"][:2]) + "): they need `pull-requests: write`.")
144
+ if to_comment:
145
+ api = make_api(token, repo, max_requests=len(to_comment) + 5)
146
+ for n, body in to_comment:
147
+ try:
148
+ api.send("POST", f"/repos/{repo}/issues/{n}/comments", {"body": body})
149
+ except (APIError, OutOfBudget) as exc:
150
+ notes.append(f"Could not post the pre-flight note on #{n} ({exc}); it needs `pull-requests: write`.")
151
+ summary = env.get("GITHUB_STEP_SUMMARY")
152
+ if summary:
153
+ note = "" if stats["complete"] else "\n\nStill reading older pull requests (GitHub's hourly limit); the next run continues."
154
+ note += "".join(f"\n\n{n}" for n in notes)
155
+ if stats["no_checks"]:
156
+ note += "\n\nAdd `checks: read` and `statuses: read` to the workflow permissions to see which PRs have failing checks."
157
+ if stats.get("no_deployments"):
158
+ note += "\n\nAdd `deployments: read` to the workflow permissions to measure delivery from deployments."
159
+ with open(summary, "a", encoding="utf-8") as f:
160
+ f.write(render_markdown(result, repo) + note + "\n")
161
+ ws.after_commit(after_commit, LABEL)
162
+ return out
163
+
164
+
165
+ def run_flow_action(env: dict, now: datetime | None = None, months: int = 12, make_gql=make_gql, make_api=ActionsAPI,
166
+ preflight_comments: bool = False, workspace=None) -> dict | None:
167
+ """One Team Flow run: read PRs changed since the last run, keep them in the data branch, rebuild the page."""
168
+ if env.get("GITHUB_EVENT_NAME") not in FLOW_EVENTS:
169
+ return None
170
+ from stackvitals.core.workspace import run_job
171
+ now = now or datetime.now(timezone.utc)
172
+
173
+ def step(ws):
174
+ return flow_step(ws, env, now, months, make_gql, make_api, preflight_comments)
175
+ if workspace is not None:
176
+ return step(workspace)
177
+ job = run_job(env, [(LABEL, step)], now, message=f"stackvitals: team flow {now:%Y-%m-%d %H:%M} UTC", catch=False)
178
+ return {**job["results"][LABEL], "pushed": job["outcome"] == "pushed"}