Fix: envparse.py strip mode now also drops top-level 'name:' key that docker compose config emits but Swarm's stack deploy schema rejects
ci/woodpecker/push/deploy Pipeline was successful

This commit is contained in:
2026-08-26 22:37:38 -07:00
parent 87a254c902
commit b620682a41
+31 -3
View File
@@ -156,9 +156,6 @@ def collapse_depends_on(text):
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:
@@ -166,6 +163,36 @@ def collapse_depends_on(text):
return '\n'.join(out)
# ──────────────────────────────────────────────────────────────────────────
# 2026-08-26 FIX — top-level `name:` property rejected by Swarm:
#
# `docker compose config` (Compose Spec output) emits a top-level
# `name: <project>` key as the first line of the rendered document, e.g.:
# name: immich
# services:
# ...
#
# This is valid Compose Spec but `docker stack deploy` (Swarm mode) uses
# a stricter schema that does NOT allow a top-level `name` property, and
# rejects the whole document with: "(root) Additional property name is
# not allowed". Single-file stacks (no extras) never hit this because
# they skip `docker compose config` and go straight through envsubst on
# the raw source YAML, which never had a top-level `name:` to begin with.
#
# Fix: drop ONLY the top-level `name:` line (column 0, i.e. no leading
# whitespace) at the START of the document. Nested `name:` fields under
# networks/volumes/secrets (e.g. `name: postgresql_db-backend`, always
# indented) are legitimate and must NOT be touched.
# ──────────────────────────────────────────────────────────────────────────
_TOP_NAME_RE = re.compile(r'^name:\s*\S+\s*$')
def strip_top_level_name(text):
lines = text.split('\n')
if lines and _TOP_NAME_RE.match(lines[0]):
lines = lines[1:]
return '\n'.join(lines)
mode = sys.argv[1]
if mode == 'export':
for k, v in parse_env(sys.argv[2]):
@@ -183,4 +210,5 @@ 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)
t = strip_top_level_name(t)
sys.stdout.write(t)