From e09cb28fa306ad027d96cfe0b2ab595235436fde Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Tue, 21 Apr 2026 21:43:37 -0400 Subject: [PATCH 1/6] ci: use ping to wait for network instead of checking resolv.conf Waiting for a nameserver entry in resolv.conf is not sufficient on some distros (e.g. Slackware) where the network stack takes longer to become fully functional. Pinging 1.1.1.1 directly gives a reliable signal that the network is actually reachable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index cd43137..d390b54 100644 --- a/Makefile +++ b/Makefile @@ -93,7 +93,7 @@ test-lxd: ## Run tests in an LXD container (set LXD_DISTRO=distro/version) $(LXC) launch $(LXD_IMAGE) $(LXD_CONTAINER) fi $(LXC) exec $(LXD_CONTAINER) -- sh -c '\ - until grep -q ^nameserver /etc/resolv.conf 2>/dev/null; do sleep 1; done; \ + until ping -c1 -W2 1.1.1.1 > /dev/null 2>&1; do sleep 1; done; \ if command -v apt-get > /dev/null 2>&1; then \ apt-get update && apt-get install -y make curl python3; \ elif command -v dnf > /dev/null 2>&1; then \ From 878301b89174baa00b23eecbb28c0ee8e60b9e61 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Tue, 21 Apr 2026 22:06:40 -0400 Subject: [PATCH 2/6] ci: use /dev/tcp to wait for network instead of ping ping requires ICMP which is blocked in many CI environments and can hang indefinitely. Using bash's built-in /dev/tcp to probe TCP port 53 on 1.1.1.1 is reliable, requires no external tools, and respects connection timeouts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d390b54..46e265f 100644 --- a/Makefile +++ b/Makefile @@ -93,7 +93,7 @@ test-lxd: ## Run tests in an LXD container (set LXD_DISTRO=distro/version) $(LXC) launch $(LXD_IMAGE) $(LXD_CONTAINER) fi $(LXC) exec $(LXD_CONTAINER) -- sh -c '\ - until ping -c1 -W2 1.1.1.1 > /dev/null 2>&1; do sleep 1; done; \ + until (exec 3<>/dev/tcp/1.1.1.1/53) 2>/dev/null; do sleep 1; done; \ if command -v apt-get > /dev/null 2>&1; then \ apt-get update && apt-get install -y make curl python3; \ elif command -v dnf > /dev/null 2>&1; then \ From fbd8aa576fc1abb2733d99c033f9cfa4f541694a Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Tue, 21 Apr 2026 22:20:26 -0400 Subject: [PATCH 3/6] fix(ci): use /proc/net/route for network wait instead of /dev/tcp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /dev/tcp is a bash-only feature. The containers run 'sh -c ...' which on Alpine is busybox ash and on Debian/Ubuntu is dash — neither supports /dev/tcp, so the until loop spins forever. /proc/net/route is a Linux kernel file always present, requires no external tools, and works in any POSIX sh. The awk check succeeds once a default route with a non-zero gateway is established (fields 2 and 3 in hex: 00000000 = destination 0.0.0.0, non-zero = actual gateway). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 46e265f..538a91e 100644 --- a/Makefile +++ b/Makefile @@ -93,7 +93,7 @@ test-lxd: ## Run tests in an LXD container (set LXD_DISTRO=distro/version) $(LXC) launch $(LXD_IMAGE) $(LXD_CONTAINER) fi $(LXC) exec $(LXD_CONTAINER) -- sh -c '\ - until (exec 3<>/dev/tcp/1.1.1.1/53) 2>/dev/null; do sleep 1; done; \ + until awk 'NR>1 && $2=="00000000" && $3!="00000000" {found=1; exit} END {exit !found}' /proc/net/route 2>/dev/null; do sleep 1; done; \ if command -v apt-get > /dev/null 2>&1; then \ apt-get update && apt-get install -y make curl python3; \ elif command -v dnf > /dev/null 2>&1; then \ From 28ca719035d51113bed9d3f61e9da0cb4a1cd3ac Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Tue, 21 Apr 2026 22:24:09 -0400 Subject: [PATCH 4/6] fix(ci): use 'ip route' for network wait instead of /dev/tcp The awk approach had two problems: - $2/$3 were eaten by Make (needed $$2/$$3) - awk single quotes broke the outer sh -c '...' quoting 'ip route | grep -q "^default"' avoids both issues: no dollar signs, no nested single quotes. 'ip' is available pre-install on all LXD images (busybox on Alpine, iproute2 on everything else). Verified locally with alpine/3.21 (busybox sh): 29 passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 538a91e..2d08335 100644 --- a/Makefile +++ b/Makefile @@ -93,7 +93,7 @@ test-lxd: ## Run tests in an LXD container (set LXD_DISTRO=distro/version) $(LXC) launch $(LXD_IMAGE) $(LXD_CONTAINER) fi $(LXC) exec $(LXD_CONTAINER) -- sh -c '\ - until awk 'NR>1 && $2=="00000000" && $3!="00000000" {found=1; exit} END {exit !found}' /proc/net/route 2>/dev/null; do sleep 1; done; \ + until ip route 2>/dev/null | grep -q "^default"; do sleep 1; done; \ if command -v apt-get > /dev/null 2>&1; then \ apt-get update && apt-get install -y make curl python3; \ elif command -v dnf > /dev/null 2>&1; then \ From fcb4b311e0816c7d39d54ca3db456cdc6ca6fa5c Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Tue, 21 Apr 2026 22:37:02 -0400 Subject: [PATCH 5/6] fix(ci): exclude .venv/.git from container file transfer, clean stale .venv lxc file push has no exclude option, so switch to a tar pipe. Exclude .venv, .git, and __pycache__ to avoid transferring large/ platform-specific files to containers. Also rm -rf .venv before setup so reused containers don't have a stale host-built venv with shebangs pointing to host paths, which caused 'uv run pytest' to fail with 'No such file or directory' on Arch Linux and ubuntu-minimal-daily:26.04. Verified locally: 21/21 distros pass (plus alpine/3.21 from earlier). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Makefile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 2d08335..7e2ec48 100644 --- a/Makefile +++ b/Makefile @@ -110,7 +110,12 @@ test-lxd: ## Run tests in an LXD container (set LXD_DISTRO=distro/version) echo "No supported package manager found" >&2; exit 1; \ fi' $(LXC) exec $(LXD_CONTAINER) -- sh -c 'curl -LsSf https://astral.sh/uv/install.sh | env HOME=/root sh' - $(LXC) file push --recursive $(PWD) $(LXD_CONTAINER)/root/ - $(LXC) exec $(LXD_CONTAINER) --cwd /root/distro-support -- sh -c 'rm -f .python-version' + tar -C $(dir $(PWD)) \ + --exclude='$(notdir $(PWD))/.venv' \ + --exclude='$(notdir $(PWD))/.git' \ + --exclude='*/__pycache__' \ + -c $(notdir $(PWD)) \ + | $(LXC) exec $(LXD_CONTAINER) -- tar -C /root -x + $(LXC) exec $(LXD_CONTAINER) --cwd /root/distro-support -- sh -c 'rm -f .python-version && rm -rf .venv' $(LXC) exec $(LXD_CONTAINER) --env DEBIAN_FRONTEND=noninteractive --env UV_PYTHON_DOWNLOADS=never --cwd /root/distro-support -- sh -c 'PATH=/root/.local/bin:$$PATH make CI=1 SETUPTOOLS_SCM_PRETEND_VERSION=0.0 setup-tests' $(LXC) exec $(LXD_CONTAINER) --env DEBIAN_FRONTEND=noninteractive --env UV_PYTHON_DOWNLOADS=never --cwd /root/distro-support -- sh -c 'PATH=/root/.local/bin:$$PATH make CI=1 test' From ad0e8c30769f3bff6a419ac56e7a2486b1ae2f65 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Tue, 21 Apr 2026 23:10:40 -0400 Subject: [PATCH 6/6] fix(ci): wait for DNS readiness before package installs ip route confirms routing is up but DNS may still be initialising. Add a nslookup check (available via busybox on Alpine and widely elsewhere) after the default-route check to avoid transient DNS failures during apk/apt-get/dnf/etc. Fixes arm64 CI failure where alpine/3.23 got a DNS transient error when fetching dl-cdn.alpinelinux.org immediately after network came up. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Makefile | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 7e2ec48..46279d5 100644 --- a/Makefile +++ b/Makefile @@ -94,18 +94,20 @@ test-lxd: ## Run tests in an LXD container (set LXD_DISTRO=distro/version) fi $(LXC) exec $(LXD_CONTAINER) -- sh -c '\ until ip route 2>/dev/null | grep -q "^default"; do sleep 1; done; \ + until getent hosts cloudflare.com >/dev/null 2>&1 || nslookup cloudflare.com >/dev/null 2>&1; do sleep 1; done; \ + retry() { n=0; until [ $$n -ge 3 ]; do "$$@" && return 0; n=$$((n+1)); sleep 5; done; return 1; }; \ if command -v apt-get > /dev/null 2>&1; then \ - apt-get update && apt-get install -y make curl python3; \ + retry apt-get update && apt-get install -y make curl python3; \ elif command -v dnf > /dev/null 2>&1; then \ - dnf install -y make curl tar python3; \ + retry dnf install -y make curl tar python3; \ python3 -c "import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)" 2>/dev/null || dnf install -y python3.11; \ elif command -v zypper > /dev/null 2>&1; then \ - zypper --non-interactive install make curl python3; \ + retry zypper --non-interactive install make curl python3; \ python3 -c "import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)" 2>/dev/null || zypper --non-interactive install python310; \ elif command -v pacman > /dev/null 2>&1; then \ - pacman -Sy --noconfirm make curl python3; \ + retry pacman -Sy --noconfirm make curl python3; \ elif command -v apk > /dev/null 2>&1; then \ - apk add --no-cache make curl python3; \ + retry apk add --no-cache make curl python3; \ else \ echo "No supported package manager found" >&2; exit 1; \ fi'