Repository navigation
ci(board-snapshot): upload only the records that changed to R2, not all ~41k objects every run - #22930
Merged
Conversation
…ll ~41k objects every run `aws s3 sync` uploads when the size differs or the local mtime is newer than the object's LastModified, and a fresh checkout gives every file a new mtime, so every run re-PUT the whole archive: 1.2M PutObject in 11 days on the bucket (~$10/month of Class A operations against a 1M/month free tier). The archive branch is now fetched with its whole history but no blobs (`--filter=blob:none`, ~7 MB of commits and trees today), and before the sync each file's mtime is set to the committer time of the last commit that changed it. Every upload happens after the commit whose content it carries, so a current object is newer than its file and is skipped, while a file changed after its object's last upload (this run's edits, or an earlier run's failed upload) is newer and is sent. `--delete`, the target prefix, the daily tarball, the schedule and the fail-red/notice behaviour are unchanged. A truncated history only over-uploads; an unreadable one falls back to the old full re-upload with a warning. Verified by running the checkout, commit/push and R2 steps verbatim against a throwaway git server and a fake aws applying aws-cli's size+mtime rule: incremental uploads, deletions, a same-size edit, catch-up after a failed upload, a no-change run and a depth=1 history all leave the mirror byte-identical to the archive. With the mtime restore ablated, the same harness sees every run re-upload all objects. Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The R2 step of
board-snapshot.ymlrunsaws s3 sync archive/board/ s3://$R2_BUCKET/objectstack/board/ --delete.aws s3 syncuploads a file when its size differs from the object's, or when the local mtime is newer than the object's LastModified (aws-cliSizeAndLastModifiedSync.compare_time). A fresh CI checkout gives every file a brand-new mtime, so every run re-PUTs all ~41,000 objects. Measured on theobjectstack-boardbucket: 1.2M PutObject in 11 days (~110k a day at 4 runs a day). That is ~$10 a month of R2 Class A operations, against a free tier of 1M a month.What changes (
.github/workflows/board-snapshot.ymlonly)git fetch --depth=1becomesgit fetch --filter=blob:none. The archive branch's whole history is fetched without file contents. The worktree checkout still downloads the tip's blobs in one batch, the same content the depth-1 fetch downloaded, so the history adds only commits and trees: ~7 MB for 127 commits over 41,047 files, measured today with a blobless fetch ofboard-archive.aws s3 sync, a short inline Node script sets each file's mtime to the committer time of the last commit on the archive branch that changed it. It uses onegit log --no-renames --name-only -z --format=%x01%ct HEAD -- boardpass over trees only (~0.2 s for today's history). The sync command itself is unchanged: same source, same--delete, same target.Why that is correct:
--delete.--size-onlywas rejected, as required: anupdated_atmove keeps a record's byte count.--depthever came back) only errs in the safe direction. The boundary commit looks like the creator of every older file, which stamps those files newer than their real change, so they are re-uploaded and no stale object is skipped.::warning::and in the run's R2 status line. It costs more Class A operations and never gives a weaker copy.Unchanged: the 4×/day schedule, the daily
snapshots/YYYY-MM-DD.tar.gz(its members now carry the restored mtimes), credentials read but never echoed, unset secrets as a notice, a configured upload that fails goes red, and the--deletetarget is never widened. Comments that the change made untrue were updated: the "every later run moves a handful" note was false before this PR and is true now. A new "Only changed records are uploaded" section documents the mechanism and its two assumptions:Validation
The R2 step cannot run in a PR (skipped on
pull_request). So I ran the checkout, commit/push and R2 steps verbatim, extracted from this workflow file, against a throwaway git server (uploadpack.allowFilter) and a fakeawsthat applies aws-cli's size+mtime rule to a JSON bucket and records content hashes. After every successful run it checks that the mirror equals the archive byte for byte:The push from the blobless clone landed every run's commit on the server. Ablation: with the mtime restore replaced by
false(one line), runs 1, 3 and 4 each upload all 60 objects and the harness reports 3 failures, so the test detects the old behaviour.actionlint1.7.12 with shellcheck 0.11.0 reports exactly the one finding untouchedmainalready has (SC2086 on the intentionally unquoted$SNAPSHOT_ARGSin the snapshot step). The new script is clean. The repo's workflow-scanning gates were run locally on this branch and all exit 0:check:pm-board-snapshot,check:pm-dispatch-gates,check:pm-expected-skips,check:node-version,check:pnpm-acquisition,check:workflow-step-name-quoting,check:workflow-status-functions,check:required-contexts,check:stall-guard-budget,check:shard-attestation,check:agent-test-spelling,check:pnpm-filter-targets,check-self-test-wired,check-step-collectors,check-whole-set-label-write,check-aggregator-rosterandcheck-ci-filter-parity.Expected effect
Per run: ~41,000 PutObject goes down to the records that changed since the last upload, plus the ~41 ListObjectsV2 pages sync already reads. The archive's last five commits touched 546 distinct files, so that is roughly a hundred per run, or ~13k a month against the 1M free tier. The first run after merge should log
mtimes from history: <n> file(s) stamped from <m> commit(s)and a smallsynced N object(s), which is the post-merge check.Notes
board-snapshot R2,s3 sync) and found nothing. Earlier R2-mirror fixes in this file: board-snapshot: the R2 mirror lands atobjectstack/board/board/…, one segment deeper than the workflow documents #18049, finding(ci):board-snapshot.ymltells adopters 「there is nothing to configure」 while hard-codingobjectstack/into a--deleteR2 sync — two repos on one bucket erase each other's boards #19536.skip-changeset.Clause-②: no
🤖 Generated with Claude Code