Skip to content

Refuse to create a mirror from scratch when it already exists elsewhere - #124

Merged
yarikoptic merged 8 commits into
mainfrom
claude/refuse-recreating-mirrors
Sep 28, 2026
Merged

yarikoptic merged 8 commits into
mainfrom
claude/refuse-recreating-mirrors

Conversation

@yarikoptic-gitmate

@yarikoptic-gitmate yarikoptic-gitmate commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

AsyncDataset.ensure_installed() runs datalad create whenever the target directory isn't an installed dataset. So a Dandiset registered in the superdataset but not installed gets re-created with a new datalad-id and a one-commit history. That happens to an uninstalled submodule in a fresh clone of dandi/dandisets, and to a mirror directory removed by hand on drogon. The next push then collides with the real repository on GitHub, or overwrites it if forced. Zarr mirrors missing from zarr_root have the same problem.

This is hazard H1/H2 from the CI-mirroring plan (docs/ci-mirroring-plan.md on claude/jolly-meitner-v8jo47), split out as its own PR. It's a prerequisite for running the update from a partial checkout, and it also protects drogon today.

Changes

  • ensure_installed() takes a new before_create hook. It is awaited only when the dataset is about to be created, and raising from it cancels the creation. An installed mirror never reaches it.

  • Dandisets: raise MirrorMissingError if the superdataset's .gitmodules registers the ID, or if gh_org/<id> exists on GitHub.

  • Zarrs: raise if the Dandiset already has a submodule for that Zarr at the asset path, or if zarr_gh_org/<zarr_id> exists. This also covers backup-zarrs, which syncs into --partial-dir.

  • New GitHub.repo_exists(). A 404 is an answer here (not retried, not logged as an error), but GitHub also returns 404 for a private repo the caller can't see, so a 404 is only trusted when:

    • there is a token at all (otherwise it raises);
    • for a classic token, the reported X-OAuth-Scopes include repo, which is needed to see private repos (otherwise it raises).

    A bad token gets 401, which fails through get_repo()'s usual path. Fine-grained and App tokens don't report scopes; they are the same token we push those repos with, so they can see them.

The GitHub checks cost one GET per brand-new Dandiset or Zarr.

Testing

  • New test/test_mirror_guard.py (marked ai_generated), which needs no Docker. It checks that:
    • a registered-but-uninstalled Dandiset is refused and nothing is created;
    • a Dandiset or Zarr whose repo is already on GitHub is refused;
    • a new Dandiset gets through the checks;
    • an installed dataset never runs the check;
    • repo_exists() returns the right answer for 200 and 404, and refuses to answer without a token or with a classic token lacking repo.
  • Without the fix, the first test goes straight on to datalad create.
  • Passing locally: flake8 src test, mypy src test, the new tests, test_zarrbargo.py, test_dirty_detection.py, test_util.py, test_aioutil.py, test_cfg_dandiset.py, test_quiescence.py.
  • Not run: the Docker-based test_core.py / test_zarr.py. CI will cover those.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK

`ensure_installed()` runs `datalad create` whenever the target directory is
not an installed dataset.  For a Dandiset whose submodule is registered in
the superdataset but not installed -- a fresh clone of the superdataset, or a
mirror directory removed by hand -- that starts a second, unrelated history
(new datalad-id, one commit) under the existing name, and the next push
collides with the real repository on GitHub (or, forced, overwrites it).  The
same holds for Zarr mirrors missing from `zarr_root`.

`ensure_installed()` now takes a `before_create` hook, awaited only when the
dataset is about to be created, and raising from it vetoes the creation:

- Dandisets refuse (`MirrorMissingError`) if `.gitmodules` of the
  superdataset registers the ID, or if the GitHub repository exists.
- Zarrs refuse if the Dandiset already has the Zarr as a submodule at the
  asset path, or if the `zarr_gh_org` repository exists.

The GitHub check uses a new `GitHub.repo_exists()`, for which a 404 is an
answer rather than an error, so it is neither retried nor logged as a
failure.  An installed mirror never reaches either check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
yarikoptic-gitmate pushed a commit that referenced this pull request Sep 24, 2026
- Keep per-Dandiset state in .gitmodules (extending #69) instead of a
  separate index, including Zarr counts; drop the github-description cache
  in favour of comparing against GitHub's own.
- Document why the per-Dandiset /versions/ call can go: superdataset
  history shows publish and unembargo always come with a draft 'modified'
  bump.
- Add workflow_dispatch inputs mirroring the CLI, a per-run cap with
  size-based ordering, and a design for long-running backups (graceful
  deadline, routing to self-hosted runners, asynchronous recording),
  drawing on a review of dandi-compute.
- Private repository for embargoed Dandisets; clarify self-hosted runner
  roles; note the H1/H2 guard is in #124.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
GitHub answers 404 also for a private repository the caller may not see,
so repo_exists() now refuses to answer without a token, and on a 404 from a
classic token (the only kind reporting X-OAuth-Scopes) requires the 'repo'
scope.  A bad token gets 401, which already fails via get_repo().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK

Copy link
Copy Markdown
Collaborator Author

CI note: the test (3.x, py) jobs fail in fixture setup, not in this change. docker compose pull for dandi-cli's test archive fails with minio Error unauthorized: access to the requested resource is not authorized. The image is quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z, which dandi/dandi-cli#1921 switched to on 2026-09-14 after minio/minio was deleted from Docker Hub. That registry now refuses the pull too.

All 17 errors are Docker-backed tests. Everything else passes, including the new test_mirror_guard.py. main last went green on 2026-09-20, before the pull started failing, and the docs-only #125 fails the same way.

There's no fix yet: dandi-cli's docker-compose.yml needs another MinIO source, such as a mirrored image, bitnami/minio, or one built from source. I'm triggering one re-run to confirm.


Generated by Claude Code

…leased

MinIO made quay.io/minio/minio require a login on 2026-09-24, so pulling
dandi-cli's test docker-compose stack fails with "unauthorized" and every
Docker-backed test errors in setup.  The fix belongs in dandi-cli
(dandi/dandi-cli#1927) but reaches us only with a release, so meanwhile
layer a compose override over upstream's file via COMPOSE_FILE: same image
dandi-archive's CI uses, `command`/`tty` reset.  It is a no-op once the
upstream file carries the same change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
process_zarr() relied on nothing awaiting between "is this Zarr already
being synced?" and recording that it is.  The new submodule check awaited
in between, so a second asset of the same Zarr (test_backup_zarr_pathological:
sample.zarr and link.zarr) started a second sync_zarr() racing the first
to `datalad create` the same directory.  Record the Zarr first.

Also give test_sync_zarr_converges_github_state's fake GitHub the async
repo_exists() the creation guard now consults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.39%. Comparing base (90aacaf) to head (e63bc39).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #124      +/-   ##
==========================================
+ Coverage   76.95%   77.39%   +0.43%     
==========================================
  Files          19       19              
  Lines        3328     3375      +47     
  Branches      505      517      +12     
==========================================
+ Hits         2561     2612      +51     
+ Misses        571      565       -6     
- Partials      196      198       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread test/minio-compose-override.yml Outdated
Comment thread src/backups2datalad/datasetter.py Outdated
`Manager.gh` is created whenever `gh_org` is set, and the config requires
the Dandiset and Zarr orgs to be set together, so a missing client there
is a bug rather than a reason to skip the check.

Also require dandi >= 0.80.1 for the tests: it pulls MinIO from
bitnamilegacy/minio (dandi/dandi-cli#1927), replacing the override just
reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
@yarikoptic
yarikoptic merged commit 55a4dc1 into main Sep 28, 2026
7 checks passed
@yarikoptic
yarikoptic deleted the claude/refuse-recreating-mirrors branch September 28, 2026 20:54
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.

3 participants