Compare commits

...
Author SHA1 Message Date
jakman85andClaude Opus 5.5 0c6ee4921d fix(renovate): document and probe the five token scopes Renovate needs; lint daemon-bound actions
gate / gate (pull_request) Successful in 2m54s
gate / conformance (pull_request) Has been skipped
Run 350 cleared platform init (/user 200, /version 200, /repos 200), then stopped at
GET /api/v1/orgs/Home-Network with Gitea's own 403:
  token does not have at least one of required scope(s), required=[read:organization],
  token scope=read:package,write:repository,read:user
The workflow's header listed only those three scopes, and the operator minted exactly that.
Renovate's initRepo -> isOrg needs read:organization, and Gitea files /issues and /labels under
the issue scope, which the dashboard and PR labels need: write:issue.

- renovate.yaml: corrects the scope list (read:user, read:organization, write:repository,
  write:issue, read:package), with the call each serves. A new preflight step probes them with
  GETs, the token passed to curl on stdin, and names a missing scope instead of Renovate's bare
  "Authentication failure".
- ci/lint.sh: rejects workflow `uses:` lines that need a Docker daemon (docker://,
  renovatebot/github-action, docker/*-action), the class fixed in #84.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:55:39 -05:00
jakman85 d550a501e2 Merge pull request 'fix(renovate): run Renovate as the job container; the action needs a Docker daemon jobs do not get' (#84) from fix/renovate-first-run into main
gate / gate (push) Successful in 2m52s
gate / conformance (push) Has been skipped
Reviewed-on: #84
Reviewed-by: Keith Jackson <[email protected]>
2026-10-30 06:36:11 +00:00
jakman85andClaude Opus 5.5 365e43cdea fix(renovate): run Renovate as the job container; the action needs a Docker daemon jobs do not get
gate / gate (pull_request) Successful in 2m52s
gate / conformance (pull_request) Has been skipped
renovatebot/github-action is a wrapper around `docker run`. On this runner a job has no Docker
daemon by decision (decisions/2026-10-29-image-pipeline.md), so every run, including dispatch
run 343, died before Renovate started:

  failed to connect to the docker API at unix:///var/run/docker.sock ... no such file or directory

The pinned image ghcr.io/renovatebot/renovate:44.167.0 is now the job container, and a run step
execs renovate-entrypoint.sh. actions/checkout is dropped, since Renovate clones the repo itself.
An optional RENOVATE_GITHUB_COM_TOKEN secret feeds GITHUB_COM_TOKEN.

Local dry run (--platform=local, uid 12021, the step body verbatim): 179 deps extracted, 42
updates, and all 4 own-image digest lookups against the Gitea registry succeed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 01:23:36 -05:00
5 changed files with 379 additions and 29 deletions

No files matched your search

+138 -26
View File
@@ -60,9 +60,21 @@
# entry's "What I did NOT verify" — that gap is stated, not papered over. # entry's "What I did NOT verify" — that gap is stated, not papered over.
# #
# OPERATOR PREREQUISITE — ONE SECRET, NOT COMMITTED HERE: # OPERATOR PREREQUISITE — ONE SECRET, NOT COMMITTED HERE:
# `RENOVATE_TOKEN` — a Gitea access token for the bot account, scopes `write:repository`, # `RENOVATE_TOKEN` — a Gitea access token for the bot account (`renovate-bot`) with FIVE scopes.
# `read:user` and (since 2026-10-29) `read:package`. Set it as an Actions secret on this # Each is required by a call Renovate 44.x makes against Gitea 1.24.3, and Gitea refuses a
# repository. Nothing in this repo mints, # token missing one with `403 token does not have at least one of required scope(s)`:
# read:user GET /user (platform init: initPlatform)
# read:organization GET /orgs/{owner}, /orgs/{o}/labels (initRepo -> isOrg, then org labels)
# write:repository clone/push branches, /pulls, /statuses
# write:issue /issues, /labels, /issues/{n}/labels (dependency dashboard, PR labels;
# Gitea files all of these under the ISSUE scope, not repository)
# read:package the Gitea container registry (RENOVATE_HOST_RULES below)
# CORRECTED 2026-10-30. This list used to name only write:repository + read:user (+ read:package),
# and the operator minted exactly that. Run 350 then failed at
# `GET /api/v1/orgs/Home-Network 403 ... required=[read:organization], token scope=read:package,
# write:repository,read:user`; write:issue would have been the next wall. The preflight below now
# PROBES these scopes, so a short token fails in seconds with the missing scope named.
# Set it as an Actions secret on this repository. Nothing in this repo mints,
# stores or references its value; it is consumed by name only. # stores or references its value; it is consumed by name only.
# Until it exists, this workflow FAILS LOUDLY on every scheduled run. That is intended: a # Until it exists, this workflow FAILS LOUDLY on every scheduled run. That is intended: a
# visible red is the correct state for "automation configured but not credentialed". # visible red is the correct state for "automation configured but not credentialed".
@@ -116,15 +128,37 @@ concurrency:
jobs: jobs:
renovate: renovate:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# RENOVATE RUNS AS THE JOB CONTAINER, NOT THROUGH renovatebot/github-action. FIXED 2026-10-30.
# That action does exactly one thing: `docker run ghcr.io/renovatebot/renovate:<version>`. On
# this runner a job has NO Docker daemon, by decision (decisions/2026-10-29-image-pipeline.md:
# act_runner sets container.docker_host to `-` when DOCKER_HOST is tcp://, and the DinD daemon
# is privileged, so a job that could reach it would be root on the node). So every run of the
# action died before Renovate started. Dispatch run 343 (2026-10-30), verbatim:
# [command]/usr/bin/docker run --env LOG_LEVEL ... --env RENOVATE_TOKEN=*** ...
# failed to connect to the docker API at unix:///var/run/docker.sock; ... no such file or directory
# ::error::Error: The process '/usr/bin/docker' failed with exit code 1
# No auth, scope, config or network question was ever reached. Making the Renovate image the
# job container needs no daemon: act starts it through the runner's own DinD exactly as it
# starts the default ubuntu image, then `exec`s each step inside it.
#
# PINNED here (was the action's `renovate-version: 44.167.0`). renovate.json5 is validated
# against this exact tag — keep them in step. Renovate's github-actions manager reads
# `.gitea/workflows` and extracts `container.image`, so this pin is still self-maintained.
#
# The image runs as uid 12021 (`ubuntu`), and that is fine under act: the step command files
# ($GITHUB_STEP_SUMMARY etc.) are created mode 0666 (gitea/act v0.261.2 pkg/runner/step.go),
# HOME comes from the image env, and no step writes the workspace. There is deliberately NO
# actions/checkout: Renovate clones the repo itself from RENOVATE_ENDPOINT, and as uid 12021
# checkout could not `git init` the root-owned workspace volume anyway.
container:
image: ghcr.io/renovatebot/renovate:44.167.0
# Renovate scans every manager across the whole repo; 40m is generous for this repo's size and # Renovate scans every manager across the whole repo; 40m is generous for this repo's size and
# well under a runner's patience. Without a timeout a wedged run holds the `renovate` # well under a runner's patience. Without a timeout a wedged run holds the `renovate`
# concurrency group and silently blocks every subsequent scheduled run. # concurrency group and silently blocks every subsequent scheduled run.
timeout-minutes: 40 timeout-minutes: 40
steps: steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preflight - the token must exist, and its absence must be LOUD - name: Preflight - the token must exist, and its absence must be LOUD
shell: bash
env: env:
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
run: | run: |
@@ -139,33 +173,94 @@ jobs:
That is the failure mode that let renovate.json sit inert in this repo for months while That is the failure mode that let renovate.json sit inert in this repo for months while
seven files claimed "Renovate maintains this pin". seven files claimed "Renovate maintains this pin".
TO FIX: create a Gitea access token for the bot account with scopes TO FIX: create a Gitea access token for the bot account (renovate-bot) with scopes
`write:repository`, `read:user` and `read:package` (the last one lets Renovate read read:user, read:organization, write:repository, write:issue and read:package (the
this repo's own images in the Gitea registry), then add it to this repository as the header of .gitea/workflows/renovate.yaml says which call needs which), then add it to
Actions secret RENOVATE_TOKEN. Do not commit it anywhere. this repository as the Actions secret RENOVATE_TOKEN. Do not commit it anywhere.
MSG MSG
exit 1 exit 1
fi fi
echo "RENOVATE_TOKEN is present (value not printed)." echo "RENOVATE_TOKEN is present (value not printed)."
# Proves the step is running INSIDE the pinned Renovate image, not the default job image.
echo "renovate $(renovate --version)"
- name: Preflight - the token has every scope Renovate needs
shell: bash
env:
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
# ADDED 2026-10-30. Renovate reports EVERY platform-init failure as the bare
# "Authentication failure" (lib/modules/platform/gitea/index.ts initPlatform swallows the
# HTTP error), and a later scope gap surfaces as "Repository has unknown error". Two
# dispatches were spent learning one missing scope at a time. These probes are GETs only,
# and they name the missing scope. The token never reaches argv: curl reads it from stdin
# (`--config -`), so it is not in `ps` output or in the log.
run: |
set -uo pipefail
GITEA=https://gitea.home.keithjackson.dev
REPO=Home-Network/home-network-k3s-impl
fail=0
body=$(mktemp)
probe() { # <scope it proves> <path> <auth: bearer|basic>
local code
if [ "$3" = basic ]; then
code=$(printf 'user = "renovate:%s"\n' "$RENOVATE_TOKEN" \
| curl -sS --config - -o "$body" -w '%{http_code}' "$GITEA$2") || code=000
else
code=$(printf 'header = "Authorization: Bearer %s"\n' "$RENOVATE_TOKEN" \
| curl -sS --config - -o "$body" -w '%{http_code}' "$GITEA$2") || code=000
fi
if [ "$code" = 200 ]; then
echo "ok $1 (GET ${2%%\?*} -> 200)"
return
fi
fail=1
echo "::error::scope $1: GET ${2%%\?*} -> $code"
case "$code" in
401) echo " 401 = Gitea rejected the token VALUE (wrong, expired or revoked)." ;;
403) if grep -q 'Access denied' "$body"; then
echo " 403 'Access denied' = a Cilium/Envoy policy denial, not Gitea."
else
echo " Gitea: $(jq -r '.message // empty' "$body" 2>/dev/null | head -c 300)"
fi ;;
*) echo " body: $(head -c 200 "$body")" ;;
esac
}
probe read:user "/api/v1/user" bearer
probe read:organization "/api/v1/orgs/${REPO%%/*}" bearer
probe read:repository "/api/v1/repos/$REPO" bearer
probe read:issue "/api/v1/repos/$REPO/labels?limit=1" bearer
probe read:package "/v2/token?service=container_registry&scope=repository:home-network/toolbox:pull" basic
# WRITE scopes cannot be proven by a GET. Gitea's scope-refusal message, however, lists
# the token's scopes (names only, never the value): ask for an admin route this bot
# cannot have, and read `token scope=` from the 403. Advisory if the format changes.
printf 'header = "Authorization: Bearer %s"\n' "$RENOVATE_TOKEN" \
| curl -sS --config - -o "$body" "$GITEA/api/v1/admin/orgs?limit=1" || true
scopes=$(jq -r '.message // empty' "$body" 2>/dev/null | sed -n 's/.*token scope=//p')
if [ -n "$scopes" ]; then
echo "token scopes (from Gitea): $scopes"
for want in write:repository write:issue; do
case ",$scopes," in
*",$want,"*|*",all,"*) echo "ok $want" ;;
*) echo "::error::scope $want is missing (token scope=$scopes)"; fail=1 ;;
esac
done
else
echo "::warning::could not read the token's scope list; write:repository and write:issue are UNVERIFIED"
fi
rm -f "$body"
if [ "$fail" -ne 0 ]; then
echo "::error::RENOVATE_TOKEN failed the checks above (a 401 is the value, a 403 is a scope). Re-mint it for renovate-bot with read:user, read:organization, write:repository, write:issue, read:package and overwrite the secret."
exit 1
fi
- name: Run Renovate - name: Run Renovate
uses: renovatebot/[email protected] shell: bash
with: # The repo config is discovered by Renovate from the cloned repo (renovate.json5,
# Pinned, and Renovate maintains this pin itself: renovate.json's enabledManagers # RENOVATE_REQUIRE_CONFIG=required). The action used to ALSO load it as the global config
# includes `github-actions`, and `helpers:pinGitHubActionDigests` is in its `extends`. # via RENOVATE_CONFIG_FILE; that double-loading is gone. ci/lint.sh's `configurationFile:`
# This is the one dependency in the repo whose updater is its own updater. # check therefore has nothing to resolve in this file, and passes.
# MUST match the file's real name. #79 renamed it to renovate.json5 and left this line
# saying `renovate.json`; every scheduled run from 2026-10-18 then died in ~1s with
# ENOENT before Renovate started, and nothing in the gate noticed. ci/lint.sh now
# asserts that every `configurationFile:` here names a file that exists.
configurationFile: renovate.json5
token: ${{ secrets.RENOVATE_TOKEN }}
# PINNED 2026-10-29. Unset, this action version runs Renovate `39` (action.yml
# `renovate-version` default at v41.0.9), i.e. 39.264.0, and renovate.json5 uses
# `managerFilePatterns` (>= 40.2.0): under 39.x validation fails with five errors and
# the run stops at CONFIG_VALIDATION. #79 validated against `latest`, not this.
renovate-version: 44.167.0
env: env:
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
# Self-hosted Gitea. `platform` + `endpoint` were the two values renovate.json's own # Self-hosted Gitea. `platform` + `endpoint` were the two values renovate.json's own
# header said were "set at runtime by the operator's renovate runner" — this is that # header said were "set at runtime by the operator's renovate runner" — this is that
# runner, so they are set here rather than in the repo config, which keeps renovate.json # runner, so they are set here rather than in the repo config, which keeps renovate.json
@@ -189,6 +284,23 @@ jobs:
# token; it therefore needs scope `read:package` in addition to the ones listed in the # token; it therefore needs scope `read:package` in addition to the ones listed in the
# preflight above. Gitea ignores the username for token auth (services/auth/basic.go). # preflight above. Gitea ignores the username for token auth (services/auth/basic.go).
RENOVATE_HOST_RULES: '[{"hostType":"docker","matchHost":"gitea.home.keithjackson.dev","username":"renovate","password":"${{ secrets.RENOVATE_TOKEN }}"}]' RENOVATE_HOST_RULES: '[{"hostType":"docker","matchHost":"gitea.home.keithjackson.dev","username":"renovate","password":"${{ secrets.RENOVATE_TOKEN }}"}]'
# OPTIONAL, ADDED 2026-10-30. A github.com token (a fine-grained PAT with NO permissions,
# public-repo read only) for lookups of GitHub-hosted dependencies. Without it Renovate
# SKIPS them with `github-token-required` — measured in a local dry run: 17 deps, i.e.
# every `uses: actions/*` here plus kubevirt/kubevirt, kubevirt/containerized-data-importer
# and intel/intel-device-plugins-for-kubernetes. Unset is safe: the step below drops the
# empty variable, and those deps simply stay unmaintained, with a WARN in the log.
GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }}
run: |
set -euo pipefail
# An expression that evaluates to '' still EXPORTS the variable, as an empty string.
# Drop those so an unset value means "Renovate's default", never "set to empty".
for v in RENOVATE_DRY_RUN GITHUB_COM_TOKEN; do
[ -n "${!v:-}" ] || unset "$v"
done
# Through the image's entrypoint, as `docker run` would have gone: it sources the
# containerbase env and initialises the bundled tools before exec'ing Renovate.
exec renovate-entrypoint.sh renovate
- name: Summary - name: Summary
if: always() if: always()
+16
View File
@@ -42,3 +42,19 @@ while IFS=: read -r wf _ value; do
done < <(grep -nE '^[[:space:]]*configurationFile:' .gitea/workflows/*.y*ml 2>/dev/null || true) done < <(grep -nE '^[[:space:]]*configurationFile:' .gitea/workflows/*.y*ml 2>/dev/null || true)
[ "$missing" -eq 0 ] || exit 1 [ "$missing" -eq 0 ] || exit 1
ok "workflow configurationFile references resolve" ok "workflow configurationFile references resolve"
# A workflow must not use an action that needs a Docker daemon: jobs on this runner get NONE, by
# decision (decisions/2026-10-29-image-pipeline.md; act_runner maps docker_host to `-`). Such an
# action is valid YAML and passes every other stage, then dies at runtime with
# "failed to connect to the docker API at unix:///var/run/docker.sock". renovatebot/github-action
# did exactly that on every Renovate run until 2026-10-30 (run 343). Run the tool's image as the
# job `container:` instead (see .gitea/workflows/renovate.yaml). Container (`docker://`) actions
# and Docker's own daemon-bound actions are rejected for the same reason. Comments are ignored.
daemon_uses='uses:[[:space:]]*["'\'']?(docker://|renovatebot/github-action|docker/(build-push|setup-buildx|setup-qemu|login)-action)'
bad=0
while IFS= read -r hit; do
err "${hit%%:*}: needs a Docker daemon, which jobs here do not get: $(echo "${hit#*:*:}" | sed -E 's/^[[:space:]]+//')"
bad=1
done < <(grep -HnE "^[^#]*${daemon_uses}" .gitea/workflows/*.y*ml 2>/dev/null || true)
[ "$bad" -eq 0 ] || exit 1
ok "no workflow uses a Docker-daemon-bound action"
+108
View File
@@ -0,0 +1,108 @@
---
date: 2026-10-30
branch: fix/renovate-auth
title: Renovate's token needs five scopes, not three; the workflow documented the wrong list, and now probes it
---
## What changed
**The fix is operator-side.** `RENOVATE_TOKEN` lacks `read:organization`, and it will next lack
`write:issue`. The operator must re-mint it with five scopes:
- `read:user`
- `read:organization`
- `write:repository`
- `write:issue`
- `read:package`
The git half fixes the two things that sent the operator round this loop twice.
**`.gitea/workflows/renovate.yaml`**:
- **The header's scope list is corrected**, with the call each scope serves. The old list named
only `write:repository`, `read:user` and `read:package`, and the operator minted exactly that.
The preflight's "TO FIX" text is corrected the same way.
- **A new step, "Preflight - the token has every scope Renovate needs".**
- It probes read-only GETs: `/user` for read:user, `/orgs/Home-Network` for read:organization,
`/repos/{repo}` for read:repository, `/repos/{repo}/labels` for read:issue, and registry
`/v2/token` for read:package.
- It reads the token's own scope list out of Gitea's 403 from `/admin/orgs` (names only), and
requires `write:repository` and `write:issue` in it.
- It says what a failure means: 401 is the value; a Gitea 403 is a scope, and the probe prints
Gitea's message; an "Access denied" 403 is a Cilium/Envoy denial.
- The token is fed to curl on stdin (`--config -`), never in argv.
**`ci/lint.sh`**: a new guard rejects any non-comment `uses:` in `.gitea/workflows` that needs a
Docker daemon: `docker://`, `renovatebot/github-action` and Docker's daemon-bound
`docker/*-action`s. That is the class of bug fixed in #84.
### Root cause, with evidence
Run 350 (task 567, 06:49 UTC) **got further than 348 and 349.** Gitea's router log, filtered to
the act-runner pod `10.0.0.38`:
06:49:31 GET /api/v1/user 200 OK
06:49:31 GET /api/v1/version 200 OK
06:49:31 GET /api/v1/repos/Home-Network/home-network-k3s-impl 200 OK
06:49:31 GET /api/v1/orgs/Home-Network 403 Forbidden
There were no `/v2/` requests: Renovate stopped before extraction. From the job log, verbatim:
ERROR: Repository has unknown error (repository=Home-Network/home-network-k3s-impl)
"url": "https://gitea.home.keithjackson.dev/api/v1/orgs/Home-Network",
"message": "token does not have at least one of required scope(s), required=[read:organization], token scope=read:package,write:repository,read:user"
"x-envoy-upstream-service-time": "27", "server": "envoy"
INFO: Repository finished ... "result": "unknown-error", "exitCode": 8
**This is not Cilium.** The body is Gitea's JSON scope message, Gitea logged the request, and
`x-envoy-upstream-service-time` shows that Envoy forwarded it upstream.
**The secret WAS updated.** Runs 348 and 349 got `GET /user 403` from `tokenRequiresScopes`. Run 350
got `/user 200`, and its token scope list now includes `read:user`.
**Why `read:organization` is needed.** Renovate 44.167.0, `lib/modules/platform/gitea/index.ts`
`initRepo`, calls `helper.isOrg` → `GET /orgs/{owner}` and rethrows on error. Gitea 1.24.3,
`routers/api/v1/api.go`, puts `/orgs/{org}` behind
`tokenRequiresScopes(AccessTokenScopeCategoryOrganization)`.
**Why `write:issue` is needed next.** In the same `api.go`, `/repos/{o}/{r}/issues…`, `/labels`
and `/milestones` sit in a group with `tokenRequiresScopes(AccessTokenScopeCategoryIssue)`
(lines 1466–1582). Renovate's gitea-helper uses `/issues`, `/issues/{n}/labels`, `/labels` and
`/issues/{n}/comments`, for the dependency dashboard (`:dependencyDashboard` is extended) and for
PR labels. A dry run only reads them; a scheduled run writes them.
The other causes the coordinator listed are ruled out:
- **Schedule window and dry-run.** The run never reached branch processing.
- **Repository config.** The run never reached the clone (`"cloned": undefined`).
- **The Summary step.** It is not involved; the failure is Renovate's own exit 8.
## What I verified
- I read the whole run 350 job log (204 lines), and the router lines above. Runs 348 and 349 are
as recorded in `decisions/2026-10-30-renovate-first-run.md`'s follow-up: `/user` got 403 from
`tokenRequiresScopes`.
- **The probe step, in the pinned image.** I extracted the step body from the YAML and ran it in
`ghcr.io/renovatebot/renovate:44.167.0` (`curl` 8.5.0 and `jq` are present) with a deliberately
**bogus** token. Every probe reported 401 with the "token VALUE" diagnosis, the scope-list read
fell back to a warning, and the step exited 1.
- **The scope-list parser.** Against run 350's exact message format it reports `write:issue`
MISSING. Against a five-scope list it reports both OK. Against `all` it reports both OK.
- **The lint guard.** It flags `renovatebot/github-action` in the pre-#84 `renovate.yaml`
(dfc432c, line 152). It reports nothing on the current workflows, and it ignores commented
lines. `bash -n ci/lint.sh` passes.
- **Account facts, read-only.** `renovate-bot` is a collaborator with `write`, and the org is
public.
## What I did NOT verify
- **The probe with a real token. UNVERIFIED.** I never had the token. In particular, I have not
observed that `GET /api/v1/admin/orgs` gives renovate-bot's token a 403 with a `token scope=`
list. The source says the scope check runs before `reqSiteAdmin`. If the message format
differs, the step warns and does not fail.
- **That five scopes are enough for a full non-dry run.** They cover every helper endpoint Renovate
44.167.0 calls, mapped to Gitea 1.24.3's route categories by reading source. Branch pushes go
over git-over-HTTPS with the token; I believe that needs write:repository, but I have not
observed it.
- **Registry reads as renovate-bot.** `read:package` on a public org is expected to work. The new
probe is the first real test of it.
- **Sensitive paths.** `.gitea/**` and `ci/**` are both sensitive. CI's `approval-policy` will block
until the operator posts `/approve-override` as a PR review.
+113
View File
@@ -0,0 +1,113 @@
---
date: 2026-10-30
branch: fix/renovate-first-run
title: Renovate never started because its action needs a Docker daemon jobs here do not get; it now runs as the job container
---
## What changed
**`.gitea/workflows/renovate.yaml`**: the job no longer uses `renovatebot/github-action`. The
pinned Renovate image (`ghcr.io/renovatebot/renovate:44.167.0`, the same version the action was
pinned to) is now the job's `container:`. A `run:` step calls `renovate-entrypoint.sh renovate`
inside it. `actions/checkout` is gone, because Renovate clones the repo itself.
`GITHUB_COM_TOKEN` is wired from a new **optional** secret, `RENOVATE_GITHUB_COM_TOKEN`. The step
unsets it, and `RENOVATE_DRY_RUN`, when they evaluate to the empty string. The preflight also
prints `renovate --version`, which proves the step ran inside the pinned image.
**`renovate.json5`**: one comment now names `container.image` as the version pin, in place of
`renovate-version`. The config itself is unchanged.
### Why: the root cause
Every run of the action died before Renovate started. That includes the operator's dispatch,
run 343 (task 555, 2026-10-30 06:14 UTC), and the 11 scheduled runs before it (312–322). From
run 343's log:
RENOVATE_TOKEN is present (value not printed).
[command]/usr/bin/docker run --env LOG_LEVEL ... --env RENOVATE_TOKEN=*** --env RENOVATE_CONFIG_FILE=/github-action/renovate.json5 ...
failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /var/run/docker.sock: connect: no such file or directory
Error: The process '/usr/bin/docker' failed with exit code 1
The action is only a wrapper around `docker run`. On this runner a job deliberately gets no
Docker daemon (`decisions/2026-10-29-image-pipeline.md`): act_runner maps `docker_host` to `-` when
`DOCKER_HOST` is `tcp://`, and the DinD daemon is privileged. So this was a
platform-compatibility failure. The run never reached authentication, token scope, config or the
network. **The re-minted token is not implicated, and nothing in this log can say whether its
scopes are right.**
A job `container:` needs no daemon. act creates it through the runner's DinD the same way it
creates the default `ubuntu-latest` image, then `exec`s each step inside it. I checked this
against gitea/act v0.261.2:
- The job container's entrypoint is `/bin/sleep` (`run_context.go`). The Renovate image has
`/usr/bin/sleep`.
- The step command files are written `0o666` (`step.go`), so the image's uid 12021 can write
`$GITHUB_STEP_SUMMARY`.
- `HOME` comes from the image env.
## What I verified
- **The root cause.** I read all of run 343's job log (162 lines, job 0; job 1 is 404). It has one
error, quoted above, and no Renovate output at all. Runs 312–322 are all `failure` from the
`schedule` event.
- **The config validates against the pinned tag.** `podman run ... ghcr.io/renovatebot/renovate:44.167.0
renovate-config-validator /tmp/renovate.json5` printed `Config validated successfully`. It also
printed the known migration diff for the top-level `docker` block.
- **A dry run with the fix extracts and looks up dependencies.** This was a local reproduction. I
made a clean snapshot of this branch's tracked files. I ran it in a podman container of the pinned
image with entrypoint `/bin/sleep`, as the image's own uid 12021 with `HOME=/home/ubuntu`.
Inside it I `exec`ed the workflow step's `run:` body, extracted verbatim from the YAML, with:
- `RENOVATE_PLATFORM=local`, `RENOVATE_DRY_RUN=full` and `GITHUB_COM_TOKEN=` (empty, to
exercise the unset);
- a docker host rule for `gitea.home.keithjackson.dev` using the `gitea-admin` credential from
`~/.git-credentials`, passed through env and never printed.
The run exited 0 and logged `Dependency extraction complete`:
- flux 15 deps / 13 files
- github-actions 24 / 4
- kubernetes 131 / 67
- kustomize 2 / 2
- regex (both custom managers) 7 / 6
- **total 179 deps / 92 files**
It found **42 updates**: 29 kubernetes, 6 flux, 2 kustomize, 1 github-actions and 4 regex
`digest`. 7 of them are `major`.
- **The `# renovate-digest:` lookups against the Gitea registry succeed.** All three
`/v2/token?scope=repository:home-network/<img>:pull` requests succeeded, and so did
`HEAD /v2/home-network/<img>/manifests/main`. They proposed `digest` updates for
`conformance/versions.env`, `infra/agent/versions.env`, `platform/pm-agent/configmap.yaml` and
`platform/pm-agent/deployment.yaml`, all of which carry the zero placeholder digest today. The
`kubernetes` manager's copies of those images report `disabled`, which is the intended effect of
the packageRule.
- **The new pin is still maintained by Renovate.** The github-actions manager extracts
`ghcr.io/renovatebot/renovate:44.167.0` from `renovate.yaml` as depType `container`, and proposes
44.198.0.
- **The `regex` vs `custom.regex` question from #73/#77 is closed.** Both custom managers produced
dependencies.
- **Missing GitHub token.** The local run logged `GitHub token is required for some dependencies`.
That accounts for 17 deps skipped with `github-token-required`, which is why
`RENOVATE_GITHUB_COM_TOKEN` is offered as an optional secret.
## What I did NOT verify
- **The workflow on the real runner. UNVERIFIED.** My reproduction ran with podman, not
act_runner + DinD, and used `--platform=local`. The act behaviour (sleep entrypoint, `exec` as the
image user, 0666 command files) comes from reading the gitea/act v0.261.2 source, not from a run.
The first real evidence will be a `workflow_dispatch` after merge.
- **The Gitea platform path and `RENOVATE_TOKEN`'s scopes. UNVERIFIED.** I have no access token, only
the `gitea-admin` password, which Renovate's Gitea platform cannot use. So these were not
exercised: platform init (`GET /user`, which needs `read:user`), repo clone, and the registry
lookups using the **token** with `read:package` rather than a password.
- **Behaviour inside the schedule window.** A dispatch outside 03:00–06:00 America/New_York
(07:00–10:00 UTC during EDT) will extract and look up, but will log "not within schedule"
instead of `DRY-RUN: Would create ...` lines. I did not change this.
- **Renovate as uid 12021 doing artifact updates** (for example helm lockfiles) on a real,
non-dry run. `--dry-run=full` does not run the tools.
- **The self-bump.** Renovate will propose `ghcr.io/renovatebot/renovate` 44.167.0 → 44.198.0 as a
minor, which the default rule auto-merges. `.gitea/**` is a sensitive path, so it will in fact
wait for an administrator override. Nothing re-runs `renovate-config-validator` on that bump,
and no gate check runs the validator.
- **No alert for "Renovate red every day".** That gap is still open. This failure ran for 12 days
without anyone noticing.
- **Sensitive path.** `.gitea/**` is in `infra/gitea/approval-policy.json`, so CI's
`approval-policy` stage will block until the operator posts `/approve-override` as a PR review.
+4 -3
View File
@@ -29,10 +29,11 @@
// `managerFilePatterns` key ("Custom Manager contains disallowed fields", "Invalid configuration // `managerFilePatterns` key ("Custom Manager contains disallowed fields", "Invalid configuration
// option: flux.managerFilePatterns", ...), because 39.x only knows `fileMatch`; the rename landed // option: flux.managerFilePatterns", ...), because 39.x only knows `fileMatch`; the rename landed
// in 40.2.0. So even after the workflow found this file, every run would have stopped at // in 40.2.0. So even after the workflow found this file, every run would have stopped at
// CONFIG_VALIDATION. renovate.yaml now pins `renovate-version` to the exact release this file is // CONFIG_VALIDATION. renovate.yaml now pins the exact release this file is validated against —
// validated against. VALIDATE WITH THAT SAME TAG: // since 2026-10-30 as the job's `container.image` tag (the action is gone: it needed a Docker
// daemon jobs here do not get). VALIDATE WITH THAT SAME TAG:
// podman run --rm -v "$PWD/renovate.json5:/tmp/renovate.json5:ro,Z" \ // podman run --rm -v "$PWD/renovate.json5:/tmp/renovate.json5:ro,Z" \
// ghcr.io/renovatebot/renovate:<renovate-version from renovate.yaml> \ // ghcr.io/renovatebot/renovate:<container.image tag in renovate.yaml> \
// renovate-config-validator /tmp/renovate.json5 // renovate-config-validator /tmp/renovate.json5
// (Not `--strict`: that also fails on a pending migration of the pre-existing top-level // (Not `--strict`: that also fails on a pending migration of the pre-existing top-level
// `"docker": {"enabled": true}` block at the bottom, which is a warning, not an error.) // `"docker": {"enabled": true}` block at the bottom, which is a warning, not an error.)