diff --git a/.github/workflows/validate-renovate.yml b/.github/workflows/validate-renovate.yml new file mode 100644 index 0000000..52c1443 --- /dev/null +++ b/.github/workflows/validate-renovate.yml @@ -0,0 +1,55 @@ +name: Validate Renovate Config + +# Triggers on any path Renovate would pick up as a config (see lookup order: +# https://docs.renovatebot.com/configuration-options/#configurationoptions). +# Listing them all means a PR can't accidentally bypass validation by adding, +# say, a higher-precedence `renovate.json` while only the `.json5` is path-filtered. +on: + pull_request: + paths: + - 'renovate.json' + - 'renovate.json5' + - '.renovaterc' + - '.renovaterc.json' + - '.github/renovate.json' + - '.github/renovate.json5' + - '.github/workflows/validate-renovate.yml' + push: + branches: [develop] + paths: + - 'renovate.json' + - 'renovate.json5' + - '.renovaterc' + - '.renovaterc.json' + - '.github/renovate.json' + - '.github/renovate.json5' + - '.github/workflows/validate-renovate.yml' + +jobs: + validate: + name: renovate-config-validator + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # We've deliberately chosen `renovate.json5` (supports comments). Any + # higher-precedence config file (`renovate.json`, `.renovaterc`, + # `.github/renovate.json`) would silently override it — fail loudly + # so a stray file is caught at PR time. + - name: Refuse higher-precedence config files + run: | + higher_precedence=(renovate.json .renovaterc .renovaterc.json .github/renovate.json) + found=() + for f in "${higher_precedence[@]}"; do + [[ -f "$f" ]] && found+=("$f") + done + if (( ${#found[@]} > 0 )); then + echo "::error::Higher-precedence Renovate config file(s) present — these override renovate.json5: ${found[*]}" + exit 1 + fi + + - uses: actions/setup-node@v4 + with: + node-version: '24' + + - run: npx --yes --package renovate@43 -- renovate-config-validator --strict --no-global renovate.json5 diff --git a/renovate.json b/renovate.json deleted file mode 100644 index d9ebaee..0000000 --- a/renovate.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:recommended" - ], - "packageRules": [ - { - "description": "Security hardening for GitHub Actions (FIS-17871): pin to SHA digests, delay updates 3 days", - "matchManagers": [ - "github-actions" - ], - "groupName": "GitHub Actions", - "minimumReleaseAge": "3 days", - "pinDigests": true - } - ] -} - diff --git a/renovate.json5 b/renovate.json5 new file mode 100644 index 0000000..ab13110 --- /dev/null +++ b/renovate.json5 @@ -0,0 +1,92 @@ +{ + $schema: "https://docs.renovatebot.com/renovate-schema.json", + + // Renovate baseline: SHA-pinned GHA, 3-day release-age soak, OSV + // vulnerability alerts, per-ecosystem grouping, major-update isolation. + // Python (pip_requirements) + GHA only — this repo has no Docker / + // npm / terraform / datastore deps. + extends: [ + "config:best-practices", + // Don't widen semver ranges (`^1.2.3` stays `^1.2.3`). + ":preserveSemverRanges", + ], + + labels: ["dependencies"], + + // ---- Volume controls ---- + // Renovate has no `prWeeklyLimit`; the effective weekly cap is achieved by + // combining a weekly schedule with prConcurrentLimit + prHourlyLimit. Net + // effect: at most 4 PRs created per Monday and at most 4 open at any time. + prConcurrentLimit: 4, + prHourlyLimit: 4, + schedule: ["before 5am on monday"], + lockFileMaintenance: { enabled: true, schedule: ["before 5am on monday"] }, + // Suppress PRs for deps that fail Renovate's internal status checks + // (upstream CI red, deprecation flagged, etc.). Reduces autoclose noise. + internalChecksFilter: "strict", + + // ---- Security baseline ---- + osvVulnerabilityAlerts: true, + vulnerabilityAlerts: { + enabled: true, + labels: ["security"], + // groupName:null is INTENTIONAL: security/CVE PRs must NEVER be batched + // with manager-grouping rules. + groupName: null, + }, + + packageRules: [ + // Bundle lock file maintenance across all managers into ONE PR per cycle + // so a multi-lockfile repo doesn't exhaust prConcurrentLimit. + { + description: "Bundle lock file maintenance across all managers into one PR", + matchUpdateTypes: ["lockFileMaintenance"], + groupName: "lockfile-maintenance", + }, + + // ---- GHA SHA-pin + 3-day soak ---- + // SHA pin protects against tag-mutation supply-chain attacks; the soak + // window gives the community time to surface CVEs before they hit our + // workflows. + { + description: "GitHub Actions: pin to SHA digests + enforce 3-day release soak", + matchManagers: ["github-actions"], + pinDigests: true, + minimumReleaseAge: "3 days", + }, + + // ---- Backend Python grouping ---- + { + description: "Backend non-major (minor + patch) -> backend-non-major", + matchManagers: ["pip_requirements"], + matchUpdateTypes: ["minor", "patch"], + groupName: "backend-non-major", + minimumReleaseAge: "3 days", + }, + { + // groupName:null is INTENTIONAL. Majors must never be batched — one + // stuck major blocks every other major in the group. + description: "Backend major -> isolated PR per dep (no group)", + matchManagers: ["pip_requirements"], + matchUpdateTypes: ["major"], + groupName: null, + minimumReleaseAge: "3 days", + }, + + // ---- GHA grouping ---- + { + // 'digest' + 'pin' types included so that pinDigests:true SHA refreshes + // land in this group, not ungrouped. + description: "GHA non-major (minor, patch, digest, pin refreshes) -> gha-non-major", + matchManagers: ["github-actions"], + matchUpdateTypes: ["minor", "patch", "digest", "pin"], + groupName: "gha-non-major", + }, + { + description: "GHA major -> isolated PR per action (no group)", + matchManagers: ["github-actions"], + matchUpdateTypes: ["major"], + groupName: null, + }, + ], +}