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
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""The repo's own settings, from `.github/stackvitals.toml` on its default branch: monitor thresholds, mutes and quarantine
|
|
2
|
+
overrides. A missing file means the defaults; a bad value keeps its default and says so in the job summary."""
|
|
3
|
+
import copy
|
|
4
|
+
import re
|
|
5
|
+
import tomllib
|
|
6
|
+
from datetime import date, datetime
|
|
7
|
+
|
|
8
|
+
FILE = ".github/stackvitals.toml"
|
|
9
|
+
# Advisory severities, lowest first, as OSV and GitHub name them.
|
|
10
|
+
SEVERITIES = ("low", "moderate", "high", "critical")
|
|
11
|
+
# Trunk's documented monitor shape; the heal time keeps our own two quiet weeks (one line to change).
|
|
12
|
+
MONITORS = {
|
|
13
|
+
"pass_on_retry": {"min_events": 2, "quiet_days": 14, "quiet_runs": 10},
|
|
14
|
+
"failure_rate": {"enabled": True, "window_runs": 50, "min_runs": 20, "activate": 0.15, "resolve": 0.05},
|
|
15
|
+
"broken": {"streak": 3, "window_runs": 5, "activate": 0.8, "resolve": 0.5},
|
|
16
|
+
"slow": {"p50_ms": 5000},
|
|
17
|
+
"timeout_inflation": {"ratio": 2.0, "min_failures": 3},
|
|
18
|
+
}
|
|
19
|
+
# Where code ships and what counts as an incident (DevLake's defaults, widened for common names).
|
|
20
|
+
DELIVERY = {"deploy_workflow": r"(?i)(deploy|release|publish|push-image)", "production": ["production", "prod", "live"],
|
|
21
|
+
"incident_labels": ["incident", "outage", "sev1", "sev-1", "sev2", "sev-2", "p0"], "hotfix_labels": ["hotfix"],
|
|
22
|
+
"source": "auto", "dora_report": "2023", "fix_patterns": [], "incident_sources": [], "ignore_reverts_of": [], "production_branches": []}
|
|
23
|
+
SOURCES = ("auto", "deployments", "workflows", "releases", "merges")
|
|
24
|
+
# Where incidents may come from; an empty list keeps the automatic choice (a pager tool's alone when there is one).
|
|
25
|
+
INCIDENT_SOURCES = ("issues", "reverts", "hotfixes", "fixes", "pager", "tracker", "recorded")
|
|
26
|
+
# Who is who when GitHub's account type gets it wrong, old logins, and teams (DevLake's team and user mapping).
|
|
27
|
+
IDENTITY = {"bots": [], "people": [], "aliases": {}, "teams": {}, "knowledge_names": False}
|
|
28
|
+
# Alert thresholds: P75 hours from LinearB's 2026 "good" band, the failure rate from DORA's 2023 medium tier; 0 turns one off.
|
|
29
|
+
ALERTS = {"pickup_p75_hours": 4.0, "review_p75_hours": 14.0, "cycle_p75_hours": 72.0, "change_failure_rate": 0.15,
|
|
30
|
+
"channels": ["webhook"]}
|
|
31
|
+
CHANNELS = ("webhook", "issue")
|
|
32
|
+
# Switches that are also workflow inputs; unset means the input decides, so the app can change them without a workflow edit.
|
|
33
|
+
AUTOMATION = {k: None for k in ("pr_comments", "rerun_flaky", "quarantine", "issues", "mention_owners", "assign_owners",
|
|
34
|
+
"preflight_comments", "deps_comments", "label_stuck", "verdict_check", "verdict_strict")}
|
|
35
|
+
# Team automation writes to the repo, so each part is off until the team turns it on (user decision, 2026-10-24, after
|
|
36
|
+
# outside reviews; it was on by default before). Each says what it did in the summary.
|
|
37
|
+
AUTOMATION.update({"review_requests": False, "size_labels": False, "docs_label": False, "fix_requests": False,
|
|
38
|
+
"fix_by_copilot": False})
|
|
39
|
+
# The weekly digest: on the first scheduled run after this day and hour (UTC), to one pinned issue and/or the webhook.
|
|
40
|
+
DIGEST = {"enabled": True, "day": "monday", "hour": 6, "channels": ["issue"], "pulse": True}
|
|
41
|
+
DAYS = ("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday")
|
|
42
|
+
# Working agreements a team sets for itself (Swarmia's), all off until set; 0 or false is off.
|
|
43
|
+
AGREEMENTS = {"review_within_hours": 0, "merge_within_days": 0, "max_pr_lines": 0, "linked_issue": False,
|
|
44
|
+
"review_before_merge": False}
|
|
45
|
+
# Outside trackers, each read with one key from the repo's secrets (a key never goes in this file).
|
|
46
|
+
CONNECTORS = {"jira": {"url": "", "email": "", "scope_jql": "", "incident_jql": "", "key_expires": "", "write_back": False,
|
|
47
|
+
"done_transition": "", "team_field": "", "severity_field": "",
|
|
48
|
+
"flaky_tickets": False, "ticket_project": ""},
|
|
49
|
+
"linear": {"teams": [], "incident_labels": ["incident"], "key_expires": "", "write_back": False, "done_state": "",
|
|
50
|
+
"flaky_tickets": False, "ticket_team": ""},
|
|
51
|
+
"pagerduty": {"services": [], "urgencies": ["high"], "key_expires": ""},
|
|
52
|
+
"incident_io": {"incident_types": [], "key_expires": ""},
|
|
53
|
+
"copilot": {"org": "", "key_expires": ""},
|
|
54
|
+
# Anthropic, OpenAI and Cursor costs, each with an optional admin key; nothing to configure but the expiry.
|
|
55
|
+
"ai_spend": {"key_expires": ""},
|
|
56
|
+
"sonar": {"url": "https://sonarcloud.io", "project": "", "key_expires": ""},
|
|
57
|
+
# A forum read from its public JSON: no key, so nothing to expire.
|
|
58
|
+
"discourse": {"url": "", "team": [], "groups": []},
|
|
59
|
+
# Chat support channels, read with a bot token the user installs; team members are user ids.
|
|
60
|
+
"slack": {"channels": [], "team": [], "key_expires": ""},
|
|
61
|
+
"discord": {"channels": [], "team": [], "key_expires": ""}}
|
|
62
|
+
URGENCIES = ("high", "low")
|
|
63
|
+
GITHUB_NAME = re.compile(r"[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})")
|
|
64
|
+
# Jira Cloud sites only, so a settings change can never send the key to another server.
|
|
65
|
+
JIRA_SITE = re.compile(r"https://[A-Za-z0-9][A-Za-z0-9-]*\.atlassian\.net/?")
|
|
66
|
+
CONNECTOR_HELP = {"jira": "url is your Jira Cloud site like https://acme.atlassian.net, email an address, scope_jql and "
|
|
67
|
+
"incident_jql JQL text, key_expires a date like 2027-10-21, write_back true or false, "
|
|
68
|
+
"done_transition a transition name, team_field and severity_field a field name or id",
|
|
69
|
+
"sonar": "url is https://sonarcloud.io or your SonarQube server, project the Sonar project key",
|
|
70
|
+
"linear": "teams and incident_labels are lists of names, key_expires a date like 2027-10-21, write_back true "
|
|
71
|
+
"or false, done_state a workflow state name",
|
|
72
|
+
"pagerduty": "services is a list of service ids, urgencies high and/or low, key_expires a date",
|
|
73
|
+
"incident_io": "incident_types is a list of incident type names, key_expires a date like 2027-10-21",
|
|
74
|
+
"copilot": "org is a GitHub organization name, key_expires a date like 2027-10-21",
|
|
75
|
+
"discourse": "url is the forum's https address like https://discuss.example.org, team and groups lists of "
|
|
76
|
+
"usernames and group names",
|
|
77
|
+
"slack": "channels and team are lists of channel ids (C0123) and user ids (U0123), key_expires a date",
|
|
78
|
+
"discord": "channels and team are lists of channel ids and user ids (numbers as text), key_expires a date"}
|
|
79
|
+
# Opt-in model triage of undecided failures (ci/triage.py): off or a provider name, an optional model and endpoint url,
|
|
80
|
+
# and whether a private repo's logs may be sent.
|
|
81
|
+
TRIAGE = {"provider": "off", "model": "", "url": "", "allow_private": False}
|
|
82
|
+
TRIAGE_PROVIDERS = ("off", "gemini", "openai", "anthropic", "mistral", "groq")
|
|
83
|
+
DEFAULTS = {"ci": {"monitors": MONITORS, "mute": {}, "quarantine": {"always": [], "never": [], "max_days": 0}, "coverage": {"min_patch": 0.0},
|
|
84
|
+
"triage": TRIAGE},
|
|
85
|
+
"delivery": DELIVERY,
|
|
86
|
+
"identity": IDENTITY, "alerts": ALERTS, "automation": AUTOMATION, "digest": DIGEST, "agreements": AGREEMENTS,
|
|
87
|
+
"connectors": CONNECTORS, "deps": {"licenses": {"deny": []}, "policy": {"min_scorecard": None, "max_severity": None},
|
|
88
|
+
"outside_lookups": True, "guarddog": False}, "investment": {"labels": {}},
|
|
89
|
+
"community": {"capacity": "", "norms": ""}, "benchmarks": {"share": False, "name": False},
|
|
90
|
+
# Services inside the repo (a monorepo): name -> CODEOWNERS-style paths and a deployment-name pattern.
|
|
91
|
+
"services": {},
|
|
92
|
+
# PR rules as code ([[rules]]): when a pull request matches, its label, reviewers or comment (flow/rules.py).
|
|
93
|
+
"rules": [],
|
|
94
|
+
# The team's working calendar: weekends and holidays left out of review phases (core/calendar.py).
|
|
95
|
+
"calendar": {"weekends": [], "holidays": []}}
|
|
96
|
+
STATED_MAX = 300 # characters of each [community] statement shown beside the numbers
|
|
97
|
+
INVESTMENT = ("feature", "fix", "maintenance", "security", "docs") # the kinds of work in the investment mix
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _day(value) -> str:
|
|
101
|
+
# TOML gives dates, date-times (2027-10-21T10:00:00Z) or text; the key's expiry is kept as a plain date.
|
|
102
|
+
if isinstance(value, datetime):
|
|
103
|
+
value = value.date()
|
|
104
|
+
return value.isoformat() if isinstance(value, date) else date.fromisoformat(value).isoformat() if value else ""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# A Discourse forum: any https site (no key goes there), without a path, query or credentials.
|
|
108
|
+
FORUM = re.compile(r"https://[a-z0-9.-]+\.[a-z]{2,}/?", re.I)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _connector_ok(name: str, key: str, value) -> bool:
|
|
112
|
+
if key in ("write_back", "flaky_tickets"):
|
|
113
|
+
return isinstance(value, bool)
|
|
114
|
+
if key in ("ticket_project", "ticket_team"):
|
|
115
|
+
return isinstance(value, str) and bool(re.fullmatch(r"([A-Z][A-Z0-9_]{0,19})?", value))
|
|
116
|
+
if key in ("done_transition", "done_state"):
|
|
117
|
+
return isinstance(value, str) and len(value) <= 100
|
|
118
|
+
if key == "key_expires":
|
|
119
|
+
try:
|
|
120
|
+
return value == "" or isinstance(value, date) or isinstance(value, str) and bool(date.fromisoformat(value))
|
|
121
|
+
except ValueError:
|
|
122
|
+
return False
|
|
123
|
+
if name == "pagerduty" and key == "urgencies":
|
|
124
|
+
return isinstance(value, list) and bool(value) and set(value) <= set(URGENCIES)
|
|
125
|
+
if name == "copilot":
|
|
126
|
+
return key == "org" and isinstance(value, str) and (value == "" or bool(GITHUB_NAME.fullmatch(value)))
|
|
127
|
+
if name == "sonar":
|
|
128
|
+
return isinstance(value, str) and ((key == "url" and bool(FORUM.fullmatch(value)))
|
|
129
|
+
or (key == "project" and len(value) <= 400 and " " not in value))
|
|
130
|
+
if name == "discourse" and key == "url":
|
|
131
|
+
return isinstance(value, str) and (value == "" or bool(FORUM.fullmatch(value)))
|
|
132
|
+
if name == "jira":
|
|
133
|
+
return isinstance(value, str) and (
|
|
134
|
+
(key == "url" and bool(JIRA_SITE.fullmatch(value))) or (key == "email" and "@" in value and " " not in value)
|
|
135
|
+
or (key in ("scope_jql", "incident_jql") and len(value) <= 2000)
|
|
136
|
+
or (key in ("team_field", "severity_field") and len(value) <= 100))
|
|
137
|
+
return isinstance(value, list) and all(isinstance(x, str) and x.strip() for x in value)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _valid(default, value) -> bool:
|
|
141
|
+
if isinstance(default, bool):
|
|
142
|
+
return isinstance(value, bool)
|
|
143
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool) and value >= 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def parse(text: str | None) -> tuple[dict, list[str]]:
|
|
147
|
+
"""(settings, notes): the defaults with the file's valid values on top, and a note for everything left out."""
|
|
148
|
+
cfg, notes = copy.deepcopy(DEFAULTS), []
|
|
149
|
+
if not (text or "").strip():
|
|
150
|
+
return cfg, notes
|
|
151
|
+
try:
|
|
152
|
+
doc = tomllib.loads(text)
|
|
153
|
+
except tomllib.TOMLDecodeError as exc:
|
|
154
|
+
return cfg, [f"{FILE} couldn't be read ({exc}); the defaults were used."]
|
|
155
|
+
for section in doc:
|
|
156
|
+
if section not in DEFAULTS:
|
|
157
|
+
notes.append(f"[{section}] in {FILE} is not a section StackVitals reads ({', '.join(DEFAULTS)}); it was ignored.")
|
|
158
|
+
ci = doc.get("ci") if isinstance(doc.get("ci"), dict) else {}
|
|
159
|
+
for key in ci:
|
|
160
|
+
if key not in DEFAULTS["ci"]:
|
|
161
|
+
notes.append(f"ci.{key} in {FILE} is not a setting StackVitals knows; it was ignored.")
|
|
162
|
+
for name, values in (ci.get("monitors") or {}).items():
|
|
163
|
+
if name not in MONITORS or not isinstance(values, dict):
|
|
164
|
+
notes.append(f"[ci.monitors.{name}] in {FILE} is not a monitor StackVitals knows; it was ignored.")
|
|
165
|
+
continue
|
|
166
|
+
for key, value in values.items():
|
|
167
|
+
default = MONITORS[name].get(key)
|
|
168
|
+
if default is None or not _valid(default, value):
|
|
169
|
+
notes.append(f"ci.monitors.{name}.{key} = {value!r} in {FILE} was ignored (the default {default!r} applies).")
|
|
170
|
+
continue
|
|
171
|
+
cfg["ci"]["monitors"][name][key] = value if isinstance(default, bool) else type(default)(value)
|
|
172
|
+
for key, value in (ci.get("coverage") if isinstance(ci.get("coverage"), dict) else {}).items():
|
|
173
|
+
if key == "min_patch" and isinstance(value, (int, float)) and not isinstance(value, bool) and 0 <= value <= 1:
|
|
174
|
+
cfg["ci"]["coverage"]["min_patch"] = float(value)
|
|
175
|
+
else:
|
|
176
|
+
notes.append(f"ci.coverage.{key} = {value!r} in {FILE} was ignored (min_patch is a share from 0 to 1; 0 is off).")
|
|
177
|
+
for key, value in (ci.get("triage") if isinstance(ci.get("triage"), dict) else {}).items():
|
|
178
|
+
ok = (key == "provider" and value in TRIAGE_PROVIDERS
|
|
179
|
+
or key == "allow_private" and isinstance(value, bool)
|
|
180
|
+
or key == "model" and isinstance(value, str) and len(value) <= 100
|
|
181
|
+
or key == "url" and isinstance(value, str) and value.startswith("https://") and len(value) <= 300)
|
|
182
|
+
if ok:
|
|
183
|
+
cfg["ci"]["triage"][key] = value
|
|
184
|
+
else:
|
|
185
|
+
notes.append(f"ci.triage.{key} = {value!r} in {FILE} was ignored (provider is one of "
|
|
186
|
+
f"{', '.join(TRIAGE_PROVIDERS)}; url must be https; allow_private is true or false; GitHub Models "
|
|
187
|
+
f"was retired in July 2026).")
|
|
188
|
+
for pattern, until in (ci.get("mute") or {}).items():
|
|
189
|
+
text_date = until.isoformat() if isinstance(until, date) else str(until)
|
|
190
|
+
try:
|
|
191
|
+
date.fromisoformat(text_date)
|
|
192
|
+
except ValueError:
|
|
193
|
+
notes.append(f"ci.mute.{pattern!r} = {until!r} in {FILE} needs a date like 2026-12-31; it was ignored.")
|
|
194
|
+
continue
|
|
195
|
+
cfg["ci"]["mute"][str(pattern)] = text_date
|
|
196
|
+
d = doc.get("delivery") if isinstance(doc.get("delivery"), dict) else {}
|
|
197
|
+
for key, value in d.items():
|
|
198
|
+
default = DELIVERY.get(key)
|
|
199
|
+
ok = (isinstance(default, list) and isinstance(value, list) and all(isinstance(x, str) for x in value)) or \
|
|
200
|
+
(isinstance(default, str) and isinstance(value, str))
|
|
201
|
+
if key == "source" and value not in SOURCES or key == "dora_report" and value not in ("2021", "2023"):
|
|
202
|
+
ok = False
|
|
203
|
+
if key == "deploy_workflow" and ok:
|
|
204
|
+
try:
|
|
205
|
+
re.compile(value)
|
|
206
|
+
except re.error:
|
|
207
|
+
ok = False
|
|
208
|
+
if key == "fix_patterns" and ok:
|
|
209
|
+
# Each names the PR being fixed in its one group, like "^fix.*#([0-9]+)" or "^hotfix/([0-9]+)-".
|
|
210
|
+
try:
|
|
211
|
+
ok = len(value) <= 5 and all(re.compile(x).groups == 1 for x in value)
|
|
212
|
+
except re.error:
|
|
213
|
+
ok = False
|
|
214
|
+
if key == "incident_sources" and ok and not set(value) <= set(INCIDENT_SOURCES):
|
|
215
|
+
ok = False
|
|
216
|
+
if default is None or not ok:
|
|
217
|
+
notes.append(f"delivery.{key} = {value!r} in {FILE} was ignored (the default {default!r} applies).")
|
|
218
|
+
continue
|
|
219
|
+
cfg["delivery"][key] = value
|
|
220
|
+
ident = doc.get("identity") if isinstance(doc.get("identity"), dict) else {}
|
|
221
|
+
for key, value in ident.items():
|
|
222
|
+
if key in ("bots", "people") and isinstance(value, list) and all(isinstance(x, str) for x in value):
|
|
223
|
+
cfg["identity"][key] = list(value)
|
|
224
|
+
elif key == "aliases" and isinstance(value, dict) and all(isinstance(v, str) for v in value.values()):
|
|
225
|
+
cfg["identity"]["aliases"] = {str(k): v for k, v in value.items()}
|
|
226
|
+
elif key == "teams" and isinstance(value, dict) and all(
|
|
227
|
+
isinstance(v, list) and all(isinstance(x, str) for x in v) for v in value.values()):
|
|
228
|
+
cfg["identity"]["teams"] = {str(k): list(v) for k, v in value.items()}
|
|
229
|
+
elif key == "knowledge_names" and isinstance(value, bool):
|
|
230
|
+
cfg["identity"]["knowledge_names"] = value
|
|
231
|
+
else:
|
|
232
|
+
notes.append(f"identity.{key} in {FILE} was ignored (bots and people are lists of logins, aliases maps an "
|
|
233
|
+
f"old login to the current one, teams maps a team to its logins).")
|
|
234
|
+
from stackvitals.flow.rules import check_rule
|
|
235
|
+
for i, rule in enumerate(doc.get("rules") if isinstance(doc.get("rules"), list) else []):
|
|
236
|
+
problem = check_rule(rule)
|
|
237
|
+
if problem:
|
|
238
|
+
notes.append(f"rules[{i + 1}] in {FILE} was ignored ({problem}).")
|
|
239
|
+
else:
|
|
240
|
+
cfg["rules"].append({"name": rule["name"], "when": dict(rule.get("when") or {}), "then": dict(rule["then"]),
|
|
241
|
+
"dry_run": bool(rule.get("dry_run", False))})
|
|
242
|
+
cal = doc.get("calendar") if isinstance(doc.get("calendar"), dict) else {}
|
|
243
|
+
days = ("monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday")
|
|
244
|
+
if "weekends" in cal:
|
|
245
|
+
if isinstance(cal["weekends"], list) and all(isinstance(d, str) and d.lower() in days for d in cal["weekends"]):
|
|
246
|
+
cfg["calendar"]["weekends"] = [d.lower() for d in cal["weekends"]]
|
|
247
|
+
else:
|
|
248
|
+
notes.append(f"calendar.weekends in {FILE} was ignored (a list of day names, like [\"saturday\", \"sunday\"]).")
|
|
249
|
+
if "holidays" in cal:
|
|
250
|
+
try:
|
|
251
|
+
if not isinstance(cal["holidays"], list):
|
|
252
|
+
raise ValueError("not a list")
|
|
253
|
+
cfg["calendar"]["holidays"] = sorted({date.fromisoformat(str(h)).isoformat() for h in cal["holidays"]})
|
|
254
|
+
except (ValueError, TypeError):
|
|
255
|
+
notes.append(f"calendar.holidays in {FILE} was ignored (a list of dates, like [\"2026-12-25\"]).")
|
|
256
|
+
for name, svc in (doc.get("services") if isinstance(doc.get("services"), dict) else {}).items():
|
|
257
|
+
paths = svc.get("paths") if isinstance(svc, dict) else None
|
|
258
|
+
if not (isinstance(paths, list) and paths and all(isinstance(x, str) and x.strip() for x in paths)):
|
|
259
|
+
notes.append(f"services.{name} in {FILE} was ignored (paths is a list of CODEOWNERS-style patterns, like "
|
|
260
|
+
'["services/api/", "libs/shared/**"]).')
|
|
261
|
+
continue
|
|
262
|
+
deploy = svc.get("deploy") or ""
|
|
263
|
+
try:
|
|
264
|
+
re.compile(deploy) if isinstance(deploy, str) else None
|
|
265
|
+
except re.error:
|
|
266
|
+
deploy = None
|
|
267
|
+
if not isinstance(deploy, str):
|
|
268
|
+
notes.append(f"services.{name}.deploy in {FILE} was ignored (a pattern matched against each deployment's "
|
|
269
|
+
"environment or workflow name, like \"api-prod\").")
|
|
270
|
+
deploy = ""
|
|
271
|
+
cfg["services"][str(name)] = {"paths": list(paths), "deploy": deploy}
|
|
272
|
+
for key, value in (doc.get("alerts") if isinstance(doc.get("alerts"), dict) else {}).items():
|
|
273
|
+
if key == "channels" and isinstance(value, list) and set(value) <= set(CHANNELS):
|
|
274
|
+
cfg["alerts"]["channels"] = list(value)
|
|
275
|
+
elif key in ALERTS and key != "channels" and _valid(ALERTS[key], value):
|
|
276
|
+
cfg["alerts"][key] = float(value)
|
|
277
|
+
else:
|
|
278
|
+
notes.append(f"alerts.{key} = {value!r} in {FILE} was ignored (thresholds are numbers, 0 turns one off; "
|
|
279
|
+
f"channels are {', '.join(CHANNELS)}).")
|
|
280
|
+
for key, value in (doc.get("automation") if isinstance(doc.get("automation"), dict) else {}).items():
|
|
281
|
+
if key in AUTOMATION and isinstance(value, bool):
|
|
282
|
+
cfg["automation"][key] = value
|
|
283
|
+
else:
|
|
284
|
+
notes.append(f"automation.{key} = {value!r} in {FILE} was ignored (switches are true or false: "
|
|
285
|
+
f"{', '.join(AUTOMATION)}).")
|
|
286
|
+
for key, value in (doc.get("agreements") if isinstance(doc.get("agreements"), dict) else {}).items():
|
|
287
|
+
if key in AGREEMENTS and _valid(AGREEMENTS[key], value):
|
|
288
|
+
cfg["agreements"][key] = value
|
|
289
|
+
else:
|
|
290
|
+
notes.append(f"agreements.{key} = {value!r} in {FILE} was ignored (limits are numbers, 0 turns one off; "
|
|
291
|
+
"linked_issue and review_before_merge are true or false).")
|
|
292
|
+
for key, value in (doc.get("digest") if isinstance(doc.get("digest"), dict) else {}).items():
|
|
293
|
+
ok = ((key in ("enabled", "pulse") and isinstance(value, bool)) or (key == "day" and value in DAYS)
|
|
294
|
+
or (key == "hour" and isinstance(value, int) and not isinstance(value, bool) and 0 <= value <= 23)
|
|
295
|
+
or (key == "channels" and isinstance(value, list) and set(value) <= set(CHANNELS)))
|
|
296
|
+
if ok:
|
|
297
|
+
cfg["digest"][key] = list(value) if key == "channels" else value
|
|
298
|
+
else:
|
|
299
|
+
notes.append(f"digest.{key} = {value!r} in {FILE} was ignored (enabled is true or false, day a weekday name, "
|
|
300
|
+
f"hour 0 to 23 in UTC, channels from {', '.join(CHANNELS)}).")
|
|
301
|
+
deps = doc.get("deps") if isinstance(doc.get("deps"), dict) else {}
|
|
302
|
+
if "outside_lookups" in deps:
|
|
303
|
+
if isinstance(deps["outside_lookups"], bool):
|
|
304
|
+
cfg["deps"]["outside_lookups"] = deps["outside_lookups"]
|
|
305
|
+
else:
|
|
306
|
+
notes.append(f"deps.outside_lookups = {deps['outside_lookups']!r} in {FILE} was ignored (true or false).")
|
|
307
|
+
if "guarddog" in deps:
|
|
308
|
+
if isinstance(deps["guarddog"], bool):
|
|
309
|
+
cfg["deps"]["guarddog"] = deps["guarddog"]
|
|
310
|
+
else:
|
|
311
|
+
notes.append(f"deps.guarddog = {deps['guarddog']!r} in {FILE} was ignored (true or false).")
|
|
312
|
+
for key, value in (deps.get("licenses") if isinstance(deps.get("licenses"), dict) else {}).items():
|
|
313
|
+
if key == "deny" and isinstance(value, list) and all(isinstance(x, str) and x.strip() for x in value):
|
|
314
|
+
cfg["deps"]["licenses"]["deny"] = [x.strip() for x in value]
|
|
315
|
+
else:
|
|
316
|
+
notes.append(f"deps.licenses.{key} = {value!r} in {FILE} was ignored (deny is a list of SPDX licence ids, "
|
|
317
|
+
"* allowed, like [\"GPL-3.0*\", \"AGPL-*\"]).")
|
|
318
|
+
for key, value in (deps.get("policy") if isinstance(deps.get("policy"), dict) else {}).items():
|
|
319
|
+
if key == "min_scorecard" and isinstance(value, (int, float)) and not isinstance(value, bool) and 0 <= value <= 10:
|
|
320
|
+
cfg["deps"]["policy"]["min_scorecard"] = float(value)
|
|
321
|
+
elif key == "max_severity" and isinstance(value, str) and value.lower() in SEVERITIES:
|
|
322
|
+
cfg["deps"]["policy"]["max_severity"] = value.lower()
|
|
323
|
+
else:
|
|
324
|
+
notes.append(f"deps.policy.{key} = {value!r} in {FILE} was ignored (min_scorecard is 0 to 10; max_severity is "
|
|
325
|
+
f"one of {', '.join(SEVERITIES)}).")
|
|
326
|
+
for key, value in (doc.get("benchmarks") if isinstance(doc.get("benchmarks"), dict) else {}).items():
|
|
327
|
+
if key in DEFAULTS["benchmarks"] and isinstance(value, bool):
|
|
328
|
+
cfg["benchmarks"][key] = value
|
|
329
|
+
else:
|
|
330
|
+
notes.append(f"benchmarks.{key} = {value!r} in {FILE} was ignored (share and name are true or false).")
|
|
331
|
+
# What the project says about itself (who maintains it and how much time they have, how it works), shown beside the numbers.
|
|
332
|
+
for key, value in (doc.get("community") if isinstance(doc.get("community"), dict) else {}).items():
|
|
333
|
+
if key in DEFAULTS["community"] and isinstance(value, str):
|
|
334
|
+
cfg["community"][key] = " ".join(value.split())[:STATED_MAX]
|
|
335
|
+
else:
|
|
336
|
+
notes.append(f"community.{key} = {value!r} in {FILE} was ignored (capacity and norms are short texts).")
|
|
337
|
+
investment = doc.get("investment") if isinstance(doc.get("investment"), dict) else {}
|
|
338
|
+
for key in investment:
|
|
339
|
+
if key != "labels":
|
|
340
|
+
notes.append(f"investment.{key} in {FILE} is not a setting StackVitals knows (labels); it was ignored.")
|
|
341
|
+
for name, value in (investment.get("labels") if isinstance(investment.get("labels"), dict) else {}).items():
|
|
342
|
+
if name in INVESTMENT and isinstance(value, list) and all(isinstance(x, str) and x.strip() for x in value):
|
|
343
|
+
cfg["investment"]["labels"][name] = [x.strip() for x in value]
|
|
344
|
+
else:
|
|
345
|
+
notes.append(f"investment.labels.{name} = {value!r} in {FILE} was ignored (a kind of work, one of "
|
|
346
|
+
f"{', '.join(INVESTMENT)}, with a list of label names).")
|
|
347
|
+
for name, values in (doc.get("connectors") if isinstance(doc.get("connectors"), dict) else {}).items():
|
|
348
|
+
if name not in CONNECTORS or not isinstance(values, dict):
|
|
349
|
+
notes.append(f"[connectors.{name}] in {FILE} is not a connector StackVitals knows ({', '.join(CONNECTORS)}); "
|
|
350
|
+
"it was ignored.")
|
|
351
|
+
continue
|
|
352
|
+
for key, value in values.items():
|
|
353
|
+
if key in CONNECTORS[name] and _connector_ok(name, key, value):
|
|
354
|
+
cfg["connectors"][name][key] = (_day(value) if key == "key_expires" else list(value) if isinstance(value, list)
|
|
355
|
+
else value.rstrip("/") if key == "url" else value)
|
|
356
|
+
else:
|
|
357
|
+
notes.append(f"connectors.{name}.{key} = {value!r} in {FILE} was ignored ({CONNECTOR_HELP[name]}).")
|
|
358
|
+
quarantine = ci.get("quarantine") if isinstance(ci.get("quarantine"), dict) else {}
|
|
359
|
+
for key in quarantine:
|
|
360
|
+
if key not in ("always", "never", "max_days"):
|
|
361
|
+
notes.append(f"ci.quarantine.{key} in {FILE} is not a setting StackVitals knows (always, never, max_days); it was ignored.")
|
|
362
|
+
days = quarantine.get("max_days", 0)
|
|
363
|
+
if isinstance(days, int) and not isinstance(days, bool) and 0 <= days <= 365:
|
|
364
|
+
cfg["ci"]["quarantine"]["max_days"] = days
|
|
365
|
+
else:
|
|
366
|
+
notes.append(f"ci.quarantine.max_days in {FILE} must be a whole number of days (0 to 365); it was ignored.")
|
|
367
|
+
for key in ("always", "never"):
|
|
368
|
+
items = quarantine.get(key) or []
|
|
369
|
+
if isinstance(items, list) and all(isinstance(x, str) for x in items):
|
|
370
|
+
cfg["ci"]["quarantine"][key] = list(items)
|
|
371
|
+
else:
|
|
372
|
+
notes.append(f"ci.quarantine.{key} in {FILE} must be a list of test ids; it was ignored.")
|
|
373
|
+
return cfg, notes
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""What leaves a repo, where it goes and what switches it off: docs/privacy.md is built from this list, and the hosts
|
|
2
|
+
come from the code's own constants, so a test can check that every outside host in the code has its line here."""
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
from stackvitals.ci import triage
|
|
6
|
+
from stackvitals.ci.bench import BENCH_REPO, PUBLIC_HOME
|
|
7
|
+
from stackvitals.ci.gitlab import API as GITLAB
|
|
8
|
+
from stackvitals.connectors import incident_io, linear, pagerduty
|
|
9
|
+
from stackvitals.deps import lookalike, release, runtimes, supply, vulns
|
|
10
|
+
from stackvitals.portal.inventory import SCORECARD_API
|
|
11
|
+
from stackvitals.connectors import sonar
|
|
12
|
+
from stackvitals.flow import code_hosts
|
|
13
|
+
from stackvitals.security.repo_checks import BADGE_API
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def host(url: str) -> str:
|
|
17
|
+
return urlparse(url).netloc
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# feature, hosts, what is sent, when it runs, how to turn it off
|
|
21
|
+
FLOWS = [
|
|
22
|
+
{"feature": "Every module (reading your repo, writing its results)", "hosts": ["api.github.com", "github.com"],
|
|
23
|
+
"sends": "Requests with the workflow's own token: it reads runs, logs, pull requests, issues and files of this repo, "
|
|
24
|
+
"and writes the stackvitals-data branch, Pages, and the comments, labels and issues your switches allow.",
|
|
25
|
+
"when": "every run", "off": "Stays inside GitHub; remove a module from `modules` to stop it."},
|
|
26
|
+
{"feature": "Delivery from Bitbucket or Azure DevOps (command line)", "hosts": [host(code_hosts.BITBUCKET), host(code_hosts.AZURE)],
|
|
27
|
+
"sends": "Read-only requests for the repo you name, with BITBUCKET_TOKEN or AZURE_DEVOPS_TOKEN when set; nothing about a GitHub repo.",
|
|
28
|
+
"when": "only when you run `stackvitals delivery --bitbucket` or `--azure`", "off": "Don't run it."},
|
|
29
|
+
{"feature": "SonarQube or SonarCloud code quality", "hosts": ["sonarcloud.io"],
|
|
30
|
+
"sends": "The Sonar project key in [connectors.sonar] (and the SONAR_TOKEN when set), to read its quality gate and measures.",
|
|
31
|
+
"when": "once a day, when a project is set", "off": "Leave project under [connectors.sonar] empty."},
|
|
32
|
+
{"feature": "Security checks: the OpenSSF Best Practices badge", "hosts": [host(BADGE_API)],
|
|
33
|
+
"sends": "A public repo's address, to look up its badge level; a private repo's is never sent.",
|
|
34
|
+
"when": "once a week", "off": "Remove `deps` from `modules`."},
|
|
35
|
+
{"feature": "Benchmarks from public repos", "hosts": ["github.com"],
|
|
36
|
+
"sends": f"A download of the crawl's benchmark files from {PUBLIC_HOME or BENCH_REPO}'s releases; nothing about your repo is sent.",
|
|
37
|
+
"when": "every run, cached a day", "off": "Nothing to turn off: it is a download."},
|
|
38
|
+
{"feature": "Shared benchmark figures (opt-in)", "hosts": ["github.com"],
|
|
39
|
+
"sends": f"An issue on {BENCH_REPO}, filed by a person from a link in the run summary: this repo's rounded CI figures, "
|
|
40
|
+
"its language and star tier, a random id kept in the data branch, and its name only with `name = true`. It is "
|
|
41
|
+
"public once filed, and shows the account that filed it.",
|
|
42
|
+
"when": "only when someone files the link", "off": "`[benchmarks] share = false` (the default); close the issue to withdraw it."},
|
|
43
|
+
{"feature": "Dependency licences and Scorecards", "hosts": [host(supply.DEPS_DEV), host(supply.SCORECARD)],
|
|
44
|
+
"sends": "The name and version of every package in the dependency graph (to deps.dev; that includes the names of "
|
|
45
|
+
"private or internal packages the graph lists) and the public source repos behind them (to OpenSSF "
|
|
46
|
+
"Scorecard); never this repo's name.",
|
|
47
|
+
"when": "weekly, Dependencies module", "off": "`[deps] outside_lookups = false`."},
|
|
48
|
+
{"feature": "Known vulnerabilities in every lockfile",
|
|
49
|
+
"hosts": [host(vulns.RELEASES), "api.osv.dev", "api.deps.dev", "osv-vulnerabilities.storage.googleapis.com"],
|
|
50
|
+
"sends": "A download of the pinned OSV-Scanner from its GitHub release (checked against its SHA-256); then the name "
|
|
51
|
+
"and version of every package in the repo's lockfiles to OSV (and, for manifests without exact versions, to "
|
|
52
|
+
"deps.dev to resolve them). With outside lookups off, OSV's public databases are downloaded instead and no "
|
|
53
|
+
"package name is sent.",
|
|
54
|
+
"when": "after CI runs and on the schedule, at most every 6 hours unless a lockfile changed; Dependencies module",
|
|
55
|
+
"off": "`[deps] outside_lookups = false` (scans offline)."},
|
|
56
|
+
{"feature": "Release checks on changed packages", "hosts": [host(release.NPM), host(release.PYPI), host(release.DEPS_DEV)],
|
|
57
|
+
"sends": "The name and the old and new versions of each npm or PyPI package a pull request adds or upgrades (at most "
|
|
58
|
+
"30), to read when it was released, its install scripts, dependencies and build provenance.",
|
|
59
|
+
"when": "pull requests that change a lockfile", "off": "`[deps] outside_lookups = false`."},
|
|
60
|
+
{"feature": "Runtime versions", "hosts": [host(runtimes.EOL_API)],
|
|
61
|
+
"sends": "Only the runtime's name (nodejs, python, php, ruby or go), to read its release lines and support dates; "
|
|
62
|
+
"never the repo or its versions.",
|
|
63
|
+
"when": "weekly, Dependencies module", "off": "`[deps] outside_lookups = false`."},
|
|
64
|
+
{"feature": "GuardDog code check (opt-in)", "hosts": ["pypi.org", "files.pythonhosted.org", "registry.npmjs.org"],
|
|
65
|
+
"sends": "Downloads of GuardDog itself and of each npm or PyPI package version a pull request adds or upgrades (at "
|
|
66
|
+
"most 10), whose code it reads on the runner; nothing about the repo is sent.",
|
|
67
|
+
"when": "pull requests that change a lockfile, only with `[deps] guarddog = true`", "off": "Off by default."},
|
|
68
|
+
{"feature": "History in a bucket (opt-in)", "hosts": ["the s3-endpoint you set"],
|
|
69
|
+
"sends": "Every file of the data branch (history, pages, decisions), signed with your bucket key, to your own bucket.",
|
|
70
|
+
"when": "every run, only with `storage: s3://...`", "off": "The default, `storage: git`."},
|
|
71
|
+
{"feature": "Look-alike package check", "hosts": [host(lookalike.NPM_STATS), host(lookalike.PYPI_STATS)],
|
|
72
|
+
"sends": "Only the name of a package a pull request adds whose name is one edit from a popular one, to read its "
|
|
73
|
+
"weekly downloads.",
|
|
74
|
+
"when": "pull requests that change a lockfile", "off": "`[deps] outside_lookups = false` (names are still checked)."},
|
|
75
|
+
{"feature": "Model triage of unexplained CI failures",
|
|
76
|
+
"hosts": sorted({host(p["url"]) for p in triage.PROVIDERS.values()}) + ["the `url` you set under [ci.triage]"],
|
|
77
|
+
"sends": "The failing step's log excerpt (bounded) and the changed files' names, with your key, to the provider you "
|
|
78
|
+
"choose or the endpoint you set; at most 3 calls a run.",
|
|
79
|
+
"when": "off until `[ci.triage]` and a key are set; a private repo also needs `allow_private = true`",
|
|
80
|
+
"off": "Off by default."},
|
|
81
|
+
{"feature": "Issue trackers and incident tools",
|
|
82
|
+
"hosts": ["*.atlassian.net", host(linear.URL), host(pagerduty.URL), host(incident_io.URL)],
|
|
83
|
+
"sends": "Requests with your key that read issues, sprints and incidents; with write-back on, a comment linking the "
|
|
84
|
+
"merged pull request (and a status move).",
|
|
85
|
+
"when": "only with a key in the workflow; write-back only with `write_back = true`", "off": "Remove the key."},
|
|
86
|
+
{"feature": "A Discourse forum", "hosts": ["your forum's host"],
|
|
87
|
+
"sends": "Read requests for its public topics, with no key; only times and flags are kept, never usernames or text.",
|
|
88
|
+
"when": "only with url under [connectors.discourse]", "off": "Remove the url."},
|
|
89
|
+
{"feature": "Slack and Discord support channels", "hosts": ["slack.com", "discord.com"],
|
|
90
|
+
"sends": "Read requests with your bot token for the channels you name; only times and flags are kept, never text or "
|
|
91
|
+
"user ids.", "when": "only with a slack-token or discord-token", "off": "Remove the token."},
|
|
92
|
+
{"feature": "Copilot usage reports", "hosts": ["api.github.com", "the report file host GitHub names"],
|
|
93
|
+
"sends": "Requests with the reports token; the report file is fetched without it.",
|
|
94
|
+
"when": "only with the Copilot token", "off": "Remove the token."},
|
|
95
|
+
{"feature": "AI spend", "hosts": ["api.anthropic.com", "api.openai.com", "api.cursor.com"],
|
|
96
|
+
"sends": "Read-only cost and usage report requests, each with that provider's own admin key; nothing about the repo.",
|
|
97
|
+
"when": "only with an AI spend admin key", "off": "Remove the key."},
|
|
98
|
+
{"feature": "Chat alerts and the weekly digest", "hosts": ["your webhook's host (Slack, Teams, Discord, other)"],
|
|
99
|
+
"sends": "Alert and digest text: metric names and values, test names, this repo's name, links to its pages.",
|
|
100
|
+
"when": "only with a webhook secret", "off": "Remove the webhook."},
|
|
101
|
+
{"feature": "Test connection", "hosts": ["each connector's own host (above)", "your webhook's host", "the s3-endpoint you set",
|
|
102
|
+
"your model provider", "your GitLab host (`CI_API_V4_URL`)"],
|
|
103
|
+
"sends": "One cheap call per connector asked for, with its own key: it reads one item, never writes to a tracker. A "
|
|
104
|
+
"chat webhook gets one labelled test message (\"StackVitals connection test\"), never retried; a bucket gets "
|
|
105
|
+
"one small object (`.stackvitals-check`) written and deleted under the prefix; model triage calls the "
|
|
106
|
+
"provider's model list; GitLab reads the project and the token's own scopes, at `CI_API_V4_URL` for a "
|
|
107
|
+
"self-hosted GitLab. The results go in docs/connectors.json; on a public repo without account, channel, "
|
|
108
|
+
"team or bucket names.",
|
|
109
|
+
"when": "only when someone with write access presses Test connection, comments `/stackvitals check`, or runs "
|
|
110
|
+
"`stackvitals connectors check`", "off": "Don't run it."},
|
|
111
|
+
{"feature": "Exports (DevLake, CTRF, Postgres, OpenTelemetry)", "hosts": ["api.github.com"],
|
|
112
|
+
"sends": "Reads with your token; the files are written on your machine and sent nowhere.",
|
|
113
|
+
"when": "only when you run `stackvitals export`", "off": "Don't run it."},
|
|
114
|
+
{"feature": "Portal Scorecard checks", "hosts": [host(SCORECARD_API)],
|
|
115
|
+
"sends": "The names of the organisation's public repos; private repos are never sent.",
|
|
116
|
+
"when": "weekly, Portal module", "off": "Leave the Portal module out."},
|
|
117
|
+
{"feature": "GitLab CI", "hosts": [host(GITLAB)],
|
|
118
|
+
"sends": "Requests with your GitLab token for one project's pipelines and jobs. The GitLab job also writes the "
|
|
119
|
+
"project's stackvitals-data branch, its Pages and one merge request note, and downloads StackVitals from "
|
|
120
|
+
"github.com and its packages from PyPI.",
|
|
121
|
+
"when": "when you run `stackvitals ci gitlab`, or each pipeline once you include the GitLab job",
|
|
122
|
+
"off": "Don't run or include it."},
|
|
123
|
+
{"feature": "The app in a viewer's browser",
|
|
124
|
+
"hosts": ["your Pages site", "api.github.com", "raw.githubusercontent.com", "avatars.githubusercontent.com",
|
|
125
|
+
"api.linear.app", "your sign-in relay (optional)", "cdn.jsdelivr.net", "cdnjs.cloudflare.com"],
|
|
126
|
+
"sends": "The viewer's browser loads the app from your Pages site (its code, font and docs are all there). With a "
|
|
127
|
+
"GitHub token or sign-in, the app calls api.github.com with it to read repos and data branches, set up the "
|
|
128
|
+
"workflow, save settings and decisions, and save secrets sealed in the browser; workflow files and data "
|
|
129
|
+
"are also read from raw.githubusercontent.com, and the account picture from avatars.githubusercontent.com. "
|
|
130
|
+
"Testing a Linear key from the page sends it only to api.linear.app. Sign in with GitHub, when the site was "
|
|
131
|
+
"built with a relay, sends the one-time code (and later the refresh token) to that relay, which swaps it "
|
|
132
|
+
"for a token and keeps nothing. Explore loads DuckDB from jsDelivr; printable pages load Chart.js from "
|
|
133
|
+
"cdnjs. Nothing goes to a server of ours.",
|
|
134
|
+
"when": "when someone opens the app", "off": "Keep Pages private, or don't publish them; don't connect GitHub in the app."},
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
# Hosts the code names only as links people open or as examples; nothing is sent to them by a run.
|
|
138
|
+
LINKS = {"docs.github.com", "linear.new", "linear.app", "app.incident.io", "support.pagerduty.com", "id.atlassian.com",
|
|
139
|
+
"api.slack.com", "hooks.slack.com", "pkg.go.dev", "acme.atlassian.net", "your-site.atlassian.net",
|
|
140
|
+
"your-endpoint.example.com", "chaoss.community", "discuss.example.org", "osv.dev", "json.schemastore.org",
|
|
141
|
+
"aistudio.google.com", "docs.astral.sh", "owner.github.io"}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def hosts() -> set[str]:
|
|
145
|
+
return {h for f in FLOWS for h in f["hosts"]}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def markdown() -> str:
|
|
149
|
+
lines = ["# What leaves your repo", "",
|
|
150
|
+
"StackVitals runs in your own workflow and keeps its history in your repo. There is no StackVitals server: "
|
|
151
|
+
"nothing is sent to us. This page lists everything a run (or the app) sends anywhere, built from the code's "
|
|
152
|
+
"own list, and a test fails if the code gains an outside host this page doesn't name.", "",
|
|
153
|
+
"| Feature | Goes to | What is sent | When | To turn it off |", "|---|---|---|---|---|"]
|
|
154
|
+
for f in FLOWS:
|
|
155
|
+
lines.append(f"| {f['feature']} | {', '.join(f['hosts'])} | {f['sends']} | {f['when']} | {f['off']} |")
|
|
156
|
+
lines += ["", "## Nothing leaves GitHub", "",
|
|
157
|
+
"Leave model triage, connectors and webhooks unset (they are, by default), and "
|
|
158
|
+
"set `[deps] outside_lookups = false`. Runs then talk to GitHub only: your repo, and the benchmark download "
|
|
159
|
+
"from public releases. A private repo's Pages are private on plans that allow it; otherwise publish none and "
|
|
160
|
+
"read the job summary and the data branch.", "",
|
|
161
|
+
"## What each token can reach", "",
|
|
162
|
+
"- **The workflow token** (`GITHUB_TOKEN`): this repo only, with the permissions the workflow file grants.",
|
|
163
|
+
"- **An optional personal or App token** (`stackvitals-token`): whatever it was granted; the Portal's App is "
|
|
164
|
+
"read-only.",
|
|
165
|
+
"- **Tracker and incident keys:** only their own service's host; the Jira key goes only to an "
|
|
166
|
+
"`*.atlassian.net` site.",
|
|
167
|
+
"- **A model key:** its provider's host, or the `url` you set under `[ci.triage]`.", ""]
|
|
168
|
+
return "\n".join(lines)
|