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,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "git_landing"
|
|
4
|
+
require_relative "landed_branches"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Adapters
|
|
8
|
+
# The `Leftovers` port's adapter: after a push, lists the worktrees of branches that landed and
|
|
9
|
+
# are still there, each with what a retirement would do. It removes nothing, so a session still
|
|
10
|
+
# working in one is never moved; a person or agent retires what the list says is safe.
|
|
11
|
+
#
|
|
12
|
+
# A branch counts only if `Landing` recorded it as landed, so a worktree that was merely opened
|
|
13
|
+
# is never named. What a retirement would do is `GitLanding::Tidy`'s own verdict, so the list
|
|
14
|
+
# and `Landing.retire!` cannot disagree.
|
|
15
|
+
class GitLeftovers
|
|
16
|
+
include GitWorktrees
|
|
17
|
+
|
|
18
|
+
# Accepts the arguments every driven adapter is built with; only the registry is kept.
|
|
19
|
+
#
|
|
20
|
+
# @param aggregate [Object, nil] unused
|
|
21
|
+
# @param settings [Hash] unused
|
|
22
|
+
# @param root [String, nil] unused
|
|
23
|
+
# @param registry [Runtime::Registry, nil] where the landed `Landing` rows are read
|
|
24
|
+
def initialize(aggregate: nil, settings: {}, root: nil, registry: nil)
|
|
25
|
+
@git = Git.new
|
|
26
|
+
@registry = registry
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param held [Hash] the `SendTrain` record: `checkout` (any directory in the repository)
|
|
30
|
+
# @return [Hash{Symbol => Hash}] `report:` one line per such worktree, or that none is left
|
|
31
|
+
# @raise [ConsoleCapture::Failure] when the checkout is not a repository
|
|
32
|
+
def leftovers(**held)
|
|
33
|
+
settle(held)
|
|
34
|
+
lines = LandedBranches.of(@registry).filter_map { |branch| line(branch) }
|
|
35
|
+
{ report: { value: lines.empty? ? "no landed worktree is left" : lines.join("\n") } }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def line(branch)
|
|
41
|
+
return unless worktree_on(branch)
|
|
42
|
+
|
|
43
|
+
"#{branch}: #{GitLanding::Tidy.new(@git, @checkout, branch: branch, onto: "main").verdict}"
|
|
44
|
+
rescue ConsoleCapture::Failure => e
|
|
45
|
+
"#{branch}: could not be checked (#{e.message})"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -91,8 +91,8 @@ module Hecks
|
|
|
91
91
|
lacking = count("#{branch_tip(branch)}..#{REMOTE_MAIN}")
|
|
92
92
|
return if lacking.zero? || ancestor?(branch_tip(branch), branch_tip("main"))
|
|
93
93
|
|
|
94
|
-
raise ConsoleCapture::Failure, "
|
|
95
|
-
"pull main first (LinkedTree.pull_main!) and rebase the branch"
|
|
94
|
+
raise ConsoleCapture::Failure, "#{branch} lacks #{lacking} commit#{"s" unless lacking == 1} that origin/main has: " \
|
|
95
|
+
"pull main first (LinkedTree.pull_main!) and rebase the branch onto it"
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
98
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "git"
|
|
4
4
|
require_relative "console_capture"
|
|
5
5
|
require_relative "push_size"
|
|
6
|
+
require_relative "red_flag_guard"
|
|
6
7
|
require_relative "git_train/reading"
|
|
7
8
|
require_relative "git_train/bench"
|
|
8
9
|
require_relative "git_train/local_branch"
|
|
@@ -88,7 +89,9 @@ module Hecks
|
|
|
88
89
|
|
|
89
90
|
# The commit the branch names as the boarding begins, read once the line is held.
|
|
90
91
|
def boarded_commit(held)
|
|
91
|
-
Carriage.new(@git, @checkout, branch: branch_of(held), remote: remote_of(held)).local_head
|
|
92
|
+
head = Carriage.new(@git, @checkout, branch: branch_of(held), remote: remote_of(held)).local_head
|
|
93
|
+
RedFlagGuard.new(read("rev-parse", "--path-format=absolute", "--git-common-dir")).boarding!(head)
|
|
94
|
+
head
|
|
92
95
|
end
|
|
93
96
|
|
|
94
97
|
# Brings the branch up to its remote and settles the shape ledger, then carries the settled
|
|
@@ -106,7 +109,7 @@ module Hecks
|
|
|
106
109
|
def push_size(held)
|
|
107
110
|
ahead = read("rev-list", "--count", "#{remote_of(held)}/#{branch_of(held)}..#{branch_of(held)}").to_i
|
|
108
111
|
sentence = PushSize.sentence(ahead, PushSize.thresholds(@registry))
|
|
109
|
-
sentence.empty? ? "pushing #{ahead} commits" : "pushing #{ahead} commits; #{sentence}"
|
|
112
|
+
[ahead, sentence.empty? ? "pushing #{ahead} commits" : "pushing #{ahead} commits; #{sentence}"]
|
|
110
113
|
end
|
|
111
114
|
|
|
112
115
|
def branch_of(held) = plain(held[:branch]) || "main"
|
|
@@ -115,11 +118,12 @@ module Hecks
|
|
|
115
118
|
|
|
116
119
|
# The carriage's answer, its report opened with what the mint did and naming the boarded
|
|
117
120
|
# commit (and the settled one when settlement moved the branch).
|
|
118
|
-
def after_mint(minted, boarded, carried, size
|
|
121
|
+
def after_mint(minted, boarded, carried, size)
|
|
119
122
|
answer, settled = carried
|
|
123
|
+
commits, said = size
|
|
120
124
|
named = settled == boarded ? "boarded #{boarded[0, 9]}" : "boarded #{boarded[0, 9]}, settled to #{settled[0, 9]}"
|
|
121
|
-
report = "#{
|
|
122
|
-
answer.merge(boarded: { value: boarded }, report: { value: report })
|
|
125
|
+
report = "#{said}\n#{minted}, then #{named}: #{answer.dig(:report, :value)}"
|
|
126
|
+
answer.merge(boarded: { value: boarded }, commits: { value: commits }, report: { value: report })
|
|
123
127
|
end
|
|
124
128
|
|
|
125
129
|
# A git hook takes no arguments, so the argument reaches the pre-push hook as git
|
|
@@ -5,11 +5,13 @@ require_relative "git_main_pull"
|
|
|
5
5
|
|
|
6
6
|
module Hecks
|
|
7
7
|
module Adapters
|
|
8
|
-
# The `WorktreeAdding` port's adapter for `Worktree`: opens a linked worktree on a new branch
|
|
8
|
+
# The `WorktreeAdding` port's adapter for `Worktree`: opens a linked worktree on a new branch
|
|
9
|
+
# cut from
|
|
9
10
|
# `main` (pulled first, see {GitMainPull}), at the path named or under the checkout's
|
|
10
11
|
# `.claude/worktrees`.
|
|
11
12
|
#
|
|
12
|
-
# It never checks out `main` in a worktree, never pushes, and never forces. A branch that
|
|
13
|
+
# It never checks out `main` in a worktree, never pushes, and never forces. A branch that
|
|
14
|
+
# already exists
|
|
13
15
|
# is declined by git, and the reason travels back on the record.
|
|
14
16
|
class GitWorktreeAdd
|
|
15
17
|
include GitWorktrees
|
|
@@ -26,10 +28,12 @@ module Hecks
|
|
|
26
28
|
|
|
27
29
|
# Opens the worktree, then reads the sha it stands on.
|
|
28
30
|
#
|
|
29
|
-
# @param held [Hash] the `Worktree` record: `checkout` (a directory inside the repository; the
|
|
31
|
+
# @param held [Hash] the `Worktree` record: `checkout` (a directory inside the repository; the
|
|
32
|
+
# current
|
|
30
33
|
# directory when absent), `branch` (a new branch name), and `path` (optional)
|
|
31
34
|
# @return [Hash{Symbol => Hash}] `sha:` the commit it stands on, `report:` where it is
|
|
32
|
-
# @raise [ConsoleCapture::Failure] on a missing branch, a branch named main, or a directory
|
|
35
|
+
# @raise [ConsoleCapture::Failure] on a missing branch, a branch named main, or a directory
|
|
36
|
+
# outside a repository
|
|
33
37
|
def create(**held)
|
|
34
38
|
repo = settle(held)
|
|
35
39
|
branch = branch_named(held)
|
|
@@ -27,7 +27,8 @@ module Hecks
|
|
|
27
27
|
|
|
28
28
|
# Opens a pull request from a branch onto a base, its title and body taken from the commits.
|
|
29
29
|
#
|
|
30
|
-
# @param held [Hash] the `PullRequest` record: `branch` (required), `base` (`main` when
|
|
30
|
+
# @param held [Hash] the `PullRequest` record: `branch` (required), `base` (`main` when
|
|
31
|
+
# absent)
|
|
31
32
|
# @return [Hash{Symbol => Hash}] `outcome:` (`opened`), `number:`, `url:` and `report:`
|
|
32
33
|
# @raise [ConsoleCapture::Failure] when no branch is named, or GitHub refuses
|
|
33
34
|
def create(**held)
|
|
@@ -16,7 +16,8 @@ module Hecks
|
|
|
16
16
|
# the asks of `ModelCheckRun` and `Operation`, plus the `Follow` query.
|
|
17
17
|
#
|
|
18
18
|
# An ask is handed the whole record it was made on (its `held_state`), so each method reads the
|
|
19
|
-
# record's fields, runs the existing `Hecks::Tools::*` entry point captured, and answers the
|
|
19
|
+
# record's fields, runs the existing `Hecks::Tools::*` entry point captured, and answers the
|
|
20
|
+
# text
|
|
20
21
|
# it printed. An answer records a success; a raise records a refusal, so a check that finds
|
|
21
22
|
# something, or a run that fails, is refused with the report as its reason.
|
|
22
23
|
module InProcessOperations
|
|
@@ -86,7 +86,7 @@ module Hecks
|
|
|
86
86
|
return verdict.edge if verdict.verdict == "would_mint"
|
|
87
87
|
|
|
88
88
|
raise Runtime::NotFound, "no translation edge leads #{latest[:label]} to #{label} " \
|
|
89
|
-
"(the decision is #{verdict.verdict}) — run `hecks
|
|
89
|
+
"(the decision is #{verdict.verdict}) — run `hecks era.scaffold_translation` first"
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
# The registered translations that leave the latest era.
|
|
@@ -11,7 +11,8 @@ module Hecks
|
|
|
11
11
|
# it would remove.
|
|
12
12
|
#
|
|
13
13
|
# @param domain [Hash, String] the domain directory
|
|
14
|
-
# @return [Hash] `text:` the edge file's text, or the sentence that says nothing is to be
|
|
14
|
+
# @return [Hash] `text:` the edge file's text, or the sentence that says nothing is to be
|
|
15
|
+
# written
|
|
15
16
|
# @raise [Runtime::NotFound] if the domain holds no eras, cannot be loaded, or its chain
|
|
16
17
|
# cannot be composed
|
|
17
18
|
def compose_translation(domain:)
|
|
@@ -4,7 +4,8 @@ module Hecks
|
|
|
4
4
|
module Adapters
|
|
5
5
|
class JournalStore
|
|
6
6
|
module TranslationWrite
|
|
7
|
-
# The edge for a shape that changed under an unchanged label: the scaffold's diff, written
|
|
7
|
+
# The edge for a shape that changed under an unchanged label: the scaffold's diff, written
|
|
8
|
+
# from
|
|
8
9
|
# the label to itself. Only `TranslationWrite` includes it.
|
|
9
10
|
module ShapeEdge
|
|
10
11
|
# A line of an edge file that still marks a field as undecided.
|
|
@@ -13,7 +14,8 @@ module Hecks
|
|
|
13
14
|
private
|
|
14
15
|
|
|
15
16
|
# A held label that equals the current one with a different projection: the edge is the
|
|
16
|
-
# scaffold's diff, from the label to itself. A field the diff cannot decide refuses the
|
|
17
|
+
# scaffold's diff, from the label to itself. A field the diff cannot decide refuses the
|
|
18
|
+
# write.
|
|
17
19
|
#
|
|
18
20
|
# @raise [Runtime::WiringError] when the diff leaves a field or an aggregate to decide
|
|
19
21
|
def shape_edge_plan(domain, latest, current)
|
|
@@ -16,7 +16,8 @@ module Hecks
|
|
|
16
16
|
# `translations/` directory.
|
|
17
17
|
module TranslationWrite
|
|
18
18
|
# What a write would do: the file it writes and its text, the stale files it deletes, and
|
|
19
|
-
# the shape labels the chain passes through. `message` alone answers when nothing is to be
|
|
19
|
+
# the shape labels the chain passes through. `message` alone answers when nothing is to be
|
|
20
|
+
# done.
|
|
20
21
|
Plan = Struct.new(:message, :path, :text, :removals, :labels, keyword_init: true)
|
|
21
22
|
|
|
22
23
|
include ShapeEdge
|
|
@@ -83,7 +84,8 @@ module Hecks
|
|
|
83
84
|
|
|
84
85
|
# Writes the composed edge and deletes the stale ones; returns what each file held before.
|
|
85
86
|
#
|
|
86
|
-
# @return [Hash{String => String, nil}] each touched path with its earlier text, nil if
|
|
87
|
+
# @return [Hash{String => String, nil}] each touched path with its earlier text, nil if
|
|
88
|
+
# absent
|
|
87
89
|
def write_translation!(plan)
|
|
88
90
|
originals = [plan.path, *plan.removals].to_h { |path| [path, read_if_present(path)] }
|
|
89
91
|
File.write(plan.path, plan.text)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "started_work"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
# The branches a booted registry holds a landed `Landing` row for. The `Leftovers` port reads
|
|
8
|
+
# them to know which worktrees came from a landing, so a branch that was only opened is never
|
|
9
|
+
# listed. It writes nothing.
|
|
10
|
+
module LandedBranches
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# @param registry [Runtime::Registry, nil] the booted registry; nil reads no rows
|
|
14
|
+
# @return [Array<String>] each landed branch once, in the order its rows come
|
|
15
|
+
def of(registry)
|
|
16
|
+
aggregate = landing_aggregate(registry)
|
|
17
|
+
return [] unless aggregate
|
|
18
|
+
|
|
19
|
+
rows = registry.repository("Hecks", aggregate).all
|
|
20
|
+
rows.filter_map { |record| branch(record) }.uniq
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Object, nil] the Landing aggregate, nil when no Hecks bluebook is booted
|
|
24
|
+
def landing_aggregate(registry)
|
|
25
|
+
bluebook = registry&.bluebook("Hecks")
|
|
26
|
+
bluebook&.aggregates&.find { |candidate| candidate.hecks_name == "Landing" }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [String, nil] the row's branch, nil unless the row landed and names one
|
|
30
|
+
def branch(record)
|
|
31
|
+
return unless StartedWork.value(record, :status) == "landed"
|
|
32
|
+
|
|
33
|
+
name = StartedWork.value(record, :branch).to_s
|
|
34
|
+
name unless name.empty?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
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_boarding"
|
|
4
|
+
|
|
5
|
+
Hecks.adapter "MainBoarding" do
|
|
6
|
+
port "MainBoarding"
|
|
7
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
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/range_guard"
|
|
7
|
+
require_relative "main_integration/train"
|
|
8
|
+
require_relative "main_integration/command_runner"
|
|
9
|
+
|
|
10
|
+
module Hecks
|
|
11
|
+
module Adapters
|
|
12
|
+
# The `MainBoarding` port's adapter for `Integration.Board`: pushes a verified commit of main
|
|
13
|
+
# by boarding the push train in the train's own copy, and answers the commit the remote holds.
|
|
14
|
+
#
|
|
15
|
+
# It refuses, naming why, a run that was not verified, a boarding while another run is being
|
|
16
|
+
# pushed or watched, and a range carrying an agent's attribution trailer, a release approval or
|
|
17
|
+
# a version change. A commit other than the head of local main is answered `superseded`: the
|
|
18
|
+
# run for the head pushes it. It never runs a plain push, a force, a skipped hook or the
|
|
19
|
+
# owner's override: the train's command line is fixed and the override is not handed on.
|
|
20
|
+
class MainBoarding
|
|
21
|
+
class << self
|
|
22
|
+
# @return [#call, nil] takes `(argv, chdir:)` and answers `[ok, output]`; the command
|
|
23
|
+
# runner when nil. A spec replaces it so nothing is pushed.
|
|
24
|
+
attr_accessor :runner
|
|
25
|
+
|
|
26
|
+
# @return [String, nil] the train's copy; `Train::ROOT` when nil
|
|
27
|
+
attr_accessor :train_root
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param registry [Runtime::Registry, nil] the booted registry whose runs are read
|
|
31
|
+
# @param aggregate [Object, nil] unused
|
|
32
|
+
def initialize(registry: nil, aggregate: nil)
|
|
33
|
+
@registry = registry
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param held [Hash] the `Integration` record: `landed` and `checkout`
|
|
37
|
+
# @return [Hash{Symbol => Hash}] `outcome:` (`pushed` or `superseded`), `pushed:`, `report:`
|
|
38
|
+
# @raise [ConsoleCapture::Failure] on any refusal named above, or when the train did not push
|
|
39
|
+
def embark(**held)
|
|
40
|
+
landed = plain(held[:landed]).to_s
|
|
41
|
+
refuse_unboardable!(landed)
|
|
42
|
+
repository = MainIntegration::Repository.new(checkout_of(held))
|
|
43
|
+
head = repository.main
|
|
44
|
+
return superseded(landed, head) unless head == landed
|
|
45
|
+
|
|
46
|
+
MainIntegration::RangeGuard.new(repository).check!(repository.remote_main, landed)
|
|
47
|
+
push(landed, checkout_of(held))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def checkout_of(held) = plain(held[:checkout]) || Dir.pwd
|
|
53
|
+
|
|
54
|
+
def push(landed, checkout)
|
|
55
|
+
train = MainIntegration::Train.new(runner, root: self.class.train_root || MainIntegration::Train::ROOT)
|
|
56
|
+
note = train.refresh
|
|
57
|
+
record = train.board(checkout)
|
|
58
|
+
pushed = [record["pushed"], record["boarded"], landed].compact.first
|
|
59
|
+
{ outcome: { value: "pushed" }, pushed: { value: pushed.to_s },
|
|
60
|
+
report: { value: "#{landed[0, 10]} #{record["status"]} as #{pushed.to_s[0, 10]}; #{note}" } }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The run must have come to boarding from verified (the saga moved it as the boarding
|
|
64
|
+
# began), and no other run may be being pushed or watched.
|
|
65
|
+
def refuse_unboardable!(landed)
|
|
66
|
+
runs = MainIntegration::Runs.new(@registry)
|
|
67
|
+
run = runs.find(landed) or refuse!("no integration run is keyed by #{landed[0, 10]}")
|
|
68
|
+
refuse!("#{run.short} is #{run.state}, not verified") unless run.state == "boarding"
|
|
69
|
+
other = runs.in_flight(except: landed).first
|
|
70
|
+
refuse!("#{other.short} is #{other.state}; one run boards at a time") if other
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def refuse!(why) = raise(ConsoleCapture::Failure, "boarding refused: #{why}")
|
|
74
|
+
|
|
75
|
+
def superseded(landed, head)
|
|
76
|
+
report = "#{landed[0, 10]} is not the head of main (#{head[0, 10]}); the run for the head pushes it"
|
|
77
|
+
{ outcome: { value: "superseded" }, report: { value: report } }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def runner = self.class.runner || MainIntegration::CommandRunner.new
|
|
81
|
+
|
|
82
|
+
def plain(argument) = argument.is_a?(Hash) ? argument[:value] : argument
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../git_rehearsal/spec_map"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
class MainIntegration
|
|
8
|
+
# The checks a verification runs in a clean copy of a landed commit, each a launcher command
|
|
9
|
+
# run there: the quick gate, the specs the changes touch (the changed spec files and the
|
|
10
|
+
# specs `SpecMap` names for the other changes, in batches of at most `BATCH`), the command
|
|
11
|
+
# names, the CI pipeline and the generated corpus, the last two only compared. Nothing here
|
|
12
|
+
# runs the full spec suite.
|
|
13
|
+
class Checks
|
|
14
|
+
# The most spec files one launcher call runs, the local cap of the `test_scope` setting.
|
|
15
|
+
BATCH = 25
|
|
16
|
+
|
|
17
|
+
# The most spec files one verification runs; the rest run in CI.
|
|
18
|
+
LIMIT = 100
|
|
19
|
+
|
|
20
|
+
# The launcher, as the copy holds it.
|
|
21
|
+
LAUNCHER = "exe/hecks"
|
|
22
|
+
|
|
23
|
+
# The checks that do not depend on what changed, by name.
|
|
24
|
+
FIXED = {
|
|
25
|
+
"quick gate" => %w[gate_run.gate! stage=quick --wait],
|
|
26
|
+
"command names" => %w[conformance_run.check_command_names! --wait],
|
|
27
|
+
"ci pipeline" => %w[regeneration_run.project_ci_pipeline! check.value=true --wait],
|
|
28
|
+
"corpus" => %w[regeneration_run.regenerate_corpus! check.value=true --wait]
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
# @param directory [String] the clean copy the checks run in
|
|
32
|
+
# @param changed [Array<String>] the paths the landed range changed, relative to the copy
|
|
33
|
+
def initialize(directory, changed)
|
|
34
|
+
@directory = directory
|
|
35
|
+
@changed = changed
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Array<Array(String, Array<String>)>] each check's name and its command line
|
|
39
|
+
def all
|
|
40
|
+
named = FIXED.map { |name, args| [name, [LAUNCHER, *args]] }
|
|
41
|
+
named.first(1) + spec_batches + named.drop(1)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Array<String>] the spec files the checks run, at most `LIMIT`
|
|
45
|
+
def specs
|
|
46
|
+
own = @changed.select { |path| path.end_with?("_spec.rb") && File.file?(File.join(@directory, path)) }
|
|
47
|
+
mapped = GitRehearsal::SpecMap.new(@directory).map(@changed, limit: LIMIT).specs
|
|
48
|
+
(own | mapped).first(LIMIT)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def spec_batches
|
|
54
|
+
batches = specs.each_slice(BATCH).to_a
|
|
55
|
+
batches.each_with_index.map do |batch, index|
|
|
56
|
+
["specs #{index + 1}/#{batches.size}",
|
|
57
|
+
[LAUNCHER, "test_suite_run.run_spec_files!", "files.value=#{batch.join(" ")}", "--wait"]]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hecks/vendoring/git_environment"
|
|
4
|
+
require_relative "../shell"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Adapters
|
|
8
|
+
class MainIntegration
|
|
9
|
+
# Runs one command in a directory for an integration step, with the variables that would
|
|
10
|
+
# point it at another checkout, or let it push as the owner, left out. It answers whether
|
|
11
|
+
# the command ended well and what it printed.
|
|
12
|
+
class CommandRunner
|
|
13
|
+
# The variables of the calling process that are never handed on: Bundler's, which would
|
|
14
|
+
# run the command against the calling checkout, and the owner's push override.
|
|
15
|
+
DROPPED = %w[BUNDLE_GEMFILE BUNDLE_BIN_PATH RUBYOPT RUBYLIB HECKS_OWNER_PUSH].freeze
|
|
16
|
+
|
|
17
|
+
# @param argv [Array<String>] the command and its arguments
|
|
18
|
+
# @param chdir [String] the directory it runs in
|
|
19
|
+
# @return [Array(Boolean, String)] whether it ended with status zero, and what it printed:
|
|
20
|
+
# its standard output when it ended well, both streams when it did not
|
|
21
|
+
def call(argv, chdir:)
|
|
22
|
+
result = Shell.new.capture(*argv, env: environment, chdir: chdir)
|
|
23
|
+
[result.ok?, result.ok? ? result.out : "#{result.out}#{result.err}"]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def environment = Vendoring::GitEnvironment.clean.merge(DROPPED.to_h { |name| [name, nil] })
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../console_capture"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
class MainIntegration
|
|
8
|
+
# What a boarding refuses to push, read from the range origin/main does not hold: a commit
|
|
9
|
+
# message with an agent's attribution trailer or a release approval, and a change to the
|
|
10
|
+
# version file, which is the owner's release decision. Each refusal names what it found.
|
|
11
|
+
class RangeGuard
|
|
12
|
+
# The message lines that stop a boarding, each with the name its refusal gives it.
|
|
13
|
+
FORBIDDEN_LINES = {
|
|
14
|
+
"a Co-Authored-By: Claude trailer" => /^Co-Authored-By:.*Claude/i,
|
|
15
|
+
"a Claude-Session trailer" => /^Claude-Session:/i,
|
|
16
|
+
"a Release-Approved-By line" => /^Release-Approved-By:/i
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
# The file whose change is a release, never boarded by an integration.
|
|
20
|
+
VERSION_FILE = "lib/hecks/version.rb"
|
|
21
|
+
|
|
22
|
+
# @param repository [Repository] the checkout the range is read in
|
|
23
|
+
def initialize(repository)
|
|
24
|
+
@repository = repository
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param base [String] the commit the remote holds
|
|
28
|
+
# @param tip [String] the commit a boarding would push
|
|
29
|
+
# @return [void]
|
|
30
|
+
# @raise [ConsoleCapture::Failure] naming the first forbidden line or the version change
|
|
31
|
+
def check!(base, tip)
|
|
32
|
+
found = forbidden_line(@repository.messages(base, tip))
|
|
33
|
+
refuse!(tip, "carries #{found}") if found
|
|
34
|
+
refuse!(tip, "changes #{VERSION_FILE}, a release") if @repository.changed(base, tip).include?(VERSION_FILE)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def forbidden_line(messages)
|
|
40
|
+
FORBIDDEN_LINES.find { |_name, pattern| messages.any? { |message| message.match?(pattern) } }&.first
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def refuse!(tip, why)
|
|
44
|
+
raise ConsoleCapture::Failure, "boarding refused: the range up to #{tip[0, 10]} #{why}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require_relative "../git"
|
|
5
|
+
require_relative "../console_capture"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
class MainIntegration
|
|
10
|
+
# The checkout a run landed in, as an integration step reads it: where local main and
|
|
11
|
+
# origin/main stand, what a range changed and says, and a clean detached copy of a commit.
|
|
12
|
+
class Repository
|
|
13
|
+
# The prefix of the scratch directory a clean copy is made in.
|
|
14
|
+
COPY_PREFIX = "hecks-main-integration"
|
|
15
|
+
|
|
16
|
+
# The files a development checkout keeps for itself and a clean copy must not hold.
|
|
17
|
+
LOCAL_FILES = "lib/**/*.local.{hecksagon,world}"
|
|
18
|
+
|
|
19
|
+
# @param checkout [String] a directory inside the repository
|
|
20
|
+
def initialize(checkout)
|
|
21
|
+
@checkout = checkout
|
|
22
|
+
@git = Git.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param ref [String] a branch or commit
|
|
26
|
+
# @return [String, nil] the full commit it names, nil when it names none
|
|
27
|
+
def commit(ref)
|
|
28
|
+
done = @git.capture("rev-parse", "--verify", "--quiet", "#{ref}^{commit}", chdir: @checkout)
|
|
29
|
+
done.ok? ? done.out.strip : nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [String] the commit local main is at
|
|
33
|
+
# @raise [ConsoleCapture::Failure] when the checkout has no main
|
|
34
|
+
def main = commit("main") || raise(ConsoleCapture::Failure, "#{@checkout} has no main branch")
|
|
35
|
+
|
|
36
|
+
# @return [String] the commit origin/main is at, as last fetched; main's when none is
|
|
37
|
+
def remote_main = commit("origin/main") || main
|
|
38
|
+
|
|
39
|
+
# @param base [String] the commit the range starts after
|
|
40
|
+
# @param tip [String] the commit it ends at
|
|
41
|
+
# @return [Array<String>] the paths the range changed
|
|
42
|
+
def changed(base, tip) = read("diff", "--name-only", base, tip).split("\n").reject(&:empty?)
|
|
43
|
+
|
|
44
|
+
# @return [Array<String>] the message of every commit in the range
|
|
45
|
+
def messages(base, tip) = read("log", "--format=%B%x1e", "#{base}..#{tip}").split("\x1e").map(&:strip)
|
|
46
|
+
|
|
47
|
+
# Yields a clean detached copy of a commit, holding only what the commit tracks, and
|
|
48
|
+
# removes it once the block ends.
|
|
49
|
+
#
|
|
50
|
+
# @param tip [String] the commit to copy
|
|
51
|
+
# @yieldparam directory [String] the copy
|
|
52
|
+
# @return [Object] what the block answered
|
|
53
|
+
def clean_copy(tip)
|
|
54
|
+
Dir.mktmpdir(COPY_PREFIX) do |scratch|
|
|
55
|
+
directory = File.join(scratch, "main")
|
|
56
|
+
read("worktree", "add", "--quiet", "--detach", directory, tip)
|
|
57
|
+
begin
|
|
58
|
+
yield refuse_local_files!(directory)
|
|
59
|
+
ensure
|
|
60
|
+
@git.capture("worktree", "remove", "--force", directory, chdir: @checkout)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def refuse_local_files!(directory)
|
|
68
|
+
found = Dir.glob(LOCAL_FILES, base: directory)
|
|
69
|
+
raise ConsoleCapture::Failure, "the clean copy holds local files: #{found.join(", ")}" unless found.empty?
|
|
70
|
+
|
|
71
|
+
directory
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def read(*args)
|
|
75
|
+
done = @git.capture(*args, chdir: @checkout)
|
|
76
|
+
raise ConsoleCapture::Failure, "git #{args.first} in #{@checkout}: #{done.err.strip}" unless done.ok?
|
|
77
|
+
|
|
78
|
+
done.out
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Adapters
|
|
5
|
+
class MainIntegration
|
|
6
|
+
# The open and finished runs of the MainIntegration saga, read from the booted registry:
|
|
7
|
+
# each landed commit, the stage its run is in and the checkout it landed in. Writes nothing.
|
|
8
|
+
class Runs
|
|
9
|
+
# The saga whose instances are read.
|
|
10
|
+
SAGA = "MainIntegration"
|
|
11
|
+
|
|
12
|
+
# The stages in which a run is being pushed or watched; only one run may be in them.
|
|
13
|
+
IN_FLIGHT = %w[boarding pushed ci_running].freeze
|
|
14
|
+
|
|
15
|
+
# One run: the landed commit it is keyed by, its stage, and the checkout it landed in.
|
|
16
|
+
Run = Struct.new(:landed, :state, :checkout) do
|
|
17
|
+
# @return [String] the commit's first ten characters, for a report
|
|
18
|
+
def short = landed[0, 10]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param registry [Runtime::Registry] the booted registry whose saga instances are read
|
|
22
|
+
def initialize(registry)
|
|
23
|
+
@registry = registry
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Array<Run>] every run the saga remembers, in the order they started
|
|
27
|
+
def all
|
|
28
|
+
instances.map { |landed, instance| Run.new(landed.to_s, instance[:state].to_s, checkout_of(instance)) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param landed [String] the landed commit
|
|
32
|
+
# @return [Run, nil] the run keyed by it
|
|
33
|
+
def find(landed) = all.find { |run| run.landed == landed.to_s }
|
|
34
|
+
|
|
35
|
+
# @param landed [String] the run to leave out
|
|
36
|
+
# @return [Array<Run>] the other runs that are being pushed or watched
|
|
37
|
+
def in_flight(except: nil) = all.select { |run| run.landed != except.to_s && IN_FLIGHT.include?(run.state) }
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def instances = @registry.saga_instances[SAGA] || {}
|
|
42
|
+
|
|
43
|
+
def checkout_of(instance)
|
|
44
|
+
held = (instance[:memory] || {})[:checkout]
|
|
45
|
+
held = held[:value] if held.is_a?(Hash)
|
|
46
|
+
held.respond_to?(:value) ? held.value.to_s : held.to_s
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|