@lsts_tech/infra 1.0.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -0
  3. package/dist/bin/init.d.ts +9 -0
  4. package/dist/bin/init.d.ts.map +1 -0
  5. package/dist/bin/init.js +315 -0
  6. package/dist/bin/init.js.map +1 -0
  7. package/dist/stacks/Dns.d.ts +69 -0
  8. package/dist/stacks/Dns.d.ts.map +1 -0
  9. package/dist/stacks/Dns.js +57 -0
  10. package/dist/stacks/Dns.js.map +1 -0
  11. package/dist/stacks/ExpoSite.d.ts +72 -0
  12. package/dist/stacks/ExpoSite.d.ts.map +1 -0
  13. package/dist/stacks/ExpoSite.js +49 -0
  14. package/dist/stacks/ExpoSite.js.map +1 -0
  15. package/dist/stacks/NextSite.d.ts +86 -0
  16. package/dist/stacks/NextSite.d.ts.map +1 -0
  17. package/dist/stacks/NextSite.js +60 -0
  18. package/dist/stacks/NextSite.js.map +1 -0
  19. package/dist/stacks/Pipeline.d.ts +128 -0
  20. package/dist/stacks/Pipeline.d.ts.map +1 -0
  21. package/dist/stacks/Pipeline.js +311 -0
  22. package/dist/stacks/Pipeline.js.map +1 -0
  23. package/dist/stacks/index.d.ts +41 -0
  24. package/dist/stacks/index.d.ts.map +1 -0
  25. package/dist/stacks/index.js +38 -0
  26. package/dist/stacks/index.js.map +1 -0
  27. package/docs/CLI.md +59 -0
  28. package/docs/CONFIGURATION.md +78 -0
  29. package/docs/EXAMPLES.md +9 -0
  30. package/examples/next-and-expo/infra.config.ts +104 -0
  31. package/examples/next-only/infra.config.ts +60 -0
  32. package/package.json +102 -0
  33. package/schemas/pipeline.schema.json +25 -0
  34. package/scripts/cleanup-orphan-lambdas.sh +102 -0
  35. package/scripts/delete-amplify-app.sh +50 -0
  36. package/scripts/ensure-pipelines.sh +144 -0
  37. package/scripts/ensure-secrets.sh +58 -0
  38. package/scripts/postdeploy-update-dns.sh +158 -0
  39. package/scripts/predeploy-checks.sh +192 -0
  40. package/scripts/pulumi-deploy.sh +29 -0
  41. package/scripts/sst-deploy.sh +79 -0
  42. package/templates/buildspec.yml +77 -0
  43. package/templates/ensure-pipelines.sh +117 -0
  44. package/templates/env.example +38 -0
  45. package/templates/infra.config.ts +199 -0
  46. package/templates/secrets.schema.json +20 -0
  47. package/templates/sst-env.d.ts +50 -0
  48. package/templates/sst.config.ts +28 -0
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # postdeploy-update-dns.sh
5
+ # After a successful infra deploy this script ensures the stage domain (dev/production)
6
+ # Route53 alias points to the CloudFront distribution that serves it.
7
+ #
8
+ # Usage: postdeploy-update-dns.sh <stage> [region]
9
+ #
10
+ # Required environment variables:
11
+ # DOMAIN_ROOT — Root domain (e.g., "example.com")
12
+ #
13
+ # Optional stage overrides:
14
+ # DOMAIN_PRODUCTION, DOMAIN_DEV, DOMAIN_MOBILE, DOMAIN_CUSTOM
15
+
16
+ STAGE=${1:-dev}
17
+ REGION=${2:-us-east-1}
18
+
19
+ if [ -z "${DOMAIN_ROOT:-}" ]; then
20
+ echo "ERROR: DOMAIN_ROOT environment variable is required (e.g., DOMAIN_ROOT=example.com)"
21
+ exit 1
22
+ fi
23
+
24
+ # Polling configuration (seconds)
25
+ POLL_TIMEOUT=${POLL_TIMEOUT:-600} # total time to wait for distribution (default 10m)
26
+ POLL_INTERVAL=${POLL_INTERVAL:-15} # interval between checks
27
+
28
+ resolve_stage_domain() {
29
+ case "$STAGE" in
30
+ production) echo "${DOMAIN_PRODUCTION:-${DOMAIN_ROOT}}" ;;
31
+ dev) echo "${DOMAIN_DEV:-dev.${DOMAIN_ROOT}}" ;;
32
+ mobile) echo "${DOMAIN_MOBILE:-mobile.${DOMAIN_ROOT}}" ;;
33
+ *) echo "${DOMAIN_CUSTOM:-${STAGE}.${DOMAIN_ROOT}}" ;;
34
+ esac
35
+ }
36
+
37
+ DOMAIN=$(resolve_stage_domain)
38
+
39
+ echo "Post-deploy DNS sync: stage=$STAGE domain=$DOMAIN region=$REGION"
40
+
41
+ # Find and wait for the CloudFront distribution that has this alias
42
+ echo "Searching for CloudFront distribution with alias ${DOMAIN} (will wait up to ${POLL_TIMEOUT}s)..."
43
+ END=$((SECONDS + POLL_TIMEOUT))
44
+ FOUND_DIST_ID=""
45
+ FOUND_DIST_STATUS=""
46
+ FOUND_DIST_DOMAIN=""
47
+ while [ $SECONDS -le $END ]; do
48
+ # Query distributions and look for any that contain the alias
49
+ CF_JSON=$(mktemp)
50
+ aws cloudfront list-distributions --output json > "$CF_JSON" || true
51
+
52
+ # Find first distribution that has the alias
53
+ CID=$(jq -r --arg domain "$DOMAIN" '
54
+ (.DistributionList.Items[]? | select(.Aliases.Items? != null and (.Aliases.Items | index($domain) != null)) )
55
+ | .Id' "$CF_JSON" 2>/dev/null | head -n1 || true)
56
+
57
+ if [ -n "$CID" ] && [ "$CID" != "null" ]; then
58
+ STATUS=$(aws cloudfront get-distribution --id "$CID" --query 'Distribution.Status' --output text 2>/dev/null || echo "")
59
+ DOMAIN_NAME=$(aws cloudfront get-distribution --id "$CID" --query 'Distribution.DomainName' --output text 2>/dev/null || echo "")
60
+ echo "Found distribution $CID with status='$STATUS' and domain='$DOMAIN_NAME'"
61
+ if [ "$STATUS" = "Deployed" ]; then
62
+ FOUND_DIST_ID="$CID"
63
+ FOUND_DIST_STATUS="$STATUS"
64
+ FOUND_DIST_DOMAIN="$DOMAIN_NAME"
65
+ rm -f "$CF_JSON"
66
+ break
67
+ fi
68
+ # not yet deployed — wait and poll again
69
+ rm -f "$CF_JSON"
70
+ else
71
+ rm -f "$CF_JSON"
72
+ echo "No distribution found yet for ${DOMAIN}; retrying in ${POLL_INTERVAL}s..."
73
+ fi
74
+
75
+ sleep ${POLL_INTERVAL}
76
+ done
77
+
78
+ if [ -z "$FOUND_DIST_ID" ]; then
79
+ echo "Timed out waiting for a deployed CloudFront distribution for ${DOMAIN}. Gathering diagnostics..." >&2
80
+
81
+ # Print any distributions that reference the alias (even if not Deployed)
82
+ echo "\n-- Distributions mentioning ${DOMAIN} (all statuses) --"
83
+ aws cloudfront list-distributions --query "DistributionList.Items[?Aliases.Items!=null && contains(Aliases.Items, '${DOMAIN}')].[Id,DomainName,Status,Aliases.Items]" --output json || true
84
+
85
+ # ACM certificates in us-east-1
86
+ echo "\n-- ACM certificates for ${DOMAIN} (us-east-1) --"
87
+ aws acm list-certificates --region us-east-1 --query "CertificateSummaryList[?DomainName=='${DOMAIN}']" --output json || true
88
+
89
+ # Route53 hosted zone and apex records
90
+ echo "\n-- Route53 apex records for ${DOMAIN_ROOT} --"
91
+ HZ=$(aws route53 list-hosted-zones-by-name --dns-name "${DOMAIN_ROOT}" --query 'HostedZones[0].Id' --output text || true)
92
+ if [ -n "$HZ" ] && [ "$HZ" != "None" ]; then
93
+ HZ_ID=${HZ##*/}
94
+ echo "Hosted zone ID: $HZ_ID"
95
+ aws route53 list-resource-record-sets --hosted-zone-id "$HZ_ID" --query "ResourceRecordSets[?Name=='${DOMAIN}.']" --output json || true
96
+ else
97
+ echo "No hosted zone for ${DOMAIN_ROOT} found in this account/region"
98
+ fi
99
+
100
+ echo "\nIf SST reported creating a site but no distribution exists, ensure SST had permissions to create CloudFront and ACM resources and that the deploy completed successfully."
101
+ exit 1
102
+ fi
103
+
104
+ echo "Found distribution: $FOUND_DIST_ID"
105
+
106
+ DIST_ID="$FOUND_DIST_ID"
107
+ DIST_DOMAIN="$FOUND_DIST_DOMAIN"
108
+
109
+ # If we got here but DIST_DOMAIN is empty, attempt fallback: pick any Deployed distribution
110
+ if [ -z "$DIST_DOMAIN" ] || [ "$DIST_DOMAIN" = "null" ]; then
111
+ echo "Distribution had no DomainName in lookup; attempting fallback to any Deployed distribution..."
112
+ FALLBACK_ID=$(aws cloudfront list-distributions --query "DistributionList.Items[?Status=='Deployed'].[Id] | [0]" --output text 2>/dev/null || true)
113
+ if [ -n "$FALLBACK_ID" ] && [ "$FALLBACK_ID" != "None" ]; then
114
+ DIST_ID="$FALLBACK_ID"
115
+ DIST_DOMAIN=$(aws cloudfront get-distribution --id "$DIST_ID" --query 'Distribution.DomainName' --output text 2>/dev/null || echo "")
116
+ echo "Falling back to distribution $DIST_ID with domain $DIST_DOMAIN"
117
+ else
118
+ echo "No Deployed CloudFront distribution available to use as fallback. Exiting." >&2
119
+ exit 1
120
+ fi
121
+ fi
122
+
123
+ # Lookup hosted zone id for base domain
124
+ HOSTED_ZONE_ID=$(aws route53 list-hosted-zones-by-name --dns-name "${DOMAIN_ROOT}" --query 'HostedZones[0].Id' --output text)
125
+ if [ -z "$HOSTED_ZONE_ID" ] || [ "$HOSTED_ZONE_ID" = "None" ]; then
126
+ echo "Hosted zone for ${DOMAIN_ROOT} not found. Exiting." >&2
127
+ exit 1
128
+ fi
129
+
130
+ # Strip /hostedzone/ prefix if present
131
+ HOSTED_ZONE_ID=${HOSTED_ZONE_ID#/hostedzone/}
132
+
133
+ CHANGE_BATCH=$(mktemp)
134
+ cat > "$CHANGE_BATCH" <<EOF
135
+ {
136
+ "Comment": "UPSERT ${DOMAIN} -> ${DIST_DOMAIN}",
137
+ "Changes": [
138
+ {
139
+ "Action": "UPSERT",
140
+ "ResourceRecordSet": {
141
+ "Name": "${DOMAIN}.",
142
+ "Type": "A",
143
+ "AliasTarget": {
144
+ "HostedZoneId": "Z2FDTNDATAQYW2",
145
+ "DNSName": "${DIST_DOMAIN}",
146
+ "EvaluateTargetHealth": false
147
+ }
148
+ }
149
+ }
150
+ ]
151
+ }
152
+ EOF
153
+
154
+ echo "Submitting Route53 change to upsert alias for ${DOMAIN} -> ${DIST_DOMAIN}"
155
+ CHANGE_RESULT=$(aws route53 change-resource-record-sets --hosted-zone-id "$HOSTED_ZONE_ID" --change-batch file://"$CHANGE_BATCH")
156
+ echo "$CHANGE_RESULT"
157
+
158
+ echo "Done. Route53 change submitted."
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # predeploy-checks.sh
5
+ # Non-destructive pre-deploy checks that detect common DNS/CloudFront/ACM
6
+ # conflicts which would cause `sst deploy` to fail. Exits non-zero with a
7
+ # helpful diagnostic if a blocking issue is found.
8
+ #
9
+ # Required environment variables:
10
+ # DOMAIN_ROOT — Root domain (e.g., "example.com")
11
+ #
12
+ # Optional environment variables:
13
+ # AWS_REGION — AWS region (default: us-east-1)
14
+ # SST_STAGE — SST stage name (default: production)
15
+ # INFRA_PATH — Path to infra package (default: packages/infra)
16
+ # DOMAIN_PRODUCTION — Override domain for production stage
17
+ # DOMAIN_DEV — Override domain for dev stage
18
+ # DOMAIN_MOBILE — Override domain for mobile stage
19
+ # DOMAIN_CUSTOM — Override domain for custom stages
20
+ # PROJECT_PREFIX — Project prefix for pipeline names (e.g., "myapp")
21
+ # SKIP_PREDEPLOY_CHECKS — Set to "true" to skip all checks
22
+ # SKIP_DNS_CHECK — Set to "true" to skip DNS conflict check
23
+ # AUTO_REMOVE_CONFLICTING_DNS — Set to "true" to auto-remove conflicting DNS records
24
+ # ALLOW_PIPELINE_DESTRUCTIVE — Set to "true" to allow pipeline deletion
25
+
26
+ AWS_REGION=${AWS_REGION:-us-east-1}
27
+ STAGE=${SST_STAGE:-${1:-production}}
28
+ INFRA_PATH=${INFRA_PATH:-packages/infra}
29
+
30
+ if [ -z "${DOMAIN_ROOT:-}" ]; then
31
+ echo "ERROR: DOMAIN_ROOT environment variable is required (e.g., DOMAIN_ROOT=example.com)"
32
+ echo "Set this in your project's environment or buildspec."
33
+ exit 1
34
+ fi
35
+
36
+ # Optional overrides for CI
37
+ SKIP_PREDEPLOY_CHECKS=${SKIP_PREDEPLOY_CHECKS:-false}
38
+ SKIP_DNS_CHECK=${SKIP_DNS_CHECK:-false}
39
+ AUTO_REMOVE_CONFLICTING_DNS=${AUTO_REMOVE_CONFLICTING_DNS:-false}
40
+
41
+ echo "Pre-deploy checks: region=$AWS_REGION stage=$STAGE domain=$DOMAIN_ROOT"
42
+
43
+ check_route53_conflict() {
44
+ local name=$1
45
+ hz=$(aws route53 list-hosted-zones-by-name --dns-name "$DOMAIN_ROOT" --query 'HostedZones[0].Id' --output text 2>/dev/null || true)
46
+ if [ -z "$hz" ]; then
47
+ echo "WARN: Hosted zone for $DOMAIN_ROOT not found in $AWS_REGION"
48
+ return 0
49
+ fi
50
+ hz=${hz##*/}
51
+ hz=${hz##*/}
52
+ # Only treat A/AAAA/CNAME or alias records as blocking records for web deploys.
53
+ # Keep MX/TXT/NS/SOA since they are often required for email/zone setup and
54
+ # should not block CloudFront/website deploys.
55
+ rr=$(aws route53 list-resource-record-sets --hosted-zone-id "$hz" --query "ResourceRecordSets[?Name=='${name}.' && (Type=='A' || Type=='AAAA' || Type=='CNAME' || AliasTarget!=null)]" --output json || echo "[]")
56
+ if [ "$rr" != "[]" ]; then
57
+ echo "FAIL: Route53 A/AAAA/CNAME/ALIAS record exists for ${name} that may block deploy:"
58
+ echo "$rr"
59
+ if [ "${AUTO_REMOVE_CONFLICTING_DNS}" = "true" ]; then
60
+ echo "AUTO_REMOVE_CONFLICTING_DNS=true — attempting to delete conflicting records for ${name}"
61
+ # Delete each matching record set
62
+ echo "$rr" | jq -c '.[]' | while read -r rec; do
63
+ tmp=$(mktemp)
64
+ cat > "$tmp" <<EOF
65
+ {
66
+ "Comment": "DELETE conflicting record for ${name}",
67
+ "Changes": [
68
+ {
69
+ "Action": "DELETE",
70
+ "ResourceRecordSet": $rec
71
+ }
72
+ ]
73
+ }
74
+ EOF
75
+ if aws route53 change-resource-record-sets --hosted-zone-id "$hz" --change-batch file://"$tmp" >/dev/null 2>&1; then
76
+ echo "Deleted conflicting record: $rec"
77
+ else
78
+ echo "Failed to delete conflicting record: $rec" >&2
79
+ fi
80
+ rm -f "$tmp"
81
+ done
82
+ # Give DNS a moment to settle (don't block long)
83
+ sleep 2
84
+ return 0
85
+ fi
86
+ return 2
87
+ fi
88
+ return 0
89
+ }
90
+
91
+ check_cloudfront_alias() {
92
+ local sub=$1
93
+ out=$(aws cloudfront list-distributions --query "DistributionList.Items[?Aliases.Items!=null && contains(Aliases.Items,'${sub}')].[Id,DomainName,Status,Aliases.Items]" --output json || echo "[]")
94
+ if [ "$out" != "[]" ]; then
95
+ echo "WARN: CloudFront distribution already has alias ${sub}:"
96
+ echo "$out"
97
+ echo "This is expected if the site is already deployed by this same SST project."
98
+ return 0
99
+ fi
100
+ return 0
101
+ }
102
+
103
+ check_acm_certificate() {
104
+ local sub=$1
105
+ certs=$(aws acm list-certificates --region "$AWS_REGION" --query "CertificateSummaryList[?DomainName=='${sub}']" --output json || echo "[]")
106
+ if [ "$certs" != "[]" ]; then
107
+ echo "NOTE: ACM certificate(s) exist for ${sub}:"
108
+ echo "$certs"
109
+ fi
110
+ }
111
+
112
+ resolve_stage_domain() {
113
+ case "$STAGE" in
114
+ production) echo "${DOMAIN_PRODUCTION:-${DOMAIN_ROOT}}" ;;
115
+ dev) echo "${DOMAIN_DEV:-dev.${DOMAIN_ROOT}}" ;;
116
+ mobile) echo "${DOMAIN_MOBILE:-mobile.${DOMAIN_ROOT}}" ;;
117
+ *) echo "${DOMAIN_CUSTOM:-${STAGE}.${DOMAIN_ROOT}}" ;;
118
+ esac
119
+ }
120
+
121
+ TARGET_DOMAIN=$(resolve_stage_domain)
122
+
123
+ echo "Checking DNS records and CloudFront aliases..."
124
+
125
+ if [ "${SKIP_PREDEPLOY_CHECKS}" = "true" ]; then
126
+ echo "SKIP_PREDEPLOY_CHECKS=true — skipping DNS/CloudFront/ACM predeploy checks."
127
+ else
128
+ if [ "${SKIP_DNS_CHECK}" != "true" ]; then
129
+ check_route53_conflict "$TARGET_DOMAIN" || exit 2
130
+ elif [ "$STAGE" = "dev" ]; then
131
+ echo "SKIP_DNS_CHECK=true — skipping Route53 conflict check for $TARGET_DOMAIN"
132
+ else
133
+ echo "SKIP_DNS_CHECK=true — skipping Route53 conflict check for $TARGET_DOMAIN"
134
+ fi
135
+
136
+ check_cloudfront_alias "$TARGET_DOMAIN" || exit 2
137
+ check_acm_certificate "$TARGET_DOMAIN"
138
+ fi
139
+
140
+ echo "Pre-deploy checks passed."
141
+
142
+ # ----- Destructive change detection (pipeline protection) -----
143
+ echo "Running infra diff to detect potentially destructive changes..."
144
+ DIFF_OUT=""
145
+ if [ -d "$INFRA_PATH" ]; then
146
+ pushd "$INFRA_PATH" >/dev/null || true
147
+ if command -v npx >/dev/null 2>&1; then
148
+ DIFF_OUT=$(npx sst diff --stage "$STAGE" --no-color 2>&1 || true)
149
+ elif command -v sst >/dev/null 2>&1; then
150
+ DIFF_OUT=$(sst diff --stage "$STAGE" --no-color 2>&1 || true)
151
+ else
152
+ DIFF_OUT=""
153
+ fi
154
+ popd >/dev/null || true
155
+ fi
156
+
157
+ if [ -n "$DIFF_OUT" ]; then
158
+ echo "$DIFF_OUT" | sed -n '1,200p'
159
+ # detect pipeline deletion entries in the diff output
160
+ if echo "$DIFF_OUT" | grep -Ei 'DELETE.*(CodePipeline|AWS::CodePipeline::Pipeline|pipeline)' >/dev/null 2>&1; then
161
+ # compute expected pipeline name for this stage
162
+ PROJECT_PREFIX=${PROJECT_PREFIX:-app}
163
+ if [ "$STAGE" = "production" ]; then
164
+ PIPELINE_NAME="${PROJECT_PREFIX}-prod-pipeline"
165
+ elif [ "$STAGE" = "dev" ]; then
166
+ PIPELINE_NAME="${PROJECT_PREFIX}-dev-pipeline"
167
+ else
168
+ PIPELINE_NAME="${PROJECT_PREFIX}-${STAGE}-pipeline"
169
+ fi
170
+
171
+ ARN=$(aws codepipeline list-pipelines --region "$AWS_REGION" --query "pipelines[?name=='${PIPELINE_NAME}'].pipelineArn" --output text || true)
172
+ PROTECTED_TAG=""
173
+ if [ -n "$ARN" ] && [ "$ARN" != "None" ]; then
174
+ tags_json=$(aws codepipeline list-tags-for-resource --resource-arn "$ARN" --region "$AWS_REGION" --output json 2>/dev/null || echo "[]")
175
+ PROTECTED_TAG=$(echo "$tags_json" | jq -r '.tags[]? | select(.key=="Protected") | .value' 2>/dev/null || echo "")
176
+ fi
177
+
178
+ if [ "$PROTECTED_TAG" = "true" ]; then
179
+ echo "ERROR: planned changes include deletion of protected pipeline '$PIPELINE_NAME'. Aborting." >&2
180
+ exit 4
181
+ else
182
+ if [ "${ALLOW_PIPELINE_DESTRUCTIVE:-false}" != "true" ]; then
183
+ echo "FAIL: planned changes include deletion of pipeline '$PIPELINE_NAME'. To allow this set ALLOW_PIPELINE_DESTRUCTIVE=true and re-run." >&2
184
+ exit 4
185
+ else
186
+ echo "Warning: pipeline deletion detected but ALLOW_PIPELINE_DESTRUCTIVE=true is set — proceeding with caution."
187
+ fi
188
+ fi
189
+ fi
190
+ fi
191
+
192
+ exit 0
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # CI-safe Pulumi deploy helper
5
+ # - runs `pulumi preview` and exits unless `APPROVE=true` is set
6
+ # - in CI, wire a manual approval step to set APPROVE=true before allowing `pulumi up`
7
+
8
+
9
+ STACK=${STACK:-production}
10
+
11
+ # Run from the infra package root so Pulumi (if used) finds Pulumi program files
12
+ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
13
+ INFRA_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
14
+ cd "$INFRA_DIR"
15
+
16
+ echo "Selecting Pulumi stack: ${STACK} (cwd: $PWD)"
17
+ pulumi stack select "${STACK}"
18
+
19
+ echo "Running pulumi preview (non-interactive)"
20
+ pulumi preview --non-interactive
21
+
22
+ if [ "${APPROVE:-}" != "true" ]; then
23
+ echo "Pulumi preview completed. To apply changes set APPROVE=true and re-run this script."
24
+ echo "CI suggestion: add a manual approval action that sets APPROVE=true before invoking this script for production runs."
25
+ exit 0
26
+ fi
27
+
28
+ echo "APPROVE=true detected — running pulumi up"
29
+ pulumi up --yes
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # CI-safe SST deploy helper
5
+ # - runs `npx sst diff` for a readable preview
6
+ # - exits unless `APPROVE=true` is set (to gate destructive changes)
7
+
8
+
9
+ STACK=${STACK:-production}
10
+
11
+ # Run from the infra package root so SST finds sst.config.ts
12
+ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
13
+ INFRA_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
14
+ cd "$INFRA_DIR"
15
+
16
+ echo "Using SST stack/stage: ${STACK} (cwd: $PWD)"
17
+
18
+ echo "Running SST diff (preview of infra changes)"
19
+ # Use npx so script works without a global sst install
20
+ npx sst diff --stage "${STACK}" || true
21
+
22
+ if [ "${APPROVE:-}" != "true" ]; then
23
+ echo "SST diff completed. To apply changes set APPROVE=true and re-run this script."
24
+ echo "CI suggestion: add a manual approval action that sets APPROVE=true before invoking this script for production runs."
25
+ exit 0
26
+ fi
27
+
28
+ echo "APPROVE=true detected — running sst deploy"
29
+ echo "Attempting to clear any stale SST lock for stage ${STACK} (no-op if none)"
30
+ # If a previous run left a lock, unlock it non-interactively. This is safe when you
31
+ # have confirmed no concurrent deploy is running. In CI, ensure this only runs
32
+ # after human approval.
33
+ npx sst unlock --stage "${STACK}" || true
34
+
35
+ # If DOMAIN is provided, check CloudFront distributions for that alias and remove it
36
+ # so SST/Pulumi can create a distribution using the domain. CloudFront is global
37
+ # (us-east-1) so we query there. This step is performed only when APPROVE=true.
38
+ if [ -n "${DOMAIN:-}" ]; then
39
+ echo "Checking CloudFront distributions for alias: ${DOMAIN}"
40
+ # find distribution IDs that include the DOMAIN in their Aliases.Items
41
+ DIST_IDS=$(aws cloudfront list-distributions --query "DistributionList.Items[?contains(Aliases.Items, '${DOMAIN}')].Id" --output text 2>/dev/null || true)
42
+ if [ -n "$DIST_IDS" ]; then
43
+ for DIST_ID in $DIST_IDS; do
44
+ echo "Found CloudFront distribution $DIST_ID that contains alias ${DOMAIN}; removing alias to allow new distribution creation"
45
+ TMP_CFG="/tmp/cf-config-${DIST_ID}.json"
46
+ TMP_CFG_MOD="/tmp/cf-config-${DIST_ID}-mod.json"
47
+ ETAG=$(aws cloudfront get-distribution-config --id "$DIST_ID" --query 'ETag' --output text)
48
+ aws cloudfront get-distribution-config --id "$DIST_ID" --output json > "$TMP_CFG"
49
+ # Use python to safely remove the alias from the DistributionConfig and update Quantity
50
+ python3 - <<PY
51
+ import json,sys
52
+ f='''$TMP_CFG'''
53
+ out='''$TMP_CFG_MOD'''
54
+ domain='''$DOMAIN'''
55
+ obj=json.load(open(f))
56
+ cfg=obj.get('DistributionConfig', {})
57
+ aliases=cfg.get('Aliases', {})
58
+ items=aliases.get('Items') or []
59
+ new_items=[i for i in items if i != domain]
60
+ aliases['Items']=new_items
61
+ aliases['Quantity']=len(new_items)
62
+ cfg['Aliases']=aliases
63
+ json.dump(cfg, open(out, 'w'))
64
+ PY
65
+ # update the distribution with the modified config
66
+ if aws cloudfront update-distribution --id "$DIST_ID" --if-match "$ETAG" --distribution-config file://"$TMP_CFG_MOD" >/dev/null 2>&1; then
67
+ echo "Removed alias ${DOMAIN} from distribution $DIST_ID"
68
+ else
69
+ echo "Failed to remove alias ${DOMAIN} from distribution $DIST_ID; continue and attempt deploy (check permissions)" >&2
70
+ fi
71
+ rm -f "$TMP_CFG" "$TMP_CFG_MOD"
72
+ done
73
+ else
74
+ echo "No existing CloudFront distributions found with alias ${DOMAIN}"
75
+ fi
76
+ fi
77
+
78
+ echo "Running sst deploy"
79
+ npx sst deploy --stage "${STACK}" --yes
@@ -0,0 +1,77 @@
1
+ # buildspec.yml — AWS CodeBuild specification for SST deployments (v1.0.0)
2
+
3
+ version: 0.2
4
+
5
+ env:
6
+ variables:
7
+ SST_STAGE: "dev"
8
+ NODE_VERSION: "22"
9
+ PNPM_VERSION: "9.15.0"
10
+ INFRA_PATH: "__INFRA_PATH__"
11
+
12
+ # Domain + naming defaults
13
+ DOMAIN_ROOT: "__ROOT_DOMAIN__"
14
+ PROJECT_PREFIX: "__PROJECT_PREFIX__"
15
+ PREFIX: "__PROJECT_PREFIX__"
16
+ DOMAIN_PRODUCTION: "__ROOT_DOMAIN__"
17
+ DOMAIN_DEV: "dev.__ROOT_DOMAIN__"
18
+ DOMAIN_MOBILE: "mobile.__ROOT_DOMAIN__"
19
+
20
+ # Check controls
21
+ SKIP_DNS_CHECK: "false"
22
+ AUTO_REMOVE_CONFLICTING_DNS: "true"
23
+ POLL_TIMEOUT: "600"
24
+ POLL_INTERVAL: "15"
25
+
26
+ phases:
27
+ install:
28
+ runtime-versions:
29
+ nodejs: 22
30
+ commands:
31
+ - corepack enable pnpm
32
+ - corepack prepare pnpm@${PNPM_VERSION} --activate
33
+ - pnpm --version
34
+ - pnpm install --frozen-lockfile
35
+
36
+ pre_build:
37
+ commands:
38
+ - echo "Stage = ${SST_STAGE}"
39
+ - echo "Node = $(node --version)"
40
+ - echo "pnpm = $(pnpm --version)"
41
+ - echo "Infra = ${INFRA_PATH}"
42
+ - echo "Running pre-deploy checks..."
43
+ - |
44
+ if [ -f "${INFRA_PATH}/scripts/predeploy-checks.sh" ]; then
45
+ chmod +x "${INFRA_PATH}/scripts/predeploy-checks.sh" || true
46
+ "${INFRA_PATH}/scripts/predeploy-checks.sh" ${SST_STAGE}
47
+ elif [ -f "./node_modules/@lsts_tech/infra/scripts/predeploy-checks.sh" ]; then
48
+ chmod +x "./node_modules/@lsts_tech/infra/scripts/predeploy-checks.sh" || true
49
+ ./node_modules/@lsts_tech/infra/scripts/predeploy-checks.sh ${SST_STAGE}
50
+ else
51
+ echo "No predeploy checks script found; skipping"
52
+ fi
53
+
54
+ build:
55
+ commands:
56
+ - cd ${INFRA_PATH}
57
+ - npx sst deploy --stage ${SST_STAGE}
58
+
59
+ post_build:
60
+ commands:
61
+ - echo "SST deploy completed for stage ${SST_STAGE}"
62
+ - echo "Running post-deploy DNS sync"
63
+ - |
64
+ if [ -f "${INFRA_PATH}/scripts/postdeploy-update-dns.sh" ]; then
65
+ chmod +x "${INFRA_PATH}/scripts/postdeploy-update-dns.sh" || true
66
+ "${INFRA_PATH}/scripts/postdeploy-update-dns.sh" ${SST_STAGE} || echo "post-deploy DNS sync failed"
67
+ elif [ -f "./node_modules/@lsts_tech/infra/scripts/postdeploy-update-dns.sh" ]; then
68
+ chmod +x "./node_modules/@lsts_tech/infra/scripts/postdeploy-update-dns.sh" || true
69
+ ./node_modules/@lsts_tech/infra/scripts/postdeploy-update-dns.sh ${SST_STAGE} || echo "post-deploy DNS sync failed"
70
+ else
71
+ echo "No post-deploy DNS sync script found; skipping"
72
+ fi
73
+
74
+ cache:
75
+ paths:
76
+ - /root/.pnpm-store/**/*
77
+ - __INFRA_PATH__/.sst/**/*
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # ensure-pipelines.sh (template)
5
+ #
6
+ # AWS-only helper (v1.0.0) to ensure configured CodePipelines exist.
7
+ # Missing pipelines are created by triggering an SST deploy for their mapped stage.
8
+ #
9
+ # Usage:
10
+ # APPROVE=true bash scripts/ensure-pipelines.sh
11
+
12
+ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
13
+ INFRA_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
14
+
15
+ if [ -f "$INFRA_DIR/.env" ]; then
16
+ set -a
17
+ # shellcheck disable=SC1091
18
+ source "$INFRA_DIR/.env"
19
+ set +a
20
+ fi
21
+
22
+ DOMAIN_ROOT=${DOMAIN_ROOT:-${INFRA_ROOT_DOMAIN:-}}
23
+ if [ -z "$DOMAIN_ROOT" ]; then
24
+ echo "ERROR: Set DOMAIN_ROOT or INFRA_ROOT_DOMAIN (e.g., example.com)" >&2
25
+ exit 1
26
+ fi
27
+
28
+ REGION=${AWS_REGION:-us-east-1}
29
+ REPO_DEFAULT=${INFRA_PIPELINE_REPO:-__PIPELINE_REPO__}
30
+
31
+ # Pipeline definitions (generated by init)
32
+ declare -A PIPELINE_STAGE=(
33
+ __PIPELINE_STAGE_MAP__
34
+ )
35
+ declare -A PIPELINE_REPO=(
36
+ __PIPELINE_REPO_MAP__
37
+ )
38
+ declare -A PIPELINE_BRANCH=(
39
+ __PIPELINE_BRANCH_MAP__
40
+ )
41
+
42
+ resolve_sst_deploy_script() {
43
+ local candidates=(
44
+ "$SCRIPT_DIR/sst-deploy.sh"
45
+ "$INFRA_DIR/node_modules/@lsts_tech/infra/scripts/sst-deploy.sh"
46
+ "$INFRA_DIR/../node_modules/@lsts_tech/infra/scripts/sst-deploy.sh"
47
+ )
48
+
49
+ for candidate in "${candidates[@]}"; do
50
+ if [ -f "$candidate" ]; then
51
+ echo "$candidate"
52
+ return 0
53
+ fi
54
+ done
55
+
56
+ return 1
57
+ }
58
+
59
+ if ! command -v aws >/dev/null 2>&1; then
60
+ echo "aws CLI not found in PATH" >&2
61
+ exit 1
62
+ fi
63
+
64
+ if [ ${#PIPELINE_STAGE[@]} -eq 0 ]; then
65
+ echo "No pipelines configured in scripts/ensure-pipelines.sh. Nothing to do."
66
+ exit 0
67
+ fi
68
+
69
+ SST_DEPLOY_SCRIPT=${SST_DEPLOY_SCRIPT:-}
70
+ if [ -z "$SST_DEPLOY_SCRIPT" ]; then
71
+ SST_DEPLOY_SCRIPT=$(resolve_sst_deploy_script || true)
72
+ fi
73
+
74
+ if [ -z "$SST_DEPLOY_SCRIPT" ]; then
75
+ echo "Could not locate sst-deploy.sh. Set SST_DEPLOY_SCRIPT explicitly." >&2
76
+ exit 1
77
+ fi
78
+
79
+ cd "$INFRA_DIR"
80
+
81
+ MISSING=()
82
+ for NAME in "${!PIPELINE_STAGE[@]}"; do
83
+ PIPELINE_NAME="${NAME}-pipeline"
84
+ echo "Checking for pipeline names: $NAME or $PIPELINE_NAME (region: $REGION)"
85
+
86
+ if aws codepipeline get-pipeline --name "$NAME" --region "$REGION" >/dev/null 2>&1 || \
87
+ aws codepipeline get-pipeline --name "$PIPELINE_NAME" --region "$REGION" >/dev/null 2>&1; then
88
+ echo "Pipeline exists: $PIPELINE_NAME"
89
+ else
90
+ echo "Pipeline missing: $PIPELINE_NAME"
91
+ MISSING+=("$NAME")
92
+ fi
93
+ done
94
+
95
+ if [ ${#MISSING[@]} -eq 0 ]; then
96
+ echo "All pipelines present; nothing to do."
97
+ exit 0
98
+ fi
99
+
100
+ echo "Missing pipelines: ${MISSING[*]}"
101
+
102
+ if [ "${APPROVE:-}" != "true" ]; then
103
+ echo "To create missing pipelines, re-run with APPROVE=true."
104
+ exit 1
105
+ fi
106
+
107
+ for NAME in "${MISSING[@]}"; do
108
+ STAGE=${PIPELINE_STAGE[$NAME]:-production}
109
+ REPO=${PIPELINE_REPO[$NAME]:-$REPO_DEFAULT}
110
+ BRANCH=${PIPELINE_BRANCH[$NAME]:-main}
111
+
112
+ echo "Creating pipeline '$NAME' via SST deploy (stage: $STAGE, repo: $REPO, branch: $BRANCH)"
113
+ APPROVE=true STACK="$STAGE" bash "$SST_DEPLOY_SCRIPT"
114
+ done
115
+
116
+ echo "Done. Current pipelines:"
117
+ aws codepipeline list-pipelines --region "$REGION" --query "pipelines[].name" --output table || true