hecks 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
  3. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  4. data/lib/hecks/adapters/driven/prism.rb +1 -1
  5. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  6. data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
  7. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
  8. data/lib/hecks/bluebook/dsl/world_builder.rb +42 -3
  9. data/lib/hecks/bluebook/hexagon.rb +15 -7
  10. data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
  11. data/lib/hecks/bluebook/model_check.rb +1 -1
  12. data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
  13. data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
  14. data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
  15. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
  16. data/lib/hecks/embryonaut_bluebook.rb +29 -3
  17. data/lib/hecks/forms/value_object_shape.rb +1 -1
  18. data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
  19. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  20. data/lib/hecks/fuzzing/isolated_boot.rb +59 -5
  21. data/lib/hecks/language/world/world.bluebook +32 -2
  22. data/lib/hecks/ports/key_vault.rb +1 -1
  23. data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
  24. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +2 -1
  25. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
  26. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
  27. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +1 -1
  28. data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
  29. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
  30. data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
  31. data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
  32. data/lib/hecks/ports/persistence.rb +4 -0
  33. data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
  34. data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
  35. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
  36. data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
  37. data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
  38. data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
  39. data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
  40. data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
  41. data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
  42. data/lib/hecks/projections/deploy/fargate.rb +187 -138
  43. data/lib/hecks/projections/deploy/lambda.rb +5 -5
  44. data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
  45. data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
  46. data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
  47. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
  48. data/lib/hecks/projections/deploy/preview/script.rb +40 -0
  49. data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
  50. data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
  51. data/lib/hecks/projections/deploy/preview/template.rb +247 -0
  52. data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
  53. data/lib/hecks/projections/deploy/preview.rb +103 -0
  54. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
  55. data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
  56. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
  57. data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
  58. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
  59. data/lib/hecks/projections/deploy/scripts.rb +171 -0
  60. data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
  61. data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
  62. data/lib/hecks/projections/deploy/smoke.rb +159 -0
  63. data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
  64. data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
  65. data/lib/hecks/projections/deploy/template_diff.rb +173 -0
  66. data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
  67. data/lib/hecks/projections.rb +2 -0
  68. data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
  69. data/lib/hecks/release/gem_pin.rb +112 -0
  70. data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
  71. data/lib/hecks/runtime/loader.rb +26 -2
  72. data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
  73. data/lib/hecks/runtime/registry/verification.rb +4 -2
  74. data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
  75. data/lib/hecks/runtime/registry.rb +14 -9
  76. data/lib/hecks/runtime/tenant_check.rb +1 -1
  77. data/lib/hecks/vendoring/git_environment.rb +35 -0
  78. data/lib/hecks/vendoring/git_source.rb +102 -0
  79. data/lib/hecks/vendoring.rb +102 -0
  80. data/lib/hecks/version.rb +1 -1
  81. data/lib/hecks.rb +2 -1
  82. metadata +45 -1
@@ -1,5 +1,9 @@
1
1
  require_relative "../../projector"
2
2
  require_relative "shared"
3
+ require_relative "scripts"
4
+ require_relative "preview"
5
+ require_relative "fargate/settings"
6
+ require_relative "fargate/assembly"
3
7
 
4
8
  module Hecks
5
9
  module Projections
@@ -33,9 +37,8 @@ module Hecks
33
37
  # never `sam deploy`, so there is no `samconfig.toml` here. A
34
38
  # `Dockerfile` packages `rust/host`'s own compiled binary — built by
35
39
  # the generated Makefile before `docker build` ever runs, the same
36
- # "build outside the container, ship the artifact" shape
37
- # `lifeadelics/domain/Dockerfile` already uses for a tebako-pressed
38
- # Ruby binary.
40
+ # "build outside the container, ship the artifact" shape a
41
+ # tebako-pressed Ruby binary's Dockerfile already uses.
39
42
  #
40
43
  # ## What this assumes, and does not build
41
44
  #
@@ -58,9 +61,56 @@ module Hecks
58
61
  # is ARM64, and the Makefile cross-compiles with
59
62
  # aarch64-unknown-linux-gnu (plus the GNU cross-linker on macOS).
60
63
  # A Shared-mode ALB sits in OwningPublicSubnetAId/BId — the private
61
- # pair is unreachable from the internet (found live,
62
- # lifeadelics-platform). SessionSecret is always minted: HECKS_SERVE_MODE
64
+ # pair is unreachable from the internet (found live on a
65
+ # shared-database stack). SessionSecret is always minted: HECKS_SERVE_MODE
63
66
  # always runs web.rs, which panics on an empty SESSION_SECRET.
67
+ #
68
+ # ## Settings beyond the baseline
69
+ #
70
+ # `region`, `cpu`, `memory`, `port`, `database`, `web`, `stack_name`, `stack_prefix`,
71
+ # `schema`, `desired_count`, `owner` and `owner_stack` are the baseline settings. A world
72
+ # that sets none of the keys below renders the single-container stack described above,
73
+ # unchanged. Each key is optional, checked before anything is rendered, and refused with its
74
+ # own name when malformed; the module named beside it holds the full shape.
75
+ #
76
+ # - `domain_container`: renames the domain container, its repository, image-tag parameter
77
+ # and health path (`Containers`).
78
+ # - `containers`: adds containers to the same task, each with a repository, port, health
79
+ # path, environment and secrets (`Containers`).
80
+ # - `default_container`: the container the listener forwards to when no route matches
81
+ # (`Containers`).
82
+ # - `routes`: sends path patterns to a container through listener rules, with priorities
83
+ # (`Containers`).
84
+ # - `logical_ids`: fixes the logical id of each resource so an existing stack's ids are
85
+ # reproduced (`Settings`).
86
+ # - `names`: fixes the AWS name of the cluster, log group, service, load balancer, task
87
+ # family, the load balancer's security group description, and the task role's database
88
+ # policy name (`Settings`).
89
+ # - `cdn`: aliases, certificate, behaviors and cache policies, an origin secret header,
90
+ # S3 origins and retention (`Cdn`).
91
+ # - `alerts`: a `SNS` topic, an email subscription, alarms and a synthetic check
92
+ # (`Monitoring`).
93
+ # - `buckets`, `generated_secrets` and `session_secret`: extra S3 buckets, generated
94
+ # secrets and the session secret's name (`Extras`).
95
+ # - `task_policies` and `execution_policies`: extra IAM policies on the task role and the
96
+ # execution role (`Extras`).
97
+ # - `parameters` and `outputs`: extra template parameters and outputs (`Extras`).
98
+ # - `execute_command`: enables `aws ecs execute-command` and grants the task role its SSM
99
+ # actions (`Settings`).
100
+ # - `health_check_grace_period`: seconds `ECS` ignores failing health checks after a task
101
+ # starts (`Settings`).
102
+ # - `deregistration_delay`: seconds a target group drains a target (`Settings`).
103
+ # - `desired_count_parameter`: makes the desired count a template parameter (`Settings`).
104
+ # - `domain_env`: extra environment for the domain container; a name the generator sets
105
+ # is replaced, and a nil value removes it (`Settings`).
106
+ # - `execution_role_database_grant`: false leaves the database secret out of the execution
107
+ # role (`Settings`).
108
+ # - `install_dir` and `build_context_dir`: where the Dockerfile installs the host and its
109
+ # sidecars, and where it copies them from (`Settings`).
110
+ # - `db_name_parameter`: for `database "Shared"`, a parameter naming the shared database
111
+ # (`Settings`).
112
+ #
113
+ # Per-branch preview stacks and the deploy and smoke scripts are not generated here.
64
114
  module Fargate
65
115
  extend Projector::Target
66
116
 
@@ -183,7 +233,22 @@ module Hecks
183
233
  stack_name = "#{stack_prefix}-#{infra_name}"
184
234
  desired_count = deploy_settings.fetch(:desired_count, 1)
185
235
 
186
- db_id = "#{logical_id.sub(/Service\z/, '')}Db"
236
+ # Every optional setting, checked; a world that sets none resolves to the
237
+ # derived ids and names the generator has always used.
238
+ begin
239
+ plan = Settings.resolve(
240
+ deploy_settings,
241
+ infra_name: infra_name, logical_id: logical_id, db_id: "#{logical_id.sub(/Service\z/, '')}Db",
242
+ stack_name: stack_name, port: port, shared: shared
243
+ )
244
+ rescue ArgumentError => e
245
+ raise ArgumentError, "#{world_file}'s deployed_to(\"AwsFargate\"): #{e.message}"
246
+ end
247
+ ids = plan.ids
248
+ names = plan.names
249
+ layout = plan.layout
250
+
251
+ db_id = ids[:database_prefix]
187
252
  db_ref_id = aurora ? "#{db_id}Cluster" : db_id
188
253
  secret_sub = aurora ? "#{db_id}Secret" : "#{db_ref_id}.MasterUserSecret.SecretArn"
189
254
  secret_intrinsic = aurora ? "!Ref #{db_id}Secret" : "!GetAtt #{db_ref_id}.MasterUserSecret.SecretArn"
@@ -208,20 +273,31 @@ module Hecks
208
273
  # `Lambda.call`'s own Shared-mode `VpcConfig`); the `ECS` task and
209
274
  # the `ALB`-ingress rule both reach through the borrowed owner's
210
275
  # own security group instead.
211
- compute_security_group_ref = shared ? "!Ref OwningSecurityGroupId" : "!Ref #{logical_id}SecurityGroup"
212
-
213
- ecr_repository_id = "#{logical_id}Repository"
214
- cluster_id = "#{logical_id}Cluster"
215
- task_definition_id = "#{logical_id}TaskDefinition"
216
- execution_role_id = "#{logical_id}ExecutionRole"
217
- task_role_id = "#{logical_id}TaskRole"
218
- log_group_id = "#{logical_id}LogGroup"
219
- target_group_id = "#{logical_id}TargetGroup"
220
- alb_id = "#{logical_id}Alb"
221
- alb_sg_id = "#{logical_id}AlbSecurityGroup"
222
- listener_id = "#{logical_id}Listener"
223
- distribution_id = "#{logical_id}Distribution"
224
- session_secret_id = "#{logical_id}SessionSecret"
276
+ compute_security_group_ref = shared ? "!Ref OwningSecurityGroupId" : "!Ref #{ids[:compute_prefix]}SecurityGroup"
277
+
278
+ # `logical_id` stays the derived name in prose (descriptions, comments);
279
+ # the ids below are what the resources are actually declared with, which
280
+ # the world's `logical_ids` setting may replace.
281
+ service_id = ids[:service]
282
+ ecr_repository_id = ids[:ecr_repository]
283
+ cluster_id = ids[:cluster]
284
+ task_definition_id = ids[:task_definition]
285
+ execution_role_id = ids[:execution_role]
286
+ task_role_id = ids[:task_role]
287
+ log_group_id = ids[:log_group]
288
+ target_group_id = ids[:target_group]
289
+ alb_id = ids[:alb]
290
+ alb_sg_id = ids[:alb_security_group]
291
+ listener_id = ids[:listener]
292
+ distribution_id = ids[:distribution]
293
+ session_secret_id = ids[:session_secret]
294
+ domain_container = layout.domain
295
+ alb_sg_description = names[:alb_security_group_description] ||
296
+ "#{alb_sg_id} - HTTP ingress from CloudFront only, forwarded to #{logical_id} only"
297
+ port_range = Containers.port_range(layout)
298
+ vpc_ref = shared ? "!Ref OwningVpcId" : "!Ref #{db_id}Vpc"
299
+ sidecar_dir = plan.install_dir
300
+ db_name_ref = plan.db_name_parameter ? "!Ref #{plan.db_name_parameter}" : nil
225
301
 
226
302
  stack_outputs = Shared.stack_outputs(
227
303
  shared: shared, db_id: db_id, db_ref_id: db_ref_id, secret_intrinsic: secret_intrinsic,
@@ -231,7 +307,7 @@ module Hecks
231
307
  Shared.check_bastion_parameters!(bastion_parameters, stack_outputs)
232
308
 
233
309
  always_params_yaml = <<~ALWAYSPARAMS.rstrip
234
- ImageTag:
310
+ #{domain_container.tag_parameter}:
235
311
  Type: String
236
312
  Default: latest
237
313
  Description: ECR image tag this task pulls — never hardcode latest in the TaskDefinition; a first deploy and a later rollout share this one parameter.
@@ -277,7 +353,7 @@ module Hecks
277
353
  OwningSubnetBId:
278
354
  Type: AWS::EC2::Subnet::Id
279
355
  # PUBLIC subnets, NOT OwningSubnetAId/OwningSubnetBId above — a
280
- # real, live deploy (lifeadelics-platform) found the ALB placed in
356
+ # real, live deploy (a shared-database stack) found the ALB placed in
281
357
  # the private pair creates successfully and reports its target
282
358
  # health as healthy (health checks run from inside the VPC), yet
283
359
  # is completely unreachable from outside it:
@@ -299,7 +375,8 @@ module Hecks
299
375
  OwningDatabaseSecretArn:
300
376
  Type: String
301
377
  SHAREDPARAMS
302
- parameters_yaml = [always_params_yaml, oauth_params_yaml, owning_params_yaml].reject(&:empty?).join("\n")
378
+ extra_params_yaml = Settings.parameters_yaml(plan, default_count: desired_count, shared_db_name: owner_db_name).rstrip
379
+ parameters_yaml = [always_params_yaml, oauth_params_yaml, owning_params_yaml, extra_params_yaml].reject(&:empty?).join("\n")
303
380
 
304
381
  template_yaml = <<~YAML
305
382
  # GENERATED by bin/project_deploy #{domain} — re-run it to refresh
@@ -320,15 +397,15 @@ module Hecks
320
397
  Resources:
321
398
  #{shared ? "" : Shared.vpc_and_database_yaml(
322
399
  db_id: db_id, db_name: db_name, infra_name: infra_name, aurora: aurora,
323
- google_oauth_present: network_needs_internet, compute_logical_id: logical_id,
400
+ google_oauth_present: network_needs_internet, compute_logical_id: ids[:compute_prefix],
324
401
  compute_description: "#{logical_id} - inbound from #{alb_sg_id} only, egress rules attached separately below"
325
402
  ).each_line.with_index.map { |l, i| (i.zero? ? "" : " ") + l }.join.rstrip}
326
403
 
327
404
  #{alb_sg_id}:
328
405
  Type: AWS::EC2::SecurityGroup
329
406
  Properties:
330
- VpcId: #{shared ? "!Ref OwningVpcId" : "!Ref #{db_id}Vpc"}
331
- GroupDescription: #{alb_sg_id} - HTTP ingress from CloudFront only, forwarded to #{logical_id} only
407
+ VpcId: #{vpc_ref}
408
+ GroupDescription: #{alb_sg_description}
332
409
  SecurityGroupIngress:
333
410
  # pl-3b927c52 — com.amazonaws.global.cloudfront.origin-
334
411
  # facing, AWS's own global, account-agnostic managed
@@ -348,31 +425,31 @@ module Hecks
348
425
  ToPort: 80
349
426
  SourcePrefixListId: pl-3b927c52
350
427
 
351
- #{logical_id}IngressFromAlb:
428
+ #{ids[:ingress_from_alb]}:
352
429
  Type: AWS::EC2::SecurityGroupIngress
353
430
  Properties:
354
431
  GroupId: #{compute_security_group_ref}
355
432
  IpProtocol: tcp
356
- FromPort: #{port}
357
- ToPort: #{port}
433
+ FromPort: #{port_range.min}
434
+ ToPort: #{port_range.max}
358
435
  SourceSecurityGroupId: !Ref #{alb_sg_id}
359
436
 
360
437
  #{ecr_repository_id}:
361
438
  Type: AWS::ECR::Repository
362
439
  Properties:
363
- RepositoryName: #{infra_name}
440
+ RepositoryName: #{domain_container.repository_name}
364
441
  ImageScanningConfiguration:
365
442
  ScanOnPush: true
366
443
 
367
444
  #{cluster_id}:
368
445
  Type: AWS::ECS::Cluster
369
446
  Properties:
370
- ClusterName: #{stack_name}
447
+ ClusterName: #{names[:cluster]}
371
448
 
372
449
  #{log_group_id}:
373
450
  Type: AWS::Logs::LogGroup
374
451
  Properties:
375
- LogGroupName: /ecs/#{stack_name}
452
+ LogGroupName: #{names[:log_group]}
376
453
  RetentionInDays: 30
377
454
 
378
455
  # Always minted — HECKS_SERVE_MODE always runs web.rs, which
@@ -382,6 +459,7 @@ module Hecks
382
459
  #{session_secret_id}:
383
460
  Type: AWS::SecretsManager::Secret
384
461
  Properties:
462
+ # TMPL:session_secret_properties
385
463
  GenerateSecretString:
386
464
  SecretStringTemplate: '{}'
387
465
  GenerateStringKey: session_secret
@@ -406,13 +484,8 @@ module Hecks
406
484
  ManagedPolicyArns:
407
485
  - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
408
486
  Policies:
409
- - PolicyName: DbSecretRead
410
- PolicyDocument:
411
- Version: '2012-10-17'
412
- Statement:
413
- - Effect: Allow
414
- Action: secretsmanager:GetSecretValue
415
- Resource: !Sub "${#{db_secret_ref}}"
487
+ # TMPL:execution_database_grant
488
+ # TMPL:extra_execution_policies
416
489
 
417
490
  # The container's OWN runtime permissions — rust/host fetches
418
491
  # DB_SECRET_ARN itself, over the AWS SDK, the same "never let
@@ -430,7 +503,7 @@ module Hecks
430
503
  Principal: { Service: ecs-tasks.amazonaws.com }
431
504
  Action: sts:AssumeRole
432
505
  Policies:
433
- - PolicyName: DbSecretRead
506
+ - PolicyName: #{names[:db_secret_policy]}
434
507
  PolicyDocument:
435
508
  Version: '2012-10-17'
436
509
  Statement:
@@ -444,13 +517,14 @@ module Hecks
444
517
  - Effect: Allow
445
518
  Action: secretsmanager:GetSecretValue
446
519
  Resource: !Ref #{session_secret_id}
447
- #{oauth_task_policy_yaml.each_line.with_index.map { |l, i| i.zero? ? l : " " + l }.join}
520
+ #{oauth_task_policy_yaml.empty? ? "" : "# TMPL:oauth_task_policy"}
448
521
  # TMPL:cross_domain_fargate_policies
522
+ # TMPL:extra_task_policies
449
523
 
450
524
  #{task_definition_id}:
451
525
  Type: AWS::ECS::TaskDefinition
452
526
  Properties:
453
- Family: #{infra_name}
527
+ Family: #{names[:family]}
454
528
  RequiresCompatibilities: [FARGATE]
455
529
  NetworkMode: awsvpc
456
530
  # ARM64, not Fargate's own x86_64 default — matching the
@@ -467,8 +541,9 @@ module Hecks
467
541
  ExecutionRoleArn: !GetAtt #{execution_role_id}.Arn
468
542
  TaskRoleArn: !GetAtt #{task_role_id}.Arn
469
543
  ContainerDefinitions:
470
- - Name: #{infra_name}
471
- Image: !Sub "${#{ecr_repository_id}.RepositoryUri}:${ImageTag}"
544
+ - Name: #{domain_container.name}
545
+ Image: !Sub "${#{ecr_repository_id}.RepositoryUri}:${#{domain_container.tag_parameter}}"
546
+ # TMPL:domain_essential
472
547
  PortMappings:
473
548
  - ContainerPort: #{port}
474
549
  LogConfiguration:
@@ -476,7 +551,7 @@ module Hecks
476
551
  Options:
477
552
  awslogs-group: !Ref #{log_group_id}
478
553
  awslogs-region: !Ref AWS::Region
479
- awslogs-stream-prefix: #{infra_name}
554
+ awslogs-stream-prefix: #{domain_container.name}
480
555
  Environment:
481
556
  - Name: HECKS_DOMAIN
482
557
  Value: #{declared_domain_name}
@@ -511,20 +586,21 @@ module Hecks
511
586
  # dispatch) regardless of `web`/HECKS_SERVE_MODE. A
512
587
  # container built with neither set crashes before
513
588
  # ever reaching its own serve loop — found live
514
- # deploying lifeadelics-platform's own domain
515
- # container, fixed here so every Fargate domain ships
589
+ # deploying a real domain container, fixed here so every Fargate domain ships
516
590
  # both sidecars by default. Paths match the
517
591
  # Dockerfile's own COPY destinations, below.
518
592
  - Name: HECKS_WASM_PATH
519
- Value: /usr/local/bin/#{domain_name}.wasm
593
+ Value: #{sidecar_dir}/#{domain_name}.wasm
520
594
  - Name: HECKS_IR_PATH
521
- Value: /usr/local/bin/#{domain_name}.ir.json
595
+ Value: #{sidecar_dir}/#{domain_name}.ir.json
522
596
  - Name: SESSION_SECRET_ARN
523
597
  Value: !Ref #{session_secret_id}
524
598
  - Name: HECKS_CHECKOUT_DOMAIN
525
599
  Value: #{declared_domain_name}
526
- #{oauth_task_env_yaml.each_line.with_index.map { |l, i| i.zero? ? l : " " + l }.join}
600
+ #{oauth_task_env_yaml.empty? ? "" : "# TMPL:oauth_task_env"}
527
601
  # TMPL:db_env
602
+ # TMPL:domain_env
603
+ # TMPL:extra_containers
528
604
 
529
605
  #{target_group_id}:
530
606
  Type: AWS::ElasticLoadBalancingV2::TargetGroup
@@ -532,14 +608,15 @@ module Hecks
532
608
  TargetType: ip
533
609
  Port: #{port}
534
610
  Protocol: HTTP
535
- VpcId: #{shared ? "!Ref OwningVpcId" : "!Ref #{db_id}Vpc"}
536
- HealthCheckPath: /
611
+ VpcId: #{vpc_ref}
612
+ HealthCheckPath: #{domain_container.health_path}
537
613
  HealthCheckPort: "#{port}"
614
+ # TMPL:target_group_attributes
538
615
 
539
616
  #{alb_id}:
540
617
  Type: AWS::ElasticLoadBalancingV2::LoadBalancer
541
618
  Properties:
542
- Name: #{stack_name}-alb
619
+ Name: #{names[:alb]}
543
620
  Scheme: internet-facing
544
621
  Type: application
545
622
  SecurityGroups: [!Ref #{alb_sg_id}]
@@ -553,98 +630,40 @@ module Hecks
553
630
  Protocol: HTTP
554
631
  DefaultActions:
555
632
  - Type: forward
556
- TargetGroupArn: !Ref #{target_group_id}
633
+ TargetGroupArn: !Ref #{layout.default.target_group_id}
557
634
 
558
- #{logical_id}:
635
+ # TMPL:extra_target_groups
636
+ #{service_id}:
559
637
  Type: AWS::ECS::Service
560
- DependsOn: #{listener_id}
638
+ DependsOn: #{Containers.depends_on(layout, listener_id)}
561
639
  Properties:
562
- ServiceName: #{stack_name}
640
+ ServiceName: #{names[:service]}
563
641
  Cluster: !Ref #{cluster_id}
564
642
  TaskDefinition: !Ref #{task_definition_id}
565
- DesiredCount: #{desired_count}
643
+ DesiredCount: #{plan.desired_count_parameter ? "!Ref #{plan.desired_count_parameter}" : desired_count}
566
644
  LaunchType: FARGATE
645
+ # TMPL:service_tuning
567
646
  NetworkConfiguration:
568
647
  AwsvpcConfiguration:
569
648
  AssignPublicIp: DISABLED
570
649
  Subnets: #{shared ? "[!Ref OwningSubnetAId, !Ref OwningSubnetBId]" : "[!Ref #{db_id}SubnetA, !Ref #{db_id}SubnetB]"}
571
650
  SecurityGroups: [#{compute_security_group_ref}]
572
651
  LoadBalancers:
573
- - ContainerName: #{infra_name}
652
+ - ContainerName: #{domain_container.name}
574
653
  ContainerPort: #{port}
575
654
  TargetGroupArn: !Ref #{target_group_id}
655
+ # TMPL:extra_load_balancers
576
656
 
577
- # Real HTTPS (the ALB's own Listener above is HTTP-only —
578
- # nothing else in this stack terminates TLS) and, just as
579
- # important, the ONE safe default this generator can offer
580
- # for caching it has no way to reason about: Managed-
581
- # CachingDisabled. This domain's own routes — including
582
- # every hecks-native /login, /logout, /auth/google(/callback),
583
- # /admin/members request (web.rs's own auth_gate/auth_route,
584
- # generic across every domain, not just this one's own
585
- # dispatch commands) — are all session-cookie-driven, and
586
- # this generator has no way to tell which of a domain's own
587
- # paths would ever be safe to cache. Found live, the hard
588
- # way (lifeadelics, 2026-09-21): a hand-authored CloudFront
589
- # stack applied the OPPOSITE default — a custom, cookie-
590
- # blind cache policy with a 90-120s TTL — and it served one
591
- # signed-in session's own response (a short-lived SSO
592
- # handoff token among them) back to a different, unrelated
593
- # request within that window. A domain that DOES know one
594
- # of its own paths is genuinely safe to cache (a public,
595
- # non-personalized page) adds its own more specific
596
- # CacheBehavior by hand, the same way lifeadelics's own
597
- # hand-extended three-container stack already does for
598
- # /_astro/*, /videos/*, and friends — never by loosening
599
- # this one.
600
- #{distribution_id}:
601
- Type: AWS::CloudFront::Distribution
602
- Properties:
603
- DistributionConfig:
604
- Enabled: true
605
- HttpVersion: http2
606
- # No ACM/custom domain here — this generator has no
607
- # notion of one (deploy.bluebook's own FargateTarget
608
- # declares no `domain` attribute for it) and CloudFront
609
- # requires an ACM cert in us-east-1 specifically to
610
- # attach a custom Aliases entry, a real cross-region
611
- # dependency this generator can't assume. CloudFront's
612
- # own default *.cloudfront.net certificate/hostname
613
- # are what Outputs.CloudFrontDomain below reports;
614
- # point a real domain's DNS at it by hand, same
615
- # "generated, extend by hand" posture this whole file
616
- # already has for anything past its own baseline.
617
- ViewerCertificate:
618
- CloudFrontDefaultCertificate: true
619
- Origins:
620
- - Id: #{alb_id}Origin
621
- DomainName: !GetAtt #{alb_id}.DNSName
622
- CustomOriginConfig:
623
- OriginProtocolPolicy: http-only
624
- HTTPPort: 80
625
- HTTPSPort: 443
626
- DefaultCacheBehavior:
627
- TargetOriginId: #{alb_id}Origin
628
- ViewerProtocolPolicy: redirect-to-https
629
- Compress: true
630
- AllowedMethods: [GET, HEAD, OPTIONS, PUT, PATCH, POST, DELETE]
631
- CachedMethods: [GET, HEAD]
632
- # Managed-CachingDisabled — see this resource's own
633
- # header comment for why nothing else is safe here
634
- # by default.
635
- CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
636
- # Managed-AllViewer — forwards every cookie/header/
637
- # query string through uncached, so rust/host's own
638
- # session-cookie-based auth sees the real request
639
- # exactly as the browser sent it.
640
- OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3
657
+ # TMPL:distribution
641
658
 
659
+ # TMPL:extra_resources
642
660
  Outputs:
643
661
  ServiceUrl:
644
662
  Value: !Sub "http://${#{alb_id}.DNSName}"
645
663
  CloudFrontDomain:
646
664
  Value: !GetAtt #{distribution_id}.DomainName
647
665
  #{stack_outputs.map { |o| "#{o[:key]}:\n Value: #{o[:ref]}" }.join("\n ")}
666
+ # TMPL:extra_outputs
648
667
  YAML
649
668
 
650
669
  # Spliced in after the heredoc renders, not interpolated inside
@@ -661,11 +680,27 @@ module Hecks
661
680
  # deep as it should have). A plain `String#sub` after the fact,
662
681
  # capturing the marker's own real indentation, has no such
663
682
  # interaction with the text it replaces into.
664
- template_yaml = template_yaml.sub(/^([ \t]*)# TMPL:db_env\n/) { db_env_yaml(shared: shared, owner_db_name: owner_db_name, db_ref_id: db_ref_id, db_name: db_name, secret_sub: secret_sub, hecks_schema: hecks_schema, base: $1) }
683
+ template_yaml = template_yaml.sub(/^([ \t]*)# TMPL:db_env\n/) { db_env_yaml(shared: shared, owner_db_name: owner_db_name, db_ref_id: db_ref_id, db_name: db_name, secret_sub: secret_sub, hecks_schema: hecks_schema, db_name_ref: db_name_ref, base: $1) }
684
+ # A multi-line block is indented by the marker's own rendered column, never a
685
+ # hand-computed one: the heredoc above is dedented after its interpolations
686
+ # evaluate, so a column counted in the source ran twelve too deep and made the
687
+ # Google sign-in blocks invalid YAML.
688
+ template_yaml = Yaml.splice(template_yaml, "oauth_task_policy", "#{oauth_task_policy_yaml}\n")
689
+ template_yaml = Yaml.splice(template_yaml, "oauth_task_env", "#{oauth_task_env_yaml}\n")
665
690
  template_yaml = template_yaml.sub(/^([ \t]*)# TMPL:cross_domain_fargate_policies\n/) {
666
691
  cross_domain_fargate_targets.empty? ? "" : cross_domain_fargate_policy_yaml(cross_domain_fargate_targets, $1)
667
692
  }
668
693
 
694
+ begin
695
+ template_yaml = Assembly.apply(
696
+ template_yaml, plan,
697
+ stack_name: stack_name, vpc_ref: vpc_ref, listener_id: listener_id, alb_id: alb_id, distribution_id: distribution_id,
698
+ db_secret_ref: db_secret_ref
699
+ )
700
+ rescue ArgumentError => e
701
+ raise ArgumentError, "#{world_file}'s deployed_to(\"AwsFargate\"): #{e.message}"
702
+ end
703
+
669
704
  bastion_yaml = shared ? nil : Shared.bastion_yaml(
670
705
  domain: domain, infra_name: infra_name, stack_name: stack_name, db_id: db_id,
671
706
  google_oauth_present: network_needs_internet, bastion_parameters: bastion_parameters
@@ -673,8 +708,8 @@ module Hecks
673
708
 
674
709
  dockerfile = <<~DOCKERFILE
675
710
  # GENERATED by bin/project_deploy #{domain} — re-run it to refresh this
676
- # file rather than hand-editing. Modeled on lifeadelics/domain/Dockerfile's
677
- # own shape: build the binary outside the image (this directory's own
711
+ # file rather than hand-editing. Modeled on a Ruby deployment's own
712
+ # Dockerfile shape: build the binary outside the image (this directory's own
678
713
  # Makefile, before `docker build` runs), ship only the result — one COPY,
679
714
  # not a from-scratch toolchain install on every deploy.
680
715
  FROM debian:bookworm-slim
@@ -688,18 +723,18 @@ module Hecks
688
723
  RUN apt-get update -qq && apt-get install -y --no-install-recommends -qq ca-certificates libpq5 \\
689
724
  && rm -rf /var/lib/apt/lists/*
690
725
 
691
- COPY #{domain_name}-host /usr/local/bin/#{domain_name}-host
726
+ COPY #{plan.build_context_dir}#{domain_name}-host #{sidecar_dir}/#{domain_name}-host
692
727
  # The .wasm/.ir.json sidecars main.rs requires at boot —
693
728
  # HECKS_WASM_PATH/HECKS_IR_PATH (template.yaml's own
694
729
  # ContainerDefinitions Environment) point at these exact paths.
695
- COPY #{domain_name}.wasm /usr/local/bin/#{domain_name}.wasm
696
- COPY #{domain_name}.ir.json /usr/local/bin/#{domain_name}.ir.json
730
+ COPY #{plan.build_context_dir}#{domain_name}.wasm #{sidecar_dir}/#{domain_name}.wasm
731
+ COPY #{plan.build_context_dir}#{domain_name}.ir.json #{sidecar_dir}/#{domain_name}.ir.json
697
732
 
698
733
  ENV PORT=#{port}
699
734
  ENV BIND=0.0.0.0
700
735
  EXPOSE #{port}
701
736
 
702
- CMD ["#{domain_name}-host"]
737
+ CMD ["#{sidecar_dir == Settings::DEFAULT_INSTALL_DIR ? domain_name : "#{sidecar_dir}/#{domain_name}"}-host"]
703
738
  DOCKERFILE
704
739
 
705
740
  makefile_content = <<~MAKE
@@ -728,7 +763,7 @@ module Hecks
728
763
  \t@rustup target list --installed 2>/dev/null | grep -qx aarch64-unknown-linux-gnu || rustup target add aarch64-unknown-linux-gnu
729
764
  # GNU cross-linker, not Apple clang — rustc's aarch64-unknown-linux-gnu
730
765
  # target emits `-Wl,--fix-cortex-a53-843419`, which macOS ld rejects
731
- # (found live building lifeadelics-platform's domain image). Same
766
+ # (found live building a domain image). Same
732
767
  # toolchain Lambda's generated Makefile already documents.
733
768
  \t@command -v aarch64-linux-gnu-gcc >/dev/null 2>&1 || { echo "aarch64-linux-gnu-gcc isn't on PATH. Install once with: brew tap messense/macos-cross-toolchains && brew install aarch64-unknown-linux-gnu"; exit 1; }
734
769
  # The .wasm/.ir.json sidecars main.rs requires at boot,
@@ -752,17 +787,17 @@ module Hecks
752
787
  # linux/arm64, not the generator's old amd64 default — has to
753
788
  # match RuntimePlatform/the binary this Makefile's own build:
754
789
  # step just cross-compiled, above.
755
- \tdocker build --platform linux/arm64 -t #{infra_name}:$(IMAGE_TAG) .
790
+ \tdocker build --platform linux/arm64 -t #{domain_container.repository_name}:$(IMAGE_TAG) .
756
791
 
757
792
  .PHONY: docker-push
758
793
  docker-push: ecr-login
759
794
  \tACCOUNT_ID=$$(aws sts get-caller-identity --query Account --output text); \\
760
- \t\tdocker tag #{infra_name}:$(IMAGE_TAG) $$ACCOUNT_ID.dkr.ecr.$(REGION).amazonaws.com/#{infra_name}:$(IMAGE_TAG); \\
761
- \t\tdocker push $$ACCOUNT_ID.dkr.ecr.$(REGION).amazonaws.com/#{infra_name}:$(IMAGE_TAG)
795
+ \t\tdocker tag #{domain_container.repository_name}:$(IMAGE_TAG) $$ACCOUNT_ID.dkr.ecr.$(REGION).amazonaws.com/#{domain_container.repository_name}:$(IMAGE_TAG); \\
796
+ \t\tdocker push $$ACCOUNT_ID.dkr.ecr.$(REGION).amazonaws.com/#{domain_container.repository_name}:$(IMAGE_TAG)
762
797
 
763
798
  .PHONY: deploy
764
799
  deploy: docker-build docker-push
765
- #{shared ? "\t@echo \"Looking up #{owner_stack_name}'s shared VpcId/PrivateSubnetAId/PrivateSubnetBId/PublicSubnetId/BastionSubnetId/FunctionSecurityGroupId/DatabaseEndpoint/DatabaseSecretArn outputs to pass as $(STACK)'s Owning* parameters...\"\n\tOWNER_VPC_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query \"Stacks[0].Outputs[?OutputKey=='VpcId'].OutputValue\" --output text); \\\n\t\tOWNER_SUBNET_A_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query \"Stacks[0].Outputs[?OutputKey=='PrivateSubnetAId'].OutputValue\" --output text); \\\n\t\tOWNER_SUBNET_B_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query \"Stacks[0].Outputs[?OutputKey=='PrivateSubnetBId'].OutputValue\" --output text); \\\n\t\tOWNER_PUBLIC_SUBNET_A_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query \"Stacks[0].Outputs[?OutputKey=='PublicSubnetId'].OutputValue\" --output text); \\\n\t\tOWNER_PUBLIC_SUBNET_B_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query \"Stacks[0].Outputs[?OutputKey=='BastionSubnetId'].OutputValue\" --output text); \\\n\t\tOWNER_SG_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query \"Stacks[0].Outputs[?OutputKey=='FunctionSecurityGroupId'].OutputValue\" --output text); \\\n\t\tOWNER_DB_HOST=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query \"Stacks[0].Outputs[?OutputKey=='DatabaseEndpoint'].OutputValue\" --output text); \\\n\t\tOWNER_DB_SECRET_ARN=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query \"Stacks[0].Outputs[?OutputKey=='DatabaseSecretArn'].OutputValue\" --output text); \\\n\t\taws cloudformation deploy --template-file template.yaml --stack-name $(STACK) --region $(REGION) --capabilities CAPABILITY_IAM \\\n\t\t\t--parameter-overrides ImageTag=$(IMAGE_TAG) OwningVpcId=$$OWNER_VPC_ID OwningSubnetAId=$$OWNER_SUBNET_A_ID OwningSubnetBId=$$OWNER_SUBNET_B_ID OwningPublicSubnetAId=$$OWNER_PUBLIC_SUBNET_A_ID OwningPublicSubnetBId=$$OWNER_PUBLIC_SUBNET_B_ID OwningSecurityGroupId=$$OWNER_SG_ID OwningDatabaseEndpoint=$$OWNER_DB_HOST OwningDatabaseSecretArn=$$OWNER_DB_SECRET_ARN" : "\taws cloudformation deploy --template-file template.yaml --stack-name $(STACK) --region $(REGION) --capabilities CAPABILITY_IAM --parameter-overrides ImageTag=$(IMAGE_TAG)"}
800
+ #{shared ? "\t@echo \"Looking up #{owner_stack_name}'s shared VpcId/PrivateSubnetAId/PrivateSubnetBId/PublicSubnetId/BastionSubnetId/FunctionSecurityGroupId/DatabaseEndpoint/DatabaseSecretArn outputs to pass as $(STACK)'s Owning* parameters...\"\n\tOWNER_VPC_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query \"Stacks[0].Outputs[?OutputKey=='VpcId'].OutputValue\" --output text); \\\n\t\tOWNER_SUBNET_A_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query \"Stacks[0].Outputs[?OutputKey=='PrivateSubnetAId'].OutputValue\" --output text); \\\n\t\tOWNER_SUBNET_B_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query \"Stacks[0].Outputs[?OutputKey=='PrivateSubnetBId'].OutputValue\" --output text); \\\n\t\tOWNER_PUBLIC_SUBNET_A_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query \"Stacks[0].Outputs[?OutputKey=='PublicSubnetId'].OutputValue\" --output text); \\\n\t\tOWNER_PUBLIC_SUBNET_B_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query \"Stacks[0].Outputs[?OutputKey=='BastionSubnetId'].OutputValue\" --output text); \\\n\t\tOWNER_SG_ID=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query \"Stacks[0].Outputs[?OutputKey=='FunctionSecurityGroupId'].OutputValue\" --output text); \\\n\t\tOWNER_DB_HOST=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query \"Stacks[0].Outputs[?OutputKey=='DatabaseEndpoint'].OutputValue\" --output text); \\\n\t\tOWNER_DB_SECRET_ARN=$$(aws cloudformation describe-stacks --stack-name #{owner_stack_name} --query \"Stacks[0].Outputs[?OutputKey=='DatabaseSecretArn'].OutputValue\" --output text); \\\n\t\taws cloudformation deploy --template-file template.yaml --stack-name $(STACK) --region $(REGION) --capabilities CAPABILITY_IAM \\\n\t\t\t--parameter-overrides #{domain_container.tag_parameter}=$(IMAGE_TAG) OwningVpcId=$$OWNER_VPC_ID OwningSubnetAId=$$OWNER_SUBNET_A_ID OwningSubnetBId=$$OWNER_SUBNET_B_ID OwningPublicSubnetAId=$$OWNER_PUBLIC_SUBNET_A_ID OwningPublicSubnetBId=$$OWNER_PUBLIC_SUBNET_B_ID OwningSecurityGroupId=$$OWNER_SG_ID OwningDatabaseEndpoint=$$OWNER_DB_HOST OwningDatabaseSecretArn=$$OWNER_DB_SECRET_ARN" : "\taws cloudformation deploy --template-file template.yaml --stack-name $(STACK) --region $(REGION) --capabilities CAPABILITY_IAM --parameter-overrides #{domain_container.tag_parameter}=$(IMAGE_TAG)"}
766
801
  \t$(MAKE) mint-era
767
802
 
768
803
  #{shared ? <<~SHAREDMINT.rstrip : <<~OWNMINT.rstrip
@@ -827,7 +862,19 @@ module Hecks
827
862
  files["bastion.yaml"] = bastion_yaml if bastion_yaml
828
863
  files["Dockerfile"] = dockerfile
829
864
  files["Makefile"] = makefile_content
830
- files
865
+ # Opt-in per-branch previews: nothing is added unless the domain declares a `preview`
866
+ # setting under deployed_to("AwsFargate"). See Preview's own header for the keys.
867
+ preview_main = {
868
+ infra_name: infra_name, stack_name: stack_name, stack_prefix: stack_prefix, region: region,
869
+ cpu: cpu, memory: memory, db_name: shared ? owner_db_name : db_name,
870
+ owner_stack: shared ? owner_stack_name : nil, name: infra_name, port: port,
871
+ image: "#{domain_container.repository_name}:latest", domain: declared_domain_name, web: target.state[:web].value,
872
+ wasm_path: "/usr/local/bin/#{domain_name}.wasm", ir_path: "/usr/local/bin/#{domain_name}.ir.json",
873
+ schema: hecks_schema
874
+ }
875
+ files.merge!(Preview.call(deploy_settings: deploy_settings, main: preview_main))
876
+ Scripts.extend_files(files, deploy_settings: deploy_settings, plan: plan,
877
+ stack_name: stack_name, region: region)
831
878
  end
832
879
 
833
880
  # Renders the container's own `DB_HOST`/`DB_NAME`/`DB_SECRET_ARN`
@@ -846,17 +893,19 @@ module Hecks
846
893
  # @param secret_sub [String] the `${...}`-ready identifier for this domain's
847
894
  # own database secret
848
895
  # @param hecks_schema [String, nil] the Postgres schema to set, or nil for none
896
+ # @param db_name_ref [String, nil] an intrinsic (such as `!Ref Param`) that names the shared
897
+ # database in place of `owner_db_name`, or nil to write the name itself
849
898
  # @param base [String] the marker line's own rendered indentation whitespace
850
899
  # @return [String] the rendered `ContainerDefinitions[0].Environment` entries,
851
900
  # ending in exactly one trailing newline
852
- def db_env_yaml(shared:, owner_db_name:, db_ref_id:, db_name:, secret_sub:, hecks_schema:, base:)
901
+ def db_env_yaml(shared:, owner_db_name:, db_ref_id:, db_name:, secret_sub:, hecks_schema:, base:, db_name_ref: nil)
853
902
  lines =
854
903
  if shared
855
904
  [
856
905
  "- Name: DB_HOST",
857
906
  " Value: !Ref OwningDatabaseEndpoint",
858
907
  "- Name: DB_NAME",
859
- " Value: #{owner_db_name}",
908
+ " Value: #{db_name_ref || owner_db_name}",
860
909
  "- Name: DB_SECRET_ARN",
861
910
  " Value: !Sub \"${OwningDatabaseSecretArn}\"",
862
911
  ]
@@ -1039,10 +1039,10 @@ module Hecks
1039
1039
  # matching this stack's own name), a question `infra_name`
1040
1040
  # answers and a domain's own business identity does not.
1041
1041
  # Real, live, and confirmed the hard way: this was still
1042
- # wired to `declared_domain_name` when EmbryonautFoundersApp
1042
+ # wired to `declared_domain_name` when a client app
1043
1043
  # first deployed, and WebFunction immediately threw
1044
1044
  # `ResourceNotFoundException: Function not found:
1045
- # hecks-embryonautfoundersapp` on every read AND on the
1045
+ # hecks-<domain>` on every read AND on the
1046
1046
  # Google OAuth callback itself — a real signed-in user hit
1047
1047
  # this within minutes of the deploy that introduced it.
1048
1048
  DOMAIN_NAME: #{infra_name}
@@ -1317,7 +1317,7 @@ bastion_yaml = shared ? nil : Shared.bastion_yaml(
1317
1317
  # aggregates with no snapshot table at all -- it only provisions the
1318
1318
  # ONE bluebook it's called with (era_resolver.rb's own `bluebook.
1319
1319
  # aggregates.each`), never the whole registry. Found live minting
1320
- # lifeadelics' own era: `Payments::Payment.Initiate` refused with
1320
+ # a client site's own era: `Payments::Payment.Initiate` refused with
1321
1321
  # "relation \\"payment_head_snapshot_1\\" does not exist" the first
1322
1322
  # time this domain's own checkout route ran for real. The automated
1323
1323
  # (non-Shared) recipe below now does this correctly for every OTHER
@@ -1490,7 +1490,7 @@ bastion_yaml = shared ? nil : Shared.bastion_yaml(
1490
1490
  # `Hecks::Bluebook::Behaviour::World#for_binding` nor
1491
1491
  # `PostgresEra.connect_for` ever reads `ENV["DATABASE_URL"]` or
1492
1492
  # `ENV["HECKS_SCHEMA"]` — both scripts call
1493
- # `registry.world(bluebook.name)&.for_binding(...)`, a pure hash lookup
1493
+ # `registry.binding_settings(...)`, a pure hash lookup
1494
1494
  # against whatever literal `database "..."` string #{domain}'s own
1495
1495
  # `.world` file declares. Without `db_env_blind:` below, this whole
1496
1496
  # recipe would stand up a real bastion, punch a live 5432 ingress rule
@@ -1722,7 +1722,7 @@ bastion_yaml = shared ? nil : Shared.bastion_yaml(
1722
1722
  # `shared` are collected together here rather than spelled as an
1723
1723
  # if/elsif/else, the same shape `Parameters:` above holds to: the
1724
1724
  # two are independent facts about a domain, not alternatives, and
1725
- # lifeadelics is both — an elsif shape would silently drop the
1725
+ # a client site can be both — an elsif shape would silently drop the
1726
1726
  # Owning* overrides whenever OAuth is also present. Caught live
1727
1727
  # the first time a Shared-mode domain with real Google OAuth
1728
1728
  # actually ran `make deploy`: `sam deploy` refused