hecks 7.0.0 → 7.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/lib/hecks/adapters/driven/postgres_era/lineage/chain_sql.rb +2 -1
- data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +4 -2
- data/lib/hecks/adapters/driven/postgres_era/lineage/split_sql.rb +16 -8
- data/lib/hecks/adapters/driven/postgres_era/lineage/transform_sql.rb +7 -3
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +2 -1
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/mint_request.rb +2 -1
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/refusals.rb +2 -1
- data/lib/hecks/adapters/driving/cli_runner/bang_rule.rb +3 -0
- data/lib/hecks/adapters/driving/cli_runner/completion_words.rb +2 -0
- data/lib/hecks/adapters/driving/cli_runner/read_context.rb +6 -3
- data/lib/hecks/adapters/driving/cli_runner/renamed_names.rb +2 -1
- data/lib/hecks/adapters/driving/cli_runner/resolution.rb +2 -2
- data/lib/hecks/adapters/driving/cli_runner/resume_words.rb +1 -0
- data/lib/hecks/adapters/driving/cli_runner/run_reference.rb +6 -3
- data/lib/hecks/adapters/driving/cli_runner/search_words.rb +2 -0
- data/lib/hecks/adapters/driving/cli_runner/tour_words.rb +1 -0
- data/lib/hecks/adapters/driving/cli_runner/verb_names.rb +2 -1
- data/lib/hecks/adapters/driving/cli_runner/verbosity.rb +2 -0
- data/lib/hecks/adapters/driving/cli_runner/word_help.rb +40 -0
- data/lib/hecks/adapters/driving/cli_runner.rb +2 -0
- data/lib/hecks/adapters/driving/mcp/progress_sink.rb +4 -2
- data/lib/hecks/adapters/driving/mcp.rb +2 -1
- data/lib/hecks/adapters/driving/mcp_scope/argument_policy.rb +2 -1
- data/lib/hecks/adapters/driving/ruby/aggregate_module/hooks.rb +28 -14
- data/lib/hecks/adapters/driving/ruby/aggregate_module.rb +4 -2
- data/lib/hecks/assembly.rb +18 -4
- data/lib/hecks/behaviors/expectations/adapter_selection.rb +82 -0
- data/lib/hecks/behaviors/expectations/boot.rb +12 -5
- data/lib/hecks/bluebook/acknowledgements.rb +1 -1
- data/lib/hecks/bluebook/command.rb +3 -2
- data/lib/hecks/bluebook/declaration.rb +2 -1
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +9 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder/metadata.rb +6 -4
- data/lib/hecks/bluebook/dsl/bluebook_builder/modules.rb +6 -3
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation/correlation_keys.rb +2 -1
- data/lib/hecks/bluebook/dsl/command_builder/folded_sets.rb +4 -2
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +2 -1
- data/lib/hecks/bluebook/hecksagon.rb +6 -3
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +9 -2
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/aggregate_checks.rb +2 -1
- data/lib/hecks/bluebook/model_check/classification_checks.rb +51 -0
- data/lib/hecks/bluebook/model_check/context_checks.rb +2 -0
- data/lib/hecks/bluebook/model_check/redundant_relationships.rb +2 -1
- data/lib/hecks/ci_speed/adapters/shard_partitioner.adapter +7 -0
- data/lib/hecks/ci_speed/adapters/shard_partitioner.rb +66 -0
- data/lib/hecks/ci_speed/adapters/timings_source.adapter +7 -0
- data/lib/hecks/ci_speed/adapters/timings_source.rb +68 -0
- data/lib/hecks/ci_speed/ci_speed.bluebook +111 -0
- data/lib/hecks/ci_speed/ci_speed.ports.hecksagon +22 -0
- data/lib/hecks/ci_workflows/ci_workflows.bluebook +50 -6
- data/lib/hecks/coder/coder.bluebook +3 -1
- data/lib/hecks/command_names/index.rb +1 -1
- data/lib/hecks/command_names/resolution.rb +15 -5
- data/lib/hecks/composer/toolbox.rb +2 -1
- data/lib/hecks/corpus.rb +4 -0
- data/lib/hecks/doc/reference/readme.rb +1 -1
- data/lib/hecks/framework_bluebooks.rb +3 -2
- data/lib/hecks/fuzzing/differential.rb +1 -1
- data/lib/hecks/fuzzing/era_boundary.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +1 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +1 -1
- data/lib/hecks/fuzzing/target_capabilities.rb +1 -1
- data/lib/hecks/hecks/adapters/ci_runs.adapter +7 -0
- data/lib/hecks/hecks/adapters/ci_runs.rb +80 -0
- data/lib/hecks/hecks/adapters/claude_agent_list.rb +43 -0
- data/lib/hecks/hecks/adapters/claude_agents.rb +50 -19
- data/lib/hecks/hecks/adapters/claude_jobs.rb +36 -0
- data/lib/hecks/hecks/adapters/codebase/corpus_queries.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/minitest_runs.rb +4 -2
- data/lib/hecks/hecks/adapters/codebase/run_options.rb +2 -1
- data/lib/hecks/hecks/adapters/codebase/test_suite.rb +3 -19
- data/lib/hecks/hecks/adapters/dispatching/brief_text.rb +5 -3
- data/lib/hecks/hecks/adapters/dispatching/launcher.rb +2 -1
- data/lib/hecks/hecks/adapters/dispatching/plan.rb +8 -4
- data/lib/hecks/hecks/adapters/dispatching/settings.rb +2 -1
- data/lib/hecks/hecks/adapters/dispatching/settler.rb +4 -2
- data/lib/hecks/hecks/adapters/dispatching.rb +6 -3
- data/lib/hecks/hecks/adapters/doctor_reading.rb +4 -2
- data/lib/hecks/hecks/adapters/git_landing/carried.rb +42 -0
- data/lib/hecks/hecks/adapters/git_landing/finisher.rb +2 -1
- data/lib/hecks/hecks/adapters/git_landing/flagged.rb +32 -0
- data/lib/hecks/hecks/adapters/git_landing/tidy.rb +27 -14
- data/lib/hecks/hecks/adapters/git_landing.rb +30 -13
- data/lib/hecks/hecks/adapters/git_leftovers.adapter +7 -0
- data/lib/hecks/hecks/adapters/git_leftovers.rb +49 -0
- data/lib/hecks/hecks/adapters/git_main_pull.rb +2 -2
- data/lib/hecks/hecks/adapters/git_train.rb +9 -5
- data/lib/hecks/hecks/adapters/git_worktree_add.rb +8 -4
- data/lib/hecks/hecks/adapters/github_pull_requests.rb +2 -1
- data/lib/hecks/hecks/adapters/in_process_operations.rb +2 -1
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +1 -1
- data/lib/hecks/hecks/adapters/journal_store/readings/composition.rb +2 -1
- data/lib/hecks/hecks/adapters/journal_store/translation_write/shape_edge.rb +4 -2
- data/lib/hecks/hecks/adapters/journal_store/translation_write.rb +4 -2
- data/lib/hecks/hecks/adapters/landed_branches.rb +38 -0
- data/lib/hecks/hecks/adapters/main_boarding.adapter +7 -0
- data/lib/hecks/hecks/adapters/main_boarding.rb +85 -0
- data/lib/hecks/hecks/adapters/main_integration/checks.rb +63 -0
- data/lib/hecks/hecks/adapters/main_integration/command_runner.rb +32 -0
- data/lib/hecks/hecks/adapters/main_integration/range_guard.rb +49 -0
- data/lib/hecks/hecks/adapters/main_integration/repository.rb +83 -0
- data/lib/hecks/hecks/adapters/main_integration/runs.rb +51 -0
- data/lib/hecks/hecks/adapters/main_integration/train.rb +74 -0
- data/lib/hecks/hecks/adapters/main_integration.adapter +7 -0
- data/lib/hecks/hecks/adapters/main_integration.rb +77 -0
- data/lib/hecks/hecks/adapters/main_verification.adapter +7 -0
- data/lib/hecks/hecks/adapters/main_verification.rb +89 -0
- data/lib/hecks/hecks/adapters/push_recorder.adapter +7 -0
- data/lib/hecks/hecks/adapters/push_recorder.rb +78 -0
- data/lib/hecks/hecks/adapters/red_flag_guard.rb +62 -0
- data/lib/hecks/hecks/adapters/red_flag_marker.rb +101 -0
- data/lib/hecks/hecks/adapters/red_flag_report.rb +59 -0
- data/lib/hecks/hecks/adapters/red_flag_survey.rb +65 -0
- data/lib/hecks/hecks/adapters/red_flagging.adapter +7 -0
- data/lib/hecks/hecks/adapters/red_flagging.rb +98 -0
- data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +6 -1
- data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +6 -3
- data/lib/hecks/hecks/adapters/settings_reading/event_tap_row.rb +33 -0
- data/lib/hecks/hecks/adapters/settings_reading.rb +3 -1
- data/lib/hecks/hecks/codebase.behaviors +0 -6
- data/lib/hecks/hecks/custodian.behaviors +22 -1
- data/lib/hecks/hecks/delegate/delegate.bluebook +128 -15
- data/lib/hecks/hecks/deliver/deliver.bluebook +450 -2
- data/lib/hecks/hecks/environments/memory.world +5 -0
- data/lib/hecks/hecks/hecks.contextmap +8 -0
- data/lib/hecks/hecks/hecks.hecksagon +106 -6
- data/lib/hecks/hecks/hecks.world +9 -1
- data/lib/hecks/hecks/migrate/migrate.bluebook +6 -1
- data/lib/hecks/hecks/redflag/redflag.bluebook +249 -0
- data/lib/hecks/hecks/test/test.bluebook +0 -19
- data/lib/hecks/language/adapter.bluebook +13 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +8 -9
- data/lib/hecks/language/sentence/sentence.bluebook +2 -0
- data/lib/hecks/pipeline/pipeline.bluebook +251 -0
- data/lib/hecks/pipeline/pipeline.hecksagon +17 -0
- data/lib/hecks/pipeline_board/adapters/cache.rb +43 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_fill.rb +56 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_git.rb +82 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_github/runs.rb +66 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_github.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_github.rb +70 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_jobs.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_jobs.rb +87 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_journal/chain.rb +40 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_journal/tap.rb +75 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_journal.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_journal.rb +83 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/ci_tiles.rb +85 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/fanout.rb +73 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/gather.rb +57 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/metrics.rb +36 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/strip.rb +102 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/tile_kit.rb +57 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages/work_tiles.rb +59 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_stages.rb +84 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_tickets.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_tickets.rb +77 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/page.html +292 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/sequence.html +190 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/server.rb +105 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/snapshot.rb +57 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/stream.rb +59 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web/wiring.html +536 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_web.rb +80 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_wiring/details.rb +85 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_wiring/graph.rb +120 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_wiring.adapter +7 -0
- data/lib/hecks/pipeline_board/adapters/pipeline_wiring.rb +36 -0
- data/lib/hecks/pipeline_board/adapters/probe.rb +48 -0
- data/lib/hecks/pipeline_board/pipeline_board.bluebook +144 -0
- data/lib/hecks/pipeline_board/pipeline_board.hecksagon +12 -0
- data/lib/hecks/pipeline_board/pipeline_board.ports.hecksagon +47 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +2 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/compose.rb +4 -3
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -1
- data/lib/hecks/projections/ci_pipeline/table.rb +2 -1
- data/lib/hecks/projections/deploy/lambda/stack/recipes.rb +2 -2
- data/lib/hecks/projections/undo.rb +2 -1
- data/lib/hecks/projector/cli_projector/command_help.rb +5 -4
- data/lib/hecks/projector/cli_projector/renames.rb +4 -2
- data/lib/hecks/projector/cli_projector/usage.rb +4 -3
- data/lib/hecks/projector/exporter/adapters.rb +3 -2
- data/lib/hecks/projector/narrate_projector/use_cases.rb +74 -0
- data/lib/hecks/projector/narrate_projector.rb +10 -3
- data/lib/hecks/quality_control/adapters/main_maintenance.rb +9 -5
- data/lib/hecks/quality_control/adapters/qa_auto_mint.rb +21 -2
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +2 -1
- data/lib/hecks/quality_control/cli/qa_fix_main_bug/publishing.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_fix_main_bug.rb +10 -8
- data/lib/hecks/quality_control/cli/qa_generated_domains/promotion.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_main_gate/gate_run.rb +6 -3
- data/lib/hecks/quality_control/cli/qa_main_gate/logging.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_main_gate/shell.rb +4 -2
- data/lib/hecks/quality_control/cli/qa_main_gate/sweeping.rb +8 -4
- data/lib/hecks/quality_control/cli/qa_main_gate/worktree.rb +3 -2
- data/lib/hecks/quality_control/cli/qa_main_gate.rb +10 -5
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode/children.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_sweep.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_tick.rb +6 -3
- data/lib/hecks/quality_control/quality_control.bluebook +11 -11
- data/lib/hecks/quality_control/quality_control.ports.hecksagon +6 -3
- data/lib/hecks/query_ir/touchpoints.rb +2 -1
- data/lib/hecks/refused.rb +2 -1
- data/lib/hecks/release/approval.rb +4 -2
- data/lib/hecks/runtime/event_log.rb +27 -0
- data/lib/hecks/runtime/event_tap.rb +106 -0
- data/lib/hecks/runtime/loader/boot_steps.rb +1 -1
- data/lib/hecks/runtime/loader.rb +35 -19
- data/lib/hecks/runtime/registry/durability_warnings.rb +12 -5
- data/lib/hecks/runtime/registry/runtime_state.rb +4 -1
- data/lib/hecks/runtime/registry/saga_anchor.rb +54 -0
- data/lib/hecks/runtime/registry/saga_durability_rule.rb +66 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +58 -12
- data/lib/hecks/runtime/registry/verification.rb +7 -3
- data/lib/hecks/runtime/saga_interpreter/lifecycle.rb +2 -1
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/rust_format.rb +2 -1
- data/lib/hecks/shape_ledger/edge_hygiene.rb +10 -5
- data/lib/hecks/shape_ledger.rb +1 -1
- data/lib/hecks/storehouse/audit.rb +2 -1
- data/lib/hecks/storehouse/identity.rb +6 -3
- data/lib/hecks/storehouse.rb +2 -1
- data/lib/hecks/tickets/bluebook/tickets.bluebook +5 -4
- data/lib/hecks/tools/ci_workflows.rb +2 -1
- data/lib/hecks/tools/document.rb +12 -1
- data/lib/hecks/tools/downstream_model_check.rb +4 -2
- data/lib/hecks/tools/fuzz_sweep/shards.rb +9 -30
- data/lib/hecks/tools/model_check/examination.rb +25 -2
- data/lib/hecks/tools/move_disables.rb +4 -2
- data/lib/hecks/tools/regeneration_run/scratch.rb +2 -1
- data/lib/hecks/tools/rspec_shard_files.rb +12 -15
- data/lib/hecks/tools/sequence_script.rb +1 -1
- data/lib/hecks/tools/shard_planner.rb +65 -0
- data/lib/hecks/tools/stage_progress.rb +2 -1
- data/lib/hecks/tools/stress_concurrency_specs.rb +2 -1
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +8 -9
- data/lib/hecks.rb +5 -2
- data/prebuilt/{13c04bdc5970295b310f0a980d397ba722232c8da99cc1a0cdb383249921e871.marshal → 4b81646f769ae5b1e7457f9b11c48d8bc1093e96a1dd6fb61864065121ca6f01.marshal} +0 -0
- data/prebuilt/{e3857b77f47d9ecc1787b2e1f5136364f3cb3444ef4aea5479f3b98167affea2.marshal → 681890632e3f4a99646623dcae44af36b1bad66c515f5abf67a2460372b9c6fd.marshal} +0 -0
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/parser/src/emit.rs +5 -1
- data/rust/parser/src/ir.rs +2 -0
- data/rust/parser/src/keywords.rs +12 -0
- data/rust/parser/src/main.rs +1 -0
- data/rust/parser/src/parse/adapter.rs +1 -0
- data/rust/parser/src/phrases.rs +16 -0
- data/rust/parser/src/sentence/examples.rs +93 -0
- data/rust/parser/src/sentence/mod.rs +1 -0
- metadata +89 -5
- data/lib/hecks/tools/refresh_rspec_runtime_baseline.rb +0 -166
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Pipeline: one change's passage from a planned Work row to a release, declared as its own
|
|
2
|
+
# aggregate instead of read off six other domains. A Passage is keyed by the Work row's key, which
|
|
3
|
+
# is also the branch a landing finishes, so the events the other domains already emit can find it.
|
|
4
|
+
# Every stage is advanced by a policy reacting to one of those events; nothing here asks the
|
|
5
|
+
# outside anything.
|
|
6
|
+
#
|
|
7
|
+
# Tickets Work.WorkPlanned -> Plan (planned)
|
|
8
|
+
# Tickets Work.WorkStarted -> Dispatch (dispatched)
|
|
9
|
+
# Hecks Landing.LandedWorkFound -> Land (landed)
|
|
10
|
+
# Hecks Integration.MainPushed -> Push (pushed), every landed passage at once
|
|
11
|
+
# Hecks RedFlag.RedFlagRaised -> Hold (held), every pushed passage
|
|
12
|
+
# Hecks RedFlag.RedFlagCleared -> Verify (verified), every pushed or held passage
|
|
13
|
+
# Hecks PromotionRun.PromotionCompleted, promoted -> Promote (promoted), every verified passage
|
|
14
|
+
# Hecks Release.Published -> Release (released), every promoted passage
|
|
15
|
+
#
|
|
16
|
+
# A push, a green run, a promotion and a release each cover every passage waiting at the stage
|
|
17
|
+
# before, because main moves as one line: a commit pushed, verified or promoted carries everything
|
|
18
|
+
# landed before it. Stalling is a mark, not a stage: a passage that sat too long where it is keeps
|
|
19
|
+
# its stage and gains the reason, so the board can show it in place.
|
|
20
|
+
|
|
21
|
+
Hecks.bluebook "Pipeline" do
|
|
22
|
+
vision "Every change is one passage, from the Work row that planned it to the release that shipped it, and where each one stands is a fact the pipeline itself holds."
|
|
23
|
+
|
|
24
|
+
supporting
|
|
25
|
+
|
|
26
|
+
aggregate "Passage" do
|
|
27
|
+
description "One change's journey through the pipeline: the stage it is at, what carried it there, and whether it has stalled."
|
|
28
|
+
created_by "Passage.plan!"
|
|
29
|
+
|
|
30
|
+
identified_by :change
|
|
31
|
+
|
|
32
|
+
attribute :change, ChangeKey
|
|
33
|
+
attribute :title, Title, optional: true
|
|
34
|
+
attribute :session, Word, optional: true
|
|
35
|
+
attribute :landed, Word, optional: true
|
|
36
|
+
attribute :pushed, Word, optional: true
|
|
37
|
+
attribute :flag, Word, optional: true
|
|
38
|
+
attribute :stalled, Reason, optional: true
|
|
39
|
+
|
|
40
|
+
# The Work row's key, which the branch that lands it is named after.
|
|
41
|
+
value_object "ChangeKey" do
|
|
42
|
+
attribute :value, String, matches: :present
|
|
43
|
+
invariant("a passage is named by its change") { !value.to_s.empty? }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
value_object "Title" do
|
|
47
|
+
attribute :value, String
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# A session id, a commit, or a red flag's id.
|
|
51
|
+
value_object "Word" do
|
|
52
|
+
attribute :value, String, matches: :present
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Why a passage stalled; empty once it moved on.
|
|
56
|
+
value_object "Reason" do
|
|
57
|
+
attribute :value, String, default: ""
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
lifecycle :stage, default: "planned" do
|
|
61
|
+
mark :terminal, "released"
|
|
62
|
+
transition "Dispatch" => "dispatched", from: "planned"
|
|
63
|
+
transition "Land" => "landed", from: %w[planned dispatched]
|
|
64
|
+
transition "Push" => "pushed", from: "landed"
|
|
65
|
+
transition "Hold" => "held", from: "pushed"
|
|
66
|
+
transition "Verify" => "verified", from: %w[pushed held]
|
|
67
|
+
transition "Promote" => "promoted", from: "verified"
|
|
68
|
+
transition "Release" => "released", from: "promoted"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
command "Plan" do
|
|
72
|
+
role "System"
|
|
73
|
+
goal "Open a passage for a Work row the moment it is planned"
|
|
74
|
+
|
|
75
|
+
attribute :change, ChangeKey, sets: true
|
|
76
|
+
attribute :title, Title, optional: true, sets: true
|
|
77
|
+
|
|
78
|
+
emits PassagePlanned
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
command "Dispatch" do
|
|
82
|
+
role "System"
|
|
83
|
+
goal "Record the session a planned change was handed to"
|
|
84
|
+
|
|
85
|
+
reference_to Passage
|
|
86
|
+
attribute :session, Word, sets: true
|
|
87
|
+
sets :stalled, to: { value: "" }
|
|
88
|
+
|
|
89
|
+
emits PassageDispatched
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
command "Land" do
|
|
93
|
+
role "System"
|
|
94
|
+
goal "Record that the change's branch landed on local main"
|
|
95
|
+
|
|
96
|
+
reference_to Passage
|
|
97
|
+
sets :stalled, to: { value: "" }
|
|
98
|
+
|
|
99
|
+
emits PassageLanded
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
command "Push" do
|
|
103
|
+
role "System"
|
|
104
|
+
goal "Record the main commit that carried the landed change to the remote"
|
|
105
|
+
|
|
106
|
+
reference_to Passage
|
|
107
|
+
attribute :pushed, Word, sets: true
|
|
108
|
+
sets :stalled, to: { value: "" }
|
|
109
|
+
|
|
110
|
+
emits PassagePushed
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
command "Hold" do
|
|
114
|
+
role "System"
|
|
115
|
+
goal "Record that main went red while this change waits for a green run"
|
|
116
|
+
|
|
117
|
+
reference_to Passage
|
|
118
|
+
attribute :flag, Word, sets: true
|
|
119
|
+
|
|
120
|
+
emits PassageHeld
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
command "Verify" do
|
|
124
|
+
role "System"
|
|
125
|
+
goal "Record that a green run of main covers the pushed change"
|
|
126
|
+
|
|
127
|
+
reference_to Passage
|
|
128
|
+
sets :stalled, to: { value: "" }
|
|
129
|
+
|
|
130
|
+
emits PassageVerified
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
command "Promote" do
|
|
134
|
+
role "System"
|
|
135
|
+
goal "Record that stable moved past the verified change"
|
|
136
|
+
|
|
137
|
+
reference_to Passage
|
|
138
|
+
sets :stalled, to: { value: "" }
|
|
139
|
+
|
|
140
|
+
emits PassagePromoted
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
command "Release" do
|
|
144
|
+
role "System"
|
|
145
|
+
goal "Record that a published release carries the change"
|
|
146
|
+
|
|
147
|
+
reference_to Passage
|
|
148
|
+
|
|
149
|
+
emits PassageReleased
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
command "Stall" do
|
|
153
|
+
role "Maintainer"
|
|
154
|
+
goal "Mark a passage that has sat too long at its stage, with the reason, without moving it"
|
|
155
|
+
|
|
156
|
+
reference_to Passage
|
|
157
|
+
attribute :stalled, Reason, sets: true
|
|
158
|
+
|
|
159
|
+
emits PassageStalled
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
query "InFlight" do
|
|
163
|
+
description "Every passage that has not been released, oldest change first."
|
|
164
|
+
where(stage: { in: "planned,dispatched,landed,pushed,held,verified,promoted" })
|
|
165
|
+
order_by :change
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
query "AtStage" do
|
|
169
|
+
description "The passages standing at one stage."
|
|
170
|
+
attribute :stage, String
|
|
171
|
+
where(stage: :stage)
|
|
172
|
+
order_by :change
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
query "Landed" do
|
|
176
|
+
description "The passages on local main that no push has carried yet."
|
|
177
|
+
where(stage: "landed")
|
|
178
|
+
order_by :change
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
query "Pushed" do
|
|
182
|
+
description "The passages on the remote waiting for a green run of main, held by a red flag or not."
|
|
183
|
+
where(stage: { in: "pushed,held" })
|
|
184
|
+
order_by :change
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
query "Verified" do
|
|
188
|
+
description "The passages a green run covers that stable has not moved past."
|
|
189
|
+
where(stage: "verified")
|
|
190
|
+
order_by :change
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
query "Promoted" do
|
|
194
|
+
description "The passages on stable that no release has shipped."
|
|
195
|
+
where(stage: "promoted")
|
|
196
|
+
order_by :change
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
query "Stalled" do
|
|
200
|
+
description "Every passage marked as stalled, with the reason."
|
|
201
|
+
where(stalled: { ne: "" })
|
|
202
|
+
order_by :change
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
policy "PlanWhenWorkPlanned" do
|
|
207
|
+
on "Work.WorkPlanned"
|
|
208
|
+
trigger Passage::Plan, with: { change: :work, title: :title }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
policy "DispatchWhenWorkStarted" do
|
|
212
|
+
on "Work.WorkStarted"
|
|
213
|
+
trigger Passage::Dispatch, with: { change: :work, session: :claimed_by }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
policy "LandWhenWorkLanded" do
|
|
217
|
+
on "Landing.LandedWorkFound"
|
|
218
|
+
trigger Passage::Land, with: { change: :branch }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
policy "PushEveryLandedWhenMainPushed" do
|
|
222
|
+
on "Integration.MainPushed"
|
|
223
|
+
for_each "Passage.Landed"
|
|
224
|
+
trigger Passage::Push, with: { change: :change, pushed: :pushed }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
policy "HoldEveryPushedWhenMainRed" do
|
|
228
|
+
on "RedFlag.RedFlagRaised"
|
|
229
|
+
for_each "Passage.Pushed"
|
|
230
|
+
trigger Passage::Hold, with: { change: :change, flag: :flag }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
policy "VerifyEveryPushedWhenMainGreen" do
|
|
234
|
+
on "RedFlag.RedFlagCleared"
|
|
235
|
+
for_each "Passage.Pushed"
|
|
236
|
+
trigger Passage::Verify, with: { change: :change }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
policy "PromoteEveryVerifiedWhenStableMoves" do
|
|
240
|
+
on "PromotionRun.PromotionCompleted"
|
|
241
|
+
where { promoted.value == true }
|
|
242
|
+
for_each "Passage.Verified"
|
|
243
|
+
trigger Passage::Promote, with: { change: :change }
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
policy "ReleaseEveryPromotedWhenPublished" do
|
|
247
|
+
on "Release.Published"
|
|
248
|
+
for_each "Passage.Promoted"
|
|
249
|
+
trigger Passage::Release, with: { change: :change }
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# The Pipeline bluebook, bound so it boots standing alone (a behaviors or model_check boot of this
|
|
2
|
+
# directory). It persists in Memory like every tooling domain and declares no port: every stage is
|
|
3
|
+
# advanced by an event another domain already emits.
|
|
4
|
+
Hecks.hecksagon "Pipeline" do
|
|
5
|
+
attaches "Governance"
|
|
6
|
+
persisted_by "Memory"
|
|
7
|
+
|
|
8
|
+
# The events that move a passage, as the Tickets and Hecks domains emit them.
|
|
9
|
+
%w[Tickets.WorkPlanned Tickets.WorkStarted Hecks.LandedWorkFound Hecks.MainPushed
|
|
10
|
+
Hecks.RedFlagRaised Hecks.RedFlagCleared Hecks.PromotionCompleted Hecks.Published]
|
|
11
|
+
.each { |event| subscribe event }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# The anti-corruption layer for Governance, which the bluebook's roles are checked against.
|
|
15
|
+
Hecks.hecksagon "Governance" do
|
|
16
|
+
persisted_by "Memory"
|
|
17
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "probe"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
module PipelineBoard
|
|
8
|
+
# Keeps each outside read for a few seconds, failures included, so a page polling every ten
|
|
9
|
+
# seconds cannot hammer `gh` or the disk.
|
|
10
|
+
class Cache
|
|
11
|
+
# @param ttl [Numeric] seconds an answer is kept
|
|
12
|
+
# @param clock [#call] the time in seconds; the monotonic clock when none is given
|
|
13
|
+
def initialize(ttl: 20, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
|
|
14
|
+
@ttl = ttl
|
|
15
|
+
@clock = clock
|
|
16
|
+
@held = {}
|
|
17
|
+
@lock = Mutex.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param key [Object] what is read
|
|
21
|
+
# @return [Object] the held answer while it is fresh, else the block's; a block that raises
|
|
22
|
+
# {Unavailable} is held as the failure and raised again until it expires
|
|
23
|
+
def fetch(key, &block)
|
|
24
|
+
@lock.synchronize do
|
|
25
|
+
kept = @held[key]
|
|
26
|
+
kept = @held[key] = [@clock.call, attempt(&block)] unless kept && fresh?(kept)
|
|
27
|
+
kept.last.is_a?(Unavailable) ? raise(kept.last) : kept.last
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def fresh?(kept) = @clock.call - kept.first < @ttl
|
|
34
|
+
|
|
35
|
+
def attempt
|
|
36
|
+
yield
|
|
37
|
+
rescue Unavailable => e
|
|
38
|
+
e
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "cache"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Adapters
|
|
8
|
+
# Reads `Dispatch.Fill` as a dry run: which planned rows it would start and which it skips and
|
|
9
|
+
# why, plus the load and cap it saw. It never passes `confirm`, so nothing starts.
|
|
10
|
+
class PipelineFill
|
|
11
|
+
HEADER = /\Aload ([\d.]+) \(limit ([\d.]+), (\d+) cpus\); cap (\d+), (\d+) started/
|
|
12
|
+
WOULD = /\Awould start (\S+): (.*)\z/
|
|
13
|
+
SKIPPED = /\A(\S+) skipped: (.*)\z/
|
|
14
|
+
|
|
15
|
+
# The JSON document the launcher prints, after any log lines.
|
|
16
|
+
JSON_BODY = /\{.*\}/m
|
|
17
|
+
|
|
18
|
+
# @param aggregate [Object, nil] unused
|
|
19
|
+
# @param settings [Hash] `runner:` stands in for the launcher (a callable taking the
|
|
20
|
+
# arguments, answering its standard output), `ttl:` for how long the preview is kept
|
|
21
|
+
# @param root [String, nil] the checkout the launcher runs in
|
|
22
|
+
def initialize(aggregate: nil, settings: {}, root: nil)
|
|
23
|
+
@root = root || Dir.pwd
|
|
24
|
+
@runner = settings[:runner] || ->(*args) { PipelineBoard::Probe.run(*args, chdir: @root, seconds: 120) }
|
|
25
|
+
@cache = PipelineBoard::Cache.new(ttl: settings.fetch(:ttl, 60))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Hash] `load`, `limit`, `cap`, `started`, `would_start` (slug and title) and
|
|
29
|
+
# `skipped` (slug and reason)
|
|
30
|
+
# @raise [PipelineBoard::Unavailable] when the launcher cannot answer
|
|
31
|
+
def preview
|
|
32
|
+
@cache.fetch(:fill) { parse(report) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def report
|
|
38
|
+
out = @runner.call("exe/hecks", "Dispatch.Fill!", "repo.value=#{@root}", "budget_minutes.value=30", "--wait")
|
|
39
|
+
JSON.parse(out[JSON_BODY].to_s).dig("state", "report").to_s
|
|
40
|
+
rescue JSON::ParserError
|
|
41
|
+
raise PipelineBoard::Unavailable, "Dispatch.Fill printed no report"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse(text)
|
|
45
|
+
lines = text.lines.map(&:strip)
|
|
46
|
+
counts = HEADER.match(lines.first.to_s) or raise PipelineBoard::Unavailable, "unreadable fill report"
|
|
47
|
+
{ load: counts[1].to_f, limit: counts[2].to_f, cap: counts[4].to_i, started: counts[5].to_i,
|
|
48
|
+
would_start: pairs(lines, WOULD, :slug, :title), skipped: pairs(lines, SKIPPED, :slug, :reason) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def pairs(lines, pattern, first, second)
|
|
52
|
+
lines.filter_map { |line| (found = pattern.match(line)) && { first => found[1], second => found[2] } }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "cache"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
# The `Git` port's adapter: where local main stands against `origin/main`, what the push
|
|
10
|
+
# train's copy last pushed, and whether `stable` has caught up with main. It reads refs and
|
|
11
|
+
# never fetches, so it writes nothing, not even a remote-tracking ref.
|
|
12
|
+
class PipelineGit
|
|
13
|
+
# @param aggregate [Object, nil] unused
|
|
14
|
+
# @param settings [Hash] `git:` stands in for the program (a callable taking a directory
|
|
15
|
+
# and the arguments, answering standard output), `train:` for the train's checkout,
|
|
16
|
+
# `ttl:` for how long a read is kept
|
|
17
|
+
# @param root [String, nil] the checkout whose main is read
|
|
18
|
+
def initialize(aggregate: nil, settings: {}, root: nil)
|
|
19
|
+
@root = root || Dir.pwd
|
|
20
|
+
@git = settings[:git] || ->(dir, *args) { PipelineBoard::Probe.run("git", "-C", dir, *args) }
|
|
21
|
+
@train = settings.fetch(:train) { File.join(Dir.home, ".claude", "hecks-train") }
|
|
22
|
+
@cache = PipelineBoard::Cache.new(ttl: settings.fetch(:ttl, 15))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [Hash] `ahead`, `behind` (local main against origin/main), `dirty` (changed files in
|
|
26
|
+
# this checkout) and `sha`
|
|
27
|
+
# @raise [PipelineBoard::Unavailable] when git cannot answer
|
|
28
|
+
def local_main
|
|
29
|
+
behind, ahead = ask(@root, "rev-list", "--left-right", "--count", "origin/main...main").split.map(&:to_i)
|
|
30
|
+
{ sha: short("main"), ahead: ahead, behind: behind, oldest_unpushed: oldest_unpushed,
|
|
31
|
+
dirty: ask(@root, "status", "--porcelain").lines.size }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [Hash] the commit the train's checkout stands on: `sha`, `at`, and `pushed`
|
|
35
|
+
# (whether origin/main holds it)
|
|
36
|
+
# @raise [PipelineBoard::Unavailable] when the train's checkout cannot be read
|
|
37
|
+
def train
|
|
38
|
+
sha, at = ask(@train, "log", "-1", "--format=%h %cI").split
|
|
39
|
+
{ sha: sha, at: at, pushed: contains?("origin/main", sha) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [String, nil] when the oldest commit on local main that origin/main lacks was made
|
|
43
|
+
def oldest_unpushed = ask(@root, "log", "--format=%cI", "origin/main..main").lines.last&.strip
|
|
44
|
+
|
|
45
|
+
# @param shas [Array<String>] commits to date
|
|
46
|
+
# @return [Hash{String => String}] each known commit's short sha with when it was made
|
|
47
|
+
def commit_times(shas)
|
|
48
|
+
dated = ask(@root, "log", "-n", "600", "--format=%H %cI", "origin/main").lines.to_h(&:split)
|
|
49
|
+
shas.filter_map { |sha| (full = dated.keys.find { |key| key.start_with?(sha) }) && [sha, dated[full]] }.to_h
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The standing red flag of the repository, as its marker file holds it, or nil when main is
|
|
53
|
+
# not flagged.
|
|
54
|
+
#
|
|
55
|
+
# @return [Hash, nil] the flag: id, commit, raised_at, runs and checkins
|
|
56
|
+
def red_flag
|
|
57
|
+
common = ask(@root, "rev-parse", "--path-format=absolute", "--git-common-dir").strip
|
|
58
|
+
path = File.join(common, "hecks-red-flag.json")
|
|
59
|
+
JSON.parse(File.read(path)) if File.exist?(path)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Hash] `behind`, the commits on origin/main that stable lacks, and `sha`
|
|
63
|
+
# @raise [PipelineBoard::Unavailable] when git cannot answer
|
|
64
|
+
def stable
|
|
65
|
+
{ sha: short("origin/stable"), behind: ask(@root, "rev-list", "--count", "origin/stable..origin/main").to_i }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def short(ref) = ask(@root, "rev-parse", "--short", ref).strip
|
|
71
|
+
|
|
72
|
+
def contains?(ref, sha)
|
|
73
|
+
ask(@root, "merge-base", "--is-ancestor", sha, ref)
|
|
74
|
+
true
|
|
75
|
+
rescue PipelineBoard::Unavailable
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def ask(dir, *args) = @cache.fetch([dir, *args]) { @git.call(dir, *args) }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
module PipelineBoard
|
|
8
|
+
# Turns `gh` run and job records into the rows the board shows. A settled run never changes,
|
|
9
|
+
# so its failed-job count is read once.
|
|
10
|
+
module PipelineRuns
|
|
11
|
+
RUN_FIELDS = "databaseId,headSha,startedAt,updatedAt,conclusion,status"
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def rows(limit)
|
|
16
|
+
runs = read("run", "list", "--branch", self.class::BRANCH, "--workflow", self.class::WORKFLOW,
|
|
17
|
+
"--limit", limit.to_s, "--json", RUN_FIELDS)
|
|
18
|
+
runs.reverse.map { |run| Thread.new { row(run) } }.map(&:value)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def row(run)
|
|
22
|
+
{ id: run["databaseId"], sha: run["headSha"].to_s[0, 7], started: run["startedAt"],
|
|
23
|
+
wall_minutes: minutes(run["startedAt"], run["updatedAt"]), status: run["status"],
|
|
24
|
+
conclusion: run["conclusion"].to_s, failed_jobs: failed_jobs(run) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The jobs that failed, nil when GitHub would not list the run's jobs (so the board says so
|
|
28
|
+
# instead of counting zero).
|
|
29
|
+
def failed_jobs(run)
|
|
30
|
+
return counted(run) unless run["status"] == "completed"
|
|
31
|
+
|
|
32
|
+
@settled[run["databaseId"]] ||= counted(run)
|
|
33
|
+
rescue PipelineBoard::Unavailable
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def counted(run) = jobs_of(run["databaseId"]).count { |job| failed?(job) }
|
|
38
|
+
|
|
39
|
+
def failed?(job) = %w[failure timed_out cancelled startup_failure].include?(job["conclusion"])
|
|
40
|
+
|
|
41
|
+
def jobs_of(id) = read("run", "view", id.to_s, "--json", "jobs")["jobs"] || []
|
|
42
|
+
|
|
43
|
+
def slowest(limit)
|
|
44
|
+
done = read("run", "list", "--branch", self.class::BRANCH, "--workflow", self.class::WORKFLOW,
|
|
45
|
+
"--status", "completed", "--limit", "1", "--json", RUN_FIELDS).first
|
|
46
|
+
return [] unless done
|
|
47
|
+
|
|
48
|
+
timed = jobs_of(done["databaseId"]).filter_map { |job| timed(job) }
|
|
49
|
+
timed.sort_by { |job| -job[:minutes] }.first(limit)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def timed(job)
|
|
53
|
+
return unless job["startedAt"] && job["completedAt"]
|
|
54
|
+
|
|
55
|
+
{ name: job["name"], minutes: minutes(job["startedAt"], job["completedAt"]), conclusion: job["conclusion"].to_s }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def minutes(from, upto)
|
|
59
|
+
return 0.0 unless from && upto
|
|
60
|
+
|
|
61
|
+
((Time.parse(upto) - Time.parse(from)) / 60.0).round(1)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Binds the `GitHub` port to `PipelineGitHub`, which answers `CiRuns` and `SlowestJobs` with CI as GitHub reports it.
|
|
2
|
+
# Required by name, not relative, so a copy of this directory shares the one implementation loaded.
|
|
3
|
+
require "hecks/pipeline_board/adapters/pipeline_github"
|
|
4
|
+
|
|
5
|
+
Hecks.adapter "PipelineGitHub" do
|
|
6
|
+
port "GitHub"
|
|
7
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "cache"
|
|
6
|
+
require_relative "pipeline_github/runs"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module Adapters
|
|
10
|
+
# The `GitHub` port's adapter: answers `CiRuns` and `SlowestJobs` with CI as GitHub reports it,
|
|
11
|
+
# read through `gh run list` and `gh run view`. It only reads, and keeps every read for a few
|
|
12
|
+
# seconds.
|
|
13
|
+
class PipelineGitHub
|
|
14
|
+
include PipelineBoard::PipelineRuns
|
|
15
|
+
|
|
16
|
+
WORKFLOW = "ci.yml"
|
|
17
|
+
BRANCH = "main"
|
|
18
|
+
|
|
19
|
+
# A release tag that names a version, such as `v7.0.0`.
|
|
20
|
+
VERSION_TAG = /\Av\d/
|
|
21
|
+
|
|
22
|
+
# @param aggregate [Object, nil] unused
|
|
23
|
+
# @param settings [Hash] `gh:` stands in for the program (a callable taking the arguments and
|
|
24
|
+
# answering parsed JSON), `ttl:` for how long a read is kept
|
|
25
|
+
# @param root [String, nil] the checkout `gh` runs in
|
|
26
|
+
def initialize(aggregate: nil, settings: {}, root: nil)
|
|
27
|
+
@gh = settings[:gh] || ->(*args) { PipelineBoard::Probe.json("gh", *args, chdir: root) }
|
|
28
|
+
@cache = PipelineBoard::Cache.new(ttl: settings.fetch(:ttl, 20))
|
|
29
|
+
@settled = {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param limit [Hash, Integer, nil] how many runs (20 when none is named)
|
|
33
|
+
# @return [Hash{Symbol => String}] `text:` the runs, oldest first, as JSON
|
|
34
|
+
# @raise [PipelineBoard::Unavailable] when `gh` cannot answer
|
|
35
|
+
def ci_runs(limit: nil)
|
|
36
|
+
{ text: JSON.generate(rows(count(limit, 20))) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param limit [Hash, Integer, nil] how many jobs (8 when none is named)
|
|
40
|
+
# @return [Hash{Symbol => String}] `text:` the last completed run's slowest jobs as JSON
|
|
41
|
+
# @raise [PipelineBoard::Unavailable] when `gh` cannot answer
|
|
42
|
+
def slowest_jobs(limit: nil)
|
|
43
|
+
{ text: JSON.generate(slowest(count(limit, 8))) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Hash, nil] the newest `v` release: `tag`, `published` and `published_at`
|
|
47
|
+
# @raise [PipelineBoard::Unavailable] when `gh` cannot answer
|
|
48
|
+
def latest_release
|
|
49
|
+
listed = read("release", "list", "--limit", "20", "--json", "tagName,isDraft,isPrerelease,publishedAt")
|
|
50
|
+
found = listed.find { |release| release["tagName"].to_s.match?(VERSION_TAG) }
|
|
51
|
+
raise PipelineBoard::Unavailable, "no v release found" unless found
|
|
52
|
+
|
|
53
|
+
{ tag: found["tagName"], published: !found["isDraft"], published_at: found["publishedAt"] }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [Hash, nil] the newest CI run on main, as {#ci_runs} rows are
|
|
57
|
+
# @raise [PipelineBoard::Unavailable] when `gh` cannot answer
|
|
58
|
+
def latest_run = rows(1).last
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def count(value, default)
|
|
63
|
+
number = value.is_a?(Hash) ? value[:value] : value
|
|
64
|
+
number.to_i.positive? ? number.to_i : default
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def read(*args) = @cache.fetch(args) { @gh.call(*args) }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Binds the `Jobs` port to `PipelineJobs`, which answers `Sessions` from each job's state file and `claude agents`.
|
|
2
|
+
# Required by name, not relative, so a copy of this directory shares the one implementation loaded.
|
|
3
|
+
require "hecks/pipeline_board/adapters/pipeline_jobs"
|
|
4
|
+
|
|
5
|
+
Hecks.adapter "PipelineJobs" do
|
|
6
|
+
port "Jobs"
|
|
7
|
+
end
|