ci(board-snapshot): sync the snapshot directory so the R2 mirror matches the documented prefix - #18136
Conversation
…hes the documented prefix The R2 step synced the archive worktree ROOT while the snapshot is written to `archive/board`, so every key landed at `objectstack/board/board/...` — one segment deeper than the `mirror` prefix on the line above it documents, and visible only from the bucket side. Sync `archive/board/` instead, which maps `archive/board/X` to `objectstack/board/X`. `--delete` is kept and is what migrates the bucket: the stale `objectstack/board/board/` tree is inside the sync target and has no local counterpart, so the first scheduled run after this change removes it. The target is deliberately NOT widened to `objectstack/` to get the same mapping — that would put `objectstack/snapshots/` inside a `--delete` target and eat the daily tarballs. The two `.git` excludes are dropped as dead: they matched `archive/.git`, the linked worktree's gitdir FILE, which is outside the new source. The daily tarball step, the four secrets and the workflow permissions are untouched. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewHead: ① derived judgments (seat-measured on the fetched head
② semver: unchanged — nothing published. ③ boundary flags: the post-change bucket layout is NOT MEASURED here (no R2 credentials in any seat container) — the maintainer's one-run verification, after the next scheduled run (cron Implemented-by: Verdict: PASS — path face clean ⇒ this seat lands: ready through 维护者速读(终稿)一行改动:R2 同步源从 Generated by Claude Code |
Fixes #18049
What this changes
One line in the R2 sync step of
.github/workflows/board-snapshot.yml, plus the comment in that step that described the old source.The step synced the archive worktree ROOT (
aws s3 sync archive/ "$mirror") while the snapshot is written toarchive/board(SNAPSHOT_ARGS: --out=archive/board, near line 237). Withmirror="s3://${R2_BUCKET}/objectstack/board/", that mapsarchive/board/Xontoobjectstack/board/+board/X, so the whole mirror sat one segment deeper than themirrorprefix on the line above it documents — theobjectstack/board/board/...layout the card measured from the bucket side (10912 of 10913 objects, run 34765639736). Syncingarchive/board/mapsarchive/board/Xtoobjectstack/board/X.Migration:
--deletedoes it, and the target is deliberately not widened--deleteis kept, and it is what migrates the bucket: the staleobjectstack/board/board/tree is inside the new sync target and has no local counterpart, so the first scheduled run after this merges removes it. No cleanup step is needed.The target is NOT widened to
objectstack/to reach the same mapping — that spelling would putobjectstack/snapshots/inside a--deletetarget and eat the daily tarballs, which is the failure the step's own comment already warns about. The daily tarball step is untouched: it writes its own key from-C archive .and still carries./board/..., self-describing.No secret name or value is touched, no secret is added, and the workflow's
permissions:block is unchanged.The two
.gitexcludes are dropped, and whyThey are dead after this change, not merely redundant.
archive/is a linked worktree, so its.gitis a FILE atarchive/.git; aws-cli filter patterns are matched against each path relative to the SOURCE directory, and with the source nowarchive/board/that file is outside the source entirely. Nothing underarchive/board/is or can be named.git— the snapshot script writes the board tree there.Dropping them is behaviour-neutral. Keeping them would have left a live-looking guard whose accompanying comment describes a path this step no longer reads, inside the very step whose comment/code disagreement is what this card is about. The tarball step's own
--exclude=.gitstill applies to-C archive .and is left as it is.Acceptance, both directions
Measured in the worktree at
9dfcfb46a:grep -n 'aws s3 sync archive/board/' .github/workflows/board-snapshot.ymlgrep -c 'aws s3 sync archive/ ' .github/workflows/board-snapshot.ymlgrep -n 'mirror=' ...git diff --stat origin/mainA
yaml.safe_loadof the file parses; the job still has 9 steps and everyif:guard is byte-unchanged. Theset +e/code=$?/set -eshape around the sync is byte-identical, so the step's exit-code capture is untouched.Verification the maintainer can run, once
The bucket cannot be re-measured from this container (no R2 credentials), so the post-change layout is NOT MEASURED here. After the next scheduled run (
7 2,8,14,20 * * *), oneListObjectsV2against prefixobjectstack/board/should showmanifest.json,comments/,issues/andreviews/at the top level of that prefix and noboard/subtree under it, whileobjectstack/snapshots/still holds the daily tarballs. That first run'sdeletedcount in the step summary should be roughly the object count of the old tree.Not reachable at PR time
The R2 step is guarded by
if: github.event_name != 'pull_request', as are the archive checkout and the commit/push steps. Apull_requestrun therefore exercises the snapshot on a real runner (--out=board --dry-run --limit=5 --max-requests=25) and never touches the bucket. The changed line cannot be proven green by this PR's own CI; it is covered by the greps above and by the one-run verification described above.Gates
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths — it takes its own change set from the merge base) and run in the foreground, each exit code captured before any pipe: 36 derived families, 35 exit 0, reconciled with--ran.pnpm check:pm-dispatch-gatesis NOT MEASURED here: its self-test suite streamed thousands of passing assertions and zero failures, but did not terminate inside a 520 s budget in this shared container. It runs in CI's requiredLint & Repo Gatesjob.Changeset
skip-changeset: no package'sfiles[]ships.github/**, so this diff publishes nothing from any released package.Acceptance notes
board/is the only entry the archive branch tree holds, but after this change the sync mirrors that subtree rather than the tree root. Left as written — rewriting it is prose polish, not a defect, and this card's surface is the sync step itself.Generated by Claude Code
Generated by Claude Code