From 9e29e508ceb851a3da289b7e650df2b2c5ab9677 Mon Sep 17 00:00:00 2001 From: Matthias Linhuber Date: Thu, 27 Aug 2026 17:08:38 +0200 Subject: [PATCH] chore(renovate): add org-wide shared Renovate config The org has no consistent dependency or security automation. Ten of twelve code repos have none at all, the Renovate app is not installed, and the two existing renovate.json stubs are dead config nothing acts on. This adds the single source of truth every repo will extend: - renovate-config.json: the org policy. Security fixes raised immediately and labeled, everything else batched into Monday morning and grouped into a handful of combined PRs. No automerge - six repos have no tests. - Two custom managers for things no built-in manager can see: the student IDE images that EduIDE-deployment pins as a plain YAML list under preloading.images (12 production images, currently invisible), and the theiaPlugins GitHub release URLs in EduIDE/package.json. - A CI job running renovate-config-validator --strict. A typo in this file breaks eleven repos at once, so it gets its own gate. - A reusable dependency-review workflow for repos to call on PRs. - renovate/README.md covering how to add a repo, change policy org-wide, and test a preset change before it reaches everything. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QLGHEpzx7D9NYHx4fCmHa9 --- .github/workflows/ci.yml | 19 ++ .github/workflows/dependency-review.yml | 34 +++ renovate-config.json | 266 ++++++++++++++++++++++++ renovate.json | 4 + renovate/README.md | 126 +++++++++++ 5 files changed, 449 insertions(+) create mode 100644 .github/workflows/dependency-review.yml create mode 100644 renovate-config.json create mode 100644 renovate.json create mode 100644 renovate/README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 080981a..018b613 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,25 @@ permissions: contents: read jobs: + renovate-config: + name: Validate shared Renovate config + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # renovate-config.json is extended by every repo in the org, so a typo here + # breaks all of them at once. The version is pinned deliberately: an + # unpinned `npx renovate` resolves to whatever is in the npx cache, which + # is how you end up validating against a release that predates the options + # you are using. + - name: Validate + env: + # renovate: datasource=npm depName=renovate + RENOVATE_VERSION: "44.46.7" + run: | + set -euo pipefail + npx --yes --package "renovate@${RENOVATE_VERSION}" -- \ + renovate-config-validator --strict renovate-config.json + lint: name: actionlint runs-on: ubuntu-latest diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..d27e007 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,34 @@ +name: Dependency Review + +# Fails a PR that introduces a dependency with a known advisory. Renovate tells +# you about vulnerabilities you already have; this catches the ones a PR is +# about to add. Public repos only - the underlying API needs GitHub Advanced +# Security on private repos. +# +# Call it from a repo like this: +# +# jobs: +# dependency-review: +# uses: EduIDE/.github/.github/workflows/dependency-review.yml@main + +on: + workflow_call: + inputs: + fail-on-severity: + description: "Minimum severity that fails the check (low, moderate, high, critical)" + required: false + default: high + type: string + +permissions: + contents: read + +jobs: + review: + name: Dependency review + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/dependency-review-action@v4 + with: + fail-on-severity: ${{ inputs.fail-on-severity }} diff --git a/renovate-config.json b/renovate-config.json new file mode 100644 index 0000000..37e642d --- /dev/null +++ b/renovate-config.json @@ -0,0 +1,266 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "description": [ + "EduIDE org-wide Renovate policy. Single source of truth for every repo.", + "Consume it with: { \"extends\": [\"local>EduIDE/.github:renovate-config\"] }", + "Policy: no automerge anywhere. Security fixes raised immediately, everything else batched Monday morning.", + "See renovate/README.md in this repo for how to change or test this file." + ], + + "extends": [ + "config:recommended", + ":semanticCommitTypeAll(chore)", + "abandonments:recommended", + "customManagers:githubActionsVersions", + "customManagers:dockerfileVersions" + ], + + "timezone": "Europe/Berlin", + "schedule": ["* 0-6 * * 1"], + + "dependencyDashboard": true, + "dependencyDashboardTitle": "Renovate Dependency Dashboard", + "dependencyDashboardLabels": ["dependencies", "renovate"], + "dependencyDashboardOSVVulnerabilitySummary": "unresolved", + + "labels": ["dependencies"], + "semanticCommits": "enabled", + + "automerge": false, + "platformAutomerge": false, + + "prConcurrentLimit": 5, + "prHourlyLimit": 2, + "branchConcurrentLimit": 8, + + "minimumReleaseAge": "5 days", + "internalChecksFilter": "strict", + + "rangeStrategy": "auto", + "postUpdateOptions": ["gomodTidy"], + "separateMajorMinor": true, + "separateMultipleMajor": false, + "separateMinorPatch": false, + + "pinDigests": false, + "updatePinnedDependencies": true, + "configMigration": true, + + "osvVulnerabilityAlerts": true, + "vulnerabilityAlerts": { + "description": "Security fixes bypass the Monday schedule and the release-age quarantine. Rate limits are bypassed unconditionally by Renovate itself.", + "enabled": true, + "schedule": [], + "minimumReleaseAge": null, + "dependencyDashboardApproval": false, + "prCreation": "immediate", + "commitMessagePrefix": "fix(security):", + "addLabels": ["security"], + "automerge": false + }, + + "lockFileMaintenance": { + "description": "Monthly sweep so in-range (caret) updates still reach the lockfiles. Load-bearing: with rangeStrategy auto, this is the only path for them.", + "enabled": true, + "schedule": ["* 0-6 1 * *"], + "minimumReleaseAge": null, + "commitMessageAction": "Refresh", + "automerge": false + }, + + "ignorePaths": [ + "**/node_modules/**", + "**/bower_components/**", + "**/vendor/**", + "**/__tests__/**", + "**/__fixtures__/**", + "**/.worktrees/**", + "**/.vscode-test/**", + "**/.vscode-test-web/**", + "**/.docusaurus/**", + "**/dist/**", + "**/build/**", + "**/out/**" + ], + + "customManagers": [ + { + "customType": "regex", + "description": "EduIDE-deployment pins the student IDE images as plain YAML list items under preloading.images. The helm-values manager only understands image dicts and scalars, so without this they would never be updated.", + "managerFilePatterns": ["/^deployments/.+/values\\.ya?ml$/"], + "matchStrings": [ + "-\\s+(?ghcr\\.io/[^\\s:\"']+):(?[^\\s\"']+)" + ], + "datasourceTemplate": "docker" + }, + { + "customType": "regex", + "description": "theiaPlugins in EduIDE/package.json pins plugin tarballs by GitHub release URL. No built-in manager reads custom manifest keys.", + "managerFilePatterns": ["/(^|/)package\\.json$/"], + "matchStrings": [ + "https://github\\.com/(?[^/\"]+/[^/\"]+)/releases/download/(?[^/\"]+)/" + ], + "datasourceTemplate": "github-releases" + } + ], + + "packageRules": [ + { + "description": "Never bump engines.* - these are compatibility floors, not dependencies.", + "matchManagers": ["npm"], + "matchDepTypes": ["engines"], + "enabled": false + }, + { + "description": "@types/vscode must stay in lockstep with engines.vscode - raise both by hand.", + "matchPackageNames": ["@types/vscode"], + "enabled": false + }, + { + "description": "Never bump the go / toolchain directive in go.mod.", + "matchManagers": ["gomod"], + "matchDepTypes": ["golang", "toolchain"], + "enabled": false + }, + { + "description": "Anything on a -SNAPSHOT is built in-repo, not resolved from a registry.", + "matchCurrentValue": "/-SNAPSHOT$/", + "enabled": false + }, + { + "description": "Internal Theia Cloud Java modules are reactor modules, not external deps.", + "matchDatasources": ["maven"], + "matchPackageNames": ["/^org\\.eclipse\\.theia\\.cloud:/"], + "enabled": false + }, + { + "description": "Pin / digest / rollback updates have no release timestamp, so exempt them from the quarantine.", + "matchUpdateTypes": ["pin", "pinDigest", "digest", "rollback"], + "minimumReleaseAge": null + }, + + { + "description": "All GitHub Actions in one PR.", + "matchManagers": ["github-actions"], + "matchUpdateTypes": ["minor", "patch", "digest", "pin"], + "groupName": "github actions", + "groupSlug": "github-actions", + "semanticCommitType": "ci", + "addLabels": ["github-actions"] + }, + { + "description": "All non-major npm devDependencies in one PR.", + "matchManagers": ["npm"], + "matchDepTypes": ["devDependencies", "optionalDependencies"], + "matchUpdateTypes": ["minor", "patch", "pin"], + "groupName": "npm dev dependencies", + "groupSlug": "npm-dev", + "addLabels": ["javascript"] + }, + { + "description": "All non-major npm runtime dependencies in one PR.", + "matchManagers": ["npm"], + "matchDepTypes": ["dependencies", "peerDependencies", "resolutions", "overrides"], + "matchUpdateTypes": ["minor", "patch", "pin"], + "groupName": "npm dependencies", + "groupSlug": "npm-prod", + "addLabels": ["javascript"] + }, + { + "description": "All non-major JVM dependencies in one PR.", + "matchManagers": ["maven", "maven-wrapper", "gradle", "gradle-wrapper"], + "matchUpdateTypes": ["minor", "patch"], + "groupName": "java dependencies", + "groupSlug": "java", + "addLabels": ["java"] + }, + { + "description": "All non-major Go modules in one PR.", + "matchManagers": ["gomod"], + "matchUpdateTypes": ["minor", "patch", "digest"], + "groupName": "go modules", + "groupSlug": "gomod", + "addLabels": ["go"] + }, + { + "description": "Container base images from Dockerfiles and compose files in one PR.", + "matchManagers": ["dockerfile", "docker-compose"], + "matchUpdateTypes": ["minor", "patch", "digest", "pin"], + "groupName": "container base images", + "groupSlug": "docker", + "addLabels": ["docker"] + }, + { + "description": "Helm chart dependencies from Chart.yaml in one PR.", + "matchManagers": ["helmv3", "helmfile"], + "matchUpdateTypes": ["minor", "patch"], + "groupName": "helm charts", + "groupSlug": "helm", + "addLabels": ["helm"] + }, + { + "description": "Image tags in values.yaml decide what actually runs - keep them apart from chart dependency bumps so one can be reverted without the other. Matched by path as well as manager, because the preloading.images list is picked up by a custom regex manager and must land in the same group.", + "matchManagers": ["helm-values"], + "matchUpdateTypes": ["minor", "patch", "digest", "pin"], + "groupName": "deployed image tags", + "groupSlug": "helm-values", + "addLabels": ["helm", "deployment"] + }, + { + "description": "The preloading.images list entries, same group as the rest of the deployed image tags.", + "matchFileNames": ["deployments/**"], + "matchUpdateTypes": ["minor", "patch", "digest", "pin"], + "groupName": "deployed image tags", + "groupSlug": "helm-values", + "addLabels": ["helm", "deployment"] + }, + { + "description": "Tool versions pinned via '# renovate:' comments in workflows and Dockerfiles ride along with their ecosystem group.", + "matchManagers": ["custom.regex"], + "matchFileNames": [".github/workflows/**", "**/Dockerfile*", "**/*.Dockerfile*"], + "matchUpdateTypes": ["minor", "patch"], + "groupName": "pinned tool versions", + "groupSlug": "tool-versions", + "addLabels": ["tooling"] + }, + { + "description": "Terraform providers, modules and helm_release charts in one PR.", + "matchManagers": ["terraform", "terraform-version"], + "matchUpdateTypes": ["minor", "patch"], + "groupName": "terraform", + "groupSlug": "terraform", + "addLabels": ["terraform"] + }, + + { + "description": "Majors get their own PR, and only appear once a human ticks the box on the Dependency Dashboard.", + "matchUpdateTypes": ["major"], + "dependencyDashboardApproval": true, + "minimumReleaseAge": "14 days", + "addLabels": ["major"] + }, + + { + "description": "Eclipse Theia is a monorepo Renovate does not know about - these must move as one, majors included.", + "matchPackageNames": ["@theia/**", "@eclipse-theia/**", "@eclipse-theiacloud/**"], + "groupName": "eclipse theia", + "groupSlug": "theia", + "dependencyDashboardApproval": true, + "addLabels": ["theia"] + }, + { + "description": "Quarkus platform BOM and extensions must move as one.", + "matchDatasources": ["maven"], + "matchPackageNames": ["/^io\\.quarkus[.:]/"], + "groupName": "quarkus", + "groupSlug": "quarkus", + "addLabels": ["java"] + }, + { + "description": "First-party EduIDE artifacts follow our own release train - no supply-chain quarantine.", + "matchPackageNames": ["ghcr.io/eduide/**", "theiacloud/**"], + "minimumReleaseAge": null, + "addLabels": ["eduide"] + } + ] +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..937744e --- /dev/null +++ b/renovate.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["local>EduIDE/.github:renovate-config"] +} diff --git a/renovate/README.md b/renovate/README.md new file mode 100644 index 0000000..60693ac --- /dev/null +++ b/renovate/README.md @@ -0,0 +1,126 @@ +# Renovate + +Dependency and security updates for the EduIDE org are driven by [Renovate](https://docs.renovatebot.com/). +All policy lives in one file: [`renovate-config.json`](../renovate-config.json) at the root of this repo. + +Every managed repo carries a three-line `renovate.json` that points at it: + +```json +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["local>EduIDE/.github:renovate-config"] +} +``` + +Change the shared file and the change reaches every repo. That is the whole idea - resist +the urge to solve a problem by editing eleven repo-local configs. + +## What the policy does + +| | | +|---|---| +| **Security fixes** | Raised immediately, outside the schedule, prefixed `fix(security):` and labeled `security`. Rate limits and the release-age quarantine do not apply. | +| **Everything else** | Batched into Monday 00:00-07:00 Europe/Berlin, grouped into a handful of combined PRs. | +| **Majors** | Never appear unasked. They sit on the Dependency Dashboard until someone ticks the box. | +| **Automerge** | Off everywhere. Deliberate - see below. | +| **Supply chain** | `minimumReleaseAge: 5 days`, so a compromised release that gets yanked within a few days never reaches a PR. | + +Groups, roughly one PR each: github actions, npm dependencies, npm dev dependencies, +java dependencies, go modules, container base images, helm charts, deployed image tags, +terraform, eclipse theia, quarkus. + +### Why no automerge + +Six of the eleven repos have no tests at all, and two more have tests CI does not run. +Compile-level signal catches a dependency that fails to build; it does not catch one that +builds and misbehaves. Turning automerge on is a one-line change to the shared preset once +the test situation improves - `"automerge": true` under whichever `packageRules` entry you +trust. + +## Add a new repo + +1. Commit the three-line `renovate.json` above to the repo. +2. Add the repo to the Renovate app's **selected repositories** list + (org settings → GitHub Apps → Renovate → Configure). +3. Enable Dependabot **alerts** and the dependency graph on the repo (Settings → Advanced + Security). Renovate reads GitHub's advisory alerts, so without this the security path is + dead. Leave Dependabot *security updates* off - Renovate raises those PRs. +4. Delete any leftover `.github/dependabot.yml` or `.whitesource`. + +Repo-specific exclusions belong in `packageRules` with `matchFileNames` and +`"enabled": false` - **not** in a repo-local `ignorePaths`. `ignorePaths` is +`mergeable: false` in Renovate, so setting it in a repo silently replaces the shared list +rather than adding to it. + +## Change policy for every repo + +Edit `renovate-config.json`, open a PR, merge. Renovate picks up the new preset on its next +run. To force one repo to re-read it immediately, tick the "Check this box to trigger a +request for Renovate to run again" checkbox at the bottom of that repo's Dependency +Dashboard issue. + +## Test a change before it hits all eleven repos + +The CI job in this repo runs `renovate-config-validator --strict` on every PR that touches +the preset. That catches schema errors, unknown options and deprecations - it does **not** +catch a rule that is valid but does the wrong thing. + +For behaviour, point one repo at your branch. Preset references take a git ref suffix: + +```json +{ "extends": ["local>EduIDE/.github:renovate-config#my-branch"] } +``` + +Do that on a throwaway branch of a small repo, let Renovate run, look at the PRs it opens, +then drop the suffix. + +To see what Renovate would do without writing anything: + +```bash +LOG_LEVEL=debug npx --yes renovate@44.46.7 \ + --platform=github --token="$GITHUB_TOKEN" \ + --dry-run=full --schedule="" --require-config=optional \ + EduIDE/EduIDE EduIDE/EduIDE-deployment +``` + +`--schedule=""` clears the Monday window so you do not have to wait until Monday. +Grep the output for `Dependency extraction complete` and check the per-manager file counts. +This is the only reliable way to verify a custom manager, because a regex that matches +nothing fails silently and looks identical to a regex that found nothing to update. + +Pin the version in that command. An unpinned `npx renovate` resolves to whatever is sitting +in the npx cache, which may be years old and will reject options that are perfectly valid. + +## Custom managers + +Two things in this org are invisible to Renovate's built-in managers: + +- **`EduIDE-deployment/deployments/*/values.yaml`** pins the student IDE images as a plain + YAML list under `preloading.images:`. The `helm-values` manager only understands + `image: {repository, tag}` dicts and `image: repo:tag` scalars, so a regex manager covers + the list form. These are the images students actually run, so this is the highest-value + update surface in the org. +- **`EduIDE/package.json` `theiaPlugins`** pins plugin tarballs by URL. No manager reads + custom manifest keys. + +### Known gap: open-vsx + +`theiaPlugins` also pins two VSIXs from open-vsx.org (`vscjava.vscode-java-pack`, +`vscjava.vscode-java-dependency`). Renovate has **no open-vsx datasource**, so these are not +tracked and must be bumped by hand. The only way to automate it today is a +`customDatasources` entry against the open-vsx API, which is still flagged experimental +upstream; not worth the fragility for two pins. Revisit if open-vsx support lands. + +## Things that are deliberate, not oversights + +- **`pinDigests` is off.** Several workflows call `ls1intum/.github/...@feature/...`, which + is a moving branch. Digest pinning would freeze them at a SHA with no update path - the + opposite of the intent. Getting those onto tags is a prerequisite for ever turning digest + pinning on. +- **`rangeStrategy` is `auto`**, which means a caret range like `^4.10.5` produces no PR when + 4.11.0 ships; it reaches the lockfile via monthly `lockFileMaintenance` instead. This keeps + the noise down. Switch to `"bump"` if you would rather see manifest ranges move every week. +- **`-SNAPSHOT` versions and `org.eclipse.theia.cloud:*` are disabled** - they are reactor + modules built in-repo, not registry artifacts. +- **`engines.*`, `@types/vscode` and the `go` directive are frozen.** They are compatibility + floors. Raising them is a decision, not a chore.