ci(board-snapshot): mirror the board archive to Cloudflare R2 - #17998
Conversation
…nch push The archive branch survives an account suspension but not an action against the repository itself. Add one step after the archive commit that mirrors the archive checkout to an R2 prefix with --delete, and writes one tarball per UTC day outside that prefix, since R2 has no bucket versioning and a --delete sync keeps no history. Until the four repository secrets exist the step prints one notice and exits 0; a configured upload that fails goes red. pull_request runs never upload, the same guard the archive commit already carries. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewHead: ① derived judgments — one new step,
Seat measurements: 31 checks on ② semver: no package touched; ③ boundary flags: Implemented-by: Verdict: PASS — the R2 sink as ruled (chat 2026-09-13 「Cloudflare R2」, card #17989 narrowed by claim 5652627427). Landing: the seat flips ready and arms auto-merge now; the merge queue's method lands it as one squash commit. Generated by Claude Code |
|
Landing step (NOT GOVERNED, measured: Generated by Claude Code |
Fixes #17989
One step in
.github/workflows/board-snapshot.yml, right after 「Commit and push the archive」: it mirrors the archive checkout to Cloudflare R2 with--deleteand, once per UTC day, writes the tree as a tarball.scripts/pm/board-snapshot.mjsis untouched and so is what the snapshot reads and commits.Why: the
board-archivebranch survives an account suspension, because a branch belongs to the repository and not to a user — it does not survive an action against the repository itself. The maintainer's answer, verbatim and untranslated: 「那如果推 s3 呢?」「Cloudflare R2」.It lands before the bucket exists: with any of
R2_ACCOUNT_ID/R2_BUCKET/R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEYunset the step prints one notice and exits 0, while a CONFIGURED upload that fails goes red.pull_requestruns never upload, the same guard the archive commit carries. No secret value is in the diff, the log or the summary, and no dependency is added —awsis the ubuntu-latest runner's own CLI.Two decisions to overrule if you disagree: the daily tarball goes to
objectstack/snapshots/, OUTSIDE the mirroredobjectstack/board/prefix, because inside it the next--deletesync deletes it and the history is one day long; and.gitis excluded under two patterns, because in a linked worktree it is a FILE that.git/*does not match.The step's shell, exactly as it lands:
Verified offline with a stand-in on PATH in place of
aws— nothing in this branch ever contacted R2. Every case ran the shell above as a YAML parser extracted it, never as retyped text:::notice::line, exit 0, zeroawsinvocationss3 lsmiss,tar,s3 cp;status=synced 2 object(s), deleted 1; daily tarball written for 2026-09-13already written::error::line, exit 1The tarball built in the third case holds
./board/...and no.git.Gates:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 35 families for this diff; all 35 ran, all 35 exited 0, reconciled through--ran(35 accounted, 0 NOT-MEASURED, 0 UNRUN). The nine workflow-roster gates the tool cannot place for a.github/workflowspath were run as well: eight exited 0, andcheck:single-claim-pathsexits 2 as NOT WIRED without aPR_NUMBER— its declared list holds only.objectui-sha, which this diff does not touch.skip-changeset:.github/**publishes nothing from any released package.Acceptance notes
objectstack/board/board/..., because the sync source is the archive checkout and that branch's tree root holds aboard/directory. Keeping the ruling's source path is what makes the mirror and the daily tarball the same shape, both rooted at the branch tip.AWS_REQUEST_CHECKSUM_CALCULATION: when_requiredis set for the S3-compatible endpoint and is NOT measured here — nothing in this run contacted R2. The first configured run is its measurement.Generated by Claude Code