Copy the checkout into the container, because a socket is not a shared filesystem
ci / build and test (push) Successful in 1m51s
ci / android head (push) Failing after 6m24s
ci / api image (push) Successful in 23s

The container started and could not see the repository:

  MSBUILD : error MSB1009: Project file does not exist.
  Switch: src/DodoSSH.Client.Android/DodoSSH.Client.Android.csproj

`-v "$PWD:/build"` cannot work here. This runner is itself a container holding the host's Docker
socket, so the workspace path it reports — /root/.cache/act/<hash>/… — exists in the runner and not on
the daemon's host, which is where Docker resolves a bind source. It finds nothing, creates an empty
directory, and mounts that. Nothing about the failure says so.

Nothing earlier in this workflow would have caught it either, and that is the part worth keeping: the
image job's `docker build` sends its context over the API and Testcontainers mounts nothing, so neither
of the two places this repository already used Docker proves a bind mount would work. I read a working
daemon as a shared filesystem, and they are not the same claim.

`docker cp` goes over the same API and so does not care where the daemon lives. In with the whole
checkout, .git included, since MinVer and the versionCode both read it — the repository is well under a
megabyte packed. Out with the staged package. The NuGet cache becomes a named volume for the same
reason: it lives on the daemon and needs no path either side has to agree on.

The two container steps collapse into one, since with a copy in and a copy out there is nothing to be
gained by paying for both twice, and scripts/ci-android.sh is now what runs inside — a file that can be
read and executed on its own rather than a heredoc inside a workflow.

Exercised locally against a real clone, every step as the job runs it: create, cp in, start --attach,
cp out, parse the manifest on the outside.

  package: name='dev.dodotech.dodossh.nightly' versionCode='196' versionName='0.0.0-alpha.0.129'

The second run took 2m25s against the first run's 8m, which is the named volume doing its job.
This commit is contained in:
2026-08-05 07:29:53 +02:00
parent 30a3edb1d4
commit a18ca56fde
3 changed files with 150 additions and 106 deletions
+25 -5
View File
@@ -613,11 +613,31 @@ a loader for glibc *executables*, which is a different problem; there is no shim
variant of the pack. **This is the end of the road on Alpine, not a harder step along it.**
So the android job builds in a container instead. `build/android-build.Dockerfile` is Microsoft's own
`sdk:10.0-noble` plus a JDK, the Android SDK and the workload; the job keeps on the host only what the
host is good at — checkout, git, publishing — and hands the build to the image over a bind-mounted
checkout. The daemon needed no arranging: the `image` job already builds with it and every Testcontainers
suite reaches it over the socket. The image is tagged by the digest of the Dockerfile that made it, so on
a persistent runner every run after the first is a cache hit.
`sdk:10.0-noble` plus a JDK, the Android SDK and the workload; `scripts/ci-android.sh` is everything that
has to happen inside it; and the job keeps on the host only what the host is good at — checkout, git,
publishing. The daemon needed no arranging: the `image` job already builds with it and every
Testcontainers suite reaches it over the socket. The image is tagged by the digest of the Dockerfile that
made it, so on a persistent runner every run after the first is a cache hit.
**A bind mount into that container does not work, and it does not fail either.** This runner is itself a
container holding the host's Docker socket, so the workspace path it reports — `/root/.cache/act/<hash>/…`
— exists in the runner and not on the daemon's host, which is where Docker resolves a bind source. It
finds nothing, creates an empty directory and mounts that. The container then starts perfectly and says:
```
MSBUILD : error MSB1009: Project file does not exist.
```
Nothing in that names an empty mount, and nothing earlier in the job would have caught it: `docker build`
sends its context over the API and Testcontainers mounts nothing, so neither of the two places this
repository was already using Docker proves that a bind mount would work. **A socket is not a shared
filesystem, and every check that looked like it said otherwise was answering a different question.**
`docker cp` goes over the same API and therefore does not care where the daemon lives, which is what the
job does now — in with the whole checkout including `.git`, since MinVer and the versionCode both read
it, and out with the staged package. The repository is well under a megabyte packed, so it costs a
moment. The NuGet cache is a named volume for the same reason: it lives on the daemon and needs no path
either side has to agree on.
Three smaller things worth keeping. **The image is built rather than pulled**, because a community image
with the Android SDK already in it would put a stranger in the path of a package this project signs and