From 1326ba1bcc8046039740bf2add796785329f5f73 Mon Sep 17 00:00:00 2001 From: le king fu Date: Sat, 3 Oct 2026 13:49:36 -0400 Subject: [PATCH 1/3] ci: add a throwaway probe of the runner cache server (#234) Diagnostic only, never to be merged. Since #232 no workflow calls the Actions cache, so nothing has shown whether a job container can reach the cache server today. This job prints the cache address the runner advertises (path redacted, since job logs are public), probes it over TCP next to other ports and host addresses, and round-trips a tiny payload through actions/cache. It is green only if the payload comes back. Refs #234 Co-Authored-By: Claude Opus 5.5 --- .forgejo/workflows/diag-cache-probe.yml | 144 ++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 .forgejo/workflows/diag-cache-probe.yml diff --git a/.forgejo/workflows/diag-cache-probe.yml b/.forgejo/workflows/diag-cache-probe.yml new file mode 100644 index 0000000..1096760 --- /dev/null +++ b/.forgejo/workflows/diag-cache-probe.yml @@ -0,0 +1,144 @@ +name: Diag — cache server reachability (#234) + +# THROWAWAY DIAGNOSTIC — never merge. Lives on its own branch for #234. +# +# Since #232 no workflow calls the Actions cache: in July both the restore +# (getCacheEntry) and the save (reserveCache) ended in "Request timeout". The +# suspected cause is that the job container cannot reach the cache address the +# runner hands it. This job checks that from inside a job container, without +# host access: +# 1. which cache address the runner advertises (path redacted: it may carry a +# per-run token, and this repository's job logs are public); +# 2. raw TCP reachability of that address, next to other ports and other host +# addresses, to tell "dropped" from "refused" from "open"; +# 3. an end-to-end round trip through actions/cache with a tiny payload. +# The job is green only if the payload comes back from the cache server. +# +# Steps that use bash features set `shell: bash`: inside a container the runner +# defaults to `sh`, and a job-level `defaults` block is refused by the runner. + +on: + pull_request: + paths: + - '.forgejo/workflows/diag-cache-probe.yml' + +permissions: + contents: read + +jobs: + probe: + runs-on: ubuntu + container: ubuntu:22.04 + timeout-minutes: 20 + steps: + - name: Cache address and TCP reachability + shell: bash + run: | + echo "Runner-provided variables (names only):" + compgen -e | grep -E 'ACTIONS|RUNNER|FORGEJO|GITEA' | sort | sed 's/^/ /' + compgen -e | grep -E '_RUNNER_VERSION$' | while read -r v; do echo " $v=${!v}"; done + + url="${ACTIONS_CACHE_URL:-}" + if [ -z "$url" ]; then + echo "ACTIONS_CACHE_URL is empty: the runner gives this job no cache server." + exit 0 + fi + base=$(printf '%s' "$url" | sed -E 's#^(https?://[^/]+).*#\1#') + scheme=${base%%://*} + hostport=${base#*://} + case "$hostport" in + \[*\]:*) host=${hostport%]:*}; host=${host#[}; port=${hostport##*]:} ;; + *:*) host=${hostport%:*}; port=${hostport##*:} ;; + *) host=$hostport; port="" ;; + esac + if [ -z "$port" ]; then + if [ "$scheme" = https ]; then port=443; else port=80; fi + fi + shown=$host + case "$host" in *:*) shown="[$host]" ;; esac + echo + echo "Cache address advertised by the runner: $scheme://$shown:$port/" + forge=$(getent ahostsv4 git.lacompagniemaximus.com | awk '{ print $1; exit }') + echo "git.lacompagniemaximus.com resolves to (IPv4): ${forge:-unresolved}" + echo "Container addresses: $(hostname -I 2>/dev/null || echo unknown)" + gw_hex=$(awk '$2 == "00000000" { print $3; exit }' /proc/net/route) + gw="" + if [ -n "$gw_hex" ]; then + gw=$(printf '%d.%d.%d.%d' "0x${gw_hex:6:2}" "0x${gw_hex:4:2}" "0x${gw_hex:2:2}" "0x${gw_hex:0:2}") + fi + echo "Default gateway (the host, seen from this job network): ${gw:-unknown}" + + probe() { + local label=$1 h=$2 p=$3 start ms rc err verdict + start=$(date +%s%N) + err=$(timeout 10 bash -c "exec 3<>/dev/tcp/$h/$p" 2>&1) + rc=$? + ms=$(( ($(date +%s%N) - start) / 1000000 )) + case $rc in + 0) verdict="OPEN" ;; + 124) verdict="TIMEOUT" ;; + *) case "$err" in + *refused*) verdict="REFUSED" ;; + *) verdict="ERROR (${err##*: })" ;; + esac ;; + esac + printf ' %-36s %-26s %-8s %6d ms\n' "$label" "$h:$p" "$verdict" "$ms" + } + + echo + echo "TCP probes, 10 s each. OPEN = handshake done; REFUSED = the host answered" + echo "but nothing listens there; TIMEOUT = no answer (packets dropped, or the" + echo "address is unreachable from this network)." + probe "advertised cache address" "$host" "$port" + probe "same host, port 22 (sshd)" "$host" 22 + probe "same host, port 443 (Traefik)" "$host" 443 + if [ -n "$gw" ]; then + probe "job network gateway, cache port" "$gw" "$port" + probe "job network gateway, port 22" "$gw" 22 + fi + probe "docker0 172.17.0.1, cache port" 172.17.0.1 "$port" + + - name: Install Node.js 20 + run: | + apt-get update + apt-get install -y --no-install-recommends curl ca-certificates + # actions/cache is a JavaScript action: it needs `node` in the container PATH. + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - + apt-get install -y nodejs + node --version + + - name: Write a tiny payload + id: payload + shell: bash + run: | + key="diag-234-$(date -u +%Y%m%dT%H%M%SZ)-$RANDOM" + mkdir -p probe-payload + printf '%s' "$key" > probe-payload/stamp + echo "key=$key" >> "$GITHUB_OUTPUT" + echo "payload key: $key" + + - name: Save it (actions/cache/save) + uses: https://github.com/actions/cache/save@v4 + with: + path: probe-payload + key: ${{ steps.payload.outputs.key }} + + - name: Delete the local copy + run: rm -rf probe-payload + + - name: Restore it (actions/cache/restore, fails on a miss) + uses: https://github.com/actions/cache/restore@v4 + with: + path: probe-payload + key: ${{ steps.payload.outputs.key }} + fail-on-cache-miss: true + + - name: Check the payload made the round trip + shell: bash + run: | + if [ "$(cat probe-payload/stamp 2>/dev/null)" = "${{ steps.payload.outputs.key }}" ]; then + echo "ROUND TRIP OK: the payload came back from the cache server." + else + echo "ROUND TRIP FAILED: the payload is missing or different." + exit 1 + fi -- 2.45.2 From 68aef2a74a2039b00a3487ea7dfe17ae061f5ce3 Mon Sep 17 00:00:00 2001 From: le king fu Date: Sat, 3 Oct 2026 13:53:44 -0400 Subject: [PATCH 2/3] ci: keep the cache probe running past an expected failure (#234) The runner starts `shell: bash` steps as `bash -e -o pipefail`, so run 453 stopped at the first TIMEOUT instead of probing the other addresses. Each probe now swallows its own exit code, and the env listing and the forge lookup no longer abort the step when they come back empty. Refs #234 Co-Authored-By: Claude Opus 5.5 --- .forgejo/workflows/diag-cache-probe.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.forgejo/workflows/diag-cache-probe.yml b/.forgejo/workflows/diag-cache-probe.yml index 1096760..5ca1370 100644 --- a/.forgejo/workflows/diag-cache-probe.yml +++ b/.forgejo/workflows/diag-cache-probe.yml @@ -16,6 +16,9 @@ name: Diag — cache server reachability (#234) # # Steps that use bash features set `shell: bash`: inside a container the runner # defaults to `sh`, and a job-level `defaults` block is refused by the runner. +# The runner starts those steps as `bash -e -o pipefail`, so every probe that +# is expected to fail must swallow its own exit code (run 453 stopped at the +# first TIMEOUT for that reason). on: pull_request: @@ -35,8 +38,8 @@ jobs: shell: bash run: | echo "Runner-provided variables (names only):" - compgen -e | grep -E 'ACTIONS|RUNNER|FORGEJO|GITEA' | sort | sed 's/^/ /' - compgen -e | grep -E '_RUNNER_VERSION$' | while read -r v; do echo " $v=${!v}"; done + compgen -e | grep -E 'ACTIONS|RUNNER|FORGEJO|GITEA' | sort | sed 's/^/ /' || true + compgen -e | grep -E '_RUNNER_VERSION$' | while read -r v; do echo " $v=${!v}"; done || true url="${ACTIONS_CACHE_URL:-}" if [ -z "$url" ]; then @@ -58,7 +61,7 @@ jobs: case "$host" in *:*) shown="[$host]" ;; esac echo echo "Cache address advertised by the runner: $scheme://$shown:$port/" - forge=$(getent ahostsv4 git.lacompagniemaximus.com | awk '{ print $1; exit }') + forge=$(getent ahostsv4 git.lacompagniemaximus.com | awk '{ print $1; exit }') || true echo "git.lacompagniemaximus.com resolves to (IPv4): ${forge:-unresolved}" echo "Container addresses: $(hostname -I 2>/dev/null || echo unknown)" gw_hex=$(awk '$2 == "00000000" { print $3; exit }' /proc/net/route) @@ -71,8 +74,7 @@ jobs: probe() { local label=$1 h=$2 p=$3 start ms rc err verdict start=$(date +%s%N) - err=$(timeout 10 bash -c "exec 3<>/dev/tcp/$h/$p" 2>&1) - rc=$? + err=$(timeout 10 bash -c "exec 3<>/dev/tcp/$h/$p" 2>&1) && rc=0 || rc=$? ms=$(( ($(date +%s%N) - start) / 1000000 )) case $rc in 0) verdict="OPEN" ;; @@ -114,7 +116,7 @@ jobs: key="diag-234-$(date -u +%Y%m%dT%H%M%SZ)-$RANDOM" mkdir -p probe-payload printf '%s' "$key" > probe-payload/stamp - echo "key=$key" >> "$GITHUB_OUTPUT" + echo "key=$key" >> "${GITHUB_OUTPUT:-$FORGEJO_OUTPUT}" echo "payload key: $key" - name: Save it (actions/cache/save) -- 2.45.2 From bbfd32b95ee9932bf42a184f05c4a03454239644 Mon Sep 17 00:00:00 2001 From: le king fu Date: Sat, 3 Oct 2026 14:20:50 -0400 Subject: [PATCH 3/3] ci: re-run the cache probe after the runner fix (#234) The runner now reads a config file with a fixed cache proxy port (28100), and ufw lets the Docker job networks (10.0.0.0/8) reach that port only. Refs #234 Co-Authored-By: Claude Opus 5.5 -- 2.45.2