diff --git a/lib/content/dependabot-yml.hbs b/lib/content/dependabot-yml.hbs index 2b87781a..bac49f3e 100644 --- a/lib/content/dependabot-yml.hbs +++ b/lib/content/dependabot-yml.hbs @@ -5,7 +5,7 @@ updates: - package-ecosystem: npm directory: / schedule: - interval: daily + interval: {{ interval }} target-branch: "{{ branch }}" allow: - dependency-type: direct diff --git a/lib/content/index.js b/lib/content/index.js index 678e9536..7f6cac44 100644 --- a/lib/content/index.js +++ b/lib/content/index.js @@ -187,6 +187,7 @@ module.exports = { esm: false, updateNpm: true, dependabot: 'increase-if-necessary', + dependabotInterval: 'daily', unwantedPackages: [ 'eslint', 'eslint-plugin-node', diff --git a/lib/util/dependabot.js b/lib/util/dependabot.js index 5e0df40c..66d0fbb3 100644 --- a/lib/util/dependabot.js +++ b/lib/util/dependabot.js @@ -4,8 +4,8 @@ const { minimatch } = require('minimatch') const parseDependabotConfig = v => (typeof v === 'string' ? { strategy: v } : (v ?? {})) module.exports = (config, defaultConfig, branches) => { - const { dependabot } = config - const { dependabot: defaultDependabot } = defaultConfig + const { dependabot, dependabotInterval } = config + const { dependabot: defaultDependabot, dependabotInterval: defaultInterval } = defaultConfig if (!dependabot) { return false @@ -15,8 +15,13 @@ module.exports = (config, defaultConfig, branches) => { .filter(b => dependabot[b] !== false) .map(branch => { const isReleaseBranch = minimatch(branch, config.releaseBranch) + + // Determine the interval to use: branch-specific > package-specific > default + const interval = parseDependabotConfig(dependabot[branch]).interval || dependabotInterval || defaultInterval + return { branch, + interval, allowNames: isReleaseBranch ? [NAME] : [], labels: isReleaseBranch ? ['Backport', branch] : [], ...parseDependabotConfig(defaultDependabot), diff --git a/test/apply/dependabot.js b/test/apply/dependabot.js index 6f44e04e..bb2e35a4 100644 --- a/test/apply/dependabot.js +++ b/test/apply/dependabot.js @@ -5,7 +5,11 @@ const setup = require('../setup.js') const setupDependabot = async (t, { branches = ['main'], ...config } = {}) => { const s = await setup(t, { package: { - templateOSS: config, + templateOSS: { + ...config, + // Include branches in the templateOSS config so they get processed + branches: branches.length > 1 ? branches : undefined, + }, }, mocks: { '@npmcli/git': { @@ -101,3 +105,170 @@ t.test('no dependabot', async t => { t.equal(s.dependabot, false) t.equal(s.postDependabot, false) }) + +t.test('custom interval', async t => { + const s = await setupDependabot(t, { + dependabotInterval: 'weekly', + }) + + t.match(s.dependabot[0], { + schedule: { interval: 'weekly' }, + }) +}) + +t.test('branch-specific interval', async t => { + const s = await setupDependabot(t, { + dependabot: { + main: { interval: 'monthly' }, + }, + }) + + t.match(s.dependabot[0], { + schedule: { interval: 'monthly' }, + }) +}) + +t.test('mixed interval configuration', async t => { + const s = await setupDependabot(t, { + branches: ['main', 'develop'], + dependabotInterval: 'weekly', + dependabot: { + main: { interval: 'monthly' }, + }, + }) + + t.equal(s.dependabot.length, 2) + + // main branch should use branch-specific interval + const mainBranch = s.dependabot.find(d => d['target-branch'] === 'main') + t.match(mainBranch, { + schedule: { interval: 'monthly' }, + }) + + // develop branch should use global interval + const developBranch = s.dependabot.find(d => d['target-branch'] === 'develop') + t.match(developBranch, { + schedule: { interval: 'weekly' }, + }) +}) + +t.test('branch-specific interval with strategy', async t => { + const s = await setupDependabot(t, { + dependabot: { + main: { interval: 'weekly', strategy: 'auto' }, + }, + }) + + t.match(s.dependabot[0], { + schedule: { interval: 'weekly' }, + 'versioning-strategy': 'auto', + }) +}) + +t.test('global interval with branch-specific strategy only', async t => { + const s = await setupDependabot(t, { + dependabotInterval: 'monthly', + dependabot: { + main: { strategy: 'lockfile-only' }, + }, + }) + + t.match(s.dependabot[0], { + schedule: { interval: 'monthly' }, + 'versioning-strategy': 'lockfile-only', + }) +}) + +t.test('fallback to daily when no interval specified', async t => { + const s = await setupDependabot(t, { + dependabot: 'increase-if-necessary', + }) + + t.match(s.dependabot[0], { + schedule: { interval: 'daily' }, + 'versioning-strategy': 'increase-if-necessary', + }) +}) + +t.test('mixed branches with some having interval and some not', async t => { + const s = await setupDependabot(t, { + branches: ['main', 'develop', 'staging'], + dependabotInterval: 'weekly', + dependabot: { + main: { interval: 'monthly' }, + develop: { strategy: 'auto' }, + // staging gets global interval + }, + }) + + t.equal(s.dependabot.length, 3) + + const mainBranch = s.dependabot.find(d => d['target-branch'] === 'main') + t.match(mainBranch, { + schedule: { interval: 'monthly' }, + }) + + const developBranch = s.dependabot.find(d => d['target-branch'] === 'develop') + t.match(developBranch, { + schedule: { interval: 'weekly' }, + 'versioning-strategy': 'auto', + }) + + const stagingBranch = s.dependabot.find(d => d['target-branch'] === 'staging') + t.match(stagingBranch, { + schedule: { interval: 'weekly' }, + }) +}) + +t.test('empty branch config falls back to global interval', async t => { + const s = await setupDependabot(t, { + dependabotInterval: 'monthly', + dependabot: { + main: {}, // empty object should fall back to global interval + }, + }) + + t.match(s.dependabot[0], { + schedule: { interval: 'monthly' }, + }) +}) + +t.test('no package interval and no branch interval falls back to default', async t => { + const s = await setupDependabot(t, { + // no dependabotInterval at package level + dependabot: { + main: {}, // empty object, no interval + }, + }) + + t.match(s.dependabot[0], { + schedule: { interval: 'daily' }, // should fall back to default + }) +}) + +t.test('branch config as string without interval falls back properly', async t => { + const s = await setupDependabot(t, { + // no dependabotInterval at package level + dependabot: { + main: 'auto', // string config, no interval property + }, + }) + + t.match(s.dependabot[0], { + schedule: { interval: 'daily' }, // should fall back to default + 'versioning-strategy': 'auto', + }) +}) + +t.test('falsy package interval and no branch interval falls back to default', async t => { + const s = await setupDependabot(t, { + dependabotInterval: null, // explicitly falsy + dependabot: { + main: {}, // empty object, no interval + }, + }) + + t.match(s.dependabot[0], { + schedule: { interval: 'daily' }, // should fall back to default + }) +})