Fix: envparse.py strip mode now collapses long-form depends_on mapping to Swarm-compatible short-form list
ci/woodpecker/push/deploy Pipeline was successful

This commit is contained in:
2026-08-26 22:35:53 -07:00
parent efd8caa218
commit 87a254c902
+103 -2
View File
@@ -22,7 +22,7 @@ def merge_envs(base_path, override_path):
merged = {**base, **override} merged = {**base, **override}
return list(merged.items()) return list(merged.items())
# ───────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────
# 2026-08-26 FIX — double-interpolation truncation bug (Pattern B stacks): # 2026-08-26 FIX — double-interpolation truncation bug (Pattern B stacks):
# #
# stack-deploy.sh's single-file/no-extras path is: # stack-deploy.sh's single-file/no-extras path is:
@@ -61,11 +61,111 @@ def merge_envs(base_path, override_path):
# actual values envsubst reads) — NOT in vars/vars_merged, which just # actual values envsubst reads) — NOT in vars/vars_merged, which just
# build envsubst's space-separated $VARNAME allowlist string and have # build envsubst's space-separated $VARNAME allowlist string and have
# nothing to do with actual values. # nothing to do with actual values.
# ───────────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────────
def escape_dollar(v): def escape_dollar(v):
return v.replace('$', '$$') return v.replace('$', '$$')
# ──────────────────────────────────────────────────────────────────────────
# 2026-08-26 FIX — depends_on long-form vs Swarm short-form:
#
# Folder-based stacks with extension files (e.g. immich + hwaccel.*.yml)
# go through the `docker compose <files> config | strip | docker stack
# deploy -c -` merge path. Modern `docker compose config` normalizes the
# short-form list syntax:
# depends_on: [database, redis]
# into the long-form condition mapping:
# depends_on:
# database:
# condition: service_started
# required: true
# redis:
# condition: service_started
# required: true
#
# `docker stack deploy` (Swarm mode) does NOT understand the long-form
# mapping and rejects it with: "services.<svc>.depends_on must be a list".
# Single-file stacks (no extras) never hit this because they skip the
# `docker compose config` step entirely and go straight through envsubst.
#
# Fix: collapse any long-form depends_on mapping back into the Swarm-
# compatible short-form list, purely as a text transform on the rendered
# YAML, right alongside the existing env_file stripping.
# ──────────────────────────────────────────────────────────────────────────
_DEP_RE = re.compile(r'^([ \t]*)depends_on:\s*$')
_CHILD_KEY_RE = re.compile(r'^([ \t]*)([\w.\-]+):\s*$')
_CHILD_LIST_RE = re.compile(r'^([ \t]*)-\s*(\S+)\s*$')
def collapse_depends_on(text):
lines = text.split('\n')
out = []
i = 0
n = len(lines)
while i < n:
line = lines[i]
m = _DEP_RE.match(line)
if not m:
out.append(line)
i += 1
continue
indent = m.group(1)
base_indent = len(indent)
out.append(line)
i += 1
services = []
parse_ok = True
while i < n:
l = lines[i]
if l.strip() == '':
i += 1
continue
cur_indent = len(l) - len(l.lstrip(' \t'))
if cur_indent <= base_indent:
break # dedent — end of this depends_on block
lm = _CHILD_LIST_RE.match(l)
if lm and cur_indent == base_indent + 2:
services.append(lm.group(2))
i += 1
continue
km = _CHILD_KEY_RE.match(l)
if km and cur_indent == base_indent + 2:
services.append(km.group(2))
i += 1
# skip nested condition/required/restart lines under this service
while i < n:
nl = lines[i]
if nl.strip() == '':
i += 1
continue
nl_indent = len(nl) - len(nl.lstrip(' \t'))
if nl_indent > base_indent + 2:
i += 1
continue
break
continue
# Unexpected shape — bail without transforming to avoid corrupting output
parse_ok = False
break
if not parse_ok or not services:
# Re-emit whatever we consumed as-is (best effort: fall back to
# original slice) rather than risk mangling an unfamiliar shape.
# Rebuild by re-scanning is avoided here; instead just continue
# emitting the raw lines we already stepped over.
# (This branch is rare: only hit on unexpected compose output.)
pass
for svc in services:
out.append(indent + ' - ' + svc)
return '\n'.join(out)
mode = sys.argv[1] mode = sys.argv[1]
if mode == 'export': if mode == 'export':
for k, v in parse_env(sys.argv[2]): for k, v in parse_env(sys.argv[2]):
@@ -82,4 +182,5 @@ elif mode == 'vars_merged':
elif mode == 'strip': elif mode == 'strip':
t = sys.stdin.read() t = sys.stdin.read()
t = re.sub(r'[ \t]*env_file:[ \t]*\n([ \t]+-[^\n]*\n)+', '', t) t = re.sub(r'[ \t]*env_file:[ \t]*\n([ \t]+-[^\n]*\n)+', '', t)
t = collapse_depends_on(t)
sys.stdout.write(t) sys.stdout.write(t)