@lsts_tech/infra 1.0.0 → 1.0.2
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/README.md +58 -70
- package/dist/bin/init.d.ts +4 -3
- package/dist/bin/init.d.ts.map +1 -1
- package/dist/bin/init.js +619 -117
- package/dist/bin/init.js.map +1 -1
- package/dist/src/auth/index.d.ts +17 -0
- package/dist/src/auth/index.d.ts.map +1 -0
- package/dist/src/auth/index.js +18 -0
- package/dist/src/auth/index.js.map +1 -0
- package/dist/stacks/Dns.d.ts +24 -14
- package/dist/stacks/Dns.d.ts.map +1 -1
- package/dist/stacks/Dns.js +69 -18
- package/dist/stacks/Dns.js.map +1 -1
- package/dist/stacks/Pipeline.d.ts +7 -0
- package/dist/stacks/Pipeline.d.ts.map +1 -1
- package/dist/stacks/Pipeline.js +60 -7
- package/dist/stacks/Pipeline.js.map +1 -1
- package/docs/CLI.md +58 -15
- package/docs/CONFIGURATION.md +73 -30
- package/docs/EXAMPLES.md +5 -1
- package/examples/delegated-subdomain/infra.config.ts +102 -0
- package/examples/next-and-expo/infra.config.ts +33 -28
- package/examples/next-only/infra.config.ts +35 -22
- package/package.json +10 -4
- package/scripts/ensure-pipelines.sh +151 -43
- package/scripts/postdeploy-update-dns.sh +42 -11
- package/scripts/predeploy-checks.sh +38 -5
- package/templates/buildspec.yml +23 -0
- package/templates/ensure-pipelines.sh +157 -22
- package/templates/env.example +15 -0
- package/templates/infra.config.expo-web.ts +153 -0
- package/templates/infra.config.next-only.ts +159 -0
- package/templates/infra.config.ts +21 -4
- package/templates/pipelines.example.json +19 -0
- package/templates/private.example.json +13 -0
- package/templates/scaffold.gitignore +29 -0
- package/templates/scaffold.package.json +25 -0
- package/templates/scaffold.tsconfig.json +22 -0
- package/templates/secrets.schema.expo-web.json +8 -0
|
@@ -4,7 +4,9 @@ set -euo pipefail
|
|
|
4
4
|
# ensure-pipelines.sh
|
|
5
5
|
#
|
|
6
6
|
# AWS-only helper (v1.0.0) to ensure configured CodePipelines exist.
|
|
7
|
-
# Missing pipelines are created by triggering an
|
|
7
|
+
# Missing pipelines are created by triggering an explicit production deploy with:
|
|
8
|
+
# INFRA_CREATE_PIPELINES=true
|
|
9
|
+
# INFRA_PIPELINES=<missing-stage>
|
|
8
10
|
#
|
|
9
11
|
# Usage:
|
|
10
12
|
# APPROVE=true bash scripts/ensure-pipelines.sh
|
|
@@ -25,47 +27,145 @@ if [ -z "$DOMAIN_ROOT" ]; then
|
|
|
25
27
|
exit 1
|
|
26
28
|
fi
|
|
27
29
|
|
|
30
|
+
if ! command -v aws >/dev/null 2>&1; then
|
|
31
|
+
echo "aws CLI not found in PATH" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
36
|
+
echo "node is required for parsing pipeline config" >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
28
40
|
REGION=${AWS_REGION:-us-east-1}
|
|
29
41
|
PIPELINE_PREFIX=${INFRA_PIPELINE_PREFIX:-myapp}
|
|
30
|
-
|
|
42
|
+
PIPELINES_RAW=${INFRA_PIPELINES:-production,dev}
|
|
43
|
+
PIPELINES_CONFIG_PATH=${INFRA_PIPELINES_CONFIG_PATH:-$INFRA_DIR/config/pipelines.json}
|
|
31
44
|
REPO_DEFAULT=${INFRA_PIPELINE_REPO:-myorg/myrepo}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
|
|
46
|
+
BRANCH_PROD_DEFAULT=${INFRA_PIPELINE_BRANCH_PROD:-main}
|
|
47
|
+
BRANCH_DEV_DEFAULT=${INFRA_PIPELINE_BRANCH_DEV:-develop}
|
|
48
|
+
BRANCH_MOBILE_DEFAULT=${INFRA_PIPELINE_BRANCH_MOBILE:-mobile}
|
|
49
|
+
|
|
50
|
+
declare -A STAGE_ENABLED=(
|
|
51
|
+
[production]=false
|
|
52
|
+
[dev]=false
|
|
53
|
+
[mobile]=false
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
declare -A STAGE_BRANCH=(
|
|
57
|
+
[production]="$BRANCH_PROD_DEFAULT"
|
|
58
|
+
[dev]="$BRANCH_DEV_DEFAULT"
|
|
59
|
+
[mobile]="$BRANCH_MOBILE_DEFAULT"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
declare -A STAGE_REPO=(
|
|
63
|
+
[production]="$REPO_DEFAULT"
|
|
64
|
+
[dev]="$REPO_DEFAULT"
|
|
65
|
+
[mobile]="$REPO_DEFAULT"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
declare -A STAGE_SUFFIX=(
|
|
69
|
+
[production]="prod"
|
|
70
|
+
[dev]="dev"
|
|
71
|
+
[mobile]="mobile"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
normalize_stage() {
|
|
75
|
+
local input
|
|
76
|
+
input=$(echo "$1" | tr '[:upper:]' '[:lower:]' | xargs)
|
|
77
|
+
case "$input" in
|
|
78
|
+
prod) echo "production" ;;
|
|
79
|
+
production|dev|mobile) echo "$input" ;;
|
|
80
|
+
*) echo "" ;;
|
|
81
|
+
esac
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for token in $(echo "$PIPELINES_RAW" | tr ',' ' '); do
|
|
85
|
+
stage=$(normalize_stage "$token")
|
|
86
|
+
if [ -n "$stage" ]; then
|
|
87
|
+
STAGE_ENABLED["$stage"]=true
|
|
88
|
+
fi
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
if [ -f "$PIPELINES_CONFIG_PATH" ]; then
|
|
92
|
+
echo "Loading runtime pipeline config from $PIPELINES_CONFIG_PATH"
|
|
93
|
+
while IFS=$'\t' read -r stage enabled branch repo; do
|
|
94
|
+
stage=$(normalize_stage "$stage")
|
|
95
|
+
if [ -z "$stage" ]; then
|
|
96
|
+
continue
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
if [ "$enabled" = "true" ]; then
|
|
100
|
+
STAGE_ENABLED["$stage"]=true
|
|
101
|
+
elif [ "$enabled" = "false" ]; then
|
|
102
|
+
STAGE_ENABLED["$stage"]=false
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
if [ -n "$branch" ]; then
|
|
106
|
+
STAGE_BRANCH["$stage"]="$branch"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
if [ -n "$repo" ]; then
|
|
110
|
+
STAGE_REPO["$stage"]="$repo"
|
|
111
|
+
fi
|
|
112
|
+
done < <(node -e '
|
|
113
|
+
const fs = require("fs");
|
|
114
|
+
const path = process.argv[1];
|
|
115
|
+
const raw = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
116
|
+
const src = raw.pipelines ?? raw;
|
|
117
|
+
function toStage(v) {
|
|
118
|
+
const n = String(v || "").trim().toLowerCase();
|
|
119
|
+
if (n === "prod") return "production";
|
|
120
|
+
if (n === "production" || n === "dev" || n === "mobile") return n;
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
function emit(stage, cfg) {
|
|
124
|
+
const enabled = cfg && typeof cfg.enabled !== "undefined" ? Boolean(cfg.enabled) : true;
|
|
125
|
+
const branch = cfg && typeof cfg.branch === "string" ? cfg.branch.trim() : "";
|
|
126
|
+
const repo = cfg && typeof cfg.repo === "string" ? cfg.repo.trim() : "";
|
|
127
|
+
process.stdout.write([stage, String(enabled), branch, repo].join("\t") + "\n");
|
|
128
|
+
}
|
|
129
|
+
if (Array.isArray(src)) {
|
|
130
|
+
for (const item of src) {
|
|
131
|
+
const stage = toStage(item && item.stage);
|
|
132
|
+
if (!stage) continue;
|
|
133
|
+
emit(stage, item || {});
|
|
134
|
+
}
|
|
135
|
+
} else if (src && typeof src === "object") {
|
|
136
|
+
for (const [key, value] of Object.entries(src)) {
|
|
137
|
+
const stage = toStage(key);
|
|
138
|
+
if (!stage) continue;
|
|
139
|
+
emit(stage, value || {});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
' "$PIPELINES_CONFIG_PATH")
|
|
143
|
+
else
|
|
144
|
+
echo "Runtime pipeline config not found at $PIPELINES_CONFIG_PATH; using env defaults"
|
|
145
|
+
fi
|
|
35
146
|
|
|
36
147
|
declare -A PIPELINE_STAGE=()
|
|
37
148
|
declare -A PIPELINE_REPO=()
|
|
38
149
|
declare -A PIPELINE_BRANCH=()
|
|
39
150
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
local branch=${2:-main}
|
|
44
|
-
|
|
45
|
-
if [ "$stage" = "production" ]; then
|
|
46
|
-
name="${PIPELINE_PREFIX}-prod"
|
|
151
|
+
for stage in production dev mobile; do
|
|
152
|
+
if [ "${STAGE_ENABLED[$stage]}" != "true" ]; then
|
|
153
|
+
continue
|
|
47
154
|
fi
|
|
48
155
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
PIPELINE_BRANCH["$name"]="$branch"
|
|
52
|
-
}
|
|
156
|
+
suffix=${STAGE_SUFFIX[$stage]}
|
|
157
|
+
name="${PIPELINE_PREFIX}-${suffix}"
|
|
53
158
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
case "$stage" in
|
|
58
|
-
production) add_pipeline "production" "$BRANCH_PROD" ;;
|
|
59
|
-
dev) add_pipeline "dev" "$BRANCH_DEV" ;;
|
|
60
|
-
mobile) add_pipeline "mobile" "$BRANCH_MOBILE" ;;
|
|
61
|
-
""|none) ;;
|
|
62
|
-
*)
|
|
63
|
-
# Allow custom stage names while defaulting to main branch.
|
|
64
|
-
add_pipeline "$stage" "main"
|
|
65
|
-
;;
|
|
66
|
-
esac
|
|
159
|
+
PIPELINE_STAGE["$name"]="$stage"
|
|
160
|
+
PIPELINE_REPO["$name"]="${STAGE_REPO[$stage]}"
|
|
161
|
+
PIPELINE_BRANCH["$name"]="${STAGE_BRANCH[$stage]}"
|
|
67
162
|
done
|
|
68
163
|
|
|
164
|
+
if [ ${#PIPELINE_STAGE[@]} -eq 0 ]; then
|
|
165
|
+
echo "No pipelines configured. Set INFRA_PIPELINES and/or config/pipelines.json."
|
|
166
|
+
exit 0
|
|
167
|
+
fi
|
|
168
|
+
|
|
69
169
|
resolve_sst_deploy_script() {
|
|
70
170
|
local candidates=(
|
|
71
171
|
"$SCRIPT_DIR/sst-deploy.sh"
|
|
@@ -83,16 +183,6 @@ resolve_sst_deploy_script() {
|
|
|
83
183
|
return 1
|
|
84
184
|
}
|
|
85
185
|
|
|
86
|
-
if ! command -v aws >/dev/null 2>&1; then
|
|
87
|
-
echo "aws CLI not found in PATH" >&2
|
|
88
|
-
exit 1
|
|
89
|
-
fi
|
|
90
|
-
|
|
91
|
-
if [ ${#PIPELINE_STAGE[@]} -eq 0 ]; then
|
|
92
|
-
echo "No pipelines configured in scripts/ensure-pipelines.sh. Nothing to do."
|
|
93
|
-
exit 0
|
|
94
|
-
fi
|
|
95
|
-
|
|
96
186
|
SST_DEPLOY_SCRIPT=${SST_DEPLOY_SCRIPT:-}
|
|
97
187
|
if [ -z "$SST_DEPLOY_SCRIPT" ]; then
|
|
98
188
|
SST_DEPLOY_SCRIPT=$(resolve_sst_deploy_script || true)
|
|
@@ -131,13 +221,31 @@ if [ "${APPROVE:-}" != "true" ]; then
|
|
|
131
221
|
exit 1
|
|
132
222
|
fi
|
|
133
223
|
|
|
224
|
+
declare -A PROCESSED_STAGE=()
|
|
134
225
|
for NAME in "${MISSING[@]}"; do
|
|
135
|
-
STAGE=${PIPELINE_STAGE[$NAME]
|
|
226
|
+
STAGE=${PIPELINE_STAGE[$NAME]}
|
|
227
|
+
|
|
228
|
+
if [ "${PROCESSED_STAGE[$STAGE]:-false}" = "true" ]; then
|
|
229
|
+
continue
|
|
230
|
+
fi
|
|
231
|
+
|
|
136
232
|
REPO=${PIPELINE_REPO[$NAME]:-$REPO_DEFAULT}
|
|
137
|
-
BRANCH=${PIPELINE_BRANCH[$NAME]:-main}
|
|
138
233
|
|
|
139
|
-
echo "Creating pipeline '$
|
|
140
|
-
|
|
234
|
+
echo "Creating pipeline for stage '$STAGE' via production deploy"
|
|
235
|
+
echo " repo : $REPO"
|
|
236
|
+
echo " branch : ${PIPELINE_BRANCH[$NAME]}"
|
|
237
|
+
|
|
238
|
+
APPROVE=true \
|
|
239
|
+
STACK="production" \
|
|
240
|
+
INFRA_CREATE_PIPELINES=true \
|
|
241
|
+
INFRA_PIPELINES="$STAGE" \
|
|
242
|
+
INFRA_PIPELINE_REPO="$REPO" \
|
|
243
|
+
INFRA_PIPELINE_BRANCH_PROD="${STAGE_BRANCH[production]}" \
|
|
244
|
+
INFRA_PIPELINE_BRANCH_DEV="${STAGE_BRANCH[dev]}" \
|
|
245
|
+
INFRA_PIPELINE_BRANCH_MOBILE="${STAGE_BRANCH[mobile]}" \
|
|
246
|
+
bash "$SST_DEPLOY_SCRIPT"
|
|
247
|
+
|
|
248
|
+
PROCESSED_STAGE[$STAGE]=true
|
|
141
249
|
done
|
|
142
250
|
|
|
143
251
|
echo "Done. Current pipelines:"
|
|
@@ -38,6 +38,33 @@ DOMAIN=$(resolve_stage_domain)
|
|
|
38
38
|
|
|
39
39
|
echo "Post-deploy DNS sync: stage=$STAGE domain=$DOMAIN region=$REGION"
|
|
40
40
|
|
|
41
|
+
# Resolve the best hosted zone by progressively walking up parent domains.
|
|
42
|
+
# Example: dev.airs.alternun.co -> airs.alternun.co -> alternun.co
|
|
43
|
+
find_hosted_zone() {
|
|
44
|
+
local domain=$1
|
|
45
|
+
local clean=${domain%.}
|
|
46
|
+
IFS='.' read -r -a labels <<< "$clean"
|
|
47
|
+
|
|
48
|
+
if [ "${#labels[@]}" -lt 2 ]; then
|
|
49
|
+
return 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
for ((i=0; i<=${#labels[@]}-2; i++)); do
|
|
53
|
+
local candidate
|
|
54
|
+
candidate=$(IFS='.'; echo "${labels[*]:i}")
|
|
55
|
+
local hz
|
|
56
|
+
hz=$(aws route53 list-hosted-zones-by-name --dns-name "$candidate" --query "HostedZones[?Name=='${candidate}.']|[0].Id" --output text 2>/dev/null || true)
|
|
57
|
+
if [ -n "$hz" ] && [ "$hz" != "None" ]; then
|
|
58
|
+
hz=${hz##*/}
|
|
59
|
+
hz=${hz##*/}
|
|
60
|
+
echo "${hz}|${candidate}"
|
|
61
|
+
return 0
|
|
62
|
+
fi
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
return 1
|
|
66
|
+
}
|
|
67
|
+
|
|
41
68
|
# Find and wait for the CloudFront distribution that has this alias
|
|
42
69
|
echo "Searching for CloudFront distribution with alias ${DOMAIN} (will wait up to ${POLL_TIMEOUT}s)..."
|
|
43
70
|
END=$((SECONDS + POLL_TIMEOUT))
|
|
@@ -88,13 +115,14 @@ if [ -z "$FOUND_DIST_ID" ]; then
|
|
|
88
115
|
|
|
89
116
|
# Route53 hosted zone and apex records
|
|
90
117
|
echo "\n-- Route53 apex records for ${DOMAIN_ROOT} --"
|
|
91
|
-
HZ=$(
|
|
92
|
-
if [ -n "$HZ" ]
|
|
93
|
-
HZ_ID=${HZ
|
|
94
|
-
|
|
118
|
+
HZ=$(find_hosted_zone "${DOMAIN}" || true)
|
|
119
|
+
if [ -n "$HZ" ]; then
|
|
120
|
+
HZ_ID=${HZ%%|*}
|
|
121
|
+
HZ_NAME=${HZ##*|}
|
|
122
|
+
echo "Hosted zone ID: $HZ_ID (zone: $HZ_NAME)"
|
|
95
123
|
aws route53 list-resource-record-sets --hosted-zone-id "$HZ_ID" --query "ResourceRecordSets[?Name=='${DOMAIN}.']" --output json || true
|
|
96
124
|
else
|
|
97
|
-
echo "No hosted zone for ${
|
|
125
|
+
echo "No hosted zone for ${DOMAIN} (or parent domains) found in this account"
|
|
98
126
|
fi
|
|
99
127
|
|
|
100
128
|
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."
|
|
@@ -120,15 +148,18 @@ if [ -z "$DIST_DOMAIN" ] || [ "$DIST_DOMAIN" = "null" ]; then
|
|
|
120
148
|
fi
|
|
121
149
|
fi
|
|
122
150
|
|
|
123
|
-
# Lookup hosted zone id
|
|
124
|
-
|
|
125
|
-
if [ -z "$
|
|
126
|
-
echo "Hosted zone for ${
|
|
151
|
+
# Lookup hosted zone id by exact domain, then parent fallback
|
|
152
|
+
HOSTED_ZONE_INFO=$(find_hosted_zone "${DOMAIN}" || true)
|
|
153
|
+
if [ -z "$HOSTED_ZONE_INFO" ]; then
|
|
154
|
+
echo "Hosted zone for ${DOMAIN} (or parent domains) not found. Exiting." >&2
|
|
127
155
|
exit 1
|
|
128
156
|
fi
|
|
129
157
|
|
|
130
|
-
|
|
131
|
-
|
|
158
|
+
HOSTED_ZONE_ID=${HOSTED_ZONE_INFO%%|*}
|
|
159
|
+
HOSTED_ZONE_NAME=${HOSTED_ZONE_INFO##*|}
|
|
160
|
+
if [ "$HOSTED_ZONE_NAME" != "$DOMAIN_ROOT" ]; then
|
|
161
|
+
echo "Using parent hosted zone ${HOSTED_ZONE_NAME} for ${DOMAIN}"
|
|
162
|
+
fi
|
|
132
163
|
|
|
133
164
|
CHANGE_BATCH=$(mktemp)
|
|
134
165
|
cat > "$CHANGE_BATCH" <<EOF
|
|
@@ -40,15 +40,48 @@ AUTO_REMOVE_CONFLICTING_DNS=${AUTO_REMOVE_CONFLICTING_DNS:-false}
|
|
|
40
40
|
|
|
41
41
|
echo "Pre-deploy checks: region=$AWS_REGION stage=$STAGE domain=$DOMAIN_ROOT"
|
|
42
42
|
|
|
43
|
+
# Resolve the best hosted zone by progressively walking up parent domains.
|
|
44
|
+
# Example: dev.airs.alternun.co -> airs.alternun.co -> alternun.co
|
|
45
|
+
find_hosted_zone() {
|
|
46
|
+
local domain=$1
|
|
47
|
+
local clean=${domain%.}
|
|
48
|
+
IFS='.' read -r -a labels <<< "$clean"
|
|
49
|
+
|
|
50
|
+
if [ "${#labels[@]}" -lt 2 ]; then
|
|
51
|
+
return 1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
for ((i=0; i<=${#labels[@]}-2; i++)); do
|
|
55
|
+
local candidate
|
|
56
|
+
candidate=$(IFS='.'; echo "${labels[*]:i}")
|
|
57
|
+
local hz
|
|
58
|
+
hz=$(aws route53 list-hosted-zones-by-name --dns-name "$candidate" --query "HostedZones[?Name=='${candidate}.']|[0].Id" --output text 2>/dev/null || true)
|
|
59
|
+
if [ -n "$hz" ] && [ "$hz" != "None" ]; then
|
|
60
|
+
hz=${hz##*/}
|
|
61
|
+
hz=${hz##*/}
|
|
62
|
+
echo "${hz}|${candidate}"
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
done
|
|
66
|
+
|
|
67
|
+
return 1
|
|
68
|
+
}
|
|
69
|
+
|
|
43
70
|
check_route53_conflict() {
|
|
44
71
|
local name=$1
|
|
45
|
-
hz
|
|
46
|
-
|
|
47
|
-
|
|
72
|
+
local hz
|
|
73
|
+
local zone_domain
|
|
74
|
+
local resolved
|
|
75
|
+
resolved=$(find_hosted_zone "$name" || true)
|
|
76
|
+
if [ -z "$resolved" ]; then
|
|
77
|
+
echo "WARN: Hosted zone for $name (or parent) not found in $AWS_REGION"
|
|
48
78
|
return 0
|
|
49
79
|
fi
|
|
50
|
-
hz=${
|
|
51
|
-
|
|
80
|
+
hz=${resolved%%|*}
|
|
81
|
+
zone_domain=${resolved##*|}
|
|
82
|
+
if [ "$zone_domain" != "$DOMAIN_ROOT" ]; then
|
|
83
|
+
echo "INFO: Using parent hosted zone '$zone_domain' for record '$name'"
|
|
84
|
+
fi
|
|
52
85
|
# Only treat A/AAAA/CNAME or alias records as blocking records for web deploys.
|
|
53
86
|
# Keep MX/TXT/NS/SOA since they are often required for email/zone setup and
|
|
54
87
|
# should not block CloudFront/website deploys.
|
package/templates/buildspec.yml
CHANGED
|
@@ -17,6 +17,11 @@ env:
|
|
|
17
17
|
DOMAIN_DEV: "dev.__ROOT_DOMAIN__"
|
|
18
18
|
DOMAIN_MOBILE: "mobile.__ROOT_DOMAIN__"
|
|
19
19
|
|
|
20
|
+
# Pipeline safety defaults
|
|
21
|
+
INFRA_CREATE_PIPELINES: "false"
|
|
22
|
+
INFRA_PIPELINE_PERMISSIONS_MODE: "__PIPELINE_PERMISSIONS_MODE__"
|
|
23
|
+
INFRA_PIPELINES_CONFIG_PATH: "config/pipelines.json"
|
|
24
|
+
|
|
20
25
|
# Check controls
|
|
21
26
|
SKIP_DNS_CHECK: "false"
|
|
22
27
|
AUTO_REMOVE_CONFLICTING_DNS: "true"
|
|
@@ -39,6 +44,19 @@ phases:
|
|
|
39
44
|
- echo "Node = $(node --version)"
|
|
40
45
|
- echo "pnpm = $(pnpm --version)"
|
|
41
46
|
- echo "Infra = ${INFRA_PATH}"
|
|
47
|
+
- echo "Running infra doctor"
|
|
48
|
+
- |
|
|
49
|
+
if [ -f "${INFRA_PATH}/dist/bin/init.js" ]; then
|
|
50
|
+
node "${INFRA_PATH}/dist/bin/init.js" doctor --target "${INFRA_PATH}" || (echo "infra doctor failed" && exit 1)
|
|
51
|
+
elif [ -f "${INFRA_PATH}/bin/init.ts" ]; then
|
|
52
|
+
echo "Building local infra CLI for doctor..."
|
|
53
|
+
(cd "${INFRA_PATH}" && pnpm run build)
|
|
54
|
+
node "${INFRA_PATH}/dist/bin/init.js" doctor --target "${INFRA_PATH}" || (echo "infra doctor failed" && exit 1)
|
|
55
|
+
elif [ -f "./node_modules/@lsts_tech/infra/dist/bin/init.js" ]; then
|
|
56
|
+
node "./node_modules/@lsts_tech/infra/dist/bin/init.js" doctor --target "${INFRA_PATH}" || (echo "infra doctor failed" && exit 1)
|
|
57
|
+
else
|
|
58
|
+
echo "Could not locate an infra doctor binary." && exit 1
|
|
59
|
+
fi
|
|
42
60
|
- echo "Running pre-deploy checks..."
|
|
43
61
|
- |
|
|
44
62
|
if [ -f "${INFRA_PATH}/scripts/predeploy-checks.sh" ]; then
|
|
@@ -58,6 +76,11 @@ phases:
|
|
|
58
76
|
|
|
59
77
|
post_build:
|
|
60
78
|
commands:
|
|
79
|
+
- |
|
|
80
|
+
if [ "${CODEBUILD_BUILD_SUCCEEDING:-0}" != "1" ]; then
|
|
81
|
+
echo "Build failed; skipping post-deploy DNS sync."
|
|
82
|
+
exit 0
|
|
83
|
+
fi
|
|
61
84
|
- echo "SST deploy completed for stage ${SST_STAGE}"
|
|
62
85
|
- echo "Running post-deploy DNS sync"
|
|
63
86
|
- |
|
|
@@ -4,7 +4,9 @@ set -euo pipefail
|
|
|
4
4
|
# ensure-pipelines.sh (template)
|
|
5
5
|
#
|
|
6
6
|
# AWS-only helper (v1.0.0) to ensure configured CodePipelines exist.
|
|
7
|
-
# Missing pipelines are created by triggering an
|
|
7
|
+
# Missing pipelines are created by triggering an explicit production deploy with:
|
|
8
|
+
# INFRA_CREATE_PIPELINES=true
|
|
9
|
+
# INFRA_PIPELINES=<missing-stage>
|
|
8
10
|
#
|
|
9
11
|
# Usage:
|
|
10
12
|
# APPROVE=true bash scripts/ensure-pipelines.sh
|
|
@@ -25,20 +27,145 @@ if [ -z "$DOMAIN_ROOT" ]; then
|
|
|
25
27
|
exit 1
|
|
26
28
|
fi
|
|
27
29
|
|
|
30
|
+
if ! command -v aws >/dev/null 2>&1; then
|
|
31
|
+
echo "aws CLI not found in PATH" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
36
|
+
echo "node is required for parsing pipeline config" >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
28
40
|
REGION=${AWS_REGION:-us-east-1}
|
|
41
|
+
PIPELINE_PREFIX=${INFRA_PIPELINE_PREFIX:-__PROJECT_PREFIX__}
|
|
42
|
+
PIPELINES_RAW=${INFRA_PIPELINES:-__PIPELINES_DEFAULT__}
|
|
43
|
+
PIPELINES_CONFIG_PATH=${INFRA_PIPELINES_CONFIG_PATH:-$INFRA_DIR/config/pipelines.json}
|
|
29
44
|
REPO_DEFAULT=${INFRA_PIPELINE_REPO:-__PIPELINE_REPO__}
|
|
30
45
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
BRANCH_PROD_DEFAULT=${INFRA_PIPELINE_BRANCH_PROD:-__BRANCH_PROD__}
|
|
47
|
+
BRANCH_DEV_DEFAULT=${INFRA_PIPELINE_BRANCH_DEV:-__BRANCH_DEV__}
|
|
48
|
+
BRANCH_MOBILE_DEFAULT=${INFRA_PIPELINE_BRANCH_MOBILE:-__BRANCH_MOBILE__}
|
|
49
|
+
|
|
50
|
+
declare -A STAGE_ENABLED=(
|
|
51
|
+
[production]=false
|
|
52
|
+
[dev]=false
|
|
53
|
+
[mobile]=false
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
declare -A STAGE_BRANCH=(
|
|
57
|
+
[production]="$BRANCH_PROD_DEFAULT"
|
|
58
|
+
[dev]="$BRANCH_DEV_DEFAULT"
|
|
59
|
+
[mobile]="$BRANCH_MOBILE_DEFAULT"
|
|
34
60
|
)
|
|
35
|
-
|
|
36
|
-
|
|
61
|
+
|
|
62
|
+
declare -A STAGE_REPO=(
|
|
63
|
+
[production]="$REPO_DEFAULT"
|
|
64
|
+
[dev]="$REPO_DEFAULT"
|
|
65
|
+
[mobile]="$REPO_DEFAULT"
|
|
37
66
|
)
|
|
38
|
-
|
|
39
|
-
|
|
67
|
+
|
|
68
|
+
declare -A STAGE_SUFFIX=(
|
|
69
|
+
[production]="prod"
|
|
70
|
+
[dev]="dev"
|
|
71
|
+
[mobile]="mobile"
|
|
40
72
|
)
|
|
41
73
|
|
|
74
|
+
normalize_stage() {
|
|
75
|
+
local input
|
|
76
|
+
input=$(echo "$1" | tr '[:upper:]' '[:lower:]' | xargs)
|
|
77
|
+
case "$input" in
|
|
78
|
+
prod) echo "production" ;;
|
|
79
|
+
production|dev|mobile) echo "$input" ;;
|
|
80
|
+
*) echo "" ;;
|
|
81
|
+
esac
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for token in $(echo "$PIPELINES_RAW" | tr ',' ' '); do
|
|
85
|
+
stage=$(normalize_stage "$token")
|
|
86
|
+
if [ -n "$stage" ]; then
|
|
87
|
+
STAGE_ENABLED["$stage"]=true
|
|
88
|
+
fi
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
if [ -f "$PIPELINES_CONFIG_PATH" ]; then
|
|
92
|
+
echo "Loading runtime pipeline config from $PIPELINES_CONFIG_PATH"
|
|
93
|
+
while IFS=$'\t' read -r stage enabled branch repo; do
|
|
94
|
+
stage=$(normalize_stage "$stage")
|
|
95
|
+
if [ -z "$stage" ]; then
|
|
96
|
+
continue
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
if [ "$enabled" = "true" ]; then
|
|
100
|
+
STAGE_ENABLED["$stage"]=true
|
|
101
|
+
elif [ "$enabled" = "false" ]; then
|
|
102
|
+
STAGE_ENABLED["$stage"]=false
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
if [ -n "$branch" ]; then
|
|
106
|
+
STAGE_BRANCH["$stage"]="$branch"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
if [ -n "$repo" ]; then
|
|
110
|
+
STAGE_REPO["$stage"]="$repo"
|
|
111
|
+
fi
|
|
112
|
+
done < <(node -e '
|
|
113
|
+
const fs = require("fs");
|
|
114
|
+
const path = process.argv[1];
|
|
115
|
+
const raw = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
116
|
+
const src = raw.pipelines ?? raw;
|
|
117
|
+
function toStage(v) {
|
|
118
|
+
const n = String(v || "").trim().toLowerCase();
|
|
119
|
+
if (n === "prod") return "production";
|
|
120
|
+
if (n === "production" || n === "dev" || n === "mobile") return n;
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
function emit(stage, cfg) {
|
|
124
|
+
const enabled = cfg && typeof cfg.enabled !== "undefined" ? Boolean(cfg.enabled) : true;
|
|
125
|
+
const branch = cfg && typeof cfg.branch === "string" ? cfg.branch.trim() : "";
|
|
126
|
+
const repo = cfg && typeof cfg.repo === "string" ? cfg.repo.trim() : "";
|
|
127
|
+
process.stdout.write([stage, String(enabled), branch, repo].join("\t") + "\n");
|
|
128
|
+
}
|
|
129
|
+
if (Array.isArray(src)) {
|
|
130
|
+
for (const item of src) {
|
|
131
|
+
const stage = toStage(item && item.stage);
|
|
132
|
+
if (!stage) continue;
|
|
133
|
+
emit(stage, item || {});
|
|
134
|
+
}
|
|
135
|
+
} else if (src && typeof src === "object") {
|
|
136
|
+
for (const [key, value] of Object.entries(src)) {
|
|
137
|
+
const stage = toStage(key);
|
|
138
|
+
if (!stage) continue;
|
|
139
|
+
emit(stage, value || {});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
' "$PIPELINES_CONFIG_PATH")
|
|
143
|
+
else
|
|
144
|
+
echo "Runtime pipeline config not found at $PIPELINES_CONFIG_PATH; using env defaults"
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
declare -A PIPELINE_STAGE=()
|
|
148
|
+
declare -A PIPELINE_REPO=()
|
|
149
|
+
declare -A PIPELINE_BRANCH=()
|
|
150
|
+
|
|
151
|
+
for stage in production dev mobile; do
|
|
152
|
+
if [ "${STAGE_ENABLED[$stage]}" != "true" ]; then
|
|
153
|
+
continue
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
suffix=${STAGE_SUFFIX[$stage]}
|
|
157
|
+
name="${PIPELINE_PREFIX}-${suffix}"
|
|
158
|
+
|
|
159
|
+
PIPELINE_STAGE["$name"]="$stage"
|
|
160
|
+
PIPELINE_REPO["$name"]="${STAGE_REPO[$stage]}"
|
|
161
|
+
PIPELINE_BRANCH["$name"]="${STAGE_BRANCH[$stage]}"
|
|
162
|
+
done
|
|
163
|
+
|
|
164
|
+
if [ ${#PIPELINE_STAGE[@]} -eq 0 ]; then
|
|
165
|
+
echo "No pipelines configured. Set INFRA_PIPELINES and/or config/pipelines.json."
|
|
166
|
+
exit 0
|
|
167
|
+
fi
|
|
168
|
+
|
|
42
169
|
resolve_sst_deploy_script() {
|
|
43
170
|
local candidates=(
|
|
44
171
|
"$SCRIPT_DIR/sst-deploy.sh"
|
|
@@ -56,16 +183,6 @@ resolve_sst_deploy_script() {
|
|
|
56
183
|
return 1
|
|
57
184
|
}
|
|
58
185
|
|
|
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
186
|
SST_DEPLOY_SCRIPT=${SST_DEPLOY_SCRIPT:-}
|
|
70
187
|
if [ -z "$SST_DEPLOY_SCRIPT" ]; then
|
|
71
188
|
SST_DEPLOY_SCRIPT=$(resolve_sst_deploy_script || true)
|
|
@@ -104,13 +221,31 @@ if [ "${APPROVE:-}" != "true" ]; then
|
|
|
104
221
|
exit 1
|
|
105
222
|
fi
|
|
106
223
|
|
|
224
|
+
declare -A PROCESSED_STAGE=()
|
|
107
225
|
for NAME in "${MISSING[@]}"; do
|
|
108
|
-
STAGE=${PIPELINE_STAGE[$NAME]
|
|
226
|
+
STAGE=${PIPELINE_STAGE[$NAME]}
|
|
227
|
+
|
|
228
|
+
if [ "${PROCESSED_STAGE[$STAGE]:-false}" = "true" ]; then
|
|
229
|
+
continue
|
|
230
|
+
fi
|
|
231
|
+
|
|
109
232
|
REPO=${PIPELINE_REPO[$NAME]:-$REPO_DEFAULT}
|
|
110
|
-
BRANCH=${PIPELINE_BRANCH[$NAME]:-main}
|
|
111
233
|
|
|
112
|
-
echo "Creating pipeline '$
|
|
113
|
-
|
|
234
|
+
echo "Creating pipeline for stage '$STAGE' via production deploy"
|
|
235
|
+
echo " repo : $REPO"
|
|
236
|
+
echo " branch : ${PIPELINE_BRANCH[$NAME]}"
|
|
237
|
+
|
|
238
|
+
APPROVE=true \
|
|
239
|
+
STACK="production" \
|
|
240
|
+
INFRA_CREATE_PIPELINES=true \
|
|
241
|
+
INFRA_PIPELINES="$STAGE" \
|
|
242
|
+
INFRA_PIPELINE_REPO="$REPO" \
|
|
243
|
+
INFRA_PIPELINE_BRANCH_PROD="${STAGE_BRANCH[production]}" \
|
|
244
|
+
INFRA_PIPELINE_BRANCH_DEV="${STAGE_BRANCH[dev]}" \
|
|
245
|
+
INFRA_PIPELINE_BRANCH_MOBILE="${STAGE_BRANCH[mobile]}" \
|
|
246
|
+
bash "$SST_DEPLOY_SCRIPT"
|
|
247
|
+
|
|
248
|
+
PROCESSED_STAGE[$STAGE]=true
|
|
114
249
|
done
|
|
115
250
|
|
|
116
251
|
echo "Done. Current pipelines:"
|
package/templates/env.example
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# Provider support in v1.0.0: AWS only
|
|
2
2
|
INFRA_PROVIDER=__PROVIDER__
|
|
3
3
|
|
|
4
|
+
# Deployment profile: next-only | next-expo | expo-web
|
|
5
|
+
INFRA_PROFILE=__PROFILE__
|
|
6
|
+
|
|
4
7
|
# SST app identity
|
|
5
8
|
INFRA_APP_NAME=__APP_NAME__
|
|
9
|
+
INFRA_USE_EXTERNAL_CERTS=false
|
|
6
10
|
|
|
7
11
|
# Core domain and repository
|
|
8
12
|
INFRA_ROOT_DOMAIN=__ROOT_DOMAIN__
|
|
13
|
+
INFRA_HOSTED_ZONE_DOMAIN=
|
|
9
14
|
INFRA_PIPELINE_REPO=__PIPELINE_REPO__
|
|
10
15
|
INFRA_PIPELINE_PREFIX=__PROJECT_PREFIX__
|
|
11
16
|
INFRA_PROJECT_TAG=__PROJECT_PREFIX__
|
|
@@ -16,6 +21,16 @@ INFRA_PIPELINE_BRANCH_PROD=__BRANCH_PROD__
|
|
|
16
21
|
INFRA_PIPELINE_BRANCH_DEV=__BRANCH_DEV__
|
|
17
22
|
INFRA_PIPELINE_BRANCH_MOBILE=__BRANCH_MOBILE__
|
|
18
23
|
|
|
24
|
+
# Pipeline definitions can be provided in local config/pipelines.json
|
|
25
|
+
INFRA_PIPELINES_CONFIG_PATH=config/pipelines.json
|
|
26
|
+
|
|
27
|
+
# Explicit control: keep false for normal deploys.
|
|
28
|
+
# Set true only when intentionally creating/updating CodePipelines.
|
|
29
|
+
INFRA_CREATE_PIPELINES=__CREATE_PIPELINES_DEFAULT__
|
|
30
|
+
|
|
31
|
+
# CodeBuild role mode: admin | least-privilege
|
|
32
|
+
INFRA_PIPELINE_PERMISSIONS_MODE=__PIPELINE_PERMISSIONS_MODE__
|
|
33
|
+
|
|
19
34
|
# Optional Expo static site deployment
|
|
20
35
|
INFRA_ENABLE_EXPO_SITE=__ENABLE_EXPO_SITE__
|
|
21
36
|
|