Skip to content

Carry on past a failed app, and report what actually made it - #10

Merged
AdamXweb merged 1 commit into
AdamXweb:mainfrom
adamXbot:fix/continue-past-app-failures
Aug 4, 2026
Merged

AdamXweb merged 1 commit into
AdamXweb:mainfrom
adamXbot:fix/continue-past-app-failures

Conversation

@adamXbot

@adamXbot adamXbot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

The bug

An update across 30 apps died at app 1 of 30:

[error] - [1/30] Starting audiobookshelf failed:
    Error response from daemon: error while creating mount source path '/Volumes/Media/Audiobooks': mkdir /Volumes/Media: permission denied
[warn] - Command finished with errors (exit 1)

run_step returned non-zero, set -e took the whole run down, and the other 29 apps were never touched. No summary was printed, and "Command finished with errors (exit 1)" was the only account of what happened — no sign of what had been updated, or how many apps succeeded versus failed.

What changed

One bad app no longer ends the run. Per-app steps are guarded explicitly rather than left to set -e: a failure is reported where it happens and the run carries on to the next app. enter_app returns instead of exiting, so a missing or unusable folder is skipped the same way. A backup whose archive fails now starts the app back up rather than leaving it stopped, and drops the partial archive.

Every run says what actually made it. The per-app table marks failed and skipped rows, and the run closes with a plain tally:

  ● authelia                 updated         4s
  ○ audiobookshelf           failed         12s
  ○ larapaper                skipped         0s
[warn] - Updated 27 of 30 apps in 3m 4s - 1 failed, 2 skipped
  failed:  audiobookshelf, vaultwarden
  skipped: larapaper, pinchflat-X

skipped means the app was left untouched (its pull failed, so it stays on the image it has); failed means the command was attempted and didn't work. The command exits non-zero when either bucket is non-empty, so cron and wrapper scripts can tell without parsing output, and the webhook carries the same tally.

Hints for the usual docker failures. Where the cause is recognisable, a [hint] line says what to do about it. All three failures from the original run now explain themselves:

[warn] - Pull failed for larapaper - leaving it on its current image
    read /Volumes/Fast/docker_volumes/larapaper/compose.yaml: is a directory
[hint] - something the compose file reads is a directory where a file was expected - a *folder* named compose.yaml/compose.yml beside the real compose file will do this

[warn] - Pull failed for pinchflat-X - leaving it on its current image
    no matching manifest for linux/arm64/v8 in the manifest list entries
[hint] - that image has no build for this machine (aarch64) - check which platforms the image publishes, or pin a tag that covers yours

[error] - [1/10] Starting audiobookshelf failed:
    Error response from daemon: error while creating mount source path '/Volumes/Media/Audiobooks': mkdir /Volumes/Media: permission denied
[hint] - docker couldn't create or reach a bind-mount path on the host - check those volume paths exist and are readable (an external or network drive has to be mounted first)

Also covered: a port already allocated, an image or tag that doesn't exist or is private, and a full disk. Unrecognised failures get no hint — the command's own output stays the source of truth.

Bug found along the way

Without a terminal (cron, pipes) run_step returned 0 unconditionally, so a failed step was never labelled as one and the run died with only docker's raw output as a clue. That path now captures, reports and propagates failures the same way the terminal path does.

Verification

End-to-end runs against a stub docker reproducing the exact failure mix from the report — 10 apps, two failing their pull (compose-file-is-a-directory, architecture mismatch), two failing to start (bind-mount permission, port conflict):

  • The run now completes all 10 apps instead of stopping at 1, reports Updated 6 of 10 apps - 2 failed, 2 skipped, and exits 1.
  • Missing app folder mid-list: reported, skipped, run continues, exit 1.
  • Backup with an unwritable archive target: archive fails, app is started again, recorded failed, run continues, exit 1.
  • Fully successful runs still print Started all 3 apps in 0s. and exit 0; single-app runs read correctly (Started 1 app, Started 0 of 1 app - 1 failed).
  • --dry-run unaffected.

Each of the above under Debian dash, with and without a pty. shellcheck clean (via the CI's own image), sh -n and dash -n pass.

🤖 Generated with Claude Code

An update over 30 apps died at [1/30]: audiobookshelf couldn't start
(a bind-mount path the daemon couldn't reach), run_step returned
non-zero, and `set -e` took the whole run down with it. The other 29
apps were never touched, no summary was printed, and the only clue was
"Command finished with errors (exit 1)".

Per-app steps are now guarded rather than left to `set -e`. A failure
is reported where it happens and the run carries on to the next app;
enter_app returns instead of exiting so a missing or unusable folder is
skipped the same way. A backup whose archive fails now starts the app
again rather than leaving it stopped, and drops the partial archive.

Every run closes with a tally of what actually made it - "Updated 27 of
30 apps in 3m 4s - 1 failed, 2 skipped" - followed by the names in each
bucket, with the per-app table marking failed and skipped rows. The
command exits non-zero when either bucket is non-empty so cron and
wrapper scripts can tell without parsing output, and the webhook
carries the same tally.

Where a failure has a recognisable cause, a [hint] line now says what
to do about it: an image with no build for this architecture, a
bind-mount path docker can't reach, a port already allocated, an image
or tag that doesn't exist, a full disk, or a *folder* named
compose.yaml shadowing the real compose file. Unrecognised failures get
no hint - the command's own output stays the source of truth.

Also fixes run_step silently swallowing failures without a terminal: it
returned 0 unconditionally in that path, so under cron a failed step
was never labelled and the run died with only docker's raw output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AdamXweb
AdamXweb merged commit a714058 into AdamXweb:main Aug 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants