From 630e3665ff422ddcc10b55b48a747cfbf9b9d270 Mon Sep 17 00:00:00 2001 From: Remco Beckers Date: Wed, 14 Oct 2026 11:39:49 +0200 Subject: [PATCH 1/3] Automerge compatible GitHub Actions updates separately --- README.md | 2 ++ renovate.json | 16 +++++++++++ scripts/renovate-config-test.cjs | 49 ++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/README.md b/README.md index f482ae0..5793d39 100644 --- a/README.md +++ b/README.md @@ -458,6 +458,8 @@ This project uses GitHub Actions and GoReleaser for automated releases: ### Dependency updates Renovate runs daily and keeps compiler updates separate from library updates. +GitHub Actions patches, minors, and digest updates use a separate automerge PR. +Major action upgrades require manual review. Module patches and `golang.org/x/*` module minors share one library PR. Compiler updates stay within the release line declared in `renovate.json`. CI rejects a module update that selects a compiler from another release line. diff --git a/renovate.json b/renovate.json index ffd2080..d906dd9 100644 --- a/renovate.json +++ b/renovate.json @@ -8,6 +8,22 @@ "prHourlyLimit": 0, "rebaseWhen": "behind-base-branch", "packageRules": [ + { + "description": "Keep GitHub Actions updates in a separate PR", + "matchManagers": ["github-actions"], + "matchDepTypes": ["action"], + "groupName": "GitHub Actions", + "groupSlug": "github-actions" + }, + { + "description": "Automerge compatible action updates after required CI and App approval", + "matchManagers": ["github-actions"], + "matchDepTypes": ["action"], + "matchUpdateTypes": ["patch", "minor", "digest", "pin", "pinDigest"], + "automerge": true, + "automergeType": "pr", + "platformAutomerge": true + }, { "description": "Track direct and indirect Go module updates", "matchManagers": ["gomod"], diff --git a/scripts/renovate-config-test.cjs b/scripts/renovate-config-test.cjs index 701e61f..eaabce0 100644 --- a/scripts/renovate-config-test.cjs +++ b/scripts/renovate-config-test.cjs @@ -68,7 +68,56 @@ async function main() { ); assert.equal(libraries.filter(branch => branch.automerge).length, 2); } + const actionBranches = await flattenUpdates(config, { + 'github-actions': [{ + packageFile: '.github/workflows/ci.yml', + deps: [{ + depName: 'actions/checkout', + packageName: 'actions/checkout', + depType: 'action', + datasource: 'github-tags', + currentValue: 'v7.0.0', + updates: [ + { updateType: 'patch', newValue: 'v7.0.1', newVersion: 'v7.0.1' }, + { updateType: 'minor', newValue: 'v7.1.0', newVersion: 'v7.1.0' }, + { updateType: 'digest', newValue: 'v7.0.0', newDigest: 'a'.repeat(40) }, + { updateType: 'pin', newValue: 'v7.0.0', newVersion: 'v7.0.0' }, + { updateType: 'pinDigest', newValue: 'v7.0.0', newDigest: 'a'.repeat(40) }, + { updateType: 'major', newValue: 'v8.0.0', newVersion: 'v8.0.0' }, + ], + }], + }], + gomod: [{ + packageFile: 'go.mod', + deps: [{ + depName: 'example.com/library', + packageName: 'example.com/library', + depType: 'require', + datasource: 'go', + currentValue: 'v1.0.0', + updates: [{ updateType: 'patch', newValue: 'v1.0.1', newVersion: 'v1.0.1' }], + }], + }], + }); + for (const action of actionBranches.filter(branch => branch.manager === 'github-actions')) { + assert.equal(action.automerge, action.updateType !== 'major'); + assert.equal(action.branchName, action.updateType === 'major' + ? 'renovate/major-github-actions' : 'renovate/github-actions'); + if (action.automerge) { + assert.equal(action.platformAutomerge, true); + assert.equal(action.automergeType, 'pr'); + } + } + assert.equal(actionBranches.filter(branch => branch.branchName === 'renovate/github-actions').length, 5); + assert.equal(actionBranches.find(branch => branch.manager === 'gomod').branchName, 'renovate/go-libraries'); + for (const depType of ['github-runner', 'docker', 'uses-with']) { + const policy = await applyPackageRules({ + ...config, manager: 'github-actions', depType, updateType: 'patch', + }); + assert.equal(policy.automerge, false); + } console.log('PASS: available module patches automerge independently of manual minors'); + console.log('PASS: compatible GitHub Actions updates automerge separately; major actions stay manual'); } main().catch(error => { From d56fa6e662d1344e78a0cdcf58f33d37ebc61d57 Mon Sep 17 00:00:00 2001 From: Remco Beckers Date: Thu, 15 Oct 2026 09:14:57 +0200 Subject: [PATCH 2/3] Run Renovate five hours earlier --- .github/workflows/renovate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml index bedbd55..997c1c7 100644 --- a/.github/workflows/renovate.yml +++ b/.github/workflows/renovate.yml @@ -2,7 +2,7 @@ name: Renovate on: schedule: - - cron: "0 5 * * *" + - cron: "0 0 * * *" workflow_dispatch: permissions: {} From c06950427213a889094185120029c7e08c96c043 Mon Sep 17 00:00:00 2001 From: Remco Beckers Date: Thu, 15 Oct 2026 13:33:49 +0200 Subject: [PATCH 3/3] Resolve recommended preset correctly in tests --- scripts/renovate-config-test.cjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/renovate-config-test.cjs b/scripts/renovate-config-test.cjs index c417a4e..1428ff7 100644 --- a/scripts/renovate-config-test.cjs +++ b/scripts/renovate-config-test.cjs @@ -16,7 +16,9 @@ require(path.join(renovate, 'config/global.js')).GlobalConfig.set({ platform: 'g async function main() { const repo = path.resolve(__dirname, '..'); - let config = mergeChildConfig(getConfig(), await resolveConfigPresets({ extends: ['config:recommended'] })); + const { config: recommended } = await resolveConfigPresets({ extends: ['config:recommended'] }); + assert(recommended.packageRules?.length, 'config:recommended must include package rules'); + let config = mergeChildConfig(getConfig(), recommended); assert(process.argv[3], 'Pass the shared renovate-config checkout as the third argument'); for (const preset of ['default', 'go']) { const shared = JSON.parse(fs.readFileSync(path.join(process.argv[3], `${preset}.json`)));