@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.
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/bin/init.d.ts +9 -0
- package/dist/bin/init.d.ts.map +1 -0
- package/dist/bin/init.js +315 -0
- package/dist/bin/init.js.map +1 -0
- package/dist/stacks/Dns.d.ts +69 -0
- package/dist/stacks/Dns.d.ts.map +1 -0
- package/dist/stacks/Dns.js +57 -0
- package/dist/stacks/Dns.js.map +1 -0
- package/dist/stacks/ExpoSite.d.ts +72 -0
- package/dist/stacks/ExpoSite.d.ts.map +1 -0
- package/dist/stacks/ExpoSite.js +49 -0
- package/dist/stacks/ExpoSite.js.map +1 -0
- package/dist/stacks/NextSite.d.ts +86 -0
- package/dist/stacks/NextSite.d.ts.map +1 -0
- package/dist/stacks/NextSite.js +60 -0
- package/dist/stacks/NextSite.js.map +1 -0
- package/dist/stacks/Pipeline.d.ts +128 -0
- package/dist/stacks/Pipeline.d.ts.map +1 -0
- package/dist/stacks/Pipeline.js +311 -0
- package/dist/stacks/Pipeline.js.map +1 -0
- package/dist/stacks/index.d.ts +41 -0
- package/dist/stacks/index.d.ts.map +1 -0
- package/dist/stacks/index.js +38 -0
- package/dist/stacks/index.js.map +1 -0
- package/docs/CLI.md +59 -0
- package/docs/CONFIGURATION.md +78 -0
- package/docs/EXAMPLES.md +9 -0
- package/examples/next-and-expo/infra.config.ts +104 -0
- package/examples/next-only/infra.config.ts +60 -0
- package/package.json +102 -0
- package/schemas/pipeline.schema.json +25 -0
- package/scripts/cleanup-orphan-lambdas.sh +102 -0
- package/scripts/delete-amplify-app.sh +50 -0
- package/scripts/ensure-pipelines.sh +144 -0
- package/scripts/ensure-secrets.sh +58 -0
- package/scripts/postdeploy-update-dns.sh +158 -0
- package/scripts/predeploy-checks.sh +192 -0
- package/scripts/pulumi-deploy.sh +29 -0
- package/scripts/sst-deploy.sh +79 -0
- package/templates/buildspec.yml +77 -0
- package/templates/ensure-pipelines.sh +117 -0
- package/templates/env.example +38 -0
- package/templates/infra.config.ts +199 -0
- package/templates/secrets.schema.json +20 -0
- package/templates/sst-env.d.ts +50 -0
- 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
|