@lsts_tech/infra 1.0.1 → 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.
Files changed (39) hide show
  1. package/README.md +54 -73
  2. package/dist/bin/init.d.ts +4 -3
  3. package/dist/bin/init.d.ts.map +1 -1
  4. package/dist/bin/init.js +619 -117
  5. package/dist/bin/init.js.map +1 -1
  6. package/dist/src/auth/index.d.ts +17 -0
  7. package/dist/src/auth/index.d.ts.map +1 -0
  8. package/dist/src/auth/index.js +18 -0
  9. package/dist/src/auth/index.js.map +1 -0
  10. package/dist/stacks/Dns.d.ts +24 -14
  11. package/dist/stacks/Dns.d.ts.map +1 -1
  12. package/dist/stacks/Dns.js +69 -18
  13. package/dist/stacks/Dns.js.map +1 -1
  14. package/dist/stacks/Pipeline.d.ts +7 -0
  15. package/dist/stacks/Pipeline.d.ts.map +1 -1
  16. package/dist/stacks/Pipeline.js +60 -7
  17. package/dist/stacks/Pipeline.js.map +1 -1
  18. package/docs/CLI.md +58 -15
  19. package/docs/CONFIGURATION.md +73 -30
  20. package/docs/EXAMPLES.md +5 -1
  21. package/examples/delegated-subdomain/infra.config.ts +102 -0
  22. package/examples/next-and-expo/infra.config.ts +33 -28
  23. package/examples/next-only/infra.config.ts +35 -22
  24. package/package.json +10 -4
  25. package/scripts/ensure-pipelines.sh +151 -43
  26. package/scripts/postdeploy-update-dns.sh +42 -11
  27. package/scripts/predeploy-checks.sh +38 -5
  28. package/templates/buildspec.yml +23 -0
  29. package/templates/ensure-pipelines.sh +157 -22
  30. package/templates/env.example +15 -0
  31. package/templates/infra.config.expo-web.ts +153 -0
  32. package/templates/infra.config.next-only.ts +159 -0
  33. package/templates/infra.config.ts +21 -4
  34. package/templates/pipelines.example.json +19 -0
  35. package/templates/private.example.json +13 -0
  36. package/templates/scaffold.gitignore +29 -0
  37. package/templates/scaffold.package.json +25 -0
  38. package/templates/scaffold.tsconfig.json +22 -0
  39. 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 SST deploy for their mapped stage.
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
- PIPELINES_CSV=${INFRA_PIPELINES:-production,dev,mobile}
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
- BRANCH_PROD=${INFRA_PIPELINE_BRANCH_PROD:-main}
33
- BRANCH_DEV=${INFRA_PIPELINE_BRANCH_DEV:-develop}
34
- BRANCH_MOBILE=${INFRA_PIPELINE_BRANCH_MOBILE:-mobile}
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
- add_pipeline() {
41
- local stage=$1
42
- local name="${PIPELINE_PREFIX}-${stage}"
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
- PIPELINE_STAGE["$name"]="$stage"
50
- PIPELINE_REPO["$name"]="$REPO_DEFAULT"
51
- PIPELINE_BRANCH["$name"]="$branch"
52
- }
156
+ suffix=${STAGE_SUFFIX[$stage]}
157
+ name="${PIPELINE_PREFIX}-${suffix}"
53
158
 
54
- IFS=',' read -r -a STAGE_LIST <<< "$PIPELINES_CSV"
55
- for raw in "${STAGE_LIST[@]}"; do
56
- stage=$(echo "$raw" | xargs)
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]:-production}
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 '$NAME' via SST deploy (stage: $STAGE, repo: $REPO, branch: $BRANCH)"
140
- APPROVE=true STACK="$STAGE" bash "$SST_DEPLOY_SCRIPT"
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=$(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"
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 ${DOMAIN_ROOT} found in this account/region"
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 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
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
- # Strip /hostedzone/ prefix if present
131
- HOSTED_ZONE_ID=${HOSTED_ZONE_ID#/hostedzone/}
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=$(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"
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=${hz##*/}
51
- hz=${hz##*/}
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.
@@ -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 SST deploy for their mapped stage.
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
- # Pipeline definitions (generated by init)
32
- declare -A PIPELINE_STAGE=(
33
- __PIPELINE_STAGE_MAP__
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
- declare -A PIPELINE_REPO=(
36
- __PIPELINE_REPO_MAP__
61
+
62
+ declare -A STAGE_REPO=(
63
+ [production]="$REPO_DEFAULT"
64
+ [dev]="$REPO_DEFAULT"
65
+ [mobile]="$REPO_DEFAULT"
37
66
  )
38
- declare -A PIPELINE_BRANCH=(
39
- __PIPELINE_BRANCH_MAP__
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]:-production}
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 '$NAME' via SST deploy (stage: $STAGE, repo: $REPO, branch: $BRANCH)"
113
- APPROVE=true STACK="$STAGE" bash "$SST_DEPLOY_SCRIPT"
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:"
@@ -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