openlanes 3.0.1 → 3.1.0
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.
- checksums.yaml +4 -4
- data/Brewfile +42 -0
- data/agents/bb8-resolver.md +49 -0
- data/agents/c3po-operator.md +4 -0
- data/agents/chewie-verifier.md +85 -0
- data/agents/hansolo-reviewer.md +20 -15
- data/agents/lando-broker.md +81 -19
- data/agents/leah-researcher.md +6 -6
- data/agents/luke-backend.md +41 -20
- data/agents/{palpatine-planner.md → luthen-rael-planner.md} +15 -13
- data/agents/palpatine-risk-assessor.md +112 -0
- data/agents/r2d2-mechanic.md +5 -1
- data/agents/rey-frontend.md +38 -19
- data/agents/yoda-writer.md +7 -7
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/plan.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/pull-requests.md +10 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/spec.md +5 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/final.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/plan/verification/1/report.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/repo/bin/greet +1 -0
- data/evals/cases/chewie-verifier/verify-medium/fail/run.json +1 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/plan.md +7 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/pull-requests.md +10 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/spec.md +5 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/final.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/report.md +14 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/plan/verification/1/step-01.png +0 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/repo/bin/greet +1 -0
- data/evals/cases/chewie-verifier/verify-medium/pass/run.json +1 -0
- data/evals/cases/chewie-verifier/verify-medium.yml +48 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/implementation-plan.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/pull-requests.md +12 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/lib/cart.rb +10 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/repo/spec/cart_spec.rb +9 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/fail/run.json +1 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/implementation-plan.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/plan.md +3 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/pull-requests.md +15 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/plan/spec.md +5 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/lib/cart.rb +10 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/repo/spec/cart_spec.rb +9 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum/pass/run.json +1 -0
- data/evals/cases/hansolo-reviewer/coverage-minimum.yml +76 -0
- data/evals/cases/luthen-rael-planner/plan-deep/fail/plan/plan.md +8 -0
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan.md +2 -2
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep.yml +1 -1
- data/evals/cases/luthen-rael-planner/plan-fast/fail/plan/plan.md +12 -0
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan.md +2 -2
- data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast.yml +1 -1
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/plan.md +20 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/pull-requests.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/plan/spec.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/fail/run.json +1 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/plan.md +25 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/pull-requests.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/plan/spec.md +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/repo/db/migrate/20261020120000_drop_legacy_email.rb +5 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium/pass/run.json +1 -0
- data/evals/cases/palpatine-risk-assessor/risk-medium.yml +47 -0
- data/evals/cases/yoda-writer/spec-medium/pass/plan/spec.md +1 -1
- data/lib/openlanes/adapters/base.rb +18 -4
- data/lib/openlanes/adapters/capabilities.rb +29 -0
- data/lib/openlanes/adapters/claude.rb +14 -11
- data/lib/openlanes/adapters/codex.rb +9 -0
- data/lib/openlanes/adapters/pi.rb +10 -0
- data/lib/openlanes/adapters/stream.rb +145 -1
- data/lib/openlanes/adapters.rb +7 -0
- data/lib/openlanes/agents/agent.rb +4 -3
- data/lib/openlanes/agents/roster.rb +14 -2
- data/lib/openlanes/agents/routing.rb +43 -3
- data/lib/openlanes/apps/definition.rb +152 -0
- data/lib/openlanes/apps/framing.rb +114 -0
- data/lib/openlanes/apps/listing.rb +60 -0
- data/lib/openlanes/apps/ports.rb +168 -0
- data/lib/openlanes/apps/probe.rb +63 -0
- data/lib/openlanes/apps/process_spec.rb +39 -0
- data/lib/openlanes/apps/runner.rb +174 -0
- data/lib/openlanes/apps/status.rb +62 -0
- data/lib/openlanes/apps/supervisor.rb +277 -0
- data/lib/openlanes/apps/watcher.rb +50 -0
- data/lib/openlanes/apps.rb +41 -0
- data/lib/openlanes/cli/api/api.rb +10 -0
- data/lib/openlanes/cli/api/subcommands/docs.rb +63 -0
- data/lib/openlanes/cli/app/app.rb +31 -0
- data/lib/openlanes/cli/app/subcommands/start.rb +114 -0
- data/lib/openlanes/cli/app/subcommands/status.rb +38 -0
- data/lib/openlanes/cli/app/subcommands/stop.rb +44 -0
- data/lib/openlanes/cli/base.rb +89 -16
- data/lib/openlanes/cli/coverage/coverage.rb +68 -0
- data/lib/openlanes/cli/create/create.rb +11 -265
- data/lib/openlanes/cli/eval/eval.rb +4 -1
- data/lib/openlanes/cli/init/init.rb +39 -2
- data/lib/openlanes/cli/linear/subcommands/import.rb +1 -1
- data/lib/openlanes/cli/linear/subcommands/projects.rb +1 -1
- data/lib/openlanes/cli/list_plans/list_plans.rb +8 -2
- data/lib/openlanes/cli/mail/subcommands/ack.rb +52 -0
- data/lib/openlanes/cli/mail/subcommands/render.rb +31 -0
- data/lib/openlanes/cli/mail/subcommands/send.rb +26 -0
- data/lib/openlanes/cli/resync/subcommands/dirs.rb +53 -12
- data/lib/openlanes/cli/resync/subcommands/prs.rb +8 -69
- data/lib/openlanes/cli/review/review.rb +72 -0
- data/lib/openlanes/cli/run/run.rb +22 -486
- data/lib/openlanes/cli/setup/setup.rb +51 -0
- data/lib/openlanes/cli/start/start.rb +7 -51
- data/lib/openlanes/cli/state/state.rb +1 -1
- data/lib/openlanes/cli/state/subcommands/resume.rb +61 -0
- data/lib/openlanes/cli/state/subcommands/sign.rb +1 -1
- data/lib/openlanes/cli/unblock/unblock.rb +11 -205
- data/lib/openlanes/cli/web/web.rb +84 -8
- data/lib/openlanes/cli/worktree/worktree.rb +1 -1
- data/lib/openlanes/cli/worktree/worktree_prune.rb +77 -0
- data/lib/openlanes/cli.rb +37 -2
- data/lib/openlanes/coverage/cobertura.rb +49 -0
- data/lib/openlanes/coverage/config.rb +194 -0
- data/lib/openlanes/coverage/istanbul.rb +31 -0
- data/lib/openlanes/coverage/lcov.rb +38 -0
- data/lib/openlanes/coverage/report.rb +71 -0
- data/lib/openlanes/coverage/simplecov.rb +70 -0
- data/lib/openlanes/coverage.rb +33 -0
- data/lib/openlanes/engine/agent_locks.rb +231 -0
- data/lib/openlanes/engine/arbitration.rb +25 -0
- data/lib/openlanes/engine/board.rb +36 -5
- data/lib/openlanes/engine/conversions.rb +160 -0
- data/lib/openlanes/engine/coverage_gate.rb +217 -0
- data/lib/openlanes/engine/dispatcher.rb +1175 -117
- data/lib/openlanes/engine/event_journal/replay.rb +96 -0
- data/lib/openlanes/engine/event_journal.rb +148 -0
- data/lib/openlanes/engine/extensions.rb +271 -0
- data/lib/openlanes/engine/merging.rb +459 -0
- data/lib/openlanes/engine/narrator.rb +6 -6
- data/lib/openlanes/engine/plan_locks.rb +114 -0
- data/lib/openlanes/engine/pr_outcomes.rb +161 -0
- data/lib/openlanes/engine/pr_watch.rb +83 -0
- data/lib/openlanes/engine/progress_log.rb +12 -120
- data/lib/openlanes/engine/quality_question.rb +125 -0
- data/lib/openlanes/engine/rebase.rb +200 -0
- data/lib/openlanes/engine/run_workflow.rb +776 -0
- data/lib/openlanes/engine/runner.rb +186 -15
- data/lib/openlanes/engine/start_workflow.rb +77 -0
- data/lib/openlanes/engine/state_file.rb +101 -34
- data/lib/openlanes/engine/tally.rb +1 -1
- data/lib/openlanes/engine/verification/app_session.rb +104 -0
- data/lib/openlanes/engine/verification/driver.rb +314 -0
- data/lib/openlanes/engine/verification/outcome.rb +516 -0
- data/lib/openlanes/engine/verification/report.rb +95 -0
- data/lib/openlanes/engine/verification/round.rb +100 -0
- data/lib/openlanes/engine/verification/screenshots.rb +120 -0
- data/lib/openlanes/engine/verification.rb +57 -0
- data/lib/openlanes/engine.rb +30 -1
- data/lib/openlanes/execution/boundary.rb +212 -0
- data/lib/openlanes/execution/child.rb +4 -2
- data/lib/openlanes/execution/clock.rb +36 -6
- data/lib/openlanes/execution/control.rb +32 -0
- data/lib/openlanes/execution/executor.rb +114 -35
- data/lib/openlanes/execution/process_start.rb +63 -0
- data/lib/openlanes/execution/prompt.rb +223 -34
- data/lib/openlanes/execution/transcript.rb +159 -8
- data/lib/openlanes/execution.rb +21 -0
- data/lib/openlanes/lifecycle/adoption.rb +25 -8
- data/lib/openlanes/lifecycle/aging.rb +166 -0
- data/lib/openlanes/lifecycle/allocator.rb +216 -0
- data/lib/openlanes/lifecycle/brief.rb +32 -2
- data/lib/openlanes/lifecycle/completion.rb +12 -6
- data/lib/openlanes/lifecycle/create_workflow.rb +272 -0
- data/lib/openlanes/lifecycle/creator.rb +17 -13
- data/lib/openlanes/lifecycle/lanes.rb +1 -1
- data/lib/openlanes/lifecycle/resync.rb +170 -42
- data/lib/openlanes/lifecycle/unblock_workflow.rb +234 -0
- data/lib/openlanes/lifecycle/unblocker.rb +76 -2
- data/lib/openlanes/lifecycle.rb +37 -1
- data/lib/openlanes/linear/mapping.rb +4 -1
- data/lib/openlanes/linear/unit.rb +1 -1
- data/lib/openlanes/live/chat.rb +60 -11
- data/lib/openlanes/live/choice_converter.rb +166 -0
- data/lib/openlanes/live/choice_judge.rb +109 -0
- data/lib/openlanes/live/claims.rb +9 -1
- data/lib/openlanes/live/command.rb +13 -10
- data/lib/openlanes/live/commands.rb +217 -30
- data/lib/openlanes/live/copilot/slash.rb +82 -0
- data/lib/openlanes/live/copilot.rb +38 -4
- data/lib/openlanes/live/event_bus/base.rb +52 -0
- data/lib/openlanes/live/event_bus/memory_bus.rb +14 -0
- data/lib/openlanes/live/event_bus/redis_bus.rb +10 -0
- data/lib/openlanes/live/event_bus/transaction.rb +27 -0
- data/lib/openlanes/live/event_bus.rb +29 -0
- data/lib/openlanes/live/interviewer.rb +146 -0
- data/lib/openlanes/live/keys.rb +56 -0
- data/lib/openlanes/live/mail.rb +143 -0
- data/lib/openlanes/live/memory_store.rb +12 -1
- data/lib/openlanes/live/news.rb +61 -0
- data/lib/openlanes/live/node.rb +20 -0
- data/lib/openlanes/live/outage.rb +225 -0
- data/lib/openlanes/live/outage_report.rb +89 -0
- data/lib/openlanes/live/publisher.rb +42 -2
- data/lib/openlanes/live/run_record.rb +39 -1
- data/lib/openlanes/live/run_state.rb +19 -2
- data/lib/openlanes/live/session.rb +31 -3
- data/lib/openlanes/live/snapshot.rb +103 -42
- data/lib/openlanes/live/stations.rb +4 -4
- data/lib/openlanes/live/store.rb +18 -1
- data/lib/openlanes/live/subscriber.rb +22 -2
- data/lib/openlanes/live.rb +66 -1
- data/lib/openlanes/plans/blocked_file.rb +117 -0
- data/lib/openlanes/plans/choices.rb +228 -0
- data/lib/openlanes/plans/dependencies.rb +304 -0
- data/lib/openlanes/plans/feature.rb +75 -16
- data/lib/openlanes/plans/journal.rb +169 -0
- data/lib/openlanes/plans/last_known_state.rb +82 -0
- data/lib/openlanes/plans/lock.rb +171 -0
- data/lib/openlanes/plans/mailbox.rb +145 -4
- data/lib/openlanes/plans/plan_state.rb +590 -9
- data/lib/openlanes/plans/pull_request_machine.rb +163 -0
- data/lib/openlanes/plans/review_label.rb +139 -0
- data/lib/openlanes/plans/spec.rb +239 -3
- data/lib/openlanes/plans/state_machine.rb +63 -30
- data/lib/openlanes/plans/status.rb +60 -11
- data/lib/openlanes/plans/subject.rb +4 -0
- data/lib/openlanes/plans/tree.rb +101 -10
- data/lib/openlanes/plans/verdict.rb +124 -0
- data/lib/openlanes/plans.rb +19 -0
- data/lib/openlanes/presentation/api_docs.rb +76 -0
- data/lib/openlanes/presentation/documentation.rb +105 -12
- data/lib/openlanes/presentation/pr_changes.rb +90 -0
- data/lib/openlanes/presentation/reporter.rb +64 -5
- data/lib/openlanes/presentation.rb +20 -1
- data/lib/openlanes/setup/check.rb +16 -0
- data/lib/openlanes/setup/machine.rb +179 -0
- data/lib/openlanes/setup.rb +34 -0
- data/lib/openlanes/support/clock.rb +57 -0
- data/lib/openlanes/support/config.rb +318 -6
- data/lib/openlanes/support/log.rb +206 -0
- data/lib/openlanes/support/refusal.rb +15 -0
- data/lib/openlanes/support/runtime.rb +61 -0
- data/lib/openlanes/support/spawner.rb +51 -0
- data/lib/openlanes/support/ui.rb +39 -24
- data/lib/openlanes/vcs/ci_gate.rb +31 -0
- data/lib/openlanes/vcs/github.rb +362 -12
- data/lib/openlanes/vcs/label_migration.rb +132 -0
- data/lib/openlanes/vcs/label_sync.rb +75 -0
- data/lib/openlanes/vcs/publisher.rb +226 -8
- data/lib/openlanes/vcs/refresh.rb +337 -0
- data/lib/openlanes/vcs/rename_date.rb +83 -0
- data/lib/openlanes/vcs/review.rb +66 -0
- data/lib/openlanes/vcs/worktree.rb +167 -6
- data/lib/openlanes/vcs.rb +18 -0
- data/lib/openlanes/version.rb +1 -1
- data/lib/openlanes/web/admin_auth.rb +104 -0
- data/lib/openlanes/web/app.rb +115 -16
- data/lib/openlanes/web/context.rb +15 -9
- data/lib/openlanes/web/demo.rb +29 -14
- data/lib/openlanes/web/demo_board.rb +63 -5
- data/lib/openlanes/web/events.rb +86 -7
- data/lib/openlanes/web/launcher.rb +69 -7
- data/lib/openlanes/web/listen.rb +133 -0
- data/lib/openlanes/web/plan_files.rb +136 -0
- data/lib/openlanes/web/request_log.rb +108 -0
- data/lib/openlanes/web/server.rb +29 -10
- data/lib/openlanes/web.rb +6 -4
- data/lib/openlanes.rb +45 -7
- data/openlanes.gemspec +9 -4
- data/schemas/journal-event.schema.json +72 -0
- data/schemas/plan-state.schema.json +163 -2
- data/skills/openlanes/SKILL.md +2 -2
- data/skills/openlanes-documents/SKILL.md +4 -4
- data/skills/openlanes-documents/references/blocked-template.md +2 -2
- data/skills/openlanes-documents/references/closing-templates.md +1 -1
- data/skills/openlanes-documents/references/plan-template.md +12 -4
- data/skills/openlanes-documents/references/retroactive-spec.md +1 -1
- data/skills/openlanes-run/SKILL.md +5 -5
- data/skills/openlanes-spec/SKILL.md +1 -1
- data/skills/openlanes-status/SKILL.md +1 -1
- data/web/dist/assets/{index-BFPI-zXI.js → index-690ayLS4.js} +2 -2
- data/web/dist/assets/index-6ZmW4slI.js +199 -0
- data/web/dist/assets/{index-bpsFIpXE.js → index-B5KzlXp1.js} +2 -2
- data/web/dist/assets/{index-DtA46i32.js → index-BAo_v9bU.js} +2 -2
- data/web/dist/assets/{index-CnJfB0l2.js → index-BDm7g-xG.js} +2 -2
- data/web/dist/assets/{index-DXgeajd6.js → index-BFH9bC7v.js} +2 -2
- data/web/dist/assets/{index-DH0gDlee.js → index-BVHPV7z4.js} +2 -2
- data/web/dist/assets/{index-DaSjQ-H4.js → index-B_CPqjnA.js} +2 -2
- data/web/dist/assets/{index-Dbxvk-EC.js → index-BhwBH5ct.js} +2 -2
- data/web/dist/assets/{index-6MZkmh3k.js → index-Bq5AdwEt.js} +2 -2
- data/web/dist/assets/{index-B5CKJPm-.js → index-CAW6ClM2.js} +2 -2
- data/web/dist/assets/index-Cp86u27u.css +1 -0
- data/web/dist/assets/{index-CnvkZu5Y.js → index-Culm4pMO.js} +2 -2
- data/web/dist/assets/{index-IqD8mBos.js → index-DElDJfp1.js} +2 -2
- data/web/dist/assets/{index-C3zc4gFe.js → index-Dja7osNW.js} +2 -2
- data/web/dist/assets/{index-C2jwoWVV.js → index-DjhU3FzU.js} +2 -2
- data/web/dist/assets/{index-pvJi-JJO.js → index-Dutbxj4o.js} +2 -2
- data/web/dist/assets/{index-BF6QYroq.js → index-RkDhPMDt.js} +2 -2
- data/web/dist/assets/{index-DbueqOK5.js → index-_mptZjlC.js} +2 -2
- data/web/dist/index.html +2 -2
- data/web/src/api/fixtures/board.json +203 -28
- metadata +1053 -889
- data/bin/factory-e2e +0 -157
- data/bin/plan-number +0 -164
- data/bin/setup +0 -26
- data/evals/cases/palpatine-planner/plan-deep/fail/plan/plan.md +0 -8
- data/evals/cases/palpatine-planner/plan-fast/fail/plan/plan.md +0 -12
- data/web/dist/assets/index-BYJCWlk0.js +0 -198
- data/web/dist/assets/index-CjInBi5X.css +0 -1
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/fail/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-deep/pass/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/fail/run.json +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-backend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/plan-frontend.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/plan/spec.md +0 -0
- /data/evals/cases/{palpatine-planner → luthen-rael-planner}/plan-fast/pass/run.json +0 -0
- /data/exe/{depot → ol} +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
module OpenLanes
|
|
7
|
+
module Engine
|
|
8
|
+
# Decides whether a pull request's checkout has earned `ready-to-merge` on
|
|
9
|
+
# coverage (decision M15): runs the test command the checkout's
|
|
10
|
+
# `.openlanes.yml` declares under `coverage:`, reads the line figure (from
|
|
11
|
+
# the report it wrote, or from what `report_command` prints), and compares
|
|
12
|
+
# it with the declared minimum.
|
|
13
|
+
#
|
|
14
|
+
# A checkout with no `coverage:` section withholds too, but as
|
|
15
|
+
# {Verdict#unconfigured?}: nothing was measured, so nothing failed, and
|
|
16
|
+
# the verifier sends the pull request back for the section rather than
|
|
17
|
+
# for a defect.
|
|
18
|
+
#
|
|
19
|
+
# Everything that is not a clean, fresh, readable report at or above the
|
|
20
|
+
# minimum withholds, each with its reason. In particular the command's
|
|
21
|
+
# own verdict comes first: a suite that fails can still leave a report
|
|
22
|
+
# behind, and a figure from a red run is not one to merge on.
|
|
23
|
+
#
|
|
24
|
+
# Nothing applies `ready-to-merge` yet. The verifier (plan 014) or
|
|
25
|
+
# `Vcs::Review` (plan 005), whichever lands second, asks this gate before
|
|
26
|
+
# setting the label and refuses with {Verdict#reason}.
|
|
27
|
+
#
|
|
28
|
+
# Running a project's command is arbitrary code, so it runs only in the
|
|
29
|
+
# checkout it is handed, never the main tree, and through the same
|
|
30
|
+
# {Execution::Child} the executor uses: its own process group, the
|
|
31
|
+
# environment Bundler was loaded under, and every process reaped when the
|
|
32
|
+
# harness exits.
|
|
33
|
+
class CoverageGate
|
|
34
|
+
# Seconds a test command may run before it is stopped and counted as 0%.
|
|
35
|
+
TIMEOUT = 1800
|
|
36
|
+
|
|
37
|
+
# Lines of the command's output a withheld verdict keeps.
|
|
38
|
+
TAIL = 20
|
|
39
|
+
|
|
40
|
+
# @!attribute [r] passed
|
|
41
|
+
# @return [Boolean]
|
|
42
|
+
# @!attribute [r] percent
|
|
43
|
+
# @return [Float] the line figure the decision used; 0.0 when the
|
|
44
|
+
# report said nothing or the command failed
|
|
45
|
+
# @!attribute [r] minimum
|
|
46
|
+
# @return [Float, nil] nil when the project declares no coverage
|
|
47
|
+
# @!attribute [r] reason
|
|
48
|
+
# @return [String] one sentence, the figure and the minimum included,
|
|
49
|
+
# fit for a refusal or a PR comment
|
|
50
|
+
# @!attribute [r] config
|
|
51
|
+
# @return [OpenLanes::Coverage::Config, nil]
|
|
52
|
+
# @!attribute [r] output
|
|
53
|
+
# @return [Array<String>] the command's last {TAIL} lines; empty when
|
|
54
|
+
# it never ran
|
|
55
|
+
# @!attribute [r] unconfigured
|
|
56
|
+
# @return [Boolean] withheld only because the checkout's
|
|
57
|
+
# `.openlanes.yml` has no `coverage:` section
|
|
58
|
+
Verdict = Data.define(:passed, :percent, :minimum, :reason, :config, :output, :unconfigured) do
|
|
59
|
+
def initialize(percent: 0.0, minimum: nil, config: nil, output: [], unconfigured: false, **rest) = super
|
|
60
|
+
|
|
61
|
+
alias_method :passed?, :passed
|
|
62
|
+
alias_method :unconfigured?, :unconfigured
|
|
63
|
+
|
|
64
|
+
# @return [Boolean]
|
|
65
|
+
def withheld? = !passed
|
|
66
|
+
|
|
67
|
+
# @return [String]
|
|
68
|
+
def to_s = passed ? reason : "#{Plans::ReviewLabel::READY_TO_MERGE} withheld: #{reason}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# What one run of the command left behind.
|
|
72
|
+
Run = Data.define(:status, :timed_out, :output, :started)
|
|
73
|
+
|
|
74
|
+
# @param timeout [Numeric] seconds, see {TIMEOUT}
|
|
75
|
+
# @param spawner [#spawn] {Execution::Child}, or a stand-in answering
|
|
76
|
+
# `spawn(argv, chdir:)` with something that has `each_chunk`, `wait`
|
|
77
|
+
# and `terminate`
|
|
78
|
+
def initialize(timeout: TIMEOUT, spawner: Execution::Child)
|
|
79
|
+
@timeout = timeout
|
|
80
|
+
@spawner = spawner
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @param checkout [#path, #dirty?] the pull request's checkout, such as
|
|
84
|
+
# a {Vcs::Worktree::Checkout}
|
|
85
|
+
# @return [Verdict]
|
|
86
|
+
def call(checkout:)
|
|
87
|
+
root = checkout.path
|
|
88
|
+
return withhold("#{root} has uncommitted changes; coverage is measured only in a clean checkout") if checkout.dirty?
|
|
89
|
+
|
|
90
|
+
config = Coverage.config(root)
|
|
91
|
+
unless config
|
|
92
|
+
return Verdict.new(passed: false,
|
|
93
|
+
unconfigured: true,
|
|
94
|
+
reason: "#{OpenLanes::Config::FILE} in #{root} has no #{Coverage::Config::SECTION}: section, so nothing was measured")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
judge(config, root, run(config.command, root))
|
|
98
|
+
rescue Coverage::Invalid => e
|
|
99
|
+
withhold(e.message)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Floors exactly. Flooring `percent * 100` as a Float printed 10.03 as
|
|
103
|
+
# 10.02%, because the product is 1002.9999999999999; a Float is read
|
|
104
|
+
# back as the shortest decimal it prints as, which is the one it meant.
|
|
105
|
+
#
|
|
106
|
+
# @param percent [Rational, Float] {Coverage::Report#ratio} where there is one
|
|
107
|
+
# @return [String] floored to two decimals, so 94.995 never prints as
|
|
108
|
+
# the 95.00 it did not reach
|
|
109
|
+
def self.figure(percent)
|
|
110
|
+
exact = percent.is_a?(Float) ? Rational(percent.to_s) : percent.to_r
|
|
111
|
+
format("%.2f%%", exact.floor(2))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
# @return [Verdict]
|
|
117
|
+
def judge(config, root, run)
|
|
118
|
+
source = config.source
|
|
119
|
+
failed = failure(config, root, run)
|
|
120
|
+
return withhold("coverage counts as 0%: #{failed}", config:, run:) if failed
|
|
121
|
+
|
|
122
|
+
report = report_of(config, root)
|
|
123
|
+
return withhold("coverage counts as 0%: #{report.problem}", config:, run:) unless report.read?
|
|
124
|
+
|
|
125
|
+
figure = self.class.figure(report.ratio)
|
|
126
|
+
minimum = format("%g%%", config.minimum)
|
|
127
|
+
if report.ratio >= config.minimum
|
|
128
|
+
Verdict.new(passed: true,
|
|
129
|
+
percent: report.percent,
|
|
130
|
+
minimum: config.minimum,
|
|
131
|
+
config:,
|
|
132
|
+
reason: "coverage #{figure} meets the #{minimum} minimum (#{source})",
|
|
133
|
+
output: run.output)
|
|
134
|
+
else
|
|
135
|
+
withhold("coverage #{figure} is below the #{minimum} minimum (#{source})",
|
|
136
|
+
config:,
|
|
137
|
+
run:,
|
|
138
|
+
percent: report.percent)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Why the run cannot be trusted, before its report is even read.
|
|
143
|
+
#
|
|
144
|
+
# @return [String, nil]
|
|
145
|
+
def failure(config, root, run)
|
|
146
|
+
return "`#{config.command}` ran past #{@timeout}s and was stopped" if run.timed_out
|
|
147
|
+
return "`#{config.command}` exited #{run.status.exitstatus || run.status}" unless run.status.success?
|
|
148
|
+
|
|
149
|
+
path = config.report_path(root)
|
|
150
|
+
# A report_command reads whatever it reads; there is no file here
|
|
151
|
+
# whose age could say whether the command wrote it.
|
|
152
|
+
return unless path
|
|
153
|
+
|
|
154
|
+
# A whole second's grace: some filesystems keep mtimes in seconds.
|
|
155
|
+
return unless File.exist?(path) && File.mtime(path) < Time.at(run.started.to_i)
|
|
156
|
+
|
|
157
|
+
"#{config.report} is older than this run, so the command did not write it"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# The figure, from the report file or from `report_command`'s STDOUT.
|
|
161
|
+
#
|
|
162
|
+
# @return [Coverage::Report]
|
|
163
|
+
def report_of(config, root)
|
|
164
|
+
return Coverage.read(format: config.format, path: config.report_path(root)) unless config.report_command?
|
|
165
|
+
|
|
166
|
+
printed(config.report_command, root)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# What `report_command` printed, as a {Coverage::Report}, so it is
|
|
170
|
+
# floored and compared exactly as a parsed report is: the number over
|
|
171
|
+
# 100 lines, held as a Rational. Anything but one number from 0 to 100
|
|
172
|
+
# is a report that could not be read, and counts as 0% the same way.
|
|
173
|
+
# STDERR goes to a file of its own, because a warning `jq` prints is
|
|
174
|
+
# not part of the figure.
|
|
175
|
+
#
|
|
176
|
+
# @return [Coverage::Report]
|
|
177
|
+
def printed(command, root)
|
|
178
|
+
Tempfile.create("openlanes-coverage-stderr") do |stderr|
|
|
179
|
+
run = run("{ #{command}\n} 2>#{Shellwords.escape(stderr.path)}", root)
|
|
180
|
+
return Coverage::Report.new(problem: "`#{command}` ran past #{@timeout}s and was stopped") if run.timed_out
|
|
181
|
+
|
|
182
|
+
text = run.output.join("\n").strip
|
|
183
|
+
figure = text.match?(/\A\d+(\.\d+)?\z/) && Rational(text)
|
|
184
|
+
return Coverage::Report.new(covered: figure, relevant: 100) if run.status.success? && figure && figure <= 100
|
|
185
|
+
|
|
186
|
+
said = run.status.success? ? "printed #{text.inspect}" : "exited #{run.status.exitstatus || run.status}"
|
|
187
|
+
error = File.read(stderr.path).lines.first.to_s.strip
|
|
188
|
+
Coverage::Report.new(problem: "`#{command}` #{said}, not a line percentage#{" (#{error})" unless error.empty?}")
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# @return [Run]
|
|
193
|
+
def run(command, root)
|
|
194
|
+
started = Clock::System.now
|
|
195
|
+
child = @spawner.spawn(["sh", "-c", command], chdir: root)
|
|
196
|
+
output = +""
|
|
197
|
+
reader = Thread.new { child.each_chunk { output = keep_tail(output << it) } }
|
|
198
|
+
timed_out = reader.join(@timeout).nil?
|
|
199
|
+
if timed_out
|
|
200
|
+
child.terminate
|
|
201
|
+
reader.join
|
|
202
|
+
end
|
|
203
|
+
Run.new(status: child.wait, timed_out:, output: output.lines.last(TAIL).map(&:chomp), started:)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Bounds memory on a chatty suite; only the tail is ever shown.
|
|
207
|
+
#
|
|
208
|
+
# @return [String]
|
|
209
|
+
def keep_tail(text) = text.bytesize > 65_536 ? text.byteslice(-32_768..).scrub : text
|
|
210
|
+
|
|
211
|
+
# @return [Verdict]
|
|
212
|
+
def withhold(reason, config: nil, run: nil, percent: 0.0)
|
|
213
|
+
Verdict.new(passed: false, percent:, minimum: config&.minimum, reason:, config:, output: run&.output || [])
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|