Two things, both about not finding out late. No workflow declared `permissions`, so all eighteen jobs across the four workflows ran on a token with the default full repository scope. Every one of them only checks out code, and deploy reaches the cluster over SSH with the deploy key, and Renovate writes through its own bot PAT rather than the Actions token. So `contents: read` is all any of them needed. The panel image ships 15 known advisories and nothing was looking. Add a scan-deps job that fails on anything new, and record the eight current ones by ID in the workflow. It is a list rather than a baseline count so that the diff that accepts an advisory says so in words, and it lives in our workflow instead of the package manifest so a subtree sync from forust/userbot cannot quietly widen the exemption. Both halves were checked to fail on a regression, not just to pass today: removing one --ignore-vuln turns the Python step red, and dropping --audit-level to moderate turns the npm one red on the devalue advisory. npm audits production dependencies only. All seven findings in the full tree are build- or test-time: the esbuild advisory needs a vite dev server exposed to the internet, and nanoid's infinite loop needs a custom generator called with size 0, which postcss does not do. None are in the 91 kB bundle the panel serves, so failing on them would be noise that trains people to ignore the job. The starlette entries are the reason the job is not "fail on everything": fastapi 0.115.12 pins starlette<0.47.0 and the last four fixes need 0.49.1 through 1.3.1, so clearing them is a jump to fastapi 0.141.x and is upstream's call, not a drive-by. Four of the seven are reachable in principle, which the comment on the job sets out. The panel answers only on userbot.workstation.internal with no public route, which is what keeps those four from being an internet-facing DoS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renovate for Gitea
Renovate runs as a Kubernetes CronJob and creates container image update pull requests in Gitea. It does not deploy changes itself.
Kubernetes
Create a dedicated Gitea user named renovate-bot, create a repository access
token, and grant it repository read/write plus issue read/write permissions.
Add read:packages if Renovate must inspect private Gitea registry images.
Create the ignored Secret locally; never commit the PAT:
cp renovate/k8s/secrets.yaml.example renovate/k8s/secrets.yaml
$EDITOR renovate/k8s/secrets.yaml
kubectl apply -f renovate/k8s/namespace.yaml
kubectl apply -f renovate/k8s/secrets.yaml
kubectl apply -f renovate/k8s/configmap.yaml
kubectl apply -f renovate/k8s/cronjob.yaml
The renovate/k8s/active marker makes the normal deployment workflow include
the namespace, ConfigMap, and CronJob. The Secret is intentionally excluded
from Git and must be applied separately after every new cluster.
Run it immediately instead of waiting for the six-hour schedule.
Two options, both use the same renovate/renovate.json:
kubectl create job --from=cronjob/renovate renovate-manual-$(date +%s) -n renovate
or the renovate-run Actions workflow (Actions tab → renovate-run →
Run workflow). It runs the same image as the CronJob on the self-hosted runner
via Docker — the tag is read out of renovate/k8s/cronjob.yaml at run time
rather than hardcoded, so the two cannot drift apart. Required Actions secrets
(repo or org settings):
RENOVATE_TOKEN— renovate-bot PAT (repository + issue read/write).RENOVATE_GITHUB_COM_TOKEN— optional, for changelogs and GitHub rate limits.
Inputs: repositories (default forust/homelab), log_level
(info/debug). Only one run at a time (concurrency group
renovate-run), same as the CronJob Forbid policy.
Inspect runs with:
kubectl get cronjob,jobs,pods -n renovate
kubectl logs -n renovate job/<job-name>
RENOVATE_GITHUB_COM_TOKEN is optional but recommended for changelogs and
GitHub API rate limits. Set it in the Kubernetes Secret if available.
Compose
Copy .env.example to .env, set the PAT, and run:
docker compose -f renovate-compose.yaml run --rm renovate
The Compose file is intentionally named renovate-compose.yaml, so the
repository's automatic deployment discovery does not start it accidentally.
Configuration
renovate/renovate.json is the single source of truth. The Compose file and the
renovate-run workflow mount that file directly.
A ConfigMap cannot read from the repository, so the CronJob needs the config
inlined. renovate/k8s/configmap.yaml is therefore a generated copy:
.gitea/workflows/sync-renovate-configmap.sh # regenerate after editing
.gitea/workflows/sync-renovate-configmap.sh --check # fail if out of date
The renovate-ci workflow runs the --check form on every PR and push, so a
config edit that forgets to regenerate the ConfigMap cannot be merged.
Beyond images, customManagers in the config track:
- Helm chart versions pinned in
.gitea/workflows/deploy-lib.sh. The built-inhelmv3manager only readsChart.yamlandhelm-valuesonly reads values files, so neither sees a version written into ahelm upgradecommand — these are declared ascustom.regexmanagers against thehelmdatasource. - CI linter versions in
.gitea/workflows/tool-versions.env.
The Renovate image tag is deliberately not in tool-versions.env:
renovate/k8s/cronjob.yaml owns it, and the workflows read it from there.
How updates flow
Renovate scans both compose.yaml files and Kubernetes manifests, opens a
branch and PR with image tag changes, and waits for CI. After merge, the
existing deployment workflow applies Kubernetes changes or redeploys Compose
stacks. Renovate never updates running workloads directly.