@@ -100,7 +100,7 @@ _ft_snapshot() {
100100 { set +x; } 2> /dev/null
101101 printf ' [DEPLOY] -- SYSTEM SNAPSHOT ----------------------------------------\n' >&2
102102 printf ' [DEPLOY] slot_file = %s\n' " $( cat " ${ACTIVE_SLOT_FILE:-/ var/ run/ api/ active-slot} " 2> /dev/null || echo ' MISSING' ) " >&2
103- printf ' [DEPLOY] nginx_upstream = %s\n' " $( grep -oE ' server (api-blue|api-green):3000' " ${NGINX_CONF:- $HOME / api/ infra/ nginx/ live/ api.conf} " 2> /dev/null | head -1 || echo ' unreadable' ) " >&2
103+ printf ' [DEPLOY] nginx_upstream = %s\n' " $( grep -oE ' http:// (api-blue|api-green):3000' " ${NGINX_CONF:- $HOME / api/ infra/ nginx/ live/ api.conf} " 2> /dev/null | grep -oE ' api-blue|api-green ' | head -1 || echo ' unreadable' ) " >&2
104104 printf ' [DEPLOY] containers =\n' >&2
105105 docker ps --format ' [DEPLOY] {{.Names}} -> {{.Status}} ({{.Ports}})' 1>&2 2> /dev/null \
106106 || printf ' [DEPLOY] (docker ps unavailable)\n' >&2
150150# ---------------------------------------------------------------------------
151151# CONSTANTS
152152# ---------------------------------------------------------------------------
153- IMAGE=" ghcr.io/fieldtrack-tech/api:${1:- latest} "
154- IMAGE_SHA=" ${1:- latest} "
153+ # Immutable SHA tags ONLY — 'latest' is forbidden in production.
154+ # Reject empty and 'latest' before any Docker operation so failures are
155+ # loud and attributed to the caller rather than appearing as pull errors.
156+ IMAGE_SHA=" ${1:- } "
157+ if [ -z " $IMAGE_SHA " ] || [ " $IMAGE_SHA " = " latest" ]; then
158+ printf ' [DEPLOY] ts=%s state=INIT level=ERROR msg="image SHA required -- latest tag is forbidden in production" sha=%s\n' \
159+ " $( date -u +" %Y-%m-%dT%H:%M:%SZ" ) " " ${IMAGE_SHA:- <empty>} " >&2
160+ exit 2
161+ fi
162+ IMAGE=" ghcr.io/fieldtrack-tech/api:$IMAGE_SHA "
155163
156164BLUE_NAME=" api-blue"
157165GREEN_NAME=" api-green"
@@ -403,7 +411,7 @@ _ft_resolve_slot() {
403411 elif [ " $blue_running " = " true" ] && [ " $green_running " = " true" ]; then
404412 # Both running -- read nginx upstream container as authoritative tiebreaker.
405413 local nginx_upstream
406- nginx_upstream=$( grep -oE ' server (api-blue|api-green):3000' " $NGINX_CONF " 2> /dev/null | grep -oE ' api-blue|api-green' | head -1 || echo " " )
414+ nginx_upstream=$( grep -oE ' http:// (api-blue|api-green):3000' " $NGINX_CONF " 2> /dev/null | grep -oE ' api-blue|api-green' | head -1 || echo " " )
407415 if [ " $nginx_upstream " = " api-blue" ]; then recovered_slot=" blue"
408416 elif [ " $nginx_upstream " = " api-green" ]; then recovered_slot=" green"
409417 else
@@ -943,7 +951,8 @@ _ft_log "msg='nginx reloaded' upstream=$INACTIVE_NAME:$APP_PORT"
943951
944952# Upstream sanity check -- confirm nginx config actually points at the new container.
945953# Catches template substitution failures before traffic is affected.
946- _RELOAD_CONTAINER=$( grep -oE ' server (api-blue|api-green):3000' " $NGINX_CONF " 2> /dev/null | grep -oE ' api-blue|api-green' | head -1 || echo " " )
954+ # Upstream sanity: live config must contain http://INACTIVE_NAME:3000 (set $api_backend format)
955+ _RELOAD_CONTAINER=$( grep -oE ' http://(api-blue|api-green):3000' " $NGINX_CONF " 2> /dev/null | grep -oE ' api-blue|api-green' | head -1 || echo " " )
947956if [ " $_RELOAD_CONTAINER " != " $INACTIVE_NAME " ]; then
948957 _ft_log " level=ERROR msg='nginx upstream sanity check failed after reload' expected=$INACTIVE_NAME actual=${_RELOAD_CONTAINER:- unreadable} "
949958 cp " $NGINX_BACKUP " " $NGINX_CONF "
@@ -1030,8 +1039,8 @@ for _attempt in 1 2 3 4 5; do
10301039 sleep 5
10311040done
10321041
1033- # Container alignment check -- live nginx config MUST point at the new container .
1034- _NGINX_CONTAINER=$( grep -oE ' server (api-blue|api-green):3000' " $NGINX_CONF " 2> /dev/null | grep -oE ' api-blue|api-green' | head -1 || echo " " )
1042+ # Container alignment check -- live nginx config MUST contain http://INACTIVE_NAME:3000 .
1043+ _NGINX_CONTAINER=$( grep -oE ' http:// (api-blue|api-green):3000' " $NGINX_CONF " 2> /dev/null | grep -oE ' api-blue|api-green' | head -1 || echo " " )
10351044if [ -n " $_NGINX_CONTAINER " ] && [ " $_NGINX_CONTAINER " != " $INACTIVE_NAME " ]; then
10361045 _ft_log " level=ERROR msg='nginx container mismatch -- slot switch did not take effect' expected=$INACTIVE_NAME actual=$_NGINX_CONTAINER "
10371046 _PUB_PASSED=false
@@ -1193,8 +1202,8 @@ else
11931202 _FT_TRUTH_CHECK_PASSED=false
11941203fi
11951204
1196- # (2) Verify nginx upstream container matches target
1197- _NGINX_CONTAINER=$( grep -oE ' server (api-blue|api-green):3000' " $NGINX_CONF " 2> /dev/null | grep -oE ' api-blue|api-green' | head -1 || echo " " )
1205+ # (2) Verify nginx upstream container matches target (set $api_backend format)
1206+ _NGINX_CONTAINER=$( grep -oE ' http:// (api-blue|api-green):3000' " $NGINX_CONF " 2> /dev/null | grep -oE ' api-blue|api-green' | head -1 || echo " " )
11981207if [ -n " $_NGINX_CONTAINER " ]; then
11991208 if [ " $_NGINX_CONTAINER " != " $INACTIVE_NAME " ]; then
12001209 _ft_log " level=ERROR msg='truth check failed: nginx container mismatch' expected=$INACTIVE_NAME actual=$_NGINX_CONTAINER "
0 commit comments