hecks 3.2.1 → 3.3.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/bluebook/assembly/contracts.rb +2 -1
  3. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  4. data/lib/hecks/bluebook/dsl/command_builder.rb +2 -39
  5. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +1 -1
  6. data/lib/hecks/bluebook/dsl/need_word.rb +50 -0
  7. data/lib/hecks/bluebook/dsl/query_builder.rb +6 -1
  8. data/lib/hecks/bluebook/query.rb +8 -5
  9. data/lib/hecks/cli/interview_draft.rb +86 -31
  10. data/lib/hecks/cli/interview_session.rb +77 -26
  11. data/lib/hecks/cli/interview_shape.rb +179 -0
  12. data/lib/hecks/doors/cli_runner.rb +14 -1
  13. data/lib/hecks/doors/launcher_options.rb +13 -3
  14. data/lib/hecks/doors/mcp_door_scope.rb +2 -1
  15. data/lib/hecks/doors.rb +1 -1
  16. data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
  17. data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
  18. data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
  19. data/lib/hecks/embryonaut_bluebook.rb +2 -0
  20. data/lib/hecks/fuzzing/qa_settings.rb +15 -34
  21. data/lib/hecks/gate/stages.yml +113 -8
  22. data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
  23. data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
  24. data/lib/hecks/hecks/adapters/git.rb +59 -0
  25. data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
  26. data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
  27. data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
  28. data/lib/hecks/hecks/adapters/site_toolchain.rb +67 -0
  29. data/lib/hecks/hecks/codebase.behaviors +6 -0
  30. data/lib/hecks/hecks/codebase.bluebook +21 -0
  31. data/lib/hecks/hecks/custodian.behaviors +6 -0
  32. data/lib/hecks/hecks/custodian.bluebook +217 -0
  33. data/lib/hecks/hecks/hecks.hecksagon +24 -0
  34. data/lib/hecks/hecks/hecks.world +8 -3
  35. data/lib/hecks/hecks/site.world +5 -2
  36. data/lib/hecks/language/bluebook/query.bluebook +25 -0
  37. data/lib/hecks/language/bluebook/vocabulary.bluebook +213 -0
  38. data/lib/hecks/language/hecksagon/hecksagon.bluebook +1 -1
  39. data/lib/hecks/language/oidc.json +5 -0
  40. data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
  41. data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
  42. data/lib/hecks/projections/deploy/box/settings.rb +17 -4
  43. data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
  44. data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
  45. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
  46. data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
  47. data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
  48. data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
  49. data/lib/hecks/projections/deploy/box.rb +15 -2
  50. data/lib/hecks/projections/deploy/fargate.rb +3 -3
  51. data/lib/hecks/projections/deploy/lambda.rb +8 -8
  52. data/lib/hecks/projections/model/deviations.rb +1 -1
  53. data/lib/hecks/projections/site/live_edge.rb +162 -0
  54. data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
  55. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
  56. data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
  57. data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
  58. data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
  59. data/lib/hecks/quality_control/quality_control.bluebook +76 -124
  60. data/lib/hecks/runtime/interpreting.rb +3 -22
  61. data/lib/hecks/runtime/needs.rb +48 -0
  62. data/lib/hecks/runtime/query_interpreter.rb +3 -0
  63. data/lib/hecks/runtime/registry/verification.rb +3 -3
  64. data/lib/hecks/rust_build/host.rb +147 -0
  65. data/lib/hecks/rust_build/kernel_input.rb +40 -18
  66. data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
  67. data/lib/hecks/rust_build/project_rust.rb +3 -10
  68. data/lib/hecks/rust_build.rb +1 -0
  69. data/lib/hecks/site/bluebook/site.bluebook +53 -1
  70. data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
  71. data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
  72. data/lib/hecks/tools/gate.rb +1 -1
  73. data/lib/hecks/tools/rust_comment_style.rb +1 -3
  74. data/lib/hecks/tools/tools_doc.rb +204 -0
  75. data/lib/hecks/tools.rb +1 -0
  76. data/lib/hecks/version.rb +1 -1
  77. data/lib/hecks/vocabulary.rb +151 -0
  78. data/qa/settings.yml +1 -2
  79. data/rust/build/src/lineage_pass.rs +30 -15
  80. data/rust/build/src/main.rs +4 -2
  81. data/rust/build/src/pipeline.rs +8 -19
  82. data/rust/codegen/src/bridging.rs +7 -7
  83. data/rust/codegen/src/json.rs +214 -23
  84. data/rust/codegen/src/literal.rs +6 -3
  85. data/rust/codegen/src/main.rs +20 -9
  86. data/rust/codegen/src/naming.rs +4 -2
  87. data/rust/codegen/src/optional_pass.rs +226 -0
  88. data/rust/{build → codegen}/src/sidecars.rs +9 -5
  89. data/rust/host/HECKS_RELEASE +1 -1
  90. data/rust/host/src/api.rs +1 -1
  91. data/rust/host/src/dispatch.rs +5 -0
  92. data/rust/host/src/needs.rs +99 -2
  93. data/rust/host/src/resend.rs +4 -4
  94. data/rust/host/src/ui_schema.rs +1 -1
  95. data/rust/parser/src/emit.rs +12 -0
  96. data/rust/parser/src/ir.rs +2 -0
  97. data/rust/parser/src/keywords.rs +2 -0
  98. data/rust/parser/src/main.rs +1 -0
  99. data/rust/parser/src/parse/command.rs +2 -44
  100. data/rust/parser/src/parse/mod.rs +1 -0
  101. data/rust/parser/src/parse/needs.rs +60 -0
  102. data/rust/parser/src/parse/query.rs +3 -0
  103. data/rust/src/kernel/cli.rs +16 -6
  104. data/rust/src/kernel/needs.rs +104 -9
  105. metadata +22 -8
  106. data/lib/hecks/rust_build/append_optionals.rb +0 -79
  107. data/lib/hecks/rust_build/rust_literal.rb +0 -33
  108. data/lib/hecks/three_zero/forms.yml +0 -127
  109. data/lib/hecks/three_zero.rb +0 -12
  110. data/rust/build/src/json.rs +0 -421
  111. data/rust/build/src/optional_pass.rs +0 -150
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bash
2
+ # GENERATED by hecks deploy project -- re-run it to refresh this file rather than hand-editing.
3
+ # Settings come from the domain's deployed_to("AwsBox") block (hosting_scripts true).
4
+ #
5
+ # Deploys ONE container's freshly built local image to @@STACK@@'s app box, under a fresh, unique ECR tag.
6
+ #
7
+ # bash deploy-service.sh <service>
8
+ # EXISTING_TAG=<tag> bash deploy-service.sh <service> redeploy a tag already in ECR (no push)
9
+ #
10
+ # Why it exists: pushing a floating tag and restarting a container looks like a deploy, but nothing
11
+ # records which image the box was told to run, and a box that pulls a tag it already has keeps the old
12
+ # image. This script pushes the local image under a never-reused tag (<service>-<UTC timestamp>),
13
+ # refuses a tag that is already in ECR, and names that tag in the roll.
14
+ @@MODE_SUMMARY@@
15
+ #
16
+ # It ends with smoke-after-deploy.sh, whose own exit codes (20-23) are distinct from a failed deploy
17
+ # step's. SKIP_POST_DEPLOY_SMOKE=1 skips it.
18
+ #
19
+ # Environment:
20
+ # LOCAL_IMAGE local image to push, default <ecr repository>:latest
21
+ # EXISTING_TAG redeploy this tag from ECR instead of building and pushing
22
+ # SETTLE_TIMEOUT_SECS default 1200, how long to wait for a stack update
23
+ # SKIP_POST_DEPLOY_SMOKE=1 passed through to smoke-after-deploy.sh
24
+ set -euo pipefail
25
+
26
+ @@SETTINGS@@
27
+
28
+ @@SERVICES@@
29
+
30
+ SERVICE_NAME="${1:?usage: deploy-service.sh <service> (one of: ${SERVICES})}"
31
+ resolve_service "$SERVICE_NAME"
32
+
33
+ cd "$(dirname "${BASH_SOURCE[0]}")"
34
+ SETTLE_TIMEOUT_SECS="${SETTLE_TIMEOUT_SECS:-1200}"
35
+ ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
36
+ ECR_HOST="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com"
37
+ SCRATCH_DIR="$(mktemp -d)"
38
+ trap 'rm -rf "$SCRATCH_DIR"' EXIT
39
+
40
+ # True once $1 (a value of $SECONDS) is further in the past than the settle timeout.
41
+ timed_out() { [ $((SECONDS - $1)) -gt "$SETTLE_TIMEOUT_SECS" ]; }
42
+
43
+ @@REPOSITORY_STEP@@
44
+ TAG="${EXISTING_TAG:-${SERVICE_NAME}-$(date -u +%Y%m%d%H%M%S)}"
45
+ IMAGE_URI="${ECR_HOST}/${ECR_REPOSITORY}:${TAG}"
46
+ LOCAL_IMAGE="${LOCAL_IMAGE:-${ECR_REPOSITORY}:latest}"
47
+
48
+ if [ -n "${EXISTING_TAG:-}" ]; then
49
+ echo "==> reusing ${IMAGE_URI} (no push)"
50
+ aws ecr describe-images --repository-name "$ECR_REPOSITORY" --region "$REGION" \
51
+ --image-ids "imageTag=${TAG}" --query 'imageDetails[0].imageTags' --output text > /dev/null \
52
+ || { echo "tag ${TAG} is not in ECR" >&2; exit 1; }
53
+ else
54
+ if aws ecr describe-images --repository-name "$ECR_REPOSITORY" --region "$REGION" \
55
+ --image-ids "imageTag=${TAG}" > /dev/null 2>&1; then
56
+ echo "tag ${TAG} is already in ECR; a tag is never reused. Run again." >&2
57
+ exit 1
58
+ fi
59
+ echo "==> tagging ${LOCAL_IMAGE} as ${IMAGE_URI}"
60
+ docker tag "$LOCAL_IMAGE" "$IMAGE_URI"
61
+
62
+ # A private docker config with no credential helper: a helper that blocks on a keychain prompt
63
+ # hangs forever in any unattended shell.
64
+ echo "==> ecr login"
65
+ DOCKER_CONFIG_DIR="${SCRATCH_DIR}/docker-config"
66
+ mkdir -p "$DOCKER_CONFIG_DIR"
67
+ echo '{}' > "${DOCKER_CONFIG_DIR}/config.json"
68
+ aws ecr get-login-password --region "$REGION" \
69
+ | docker --config "$DOCKER_CONFIG_DIR" login --username AWS --password-stdin "$ECR_HOST"
70
+
71
+ echo "==> pushing ${IMAGE_URI}"
72
+ docker --config "$DOCKER_CONFIG_DIR" push "$IMAGE_URI"
73
+ fi
74
+
75
+ @@ROLL_STEPS@@
76
+
77
+ # The deploy itself is done. End with the smoke, which reports separately.
78
+ echo "==> deploy succeeded"
79
+ bash ./smoke-after-deploy.sh
@@ -0,0 +1,77 @@
1
+ echo "==> setting the stack's ${CFN_PARAM_KEY} to ${TAG} (registers the new task definition revision)"
2
+ stack_parameters() {
3
+ aws cloudformation describe-stacks --stack-name "$IMAGE_STACK" --region "$REGION" \
4
+ --query 'Stacks[0].Parameters[].{k:ParameterKey,v:ParameterValue}' --output json
5
+ }
6
+ stack_parameters > "${SCRATCH_DIR}/params-before.json"
7
+ ALL_PARAM_KEYS="$(jq -r '.[].k' "${SCRATCH_DIR}/params-before.json")"
8
+ CFN_ARGS=()
9
+ found=0
10
+ for key in $ALL_PARAM_KEYS; do
11
+ if [ "$key" = "$CFN_PARAM_KEY" ]; then
12
+ CFN_ARGS+=("ParameterKey=${key},ParameterValue=${TAG}")
13
+ found=1
14
+ else
15
+ CFN_ARGS+=("ParameterKey=${key},UsePreviousValue=true")
16
+ fi
17
+ done
18
+ if [ "$found" != "1" ]; then
19
+ echo "stack ${IMAGE_STACK} has no parameter named ${CFN_PARAM_KEY}; the image was pushed but nothing was rolled" >&2
20
+ exit 1
21
+ fi
22
+
23
+ if UPDATE_OUT="$(aws cloudformation update-stack --stack-name "$IMAGE_STACK" --region "$REGION" \
24
+ --use-previous-template --capabilities CAPABILITY_IAM --parameters "${CFN_ARGS[@]}" \
25
+ --query 'StackId' --output text 2>&1)"; then
26
+ echo "==> waiting for the CloudFormation stack update to settle"
27
+ started=$SECONDS
28
+ while true; do
29
+ stack_status="$(aws cloudformation describe-stacks --stack-name "$IMAGE_STACK" --region "$REGION" \
30
+ --query 'Stacks[0].StackStatus' --output text)"
31
+ case "$stack_status" in
32
+ UPDATE_COMPLETE) echo " -> ${stack_status}"; break ;;
33
+ *ROLLBACK*|*FAILED*)
34
+ echo " -> ${stack_status}: the stack update failed, investigate before rolling anything" >&2
35
+ exit 1 ;;
36
+ esac
37
+ if timed_out "$started"; then
38
+ echo "==> the stack update did not settle within ${SETTLE_TIMEOUT_SECS}s (last: ${stack_status})" >&2
39
+ exit 1
40
+ fi
41
+ sleep 10
42
+ done
43
+ elif echo "$UPDATE_OUT" | grep -q "No updates are to be performed"; then
44
+ echo " -> the stack already has ${TAG}; nothing to update"
45
+ else
46
+ echo "$UPDATE_OUT" >&2
47
+ exit 1
48
+ fi
49
+
50
+ echo "==> checking that only ${CFN_PARAM_KEY} changed"
51
+ stack_parameters > "${SCRATCH_DIR}/params-after.json"
52
+ CHANGED="$(jq -rn --slurpfile before "${SCRATCH_DIR}/params-before.json" --slurpfile after "${SCRATCH_DIR}/params-after.json" '
53
+ ($before[0] | map({(.k): .v}) | add // {}) as $b
54
+ | ($after[0] | map({(.k): .v}) | add // {}) as $a
55
+ | [($b + $a) | keys[] | select($b[.] != $a[.])] | join(" ")')"
56
+ NOW="$(jq -r --arg k "$CFN_PARAM_KEY" '.[] | select(.k == $k) | .v' "${SCRATCH_DIR}/params-after.json")"
57
+ if [ "$NOW" != "$TAG" ] || { [ -n "$CHANGED" ] && [ "$CHANGED" != "$CFN_PARAM_KEY" ]; }; then
58
+ echo "expected only ${CFN_PARAM_KEY} to change to ${TAG}; it is ${NOW} and changed: ${CHANGED:-nothing}" >&2
59
+ exit 1
60
+ fi
61
+ echo " -> ${CFN_PARAM_KEY} is ${TAG}; no other parameter changed"
62
+
63
+ # The newest active revision of the family is what the stack just registered. Roll it only if it
64
+ # really carries the image pushed above: a stack update that registered something else would
65
+ # otherwise roll an old image and still look like a deploy.
66
+ TD="$(aws ecs describe-task-definition --task-definition "$FAMILY" --region "$REGION" \
67
+ --query 'taskDefinition.[family,revision]' --output text | tr '\t' ':')"
68
+ RUNNING_IMAGE="$(aws ecs describe-task-definition --task-definition "$TD" --region "$REGION" \
69
+ --query "taskDefinition.containerDefinitions[?name=='${SERVICE_NAME}'].image | [0]" --output text)"
70
+ if [ "$RUNNING_IMAGE" != "$IMAGE_URI" ]; then
71
+ echo "task definition ${TD} has ${RUNNING_IMAGE}, not ${IMAGE_URI}; refusing to roll" >&2
72
+ exit 1
73
+ fi
74
+ echo "==> task definition ${TD} carries ${SERVICE_NAME}=${TAG}"
75
+
76
+ bash ./deploy-box.sh "$TD"
77
+ export TASKDEF="$TD"
@@ -0,0 +1,12 @@
1
+ # The active task definition names the image repository of every container, so the push goes where
2
+ # the box already pulls from.
3
+ active_image() {
4
+ aws ecs describe-task-definition --task-definition "$FAMILY" --region "$REGION" \
5
+ --query "taskDefinition.containerDefinitions[?name=='${SERVICE_NAME}'].image | [0]" --output text
6
+ }
7
+ CURRENT_IMAGE="$(active_image)"
8
+ case "$CURRENT_IMAGE" in
9
+ None|"") echo "task definition family ${FAMILY} has no container named ${SERVICE_NAME}" >&2; exit 1 ;;
10
+ esac
11
+ ECR_REPOSITORY="${CURRENT_IMAGE#*/}"
12
+ ECR_REPOSITORY="${ECR_REPOSITORY%%[:@]*}"
@@ -0,0 +1,24 @@
1
+ # GENERATED by hecks deploy project -- re-run it to refresh this file rather than hand-editing.
2
+ # Included by the Makefile beside it; settings come from the domain's deployed_to("AwsBox") block
3
+ # (hosting_scripts true).
4
+
5
+ HECKS ?= hecks
6
+ SERVICE ?= @@SERVICE@@
7
+ URL ?= @@URL@@
8
+
9
+ .PHONY: deploy-service smoke-after-deploy check-era
10
+
11
+ # Pushes one container's already-built local image under a fresh tag and rolls it onto the box.
12
+ # Build the image first; see deploy-service.sh.
13
+ deploy-service:
14
+ bash ./deploy-service.sh $(SERVICE)
15
+
16
+ # Waits for the last roll to settle, then dispatches and follows the smoke.
17
+ # SKIP_POST_DEPLOY_SMOKE=1 opts out, DRY_RUN=1 dispatches nothing.
18
+ smoke-after-deploy:
19
+ bash ./smoke-after-deploy.sh
20
+
21
+ # Compares the era a live host reports at GET /version with expected-era.
22
+ check-era:
23
+ @test -n "$(URL)" || { echo "usage: make check-era URL=https://host.example"; exit 1; }
24
+ HECKS_ENVIRONMENT=memory $(HECKS) host.check_era host="$(URL)" expected=expected-era --wait
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env bash
2
+ # GENERATED by hecks deploy project -- re-run it to refresh this file rather than hand-editing.
3
+ # Settings come from the domain's deployed_to("AwsBox") block (hosting_scripts true).
4
+ #
5
+ # Runs the production smoke once a roll of @@STACK@@'s app box has settled, so a deploy ends with a real
6
+ # check of the live site instead of waiting for the next scheduled run. Called at the end of
7
+ # deploy-service.sh and of `make deploy`; also runnable by hand:
8
+ #
9
+ # bash smoke-after-deploy.sh
10
+ #
11
+ # What it does, in order:
12
+ # 1. Waits for the roll to settle: two consecutive checks, one interval apart, must agree that
13
+ @@SETTLE_CHECKS@@
14
+ # Gives up after the timeout. The box is read through SSM, the way deploy-box.sh rolls it.
15
+ # 2. Dispatches the smoke workflow on the configured ref, finds the run that dispatch created (a
16
+ # workflow_dispatch run that did not exist before the dispatch, never a scheduled one), waits for it,
17
+ # and prints its conclusion, the failed check names and its URL.
18
+ #
19
+ # It only ever READS AWS; the smoke itself runs on GitHub against the live site.
20
+ #
21
+ # Exit codes (chosen to stay clear of the ones a failed deploy step produces: 1 from bash, 2 from bash
22
+ # misuse, 254/255 from the aws CLI):
23
+ # 0 the smoke passed, was skipped on purpose (SKIP_POST_DEPLOY_SMOKE=1), or was left running
24
+ # (SMOKE_ASYNC=1)
25
+ # 20 the roll did not settle in time, or a stack ended in a failed state; the smoke was NOT run
26
+ # 21 `gh` is missing or not signed in, or no repository is configured; the smoke was NOT run (the
27
+ # manual command is printed)
28
+ # 22 the smoke ran and FAILED
29
+ # 23 the smoke could not be dispatched, found or finished; result unknown
30
+ #
31
+ # Environment:
32
+ # SKIP_POST_DEPLOY_SMOKE=1 skip everything and say so
33
+ # SMOKE_ASYNC=1 dispatch the smoke and return as soon as its run is found, printing the
34
+ # run URL, instead of waiting for it to finish (exit 0: the result is NOT
35
+ # known yet)
36
+ # DRY_RUN=1 wait for the roll as usual, then print what would be dispatched without
37
+ # dispatching anything
38
+ # SETTLE_TIMEOUT_SECS default 1200
39
+ # SETTLE_CHECK_INTERVAL_SECS default 5
40
+ # SMOKE_RUN_TIMEOUT_SECS default 1800, how long to wait for the run
41
+ # SSM_POLL_SECS default 2, how often to ask SSM whether a command on the box finished
42
+ # REPO GitHub repository (owner/name) holding the workflow
43
+ # WORKFLOW workflow file name
44
+ # SMOKE_REF ref to dispatch on
45
+ # TASKDEF the task definition the box should run (family or family:revision);
46
+ # default: the latest revision of the family. deploy-service.sh and
47
+ # `make deploy TASKDEF=...` pass the one they rolled.
48
+ set -uo pipefail
49
+
50
+ @@SETTINGS@@
51
+
52
+ SETTLE_TIMEOUT_SECS="${SETTLE_TIMEOUT_SECS:-1200}"
53
+ SETTLE_CHECK_INTERVAL_SECS="${SETTLE_CHECK_INTERVAL_SECS:-5}"
54
+ SMOKE_RUN_TIMEOUT_SECS="${SMOKE_RUN_TIMEOUT_SECS:-1800}"
55
+ SSM_POLL_SECS="${SSM_POLL_SECS:-2}"
56
+ MANUAL_COMMAND="bash smoke-after-deploy.sh"
57
+
58
+ if [ "${SKIP_POST_DEPLOY_SMOKE:-}" = "1" ]; then
59
+ echo "==> post-deploy smoke SKIPPED (SKIP_POST_DEPLOY_SMOKE=1)."
60
+ echo " Run it yourself when ready: ${MANUAL_COMMAND}"
61
+ exit 0
62
+ fi
63
+
64
+ if ! command -v gh >/dev/null 2>&1 || ! gh auth status >/dev/null 2>&1; then
65
+ echo "==> post-deploy smoke NOT RUN: the GitHub CLI (gh) is missing or not signed in." >&2
66
+ echo " Run it by hand: gh workflow run ${WORKFLOW} --ref ${SMOKE_REF}" >&2
67
+ echo " or, once gh works: ${MANUAL_COMMAND}" >&2
68
+ exit 21
69
+ fi
70
+
71
+ if [ -z "$REPO" ]; then
72
+ REPO="$(gh repo view --json nameWithOwner --jq .nameWithOwner 2>/dev/null || true)"
73
+ fi
74
+ if [ -z "$REPO" ]; then
75
+ echo "==> post-deploy smoke NOT RUN: no GitHub repository is configured. Set REPO=owner/name, or the" >&2
76
+ echo " smoke_repo setting, or run from a clone gh can read." >&2
77
+ exit 21
78
+ fi
79
+ MANUAL_DISPATCH="gh workflow run ${WORKFLOW} --repo ${REPO} --ref ${SMOKE_REF}"
80
+
81
+ # The status of a stack, or nothing when it cannot be read.
82
+ stack_status() {
83
+ aws cloudformation describe-stacks --stack-name "$1" --region "$REGION" \
84
+ --query 'Stacks[0].StackStatus' --output text 2>/dev/null
85
+ }
86
+
87
+ # Runs a script on the box over SSM and prints its output; fails when it does not succeed.
88
+ box_run() {
89
+ local payload command_id state
90
+ payload="$(jq -n --arg c "$1" '{commands: [$c]}')"
91
+ command_id="$(aws ssm send-command --instance-ids "$BOX_ID" --document-name AWS-RunShellScript \
92
+ --region "$REGION" --parameters "$payload" --query 'Command.CommandId' --output text 2>/dev/null)" || return 1
93
+ for _ in $(seq 1 60); do
94
+ sleep "$SSM_POLL_SECS"
95
+ state="$(aws ssm get-command-invocation --command-id "$command_id" --instance-id "$BOX_ID" \
96
+ --region "$REGION" --query Status --output text 2>/dev/null || echo Pending)"
97
+ [ "$state" != InProgress ] && [ "$state" != Pending ] && break
98
+ done
99
+ [ "$state" = Success ] || return 1
100
+ aws ssm get-command-invocation --command-id "$command_id" --instance-id "$BOX_ID" --region "$REGION" \
101
+ --query StandardOutputContent --output text
102
+ }
103
+
104
+ # Prints "ok <signature>" when the box looks settled, otherwise "wait <why>". The signature names
105
+ # every image the box runs, so two checks agree only when nothing changed between them.
106
+ box_state() {
107
+ local report compose ps name image wanted signature
108
+ report="$(box_run "cd ${BOX_DIR} && cat compose.json && echo ===PS=== && docker compose -f compose.json ps --format '{{.Service}} {{.Status}}'")" \
109
+ || { echo "wait could not read the box"; return; }
110
+ compose="${report%%===PS===*}"
111
+ ps="${report#*===PS===}"
112
+ for name in $EXPECTED_SERVICES; do
113
+ printf '%s\n' "$ps" | grep -E "^${name} Up " | grep -qvE " Up (Less than a second|[0-4] seconds?)|Restarting" \
114
+ || { echo "wait ${name} is not up and settled"; return; }
115
+ done
116
+ signature=""
117
+ for name in $EXPECTED_SERVICES; do
118
+ image="$(printf '%s' "$compose" | jq -r --arg n "$name" '.services[$n].image // empty' 2>/dev/null)"
119
+ [ "$name" = caddy ] || [ -n "$image" ] || { echo "wait ${name} is not in the box's Compose file"; return; }
120
+ if [ -n "$TASK_DEFINITION" ] && [ "$name" != caddy ]; then
121
+ wanted="$(aws ecs describe-task-definition --task-definition "$TASK_DEFINITION" --region "$REGION" \
122
+ --query "taskDefinition.containerDefinitions[?name=='${name}'].image | [0]" --output text 2>/dev/null)"
123
+ [ "$image" = "$wanted" ] || { echo "wait ${name} runs ${image}, ${TASK_DEFINITION} has ${wanted}"; return; }
124
+ fi
125
+ signature="${signature}${name}=${image} "
126
+ done
127
+ echo "ok ${signature}"
128
+ }
129
+
130
+ wait_for_settled_roll() {
131
+ echo "==> waiting for the roll to settle (box ${BOX_STACK}@@IMAGE_STACK_WAIT@@; timeout ${SETTLE_TIMEOUT_SECS}s)"
132
+ local start=$SECONDS previous="" status stacks state signature why s
133
+ BOX_ID="$(aws cloudformation describe-stacks --stack-name "$BOX_STACK" --region "$REGION" \
134
+ --query "Stacks[0].Outputs[?OutputKey=='InstanceId'].OutputValue" --output text 2>/dev/null)"
135
+ if [ -z "$BOX_ID" ] || [ "$BOX_ID" = None ]; then
136
+ echo "==> no instance in stack ${BOX_STACK}. The smoke was NOT run." >&2
137
+ return 20
138
+ fi
139
+ while true; do
140
+ stacks=""
141
+ for s in $WATCHED_STACKS; do
142
+ status="$(stack_status "$s")"
143
+ case "$status" in
144
+ *ROLLBACK*|*FAILED*)
145
+ echo "==> roll did not settle: stack ${s} is ${status}. The smoke was NOT run." >&2
146
+ return 20 ;;
147
+ esac
148
+ stacks="${stacks}${s}=${status:-unreadable} "
149
+ done
150
+ if printf '%s' "$stacks" | tr ' ' '\n' | grep -v '^$' | grep -qvE '=(CREATE|UPDATE)_COMPLETE$'; then
151
+ state="wait ${stacks}"
152
+ else
153
+ state="$(box_state)"
154
+ fi
155
+ if [ "${state%% *}" = "ok" ]; then
156
+ signature="${stacks}${state#ok }"
157
+ if [ "$signature" = "$previous" ]; then
158
+ echo " -> settled on ${state#ok } (two consecutive checks agree)"
159
+ return 0
160
+ fi
161
+ echo " -> looks settled (${state#ok }); confirming in ${SETTLE_CHECK_INTERVAL_SECS}s"
162
+ previous="$signature"
163
+ else
164
+ why="${state#wait }"
165
+ echo " -> not yet: ${why}"
166
+ previous=""
167
+ fi
168
+ if [ $((SECONDS - start + SETTLE_CHECK_INTERVAL_SECS)) -gt "$SETTLE_TIMEOUT_SECS" ]; then
169
+ echo "==> roll did not settle within ${SETTLE_TIMEOUT_SECS}s (last: ${state#wait }). The smoke was NOT run." >&2
170
+ return 20
171
+ fi
172
+ sleep "$SETTLE_CHECK_INTERVAL_SECS"
173
+ done
174
+ }
175
+
176
+ # Ids of the workflow_dispatch runs that exist now, one per line.
177
+ dispatch_run_ids() {
178
+ gh run list --repo "$REPO" --workflow "$WORKFLOW" --event workflow_dispatch --limit 50 \
179
+ --json databaseId --jq '.[].databaseId' 2>/dev/null
180
+ }
181
+
182
+ print_failed_checks() {
183
+ local run_id="$1" log names
184
+ log="$(gh run view "$run_id" --repo "$REPO" --log 2>/dev/null || true)"
185
+ # Log lines are "<job>\t<step>\t<timestamp> <text>"; a smoke that prints " <check>... FAILED: <why>"
186
+ # for each failing check is listed by name.
187
+ names="$(printf '%s\n' "$log" | awk -F'\t' 'NF >= 3 { print $3 }' | sed -E 's/^[0-9T:.-]+Z //' \
188
+ | grep -E '\.\.\. FAILED:' | sed -E 's/^ +//' || true)"
189
+ if [ -n "$names" ]; then
190
+ echo " failed checks:"
191
+ printf '%s\n' "$names" | sed 's/^/ - /'
192
+ else
193
+ echo " (no failed check names in the log; the run may have failed before the checks. Last log lines:)"
194
+ printf '%s\n' "$log" | tail -n 15 | sed 's/^/ /'
195
+ fi
196
+ }
197
+
198
+ run_smoke() {
199
+ local before run_id="" waited=0 url conclusion status
200
+ if ! before="$(dispatch_run_ids)"; then
201
+ 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
202
+ return 23
203
+ fi
204
+
205
+ echo "==> dispatching the production smoke: ${MANUAL_DISPATCH}"
206
+ if ! gh workflow run "$WORKFLOW" --repo "$REPO" --ref "$SMOKE_REF"; then
207
+ echo "==> could not dispatch the smoke workflow. Deploy succeeded; run it by hand: ${MANUAL_DISPATCH}" >&2
208
+ return 23
209
+ fi
210
+
211
+ # The dispatch call does not return the run, so find the workflow_dispatch run that was not there
212
+ # before it (scheduled runs are never candidates).
213
+ while [ -z "$run_id" ] && [ "$waited" -lt 180 ]; do
214
+ sleep 2
215
+ waited=$((waited + 2))
216
+ run_id="$(comm -13 <(printf '%s\n' "$before" | sort) <(dispatch_run_ids | sort) | sort -n | tail -n 1)"
217
+ done
218
+ if [ -z "$run_id" ]; then
219
+ echo "==> dispatched, but the new run never appeared. Deploy succeeded; check https://github.com/${REPO}/actions/workflows/${WORKFLOW}" >&2
220
+ return 23
221
+ fi
222
+ url="https://github.com/${REPO}/actions/runs/${run_id}"
223
+ echo "==> smoke run ${run_id}: ${url}"
224
+ if [ "${SMOKE_ASYNC:-}" = "1" ]; then
225
+ echo "==> deploy succeeded; the smoke is still running (SMOKE_ASYNC=1), so its result is NOT known yet."
226
+ echo " Watch it: gh run watch ${run_id} --repo ${REPO} or open ${url}"
227
+ return 0
228
+ fi
229
+
230
+ waited=0
231
+ while true; do
232
+ status="$(gh run view "$run_id" --repo "$REPO" --json status --jq .status 2>/dev/null || true)"
233
+ [ "$status" = "completed" ] && break
234
+ if [ "$waited" -ge "$SMOKE_RUN_TIMEOUT_SECS" ]; then
235
+ echo "==> deploy succeeded, but the smoke run did not finish within ${SMOKE_RUN_TIMEOUT_SECS}s: see ${url}" >&2
236
+ return 23
237
+ fi
238
+ sleep 5
239
+ waited=$((waited + 5))
240
+ done
241
+
242
+ conclusion="$(gh run view "$run_id" --repo "$REPO" --json conclusion --jq .conclusion || true)"
243
+ echo "==> smoke conclusion: ${conclusion} ${url}"
244
+ if [ "$conclusion" = "success" ]; then
245
+ echo "==> deploy succeeded, post-deploy smoke passed."
246
+ return 0
247
+ fi
248
+ print_failed_checks "$run_id"
249
+ echo "==> deploy succeeded, post-deploy smoke FAILED: see ${url}" >&2
250
+ return 22
251
+ }
252
+
253
+ rc=0
254
+ wait_for_settled_roll || rc=$?
255
+ [ "$rc" -eq 0 ] || exit "$rc"
256
+
257
+ if [ "${DRY_RUN:-}" = "1" ]; then
258
+ echo "==> DRY_RUN=1: would dispatch: ${MANUAL_DISPATCH}"
259
+ newest="$(dispatch_run_ids | sort -n | tail -n 1 || true)"
260
+ if [ -n "$newest" ]; then
261
+ echo " then pick the workflow_dispatch run that appears after it; the newest existing one is"
262
+ echo " https://github.com/${REPO}/actions/runs/${newest} (not the one it would wait for)"
263
+ else
264
+ echo " (no workflow_dispatch runs exist yet)"
265
+ fi
266
+ echo " then wait for it and report its conclusion and failed checks. Nothing dispatched."
267
+ exit 0
268
+ fi
269
+
270
+ run_smoke || rc=$?
271
+ exit "$rc"
@@ -1,6 +1,7 @@
1
1
  require "json"
2
2
  require_relative "../../projector"
3
3
  require_relative "box/settings"
4
+ require_relative "box/hosting"
4
5
 
5
6
  module Hecks
6
7
  module Projections
@@ -94,7 +95,7 @@ module Hecks
94
95
  "fetch-secrets.sh" => File.read(File.join(TEMPLATE_DIR, "fetch-secrets.sh")),
95
96
  "deploy-box.sh" => deploy_box_sh(plan, region),
96
97
  "Makefile" => makefile(plan)
97
- }.merge(migration_files(plan))
98
+ }.merge(migration_files(plan)).then { |files| Hosting.extend_files(files, plan: plan, region: region) }
98
99
  end
99
100
 
100
101
  # The tooling that moves a project's data from its old database into the RDS instance, for
@@ -546,7 +547,7 @@ module Hecks
546
547
  # @param plan [Settings::Plan] the resolved settings
547
548
  # @return [String] a Makefile whose targets create the two stacks and roll the box
548
549
  def makefile(plan)
549
- <<~MAKE
550
+ base = <<~MAKE
550
551
  # #{plan.infra_name}: one app box and one RDS instance.
551
552
  # make stacks VPC=vpc-... PRIVATE_SUBNETS=subnet-a,subnet-b PUBLIC_SUBNET=subnet-c [REHEARSAL=true]
552
553
  # make deploy #{plan.task_definition ? '[TASKDEF=family:revision]' : '[TAGS="web=20260101 worker=20260101"]'}
@@ -570,6 +571,18 @@ module Hecks
570
571
  deploy:
571
572
  \tbash ./deploy-box.sh $(#{plan.task_definition ? 'TASKDEF' : 'TAGS'})
572
573
  MAKE
574
+ "#{base}#{hosting_makefile_tail(plan)}"
575
+ end
576
+
577
+ # With hosting scripts, a deploy ends with the post-deploy smoke and the Makefile includes
578
+ # the fragment that adds the hosting targets.
579
+ #
580
+ # @param plan [Settings::Plan] the resolved settings
581
+ # @return [String] the Makefile lines that follow the `deploy` recipe, or nothing
582
+ def hosting_makefile_tail(plan)
583
+ return "" unless plan.hosting
584
+
585
+ "\t#{'TASKDEF="$(TASKDEF)" ' if plan.task_definition}bash ./smoke-after-deploy.sh\n\ninclude hosting.mk\n"
573
586
  end
574
587
 
575
588
  # Fills `@@NAME@@` markers. A marker alone on its line is replaced together with the line,
@@ -100,7 +100,7 @@ module Hecks
100
100
  deployed_to("AwsFargate") do
101
101
  ...
102
102
  database "Shared"
103
- owner "Embryonaut"
103
+ owner "Core"
104
104
  end
105
105
 
106
106
  naming the already-deployed domain whose Postgres instance this one borrows.
@@ -327,8 +327,8 @@ module Hecks
327
327
  RetentionInDays: 30
328
328
 
329
329
  # Always minted — HECKS_SERVE_MODE always runs web.rs, which
330
- # panics on an empty SESSION_SECRET (found live: hecksagain-pizzas
331
- # 502 after the Aurora cutover). rust/host fetches this at cold
330
+ # panics on an empty SESSION_SECRET (seen live as a 502
331
+ # after a database cutover). rust/host fetches this at cold
332
332
  # start (secrets.rs), never a plain env var.
333
333
  #{session_secret_id}:
334
334
  Type: AWS::SecretsManager::Secret
@@ -147,7 +147,7 @@ module Hecks
147
147
  deployed_to("AwsLambda") do
148
148
  ...
149
149
  database "Shared"
150
- owner "Embryonaut"
150
+ owner "Core"
151
151
  end
152
152
 
153
153
  naming the already-deployed domain whose Postgres instance this one borrows.
@@ -273,7 +273,7 @@ module Hecks
273
273
  # GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET/GOOGLE_REDIRECT_URI
274
274
  # deliberately NOT set here — #{domain}'s own .env.local has
275
275
  # no real GOOGLE_CLIENT_ID yet, matching Fly's own current
276
- # reality (fly secrets list -a embryonaut-founder-app: only
276
+ # reality (its secrets list holds only
277
277
  # DATABASE_URL and SESSION_SECRET) that nobody has configured
278
278
  # OAuth for this app at all. Drop real credentials into
279
279
  # .env.local and regenerate to wire this up.
@@ -410,7 +410,7 @@ module Hecks
410
410
  # `stack_name` above exactly — one name, two places it's read
411
411
  # from, never out of sync since both derive from `infra_name`.
412
412
  FunctionName: #{stack_name}
413
- # EXPLICIT, not omitted -- `sam build EmbryonautFunction` and
413
+ # EXPLICIT, not omitted -- `sam build WebFunction` and
414
414
  # `sam build --use-container WebFunction` run as TWO SEPARATE
415
415
  # single-resource invocations (deploy:'s own comment on why:
416
416
  # cargo-lambda cross-compiles on this host directly, pg's
@@ -434,7 +434,7 @@ module Hecks
434
434
  # "." NOT "#{logical_id}" -- SAM's Makefile build workflow
435
435
  # requires the Makefile to live INSIDE CodeUri itself
436
436
  # (confirmed live: "Makefile not found at
437
- # .../EmbryonautFunction/Makefile" when CodeUri named a
437
+ # .../WebFunction/Makefile" when CodeUri named a
438
438
  # directory of its own). This project's one Makefile lives at
439
439
  # the deploy directory's own root, alongside this template --
440
440
  # "." is where it actually is.
@@ -568,7 +568,7 @@ module Hecks
568
568
  "SubnetIds: [!Ref #{db_id}SubnetA, !Ref #{db_id}SubnetB]\n SecurityGroupIds: [!Ref #{logical_id}SecurityGroup]"
569
569
  end}
570
570
  # AWS_IAM by default — this dispatches real domain commands
571
- # (cap-table/governance-shaped data, for Embryonaut's own use);
571
+ # (cap-table/governance-shaped data);
572
572
  # an unauthenticated public URL is the wrong default for that,
573
573
  # even though it's the simpler one to demo with. NONE only when
574
574
  # `web "Rust"` (rust/host/src/web.rs) makes this the public web
@@ -586,7 +586,7 @@ module Hecks
586
586
  # THE WEB APP'S OWN LAMBDA — opt-in, see `web_handler_present` above.
587
587
  # VPC-attached for the SAME reason #{logical_id} is (Member's own
588
588
  # persistence needs the private RDS instance directly, permanently —
589
- # embryonaut.hecksagon's own comment on why), sharing
589
+ # the hecksagon's own comment on why), sharing
590
590
  # #{logical_id}SecurityGroup rather than minting a second one, since
591
591
  # both need the identical egress-to-DB rule. That VPC attachment
592
592
  # ALSO cuts off the public internet by default (this stack's own "NO
@@ -674,7 +674,7 @@ module Hecks
674
674
  # without reworking that whole from-source pg build for a newer
675
675
  # image is a real, separate undertaking (untested container image
676
676
  # availability, a different vendor/bundle native-extension path)
677
- # out of scope here. Matches Embryonaut's own live, currently-
677
+ # out of scope here. Matches the live, currently-
678
678
  # deployed choice exactly, so both consumers of this one shared
679
679
  # WebFunction code path stay on the same, PROVEN runtime.
680
680
  Properties:
@@ -763,7 +763,7 @@ module Hecks
763
763
  # documents. But THIS function's consumer (`lambda_handler.rb`)
764
764
  # is not a file this generator writes or this repo carries --
765
765
  # it lives in the deploying app's own domain directory (real,
766
- # live example: Embryonaut's own Sinatra app), outside this
766
+ # live example: a Sinatra app), outside this
767
767
  # codebase entirely. Redeploying a domain with an existing
768
768
  # `lambda_handler.rb` BEFORE that handler is updated to fetch
769
769
  # these itself (`aws-sdk-secretsmanager`, not yet in this
@@ -48,7 +48,7 @@ module Hecks
48
48
  # Fields emitted by `to_h`'s own merge rather than `emits_ir`, for constructs
49
49
  # whose shape is not fixed.
50
50
  DYNAMIC_TAIL = {
51
- "Query" => %i[options returns],
51
+ "Query" => %i[options returns needs],
52
52
  "ReadModel" => %i[options group_by aggregate_heads count median_field sum_field avg_field
53
53
  min_field max_field percentile_field percentile_at any_field all_field]
54
54
  }.freeze