CI container gate fails the stable-upgrade test: the previous release, downloaded to the job container's /tmp, reaches the gate as an empty directory
#1
CI's package and container gate job can't pass on the Zayin runner as written. tools/validate-trixie-release.sh hands the built .deb to a Debian container with a bind mount:
docker run --rm -i -v "$candidate:/packages/candidate.deb:ro" ... debian:trixie-slim ...
On the Zayin runner, ekklesia-ci jobs run inside a container (docker.gitea.com/runner-images:ubuntu-latest) whose docker CLI talks to the host's Docker daemon. A bind-mount source is resolved on the host, where /workspace/<task>/Zayin/Classroom_Desktop/dist/…deb doesn't exist. Docker then creates an empty directory there, so the gate's container gets /packages/candidate.debas a directory and installation can't work.
No CI run on Gitea has ever built and gated the package successfully (details below).
Evidence
Reproduced with the same arrangement: a job-style container (docker:27-cli) with the host's Docker socket writes a file, then starts the gate container with -v <that file>:/packages/candidate.deb:ro:
inside the job container:
-rw-r--r-- 1 root root 12 … /tmp/claude-bindprobe-14691/pkg.deb
what the gate container sees:
drwxr-xr-x 2 root root 40 … /packages/candidate.deb
dpkg-deb: error: error reading archive magic version number from file /packages/candidate.deb: Is a directory
built 0.2.6.6, then the gate step failed after 2 s; job shown as cancelled
In run #4 the gate step ran 01:24:19 → 01:24:21. Its output stops at ecc510c1e359: Pulling fs layer, with no error message at all. Then the upload-artifact@v3 step failed five times with Artifact service responded with 500 (Gitea, through Caddy) over 4 minutes, and the run ended cancelled. The checks job's upload in the same run succeeded, so the 500 may be transient or size-related. It's worth checking the Gitea server log for 2026-10-13 01:24–01:28 UTC.
The cause of the 2-second failure isn't in the log, so I can't confirm it was the bind mount rather than the image pull. But the bind mount makes the step fail even once the pull works.
Why it passed before
docs/releasing.md → What was measured says the gate passed on the development VM's runner, and its admin checklist asks for the existing runner to be relabelled ekklesia-ci:host, i.e. host mode, where paths are shared and bind mounts work. The Zayin runner serves ekklesia-ci in container mode.
Options
Make the gate independent of where docker runs (recommended). Create the container, copy the packages in, then start it:
docker cp streams from the CLI's own filesystem, so this works on a host runner, in a container job, and on a developer machine.
Run ekklesia-ci jobs in host mode, as the docs intended. This needs a runner whose label is ekklesia-ci:host. It weakens isolation for code nobody has reviewed yet (CI runs on every push), which the workflow header calls out.
Stage files on the Docker host, as Ekklesia's CI does (staging directories under the host's /tmp). More moving parts than option 1.
Separately:
Make the gate step print the underlying error. A gate that fails in 2 s with nothing in the log can't be diagnosed. Check whether docker pull/run stderr is reaching the job log.
Consider whether a failed upload should turn the run failed rather than cancelled.
Not checked
The runner host itself (it isn't on the development machine), its act_runner version and config, and whether debian:trixie-slim pulls there at all.
Found by Claude during a review of Classroom_Desktop's CI, 2026-10-14 (found-by-claude).
## Summary
CI's **package and container gate** job can't pass on the Zayin runner as written. `tools/validate-trixie-release.sh` hands the built `.deb` to a Debian container with a bind mount:
```bash
docker run --rm -i -v "$candidate:/packages/candidate.deb:ro" ... debian:trixie-slim ...
```
On the Zayin runner, `ekklesia-ci` jobs run **inside a container** (`docker.gitea.com/runner-images:ubuntu-latest`) whose `docker` CLI talks to the **host's** Docker daemon. A bind-mount source is resolved on the host, where `/workspace/<task>/Zayin/Classroom_Desktop/dist/…deb` doesn't exist. Docker then creates an empty directory there, so the gate's container gets `/packages/candidate.deb` **as a directory** and installation can't work.
No CI run on Gitea has ever built and gated the package successfully (details below).
## Evidence
**Reproduced** with the same arrangement: a job-style container (`docker:27-cli`) with the host's Docker socket writes a file, then starts the gate container with `-v <that file>:/packages/candidate.deb:ro`:
```
inside the job container:
-rw-r--r-- 1 root root 12 … /tmp/claude-bindprobe-14691/pkg.deb
what the gate container sees:
drwxr-xr-x 2 root root 40 … /packages/candidate.deb
dpkg-deb: error: error reading archive magic version number from file /packages/candidate.deb: Is a directory
```
**CI history** (all four runs of `ci.yml`):
| Run | Branch / commit | `checks` | `package and container gate` |
|---|---|---|---|
| [#1](https://git.zayin.net/Zayin/Classroom_Desktop/actions/runs/89) | master `14291fd` | failed (`test-release-ci`, see #2) | skipped |
| [#2](https://git.zayin.net/Zayin/Classroom_Desktop/actions/runs/90) | release/0.2 `14291fd` | passed | **reported success, but its "Build the package" step failed and the gate step was skipped** (old `git.riverofglorymn.org` runner) |
| [#3](https://git.zayin.net/Zayin/Classroom_Desktop/actions/runs/95) | master `0bc8851` | failed (`test-release-ci`, see #2) | skipped |
| [#4](https://git.zayin.net/Zayin/Classroom_Desktop/actions/runs/96) | master `719c0f7` (HEAD) | **passed, 31/31** | built `0.2.6.6`, then the gate step **failed after 2 s**; job shown as *cancelled* |
In run #4 the gate step ran 01:24:19 → 01:24:21. Its output stops at `ecc510c1e359: Pulling fs layer`, **with no error message at all**. Then the `upload-artifact@v3` step failed five times with `Artifact service responded with 500` (Gitea, through Caddy) over 4 minutes, and the run ended *cancelled*. The `checks` job's upload in the same run succeeded, so the 500 may be transient or size-related. It's worth checking the Gitea server log for 2026-10-13 01:24–01:28 UTC.
The cause of the 2-second failure isn't in the log, so I can't confirm it was the bind mount rather than the image pull. But the bind mount makes the step fail even once the pull works.
## Why it passed before
`docs/releasing.md` → *What was measured* says the gate passed on the development VM's runner, and its admin checklist asks for the existing runner to be relabelled `ekklesia-ci:host`, i.e. **host mode**, where paths are shared and bind mounts work. The Zayin runner serves `ekklesia-ci` in container mode.
## Options
1. **Make the gate independent of where `docker` runs (recommended).** Create the container, copy the packages in, then start it:
```bash
id=$(docker create -i debian:trixie-slim bash -s -- ...)
docker cp "$candidate" "$id:/packages/candidate.deb" # and previous.deb
docker start -ai "$id" <<'CONTAINER' ... CONTAINER
docker rm -f "$id"
```
`docker cp` streams from the CLI's own filesystem, so this works on a host runner, in a container job, and on a developer machine.
2. **Run `ekklesia-ci` jobs in host mode**, as the docs intended. This needs a runner whose label is `ekklesia-ci:host`. It weakens isolation for code nobody has reviewed yet (CI runs on every push), which the workflow header calls out.
3. **Stage files on the Docker host**, as Ekklesia's CI does (staging directories under the host's `/tmp`). More moving parts than option 1.
Separately:
- Make the gate step **print the underlying error**. A gate that fails in 2 s with nothing in the log can't be diagnosed. Check whether `docker pull`/`run` stderr is reaching the job log.
- Consider whether a failed upload should turn the run *failed* rather than *cancelled*.
## Not checked
The runner host itself (it isn't on the development machine), its act_runner version and config, and whether `debian:trixie-slim` pulls there at all.
---
_Found by Claude during a review of Classroom_Desktop's CI, 2026-10-14 (`found-by-claude`)._
Correction from a fresh run: it's the previous release's file, not the new package
PR #7 (docs only) ran CI on the current runner (run #5 push, run #6 pull request). This time the image pull worked and the gate got further, which narrows the cause:
== fresh install of 0.2.6.6 (a new machine, on stable)
PASS fresh install of 0.2.6.6 (a new machine, on stable) (6 min)
SKIP upgrade 0.2.6.6 -> 0.2.6.6 on a canary room: canary serves 0.2.6.6, not older than this commit's VERSION (a CI build, not a release)
== upgrade 0.2.6.4 -> 0.2.6.6 on a stable room (stable serves 0.2.6.4)
E: Read error - read (21: Is a directory)
E: Internal error, could not locate member control.tar{.zst,.lz4,.gz,.xz,.bz2,.lzma,}
E: Could not read meta data from /packages/previous.deb
The candidate mounts fine. It's built into the workspace (/workspace/<task>/Zayin/Classroom_Desktop/dist/…deb), and the runner evidently shares that workspace with the Docker host at the same path.
The previous release doesn't.tools/release-gate.sh:117 downloads it into work="$(mktemp -d)", which is /tmp inside the job container and doesn't exist on the host. Docker creates an empty directory there, and /packages/previous.deb is that directory.
So the fresh-install path already works in CI. Only the upgrade-from-stable path fails, and it fails every time stable is older than the commit, which is always true until the next promotion.
Fix options, revised
Smallest: download into the shared workspace instead of /tmp, e.g. work="$(mktemp -d "$ROOT/.gate.XXXXXX")". Add it to .gitignore, and clean it up on exit; the existing "tree unchanged" test runs before the gate. This depends on the runner keeping its shared workspace.
Most robust (unchanged recommendation):docker create + docker cp + docker start, so no host path is involved at all.
Earlier run #4's 2-second failure with no output was most likely the image pull itself (it never reached the install). This run pulled fine.
Status names CI reports, for branch protection: CI / checks (pull_request), CI / checks (push), CI / package and container gate (pull_request), CI / package and container gate (push).
## Correction from a fresh run: it's the *previous* release's file, not the new package
PR #7 (docs only) ran CI on the current runner ([run #5](https://git.zayin.net/Zayin/Classroom_Desktop/actions/runs/300) push, [run #6](https://git.zayin.net/Zayin/Classroom_Desktop/actions/runs/301) pull request). This time the image pull worked and the gate got further, which narrows the cause:
```
== fresh install of 0.2.6.6 (a new machine, on stable)
PASS fresh install of 0.2.6.6 (a new machine, on stable) (6 min)
SKIP upgrade 0.2.6.6 -> 0.2.6.6 on a canary room: canary serves 0.2.6.6, not older than this commit's VERSION (a CI build, not a release)
== upgrade 0.2.6.4 -> 0.2.6.6 on a stable room (stable serves 0.2.6.4)
E: Read error - read (21: Is a directory)
E: Internal error, could not locate member control.tar{.zst,.lz4,.gz,.xz,.bz2,.lzma,}
E: Could not read meta data from /packages/previous.deb
```
- **The candidate mounts fine.** It's built into the workspace (`/workspace/<task>/Zayin/Classroom_Desktop/dist/…deb`), and the runner evidently shares that workspace with the Docker host at the same path.
- **The previous release doesn't.** `tools/release-gate.sh:117` downloads it into `work="$(mktemp -d)"`, which is `/tmp` inside the job container and doesn't exist on the host. Docker creates an empty directory there, and `/packages/previous.deb` is that directory.
So the fresh-install path already works in CI. Only the upgrade-from-stable path fails, and it fails every time stable is older than the commit, which is always true until the next promotion.
### Fix options, revised
1. **Smallest:** download into the shared workspace instead of `/tmp`, e.g. `work="$(mktemp -d "$ROOT/.gate.XXXXXX")"`. Add it to `.gitignore`, and clean it up on exit; the existing "tree unchanged" test runs before the gate. This depends on the runner keeping its shared workspace.
2. **Most robust (unchanged recommendation):** `docker create` + `docker cp` + `docker start`, so no host path is involved at all.
Earlier run #4's 2-second failure with no output was most likely the image pull itself (it never reached the install). This run pulled fine.
Status names CI reports, for branch protection: `CI / checks (pull_request)`, `CI / checks (push)`, `CI / package and container gate (pull_request)`, `CI / package and container gate (push)`.
ekklesia-bot
changed title from CI container gate cannot pass on the Zayin runner: the .deb bind mount reaches the gate as an empty directory to CI container gate fails the stable-upgrade test: the previous release, downloaded to the job container's /tmp, reaches the gate as an empty directory2026-10-14 04:49:29 +00:00
Priority note (2026-10-14):CI / package and container gate is now a required status check on this repository, so until this is fixed every pull request shows as blocked and needs a maintainer override to merge. That makes this the first CI fix to do (see the order in #6).
**Priority note (2026-10-14):** `CI / package and container gate` is now a **required** status check on this repository, so until this is fixed every pull request shows as blocked and needs a maintainer override to merge. That makes this the first CI fix to do (see the order in #6).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
CI's package and container gate job can't pass on the Zayin runner as written.
tools/validate-trixie-release.shhands the built.debto a Debian container with a bind mount:On the Zayin runner,
ekklesia-cijobs run inside a container (docker.gitea.com/runner-images:ubuntu-latest) whosedockerCLI talks to the host's Docker daemon. A bind-mount source is resolved on the host, where/workspace/<task>/Zayin/Classroom_Desktop/dist/…debdoesn't exist. Docker then creates an empty directory there, so the gate's container gets/packages/candidate.debas a directory and installation can't work.No CI run on Gitea has ever built and gated the package successfully (details below).
Evidence
Reproduced with the same arrangement: a job-style container (
docker:27-cli) with the host's Docker socket writes a file, then starts the gate container with-v <that file>:/packages/candidate.deb:ro:CI history (all four runs of
ci.yml):checkspackage and container gate14291fdtest-release-ci, see #2)14291fdgit.riverofglorymn.orgrunner)0bc8851test-release-ci, see #2)719c0f7(HEAD)0.2.6.6, then the gate step failed after 2 s; job shown as cancelledIn run #4 the gate step ran 01:24:19 → 01:24:21. Its output stops at
ecc510c1e359: Pulling fs layer, with no error message at all. Then theupload-artifact@v3step failed five times withArtifact service responded with 500(Gitea, through Caddy) over 4 minutes, and the run ended cancelled. Thechecksjob's upload in the same run succeeded, so the 500 may be transient or size-related. It's worth checking the Gitea server log for 2026-10-13 01:24–01:28 UTC.The cause of the 2-second failure isn't in the log, so I can't confirm it was the bind mount rather than the image pull. But the bind mount makes the step fail even once the pull works.
Why it passed before
docs/releasing.md→ What was measured says the gate passed on the development VM's runner, and its admin checklist asks for the existing runner to be relabelledekklesia-ci:host, i.e. host mode, where paths are shared and bind mounts work. The Zayin runner servesekklesia-ciin container mode.Options
dockerruns (recommended). Create the container, copy the packages in, then start it:docker cpstreams from the CLI's own filesystem, so this works on a host runner, in a container job, and on a developer machine.ekklesia-cijobs in host mode, as the docs intended. This needs a runner whose label isekklesia-ci:host. It weakens isolation for code nobody has reviewed yet (CI runs on every push), which the workflow header calls out./tmp). More moving parts than option 1.Separately:
docker pull/runstderr is reaching the job log.Not checked
The runner host itself (it isn't on the development machine), its act_runner version and config, and whether
debian:trixie-slimpulls there at all.Found by Claude during a review of Classroom_Desktop's CI, 2026-10-14 (
found-by-claude).Correction from a fresh run: it's the previous release's file, not the new package
PR #7 (docs only) ran CI on the current runner (run #5 push, run #6 pull request). This time the image pull worked and the gate got further, which narrows the cause:
/workspace/<task>/Zayin/Classroom_Desktop/dist/…deb), and the runner evidently shares that workspace with the Docker host at the same path.tools/release-gate.sh:117downloads it intowork="$(mktemp -d)", which is/tmpinside the job container and doesn't exist on the host. Docker creates an empty directory there, and/packages/previous.debis that directory.So the fresh-install path already works in CI. Only the upgrade-from-stable path fails, and it fails every time stable is older than the commit, which is always true until the next promotion.
Fix options, revised
/tmp, e.g.work="$(mktemp -d "$ROOT/.gate.XXXXXX")". Add it to.gitignore, and clean it up on exit; the existing "tree unchanged" test runs before the gate. This depends on the runner keeping its shared workspace.docker create+docker cp+docker start, so no host path is involved at all.Earlier run #4's 2-second failure with no output was most likely the image pull itself (it never reached the install). This run pulled fine.
Status names CI reports, for branch protection:
CI / checks (pull_request),CI / checks (push),CI / package and container gate (pull_request),CI / package and container gate (push).CI container gate cannot pass on the Zayin runner: the .deb bind mount reaches the gate as an empty directoryto CI container gate fails the stable-upgrade test: the previous release, downloaded to the job container's /tmp, reaches the gate as an empty directoryPriority note (2026-10-14):
CI / package and container gateis now a required status check on this repository, so until this is fixed every pull request shows as blocked and needs a maintainer override to merge. That makes this the first CI fix to do (see the order in #6).