Bake .git/HEAD into Docker image so branch detection actually works

The SpoolBuddy remote-update flow always pulled `main` on the remote
  device when Bambuddy ran under Docker, regardless of which branch the
  image was built from. Root cause: the Dockerfile COPYs only backend/
  and static/, and .dockerignore excluded .git entirely, so the container
  had no git metadata anywhere. detect_current_branch() silently fell
  through its file-read path and returned the GIT_BRANCH env-var default
  of "main".

  The old subprocess-based implementation had the same bug but it was
  masked twice: no .git in the image AND no `git` binary in the image,
  so git rev-parse raised FileNotFoundError, the bare except swallowed
  it, and the fallback kicked in.

  Let the one file we actually need (.git/HEAD — ~20 bytes containing
  `ref: refs/heads/<branch>`) through the .dockerignore filter and COPY
  it into the image at /app/.git/HEAD. detect_current_branch() already
  reads exactly that path, so no Python code changes are needed. Bind-
  mount development setups are unaffected — the bind mount overlays the
  baked-in file with the live repo's .git/HEAD.

  Verified with a throwaway alpine build using the same .dockerignore
  pattern: .git/HEAD passes through, decoy .git/refs and .git/objects
  entries are excluded, and COPY writes the expected content into the
  image.
This commit is contained in:
maziggy
2026-04-10 11:11:39 +02:00
parent 7e7372c469
commit 2722ed1538
3 changed files with 18 additions and 2 deletions
+8 -1
View File
@@ -1,5 +1,12 @@
# Git
.git
# Exclude all .git contents EXCEPT HEAD. HEAD is a tiny text file (under
# 100 bytes) containing e.g. `ref: refs/heads/dev`, which the Dockerfile
# copies into the image so detect_current_branch() in spoolbuddy_ssh.py
# can report the correct branch for SpoolBuddy remote updates. Without
# this, the production image has no git metadata at all and always falls
# back to "main" regardless of which branch the operator built from.
.git/*
!.git/HEAD
.gitignore
# Python
+1 -1
View File
@@ -13,7 +13,7 @@ All notable changes to Bambuddy will be documented in this file.
### Fixed
- **External Sidebar Link Icon Not Showing** ([#878](https://github.com/maziggy/bambuddy/issues/878)) — Custom icons uploaded for external sidebar links rendered correctly in the edit dialog but were missing from the sidebar itself, and opening the icon URL directly returned `{"detail":"Valid camera stream token required..."}`. The sidebar `<img>` tag in `Layout.tsx` used a raw `/api/v1/external-links/{id}/icon` URL, but that endpoint is protected by a query-string stream token (the same mechanism used for camera streams and archive thumbnails, because `<img>` tags cannot send Authorization headers). The edit dialog already routed through `api.getExternalLinkIconUrl()`, which wraps the URL via `withStreamToken()`; the sidebar now does the same, so icons appear when auth is enabled.
- **Shortest Job First Toggle Disappears After Clicking** ([#879](https://github.com/maziggy/bambuddy/issues/879)) — The SJF toggle badge on the queue page was rendered inside the Pending Queue section header, which is only shown when there is at least one pending item and the list view is active. Clicking the toggle often coincided with the scheduler starting the only pending print, at which point the Pending section unmounted and the toggle vanished along with it — making it look like the button had disappeared after clicking. The toggle has been moved to the top of the queue page, next to the list/timeline view switcher, so it stays reachable regardless of pending-item count, active filters, or the selected view mode.
- **SpoolBuddy Update Fails in Docker with "no user exists for uid 1000/1001"** — The SpoolBuddy remote-update flow shelled out to the OpenSSH `ssh-keygen` and `ssh` binaries for keypair creation and command execution. Both binaries call `getpwuid(getuid())` at startup and abort with `No user exists for uid <N>` when the container runs under an arbitrary PUID that is not listed in `/etc/passwd` (the stock `python:3.13-slim` image only has an entry for root, so running with `user: "1000:1000"`, `"1001:1001"`, or any non-root user tripped the same error). The entire SpoolBuddy update path is now subprocess-free: keypairs are generated in-process via the `cryptography` library (already a dependency), SSH commands run through the pure-Python `asyncssh` client, and git-branch detection reads `.git/HEAD` directly instead of shelling out to `git`. asyncssh also calls `getpass.getuser()` for local `~/.ssh/config` host matching, which hit the same passwd lookup failure; the Docker image now sets `LOGNAME=bambuddy`, `USER=bambuddy`, and `HOME=/app` so `getpass.getuser()` resolves via env vars before touching the passwd database, and `asyncssh.connect()` is called with `config=[]` so it does not attempt to load `~/.ssh/config` at all. Branch detection also now looks for `.git` in the *application root* rather than `settings.base_dir` — in Docker the data directory is a separate volume (`DATA_DIR=/app/data`) and never contains `.git`, so the previous code always silently fell through to the `main` fallback and pushed the wrong branch to the SpoolBuddy device regardless of which branch Bambuddy itself was on. Native installs behave identically — they already worked because the running user was always in `/etc/passwd` and `base_dir` happened to coincide with the project root. Regression tests assert that neither keypair creation nor command execution spawns any subprocess, and that branch detection reads from the application root even when a decoy `.git` sits inside the data dir.
- **SpoolBuddy Update Fails in Docker with "no user exists for uid 1000/1001"** — The SpoolBuddy remote-update flow shelled out to the OpenSSH `ssh-keygen` and `ssh` binaries for keypair creation and command execution. Both binaries call `getpwuid(getuid())` at startup and abort with `No user exists for uid <N>` when the container runs under an arbitrary PUID that is not listed in `/etc/passwd` (the stock `python:3.13-slim` image only has an entry for root, so running with `user: "1000:1000"`, `"1001:1001"`, or any non-root user tripped the same error). The entire SpoolBuddy update path is now subprocess-free: keypairs are generated in-process via the `cryptography` library (already a dependency), SSH commands run through the pure-Python `asyncssh` client, and git-branch detection reads `.git/HEAD` directly instead of shelling out to `git`. asyncssh also calls `getpass.getuser()` for local `~/.ssh/config` host matching, which hit the same passwd lookup failure; the Docker image now sets `LOGNAME=bambuddy`, `USER=bambuddy`, and `HOME=/app` so `getpass.getuser()` resolves via env vars before touching the passwd database, and `asyncssh.connect()` is called with `config=[]` so it does not attempt to load `~/.ssh/config` at all. Branch detection also now looks for `.git/HEAD` in the *application root* rather than `settings.base_dir` — in Docker the data directory is a separate volume (`DATA_DIR=/app/data`) that never contains `.git`. Finally, the Docker build now bakes `.git/HEAD` into the image (`.dockerignore` allows this single 20-byte file through the context filter) so the production image knows which branch it was built from; previously the `.git` directory was excluded from the build context entirely, leaving the container with no git metadata and causing the SpoolBuddy update flow to always pull `main` on the remote device regardless of which branch Bambuddy itself was built from. Native installs behave identically — they already worked because the running user was always in `/etc/passwd` and `.git/HEAD` was readable from the project root. Regression tests assert that neither keypair creation nor command execution spawns any subprocess, and that branch detection reads from the application root even when a decoy `.git` sits inside the data dir.
- **Camera Stream "6 of 5" Reconnect Counter + ffmpeg Log Flood** ([#925](https://github.com/maziggy/bambuddy/issues/925)) — Two bugs surfaced while investigating camera reconnect behaviour. First, the camera page briefly displayed "Reconnecting attempt 6 of 5" before giving up, because the attempt counter could be incremented to the maximum while the reconnect banner was still rendering. The displayed value is now clamped to the configured maximum. Second, every failed ffmpeg spawn logged the full ~20-line ffmpeg version/configuration banner, producing hundreds of lines of noise per failed camera click (one reported click produced 555 log lines across 30 retries). A new stderr summarizer strips the ffmpeg banner before logging so only the actual error lines remain. The underlying "camera service stops accepting new connections after prolonged uptime" behaviour in the X1C firmware is still under investigation.
- **LDAP POSIX Primary Group Ignored** — LDAP authentication only looked at groups that listed the user explicitly via `memberUid` (supplementary group membership). A user's POSIX primary group — referenced by the `gidNumber` attribute on the user object and matching the `gidNumber` on a `posixGroup` — was ignored entirely, so users whose role came from their primary group landed without the expected permissions. The authenticator now also searches for `posixGroup` entries whose `gidNumber` matches the user's primary `gidNumber`, and dedupes DNs case-insensitively before resolving the group mapping (LDAP DNs are case-insensitive by spec).
- **Support Bundle Leaks Virtual Printer IP Address** — The debug support bundle included the `virtual_printer_remote_interface_ip` setting value unmasked in `support-info.json`. The setting key didn't match any of the existing sensitive-key filters, so the raw IP address was included in the bundle. Added `_ip` to the sensitive key filter so IP address settings are excluded from support bundles. Log file content was already covered by the existing IPv4 regex redaction.
+9
View File
@@ -41,6 +41,15 @@ RUN --mount=type=cache,target=/root/.cache/pip \
# Copy backend
COPY backend/ ./backend/
# Capture the current git branch at build time. `.git/HEAD` is the only
# .git metadata the build context lets through (see .dockerignore); it
# contains `ref: refs/heads/<branch>`, which the SpoolBuddy remote-update
# flow reads at runtime via detect_current_branch() in spoolbuddy_ssh.py.
# Without this, the production image has no git metadata at all and would
# always pull `main` on the remote device regardless of which branch
# Bambuddy itself was built from.
COPY .git/HEAD ./.git/HEAD
# Copy built frontend from builder stage
COPY --from=frontend-builder /app/static ./static