From 1831b2e237cc1906642f742ca1251ad4f368eb90 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Mon, 2 Nov 2026 10:46:21 -0500 Subject: [PATCH 1/2] Rewrite a drifting Dependabot config with render, not check The reconciler detected drift and then fixed none of it. `check --write` creates the file, or appends an ecosystem the file is missing; it does not rewrite one that covers everything and is simply wrong -- which is what drift is. So every repository reached `changed_paths`, found nothing, and came back `skipped:write-made-no-change`. Measured 2026-11-02, dry run across simtabi: cli-kit-js, mesobia, netsapiens-api, repo-templates and sis-sdk all returned that status. Nothing had ever been proposed. `render --write` replaces everything outside the local block, which is what the reconciler is for; the same three repositories now report `would-open`. The reason the suite did not catch it is in the fake, not in the tests: `git clone` made an empty directory, so every repository looked like a first-time write and `check --write` always had a file to create. It now materialises a wrong file for a repository the generator calls drifting, which is the only state in which the bug exists. `check --write` in the fake wrote unconditionally, making the double more capable than the real generator. `git status --porcelain` called the tree dirty whenever the file existed. With a file present from the clone, existence proves nothing, so it now compares contents. With those three corrected and `check --write` still in place, five tests fail. That was the red; `render` is the green. --- dependabot/reconcile.py | 9 ++++- dependabot/tests/test_reconcile.py | 57 +++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/dependabot/reconcile.py b/dependabot/reconcile.py index e5ea09b..39b6e7f 100644 --- a/dependabot/reconcile.py +++ b/dependabot/reconcile.py @@ -133,9 +133,16 @@ def reconcile_repo(repo, owner, gh, shell, opts, policy, workdir): if rc != 1: return f"skipped:generator-exit-{rc}", "the generator could not render this file safely" + # `render --write`, not `check --write`. `check --write` only creates the file or appends + # an ecosystem it is missing, so a repository whose file covers everything and is merely + # wrong -- which is what drift is -- came back "skipped:write-made-no-change" and was + # never fixed. `render` replaces everything outside the local block, which is the whole + # point of the reconciler. Measured 2026-11-02: all five repositories sampled across + # simtabi reported that status, so nothing had ever been proposed. + # # `--strict` is check-only: the generator refuses it beside `--write` with exit 2, which # silently skipped every repository from the day agent-kit started enforcing that. - write = [sys.executable, opts.generator_script, "check", dest, "--write", "--policy", opts.policy] + write = [sys.executable, opts.generator_script, "render", dest, "--write", "--policy", opts.policy] rc = shell.run(write, check=False).returncode if rc not in (0, 1): return f"skipped:write-exit-{rc}", "" diff --git a/dependabot/tests/test_reconcile.py b/dependabot/tests/test_reconcile.py index 62e254e..eb527f4 100644 --- a/dependabot/tests/test_reconcile.py +++ b/dependabot/tests/test_reconcile.py @@ -12,6 +12,9 @@ POLICY = common.load_policy() RENDERED = "# managed\nversion: 2\nupdates: []\n" +# What a drifting repository actually has on disk: a dependabot.yml that covers every +# ecosystem and is simply wrong. `check --write` has nothing to create or append for it. +DRIFTED = "version: 2\nupdates: [] # hand written\n" def repo(name, **kw): @@ -47,7 +50,17 @@ def __init__(self, generator_rc): def run(self, cmd, check=True, input_text=None): self.cmds.append(cmd) if "clone" in cmd: - os.makedirs(cmd[-1]) + dest = cmd[-1] + os.makedirs(dest) + # A repository the generator calls drifting has a file already; one it calls + # exit-2 has nothing to inspect. Cloning into an empty directory made every + # repository look like a first-time write, which is why `check --write` looked + # as if it fixed things. + name = os.path.basename(dest).split("__", 1)[1] + if self.generator_rc.get(name) == 1: + os.makedirs(os.path.join(dest, ".github"), exist_ok=True) + with open(os.path.join(dest, ".github", "dependabot.yml"), "w") as fh: + fh.write(DRIFTED) return subprocess.CompletedProcess(cmd, 0, "", "") if "check" in cmd: if "--strict" in cmd and "--write" in cmd: @@ -56,13 +69,35 @@ def run(self, cmd, check=True, input_text=None): dest = cmd[cmd.index("check") + 1] name = os.path.basename(dest).split("__", 1)[1] if "--write" in cmd: - os.makedirs(os.path.join(dest, ".github"), exist_ok=True) - with open(os.path.join(dest, ".github", "dependabot.yml"), "w") as fh: - fh.write(RENDERED) + # `check --write` creates the file, or appends an ecosystem the file is + # missing. It does not rewrite one that is merely wrong -- which is exactly + # what a drifting repository has. This fake used to write unconditionally, + # so it was more capable than the generator and hid the reconciler never + # fixing anything: every repository came back + # "skipped:write-made-no-change" in the real run. + target = os.path.join(dest, ".github", "dependabot.yml") + if not os.path.exists(target): + os.makedirs(os.path.dirname(target), exist_ok=True) + with open(target, "w") as fh: + fh.write(RENDERED) return subprocess.CompletedProcess(cmd, self.generator_rc[name], "", "") + if "render" in cmd: + # `render --write` replaces everything outside the local block, which is what + # bringing a drifting file to the standard needs. + dest = cmd[cmd.index("render") + 1] + if "--write" in cmd: + target = os.path.join(dest, ".github", "dependabot.yml") + os.makedirs(os.path.dirname(target), exist_ok=True) + with open(target, "w") as fh: + fh.write(RENDERED) + return subprocess.CompletedProcess(cmd, 0, RENDERED, "") if "status" in cmd: dest = cmd[2] - dirty = os.path.exists(os.path.join(dest, ".github", "dependabot.yml")) + target = os.path.join(dest, ".github", "dependabot.yml") + # Dirty means the working tree differs from what was cloned, not merely that a + # file exists: with a drifting file present from the clone, existence proves + # nothing. + dirty = os.path.exists(target) and open(target).read() != DRIFTED return subprocess.CompletedProcess(cmd, 0, "?? .github/dependabot.yml\0" if dirty else "", "") return subprocess.CompletedProcess(cmd, 0, "", "") @@ -173,6 +208,18 @@ def test_write_never_carries_strict(self): self.assertTrue(writes, "drifting repositories are written") self.assertFalse([c for c in writes if "--strict" in c]) + def test_the_rewrite_uses_render_not_check(self): + # `check --write` creates or appends; it does not rewrite a file that is merely + # wrong, which is what drift is. Measured 2026-11-02: every repository sampled came + # back "skipped:write-made-no-change", so the reconciler had never proposed anything. + code, _, shell = self.run_main(self.base_routes(), self.RC, "--apply") + self.assertEqual(code, 0) + writes = [c for c in shell.cmds if "--write" in c] + self.assertTrue(writes, "drifting repositories are written") + for cmd in writes: + self.assertIn("render", cmd) + self.assertNotIn("check", cmd) + def test_a_failed_write_fails_the_run(self): class WriteFails(FakeShell): def run(self, cmd, check=True, input_text=None): From a8bc6572a4990c04974f2828cd929f6d3ca27ad4 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Mon, 2 Nov 2026 10:46:51 -0500 Subject: [PATCH 2/2] Ignore __pycache__ Running the dependabot and bot test suites leaves bytecode beside the sources, so every session after the first one opens on a dirty tree and has to decide whether five .pyc files are work. They are not. The directories here were written at 10:45 today by whichever run came first. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 40ddbcd..5a2d23f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store .claude/ +__pycache__/