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.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/script_runner.adapter +3 -0
- data/lib/hecks/adapters/driven/script_runner.rb +93 -0
- data/lib/hecks/adapters/driven.rb +1 -0
- data/lib/hecks/adapters/driving/cli_runner/resolution.rb +6 -3
- data/lib/hecks/adapters/driving/cli_runner.rb +1 -0
- data/lib/hecks/adapters/driving/newcomer.rb +50 -0
- data/lib/hecks/bluebook/assembly/contracts/read_models.rb +1 -1
- data/lib/hecks/bluebook/assembly/marks.rb +3 -15
- data/lib/hecks/bluebook/assembly/scalars.rb +39 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +6 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +6 -3
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +16 -12
- data/lib/hecks/bluebook/meta_validator.rb +28 -21
- data/lib/hecks/bluebook.rb +1 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +17 -3
- data/lib/hecks/framework/bluebook/change.behaviors +30 -0
- data/lib/hecks/framework/bluebook/change.bluebook +162 -0
- data/lib/hecks/framework/bluebook/change.hecksagon +21 -0
- data/lib/hecks/framework/change.oidc.json +34 -0
- data/lib/hecks/framework/console_settings.oidc.json +103 -0
- data/lib/hecks/framework/governance.oidc.json +28 -0
- data/lib/hecks/framework/identity.oidc.json +18 -0
- data/lib/hecks/framework/privacy.oidc.json +30 -0
- data/lib/hecks/gate/stages.yml +35 -0
- data/lib/hecks/grammar/translation.oidc.json +48 -0
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +12 -1
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +2 -1
- data/lib/hecks/hecks/adapters/rehearsal/postgres_store.rb +5 -1
- data/lib/hecks/hecks/hecks.world +3 -0
- data/lib/hecks/language/bluebook/bluebook.behaviors +23 -0
- data/lib/hecks/language/bluebook/policy.behaviors +19 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +52 -0
- data/lib/hecks/projection_files/expression_tables.rb +7 -2
- data/lib/hecks/projections/deploy/box/compose.rb +6 -4
- data/lib/hecks/projections/deploy/box/edge.rb +62 -0
- data/lib/hecks/projections/deploy/box/logging.rb +99 -0
- data/lib/hecks/projections/deploy/box/roll_scripts.rb +16 -8
- data/lib/hecks/projections/deploy/box/settings/cdn.rb +40 -0
- data/lib/hecks/projections/deploy/box/settings/logs.rb +40 -0
- data/lib/hecks/projections/deploy/box/settings/patterns.rb +11 -0
- data/lib/hecks/projections/deploy/box/settings/types.rb +16 -1
- data/lib/hecks/projections/deploy/box/settings.rb +6 -2
- data/lib/hecks/projections/deploy/box/stack_yaml.rb +5 -2
- data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +16 -0
- data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +8 -4
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +3 -0
- data/lib/hecks/projections/deploy/box/templates/edge.mk.tmpl +17 -0
- data/lib/hecks/projections/deploy/box/templates/edge.yaml.tmpl +65 -0
- data/lib/hecks/projections/deploy/box/templates/log-group.yaml.tmpl +12 -0
- data/lib/hecks/projections/deploy/box/templates/log-policy.yaml.tmpl +7 -0
- data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +5 -4
- data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +5 -4
- data/lib/hecks/projections/deploy/box.rb +15 -3
- data/lib/hecks/projections/deploy/fargate/cdn/lines.rb +12 -1
- data/lib/hecks/projections/site/admin_cms/templates/membership.rb +10 -7
- data/lib/hecks/projections/site/edge/behaviours.rb +10 -6
- data/lib/hecks/projections/site/edge/checks.rb +2 -0
- data/lib/hecks/projections/site/edge/function_checks.rb +104 -0
- data/lib/hecks/projections/site/edge/function_code.rb +86 -0
- data/lib/hecks/projections/site/edge/redirect_host.js.tmpl +65 -0
- data/lib/hecks/projections/site/edge.rb +35 -9
- data/lib/hecks/projections/site/site_cdn.rb +21 -7
- data/lib/hecks/projections/site/site_root/workflow.rb +5 -0
- data/lib/hecks/projections/site/table/row_builder.rb +12 -2
- data/lib/hecks/projections/site/table.rb +4 -2
- data/lib/hecks/projector/cli_audience.rb +20 -0
- data/lib/hecks/projector/cli_projector/usage.rb +15 -4
- data/lib/hecks/rust_build/host.rb +11 -4
- data/lib/hecks/rust_build/prebuilt_binary.rb +101 -0
- data/lib/hecks/site/bluebook/site.bluebook +16 -1
- data/lib/hecks/tools/ci_gate_decision.rb +35 -0
- data/lib/hecks/tools/ci_gates.rb +20 -1
- data/lib/hecks/tools/lanes/files.rb +8 -0
- data/lib/hecks/tools/lanes/prebuilt_workflow.rb +81 -0
- data/lib/hecks/tools/lanes.rb +1 -0
- data/lib/hecks/tools/oidc_manifests.rb +44 -6
- data/lib/hecks/tools/release_only.rb +111 -0
- data/lib/hecks/tools/site_routes/templates.rb +13 -9
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +15 -1
- data/prebuilt/2b59737fbdbd5f44edda0fddce2611f45d76087b3a7c782d1f1a17ea58c77948.marshal +0 -0
- data/prebuilt/58208399bada10b0db99928c48e38789288fc58f7b71a72b21bfa386680e32ed.marshal +0 -0
- data/prebuilt/{verdicts-230ac18c25abfd3d92ede032bee2661a4c2cd2773ab0cf0e2ae52546ba2f7af9.json → verdicts-48318ca172edd21443b1ad02801c697fff100b746e828f09db987647641a5df2.json} +1 -1
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/auth.rs +58 -0
- data/rust/host/src/extension.rs +13 -1
- data/rust/host/src/lib.rs +1 -0
- data/rust/host/src/platform_signin/attack_tests.rs +1100 -0
- data/rust/host/src/platform_signin/config.rs +512 -0
- data/rust/host/src/platform_signin/cookie.rs +237 -0
- data/rust/host/src/platform_signin/extension.rs +190 -0
- data/rust/host/src/platform_signin/fetcher.rs +322 -0
- data/rust/host/src/platform_signin/handler.rs +251 -0
- data/rust/host/src/platform_signin/handler_tests.rs +554 -0
- data/rust/host/src/platform_signin/jti.rs +196 -0
- data/rust/host/src/platform_signin/jwks.rs +542 -0
- data/rust/host/src/platform_signin/jws.rs +249 -0
- data/rust/host/src/platform_signin/known_answer_tests.rs +237 -0
- data/rust/host/src/platform_signin/mapping_tests.rs +254 -0
- data/rust/host/src/platform_signin/mod.rs +75 -0
- data/rust/host/src/platform_signin/mode_tests.rs +382 -0
- data/rust/host/src/platform_signin/pages.rs +204 -0
- data/rust/host/src/platform_signin/query.rs +233 -0
- data/rust/host/src/platform_signin/session.rs +151 -0
- data/rust/host/src/platform_signin/settings.rs +385 -0
- data/rust/host/src/platform_signin/strict_json.rs +351 -0
- data/rust/host/src/platform_signin/test_support.rs +235 -0
- data/rust/host/src/platform_signin/ttl_tests.rs +94 -0
- data/rust/host/src/platform_signin/verify.rs +604 -0
- data/rust/host/src/platform_signin/wiring_support.rs +319 -0
- data/rust/host/src/web.rs +50 -6
- data/rust/parser/src/parse/value_object.rs +22 -10
- metadata +59 -7
- data/prebuilt/05c358d38d661a3774f813d49c66f5b68939af94432d66257a56f49ee2598994.marshal +0 -0
- data/prebuilt/26c40c61f09b09b11651c64668eda3ceb180f83cf2a3802d3033d2fd99ac841b.marshal +0 -0
- /data/lib/hecks/framework/{oidc.json → compliance.oidc.json} +0 -0
- /data/lib/hecks/grammar/{oidc.json → expression.oidc.json} +0 -0
|
@@ -7,6 +7,7 @@ require_relative "edge/row_reader"
|
|
|
7
7
|
require_relative "edge/checks"
|
|
8
8
|
require_relative "edge/behaviours"
|
|
9
9
|
require_relative "edge/rules"
|
|
10
|
+
require_relative "edge/function_code"
|
|
10
11
|
|
|
11
12
|
module Hecks
|
|
12
13
|
module Projections
|
|
@@ -16,17 +17,33 @@ module Hecks
|
|
|
16
17
|
#
|
|
17
18
|
# The routes are the table's own `Route` rows. What a table cannot know, the names a template
|
|
18
19
|
# gives its policies, origins, target groups and listener, a project declares as `member`
|
|
19
|
-
# rows of
|
|
20
|
-
# `EdgePolicy`, `EdgeOrigin` and `
|
|
21
|
-
#
|
|
20
|
+
# rows of five more value objects of the same chapter: `Edge` (the template and listener),
|
|
21
|
+
# `EdgePolicy`, `EdgeOrigin`, `EdgeRule` and `EdgeFunction` (a viewer-request function a route
|
|
22
|
+
# names with `edge:`). `docs/site-routes.md` lists their fields. A project that declares none
|
|
23
|
+
# of the five has no edge to project.
|
|
22
24
|
class Edge
|
|
23
25
|
Setting = Struct.new(:template, :listener, :secret_header, :secret_value, :alb, keyword_init: true)
|
|
24
26
|
Policy = Struct.new(:cache_class, :origin, :cache, :origin_request, :response_headers, keyword_init: true)
|
|
25
27
|
Upstream = Struct.new(:origin, :id, :target_group, keyword_init: true)
|
|
26
28
|
Rule = Struct.new(:rule, :priority, :origin, keyword_init: true)
|
|
27
29
|
|
|
30
|
+
# A function the generator writes: `kind` says which, and the rest are its settings, the
|
|
31
|
+
# lists as comma-separated text.
|
|
32
|
+
Function = Struct.new(:name, :kind, :host, :aliases, :paths, keyword_init: true) do
|
|
33
|
+
# @return [Array<String>] the alias hosts that are redirected
|
|
34
|
+
def alias_list = split(aliases)
|
|
35
|
+
|
|
36
|
+
# @return [Array<String>] the exact paths the function watches
|
|
37
|
+
def path_list = split(paths)
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def split(text) = text.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
42
|
+
end
|
|
43
|
+
|
|
28
44
|
# The value objects a project declares its edge rows in, by the kind of record each holds.
|
|
29
|
-
OBJECTS = { setting: "Edge", policy: "EdgePolicy", upstream: "EdgeOrigin", rule: "EdgeRule"
|
|
45
|
+
OBJECTS = { setting: "Edge", policy: "EdgePolicy", upstream: "EdgeOrigin", rule: "EdgeRule",
|
|
46
|
+
function: "EdgeFunction" }.freeze
|
|
30
47
|
|
|
31
48
|
# The fields each kind of row may carry, with the Ruby class each takes.
|
|
32
49
|
FIELDS = {
|
|
@@ -34,14 +51,15 @@ module Hecks
|
|
|
34
51
|
policy: { cache_class: String, origin: String, cache: String, origin_request: String,
|
|
35
52
|
response_headers: String },
|
|
36
53
|
upstream: { origin: String, id: String, target_group: String },
|
|
37
|
-
rule: { rule: String, priority: Integer, origin: String }
|
|
54
|
+
rule: { rule: String, priority: Integer, origin: String },
|
|
55
|
+
function: { name: String, kind: String, host: String, aliases: String, paths: String }
|
|
38
56
|
}.freeze
|
|
39
57
|
|
|
40
58
|
# The fields a row of each kind must carry. The template may instead be named to the tool.
|
|
41
59
|
REQUIRED = { setting: [], policy: %i[cache_class cache], upstream: %i[origin id],
|
|
42
|
-
rule: %i[rule priority origin] }.freeze
|
|
60
|
+
rule: %i[rule priority origin], function: %i[name kind host aliases paths] }.freeze
|
|
43
61
|
|
|
44
|
-
STRUCTS = { setting: Setting, policy: Policy, upstream: Upstream, rule: Rule }.freeze
|
|
62
|
+
STRUCTS = { setting: Setting, policy: Policy, upstream: Upstream, rule: Rule, function: Function }.freeze
|
|
45
63
|
|
|
46
64
|
# @return [Setting] the template and the listener
|
|
47
65
|
attr_reader :setting
|
|
@@ -51,6 +69,8 @@ module Hecks
|
|
|
51
69
|
attr_reader :upstreams
|
|
52
70
|
# @return [Array<Rule>] the listener rules
|
|
53
71
|
attr_reader :rules
|
|
72
|
+
# @return [Array<Function>] the edge functions the generator writes
|
|
73
|
+
attr_reader :functions
|
|
54
74
|
# @return [Hash{Symbol => Object}] the default behaviour as `Cdn.behavior_lines` takes it
|
|
55
75
|
attr_reader :default_behaviour
|
|
56
76
|
# @return [Array<Hash{Symbol => Object}>] the other behaviours, in matching order
|
|
@@ -65,11 +85,12 @@ module Hecks
|
|
|
65
85
|
# @param vocabulary [Hash{Symbol => Array<String>}] the Site chapter's closed sets
|
|
66
86
|
# @param template [String, nil] the template the caller names, which stands in for the
|
|
67
87
|
# `Edge` row's own `template:`
|
|
68
|
-
# @return [Edge, nil] the checked edge, or nil when the chapter declares none
|
|
88
|
+
# @return [Edge, nil] the checked edge, or nil when the chapter declares none and no route
|
|
89
|
+
# names an edge function
|
|
69
90
|
# @raise [Table::Invalid] when a row is refused or the edge contradicts the routes
|
|
70
91
|
def self.read(chapter, table:, vocabulary:, template: nil)
|
|
71
92
|
declared = OBJECTS.transform_values { |name| Table.rows_of(chapter, name) }
|
|
72
|
-
return nil if declared.values.all?(&:empty?)
|
|
93
|
+
return nil if declared.values.all?(&:empty?) && table.rows.none?(&:edge)
|
|
73
94
|
|
|
74
95
|
problems = []
|
|
75
96
|
records = declared.to_h { |kind, members| [kind, build(kind, members, problems)] }
|
|
@@ -104,6 +125,7 @@ module Hecks
|
|
|
104
125
|
@policies = records.fetch(:policy)
|
|
105
126
|
@upstreams = records.fetch(:upstream)
|
|
106
127
|
@rules = records.fetch(:rule)
|
|
128
|
+
@functions = records.fetch(:function)
|
|
107
129
|
end
|
|
108
130
|
private :assign
|
|
109
131
|
|
|
@@ -119,6 +141,10 @@ module Hecks
|
|
|
119
141
|
# unless the `Edge` row says `alb: false`
|
|
120
142
|
def alb? = setting.nil? || setting.alb != false
|
|
121
143
|
|
|
144
|
+
# @param name [String] a logical id
|
|
145
|
+
# @return [Function, nil] the function row of that name
|
|
146
|
+
def function(name) = functions.find { |candidate| candidate.name == name }
|
|
147
|
+
|
|
122
148
|
# @param origin [String] an origin of the Origin set
|
|
123
149
|
# @return [Upstream, nil] its CloudFront origin and target group
|
|
124
150
|
def upstream(origin) = upstreams.find { |candidate| candidate.origin == origin }
|
|
@@ -7,12 +7,14 @@ require_relative "edge"
|
|
|
7
7
|
module Hecks
|
|
8
8
|
module Projections
|
|
9
9
|
module Site
|
|
10
|
-
# The edge of a site's route table as the
|
|
11
|
-
#
|
|
10
|
+
# The edge of a site's route table as the blocks of a CloudFormation template that depend on
|
|
11
|
+
# it: the distribution's cache behaviours, the load balancer's listener rules and the
|
|
12
|
+
# functions the behaviours attach.
|
|
12
13
|
#
|
|
13
|
-
# The blocks are returned by name, `"behaviors"` and `"
|
|
14
|
-
# left margin; a project whose `Edge` row says `alb: false` has no load
|
|
15
|
-
#
|
|
14
|
+
# The blocks are returned by name, `"behaviors"`, `"listener_rules"` and `"edge_functions"`,
|
|
15
|
+
# as text from the left margin; a project whose `Edge` row says `alb: false` has no load
|
|
16
|
+
# balancer and gets no `"listener_rules"`, and one with no `EdgeFunction` row gets no
|
|
17
|
+
# `"edge_functions"`. `Regions` puts each between the markers of the same name in the
|
|
16
18
|
# project's template, at the markers' indentation. The text is a pure function of the table
|
|
17
19
|
# and the edge rows.
|
|
18
20
|
module SiteCdn
|
|
@@ -34,9 +36,21 @@ module Hecks
|
|
|
34
36
|
edge = options[:edge] || Edge.read(bluebook, table: table, vocabulary: Table.vocabulary(registry))
|
|
35
37
|
return {} unless edge
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
regions(edge)
|
|
40
|
+
end
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
# @param edge [Edge] a checked edge
|
|
43
|
+
# @return [Hash{String => String}] the regions the edge needs
|
|
44
|
+
def regions(edge)
|
|
45
|
+
{ "behaviors" => behaviors(edge) }
|
|
46
|
+
.merge(edge.alb? ? { "listener_rules" => listener_rules(edge) } : {})
|
|
47
|
+
.merge(edge.functions.any? ? { "edge_functions" => edge_functions(edge) } : {})
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param edge [Edge] a checked edge
|
|
51
|
+
# @return [String] one function resource per `EdgeFunction` row, ending in a newline
|
|
52
|
+
def edge_functions(edge)
|
|
53
|
+
edge.functions.map { |function| Edge::FunctionCode.resource(function) }.join("\n")
|
|
40
54
|
end
|
|
41
55
|
|
|
42
56
|
# @param edge [Edge] a checked edge
|
|
@@ -32,6 +32,11 @@ module Hecks
|
|
|
32
32
|
permissions:
|
|
33
33
|
contents: read
|
|
34
34
|
|
|
35
|
+
# A newer push to the same PR supersedes the run in flight; pushes to main each keep their own run.
|
|
36
|
+
concurrency:
|
|
37
|
+
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
|
|
38
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
39
|
+
|
|
35
40
|
jobs:
|
|
36
41
|
routes-current-and-parity:
|
|
37
42
|
runs-on: ubuntu-24.04
|
|
@@ -11,7 +11,7 @@ module Hecks
|
|
|
11
11
|
:indexable, :switch, :off, :preview, :label, :seo, :redirect_to, :aliases,
|
|
12
12
|
:nav_group, :nav_order, :mobile_order, :footer_column, :footer_order,
|
|
13
13
|
:admin_key, :admin_order, :compress, :alb_rule, :cdn,
|
|
14
|
-
:seo_title, :edge_verbs, :mobile_heading, :explicit_cache, keyword_init: true)
|
|
14
|
+
:seo_title, :edge_verbs, :mobile_heading, :explicit_cache, :edge, keyword_init: true)
|
|
15
15
|
|
|
16
16
|
# Turns a declared member into a `Row`: checks its fields and their types, fills the
|
|
17
17
|
# defaults, and checks each closed-set value against the Site chapter's vocabulary.
|
|
@@ -23,7 +23,10 @@ module Hecks
|
|
|
23
23
|
aliases: String, nav_group: String, nav_order: Integer, mobile_order: Integer,
|
|
24
24
|
footer_column: String, footer_order: Integer, admin_key: String,
|
|
25
25
|
admin_order: Integer, compress: :bool, alb_rule: String, cdn: :bool,
|
|
26
|
-
seo_title: String, edge_methods: String, mobile_heading: String }.freeze
|
|
26
|
+
seo_title: String, edge_methods: String, mobile_heading: String, edge: String }.freeze
|
|
27
|
+
|
|
28
|
+
# What an `edge:` names: the logical id of an `EdgeFunction` row.
|
|
29
|
+
EDGE_NAME = /\A[A-Za-z][A-Za-z0-9]*\z/
|
|
27
30
|
|
|
28
31
|
# The value objects of the Site chapter's `Route` aggregate whose members are the closed
|
|
29
32
|
# sets, by the row field each constrains.
|
|
@@ -103,6 +106,13 @@ module Hecks
|
|
|
103
106
|
problem(label, "has #{field} #{row[field].inspect}; #{field} is one of #{@vocabulary.fetch(field).join(", ")}")
|
|
104
107
|
end
|
|
105
108
|
check_verbs(row, label)
|
|
109
|
+
check_edge(row, label)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def check_edge(row, label)
|
|
113
|
+
return unless row.edge.is_a?(String) && !EDGE_NAME.match?(row.edge)
|
|
114
|
+
|
|
115
|
+
problem(label, "has edge #{row.edge.inspect}; an edge is the logical id of an EdgeFunction row")
|
|
106
116
|
end
|
|
107
117
|
|
|
108
118
|
def check_verbs(row, label)
|
|
@@ -60,7 +60,8 @@ module Hecks
|
|
|
60
60
|
|
|
61
61
|
# @param registry [Runtime::Registry] a registry holding the Site chapter
|
|
62
62
|
# @return [Hash{Symbol => Array<String>}] each constrained field to its allowed values, and
|
|
63
|
-
# `:http_method` to the verbs a route may answer
|
|
63
|
+
# `:http_method` to the verbs a route may answer and `:edge_function_kind` to the kinds of
|
|
64
|
+
# `EdgeFunction` row
|
|
64
65
|
# @raise [Invalid] when the registry does not hold the Site chapter
|
|
65
66
|
def self.vocabulary(registry)
|
|
66
67
|
site = registry.bluebook("Site") or
|
|
@@ -69,7 +70,8 @@ module Hecks
|
|
|
69
70
|
members = lambda do |name|
|
|
70
71
|
route.value_objects.find { |object| object.hecks_name == name }.members.map { |member| member.fetch(:value) }
|
|
71
72
|
end
|
|
72
|
-
RowBuilder::VOCABULARY.transform_values(&members)
|
|
73
|
+
closed = RowBuilder::VOCABULARY.transform_values(&members)
|
|
74
|
+
closed.merge(http_method: members.call("HttpMethod"), edge_function_kind: members.call("EdgeFunctionKind"))
|
|
73
75
|
end
|
|
74
76
|
|
|
75
77
|
# @param chapter [Bluebook::Chapter] a chapter to search
|
|
@@ -13,6 +13,26 @@ module Hecks
|
|
|
13
13
|
hidden.empty? ? specs : specs.reject { |_, spec| hidden.include?(spec[:group]) }
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# The specs the options' audience is shown: those not hidden, and only those listed, if any
|
|
17
|
+
# are.
|
|
18
|
+
def audience_specs(specs, options)
|
|
19
|
+
only_listed(without_hidden(specs, options[:hide]), options[:only])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The specs a newcomer is shown: only those named, or all of them when none are.
|
|
23
|
+
def only_listed(specs, only)
|
|
24
|
+
listed = Array(only)
|
|
25
|
+
listed.empty? ? specs : specs.slice(*listed)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The line saying the help lists only the first steps and how to list the rest, or no line
|
|
29
|
+
# when it lists everything.
|
|
30
|
+
def newcomer_hint(program, only)
|
|
31
|
+
return [] if Array(only).empty?
|
|
32
|
+
|
|
33
|
+
[" #{program} --all also list every command (no bluebook is in this folder yet)"]
|
|
34
|
+
end
|
|
35
|
+
|
|
16
36
|
# The attached chapters the help points at, one line each, under the call that opens them.
|
|
17
37
|
#
|
|
18
38
|
# @param chapters [Array<Array(String, String)>, nil] each chapter's word and its summary
|
|
@@ -34,8 +34,7 @@ module Hecks
|
|
|
34
34
|
|
|
35
35
|
# The whole help: the two tables, the chapters, then the hints.
|
|
36
36
|
def full_usage(bluebook, commands, questions, options, program)
|
|
37
|
-
shown = [CliAudience.
|
|
38
|
-
CliAudience.without_hidden(questions, options[:hide])]
|
|
37
|
+
shown = [commands, questions].map { |specs| CliAudience.audience_specs(specs, options) }
|
|
39
38
|
[usage_header(bluebook, program),
|
|
40
39
|
tables(*shown, all: options[:all], notes: aggregate_notes(bluebook)),
|
|
41
40
|
CliAudience.chapter_lines(program, options[:chapters]),
|
|
@@ -57,6 +56,11 @@ module Hecks
|
|
|
57
56
|
# The pointers after the tables: `--help`, `--all`, `--maintainer` and an example call.
|
|
58
57
|
def usage_footer(program, shown, options, hidden_count)
|
|
59
58
|
out = ["", " #{program} <command> --help what one command wants, and every way it refuses"]
|
|
59
|
+
if Array(options[:only]).any?
|
|
60
|
+
out.concat(CliAudience.newcomer_hint(program, options[:only]))
|
|
61
|
+
return out << " start with #{program} init! — it writes the stub files of a new domain"
|
|
62
|
+
end
|
|
63
|
+
|
|
60
64
|
out.concat(all_hint(program, *shown)) unless options[:all]
|
|
61
65
|
out.concat(CliAudience.maintainer_hint(program, hidden_count))
|
|
62
66
|
out << " a command is called with its aggregate — #{example_qualified(shown.first)}"
|
|
@@ -64,8 +68,15 @@ module Hecks
|
|
|
64
68
|
|
|
65
69
|
# The commands table then the queries table.
|
|
66
70
|
def tables(commands, questions, all:, notes:)
|
|
67
|
-
["commands:", *listing(commands, all: all, notes: notes) { |spec| spec[:summary] },
|
|
68
|
-
|
|
71
|
+
["commands:", *listing(commands, all: all, notes: notes) { |spec| spec[:summary] },
|
|
72
|
+
*queries_table(questions, all: all, notes: notes)]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The queries table, or nothing when the audience is shown no query.
|
|
76
|
+
def queries_table(questions, all:, notes:)
|
|
77
|
+
return [] if questions.empty?
|
|
78
|
+
|
|
79
|
+
["", "queries (nothing here changes anything):",
|
|
69
80
|
*listing(questions, all: all, notes: notes) { |spec| first_sentence(spec[:summary]) }]
|
|
70
81
|
end
|
|
71
82
|
|
|
@@ -4,6 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "open3"
|
|
5
5
|
require_relative "../rust_build"
|
|
6
6
|
require_relative "wasm"
|
|
7
|
+
require_relative "prebuilt_binary"
|
|
7
8
|
|
|
8
9
|
module Hecks
|
|
9
10
|
module RustBuild
|
|
@@ -102,13 +103,19 @@ module Hecks
|
|
|
102
103
|
def compile(target)
|
|
103
104
|
host_dir = File.join(RustBuild.rust_dir, "host")
|
|
104
105
|
target_dir = ENV.fetch("CARGO_TARGET_DIR", File.join(host_dir, "target"))
|
|
106
|
+
binary = File.join(target_dir, target, "release", BINARY)
|
|
107
|
+
return binary if PrebuiltBinary.fetch("host", host_dir, target: target, into: binary)
|
|
108
|
+
|
|
109
|
+
cargo_build(target, host_dir, { "CARGO_TARGET_DIR" => target_dir }.merge(linker_for(target)))
|
|
110
|
+
binary
|
|
111
|
+
rescue Failure => e
|
|
112
|
+
raise Failure, "#{e.message}#{cross_hint(target)}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def cargo_build(target, host_dir, env)
|
|
105
116
|
puts "== cargo build --release --target #{target} --bin #{BINARY} (rust/host) =="
|
|
106
|
-
env = { "CARGO_TARGET_DIR" => target_dir }.merge(linker_for(target))
|
|
107
117
|
RustBuild.command!("rustup", "run", TOOLCHAIN, "cargo", "build", "--release", "--target", target,
|
|
108
118
|
"--bin", BINARY, chdir: host_dir, env: env)
|
|
109
|
-
File.join(target_dir, target, "release", BINARY)
|
|
110
|
-
rescue Failure => e
|
|
111
|
-
raise Failure, "#{e.message}#{cross_hint(target)}"
|
|
112
119
|
end
|
|
113
120
|
|
|
114
121
|
def linker_for(target)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "hecks/vocabulary"
|
|
8
|
+
|
|
9
|
+
module Hecks
|
|
10
|
+
module RustBuild
|
|
11
|
+
# A binary hecks has already compiled and published, named by what went into it (ADR 0110).
|
|
12
|
+
#
|
|
13
|
+
# The `PrebuiltBinary` rows of the Vocabulary chapter say which binaries are published: the
|
|
14
|
+
# crate to key, the Cargo binary, the target it is built for. A release tagged `<name>-<key>`
|
|
15
|
+
# holds the binary and its `.sha256`; the key is a hash of the crate's build inputs, so the
|
|
16
|
+
# same bytes are the same release, whichever commit they come from. `fetch` downloads it and
|
|
17
|
+
# checks its hash, and answers nil on any miss (not published yet, no network, a hash that
|
|
18
|
+
# does not match) so a caller compiles as it always did. `HECKS_PREBUILT=off` turns the
|
|
19
|
+
# download off.
|
|
20
|
+
module PrebuiltBinary
|
|
21
|
+
# Where releases are published.
|
|
22
|
+
REPOSITORY = "heckslabs/hecks"
|
|
23
|
+
|
|
24
|
+
# Left out of the key: Cargo's own output, and files that are never part of the binary.
|
|
25
|
+
EXCLUDED = %w[target tests .gitignore].freeze
|
|
26
|
+
|
|
27
|
+
MAX_REDIRECTS = 5
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# @param name [String] a `PrebuiltBinary` row's name
|
|
32
|
+
# @return [Hash{String => String}] the row
|
|
33
|
+
# @raise [KeyError] when no row has the name
|
|
34
|
+
def row(name)
|
|
35
|
+
Hecks::Vocabulary.rows("PrebuiltBinary").find { |found| found["name"] == name } or
|
|
36
|
+
raise KeyError, "no PrebuiltBinary row named #{name.inspect}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param crate_dir [String] the crate's directory
|
|
40
|
+
# @return [String] a hash of every file that goes into the build: each path and its bytes, in
|
|
41
|
+
# path order, so it does not depend on the checkout it is read from
|
|
42
|
+
def key(crate_dir)
|
|
43
|
+
files = Dir.glob("**/*", File::FNM_DOTMATCH, base: crate_dir).sort.select do |path|
|
|
44
|
+
File.file?(File.join(crate_dir, path)) && !EXCLUDED.include?(path.split("/").first)
|
|
45
|
+
end
|
|
46
|
+
digest = Digest::SHA256.new
|
|
47
|
+
files.each { |path| digest << "#{path}\0#{File.binread(File.join(crate_dir, path))}\0" }
|
|
48
|
+
digest.hexdigest[0, 24]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param name [String] a row's name
|
|
52
|
+
# @param key [String] the crate's key
|
|
53
|
+
# @return [String] the release tag
|
|
54
|
+
def tag(name, key) = "#{name}-#{key}"
|
|
55
|
+
|
|
56
|
+
# @param name [String] a row's name
|
|
57
|
+
# @return [String] the file the release holds the binary as
|
|
58
|
+
def asset(name)
|
|
59
|
+
found = row(name)
|
|
60
|
+
"#{found["executable"]}-#{found["target"]}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @param name [String] a row's name
|
|
64
|
+
# @param crate_dir [String] the crate's directory
|
|
65
|
+
# @param target [String] the triple the caller builds for
|
|
66
|
+
# @param into [String] the file to write the binary to
|
|
67
|
+
# @return [String, nil] `into`, or nil when the binary is not published for this target or
|
|
68
|
+
# could not be fetched and verified
|
|
69
|
+
def fetch(name, crate_dir, target:, into:)
|
|
70
|
+
return if ENV["HECKS_PREBUILT"] == "off" || row(name)["target"] != target
|
|
71
|
+
|
|
72
|
+
body = verified(name, crate_dir) or return
|
|
73
|
+
FileUtils.mkdir_p(File.dirname(into))
|
|
74
|
+
File.binwrite(into, body)
|
|
75
|
+
File.chmod(0o755, into)
|
|
76
|
+
puts "== downloaded the prebuilt #{asset(name)} (#{tag(name, key(crate_dir))}); nothing compiled =="
|
|
77
|
+
into
|
|
78
|
+
rescue SystemCallError, SocketError, Timeout::Error, OpenSSL::SSL::SSLError
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @return [String, nil] the published binary, nil unless its bytes hash to the published
|
|
83
|
+
# sha256
|
|
84
|
+
def verified(name, crate_dir)
|
|
85
|
+
base = "https://github.com/#{REPOSITORY}/releases/download/#{tag(name, key(crate_dir))}/#{asset(name)}"
|
|
86
|
+
expected = get("#{base}.sha256")&.split&.first
|
|
87
|
+
body = expected && get(base)
|
|
88
|
+
body if body && Digest::SHA256.hexdigest(body) == expected
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @return [String, nil] the body at the URL, following redirects, nil unless it answers 200
|
|
92
|
+
def get(url, redirects = MAX_REDIRECTS)
|
|
93
|
+
response = Net::HTTP.get_response(URI(url))
|
|
94
|
+
return response.body if response.is_a?(Net::HTTPSuccess)
|
|
95
|
+
return get(response["location"], redirects - 1) if response.is_a?(Net::HTTPRedirection) && redirects.positive?
|
|
96
|
+
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
# The same command projects the CDN: a project that adds Edge, EdgePolicy, EdgeOrigin and EdgeRule rows
|
|
13
13
|
# to its chapter has the CloudFront behaviours and the listener rules written into the marked regions
|
|
14
14
|
# of the template it owns (the `:site_cdn` projection). Those rows are the template's facts, so only the
|
|
15
|
-
# closed sets they draw on, cache class and
|
|
15
|
+
# closed sets they draw on, cache class, origin and the kind of an EdgeFunction row, are named here. A
|
|
16
|
+
# route's `edge:` names an EdgeFunction row, a viewer-request function the projection writes into the
|
|
17
|
+
# template's `edge_functions` region and attaches to that route's behaviour.
|
|
16
18
|
#
|
|
17
19
|
# Each run records that it was requested before the SiteToolchain port is asked, and what came of
|
|
18
20
|
# it after. No command declares a role: the chapter boots standing alone.
|
|
@@ -33,6 +35,7 @@ Hecks.bluebook "Site" do
|
|
|
33
35
|
attribute :source, Source, optional: true
|
|
34
36
|
attribute :http_method, HttpMethod, optional: true
|
|
35
37
|
attribute :preview, Preview, optional: true
|
|
38
|
+
attribute :edge, EdgeName, optional: true
|
|
36
39
|
|
|
37
40
|
# `/blog/:slug.html` names a parameter; a trailing `/*` matches everything beneath a prefix. A
|
|
38
41
|
# row with a `command:` or `query:` source may leave its path out, and the projection derives it
|
|
@@ -78,6 +81,18 @@ Hecks.bluebook "Site" do
|
|
|
78
81
|
attribute :value, String, one_of: ["public", "draft"]
|
|
79
82
|
end
|
|
80
83
|
|
|
84
|
+
# The logical id of an `EdgeFunction` row: the viewer-request function the route's behaviour runs.
|
|
85
|
+
value_object "EdgeName" do
|
|
86
|
+
attribute :value, String, pattern: '\A[A-Za-z][A-Za-z0-9]*\z'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# What an `EdgeFunction` row asks the projection to write: `redirect_host` answers a request for
|
|
90
|
+
# a watched path on an alias host with a redirect to the canonical host and leaves every other
|
|
91
|
+
# request alone.
|
|
92
|
+
value_object "EdgeFunctionKind" do
|
|
93
|
+
attribute :value, String, one_of: ["redirect_host"]
|
|
94
|
+
end
|
|
95
|
+
|
|
81
96
|
# The HTTP verbs a route answers.
|
|
82
97
|
value_object "HttpMethod" do
|
|
83
98
|
attribute :value, String, one_of: ["GET", "POST", "PUT", "PATCH", "DELETE"]
|
|
@@ -5,6 +5,7 @@ require "open3"
|
|
|
5
5
|
require "hecks/vocabulary"
|
|
6
6
|
require_relative "../tools"
|
|
7
7
|
require_relative "ci_gates"
|
|
8
|
+
require_relative "release_only"
|
|
8
9
|
|
|
9
10
|
module Hecks
|
|
10
11
|
module Tools
|
|
@@ -43,6 +44,8 @@ module Hecks
|
|
|
43
44
|
# @param env [Hash{String => String}] the runner's environment
|
|
44
45
|
# @return [Boolean] whether the gated job should run
|
|
45
46
|
def touched?(gate, root:, env:)
|
|
47
|
+
return !inherits?(gate, root: root, env: env) if gate.fetch("mode") == "release_only"
|
|
48
|
+
|
|
46
49
|
changed, reason = change_set(gate, root: root, env: env)
|
|
47
50
|
if reason
|
|
48
51
|
explain(gate, reason)
|
|
@@ -54,6 +57,38 @@ module Hecks
|
|
|
54
57
|
gate.fetch("mode") == "touches" ? matches.any? : matches.any?(false)
|
|
55
58
|
end
|
|
56
59
|
|
|
60
|
+
# Whether the change is only a release bump on a base whose required checks all passed, so
|
|
61
|
+
# the run may inherit the base's certification (`ReleaseOnly`). Anything unconfirmed answers
|
|
62
|
+
# false and the suite runs.
|
|
63
|
+
#
|
|
64
|
+
# @param gate [Hash{String => String}] the `CiGate` row
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
def inherits?(gate, root:, env:)
|
|
67
|
+
base = base_of(gate, root: root, env: env)
|
|
68
|
+
reason = inheritance_refusal(base, root: root, env: env)
|
|
69
|
+
warn "decide_ci_gate: #{reason}: running the suite rather than inheriting certification" if reason
|
|
70
|
+
reason.nil?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [String, nil] why the change cannot inherit, nil when it can
|
|
74
|
+
def inheritance_refusal(base, root:, env:)
|
|
75
|
+
return "no usable base to diff against" if base.nil? || base.empty? || base == NO_COMMIT
|
|
76
|
+
|
|
77
|
+
changes = ReleaseOnly.changes(base, env.fetch("GITHUB_SHA", "HEAD"), root: root)
|
|
78
|
+
return "git diff itself failed" if changes.nil?
|
|
79
|
+
return "the change is not only release files and lines" unless ReleaseOnly.release_only?(changes)
|
|
80
|
+
|
|
81
|
+
base_refusal(base, env)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @return [String, nil] why the base is not certified, nil when it is
|
|
85
|
+
def base_refusal(base, env)
|
|
86
|
+
runs = ReleaseOnly.check_runs(env.fetch("GITHUB_REPOSITORY", ""), base, env: env)
|
|
87
|
+
return "the base's check runs could not be read" if runs.nil?
|
|
88
|
+
|
|
89
|
+
"the base is not fully certified" unless ReleaseOnly.certified?(runs)
|
|
90
|
+
end
|
|
91
|
+
|
|
57
92
|
# @return [Array(Array<String>, nil), Array(nil, String)] the changed paths, or the reason the
|
|
58
93
|
# change cannot be confirmed
|
|
59
94
|
def change_set(gate, root:, env:)
|
data/lib/hecks/tools/ci_gates.rb
CHANGED
|
@@ -46,7 +46,7 @@ module Hecks
|
|
|
46
46
|
|
|
47
47
|
# The values a row's `mode` and `push` may take: the language holds one closed set to a value
|
|
48
48
|
# object, so the rest are held here.
|
|
49
|
-
MODES = %w[touches skips_unless].freeze
|
|
49
|
+
MODES = %w[touches skips_unless release_only].freeze
|
|
50
50
|
PUSHES = %w[skip before_sha].freeze
|
|
51
51
|
|
|
52
52
|
# @return [Array<Hash{String => String}>] the `CiGate` rows
|
|
@@ -111,16 +111,35 @@ module Hecks
|
|
|
111
111
|
" #{name}:",
|
|
112
112
|
" runs-on: ubuntu-latest",
|
|
113
113
|
" timeout-minutes: 10",
|
|
114
|
+
*job_permissions(gate),
|
|
114
115
|
*job_condition(gate)]
|
|
115
116
|
end
|
|
116
117
|
|
|
117
118
|
# @return [Array<String>] the deciding step and the closing marker
|
|
118
119
|
def job_footer(gate, name)
|
|
119
120
|
[" name: #{quoted(gate.fetch("label"))}",
|
|
121
|
+
*job_env(gate),
|
|
120
122
|
" run: bundle exec exe/hecks regeneration_run.decide_ci_gate gate=#{name} --wait",
|
|
121
123
|
" # END GENERATED ci_gate #{name}"]
|
|
122
124
|
end
|
|
123
125
|
|
|
126
|
+
# A `release_only` gate reads the base commit's check runs through the API, so its job may
|
|
127
|
+
# read checks and nothing more.
|
|
128
|
+
#
|
|
129
|
+
# @return [Array<String>] the job's `permissions:` lines, none for any other mode
|
|
130
|
+
def job_permissions(gate)
|
|
131
|
+
return [] unless gate.fetch("mode") == "release_only"
|
|
132
|
+
|
|
133
|
+
[" permissions:", " contents: read", " checks: read"]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @return [Array<String>] the deciding step's `env:` lines, none unless the gate calls the API
|
|
137
|
+
def job_env(gate)
|
|
138
|
+
return [] unless gate.fetch("mode") == "release_only"
|
|
139
|
+
|
|
140
|
+
[" env:", " GH_TOKEN: ${{ github.token }}"]
|
|
141
|
+
end
|
|
142
|
+
|
|
124
143
|
# @param gate [Hash{String => String}] a `CiGate` row
|
|
125
144
|
# @return [Array<String>] the job-level `if:` lines for a gate that skips push runs
|
|
126
145
|
def job_condition(gate)
|
|
@@ -18,6 +18,7 @@ module Hecks
|
|
|
18
18
|
def projection
|
|
19
19
|
rulesets.merge(workflow(Lanes::WORKFLOW, "follows") { |lanes| Lanes.workflow(lanes) })
|
|
20
20
|
.merge(workflow(Lanes::WATCH_WORKFLOW, "alert_after") { |lanes| WatchWorkflow.text(lanes) })
|
|
21
|
+
.merge(prebuilt)
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
# @param files [Hash{String => String}] the projection
|
|
@@ -44,6 +45,13 @@ module Hecks
|
|
|
44
45
|
Lanes.rulesets.to_h { |name, ruleset| [ruleset_path(name), "#{JSON.pretty_generate(ruleset)}\n"] }
|
|
45
46
|
end
|
|
46
47
|
|
|
48
|
+
# The workflow that publishes each `PrebuiltBinary` row.
|
|
49
|
+
def prebuilt
|
|
50
|
+
Hecks::Vocabulary.rows("PrebuiltBinary").to_h do |row|
|
|
51
|
+
[File.join(@root, PrebuiltWorkflow.path(row)), PrebuiltWorkflow.text(row)]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
47
55
|
# The workflow at `path` for the lanes whose `column` is set, or no file if none is.
|
|
48
56
|
def workflow(path, column)
|
|
49
57
|
lanes = Lanes.lanes.reject { |lane| lane[column].to_s.empty? }
|