hecks 3.11.0 → 3.13.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/script_runner.adapter +3 -0
  3. data/lib/hecks/adapters/driven/script_runner.rb +93 -0
  4. data/lib/hecks/adapters/driven.rb +1 -0
  5. data/lib/hecks/adapters/driving/cli_runner/resolution.rb +6 -3
  6. data/lib/hecks/adapters/driving/cli_runner.rb +1 -0
  7. data/lib/hecks/adapters/driving/newcomer.rb +50 -0
  8. data/lib/hecks/bluebook/assembly/contracts/read_models.rb +1 -1
  9. data/lib/hecks/bluebook/assembly/marks.rb +3 -15
  10. data/lib/hecks/bluebook/assembly/scalars.rb +39 -0
  11. data/lib/hecks/bluebook/meta_validator/readings.rb +6 -0
  12. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +6 -3
  13. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +16 -12
  14. data/lib/hecks/bluebook/meta_validator.rb +28 -21
  15. data/lib/hecks/bluebook.rb +1 -0
  16. data/lib/hecks/deploy/bluebook/deploy.bluebook +17 -3
  17. data/lib/hecks/framework/bluebook/change.behaviors +30 -0
  18. data/lib/hecks/framework/bluebook/change.bluebook +162 -0
  19. data/lib/hecks/framework/bluebook/change.hecksagon +21 -0
  20. data/lib/hecks/framework/change.oidc.json +34 -0
  21. data/lib/hecks/framework/console_settings.oidc.json +103 -0
  22. data/lib/hecks/framework/governance.oidc.json +28 -0
  23. data/lib/hecks/framework/identity.oidc.json +18 -0
  24. data/lib/hecks/framework/privacy.oidc.json +30 -0
  25. data/lib/hecks/gate/stages.yml +35 -0
  26. data/lib/hecks/grammar/translation.oidc.json +48 -0
  27. data/lib/hecks/hecks/adapters/codebase/test_runner.rb +12 -1
  28. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +2 -1
  29. data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +5 -1
  30. data/lib/hecks/hecks/hecks.world +3 -0
  31. data/lib/hecks/language/bluebook/bluebook.behaviors +23 -0
  32. data/lib/hecks/language/bluebook/policy.behaviors +19 -0
  33. data/lib/hecks/language/bluebook/vocabulary.bluebook +52 -0
  34. data/lib/hecks/projection_files/expression_tables.rb +7 -2
  35. data/lib/hecks/projections/deploy/box/compose.rb +6 -4
  36. data/lib/hecks/projections/deploy/box/edge.rb +62 -0
  37. data/lib/hecks/projections/deploy/box/logging.rb +99 -0
  38. data/lib/hecks/projections/deploy/box/roll_scripts.rb +16 -8
  39. data/lib/hecks/projections/deploy/box/settings/cdn.rb +40 -0
  40. data/lib/hecks/projections/deploy/box/settings/logs.rb +40 -0
  41. data/lib/hecks/projections/deploy/box/settings/patterns.rb +11 -0
  42. data/lib/hecks/projections/deploy/box/settings/types.rb +16 -1
  43. data/lib/hecks/projections/deploy/box/settings.rb +6 -2
  44. data/lib/hecks/projections/deploy/box/stack_yaml.rb +5 -2
  45. data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +16 -0
  46. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +8 -4
  47. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +3 -0
  48. data/lib/hecks/projections/deploy/box/templates/edge.mk.tmpl +17 -0
  49. data/lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl +65 -0
  50. data/lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl +12 -0
  51. data/lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl +7 -0
  52. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +5 -4
  53. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +5 -4
  54. data/lib/hecks/projections/deploy/box.rb +15 -3
  55. data/lib/hecks/projections/deploy/fargate/cdn/lines.rb +12 -1
  56. data/lib/hecks/projections/site/admin_cms/templates/membership.rb +10 -7
  57. data/lib/hecks/projections/site/edge/behaviours.rb +10 -6
  58. data/lib/hecks/projections/site/edge/checks.rb +2 -0
  59. data/lib/hecks/projections/site/edge/function_checks.rb +104 -0
  60. data/lib/hecks/projections/site/edge/function_code.rb +86 -0
  61. data/lib/hecks/projections/site/edge/redirect_host.js.tmpl +65 -0
  62. data/lib/hecks/projections/site/edge.rb +35 -9
  63. data/lib/hecks/projections/site/site_cdn.rb +21 -7
  64. data/lib/hecks/projections/site/site_root/workflow.rb +5 -0
  65. data/lib/hecks/projections/site/table/row_builder.rb +12 -2
  66. data/lib/hecks/projections/site/table.rb +4 -2
  67. data/lib/hecks/projector/cli_audience.rb +20 -0
  68. data/lib/hecks/projector/cli_projector/usage.rb +15 -4
  69. data/lib/hecks/rust_build/host.rb +11 -4
  70. data/lib/hecks/rust_build/prebuilt_binary.rb +101 -0
  71. data/lib/hecks/site/bluebook/site.bluebook +16 -1
  72. data/lib/hecks/tools/ci_gate_decision.rb +35 -0
  73. data/lib/hecks/tools/ci_gates.rb +20 -1
  74. data/lib/hecks/tools/lanes/files.rb +8 -0
  75. data/lib/hecks/tools/lanes/prebuilt_workflow.rb +81 -0
  76. data/lib/hecks/tools/lanes.rb +1 -0
  77. data/lib/hecks/tools/oidc_manifests.rb +44 -6
  78. data/lib/hecks/tools/release_only.rb +111 -0
  79. data/lib/hecks/tools/site_routes/templates.rb +13 -9
  80. data/lib/hecks/version.rb +1 -1
  81. data/lib/hecks/vocabulary.rb +15 -1
  82. data/prebuilt/2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal +0 -0
  83. data/prebuilt/58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal +0 -0
  84. data/prebuilt/{verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json → verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json} +1 -1
  85. data/rust/host/HECKS_RELEASE +1 -1
  86. data/rust/host/src/auth.rs +58 -0
  87. data/rust/host/src/extension.rs +13 -1
  88. data/rust/host/src/lib.rs +1 -0
  89. data/rust/host/src/platform_signin/attack_tests.rs +1100 -0
  90. data/rust/host/src/platform_signin/config.rs +512 -0
  91. data/rust/host/src/platform_signin/cookie.rs +237 -0
  92. data/rust/host/src/platform_signin/extension.rs +190 -0
  93. data/rust/host/src/platform_signin/fetcher.rs +322 -0
  94. data/rust/host/src/platform_signin/handler.rs +251 -0
  95. data/rust/host/src/platform_signin/handler_tests.rs +554 -0
  96. data/rust/host/src/platform_signin/jti.rs +196 -0
  97. data/rust/host/src/platform_signin/jwks.rs +542 -0
  98. data/rust/host/src/platform_signin/jws.rs +249 -0
  99. data/rust/host/src/platform_signin/known_answer_tests.rs +237 -0
  100. data/rust/host/src/platform_signin/mapping_tests.rs +254 -0
  101. data/rust/host/src/platform_signin/mod.rs +75 -0
  102. data/rust/host/src/platform_signin/mode_tests.rs +382 -0
  103. data/rust/host/src/platform_signin/pages.rs +204 -0
  104. data/rust/host/src/platform_signin/query.rs +233 -0
  105. data/rust/host/src/platform_signin/session.rs +151 -0
  106. data/rust/host/src/platform_signin/settings.rs +385 -0
  107. data/rust/host/src/platform_signin/strict_json.rs +351 -0
  108. data/rust/host/src/platform_signin/test_support.rs +235 -0
  109. data/rust/host/src/platform_signin/ttl_tests.rs +94 -0
  110. data/rust/host/src/platform_signin/verify.rs +604 -0
  111. data/rust/host/src/platform_signin/wiring_support.rs +319 -0
  112. data/rust/host/src/web.rs +50 -6
  113. data/rust/parser/src/parse/value_object.rs +22 -10
  114. metadata +59 -7
  115. data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
  116. data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
  117. /data/lib/hecks/framework/{oidc.json → compliance.oidc.json} +0 -0
  118. /data/lib/hecks/grammar/{oidc.json → expression.oidc.json} +0 -0
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Tools
5
+ module Lanes
6
+ # The workflow that builds and publishes one `PrebuiltBinary` row (ADR 0110): after a push to
7
+ # `main` that touches the row's crate, it builds the binary on the row's runner and, unless
8
+ # a release for that key already exists, publishes it with its sha256. What the key is and
9
+ # where a client looks is `Hecks::RustBuild::PrebuiltBinary`, not anything written here.
10
+ module PrebuiltWorkflow
11
+ module_function
12
+
13
+ # @param row [Hash{String => String}] a `PrebuiltBinary` row
14
+ # @return [String] the path of the workflow, relative to the checkout
15
+ def path(row) = ".github/workflows/publish-#{row["name"]}.yml"
16
+
17
+ # @param row [Hash{String => String}] a `PrebuiltBinary` row
18
+ # @return [String] the workflow
19
+ def text(row)
20
+ [*header(row), *trigger(row), *job(row)].join("\n") << "\n"
21
+ end
22
+
23
+ def header(row)
24
+ name = row["name"]
25
+ ["name: Publish #{name}", "",
26
+ "# Generated by hecks project_lanes from the PrebuiltBinary rows of the Vocabulary chapter. Do not hand-edit.",
27
+ "#",
28
+ "# Builds #{row["executable"]} from #{row["crate"]} for #{row["target"]} and publishes it as the release",
29
+ "# #{name}-<key>, where the key is a hash of the crate's build inputs. A client that needs the same",
30
+ "# bytes downloads it instead of compiling (Hecks::RustBuild::PrebuiltBinary); a key that already has a",
31
+ "# release is left alone, so a push that changed nothing the build reads publishes nothing."]
32
+ end
33
+
34
+ def trigger(row)
35
+ ["on:", " push:", " branches: [main]", " paths:", " - \"#{row["crate"]}/**\"",
36
+ " - \".github/workflows/publish-#{row["name"]}.yml\"",
37
+ " - \"lib/hecks/rust_build/prebuilt_binary.rb\"",
38
+ " workflow_dispatch: {}", "",
39
+ "concurrency:", " group: publish-#{row["name"]}", " cancel-in-progress: false", "",
40
+ "permissions:", " contents: write", ""]
41
+ end
42
+
43
+ def job(row)
44
+ ["jobs:", " publish:", " runs-on: #{row["runner"]}", " timeout-minutes: 40", " steps:",
45
+ " - uses: actions/checkout@v4", " - uses: ./.github/actions/setup-ruby", *key_step(row),
46
+ *build_steps(row), *release_step(row)]
47
+ end
48
+
49
+ def key_step(row)
50
+ [" - name: Name the release", " id: key", " run: |",
51
+ " key=$(bundle exec ruby -Ilib -rhecks/rust_build/prebuilt_binary -e \\",
52
+ " 'puts Hecks::RustBuild::PrebuiltBinary.key(ARGV.fetch(0))' #{row["crate"]})",
53
+ " tag=#{row["name"]}-$key",
54
+ " echo \"tag=$tag\" >> \"$GITHUB_OUTPUT\"",
55
+ " if gh release view \"$tag\" > /dev/null 2>&1; then echo \"exists=true\" >> \"$GITHUB_OUTPUT\"; fi",
56
+ " env:", " GH_TOKEN: ${{ github.token }}"]
57
+ end
58
+
59
+ def build_steps(row)
60
+ [" - name: Build #{row["executable"]}", " if: steps.key.outputs.exists != 'true'",
61
+ " working-directory: #{row["crate"]}", " run: |",
62
+ " rustup toolchain install stable --profile minimal",
63
+ " rustup run stable cargo build --release --target #{row["target"]} --bin #{row["executable"]}"]
64
+ end
65
+
66
+ def release_step(row)
67
+ asset = "#{row["executable"]}-#{row["target"]}"
68
+ [" - name: Publish the release", " if: steps.key.outputs.exists != 'true'",
69
+ " env:", " GH_TOKEN: ${{ github.token }}", " TAG: ${{ steps.key.outputs.tag }}",
70
+ " run: |",
71
+ " mkdir out",
72
+ " cp #{row["crate"]}/target/#{row["target"]}/release/#{row["executable"]} out/#{asset}",
73
+ " (cd out && sha256sum #{asset} > #{asset}.sha256)",
74
+ " gh release create \"$TAG\" out/#{asset} out/#{asset}.sha256 --prerelease --target \"$GITHUB_SHA\" \\",
75
+ " --title \"$TAG\" \\",
76
+ " --notes \"#{row["executable"]} for #{row["target"]}, built from #{row["crate"]} at $GITHUB_SHA.\""]
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -78,5 +78,6 @@ require_relative "lanes/rows"
78
78
  require_relative "lanes/ruleset"
79
79
  require_relative "lanes/workflow"
80
80
  require_relative "lanes/watch_workflow"
81
+ require_relative "lanes/prebuilt_workflow"
81
82
  require_relative "lanes/files"
82
83
  require_relative "lanes/live"
@@ -6,7 +6,8 @@ require_relative "../tools"
6
6
  module Hecks
7
7
  module Tools
8
8
  # Projects each domain's OIDC client and scope manifest (`Hecks::Projections::OIDC`) into an
9
- # `oidc.json` beside the domain.
9
+ # `oidc.json` beside the domain, or into one `<chapter>.oidc.json` for each chapter of a
10
+ # directory that holds several.
10
11
  #
11
12
  # A domain that has no bluebook is skipped and one that cannot be projected is reported, so a
12
13
  # sweep over the whole checkout carries on past either. `hecks deploy
@@ -45,20 +46,57 @@ module Hecks
45
46
  .uniq.sort
46
47
  end
47
48
 
49
+ # Projects one manifest for each chapter the directory holds.
50
+ #
48
51
  # @param root [String] the checkout
49
52
  # @param path [String] a domain directory, relative to `root`
50
53
  # @return [void]
51
54
  def project(root, path)
52
- runtime = Hecks.boot(File.join(root, path), install_driving: false)
53
- name = runtime.registry.bluebooks.keys.first
54
- return warn(" #{path}: no bluebook found — skipped") unless name
55
+ registry = Hecks.boot(File.join(root, path), install_driving: false).registry
56
+ return warn(" #{path}: no bluebook found — skipped") if registry.bluebooks.empty?
55
57
 
56
- write_manifest(runtime.registry, name, File.join(root, path, "oidc.json"))
57
- puts " #{path}/oidc.json <- #{name}"
58
+ project_chapters(registry, root, path)
58
59
  rescue StandardError => e
59
60
  warn " #{path}: cannot project — #{e.message.lines.first.strip}"
60
61
  end
61
62
 
63
+ # The chapters a directory declares: those with a `.bluebook` file under it. A chapter it
64
+ # only attaches (Governance, for one) is loaded from elsewhere and has its own directory's
65
+ # manifest.
66
+ #
67
+ # @param registry [Hecks::Runtime::Registry] the booted registry of the directory
68
+ # @param directory [String] the directory that was booted
69
+ # @return [Array<String>] the chapter names, in load order; the first loaded when none has a
70
+ # recorded source
71
+ def chapters_of(registry, directory)
72
+ under = "#{File.expand_path(directory)}/"
73
+ own = registry.bluebooks.keys.select do |name|
74
+ Array(registry.bluebook_sources[name]).any? { |source| File.expand_path(source).start_with?(under) }
75
+ end
76
+ own.empty? ? registry.bluebooks.keys.first(1) : own
77
+ end
78
+
79
+ # @param registry [Hecks::Runtime::Registry] the booted registry of the directory
80
+ # @param root [String] the checkout
81
+ # @param path [String] the domain directory, relative to `root`
82
+ # @return [void]
83
+ def project_chapters(registry, root, path)
84
+ names = chapters_of(registry, File.join(root, path))
85
+ names.each do |name|
86
+ file = file_for(names, name)
87
+ write_manifest(registry, name, File.join(root, path, file))
88
+ puts " #{path}/#{file} <- #{name}"
89
+ end
90
+ end
91
+
92
+ # A domain of one chapter keeps `oidc.json`; a directory of several chapters, such as the
93
+ # framework members, gets one manifest each, so none of them stands for the others.
94
+ #
95
+ # @param names [Array<String>] every chapter the directory holds
96
+ # @param name [String] the chapter the manifest is for
97
+ # @return [String] the manifest's file name: `oidc.json`, or `<chapter>.oidc.json`
98
+ def file_for(names, name) = names.one? ? "oidc.json" : "#{Naming.snake(name)}.oidc.json"
99
+
62
100
  # @param registry [Hecks::Runtime::Registry] the booted registry
63
101
  # @param name [String] the bluebook to project
64
102
  # @param out [String] where the manifest goes
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+ require "hecks/vocabulary"
6
+ require_relative "../tools"
7
+
8
+ module Hecks
9
+ module Tools
10
+ # Decides whether a commit is only a release bump on a base that is fully certified, so CI may
11
+ # inherit the base's certification instead of running the suite again.
12
+ #
13
+ # The files a bump edits, and which of their lines it may change, are the `ReleaseFile` rows of
14
+ # the Vocabulary chapter. A change is a release bump when it touches at least one file, every
15
+ # file it touches is a row, and every line it adds or removes in a file matches that row's
16
+ # `lines` pattern (an empty pattern admits any line). Whether the base is certified is read
17
+ # from the base commit's own check runs: every `RequiredCheck` row that is not marked
18
+ # `runs: always` must have its latest run on the base succeed or be skipped.
19
+ #
20
+ # Every case that cannot be confirmed answers "not a release bump", so the suite runs.
21
+ module ReleaseOnly
22
+ GREEN = %w[success skipped].freeze
23
+
24
+ module_function
25
+
26
+ # @param changes [Hash{String => Array<String>}] each changed path to the lines it adds or
27
+ # removes
28
+ # @param files [Array<Hash{String => String}>] the `ReleaseFile` rows
29
+ # @return [Boolean] whether the change touches only release files, and only their release
30
+ # lines
31
+ def release_only?(changes, files: Hecks::Vocabulary.rows("ReleaseFile"))
32
+ return false if changes.empty?
33
+
34
+ by_path = files.to_h { |row| [row.fetch("path"), row.fetch("lines", "")] }
35
+ changes.all? { |path, lines| by_path.key?(path) && lines_allowed?(lines, by_path.fetch(path)) }
36
+ end
37
+
38
+ # @param lines [Array<String>] the changed lines of one file
39
+ # @param pattern [String] the row's `lines` regex, empty for any line
40
+ # @return [Boolean] whether every changed line matches
41
+ def lines_allowed?(lines, pattern)
42
+ return true if pattern.to_s.empty?
43
+
44
+ regex = Regexp.new(pattern)
45
+ lines.all? { |line| line.match?(regex) }
46
+ end
47
+
48
+ # @param runs [Array<Hash{String => String}>] the base commit's check runs: `name`,
49
+ # `conclusion` and `started_at`
50
+ # @param required [Array<Hash{String => String}>] the `RequiredCheck` rows
51
+ # @return [Boolean] whether every check that must be inherited has a green latest run
52
+ def certified?(runs, required: Hecks::Vocabulary.rows("RequiredCheck"))
53
+ latest = latest_runs(runs)
54
+ inherited = required.reject { |row| row["runs"] == "always" }.map { |row| row.fetch("name") }
55
+ inherited.all? { |name| GREEN.include?(latest[name].to_h["conclusion"]) }
56
+ end
57
+
58
+ # @return [Hash{String => Hash}] each check's most recently started run
59
+ def latest_runs(runs)
60
+ runs.group_by { |run| run["name"] }.transform_values { |group| group.max_by { |run| run["started_at"].to_s } }
61
+ end
62
+
63
+ # @param base [String] the commit the change is measured against
64
+ # @param head [String] the commit under test
65
+ # @param root [String] the checkout
66
+ # @return [Hash{String => Array<String>}, nil] each changed path to its changed lines, or nil
67
+ # when git could not say
68
+ def changes(base, head, root:)
69
+ out, _err, status = Open3.capture3("git", "-C", root, "diff", "-U0", "--no-color", base, head)
70
+ status.success? ? parse(out) : nil
71
+ end
72
+
73
+ # @param diff [String] `git diff -U0` output
74
+ # @return [Hash{String => Array<String>}] each changed path to the lines it adds or removes
75
+ def parse(diff)
76
+ found = Hash.new { |hash, key| hash[key] = [] }
77
+ path = nil
78
+ diff.each_line do |line|
79
+ path = file_of(line.chomp) || path
80
+ found[path] << line[1..].chomp if path && content_line?(line)
81
+ end
82
+ found.to_h
83
+ end
84
+
85
+ # @return [String, nil] the path a `+++` or `---` header names, nil for any other line
86
+ def file_of(line)
87
+ return line.delete_prefix("+++ b/") if line.start_with?("+++ b/")
88
+
89
+ line.delete_prefix("--- a/") if line.start_with?("--- a/")
90
+ end
91
+
92
+ # @return [Boolean] whether the line is an added or removed line, not a header
93
+ def content_line?(line)
94
+ line.match?(/\A[+-]/) && !line.start_with?("+++ ", "--- ")
95
+ end
96
+
97
+ # @param repository [String] `owner/name`
98
+ # @param sha [String] the commit whose check runs are read
99
+ # @param env [Hash{String => String}] carries `GH_TOKEN` for the API call
100
+ # @return [Array<Hash{String => String}>, nil] the check runs, or nil when the API failed
101
+ def check_runs(repository, sha, env: ENV)
102
+ path = "repos/#{repository}/commits/#{sha}/check-runs?per_page=100"
103
+ jq = ".check_runs[] | {name, conclusion, started_at}"
104
+ out, _err, status = Open3.capture3(env.to_h.slice("GH_TOKEN"), "gh", "api", path, "--paginate", "--jq", jq)
105
+ status.success? ? out.lines.map { |line| JSON.parse(line) } : nil
106
+ rescue JSON::ParserError
107
+ nil
108
+ end
109
+ end
110
+ end
111
+ end
@@ -27,22 +27,19 @@ module Hecks
27
27
  regions = Projector.call(:site_cdn, bluebook: site.chapter, options: { table: site.table, edge: edge })
28
28
  relative = template || edge.setting.template
29
29
  source = template_source(root, template, relative)
30
- text = rewritten(File.read(source), regions, relative, edge)
30
+ text = rewritten(File.read(source), regions, relative)
31
31
  { destination(source, out, template, relative) => text }
32
32
  end
33
33
 
34
34
  # @param text [String] the template
35
35
  # @param regions [Hash{String => String}] each region's name to the block that goes in it
36
36
  # @param relative [String] the template's name, for a message
37
- # @param edge [Projections::Site::Edge] the checked edge
38
37
  # @return [String] the template with each region rewritten
39
- # @raise [SystemExit] when a region is missing, or a listener_rules region is left behind
40
- # by an edge with no load balancer
41
- def rewritten(text, regions, relative, edge)
42
- if !edge.alb? && Projections::Site::Regions.region?(text, "listener_rules")
43
- abort "project_site: #{relative} has a BEGIN/END GENERATED site_cdn listener_rules region, " \
44
- "and the Edge row says alb: false; remove the region"
45
- end
38
+ # @raise [SystemExit] when a region is missing, or a listener_rules or edge_functions region
39
+ # is left behind by an edge that has no load balancer or no EdgeFunction row
40
+ def rewritten(text, regions, relative)
41
+ refuse_leftover(text, regions, relative, "listener_rules", "the Edge row says alb: false")
42
+ refuse_leftover(text, regions, relative, "edge_functions", "the project declares no EdgeFunction rows")
46
43
  regions.reduce(text) do |current, (name, block)|
47
44
  unless Projections::Site::Regions.region?(current, name)
48
45
  abort "project_site: #{relative} has no BEGIN/END GENERATED site_cdn #{name} region"
@@ -52,6 +49,13 @@ module Hecks
52
49
  end
53
50
  end
54
51
 
52
+ # Refuses a region the edge does not use, so the stale text in it is removed by hand.
53
+ def refuse_leftover(text, regions, relative, name, reason)
54
+ return if regions.key?(name) || !Projections::Site::Regions.region?(text, name)
55
+
56
+ abort "project_site: #{relative} has a BEGIN/END GENERATED site_cdn #{name} region, and #{reason}; remove the region"
57
+ end
58
+
55
59
  # @return [Hash] nothing, after refusing a template named for a project with no edge
56
60
  def missing_edge(template)
57
61
  abort "project_site: --template names #{template}, but the project declares no Edge rows" if template
data/lib/hecks/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Hecks
2
2
  # Read directly by the gemspec, so this file must never require the
3
3
  # rest of the gem (its dependencies aren't resolved yet at that point).
4
- VERSION = "3.11.0".freeze
4
+ VERSION = "3.13.0".freeze
5
5
  end
@@ -51,6 +51,7 @@ module Hecks
51
51
  ].freeze,
52
52
  "CiGate" => [
53
53
  {"name"=>"runtime_changed", "workflow"=>"ci.yml", "mode"=>"touches", "pattern"=>"^lib/hecks/runtime/", "push"=>"before_sha", "label"=>"does this change touch lib/hecks/runtime/**?"}.freeze,
54
+ {"name"=>"release_only", "workflow"=>"ci.yml", "mode"=>"release_only", "pattern"=>"", "push"=>"before_sha", "label"=>"is this change only a release bump on a certified base?"}.freeze,
54
55
  {"name"=>"postgres_io_relevant_changed", "workflow"=>"ci-postgres-io-parallel.yml", "mode"=>"skips_unless", "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$)", "push"=>"skip", "label"=>"does this change touch anything rspec_postgres_io_parallel covers?"}.freeze
55
56
  ].freeze,
56
57
  "Comparison" => [
@@ -134,6 +135,9 @@ module Hecks
134
135
  {"name"=>"replace"}.freeze,
135
136
  {"name"=>"scale_call"}.freeze
136
137
  ].freeze,
138
+ "PrebuiltBinary" => [
139
+ {"name"=>"host", "crate"=>"rust/host", "executable"=>"bootstrap", "target"=>"x86_64-unknown-linux-gnu", "runner"=>"ubuntu-24.04"}.freeze
140
+ ].freeze,
137
141
  "Primitive" => [
138
142
  {"name"=>"String"}.freeze,
139
143
  {"name"=>"Integer"}.freeze,
@@ -363,6 +367,15 @@ module Hecks
363
367
  {"refusal"=>"AttributeAbsent", "site"=>"absent_read", "template"=>"{aggregate} {field} is absent on this record — declared, not optional, and added since it was written. Backfill it in a translation (backfill :{field}, default: ...), or declare it optional: true"}.freeze,
364
368
  {"refusal"=>"ProjectionAbsent", "site"=>"absent_read", "template"=>"{aggregate} {field} is not yet projected on this record — declared via projects :{field}, but no rebuild sweep has populated it. Run the sweep, or read {reference}.{remote_field} directly if this rule cannot wait"}.freeze
365
369
  ].freeze,
370
+ "ReleaseFile" => [
371
+ {"path"=>"lib/hecks/version.rb", "lines"=>"^\\s*VERSION = \"[0-9]+\\.[0-9]+\\.[0-9]+\"\\.freeze$"}.freeze,
372
+ {"path"=>"CHANGELOG.md", "lines"=>""}.freeze,
373
+ {"path"=>"README.md", "lines"=>"^(\\*\\*Status:\\*\\* )?Current release: `[0-9]+\\.[0-9]+\\.[0-9]+`"}.freeze,
374
+ {"path"=>"Gemfile.lock", "lines"=>"^ hecks \\([0-9]+\\.[0-9]+\\.[0-9]+\\)$"}.freeze,
375
+ {"path"=>"rust/host/HECKS_RELEASE", "lines"=>"^[0-9]+\\.[0-9]+\\.[0-9]+$"}.freeze,
376
+ {"path"=>"packages/hecks-client/package.json", "lines"=>"^\\s*\"version\": \"[0-9]+\\.[0-9]+\\.[0-9]+\",?$"}.freeze,
377
+ {"path"=>"packages/hecks-client/package-lock.json", "lines"=>"^\\s*\"version\": \"[0-9]+\\.[0-9]+\\.[0-9]+\",?$"}.freeze
378
+ ].freeze,
366
379
  "RequiredCheck" => [
367
380
  {"name"=>"rspec"}.freeze,
368
381
  {"name"=>"checks"}.freeze,
@@ -372,7 +385,8 @@ module Hecks
372
385
  {"name"=>"rspec_rust_parser"}.freeze,
373
386
  {"name"=>"rspec_rust_codegen"}.freeze,
374
387
  {"name"=>"rspec_rust_host"}.freeze,
375
- {"name"=>"rspec_fuzzing"}.freeze
388
+ {"name"=>"rspec_fuzzing"}.freeze,
389
+ {"name"=>"release_consistency", "runs"=>"always"}.freeze
376
390
  ].freeze,
377
391
  "RetiredScript" => [
378
392
  {"script"=>"ir", "section"=>"Custodian", "aggregate"=>"Introspection", "verb"=>"ir"}.freeze,