Fix: stack-deploy.sh now picks export_raw/export_raw_merged for folder+extras stacks (docker compose config path) vs export/export_merged for single-file stacks, matching envparse.py's dual-escaping fix
ci/woodpecker/push/deploy Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful

This commit is contained in:
2026-08-26 23:26:32 -07:00
parent 4af1209565
commit 162700bb4f
+30 -8
View File
@@ -28,6 +28,16 @@
# source path exists, and flags suspicious-looking empty Postgres data # source path exists, and flags suspicious-looking empty Postgres data
# dirs, before anything touches Swarm. # dirs, before anything touches Swarm.
# 3. docker stack deploy -c <tempfile> <stack> # 3. docker stack deploy -c <tempfile> <stack>
#
# Dollar-escaping (2026-08-26, see envparse.py comments for full detail):
# - Single-file path (no extras): envsubst does no '$' escaping of its
# own, and only Swarm's `docker stack deploy` interpolation pass runs
# downstream -> use export/export_merged (escapes '$' -> '$$' once).
# - Folder+extras path: `docker compose config` ALSO does its own '$'
# escaping on top of Swarm's -> use export_raw/export_raw_merged (no
# pre-escaping) or values get doubled twice. Getting this wrong
# silently corrupts any secret/hash containing '$' (confirmed impact:
# LITELLM keys truncated, IMMICH_KIOSK_BASICAUTH bcrypt hash mismatched).
set -euo pipefail set -euo pipefail
@@ -94,6 +104,12 @@ for f in "${EXTRAS[@]:-}"; do
[ -n "$f" ] && F_FLAGS+=(-f "$f") [ -n "$f" ] && F_FLAGS+=(-f "$f")
done done
# Whether the render will go through `docker compose config` (folder mode
# with 1+ extras). This determines which escaping mode is correct — see
# header comment and envparse.py for why these must differ.
USES_COMPOSE_CONFIG=0
[ "${#F_FLAGS[@]}" -gt 2 ] && USES_COMPOSE_CONFIG=1
# ── Load env (global base + optional stack override) ───────────────────────── # ── Load env (global base + optional stack override) ─────────────────────────
HINT=" Hint: 'secret not found' means Woodpecker hasn't provisioned secrets yet.\n Trigger the pipeline: https://woodpecker.bryanmail.net\n" HINT=" Hint: 'secret not found' means Woodpecker hasn't provisioned secrets yet.\n Trigger the pipeline: https://woodpecker.bryanmail.net\n"
@@ -103,19 +119,25 @@ STACK_EXISTS=0
[ -f "$GLOBAL_ENV" ] && GLOBAL_EXISTS=1 [ -f "$GLOBAL_ENV" ] && GLOBAL_EXISTS=1
[ -f "$ENVFILE" ] && STACK_EXISTS=1 [ -f "$ENVFILE" ] && STACK_EXISTS=1
if [ "$USES_COMPOSE_CONFIG" -eq 1 ]; then
EXPORT_MODE="export_raw"; EXPORT_MERGED_MODE="export_raw_merged"
else
EXPORT_MODE="export"; EXPORT_MERGED_MODE="export_merged"
fi
if [ "$GLOBAL_EXISTS" -eq 1 ] && [ "$STACK_EXISTS" -eq 1 ]; then if [ "$GLOBAL_EXISTS" -eq 1 ] && [ "$STACK_EXISTS" -eq 1 ]; then
echo " Env: $GLOBAL_ENV + $ENVFILE (stack overrides global)" echo " Env: $GLOBAL_ENV + $ENVFILE (stack overrides global) [$EXPORT_MERGED_MODE]"
eval "$(python3 "$PY" export_merged "$GLOBAL_ENV" "$ENVFILE")" eval "$(python3 "$PY" "$EXPORT_MERGED_MODE" "$GLOBAL_ENV" "$ENVFILE")"
VARS="$(python3 "$PY" vars_merged "$GLOBAL_ENV" "$ENVFILE")" VARS="$(python3 "$PY" vars_merged "$GLOBAL_ENV" "$ENVFILE")"
elif [ "$GLOBAL_EXISTS" -eq 1 ]; then elif [ "$GLOBAL_EXISTS" -eq 1 ]; then
echo " Env: $GLOBAL_ENV (no stack env)" echo " Env: $GLOBAL_ENV (no stack env) [$EXPORT_MODE]"
eval "$(python3 "$PY" export "$GLOBAL_ENV")" eval "$(python3 "$PY" "$EXPORT_MODE" "$GLOBAL_ENV")"
VARS="$(python3 "$PY" vars "$GLOBAL_ENV")" VARS="$(python3 "$PY" vars "$GLOBAL_ENV")"
elif [ "$STACK_EXISTS" -eq 1 ]; then elif [ "$STACK_EXISTS" -eq 1 ]; then
echo " Env: $ENVFILE (no global env)" echo " Env: $ENVFILE (no global env) [$EXPORT_MODE]"
eval "$(python3 "$PY" export "$ENVFILE")" eval "$(python3 "$PY" "$EXPORT_MODE" "$ENVFILE")"
VARS="$(python3 "$PY" vars "$ENVFILE")" VARS="$(python3 "$PY" vars "$ENVFILE")"
else else
@@ -131,7 +153,7 @@ RENDERED="$(mktemp /tmp/stack-deploy.XXXXXX.yml)"
trap 'rm -f "$RENDERED"' EXIT trap 'rm -f "$RENDERED"' EXIT
if [ -n "$VARS" ]; then if [ -n "$VARS" ]; then
if [ "${#F_FLAGS[@]}" -gt 2 ]; then if [ "$USES_COMPOSE_CONFIG" -eq 1 ]; then
# Folder mode with extras: merge via docker compose config # Folder mode with extras: merge via docker compose config
docker compose "${F_FLAGS[@]}" config \ docker compose "${F_FLAGS[@]}" config \
| python3 "$PY" strip \ | python3 "$PY" strip \
@@ -144,7 +166,7 @@ if [ -n "$VARS" ]; then
> "$RENDERED" > "$RENDERED"
fi fi
else else
if [ "${#F_FLAGS[@]}" -gt 2 ]; then if [ "$USES_COMPOSE_CONFIG" -eq 1 ]; then
docker compose "${F_FLAGS[@]}" config \ docker compose "${F_FLAGS[@]}" config \
| python3 "$PY" strip \ | python3 "$PY" strip \
> "$RENDERED" > "$RENDERED"