Files
familienarchiv/docs/adr/041-renovate-runner-setup.md
Marcel d56a9eb401 docs(adr): add ADR-041 Renovate runner stand-up (two-token, OSV, digest pin)
Records the negative space for #818: why no auto GITEA_TOKEN, why two
tokens not one, why digest-pin on the Renovate action, OSV-vs-platform
distinction on self-hosted Gitea, why the weekly schedule does not mute
security PRs, why lockFileMaintenance has no automerge, and why there is
no l2-containers.puml entry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 11:25:45 +02:00

6.0 KiB

ADR-041 — Renovate runner stand-up: two-token model, OSV surfacing, digest pinning

Date: 2026-06-13
Status: Accepted
Issue: #818


Context

Issue #817 (esbuild/cookie advisory) revealed that main had no early-warning mechanism for newly-published advisories. An advisory landed against already-pinned versions, turned the npm audit --audit-level=high --omit=dev gate red on main, and then ambushed the next unrelated PR (#774). The author who hit it did not cause it and had no warning.

renovate.json existed but renovatebot had never actually run: there was no .gitea/workflows/renovate.yml and zero Renovate-authored PRs in the repo's entire history. The three packageRules (bucket4j / tiptap / privileged-digest) were silently inert.

This ADR records the negative space — why specific design choices were made, so future maintainers do not "tidy up" toward a worse outcome.


Decision

Why there is no auto-provided GITEA_TOKEN

Self-hosted Gitea runners do not auto-inject a GITEA_TOKEN equivalent. docs/infrastructure/ci-gitea.md (and its current line ~251) explicitly states the token "must be created manually." No existing workflow in this repo references GITEA_TOKEN for API calls — only for container registry auth (docker login). Both RENOVATE_TOKEN and NIGHTLY_AUDIT_TOKEN must be manually provisioned as Gitea secrets by a repository admin.

Why two tokens, not one

The two jobs have different blast radii on token compromise:

Token Scopes Used by
RENOVATE_TOKEN contents + pull_request + issues Renovate — must read/write files and open PRs
NIGHTLY_AUDIT_TOKEN issues only Nightly audit — only needs to file a tracking issue

The nightly job's token appears in step env: and is passed to curl -H. A leak via runner logs, process arguments, or a misconfigured step would expose the token. An issues-only token cannot push branches, open PRs, or read repository contents — the leaked token's blast radius is limited to creating/editing issues.

A single broad token would give any leak path full contents + pull_request write access to the repository. That risk is asymmetric with the upside (one fewer secret).

Both tokens belong to one dedicated bot account (consistent authorship; one identity to audit and rotate). Branch protection on main must forbid the bot pushing directly, because a contents-scoped token can push to any unprotected branch.

Why the Renovate action is digest-pinned

renovatebot/github-action executes with the RENOVATE_TOKEN in scope. That token carries contents + pull_request + issues — enough to read files, open PRs, and write issues. An unpinned @v40 tag can be re-pointed by the upstream maintainer (or a compromised maintainer account) at any time. A pinned digest (@<sha>) cannot be silently modified; the SHA is immutable. This is the same threat model applied to all privileged CI steps in this repo (see the matchFileNames rule in renovate.json for .gitea/workflows/**).

Renovate itself will open a PR to bump the digest when a new release ships, which is the intended update path.

Why osvVulnerabilityAlerts is the load-bearing detector on Gitea

Renovate's vulnerabilityAlerts config key triggers off a platform vulnerability graph. GitHub exposes the GitHub Advisory Database via its API; Gitea does not expose an equivalent vulnerability graph. On self-hosted Gitea, vulnerabilityAlerts is effectively a label carrier — it attaches the configured labels to PRs that osvVulnerabilityAlerts already detected, but it is not an independent detector.

osvVulnerabilityAlerts: true is the load-bearing flag: Renovate queries OSV.dev directly (platform-agnostic). The runner host must be able to reach OSV.dev over HTTPS — if egress is filtered, allow osv.dev:443 or the flag silently no-ops.

Why the root schedule does not mute security PRs

"schedule": ["before 6am on monday"] in renovate.json batches routine dependency updates (version bumps outside any security context) to a weekly window. This reduces noise from routine update PRs while still allowing review before merge.

Security and vulnerability PRs bypass the schedule by design — Renovate raises them immediately regardless of the schedule window. A future "tidy-up" that removes or widens the schedule cannot mute vulnerability alerts; this is worth stating explicitly to prevent that misunderstanding.

Why lockFileMaintenance has no automerge

lockFileMaintenance refreshes transitive pins weekly so the dependency tree drifts into fewer advisories over time. It is explicitly set without automerge: true because a weekly transitive pin refresh can silently break the build if a transitive dep introduces a breaking change. These PRs are small and should be reviewed.

Why there is no entry in l2-containers.puml

docs/architecture/c4/l2-containers.puml documents long-lived infrastructure containers (services that run continuously). Renovate is a scheduled CI job that runs on a Gitea Actions runner and exits — it is not a long-lived container. Adding it to the container diagram would misrepresent the architecture. This omission is deliberate, not an oversight.


Consequences

  • Newly-published advisories against our frontend dependencies are surfaced within one day (daily Renovate cron) rather than at the next contributor PR.
  • A nightly npm audit job provides an independent signal for dev-dependency advisories that Renovate may not cover via OSV.
  • Two secrets (RENOVATE_TOKEN, NIGHTLY_AUDIT_TOKEN) must be manually provisioned and rotated annually (or on suspected compromise). See docs/infrastructure/ci-gitea.md for the runbook.
  • The bot account must be kept active and branch protection on main must forbid it pushing directly. These are operational prerequisites, not code invariants.