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.