hecks 2.6.0 → 2.7.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/google_authentication.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +1 -1
- 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/port_operation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +42 -3
- data/lib/hecks/bluebook/hexagon.rb +15 -7
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
- data/lib/hecks/bluebook/model_check.rb +1 -1
- 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/forms/value_object_shape.rb +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
- data/lib/hecks/fuzzing/era_boundary.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +59 -5
- data/lib/hecks/language/world/world.bluebook +32 -2
- data/lib/hecks/ports/key_vault.rb +1 -1
- data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +2 -1
- 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/translation/rule_compiler.rb +1 -1
- 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.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 +187 -138
- data/lib/hecks/projections/deploy/lambda.rb +5 -5
- 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.rb +2 -0
- data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
- data/lib/hecks/release/gem_pin.rb +112 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
- data/lib/hecks/runtime/loader.rb +26 -2
- data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
- data/lib/hecks/runtime/registry/verification.rb +4 -2
- data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
- data/lib/hecks/runtime/registry.rb +14 -9
- data/lib/hecks/runtime/tenant_check.rb +1 -1
- 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.rb +2 -1
- metadata +45 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a91dfe0fb86ceb9bb96ecc6b08da680213fed526f49f922f1a2f66d9cb89c80e
|
|
4
|
+
data.tar.gz: 22fc90a2e361f45e27796248c22391997281e62fbfeec01b25ba35378e6e5681
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 824fd1d23971f6dacfd3c3ea18a35f5d0d61e7f07e2056b789d7be7d105a7bc47d13a61e0391d7aeb46d20bcfe8400e307bf05728ed10f9e4c69b46ac7d0df45
|
|
7
|
+
data.tar.gz: 3f6e4679c8187f212f913de1d17944c3bac2ed0e4385bcff3bcd259e1b6ccb49d6c8270c7f84885fcc3f308e9ca7def653341050c649d1a048b8a91228ee65e7
|
|
@@ -6,7 +6,7 @@ module Hecks
|
|
|
6
6
|
module Adapters
|
|
7
7
|
# **Google's own OIDC handshake** — the `authentication` port's one real
|
|
8
8
|
# implementation today. Consolidates what was hand-rolled per-app
|
|
9
|
-
# (
|
|
9
|
+
# (a console app's `google_auth.rb` did this on its own) into
|
|
10
10
|
# one adapter, so any hecks-based app gets Google sign-in for free, the
|
|
11
11
|
# same "one adapter, reusable everywhere" value every other adapter
|
|
12
12
|
# in this directory already has.
|
|
@@ -29,7 +29,7 @@ module Hecks
|
|
|
29
29
|
# exist precisely so a domain whose AWS identity predates a rename
|
|
30
30
|
# keeps targeting the stack that is actually live rather than
|
|
31
31
|
# standing up a second, empty one beside it. A real one:
|
|
32
|
-
#
|
|
32
|
+
# a client site deploys under a fixed stack name, and no
|
|
33
33
|
# value of `domain` (or of `DOMAIN_NAME`, the deployed-Lambda
|
|
34
34
|
# override callers already pass) can make this computation produce
|
|
35
35
|
# a name with no dash after "hecks". Every Ruby-side read and
|
|
@@ -72,7 +72,7 @@ module Hecks
|
|
|
72
72
|
# That's invisible to anything that virtualizes the filesystem at
|
|
73
73
|
# the Ruby level instead of the OS level (e.g. tebako's memfs,
|
|
74
74
|
# which presses a hecks-based app into a single executable —
|
|
75
|
-
# see domain/README.md's "Deploying" section in
|
|
75
|
+
# see domain/README.md's "Deploying" section in a client site's domain for
|
|
76
76
|
# why that matters). ::Prism.parse(File.read(file)) parses the
|
|
77
77
|
# exact same bytes, just read through Ruby's File.read first,
|
|
78
78
|
# which those tools do intercept.
|
|
@@ -82,6 +82,8 @@ module Hecks
|
|
|
82
82
|
["Type", "one_of"] => :one_of_impl,
|
|
83
83
|
["World", "realm"] => :realm_impl,
|
|
84
84
|
["World", "latest"] => :latest_impl,
|
|
85
|
+
["World", "default_database"] => :default_database_impl,
|
|
86
|
+
["World", "default_adapter"] => :default_adapter_impl,
|
|
85
87
|
["DomainPort", "operation"] => :tells_impl,
|
|
86
88
|
["DomainPort", "tells"] => :tells_impl,
|
|
87
89
|
["DomainPort", "asks"] => :asks_impl,
|
|
@@ -22,7 +22,7 @@ module Hecks
|
|
|
22
22
|
# same as an absent `to:` and falls through to `from` (also
|
|
23
23
|
# absent), so `then_set :accepted, to: false` raised "names no
|
|
24
24
|
# operation" for the one value most likely to be written that way
|
|
25
|
-
# (a boolean flip). Confirmed real, live:
|
|
25
|
+
# (a boolean flip). Confirmed real, live: an external project's
|
|
26
26
|
# dream_interpretation.bluebook (`then_set :accepted, to: false`)
|
|
27
27
|
# and transparency.bluebook (`then_set :always, to: false`). TODO
|
|
28
28
|
# upstream via bin/evolve.
|
|
@@ -262,7 +262,7 @@ module Hecks
|
|
|
262
262
|
# era keeps booting, which is the whole point of the rename column.
|
|
263
263
|
#
|
|
264
264
|
# Vendored addition, not (yet) upstream hecks: `then_set
|
|
265
|
-
# :target, from: :source_field` (
|
|
265
|
+
# :target, from: :source_field` (an external project, found
|
|
266
266
|
# live in body/doctor/bluebook/doctor.bluebook) -- semantically
|
|
267
267
|
# identical to `to:` (copy this argument/field into the target),
|
|
268
268
|
# different word. TODO upstream via bin/evolve (migration plan
|
|
@@ -271,7 +271,7 @@ module Hecks
|
|
|
271
271
|
#
|
|
272
272
|
# Vendored addition, not (yet) upstream hecks (migration plan
|
|
273
273
|
# task 4, i106 in-DSL math): `multiply:`/`clamp:` -- per-tick organ
|
|
274
|
-
# math (
|
|
274
|
+
# math (an external project's organ bluebook: strength decays ×0.98,
|
|
275
275
|
# weight/strength clamp to [0, 1]), moved into the bluebook itself
|
|
276
276
|
# from shell-side awk. `multiply:` mirrors
|
|
277
277
|
# increment/decrement's shape exactly (a Numeric amount, applied
|
|
@@ -34,7 +34,7 @@ module Hecks
|
|
|
34
34
|
# only external facts with attribute" the refusal message
|
|
35
35
|
# describes) rather than folding `to:` into identity_attribute's
|
|
36
36
|
# existing Reference-attribute scan, which every operation already
|
|
37
|
-
# in the corpus (Banking, pizzas,
|
|
37
|
+
# in the corpus (Banking, pizzas, a client site's own vendored
|
|
38
38
|
# PaymentGateway) still relies on unchanged.
|
|
39
39
|
#
|
|
40
40
|
# @param name [String] the operation's name
|
|
@@ -11,8 +11,17 @@ module Hecks
|
|
|
11
11
|
# Starts with no settings recorded.
|
|
12
12
|
def initialize = @values = {}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
# A call with a block records the block's own settings as a nested Hash, so a bind can
|
|
15
|
+
# group settings (`preview do ... end`); a call without one records its argument.
|
|
16
|
+
def method_missing(key, *args, &block)
|
|
17
|
+
@values[key.to_sym] =
|
|
18
|
+
if block
|
|
19
|
+
nested = SettingsCollector.new
|
|
20
|
+
nested.instance_eval(&block)
|
|
21
|
+
nested.to_h
|
|
22
|
+
else
|
|
23
|
+
args.size == 1 ? args.first : args
|
|
24
|
+
end
|
|
16
25
|
end
|
|
17
26
|
|
|
18
27
|
def respond_to_missing?(_name, _include_private = false) = true
|
|
@@ -96,6 +105,35 @@ module Hecks
|
|
|
96
105
|
@latest = required(value, "latest version")
|
|
97
106
|
end
|
|
98
107
|
|
|
108
|
+
# Names the connection every chapter's database-taking persistence adapter uses
|
|
109
|
+
# unless that chapter's own world settings name one.
|
|
110
|
+
#
|
|
111
|
+
# Answers the `default_database` word through the table's `calls:` column, the
|
|
112
|
+
# same way `realm_impl` does. A chapter's own
|
|
113
|
+
# `persisted_by("Adapter") { database ... }` still wins for that adapter.
|
|
114
|
+
#
|
|
115
|
+
# @param value [String] the connection, such as a database URL; blankness is
|
|
116
|
+
# judged by the world language at `build`, not here
|
|
117
|
+
# @return [String] the connection as stored
|
|
118
|
+
def default_database_impl(value)
|
|
119
|
+
@default_database = required(value, "default database")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Names the persistence adapter every aggregate binds to unless its own chapter's
|
|
123
|
+
# hecksagon binds it.
|
|
124
|
+
#
|
|
125
|
+
# Answers the `default_adapter` word through the table's `calls:` column, the same
|
|
126
|
+
# way `realm_impl` does. Sits between an explicit bind and the framework's
|
|
127
|
+
# in-memory fallback; whether the adapter really is a persistence adapter is
|
|
128
|
+
# judged at boot, where adapters are known.
|
|
129
|
+
#
|
|
130
|
+
# @param value [String, Symbol] the adapter's declared name, such as `"PostgresEra"`;
|
|
131
|
+
# blankness is judged by the world language at `build`, not here
|
|
132
|
+
# @return [String] the adapter name as stored
|
|
133
|
+
def default_adapter_impl(value)
|
|
134
|
+
@default_adapter = required(value, "default adapter")
|
|
135
|
+
end
|
|
136
|
+
|
|
99
137
|
def method_missing(verb, *args, **kwargs, &block)
|
|
100
138
|
result = word_gate_dispatch(verb, args, kwargs, block)
|
|
101
139
|
return result unless result.equal?(WordGate::NOT_ADMITTED)
|
|
@@ -134,7 +172,8 @@ module Hecks
|
|
|
134
172
|
# as a blank `realm`
|
|
135
173
|
def build
|
|
136
174
|
MetaValidator.call_world(
|
|
137
|
-
World.new(domain: @domain, realm: @realm, latest: @latest, settings: @settings
|
|
175
|
+
World.new(domain: @domain, realm: @realm, latest: @latest, settings: @settings,
|
|
176
|
+
default_database: @default_database, default_adapter: @default_adapter)
|
|
138
177
|
)
|
|
139
178
|
end
|
|
140
179
|
|
|
@@ -97,9 +97,10 @@ module Hecks
|
|
|
97
97
|
include Hecks::IR
|
|
98
98
|
include Behaviour::World
|
|
99
99
|
|
|
100
|
-
emits_ir(domain: :domain, realm: :realm, latest: :latest, settings: :settings
|
|
100
|
+
emits_ir(domain: :domain, realm: :realm, latest: :latest, settings: :settings,
|
|
101
|
+
default_database: :default_database, default_adapter: :default_adapter)
|
|
101
102
|
|
|
102
|
-
attr_reader :domain, :realm, :latest, :settings
|
|
103
|
+
attr_reader :domain, :realm, :latest, :settings, :default_database, :default_adapter
|
|
103
104
|
|
|
104
105
|
# @param domain [String, Symbol] the domain this world configures
|
|
105
106
|
# @param realm [String, Symbol, nil] the declared realm/version marker, or `nil`
|
|
@@ -108,11 +109,18 @@ module Hecks
|
|
|
108
109
|
# if none is declared
|
|
109
110
|
# @param settings [Hash] the declared adapter bind settings, keyed by verb and,
|
|
110
111
|
# for a qualified entry, `"verb:adapter"`
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
112
|
+
# @param default_database [String, nil] the connection every chapter's
|
|
113
|
+
# database-taking persistence adapter uses unless its own settings name one,
|
|
114
|
+
# or `nil` if none is declared
|
|
115
|
+
# @param default_adapter [String, nil] the persistence adapter every aggregate
|
|
116
|
+
# binds to unless its chapter's hecksagon binds it, or `nil` if none is declared
|
|
117
|
+
def initialize(domain:, realm: nil, latest: nil, settings: {}, default_database: nil, default_adapter: nil)
|
|
118
|
+
@domain = domain.to_s
|
|
119
|
+
@realm = realm&.to_s
|
|
120
|
+
@latest = latest&.to_s
|
|
121
|
+
@settings = settings
|
|
122
|
+
@default_database = default_database&.to_s
|
|
123
|
+
@default_adapter = default_adapter&.to_s
|
|
116
124
|
end
|
|
117
125
|
end
|
|
118
126
|
end
|
|
@@ -40,7 +40,9 @@ module Hecks
|
|
|
40
40
|
def judge!
|
|
41
41
|
domain = @world.domain
|
|
42
42
|
send_to("World::World.Declare", domain, domain: v(domain),
|
|
43
|
-
realm: v(@world.realm), latest: v(@world.latest)
|
|
43
|
+
realm: v(@world.realm), latest: v(@world.latest),
|
|
44
|
+
default_database: v(@world.default_database),
|
|
45
|
+
default_adapter: v(@world.default_adapter))
|
|
44
46
|
|
|
45
47
|
Hash(@world.settings).each do |verb, values|
|
|
46
48
|
# the DSL records each binding twice — once by verb, once by
|
|
@@ -793,7 +793,7 @@ module Hecks
|
|
|
793
793
|
|
|
794
794
|
# **Typo detection, deliberately weaker** — `known_domains` can only
|
|
795
795
|
# ever be a monorepo-scoped heuristic: a real external hecks
|
|
796
|
-
# consumer's own domain (this repo's own embryonaut
|
|
796
|
+
# consumer's own domain (this repo's own embryonaut client-
|
|
797
797
|
# shaped case) lives in a genuinely separate repository this
|
|
798
798
|
# corpus scan can never see, so a target this check cannot find
|
|
799
799
|
# is "unknown to THIS corpus," never proof of a typo. A target
|
|
@@ -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
|