hecks 2.6.0 → 2.8.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/exe/hecks +13 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +2 -2
- data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
- data/lib/hecks/behaviors/expectations.rb +2 -2
- data/lib/hecks/bluebook/assembly/contract.rb +1 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
- data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
- data/lib/hecks/bluebook/assembly.rb +1 -1
- data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
- data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
- data/lib/hecks/bluebook/capabilities.rb +2 -2
- data/lib/hecks/bluebook/chapter.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
- data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +47 -3
- data/lib/hecks/bluebook/hexagon.rb +15 -7
- data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
- data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
- data/lib/hecks/bluebook/model_check.rb +13 -14
- data/lib/hecks/cache_dir.rb +103 -0
- data/lib/hecks/cli/document.rb +104 -0
- data/lib/hecks/cli/ir.rb +47 -0
- data/lib/hecks/cli/mcp.rb +32 -0
- data/lib/hecks/cli/mcp_door.rb +461 -0
- data/lib/hecks/cli/model_check.rb +270 -0
- data/lib/hecks/cli/project_cli.rb +123 -0
- data/lib/hecks/cli/project_diagrams.rb +43 -0
- data/lib/hecks/cli/run.rb +219 -0
- data/lib/hecks/cli/smoke_test.rb +59 -0
- data/lib/hecks/cli/stores.rb +54 -0
- data/lib/hecks/cli.rb +166 -0
- data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
- data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
- data/lib/hecks/embryonaut_bluebook.rb +29 -3
- data/lib/hecks/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/language/world/world.bluebook +32 -2
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/key_vault.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +13 -7
- data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
- data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
- data/lib/hecks/ports/persistence/state_codec.rb +1 -1
- data/lib/hecks/ports/persistence.rb +4 -0
- data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
- data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
- data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
- data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
- data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
- data/lib/hecks/projections/deploy/fargate.rb +188 -139
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
- data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
- data/lib/hecks/projections/deploy/preview/script.rb +40 -0
- data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
- data/lib/hecks/projections/deploy/preview/template.rb +247 -0
- data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
- data/lib/hecks/projections/deploy/preview.rb +103 -0
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
- data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
- data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
- data/lib/hecks/projections/deploy/scripts.rb +171 -0
- data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
- data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
- data/lib/hecks/projections/deploy/smoke.rb +159 -0
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
- data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
- data/lib/hecks/projections/deploy/template_diff.rb +173 -0
- data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
- data/lib/hecks/projections/reference.rb +7 -1
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
- data/lib/hecks/release/gem_pin.rb +112 -0
- data/lib/hecks/release/runner/ci_publisher.rb +96 -0
- data/lib/hecks/release/runner/commands.rb +68 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +44 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +89 -0
- data/lib/hecks/release/runner/preflight.rb +119 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +87 -0
- data/lib/hecks/release/runner.rb +244 -0
- data/lib/hecks/router/namespace_installer.rb +1 -1
- data/lib/hecks/runtime/command_interpreter.rb +3 -3
- data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
- data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
- data/lib/hecks/runtime/entity_element.rb +1 -1
- data/lib/hecks/runtime/entity_interpreter.rb +1 -1
- data/lib/hecks/runtime/invocation.rb +1 -1
- data/lib/hecks/runtime/loader.rb +27 -3
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +3 -3
- data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
- data/lib/hecks/runtime/reference_hop.rb +4 -4
- data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
- data/lib/hecks/runtime/registry/verification.rb +15 -13
- data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
- data/lib/hecks/runtime/registry.rb +22 -13
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/runtime/tenant_check.rb +1 -1
- data/lib/hecks/runtime/value/coercion.rb +3 -3
- data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
- data/lib/hecks/storehouse.rb +18 -12
- data/lib/hecks/vendoring/git_environment.rb +35 -0
- data/lib/hecks/vendoring/git_source.rb +102 -0
- data/lib/hecks/vendoring.rb +102 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +8 -2
- metadata +72 -58
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -604
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require_relative "../../hecks"
|
|
2
|
+
require_relative "../bluebook/smoke_test"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module CLI
|
|
6
|
+
# The command behind `bin/smoke_test` and `hecks smoke_test`: boots a real domain
|
|
7
|
+
# and dispatches one synthesized call per declared command and report
|
|
8
|
+
# (`Bluebook::SmokeTest`), printing every failure rather than the first.
|
|
9
|
+
#
|
|
10
|
+
# It needs a real `.hecksagon`: a bare `.bluebook` with nothing bound to an
|
|
11
|
+
# adapter has nothing to run commands against. A failure may be a domain bug or
|
|
12
|
+
# a value the synthesizer's naive choices (0, an empty list, "smoke-test") cannot
|
|
13
|
+
# satisfy; the message underneath says which. Smoke-testing proves nothing
|
|
14
|
+
# crashes, not that an answer is right.
|
|
15
|
+
module SmokeTest
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Smoke-tests the domain `argv` names, or every wired domain under
|
|
19
|
+
# `root/examples/`, and exits with the verdict.
|
|
20
|
+
#
|
|
21
|
+
# @param argv [Array<String>] an optional domain directory
|
|
22
|
+
# @param root [String] the directory whose `examples/*` are tested when `argv`
|
|
23
|
+
# names no domain
|
|
24
|
+
# @return [void]
|
|
25
|
+
# @raise [SystemExit] always: 0 when clean, 1 with failures or no domain found
|
|
26
|
+
def call(argv, root:)
|
|
27
|
+
dir_arg = argv.first
|
|
28
|
+
targets = dir_arg ? [dir_arg] : Dir.glob("#{root}/examples/*/").map { |d| d.chomp("/") }.select { |d| wired?(d) }
|
|
29
|
+
abort "no wired example domains found (none of examples/* has a .hecksagon)" if targets.empty?
|
|
30
|
+
|
|
31
|
+
ok = targets.reduce(true) { |all_ok, dir| clean?(dir) && all_ok }
|
|
32
|
+
|
|
33
|
+
puts
|
|
34
|
+
puts ok ? "Every declared command and report dispatched cleanly." : "SMOKE TEST FOUND PROBLEMS."
|
|
35
|
+
exit(ok ? 0 : 1)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Says whether `dir`, or something nested under it, is a bindable domain.
|
|
39
|
+
#
|
|
40
|
+
# @param dir [String] a directory to check
|
|
41
|
+
# @return [Boolean] true if `dir` contains at least one `.hecksagon` file
|
|
42
|
+
def wired?(dir) = !Dir.glob(File.join(dir, "**/*.hecksagon")).empty?
|
|
43
|
+
|
|
44
|
+
# @api private
|
|
45
|
+
def clean?(dir)
|
|
46
|
+
puts "── #{File.basename(dir)}"
|
|
47
|
+
failures = Bluebook::SmokeTest.call(dir)
|
|
48
|
+
if failures.empty?
|
|
49
|
+
puts " clean — every declared command and report dispatched cleanly"
|
|
50
|
+
return true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
puts " #{failures.size} failure(s)"
|
|
54
|
+
failures.each { |f| puts " #{f}" }
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "../../hecks"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module CLI
|
|
6
|
+
# The command behind `bin/stores` and `hecks stores`: every aggregate's current
|
|
7
|
+
# records as JSON, the head rather than the journal (`bin/history` prints the
|
|
8
|
+
# full write history).
|
|
9
|
+
module Stores
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Boots the domain `argv` names and prints its stores as one JSON document.
|
|
13
|
+
#
|
|
14
|
+
# @param argv [Array<String>] the domain directory, first
|
|
15
|
+
# @param program [String] the name the error message calls this command by
|
|
16
|
+
# @return [void]
|
|
17
|
+
# @raise [IndexError] when `argv` is empty
|
|
18
|
+
# @raise [SystemExit] when the domain directory does not exist
|
|
19
|
+
def call(argv, program:)
|
|
20
|
+
domain = argv.fetch(0)
|
|
21
|
+
unless Dir.exist?(domain)
|
|
22
|
+
warn "#{program}: no such domain #{domain.inspect}"
|
|
23
|
+
exit 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
registry = Hecks.boot(domain).registry
|
|
27
|
+
stores = registry.bluebooks.each_with_object({}) do |(name, bluebook), all|
|
|
28
|
+
bluebook.aggregates.each do |aggregate|
|
|
29
|
+
all[aggregate.storage_name] = data_for(registry.repository(name, aggregate))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
puts JSON.generate(stores)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Reads one aggregate's current records as plain JSON-ready hashes, ordered by id.
|
|
37
|
+
#
|
|
38
|
+
# @param repository [Ports::Persistence::AppendOnly] the aggregate's repository
|
|
39
|
+
# @return [Array<Hash{String => Object}>] one hash per stored `Runtime::Instance`,
|
|
40
|
+
# sorted by id; `[]` when the aggregate has no records
|
|
41
|
+
def records(repository)
|
|
42
|
+
repository.all.sort_by(&:id).map { |instance| JSON.parse(JSON.generate(instance.to_h)) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Shapes one aggregate's dump for the top-level stores document.
|
|
46
|
+
#
|
|
47
|
+
# @param repository [Ports::Persistence::AppendOnly] the aggregate's repository
|
|
48
|
+
# @return [Hash{String => Object}] `"authoritative"` mapped to `records(repository)`
|
|
49
|
+
def data_for(repository)
|
|
50
|
+
{ "authoritative" => records(repository) }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/hecks/cli.rb
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
# The `hecks` command an installed gem puts on the path: one router over the
|
|
3
|
+
# domain-operator tools, each of which also runs from a checkout as `bin/<name>`.
|
|
4
|
+
#
|
|
5
|
+
# ## A router, not a second copy
|
|
6
|
+
#
|
|
7
|
+
# Every subcommand's logic lives in `lib/hecks/cli/`, and the matching `bin/`
|
|
8
|
+
# script is a thin wrapper over the same entry point, so `hecks run` and
|
|
9
|
+
# `bin/run` cannot drift apart. The domain-scoped verbs go through
|
|
10
|
+
# `Facade::CliRunner`, the same way `bin/run` does.
|
|
11
|
+
#
|
|
12
|
+
# ## What a checkout has and an install does not
|
|
13
|
+
#
|
|
14
|
+
# The `bin/` wrappers pass the repository root, which is where they look for
|
|
15
|
+
# `examples/`, `rust/` and `docs/generated/`. Here the root is the working
|
|
16
|
+
# directory, so a command that lists or writes files does it where the caller
|
|
17
|
+
# stands. `model_check` with no domain sweeps the repository's corpus, which an
|
|
18
|
+
# install does not carry, so here it needs at least one domain path.
|
|
19
|
+
#
|
|
20
|
+
# ## Loading
|
|
21
|
+
#
|
|
22
|
+
# Nothing here requires the framework. A subcommand loads it when it runs, and
|
|
23
|
+
# `mcp` checks its transport before anything else loads, as
|
|
24
|
+
# `McpStdioGuard.enforce_stdio!` requires.
|
|
25
|
+
#
|
|
26
|
+
# See `docs/decisions/0066-the-gem-ships-a-hecks-executable-and-dev-tooling-stays-in-the-repo.md`.
|
|
27
|
+
module CLI
|
|
28
|
+
# One subcommand's help: a one-line summary and its usage line.
|
|
29
|
+
Command = Struct.new(:summary, :usage)
|
|
30
|
+
|
|
31
|
+
# Every subcommand, in the order the help lists them.
|
|
32
|
+
COMMANDS = {
|
|
33
|
+
"run" => Command.new(
|
|
34
|
+
"Dispatch a verb, or run a JSON step list, against a domain.",
|
|
35
|
+
"hecks run [domain] <verb [name=value …] | script.json | - | '{\"steps\":[…]}'>"
|
|
36
|
+
),
|
|
37
|
+
"docs" => Command.new(
|
|
38
|
+
"Print a domain's usage document, projected from its bluebook.",
|
|
39
|
+
"hecks docs [domain-path] [aggregate]"
|
|
40
|
+
),
|
|
41
|
+
"narrate" => Command.new(
|
|
42
|
+
"Print a domain read back in English, projected from its bluebook.",
|
|
43
|
+
"hecks narrate [domain-path] [aggregate]"
|
|
44
|
+
),
|
|
45
|
+
"ir" => Command.new(
|
|
46
|
+
"Print a booted domain's IR as JSON.",
|
|
47
|
+
"hecks ir <domain> [--translations] | hecks ir --meta"
|
|
48
|
+
),
|
|
49
|
+
"stores" => Command.new(
|
|
50
|
+
"Print every aggregate's current records as JSON.",
|
|
51
|
+
"hecks stores <domain>"
|
|
52
|
+
),
|
|
53
|
+
"model_check" => Command.new(
|
|
54
|
+
"Statically check a domain's IR for dead states and unreachable steps.",
|
|
55
|
+
"hecks model_check [--strict] [--profile client] <domain> [<domain> …]"
|
|
56
|
+
),
|
|
57
|
+
"smoke_test" => Command.new(
|
|
58
|
+
"Boot a domain and dispatch every declared command and report once.",
|
|
59
|
+
"hecks smoke_test [domain]"
|
|
60
|
+
),
|
|
61
|
+
"project_diagrams" => Command.new(
|
|
62
|
+
"Write a domain's Mermaid diagrams under ./docs/generated/diagrams/.",
|
|
63
|
+
"hecks project_diagrams <domain-path> <ChapterName>"
|
|
64
|
+
),
|
|
65
|
+
"project_cli" => Command.new(
|
|
66
|
+
"Write a command-line launcher beside each domain, named after its bluebook.",
|
|
67
|
+
"hecks project_cli [domain-path …]"
|
|
68
|
+
),
|
|
69
|
+
"mcp" => Command.new(
|
|
70
|
+
"Serve the MCP door over stdio (no authentication; stdio only).",
|
|
71
|
+
"hecks mcp [--stdio]"
|
|
72
|
+
)
|
|
73
|
+
}.freeze
|
|
74
|
+
|
|
75
|
+
HELP_FLAGS = ["--help", "-h"].freeze
|
|
76
|
+
|
|
77
|
+
# Exit status for a missing or unknown subcommand.
|
|
78
|
+
USAGE_STATUS = 2
|
|
79
|
+
|
|
80
|
+
module_function
|
|
81
|
+
|
|
82
|
+
# Routes `argv` to its subcommand and runs it.
|
|
83
|
+
#
|
|
84
|
+
# A subcommand followed by only `--help` or `-h` prints that subcommand's usage
|
|
85
|
+
# without running it; any other arguments pass through untouched, so
|
|
86
|
+
# `hecks run order.place --help` still reaches the verb's own help.
|
|
87
|
+
#
|
|
88
|
+
# @param argv [Array<String>] the command line, subcommand first
|
|
89
|
+
# @param out [#puts] where help goes
|
|
90
|
+
# @param err [#puts] where usage errors go
|
|
91
|
+
# @return [Integer] the process exit status for a subcommand that returns; a
|
|
92
|
+
# subcommand that fails exits the process itself
|
|
93
|
+
def start(argv, out: $stdout, err: $stderr)
|
|
94
|
+
name, *rest = argv
|
|
95
|
+
if name.nil? || !COMMANDS.key?(name)
|
|
96
|
+
asked = HELP_FLAGS.include?(name) || name == "help"
|
|
97
|
+
err.puts "hecks: unknown command #{name.inspect}" unless asked || name.nil?
|
|
98
|
+
overview(asked ? out : err)
|
|
99
|
+
return asked ? 0 : USAGE_STATUS
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if rest.length == 1 && HELP_FLAGS.include?(rest.first)
|
|
103
|
+
help(COMMANDS.fetch(name), out)
|
|
104
|
+
else
|
|
105
|
+
dispatch(name, rest)
|
|
106
|
+
end
|
|
107
|
+
0
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Runs one known subcommand with the working directory as its root.
|
|
111
|
+
#
|
|
112
|
+
# @param name [String] a key of `COMMANDS`
|
|
113
|
+
# @param argv [Array<String>] the arguments after the subcommand
|
|
114
|
+
# @return [void]
|
|
115
|
+
# @raise [SystemExit] when the subcommand exits the process itself
|
|
116
|
+
def dispatch(name, argv)
|
|
117
|
+
program = "hecks #{name}"
|
|
118
|
+
case name
|
|
119
|
+
when "run"
|
|
120
|
+
require_relative "cli/run"
|
|
121
|
+
Run.call(argv, program: program)
|
|
122
|
+
when "docs", "narrate"
|
|
123
|
+
require_relative "cli/document"
|
|
124
|
+
Document.call(argv, projection: name.to_sym, program: program, root: Dir.pwd)
|
|
125
|
+
when "ir"
|
|
126
|
+
require_relative "cli/ir"
|
|
127
|
+
Ir.call(argv, program: program)
|
|
128
|
+
when "stores"
|
|
129
|
+
require_relative "cli/stores"
|
|
130
|
+
Stores.call(argv, program: program)
|
|
131
|
+
when "model_check"
|
|
132
|
+
require_relative "cli/model_check"
|
|
133
|
+
ModelCheck.call(argv, program: program)
|
|
134
|
+
when "smoke_test"
|
|
135
|
+
require_relative "cli/smoke_test"
|
|
136
|
+
SmokeTest.call(argv, root: Dir.pwd)
|
|
137
|
+
when "project_diagrams"
|
|
138
|
+
require_relative "cli/project_diagrams"
|
|
139
|
+
ProjectDiagrams.call(argv, program: program, root: Dir.pwd)
|
|
140
|
+
when "project_cli"
|
|
141
|
+
require_relative "cli/project_cli"
|
|
142
|
+
ProjectCli.call(argv, program: program, root: Dir.pwd, remove_stale_bin: false)
|
|
143
|
+
when "mcp"
|
|
144
|
+
require_relative "cli/mcp"
|
|
145
|
+
Mcp.call(argv)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @api private
|
|
150
|
+
def overview(io)
|
|
151
|
+
io.puts "usage: hecks <command> [arguments]"
|
|
152
|
+
io.puts ""
|
|
153
|
+
width = COMMANDS.keys.map(&:length).max
|
|
154
|
+
COMMANDS.each { |name, command| io.puts " #{name.ljust(width)} #{command.summary}" }
|
|
155
|
+
io.puts ""
|
|
156
|
+
io.puts "Run `hecks <command> --help` for one command's usage."
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# @api private
|
|
160
|
+
def help(command, io)
|
|
161
|
+
io.puts "usage: #{command.usage}"
|
|
162
|
+
io.puts ""
|
|
163
|
+
io.puts command.summary
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module EmbryonautBluebook
|
|
5
|
+
# What a vendored package records about the release it came from:
|
|
6
|
+
# `vendor/embryonaut_bluebooks/<name>/bluebook.lock`.
|
|
7
|
+
#
|
|
8
|
+
# ## File format
|
|
9
|
+
#
|
|
10
|
+
# One `key: value` line each for `package`, `version`, `tag`, `commit`
|
|
11
|
+
# and `digest`, then one `shape: <Domain> <label>` line per domain:
|
|
12
|
+
#
|
|
13
|
+
# package: payments
|
|
14
|
+
# version: 1.2.0
|
|
15
|
+
# tag: payments-v1.2.0
|
|
16
|
+
# commit: <full commit id the tag points at>
|
|
17
|
+
# digest: <sha256 of the vendored *.bluebook files>
|
|
18
|
+
# shape: Payments d33c23
|
|
19
|
+
#
|
|
20
|
+
# The digest is the sha256 of the `<sha256> <name>` lines of every
|
|
21
|
+
# top-level `*.bluebook` file, sorted by name. The source repository's own
|
|
22
|
+
# `bin/bluebook_digest` prints the same number for the tagged release, which
|
|
23
|
+
# is how a vendored copy is proven to be exactly what it claims to be.
|
|
24
|
+
Lock = Struct.new(:package, :version, :tag, :commit, :digest, :shape, keyword_init: true) do
|
|
25
|
+
# Computes the content digest of a directory's `*.bluebook` files.
|
|
26
|
+
#
|
|
27
|
+
# @param dir [String] the directory to measure
|
|
28
|
+
# @return [String, nil] 64 lowercase hex characters, nil when the directory holds
|
|
29
|
+
# no bluebook file
|
|
30
|
+
def self.digest_of(dir)
|
|
31
|
+
names = Dir.children(dir).select { |name| name.end_with?(".bluebook") }.sort
|
|
32
|
+
return nil if names.empty?
|
|
33
|
+
|
|
34
|
+
lines = names.map { |name| "#{Digest::SHA256.file(File.join(dir, name)).hexdigest} #{name}\n" }
|
|
35
|
+
Digest::SHA256.hexdigest(lines.join)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Reads a lock file.
|
|
39
|
+
#
|
|
40
|
+
# @param path [String] the `bluebook.lock` path
|
|
41
|
+
# @return [Lock, nil] the parsed lock, nil when the file does not exist
|
|
42
|
+
def self.read(path)
|
|
43
|
+
return nil unless File.file?(path)
|
|
44
|
+
|
|
45
|
+
parse(File.read(path))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Parses a lock's text.
|
|
49
|
+
#
|
|
50
|
+
# @param text [String] the file's content
|
|
51
|
+
# @return [Lock] the lock, with `shape` as an Array of lines (empty when none)
|
|
52
|
+
def self.parse(text)
|
|
53
|
+
fields = { shape: [] }
|
|
54
|
+
text.each_line do |line|
|
|
55
|
+
key, _, value = line.chomp.partition(": ")
|
|
56
|
+
next if key.empty?
|
|
57
|
+
|
|
58
|
+
key == "shape" ? fields[:shape] << value : fields[key.to_sym] = value
|
|
59
|
+
end
|
|
60
|
+
new(**fields.slice(*members))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Renders the lock as the text written to `bluebook.lock`.
|
|
64
|
+
#
|
|
65
|
+
# @return [String] the lines described above, newline terminated
|
|
66
|
+
def to_s
|
|
67
|
+
header = %i[package version tag commit digest].map { |key| "#{key}: #{self[key]}\n" }
|
|
68
|
+
(header + Array(shape).map { |line| "shape: #{line}\n" }).join
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require_relative "../ports/persistence/plugins/era/storage_shape"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module EmbryonautBluebook
|
|
5
|
+
# The storage-shape labels of a directory of `*.bluebook` files.
|
|
6
|
+
#
|
|
7
|
+
# A label is what `PostgresEra` mints an era from, so a vendored package
|
|
8
|
+
# whose label changed is one whose next deploy mints a new era. This reads
|
|
9
|
+
# the files the way the era guard reads a held era's text: parse only, no
|
|
10
|
+
# boot, no wiring, no database, each file loaded into a scratch registry.
|
|
11
|
+
module Shape
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Measures every domain a directory's bluebook files declare.
|
|
15
|
+
#
|
|
16
|
+
# @param dir [String] a directory holding `*.bluebook` files
|
|
17
|
+
# @return [Array<String>] one `"<Domain> <label>"` line per domain, sorted by domain name
|
|
18
|
+
# @raise [Vendoring::Error] if the directory holds no bluebook file, or the files do
|
|
19
|
+
# not load, the refusal a boot would raise
|
|
20
|
+
def labels(dir)
|
|
21
|
+
files = Dir[File.join(dir, "*.bluebook")]
|
|
22
|
+
raise Vendoring::Error, "no *.bluebook files in #{dir}" if files.empty?
|
|
23
|
+
|
|
24
|
+
load_all(files).bluebooks.sort_by { |name, _| name.to_s }.map do |name, bluebook|
|
|
25
|
+
"#{name} #{Runtime::StorageShape.mint_label(bluebook)}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Loads bluebook files into a registry of their own.
|
|
30
|
+
#
|
|
31
|
+
# @param files [Array<String>] absolute bluebook file paths, in load order
|
|
32
|
+
# @return [Runtime::Registry] the scratch registry holding what the files declared
|
|
33
|
+
# @raise [Vendoring::Error] if any file fails to load
|
|
34
|
+
def load_all(files)
|
|
35
|
+
scratch = Runtime::Registry.new
|
|
36
|
+
library = Ports::Loading.bootstrap
|
|
37
|
+
Hecks.with_registry(scratch) do
|
|
38
|
+
library.load_library
|
|
39
|
+
files.each { |file| Kernel.load(file) }
|
|
40
|
+
end
|
|
41
|
+
scratch
|
|
42
|
+
rescue StandardError, ScriptError => e
|
|
43
|
+
raise Vendoring::Error, "the bluebook files do not load: #{e.class}: #{e.message.lines.first.to_s.strip}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require_relative "../vendoring"
|
|
3
|
+
require_relative "lock"
|
|
4
|
+
require_relative "shape"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module EmbryonautBluebook
|
|
8
|
+
# Vendors one package of the bluebook registry into a consuming project.
|
|
9
|
+
#
|
|
10
|
+
# ## What it does
|
|
11
|
+
#
|
|
12
|
+
# Pins the package's top-level `bluebook/*.bluebook` files from one commit
|
|
13
|
+
# of a local source repository into
|
|
14
|
+
# `<root>/vendor/embryonaut_bluebooks/<name>/bluebook/`, where `load!`
|
|
15
|
+
# resolves them. Only bluebook files are taken: a `.hecksagon`, `.port` or
|
|
16
|
+
# `.adapter` is a wiring decision for whoever deploys, never part of the
|
|
17
|
+
# vendored package.
|
|
18
|
+
#
|
|
19
|
+
# ## Releases and bare commits
|
|
20
|
+
#
|
|
21
|
+
# The source repository tags each release `<name>-v<X.Y.Z>` on a commit
|
|
22
|
+
# whose `<name>/bluebook.yml` says that version. A release pin (`ref` nil for
|
|
23
|
+
# the newest tag, `"1.2.0"`, or the tag name) also writes `bluebook.lock`,
|
|
24
|
+
# which records version, tag, commit, content digest and storage shape, and
|
|
25
|
+
# holds two refusals, because a production project is bound to `PostgresEra`:
|
|
26
|
+
#
|
|
27
|
+
# - a version lower than the vendored one, unless `allow_downgrade`;
|
|
28
|
+
# - a storage-shape change that does not raise the version by at least a
|
|
29
|
+
# minor, so a new era shows in the version number and not only in a hash.
|
|
30
|
+
#
|
|
31
|
+
# Any other commit-ish pins that commit and writes only the marker, with no
|
|
32
|
+
# lock and no version checks. Either way nothing on disk changes when a
|
|
33
|
+
# pin is refused or its files do not load.
|
|
34
|
+
class Vendor
|
|
35
|
+
# A release version, `X.Y.Z`.
|
|
36
|
+
VERSION = /\A\d+\.\d+\.\d+\z/
|
|
37
|
+
|
|
38
|
+
# A package name is a directory name and a bluebook name stem.
|
|
39
|
+
NAME = /\A[a-z][a-z0-9_]*\z/
|
|
40
|
+
|
|
41
|
+
# What a vendoring changed.
|
|
42
|
+
#
|
|
43
|
+
# @!attribute [r] previous_version
|
|
44
|
+
# @return [String, nil] the version recorded before, nil when there was none
|
|
45
|
+
# @!attribute [r] previous_shape
|
|
46
|
+
# @return [Array<String>, nil] the shape lines measured before, nil when there was
|
|
47
|
+
# no earlier copy
|
|
48
|
+
Result = Struct.new(:package, :commit, :version, :tag, :digest, :shape,
|
|
49
|
+
:previous_version, :previous_shape, :dir, keyword_init: true) do
|
|
50
|
+
# Says whether this was a release pin, which writes a lock.
|
|
51
|
+
#
|
|
52
|
+
# @return [Boolean] true for a release, false for a bare commit
|
|
53
|
+
def release? = !tag.nil?
|
|
54
|
+
|
|
55
|
+
# Says whether the storage shape moved since the earlier copy.
|
|
56
|
+
#
|
|
57
|
+
# @return [Boolean] true when an earlier copy existed and its shape differs
|
|
58
|
+
def shape_changed? = !previous_shape.nil? && previous_shape != shape
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param name [String, Symbol] the package's directory name, such as `"payments"`
|
|
62
|
+
# @param from [String] path of the local source repository
|
|
63
|
+
# @param ref [String, nil] `X.Y.Z`, a release tag name, any commit-ish, or nil for the
|
|
64
|
+
# newest release
|
|
65
|
+
# @param root [String] the consuming project's root
|
|
66
|
+
# @param allow_downgrade [Boolean] whether a release older than the vendored one is allowed
|
|
67
|
+
# @raise [Vendoring::Error] if the name is not a plain package name
|
|
68
|
+
def initialize(name, from:, root:, ref: nil, allow_downgrade: false)
|
|
69
|
+
@name = name.to_s
|
|
70
|
+
raise Vendoring::Error, "#{name.inspect} is not a package name" unless @name.match?(NAME)
|
|
71
|
+
|
|
72
|
+
@source = Vendoring::GitSource.new(from)
|
|
73
|
+
@ref = ref&.to_s
|
|
74
|
+
@root = root
|
|
75
|
+
@allow_downgrade = allow_downgrade
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Pins the package and reports what changed.
|
|
79
|
+
#
|
|
80
|
+
# @return [Result] the commit, version and shape now vendored
|
|
81
|
+
# @raise [Vendoring::Error] if the release or commit is not found, the tagged commit
|
|
82
|
+
# disagrees with its own version, the files do not load, or a refusal above applies
|
|
83
|
+
def call
|
|
84
|
+
tag, version = release
|
|
85
|
+
previous_version, previous_shape = existing
|
|
86
|
+
shape = nil
|
|
87
|
+
pin = Vendoring.pin(from: @source.path, ref: tag || @ref, subtree: "#{@name}/bluebook",
|
|
88
|
+
into: package_dir, glob: "*.bluebook") do |staged, commit|
|
|
89
|
+
shape = Shape.labels(staged)
|
|
90
|
+
next nil unless tag
|
|
91
|
+
|
|
92
|
+
refuse_unless_acceptable!(version, previous_version, previous_shape, shape)
|
|
93
|
+
{ "bluebook.lock" => lock(version, tag, commit, staged, shape).to_s }
|
|
94
|
+
end
|
|
95
|
+
build_result(pin, tag, version, shape, previous_version, previous_shape)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def package_dir = File.join(@root, "vendor", "embryonaut_bluebooks", @name)
|
|
101
|
+
|
|
102
|
+
# Resolves what `ref` asks for into a release tag and its version.
|
|
103
|
+
#
|
|
104
|
+
# @return [Array(String, String), nil] tag and version for a release, nil for a bare commit
|
|
105
|
+
def release
|
|
106
|
+
tag = release_tag
|
|
107
|
+
return nil unless tag
|
|
108
|
+
|
|
109
|
+
version = tag.delete_prefix("#{@name}-v")
|
|
110
|
+
tagged = @source.read(tag, "#{@name}/bluebook.yml")&.[](/^version: *(\S+)/, 1)
|
|
111
|
+
unless tagged == version
|
|
112
|
+
raise Vendoring::Error, "#{tag} points at a commit whose bluebook.yml says version #{tagged.inspect}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
[tag, version]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def release_tag
|
|
119
|
+
return newest_release_tag if @ref.nil?
|
|
120
|
+
return existing_tag("#{@name}-v#{@ref}") if @ref.match?(VERSION)
|
|
121
|
+
|
|
122
|
+
@ref.match?(/\A#{Regexp.escape(@name)}-v\d+\.\d+\.\d+\z/) ? existing_tag(@ref) : nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def newest_release_tag
|
|
126
|
+
tags = release_tags
|
|
127
|
+
raise Vendoring::Error, "no #{@name}-v* release tag in #{@source.path}" if tags.empty?
|
|
128
|
+
|
|
129
|
+
"#{@name}-v#{tags.max_by { |version| Gem::Version.new(version) }}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def existing_tag(tag)
|
|
133
|
+
return tag if release_tags.include?(tag.delete_prefix("#{@name}-v"))
|
|
134
|
+
|
|
135
|
+
raise Vendoring::Error, "no release #{tag} in #{@source.path} (releases: #{release_tags.sort_by do |v|
|
|
136
|
+
Gem::Version.new(v)
|
|
137
|
+
end.join(' ')})"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def release_tags
|
|
141
|
+
@source.tags("#{@name}-v*").map { |tag| tag.delete_prefix("#{@name}-v") }.grep(VERSION)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The version and storage shape of what is vendored now, measured from
|
|
145
|
+
# its files as they stand rather than read back, so a copy vendored before
|
|
146
|
+
# locks existed still gets a real "before".
|
|
147
|
+
def existing
|
|
148
|
+
version = Lock.read(File.join(package_dir, "bluebook.lock"))&.version
|
|
149
|
+
dir = File.join(package_dir, "bluebook")
|
|
150
|
+
shape = File.directory?(dir) ? Shape.labels(dir) : nil
|
|
151
|
+
[version, shape]
|
|
152
|
+
rescue Vendoring::Error
|
|
153
|
+
[version, nil]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def refuse_unless_acceptable!(version, previous_version, previous_shape, shape)
|
|
157
|
+
return unless previous_version
|
|
158
|
+
|
|
159
|
+
refuse_downgrade!(version, previous_version)
|
|
160
|
+
return if previous_shape.nil? || previous_shape == shape
|
|
161
|
+
return if minor_or_more?(previous_version, version)
|
|
162
|
+
|
|
163
|
+
raise Vendoring::Error,
|
|
164
|
+
"#{@name} #{previous_version} -> #{version} changes the storage shape but is only a patch bump " \
|
|
165
|
+
"(before: #{previous_shape.join(' ')}; after: #{shape.join(' ')}). Raise the package's version " \
|
|
166
|
+
"by at least a minor in the source repository and release again. Nothing was changed."
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def refuse_downgrade!(version, previous_version)
|
|
170
|
+
return if @allow_downgrade || Gem::Version.new(version) >= Gem::Version.new(previous_version)
|
|
171
|
+
|
|
172
|
+
raise Vendoring::Error, "#{@name} #{previous_version} is vendored; #{version} is older. " \
|
|
173
|
+
"Pass allow_downgrade to do it anyway."
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def minor_or_more?(old, new)
|
|
177
|
+
old_major, old_minor = old.split(".").map(&:to_i)
|
|
178
|
+
new_major, new_minor = new.split(".").map(&:to_i)
|
|
179
|
+
new_major > old_major || (new_major == old_major && new_minor > old_minor)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def lock(version, tag, commit, staged, shape)
|
|
183
|
+
Lock.new(package: @name, version: version, tag: tag, commit: commit,
|
|
184
|
+
digest: Lock.digest_of(staged), shape: shape)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def build_result(pin, tag, version, shape, previous_version, previous_shape)
|
|
188
|
+
Result.new(package: @name, commit: pin.commit, version: version, tag: tag,
|
|
189
|
+
digest: Lock.digest_of(pin.dir), shape: shape, dir: pin.dir,
|
|
190
|
+
previous_version: previous_version, previous_shape: previous_shape)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
require_relative "vendor"
|
|
3
|
+
|
|
4
|
+
module Hecks
|
|
5
|
+
module EmbryonautBluebook
|
|
6
|
+
# The command line behind `bin/vendor_bluebook`.
|
|
7
|
+
#
|
|
8
|
+
# It lives in the gem, not in a script, so a consuming project reaches it
|
|
9
|
+
# through its own bundle without a copy of the script:
|
|
10
|
+
#
|
|
11
|
+
# bundle exec ruby -rhecks \
|
|
12
|
+
# -e 'exit Hecks::EmbryonautBluebook::VendorCli.run(ARGV)' payments@1.2.0
|
|
13
|
+
#
|
|
14
|
+
# ## Usage
|
|
15
|
+
#
|
|
16
|
+
# vendor_bluebook <package>[@<version-or-commit>] [--from <path>] [--root <path>]
|
|
17
|
+
#
|
|
18
|
+
# `--from` defaults to `$EMBRYONAUT_BLUEBOOKS_SRC` and `--root` to the current
|
|
19
|
+
# directory. `ALLOW_DOWNGRADE=1` in the environment permits a release older
|
|
20
|
+
# than the vendored one.
|
|
21
|
+
module VendorCli
|
|
22
|
+
USAGE = "usage: vendor_bluebook <package>[@<version-or-commit>] [--from PATH] [--root PATH]".freeze
|
|
23
|
+
|
|
24
|
+
# Runs the command and reports on the given streams.
|
|
25
|
+
#
|
|
26
|
+
# @param argv [Array<String>] the command-line arguments
|
|
27
|
+
# @param env [Hash{String => String}] the environment
|
|
28
|
+
# @param out [IO] where the report goes
|
|
29
|
+
# @param err [IO] where a refusal goes
|
|
30
|
+
# @param root [String] the default project root
|
|
31
|
+
# @return [Integer] the exit status, 0 when the package was vendored
|
|
32
|
+
def self.run(argv, env: ENV, out: $stdout, err: $stderr, root: Dir.pwd)
|
|
33
|
+
options = { from: env["EMBRYONAUT_BLUEBOOKS_SRC"], root: root }
|
|
34
|
+
spec = parse(argv, options)
|
|
35
|
+
return usage(err) unless spec && options[:from]
|
|
36
|
+
|
|
37
|
+
name, _, ref = spec.partition("@")
|
|
38
|
+
result = Vendor.new(name, from: options[:from], root: options[:root], ref: ref.empty? ? nil : ref,
|
|
39
|
+
allow_downgrade: env["ALLOW_DOWNGRADE"] == "1").call
|
|
40
|
+
report(result, options[:from], out)
|
|
41
|
+
0
|
|
42
|
+
rescue Vendoring::Error, OptionParser::ParseError => e
|
|
43
|
+
err.puts(e.message)
|
|
44
|
+
1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Reads the options into a hash and picks out the package argument.
|
|
48
|
+
#
|
|
49
|
+
# @param argv [Array<String>] the command-line arguments
|
|
50
|
+
# @param options [Hash{Symbol => String}] receives `:from` and `:root` when given
|
|
51
|
+
# @return [String, nil] the one positional argument, nil unless exactly one was given
|
|
52
|
+
def self.parse(argv, options)
|
|
53
|
+
parser = OptionParser.new
|
|
54
|
+
parser.on("--from PATH") { |path| options[:from] = path }
|
|
55
|
+
parser.on("--root PATH") { |path| options[:root] = path }
|
|
56
|
+
rest = parser.parse(argv)
|
|
57
|
+
rest.first if rest.length == 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.usage(err)
|
|
61
|
+
err.puts(USAGE)
|
|
62
|
+
2
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.report(result, from, out)
|
|
66
|
+
label = result.release? ? "#{result.version} (#{result.tag}, #{result.commit[0, 7]})" : result.commit[0, 12]
|
|
67
|
+
out.puts("Vendored embryonaut_bluebooks/#{result.package} #{label} from #{from} into #{result.dir}")
|
|
68
|
+
out.puts(shape_line(result))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The shape label tells a re-vendor that only moved the pin apart from one
|
|
72
|
+
# that will mint a new era on the next deploy.
|
|
73
|
+
def self.shape_line(result)
|
|
74
|
+
now = result.shape.join(" ")
|
|
75
|
+
if result.previous_shape.nil?
|
|
76
|
+
"Shape: #{now} (no earlier vendored copy to compare against)"
|
|
77
|
+
elsif result.shape_changed?
|
|
78
|
+
"SHAPE CHANGED: the next deploy mints a new era; add its translation edge first.\n " \
|
|
79
|
+
"before: #{result.previous_shape.join(' ')}\n after: #{now}"
|
|
80
|
+
else
|
|
81
|
+
"Shape unchanged: #{now}. No new era on deploy."
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private_class_method :parse, :usage, :report, :shape_line
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|