hecks 3.11.0 → 3.12.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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +6 -3
  3. data/lib/hecks/adapters/driving/cli_runner.rb +1 -0
  4. data/lib/hecks/adapters/driving/newcomer.rb +50 -0
  5. data/lib/hecks/bluebook/assembly/contracts/read_models.rb +1 -1
  6. data/lib/hecks/bluebook/assembly/marks.rb +3 -15
  7. data/lib/hecks/bluebook/assembly/scalars.rb +39 -0
  8. data/lib/hecks/bluebook/meta_validator/readings.rb +6 -0
  9. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +6 -3
  10. data/lib/hecks/bluebook.rb +1 -0
  11. data/lib/hecks/deploy/bluebook/deploy.bluebook +16 -2
  12. data/lib/hecks/gate/stages.yml +32 -0
  13. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +5 -1
  14. data/lib/hecks/hecks/hecks.world +3 -0
  15. data/lib/hecks/language/bluebook/bluebook.behaviors +23 -0
  16. data/lib/hecks/language/bluebook/policy.behaviors +19 -0
  17. data/lib/hecks/language/bluebook/vocabulary.bluebook +52 -0
  18. data/lib/hecks/projection_files/expression_tables.rb +7 -2
  19. data/lib/hecks/projections/deploy/box/compose.rb +6 -4
  20. data/lib/hecks/projections/deploy/box/edge.rb +62 -0
  21. data/lib/hecks/projections/deploy/box/logging.rb +99 -0
  22. data/lib/hecks/projections/deploy/box/roll_scripts.rb +16 -8
  23. data/lib/hecks/projections/deploy/box/settings/cdn.rb +40 -0
  24. data/lib/hecks/projections/deploy/box/settings/logs.rb +40 -0
  25. data/lib/hecks/projections/deploy/box/settings/patterns.rb +11 -0
  26. data/lib/hecks/projections/deploy/box/settings/types.rb +16 -1
  27. data/lib/hecks/projections/deploy/box/settings.rb +6 -2
  28. data/lib/hecks/projections/deploy/box/stack_yaml.rb +5 -2
  29. data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +16 -0
  30. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +8 -4
  31. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +3 -0
  32. data/lib/hecks/projections/deploy/box/templates/edge.mk.tmpl +17 -0
  33. data/lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl +65 -0
  34. data/lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl +12 -0
  35. data/lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl +7 -0
  36. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +5 -4
  37. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +5 -4
  38. data/lib/hecks/projections/deploy/box.rb +15 -3
  39. data/lib/hecks/projections/site/site_root/workflow.rb +5 -0
  40. data/lib/hecks/projector/cli_audience.rb +20 -0
  41. data/lib/hecks/projector/cli_projector/usage.rb +15 -4
  42. data/lib/hecks/rust_build/host.rb +11 -4
  43. data/lib/hecks/rust_build/prebuilt_binary.rb +101 -0
  44. data/lib/hecks/tools/ci_gate_decision.rb +35 -0
  45. data/lib/hecks/tools/ci_gates.rb +20 -1
  46. data/lib/hecks/tools/lanes/files.rb +8 -0
  47. data/lib/hecks/tools/lanes/prebuilt_workflow.rb +81 -0
  48. data/lib/hecks/tools/lanes.rb +1 -0
  49. data/lib/hecks/tools/release_only.rb +111 -0
  50. data/lib/hecks/version.rb +1 -1
  51. data/lib/hecks/vocabulary.rb +15 -1
  52. data/prebuilt/9ee54e2f0d4d6d3fe046827779b221e02de665fba911a048ae5dd8b8fbfb7d90.marshal +0 -0
  53. data/prebuilt/d74a00e996222c1d8b676d5959092effc46b4dc59e0555f3bf9554ac58e3094a.marshal +0 -0
  54. data/prebuilt/{verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json → verdicts-df19f35868d3284a70f55e42dcef87ef4e65e6a678e5135f72e1f6cff4c1357b.json} +1 -1
  55. data/rust/host/HECKS_RELEASE +1 -1
  56. data/rust/host/src/extension.rs +13 -1
  57. data/rust/host/src/lib.rs +1 -0
  58. data/rust/host/src/platform_signin/attack_tests.rs +1100 -0
  59. data/rust/host/src/platform_signin/config.rs +512 -0
  60. data/rust/host/src/platform_signin/cookie.rs +237 -0
  61. data/rust/host/src/platform_signin/extension.rs +190 -0
  62. data/rust/host/src/platform_signin/fetcher.rs +322 -0
  63. data/rust/host/src/platform_signin/handler.rs +251 -0
  64. data/rust/host/src/platform_signin/handler_tests.rs +554 -0
  65. data/rust/host/src/platform_signin/jti.rs +196 -0
  66. data/rust/host/src/platform_signin/jwks.rs +542 -0
  67. data/rust/host/src/platform_signin/jws.rs +249 -0
  68. data/rust/host/src/platform_signin/known_answer_tests.rs +237 -0
  69. data/rust/host/src/platform_signin/mapping_tests.rs +254 -0
  70. data/rust/host/src/platform_signin/mod.rs +75 -0
  71. data/rust/host/src/platform_signin/mode_tests.rs +382 -0
  72. data/rust/host/src/platform_signin/pages.rs +204 -0
  73. data/rust/host/src/platform_signin/query.rs +233 -0
  74. data/rust/host/src/platform_signin/session.rs +151 -0
  75. data/rust/host/src/platform_signin/settings.rs +385 -0
  76. data/rust/host/src/platform_signin/strict_json.rs +351 -0
  77. data/rust/host/src/platform_signin/test_support.rs +235 -0
  78. data/rust/host/src/platform_signin/ttl_tests.rs +94 -0
  79. data/rust/host/src/platform_signin/verify.rs +604 -0
  80. data/rust/host/src/platform_signin/wiring_support.rs +319 -0
  81. data/rust/host/src/web.rs +50 -6
  82. data/rust/parser/src/parse/value_object.rs +22 -10
  83. metadata +42 -4
  84. data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
  85. data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 130184b20d584f87b5ede5d45364b213c07677a3330fa198bee6680077ef1899
4
- data.tar.gz: f60d730c4d1fc428703bdcc3dcd63f07ea10b77386e0eab4dcab12180e04cac4
3
+ metadata.gz: beba8cd60d4c859286fa1121e3b382cc63e826e010dfd336714befa036dfad94
4
+ data.tar.gz: 16deb6ba2dd98c8d09385e77a9773f92ef12726d097e71601d2e3cfdbb83ae00
5
5
  SHA512:
6
- metadata.gz: 5afb2bcf079a1024b02260e7e428cbb5dbb7661d4df86e093c7813879b0617aae35c63bf25c4fac8025542e5ed05c0cd199eb1046b8ce52555e5ca1717c9662a
7
- data.tar.gz: e5cc44483d1cf6eff9c9fcbc0beef4b0c42b70c46ce0a9d50c26b4f4012604dd17200104036833e062bdb3921232c0938d58841c779c7a952bc0562e919b78a3
6
+ metadata.gz: fe730368d2f8942c67a5510c5b8344bdad86f9aa0990515903e147da4fe71fbc6f17880ea2a1b26de4ffa0d24ad20c416455df58ee8a66617e1f832b2637e2b0
7
+ data.tar.gz: a41080c78fc7b1e9d43347fd4b965d6811c246baee7072214c5f7895564eccc94ce699b86bc739de0f3d7d7830fb8057e6bf2e0699496e201ffd057c7e6a39d5
@@ -82,8 +82,9 @@ module Hecks
82
82
  # with the first sentence of what it is for.
83
83
  #
84
84
  # @return [Hash{Symbol => Object}] `:hide` and `:chapters` (pairs of word and summary)
85
- def audience(runtime, launcher, maintainer: LauncherOptions.maintainer?)
86
- shown = LauncherOptions.audience(launcher, maintainer)
85
+ def audience(runtime, launcher, maintainer: LauncherOptions.maintainer?,
86
+ newcomer: !maintainer && Newcomer.applies?)
87
+ shown = (newcomer && Newcomer.audience(launcher)) || LauncherOptions.audience(launcher, maintainer)
87
88
  return shown unless shown[:chapters]
88
89
 
89
90
  shown.merge(chapters: chapter_summaries(runtime, shown[:chapters]))
@@ -102,7 +103,9 @@ module Hecks
102
103
 
103
104
  # Names who the help is for, so the help remembered for one is not given to the other.
104
105
  def audience_key
105
- LauncherOptions.maintainer? ? "maintainer" : "project"
106
+ return "maintainer" if LauncherOptions.maintainer?
107
+
108
+ Newcomer.applies? ? "newcomer" : "project"
106
109
  end
107
110
 
108
111
  # Reads the entry words of a line: whether it asks a query, the bare name, and the words
@@ -3,6 +3,7 @@ require_relative "cli"
3
3
  require_relative "command_request"
4
4
  require_relative "json"
5
5
  require_relative "launcher_options"
6
+ require_relative "newcomer"
6
7
  require_relative "../../projector"
7
8
  require_relative "../../ports/clock"
8
9
  require_relative "cli_runner/answers"
@@ -0,0 +1,50 @@
1
+ require_relative "launcher_options"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ module Driving
6
+ # The launcher's help for someone starting out: a folder with no bluebook in it has no use yet
7
+ # for the commands that act on a domain, so the help lists only the `newcomer` commands the
8
+ # chapter's `launcher` setting names (`hecks --all` lists the rest).
9
+ module Newcomer
10
+ # How deep under a directory a bluebook may sit and still make it a project:
11
+ # `Hecks.bluebook` beside it, or `bluebook/pizzas.bluebook` one folder down, as the
12
+ # examples keep theirs.
13
+ PROJECT_GLOBS = ["*.bluebook", "*/*.bluebook", "*/*/*.bluebook"].freeze
14
+
15
+ module_function
16
+
17
+ # Whether the person is in a folder with no bluebook in it, outside a hecks checkout.
18
+ #
19
+ # @param dir [String] where the command line was typed
20
+ # @param env [#[]] the environment
21
+ # @return [Boolean] true when no `.bluebook` stands in `dir` or up to two folders below it
22
+ def applies?(dir = Dir.pwd, env = ENV)
23
+ return false if LauncherOptions.maintainer?(dir, env)
24
+
25
+ PROJECT_GLOBS.none? { |pattern| bluebook_under?(dir, pattern) }
26
+ end
27
+
28
+ # The help options a newcomer gets: only the setting's `newcomer` commands, and no chapters.
29
+ #
30
+ # @param settings [Hash, nil] the chapter's `launcher` setting
31
+ # @return [Hash{Symbol => Object}, nil] `:only` and `:chapters`, or nil when the chapter
32
+ # names no newcomer commands
33
+ def audience(settings)
34
+ return unless settings && settings[:newcomer]
35
+
36
+ { only: Array(settings[:newcomer]).map(&:to_s), chapters: [] }
37
+ end
38
+
39
+ # Whether `pattern` finds a bluebook under `dir`; a folder the person may not read holds
40
+ # none.
41
+ # @api private
42
+ def bluebook_under?(dir, pattern)
43
+ Dir.glob(File.join(File.expand_path(dir), pattern)).any?
44
+ rescue SystemCallError
45
+ false
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -61,7 +61,7 @@ module Hecks
61
61
  "Member" => Contract.new(
62
62
  holder: nil, make: nil,
63
63
  fields: {},
64
- rows: { pairs: :pair_rows },
64
+ rows: { pairs: :member_pair_rows },
65
65
  derived: { position: :walk, pairs: [:folded, %i[members], nil] }
66
66
  )
67
67
  }.freeze
@@ -35,10 +35,10 @@ module Hecks
35
35
  # One identity part, as the String path `identity_paths` splits.
36
36
  def identity_path(part) = part[:value].to_s
37
37
 
38
- # A member's fields, an open map, so the pairs arrive as a list.
39
- # Values are unmarked because `ValueObject#to_h` spells them with `to_s`.
38
+ # A member's fields, an open map, so the pairs arrive as a list. Each value is already
39
+ # typed: `Reconstruction` reads a stored cell with `Scalars.unmark` before it gets here.
40
40
  def member(pairs)
41
- pairs.to_h { |key, value| [key.to_sym, unmark_scalar(value)] }
41
+ pairs.to_h { |key, value| [key.to_sym, value] }
42
42
  end
43
43
 
44
44
  # A read model's gathered head; `as` is a Symbol because `ReadModel#to_h` spells it `to_s`.
@@ -49,18 +49,6 @@ module Hecks
49
49
  # Matches the builder's native `{field: :symbol}` shape.
50
50
  def group_by_field(row) = { field: row[:field].to_sym }
51
51
 
52
- # Reads a member's scalar back from text. Unlike `read`, a bare word stays a
53
- # String, because a closed set admits words far more often than symbols.
54
- def unmark_scalar(value)
55
- text = value.to_s
56
- return true if text == "true"
57
- return false if text == "false"
58
- return text.to_i if text.match?(/\A-?\d+\z/)
59
- return text.to_f if text.match?(/\A-?\d+\.\d+\z/)
60
-
61
- text
62
- end
63
-
64
52
  # A saga's argument bindings; Literal marks a Symbol with a leading colon.
65
53
  def bindings(with) = Array(with).to_h { |key, value| [key.to_sym, read(value)] }
66
54
 
@@ -0,0 +1,39 @@
1
+ module Hecks
2
+ module Bluebook
3
+ class Assembly
4
+ # How a `member` cell is spelled as stored text, and read back. The meta-domain holds a cell
5
+ # as text, so a quoted `"true"` and a bare `true` need different spellings: a boolean or
6
+ # number is its own spelling, and a String that would read back as one, or as quoted text,
7
+ # is quoted so it stays a String. The Rust parser reads a cell the same way (`cell_value`
8
+ # in `parse/value_object.rs`).
9
+ module Scalars
10
+ module_function
11
+
12
+ # @param value [Object] a cell as the builder holds it
13
+ # @return [Object] `value` itself unless it is a String that reads back as another type, in
14
+ # which case its quoted spelling
15
+ def mark(value)
16
+ return value unless value.is_a?(String)
17
+
18
+ unmark(value) == value ? value : Literal.quote(value)
19
+ end
20
+
21
+ # Unlike `Literal.read`, a bare word stays a String, because a closed set admits words far
22
+ # more often than symbols; quoted text is a String whatever it spells.
23
+ #
24
+ # @param value [#to_s] a cell's stored text
25
+ # @return [Object] the String, boolean or number the text spells
26
+ def unmark(value)
27
+ text = value.to_s
28
+ return Literal.unquote(text) if Literal.quoted?(text)
29
+ return true if text == "true"
30
+ return false if text == "false"
31
+ return text.to_i if text.match?(/\A-?\d+\z/)
32
+ return text.to_f if text.match?(/\A-?\d+\.\d+\z/)
33
+
34
+ text
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -70,6 +70,12 @@ module Hecks
70
70
  Array(map&.to_h).map { |key, value| { key: key, value: value } }
71
71
  end
72
72
 
73
+ # A member's cells, each spelled so a quoted `"true"` stays a String and a bare `true` a
74
+ # boolean.
75
+ def member_pair_rows(map)
76
+ Array(map&.to_h).map { |key, value| { key: key, value: Assembly::Scalars.mark(value) } }
77
+ end
78
+
73
79
  # Flattens options via `extra_options_to_h` — a new option needs no
74
80
  # change here. `filters: true` reads wheres/order_by/limit live off
75
81
  # the node, not `to_h`, so wire-format changes there don't affect it.
@@ -41,11 +41,14 @@ module Hecks
41
41
  # dispatched state, one member at a time, not behind a `DeclaredIn` query.
42
42
  def members_row(row) = members_of(row)
43
43
 
44
- # The key is stringified, never the value: a pair's value keeps whatever
45
- # native Ruby type the source line actually wrote.
44
+ # The key is stringified, never the value: a pair's value is read back from its text by
45
+ # `Assembly::Scalars`, so a quoted cell returns a String and `true`, `false` and numbers
46
+ # return as the builder holds them.
46
47
  def members_of(value_object_row)
47
48
  Array(value_object_row[:members]).map do |member|
48
- Array(member[:pairs]).map { |pair| [text(pair[:key]).to_s, text(pair[:value])] }
49
+ Array(member[:pairs]).map do |pair|
50
+ [text(pair[:key]).to_s, Assembly::Scalars.unmark(text(pair[:value]))]
51
+ end
49
52
  end
50
53
  end
51
54
 
@@ -35,6 +35,7 @@ require_relative "bluebook/assembly/contract"
35
35
  require_relative "bluebook/assembly/contracts"
36
36
  require_relative "bluebook/assembly/specializer"
37
37
  require_relative "bluebook/assembly/build"
38
+ require_relative "bluebook/assembly/scalars"
38
39
  require_relative "bluebook/assembly/marks"
39
40
  require_relative "bluebook/assembly/aggregate_assembly"
40
41
  require_relative "bluebook/assembly"
@@ -212,6 +212,7 @@ Hecks.bluebook "Deploy" do
212
212
  attribute :volume_gb, VolumeGb
213
213
  attribute :database_class, DatabaseClass
214
214
  attribute :storage_gb, StorageGb
215
+ attribute :cdn_aliases, list_of(CdnAlias), optional: true
215
216
 
216
217
  value_object "DomainName" do
217
218
  attribute :value, String
@@ -251,8 +252,19 @@ Hecks.bluebook "Deploy" do
251
252
  invariant("storage is at most 65536 GB") { value <= 65536 }
252
253
  end
253
254
 
254
- # All six required for the same reason as `FargateTarget.Declare`; this validates a target
255
- # already resolved by `hecks deploy project`'s defaults.
255
+ # A host name the CloudFront edge answers on, such as app.example.com. Lowercase labels only:
256
+ # CloudFront refuses a name its certificate does not cover at deploy, which this cannot see.
257
+ value_object "CdnAlias" do
258
+ attribute :value, String
259
+ invariant("a CDN alias is a lowercase host name such as app.example.com") do
260
+ value.to_s.match?(/\A([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]\z/)
261
+ end
262
+ invariant("a CDN alias is at most 253 characters") { value.to_s.length <= 253 }
263
+ end
264
+
265
+ # The first six are required for the same reason as `FargateTarget.Declare`; this validates a
266
+ # target already resolved by `hecks deploy project`'s defaults. `cdn_aliases` is the one
267
+ # optional setting: naming at least one asks for the CloudFront edge stack.
256
268
  command "Declare" do
257
269
  goal "Validate one domain's AwsBox deploy target before anything is generated from it"
258
270
 
@@ -262,6 +274,7 @@ Hecks.bluebook "Deploy" do
262
274
  attribute :volume_gb, VolumeGb
263
275
  attribute :database_class, DatabaseClass
264
276
  attribute :storage_gb, StorageGb
277
+ attribute :cdn_aliases, list_of(CdnAlias), optional: true
265
278
 
266
279
  sets :domain
267
280
  sets :region
@@ -269,6 +282,7 @@ Hecks.bluebook "Deploy" do
269
282
  sets :volume_gb
270
283
  sets :database_class
271
284
  sets :storage_gb
285
+ sets :cdn_aliases
272
286
 
273
287
  emits "BoxTargetDeclared"
274
288
  end
@@ -94,6 +94,38 @@ unguarded_push:
94
94
  - *check_comment_blocks
95
95
  - *check_fuzzing
96
96
 
97
+ # The Rust io specs `ci-rust-io.yml` runs in its `rspec_rust_io` job, for a push that wants their
98
+ # answer first: `hecks gate_run.gate stage=rust_io --wait`. No hook or workflow runs it. It sets CI
99
+ # as the job does, so a skipped example fails here as it fails there. rust/src/generated is not
100
+ # committed, so the check materializes it first, as the job's generated-rust action does. The specs
101
+ # mutate rust/, and a stage's checks run at once, so the job's two legs run one after the other in
102
+ # a single check. spec/gate_rust_io_stage_spec.rb pins the spec lists to the workflow's matrix.
103
+ rust_io:
104
+ env:
105
+ CI: "true"
106
+ HECKS_ENVIRONMENT: memory
107
+ checks:
108
+ - id: rust_io
109
+ title: rust io specs, conformance, coercion, has_many, semantics corpus and feature exclusivity
110
+ run:
111
+ - sh
112
+ - -ec
113
+ - |
114
+ # Building the banking crate makes banking the default feature and rewrites the module
115
+ # list, both tracked. They are put back on the way out, and the check will not start over
116
+ # a person's uncommitted edits to them.
117
+ rewritten="rust/Cargo.toml rust/src/generated/mod.rs"
118
+ if [ -n "$(git status --porcelain -- $rewritten)" ]; then
119
+ echo "uncommitted changes in $rewritten; commit or discard them first" >&2
120
+ exit 1
121
+ fi
122
+ trap 'git checkout -q -- $rewritten' EXIT
123
+ env -u CARGO_TARGET_DIR bundle exec exe/hecks regeneration_run.regenerate_corpus --confirm --wait
124
+ bundle exec exe/hecks build.project_rust domain=examples/banking --wait
125
+ bundle exec rspec --tag io --profile 8 spec/rust_conformance_spec.rb spec/rust_conformance_fuzz_spec.rb spec/rust_numeric_coercion_spec.rb spec/rust_generated/has_many_spec.rb spec/semantics_corpus_spec.rb
126
+ bundle exec rspec --tag io --profile 8 spec/rust_generated/domain_feature_exclusivity_spec.rb
127
+ blocked: a Rust io spec failed, or skipped; the crate it built is under rust/target.
128
+
97
129
  # The checks `.github/workflows/ci-checks.yml` runs. It sets no `env`: the job's journal choice
98
130
  # (the `hecks-environment` action) reaches every check, as it reached each command before.
99
131
  ci:
@@ -119,9 +119,13 @@ module Hecks
119
119
  ["pg_restore", "--no-owner", "--no-privileges", "-d", database_name, file]
120
120
  end
121
121
 
122
+ # What the URL names, and nothing it leaves out: `postgres:///postgres` parses with an empty
123
+ # host, which would override a `PGHOST` the environment sets and send the restore to the
124
+ # local socket while the rest of the rehearsal reached the server by TCP.
122
125
  def restore_env
123
126
  uri = URI.parse(server_url)
124
- { "PGHOST" => uri.host, "PGPORT" => uri.port&.to_s, "PGUSER" => uri.user, "PGPASSWORD" => uri.password }.compact
127
+ { "PGHOST" => uri.host, "PGPORT" => uri.port&.to_s, "PGUSER" => uri.user, "PGPASSWORD" => uri.password }
128
+ .reject { |_name, value| value.to_s.empty? }
125
129
  end
126
130
  end
127
131
  end
@@ -36,6 +36,8 @@
36
36
  # checkout runs (Codebase, and releasing hecks itself): the help leaves them out anywhere else and
37
37
  # `--maintainer` lists them; `chapters` names the attached chapters the help points at, one line each
38
38
  # (`hecks deploy`), and `maintainer_chapters` the language's own, added inside a checkout.
39
+ # `newcomer` names the only commands the help lists in a folder holding no bluebook yet
40
+ # (`hecks --all` lists the rest), so a first run shows how to start and nothing else.
39
41
  Hecks.world "Hecks" do
40
42
  realm "Hecks"
41
43
  default_adapter "PostgresEra"
@@ -58,6 +60,7 @@ Hecks.world "Hecks" do
58
60
  maintainer: %w[LanguageRun KernelRun ConformanceRun RegenerationRun GateRun StyleRun InspectionRun
59
61
  CodemodRun TestSuiteRun CorpusRun PublishingRun PromotionRun Release
60
62
  SyntaxBootCache],
63
+ newcomer: %w[launch.init launch.interview operation.open_console],
61
64
  chapters: %w[Deploy Governance Tenancy Site Tickets QualityControl],
62
65
  maintainer_chapters: %w[Bluebook Hecksagon World Adapter Port Translation Expression],
63
66
  memory_commands: %w[console init interview],
@@ -0,0 +1,23 @@
1
+ Hecks.behaviors "Bluebook" do
2
+ vision "A chapter is opened with a name, a vision and a classification, and refuses a classification the language does not admit."
3
+
4
+ loads "bluebook.bluebook", "bluebook.hecksagon"
5
+
6
+ test "Declare opens a chapter" do
7
+ tests "Declare", on: "Bluebook"
8
+ input name: "Pizzas", vision: "Pizzas, built and sold.", classification: "core"
9
+ expect emits: ["ChapterDeclared"]
10
+ end
11
+
12
+ test "Declare refuses a classification that is not core, supporting or generic" do
13
+ tests "Declare", on: "Bluebook"
14
+ input name: "Pizzas", vision: "Pizzas, built and sold.", classification: "huge"
15
+ expect refused: "a chapter is core, supporting, or generic"
16
+ end
17
+
18
+ test "Declare refuses a chapter with no name" do
19
+ tests "Declare", on: "Bluebook"
20
+ input name: "", vision: "Pizzas, built and sold.", classification: "core"
21
+ expect refused: "a chapter is named"
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ Hecks.behaviors "Bluebook" do
2
+ vision "A policy hangs off a chapter and names the event it watches; an unnamed policy is refused."
3
+
4
+ loads "bluebook.bluebook", "policy.bluebook", "bluebook.hecksagon"
5
+
6
+ test "Declare puts a policy on a chapter" do
7
+ tests "Declare", on: "Policy"
8
+ setup "Bluebook::Bluebook.Declare", name: "Pizzas", vision: "Pizzas, built and sold.", classification: "core"
9
+ input bluebook: "Pizzas", name: "NotifyKitchen", on_event: "OrderPlaced"
10
+ expect emits: ["ReactionDeclared"]
11
+ end
12
+
13
+ test "Declare refuses a policy with no name" do
14
+ tests "Declare", on: "Policy"
15
+ setup "Bluebook::Bluebook.Declare", name: "Pizzas", vision: "Pizzas, built and sold.", classification: "core"
16
+ input bluebook: "Pizzas", name: "", on_event: "OrderPlaced"
17
+ expect refused: "a policy is named"
18
+ end
19
+ end
@@ -611,6 +611,10 @@ Hecks.bluebook "Bluebook" do
611
611
  # `mode` is how `pattern` (an extended regex over changed paths) decides:
612
612
  # `touches` runs the job when any changed path matches, `skips_unless` runs it
613
613
  # when any changed path does NOT match (the pattern is the safe-to-skip list).
614
+ # `release_only` answers a different question and ignores `pattern`: `touched` is false only
615
+ # when the change edits nothing but the `ReleaseFile` rows' files and lines and the base it is
616
+ # measured against has every inheritable `RequiredCheck` green (ADR 0107), so the run may
617
+ # inherit that certification.
614
618
  # `push` is what a push event does: `skip` leaves the gate out of push runs,
615
619
  # `before_sha` diffs against the push's `before` commit.
616
620
  value_object "CiGate" do
@@ -624,19 +628,44 @@ Hecks.bluebook "Bluebook" do
624
628
  member name: "runtime_changed", workflow: "ci.yml", mode: "touches",
625
629
  pattern: '^lib/hecks/runtime/', push: "before_sha",
626
630
  label: "does this change touch lib/hecks/runtime/**?"
631
+ member name: "release_only", workflow: "ci.yml", mode: "release_only",
632
+ pattern: "", push: "before_sha",
633
+ label: "is this change only a release bump on a certified base?"
627
634
  member name: "postgres_io_relevant_changed", workflow: "ci-postgres-io-parallel.yml", mode: "skips_unless",
628
635
  pattern: '^(docs/|editors/|release/|deploy/|\.claude/|\.githooks/|rust/(parser|codegen|host|build|lsp|web|tests)/|\.rubocop\.yml$|\.rubocop_todo\.yml$|\.mcp\.json$|\.rspec-local\.example$|README\.md$|CHANGELOG\.md$|CONTRIBUTING\.md$|SECURITY\.md$|LICENSE$|\.gitignore$)',
629
636
  push: "skip",
630
637
  label: "does this change touch anything rspec_postgres_io_parallel covers?"
631
638
  end
632
639
 
640
+ # The files a release bump edits, and the lines of each it may change. A commit whose whole
641
+ # diff against a certified base falls inside these rows inherits the base's certification
642
+ # (`CiGate` `release_only`, ADR 0107): its checks report success without their jobs running,
643
+ # and the `release_consistency` check still runs on it. `path` is the file, exact; `lines` is
644
+ # an extended regex every added or removed line of that file must match, empty for any line.
645
+ value_object "ReleaseFile" do
646
+ attribute :path, String
647
+ attribute :lines, String
648
+
649
+ member path: "lib/hecks/version.rb", lines: "^\\s*VERSION = \"[0-9]+\\.[0-9]+\\.[0-9]+\"\\.freeze$"
650
+ member path: "CHANGELOG.md", lines: ""
651
+ member path: "README.md", lines: "^(\\*\\*Status:\\*\\* )?Current release: `[0-9]+\\.[0-9]+\\.[0-9]+`"
652
+ member path: "Gemfile.lock", lines: "^ hecks \\([0-9]+\\.[0-9]+\\.[0-9]+\\)$"
653
+ member path: "rust/host/HECKS_RELEASE", lines: "^[0-9]+\\.[0-9]+\\.[0-9]+$"
654
+ member path: "packages/hecks-client/package.json", lines: "^\\s*\"version\": \"[0-9]+\\.[0-9]+\\.[0-9]+\",?$"
655
+ member path: "packages/hecks-client/package-lock.json", lines: "^\\s*\"version\": \"[0-9]+\\.[0-9]+\\.[0-9]+\",?$"
656
+ end
657
+
633
658
  # The checks a commit must have passed before a `Promotion` moves it onto a guarded lane. Each
634
659
  # is the name of a job in .github/workflows that reports to a commit as a check run; a check
635
660
  # that a path gate skipped reports `skipped`, which counts as passed. `hecks promote` reads
636
661
  # these rows, and hecks project_lanes writes them into the ruleset of any lane that requires
637
662
  # them, so the list lives here and nowhere else.
663
+ #
664
+ # `runs` is `always` for a check a release bump still runs (it is not inherited from the base);
665
+ # empty for a check a bump may inherit.
638
666
  value_object "RequiredCheck" do
639
667
  attribute :name, String
668
+ attribute :runs, String
640
669
 
641
670
  member name: "rspec"
642
671
  member name: "checks"
@@ -647,6 +676,7 @@ Hecks.bluebook "Bluebook" do
647
676
  member name: "rspec_rust_codegen"
648
677
  member name: "rspec_rust_host"
649
678
  member name: "rspec_fuzzing"
679
+ member name: "release_consistency", runs: "always"
650
680
  end
651
681
 
652
682
  # The branches a change travels along. `main` takes pushes with no gate; `stable` takes only
@@ -679,6 +709,28 @@ Hecks.bluebook "Bluebook" do
679
709
  release_trailer: "Release-Approved-By"
680
710
  end
681
711
 
712
+ # A binary hecks builds once per change to its crate and publishes, so a client that needs the
713
+ # same binary downloads it instead of compiling it (ADR 0110). `hecks regeneration_run.project_lanes`
714
+ # writes .github/workflows/publish-<name>.yml from each row; `Hecks::RustBuild::PrebuiltBinary`
715
+ # reads the same rows to name what to download.
716
+ #
717
+ # `crate` is the directory under the checkout that is built and keyed: the key is a hash of every
718
+ # file in it that goes into the build, so the same bytes in the crate are the same release.
719
+ # `executable` is the Cargo binary and `target` the Rust triple it is built for. `runner` is the
720
+ # GitHub-hosted image it is built on, pinned (never `-latest`) because a binary links the
721
+ # runner's glibc and must run on every host that runs that image or a newer one.
722
+ # The release is tagged `<name>-<key>` and carries `<executable>-<target>` and its `.sha256`.
723
+ value_object "PrebuiltBinary" do
724
+ attribute :name, String
725
+ attribute :crate, String
726
+ attribute :executable, String
727
+ attribute :target, String
728
+ attribute :runner, String
729
+
730
+ member name: "host", crate: "rust/host", executable: "bootstrap", target: "x86_64-unknown-linux-gnu",
731
+ runner: "ubuntu-24.04"
732
+ end
733
+
682
734
  # The dials of the hecks_qa loop: what each is called, the type of value it takes and what it
683
735
  # does. qa/settings.yml holds the values, which are edited and reviewed as a diff; this table
684
736
  # holds the rest, so the loader (`Hecks::Fuzzing::QaSettings`) checks the file against it and
@@ -47,10 +47,15 @@ module Hecks
47
47
  declared = algebra.fetch(operator[:symbol]) do
48
48
  raise Refused, "#{operator[:symbol]} is admitted but Vocabulary::Comparison declares no algebra for it"
49
49
  end
50
- row.merge(compares_less_than: declared[:compares_less_than],
51
- compares_equal: declared[:compares_equal], negated: declared[:negated])
50
+ row.merge(compares_less_than: spelled_true?(declared[:compares_less_than]),
51
+ compares_equal: spelled_true?(declared[:compares_equal]),
52
+ negated: spelled_true?(declared[:negated]))
52
53
  end
53
54
 
55
+ # @param cell [Object] a `Comparison` cell, declared as the text `"true"` or `"false"`
56
+ # @return [Boolean] whether the cell spells true
57
+ def spelled_true?(cell) = cell.to_s == "true"
58
+
54
59
  # @param dropped [Hash{String => Array<String>}] operator to the guards evaluating through it
55
60
  # @return [String] one line for each operator a projection would strand
56
61
  def dropped_message(dropped)
@@ -13,11 +13,13 @@ module Hecks
13
13
  # an ECS task definition when the world names one
14
14
  def render_compose_sh(plan, region)
15
15
  if plan.task_definition
16
- template("render-compose-taskdef.sh.tmpl", "STACK" => plan.infra_name, "FAMILY" => plan.task_definition,
17
- "PROXY_IMAGE" => plan.proxy_image)
16
+ template("render-compose-taskdef.sh.tmpl",
17
+ { "STACK" => plan.infra_name, "FAMILY" => plan.task_definition, "PROXY_IMAGE" => plan.proxy_image }
18
+ .merge(compose_log_values(plan, region, "$s.name")))
18
19
  else
19
- template("render-compose.sh.tmpl", "STACK" => plan.infra_name, "REGION" => region,
20
- "DB_NAME" => plan.database_name, "PROXY_IMAGE" => plan.proxy_image)
20
+ template("render-compose.sh.tmpl",
21
+ { "STACK" => plan.infra_name, "REGION" => region, "DB_NAME" => plan.database_name,
22
+ "PROXY_IMAGE" => plan.proxy_image }.merge(compose_log_values(plan, region, ".name")))
21
23
  end
22
24
  end
23
25
 
@@ -0,0 +1,62 @@
1
+ module Hecks
2
+ module Projections
3
+ module Deploy
4
+ module Box
5
+ # The CloudFront edge of a box deploy, for a world that declares `cdn`: the `edge.yaml`
6
+ # stack, the `edge.mk` that deploys it, and the two pieces `make stacks` gains so the box
7
+ # admits the CDN's addresses. Mixed into `Box`, which supplies `template`.
8
+ module Edge
9
+ # The AWS-managed prefix list of CloudFront's origin-facing addresses. Its id differs in
10
+ # every region, so it is looked up by this name rather than written down.
11
+ PREFIX_LIST_NAME = "com.amazonaws.global.cloudfront.origin-facing".freeze
12
+
13
+ # @param plan [Settings::Plan] the resolved settings
14
+ # @param region [String] the validated region
15
+ # @return [Hash{String => String}] `edge.yaml` and `edge.mk`, or nothing without a `cdn`
16
+ def edge_files(plan, region)
17
+ return {} unless plan.cdn
18
+
19
+ { "edge.yaml" => edge_yaml(plan),
20
+ "edge.mk" => template("edge.mk.tmpl", "EDGE_STACK" => plan.edge_stack, "REGION" => region) }
21
+ end
22
+
23
+ # @param plan [Settings::Plan] the resolved settings, with a `cdn`
24
+ # @return [String] the CloudFront stack
25
+ def edge_yaml(plan)
26
+ values = { "STACK" => plan.infra_name, "ALIASES" => plan.cdn.aliases.join(","),
27
+ "ORIGIN_HEADER" => plan.origin_header, "ORIGIN_SECRET" => plan.origin_secret }
28
+ template("edge.yaml.tmpl", values)
29
+ end
30
+
31
+ # @param plan [Settings::Plan] the resolved settings
32
+ # @param region [String] the validated region
33
+ # @return [Hash{String => String}] the Makefile markers `CDN_CHECK` and `CDN_PARAM`:
34
+ # empty without a `cdn`, so a world that does not opt in renders the same Makefile
35
+ def edge_makefile_values(plan, region)
36
+ return { "CDN_CHECK" => "", "CDN_PARAM" => "" } unless plan.cdn
37
+
38
+ { "CDN_CHECK" => prefix_list_check(region), "CDN_PARAM" => "\t\tCdnPrefixListId=#{prefix_list_lookup(region)} \\" }
39
+ end
40
+
41
+ private
42
+
43
+ def prefix_list_query(region)
44
+ "aws ec2 describe-managed-prefix-lists --region #{region} --filters " \
45
+ "Name=prefix-list-name,Values=#{PREFIX_LIST_NAME} --query \"PrefixLists[0].PrefixListId\" --output text"
46
+ end
47
+
48
+ def prefix_list_lookup(region)
49
+ "$$(#{prefix_list_query(region)})"
50
+ end
51
+
52
+ # Stops `make stacks` before it changes anything when the prefix list cannot be found,
53
+ # because a box deployed without it has no inbound rule and the CDN gets a 504.
54
+ def prefix_list_check(region)
55
+ "\t@#{prefix_list_query(region)} | grep -q '^pl-' || { echo \"no #{PREFIX_LIST_NAME} " \
56
+ "prefix list in #{region}: the box would admit no CDN and answer 504\" >&2; exit 1; }"
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end