From 1043c1ae4894f02d530a125796cf54f8ffccc4c0 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sun, 26 Apr 2026 16:06:05 +0200 Subject: [PATCH] fix: emit dependabot.yml without YAML anchors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Root cause The bot-generated `.github/dependabot.yml` in PR #25 was rejected by GitHub's dependabot parser: ```yaml labels: &ref_0 - dependencies ... labels: *ref_0 ``` `js-yaml`'s default `dump()` deduplicates repeated structures (here, the shared `['dependencies']` labels array) using YAML anchors / aliases (`&ref_0` / `*ref_0`). The construct is valid YAML, but GitHub's dependabot.yml parser does not accept it — the `.github/dependabot.yml` status check on the bot's auto-generated PRs FAILs. ## Fix Pass `{ noRefs: true }` to `yaml.dump` in: - `src/dependabot.js:26` — the upsert/PR path that writes the file - `src/app.js:465` — the `/generate-dependabot` ChatOps preview block Added a regression test (`emits dependabot.yml without YAML anchors even when labels are shared across updates`) that constructs a config with two updates sharing the same `labels` array and asserts the dumped output contains neither `&ref_` nor `*ref_`. ## Test plan - [x] All 753 tests pass (was 752 — added 1 regression test) - [x] eslint clean - [ ] After merge + self-update + close-and-reopen of #25: the bot's auto-generated dependabot.yml passes GitHub's `.github/dependabot.yml` check. ## Risk & rollout - Risk: low. Single-option change to a YAML serializer call. The output is semantically identical — only the wire format changes. - Rollout: self-update on merge. Then close PR #25 (if still open) so the bot regenerates a clean version on the next non-bot PR cycle. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) --- __tests__/integration/dependabot.test.js | 29 +++++++++++++++++++++--- src/app.js | 2 +- src/dependabot.js | 5 +++- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/__tests__/integration/dependabot.test.js b/__tests__/integration/dependabot.test.js index f30c9b1..5802cb3 100644 --- a/__tests__/integration/dependabot.test.js +++ b/__tests__/integration/dependabot.test.js @@ -80,7 +80,7 @@ describe('dependabot', () => { 'owner', 'repo', '.github/dependabot.yml', - yaml.dump(depConfig), + yaml.dump(depConfig, { noRefs: true }), 'Add/Update Dependabot configuration' ); expect(createConfigurationPR).not.toHaveBeenCalled(); @@ -96,12 +96,35 @@ describe('dependabot', () => { 'owner', 'repo', '.github/dependabot.yml', - yaml.dump(depConfig), + yaml.dump(depConfig, { noRefs: true }), 'Add/Update Dependabot configuration' ); expect(createConfigurationPR).not.toHaveBeenCalled(); }); + it('emits dependabot.yml without YAML anchors even when labels are shared across updates', async () => { + // Regression: js-yaml's default dump deduplicates repeated structures with + // anchors (&ref_0 / *ref_0). GitHub's dependabot.yml parser rejects + // anchors. The fix is yaml.dump(cfg, { noRefs: true }). + const sharedLabels = ['dependencies']; + const sharedConfig = { + version: 2, + updates: [ + { 'package-ecosystem': 'npm', directory: '/', schedule: { interval: 'weekly' }, labels: sharedLabels }, + { 'package-ecosystem': 'docker', directory: '/', schedule: { interval: 'weekly' }, labels: sharedLabels } + ] + }; + _setConfigForTesting({}); + + await applyDependabotConfig(octokit, 'owner', 'repo', sharedConfig); + + const dumpedContent = upsertRepoFile.mock.calls[0][4]; + expect(dumpedContent).not.toMatch(/&ref_/); + expect(dumpedContent).not.toMatch(/\*ref_/); + // Both updates should still carry the labels inline + expect(dumpedContent.match(/labels:/g) || []).toHaveLength(2); + }); + it('applies config via PR when use_pull_requests is true', async () => { _setConfigForTesting({ change_strategy: { use_pull_requests: true } @@ -114,7 +137,7 @@ describe('dependabot', () => { 'owner', 'repo', '.github/dependabot.yml', - yaml.dump(depConfig), + yaml.dump(depConfig, { noRefs: true }), 'Update Dependabot configuration' ); expect(upsertRepoFile).not.toHaveBeenCalled(); diff --git a/src/app.js b/src/app.js index 0024f76..7d9ef92 100644 --- a/src/app.js +++ b/src/app.js @@ -462,7 +462,7 @@ function registerApp(app, options = {}) { const yaml = (await import('js-yaml')).default; let body = `## Generated Dependabot Configuration for ${owner}/${repo}\n\n`; body += `${result.report}\n\n`; - body += '```yaml\n' + yaml.dump(result.config) + '```\n\n'; + body += '```yaml\n' + yaml.dump(result.config, { noRefs: true }) + '```\n\n'; body += 'Applying configuration...'; await context.octokit.issues.createComment({ diff --git a/src/dependabot.js b/src/dependabot.js index 5ca763c..88174b4 100644 --- a/src/dependabot.js +++ b/src/dependabot.js @@ -23,7 +23,10 @@ async function applyDependabotConfig(octokit, owner, repo, dependabotConfig) { getLogger().info(`Applying Dependabot configuration to ${owner}/${repo}`); const usePR = config.change_strategy?.use_pull_requests || false; - const yamlContent = yaml.dump(dependabotConfig); + // noRefs: true inlines repeated structures (e.g. shared `labels` arrays) + // instead of emitting YAML anchors (`&ref_0` / `*ref_0`) that GitHub's + // dependabot.yml parser rejects. + const yamlContent = yaml.dump(dependabotConfig, { noRefs: true }); if (usePR) { await createConfigurationPR(