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.
- mergescout/__init__.py +1 -0
- mergescout/archive/__init__.py +0 -0
- mergescout/archive/__main__.py +30 -0
- mergescout/archive/convert.py +83 -0
- mergescout/archive/events.py +55 -0
- mergescout/archive/manifest.py +45 -0
- mergescout/archive/stream.py +83 -0
- mergescout/bench/__init__.py +1 -0
- mergescout/bench/__main__.py +135 -0
- mergescout/bench/ci.py +257 -0
- mergescout/bench/deploys.py +115 -0
- mergescout/bench/matched.py +22 -0
- mergescout/bench/prior.py +42 -0
- mergescout/bench/report.py +230 -0
- mergescout/bench/shared.py +112 -0
- mergescout/bench/stars.py +57 -0
- mergescout/cli.py +89 -0
- mergescout/config.py +35 -0
- mergescout/crawl/__init__.py +0 -0
- mergescout/crawl/__main__.py +97 -0
- mergescout/crawl/chain.py +34 -0
- mergescout/crawl/fetch.py +146 -0
- mergescout/crawl/outcomes.py +69 -0
- mergescout/crawl/queries.py +250 -0
- mergescout/crawl/runner.py +176 -0
- mergescout/crawl/seed.py +77 -0
- mergescout/crawl/state.py +169 -0
- mergescout/crawl/writer.py +87 -0
- mergescout/discover/__init__.py +0 -0
- mergescout/discover/__main__.py +58 -0
- mergescout/discover/sweep.py +286 -0
- mergescout/github/__init__.py +0 -0
- mergescout/github/graphql.py +202 -0
- mergescout/github/identities.py +35 -0
- mergescout/github/releases.py +137 -0
- mergescout/labels.py +20 -0
- mergescout/mcp_server.py +228 -0
- mergescout/measure/__init__.py +0 -0
- mergescout/measure/__main__.py +75 -0
- mergescout/measure/api_cost.py +91 -0
- mergescout/measure/count_repos.py +30 -0
- mergescout/measure/jsonl.py +14 -0
- mergescout/measure/report.py +56 -0
- mergescout/measure/sample.py +37 -0
- mergescout/owners.py +22 -0
- mergescout/setup_check.py +64 -0
- mergescout/signals/__init__.py +0 -0
- mergescout/signals/__main__.py +96 -0
- mergescout/signals/bump_ledger.py +258 -0
- mergescout/signals/bumps.py +104 -0
- mergescout/signals/check.py +141 -0
- mergescout/signals/community_bench.py +149 -0
- mergescout/signals/delivery_bench.py +103 -0
- mergescout/signals/deps_map.py +261 -0
- mergescout/signals/deps_trends.py +117 -0
- mergescout/signals/evidence.py +94 -0
- mergescout/signals/exploits.py +43 -0
- mergescout/signals/flow_bench.py +258 -0
- mergescout/signals/identity.py +73 -0
- mergescout/signals/issues.py +76 -0
- mergescout/signals/labels.py +79 -0
- mergescout/signals/osv.py +232 -0
- mergescout/signals/owners_watch.py +80 -0
- mergescout/signals/repo_stats.py +96 -0
- mergescout/signals/rules.py +70 -0
- mergescout/signals/silver.py +33 -0
- mergescout/signals/top_packages.py +26 -0
- mergescout/signals/triage.py +71 -0
- mergescout/signals/triage_math.py +120 -0
- mergescout/signals/walls.py +91 -0
- mergescout/snapshot/__init__.py +0 -0
- mergescout/snapshot/__main__.py +215 -0
- mergescout/snapshot/build.py +147 -0
- mergescout/snapshot/publish.py +70 -0
- mergescout/snapshot/schema.py +34 -0
- mergescout/snapshot/score.py +30 -0
- mergescout/store/__init__.py +0 -0
- mergescout/store/__main__.py +46 -0
- mergescout/store/sync.py +148 -0
- mergescout/timeutil.py +11 -0
- mergescout/tool/__init__.py +0 -0
- mergescout/tool/action_pack.py +62 -0
- mergescout/tool/live.py +201 -0
- mergescout/tool/queries.py +293 -0
- mergescout/tool/refs.py +31 -0
- mergescout/tool/store.py +129 -0
- mergescout/tool/tracking.py +83 -0
- stackvitals/__init__.py +2 -0
- stackvitals/__main__.py +4 -0
- stackvitals/ci/__init__.py +1 -0
- stackvitals/ci/action.py +574 -0
- stackvitals/ci/analyze.py +577 -0
- stackvitals/ci/api.py +218 -0
- stackvitals/ci/automation.py +212 -0
- stackvitals/ci/bench.py +195 -0
- stackvitals/ci/burnin.py +102 -0
- stackvitals/ci/cli.py +570 -0
- stackvitals/ci/collect.py +239 -0
- stackvitals/ci/cost.py +107 -0
- stackvitals/ci/coverage.py +155 -0
- stackvitals/ci/feedback.py +699 -0
- stackvitals/ci/fingerprint.py +220 -0
- stackvitals/ci/gitlab.py +121 -0
- stackvitals/ci/gitlab_action.py +125 -0
- stackvitals/ci/impact.py +125 -0
- stackvitals/ci/investigate.py +59 -0
- stackvitals/ci/kinds.py +52 -0
- stackvitals/ci/monitors.py +105 -0
- stackvitals/ci/owners.py +116 -0
- stackvitals/ci/quarantine_runtime.py +90 -0
- stackvitals/ci/report.py +435 -0
- stackvitals/ci/retry.py +67 -0
- stackvitals/ci/selfheal.py +102 -0
- stackvitals/ci/shards.py +120 -0
- stackvitals/ci/share.py +59 -0
- stackvitals/ci/store.py +93 -0
- stackvitals/ci/testreports.py +351 -0
- stackvitals/ci/tickets.py +106 -0
- stackvitals/ci/track_record.json +11 -0
- stackvitals/ci/triage.py +130 -0
- stackvitals/ci/triggers.py +147 -0
- stackvitals/ci/upload.py +85 -0
- stackvitals/ci/validate.py +91 -0
- stackvitals/cli.py +111 -0
- stackvitals/codeowners.py +69 -0
- stackvitals/community/__init__.py +0 -0
- stackvitals/community/action.py +140 -0
- stackvitals/community/affiliation.py +51 -0
- stackvitals/community/analyze.py +507 -0
- stackvitals/community/build.py +56 -0
- stackvitals/community/chaoss.py +70 -0
- stackvitals/community/cli.py +79 -0
- stackvitals/community/collect.py +240 -0
- stackvitals/community/context.py +54 -0
- stackvitals/community/report.py +250 -0
- stackvitals/connectors/__init__.py +244 -0
- stackvitals/connectors/ai_spend.py +190 -0
- stackvitals/connectors/base.py +82 -0
- stackvitals/connectors/check.py +686 -0
- stackvitals/connectors/copilot.py +104 -0
- stackvitals/connectors/discord.py +103 -0
- stackvitals/connectors/discourse.py +117 -0
- stackvitals/connectors/incident_io.py +106 -0
- stackvitals/connectors/jira.py +210 -0
- stackvitals/connectors/linear.py +140 -0
- stackvitals/connectors/pagerduty.py +96 -0
- stackvitals/connectors/slack.py +110 -0
- stackvitals/connectors/sonar.py +77 -0
- stackvitals/connectors/store.py +120 -0
- stackvitals/connectors/tracker.py +108 -0
- stackvitals/connectors/writeback.py +105 -0
- stackvitals/contribute/__init__.py +1 -0
- stackvitals/contribute/brief.py +297 -0
- stackvitals/contribute/cli.py +64 -0
- stackvitals/core/__init__.py +1 -0
- stackvitals/core/actors.py +103 -0
- stackvitals/core/agent_ledger.py +120 -0
- stackvitals/core/agreements.py +95 -0
- stackvitals/core/ai_outcomes.py +276 -0
- stackvitals/core/alerts.py +224 -0
- stackvitals/core/appdata.py +112 -0
- stackvitals/core/bucket.py +190 -0
- stackvitals/core/bugs.py +43 -0
- stackvitals/core/calendar.py +34 -0
- stackvitals/core/capex.py +57 -0
- stackvitals/core/change_risk.py +181 -0
- stackvitals/core/changes.py +133 -0
- stackvitals/core/chat.py +143 -0
- stackvitals/core/config.py +373 -0
- stackvitals/core/dataflow.py +168 -0
- stackvitals/core/decisions.py +275 -0
- stackvitals/core/delivery.py +954 -0
- stackvitals/core/delivery_peers.py +98 -0
- stackvitals/core/delivery_report.py +429 -0
- stackvitals/core/deliverycli.py +147 -0
- stackvitals/core/devlake_dora.sql +485 -0
- stackvitals/core/digest.py +333 -0
- stackvitals/core/explaincli.py +107 -0
- stackvitals/core/explore.py +296 -0
- stackvitals/core/export.py +342 -0
- stackvitals/core/exportcli.py +117 -0
- stackvitals/core/findings.py +350 -0
- stackvitals/core/health.py +94 -0
- stackvitals/core/identity.py +82 -0
- stackvitals/core/insights.py +365 -0
- stackvitals/core/insightscli.py +43 -0
- stackvitals/core/installdoc.py +69 -0
- stackvitals/core/investment.py +187 -0
- stackvitals/core/joins.py +43 -0
- stackvitals/core/llmsdoc.py +40 -0
- stackvitals/core/merge_queue.py +70 -0
- stackvitals/core/metrics.py +109 -0
- stackvitals/core/model.py +285 -0
- stackvitals/core/projectcli.py +66 -0
- stackvitals/core/propose.py +86 -0
- stackvitals/core/pulse.py +85 -0
- stackvitals/core/recorded.py +145 -0
- stackvitals/core/releases.py +39 -0
- stackvitals/core/requests.py +528 -0
- stackvitals/core/resync.py +35 -0
- stackvitals/core/schema.py +205 -0
- stackvitals/core/services.py +203 -0
- stackvitals/core/settings.py +96 -0
- stackvitals/core/sqlcli.py +111 -0
- stackvitals/core/stats.py +222 -0
- stackvitals/core/store.py +125 -0
- stackvitals/core/summary.py +104 -0
- stackvitals/core/time.py +29 -0
- stackvitals/core/tomledit.py +149 -0
- stackvitals/core/uicli.py +138 -0
- stackvitals/core/webapp.py +79 -0
- stackvitals/core/what_changed.py +76 -0
- stackvitals/core/workspace.py +194 -0
- stackvitals/deps/__init__.py +1 -0
- stackvitals/deps/action.py +299 -0
- stackvitals/deps/actions_vulns.py +89 -0
- stackvitals/deps/cli.py +77 -0
- stackvitals/deps/dossier.py +341 -0
- stackvitals/deps/evidence.py +44 -0
- stackvitals/deps/fixplan.py +40 -0
- stackvitals/deps/guarddog.py +32 -0
- stackvitals/deps/hygiene.py +47 -0
- stackvitals/deps/imports.py +113 -0
- stackvitals/deps/lockfiles.py +198 -0
- stackvitals/deps/lookalike.py +80 -0
- stackvitals/deps/reach.py +59 -0
- stackvitals/deps/release.py +110 -0
- stackvitals/deps/runtimes.py +161 -0
- stackvitals/deps/supply.py +454 -0
- stackvitals/deps/vulns.py +588 -0
- stackvitals/editors/__init__.py +40 -0
- stackvitals/editors/vscode/extension.js +101 -0
- stackvitals/editors/vscode/marks.js +92 -0
- stackvitals/editors/vscode/package.json +26 -0
- stackvitals/flow/__init__.py +1 -0
- stackvitals/flow/action.py +178 -0
- stackvitals/flow/analyze.py +418 -0
- stackvitals/flow/automate.py +72 -0
- stackvitals/flow/build.py +46 -0
- stackvitals/flow/cli.py +93 -0
- stackvitals/flow/code_hosts.py +146 -0
- stackvitals/flow/collect.py +451 -0
- stackvitals/flow/gitlab_mr.py +75 -0
- stackvitals/flow/knowledge.py +202 -0
- stackvitals/flow/peers.py +90 -0
- stackvitals/flow/preflight.py +210 -0
- stackvitals/flow/report.py +253 -0
- stackvitals/flow/rules.py +135 -0
- stackvitals/flow/store.py +108 -0
- stackvitals/flow/survival.py +84 -0
- stackvitals/flow/triage.py +62 -0
- stackvitals/mcp_server.py +670 -0
- stackvitals/portal/__init__.py +0 -0
- stackvitals/portal/action.py +205 -0
- stackvitals/portal/batch.py +80 -0
- stackvitals/portal/campaigns.py +104 -0
- stackvitals/portal/catalog.py +104 -0
- stackvitals/portal/checks.py +63 -0
- stackvitals/portal/cli.py +172 -0
- stackvitals/portal/descriptor.py +105 -0
- stackvitals/portal/export.py +43 -0
- stackvitals/portal/github.py +48 -0
- stackvitals/portal/golden.py +95 -0
- stackvitals/portal/history.py +30 -0
- stackvitals/portal/inventory.py +264 -0
- stackvitals/portal/owners.py +78 -0
- stackvitals/portal/pages.py +153 -0
- stackvitals/portal/rollup.py +32 -0
- stackvitals/portal/scorecard.py +366 -0
- stackvitals/security/__init__.py +2 -0
- stackvitals/security/probes.py +154 -0
- stackvitals/security/repo_checks.py +132 -0
- stackvitals/security/workflows.py +111 -0
- stackvitals/shell.py +107 -0
- stackvitals/site.py +51 -0
- stackvitals/web/app/LICENSES.md +428 -0
- stackvitals/web/app/boot.js +1 -0
- stackvitals/web/app/chunks/AddRepo-KRPZXWCY.js +1 -0
- stackvitals/web/app/chunks/SetupRunning-XWFQAKRZ.js +1 -0
- stackvitals/web/app/chunks/accuracy-data-OQTDXI7Y.js +1 -0
- stackvitals/web/app/chunks/agents-data-BAL6QAOF.js +11 -0
- stackvitals/web/app/chunks/chunk-24LFKFZ6.js +1 -0
- stackvitals/web/app/chunks/chunk-3G4SQNUC.js +1 -0
- stackvitals/web/app/chunks/chunk-3IXWEOIJ.js +154 -0
- stackvitals/web/app/chunks/chunk-3ROQFHPV.js +1 -0
- stackvitals/web/app/chunks/chunk-45PGAQRO.js +8 -0
- stackvitals/web/app/chunks/chunk-4H53OYLG.js +1 -0
- stackvitals/web/app/chunks/chunk-4LUI7O5P.js +1 -0
- stackvitals/web/app/chunks/chunk-4SQPQ5TB.js +1 -0
- stackvitals/web/app/chunks/chunk-4WKQTWUC.js +2 -0
- stackvitals/web/app/chunks/chunk-576YHDEK.js +8 -0
- stackvitals/web/app/chunks/chunk-6BDBXA6V.js +1 -0
- stackvitals/web/app/chunks/chunk-6RLVD2DT.js +1 -0
- stackvitals/web/app/chunks/chunk-763NH7PP.js +1 -0
- stackvitals/web/app/chunks/chunk-76WAVKUG.js +1 -0
- stackvitals/web/app/chunks/chunk-7II2BD5F.js +1 -0
- stackvitals/web/app/chunks/chunk-7TJCBOAY.js +1 -0
- stackvitals/web/app/chunks/chunk-ASYNLVOP.js +1 -0
- stackvitals/web/app/chunks/chunk-CTKDYZMG.js +1 -0
- stackvitals/web/app/chunks/chunk-CVB26NMC.js +1 -0
- stackvitals/web/app/chunks/chunk-G7NGB5UX.js +1 -0
- stackvitals/web/app/chunks/chunk-GPVLJRZD.js +1 -0
- stackvitals/web/app/chunks/chunk-GQYFN4PE.js +75 -0
- stackvitals/web/app/chunks/chunk-GT4EAKAO.js +2 -0
- stackvitals/web/app/chunks/chunk-IYMBFPCV.js +1 -0
- stackvitals/web/app/chunks/chunk-LAAGCPU3.js +1 -0
- stackvitals/web/app/chunks/chunk-MAPYGK3P.js +1 -0
- stackvitals/web/app/chunks/chunk-P5TXDD45.js +1 -0
- stackvitals/web/app/chunks/chunk-PABICGBL.js +1 -0
- stackvitals/web/app/chunks/chunk-RPXHAG6F.js +1 -0
- stackvitals/web/app/chunks/chunk-RQZPSJ2Y.js +1 -0
- stackvitals/web/app/chunks/chunk-S2ZBMEVA.js +1 -0
- stackvitals/web/app/chunks/chunk-S654BBEP.js +6 -0
- stackvitals/web/app/chunks/chunk-T7QVTNXF.js +1 -0
- stackvitals/web/app/chunks/chunk-THFUOCPC.js +273 -0
- stackvitals/web/app/chunks/chunk-TO4VVRZ7.js +1 -0
- stackvitals/web/app/chunks/chunk-VQ5YXO54.js +1 -0
- stackvitals/web/app/chunks/chunk-VWKSMFWJ.js +1 -0
- stackvitals/web/app/chunks/chunk-Y6LUXDM6.js +1 -0
- stackvitals/web/app/chunks/chunk-YTANGYYH.js +1 -0
- stackvitals/web/app/chunks/connect-KONYWYTT.js +1 -0
- stackvitals/web/app/chunks/connections-data-2DYS4UVW.js +32 -0
- stackvitals/web/app/chunks/logos-N5IINUAM.js +1 -0
- stackvitals/web/app/chunks/more-KMJMJQD3.js +10 -0
- stackvitals/web/app/chunks/more-PC6FVS3P.js +1 -0
- stackvitals/web/app/chunks/more-RCIVCUDE.js +2 -0
- stackvitals/web/app/chunks/more-ZKKRY6NO.css +1 -0
- stackvitals/web/app/chunks/sealbox-35YL4EUH.js +3 -0
- stackvitals/web/app/chunks/session-DYYTOE2K.js +1 -0
- stackvitals/web/app/chunks/settingsApply-2HRJEZYM.js +1 -0
- stackvitals/web/app/chunks/settingsReview-GX2FZQ4M.js +3 -0
- stackvitals/web/app/chunks/uPlot.esm-OIWT3AHJ.js +3 -0
- stackvitals/web/app/demo/ci/impact.json +1 -0
- stackvitals/web/app/demo/ci/quarantine.json +1 -0
- stackvitals/web/app/demo/ci/tickets.json +1 -0
- stackvitals/web/app/demo/mobile/alerts.json +1 -0
- stackvitals/web/app/demo/mobile/app.json +1 -0
- stackvitals/web/app/demo/mobile/community.json +1 -0
- stackvitals/web/app/demo/mobile/connectors.json +1 -0
- stackvitals/web/app/demo/mobile/data.json +1 -0
- stackvitals/web/app/demo/mobile/decisions.json +1 -0
- stackvitals/web/app/demo/mobile/delivery.json +1 -0
- stackvitals/web/app/demo/mobile/deps.json +1 -0
- stackvitals/web/app/demo/mobile/flow.json +1 -0
- stackvitals/web/app/demo/mobile/health.json +1 -0
- stackvitals/web/app/demo/mobile/insights.json +1 -0
- stackvitals/web/app/demo/mobile/pulse.json +1 -0
- stackvitals/web/app/demo/mobile/security.json +1 -0
- stackvitals/web/app/demo/mobile/summary/ci_jobs.jsonl +5 -0
- stackvitals/web/app/demo/mobile/summary/devlake_project_pr_metrics.jsonl +14 -0
- stackvitals/web/app/demo/mobile/summary/devlake_pull_requests.jsonl +14 -0
- stackvitals/web/app/demo/mobile/summary/manifest.json +1 -0
- stackvitals/web/app/demo/mobile/summary/prs.jsonl +19 -0
- stackvitals/web/app/demo/mobile/summary/tests.jsonl +1 -0
- stackvitals/web/app/demo/mobile/supply.json +1 -0
- stackvitals/web/app/demo/mobile/vulns.json +1 -0
- stackvitals/web/app/demo/payments-api/alerts.json +1 -0
- stackvitals/web/app/demo/payments-api/app.json +1 -0
- stackvitals/web/app/demo/payments-api/community.json +1 -0
- stackvitals/web/app/demo/payments-api/connectors.json +1 -0
- stackvitals/web/app/demo/payments-api/data.json +1 -0
- stackvitals/web/app/demo/payments-api/decisions.json +1 -0
- stackvitals/web/app/demo/payments-api/delivery.json +1 -0
- stackvitals/web/app/demo/payments-api/deps.json +1 -0
- stackvitals/web/app/demo/payments-api/flow.json +1 -0
- stackvitals/web/app/demo/payments-api/health.json +1 -0
- stackvitals/web/app/demo/payments-api/insights.json +1 -0
- stackvitals/web/app/demo/payments-api/pulse.json +1 -0
- stackvitals/web/app/demo/payments-api/security.json +1 -0
- stackvitals/web/app/demo/payments-api/summary/ci_jobs.jsonl +5 -0
- stackvitals/web/app/demo/payments-api/summary/devlake_project_pr_metrics.jsonl +14 -0
- stackvitals/web/app/demo/payments-api/summary/devlake_pull_requests.jsonl +14 -0
- stackvitals/web/app/demo/payments-api/summary/manifest.json +1 -0
- stackvitals/web/app/demo/payments-api/summary/prs.jsonl +22 -0
- stackvitals/web/app/demo/payments-api/summary/tests.jsonl +3 -0
- stackvitals/web/app/demo/payments-api/supply.json +1 -0
- stackvitals/web/app/demo/payments-api/vulns.json +1 -0
- stackvitals/web/app/demo/portal.json +1 -0
- stackvitals/web/app/demo/settings.json +1 -0
- stackvitals/web/app/demo/web-app/alerts.json +1 -0
- stackvitals/web/app/demo/web-app/app.json +1 -0
- stackvitals/web/app/demo/web-app/community.json +1 -0
- stackvitals/web/app/demo/web-app/connectors.json +1 -0
- stackvitals/web/app/demo/web-app/data.json +1 -0
- stackvitals/web/app/demo/web-app/decisions.json +1 -0
- stackvitals/web/app/demo/web-app/delivery.json +1 -0
- stackvitals/web/app/demo/web-app/deps.json +1 -0
- stackvitals/web/app/demo/web-app/flow.json +1 -0
- stackvitals/web/app/demo/web-app/health.json +1 -0
- stackvitals/web/app/demo/web-app/insights.json +1 -0
- stackvitals/web/app/demo/web-app/pulse.json +1 -0
- stackvitals/web/app/demo/web-app/security.json +1 -0
- stackvitals/web/app/demo/web-app/summary/ci_jobs.jsonl +5 -0
- stackvitals/web/app/demo/web-app/summary/devlake_project_pr_metrics.jsonl +14 -0
- stackvitals/web/app/demo/web-app/summary/devlake_pull_requests.jsonl +14 -0
- stackvitals/web/app/demo/web-app/summary/manifest.json +1 -0
- stackvitals/web/app/demo/web-app/summary/prs.jsonl +18 -0
- stackvitals/web/app/demo/web-app/summary/tests.jsonl +2 -0
- stackvitals/web/app/demo/web-app/supply.json +1 -0
- stackvitals/web/app/demo/web-app/vulns.json +1 -0
- stackvitals/web/app/fonts/plex-mono-400.woff2 +0 -0
- stackvitals/web/app/fonts/plex-sans-400.woff2 +0 -0
- stackvitals/web/app/fonts/plex-sans-600.woff2 +0 -0
- stackvitals/web/app/fonts/plex-sans-condensed-700.woff2 +0 -0
- stackvitals/web/app/help/accuracy.json +1 -0
- stackvitals/web/app/help/connectors.json +1 -0
- stackvitals/web/app/help/getting-started.json +1 -0
- stackvitals/web/app/help/index.json +1 -0
- stackvitals/web/app/help/modules.json +1 -0
- stackvitals/web/app/help/privacy-and-security.json +1 -0
- stackvitals/web/app/help/reference.json +1 -0
- stackvitals/web/app/main.css +1 -0
- stackvitals/web/app/main.js +1 -0
- stackvitals/web/app/pages/about.css +1 -0
- stackvitals/web/app/pages/about.js +1 -0
- stackvitals/web/app/pages/activity.css +1 -0
- stackvitals/web/app/pages/activity.js +1 -0
- stackvitals/web/app/pages/agents.css +1 -0
- stackvitals/web/app/pages/agents.js +2 -0
- stackvitals/web/app/pages/ci.css +1 -0
- stackvitals/web/app/pages/ci.js +1 -0
- stackvitals/web/app/pages/community.css +1 -0
- stackvitals/web/app/pages/community.js +1 -0
- stackvitals/web/app/pages/connections.css +1 -0
- stackvitals/web/app/pages/connections.js +6 -0
- stackvitals/web/app/pages/delivery.css +1 -0
- stackvitals/web/app/pages/delivery.js +45 -0
- stackvitals/web/app/pages/deps.css +1 -0
- stackvitals/web/app/pages/deps.js +7 -0
- stackvitals/web/app/pages/explore.css +1 -0
- stackvitals/web/app/pages/explore.js +7 -0
- stackvitals/web/app/pages/health.css +1 -0
- stackvitals/web/app/pages/health.js +1 -0
- stackvitals/web/app/pages/help.css +1 -0
- stackvitals/web/app/pages/help.js +6 -0
- stackvitals/web/app/pages/insights.css +1 -0
- stackvitals/web/app/pages/insights.js +1 -0
- stackvitals/web/app/pages/investment.css +1 -0
- stackvitals/web/app/pages/investment.js +3 -0
- stackvitals/web/app/pages/overview.css +1 -0
- stackvitals/web/app/pages/overview.js +1 -0
- stackvitals/web/app/pages/portal.css +1 -0
- stackvitals/web/app/pages/portal.js +51 -0
- stackvitals/web/app/pages/prs.css +1 -0
- stackvitals/web/app/pages/prs.js +4 -0
- stackvitals/web/app/pages/settings.css +1 -0
- stackvitals/web/app/pages/settings.js +1 -0
- stackvitals/web/app/pages/setup.css +1 -0
- stackvitals/web/app/pages/setup.js +2 -0
- stackvitals/web/app/pages/standing.css +1 -0
- stackvitals/web/app/pages/standing.js +1 -0
- stackvitals/web/app/pages/welcome-sheet.js +1 -0
- stackvitals/web/app/pages/welcome.js +1 -0
- stackvitals/web/app/settings-workflow.json +1 -0
- stackvitals/web/index.html +34 -0
- stackvitals/web/site/index.html +108 -0
- stackvitals/web/site/integrations.html +15 -0
- stackvitals/web/site/setup.html +15 -0
- stackvitals/web/site/site.css +68 -0
- stackvitals-0.1.0.dist-info/METADATA +134 -0
- stackvitals-0.1.0.dist-info/RECORD +465 -0
- stackvitals-0.1.0.dist-info/WHEEL +4 -0
- stackvitals-0.1.0.dist-info/entry_points.txt +3 -0
- stackvitals-0.1.0.dist-info/licenses/LICENSE +202 -0
- stackvitals-0.1.0.dist-info/licenses/NOTICE +16 -0
stackvitals/cli.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
MODULES = {
|
|
6
|
+
"ci": "CI health: flaky and slow jobs (`stackvitals ci report owner/repo`)",
|
|
7
|
+
"flow": "Team Flow: who acts next on pull requests (`stackvitals flow report owner/repo`)",
|
|
8
|
+
"deps": "Dependency Steward: should you add or upgrade a package (`stackvitals deps check requests`)",
|
|
9
|
+
"portal": "Portal: what an org owns, who owns it, and its shape (`stackvitals portal report my-org`)",
|
|
10
|
+
"community": "Community: where newcomers drop out and how fast they get a first answer (`stackvitals community report owner/repo`)",
|
|
11
|
+
"contribute": "Contribute: open-source issues you can get merged, and how changes get merged in a repo (`stackvitals contribute brief owner/repo`)",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main(argv: list[str] | None = None) -> int:
|
|
16
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
17
|
+
parser = argparse.ArgumentParser(prog="stackvitals", description="Free, open-source developer suite.")
|
|
18
|
+
sub = parser.add_subparsers(dest="module", required=True)
|
|
19
|
+
for name, text in MODULES.items():
|
|
20
|
+
sub.add_parser(name, add_help=False, help=text)
|
|
21
|
+
sub.add_parser("action", add_help=False, help="Run inside GitHub Actions (used by the StackVitals Action)")
|
|
22
|
+
serve = sub.add_parser("serve", help="Run the MCP server on stdio (MergeScout and CI Health tools)")
|
|
23
|
+
serve.add_argument("--tools", choices=["core", "all"], help="core: the 11 core tools (default); all: every tool "
|
|
24
|
+
"(or set STACKVITALS_MCP_TOOLS=all)")
|
|
25
|
+
sub.add_parser("record", add_help=False, help="Send a deployment or incident from any tool (`stackvitals record deployment owner/repo --id 42 --sha abc1234`)")
|
|
26
|
+
sub.add_parser("project", add_help=False, help="DORA for one project across several repos, as DevLake counts a project (`stackvitals project web org/a org/b`)")
|
|
27
|
+
sub.add_parser("delivery", add_help=False, help="Cycle time, DORA and stuck PRs (`stackvitals delivery owner/repo`)")
|
|
28
|
+
sub.add_parser("ui", add_help=False, help="Open a repo's pages, Explore included, from a local copy (`stackvitals ui owner/repo`)")
|
|
29
|
+
sub.add_parser("explain", add_help=False, help="What a headline number was counted from (`stackvitals explain owner/repo lead-time`)")
|
|
30
|
+
sub.add_parser("export", add_help=False, help="A repo's data as DevLake tables or a CTRF test report (`stackvitals export owner/repo --format devlake|ctrf`)")
|
|
31
|
+
sub.add_parser("insights", add_help=False, help="What to fix first, and how long merged PRs sat with failing checks (`stackvitals insights owner/repo`)")
|
|
32
|
+
sub.add_parser("sql", add_help=False, help="SQL over a repo's summaries (`stackvitals sql --repo owner/name \"select ...\"`)")
|
|
33
|
+
sub.add_parser("propose", add_help=False, help="A link proposing a settings change or decision that a person submits (`stackvitals propose owner/repo --set alerts.pickup_p75_hours=6`)")
|
|
34
|
+
sub.add_parser("site", add_help=False, help="Build the project site: landing page, the app and its demo (`stackvitals site build --out DIR`)")
|
|
35
|
+
sub.add_parser("connectors", add_help=False, help="Test each connector's key (`stackvitals connectors check jira slack`, or --all)")
|
|
36
|
+
sub.add_parser("editor", add_help=False, help="Mark flaky tests in your editor (`stackvitals editor vscode|cursor|vscodium`)")
|
|
37
|
+
# Module commands parse their own arguments.
|
|
38
|
+
args, rest = parser.parse_known_args(argv)
|
|
39
|
+
if args.module == "ci":
|
|
40
|
+
from stackvitals.ci.cli import main as ci_main
|
|
41
|
+
return ci_main(rest)
|
|
42
|
+
if args.module == "flow":
|
|
43
|
+
import stackvitals.flow.cli as flow_cli
|
|
44
|
+
return flow_cli.main(rest)
|
|
45
|
+
if args.module == "deps":
|
|
46
|
+
from stackvitals.deps.cli import main as deps_main
|
|
47
|
+
return deps_main(rest)
|
|
48
|
+
if args.module == "portal":
|
|
49
|
+
from stackvitals.portal.cli import main as portal_main
|
|
50
|
+
return portal_main(rest)
|
|
51
|
+
if args.module == "community":
|
|
52
|
+
from stackvitals.community.cli import main as community_main
|
|
53
|
+
return community_main(rest)
|
|
54
|
+
if args.module == "action":
|
|
55
|
+
from stackvitals.ci.cli import action_main
|
|
56
|
+
return action_main(rest)
|
|
57
|
+
if args.module == "ui":
|
|
58
|
+
from stackvitals.core.uicli import main as ui_main
|
|
59
|
+
return ui_main(rest)
|
|
60
|
+
if args.module == "explain":
|
|
61
|
+
from stackvitals.core.explaincli import main as explain_main
|
|
62
|
+
return explain_main(rest)
|
|
63
|
+
if args.module == "export":
|
|
64
|
+
from stackvitals.core.exportcli import main as export_main
|
|
65
|
+
return export_main(rest)
|
|
66
|
+
if args.module == "project":
|
|
67
|
+
from stackvitals.core.projectcli import main as project_main
|
|
68
|
+
return project_main(rest)
|
|
69
|
+
if args.module == "record":
|
|
70
|
+
from stackvitals.core.recorded import main as record_main
|
|
71
|
+
return record_main(rest)
|
|
72
|
+
if args.module == "delivery":
|
|
73
|
+
from stackvitals.core.deliverycli import main as delivery_main
|
|
74
|
+
return delivery_main(rest)
|
|
75
|
+
if args.module == "insights":
|
|
76
|
+
from stackvitals.core.insightscli import main as insights_main
|
|
77
|
+
return insights_main(rest)
|
|
78
|
+
if args.module == "propose":
|
|
79
|
+
from stackvitals.core.propose import main as propose_main
|
|
80
|
+
return propose_main(rest)
|
|
81
|
+
if args.module == "site":
|
|
82
|
+
from stackvitals.site import main as site_main
|
|
83
|
+
return site_main(rest)
|
|
84
|
+
if args.module == "editor":
|
|
85
|
+
from stackvitals.editors import main as editor_main
|
|
86
|
+
return editor_main(rest)
|
|
87
|
+
if args.module == "connectors":
|
|
88
|
+
from stackvitals.connectors.check import main as connectors_main
|
|
89
|
+
return connectors_main(rest)
|
|
90
|
+
if args.module == "sql":
|
|
91
|
+
from stackvitals.core.sqlcli import main as sql_main
|
|
92
|
+
return sql_main(rest)
|
|
93
|
+
if args.module == "serve":
|
|
94
|
+
import logging
|
|
95
|
+
|
|
96
|
+
from stackvitals.mcp_server import build_server
|
|
97
|
+
# Request lines from the HTTP library would only clutter the AI client's log.
|
|
98
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
99
|
+
# The 11 core tools by default; `--tools all` (or STACKVITALS_MCP_TOOLS=all) loads every one.
|
|
100
|
+
tools = args.tools or os.environ.get("STACKVITALS_MCP_TOOLS", "core") # the command line wins over the environment
|
|
101
|
+
build_server(tools="all" if tools == "all" else "core").run("stdio")
|
|
102
|
+
return 0
|
|
103
|
+
if rest[:1] == ["brief"]:
|
|
104
|
+
import stackvitals.contribute.cli as brief_cli
|
|
105
|
+
return brief_cli.main(rest[1:])
|
|
106
|
+
import mergescout.cli as mergescout_cli
|
|
107
|
+
return mergescout_cli.main(rest)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""CODEOWNERS read the way GitHub reads it, shared by CI Health, Team Flow and Portal: gitignore-style patterns, the last
|
|
2
|
+
matching line wins, a line without owners leaves its paths without owners."""
|
|
3
|
+
import re
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
|
|
6
|
+
# GitHub's lookup order: the first file found is the one it uses.
|
|
7
|
+
PATHS = (".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse(text: str | None, skip=frozenset()) -> list[tuple[str, list[str]]]:
|
|
11
|
+
"""(pattern, owners) per rule line; comments, blank lines, "!" lines (unsupported by GitHub) and the line numbers in
|
|
12
|
+
`skip` (lines GitHub's error check calls invalid, which GitHub skips) are left out."""
|
|
13
|
+
rules = []
|
|
14
|
+
for n, raw in enumerate((text or "").lstrip("\ufeff").splitlines(), 1):
|
|
15
|
+
line = raw.strip()
|
|
16
|
+
if n in skip:
|
|
17
|
+
continue
|
|
18
|
+
if not line or line.startswith(("#", "!")):
|
|
19
|
+
continue
|
|
20
|
+
# A space inside a path is written "\ ".
|
|
21
|
+
pattern, *rest = re.split(r"(?<!\\)\s+", line)
|
|
22
|
+
owners = []
|
|
23
|
+
for token in rest:
|
|
24
|
+
if token.startswith("#"):
|
|
25
|
+
break
|
|
26
|
+
owners.append(token)
|
|
27
|
+
rules.append((pattern.replace("\\ ", " "), owners))
|
|
28
|
+
return rules
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _glob(segment: str) -> str:
|
|
32
|
+
return "".join("[^/]*" if c == "*" else "[^/]" if c == "?" else re.escape(c) for c in segment)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@lru_cache(maxsize=65536)
|
|
36
|
+
def _regex(pattern: str) -> re.Pattern:
|
|
37
|
+
# A slash at the start or in the middle ties the pattern to the repo root; otherwise it matches at any depth.
|
|
38
|
+
anchored = pattern.startswith("/") or "/" in pattern.strip("/")
|
|
39
|
+
segs = pattern.strip("/").split("/")
|
|
40
|
+
rx = "" if anchored else "(?:.*/)?"
|
|
41
|
+
for i, seg in enumerate(segs):
|
|
42
|
+
first, last = i == 0, i == len(segs) - 1
|
|
43
|
+
if seg == "**":
|
|
44
|
+
rx += (".*" if first else "/.*") if last else ("(?:.*/)?" if first else "/(?:.*/)?")
|
|
45
|
+
continue
|
|
46
|
+
if not first and segs[i - 1] != "**":
|
|
47
|
+
rx += "/"
|
|
48
|
+
rx += _glob(seg)
|
|
49
|
+
if segs[-1] == "**":
|
|
50
|
+
pass
|
|
51
|
+
elif pattern.endswith("/"):
|
|
52
|
+
rx += "/.*" # a folder: everything inside it
|
|
53
|
+
elif not (segs[-1] == "*" and len(segs) > 1):
|
|
54
|
+
rx += "(?:/.*)?" # a file, or a folder and everything inside it; "docs/*" stays one level deep
|
|
55
|
+
return re.compile(rx)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def matches(pattern: str, path: str) -> bool:
|
|
59
|
+
return bool(_regex(pattern).fullmatch(path.lstrip("/")))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def owners_for(path: str, rules: list[tuple[str, list[str]]], unknown=frozenset()) -> list[str]:
|
|
63
|
+
"""The owners GitHub would request for this path, as written (@user, @org/team, email). Owners GitHub ignores
|
|
64
|
+
(`unknown`, lowercase, from its error check) are left out; the matching line still decides, as on GitHub."""
|
|
65
|
+
found = []
|
|
66
|
+
for pattern, owners in rules:
|
|
67
|
+
if matches(pattern, path):
|
|
68
|
+
found = owners
|
|
69
|
+
return [o for o in found if o.lower() not in unknown]
|
|
File without changes
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections import Counter
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from stackvitals.ci import bench as ci_bench
|
|
7
|
+
from stackvitals.ci.action import BRANCH, DataBranch, GitError
|
|
8
|
+
from stackvitals.ci.api import ActionsAPI, APIError, OutOfBudget
|
|
9
|
+
from stackvitals.community import affiliation
|
|
10
|
+
from stackvitals.connectors.store import ConnectorStore
|
|
11
|
+
from stackvitals.community.build import build
|
|
12
|
+
from stackvitals.community.context import read_context
|
|
13
|
+
from stackvitals.community.collect import _read, collect_discussions, collect_issues, collect_stars, collect_traffic
|
|
14
|
+
from stackvitals.community.report import badges, render_html, render_markdown
|
|
15
|
+
from stackvitals.core.store import published
|
|
16
|
+
from stackvitals.flow.action import FLOW_EVENTS
|
|
17
|
+
from stackvitals.flow.action import LABEL as FLOW_LABEL
|
|
18
|
+
from stackvitals.flow.action import make_gql
|
|
19
|
+
from stackvitals.flow.collect import collect
|
|
20
|
+
from stackvitals.flow.store import FlowStore
|
|
21
|
+
from stackvitals import shell
|
|
22
|
+
from stackvitals.core.actors import kind_of
|
|
23
|
+
from stackvitals.core.config import parse
|
|
24
|
+
from stackvitals.core.delivery import apply_identity
|
|
25
|
+
|
|
26
|
+
BENCH_FILE, STARS_FILE = "community-benchmarks.json", "stars-benchmarks.json"
|
|
27
|
+
LABEL = "Community"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_benchmarks(cache_dir: Path, token: str | None = None) -> tuple[dict | None, dict | None]:
|
|
31
|
+
# In the Action: public files only (none before launch). Locally, the user's own token may read them.
|
|
32
|
+
ok = lambda d: d if isinstance(d, dict) and isinstance(d.get("groups"), dict) else None # noqa: E731
|
|
33
|
+
return (ci_bench.load(token, cache_dir, name=BENCH_FILE, url_env="STACKVITALS_COMMUNITY_BENCH_URL", validate=ok, default_url=None),
|
|
34
|
+
ci_bench.load(token, cache_dir, name=STARS_FILE, url_env="STACKVITALS_STARS_BENCH_URL", validate=ok, default_url=None))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def write_community_outputs(root: Path, result: dict, repo: str) -> None:
|
|
38
|
+
docs = root / "docs"
|
|
39
|
+
docs.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
(docs / "community.html").write_text(render_html(result, repo, shell.present(docs) | {"community.html"}), encoding="utf-8")
|
|
41
|
+
(docs / "community.json").write_text(json.dumps(published(result), indent=1), encoding="utf-8")
|
|
42
|
+
for name, badge in badges(result).items():
|
|
43
|
+
(docs / name).write_text(json.dumps(badge), encoding="utf-8")
|
|
44
|
+
(docs / ".nojekyll").write_text("", encoding="utf-8")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def community_step(ws, env: dict, now: datetime, make_gql=make_gql, make_api=ActionsAPI, flow_ran: bool | None = None) -> dict:
|
|
48
|
+
"""Community's part of a job: issues, star history and (with its own token) traffic into the shared checkout,
|
|
49
|
+
beside Team Flow's PR history; the page, the badges and (after the commit) the summary."""
|
|
50
|
+
repo, token = ws.repo, env.get("STACKVITALS_TOKEN") or env.get("GITHUB_TOKEN")
|
|
51
|
+
traffic_token = (env.get("STACKVITALS_TRAFFIC_TOKEN") or "").strip()
|
|
52
|
+
owner, name = repo.split("/")
|
|
53
|
+
if flow_ran is None:
|
|
54
|
+
# Team Flow ran earlier in this same job: its PRs are already in the checkout.
|
|
55
|
+
flow_ran = ws.results.get(FLOW_LABEL) is not None
|
|
56
|
+
notes = []
|
|
57
|
+
prs, gql = FlowStore(ws.path), make_gql(token)
|
|
58
|
+
# Team Flow already read the PRs this run; otherwise Community keeps the same history itself.
|
|
59
|
+
points = 0 if flow_ran else collect(gql, prs, owner, name, now)["points"]
|
|
60
|
+
issues = FlowStore(ws.path, "community/issues")
|
|
61
|
+
got = collect_issues(gql, issues, owner, name, now)
|
|
62
|
+
if got.get("refused"):
|
|
63
|
+
notes.append("Add `issues: read` to the workflow permissions to include issues.")
|
|
64
|
+
folder = ws.path / "community"
|
|
65
|
+
try:
|
|
66
|
+
collect_stars(make_api(token, repo, max_requests=110), folder, now)
|
|
67
|
+
except (APIError, OutOfBudget) as exc:
|
|
68
|
+
notes.append(f"Star history wasn't read this time ({exc}).")
|
|
69
|
+
if traffic_token:
|
|
70
|
+
try:
|
|
71
|
+
collect_traffic(make_api(traffic_token, repo, max_requests=10), folder, now)
|
|
72
|
+
except (APIError, OutOfBudget) as exc:
|
|
73
|
+
notes.append("The traffic token can't read traffic: it needs the repository permission "
|
|
74
|
+
f"\"Administration: read\" ({str(exc)[:120]}).")
|
|
75
|
+
bench, star_bench = load_benchmarks(ws.work / "bench")
|
|
76
|
+
# The repo's [identity] settings decide who is a bot or a newcomer; the stored history stays as GitHub said.
|
|
77
|
+
settings = ws.cache.get("settings") or parse("")[0]
|
|
78
|
+
ident = settings.get("identity")
|
|
79
|
+
pr_data = apply_identity(prs.load(), ident)
|
|
80
|
+
context = _context(gql, folder, owner, name, now)
|
|
81
|
+
stated = {k: v for k, v in (settings.get("community") or {}).items() if v}
|
|
82
|
+
if stated:
|
|
83
|
+
context = {**(context or {}), "stated": stated}
|
|
84
|
+
talks = FlowStore(ws.path, "community/discussions")
|
|
85
|
+
if (context or {}).get("discussions"):
|
|
86
|
+
points += collect_discussions(gql, talks, owner, name, now)["points"]
|
|
87
|
+
# The people behind the last 180 days' merged work, busiest first, for the elephant factor.
|
|
88
|
+
since = (now - timedelta(days=180)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
89
|
+
authors = Counter(p["au"] for p in pr_data.get("closed") or [] if p.get("mg") and p["mg"] >= since and p.get("au")
|
|
90
|
+
and kind_of(p["au"], p.get("at")) == "person")
|
|
91
|
+
aff = affiliation.collect(gql, [a for a, _n in authors.most_common()])
|
|
92
|
+
result = build(pr_data, apply_identity(issues.load(), ident), _read(folder / "stars.json"),
|
|
93
|
+
_read(folder / "traffic.json"), prs.repo(), now, bench=bench, star_bench=star_bench,
|
|
94
|
+
context=context, affiliation=aff, discussions=talks.load(),
|
|
95
|
+
forum=ConnectorStore(ws.path, "discourse").load().get("topic"),
|
|
96
|
+
chat=[t for n in ("slack", "discord") for t in ConnectorStore(ws.path, n).load().get("topic") or []])
|
|
97
|
+
write_community_outputs(ws.path, result, repo)
|
|
98
|
+
out = {"issues": got["issues"], "points": points + got["points"], "complete": got["complete"],
|
|
99
|
+
"waiting": result["waiting_total"], "score": (result.get("score") or {}).get("score")}
|
|
100
|
+
|
|
101
|
+
def after_commit():
|
|
102
|
+
summary = env.get("GITHUB_STEP_SUMMARY")
|
|
103
|
+
if summary:
|
|
104
|
+
note = "" if got["complete"] or got.get("refused") else "\n\nStill reading older issues (GitHub's hourly limit); the next run continues."
|
|
105
|
+
note += "".join(f"\n\n{n}" for n in notes)
|
|
106
|
+
with open(summary, "a", encoding="utf-8") as f:
|
|
107
|
+
f.write(render_markdown(result, repo) + note + "\n")
|
|
108
|
+
ws.after_commit(after_commit, LABEL)
|
|
109
|
+
return out
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _context(gql, folder: Path, owner: str, name: str, now: datetime) -> dict | None:
|
|
113
|
+
"""What the repo states about itself, read once a week (a file changes rarely)."""
|
|
114
|
+
kept = _read(folder / "context.json")
|
|
115
|
+
if kept.get("at", "") >= (now - timedelta(days=7)).strftime("%Y-%m-%d"):
|
|
116
|
+
return kept.get("facts")
|
|
117
|
+
facts = read_context(gql, owner, name)
|
|
118
|
+
if facts is None:
|
|
119
|
+
return kept.get("facts")
|
|
120
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
(folder / "context.json").write_text(json.dumps({"at": now.strftime("%Y-%m-%d"), "facts": facts}, indent=1),
|
|
122
|
+
encoding="utf-8")
|
|
123
|
+
return facts
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def run_community_action(env: dict, now: datetime | None = None, make_gql=make_gql, make_api=ActionsAPI,
|
|
127
|
+
flow_ran: bool | None = None, workspace=None) -> dict | None:
|
|
128
|
+
"""One Community run on the schedule or by hand: issues, star history and (with its own token) traffic, kept in the
|
|
129
|
+
data branch beside Team Flow's PR history, then the page, the badges and the summary."""
|
|
130
|
+
if env.get("GITHUB_EVENT_NAME") not in FLOW_EVENTS:
|
|
131
|
+
return None
|
|
132
|
+
from stackvitals.core.workspace import run_job
|
|
133
|
+
now = now or datetime.now(timezone.utc)
|
|
134
|
+
|
|
135
|
+
def step(ws):
|
|
136
|
+
return community_step(ws, env, now, make_gql, make_api, bool(flow_ran) if workspace is None else flow_ran)
|
|
137
|
+
if workspace is not None:
|
|
138
|
+
return step(workspace)
|
|
139
|
+
job = run_job(env, [(LABEL, step)], now, message=f"stackvitals: community {now:%Y-%m-%d %H:%M} UTC", catch=False)
|
|
140
|
+
return {**job["results"][LABEL], "pushed": job["outcome"] == "pushed"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Which organisation the people behind the merged work say they work for, for the elephant factor (CHAOSS): the
|
|
2
|
+
company on their public profile, else unknown (counted and shown, never guessed). Membership of the org that owns the
|
|
3
|
+
repo isn't used: volunteer projects' orgs aren't employers. Looked up each run and kept only in memory, so no person's
|
|
4
|
+
employer is written to the data branch."""
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from mergescout.github.graphql import GraphQLError, RateLimited
|
|
10
|
+
|
|
11
|
+
BATCH, MAX_PEOPLE = 25, 150
|
|
12
|
+
SUFFIX = re.compile(r"\b(inc|llc|ltd|limited|corp|corporation|gmbh|co|company|plc|sa|ag|bv|oy|ab|the)\b\.?")
|
|
13
|
+
NOT_A_COMPANY = {"freelance", "freelancer", "independent", "self", "self-employed", "selfemployed", "none", "n/a", "na",
|
|
14
|
+
"student", "retired", "personal", "home", "me", "myself", "open source", "opensource", "unemployed"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def key(login: str) -> str:
|
|
18
|
+
return hashlib.sha1((login or "").lower().encode()).hexdigest()[:12]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def normalize(company: str | None) -> str | None:
|
|
22
|
+
"""A company field as one organisation name: "@Google LLC" and "google, inc." are google; self-employment is none."""
|
|
23
|
+
text = (company or "").strip().lower()
|
|
24
|
+
if not text:
|
|
25
|
+
return None
|
|
26
|
+
text = text.split()[0] if text.startswith("@") else text # "@acme @other": the first one
|
|
27
|
+
text = re.split(r"[,/|;(]", text.lstrip("@"))[0]
|
|
28
|
+
text = SUFFIX.sub("", text).strip(" .-_")
|
|
29
|
+
text = re.sub(r"\s+", " ", text)
|
|
30
|
+
return None if not text or text in NOT_A_COMPANY else text
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _query(logins: list[str]) -> str:
|
|
34
|
+
parts = [f"u{i}: user(login: {json.dumps(x)}) {{ login company }}" for i, x in enumerate(logins)]
|
|
35
|
+
return "query { rateLimit { cost remaining resetAt } " + " ".join(parts) + " }"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def collect(gql, logins: list[str]) -> dict[str, str | None]:
|
|
39
|
+
"""Each login's organisation (by hash), looked up fresh each run and never written down: a hash of a public login
|
|
40
|
+
can be reversed, so a kept file would name each person's employer. At most 150 people (the busiest first)."""
|
|
41
|
+
out = {}
|
|
42
|
+
for i in range(0, min(len(logins), MAX_PEOPLE), BATCH):
|
|
43
|
+
batch = logins[i:min(i + BATCH, MAX_PEOPLE)]
|
|
44
|
+
try:
|
|
45
|
+
# Partial: a renamed or deleted account answers null instead of failing the whole batch.
|
|
46
|
+
data = gql.query(_query(batch), {}, partial=True).data or {}
|
|
47
|
+
except (GraphQLError, RateLimited):
|
|
48
|
+
break
|
|
49
|
+
for j, login in enumerate(batch):
|
|
50
|
+
out[key(login)] = normalize((data.get(f"u{j}") or {}).get("company"))
|
|
51
|
+
return out
|