Skip to content

Fix deploy hangs and env_file word-splitting, on a new test harness - #22

Merged
ericof merged 4 commits into
mainfrom
issues-21-13-19
Sep 4, 2026
Merged

Fix deploy hangs and env_file word-splitting, on a new test harness#22
ericof merged 4 commits into
mainfrom
issues-21-13-19

Conversation

@ericof

@ericof ericof commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fixes the three open deploy-hang / env_file bugs, on top of a test harness the repository did not have.

Closes #13
Closes #19
Closes #21

The fixes

#21env_file values with spaces. The file went through an unquoted $(...), so the shell word-split it before export ran. Now read one line at a time, value taken verbatim to match docker --env-file. A line that is not NAME=VALUE is rejected naming that line, which also makes #3's failure legible without changing the option's semantics.

#13 — completed replicated-job services. A finished job reports 0/1 (1/1 completed); the completion count was cut away before anything read it, leaving current != target forever.

#19 — services the stack file scales to zero. As @davisagli noted on the issue, this is indistinguishable from #13 by the replicas field alone. So it consults .Spec.Mode.Replicated.Replicas — what the stack file asks for — instead of the observed target, re-read every poll so a stale target corrects itself. A service that genuinely wants replicas still times out, and there is a test for that. Both failure paths now dump docker stack ps --no-trunc, which @fredvd asked for.

Test harness

Every bug here was in code with no test and no lint, so the first commit adds both. The runner image is built FROM the action image, not bats/bats: the scripts need GNU xargs from findutils, and against busybox xargs the env_file tests fail for reasons unrelated to the code. Tests drive a fake docker CLI from fixtures, so no Swarm is needed. Each bug test was committed skipped, then un-skipped by the commit that fixes it.

Also: stack-wait.sh is now maintained as a fork of sudo-bmitch upstream rather than tracking it; towncrier for the change log with a CI check; a dependabot config.

Worth a second opinion

  1. stack-wait.sh now rejects unknown flags instead of ignoring them — a typo'd flag used to still deploy.
  2. env_file now fails on a malformed line instead of exporting nothing and continuing.
  3. Not included: --detach=false, suggested on Dont' wait for services that are by default scaled to zero. #19. It makes deploy block until convergence, replacing the configurable deploy_timeout with docker's own waiting, and could hang on precisely these services.

Verification

make lint clean, make test 43 passing / 0 skipped. Both fixes mutation-tested — removing either fails its own tests independently. make create-tag run end-to-end in a sandboxed clone to confirm the release path.

Introduce a bats suite for the two shell scripts, run in CI alongside
shellcheck. Neither existed before, so every open shell bug in the
tracker sits in code with no test and no lint covering it.

The runner image is built FROM the action image on purpose: the scripts
need GNU xargs from findutils, and against the busybox xargs in a plain
alpine image the env_file tests fail for reasons unrelated to the code.

Tests drive a fake docker CLI from fixtures, so no Swarm is needed. A
test documenting an open bug calls skip with a link to the issue and
asserts the desired behaviour, so fixing the bug means deleting the skip
line rather than writing a new test. Issues 13, 19 and 21 each have one.

scripts/stack-wait.sh is now maintained as a fork rather than tracking
upstream, and its header says so. All shellcheck findings are resolved;
where word splitting is intentional the line carries an explanatory
disable instead of a change. One behaviour change: an unknown flag is
now rejected instead of being silently ignored.

Also adds a dependabot config covering both the workflows and the
Dockerfile base image, and bumps actions/checkout to v7.

Refs #13, #19, #21
The whole file was passed through an unquoted command substitution, so
the shell word-split it before export ran. Any value with a space in it
was truncated to its first token and the remainder was handed to export
as a separate argument, which then failed with a message naming the
second token:

    export: `-Xmx1536m': not a valid identifier

No quoting style in the env file could work around it, because the split
happened after substitution. JVM options were the obvious casualty, but
connection strings and user agents hit it too.

Read one line at a time instead and pass export a single quoted
argument. Values are taken verbatim, matching `docker --env-file`, so
quotes in the file are part of the value rather than delimiters around
it. The SC2046 suppression that was hiding this is gone.

A line that is not NAME=VALUE is now rejected with a message naming the
offending line, rather than reaching export and producing one about
identifiers. This is also the error someone gets after passing a path
where the option wants the variables themselves.

Note that ENV_FILE carries no trailing newline, so the loop needs the
-n test to avoid dropping the last line.

Closes #21
Both open timeout reports come from the same place: the wait loop treats
"fewer replicas running than wanted" as "still converging", which is
wrong for any service that is not supposed to be running.

A `mode: replicated-job` service that has finished reports its replicas
as "0/1 (1/1 completed)". The completion count was cut away before
anything looked at it, leaving current(0) != target(1) forever. Read the
whole field and use the count: a job is settled once every requested run
has completed, and reported as job_running until then.

A service the stack file declares with `replicas: 0` is a different
case, and indistinguishable from the one above by the replicas field
alone -- there is no completion count to disambiguate a finished run
from a service that failed to start. So consult the spec instead of the
observed target. The two disagree exactly when this bites: stack deploy
returns before the new spec is applied, and an external scheduler may
have scaled the service since. The spec is re-read every poll, so a
stale target corrects itself rather than being trusted once.

A service whose spec really does ask for replicas still times out; that
is covered by a test, because the shortcut above would be easy to widen
into swallowing genuine failures.

On either failure the task list is now dumped via docker stack ps. The
per-service log only reports changes, so working out which service
stalled previously meant reading back through the whole log.

Not included: passing --detach=false to docker stack deploy, suggested
on the second issue. It would make deploy block until convergence, which
replaces this script's configurable timeout with docker's own waiting
and could hang on precisely the services above. Worth its own
discussion rather than folding in here.

Closes #13
Closes #19
Adds the towncrier config, an empty CHANGELOG.md, the fragment template
and a workflow that fails a pull request carrying no news fragment. News
fragments for the three fixes on this branch come with it.

Two things had to follow from it. The changelog workflow exempts a pull
request labelled "skip changelog", and that label did not exist in the
repository, so the escape hatch worked for nobody; it exists now.
Dependabot cannot write a fragment for itself, so both of its update
blocks carry that label, otherwise every dependency bump would arrive
with a failing check.

The generated CHANGELOG.md pointed contributors at Plone's contributing
guide. It points at this repository's README instead, which now
documents the fragment types, the naming convention and the label.

Also excludes the changelog files from the docker build context, the way
README.md and Makefile already are.
@ericof
ericof requested a review from davisagli September 4, 2026 00:19
@ericof
ericof merged commit b4bc31e into main Sep 4, 2026
3 checks passed
@ericof
ericof deleted the issues-21-13-19 branch September 4, 2026 13:33

@davisagli davisagli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ericof thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants