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,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../console_capture"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Adapters
|
|
8
|
+
class MainIntegration
|
|
9
|
+
# The push train's own copy (`~/.claude/hecks-train`), as a boarding uses it: brought up to
|
|
10
|
+
# origin/main when it has no tracked changes, then asked to board the checkout's main. The
|
|
11
|
+
# train runs in its own process and store, so what it pushed is read from its answer. The
|
|
12
|
+
# command line is fixed: no plain push, no force, no skipped hook, no owner override.
|
|
13
|
+
class Train
|
|
14
|
+
# Where the train's copy is.
|
|
15
|
+
ROOT = File.join(Dir.home, ".claude", "hecks-train")
|
|
16
|
+
|
|
17
|
+
# The statuses of a boarding the remote holds: pushed now, or carried by an earlier push.
|
|
18
|
+
BOARDED = %w[pushed carried].freeze
|
|
19
|
+
|
|
20
|
+
# Where the launcher's JSON answer starts in what it printed.
|
|
21
|
+
RECORD_START = /^\{/
|
|
22
|
+
|
|
23
|
+
# @param runner [#call] takes `(argv, chdir:)` and answers `[ok, output]`
|
|
24
|
+
# @param root [String] the train's copy
|
|
25
|
+
def initialize(runner, root: ROOT)
|
|
26
|
+
@runner = runner
|
|
27
|
+
@root = root
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Brings the copy up to origin/main, unless it has tracked changes.
|
|
31
|
+
#
|
|
32
|
+
# @return [String] where the copy stands, or why it was left
|
|
33
|
+
def refresh
|
|
34
|
+
changed = run("git", "status", "--porcelain", "--untracked-files=no").last.strip
|
|
35
|
+
return "train copy left as it is: it has tracked changes" unless changed.empty?
|
|
36
|
+
|
|
37
|
+
fetched = run("git", "fetch", "-q", "origin", "main").first
|
|
38
|
+
moved = fetched && run("git", "checkout", "-q", "--detach", "FETCH_HEAD").first
|
|
39
|
+
moved ? "train copy at origin/main" : "train copy left as it is: it could not be refreshed"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param checkout [String] the checkout whose main is boarded
|
|
43
|
+
# @return [Array<String>] the command line the boarding runs
|
|
44
|
+
def command(checkout)
|
|
45
|
+
[File.join(@root, "exe", "hecks"), "SendTrain.board!", "checkout=#{checkout}", "remote=origin", "branch=main",
|
|
46
|
+
"--confirm", "--wait"]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @param checkout [String] the checkout whose main is boarded
|
|
50
|
+
# @return [Hash{String => Object}] the boarding's record as the train answered it
|
|
51
|
+
# @raise [ConsoleCapture::Failure] when the train refused, faulted or answered unreadably
|
|
52
|
+
def board(checkout)
|
|
53
|
+
ok, output = run(*command(checkout))
|
|
54
|
+
state = record_in(output)
|
|
55
|
+
return state if ok && BOARDED.include?(state["status"].to_s)
|
|
56
|
+
|
|
57
|
+
raise ConsoleCapture::Failure, "the push train did not push: #{state["refusal"] || output.to_s.strip[-300..] || output}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def run(*argv) = @runner.call(argv, chdir: @root)
|
|
63
|
+
|
|
64
|
+
def record_in(output)
|
|
65
|
+
text = output.to_s
|
|
66
|
+
start = text.index(RECORD_START)
|
|
67
|
+
start ? JSON.parse(text[start..]).fetch("state", {}) : {}
|
|
68
|
+
rescue JSON::ParserError
|
|
69
|
+
{}
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Required by name, not relative to this file: a fuzz or model check boots a copy of this directory,
|
|
2
|
+
# and the implementation must load once, from `lib/`, not from the copy.
|
|
3
|
+
require "hecks/hecks/adapters/main_integration"
|
|
4
|
+
|
|
5
|
+
Hecks.adapter "MainIntegration" do
|
|
6
|
+
port "Integrating"
|
|
7
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "hecks/runtime/dispatcher"
|
|
5
|
+
require "hecks/runtime/errors"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
# The `Integrating` port's adapter for `Integration.Advance`: dispatches the next step of every
|
|
10
|
+
# open MainIntegration run, once. A landed run is verified, a verified one boarded, and a pushed
|
|
11
|
+
# or watched one has its CI run read. A run in any other stage (being verified or boarded,
|
|
12
|
+
# green, red, superseded or waiting for a person) is left alone, so a second advance in the
|
|
13
|
+
# same stage dispatches nothing.
|
|
14
|
+
#
|
|
15
|
+
# Each step is a dispatch of its own through the boot whose advance asked, not a dispatch from
|
|
16
|
+
# inside the saga, so it starts its own reaction chain. A boarding needs the advance's
|
|
17
|
+
# `confirm`, and waits while another run is being pushed or watched.
|
|
18
|
+
class MainIntegration
|
|
19
|
+
# The command each stage's next step dispatches.
|
|
20
|
+
NEXT = { "landed" => "Verify", "verified" => "Board", "pushed" => "Observe", "ci_running" => "Observe" }.freeze
|
|
21
|
+
|
|
22
|
+
# @param registry [Runtime::Registry, nil] the booted registry whose runs are advanced
|
|
23
|
+
# @param aggregate [Object, nil] unused
|
|
24
|
+
def initialize(registry: nil, aggregate: nil)
|
|
25
|
+
@registry = registry
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Dispatches each open run's next step.
|
|
29
|
+
#
|
|
30
|
+
# @param held [Hash] the `Integration` record: `confirm` lets a verified run board
|
|
31
|
+
# @return [Hash{Symbol => Hash}] `report:` one line per open run: what was dispatched and
|
|
32
|
+
# the stage it left the run in, or why nothing was
|
|
33
|
+
def step(**held)
|
|
34
|
+
confirm = plain(held[:confirm]) == true
|
|
35
|
+
lines = runs.all.select { |run| NEXT.key?(run.state) }.map { |run| advance(run, confirm) }
|
|
36
|
+
{ report: { value: lines.empty? ? "no open integration run" : lines.join("\n") } }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def runs = Runs.new(@registry)
|
|
42
|
+
|
|
43
|
+
def advance(run, confirm)
|
|
44
|
+
held_back = boarding_held_back(run, confirm) if NEXT[run.state] == "Board"
|
|
45
|
+
return "#{run.short} #{run.state}: #{held_back}" if held_back
|
|
46
|
+
|
|
47
|
+
dispatch(run, NEXT[run.state], confirm)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def boarding_held_back(run, confirm)
|
|
51
|
+
other = runs.in_flight(except: run.landed).first
|
|
52
|
+
return "waits for #{other.short}, which is #{other.state}" if other
|
|
53
|
+
return "would board; advance with --confirm to push main" unless confirm
|
|
54
|
+
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def dispatch(run, command, confirm)
|
|
59
|
+
Runtime::Dispatcher.new(@registry).dispatch_flat("Hecks::Integration.#{command}", arguments(run, command, confirm))
|
|
60
|
+
"#{run.short} #{run.state} -> #{command}: now #{runs.find(run.landed)&.state}"
|
|
61
|
+
rescue *Runtime::DOMAIN_REFUSALS => e
|
|
62
|
+
"#{run.short} #{run.state} -> #{command} refused: #{e.message}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def arguments(run, command, confirm)
|
|
66
|
+
args = { run: SecureRandom.uuid, landed: run.landed }
|
|
67
|
+
args[:checkout] = run.checkout unless run.checkout.empty?
|
|
68
|
+
args[:confirm] = true if command == "Board" && confirm
|
|
69
|
+
args
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def plain(argument) = argument.is_a?(Hash) ? argument[:value] : argument
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
require_relative "main_integration/runs"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Required by name, not relative to this file: a fuzz or model check boots a copy of this directory,
|
|
2
|
+
# and the implementation must load once, from `lib/`, not from the copy.
|
|
3
|
+
require "hecks/hecks/adapters/main_verification"
|
|
4
|
+
|
|
5
|
+
Hecks.adapter "MainVerification" do
|
|
6
|
+
port "MainVerifying"
|
|
7
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "console_capture"
|
|
4
|
+
require_relative "main_integration"
|
|
5
|
+
require_relative "main_integration/repository"
|
|
6
|
+
require_relative "main_integration/checks"
|
|
7
|
+
require_relative "main_integration/command_runner"
|
|
8
|
+
|
|
9
|
+
module Hecks
|
|
10
|
+
module Adapters
|
|
11
|
+
# The `MainVerifying` port's adapter for `Integration.Verify`: checks a landed commit in a
|
|
12
|
+
# clean detached copy of it, holding no local Hecks files, by running every check `Checks`
|
|
13
|
+
# names there. A commit other than the head of local main is answered `superseded` without a
|
|
14
|
+
# check: the run for the head verifies both. A check that fails refuses the
|
|
15
|
+
# verification, naming each failing check and what it printed last.
|
|
16
|
+
class MainVerification
|
|
17
|
+
# How much of a failing check's output a refusal keeps.
|
|
18
|
+
TAIL = 400
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
# @return [#call, nil] takes `(argv, chdir:)` and answers `[ok, output]`; the command
|
|
22
|
+
# runner when nil. A spec replaces it so no check has to run.
|
|
23
|
+
attr_accessor :runner
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param aggregate [Object, nil] unused
|
|
27
|
+
# @param settings [Hash] unused
|
|
28
|
+
# @param root [String, nil] unused
|
|
29
|
+
def initialize(aggregate: nil, settings: {}, root: nil)
|
|
30
|
+
@failures = []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param held [Hash] the `Integration` record: `landed` and `checkout`
|
|
34
|
+
# @return [Hash{Symbol => Hash}] `outcome:` (`verified` or `superseded`) and `report:`
|
|
35
|
+
# @raise [ConsoleCapture::Failure] naming every check that failed
|
|
36
|
+
def check(**held)
|
|
37
|
+
landed = plain(held[:landed]).to_s
|
|
38
|
+
repository = MainIntegration::Repository.new(plain(held[:checkout]) || Dir.pwd)
|
|
39
|
+
head = repository.main
|
|
40
|
+
return superseded(landed, head) unless head == landed
|
|
41
|
+
|
|
42
|
+
passed = run_checks(repository, landed)
|
|
43
|
+
answer("verified", "#{landed[0, 10]} verified: #{passed.join(", ")}")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def run_checks(repository, landed)
|
|
49
|
+
changed = repository.changed(repository.remote_main, landed)
|
|
50
|
+
repository.clean_copy(landed) do |directory|
|
|
51
|
+
checks = MainIntegration::Checks.new(directory, changed).all
|
|
52
|
+
failed = checks.reject { |name, argv| record(name, argv, directory) }
|
|
53
|
+
refuse!(landed) unless failed.empty?
|
|
54
|
+
checks.map(&:first)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def record(name, argv, directory)
|
|
59
|
+
ok, output = runner.call(argv, chdir: directory)
|
|
60
|
+
@failures << "#{name}: #{tail(output)}" unless ok
|
|
61
|
+
ok
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def refuse!(landed)
|
|
65
|
+
raise ConsoleCapture::Failure, "#{landed[0, 10]} failed verification: #{@failures.join("; ")}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def superseded(landed, head)
|
|
69
|
+
answer("superseded", "#{landed[0, 10]} is not the head of main (#{head[0, 10]}); the run for the head carries it")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def answer(outcome, report) = { outcome: { value: outcome }, report: { value: report } }
|
|
73
|
+
|
|
74
|
+
def tail(output)
|
|
75
|
+
text = last_lines(output).split.join(" ")
|
|
76
|
+
text.length > TAIL ? text[-TAIL..] : text
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def last_lines(output)
|
|
80
|
+
lines = output.to_s.strip.lines
|
|
81
|
+
lines.last(3).join(" ")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def runner = self.class.runner || MainIntegration::CommandRunner.new
|
|
85
|
+
|
|
86
|
+
def plain(argument) = argument.is_a?(Hash) ? argument[:value] : argument
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Required by name, not relative to this file: a fuzz or model check boots a copy of this directory,
|
|
2
|
+
# and the implementation must load once, from `lib/`, not from the copy.
|
|
3
|
+
require "hecks/hecks/adapters/push_recorder"
|
|
4
|
+
|
|
5
|
+
Hecks.adapter "PushRecorder" do
|
|
6
|
+
port "PushRecords"
|
|
7
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "shell"
|
|
4
|
+
require_relative "console_capture"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Adapters
|
|
8
|
+
# The `PushRecords` port's adapter: records how a boarding of the push train ended in the
|
|
9
|
+
# boarded checkout, by running that checkout's own launcher with `PushOutcome.record!`.
|
|
10
|
+
#
|
|
11
|
+
# The train runs in Memory in a checkout of its own; the boarded checkout's launcher boots that
|
|
12
|
+
# checkout's domain, whose own overlay says where its records live, so the outcome lands in the
|
|
13
|
+
# same journal as its landings. It records and decides nothing: the outcome is the run's
|
|
14
|
+
# status, and a recording that fails is refused, which leaves the push as it was.
|
|
15
|
+
class PushRecorder
|
|
16
|
+
# The launcher, relative to the checkout's top directory.
|
|
17
|
+
LAUNCHER = File.join("exe", "hecks")
|
|
18
|
+
|
|
19
|
+
# The fields of the run recorded as they are held, beside the ones the outcome names.
|
|
20
|
+
COPIED = %i[branch pushed commits].freeze
|
|
21
|
+
|
|
22
|
+
# @param aggregate [Object, nil] unused
|
|
23
|
+
# @param settings [Hash] `launcher:` the program to run instead of the checkout's own
|
|
24
|
+
# @param root [String, nil] unused
|
|
25
|
+
def initialize(aggregate: nil, settings: {}, root: nil)
|
|
26
|
+
@launcher = settings[:launcher]
|
|
27
|
+
@shell = Shell.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param held [Hash] the `SendTrain` record: `run`, `status`, `checkout`, `branch`, `pushed`,
|
|
31
|
+
# `commits`, and `report` or `refusal`
|
|
32
|
+
# @return [Hash{Symbol => Hash}] `report:` what the checkout's launcher said it recorded
|
|
33
|
+
# @raise [ConsoleCapture::Failure] when no checkout is named or the launcher refuses
|
|
34
|
+
def record_outcome(**held)
|
|
35
|
+
top = top_of(plain(held[:checkout]))
|
|
36
|
+
result = @shell.capture(*command(top), *arguments(held, top), "--wait", env: bundle(top), chdir: top)
|
|
37
|
+
raise ConsoleCapture::Failure, "#{top} did not record the push: #{said(result)}" unless recorded?(result)
|
|
38
|
+
|
|
39
|
+
{ report: { value: "recorded the #{plain(held[:status])} push in #{top}" } }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def top_of(checkout)
|
|
45
|
+
raise ConsoleCapture::Failure, "the boarding names no checkout to record in" if checkout.to_s.empty?
|
|
46
|
+
|
|
47
|
+
result = @shell.capture("git", "rev-parse", "--show-toplevel", chdir: checkout)
|
|
48
|
+
raise ConsoleCapture::Failure, "#{checkout} is not a checkout: #{said(result)}" unless result.ok?
|
|
49
|
+
|
|
50
|
+
result.out.strip
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def command(top) = [@launcher || File.join(top, LAUNCHER)]
|
|
54
|
+
|
|
55
|
+
# The checkout's launcher runs as it runs there: on its own Gemfile, not the train's this
|
|
56
|
+
# process inherited, and with no environment overlay, which would win over the checkout's
|
|
57
|
+
# own store.
|
|
58
|
+
def bundle(top)
|
|
59
|
+
gemfile = @launcher ? {} : { "BUNDLE_GEMFILE" => File.join(top, "Gemfile") }
|
|
60
|
+
{ "HECKS_ENVIRONMENT" => nil, **gemfile }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def arguments(held, top)
|
|
64
|
+
value = held.to_h { |name, field| [name, plain(field)] }
|
|
65
|
+
report = value[:report].nil? ? value[:refusal] : value[:report]
|
|
66
|
+
fields = { run: "#{value[:run]}-#{value[:status]}", checkout: File.join(top, ""), outcome: value[:status],
|
|
67
|
+
report: report, **value.slice(*COPIED) }
|
|
68
|
+
["PushOutcome.record!", *fields.compact.map { |name, field| "#{name}.value=#{field}" }]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def recorded?(result) = result.ok? && !result.out.include?('"refusal": "')
|
|
72
|
+
|
|
73
|
+
def said(result) = (result.err.strip.empty? ? result.out : result.err).strip[0, 300]
|
|
74
|
+
|
|
75
|
+
def plain(argument) = argument.is_a?(Hash) ? argument[:value] : argument
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "red_flag_marker"
|
|
4
|
+
require_relative "console_capture"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Adapters
|
|
8
|
+
# What a standing red flag refuses. While the flag stands, nothing lands on main and nothing is
|
|
9
|
+
# boarded on the push train except a fix for a red Work row: a branch named for a `red-*` row
|
|
10
|
+
# (with or without a `qa/` lead) lands and records its tip on the flag, and the train carries
|
|
11
|
+
# only that tip. The refusal names the flag: `main is red: flag <id>`.
|
|
12
|
+
class RedFlagGuard
|
|
13
|
+
# The Work rows that say main is red are keyed with this prefix.
|
|
14
|
+
RED_ROW = "red-"
|
|
15
|
+
|
|
16
|
+
# A branch lead the QA loop adds to a row's name.
|
|
17
|
+
QA_LEAD = "qa/"
|
|
18
|
+
|
|
19
|
+
# @param git_dir [String] the repository's common git directory
|
|
20
|
+
def initialize(git_dir)
|
|
21
|
+
@marker = RedFlagMarker.new(git_dir)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param branch [String] the branch about to land on main
|
|
25
|
+
# @return [void]
|
|
26
|
+
# @raise [ConsoleCapture::Failure] when a flag stands and the branch is not a red row's fix
|
|
27
|
+
def landing!(branch)
|
|
28
|
+
flag = @marker.read
|
|
29
|
+
return unless flag
|
|
30
|
+
return if fix?(branch)
|
|
31
|
+
|
|
32
|
+
raise ConsoleCapture::Failure, "#{refusal(flag)}; #{branch} is not a fix for a red-* Work row"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Records that a red row's fix landed, so the train may carry main's new tip.
|
|
36
|
+
#
|
|
37
|
+
# @param branch [String] the fix's branch
|
|
38
|
+
# @param tip [String] main's commit after the landing
|
|
39
|
+
# @return [void]
|
|
40
|
+
def landed(branch, tip)
|
|
41
|
+
@marker.fixed!(tip: tip, branch: branch) if @marker.read && fix?(branch)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param boarded [String] the commit the train would push
|
|
45
|
+
# @return [void]
|
|
46
|
+
# @raise [ConsoleCapture::Failure] when a flag stands and the commit is not the fix's tip
|
|
47
|
+
def boarding!(boarded)
|
|
48
|
+
flag = @marker.read
|
|
49
|
+
return unless flag
|
|
50
|
+
return if flag["fix_tip"] && flag["fix_tip"] == boarded
|
|
51
|
+
|
|
52
|
+
raise ConsoleCapture::Failure, "#{refusal(flag)}; only the tip of a red-* row's fix is boarded"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def fix?(branch) = branch.to_s.delete_prefix(QA_LEAD).start_with?(RED_ROW)
|
|
58
|
+
|
|
59
|
+
def refusal(flag) = "main is red: flag #{flag["id"]}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
# The standing red flag of one repository: a JSON file in its common git directory that exists
|
|
10
|
+
# while main is red. Landing and the push train read it, so every worktree of the
|
|
11
|
+
# repository sees the same flag and a spec's temporary repository never sees the owner's.
|
|
12
|
+
#
|
|
13
|
+
# The file holds the flag id, the main commit and CI run that raised it, the failed jobs, the
|
|
14
|
+
# red Work rows, every later red run, who checked in and what they took, and the tip of the
|
|
15
|
+
# last fix landed while the flag stood. Clearing removes the file.
|
|
16
|
+
class RedFlagMarker
|
|
17
|
+
# The marker's file name, in the common git directory.
|
|
18
|
+
FILE = "hecks-red-flag.json"
|
|
19
|
+
|
|
20
|
+
# @param git_dir [String] the repository's common git directory
|
|
21
|
+
# @param clock [#call] answers the current Time
|
|
22
|
+
def initialize(git_dir, clock: -> { Time.now })
|
|
23
|
+
@path = File.join(git_dir, FILE)
|
|
24
|
+
@clock = clock
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Hash, nil] the standing flag, or nil when main is not flagged
|
|
28
|
+
def read
|
|
29
|
+
JSON.parse(File.read(@path)) if File.exist?(@path)
|
|
30
|
+
rescue JSON::ParserError
|
|
31
|
+
{ "id" => "unreadable", "commit" => "unknown", "runs" => [], "checkins" => [] }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Raises the flag, or records another red run on the standing one.
|
|
35
|
+
#
|
|
36
|
+
# @param commit [String] the main commit whose CI run was red
|
|
37
|
+
# @param run [Hash] `ci_run:`, `jobs:`, `rows:` of that run
|
|
38
|
+
# @return [Array(Symbol, Hash)] `:raised` or `:noted`, and the flag
|
|
39
|
+
def raise!(commit:, run:)
|
|
40
|
+
flag = read
|
|
41
|
+
return [:raised, write(fresh(commit, run))] unless flag
|
|
42
|
+
|
|
43
|
+
flag["runs"] << sighting(commit, run)
|
|
44
|
+
[:noted, write(flag)]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @param session [String] who checks in
|
|
48
|
+
# @param taking [String] the Work row or cause they take
|
|
49
|
+
# @return [Hash, nil] the flag after the check-in, or nil when none stands
|
|
50
|
+
def check_in!(session:, taking:)
|
|
51
|
+
flag = read
|
|
52
|
+
return unless flag
|
|
53
|
+
|
|
54
|
+
flag["checkins"] << { "session" => session, "taking" => taking, "at" => stamp }
|
|
55
|
+
write(flag)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Records the main tip a red-row fix landed at, so the train may board it.
|
|
59
|
+
#
|
|
60
|
+
# @param tip [String] the commit main points at after the fix landed
|
|
61
|
+
# @param branch [String] the fix's branch
|
|
62
|
+
# @return [Hash, nil] the flag, or nil when none stands
|
|
63
|
+
def fixed!(tip:, branch:)
|
|
64
|
+
flag = read
|
|
65
|
+
return unless flag
|
|
66
|
+
|
|
67
|
+
flag["fix_tip"] = tip
|
|
68
|
+
flag["fix_branch"] = branch
|
|
69
|
+
write(flag)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Lowers the flag.
|
|
73
|
+
#
|
|
74
|
+
# @return [Hash, nil] the flag that was standing, or nil when none was
|
|
75
|
+
def clear!
|
|
76
|
+
flag = read
|
|
77
|
+
File.delete(@path) if flag
|
|
78
|
+
flag
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def fresh(commit, run)
|
|
84
|
+
{ "id" => "flag-#{commit.to_s[0, 10]}", "commit" => commit, "raised_at" => stamp, "runs" => [sighting(commit, run)],
|
|
85
|
+
"checkins" => [] }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def sighting(commit, run)
|
|
89
|
+
{ "commit" => commit, "ci_run" => run[:ci_run].to_s, "jobs" => run[:jobs].to_s, "rows" => run[:rows].to_s, "at" => stamp }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def write(flag)
|
|
93
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
94
|
+
File.write(@path, JSON.pretty_generate(flag))
|
|
95
|
+
flag
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def stamp = @clock.call.utc.iso8601
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
# Says a standing red flag in words: its id, the commit, how long main has been red, the failed
|
|
8
|
+
# jobs, and who has checked in to help and what they took. The pipeline board and the status
|
|
9
|
+
# query print the same sentence.
|
|
10
|
+
class RedFlagReport
|
|
11
|
+
# @param flag [Hash] the marker's content
|
|
12
|
+
# @param now [Time] the time the elapsed red time is measured to
|
|
13
|
+
def initialize(flag, now: Time.now)
|
|
14
|
+
@flag = flag
|
|
15
|
+
@now = now
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [Integer] seconds main has been red
|
|
19
|
+
def elapsed_seconds
|
|
20
|
+
[(@now - Time.iso8601(@flag.fetch("raised_at"))).to_i, 0].max
|
|
21
|
+
rescue ArgumentError, KeyError
|
|
22
|
+
0
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [String] the whole sentence, one line per fact
|
|
26
|
+
def text
|
|
27
|
+
[headline, jobs_line, rows_line, *helper_lines].compact.join("\n")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [String] the one-line form for a tile
|
|
31
|
+
def headline
|
|
32
|
+
"main is red: #{@flag["id"]} at #{@flag["commit"].to_s[0, 10]}, red for #{elapsed_seconds / 60} min, " \
|
|
33
|
+
"#{@flag["runs"].to_a.size} red run(s), #{@flag["checkins"].to_a.size} helping"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def jobs_line
|
|
39
|
+
jobs = seen("jobs")
|
|
40
|
+
"failing jobs: #{jobs.join("; ")}" unless jobs.empty?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def rows_line
|
|
44
|
+
rows = seen("rows")
|
|
45
|
+
"red rows: #{rows.join(", ")}" unless rows.empty?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The distinct, non-empty values of one field across the red runs.
|
|
49
|
+
def seen(field)
|
|
50
|
+
values = @flag["runs"].to_a.map { |run| run[field].to_s }
|
|
51
|
+
values.reject(&:empty?).uniq
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def helper_lines
|
|
55
|
+
@flag["checkins"].to_a.map { |entry| "helping: #{entry["session"]} takes #{entry["taking"]}" }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "console_capture"
|
|
5
|
+
require_relative "main_integration/command_runner"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
# Reads the newest CI run of a push to main through `gh` and says whether main is red, green or
|
|
10
|
+
# still being judged.
|
|
11
|
+
class RedFlagSurvey
|
|
12
|
+
# The workflow whose run is read.
|
|
13
|
+
WORKFLOW = "CI"
|
|
14
|
+
|
|
15
|
+
# Job conclusions that are not a failure.
|
|
16
|
+
CLEAN = %w[success skipped neutral].freeze
|
|
17
|
+
|
|
18
|
+
# @param runner [#call, nil] takes `(argv, chdir:)` and answers `[ok, output]`
|
|
19
|
+
# @param checkout [String] a directory in the repository
|
|
20
|
+
def initialize(runner:, checkout:)
|
|
21
|
+
@runner = runner || MainIntegration::CommandRunner.new
|
|
22
|
+
@checkout = checkout
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [Hash] `verdict:` (`:red`, `:green` or `:running`), `commit:` and `detail:` in the
|
|
26
|
+
# words `RedFlagging` reads a run id and failed jobs from
|
|
27
|
+
# @raise [ConsoleCapture::Failure] when `gh` cannot be read
|
|
28
|
+
def latest
|
|
29
|
+
run = gh("run", "list", "--workflow", WORKFLOW, "--branch", "main", "--event", "push", "--limit", "1",
|
|
30
|
+
"--json", "databaseId,status,conclusion,headSha").first
|
|
31
|
+
return { verdict: :running, commit: "", detail: "no #{WORKFLOW} run of main yet" } unless run
|
|
32
|
+
return running(run) unless run["status"] == "completed"
|
|
33
|
+
|
|
34
|
+
judged(run)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def running(run) = { verdict: :running, commit: run["headSha"], detail: "run #{run["databaseId"]} is #{run["status"]}" }
|
|
40
|
+
|
|
41
|
+
def judged(run)
|
|
42
|
+
id = run["databaseId"]
|
|
43
|
+
return { verdict: :green, commit: run["headSha"], detail: "run #{id} passed" } if run["conclusion"] == "success"
|
|
44
|
+
|
|
45
|
+
said = "#{run["headSha"][0, 10]}: run #{id} #{run["conclusion"]}; failed jobs: #{failed(id)}"
|
|
46
|
+
{ verdict: :red, commit: run["headSha"], detail: said }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def failed(id)
|
|
50
|
+
jobs = gh("run", "view", id.to_s, "--json", "jobs").fetch("jobs", [])
|
|
51
|
+
names = jobs.reject { |job| CLEAN.include?(job["conclusion"].to_s) }.map { |job| job["name"] }
|
|
52
|
+
names.empty? ? "none named" : names.join(", ")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def gh(*args)
|
|
56
|
+
ok, output = @runner.call(["gh", *args], chdir: @checkout)
|
|
57
|
+
raise ConsoleCapture::Failure, "gh #{args.first(2).join(" ")} failed: #{output.to_s.strip}" unless ok
|
|
58
|
+
|
|
59
|
+
JSON.parse(output)
|
|
60
|
+
rescue JSON::ParserError
|
|
61
|
+
raise ConsoleCapture::Failure, "gh #{args.first(2).join(" ")} answered no JSON"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Required by name, not relative to this file: a fuzz or model check boots a copy of this directory,
|
|
2
|
+
# and the implementation must load once, from `lib/`, not from the copy.
|
|
3
|
+
require "hecks/hecks/adapters/red_flagging"
|
|
4
|
+
|
|
5
|
+
Hecks.adapter "RedFlagging" do
|
|
6
|
+
port "Flagging"
|
|
7
|
+
end
|