Compare commits
6 Commits
main
...
feat/issue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de1a36a0d6 | ||
|
|
d56a9eb401 | ||
|
|
5dc1bf6bfb | ||
|
|
935c8eadd2 | ||
|
|
453e709a7c | ||
|
|
74fdc0cef7 |
@@ -161,3 +161,147 @@ jobs:
|
|||||||
# without first re-evaluating ADR-011.
|
# without first re-evaluating ADR-011.
|
||||||
if: always()
|
if: always()
|
||||||
run: rm -f .env.staging
|
run: rm -f .env.staging
|
||||||
|
|
||||||
|
npm-audit:
|
||||||
|
# Independent parallel job — a deploy failure cannot mask the audit signal
|
||||||
|
# and a clean audit cannot hide a broken deploy. Intentionally no `needs:`.
|
||||||
|
#
|
||||||
|
# Scans dev deps too (no --omit=dev), which is deliberately broader than the
|
||||||
|
# PR gate (ci.yml §Security audit) that uses --omit=dev. A nightly broader
|
||||||
|
# result is NOT a PR gate failure — it catches dev-tooling advisories (esbuild,
|
||||||
|
# Vite, etc.) early. See docs/infrastructure/ci-gitea.md §Nightly audit vs PR gate.
|
||||||
|
#
|
||||||
|
# Required Gitea secrets:
|
||||||
|
# NIGHTLY_AUDIT_TOKEN — PAT with issues scope only. An issues-only token
|
||||||
|
# means a leak via logs/process-args cannot push
|
||||||
|
# branches, open PRs, or read repo contents (ADR-041).
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Assert jq is available
|
||||||
|
run: which jq || sudo apt-get install -y jq
|
||||||
|
|
||||||
|
- name: Run npm audit and file tracking issue on findings
|
||||||
|
# Never run under set -x — NIGHTLY_AUDIT_TOKEN in env would leak to logs.
|
||||||
|
env:
|
||||||
|
NIGHTLY_AUDIT_TOKEN: ${{ secrets.NIGHTLY_AUDIT_TOKEN }}
|
||||||
|
run: |
|
||||||
|
MARKER="Nightly npm audit: high-severity advisory"
|
||||||
|
GITEA_URL="${{ github.server_url }}"
|
||||||
|
REPO="${{ github.repository }}"
|
||||||
|
RUN_URL="${GITEA_URL}/${REPO}/actions/runs/${{ github.run_id }}"
|
||||||
|
|
||||||
|
# --- Self-test (mirrors ci.yml §Assert pattern) ---
|
||||||
|
# Tests the exact jq test() call used in the dedupe step, before any
|
||||||
|
# API call, so a broken matcher fails loudly early rather than silently
|
||||||
|
# opening duplicate issues. Proves the regex only — create-vs-update
|
||||||
|
# decision is exercised by the workflow_dispatch AC.
|
||||||
|
echo "{\"title\": \"${MARKER}\"}" \
|
||||||
|
| jq -e --arg m "$MARKER" '.title | test($m; "i")' > /dev/null \
|
||||||
|
|| { echo "FAIL: self-test — jq test() missed tracking issue title"; exit 1; }
|
||||||
|
echo '{"title": "fix(deps): update dependency esbuild (CVE-2025-12345)"}' \
|
||||||
|
| jq -e --arg m "$MARKER" '.title | test($m; "i") | not' > /dev/null \
|
||||||
|
|| { echo "FAIL: self-test — jq test() incorrectly matched unrelated title"; exit 1; }
|
||||||
|
echo "Self-test passed."
|
||||||
|
|
||||||
|
# --- Run audit ---
|
||||||
|
# No npm ci — audit reads only the lockfile (no network, no install).
|
||||||
|
set +e
|
||||||
|
(cd frontend && npm audit --audit-level=high --json > /tmp/audit.json)
|
||||||
|
AUDIT_EXIT=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "$AUDIT_EXIT" -ne 0 ]; then
|
||||||
|
# --- Build issue body with jq (never string-concat advisory text) ---
|
||||||
|
# Advisory overview/title text is registry-controlled; string-concat
|
||||||
|
# would be an injection/escaping vector into the API body. Truncate
|
||||||
|
# raw excerpt to 500 chars so a pathological overview can't produce
|
||||||
|
# a multi-MB PATCH body.
|
||||||
|
ISSUE_BODY=$(jq -r \
|
||||||
|
--arg run_url "$RUN_URL" \
|
||||||
|
'
|
||||||
|
(.vulnerabilities // {}) as $vulns |
|
||||||
|
($vulns | to_entries |
|
||||||
|
map(select(.value.severity == "high" or .value.severity == "critical")) |
|
||||||
|
map("- **" + .key + "** (" + .value.severity + ")") |
|
||||||
|
if length > 0 then join("\n") else "_See raw output for details._" end) as $pkg_list |
|
||||||
|
"## npm audit: high/critical advisories\n\n" + $pkg_list +
|
||||||
|
"\n\n**Run:** " + $run_url +
|
||||||
|
"\n\n<details><summary>Raw audit excerpt (first 500 chars)</summary>\n\n```\n" +
|
||||||
|
(tostring | .[0:500]) +
|
||||||
|
"\n```\n\n</details>"
|
||||||
|
' /tmp/audit.json)
|
||||||
|
|
||||||
|
# --- Dedupe: fetch open security issues, match by title marker ---
|
||||||
|
# Renovate vuln PRs also carry the "security" label, so >1 open
|
||||||
|
# "security" issue WILL occur. Title-match (not just label) ensures
|
||||||
|
# we deduplicate only our own tracking issue.
|
||||||
|
OPEN_ISSUES=$(curl -sf \
|
||||||
|
-H "Authorization: token $NIGHTLY_AUDIT_TOKEN" \
|
||||||
|
"${GITEA_URL}/api/v1/repos/${REPO}/issues?state=open&type=issues&labels=security&limit=50")
|
||||||
|
|
||||||
|
MATCHED=$(echo "$OPEN_ISSUES" | jq \
|
||||||
|
--arg m "$MARKER" \
|
||||||
|
'[.[] | select(.title | test($m; "i"))] | sort_by(.created_at)')
|
||||||
|
MATCH_COUNT=$(echo "$MATCHED" | jq 'length')
|
||||||
|
|
||||||
|
if [ "$MATCH_COUNT" -gt 0 ]; then
|
||||||
|
# Patch the oldest matched issue (append run URL to body).
|
||||||
|
ISSUE_NUMBER=$(echo "$MATCHED" | jq -r '.[0].number')
|
||||||
|
EXISTING_BODY=$(echo "$MATCHED" | jq -r '.[0].body')
|
||||||
|
NEW_BODY=$(jq -n \
|
||||||
|
--arg existing "$EXISTING_BODY" \
|
||||||
|
--arg run_url "$RUN_URL" \
|
||||||
|
'$existing + "\n\n---\n\nUpdated by run: " + $run_url')
|
||||||
|
PAYLOAD=$(jq -n --arg body "$NEW_BODY" '{"body": $body}')
|
||||||
|
curl -sf -X PATCH \
|
||||||
|
-H "Authorization: token $NIGHTLY_AUDIT_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$PAYLOAD" \
|
||||||
|
"${GITEA_URL}/api/v1/repos/${REPO}/issues/${ISSUE_NUMBER}" > /dev/null
|
||||||
|
echo "Updated tracking issue #${ISSUE_NUMBER}"
|
||||||
|
else
|
||||||
|
# Closed prior issue that recurs → new issue (not reopened).
|
||||||
|
# A re-opened issue would obscure when the advisory was re-discovered.
|
||||||
|
PAYLOAD=$(jq -n \
|
||||||
|
--arg title "$MARKER" \
|
||||||
|
--arg body "$ISSUE_BODY" \
|
||||||
|
'{"title": $title, "body": $body}')
|
||||||
|
CREATED=$(curl -sf -X POST \
|
||||||
|
-H "Authorization: token $NIGHTLY_AUDIT_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$PAYLOAD" \
|
||||||
|
"${GITEA_URL}/api/v1/repos/${REPO}/issues")
|
||||||
|
NEW_NUMBER=$(echo "$CREATED" | jq -r '.number')
|
||||||
|
echo "Opened new tracking issue #${NEW_NUMBER}"
|
||||||
|
|
||||||
|
# Labels are ignored on issue create in Gitea — add in a follow-up call.
|
||||||
|
LABEL_IDS=$(curl -sf \
|
||||||
|
-H "Authorization: token $NIGHTLY_AUDIT_TOKEN" \
|
||||||
|
"${GITEA_URL}/api/v1/repos/${REPO}/labels?limit=50" \
|
||||||
|
| jq '[.[] | select(.name == "security" or .name == "devops" or .name == "P1-high") | .id]')
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token $NIGHTLY_AUDIT_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"labels\": $LABEL_IDS}" \
|
||||||
|
"${GITEA_URL}/api/v1/repos/${REPO}/issues/${NEW_NUMBER}/labels" > /dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit "$AUDIT_EXIT"
|
||||||
|
|
||||||
|
else
|
||||||
|
# --- Heartbeat: proves the job ran and found nothing ---
|
||||||
|
# "No issue created" is only meaningful evidence when paired with a
|
||||||
|
# visible positive signal. Without this, a never-ran job is
|
||||||
|
# indistinguishable from a clean run.
|
||||||
|
#
|
||||||
|
# $GITHUB_STEP_SUMMARY availability is unproven on this runner
|
||||||
|
# (act_runner populates it, but this is the first run to verify it).
|
||||||
|
# Guard before use so an unset variable does not fail the clean-path.
|
||||||
|
MSG="✅ npm audit clean $(date -u)"
|
||||||
|
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
|
||||||
|
echo "$MSG" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
fi
|
||||||
|
echo "$MSG"
|
||||||
|
fi
|
||||||
|
|||||||
44
.gitea/workflows/renovate.yml
Normal file
44
.gitea/workflows/renovate.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: Renovate
|
||||||
|
|
||||||
|
# Runs Renovate daily to surface newly-published advisories via OSV.dev
|
||||||
|
# (osvVulnerabilityAlerts) and open routine update PRs on a weekly batch
|
||||||
|
# schedule (see renovate.json §schedule). Security/vulnerability PRs are
|
||||||
|
# raised immediately regardless of the weekly schedule window.
|
||||||
|
#
|
||||||
|
# Required Gitea secrets (see docs/adr/041-renovate-runner-setup.md):
|
||||||
|
# RENOVATE_TOKEN — PAT with scopes: contents + pull_request + issues
|
||||||
|
# Belongs to a dedicated bot account. Branch protection
|
||||||
|
# on main must forbid this bot pushing directly.
|
||||||
|
#
|
||||||
|
# Platform config is injected via env vars below; the renovate.json in the
|
||||||
|
# repo root carries only dependency rules (no platform/endpoint/repos).
|
||||||
|
#
|
||||||
|
# Digest pin: renovatebot/github-action@8217b3fc286df088d7c27f3255fe8414463bc0fd
|
||||||
|
# corresponds to release v46.1.15. Update by bumping both the digest and the
|
||||||
|
# renovate-version when Renovate publishes a new release. Renovate itself
|
||||||
|
# will open a PR to bump this digest once it runs.
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 3 * * *" # daily at 03:00 UTC — cuts OSV-alert latency to ≤1 day
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
renovate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run Renovate
|
||||||
|
# Pinned by digest — this action holds contents+pull_request+issues
|
||||||
|
# scopes; an unpinned tag is a supply-chain risk (see ADR-041).
|
||||||
|
uses: renovatebot/github-action@8217b3fc286df088d7c27f3255fe8414463bc0fd # v46.1.15
|
||||||
|
with:
|
||||||
|
configurationFile: renovate.json
|
||||||
|
token: ${{ secrets.RENOVATE_TOKEN }}
|
||||||
|
renovate-version: "46.1.15"
|
||||||
|
env:
|
||||||
|
RENOVATE_PLATFORM: gitea
|
||||||
|
RENOVATE_ENDPOINT: https://git.raddatz.cloud
|
||||||
|
RENOVATE_REPOSITORIES: '["marcel/familienarchiv"]'
|
||||||
|
LOG_LEVEL: info
|
||||||
123
docs/adr/041-renovate-runner-setup.md
Normal file
123
docs/adr/041-renovate-runner-setup.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# ADR-041 — Renovate runner stand-up: two-token model, OSV surfacing, digest pinning
|
||||||
|
|
||||||
|
**Date:** 2026-06-13
|
||||||
|
**Status:** Accepted
|
||||||
|
**Issue:** [#818](https://git.raddatz.cloud/marcel/familienarchiv/issues/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](https://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.
|
||||||
@@ -462,3 +462,82 @@ jobs:
|
|||||||
name: e2e-results
|
name: e2e-results
|
||||||
path: frontend/test-results/e2e/
|
path: frontend/test-results/e2e/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Renovate + Nightly Audit — Token Model
|
||||||
|
|
||||||
|
> See [ADR-041](../adr/041-renovate-runner-setup.md) for full rationale.
|
||||||
|
|
||||||
|
### Two-token model
|
||||||
|
|
||||||
|
This repo uses two separate tokens for automated dependency work, both manually
|
||||||
|
provisioned as Gitea secrets. There is **no auto-provided `GITEA_TOKEN`** on
|
||||||
|
self-hosted Gitea runners — it must be created manually (see §Context Variable Names
|
||||||
|
table above).
|
||||||
|
|
||||||
|
| Secret | Scopes | Job | Reason |
|
||||||
|
|--------|--------|-----|--------|
|
||||||
|
| `RENOVATE_TOKEN` | `contents` + `pull_request` + `issues` | `renovate.yml` | Renovate needs to read/write files and open PRs |
|
||||||
|
| `NIGHTLY_AUDIT_TOKEN` | `issues` only | `nightly.yml` → `npm-audit` job | Only needs to file a tracking issue; an `issues`-only token cannot push branches or read contents — limits blast radius on leak |
|
||||||
|
|
||||||
|
Both tokens belong to a single dedicated bot account. **Branch protection on `main`
|
||||||
|
must forbid the bot pushing directly**, because a `contents`-scoped token can push
|
||||||
|
to any unprotected branch.
|
||||||
|
|
||||||
|
### PAT rotation cadence
|
||||||
|
|
||||||
|
Rotate both tokens:
|
||||||
|
- **Annually** (calendar reminder)
|
||||||
|
- **Immediately** on suspected compromise (runner log leak, accidental `set -x`, etc.)
|
||||||
|
|
||||||
|
Tokens are stored exclusively as Gitea secrets. Never commit them to `.env` files or
|
||||||
|
log them. The nightly audit step passes its token via `env:` at the step level, reads
|
||||||
|
it as `$NIGHTLY_AUDIT_TOKEN` in the shell, and never runs the API `curl` under
|
||||||
|
`set -x`.
|
||||||
|
|
||||||
|
### OSV vs platform alerts on Gitea
|
||||||
|
|
||||||
|
Renovate's `vulnerabilityAlerts` config key requires a *platform* vulnerability graph.
|
||||||
|
**Gitea does not expose one** — it has no equivalent to the GitHub Advisory Database
|
||||||
|
API. On this runner, `vulnerabilityAlerts` is a label carrier only: it attaches
|
||||||
|
`security` and `P1-high` labels to PRs that `osvVulnerabilityAlerts` already raised.
|
||||||
|
|
||||||
|
`osvVulnerabilityAlerts: true` is the load-bearing detector. Renovate queries
|
||||||
|
[OSV.dev](https://osv.dev) directly, which works regardless of platform. The runner
|
||||||
|
host must be able to reach `osv.dev:443`. If egress is filtered and OSV.dev is
|
||||||
|
unreachable, the flag silently no-ops — verify egress when standing up the runner.
|
||||||
|
|
||||||
|
### Nightly audit vs PR gate (divergence)
|
||||||
|
|
||||||
|
| Gate | Command | Dev deps | When |
|
||||||
|
|------|---------|----------|------|
|
||||||
|
| PR gate (`ci.yml`) | `npm audit --audit-level=high --omit=dev` | ❌ excluded | Every PR |
|
||||||
|
| Nightly audit (`nightly.yml`) | `npm audit --audit-level=high` | ✅ included | Nightly + `workflow_dispatch` |
|
||||||
|
|
||||||
|
The nightly job is **deliberately broader** — it catches dev-tooling advisories
|
||||||
|
(esbuild, Vite, vitest, etc.) that the PR gate ignores. A red nightly audit job does
|
||||||
|
**not** mean the PR gate is broken; the two signals are independent.
|
||||||
|
|
||||||
|
### Runbook: nightly-opened tracking issue
|
||||||
|
|
||||||
|
When the `npm-audit` job opens or updates the tracking issue
|
||||||
|
"Nightly npm audit: high-severity advisory":
|
||||||
|
|
||||||
|
1. **Triage severity.** Check the advisory page (link in the issue body). Is it
|
||||||
|
exploitable in production? A dev-only dep (e.g. esbuild, prettier) has no
|
||||||
|
production attack surface — treat as low urgency.
|
||||||
|
|
||||||
|
2. **Pin or upgrade.** If a non-breaking upgrade is available, update
|
||||||
|
`frontend/package.json` and regenerate the lockfile. Open a PR.
|
||||||
|
|
||||||
|
3. **Override if justified.** If the advisory does not apply (dev-only dep, no
|
||||||
|
exploitable path), add an `npm audit` override in `package.json`:
|
||||||
|
```json
|
||||||
|
"overrides": { "esbuild": ">=0.25.4" }
|
||||||
|
```
|
||||||
|
Document the rationale in the PR body. See #817 for the reference decision tree.
|
||||||
|
|
||||||
|
4. **Close the tracking issue** once the advisory is resolved or overridden and the
|
||||||
|
nightly job runs clean (verify via the `✅ npm audit clean` heartbeat in the job
|
||||||
|
summary).
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ receivers:
|
|||||||
name: Renovate
|
name: Renovate
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 3 * * 1' # every Monday at 3am
|
- cron: '0 3 * * *' # daily at 03:00 UTC — cuts OSV-alert latency to ≤1 day
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -160,32 +160,58 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Run Renovate
|
- name: Run Renovate
|
||||||
uses: renovatebot/github-action@v40
|
# Pin by digest — this action holds contents+pull_request+issues token;
|
||||||
|
# an unpinned tag is a supply-chain risk. Update digest + renovate-version
|
||||||
|
# together when Renovate publishes a new release.
|
||||||
|
uses: renovatebot/github-action@8217b3fc286df088d7c27f3255fe8414463bc0fd # v46.1.15
|
||||||
with:
|
with:
|
||||||
configurationFile: renovate.json
|
configurationFile: renovate.json
|
||||||
token: ${{ secrets.GITEA_TOKEN }}
|
token: ${{ secrets.RENOVATE_TOKEN }}
|
||||||
renovate-version: latest
|
renovate-version: "46.1.15"
|
||||||
|
env:
|
||||||
|
RENOVATE_PLATFORM: gitea
|
||||||
|
RENOVATE_ENDPOINT: https://gitea.example.com # replace with your Gitea URL
|
||||||
|
RENOVATE_REPOSITORIES: '["org/repo"]' # replace with your repo slug
|
||||||
|
LOG_LEVEL: info
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Token:** `RENOVATE_TOKEN` must be a PAT on a dedicated bot account with scopes
|
||||||
|
> `contents` + `pull_request` + `issues`. **Do not reuse** `GITEA_TOKEN` — that variable
|
||||||
|
> is not auto-provided on self-hosted Gitea runners and must be manually created anyway;
|
||||||
|
> using a single broad token violates least-privilege. See ADR-041.
|
||||||
|
|
||||||
### Renovate Configuration
|
### Renovate Configuration
|
||||||
|
|
||||||
|
The `renovate.json` in the repo root carries only dependency rules — platform and
|
||||||
|
endpoint config is injected via `env:` in the workflow above. Keep the two concerns
|
||||||
|
separate so the config file remains portable.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
// renovate.json
|
|
||||||
{
|
{
|
||||||
"platform": "gitea",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"endpoint": "https://gitea.example.com",
|
"osvVulnerabilityAlerts": true,
|
||||||
"repositories": ["org/familienarchiv"],
|
"dependencyDashboard": true,
|
||||||
"automerge": true,
|
"schedule": ["before 6am on monday"],
|
||||||
"automergeType": "pr",
|
"vulnerabilityAlerts": {
|
||||||
|
"labels": ["security", "P1-high"]
|
||||||
|
},
|
||||||
|
"lockFileMaintenance": {
|
||||||
|
"enabled": true,
|
||||||
|
"schedule": ["before 6am on monday"]
|
||||||
|
},
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": ["patch"],
|
"matchPackageNames": ["com.example:my-dep"],
|
||||||
"automerge": true
|
"automerge": true,
|
||||||
|
"matchUpdateTypes": ["patch"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Do not add `automerge: true` at the root.** Security and digest-bump PRs should
|
||||||
|
> always be reviewed manually. Per-rule `automerge` on patch-level routine deps is fine.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Secrets Management -- age + git-crypt
|
## Secrets Management -- age + git-crypt
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"osvVulnerabilityAlerts": true,
|
||||||
|
"dependencyDashboard": true,
|
||||||
|
"schedule": ["before 6am on monday"],
|
||||||
|
"vulnerabilityAlerts": {
|
||||||
|
"labels": ["security", "P1-high"]
|
||||||
|
},
|
||||||
|
"lockFileMaintenance": {
|
||||||
|
"enabled": true,
|
||||||
|
"schedule": ["before 6am on monday"]
|
||||||
|
},
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"description": "bucket4j-core is manually pinned outside the Spring BOM — track patch auto-merge, minor/major as PRs.",
|
"description": "bucket4j-core is manually pinned outside the Spring BOM — track patch auto-merge, minor/major as PRs.",
|
||||||
@@ -9,13 +19,13 @@
|
|||||||
"matchUpdateTypes": ["patch"]
|
"matchUpdateTypes": ["patch"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchPackagePatterns": ["^@tiptap/"],
|
"matchPackageNames": ["/^@tiptap/"],
|
||||||
"groupName": "tiptap",
|
"groupName": "tiptap",
|
||||||
"automerge": false
|
"automerge": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Digest bumps for images used in privileged CI steps (--privileged --pid=host) must be reviewed manually — a compromised image has root-equivalent host access. Covers .gitea/actions/** too: the reload-caddy alpine digest now lives in a composite action (#603).",
|
"description": "Digest bumps for images used in privileged CI steps (--privileged --pid=host) must be reviewed manually — a compromised image has root-equivalent host access. Covers .gitea/actions/** too: the reload-caddy alpine digest now lives in a composite action (#603).",
|
||||||
"matchPaths": [".gitea/workflows/**", ".gitea/actions/**"],
|
"matchFileNames": [".gitea/workflows/**", ".gitea/actions/**"],
|
||||||
"matchUpdateTypes": ["digest"],
|
"matchUpdateTypes": ["digest"],
|
||||||
"automerge": false,
|
"automerge": false,
|
||||||
"reviewersFromCodeOwners": false
|
"reviewersFromCodeOwners": false
|
||||||
|
|||||||
Reference in New Issue
Block a user