Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.DS_Store
.claude/
__pycache__/
9 changes: 8 additions & 1 deletion dependabot/reconcile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}", ""
Expand Down
57 changes: 52 additions & 5 deletions dependabot/tests/test_reconcile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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, "", "")

Expand Down Expand Up @@ -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):
Expand Down
Loading