diff --git a/deploy/envparse.py b/deploy/envparse.py index c74ad48..06e1946 100644 --- a/deploy/envparse.py +++ b/deploy/envparse.py @@ -22,7 +22,7 @@ def merge_envs(base_path, override_path): merged = {**base, **override} return list(merged.items()) -# ───────────────────────────────────────────────────────────────────────────── +# ────────────────────────────────────────────────────────────────────────── # 2026-08-26 FIX — double-interpolation truncation bug (Pattern B stacks): # # 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 # build envsubst's space-separated $VARNAME allowlist string and have # nothing to do with actual values. -# ───────────────────────────────────────────────────────────────────────────── +# ────────────────────────────────────────────────────────────────────────── def escape_dollar(v): 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 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..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] if mode == 'export': for k, v in parse_env(sys.argv[2]): @@ -82,4 +182,5 @@ elif mode == 'vars_merged': elif mode == 'strip': t = sys.stdin.read() t = re.sub(r'[ \t]*env_file:[ \t]*\n([ \t]+-[^\n]*\n)+', '', t) + t = collapse_depends_on(t) sys.stdout.write(t)