hecks 3.2.1 → 3.4.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/assembly/contracts.rb +2 -1
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +2 -39
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +21 -23
- data/lib/hecks/bluebook/dsl/need_word.rb +50 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +6 -1
- data/lib/hecks/bluebook/query.rb +8 -5
- data/lib/hecks/cli/interview_draft.rb +88 -33
- data/lib/hecks/cli/interview_session.rb +77 -26
- data/lib/hecks/cli/interview_shape.rb +198 -0
- data/lib/hecks/corpus.rb +3 -5
- data/lib/hecks/doors/cli_runner.rb +14 -1
- data/lib/hecks/doors/launcher_options.rb +13 -3
- data/lib/hecks/doors/mcp_door_scope.rb +2 -1
- data/lib/hecks/doors.rb +0 -17
- data/lib/hecks/embryonaut_bluebook/manifest.rb +117 -0
- data/lib/hecks/embryonaut_bluebook/registry.rb +209 -0
- data/lib/hecks/embryonaut_bluebook/vendor.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +2 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +1 -4
- data/lib/hecks/fuzzing/qa_settings.rb +15 -34
- data/lib/hecks/fuzzing/target_capabilities.rb +1 -1
- data/lib/hecks/gate/stages.yml +113 -8
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +10 -1
- data/lib/hecks/hecks/adapters/first_admin.rb +108 -0
- data/lib/hecks/hecks/adapters/git.rb +59 -0
- data/lib/hecks/hecks/adapters/in_process_access.rb +25 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +2 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +18 -3
- data/lib/hecks/hecks/adapters/site_toolchain.rb +67 -0
- data/lib/hecks/hecks/codebase.behaviors +6 -0
- data/lib/hecks/hecks/codebase.bluebook +21 -0
- data/lib/hecks/hecks/custodian.behaviors +6 -0
- data/lib/hecks/hecks/custodian.bluebook +217 -0
- data/lib/hecks/hecks/hecks.hecksagon +24 -0
- data/lib/hecks/hecks/hecks.world +8 -3
- data/lib/hecks/hecks/site.world +5 -2
- data/lib/hecks/language/bluebook/query.bluebook +25 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +213 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +0 -5
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/projections/deploy/box/hosting.rb +170 -0
- data/lib/hecks/projections/deploy/box/hosting_settings.rb +123 -0
- data/lib/hecks/projections/deploy/box/settings.rb +17 -4
- data/lib/hecks/projections/deploy/box/templates/deploy-service.services-roll.part +33 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.sh.tmpl +79 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef-roll.part +77 -0
- data/lib/hecks/projections/deploy/box/templates/deploy-service.taskdef.part +12 -0
- data/lib/hecks/projections/deploy/box/templates/hosting.mk.tmpl +24 -0
- data/lib/hecks/projections/deploy/box/templates/smoke-after-deploy.sh.tmpl +271 -0
- data/lib/hecks/projections/deploy/box.rb +15 -2
- data/lib/hecks/projections/deploy/fargate.rb +3 -3
- data/lib/hecks/projections/deploy/lambda.rb +8 -8
- data/lib/hecks/projections/model/deviations.rb +1 -1
- data/lib/hecks/projections/site/live_edge.rb +162 -0
- data/lib/hecks/quality_control/adapters/git_pr.rb +0 -14
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +1 -1
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +32 -16
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +2 -2
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +1 -2
- data/lib/hecks/quality_control/quality_control.bluebook +76 -124
- data/lib/hecks/runtime/interpreting.rb +3 -22
- data/lib/hecks/runtime/loader.rb +6 -7
- data/lib/hecks/runtime/needs.rb +48 -0
- data/lib/hecks/runtime/query_interpreter.rb +3 -0
- data/lib/hecks/runtime/registry/verification.rb +3 -3
- data/lib/hecks/runtime.rb +6 -11
- data/lib/hecks/rust_build/host.rb +147 -0
- data/lib/hecks/rust_build/kernel_input.rb +40 -18
- data/lib/hecks/rust_build/project_rust/codegen_run.rb +0 -18
- data/lib/hecks/rust_build/project_rust.rb +3 -10
- data/lib/hecks/rust_build.rb +1 -0
- data/lib/hecks/site/bluebook/site.bluebook +53 -1
- data/lib/hecks/site/bluebook/site.ports.hecksagon +6 -0
- data/lib/hecks/sme/bluebook/sme.bluebook +128 -1
- data/lib/hecks/tools/gate.rb +1 -1
- data/lib/hecks/tools/rust_comment_style.rb +1 -3
- data/lib/hecks/tools/tools_doc.rb +204 -0
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +151 -0
- data/lib/hecks.rb +6 -12
- data/qa/settings.yml +1 -2
- data/rust/build/src/lineage_pass.rs +30 -15
- data/rust/build/src/main.rs +4 -2
- data/rust/build/src/pipeline.rs +8 -19
- data/rust/codegen/src/bridging.rs +7 -7
- data/rust/codegen/src/json.rs +214 -23
- data/rust/codegen/src/literal.rs +6 -3
- data/rust/codegen/src/main.rs +20 -9
- data/rust/codegen/src/naming.rs +4 -2
- data/rust/codegen/src/optional_pass.rs +226 -0
- data/rust/{build → codegen}/src/sidecars.rs +9 -5
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +1 -1
- data/rust/host/src/dispatch.rs +5 -0
- data/rust/host/src/needs.rs +99 -2
- data/rust/host/src/resend.rs +4 -4
- data/rust/host/src/ui_schema.rs +1 -1
- data/rust/parser/src/emit.rs +12 -0
- data/rust/parser/src/ir.rs +2 -0
- data/rust/parser/src/keywords.rs +2 -4
- data/rust/parser/src/lex.rs +1 -1
- data/rust/parser/src/main.rs +1 -0
- data/rust/parser/src/parse/command.rs +2 -44
- data/rust/parser/src/parse/hecksagon.rs +15 -24
- data/rust/parser/src/parse/mod.rs +1 -0
- data/rust/parser/src/parse/needs.rs +60 -0
- data/rust/parser/src/parse/query.rs +3 -0
- data/rust/src/kernel/cli.rs +16 -6
- data/rust/src/kernel/needs.rs +104 -9
- metadata +22 -9
- data/lib/hecks/facade.rb +0 -31
- data/lib/hecks/rust_build/append_optionals.rb +0 -79
- data/lib/hecks/rust_build/rust_literal.rb +0 -33
- data/lib/hecks/three_zero/forms.yml +0 -127
- data/lib/hecks/three_zero.rb +0 -12
- data/rust/build/src/json.rs +0 -421
- data/rust/build/src/optional_pass.rs +0 -150
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative "hosting_settings"
|
|
2
|
+
|
|
1
3
|
module Hecks
|
|
2
4
|
module Projections
|
|
3
5
|
module Deploy
|
|
@@ -47,7 +49,9 @@ module Hecks
|
|
|
47
49
|
# @!attribute [r] env [Hash{String => String}] plain environment variables
|
|
48
50
|
# @!attribute [r] secrets [Hash{String => String}] environment variable => secret name,
|
|
49
51
|
# resolved on the box at deploy time and never written into a template
|
|
50
|
-
|
|
52
|
+
# @!attribute [r] tag_parameter [String] the stack parameter that holds this container's
|
|
53
|
+
# image tag, which a hosting `deploy-service.sh` sets
|
|
54
|
+
Container = Struct.new(:name, :repository, :port, :env, :secrets, :tag_parameter, keyword_init: true)
|
|
51
55
|
|
|
52
56
|
# A set of URL paths one container serves.
|
|
53
57
|
#
|
|
@@ -82,7 +86,7 @@ module Hecks
|
|
|
82
86
|
:storage_gb, :backup_days, :snapshots_keep, :database_name, :engine_version,
|
|
83
87
|
:containers, :routes, :default_container, :origin_header, :origin_secret,
|
|
84
88
|
:secret_prefixes, :writable_secrets, :origin_env, :tunnel, :tunnel_service, :proxy_image, :task_definition,
|
|
85
|
-
:migration, :s3_buckets, keyword_init: true
|
|
89
|
+
:migration, :s3_buckets, :hosting, keyword_init: true
|
|
86
90
|
) do
|
|
87
91
|
# @return [String] the CloudFormation stack that holds the database
|
|
88
92
|
def rds_stack = "#{stack_prefix}-#{infra_name}-rds"
|
|
@@ -126,7 +130,8 @@ module Hecks
|
|
|
126
130
|
tunnel: tunnel, tunnel_service: tunnel_service,
|
|
127
131
|
proxy_image: check(:proxy_image, s.fetch(:proxy_image, PROXY_IMAGE), IMAGE),
|
|
128
132
|
task_definition: task_definition, migration: read_migration(s[:migration], database_name),
|
|
129
|
-
s3_buckets: read_s3_access(s.fetch(:s3_access, []))
|
|
133
|
+
s3_buckets: read_s3_access(s.fetch(:s3_access, [])),
|
|
134
|
+
hosting: HostingSettings.read(s, task_definition)
|
|
130
135
|
)
|
|
131
136
|
end
|
|
132
137
|
|
|
@@ -224,10 +229,18 @@ module Hecks
|
|
|
224
229
|
name: name, repository: check(:repository, spec.fetch(:repository, "#{infra_name}-#{name}"), REPOSITORY),
|
|
225
230
|
port: integer(:port, spec.fetch(:port) { raise ArgumentError, "containers: #{name} has no port" }, 1, 65_535),
|
|
226
231
|
env: string_map(:env, spec.fetch(:env, {}), ENV_KEY, ENV_VALUE),
|
|
227
|
-
secrets: string_map(:secrets, spec.fetch(:secrets, {}), ENV_KEY, SECRET_NAME)
|
|
232
|
+
secrets: string_map(:secrets, spec.fetch(:secrets, {}), ENV_KEY, SECRET_NAME),
|
|
233
|
+
tag_parameter: check(:tag_parameter, spec.fetch(:tag_parameter, default_tag_parameter(name)),
|
|
234
|
+
HostingSettings::PARAMETER)
|
|
228
235
|
)
|
|
229
236
|
end
|
|
230
237
|
|
|
238
|
+
# @param name [String] a container name such as `web-app`
|
|
239
|
+
# @return [String] the image-tag parameter a stack names it by, such as `WebAppImageTag`
|
|
240
|
+
def default_tag_parameter(name)
|
|
241
|
+
"#{name.split('-').map(&:capitalize).join}ImageTag"
|
|
242
|
+
end
|
|
243
|
+
|
|
231
244
|
def read_routes(list, containers)
|
|
232
245
|
raise ArgumentError, "routes: expected a list" unless list.is_a?(Array)
|
|
233
246
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# deploy-box.sh renders every container the roll does not name at "latest". Name the tag each other
|
|
2
|
+
# container runs now, read from the box's own Compose file, so rolling one service leaves the rest alone.
|
|
3
|
+
BOX_ID="$(aws cloudformation describe-stacks --stack-name "$BOX_STACK" --region "$REGION" \
|
|
4
|
+
--query "Stacks[0].Outputs[?OutputKey=='InstanceId'].OutputValue" --output text)"
|
|
5
|
+
[ -n "$BOX_ID" ] && [ "$BOX_ID" != None ] || { echo "no instance in stack ${BOX_STACK}" >&2; exit 1; }
|
|
6
|
+
|
|
7
|
+
echo "==> reading the tags the box runs now"
|
|
8
|
+
READ_JSON="$(jq -n --arg c "cat ${BOX_DIR}/compose.json 2>/dev/null || echo '{}'" '{commands: [$c]}')"
|
|
9
|
+
COMMAND_ID="$(aws ssm send-command --instance-ids "$BOX_ID" --document-name AWS-RunShellScript --region "$REGION" \
|
|
10
|
+
--parameters "$READ_JSON" --query 'Command.CommandId' --output text)"
|
|
11
|
+
for _ in $(seq 1 30); do
|
|
12
|
+
sleep 2
|
|
13
|
+
state="$(aws ssm get-command-invocation --command-id "$COMMAND_ID" --instance-id "$BOX_ID" --region "$REGION" \
|
|
14
|
+
--query Status --output text 2>/dev/null || echo Pending)"
|
|
15
|
+
[ "$state" != InProgress ] && [ "$state" != Pending ] && break
|
|
16
|
+
done
|
|
17
|
+
[ "$state" = Success ] || { echo "could not read the box's Compose file (${state}); nothing was rolled" >&2; exit 1; }
|
|
18
|
+
aws ssm get-command-invocation --command-id "$COMMAND_ID" --instance-id "$BOX_ID" --region "$REGION" \
|
|
19
|
+
--query StandardOutputContent --output text > "${SCRATCH_DIR}/compose.json"
|
|
20
|
+
|
|
21
|
+
TAGS=()
|
|
22
|
+
for name in $SERVICES; do
|
|
23
|
+
if [ "$name" = "$SERVICE_NAME" ]; then
|
|
24
|
+
TAGS+=("${name}=${TAG}")
|
|
25
|
+
continue
|
|
26
|
+
fi
|
|
27
|
+
image="$(jq -r --arg n "$name" '.services[$n].image // empty' "${SCRATCH_DIR}/compose.json")"
|
|
28
|
+
# A container the box does not run yet starts at its "latest" image.
|
|
29
|
+
if [ -n "$image" ]; then TAGS+=("${name}=${image##*:}"); fi
|
|
30
|
+
done
|
|
31
|
+
echo "==> rolling ${TAGS[*]}"
|
|
32
|
+
|
|
33
|
+
bash ./deploy-box.sh "${TAGS[@]}"
|
|
@@ -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 "
|
|
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 (
|
|
331
|
-
#
|
|
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
|