Skip to content

refactor: dogfood bundles via symlinks instead of byte-identical copies#1357

Open
tschm wants to merge 3 commits into
mainfrom
symlink-dogfood-bundles
Open

refactor: dogfood bundles via symlinks instead of byte-identical copies#1357
tschm wants to merge 3 commits into
mainfrom
symlink-dogfood-bundles

Conversation

@tschm

@tschm tschm commented Jun 30, 2026

Copy link
Copy Markdown
Member

Why

Rhiza dogfoods its own templates: every bundle file in bundles/<name>/... also lived as a byte-identical copy at the repo root, kept in sync by test_bundle_*_sync.py. Any edit to a bundle file had to be hand-mirrored into the root copy or make test failed the whole matrix. This makes bundles/ the single source of truth and removes that double-editing burden.

What

68 root dogfood files → relative symlinks into their owning bundle (.rhiza/**, .claude/commands/*, top-level Makefile/ruff.toml/cliff.toml/LICENSE/pytest.ini, …). Edit the bundle file; the root reflects it automatically.

A few files cannot be symlinks and stay as real copies, guarded by tests rather than by symlink:

Files Why
.github/* (Dependabot, release.yml, secret_scanning.yml, PR template, rulesets) GitHub reads these blobs directly and does not follow symlinks
.github/workflows/* Actions won't run a symlinked workflow (and they differ from bundle stubs by design)
.rhiza/.gitignore git opens .gitignore with O_NOFOLLOW → ELOOP warning + ignored rules
.rhiza/utils/*.py coverage canonicalises symlinks to realpath, so make rhiza-test's --cov=.rhiza/utils would match nothing

Plus intentional mother-repo overrides that deliberately diverge (.claude/commands/rhiza_quality.md, root .gitignore, .pre-commit-config.yaml, .python-version, SECURITY.md, renovate.json).

Changes

  • utils/link_dogfood.py + make sync-self — idempotent (re)creation of the links
  • tests/bundles/test_bundle_github_sync.py — guards the .github real copies (previously unguarded)
  • kept test_bundle_rhiza_sync.py / test_bundle_claude_commands_sync.py (still guard the remaining real copies; symlinks pass trivially)
  • documented the model in CLAUDE.md

Downstream consumers are unaffected

rhiza-cli does git sparse-checkout of the requested bundle then os.walk(followlinks=True) + shutil.copy2, so it dereferences symlinks on copy — synced projects always receive real files (guarded by test_no_symlinks_in_*). This is also why the "inversion" (bundle files symlinking to root) was rejected: a bundle symlink pointing outside its sparse-checkout would dangle and crash sync. Only root files symlink into bundles; bundle files stay self-contained.

Verification

  • make test — 1067 passed
  • make rhiza-test — 212 passed, 100% coverage (symlinked test files collect fine; .rhiza/utils kept real so coverage attributes correctly)
  • make fmt, make typecheck, make deptry — all green
  • bundle + sync suites — 457 passed incl. downstream test_no_symlinks_* and the new .github guard
  • single-source-of-truth proven; make sync-self is idempotent

🤖 Generated with Claude Code

Make bundles/ the single source of truth for the mother repo's own
dogfood files. 68 root files (.rhiza/**, .claude/commands/*, top-level
Makefile/ruff.toml/cliff.toml/LICENSE/pytest.ini, etc.) are now relative
symlinks into their owning bundle, so editing the bundle file propagates
automatically — no more hand-mirroring both sides to satisfy the
byte-identity tests.

A few files cannot be symlinks and stay as real copies, guarded by
tests/bundles/test_bundle_*_sync.py rather than by symlink:
- .github/* platform config — GitHub reads blobs directly and does not
  follow symlinks (Dependabot, Actions workflows, release.yml,
  secret_scanning.yml, PR template, rulesets).
- .rhiza/.gitignore — git opens .gitignore with O_NOFOLLOW (ELOOP).
- .rhiza/utils/*.py — coverage canonicalises symlinks to realpath, so
  `make rhiza-test`'s --cov=.rhiza/utils would match nothing.

Downstream consumers are unaffected: rhiza-cli sparse-checks-out a bundle
and dereferences symlinks on copy, so synced projects always receive real
files (guarded by test_no_symlinks_in_*). Bundle files therefore stay
self-contained real files; only root files symlink into bundles.

- add utils/link_dogfood.py + `make sync-self` to (re)create the links
- add tests/bundles/test_bundle_github_sync.py for the .github copies
- document the model in CLAUDE.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 30, 2026 05:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors Rhiza’s “dogfooded” root-level template files to be relative symlinks into their authoritative bundles/<name>/... sources, reducing the need for manual byte-identical duplication while preserving exceptions where symlinks are unsupported (notably .github/ and coverage-sensitive paths).

Changes:

  • Add utils/link_dogfood.py plus a make sync-self target to (re)create idempotent relative symlinks from root dogfood files into bundles/.
  • Add a new test to guard that root .github/ platform-config files remain real copies and stay byte-identical to bundles/github/.github/ (excluding workflows).
  • Document the dogfooding/symlink model and exceptions in CLAUDE.md.

Reviewed changes

Copilot reviewed 71 out of 140 changed files in this pull request and generated 4 comments.

File Description
utils/link_dogfood.py New mother-repo utility to convert eligible root dogfood copies into relative symlinks into bundles/.
tests/bundles/test_bundle_github_sync.py New test ensuring .github/ non-workflow platform-config copies stay in sync with bundles/github/.github/.
CLAUDE.md Documentation of the dogfooding model, symlink strategy, and exceptions.
.rhiza/make.d/bundles.mk Adds make sync-self target to run the relinking script.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread utils/link_dogfood.py
Comment on lines +139 to +142
bundles_dir = root / "bundles"
if not bundles_dir.is_dir():
sys.exit(f"{YELLOW}No bundles/ directory found at {root} — run from the rhiza repo root.{RESET}")

Comment thread utils/link_dogfood.py
Comment on lines +159 to +163
owners = index.get(rel)
if not owners:
continue
root_bytes = (root / rel).read_bytes()
identical = [o for o in owners if o.read_bytes() == root_bytes]
Comment thread utils/link_dogfood.py
target = os.path.relpath(source, start=link.parent)
if link.is_symlink() and os.readlink(link) == target:
return False
link.unlink()
Comment thread utils/link_dogfood.py
Comment on lines +126 to +131
def relink(root: Path) -> int:
"""Convert every eligible root dogfood copy into a relative symlink.

A root file is eligible when it is tracked by git, not in ``_EXCLUDE``, and
byte-identical to exactly one bundle source. Ambiguous matches (identical to
more than one bundle) are skipped with a warning rather than guessed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants