hecks 3.6.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/bluebook/meta_validator/syntax_boot/disk_cache.rb +28 -3
- data/lib/hecks/bluebook/meta_validator/verdict_cache/prebuilt.rb +44 -0
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +3 -17
- data/lib/hecks/cache_dir.rb +10 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +109 -0
- data/lib/hecks/deploy/oidc.json +10 -0
- data/lib/hecks/doors/usage_cache.rb +19 -9
- data/lib/hecks/fuzzing/coverage_campaign.rb +42 -17
- data/lib/hecks/fuzzing/failure_triage.rb +80 -0
- data/lib/hecks/fuzzing/form_census/chapter_forms.rb +38 -0
- data/lib/hecks/fuzzing/form_census/reference_forms.rb +49 -0
- data/lib/hecks/fuzzing/form_census.rb +28 -52
- data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +22 -7
- data/lib/hecks/fuzzing/isolated_boot/scratch_schema.rb +27 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +6 -2
- data/lib/hecks/fuzzing/mutation/checks.rb +76 -0
- data/lib/hecks/fuzzing/mutation/finders.rb +136 -0
- data/lib/hecks/fuzzing/mutation/judge.rb +94 -0
- data/lib/hecks/fuzzing/mutation/observation.rb +38 -0
- data/lib/hecks/fuzzing/mutation/operators.rb +52 -0
- data/lib/hecks/fuzzing/mutation/report.rb +78 -0
- data/lib/hecks/fuzzing/mutation/run.rb +99 -0
- data/lib/hecks/fuzzing/mutation/selection.rb +32 -0
- data/lib/hecks/fuzzing/mutation/site.rb +21 -0
- data/lib/hecks/fuzzing/mutation/source_map.rb +59 -0
- data/lib/hecks/fuzzing/mutation.rb +12 -0
- data/lib/hecks/fuzzing/properties/catalog.rb +10 -59
- data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +63 -0
- data/lib/hecks/fuzzing/properties/read_model_names.rb +50 -0
- data/lib/hecks/fuzzing/properties/read_models.rb +138 -0
- data/lib/hecks/fuzzing/properties/role_checks.rb +50 -0
- data/lib/hecks/fuzzing/properties.rb +7 -0
- data/lib/hecks/fuzzing/replay/observation.rb +1 -1
- data/lib/hecks/fuzzing/replay/role_check.rb +49 -0
- data/lib/hecks/fuzzing/replay/session.rb +22 -6
- data/lib/hecks/fuzzing/replay.rb +1 -0
- data/lib/hecks/fuzzing/runtime_coverage.rb +70 -0
- data/lib/hecks/fuzzing/shrinker/value_simplification.rb +64 -0
- data/lib/hecks/fuzzing/shrinker.rb +3 -0
- data/lib/hecks/fuzzing.rb +3 -0
- data/lib/hecks/gate/stages.yml +1 -1
- data/lib/hecks/hecks/adapters/deploy_toolchain/answers.rb +2 -1
- data/lib/hecks/hecks/adapters/github_rulesets.rb +7 -5
- data/lib/hecks/hecks/adapters/process_pool.rb +26 -4
- data/lib/hecks/hecks/custodian.behaviors +6 -0
- data/lib/hecks/hecks/custodian.bluebook +37 -0
- data/lib/hecks/hecks/hecks.hecksagon +6 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +3 -0
- data/lib/hecks/projections/deploy/box/roll_scripts.rb +6 -4
- data/lib/hecks/projections/deploy/box/settings/patterns.rb +4 -0
- data/lib/hecks/projections/deploy/box/settings/types.rb +7 -3
- data/lib/hecks/projections/deploy/box/settings.rb +18 -0
- data/lib/hecks/projections/deploy/box/shared_database.rb +69 -0
- data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +6 -7
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/box/templates/provision-database.sh.tmpl +97 -0
- data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +4 -2
- data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +2 -1
- data/lib/hecks/projections/deploy/box.rb +14 -2
- data/lib/hecks/projections/deploy/shared_database_instance/templates/Makefile.tmpl +11 -0
- data/lib/hecks/projections/deploy/shared_database_instance/templates/README.md.tmpl +25 -0
- data/lib/hecks/projections/deploy/shared_database_instance/templates/rds.yaml.tmpl +180 -0
- data/lib/hecks/projections/deploy/shared_database_instance.rb +106 -0
- data/lib/hecks/projections/deploy/vercel/settings.rb +83 -0
- data/lib/hecks/projections/deploy/vercel.rb +134 -0
- data/lib/hecks/projections/site/site_host/templates.rb +12 -4
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/quality_control/adapters/github_checks.rb +9 -2
- data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +1 -0
- data/lib/hecks/quality_control/cli/qa_sweep/seed_loop.rb +10 -2
- data/lib/hecks/rust_build/native_build.rb +29 -1
- data/lib/hecks/rust_build/wasm.rb +16 -3
- data/lib/hecks/tools/deploy_recipe/generation.rb +59 -0
- data/lib/hecks/tools/deploy_recipe/targets.rb +56 -2
- data/lib/hecks/tools/deploy_recipe/worlds.rb +2 -1
- data/lib/hecks/tools/deploy_recipe.rb +20 -10
- data/lib/hecks/tools/fuzz_sweep/options.rb +21 -10
- data/lib/hecks/tools/fuzz_sweep/reporting.rb +25 -3
- data/lib/hecks/tools/fuzz_sweep.rb +26 -9
- data/lib/hecks/tools/lanes/files.rb +2 -4
- data/lib/hecks/tools/lanes/live.rb +3 -4
- data/lib/hecks/tools/lanes/ruleset.rb +23 -1
- data/lib/hecks/tools/lanes.rb +12 -1
- data/lib/hecks/tools/mutation_run.rb +73 -0
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +2 -0
- data/prebuilt/945481eefbdb584649d45f256307053558966a868b9c38dee7ee88f8169610f8.marshal +0 -0
- data/prebuilt/a0818e2de67364dc1ab22868b6cbf869c62da6f8d9b924c778fa5cce2e76ee94.marshal +0 -0
- data/prebuilt/verdicts-be73f0df9eee0abe2f4aadc968321c4b919d19d424a3ccfe1777a71dabf7d9e7.json +1 -0
- data/qa/settings.yml +1 -0
- data/rust/host/Cargo.toml +2 -2
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +28 -5
- data/rust/host/src/auth.rs +11 -1
- data/rust/host/src/boundary_fuzz/api.rs +260 -0
- data/rust/host/src/boundary_fuzz/auth.rs +119 -0
- data/rust/host/src/boundary_fuzz/expr_json.rs +158 -0
- data/rust/host/src/boundary_fuzz/ir.rs +78 -0
- data/rust/host/src/boundary_fuzz/server.rs +227 -0
- data/rust/host/src/boundary_fuzz/web.rs +125 -0
- data/rust/host/src/fuzz_support.rs +165 -0
- data/rust/host/src/ir.rs +4 -0
- data/rust/host/src/main.rs +9 -4
- data/rust/host/src/secrets.rs +44 -0
- data/rust/host/src/server.rs +6 -1
- data/rust/host/src/web.rs +12 -2
- data/rust/parser/fuzz/.gitignore +4 -0
- data/rust/parser/fuzz/Cargo.lock +89 -0
- data/rust/parser/fuzz/Cargo.toml +27 -0
- data/rust/parser/fuzz/README.md +19 -0
- data/rust/parser/fuzz/fuzz_targets/chapter.rs +50 -0
- data/rust/parser/fuzz/fuzz_targets/resolve.rs +35 -0
- data/rust/parser/src/parse/command.rs +14 -2
- data/rust/parser/src/parse/mod.rs +25 -0
- data/rust/parser/src/ruby_value.rs +95 -6
- data/rust/src/kernel/json.rs +34 -3
- metadata +50 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
module Properties
|
|
4
|
+
# Property: the role check at dispatch agrees with the grants the replay itself made.
|
|
5
|
+
#
|
|
6
|
+
# `CommandRules::Authorization` asks the authorization provider whether an identified caller
|
|
7
|
+
# holds a command's role, through the `assignments` verb the provider's chapter declares with
|
|
8
|
+
# `provides "authorization"`. Each `history[:role_checks]` entry carries the grants read back
|
|
9
|
+
# through that same verb before the dispatch, so the answer is recomputed from them.
|
|
10
|
+
module RoleChecks
|
|
11
|
+
# How `Replay.refusal_kind` names the refusal the role gate raises.
|
|
12
|
+
UNAUTHORIZED = "Hecks::Runtime::Unauthorized".freeze
|
|
13
|
+
|
|
14
|
+
# An identified caller holding a live grant of the role is never refused as unauthorized,
|
|
15
|
+
# and one holding none is never accepted.
|
|
16
|
+
#
|
|
17
|
+
# A refusal other than Unauthorized proves nothing about the grant: argument gates run
|
|
18
|
+
# before the role gate, so an ungranted caller can be turned away for another reason first.
|
|
19
|
+
#
|
|
20
|
+
# @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
|
|
21
|
+
# @return [true, String] true, or a message naming each dispatch the grants contradict
|
|
22
|
+
def role_checks_agree_with_grants(history)
|
|
23
|
+
offenders = Array(history[:role_checks]).filter_map { |check| role_check_offender(check) }
|
|
24
|
+
offenders.empty? || offenders.uniq.join("; ")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# A message when `check`'s outcome contradicts its grants; nil when consistent or unclear.
|
|
28
|
+
def role_check_offender(check)
|
|
29
|
+
held = role_held?(check)
|
|
30
|
+
return refused_message(check) if held && check[:outcome] == UNAUTHORIZED
|
|
31
|
+
|
|
32
|
+
accepted_message(check) if !held && check[:outcome].nil?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def role_held?(check)
|
|
36
|
+
check[:grants].any? { |grant| grant[:role] == check[:role] && !grant[:ended] }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def refused_message(check)
|
|
40
|
+
"#{check[:verb]} was refused as unauthorized for #{check[:actor_id]}, who holds a live grant of " \
|
|
41
|
+
"#{check[:role].inspect}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def accepted_message(check)
|
|
45
|
+
"#{check[:verb]} was accepted for #{check[:actor_id]}, who holds no live grant of #{check[:role].inspect}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -13,7 +13,11 @@ require_relative "properties/corrections"
|
|
|
13
13
|
require_relative "properties/outbox"
|
|
14
14
|
require_relative "properties/policy_wiring"
|
|
15
15
|
require_relative "properties/references"
|
|
16
|
+
require_relative "properties/role_checks"
|
|
17
|
+
require_relative "properties/read_model_names"
|
|
18
|
+
require_relative "properties/read_models"
|
|
16
19
|
require_relative "properties/catalog"
|
|
20
|
+
require_relative "properties/guaranteed_by_construction"
|
|
17
21
|
|
|
18
22
|
module Hecks
|
|
19
23
|
module Fuzzing
|
|
@@ -32,6 +36,9 @@ module Hecks
|
|
|
32
36
|
extend Outbox
|
|
33
37
|
extend PolicyWiring
|
|
34
38
|
extend References
|
|
39
|
+
extend RoleChecks
|
|
40
|
+
extend ReadModelNames
|
|
41
|
+
extend ReadModels
|
|
35
42
|
|
|
36
43
|
module_function
|
|
37
44
|
|
|
@@ -10,7 +10,7 @@ module Hecks
|
|
|
10
10
|
{ instances: Replay.snapshot_instances(@runtime), events: events, refusals: @refusals,
|
|
11
11
|
reactions: @runtime.reactions, sagas: @runtime.sagas, saga_instances: saga_instances,
|
|
12
12
|
queries: @queries, dry_runs: @dry_runs, dry_run_traces: @dry_run_traces,
|
|
13
|
-
fan_outs: @fan_outs, guard_checks: @guard_checks,
|
|
13
|
+
fan_outs: @fan_outs, guard_checks: @guard_checks, role_checks: @role_checks,
|
|
14
14
|
mutation_traces: @mutation_traces, outbox_traces: @outbox_traces,
|
|
15
15
|
saga_dispatches: @runtime.saga_dispatches, policy_dispatches: @runtime.policy_dispatches,
|
|
16
16
|
bluebook: @runtime.registry.bluebooks.values.first,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
module Replay
|
|
4
|
+
# Reads, before a role-gated dispatch by an identified caller, the grants that actor holds
|
|
5
|
+
# through the authorization provider's own declared `assignments` verb, so the outcome can be
|
|
6
|
+
# compared with what the dispatch actually did.
|
|
7
|
+
module RoleCheck
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# The caller and live grants for one step; nil for any step with no identified caller, no
|
|
11
|
+
# declared role, or no authorization provider (those compare role strings, not grants).
|
|
12
|
+
#
|
|
13
|
+
# @param runtime [Hecks::Runtime] the booted runtime the step will dispatch against
|
|
14
|
+
# @param step [Hash] a replay step, string-keyed
|
|
15
|
+
# @return [Hash, nil] `verb:`, `role:`, `actor_id:` and `grants:` (each `role:`, `ended:`)
|
|
16
|
+
def build(runtime, step)
|
|
17
|
+
return nil unless step["role"] && step["actor_id"]
|
|
18
|
+
|
|
19
|
+
domain, command = command_of(runtime, step["verb"])
|
|
20
|
+
return nil unless command && governed?(runtime, domain, command)
|
|
21
|
+
|
|
22
|
+
{ verb: step["verb"], role: command.role.to_s, actor_id: step["actor_id"].to_s,
|
|
23
|
+
grants: grants_for(runtime.registry, step["actor_id"]) }
|
|
24
|
+
rescue StandardError
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The `[domain, command]` an aggregate-level verb names; the command is nil when none does.
|
|
29
|
+
def command_of(runtime, verb)
|
|
30
|
+
domain, aggregate_name, command_name = Naming.split_verb(verb)
|
|
31
|
+
[domain, runtime.registry.bluebook(domain)&.aggregate(aggregate_name)&.command(command_name)]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whether the command declares a role and its domain has an authorization provider.
|
|
35
|
+
def governed?(runtime, domain, command)
|
|
36
|
+
!command.role.to_s.empty? && !runtime.registry.authorization_provider_for(domain).nil?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The actor's assignments, read through the verb the provider declares for `assignments`.
|
|
40
|
+
def grants_for(registry, actor_id)
|
|
41
|
+
provider = registry.authorization_providers.first
|
|
42
|
+
verb = provider.provided_verb(Bluebook::Capabilities::AUTHORIZATION, :assignments)
|
|
43
|
+
rows = Runtime::Dispatcher.new(registry).query(verb, actor_id: { value: actor_id.to_s })
|
|
44
|
+
rows.map { |row| { role: row[:role_name][:value], ended: !row[:ends_at].nil? } }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -18,7 +18,7 @@ module Hecks
|
|
|
18
18
|
# `outbox_ids` is a set of delivery_ids, not a size, because `runtime.outbox.rows`
|
|
19
19
|
# concatenates several stores' own arrays, so a plain "grew from N to M" tail slice
|
|
20
20
|
# could miss or misattribute rows once more than one repository has an outbox.
|
|
21
|
-
Marks = Struct.new(:reaction, :saga, :outbox_ids, :fan_out_snapshot, :guard_check, :mutation_trace)
|
|
21
|
+
Marks = Struct.new(:reaction, :saga, :outbox_ids, :fan_out_snapshot, :guard_check, :mutation_trace, :role_check)
|
|
22
22
|
|
|
23
23
|
# @param runtime [Hecks::Runtime] a freshly booted runtime
|
|
24
24
|
def initialize(runtime)
|
|
@@ -28,7 +28,7 @@ module Hecks
|
|
|
28
28
|
@dry_runs = []
|
|
29
29
|
@dry_run_traces = []
|
|
30
30
|
@fan_outs = []
|
|
31
|
-
|
|
31
|
+
start_checks
|
|
32
32
|
@mutation_traces = []
|
|
33
33
|
@outbox_traces = []
|
|
34
34
|
@fan_out_targets = fan_out_targets
|
|
@@ -61,6 +61,12 @@ module Hecks
|
|
|
61
61
|
|
|
62
62
|
private
|
|
63
63
|
|
|
64
|
+
# The two checks recorded before a dispatch and compared with its outcome afterwards.
|
|
65
|
+
def start_checks
|
|
66
|
+
@guard_checks = []
|
|
67
|
+
@role_checks = []
|
|
68
|
+
end
|
|
69
|
+
|
|
64
70
|
def play_step(step)
|
|
65
71
|
step = step.transform_keys(&:to_s)
|
|
66
72
|
args = (step["args"] || {}).transform_keys(&:to_sym)
|
|
@@ -84,7 +90,7 @@ module Hecks
|
|
|
84
90
|
result = Replay.as_step_caller(step) { @runtime.dispatch_flat(step["verb"], args) }
|
|
85
91
|
record_effects(step, result, marks)
|
|
86
92
|
rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
|
|
87
|
-
record_refusal(step, e, marks&.guard_check)
|
|
93
|
+
record_refusal(step, e, marks&.guard_check, marks&.role_check)
|
|
88
94
|
end
|
|
89
95
|
|
|
90
96
|
# Taken before dispatch, so this step's own reactions, sagas and outbox rows can be
|
|
@@ -98,17 +104,23 @@ module Hecks
|
|
|
98
104
|
def take_marks(step, args)
|
|
99
105
|
Marks.new(@runtime.reactions.size, @runtime.sagas.size, @runtime.outbox.rows.map(&:delivery_id),
|
|
100
106
|
fan_out_snapshot, GuardCheck.build(@runtime, step["verb"], args),
|
|
101
|
-
MutationTrace.build(@runtime, step["verb"], args))
|
|
107
|
+
MutationTrace.build(@runtime, step["verb"], args), RoleCheck.build(@runtime, step))
|
|
102
108
|
end
|
|
103
109
|
|
|
104
110
|
def record_effects(step, result, marks)
|
|
105
111
|
@fan_outs.concat(Replay.fan_out_findings(@runtime, marks.fan_out_snapshot, result.events,
|
|
106
112
|
@runtime.reactions[marks.reaction..]))
|
|
107
113
|
record_outbox(step, marks)
|
|
108
|
-
|
|
114
|
+
record_accepted_checks(marks)
|
|
109
115
|
record_mutation(marks.mutation_trace) if marks.mutation_trace
|
|
110
116
|
end
|
|
111
117
|
|
|
118
|
+
# An accepted dispatch: the guard let it through and the role gate did not refuse it.
|
|
119
|
+
def record_accepted_checks(marks)
|
|
120
|
+
@guard_checks << marks.guard_check.merge(actual_refused: false, actual_kind: nil) if marks.guard_check
|
|
121
|
+
@role_checks << marks.role_check.merge(outcome: nil) if marks.role_check
|
|
122
|
+
end
|
|
123
|
+
|
|
112
124
|
# Every outbox row this step's own dispatch newly wrote, across every
|
|
113
125
|
# bound repository including any a reaction cascade touched, paired with
|
|
114
126
|
# the reaction/saga rows that same dispatch produced. Skipped when empty.
|
|
@@ -139,8 +151,12 @@ module Hecks
|
|
|
139
151
|
# Only a refusal raised by the guard itself counts for the guard check. A refusal from a
|
|
140
152
|
# stage before or after enforce_givens can share TypeMismatch's class, so
|
|
141
153
|
# anything outside the two guard classes is left out — inconclusive, not a pass.
|
|
142
|
-
|
|
154
|
+
#
|
|
155
|
+
# A role check records the refusal's class whatever it is, since the property decides which
|
|
156
|
+
# outcomes are conclusive: a refusal before the role gate says nothing about the grant.
|
|
157
|
+
def record_refusal(step, error, guard_check, role_check)
|
|
143
158
|
@refusals << { verb: step["verb"], error: error.message, kind: Replay.refusal_kind(error) }
|
|
159
|
+
@role_checks << role_check.merge(outcome: Replay.refusal_kind(error)) if role_check
|
|
144
160
|
return unless guard_check && GUARD_REFUSAL_CLASSES.include?(error.class)
|
|
145
161
|
|
|
146
162
|
@guard_checks << guard_check.merge(actual_refused: true, actual_kind: error.class.name)
|
data/lib/hecks/fuzzing/replay.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "self_consistency"
|
|
|
5
5
|
require_relative "replay/filters"
|
|
6
6
|
require_relative "replay/fan_out"
|
|
7
7
|
require_relative "replay/guard_check"
|
|
8
|
+
require_relative "replay/role_check"
|
|
8
9
|
require_relative "replay/mutation_trace"
|
|
9
10
|
require_relative "replay/session"
|
|
10
11
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
# Measures which lines (and, when stdlib Coverage is running, branches) of the Ruby runtime a
|
|
4
|
+
# block reaches, as a set of stable "path:line" and "path:line:branch" strings.
|
|
5
|
+
#
|
|
6
|
+
# Lines come from a TracePoint, so code that was loaded before the measurement still counts.
|
|
7
|
+
# Branches need stdlib Coverage started before the code was loaded; when it is not running, the
|
|
8
|
+
# measurement is lines only.
|
|
9
|
+
#
|
|
10
|
+
# Pure observation: nothing here draws a random number, so a seed's keys are as reproducible
|
|
11
|
+
# as the seed.
|
|
12
|
+
module RuntimeCoverage
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# @param roots [Array<String>] directories whose files count; anything else is ignored
|
|
16
|
+
# @yield the work to measure
|
|
17
|
+
# @return [Array(Object, Set<String>)] the block's value and the keys it reached
|
|
18
|
+
def measure(roots: default_roots, &block)
|
|
19
|
+
raise ArgumentError, "RuntimeCoverage.measure needs a block" unless block
|
|
20
|
+
|
|
21
|
+
prefixes = roots.map { |root| File.join(File.expand_path(root), "") }
|
|
22
|
+
reached = Set.new
|
|
23
|
+
before = branch_snapshot
|
|
24
|
+
value = trace_lines(prefixes, reached, &block)
|
|
25
|
+
reached.merge(branch_keys(before, branch_snapshot, prefixes))
|
|
26
|
+
[value, reached]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The runtime this gem ships, the default measurement scope.
|
|
30
|
+
def default_roots = [File.expand_path("..", __dir__)]
|
|
31
|
+
|
|
32
|
+
def trace_lines(prefixes, reached)
|
|
33
|
+
value = nil
|
|
34
|
+
tracer = TracePoint.new(:line) do |point|
|
|
35
|
+
path = point.path
|
|
36
|
+
reached << "#{path}:#{point.lineno}" if in_roots?(path, prefixes)
|
|
37
|
+
end
|
|
38
|
+
tracer.enable { value = yield }
|
|
39
|
+
value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [Hash, nil] Coverage's branch table so far (files that report branches), or nil
|
|
43
|
+
# when Coverage is not running
|
|
44
|
+
def branch_snapshot
|
|
45
|
+
return unless defined?(::Coverage) && ::Coverage.respond_to?(:running?) && ::Coverage.running?
|
|
46
|
+
|
|
47
|
+
::Coverage.peek_result.filter_map { |path, entry| [path, entry[:branches]] if entry.is_a?(Hash) && entry[:branches] }.to_h
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Branch targets whose hit count grew between two snapshots.
|
|
51
|
+
def branch_keys(before, after, prefixes)
|
|
52
|
+
return [] unless before && after
|
|
53
|
+
|
|
54
|
+
after.select { |path, _branches| in_roots?(path, prefixes) }.flat_map do |path, branches|
|
|
55
|
+
grown(before[path] || {}, branches).map { |key| "#{path}:#{key}" }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def in_roots?(path, prefixes) = prefixes.any? { |prefix| path.start_with?(prefix) }
|
|
60
|
+
|
|
61
|
+
def grown(old, new)
|
|
62
|
+
new.flat_map do |origin, targets|
|
|
63
|
+
targets.filter_map do |target, count|
|
|
64
|
+
"#{origin.drop(1).join(":")}:#{target.drop(1).join(":")}" if count > (old.dig(origin, target) || 0)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Fuzzing
|
|
3
|
+
module Shrinker
|
|
4
|
+
# Pass 3 of the shrinker: offers each surviving argument value a simpler stand-in and keeps
|
|
5
|
+
# the ones the finding survives. Split out of `Shrinker` so each file holds one idea.
|
|
6
|
+
module ValueSimplification
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Offers each surviving argument a simpler value, keeping one the finding survives. Nested
|
|
10
|
+
# hashes are simplified key by key; a candidate that stops reproducing is rejected.
|
|
11
|
+
def call(steps, meter, &)
|
|
12
|
+
steps.each_index do |position|
|
|
13
|
+
original = Shrinker.args_of(steps[position])
|
|
14
|
+
next unless original.is_a?(Hash)
|
|
15
|
+
|
|
16
|
+
original.each_key do |key|
|
|
17
|
+
steps = simplify_path(steps, position, [key], meter, &)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
steps
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Simplifies the value at `path` inside step `position`'s arguments, recursing into hashes.
|
|
24
|
+
def simplify_path(steps, position, path, meter, &)
|
|
25
|
+
value = Shrinker.args_of(steps[position]).dig(*path)
|
|
26
|
+
if value.is_a?(Hash)
|
|
27
|
+
return value.each_key.reduce(steps) { |acc, key| simplify_path(acc, position, path + [key], meter, &) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
simpler_values(value).each do |replacement|
|
|
31
|
+
return steps if meter.exhausted?
|
|
32
|
+
|
|
33
|
+
candidate = with_value(steps, position, path, replacement)
|
|
34
|
+
return candidate if meter.try { yield(candidate) }
|
|
35
|
+
end
|
|
36
|
+
steps
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Simpler stand-ins for `value`, simplest first; none for what is already minimal.
|
|
40
|
+
def simpler_values(value)
|
|
41
|
+
candidates =
|
|
42
|
+
case value
|
|
43
|
+
when String then ["", value[0].to_s]
|
|
44
|
+
when Integer then [0, 1]
|
|
45
|
+
when Float then [0.0, 1.0]
|
|
46
|
+
when Array then [[], value.first(1)]
|
|
47
|
+
else []
|
|
48
|
+
end
|
|
49
|
+
candidates.uniq.reject { |candidate| candidate == value }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def with_value(steps, position, path, replacement)
|
|
53
|
+
step = steps[position]
|
|
54
|
+
args = Marshal.load(Marshal.dump(Shrinker.args_of(step)))
|
|
55
|
+
parent = path.length > 1 ? args.dig(*path[0...-1]) : args
|
|
56
|
+
parent[path.last] = replacement
|
|
57
|
+
candidate = steps.map(&:dup)
|
|
58
|
+
candidate[position] = step.merge("args" => args)
|
|
59
|
+
candidate
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -5,6 +5,8 @@ module Hecks
|
|
|
5
5
|
#
|
|
6
6
|
# Pass 1 removes step chunks (delta debugging) until the list is 1-minimal.
|
|
7
7
|
# Pass 2 drops argument keys one at a time inside each surviving step.
|
|
8
|
+
# Pass 3 replaces each surviving argument value with a simpler one (empty, zero, first
|
|
9
|
+
# element) wherever the finding still reproduces, so a repro reads as small as it is.
|
|
8
10
|
# `budget:` caps candidate checks; when spent, the best candidate so far is returned.
|
|
9
11
|
module Shrinker
|
|
10
12
|
Result = Struct.new(:steps, :attempts, :exhausted, keyword_init: true)
|
|
@@ -26,6 +28,7 @@ module Hecks
|
|
|
26
28
|
meter = Meter.new(budget)
|
|
27
29
|
current = drop_steps(steps.dup, meter, &reproduces)
|
|
28
30
|
current = drop_arguments(current, meter, &reproduces)
|
|
31
|
+
current = ValueSimplification.call(current, meter, &reproduces)
|
|
29
32
|
Result.new(steps: current, attempts: meter.used, exhausted: meter.exhausted?)
|
|
30
33
|
end
|
|
31
34
|
|
data/lib/hecks/fuzzing.rb
CHANGED
|
@@ -11,6 +11,7 @@ require_relative "fuzzing/sequence_generator"
|
|
|
11
11
|
require_relative "fuzzing/nondeterministic"
|
|
12
12
|
require_relative "fuzzing/replay"
|
|
13
13
|
require_relative "fuzzing/shrinker"
|
|
14
|
+
require_relative "fuzzing/shrinker/value_simplification"
|
|
14
15
|
require_relative "fuzzing/properties"
|
|
15
16
|
require_relative "fuzzing/qa_settings"
|
|
16
17
|
require_relative "fuzzing/rotation_priority"
|
|
@@ -20,4 +21,6 @@ require_relative "fuzzing/form_census"
|
|
|
20
21
|
require_relative "fuzzing/target_capabilities"
|
|
21
22
|
require_relative "fuzzing/rust_gap_manifest"
|
|
22
23
|
require_relative "fuzzing/structural_skips"
|
|
24
|
+
require_relative "fuzzing/runtime_coverage"
|
|
23
25
|
require_relative "fuzzing/coverage_campaign"
|
|
26
|
+
require_relative "fuzzing/failure_triage"
|
data/lib/hecks/gate/stages.yml
CHANGED
|
@@ -114,7 +114,7 @@ ci:
|
|
|
114
114
|
run: [bundle, exec, exe/hecks, fuzz_run.fuzz, adapter=sqlite, seeds=6, --wait]
|
|
115
115
|
blocked: the SQLite adapter broke a property the Memory adapter held; seeds are 1..N, so the run repeats.
|
|
116
116
|
- id: fuzz_postgres
|
|
117
|
-
title: fuzz on real Postgres,
|
|
117
|
+
title: fuzz on real Postgres, up to four workers (each in its own scratch schema)
|
|
118
118
|
run: [bundle, exec, exe/hecks, fuzz_run.fuzz, adapter=postgres, seeds=3, steps=20, --wait]
|
|
119
119
|
blocked: the Postgres adapter broke a property the Memory adapter held; it needs PGHOST, PGUSER and PGPASSWORD.
|
|
120
120
|
- id: vocabulary_drift
|
|
@@ -89,9 +89,10 @@ module Hecks
|
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
# What the script would overwrite, read from the script itself (its schemas and databases).
|
|
92
|
+
# Its schemas are the default of the `SCHEMAS=${SCHEMAS:-"..."}` line (overridable).
|
|
92
93
|
def copy_plan(script, held)
|
|
93
94
|
text = File.read(script)
|
|
94
|
-
schemas = text[/^SCHEMAS="([^"]*)"/, 1].to_s.split.join(", ")
|
|
95
|
+
schemas = text[/^SCHEMAS=(?:\$\{SCHEMAS:-)?"([^"]*)"/, 1].to_s.split.join(", ")
|
|
95
96
|
source_db, target_db = copy_databases(text, held)
|
|
96
97
|
drop = plain(held[:force]) == true ? "; force=true drops each target schema first" : ""
|
|
97
98
|
"copy schemas #{schemas} from database #{source_db} on #{plain(held[:source])} into database " \
|
|
@@ -89,17 +89,19 @@ module Hecks
|
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
def compare_checks(projected, live)
|
|
92
|
-
mine =
|
|
93
|
-
theirs =
|
|
92
|
+
mine = required(projected)
|
|
93
|
+
theirs = required(live)
|
|
94
94
|
return [] if mine == theirs
|
|
95
95
|
|
|
96
96
|
["#{projected["name"]}: required checks are #{theirs.inspect} on GitHub, #{mine.inspect} in the model"]
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
-
#
|
|
100
|
-
|
|
99
|
+
# Each check a ruleset's `required_status_checks` rule demands, as its name and the app id it
|
|
100
|
+
# must come from (nil when any app may report it), sorted.
|
|
101
|
+
def required(ruleset)
|
|
101
102
|
rule = Array(ruleset["rules"]).find { |candidate| candidate["type"] == "required_status_checks" }
|
|
102
|
-
Array(rule&.dig("parameters", "required_status_checks"))
|
|
103
|
+
Array(rule&.dig("parameters", "required_status_checks"))
|
|
104
|
+
.map { |check| [check["context"], check["integration_id"]] }.sort_by(&:to_s)
|
|
103
105
|
end
|
|
104
106
|
|
|
105
107
|
def actors(ruleset)
|
|
@@ -32,6 +32,12 @@ module Hecks
|
|
|
32
32
|
# What the sweep child runs: the `fuzz` tool, which takes the flags that follow.
|
|
33
33
|
ENTRY = 'require "hecks/tools"; Hecks::Tools.script("fuzz", ARGV)'
|
|
34
34
|
|
|
35
|
+
# What the mutation child runs: the `mutate` tool, which takes the flags that follow.
|
|
36
|
+
MUTATE_ENTRY = 'require "hecks/tools"; Hecks::Tools.script("mutate", ARGV)'
|
|
37
|
+
|
|
38
|
+
# The flags of a mutation run, and the fields of the record that fill them.
|
|
39
|
+
MUTATE_FLAGS = { "--seeds" => :seeds, "--steps" => :steps, "--budget" => :budget }.freeze
|
|
40
|
+
|
|
35
41
|
# Signals passed on to a running child's process group.
|
|
36
42
|
FORWARDED = %w[INT TERM HUP QUIT].freeze
|
|
37
43
|
|
|
@@ -63,6 +69,18 @@ module Hecks
|
|
|
63
69
|
answer(run([RbConfig.ruby, "-I", LIB, "-e", ENTRY, "--", *sweep_words(held)]))
|
|
64
70
|
end
|
|
65
71
|
|
|
72
|
+
# Mutation-tests one domain: changes its rules in small ways and reports each change its
|
|
73
|
+
# checks let through.
|
|
74
|
+
#
|
|
75
|
+
# @param held [Hash] the `Mutating` record: `domain`, `seeds`, `steps`, `budget`
|
|
76
|
+
# @return [Hash{Symbol => Hash}] `report:` what the run printed
|
|
77
|
+
# @raise [ConsoleCapture::Failure] when the run could not start
|
|
78
|
+
def probe(**held)
|
|
79
|
+
words = [plain(held[:domain]), *flag_words(held, MUTATE_FLAGS)]
|
|
80
|
+
|
|
81
|
+
answer(run([RbConfig.ruby, "-I", LIB, "-e", MUTATE_ENTRY, "--", *words]))
|
|
82
|
+
end
|
|
83
|
+
|
|
66
84
|
# Starts a child, forwards interrupts to it, and waits for it to end.
|
|
67
85
|
#
|
|
68
86
|
# @param command [Array<String>] the program, then its arguments
|
|
@@ -94,12 +112,16 @@ module Hecks
|
|
|
94
112
|
|
|
95
113
|
# The words after `--`: the domain when named, then each flag the record names.
|
|
96
114
|
def sweep_words(held)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
115
|
+
[*plain(held[:domain]), *flag_words(held, SWEEP_FLAGS), *persist_words(held)]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Each flag whose field the record holds, followed by that field's value.
|
|
119
|
+
def flag_words(held, flags)
|
|
120
|
+
flags.flat_map { |flag, key| plain(held[key]).nil? ? [] : [flag, plain(held[key]).to_s] }
|
|
101
121
|
end
|
|
102
122
|
|
|
123
|
+
def persist_words(held) = plain(held[:persist_regressions]) == true ? ["--persist-regressions"] : []
|
|
124
|
+
|
|
103
125
|
# The options a child is spawned with: its own process group, its output in `output`.
|
|
104
126
|
def spawn_options(output, chdir)
|
|
105
127
|
options = { pgroup: true, out: output, err: output }
|
|
@@ -147,6 +147,12 @@ Hecks.behaviors "Custodian" do
|
|
|
147
147
|
expect refused: "a count is positive"
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
+
test "Mutate refuses a budget that is not positive" do
|
|
151
|
+
tests "Mutate", on: "FuzzRun"
|
|
152
|
+
input run: { value: "mutants" }, domain: { value: "examples/pizzas" }, budget: { value: 0 }
|
|
153
|
+
expect refused: "a count is positive"
|
|
154
|
+
end
|
|
155
|
+
|
|
150
156
|
test "Fuzz refuses a persistence it does not know" do
|
|
151
157
|
tests "Fuzz", on: "FuzzRun"
|
|
152
158
|
input run: { value: "sweep" }, adapter: { value: "cassandra" }
|
|
@@ -1963,7 +1963,9 @@ policy "RecordTheEra" do
|
|
|
1963
1963
|
attribute :seeds, Count, optional: true
|
|
1964
1964
|
attribute :steps, Count, optional: true
|
|
1965
1965
|
attribute :workers, Count, optional: true
|
|
1966
|
+
attribute :budget, Count, optional: true
|
|
1966
1967
|
attribute :adapter, Persistence, optional: true
|
|
1968
|
+
attribute :persist_regressions, RegressionSwitch, optional: true
|
|
1967
1969
|
attribute :domains, NameList, optional: true
|
|
1968
1970
|
attribute :targets, NameList, optional: true
|
|
1969
1971
|
attribute :iterations, Count, optional: true
|
|
@@ -2006,6 +2008,11 @@ policy "RecordTheEra" do
|
|
|
2006
2008
|
attribute :value, String, pattern: '^(memory|sqlite|postgres)$'
|
|
2007
2009
|
end
|
|
2008
2010
|
|
|
2011
|
+
# Whether each distinct finding's minimized repro is also kept under `spec/corpus/regressions/`.
|
|
2012
|
+
value_object "RegressionSwitch" do
|
|
2013
|
+
attribute :value, Boolean
|
|
2014
|
+
end
|
|
2015
|
+
|
|
2009
2016
|
# Names, comma separated: benchmark domains, or the targets to measure.
|
|
2010
2017
|
value_object "NameList" do
|
|
2011
2018
|
attribute :value, String, pattern: '^[^ \t\n\r,]+(,[^ \t\n\r,]+)*$'
|
|
@@ -2047,6 +2054,7 @@ policy "RecordTheEra" do
|
|
|
2047
2054
|
attribute :steps, Count, optional: true
|
|
2048
2055
|
attribute :workers, Count, optional: true
|
|
2049
2056
|
attribute :adapter, Persistence, optional: true
|
|
2057
|
+
attribute :persist_regressions, RegressionSwitch, optional: true
|
|
2050
2058
|
|
|
2051
2059
|
sets :domain
|
|
2052
2060
|
sets :run
|
|
@@ -2054,10 +2062,34 @@ policy "RecordTheEra" do
|
|
|
2054
2062
|
sets :steps
|
|
2055
2063
|
sets :workers
|
|
2056
2064
|
sets :adapter
|
|
2065
|
+
sets :persist_regressions
|
|
2057
2066
|
|
|
2058
2067
|
emits FuzzRequested
|
|
2059
2068
|
end
|
|
2060
2069
|
|
|
2070
|
+
# Mutation-tests a domain's own checks: changes its bluebooks in small ways and keeps which
|
|
2071
|
+
# changes the fuzz properties, the corpus script and the domain's behaviors tests let through.
|
|
2072
|
+
# A survivor is a hole in those checks. `budget` caps how many mutants are tried. The run is a
|
|
2073
|
+
# child process started through the ProcessPool adapter, as a sweep is.
|
|
2074
|
+
command "Mutate" do
|
|
2075
|
+
role "Operator"
|
|
2076
|
+
goal "Mutate a domain's rules and keep whether its checks noticed each change"
|
|
2077
|
+
|
|
2078
|
+
attribute :domain, DomainPath
|
|
2079
|
+
attribute :run, FuzzKey
|
|
2080
|
+
attribute :seeds, Count, optional: true
|
|
2081
|
+
attribute :steps, Count, optional: true
|
|
2082
|
+
attribute :budget, Count, optional: true
|
|
2083
|
+
|
|
2084
|
+
sets :domain
|
|
2085
|
+
sets :run
|
|
2086
|
+
sets :seeds
|
|
2087
|
+
sets :steps
|
|
2088
|
+
sets :budget
|
|
2089
|
+
|
|
2090
|
+
emits MutateRequested
|
|
2091
|
+
end
|
|
2092
|
+
|
|
2061
2093
|
# Measures each persistence adapter, and the Rust binary when there is one to build or use.
|
|
2062
2094
|
# The report is kept in this record, which the Hecks domain binds to Memory: keeping a
|
|
2063
2095
|
# domain's benchmark history beside it, in Sqlite under the project's `.hecks/`, is decided
|
|
@@ -2148,6 +2180,11 @@ policy "RecordTheEra" do
|
|
|
2148
2180
|
trigger FuzzRun::ProcessPool::Sweep, with: { run: :run }
|
|
2149
2181
|
end
|
|
2150
2182
|
|
|
2183
|
+
policy "MutateWhenRequested" do
|
|
2184
|
+
on "FuzzRun.MutateRequested"
|
|
2185
|
+
trigger FuzzRun::ProcessPool::Probe, with: { run: :run }
|
|
2186
|
+
end
|
|
2187
|
+
|
|
2151
2188
|
policy "MeasureWhenRequested" do
|
|
2152
2189
|
on "FuzzRun.BenchRequested"
|
|
2153
2190
|
trigger FuzzRun::RustToolchain::Measure, with: { run: :run }
|
|
@@ -188,6 +188,12 @@ Hecks.hecksagon "Hecks" do
|
|
|
188
188
|
answers "FuzzAnswered"
|
|
189
189
|
refuses "FuzzRefused"
|
|
190
190
|
end
|
|
191
|
+
|
|
192
|
+
asks "Probe", to: FuzzRun do
|
|
193
|
+
attribute :run, FuzzKey
|
|
194
|
+
answers "FuzzAnswered"
|
|
195
|
+
refuses "FuzzRefused"
|
|
196
|
+
end
|
|
191
197
|
end
|
|
192
198
|
|
|
193
199
|
Hecks::FuzzRun.port "RustToolchain" do
|
|
@@ -706,6 +706,8 @@ Hecks.bluebook "Bluebook" do
|
|
|
706
706
|
meaning: "Fraction (0.0..1.0) of seeds that start from a prefix of an earlier seed's new coverage. Off by default: measured, it trades breadth for depth (banking reached 1.41x the deep-state tuples, chess lost 0.12x of all tuples), while favoring rare verbs alone never lost."
|
|
707
707
|
member name: "favor_rare_verbs", type: "Integer",
|
|
708
708
|
meaning: "How many of the least-hit verbs the next seed weights up; 0 turns the lever off."
|
|
709
|
+
member name: "runtime_coverage_feedback", type: "Boolean",
|
|
710
|
+
meaning: "Whether guided generation also admits a seed to the corpus for reaching a new line or branch of the Ruby runtime (`Hecks::Fuzzing::RuntimeCoverage`), not only a new (verb, kind, state, mutation, outcome) tuple. Off by default: tracing every line slows each seed."
|
|
709
711
|
member name: "self_consistency_checks", type: "Boolean",
|
|
710
712
|
meaning: "Whether `hecks quality_control query sweep.run` also runs the self-consistency pass (cold rehydration, replay idempotency, value-object JSON round trip) on the same generated sequence."
|
|
711
713
|
member name: "shrink_budget", type: "Integer",
|
|
@@ -802,6 +804,7 @@ Hecks.bluebook "Bluebook" do
|
|
|
802
804
|
member script: "fuzz", section: "Custodian", aggregate: "FuzzRun", verb: "fuzz"
|
|
803
805
|
member script: "generate", section: "Custodian", aggregate: "FuzzRun", verb: "generate_sequence"
|
|
804
806
|
member script: "bench", section: "Custodian", aggregate: "FuzzRun", verb: "bench"
|
|
807
|
+
member script: "(new)", section: "Custodian", aggregate: "FuzzRun", verb: "mutate"
|
|
805
808
|
member script: "(new)", section: "Custodian", aggregate: "Era", verb: "hold_first"
|
|
806
809
|
member script: "project_deploy", section: "Deploy", aggregate: "Recipe", verb: "project"
|
|
807
810
|
member script: "lint_deploy_recipes", section: "Deploy", aggregate: "MakefileCheck", verb: "lint"
|
|
@@ -44,7 +44,8 @@ module Hecks
|
|
|
44
44
|
template("deploy-box.sh.tmpl", "STACK" => plan.infra_name, "BOX_STACK" => plan.box_stack,
|
|
45
45
|
"RDS_STACK" => plan.rds_stack, "REGION" => region,
|
|
46
46
|
"DIR" => "/opt/#{plan.infra_name}", "HEALTH_CHECKS" => health_checks(plan),
|
|
47
|
-
"SMOKE_HEADER" => smoke_header(plan), "USAGE" => deploy_usage(plan)
|
|
47
|
+
"SMOKE_HEADER" => smoke_header(plan), "USAGE" => deploy_usage(plan),
|
|
48
|
+
"DB_SECRET_LINE" => database_secret_line(plan))
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
# @param plan [Settings::Plan] the resolved settings
|
|
@@ -96,9 +97,10 @@ module Hecks
|
|
|
96
97
|
# @return [String] a Makefile whose targets create the two stacks and roll the box
|
|
97
98
|
def makefile(plan)
|
|
98
99
|
hint = plan.task_definition ? "[TASKDEF=family:revision]" : '[TAGS="web=20260101 worker=20260101"]'
|
|
99
|
-
base = template("Makefile.tmpl", "STACK" => plan.infra_name, "DEPLOY_HINT" => hint,
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
base = template("Makefile.tmpl", { "STACK" => plan.infra_name, "DEPLOY_HINT" => hint,
|
|
101
|
+
"RDS_STACK" => plan.rds_stack, "BOX_STACK" => plan.box_stack,
|
|
102
|
+
"DEPLOY_RECIPE" => RollRecipe.deploy(plan) }
|
|
103
|
+
.merge(database_makefile_values(plan)))
|
|
102
104
|
plan.hosting ? "#{base}\ninclude hosting.mk\n" : base
|
|
103
105
|
end
|
|
104
106
|
|
|
@@ -23,6 +23,10 @@ module Hecks
|
|
|
23
23
|
S3_BUCKET = /\A[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]\z/
|
|
24
24
|
S3_SHAPE = "s3_access: a list of `{ bucket: \"name\", write: true }` hashes (write is optional)".freeze
|
|
25
25
|
FROM_TASKDEF = %i[env secrets repository].freeze
|
|
26
|
+
|
|
27
|
+
# The settings that size an RDS instance: with `shared_database` they are the shared
|
|
28
|
+
# instance's.
|
|
29
|
+
SHARED_INSTANCE_SETTINGS = %i[database_class storage_gb engine_version backup_days].freeze
|
|
26
30
|
# Default images, each a version tag plus the digest of its multi-architecture index,
|
|
27
31
|
# so a rebuilt box pulls the same bytes.
|
|
28
32
|
TUNNEL_IMAGE = "cloudflare/cloudflared:2026.9.3" \
|