Repository navigation
Mirror from a superdataset checkout without the mirrors (CI plan + prototype) - #125
yarikoptic-gitmate wants to merge 18 commits into
Conversation
Reviews where update-from-backup relies on having every Dandiset and Zarr mirror cloned locally (including a hazard: an uninstalled mirror is silently re-created) and proposes a committed per-Dandiset index, a 'plan' subcommand, install-on-demand, and a plan/sync/record GitHub Actions workflow, with embargoed Dandisets kept out of public logs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
- 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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
|
CI note: this PR only changes That image comes from dandi/dandi-cli#1921, merged 2026-09-14 after Docker Hub's Generated by Claude Code |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #125 +/- ##
==========================================
+ Coverage 80.67% 81.89% +1.21%
==========================================
Files 19 20 +1
Lines 3617 4048 +431
Branches 568 651 +83
==========================================
+ Hits 2918 3315 +397
- Misses 500 519 +19
- Partials 199 214 +15 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
098fbdd to
045b5b4
Compare
|
Generated by Claude Code |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
For running the update in CI (docs/ci-mirroring-plan.md): - Record per-Dandiset state in the superdataset's .gitmodules: dandiset-last-modified, dandiset-tagged-releases and, for public Dandisets only, dandiset-stats. - `plan`: which Dandisets need a sync, from .gitmodules and the archive listing alone; embargoed ones are only counted unless asked for. - `update-from-backup --clone-missing`: clone recorded but absent Dandiset and Zarr mirrors, with annex.private set before git-annex initializes, instead of refusing to create them anew. - `update-from-backup --results-dir` + `record`: sync without touching the superdataset, then commit the gitlinks and keys to a checkout without subdatasets and push, redoing it over concurrent pushes. - The superdataset description reads the keys for mirrors not installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
On drogon, and in CI once renamed, the superdataset's remote is `github`, which is what the description update looks for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
- Record a new Dandiset's URL from remote.github.url: `git remote get-url` applies insteadOf, which in CI carries the token. - On drogon, put the state keys into the commit that updates the gitlinks they describe, and write them only for the gitlinked commit. - Push a fresh clone that got commits of its own (e.g. a policy migration), as its gitlink will be recorded. - Take subjects from the whole history if the gitlinked commit is not in the clone. - Leave public mirrors without recorded stats out of the description's total with a warning instead of failing `record` after its push. - `plan`: use RemoteDandiset's public properties; cap --max at the 256 jobs of a GitHub Actions matrix; count Dandisets without state as stale longest. - Clone Zarrs for stats within zarr_limit. - Keep a copy of the workflow under docs/ci/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
- --no-github: drop the GitHub organizations from the config, so nothing is created, pushed or changed on GitHub; --anonymous: query the archive without an API key. - update-from-backup --clone-at-gitlink: start clones from the commit the superdataset records, i.e. where the cron on drogon starts. - plan: until .gitmodules records the mirrors' state, take when the superdataset last recorded each mirror from its history. - Results carry their commit's tree, to compare content across runs. - .github/workflows/mirror-dryrun.yml: plan -> one sync per Dandiset -> record --no-push; compare each result with drogon's mirror; post one PR comment with the summary, updated by every run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
The archive answers 401 to `?embargoed=true` without a key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
…onymous The repository's DANDI_API_KEY secret gets a 401 from the archive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
CI mirroring dry runRun 13.1 (pull_request, 2026-11-10T21:37:09Z): backups2datalad Plan: 3 to sync (3 forced (history)); 0 deferred, 1 quiescent, 0 embargoed skipped, 0 unembargoing; 0 no longer on the archive.
Details000018, CI's new commits:
000018, differences from drogon's diff --git a/.gitattributes b/.gitattributes
index 47bb253..4483632 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,4 +1,6 @@
* annex.backend=SHA256E
**/.git* annex.largefiles=nothing
-* annex.largefiles=((mimeencoding=binary)and(largerthan=0))
\ No newline at end of file
+### BEGIN dandiset default policy (backups2datalad)
+* annex.largefiles=((mimeencoding=binary)and(largerthan=0))or(largerthan=10MiB)
+### END dandiset default policy (backups2datalad)000024, CI's new commits:
000024, differences from drogon's diff --git a/.gitattributes b/.gitattributes
index 47bb253..4483632 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,4 +1,6 @@
* annex.backend=SHA256E
**/.git* annex.largefiles=nothing
-* annex.largefiles=((mimeencoding=binary)and(largerthan=0))
\ No newline at end of file
+### BEGIN dandiset default policy (backups2datalad)
+* annex.largefiles=((mimeencoding=binary)and(largerthan=0))or(largerthan=10MiB)
+### END dandiset default policy (backups2datalad)000030, CI's new commits:
000030, differences from drogon's diff --git a/.gitattributes b/.gitattributes
index 47bb253..4483632 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,4 +1,6 @@
* annex.backend=SHA256E
**/.git* annex.largefiles=nothing
-* annex.largefiles=((mimeencoding=binary)and(largerthan=0))
\ No newline at end of file
+### BEGIN dandiset default policy (backups2datalad)
+* annex.largefiles=((mimeencoding=binary)and(largerthan=0))or(largerthan=10MiB)
+### END dandiset default policy (backups2datalad)What
|
…hange With the cron keeping up, a run seldom has anything to sync; force-syncing a few small Dandisets from the recorded commit checks that CI reproduces drogon's mirrors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
…orded commits GitHub lacks The first canaries found the superdataset recording, for 000018, 000024 and 000030, commits that GitHub does not have. Do not fail the comparison over it, and say so in the report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
Also, for a mirror whose recorded commit is missing, take the subjects of the commits a run adds to what it cloned, not the whole history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
…d's CLI Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
A busy Dandiset (001697) is rarely at the same archive state in CI and in drogon's latest push, so the comparison now looks up drogon's commit whose .dandi/assets-state.json matches CI's in the history drogon pushed, falling back to its latest (.github/scripts/mirror_dryrun_compare.sh). The report job runs it again, so re-running that job later catches drogon's later pushes. dandiset.yaml differed in line wrapping only: ruamel.yaml 0.19 breaks long lines elsewhere than drogon's 0.18 (and leaves a trailing space). Pin it, and dandi, via .github/mirror-constraints.txt (PIP_CONSTRAINT), and in docs/ci/mirror.yml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
A metadata-only update keeps .dandi/assets-state.json, so several commits can be at one state (000018: a15423e and e53a22b); the lookup took the one that set it, older than the commit drogon's mirror was at with that state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
Drogon's dandiset.yaml (001697, 2026-11) is reproduced byte for byte by the pure-Python emitter of ruamel.yaml 0.17.40-0.18.12 only: 0.18.13+ break long lines elsewhere, and the C emitter (ruamel.yaml.clib) leaves a long '@context' URL on its key's line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK
The goal is to run the mirror update in CI instead of the drogon cron, working from a checkout of the superdataset with none of the ~1400 mirrors installed. This PR has two parts:
docs/ci-mirroring-plan.md, now v3. It includes the review findings on v2 and the changes that landed onmainsince.The workflow only wraps subcommands. Deciding what to sync, cloning, and recording all happen in backups2datalad, so drogon and CI run the same logic.
What changes in backups2datalad
.gitmodules(mirrorstate.py):dandiset-last-modified,dandiset-tagged-releases, anddandiset-stats(<commit>,<files>,<bytes>)..gitmodulesis public.update-from-backupwrites the keys into the same commit as the gitlinks they describe. Keys are only written for the gitlinked commit.backups2datalad plandecides which Dandisets need a sync from.gitmodulesplus one archive listing./versions/calls. Section 3.2 of the plan has the evidence that publish and unembargo always bumpmodified.--embargoed skipis the default: embargoed Dandisets are only counted, never named.--maxcaps the batch. A quarter of the batch goes to the Dandisets stale longest; the rest is filled smallest first.update-from-backup --clone-missing: when the superdataset (or, for a Zarr, its Dandiset) records a mirror that is not present, it is cloned rather than refused by Refuse to create a mirror from scratch when it already exists elsewhere #124's guard.annex.privateis set before git-annex first runs in the clone, so the clone's UUID never reaches the pushedgit-annexbranch..gitmodules.update-from-backup --results-dir DIRplusrecord:--results-dirsyncs and pushes the mirrors and writes one JSON file per Dandiset, without touching the superdataset.recordapplies those results to a checkout with no subdatasets: gitlinks and keys..gitmoduleskeys. Before, it required every mirror to be installed, even on runs given explicit IDs.docs/ci/mirror.yml: the GitHub Actions workflow fordandi/dandisets.plan, then a matrix of onesyncjob per Dandiset, thenrecord. It is dispatch-only while the cron still runs.dandi/dandisets.Not done yet
These are listed in section 10 of the plan:
--time-limit.planon drogon: havingupdate-from-backupuse the planner there.Testing
.gitmodules, the planner's reasons, skips, cap and embargo policies, andrecord. Therecordtest covers registering a new Dandiset, updating an existing one, a rejected push followed by a retry over a concurrent push, and re-recording idempotently.test/test_ci_flow.py) runs against the local Docker archive:plan→ upload →planagain →update-from-backup --clone-missing --results-dir→record, which has to retry over a concurrent push.test_read_annexed_file_from_commit, fails onmaintoo in that sandbox, because it can't download its test file.actionlint.🤖 Generated with Claude Code
https://claude.ai/code/session_01KoaLJfNiVVnwSDHNMg2bpK