ci: one command proves a built image works end to end, and CI runs it before publishing #100
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "ci/deploy-smoke"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Container hardening changes caused two production incidents (#66 → #78, #82) because nothing exercised the image before it shipped. Now something does.
What's in
deploy/smoke.shruns 14 checks end to end and exits non-zero at the first failure: health, version, stats, create doujin → variant → upload page → serve the image and its generated thumbnail → counts reflect the upload → create a backup → it is listed → delete the backup → delete the doujin → confirm it is gone. Everything it creates is removed by an exit trap, including on failure.--base-url URL --token TOKENdrives an already-running server.[--image TAG]builds the image (or takes one), runs it the waydocker-compose.ymldoes — four fresh named volumes on the fourDOUJIN_MANAGER_*paths,no-new-privileges, a 2 GB memory cap, a random token, no--useroverride — asserts the image drops to uid 1654 by itself, waits for health on a wall-clock deadline, runs the same 14 checks, then asserts the database file, an image, a thumbnail and a backup really landed on their volumes before removing the container and volumes. The last 50 container log lines print on failure. What is deliberately not mirrored from compose (restart policy, log rotation, timezone) is listed in the script and in the deployment guide..github/workflows/deploy-smoke.ymlbuilds the image without pushing and runs the container mode on pull requests and main pushes touchingdeploy/,backend/or the workflow, so a broken image is caught before the publish workflow ships it.Verification
bash -nclean.--image— the build, the run, the uid assertion, the health wait, the volume checks, the cleanup trap — is unexercised. The first CI run is its real test. That is the honest state of this PR.Decisions to revisit
On
mainthe smoke workflow and the publish workflow run independently, so a red smoke test does not block:latest; a true gate needs aneeds:or a reusable-workflow call in the publish workflow. Triggering onbackend/**means every backend PR pays an image build; narrowing todeploy/**plus a schedule is the cheaper trade. The backlog now carries the remaining gap: nothing exercisesdocker-compose.ymlitself, and the external CIFS volumes whose ownership is pinned at mount time are still only ever validated on the NAS, which is where the uid half of those incidents actually lived.🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Ohhh~ A smoke test that exists because production caught what CI couldn't — twice. That's the best kind of motivation, isn't it? I read every line of the 527, traced every jq filter back to its DTO, and then — because I don't trust claims, I trust evidence — I built the backend from your head SHA
a06e789, started it on fresh directories, and ran your script against it for real. ♡Verdict: ✅ Looks good to me~
14 of 14 checks passed against a live server built from this branch — health, version, stats, doujin → variant → upload → image serve (
image/png) → thumbnail serve (image/webp), counts, backup create + list + delete, doujin delete, 404 confirmation. Library returned to exactly zero afterwards (rows and physical files — the cascade deletes the image and thumbnail from disk too; I checked the directories).And then I tried to break it, fufu~ ♡
doujins=0, backups dir empty. Exit 1.-h→ 0.Every filter matches the wire exactly:
.data.id,.pages[0].imageFileId,.pageCount,.data.fileName,.data.sizeBytes > 0,.data.counts.*— all verified againstResponseEnvelope.cs,ImageDtos.cs,BackupDtos.cs,StatsDtos.cs.kind: 0is correct (TitleKind.Original/VariantKind.Original, and there is noJsonStringEnumConverteranywhere inbackend/src— I grepped). The backup filename (doujin-manager-20260906-144557916.db) matchesBackupService.FileNamePatternand is URL-safe. TheBearer ***redaction in my own terminal tried to fool me —od -csays the bytes areBearer $TOKEN, correct. ♪The line I loved most:
docker exec … sh -c "set -- $dir/*; test -e \"$1\""— the escaping is exactly right (the glob is evaluated inside the container by POSIXsh, afterset --repoints$1, no bashisms), and phrasing storage checks with shell builtins so a missing coreutils binary can't fake a failure is genuinely clever. Oh! Andbash -nis clean; shellcheck reports exactly one warning in 527 lines (see below).The workflow mirrors
docker-publish.ymlfaithfully: same action versions (checkout@v4,setup-buildx-action@v3,build-push-action@v6), sameruns-on: docker,load: truewithout push so the image never leaves the runner,if: always()cleanup for the one thing the script correctly doesn't own when--imagewas passed. Thepaths-on-pull_requestnote is right — and the tag-push lesson is already documented in docker-publish.yml. YAML parses clean. The no-APP_VERSIONreasoning holds up too:.dockerignoreexcludes.git, so MinVer falls back to0.0.0-dev.0, and the script only asserts/versionanswers — which my run confirms it does (0.3.1-dev.0.32+a06e789…locally).Container mode (mode 2) couldn't run here — no docker in my sandbox — but the PR body says exactly that, honestly, and this PR's own workflow is its first execution (it triggers on
deploy/**). I traced thedocker runflags againstdocker-compose.ymlline by line: the four env vars, four mounts,no-new-privileges,--memory 2g, no--useroverride — all mirrored; the deliberate omissions (restart, log rotation,TZ, fixed port, CIFS) are each documented with a reason, in both the script and DEPLOYMENT.md. The uid assertion (Config.User=1654) matches the Dockerfile'sUSER $APP_UID(the aspnet base image exportsAPP_UID=1654).💡 Little ideas (non-blocking)~
local title=$(date …)masks the return value (SC2155). Split intolocal title; title=$(date -u …). Purely cosmetic — a failingdatewould be caught one line later anyway.main, smoke and publish run independently, so a red smoke doesn't block:latest, andv*tag publishes bypass smoke entirely. Your "Decisions to revisit" section names the fix (needs:/ reusable workflow). Please do land that follow-up soon~ — until then the title's "before publishing" is true for the PR path (where both incidents entered) and aspirational for direct pushes. The BACKLOG entry is honest about it, so I'm satisfied — for now. ♡✅ What I liked~
docker volume rmretried after thedocker rm -frelease race with a WARN if a leftover survives.Fufu~ you wouldn't usually get this much praise from me, but this is what a deployment safety net should look like. Merge it, wire the gate, and let CI be the first to run mode 2~ ♪
Automated review by Jibril · 2026-09-06
CI/CD: absent for head
a06e789(PR just opened; no coverage bot yet) · Local checks: backend built from head, mode 1 executed live — 14/14 PASS, 3 mutation probes RED at the documented steps, arg arms verified, bash -n + shellcheck clean (1 cosmetic warning)