hecks 2.5.1 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
  3. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  4. data/lib/hecks/adapters/driven/prism.rb +1 -1
  5. data/lib/hecks/bench/cli.rb +74 -0
  6. data/lib/hecks/bench/environment.rb +100 -0
  7. data/lib/hecks/bench/postgres_probe.rb +41 -0
  8. data/lib/hecks/bench/report.rb +116 -0
  9. data/lib/hecks/bench/ruby_runner.rb +151 -0
  10. data/lib/hecks/bench/run.rb +55 -0
  11. data/lib/hecks/bench/rust_runner.rb +165 -0
  12. data/lib/hecks/bench/stats.rb +75 -0
  13. data/lib/hecks/bench/suite.rb +172 -0
  14. data/lib/hecks/bench/workload.rb +137 -0
  15. data/lib/hecks/bench.rb +28 -0
  16. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  17. data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
  18. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
  19. data/lib/hecks/bluebook/dsl/world_builder.rb +42 -3
  20. data/lib/hecks/bluebook/hexagon.rb +15 -7
  21. data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
  22. data/lib/hecks/bluebook/model_check/client_profile.rb +153 -0
  23. data/lib/hecks/bluebook/model_check.rb +21 -2
  24. data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
  25. data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
  26. data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
  27. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
  28. data/lib/hecks/embryonaut_bluebook.rb +29 -3
  29. data/lib/hecks/forms/value_object_shape.rb +1 -1
  30. data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
  31. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  32. data/lib/hecks/fuzzing/isolated_boot.rb +136 -69
  33. data/lib/hecks/language/world/world.bluebook +32 -2
  34. data/lib/hecks/mcp_stdio_guard.rb +160 -0
  35. data/lib/hecks/ports/key_vault.rb +1 -1
  36. data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
  37. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +2 -1
  38. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
  39. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
  40. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +1 -1
  41. data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
  42. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
  43. data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
  44. data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
  45. data/lib/hecks/ports/persistence.rb +4 -0
  46. data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
  47. data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
  48. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
  49. data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
  50. data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
  51. data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
  52. data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
  53. data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
  54. data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
  55. data/lib/hecks/projections/deploy/fargate.rb +187 -138
  56. data/lib/hecks/projections/deploy/lambda.rb +5 -5
  57. data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
  58. data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
  59. data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
  60. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
  61. data/lib/hecks/projections/deploy/preview/script.rb +40 -0
  62. data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
  63. data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
  64. data/lib/hecks/projections/deploy/preview/template.rb +247 -0
  65. data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
  66. data/lib/hecks/projections/deploy/preview.rb +103 -0
  67. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
  68. data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
  69. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
  70. data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
  71. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
  72. data/lib/hecks/projections/deploy/scripts.rb +171 -0
  73. data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
  74. data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
  75. data/lib/hecks/projections/deploy/smoke.rb +159 -0
  76. data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
  77. data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
  78. data/lib/hecks/projections/deploy/template_diff.rb +173 -0
  79. data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
  80. data/lib/hecks/projections.rb +2 -0
  81. data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
  82. data/lib/hecks/release/gem_pin.rb +112 -0
  83. data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
  84. data/lib/hecks/runtime/loader.rb +26 -2
  85. data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
  86. data/lib/hecks/runtime/registry/verification.rb +4 -2
  87. data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
  88. data/lib/hecks/runtime/registry.rb +14 -9
  89. data/lib/hecks/runtime/tenant_check.rb +1 -1
  90. data/lib/hecks/vendoring/git_environment.rb +35 -0
  91. data/lib/hecks/vendoring/git_source.rb +102 -0
  92. data/lib/hecks/vendoring.rb +102 -0
  93. data/lib/hecks/version.rb +1 -1
  94. data/lib/hecks.rb +3 -1
  95. metadata +59 -2
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env bash
2
+ # GENERATED by bin/project_deploy -- re-run it to refresh this file rather
3
+ # than hand-editing. Settings come from the domain's deployed_to("AwsFargate")
4
+ # block; see Hecks::Projections::Deploy::Scripts for each key.
5
+ #
6
+ # Runs the production smoke once a roll of the ECS service has settled, so a
7
+ # deploy ends with a real check of the live site instead of waiting for the
8
+ # next scheduled run. Called at the end of deploy-service.sh; also runnable by
9
+ # hand:
10
+ #
11
+ # bash smoke-after-deploy.sh
12
+ #
13
+ # What it does, in order:
14
+ # 1. Waits for the roll to settle: the stack is UPDATE_COMPLETE or
15
+ # CREATE_COMPLETE and the ECS service has a single PRIMARY, COMPLETED
16
+ # deployment with desired == running on the service's own task definition,
17
+ # on two consecutive checks one interval apart. Gives up after the timeout.
18
+ # 2. Dispatches the smoke workflow on the configured ref, finds the run that
19
+ # dispatch created (a workflow_dispatch run that did not exist before the
20
+ # dispatch, never a scheduled one), waits for it, and prints its
21
+ # conclusion, the failed check names and its URL.
22
+ #
23
+ # It only ever READS AWS (describe-stacks, describe-services); the smoke itself
24
+ # runs on GitHub against the live site.
25
+ #
26
+ # Exit codes (chosen to stay clear of the ones a failed deploy step produces:
27
+ # 1 from bash or python, 2 from bash misuse, 254/255 from the aws CLI):
28
+ # 0 the smoke passed, or was skipped on purpose (SKIP_POST_DEPLOY_SMOKE=1)
29
+ # 20 the roll did not settle in time, or the stack ended in a failed state;
30
+ # the smoke was NOT run
31
+ # 21 `gh` is missing or not signed in, or no repository or workflow is
32
+ # configured; the smoke was NOT run (the manual command is printed)
33
+ # 22 the smoke ran and FAILED
34
+ # 23 the smoke could not be dispatched, found or finished; result unknown
35
+ #
36
+ # Environment:
37
+ # SKIP_POST_DEPLOY_SMOKE=1 skip everything and say so
38
+ # DRY_RUN=1 wait for the roll as usual, then print what would
39
+ # be dispatched without dispatching anything
40
+ # SETTLE_TIMEOUT_SECS default 1200
41
+ # SETTLE_CHECK_INTERVAL_SECS default 60
42
+ # SMOKE_RUN_TIMEOUT_SECS default 1800, how long to wait for the run
43
+ # REPO GitHub repository (owner/name) holding the workflow
44
+ # WORKFLOW workflow file name
45
+ # SMOKE_REF ref to dispatch on
46
+ set -euo pipefail
47
+
48
+ #@@SETTINGS@@
49
+
50
+ SETTLE_TIMEOUT_SECS="${SETTLE_TIMEOUT_SECS:-1200}"
51
+ SETTLE_CHECK_INTERVAL_SECS="${SETTLE_CHECK_INTERVAL_SECS:-60}"
52
+ SMOKE_RUN_TIMEOUT_SECS="${SMOKE_RUN_TIMEOUT_SECS:-1800}"
53
+ MANUAL_COMMAND="bash smoke-after-deploy.sh"
54
+ MANUAL_DISPATCH="gh workflow run ${WORKFLOW} --repo ${REPO} --ref ${SMOKE_REF}"
55
+
56
+ if [ "${SKIP_POST_DEPLOY_SMOKE:-}" = "1" ]; then
57
+ echo "==> post-deploy smoke SKIPPED (SKIP_POST_DEPLOY_SMOKE=1)."
58
+ echo " Run it yourself when ready: ${MANUAL_COMMAND}"
59
+ exit 0
60
+ fi
61
+
62
+ if [ -z "$REPO" ] || [ -z "$WORKFLOW" ]; then
63
+ echo "==> post-deploy smoke NOT RUN: no smoke repository or workflow is configured." >&2
64
+ echo " Set REPO and WORKFLOW, or the smoke_repo and smoke_workflow settings, and re-run." >&2
65
+ exit 21
66
+ fi
67
+
68
+ if ! command -v gh >/dev/null 2>&1 || ! gh auth status >/dev/null 2>&1; then
69
+ echo "==> post-deploy smoke NOT RUN: the GitHub CLI (gh) is missing or not signed in." >&2
70
+ echo " Run it by hand: ${MANUAL_DISPATCH}" >&2
71
+ echo " or, once gh works: ${MANUAL_COMMAND}" >&2
72
+ exit 21
73
+ fi
74
+
75
+ stack_status() {
76
+ aws cloudformation describe-stacks --stack-name "$STACK" --region "$REGION" \
77
+ --query 'Stacks[0].StackStatus' --output text 2>/dev/null || true
78
+ }
79
+
80
+ # Prints "ok <task definition arn>" when the service looks settled, otherwise
81
+ # "wait <why>".
82
+ ecs_state() {
83
+ aws ecs describe-services --cluster "$CLUSTER" --services "$ECS_SERVICE" --region "$REGION" \
84
+ --query 'services[0].{td:taskDefinition,desired:desiredCount,running:runningCount,pending:pendingCount,deployments:deployments[].{status:status,rollout:rolloutState,td:taskDefinition}}' \
85
+ --output json 2>/dev/null | python3 -c '
86
+ import json, sys
87
+ try:
88
+ s = json.load(sys.stdin)
89
+ except ValueError:
90
+ print("wait could not read the service"); raise SystemExit
91
+ if not s:
92
+ print("wait service not found"); raise SystemExit
93
+ deps = s.get("deployments") or []
94
+ if len(deps) != 1:
95
+ print("wait %d deployments in flight" % len(deps)); raise SystemExit
96
+ d = deps[0]
97
+ if d.get("status") != "PRIMARY" or d.get("rollout") != "COMPLETED":
98
+ print("wait deployment is %s / %s" % (d.get("status"), d.get("rollout"))); raise SystemExit
99
+ if d.get("td") != s.get("td"):
100
+ print("wait deployment task definition differs from the service"); raise SystemExit
101
+ if not s.get("desired") or s.get("desired") != s.get("running") or s.get("pending"):
102
+ print("wait desired=%s running=%s pending=%s" % (s.get("desired"), s.get("running"), s.get("pending"))); raise SystemExit
103
+ print("ok " + s["td"])
104
+ ' || echo "wait could not read the service"
105
+ }
106
+
107
+ wait_for_settled_roll() {
108
+ echo "==> waiting for the roll to settle (stack ${STACK}, ECS service ${ECS_SERVICE}; timeout ${SETTLE_TIMEOUT_SECS}s)"
109
+ local start=$SECONDS previous="" status ecs signature
110
+ while true; do
111
+ status="$(stack_status)"
112
+ case "$status" in
113
+ *ROLLBACK*|*FAILED*)
114
+ echo "==> roll did not settle: stack ${STACK} is ${status}. The smoke was NOT run." >&2
115
+ return 20 ;;
116
+ esac
117
+ if [ "$status" = "UPDATE_COMPLETE" ] || [ "$status" = "CREATE_COMPLETE" ]; then
118
+ ecs="$(ecs_state)"
119
+ else
120
+ ecs="wait stack is ${status:-unreadable}"
121
+ fi
122
+ if [ "${ecs%% *}" = "ok" ]; then
123
+ signature="${status} ${ecs#ok }"
124
+ if [ "$signature" = "$previous" ]; then
125
+ echo " -> settled on ${ecs#ok } (two consecutive checks agree)"
126
+ return 0
127
+ fi
128
+ echo " -> looks settled (${status}, ${ecs#ok }); confirming in ${SETTLE_CHECK_INTERVAL_SECS}s"
129
+ previous="$signature"
130
+ else
131
+ echo " -> not yet: ${ecs#wait }"
132
+ previous=""
133
+ fi
134
+ if [ $((SECONDS - start + SETTLE_CHECK_INTERVAL_SECS)) -gt "$SETTLE_TIMEOUT_SECS" ]; then
135
+ echo "==> roll did not settle within ${SETTLE_TIMEOUT_SECS}s (last: ${ecs#wait }). The smoke was NOT run." >&2
136
+ return 20
137
+ fi
138
+ sleep "$SETTLE_CHECK_INTERVAL_SECS"
139
+ done
140
+ }
141
+
142
+ # Ids of the workflow_dispatch runs that exist now, one per line.
143
+ dispatch_run_ids() {
144
+ gh run list --repo "$REPO" --workflow "$WORKFLOW" --event workflow_dispatch --limit 50 \
145
+ --json databaseId --jq '.[].databaseId' 2>/dev/null
146
+ }
147
+
148
+ print_failed_checks() {
149
+ local run_id="$1" log names
150
+ log="$(gh run view "$run_id" --repo "$REPO" --log 2>/dev/null || true)"
151
+ # Log lines are "<job>\t<step>\t<timestamp> <text>"; a smoke that prints
152
+ # " <check>... FAILED: <why>" for each failing check is listed by name.
153
+ names="$(printf '%s\n' "$log" | awk -F'\t' 'NF >= 3 { print $3 }' | sed -E 's/^[0-9T:.-]+Z //' \
154
+ | grep -E '\.\.\. FAILED:' | sed -E 's/^ +//' || true)"
155
+ if [ -n "$names" ]; then
156
+ echo " failed checks:"
157
+ printf '%s\n' "$names" | sed 's/^/ - /'
158
+ else
159
+ echo " (no failed check names in the log; the run may have failed before the checks. Last log lines:)"
160
+ printf '%s\n' "$log" | tail -n 15 | sed 's/^/ /'
161
+ fi
162
+ }
163
+
164
+ run_smoke() {
165
+ local before run_id="" waited=0 url conclusion status
166
+ if ! before="$(dispatch_run_ids)"; then
167
+ echo "==> could not list the smoke workflow's runs, so it was NOT dispatched (it could not be told apart afterwards). Run it by hand: ${MANUAL_DISPATCH}" >&2
168
+ return 23
169
+ fi
170
+
171
+ echo "==> dispatching the production smoke: ${MANUAL_DISPATCH}"
172
+ if ! gh workflow run "$WORKFLOW" --repo "$REPO" --ref "$SMOKE_REF"; then
173
+ echo "==> could not dispatch the smoke workflow. Deploy succeeded; run it by hand: ${MANUAL_DISPATCH}" >&2
174
+ return 23
175
+ fi
176
+
177
+ # The dispatch call does not return the run, so find the workflow_dispatch
178
+ # run that was not there before it (scheduled runs are never candidates).
179
+ while [ -z "$run_id" ] && [ "$waited" -lt 180 ]; do
180
+ sleep 5
181
+ waited=$((waited + 5))
182
+ run_id="$(comm -13 <(printf '%s\n' "$before" | sort) <(dispatch_run_ids | sort) | sort -n | tail -n 1)"
183
+ done
184
+ if [ -z "$run_id" ]; then
185
+ echo "==> dispatched, but the new run never appeared. Deploy succeeded; check https://github.com/${REPO}/actions/workflows/${WORKFLOW}" >&2
186
+ return 23
187
+ fi
188
+ url="https://github.com/${REPO}/actions/runs/${run_id}"
189
+ echo "==> smoke run ${run_id}: ${url}"
190
+
191
+ waited=0
192
+ while true; do
193
+ status="$(gh run view "$run_id" --repo "$REPO" --json status --jq .status 2>/dev/null || true)"
194
+ [ "$status" = "completed" ] && break
195
+ if [ "$waited" -ge "$SMOKE_RUN_TIMEOUT_SECS" ]; then
196
+ echo "==> deploy succeeded, but the smoke run did not finish within ${SMOKE_RUN_TIMEOUT_SECS}s: see ${url}" >&2
197
+ return 23
198
+ fi
199
+ sleep 15
200
+ waited=$((waited + 15))
201
+ done
202
+
203
+ conclusion="$(gh run view "$run_id" --repo "$REPO" --json conclusion --jq .conclusion || true)"
204
+ echo "==> smoke conclusion: ${conclusion} ${url}"
205
+ if [ "$conclusion" = "success" ]; then
206
+ echo "==> deploy succeeded, post-deploy smoke passed."
207
+ return 0
208
+ fi
209
+ print_failed_checks "$run_id"
210
+ echo "==> deploy succeeded, post-deploy smoke FAILED: see ${url}" >&2
211
+ return 22
212
+ }
213
+
214
+ rc=0
215
+ wait_for_settled_roll || rc=$?
216
+ [ "$rc" -eq 0 ] || exit "$rc"
217
+
218
+ if [ "${DRY_RUN:-}" = "1" ]; then
219
+ echo "==> DRY_RUN=1: would dispatch: ${MANUAL_DISPATCH}"
220
+ newest="$(dispatch_run_ids | sort -n | tail -n 1 || true)"
221
+ if [ -n "$newest" ]; then
222
+ echo " then pick the workflow_dispatch run that appears after it; the newest existing one is"
223
+ echo " https://github.com/${REPO}/actions/runs/${newest} (not the one it would wait for)"
224
+ else
225
+ echo " (no workflow_dispatch runs exist yet)"
226
+ fi
227
+ echo " then wait for it and report its conclusion and failed checks. Nothing dispatched."
228
+ exit 0
229
+ fi
230
+
231
+ run_smoke || rc=$?
232
+ exit "$rc"
@@ -0,0 +1,171 @@
1
+ require "shellwords"
2
+ require_relative "scripts/settings"
3
+
4
+ module Hecks
5
+ module Projections
6
+ module Deploy
7
+ # The hosting scripts an AWS Fargate deploy ships beside its template:
8
+ # the steps an operator runs after `make deploy` has created the stack.
9
+ # `Fargate.call` adds them when the domain's
10
+ # `deployed_to("AwsFargate")` block opts in, and leaves its file map
11
+ # untouched otherwise.
12
+ #
13
+ # ## What it emits
14
+ #
15
+ # - `deploy-service.sh` reads the service's active task definition,
16
+ # pushes a local image under a never-reused tag, swaps one container's
17
+ # image, and syncs that container's CloudFormation `*ImageTag`
18
+ # parameter with `--use-previous-template`, checking afterwards that no
19
+ # other parameter changed.
20
+ # - `smoke-after-deploy.sh` waits for the roll to settle, then dispatches
21
+ # a GitHub workflow and reports its result. Exit codes 20 to 23 are its
22
+ # own.
23
+ # - `hosting.mk` is included by the generated `Makefile`. It pins the
24
+ # Hecks release the domain image is built from (`HECKS_ROOT` resolves
25
+ # to a cached checkout of that release's tag, never to a path on one
26
+ # machine) and adds the `deploy-service`, `smoke-after-deploy` and
27
+ # `check-era` targets.
28
+ # - `expected-era` is the allow-list `bin/check_era` compares a host's
29
+ # `GET /version` era against.
30
+ #
31
+ # ## Settings
32
+ #
33
+ # Read from `deployed_to("AwsFargate")`. Everything but `hosting_scripts`
34
+ # and `hecks_release` has a default; the ones the stack template already
35
+ # names (`stack_name`, `stack_prefix`, `region`) are not repeated here.
36
+ #
37
+ # - `hosting_scripts` opts in to every file above, when `true`. Off by default.
38
+ # - `hecks_release` is the Hecks release the domain image is built from,
39
+ # such as `"2.5.1"`. Required.
40
+ # - `hecks_source` is the repository URL the release tag is fetched from.
41
+ # Defaults to the Hecks repository.
42
+ # - `hecks_cache_dir` is where release checkouts are kept, a Make expression.
43
+ # Defaults to `$(HOME)/.cache/hecks`.
44
+ # - `ecs_cluster` and `ecs_service` name the cluster and service. They default to the
45
+ # names the generated template gives them: the stack name, or the `names` override.
46
+ # - `smoke_repo` is the GitHub `owner/name` holding the smoke workflow, and
47
+ # `smoke_workflow` its file name. With neither set, `REPO` and `WORKFLOW` must
48
+ # come from the environment.
49
+ # - `smoke_ref` is the ref the workflow is dispatched on. Defaults to `main`.
50
+ # - `expected_eras` lists the era ids `expected-era` holds. Defaults to none, so a
51
+ # host is only checked to report an era.
52
+ # - `public_url` is the host URL `make check-era` requests. Without it, `URL` must
53
+ # be passed to `make`.
54
+ #
55
+ # ## Names come from the template
56
+ #
57
+ # The containers, their ECR repositories and their image-tag parameters are not
58
+ # settings of the scripts. `deploy-service.sh` accepts exactly the containers the
59
+ # template's `Fargate::Containers::Layout` holds (the domain container, then each
60
+ # entry of the world's `containers`), and for each one pushes to the repository and
61
+ # syncs the parameter that layout names: the values the template renders. Rename them
62
+ # with `domain_container` and `containers` (see `Fargate`), and the scripts, and the
63
+ # `Makefile`'s `deploy`, follow.
64
+ module Scripts
65
+ SCRIPT_DIR = File.join(__dir__, "scripts").freeze
66
+
67
+ module_function
68
+
69
+ # Adds the hosting scripts to a Fargate file map when the domain opts in.
70
+ #
71
+ # @param files [Hash{String => String}] the Fargate target's own files, keyed by path
72
+ # @param deploy_settings [Hash{Symbol => Object}] the world's `deployed_to("AwsFargate")`
73
+ # settings
74
+ # @param plan [Fargate::Settings::Plan] the template generator's resolved settings, the
75
+ # source of every container, repository and parameter name
76
+ # @param stack_name [String] the CloudFormation stack name
77
+ # @param region [String] the validated AWS region
78
+ # @return [Hash{String => String}] `files` itself when the domain has not opted in, else
79
+ # a copy with the scripts added and the `Makefile` including `hosting.mk`
80
+ # @raise [ArgumentError] if a hosting setting is missing or invalid; see `Settings`
81
+ def extend_files(files, deploy_settings:, plan:, stack_name:, region:)
82
+ return files unless deploy_settings[:hosting_scripts] == true
83
+
84
+ settings = Settings.new(deploy_settings: deploy_settings, plan: plan,
85
+ stack_name: stack_name, region: region)
86
+ files.merge(
87
+ "Makefile" => "#{files.fetch('Makefile')}\ninclude hosting.mk\n",
88
+ "hosting.mk" => hosting_mk(settings),
89
+ "deploy-service.sh" => deploy_service_sh(settings),
90
+ "smoke-after-deploy.sh" => smoke_after_deploy_sh(settings),
91
+ "expected-era" => expected_era(settings)
92
+ )
93
+ end
94
+
95
+ # Renders `deploy-service.sh`.
96
+ #
97
+ # @param settings [Settings] the checked hosting settings
98
+ # @return [String] the script
99
+ def deploy_service_sh(settings)
100
+ render("deploy-service.sh.tmpl", "SETTINGS" => stack_constants(settings),
101
+ "SERVICES" => services_block(settings))
102
+ end
103
+
104
+ # Renders `smoke-after-deploy.sh`.
105
+ #
106
+ # @param settings [Settings] the checked hosting settings
107
+ # @return [String] the script
108
+ def smoke_after_deploy_sh(settings)
109
+ smoke = [
110
+ "REPO=\"${REPO:-#{settings.smoke_repo}}\"",
111
+ "WORKFLOW=\"${WORKFLOW:-#{settings.smoke_workflow}}\"",
112
+ "SMOKE_REF=\"${SMOKE_REF:-#{settings.smoke_ref}}\""
113
+ ]
114
+ render("smoke-after-deploy.sh.tmpl", "SETTINGS" => [stack_constants(settings), *smoke].join("\n"))
115
+ end
116
+
117
+ # Renders the `expected-era` allow-list.
118
+ #
119
+ # @param settings [Settings] the checked hosting settings
120
+ # @return [String] the header comment followed by one era id per line
121
+ def expected_era(settings)
122
+ header = File.read(File.join(SCRIPT_DIR, "expected-era.header"))
123
+ "#{([header.chomp, ''] + settings.expected_eras).join("\n")}\n"
124
+ end
125
+
126
+ # Renders `hosting.mk`.
127
+ #
128
+ # @param settings [Settings] the checked hosting settings
129
+ # @return [String] the Makefile fragment
130
+ def hosting_mk(settings)
131
+ render("hosting.mk.tmpl", "RELEASE" => settings.hecks_release, "SOURCE" => settings.hecks_source,
132
+ "CACHE_DIR" => settings.hecks_cache_dir, "URL" => settings.public_url.to_s,
133
+ "SERVICE" => settings.containers.first.name)
134
+ end
135
+
136
+ def stack_constants(settings)
137
+ {
138
+ "REGION" => settings.region, "CLUSTER" => settings.cluster,
139
+ "ECS_SERVICE" => settings.service, "STACK" => settings.stack
140
+ }.map { |name, value| "#{name}=#{Shellwords.escape(value)}" }.join("\n")
141
+ end
142
+
143
+ def services_block(settings)
144
+ arms = settings.containers.map do |container|
145
+ repository = Shellwords.escape(container.repository_name)
146
+ parameter = Shellwords.escape(container.tag_parameter)
147
+ " #{Shellwords.escape(container.name)}) ECR_REPOSITORY=#{repository}; CFN_PARAM_KEY=#{parameter} ;;"
148
+ end
149
+ <<~BASH.chomp
150
+ SERVICES='#{settings.containers.map(&:name).join(' ')}'
151
+
152
+ # Sets ECR_REPOSITORY and CFN_PARAM_KEY for one service, or exits.
153
+ resolve_service() {
154
+ case "$1" in
155
+ #{arms.join("\n")}
156
+ *) echo "unknown service '$1', must be one of: ${SERVICES}" >&2; exit 1 ;;
157
+ esac
158
+ }
159
+ BASH
160
+ end
161
+
162
+ def render(template, values)
163
+ text = File.read(File.join(SCRIPT_DIR, template))
164
+ values.reduce(text) { |out, (marker, value)| out.gsub(/#?@@#{marker}@@/) { value } }
165
+ end
166
+
167
+ private_class_method :stack_constants, :services_block, :render
168
+ end
169
+ end
170
+ end
171
+ end