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,147 @@
|
|
|
1
|
+
"""`stackvitals delivery owner/repo`: cycle time phases, DORA, stuck pull requests and sizes for any repo, from the repo's
|
|
2
|
+
published file when it runs StackVitals, else from its pull requests and CI runs read with your token."""
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from mergescout.github.graphql import GraphQLError
|
|
10
|
+
from stackvitals.ci import cli as ci_cli
|
|
11
|
+
from stackvitals.ci.api import APIError, OutOfBudget
|
|
12
|
+
from stackvitals.core import services
|
|
13
|
+
from stackvitals.core.delivery_report import NAMES, _h
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main(argv: list[str] | None = None, tok: str | None = None) -> int:
|
|
17
|
+
parser = argparse.ArgumentParser(prog="stackvitals delivery", description="Cycle time, DORA and stuck pull requests.")
|
|
18
|
+
parser.add_argument("repo", help="owner/name")
|
|
19
|
+
parser.add_argument("--days", type=int, default=90, help="history to read and measure (max 90 for CI runs)")
|
|
20
|
+
parser.add_argument("--json", action="store_true")
|
|
21
|
+
parser.add_argument("--deploys", action="store_true",
|
|
22
|
+
help="the deploy dry run: which deployments count, which workflows and environments don't, and why")
|
|
23
|
+
parser.add_argument("--gitlab", action="store_true", help="a GitLab project (group/project): merge requests and deployments")
|
|
24
|
+
parser.add_argument("--bitbucket", action="store_true", help="a Bitbucket Cloud repo (workspace/repo): pull requests and deployments")
|
|
25
|
+
parser.add_argument("--azure", action="store_true", help="an Azure DevOps repo (org/project/repo): pull requests")
|
|
26
|
+
args = parser.parse_args(argv)
|
|
27
|
+
if args.gitlab or args.bitbucket or args.azure:
|
|
28
|
+
host = "gitlab" if args.gitlab else "bitbucket" if args.bitbucket else "azure"
|
|
29
|
+
out, source = (_gitlab if host == "gitlab" else _host)(args.repo, max(1, min(args.days, 90)), *([] if host == "gitlab" else [host]))
|
|
30
|
+
if out is None:
|
|
31
|
+
return 1
|
|
32
|
+
return _show(args, out, source)
|
|
33
|
+
tok = tok or ci_cli.token()
|
|
34
|
+
if not tok:
|
|
35
|
+
print("A GitHub token is needed. Set GITHUB_TOKEN, or run `gh auth login`.", file=sys.stderr)
|
|
36
|
+
return 1
|
|
37
|
+
try:
|
|
38
|
+
out, source = services.delivery(args.repo, max(1, min(args.days, 90)), tok, ci_cli.home(),
|
|
39
|
+
log=lambda line: print(line, file=sys.stderr, flush=True))
|
|
40
|
+
except (APIError, OutOfBudget, GraphQLError, httpx.HTTPError) as exc:
|
|
41
|
+
print(f"Could not read {args.repo}: {exc}", file=sys.stderr)
|
|
42
|
+
return 1
|
|
43
|
+
return _show(args, out, source)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _gitlab(project: str, days: int):
|
|
47
|
+
from datetime import datetime, timezone
|
|
48
|
+
|
|
49
|
+
from stackvitals.ci.gitlab import GitLab, token
|
|
50
|
+
from stackvitals.core.config import parse
|
|
51
|
+
from stackvitals.core.delivery import build
|
|
52
|
+
from stackvitals.flow import gitlab_mr
|
|
53
|
+
now = datetime.now(timezone.utc)
|
|
54
|
+
try:
|
|
55
|
+
flow, repo = gitlab_mr.collect(GitLab(project, token()), now, days)
|
|
56
|
+
except (APIError, OutOfBudget, httpx.HTTPError) as exc:
|
|
57
|
+
print(f"Could not read {project} on GitLab: {exc}", file=sys.stderr)
|
|
58
|
+
return None, None
|
|
59
|
+
return build(flow, repo, {"runs": []}, [], now, days, parse("")[0]), "GitLab merge requests"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _host(repo: str, days: int, host: str):
|
|
63
|
+
from datetime import datetime, timezone
|
|
64
|
+
|
|
65
|
+
from stackvitals.core.config import parse
|
|
66
|
+
from stackvitals.core.delivery import build
|
|
67
|
+
from stackvitals.flow import code_hosts
|
|
68
|
+
now = datetime.now(timezone.utc)
|
|
69
|
+
name = {"bitbucket": "Bitbucket", "azure": "Azure DevOps"}[host]
|
|
70
|
+
try:
|
|
71
|
+
flow, facts = getattr(code_hosts, host)(repo, now, days)
|
|
72
|
+
except (APIError, OutOfBudget, httpx.HTTPError, ValueError) as exc:
|
|
73
|
+
print(f"Could not read {repo} on {name}: {exc}", file=sys.stderr)
|
|
74
|
+
return None, None
|
|
75
|
+
return build(flow, facts, {"runs": []}, [], now, days, parse("")[0]), f"{name} pull requests"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _show(args, out: dict, source: str) -> int:
|
|
79
|
+
if args.json:
|
|
80
|
+
print(json.dumps({"repo": args.repo, "source": source, "delivery": out}, indent=1))
|
|
81
|
+
return 0
|
|
82
|
+
if args.deploys:
|
|
83
|
+
print(deploy_dry_run(args.repo, out.get("deploy_check") or {}))
|
|
84
|
+
return 0
|
|
85
|
+
d = out["dora"]
|
|
86
|
+
print(f"Delivery in {args.repo}: {out['prs']} merged PRs in {out['window_days']} days ({source}).")
|
|
87
|
+
for name, label in NAMES.items():
|
|
88
|
+
s = out["phases"][name]
|
|
89
|
+
if not s.get("enough", True):
|
|
90
|
+
print(f" {label:20} not enough data (n={s['n']}, needs 10)")
|
|
91
|
+
continue
|
|
92
|
+
print(f" {label:20} P50 {_h(s['p50_h']):>10} P75 {_h(s['p75_h']):>10} n={s['n']}"
|
|
93
|
+
+ (f" ({s['linearb_band']})" if s["linearb_band"] else ""))
|
|
94
|
+
def tier(value):
|
|
95
|
+
return value or "not enough data"
|
|
96
|
+
cfr = "-" if d["change_failure_rate"] is None else f"{d['change_failure_rate']:.0%}"
|
|
97
|
+
print(f"DORA {d['report']} (deployments from {out['deploy_source'] or 'none found'}, {d['deployments']} in the window): "
|
|
98
|
+
f"{d['deployment_days_per_week']} deployment days a week ({tier(d['frequency_tier'])}), lead time "
|
|
99
|
+
f"{_h(d['lead_time_hours'])} ({tier(d['lead_time_tier'])}), change failure rate {cfr} ({tier(d['cfr_tier'])}), "
|
|
100
|
+
f"recovery {_h(d['recovery_hours'])} ({tier(d['recovery_tier'])}).")
|
|
101
|
+
if out.get("deploy_confidence") == "low":
|
|
102
|
+
names = ", ".join(f"{x['name']} ({x['n']})" for x in out.get("deploy_names") or [])
|
|
103
|
+
print(f"Deployments are a guess from workflow names: {names}. If that is not how this repo ships, set "
|
|
104
|
+
"source or deploy_workflow under [delivery] in .github/stackvitals.toml.")
|
|
105
|
+
peers = out.get("peers")
|
|
106
|
+
if peers:
|
|
107
|
+
print(f"Compared with {peers['group']} ({peers['repos']} repos in the daily crawl, measured the crawl's way):")
|
|
108
|
+
for m in peers["metrics"]:
|
|
109
|
+
good = f"; {m['linearb']['good']:.0%} of them inside LinearB's good band" if m.get("linearb") else ""
|
|
110
|
+
print(f" {m['name']}: {_h(m['you'])}, typical {_h(m['typical'])} ({_h(m['low'])}-{_h(m['high'])}), "
|
|
111
|
+
f"{m['place']}{good}")
|
|
112
|
+
for key, items in out["stuck"].items():
|
|
113
|
+
if items:
|
|
114
|
+
print(f"Stuck, {key.replace('_', ' ')}: " + ", ".join(f"#{x['pr']}" for x in items[:15]))
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def deploy_dry_run(repo: str, c: dict) -> str:
|
|
119
|
+
"""The deploy dry run as plain lines."""
|
|
120
|
+
s = c.get("settings") or {}
|
|
121
|
+
out = [f"Deployments in {repo}: {c.get('deployments', 0)} from {c.get('source') or 'no source'} "
|
|
122
|
+
f"([delivery] source = \"{s.get('source')}\", deploy_workflow = \"{s.get('deploy_workflow')}\", "
|
|
123
|
+
f"production = {s.get('production')})."]
|
|
124
|
+
for x in c.get("considered") or []:
|
|
125
|
+
out.append(f" {'counted' if x['used'] else 'passed over':12} {x['source']}: {x['found']}")
|
|
126
|
+
if c.get("order_reason"):
|
|
127
|
+
out.append(f" ({c['order_reason']})")
|
|
128
|
+
flows = c.get("workflows") or []
|
|
129
|
+
if flows:
|
|
130
|
+
out.append("Workflows:")
|
|
131
|
+
for f in flows:
|
|
132
|
+
if not f["kind"]:
|
|
133
|
+
runs = ""
|
|
134
|
+
elif f.get("used"):
|
|
135
|
+
runs = f" ({f['counted']} runs counted, {f['skipped']} skipped: failed, or not a release or the default branch)"
|
|
136
|
+
else:
|
|
137
|
+
runs = f" (passed over: {c['source'] or 'nothing'} is the source used)"
|
|
138
|
+
out.append(f" {f['name']} ({f['path']}): {f['why']}{runs}")
|
|
139
|
+
envs = c.get("environments") or []
|
|
140
|
+
if envs:
|
|
141
|
+
out.append("Environments:")
|
|
142
|
+
out += [f" {e['name']}: {e['deployments']} deployments, {'counted' if e['counted'] else 'not counted'} ({e['as']})"
|
|
143
|
+
for e in envs]
|
|
144
|
+
r = c.get("releases") or {}
|
|
145
|
+
out.append(f"Releases: {r.get('published', 0)} published, {r.get('prereleases_skipped', 0)} prereleases skipped.")
|
|
146
|
+
out.append("To change what counts, set source, deploy_workflow or production under [delivery] in .github/stackvitals.toml.")
|
|
147
|
+
return "\n".join(out)
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
-- Copyright The Apache Software Foundation. Licensed under the Apache License, Version 2.0
|
|
2
|
+
-- (https://www.apache.org/licenses/LICENSE-2.0). From Apache DevLake (https://github.com/apache/incubator-devlake);
|
|
3
|
+
-- changed by StackVitals as described below. See NOTICE.
|
|
4
|
+
-- Apache DevLake's DORA dashboard (grafana/dashboards/DORA.json), its five main panels ported from MySQL to DuckDB.
|
|
5
|
+
-- The CTEs are DevLake's own; only the dialect changed: Grafana's time macros became {{FROM}} and {{TO}}, the project
|
|
6
|
+
-- variable {{PROJECT}}, the report variable {{REPORT}}; DATE() became CAST(... AS date), WEEKDAY() isodow() - 1,
|
|
7
|
+
-- date_format() strftime(), TIMESTAMPDIFF(MINUTE) whole elapsed minutes, double-quoted strings single-quoted.
|
|
8
|
+
|
|
9
|
+
-- panel: deployment_frequency
|
|
10
|
+
with last_few_calendar_months as(
|
|
11
|
+
-- construct the last few calendar months within the selected time period in the top-right corner
|
|
12
|
+
SELECT CAST((TIMESTAMP '{{TO}}' - INTERVAL (H+T+U) DAY) AS date) as day
|
|
13
|
+
FROM ( SELECT 0 H
|
|
14
|
+
UNION ALL SELECT 100 UNION ALL SELECT 200 UNION ALL SELECT 300
|
|
15
|
+
) H CROSS JOIN ( SELECT 0 T
|
|
16
|
+
UNION ALL SELECT 10 UNION ALL SELECT 20 UNION ALL SELECT 30
|
|
17
|
+
UNION ALL SELECT 40 UNION ALL SELECT 50 UNION ALL SELECT 60
|
|
18
|
+
UNION ALL SELECT 70 UNION ALL SELECT 80 UNION ALL SELECT 90
|
|
19
|
+
) T CROSS JOIN ( SELECT 0 U
|
|
20
|
+
UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
|
|
21
|
+
UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6
|
|
22
|
+
UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9
|
|
23
|
+
) U
|
|
24
|
+
WHERE
|
|
25
|
+
(TIMESTAMP '{{TO}}' - INTERVAL (H+T+U) DAY) > TIMESTAMP '{{FROM}}'
|
|
26
|
+
),
|
|
27
|
+
|
|
28
|
+
_production_deployment_days as(
|
|
29
|
+
SELECT
|
|
30
|
+
cdc.cicd_deployment_id as deployment_id,
|
|
31
|
+
max(CAST(cdc.finished_date AS date)) as day
|
|
32
|
+
FROM cicd_deployment_commits cdc
|
|
33
|
+
JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id and pm."table" = 'cicd_scopes'
|
|
34
|
+
WHERE
|
|
35
|
+
pm.project_name in ('{{PROJECT}}')
|
|
36
|
+
and cdc.result = 'SUCCESS'
|
|
37
|
+
and cdc.environment = 'PRODUCTION'
|
|
38
|
+
GROUP BY 1
|
|
39
|
+
),
|
|
40
|
+
|
|
41
|
+
_days_weekly_deploy as(
|
|
42
|
+
SELECT
|
|
43
|
+
CAST(last_few_calendar_months.day - INTERVAL (isodow(last_few_calendar_months.day) - 1) DAY AS date) as week,
|
|
44
|
+
MAX(CASE WHEN _production_deployment_days.day is not null THEN 1 END) as weeks_deployed,
|
|
45
|
+
COUNT(distinct _production_deployment_days.day) as days_deployed
|
|
46
|
+
FROM
|
|
47
|
+
last_few_calendar_months
|
|
48
|
+
LEFT JOIN _production_deployment_days ON _production_deployment_days.day = last_few_calendar_months.day
|
|
49
|
+
GROUP BY week
|
|
50
|
+
),
|
|
51
|
+
|
|
52
|
+
_days_monthly_deploy as(
|
|
53
|
+
SELECT
|
|
54
|
+
CAST(last_few_calendar_months.day - INTERVAL (day(last_few_calendar_months.day) - 1) DAY AS date) as month,
|
|
55
|
+
MAX(CASE WHEN _production_deployment_days.day is not null THEN 1 END) as months_deployed,
|
|
56
|
+
COUNT(distinct _production_deployment_days.day) as days_deployed
|
|
57
|
+
FROM
|
|
58
|
+
last_few_calendar_months
|
|
59
|
+
LEFT JOIN _production_deployment_days ON _production_deployment_days.day = last_few_calendar_months.day
|
|
60
|
+
GROUP BY month
|
|
61
|
+
),
|
|
62
|
+
|
|
63
|
+
_days_six_months_deploy AS (
|
|
64
|
+
SELECT
|
|
65
|
+
month,
|
|
66
|
+
SUM(days_deployed) OVER (
|
|
67
|
+
ORDER BY month
|
|
68
|
+
ROWS BETWEEN 5 PRECEDING AND CURRENT ROW
|
|
69
|
+
) AS days_deployed_per_six_months,
|
|
70
|
+
COUNT(months_deployed) OVER (
|
|
71
|
+
ORDER BY month
|
|
72
|
+
ROWS BETWEEN 5 PRECEDING AND CURRENT ROW
|
|
73
|
+
) AS months_deployed_count,
|
|
74
|
+
ROW_NUMBER() OVER (
|
|
75
|
+
-- MySQL reads DATE_FORMAT(month, '%Y-%m') DIV 6 as the year DIV 6
|
|
76
|
+
PARTITION BY year(month) // 6
|
|
77
|
+
ORDER BY month DESC
|
|
78
|
+
) AS rn
|
|
79
|
+
FROM _days_monthly_deploy
|
|
80
|
+
),
|
|
81
|
+
|
|
82
|
+
_median_number_of_deployment_days_per_week_ranks as(
|
|
83
|
+
SELECT *, percent_rank() over(order by days_deployed) as ranks
|
|
84
|
+
FROM _days_weekly_deploy
|
|
85
|
+
),
|
|
86
|
+
|
|
87
|
+
_median_number_of_deployment_days_per_week as(
|
|
88
|
+
SELECT max(days_deployed) as median_number_of_deployment_days_per_week
|
|
89
|
+
FROM _median_number_of_deployment_days_per_week_ranks
|
|
90
|
+
WHERE ranks <= 0.5
|
|
91
|
+
),
|
|
92
|
+
|
|
93
|
+
_median_number_of_deployment_days_per_month_ranks as(
|
|
94
|
+
SELECT *, percent_rank() over(order by days_deployed) as ranks
|
|
95
|
+
FROM _days_monthly_deploy
|
|
96
|
+
),
|
|
97
|
+
|
|
98
|
+
_median_number_of_deployment_days_per_month as(
|
|
99
|
+
SELECT max(days_deployed) as median_number_of_deployment_days_per_month
|
|
100
|
+
FROM _median_number_of_deployment_days_per_month_ranks
|
|
101
|
+
WHERE ranks <= 0.5
|
|
102
|
+
),
|
|
103
|
+
|
|
104
|
+
_days_per_six_months_deploy_by_filter AS (
|
|
105
|
+
SELECT
|
|
106
|
+
month,
|
|
107
|
+
days_deployed_per_six_months,
|
|
108
|
+
months_deployed_count
|
|
109
|
+
FROM _days_six_months_deploy
|
|
110
|
+
WHERE rn%6 = 1
|
|
111
|
+
),
|
|
112
|
+
|
|
113
|
+
_median_number_of_deployment_days_per_six_months_ranks as(
|
|
114
|
+
SELECT *, percent_rank() over(order by days_deployed_per_six_months) as ranks
|
|
115
|
+
FROM _days_per_six_months_deploy_by_filter
|
|
116
|
+
),
|
|
117
|
+
|
|
118
|
+
_median_number_of_deployment_days_per_six_months as(
|
|
119
|
+
SELECT min(days_deployed_per_six_months) as median_number_of_deployment_days_per_six_months, min(months_deployed_count) as is_collected
|
|
120
|
+
FROM _median_number_of_deployment_days_per_six_months_ranks
|
|
121
|
+
WHERE ranks >= 0.5
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
SELECT
|
|
125
|
+
CASE
|
|
126
|
+
WHEN ('{{REPORT}}') = '2023' THEN
|
|
127
|
+
CASE
|
|
128
|
+
WHEN median_number_of_deployment_days_per_week >= 5 THEN CONCAT(median_number_of_deployment_days_per_week, ' deployment days per week(elite)')
|
|
129
|
+
WHEN median_number_of_deployment_days_per_week >= 1 THEN CONCAT(median_number_of_deployment_days_per_week, ' deployment days per week(high)')
|
|
130
|
+
WHEN median_number_of_deployment_days_per_month >= 1 THEN CONCAT(median_number_of_deployment_days_per_month, ' deployment days per month(medium)')
|
|
131
|
+
WHEN median_number_of_deployment_days_per_month < 1 and is_collected is not null THEN CONCAT(median_number_of_deployment_days_per_month, ' deployment days per month(low)')
|
|
132
|
+
ELSE 'N/A. Please check if you have collected deployments.' END
|
|
133
|
+
WHEN ('{{REPORT}}') = '2021' THEN
|
|
134
|
+
CASE
|
|
135
|
+
WHEN median_number_of_deployment_days_per_week >= 5 THEN CONCAT(median_number_of_deployment_days_per_week, ' deployment days per week(elite)')
|
|
136
|
+
WHEN median_number_of_deployment_days_per_month >= 1 THEN CONCAT(median_number_of_deployment_days_per_month, ' deployment days per month(high)')
|
|
137
|
+
WHEN median_number_of_deployment_days_per_six_months >= 1 THEN CONCAT(median_number_of_deployment_days_per_six_months, ' deployment days per six months(medium)')
|
|
138
|
+
WHEN median_number_of_deployment_days_per_six_months < 1 and is_collected is not null THEN CONCAT(median_number_of_deployment_days_per_six_months, ' deployment days per six months(low)')
|
|
139
|
+
ELSE 'N/A. Please check if you have collected deployments.' END
|
|
140
|
+
ELSE 'Invalid dora report'
|
|
141
|
+
END AS "Deployment Frequency"
|
|
142
|
+
FROM _median_number_of_deployment_days_per_week, _median_number_of_deployment_days_per_month, _median_number_of_deployment_days_per_six_months;
|
|
143
|
+
|
|
144
|
+
-- panel: lead_time
|
|
145
|
+
with _pr_stats as (
|
|
146
|
+
-- get the cycle time of PRs deployed by the deployments finished in the selected period
|
|
147
|
+
SELECT
|
|
148
|
+
distinct pr.id,
|
|
149
|
+
ppm.pr_cycle_time
|
|
150
|
+
FROM
|
|
151
|
+
pull_requests pr
|
|
152
|
+
join project_pr_metrics ppm on ppm.id = pr.id
|
|
153
|
+
join project_mapping pm on pr.base_repo_id = pm.row_id and pm."table" = 'repos'
|
|
154
|
+
join cicd_deployment_commits cdc on ppm.deployment_commit_id = cdc.id
|
|
155
|
+
WHERE
|
|
156
|
+
pm.project_name in ('{{PROJECT}}')
|
|
157
|
+
and pr.merged_date is not null
|
|
158
|
+
and ppm.pr_cycle_time is not null
|
|
159
|
+
and cdc.finished_date BETWEEN TIMESTAMP '{{FROM}}' AND TIMESTAMP '{{TO}}'
|
|
160
|
+
),
|
|
161
|
+
|
|
162
|
+
_median_change_lead_time_ranks as(
|
|
163
|
+
SELECT *, percent_rank() over(order by pr_cycle_time) as ranks
|
|
164
|
+
FROM _pr_stats
|
|
165
|
+
),
|
|
166
|
+
|
|
167
|
+
_median_change_lead_time as(
|
|
168
|
+
-- use median PR cycle time as the median change lead time
|
|
169
|
+
SELECT max(pr_cycle_time) as median_change_lead_time
|
|
170
|
+
FROM _median_change_lead_time_ranks
|
|
171
|
+
WHERE ranks <= 0.5
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
SELECT
|
|
175
|
+
CASE
|
|
176
|
+
WHEN ('{{REPORT}}') = '2023' THEN
|
|
177
|
+
CASE
|
|
178
|
+
WHEN median_change_lead_time < 24 * 60 THEN CONCAT(round(median_change_lead_time/60,1), '(elite)')
|
|
179
|
+
WHEN median_change_lead_time < 7 * 24 * 60 THEN CONCAT(round(median_change_lead_time/60,1), '(high)')
|
|
180
|
+
WHEN median_change_lead_time < 30 * 24 * 60 THEN CONCAT(round(median_change_lead_time/60,1), '(medium)')
|
|
181
|
+
WHEN median_change_lead_time >= 30 * 24 * 60 THEN CONCAT(round(median_change_lead_time/60,1), '(low)')
|
|
182
|
+
ELSE 'N/A. Please check if you have collected deployments/pull_requests.'
|
|
183
|
+
END
|
|
184
|
+
WHEN ('{{REPORT}}') = '2021' THEN
|
|
185
|
+
CASE
|
|
186
|
+
WHEN median_change_lead_time < 60 THEN CONCAT(round(median_change_lead_time/60,1), '(elite)')
|
|
187
|
+
WHEN median_change_lead_time < 7 * 24 * 60 THEN CONCAT(round(median_change_lead_time/60,1), '(high)')
|
|
188
|
+
WHEN median_change_lead_time < 180 * 24 * 60 THEN CONCAT(round(median_change_lead_time/60,1), '(medium)')
|
|
189
|
+
WHEN median_change_lead_time >= 180 * 24 * 60 THEN CONCAT(round(median_change_lead_time/60,1), '(low)')
|
|
190
|
+
ELSE 'N/A. Please check if you have collected deployments/pull_requests.'
|
|
191
|
+
END
|
|
192
|
+
ELSE 'Invalid dora report'
|
|
193
|
+
END AS median_change_lead_time
|
|
194
|
+
FROM _median_change_lead_time;
|
|
195
|
+
|
|
196
|
+
-- panel: change_failure_rate
|
|
197
|
+
with _deployments as (
|
|
198
|
+
SELECT
|
|
199
|
+
cdc.cicd_deployment_id as deployment_id,
|
|
200
|
+
max(cdc.finished_date) as deployment_finished_date
|
|
201
|
+
FROM
|
|
202
|
+
cicd_deployment_commits cdc
|
|
203
|
+
JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id
|
|
204
|
+
and pm."table" = 'cicd_scopes'
|
|
205
|
+
WHERE
|
|
206
|
+
pm.project_name in ('{{PROJECT}}')
|
|
207
|
+
and cdc.result = 'SUCCESS'
|
|
208
|
+
and cdc.environment = 'PRODUCTION'
|
|
209
|
+
GROUP BY
|
|
210
|
+
1
|
|
211
|
+
HAVING
|
|
212
|
+
max(cdc.finished_date) BETWEEN TIMESTAMP '{{FROM}}' AND TIMESTAMP '{{TO}}'
|
|
213
|
+
),
|
|
214
|
+
_failure_caused_by_deployments as (
|
|
215
|
+
-- calculate the number of incidents caused by each deployment
|
|
216
|
+
SELECT
|
|
217
|
+
d.deployment_id,
|
|
218
|
+
d.deployment_finished_date,
|
|
219
|
+
count(
|
|
220
|
+
distinct case
|
|
221
|
+
when i.id is not null then d.deployment_id
|
|
222
|
+
else null
|
|
223
|
+
end
|
|
224
|
+
) as has_incident
|
|
225
|
+
FROM
|
|
226
|
+
_deployments d
|
|
227
|
+
left join project_incident_deployment_relationships pim on d.deployment_id = pim.deployment_id
|
|
228
|
+
left join incidents i on pim.id = i.id
|
|
229
|
+
GROUP BY
|
|
230
|
+
1,
|
|
231
|
+
2
|
|
232
|
+
),
|
|
233
|
+
_change_failure_rate as (
|
|
234
|
+
SELECT
|
|
235
|
+
case
|
|
236
|
+
when count(deployment_id) is null then null
|
|
237
|
+
else sum(has_incident) / count(deployment_id)
|
|
238
|
+
end as change_failure_rate
|
|
239
|
+
FROM
|
|
240
|
+
_failure_caused_by_deployments
|
|
241
|
+
),
|
|
242
|
+
_is_collected_data as(
|
|
243
|
+
SELECT
|
|
244
|
+
CASE
|
|
245
|
+
WHEN COUNT(i.id) = 0
|
|
246
|
+
AND COUNT(cdc.id) = 0 THEN 'No All'
|
|
247
|
+
WHEN COUNT(i.id) = 0 THEN 'No Incidents'
|
|
248
|
+
WHEN COUNT(cdc.id) = 0 THEN 'No Deployments'
|
|
249
|
+
END AS is_collected
|
|
250
|
+
FROM
|
|
251
|
+
(
|
|
252
|
+
SELECT
|
|
253
|
+
1
|
|
254
|
+
) AS dummy
|
|
255
|
+
LEFT JOIN incidents i ON 1 = 1
|
|
256
|
+
LEFT JOIN cicd_deployment_commits cdc ON 1 = 1
|
|
257
|
+
)
|
|
258
|
+
SELECT
|
|
259
|
+
CASE
|
|
260
|
+
WHEN ('{{REPORT}}') = '2023' THEN CASE
|
|
261
|
+
WHEN is_collected = 'No All' THEN 'N/A. Please check if you have collected deployments/incidents.'
|
|
262
|
+
WHEN is_collected = 'No Incidents' THEN 'N/A. Please check if you have collected incidents.'
|
|
263
|
+
WHEN is_collected = 'No Deployments' THEN 'N/A. Please check if you have collected deployments.'
|
|
264
|
+
WHEN change_failure_rate <=.05 THEN CONCAT(round(change_failure_rate * 100, 1), '%(elite)')
|
|
265
|
+
WHEN change_failure_rate <=.10 THEN CONCAT(round(change_failure_rate * 100, 1), '%(high)')
|
|
266
|
+
WHEN change_failure_rate <=.15 THEN CONCAT(round(change_failure_rate * 100, 1), '%(medium)')
|
|
267
|
+
WHEN change_failure_rate >.15 THEN CONCAT(round(change_failure_rate * 100, 1), '%(low)')
|
|
268
|
+
ELSE 'N/A. Please check if you have collected deployments/incidents.'
|
|
269
|
+
END
|
|
270
|
+
WHEN ('{{REPORT}}') = '2021' THEN CASE
|
|
271
|
+
WHEN is_collected = 'No All' THEN 'N/A. Please check if you have collected deployments/incidents.'
|
|
272
|
+
WHEN is_collected = 'No Incidents' THEN 'N/A. Please check if you have collected incidents.'
|
|
273
|
+
WHEN is_collected = 'No Deployments' THEN 'N/A. Please check if you have collected deployments.'
|
|
274
|
+
WHEN change_failure_rate <=.15 THEN CONCAT(round(change_failure_rate * 100, 1), '%(elite)')
|
|
275
|
+
WHEN change_failure_rate <=.20 THEN CONCAT(round(change_failure_rate * 100, 1), '%(high)')
|
|
276
|
+
WHEN change_failure_rate <=.30 THEN CONCAT(round(change_failure_rate * 100, 1), '%(medium)')
|
|
277
|
+
WHEN change_failure_rate >.30 THEN CONCAT(round(change_failure_rate * 100, 1), '%(low)')
|
|
278
|
+
ELSE 'N/A. Please check if you have collected deployments/incidents.'
|
|
279
|
+
END
|
|
280
|
+
ELSE 'No data'
|
|
281
|
+
END AS change_failure_rate
|
|
282
|
+
FROM
|
|
283
|
+
_change_failure_rate,
|
|
284
|
+
_is_collected_data;
|
|
285
|
+
|
|
286
|
+
-- panel: recovery
|
|
287
|
+
with _deployments as (
|
|
288
|
+
SELECT
|
|
289
|
+
cdc.cicd_deployment_id as deployment_id,
|
|
290
|
+
max(cdc.finished_date) as deployment_finished_date
|
|
291
|
+
FROM
|
|
292
|
+
cicd_deployment_commits cdc
|
|
293
|
+
JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id
|
|
294
|
+
and pm."table" = 'cicd_scopes'
|
|
295
|
+
WHERE
|
|
296
|
+
pm.project_name in ('{{PROJECT}}')
|
|
297
|
+
and cdc.result = 'SUCCESS'
|
|
298
|
+
and cdc.environment = 'PRODUCTION'
|
|
299
|
+
GROUP BY
|
|
300
|
+
1
|
|
301
|
+
HAVING
|
|
302
|
+
max(cdc.finished_date) BETWEEN TIMESTAMP '{{FROM}}' AND TIMESTAMP '{{TO}}'
|
|
303
|
+
),
|
|
304
|
+
_incidents_for_deployments as (
|
|
305
|
+
SELECT
|
|
306
|
+
i.id as incident_id,
|
|
307
|
+
i.created_date as incident_create_date,
|
|
308
|
+
i.resolution_date as incident_resolution_date,
|
|
309
|
+
fd.deployment_id as caused_by_deployment,
|
|
310
|
+
fd.deployment_finished_date,
|
|
311
|
+
strftime(fd.deployment_finished_date, '%y/%m') as deployment_finished_month
|
|
312
|
+
FROM
|
|
313
|
+
incidents i
|
|
314
|
+
left join project_incident_deployment_relationships pim on i.id = pim.id
|
|
315
|
+
join _deployments fd on pim.deployment_id = fd.deployment_id
|
|
316
|
+
WHERE
|
|
317
|
+
i.resolution_date BETWEEN TIMESTAMP '{{FROM}}' AND TIMESTAMP '{{TO}}'
|
|
318
|
+
),
|
|
319
|
+
_recovery_time_ranks as (
|
|
320
|
+
SELECT
|
|
321
|
+
*,
|
|
322
|
+
percent_rank() over(
|
|
323
|
+
order by
|
|
324
|
+
trunc((epoch(incident_resolution_date) - epoch(deployment_finished_date)) / 60)
|
|
325
|
+
) as ranks
|
|
326
|
+
FROM
|
|
327
|
+
_incidents_for_deployments
|
|
328
|
+
),
|
|
329
|
+
_median_recovery_time as (
|
|
330
|
+
SELECT
|
|
331
|
+
max(
|
|
332
|
+
CAST(trunc((epoch(incident_resolution_date) - epoch(deployment_finished_date)) / 60) AS BIGINT)
|
|
333
|
+
) as median_recovery_time
|
|
334
|
+
FROM
|
|
335
|
+
_recovery_time_ranks
|
|
336
|
+
WHERE
|
|
337
|
+
ranks <= 0.5
|
|
338
|
+
),
|
|
339
|
+
|
|
340
|
+
_is_collected_data as(
|
|
341
|
+
SELECT
|
|
342
|
+
CASE
|
|
343
|
+
WHEN EXISTS(select COUNT(d.deployment_id) from _deployments) = 0 AND EXISTS(select COUNT(i.incident_id) FROM incidents) = 0 THEN 'No deployments and incidents'
|
|
344
|
+
WHEN EXISTS(select COUNT(d.deployment_id) from _deployments) = 0 THEN 'No Deployments'
|
|
345
|
+
WHEN EXISTS(select COUNT(i.incident_id) FROM incidents) = 0 THEN 'No Incidents'
|
|
346
|
+
Else 'No incidents are mapped to deployments'
|
|
347
|
+
END AS is_collected
|
|
348
|
+
FROM
|
|
349
|
+
_deployments d, _incidents_for_deployments i
|
|
350
|
+
),
|
|
351
|
+
|
|
352
|
+
_metric_recovery_time_2023_report as(
|
|
353
|
+
SELECT
|
|
354
|
+
CASE
|
|
355
|
+
WHEN ('{{REPORT}}') = '2023' THEN CASE
|
|
356
|
+
WHEN is_collected = 'No deployments and incidents' THEN 'N/A. Please check if you have collected deployments and incidents.'
|
|
357
|
+
WHEN is_collected = 'No Deployments' THEN 'N/A. Please check if you have collected deployments.'
|
|
358
|
+
WHEN is_collected = 'No Incidents' THEN 'N/A. Please check if you have collected incidents.'
|
|
359
|
+
WHEN median_recovery_time < 60 THEN CONCAT(round(median_recovery_time / 60, 1), '(elite)')
|
|
360
|
+
WHEN median_recovery_time < 24 * 60 THEN CONCAT(round(median_recovery_time / 60, 1), '(high)')
|
|
361
|
+
WHEN median_recovery_time < 7 * 24 * 60 THEN CONCAT(round(median_recovery_time / 60, 1), '(medium)')
|
|
362
|
+
WHEN median_recovery_time >= 7 * 24 * 60 THEN CONCAT(round(median_recovery_time / 60, 1), '(low)')
|
|
363
|
+
ELSE 'No data'
|
|
364
|
+
END
|
|
365
|
+
END AS median_recovery_time
|
|
366
|
+
FROM
|
|
367
|
+
_median_recovery_time,
|
|
368
|
+
_is_collected_data
|
|
369
|
+
),
|
|
370
|
+
_incidents as (
|
|
371
|
+
-- get the incidents created within the selected time period in the top-right corner
|
|
372
|
+
SELECT
|
|
373
|
+
distinct i.id,
|
|
374
|
+
cast(lead_time_minutes as BIGINT) as lead_time_minutes
|
|
375
|
+
FROM
|
|
376
|
+
incidents i
|
|
377
|
+
join project_mapping pm on i.scope_id = pm.row_id
|
|
378
|
+
and pm."table" = i."table"
|
|
379
|
+
WHERE
|
|
380
|
+
pm.project_name in ('{{PROJECT}}')
|
|
381
|
+
and i.resolution_date BETWEEN TIMESTAMP '{{FROM}}' AND TIMESTAMP '{{TO}}'
|
|
382
|
+
),
|
|
383
|
+
_median_mttr_ranks as(
|
|
384
|
+
SELECT
|
|
385
|
+
*,
|
|
386
|
+
percent_rank() over(
|
|
387
|
+
order by
|
|
388
|
+
lead_time_minutes
|
|
389
|
+
) as ranks
|
|
390
|
+
FROM
|
|
391
|
+
_incidents
|
|
392
|
+
),
|
|
393
|
+
_median_mttr as(
|
|
394
|
+
SELECT
|
|
395
|
+
max(lead_time_minutes) as median_time_to_resolve
|
|
396
|
+
FROM
|
|
397
|
+
_median_mttr_ranks
|
|
398
|
+
WHERE
|
|
399
|
+
ranks <= 0.5
|
|
400
|
+
),
|
|
401
|
+
_metric_mttr_2021_report as(
|
|
402
|
+
SELECT
|
|
403
|
+
CASE
|
|
404
|
+
WHEN ('{{REPORT}}') = '2021' THEN CASE
|
|
405
|
+
WHEN median_time_to_resolve < 60 THEN CONCAT(round(median_time_to_resolve / 60, 1), '(elite)')
|
|
406
|
+
WHEN median_time_to_resolve < 24 * 60 THEN CONCAT(round(median_time_to_resolve / 60, 1), '(high)')
|
|
407
|
+
WHEN median_time_to_resolve < 7 * 24 * 60 THEN CONCAT(
|
|
408
|
+
round(median_time_to_resolve / 60, 1),
|
|
409
|
+
'(medium)'
|
|
410
|
+
)
|
|
411
|
+
WHEN median_time_to_resolve >= 7 * 24 * 60 THEN CONCAT(round(median_time_to_resolve / 60, 1), '(low)')
|
|
412
|
+
ELSE 'N/A. Please check if you have collected incidents.'
|
|
413
|
+
END
|
|
414
|
+
END AS median_time_to_resolve
|
|
415
|
+
FROM
|
|
416
|
+
_median_mttr
|
|
417
|
+
)
|
|
418
|
+
SELECT
|
|
419
|
+
median_recovery_time AS median_time_in_hour
|
|
420
|
+
FROM
|
|
421
|
+
_metric_recovery_time_2023_report
|
|
422
|
+
WHERE
|
|
423
|
+
('{{REPORT}}') = '2023'
|
|
424
|
+
UNION
|
|
425
|
+
SELECT
|
|
426
|
+
median_time_to_resolve AS median_time_to_resolve
|
|
427
|
+
FROM
|
|
428
|
+
_metric_mttr_2021_report
|
|
429
|
+
WHERE
|
|
430
|
+
('{{REPORT}}') = '2021';
|
|
431
|
+
|
|
432
|
+
-- panel: monthly
|
|
433
|
+
with _deployments as(
|
|
434
|
+
SELECT
|
|
435
|
+
strftime(deployment_finished_date,'%y/%m') as month,
|
|
436
|
+
count(cicd_deployment_id) as deployment_count
|
|
437
|
+
FROM (
|
|
438
|
+
SELECT
|
|
439
|
+
cdc.cicd_deployment_id,
|
|
440
|
+
max(cdc.finished_date) as deployment_finished_date
|
|
441
|
+
FROM cicd_deployment_commits cdc
|
|
442
|
+
JOIN project_mapping pm on cdc.cicd_scope_id = pm.row_id and pm."table" = 'cicd_scopes'
|
|
443
|
+
WHERE
|
|
444
|
+
pm.project_name in ('{{PROJECT}}')
|
|
445
|
+
and cdc.result = 'SUCCESS'
|
|
446
|
+
and cdc.environment = 'PRODUCTION'
|
|
447
|
+
GROUP BY 1
|
|
448
|
+
HAVING max(cdc.finished_date) BETWEEN TIMESTAMP '{{FROM}}' AND TIMESTAMP '{{TO}}'
|
|
449
|
+
) _production_deployments
|
|
450
|
+
GROUP BY 1
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
SELECT
|
|
454
|
+
cm.month,
|
|
455
|
+
case when d.deployment_count is null then 0 else d.deployment_count end as "Deployment Count"
|
|
456
|
+
FROM
|
|
457
|
+
calendar_months cm
|
|
458
|
+
LEFT JOIN _deployments d on cm.month = d.month
|
|
459
|
+
WHERE month_timestamp between CAST(date_trunc('month', TIMESTAMP '{{FROM}}') AS date) AND CAST(date_trunc('month', TIMESTAMP '{{TO}}') AS date)
|
|
460
|
+
ORDER BY cm.month_timestamp;
|
|
461
|
+
|
|
462
|
+
-- panel: pr_cycle_time
|
|
463
|
+
-- From DevLake's Engineering Throughput and Cycle Time dashboard ("PR Cycle Time [Merged PRs]", monthly interval).
|
|
464
|
+
with _prs as(
|
|
465
|
+
SELECT
|
|
466
|
+
pr.id,
|
|
467
|
+
pr.merged_date as pr_merged_date,
|
|
468
|
+
-- convert null to 0 if a PR has no cycle_time to make sure cycle_time equals the sum of the four metrics below
|
|
469
|
+
coalesce(prm.pr_cycle_time/60,0) as cycle_time
|
|
470
|
+
FROM pull_requests pr
|
|
471
|
+
join project_pr_metrics prm on pr.id = prm.id
|
|
472
|
+
join project_mapping pm on pr.base_repo_id = pm.row_id and pm."table" = 'repos'
|
|
473
|
+
WHERE
|
|
474
|
+
pr.merged_date BETWEEN TIMESTAMP '{{FROM}}' AND TIMESTAMP '{{TO}}'
|
|
475
|
+
and pr.created_date >= CAST(date_trunc('month', TIMESTAMP '{{FROM}}') AS date) + INTERVAL 1 MONTH
|
|
476
|
+
and pm.project_name in ('{{PROJECT}}')
|
|
477
|
+
GROUP BY 1,2,3
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
SELECT
|
|
481
|
+
CAST(CAST(pr_merged_date AS date) - INTERVAL (day(CAST(pr_merged_date AS date)) - 1) DAY AS date) as time,
|
|
482
|
+
avg(cycle_time) as "PR Cycle Time(h)"
|
|
483
|
+
FROM _prs
|
|
484
|
+
GROUP BY 1
|
|
485
|
+
ORDER BY 1;
|