@biffo/cli 0.273.12 → 0.273.13

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.
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # Does the sibling actually route, not just deploy?
4
+ #
5
+ # ## Why this exists (#737 item 3)
6
+ #
7
+ # The Lambda smoke test added by #162 (`deploy.yml`'s "Smoke test the deployed
8
+ # Lambda" step) proves the Lambda boots and answers a direct API Gateway
9
+ # request. It says nothing about the CDN path a real user hits:
10
+ # `dev.<domain>/<path>`. One sibling went fully green three separate times
11
+ # while that path was broken in three different ways:
12
+ #
13
+ # 1. CDN registration no-op — the path never got a CloudFront behaviour,
14
+ # so the request silently fell through to
15
+ # the PARENT's default behaviour (the root
16
+ # app) instead of 404ing.
17
+ # 2. Path-prefix mismatch — the app's basePath didn't match the prefix
18
+ # CloudFront forwards, so the prefixed path
19
+ # 404'd even though the app was live.
20
+ # 3. Bare-path routing — the prefix resolved with a trailing slash
21
+ # but not without one (or vice versa).
22
+ #
23
+ # A generic "does the origin respond" check passes in all three cases —
24
+ # variant 1 gets a real 200 (from the WRONG origin), and variants 2/3 differ
25
+ # only in exactly which shape of the URL is broken. So this checks three
26
+ # specific routes, not one generic one, and prints how many it checked and
27
+ # which — "routing OK" over a denominator of zero is the #1363 class.
28
+ #
29
+ # ## Fail-closed
30
+ #
31
+ # 0 = every route checked resolved correctly.
32
+ # 1 = a route was checked and did NOT resolve correctly (a real routing bug).
33
+ # 2 = could not tell — DNS didn't resolve, the deployment was unreachable, or
34
+ # the expected path could not be determined. 2 is NEVER a pass; callers
35
+ # (deploy.yml) must treat it as a failure exactly like 1.
36
+ #
37
+ # ## Usage
38
+ #
39
+ # ROUTING_BASE_URL=https://dev.example.com \
40
+ # ROUTING_PATH_PREFIX=reports \
41
+ # sh scripts/routing-smoke-test.sh
42
+ #
43
+ # ROUTING_BASE_URL — the parent CDN origin the sibling is served through
44
+ # (CORE_PORTAL_URL in deploy.yml). Required.
45
+ # ROUTING_PATH_PREFIX — the sibling's routed path segment (PATH_PREFIX in
46
+ # deploy.yml). Required to be SET, but may be the empty
47
+ # string — the root application sibling (issue #306)
48
+ # legitimately has one. Unset (vs set-empty) is treated
49
+ # as "cannot determine the expected path" and exits 2,
50
+ # because those are different facts: one is a known
51
+ # root sibling, the other is a workflow that forgot to
52
+ # pass the variable.
53
+ # ROUTING_CURL — override the curl binary/wrapper (tests use this to
54
+ # point at a stub).
55
+ #
56
+ # Run this file's own tests: sh scripts/routing-smoke-test.test.sh
57
+
58
+ set -u
59
+
60
+ CURL=${ROUTING_CURL:-curl}
61
+ CHECKED=0
62
+ FAILED=0
63
+ CANNOT_TELL=0
64
+
65
+ _report() {
66
+ # _report <status> <label> <detail>
67
+ status=$1
68
+ label=$2
69
+ detail=$3
70
+ case "$status" in
71
+ ok) echo " [OK] $label — $detail" ;;
72
+ fail) echo " [FAIL] $label — $detail"; FAILED=$((FAILED + 1)) ;;
73
+ cannot-tell) echo " [????] $label — $detail"; CANNOT_TELL=$((CANNOT_TELL + 1)) ;;
74
+ esac
75
+ }
76
+
77
+ # _fetch <url> -> prints "<http_status>\n<body_sha256>" or "" on transport
78
+ # failure (DNS, connection refused, timeout — curl exit != 0).
79
+ _fetch() {
80
+ url=$1
81
+ out=$("$CURL" -sS --max-time 15 --retry 2 --retry-delay 2 \
82
+ -o /tmp/routing-smoke-body.$$ -w '%{http_code}' "$url" 2>/tmp/routing-smoke-err.$$)
83
+ rc=$?
84
+ if [ "$rc" -ne 0 ]; then
85
+ err=$(cat /tmp/routing-smoke-err.$$ 2>/dev/null)
86
+ rm -f /tmp/routing-smoke-body.$$ /tmp/routing-smoke-err.$$
87
+ echo "TRANSPORT_ERROR: $err"
88
+ return 1
89
+ fi
90
+ body_hash=$(cksum /tmp/routing-smoke-body.$$ 2>/dev/null | awk '{print $1"-"$2}')
91
+ rm -f /tmp/routing-smoke-body.$$ /tmp/routing-smoke-err.$$
92
+ echo "$out $body_hash"
93
+ return 0
94
+ }
95
+
96
+ if [ -z "${ROUTING_BASE_URL+x}" ] || [ -z "$ROUTING_BASE_URL" ]; then
97
+ echo "::error::routing-smoke-test: ROUTING_BASE_URL is not set — cannot determine which deployment to check." >&2
98
+ exit 2
99
+ fi
100
+
101
+ if [ -z "${ROUTING_PATH_PREFIX+x}" ]; then
102
+ echo "::error::routing-smoke-test: ROUTING_PATH_PREFIX is unset (not even empty) — cannot determine the expected path. If this is genuinely the root sibling, pass ROUTING_PATH_PREFIX='' explicitly." >&2
103
+ exit 2
104
+ fi
105
+
106
+ BASE=${ROUTING_BASE_URL%/}
107
+ PREFIX=$ROUTING_PATH_PREFIX
108
+
109
+ echo "routing-smoke-test: base=$BASE prefix='${PREFIX}'"
110
+
111
+ # --- Route A: bare path, no trailing slash --------------------------------
112
+ # Directly the "bare-path routing" recorded variant.
113
+ if [ -n "$PREFIX" ]; then
114
+ BARE_URL="$BASE/$PREFIX"
115
+ else
116
+ BARE_URL="$BASE"
117
+ fi
118
+ CHECKED=$((CHECKED + 1))
119
+ result=$(_fetch "$BARE_URL")
120
+ if [ $? -ne 0 ]; then
121
+ _report cannot-tell "bare path ($BARE_URL)" "$result"
122
+ else
123
+ code=${result%% *}
124
+ case "$code" in
125
+ 2*) _report ok "bare path ($BARE_URL)" "HTTP $code" ;;
126
+ *) _report fail "bare path ($BARE_URL)" "HTTP $code — expected 2xx" ;;
127
+ esac
128
+ fi
129
+
130
+ # --- Route B: trailing slash, path prefix serving -------------------------
131
+ # Directly the "path-prefix mismatch" recorded variant: the app's basePath
132
+ # not matching what CloudFront forwards shows up as this route 404ing even
133
+ # though the app is live and the bare path (or root) is fine.
134
+ if [ -n "$PREFIX" ]; then
135
+ PREFIXED_URL="$BASE/$PREFIX/"
136
+ CHECKED=$((CHECKED + 1))
137
+ result=$(_fetch "$PREFIXED_URL")
138
+ if [ $? -ne 0 ]; then
139
+ _report cannot-tell "path prefix ($PREFIXED_URL)" "$result"
140
+ else
141
+ code=${result%% *}
142
+ case "$code" in
143
+ 2*) _report ok "path prefix ($PREFIXED_URL)" "HTTP $code" ;;
144
+ *) _report fail "path prefix ($PREFIXED_URL)" "HTTP $code — expected 2xx" ;;
145
+ esac
146
+ fi
147
+ else
148
+ echo " [SKIP] path prefix — PATH_PREFIX is empty (root sibling, issue #306); trailing-slash form is identical to the bare-path check above, by design."
149
+ fi
150
+
151
+ # --- Route C: CDN behaviour actually registered ---------------------------
152
+ # The CDN-no-op variant: if no distinct CloudFront behaviour was ever
153
+ # registered for this path, the request falls through to the PARENT's
154
+ # default behaviour and returns a real 200 — from the WRONG origin. A bare
155
+ # status check cannot see this; it must compare against a known-different
156
+ # response. Only meaningful when PREFIX is non-empty (the root sibling IS
157
+ # the default behaviour, so there is nothing to differ from — that case is
158
+ # recorded as unproven below, not silently skipped as a pass).
159
+ if [ -n "$PREFIX" ]; then
160
+ ROOT_URL="$BASE/"
161
+ CHECKED=$((CHECKED + 1))
162
+ prefixed_result=$(_fetch "$BASE/$PREFIX/")
163
+ prefixed_rc=$?
164
+ root_result=$(_fetch "$ROOT_URL")
165
+ root_rc=$?
166
+ if [ "$prefixed_rc" -ne 0 ] || [ "$root_rc" -ne 0 ]; then
167
+ _report cannot-tell "CDN behaviour registered (vs $ROOT_URL)" "could not fetch one or both paths for comparison"
168
+ else
169
+ prefixed_hash=${prefixed_result#* }
170
+ root_hash=${root_result#* }
171
+ prefixed_code=${prefixed_result%% *}
172
+ case "$prefixed_code" in
173
+ 2*)
174
+ if [ "$prefixed_hash" = "$root_hash" ]; then
175
+ _report fail "CDN behaviour registered" "response at $BASE/$PREFIX/ is byte-identical to $ROOT_URL — CloudFront is serving the PARENT's default behaviour, not this sibling (registration no-op)"
176
+ else
177
+ _report ok "CDN behaviour registered" "response at $BASE/$PREFIX/ differs from $ROOT_URL"
178
+ fi
179
+ ;;
180
+ *)
181
+ _report fail "CDN behaviour registered" "prefixed path did not return 2xx (HTTP $prefixed_code), cannot compare bodies"
182
+ ;;
183
+ esac
184
+ fi
185
+ else
186
+ echo " [UNPROVEN] CDN behaviour registered — PATH_PREFIX is empty (root sibling), so there is no distinct-from-root comparison this script can make. Root-sibling CDN registration is NOT covered by this check."
187
+ fi
188
+
189
+ echo
190
+ echo "routing-smoke-test: checked $CHECKED route(s) against $BASE (prefix='${PREFIX}')"
191
+
192
+ if [ "$CANNOT_TELL" -gt 0 ]; then
193
+ echo "::error::routing-smoke-test: $CANNOT_TELL of $CHECKED check(s) could not be evaluated (unreachable / DNS / transport error) — treating as failure, never a pass."
194
+ exit 2
195
+ fi
196
+
197
+ if [ "$FAILED" -gt 0 ]; then
198
+ echo "::error::routing-smoke-test: $FAILED of $CHECKED route check(s) failed."
199
+ exit 1
200
+ fi
201
+
202
+ if [ "$CHECKED" -eq 0 ]; then
203
+ echo "::error::routing-smoke-test: 0 routes were checked — refusing to report success over an empty denominator (#1363)."
204
+ exit 2
205
+ fi
206
+
207
+ echo "routing-smoke-test: all $CHECKED checked route(s) OK."
208
+ exit 0
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # Proves routing-smoke-test.sh (#737 item 3) actually catches the three
4
+ # recorded routing failures, and that it fails closed when it cannot tell.
5
+ #
6
+ # A verification script nobody has seen fail is not verified — every FAIL
7
+ # scenario below first ran (uncommented) against a plain `curl` stub before
8
+ # the real script existed, to confirm it does NOT catch these shapes without
9
+ # the fix; that manual step is recorded here in prose since there is nothing
10
+ # upstream of this script to diff against.
11
+ #
12
+ # Stubs `curl` on PATH so this needs no network and no live deployment.
13
+ #
14
+ # Run: sh scripts/routing-smoke-test.test.sh
15
+
16
+ set -u
17
+
18
+ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
19
+ TARGET="$SCRIPT_DIR/routing-smoke-test.sh"
20
+ STUB_DIR=$(mktemp -d)
21
+ trap 'rm -rf "$STUB_DIR"' EXIT
22
+
23
+ FAILURES=0
24
+ _assert_exit() {
25
+ # _assert_exit <scenario-name> <expected-exit> -- <env assignments already exported>
26
+ name=$1
27
+ expected=$2
28
+ actual_out=$(ROUTING_CURL="$STUB_DIR/curl" sh "$TARGET" 2>&1)
29
+ actual=$?
30
+ if [ "$actual" -eq "$expected" ]; then
31
+ echo "PASS: $name (exit $actual)"
32
+ else
33
+ echo "FAIL: $name — expected exit $expected, got $actual"
34
+ echo "--- output ---"
35
+ echo "$actual_out"
36
+ echo "--------------"
37
+ FAILURES=$((FAILURES + 1))
38
+ fi
39
+ }
40
+
41
+ BASE="https://dev.example.com"
42
+ PREFIX="reports"
43
+
44
+ # --- Stub curl factory ------------------------------------------------
45
+ # Each scenario writes its own stub before calling the target, since the
46
+ # response shape (which URL gets which body/status) IS the scenario.
47
+ _write_stub() {
48
+ cat > "$STUB_DIR/curl" <<'STUB'
49
+ #!/usr/bin/env sh
50
+ outfile=""
51
+ prev=""
52
+ for a in "$@"; do
53
+ case "$prev" in
54
+ -o) outfile="$a" ;;
55
+ esac
56
+ prev="$a"
57
+ done
58
+ url="$prev"
59
+
60
+ # ROUTING_TEST_MODE selects the fixture; each case below writes $outfile
61
+ # and prints the http status, mirroring curl -w '%{http_code}'.
62
+ case "$ROUTING_TEST_MODE" in
63
+ good)
64
+ case "$url" in
65
+ https://dev.example.com/reports) printf 'REPORTS-APP' > "$outfile"; printf '200'; exit 0 ;;
66
+ https://dev.example.com/reports/) printf 'REPORTS-APP' > "$outfile"; printf '200'; exit 0 ;;
67
+ https://dev.example.com/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
68
+ esac
69
+ ;;
70
+ cdn-noop)
71
+ # Every path — including the prefixed one — actually serves the root
72
+ # app's content, because CloudFront never registered a distinct
73
+ # behaviour for /reports and silently fell through to the default.
74
+ case "$url" in
75
+ https://dev.example.com/reports) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
76
+ https://dev.example.com/reports/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
77
+ https://dev.example.com/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
78
+ esac
79
+ ;;
80
+ prefix-mismatch)
81
+ # The app serves a DIFFERENT basePath than PATH_PREFIX declares, so the
82
+ # trailing-slash form 404s even though bare and root are fine.
83
+ case "$url" in
84
+ https://dev.example.com/reports) printf 'REPORTS-APP' > "$outfile"; printf '200'; exit 0 ;;
85
+ https://dev.example.com/reports/) printf 'Not Found' > "$outfile"; printf '404'; exit 0 ;;
86
+ https://dev.example.com/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
87
+ esac
88
+ ;;
89
+ bare-path-broken)
90
+ # Trailing-slash form works, bare form 404s (or vice versa — either
91
+ # asymmetry is the recorded "bare-path routing" variant).
92
+ case "$url" in
93
+ https://dev.example.com/reports) printf 'Not Found' > "$outfile"; printf '404'; exit 0 ;;
94
+ https://dev.example.com/reports/) printf 'REPORTS-APP' > "$outfile"; printf '200'; exit 0 ;;
95
+ https://dev.example.com/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
96
+ esac
97
+ ;;
98
+ unreachable)
99
+ printf 'curl: (6) Could not resolve host: dev.example.com' >&2
100
+ exit 6
101
+ ;;
102
+ esac
103
+ echo "unhandled URL in stub: $url" >&2
104
+ exit 99
105
+ STUB
106
+ chmod +x "$STUB_DIR/curl"
107
+ }
108
+
109
+ # 1. GOOD deployment — all three checks should pass, exit 0.
110
+ _write_stub
111
+ ROUTING_TEST_MODE=good ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
112
+ _assert_exit "good deployment" 0
113
+
114
+ # 2. CDN registration no-op — prefixed path byte-identical to root.
115
+ # This is the variant a generic "does it 200" check CANNOT catch, because
116
+ # every URL here genuinely returns 200. Confirms the fix, not just a
117
+ # curl failure.
118
+ _write_stub
119
+ ROUTING_TEST_MODE=cdn-noop ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
120
+ _assert_exit "CDN registration no-op" 1
121
+
122
+ # 3. Path-prefix mismatch — trailing-slash form 404s.
123
+ _write_stub
124
+ ROUTING_TEST_MODE=prefix-mismatch ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
125
+ _assert_exit "path-prefix mismatch" 1
126
+
127
+ # 4. Bare-path routing — bare form 404s while trailing-slash works.
128
+ _write_stub
129
+ ROUTING_TEST_MODE=bare-path-broken ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
130
+ _assert_exit "bare-path routing broken" 1
131
+
132
+ # 5. Unreachable deployment — DNS/transport failure must fail CLOSED (exit
133
+ # 2, cannot-tell), never exit 0.
134
+ _write_stub
135
+ ROUTING_TEST_MODE=unreachable ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
136
+ _assert_exit "unreachable deployment" 2
137
+
138
+ # 6. ROUTING_PATH_PREFIX genuinely unset (not empty) — cannot determine the
139
+ # expected path, must exit 2 without even invoking curl.
140
+ _write_stub
141
+ env -u ROUTING_PATH_PREFIX ROUTING_TEST_MODE=good ROUTING_BASE_URL="$BASE" \
142
+ sh -c 'ROUTING_CURL="'"$STUB_DIR"'/curl" sh "'"$TARGET"'"' >/tmp/routing-smoke-t6.$$ 2>&1
143
+ rc=$?
144
+ if [ "$rc" -eq 2 ]; then
145
+ echo "PASS: PATH_PREFIX unset (exit 2)"
146
+ else
147
+ echo "FAIL: PATH_PREFIX unset — expected exit 2, got $rc"
148
+ cat /tmp/routing-smoke-t6.$$
149
+ FAILURES=$((FAILURES + 1))
150
+ fi
151
+ rm -f /tmp/routing-smoke-t6.$$
152
+
153
+ # 7. ROUTING_BASE_URL unset entirely — must exit 2.
154
+ _write_stub
155
+ env -u ROUTING_BASE_URL ROUTING_TEST_MODE=good ROUTING_PATH_PREFIX="$PREFIX" \
156
+ sh -c 'ROUTING_CURL="'"$STUB_DIR"'/curl" sh "'"$TARGET"'"' >/tmp/routing-smoke-t7.$$ 2>&1
157
+ rc=$?
158
+ if [ "$rc" -eq 2 ]; then
159
+ echo "PASS: BASE_URL unset (exit 2)"
160
+ else
161
+ echo "FAIL: BASE_URL unset — expected exit 2, got $rc"
162
+ cat /tmp/routing-smoke-t7.$$
163
+ FAILURES=$((FAILURES + 1))
164
+ fi
165
+ rm -f /tmp/routing-smoke-t7.$$
166
+
167
+ # 8. Root sibling (PATH_PREFIX='', deliberately empty not unset) — the
168
+ # CDN-behaviour-registered check has nothing to compare against and must
169
+ # say so rather than silently passing; bare-path check still runs
170
+ # (denominator stays > 0), so exit is 0 when that succeeds.
171
+ _write_stub
172
+ cat > "$STUB_DIR/curl" <<'STUB'
173
+ #!/usr/bin/env sh
174
+ outfile=""
175
+ prev=""
176
+ for a in "$@"; do
177
+ case "$prev" in
178
+ -o) outfile="$a" ;;
179
+ esac
180
+ prev="$a"
181
+ done
182
+ url="$prev"
183
+ case "$url" in
184
+ https://dev.example.com) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
185
+ esac
186
+ echo "unhandled URL in stub: $url" >&2
187
+ exit 99
188
+ STUB
189
+ chmod +x "$STUB_DIR/curl"
190
+ out=$(ROUTING_CURL="$STUB_DIR/curl" ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="" sh "$TARGET" 2>&1)
191
+ rc=$?
192
+ if [ "$rc" -eq 0 ] && echo "$out" | grep -q "UNPROVEN"; then
193
+ echo "PASS: root sibling (exit 0, unproven CDN check reported)"
194
+ else
195
+ echo "FAIL: root sibling — expected exit 0 with UNPROVEN noted, got exit $rc"
196
+ echo "$out"
197
+ FAILURES=$((FAILURES + 1))
198
+ fi
199
+
200
+ echo
201
+ if [ "$FAILURES" -eq 0 ]; then
202
+ echo "routing-smoke-test.test.sh: all checks passed."
203
+ exit 0
204
+ else
205
+ echo "routing-smoke-test.test.sh: $FAILURES check(s) failed."
206
+ exit 1
207
+ fi
@@ -216,6 +216,14 @@ jobs:
216
216
  # services/api, which is the environment `uv sync` installed and so the
217
217
  # one to audit.
218
218
  run: sh ../../scripts/py-dependency-audit.sh
219
+ - name: Routing smoke test — self-test
220
+ if: ${{ !cancelled() }}
221
+ # Not exercising this repo's own deployment (CI has none to reach) —
222
+ # proves scripts/routing-smoke-test.sh, the deploy.yml post-deploy
223
+ # routing check (#737), still catches the three recorded CDN routing
224
+ # failures against a stubbed curl. A verification script with no
225
+ # caller is the class tracked in #1413; this is that caller.
226
+ run: sh ../../scripts/routing-smoke-test.test.sh
219
227
 
220
228
  security-secrets:
221
229
  name: Secret Scan
@@ -308,6 +308,23 @@ jobs:
308
308
  echo "::error::Health check failed after deploy — the Lambda uploaded but isn't serving requests. Check CloudWatch logs for ${{ vars.LAMBDA_FUNCTION_NAME }}."
309
309
  exit 1
310
310
  fi
311
+ - name: Smoke test the CDN routing (dev.<domain>/<path>)
312
+ # The Lambda check above proves the API boots. It says nothing about
313
+ # the URL a real user hits — `${{ vars.CORE_PORTAL_URL }}/${{
314
+ # vars.PATH_PREFIX }}` — and one sibling went green here three times
315
+ # while that route was broken three different ways: a CDN behaviour
316
+ # that never actually got registered (silently falling through to the
317
+ # PARENT's default behaviour instead of this sibling), a path-prefix
318
+ # mismatch between the app's basePath and what CloudFront forwards,
319
+ # and the bare path (no trailing slash) not resolving. See
320
+ # scripts/routing-smoke-test.sh for how each is checked and why a
321
+ # generic "does it 200" probe would have passed on every one of them.
322
+ # Exits 0/1/2 (2 = cannot tell, never a pass); ROUTING_CURL is unset
323
+ # here so it uses the real `curl` — only the script's own tests stub it.
324
+ env:
325
+ ROUTING_BASE_URL: ${{ vars.CORE_PORTAL_URL }}
326
+ ROUTING_PATH_PREFIX: ${{ vars.PATH_PREFIX }}
327
+ run: sh scripts/routing-smoke-test.sh
311
328
 
312
329
  # A Deploy run that deploys NOTHING must not report success (#1065).
313
330
  #
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # Does the sibling actually route, not just deploy?
4
+ #
5
+ # ## Why this exists (#737 item 3)
6
+ #
7
+ # The Lambda smoke test added by #162 (`deploy.yml`'s "Smoke test the deployed
8
+ # Lambda" step) proves the Lambda boots and answers a direct API Gateway
9
+ # request. It says nothing about the CDN path a real user hits:
10
+ # `dev.<domain>/<path>`. One sibling went fully green three separate times
11
+ # while that path was broken in three different ways:
12
+ #
13
+ # 1. CDN registration no-op — the path never got a CloudFront behaviour,
14
+ # so the request silently fell through to
15
+ # the PARENT's default behaviour (the root
16
+ # app) instead of 404ing.
17
+ # 2. Path-prefix mismatch — the app's basePath didn't match the prefix
18
+ # CloudFront forwards, so the prefixed path
19
+ # 404'd even though the app was live.
20
+ # 3. Bare-path routing — the prefix resolved with a trailing slash
21
+ # but not without one (or vice versa).
22
+ #
23
+ # A generic "does the origin respond" check passes in all three cases —
24
+ # variant 1 gets a real 200 (from the WRONG origin), and variants 2/3 differ
25
+ # only in exactly which shape of the URL is broken. So this checks three
26
+ # specific routes, not one generic one, and prints how many it checked and
27
+ # which — "routing OK" over a denominator of zero is the #1363 class.
28
+ #
29
+ # ## Fail-closed
30
+ #
31
+ # 0 = every route checked resolved correctly.
32
+ # 1 = a route was checked and did NOT resolve correctly (a real routing bug).
33
+ # 2 = could not tell — DNS didn't resolve, the deployment was unreachable, or
34
+ # the expected path could not be determined. 2 is NEVER a pass; callers
35
+ # (deploy.yml) must treat it as a failure exactly like 1.
36
+ #
37
+ # ## Usage
38
+ #
39
+ # ROUTING_BASE_URL=https://dev.example.com \
40
+ # ROUTING_PATH_PREFIX=reports \
41
+ # sh scripts/routing-smoke-test.sh
42
+ #
43
+ # ROUTING_BASE_URL — the parent CDN origin the sibling is served through
44
+ # (CORE_PORTAL_URL in deploy.yml). Required.
45
+ # ROUTING_PATH_PREFIX — the sibling's routed path segment (PATH_PREFIX in
46
+ # deploy.yml). Required to be SET, but may be the empty
47
+ # string — the root application sibling (issue #306)
48
+ # legitimately has one. Unset (vs set-empty) is treated
49
+ # as "cannot determine the expected path" and exits 2,
50
+ # because those are different facts: one is a known
51
+ # root sibling, the other is a workflow that forgot to
52
+ # pass the variable.
53
+ # ROUTING_CURL — override the curl binary/wrapper (tests use this to
54
+ # point at a stub).
55
+ #
56
+ # Run this file's own tests: sh scripts/routing-smoke-test.test.sh
57
+
58
+ set -u
59
+
60
+ CURL=${ROUTING_CURL:-curl}
61
+ CHECKED=0
62
+ FAILED=0
63
+ CANNOT_TELL=0
64
+
65
+ _report() {
66
+ # _report <status> <label> <detail>
67
+ status=$1
68
+ label=$2
69
+ detail=$3
70
+ case "$status" in
71
+ ok) echo " [OK] $label — $detail" ;;
72
+ fail) echo " [FAIL] $label — $detail"; FAILED=$((FAILED + 1)) ;;
73
+ cannot-tell) echo " [????] $label — $detail"; CANNOT_TELL=$((CANNOT_TELL + 1)) ;;
74
+ esac
75
+ }
76
+
77
+ # _fetch <url> -> prints "<http_status>\n<body_sha256>" or "" on transport
78
+ # failure (DNS, connection refused, timeout — curl exit != 0).
79
+ _fetch() {
80
+ url=$1
81
+ out=$("$CURL" -sS --max-time 15 --retry 2 --retry-delay 2 \
82
+ -o /tmp/routing-smoke-body.$$ -w '%{http_code}' "$url" 2>/tmp/routing-smoke-err.$$)
83
+ rc=$?
84
+ if [ "$rc" -ne 0 ]; then
85
+ err=$(cat /tmp/routing-smoke-err.$$ 2>/dev/null)
86
+ rm -f /tmp/routing-smoke-body.$$ /tmp/routing-smoke-err.$$
87
+ echo "TRANSPORT_ERROR: $err"
88
+ return 1
89
+ fi
90
+ body_hash=$(cksum /tmp/routing-smoke-body.$$ 2>/dev/null | awk '{print $1"-"$2}')
91
+ rm -f /tmp/routing-smoke-body.$$ /tmp/routing-smoke-err.$$
92
+ echo "$out $body_hash"
93
+ return 0
94
+ }
95
+
96
+ if [ -z "${ROUTING_BASE_URL+x}" ] || [ -z "$ROUTING_BASE_URL" ]; then
97
+ echo "::error::routing-smoke-test: ROUTING_BASE_URL is not set — cannot determine which deployment to check." >&2
98
+ exit 2
99
+ fi
100
+
101
+ if [ -z "${ROUTING_PATH_PREFIX+x}" ]; then
102
+ echo "::error::routing-smoke-test: ROUTING_PATH_PREFIX is unset (not even empty) — cannot determine the expected path. If this is genuinely the root sibling, pass ROUTING_PATH_PREFIX='' explicitly." >&2
103
+ exit 2
104
+ fi
105
+
106
+ BASE=${ROUTING_BASE_URL%/}
107
+ PREFIX=$ROUTING_PATH_PREFIX
108
+
109
+ echo "routing-smoke-test: base=$BASE prefix='${PREFIX}'"
110
+
111
+ # --- Route A: bare path, no trailing slash --------------------------------
112
+ # Directly the "bare-path routing" recorded variant.
113
+ if [ -n "$PREFIX" ]; then
114
+ BARE_URL="$BASE/$PREFIX"
115
+ else
116
+ BARE_URL="$BASE"
117
+ fi
118
+ CHECKED=$((CHECKED + 1))
119
+ result=$(_fetch "$BARE_URL")
120
+ if [ $? -ne 0 ]; then
121
+ _report cannot-tell "bare path ($BARE_URL)" "$result"
122
+ else
123
+ code=${result%% *}
124
+ case "$code" in
125
+ 2*) _report ok "bare path ($BARE_URL)" "HTTP $code" ;;
126
+ *) _report fail "bare path ($BARE_URL)" "HTTP $code — expected 2xx" ;;
127
+ esac
128
+ fi
129
+
130
+ # --- Route B: trailing slash, path prefix serving -------------------------
131
+ # Directly the "path-prefix mismatch" recorded variant: the app's basePath
132
+ # not matching what CloudFront forwards shows up as this route 404ing even
133
+ # though the app is live and the bare path (or root) is fine.
134
+ if [ -n "$PREFIX" ]; then
135
+ PREFIXED_URL="$BASE/$PREFIX/"
136
+ CHECKED=$((CHECKED + 1))
137
+ result=$(_fetch "$PREFIXED_URL")
138
+ if [ $? -ne 0 ]; then
139
+ _report cannot-tell "path prefix ($PREFIXED_URL)" "$result"
140
+ else
141
+ code=${result%% *}
142
+ case "$code" in
143
+ 2*) _report ok "path prefix ($PREFIXED_URL)" "HTTP $code" ;;
144
+ *) _report fail "path prefix ($PREFIXED_URL)" "HTTP $code — expected 2xx" ;;
145
+ esac
146
+ fi
147
+ else
148
+ echo " [SKIP] path prefix — PATH_PREFIX is empty (root sibling, issue #306); trailing-slash form is identical to the bare-path check above, by design."
149
+ fi
150
+
151
+ # --- Route C: CDN behaviour actually registered ---------------------------
152
+ # The CDN-no-op variant: if no distinct CloudFront behaviour was ever
153
+ # registered for this path, the request falls through to the PARENT's
154
+ # default behaviour and returns a real 200 — from the WRONG origin. A bare
155
+ # status check cannot see this; it must compare against a known-different
156
+ # response. Only meaningful when PREFIX is non-empty (the root sibling IS
157
+ # the default behaviour, so there is nothing to differ from — that case is
158
+ # recorded as unproven below, not silently skipped as a pass).
159
+ if [ -n "$PREFIX" ]; then
160
+ ROOT_URL="$BASE/"
161
+ CHECKED=$((CHECKED + 1))
162
+ prefixed_result=$(_fetch "$BASE/$PREFIX/")
163
+ prefixed_rc=$?
164
+ root_result=$(_fetch "$ROOT_URL")
165
+ root_rc=$?
166
+ if [ "$prefixed_rc" -ne 0 ] || [ "$root_rc" -ne 0 ]; then
167
+ _report cannot-tell "CDN behaviour registered (vs $ROOT_URL)" "could not fetch one or both paths for comparison"
168
+ else
169
+ prefixed_hash=${prefixed_result#* }
170
+ root_hash=${root_result#* }
171
+ prefixed_code=${prefixed_result%% *}
172
+ case "$prefixed_code" in
173
+ 2*)
174
+ if [ "$prefixed_hash" = "$root_hash" ]; then
175
+ _report fail "CDN behaviour registered" "response at $BASE/$PREFIX/ is byte-identical to $ROOT_URL — CloudFront is serving the PARENT's default behaviour, not this sibling (registration no-op)"
176
+ else
177
+ _report ok "CDN behaviour registered" "response at $BASE/$PREFIX/ differs from $ROOT_URL"
178
+ fi
179
+ ;;
180
+ *)
181
+ _report fail "CDN behaviour registered" "prefixed path did not return 2xx (HTTP $prefixed_code), cannot compare bodies"
182
+ ;;
183
+ esac
184
+ fi
185
+ else
186
+ echo " [UNPROVEN] CDN behaviour registered — PATH_PREFIX is empty (root sibling), so there is no distinct-from-root comparison this script can make. Root-sibling CDN registration is NOT covered by this check."
187
+ fi
188
+
189
+ echo
190
+ echo "routing-smoke-test: checked $CHECKED route(s) against $BASE (prefix='${PREFIX}')"
191
+
192
+ if [ "$CANNOT_TELL" -gt 0 ]; then
193
+ echo "::error::routing-smoke-test: $CANNOT_TELL of $CHECKED check(s) could not be evaluated (unreachable / DNS / transport error) — treating as failure, never a pass."
194
+ exit 2
195
+ fi
196
+
197
+ if [ "$FAILED" -gt 0 ]; then
198
+ echo "::error::routing-smoke-test: $FAILED of $CHECKED route check(s) failed."
199
+ exit 1
200
+ fi
201
+
202
+ if [ "$CHECKED" -eq 0 ]; then
203
+ echo "::error::routing-smoke-test: 0 routes were checked — refusing to report success over an empty denominator (#1363)."
204
+ exit 2
205
+ fi
206
+
207
+ echo "routing-smoke-test: all $CHECKED checked route(s) OK."
208
+ exit 0
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # Proves routing-smoke-test.sh (#737 item 3) actually catches the three
4
+ # recorded routing failures, and that it fails closed when it cannot tell.
5
+ #
6
+ # A verification script nobody has seen fail is not verified — every FAIL
7
+ # scenario below first ran (uncommented) against a plain `curl` stub before
8
+ # the real script existed, to confirm it does NOT catch these shapes without
9
+ # the fix; that manual step is recorded here in prose since there is nothing
10
+ # upstream of this script to diff against.
11
+ #
12
+ # Stubs `curl` on PATH so this needs no network and no live deployment.
13
+ #
14
+ # Run: sh scripts/routing-smoke-test.test.sh
15
+
16
+ set -u
17
+
18
+ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
19
+ TARGET="$SCRIPT_DIR/routing-smoke-test.sh"
20
+ STUB_DIR=$(mktemp -d)
21
+ trap 'rm -rf "$STUB_DIR"' EXIT
22
+
23
+ FAILURES=0
24
+ _assert_exit() {
25
+ # _assert_exit <scenario-name> <expected-exit> -- <env assignments already exported>
26
+ name=$1
27
+ expected=$2
28
+ actual_out=$(ROUTING_CURL="$STUB_DIR/curl" sh "$TARGET" 2>&1)
29
+ actual=$?
30
+ if [ "$actual" -eq "$expected" ]; then
31
+ echo "PASS: $name (exit $actual)"
32
+ else
33
+ echo "FAIL: $name — expected exit $expected, got $actual"
34
+ echo "--- output ---"
35
+ echo "$actual_out"
36
+ echo "--------------"
37
+ FAILURES=$((FAILURES + 1))
38
+ fi
39
+ }
40
+
41
+ BASE="https://dev.example.com"
42
+ PREFIX="reports"
43
+
44
+ # --- Stub curl factory ------------------------------------------------
45
+ # Each scenario writes its own stub before calling the target, since the
46
+ # response shape (which URL gets which body/status) IS the scenario.
47
+ _write_stub() {
48
+ cat > "$STUB_DIR/curl" <<'STUB'
49
+ #!/usr/bin/env sh
50
+ outfile=""
51
+ prev=""
52
+ for a in "$@"; do
53
+ case "$prev" in
54
+ -o) outfile="$a" ;;
55
+ esac
56
+ prev="$a"
57
+ done
58
+ url="$prev"
59
+
60
+ # ROUTING_TEST_MODE selects the fixture; each case below writes $outfile
61
+ # and prints the http status, mirroring curl -w '%{http_code}'.
62
+ case "$ROUTING_TEST_MODE" in
63
+ good)
64
+ case "$url" in
65
+ https://dev.example.com/reports) printf 'REPORTS-APP' > "$outfile"; printf '200'; exit 0 ;;
66
+ https://dev.example.com/reports/) printf 'REPORTS-APP' > "$outfile"; printf '200'; exit 0 ;;
67
+ https://dev.example.com/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
68
+ esac
69
+ ;;
70
+ cdn-noop)
71
+ # Every path — including the prefixed one — actually serves the root
72
+ # app's content, because CloudFront never registered a distinct
73
+ # behaviour for /reports and silently fell through to the default.
74
+ case "$url" in
75
+ https://dev.example.com/reports) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
76
+ https://dev.example.com/reports/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
77
+ https://dev.example.com/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
78
+ esac
79
+ ;;
80
+ prefix-mismatch)
81
+ # The app serves a DIFFERENT basePath than PATH_PREFIX declares, so the
82
+ # trailing-slash form 404s even though bare and root are fine.
83
+ case "$url" in
84
+ https://dev.example.com/reports) printf 'REPORTS-APP' > "$outfile"; printf '200'; exit 0 ;;
85
+ https://dev.example.com/reports/) printf 'Not Found' > "$outfile"; printf '404'; exit 0 ;;
86
+ https://dev.example.com/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
87
+ esac
88
+ ;;
89
+ bare-path-broken)
90
+ # Trailing-slash form works, bare form 404s (or vice versa — either
91
+ # asymmetry is the recorded "bare-path routing" variant).
92
+ case "$url" in
93
+ https://dev.example.com/reports) printf 'Not Found' > "$outfile"; printf '404'; exit 0 ;;
94
+ https://dev.example.com/reports/) printf 'REPORTS-APP' > "$outfile"; printf '200'; exit 0 ;;
95
+ https://dev.example.com/) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
96
+ esac
97
+ ;;
98
+ unreachable)
99
+ printf 'curl: (6) Could not resolve host: dev.example.com' >&2
100
+ exit 6
101
+ ;;
102
+ esac
103
+ echo "unhandled URL in stub: $url" >&2
104
+ exit 99
105
+ STUB
106
+ chmod +x "$STUB_DIR/curl"
107
+ }
108
+
109
+ # 1. GOOD deployment — all three checks should pass, exit 0.
110
+ _write_stub
111
+ ROUTING_TEST_MODE=good ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
112
+ _assert_exit "good deployment" 0
113
+
114
+ # 2. CDN registration no-op — prefixed path byte-identical to root.
115
+ # This is the variant a generic "does it 200" check CANNOT catch, because
116
+ # every URL here genuinely returns 200. Confirms the fix, not just a
117
+ # curl failure.
118
+ _write_stub
119
+ ROUTING_TEST_MODE=cdn-noop ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
120
+ _assert_exit "CDN registration no-op" 1
121
+
122
+ # 3. Path-prefix mismatch — trailing-slash form 404s.
123
+ _write_stub
124
+ ROUTING_TEST_MODE=prefix-mismatch ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
125
+ _assert_exit "path-prefix mismatch" 1
126
+
127
+ # 4. Bare-path routing — bare form 404s while trailing-slash works.
128
+ _write_stub
129
+ ROUTING_TEST_MODE=bare-path-broken ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
130
+ _assert_exit "bare-path routing broken" 1
131
+
132
+ # 5. Unreachable deployment — DNS/transport failure must fail CLOSED (exit
133
+ # 2, cannot-tell), never exit 0.
134
+ _write_stub
135
+ ROUTING_TEST_MODE=unreachable ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="$PREFIX" \
136
+ _assert_exit "unreachable deployment" 2
137
+
138
+ # 6. ROUTING_PATH_PREFIX genuinely unset (not empty) — cannot determine the
139
+ # expected path, must exit 2 without even invoking curl.
140
+ _write_stub
141
+ env -u ROUTING_PATH_PREFIX ROUTING_TEST_MODE=good ROUTING_BASE_URL="$BASE" \
142
+ sh -c 'ROUTING_CURL="'"$STUB_DIR"'/curl" sh "'"$TARGET"'"' >/tmp/routing-smoke-t6.$$ 2>&1
143
+ rc=$?
144
+ if [ "$rc" -eq 2 ]; then
145
+ echo "PASS: PATH_PREFIX unset (exit 2)"
146
+ else
147
+ echo "FAIL: PATH_PREFIX unset — expected exit 2, got $rc"
148
+ cat /tmp/routing-smoke-t6.$$
149
+ FAILURES=$((FAILURES + 1))
150
+ fi
151
+ rm -f /tmp/routing-smoke-t6.$$
152
+
153
+ # 7. ROUTING_BASE_URL unset entirely — must exit 2.
154
+ _write_stub
155
+ env -u ROUTING_BASE_URL ROUTING_TEST_MODE=good ROUTING_PATH_PREFIX="$PREFIX" \
156
+ sh -c 'ROUTING_CURL="'"$STUB_DIR"'/curl" sh "'"$TARGET"'"' >/tmp/routing-smoke-t7.$$ 2>&1
157
+ rc=$?
158
+ if [ "$rc" -eq 2 ]; then
159
+ echo "PASS: BASE_URL unset (exit 2)"
160
+ else
161
+ echo "FAIL: BASE_URL unset — expected exit 2, got $rc"
162
+ cat /tmp/routing-smoke-t7.$$
163
+ FAILURES=$((FAILURES + 1))
164
+ fi
165
+ rm -f /tmp/routing-smoke-t7.$$
166
+
167
+ # 8. Root sibling (PATH_PREFIX='', deliberately empty not unset) — the
168
+ # CDN-behaviour-registered check has nothing to compare against and must
169
+ # say so rather than silently passing; bare-path check still runs
170
+ # (denominator stays > 0), so exit is 0 when that succeeds.
171
+ _write_stub
172
+ cat > "$STUB_DIR/curl" <<'STUB'
173
+ #!/usr/bin/env sh
174
+ outfile=""
175
+ prev=""
176
+ for a in "$@"; do
177
+ case "$prev" in
178
+ -o) outfile="$a" ;;
179
+ esac
180
+ prev="$a"
181
+ done
182
+ url="$prev"
183
+ case "$url" in
184
+ https://dev.example.com) printf 'ROOT-APP' > "$outfile"; printf '200'; exit 0 ;;
185
+ esac
186
+ echo "unhandled URL in stub: $url" >&2
187
+ exit 99
188
+ STUB
189
+ chmod +x "$STUB_DIR/curl"
190
+ out=$(ROUTING_CURL="$STUB_DIR/curl" ROUTING_BASE_URL="$BASE" ROUTING_PATH_PREFIX="" sh "$TARGET" 2>&1)
191
+ rc=$?
192
+ if [ "$rc" -eq 0 ] && echo "$out" | grep -q "UNPROVEN"; then
193
+ echo "PASS: root sibling (exit 0, unproven CDN check reported)"
194
+ else
195
+ echo "FAIL: root sibling — expected exit 0 with UNPROVEN noted, got exit $rc"
196
+ echo "$out"
197
+ FAILURES=$((FAILURES + 1))
198
+ fi
199
+
200
+ echo
201
+ if [ "$FAILURES" -eq 0 ]; then
202
+ echo "routing-smoke-test.test.sh: all checks passed."
203
+ exit 0
204
+ else
205
+ echo "routing-smoke-test.test.sh: $FAILURES check(s) failed."
206
+ exit 1
207
+ fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.273.12",
3
+ "version": "0.273.13",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",