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.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +1 -1
- data/lib/hecks/bench/cli.rb +74 -0
- data/lib/hecks/bench/environment.rb +100 -0
- data/lib/hecks/bench/postgres_probe.rb +41 -0
- data/lib/hecks/bench/report.rb +116 -0
- data/lib/hecks/bench/ruby_runner.rb +151 -0
- data/lib/hecks/bench/run.rb +55 -0
- data/lib/hecks/bench/rust_runner.rb +165 -0
- data/lib/hecks/bench/stats.rb +75 -0
- data/lib/hecks/bench/suite.rb +172 -0
- data/lib/hecks/bench/workload.rb +137 -0
- data/lib/hecks/bench.rb +28 -0
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +42 -3
- data/lib/hecks/bluebook/hexagon.rb +15 -7
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +153 -0
- data/lib/hecks/bluebook/model_check.rb +21 -2
- data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
- data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
- data/lib/hecks/embryonaut_bluebook.rb +29 -3
- data/lib/hecks/forms/value_object_shape.rb +1 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
- data/lib/hecks/fuzzing/era_boundary.rb +1 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +136 -69
- data/lib/hecks/language/world/world.bluebook +32 -2
- data/lib/hecks/mcp_stdio_guard.rb +160 -0
- data/lib/hecks/ports/key_vault.rb +1 -1
- data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +2 -1
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
- data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
- data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
- data/lib/hecks/ports/persistence.rb +4 -0
- data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
- data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
- data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
- data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
- data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
- data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
- data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
- data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
- data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
- data/lib/hecks/projections/deploy/fargate.rb +187 -138
- data/lib/hecks/projections/deploy/lambda.rb +5 -5
- data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
- data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
- data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
- data/lib/hecks/projections/deploy/preview/script.rb +40 -0
- data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
- data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
- data/lib/hecks/projections/deploy/preview/template.rb +247 -0
- data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
- data/lib/hecks/projections/deploy/preview.rb +103 -0
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
- data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
- data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
- data/lib/hecks/projections/deploy/scripts.rb +171 -0
- data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
- data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
- data/lib/hecks/projections/deploy/smoke.rb +159 -0
- data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
- data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
- data/lib/hecks/projections/deploy/template_diff.rb +173 -0
- data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
- data/lib/hecks/release/gem_pin.rb +112 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
- data/lib/hecks/runtime/loader.rb +26 -2
- data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
- data/lib/hecks/runtime/registry/verification.rb +4 -2
- data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
- data/lib/hecks/runtime/registry.rb +14 -9
- data/lib/hecks/runtime/tenant_check.rb +1 -1
- data/lib/hecks/vendoring/git_environment.rb +35 -0
- data/lib/hecks/vendoring/git_source.rb +102 -0
- data/lib/hecks/vendoring.rb +102 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +3 -1
- 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
|