The builder on the host's network (the last change here) didn't help: the
next publish failed exactly as before. Looking on the host showed why.
Both builders resolve git.coffeylabs.org publicly; the token isn't fetched
by the builder at all. buildx fetches registry tokens on the client side,
in the job container, and on ci-net the name git.coffeylabs.org belongs to
the gitlab container itself (172.30.0.2) -- which is how the runner clones
over plain HTTP, and which has nothing on 443. So every push asked
https://git.coffeylabs.org/jwt/auth for a token and was refused. The login
before it worked because the host's daemon does the login, and the host
resolves the name publicly.
For the publish job only, the name now points at its public address in the
job's /etc/hosts, looked up from a public resolver, as the host sees it.
/etc/hosts wins over Docker's DNS, and nothing else in the job is affected:
the checkout is done, and image layers go to the registry's own DNS-only
name, not this one. The builder goes back to the shared ci-builder; its
network was never the problem.
The lookup and the /etc/hosts write were tried in the job's own image
(docker:28-cli, same digest): it picks the first public IPv4 address and
getent then returns it.
The v2026.9.20 publish (job 513) built both platforms, then failed to
push:
failed to fetch oauth token: Post "https://git.coffeylabs.org/jwt/auth":
dial tcp 172.30.0.2:443: connect: connection refused
buildx's docker-container builder is a container of its own on the host's
daemon, and it does the push, token and all. On the network it was
created on, git.coffeylabs.org resolves to an internal address with
nothing listening on 443. The job's own `docker login` worked because it
goes through the host daemon. inbuxa-admin's first release failed the
same way.
The builder now runs on the host's network, so it resolves the name as
the login does. Only the token request goes to git.coffeylabs.org; image
layers still go to registry.coffeylabs.org, the registry's DNS-only name.
It gets a new name, ci-builder-host: `ci-builder` is a long-lived
container shared between jobs, and `create || use` would keep reusing it
on its old network.
The build directory is reused between jobs, and the node job chowns it to
the unprivileged node user for its tests. A later job running git as root
then finds the checkout owned by someone else and git refuses with
"detected dubious ownership" (exit 128). Which cached directory a job lands
on decides whether it happens, so it is intermittent: the first weekly
release dry run passed and the second failed.
The version job in the tag pipeline runs git as root too, so the same
refusal would have stopped a release from ever publishing its image. Both
jobs now mark the project directory safe before touching git.
The dry run reported tag v2026.9.20-gc927c69 as existing when it did not.
On the git in the job image (2.39), rev-parse --verify refs/tags/<name>
falls back to reading a name ending in -g<hex> as git-describe output, and
resolves it to that commit. Every commit not merged through a pull request
gets a -g<hex> version, so every such week would have been skipped as
already released -- silently, since skipping is a normal outcome.
show-ref --verify matches an exact ref and nothing else. Both tag checks
use it now. release.yml has the same code; it only worked because GitHub's
runners carry a newer git that does not fall back.
release.yml stopped running with the GitHub account, and nothing replaced
it: no tag has been cut since, so the publish job had nothing to build.
This ports its decision unchanged -- release only when main has commits
since the newest published release, and only if the tag does not already
exist -- to a job run by a Monday 09:17 UTC pipeline schedule. The schedule
lives on the project and sets RELEASE_WEEKLY=1; DRY_RUN=1 stops after the
decision.
The release, and so the tag, is created with a project access token rather
than the job token. That makes the tag an ordinary push, which starts the
tag pipeline and its publish job, replacing release.yml's direct call of
publish.yml. The checks are skipped in the release pipeline, as they were
on GitHub: main has already passed them.
publish.yml passed the computed version into the image build, and the
first port of it to GitLab CI did not. A tag pushed with that port would
have shipped an image reporting itself unversioned (or, for ihasvpn, with a
stray leading "v" no earlier build had), and tagged it with the git tag
rather than the version string.
The version is now computed the way publish.yml computed it and passed as
the build arg, and the image is tagged with it, '+' turned into '-' where a
Docker tag needs that.
The proxy test failed with ECONNREFUSED on 127.0.0.1 for a server bound
to ::1. That is not resolution order, so --dns-result-order was treating
the wrong cause and is removed: with no non-loopback IPv6 address on the
container, getaddrinfo's AI_ADDRCONFIG drops ::1 from the results
altogether and localhost can only ever come back IPv4.
The runner now puts jobs on a docker network created with --ipv6, which
is where the fix belongs. Verified by reproducing the failure on the old
network and watching it pass on the new one.
Three tests failed on the runner and pass locally, all because the job
container differs from a workstation rather than because anything
regressed: config.test.ts chmods a directory and expects the write to be
refused, which root ignores; imageproxy.test.ts binds to ::1 and asks for
localhost, which resolves to IPv4 first here; and version.test.ts shells
out to git, which the slim image does not ship.
So the job installs git, runs the suite as the image's unprivileged node
user, and asks Node for the address order the proxy test was written
against. No test changed.
GitHub Actions stopped being reachable when the account was suspended, so
this ports ci.yml and publish.yml to a .gitlab-ci.yml running on a group
runner on Web_Host. The Actions workflows stay in the tree: they are the
reference this was written from, and they work again unchanged if the
appeal succeeds.
Two differences worth knowing. Images are pinned by digest rather than the
workflows' SHA-pinned actions, because GitLab has no action allowlist to
back a tag with. And arm64 is built under QEMU instead of on a native
runner, which is slow enough that publish is tag-only.