Closes the bug part of issue #207 (reported by @tuxpowered).
Root cause
The base image (python:3.12-slim-trixie) ships with /bin/sh (dash) but no /bin/bash. The deployer invokes hook scripts via ['sh', '-c', command] (modules/core/deployer.py:187); dash then execs the script path and the kernel reads the shebang. A script starting with #!/bin/bash causes execve('/bin/bash', ...) to return ENOENT, which surfaces as exit code 127 in the deploy history.
There was also a latent inconsistency: the Dockerfile already declared useradd --create-home --shell /bin/bash certmate, so the certmate user's login shell was bash — but bash itself was never installed.
Fix
One-line change in the Dockerfile: add bash to the apt-get install -y line so /bin/bash resolves. Python deployer code is untouched — operator hooks remain executed via sh -c, the shebang still controls which interpreter runs the script body.
Operator impact
- Existing deploy hooks with
#!/bin/shkeep working (no change). - Hooks with
#!/bin/bash(the common default for ops scripts) now run instead of failing with 127. - Image size delta: roughly +6 MB compressed for bash + its data files.
Verification
Built certmate:v2.6.10-test from the merge commit and ran:
=== bash presence ===
/usr/bin/bash
GNU bash, version 5.2.37(1)-release (aarch64-unknown-linux-gnu)
=== shebang resolution: #!/bin/bash via sh -c (mirrors deployer.py:187-194) ===
shebang fired: 5.2.37(1)-release
CERTMATE_DOMAIN=test.example.com
curl present: /usr/bin/curl
exit=0
=== baseline: what 127 looked like in v2.6.9 ===
sh: 1: /tmp/missing-interp.sh: not found
exit=127 (expected non-zero for missing interp)
/bin/bash and /usr/bin/bash both resolve via Debian's usrmerge symlink (/bin -> usr/bin), so a literal #!/bin/bash shebang now works.
Full Python suite (no source-code changes): 871 passed, 14 skipped, 2 xfailed, 0 failed.
Out of scope (separate follow-ups tracked against #207)
- Click-through drill-down on Recent Executions to inspect stdout / stderr / payload / response (backend already stores these in
data/deploy_history.jsonl; the UI just doesn't surface them). - Relabel "Hook name" → "path to script or cmd" + "Description" prefix on the example field.
- A dedicated webhook configuration flow (URL + method + auth + freeform JSON payload + variable selector), distinct from the shell-script hook flow.