From c57faae757600937c33f8029ee10f199951e2c5a Mon Sep 17 00:00:00 2001 From: ThePrismSystem <10910959+ThePrismSystem@users.noreply.github.com> Date: Thu, 6 Aug 2026 11:21:11 -0500 Subject: [PATCH 1/2] chore(ci): replace dependabot with renovate Renovate now covers security updates too, so GitHub's automated security fixes are switched off to stop the two opening duplicate PRs for one CVE. Dependabot ALERTS stay on - Renovate reads them to learn what is vulnerable. Three things Dependabot could not reach: ARG BEANS_VERSION in the Dockerfile is the only pin for the beans binary, and no built-in manager covers an ARG. A custom manager watches it against the go datasource, which is the same thing scripts/build-beans.sh installs. The Node floor is stated in .nvmrc, engines.node and two Dockerfile stages. Those have drifted before - the README badge still read >=22 after the bump to 24 - so they are grouped into one PR. Non-major dev dependencies arrive as a single weekly PR instead of one per package, which is what 37 dependencies across four manifests was producing. Dev dependencies automerge once the nine required checks pass; runtime dependencies, actions and the beans pin never do. Automerged updates wait three days after publication, which is the window an account takeover has to get a malicious release merged unread. --- .github/dependabot.yml | 10 -------- cspell.config.yaml | 1 - renovate.json | 57 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 11 deletions(-) delete mode 100644 .github/dependabot.yml create mode 100644 renovate.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 3deee26..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - - package-ecosystem: "npm" - directory: "/" - schedule: - interval: "weekly" diff --git a/cspell.config.yaml b/cspell.config.yaml index 3672051..c0f1b7d 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -17,7 +17,6 @@ words: - typeof - devcontainer - codecov - - dependabot - typecheck - codegen - browsable diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..f7f8c0c --- /dev/null +++ b/renovate.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "schedule": ["before 9am on monday"], + "prConcurrentLimit": 10, + "dependencyDashboard": true, + "vulnerabilityAlerts": { + "enabled": true, + "schedule": ["at any time"], + "automerge": false, + "labels": ["security"] + }, + "packageRules": [ + { + "description": "Runtime dependencies ship to users; every bump gets read by a human.", + "matchDepTypes": ["dependencies", "engines", "packageManager"], + "automerge": false + }, + { + "description": "Non-major dev dependencies: one PR a week, merged by the gate rather than by hand.", + "matchDepTypes": ["devDependencies"], + "matchUpdateTypes": ["patch", "minor"], + "groupName": "dev dependencies (non-major)", + "automerge": true, + "minimumReleaseAge": "3 days" + }, + { + "description": "Major dev dependencies still automerge, but one at a time — a group is only as mergeable as its worst member.", + "matchDepTypes": ["devDependencies"], + "matchUpdateTypes": ["major"], + "automerge": true, + "minimumReleaseAge": "3 days" + }, + { + "description": "Actions run with a token and are not dev dependencies. Digest-pinned, and merged deliberately.", + "matchManagers": ["github-actions"], + "groupName": "github actions", + "pinDigests": true, + "automerge": false + }, + { + "description": "The Node floor is stated in four places and they have drifted before, so they move in one PR: .nvmrc, engines.node, and both Dockerfile stages.", + "matchPackageNames": ["node"], + "groupName": "node" + } + ], + "customManagers": [ + { + "customType": "regex", + "description": "ARG BEANS_VERSION is the only pin for the beans binary — the Dockerfile builds from it and CI reads it back out with sed, so nothing else may state a version. No manager covers an ARG, so this is the one place a bot can see it.", + "managerFilePatterns": ["/^Dockerfile$/"], + "matchStrings": ["ARG BEANS_VERSION=(?v[0-9][^\\s]*)"], + "depNameTemplate": "github.com/hmans/beans", + "datasourceTemplate": "go" + } + ] +} From 2c9e8d02a17c333e2105f3813eb134365614dc9e Mon Sep 17 00:00:00 2001 From: ThePrismSystem <10910959+ThePrismSystem@users.noreply.github.com> Date: Thu, 6 Aug 2026 11:54:37 -0500 Subject: [PATCH 2/2] ci: run fork pull requests on github's runners, everything else on ours A fork pull request executes its own code here - pnpm install with whatever package.json it carries, pnpm test, and docker build - and on a self-hosted runner that is arbitrary code on the LAN, beside a beans-web instance with no authentication. Anyone can open a fork PR; pushing a branch to this repo takes write access, and that is the whole of the trust boundary. Each job repeats the expression because Actions does not expand YAML anchors. On a push to main the pull_request context is absent and the expression falls through to self-hosted, which is what we want for a ref that is already merged. --- .github/workflows/ci.yml | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff69b3f..c1cb950 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,23 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# Every job below repeats this, because Actions does not expand YAML anchors: +# +# runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'self-hosted' }} +# +# A fork PR runs on GitHub's hardware; everything else runs on ours. The line +# that matters is `pnpm install` plus `pnpm test` plus `docker build` — that is +# arbitrary code from the pull request, and on a self-hosted runner it would be +# arbitrary code on our LAN, beside a beans-web instance that has no +# authentication. Opening a fork PR requires nothing; pushing a branch to this +# repo requires write access, and that is the whole of the trust boundary. +# +# On a push to main the pull_request context is absent, so the expression falls +# through to self-hosted, which is correct: that ref is already merged. jobs: lint: name: Lint - runs-on: ubuntu-latest + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'self-hosted' }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -37,7 +50,7 @@ jobs: audit: name: Audit - runs-on: ubuntu-latest + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'self-hosted' }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -55,7 +68,7 @@ jobs: typecheck: name: Typecheck - runs-on: ubuntu-latest + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'self-hosted' }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -73,7 +86,7 @@ jobs: test: name: Test (coverage) - runs-on: ubuntu-latest + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'self-hosted' }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -182,7 +195,7 @@ jobs: e2e: name: E2E - runs-on: ubuntu-latest + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'self-hosted' }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -241,7 +254,7 @@ jobs: build: name: Build needs: [lint, typecheck] - runs-on: ubuntu-latest + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'self-hosted' }} env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} steps: @@ -261,7 +274,7 @@ jobs: quality: name: Quality - runs-on: ubuntu-latest + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'self-hosted' }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -292,7 +305,7 @@ jobs: image-scan: name: Image Scan - runs-on: ubuntu-latest + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'self-hosted' }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1