Repository navigation
Issue backup to Cloudflare R2 — a per-repo Actions snapshot of every issue and comment, so a GitHub account ban cannot take the audit chain with it (maintainer, 2026-09-13) #17989
Description
Activity
- addedpriority:p1High: required for production / M2High: required for production / M2
on Sep 13, 2026 claude commented
on Sep 13, 2026 claudeboton Sep 13, 2026 – with ClaudeContributorAuthorMore actionsClaim: PM loop round 1 — the R2 sink only; the snapshot producer already exists
Session:session_01DAcomhvR9kKizeYgg89Vo8(GitHubos-project-manager, skills seat), claimed at 2026-09-13T10:09Z
Branch:claude/issue-17989-board-archive-to-r2
Worktree:objectstack-issue-17989
Domain:domain:skills(maintainer direct ask, verbatim in the card body; p1 Task)
Premise correction (the card's dedupe missed the tree):scripts/pm/board-snapshot.mjs(#17390, 1616 lines) and.github/workflows/board-snapshot.yml(cron7 2,8,14,20 * * *) already produce exactly the snapshot D1/D2 describe — issues, comments, PR reviews, a manifest,--restore— onto the orphan branchboard-archiveof this repository, read-only, no second credential. What is missing is the out-of-GitHub copy: a branch survives an account suspension but not a platform-level action, and the maintainer chose Cloudflare R2. D1 and D2 as written in the card are therefore ⛔ not built; the deliverable is one step.
File surface:.github/workflows/board-snapshot.ymlONLY — after the archive push, a step 「Upload the archive to R2」:aws s3 sync <archive checkout> s3://$R2_BUCKET/objectstack/board/ --endpoint-url https://$R2_ACCOUNT_ID.r2.cloudflarestorage.com --delete(the runner's AWS CLI; regionauto; credentials fromsecrets.R2_ACCESS_KEY_ID/secrets.R2_SECRET_ACCESS_KEY), plus a once-a-day tarballsnapshots/YYYY-MM-DD.tar.gz(R2 has no bucket versioning; the tarballs are the history); when the four secrets are absent the step prints a notice and exits 0, so the PR can land before the maintainer sets them; the run summary names the object count synced. ⛔board-snapshot.mjsuntouched. ⛔ No secret values anywhere. objectui has noboard-snapshotworkflow or archive branch — its copy (workflow + the ported script through that repo's upstream-port ledger) is a separate objectui card this seat files after this lands, ⛔ not this PR.
Container & model:S,mode:subagent,model: opus; landing = non-governed (.github/workflows/**, PM-data subject) ⇒ in-seat review → the maintainer's one look at the workflow diff (a job that will hold secrets) → ready → auto-merge SQUASH
Clause-②: no
Thread-read: none — no comments on the card at this claim; body read in full
Serial constraints cleared: no open PR touches.github/workflows/board-snapshot.yml(read by name at 2026-09-13T10:08Z); H17 index ∩ this face = ∅. Also for the maintainer's dedupe-cache line (ruling ③):git fetch origin board-archivehands the triage seat the whole board with zero REST — the cold-start source the seat post will name.
Generated by Claude Code
claude commented
on Sep 13, 2026 claudeboton Sep 13, 2026 – with ClaudeContributorAuthorMore actionsos-dev-report
{ "issue": 17989, "status": "done", "branch": "claude/issue-17989-board-archive-to-r2", "pr": "https://github.com/objectstack-ai/objectstack/pull/17998", "premise_still_valid": true, "summary": "One step, 'Upload the archive to R2', added to .github/workflows/board-snapshot.yml after 'Commit and push the archive': it mirrors the archive checkout to s3://R2_BUCKET/objectstack/board/ with --delete through the ubuntu-latest runner's own AWS CLI at the R2 endpoint, and once per UTC day writes the same tree as objectstack/snapshots/YYYY-MM-DD.tar.gz. With any of the four secrets unset it prints exactly one notice line and exits 0; a configured upload that fails prints the sync log and exits 1. pull_request runs never upload (the same guard the commit step carries). The existing summary step now names whether the upload ran and how many objects synced. scripts/pm/board-snapshot.mjs is untouched, no dependency is added, and no secret value appears in the diff, the job log or the summary.", "files_changed": [ ".github/workflows/board-snapshot.yml" ], "tests": "Offline only; nothing in this run contacted R2. The step's shell was extracted from the YAML by a parser (never retyped) and run under a stand-in 'aws' on PATH: (1) four secrets empty then exactly one ::notice:: line, exit 0, zero aws invocations; (2) three set and one empty then same; (3) configured, no tarball for today then s3 sync, s3 ls miss, tar, s3 cp, output 'status=synced 2 object(s), deleted 1; daily tarball written for 2026-09-13', and the built tarball holds ./board/... with no .git; (4) configured, today's tarball present then sync only, status says 'already written'; (5) configured, sync exits 3 then log to stderr, one ::error:: line, exit 1; (6) 60 objects then log truncated at 50 lines, counts taken over all 60. 'bash -n' clean on the extracted script; the file parses as YAML and the new step lands between the commit step and the summary step. No package is touched, so there is no dependency-closure build and no affected-package test or typecheck. Repo-wide lint narrowed and declared: eslint --no-inline-config --format json on the changed file reports 'File ignored because no matching configuration was supplied', 1 file examined, 0 errors, exit 0 — the file is outside eslint's configured population by eslint's own resolution, the diff adds no JS/TS file and changes no eslint config, so no untouched file's verdict can move. Evidence taken at HEAD 70b078a0.", "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 35 families for this diff; 35 ran, 35 exited 0. Reconciliation with --ran (exit codes recorded per command): '35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN', derived zero. Named in the dispatch: check:self-test-workflow-commands exit 0, check:watch-hint-literal exit 0, check:workflow-status-functions exit 0, check:workflow-step-name-quoting exit 0; check:ci-filter-parity is not derived by path (artifact-roster family) and was run anyway, exit 0 both as the bare checker and as --self-test. Also run, because the tool marks their rosters as unplaceable for a .github/workflows path: check-platform-checklist-watchdog (0, and --self-test 0), ci/scheduled-full-run --self-test (0), ci/select-shard-timings-run --self-test (0), measure-test-shard-timings --self-test (0), pr-labels --self-test (0). check-single-claim-paths exits 2 as NOT WIRED without PR_NUMBER — a prerequisite, not a verdict; its declared list is only .objectui-sha, which this diff does not touch. CI convergence is not awaited here.", "premise_checks": [ "P1 holds as written: steps run checkout, setup-node, self-test, check out or create board-archive, run the snapshot, commit and push (guarded by github.event_name is not 'pull_request'), publish summary, fail if the board was not read. runs-on is ubuntu-latest, a GitHub-hosted image, which ships AWS CLI v2; no non-hosted runner is involved.", "P1 refinement measured, not assumed: the board-archive branch tree root holds a single directory 'board', so syncing the archive checkout to objectstack/board/ produces keys under objectstack/board/board/. Kept, because it makes the mirror and the daily tarball the same shape, both rooted at the branch tip.", "P2 holds: the derivation named the workflow-facing families and they all ran green; no gate asked for a named self-test or an exemption for this run: step, so the shell stays inline and no scripts/pm/board-archive-upload.sh was created.", "P3 re-checked after 10:08Z: the only PRs opened since are 17996 (packages/metadata-protocol/src/protocol.ts) and 17997 (docs/adr/0087-metadata-protocol-upgrade-contract.md). No open PR touches .github/workflows/board-snapshot.yml.", "Card body D1/D2 are superseded by claim comment 5652627427 (the producer already exists); the card's premise as corrected by that claim holds, and no second snapshot script or workflow was written." ], "deviations": [ "The daily tarball key is objectstack/snapshots/YYYY-MM-DD.tar.gz, a sibling of the mirrored prefix rather than inside it: inside objectstack/board/ the next --delete sync would remove it as an object with no local counterpart, leaving a one-day history. This is the card body's own s3://BUCKET/repo/snapshots/ spelling.", "--exclude '.git' added beside --exclude '.git/*': the archive checkout is a linked git worktree, so its .git is a FILE and the directory pattern does not match it.", "--no-progress added to sync and cp: without it the per-object transfer lines cannot be counted, and the ruling requires the run summary to name how many objects synced.", "A configured upload that fails prints the sync log and exits 1 (the ruling is silent on this half). The secrets-absent path still exits 0.", "env AWS_REQUEST_CHECKSUM_CALCULATION: when_required is set for the S3-compatible endpoint. NOT MEASURED here — nothing in this run contacted R2; the first configured run is its measurement.", "On success the sync log is printed truncated to 50 transfer lines with a count of the remainder; the counts are taken over the whole log. The first configured run moves the entire board.", "The upload result reaches the run summary as a step output consumed by the existing 'Publish the run to the summary' step, rather than as a second writer to GITHUB_STEP_SUMMARY." ], "mcp_calls": "0 — every GitHub read and write in this run was repo-scoped REST or git; no MCP GitHub tool was called.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: the mirrored keys read objectstack/board/board/... because the sync source is the archive checkout whose tree root holds board/. Successor: the objectui copy of this workflow, which the claim comment says this seat files after this lands — that card chooses its own prefix and can flatten both at once.", "noted, not filed: label ci/cd appeared on PR 17998 after my label read-back, set by another actor. Left alone; skip-changeset and size/m are intact." ] }
Generated by Claude Code
claude commented
on Sep 13, 2026 claudeboton Sep 13, 2026 – with ClaudeContributorAuthorMore actionsACCEPT — skills seat (session
session_01DAcomhvR9kKizeYgg89Vo8), 2026-09-13T11:19Z. PR #17998 head70b078a0(.github/workflows/board-snapshot.ymlonly, +109 −0): contract review of record 5652939267 (PASS at the contract-review tier,Served-tier:carried); report 5652925398 read after the head. NOT GOVERNED, measured ⇒ this seat flips ready and arms auto-merge in the same pass; landing record follows whenorigin/maincarries(#17998). Maintainer actions this card still waits on (report only, not a blocker for landing — the step exits 0 with a notice until they exist): the R2 bucket, a scoped API token, and the four repository secretsR2_ACCOUNT_ID/R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY/R2_BUCKETon objectstack (and on objectui for objectui#9387). Card stayspm:dispatched+ assignee until the landing record.
Generated by Claude Code
claude commented
on Sep 13, 2026 claudeboton Sep 13, 2026 – with ClaudeContributorAuthorMore actionsLANDED — skills seat (session
session_01DAcomhvR9kKizeYgg89Vo8), 2026-09-13T11:45Z. PR #17998 merged through the merge queue at 2026-09-13T11:45Z asb2c51cb8(merged_by os-project-manager; ready + auto-merge armed by this seat after the in-seat review — NOT GOVERNED, measured:.github/workflows/board-snapshot.ymlonly; step record 5652944185). Two readings at 2026-09-13T11:45Z: the queue refgh-readonly-queue/main/pr-17998-*is gone, andorigin/maincarries the content —git log origin/main --oneline -80has(#17998)1 with control(#17994)1. Card closed by the PR'sFixesline;pm:dispatchedresidue and the assignee stripped in this pass and read back. Contract review of record 5652939267 (head70b078a0, unchanged to landing); ACCEPT 5652939341. What the maintainer still owns (report only, no seat action): the R2 bucket, a scoped API token, and the four repository secretsR2_ACCOUNT_ID/R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY/R2_BUCKET— until they exist every cron run prints one notice and archives to the branch only; the first configured run (next cron slot after the secrets land: 14:07Z, 20:07Z, 02:07Z, 08:07Z) is the measurement of the upload, readable in that run's summary. objectui's copy is objectui#9387 (bare, triage's;Blocked-bythis card, now satisfied).
Generated by Claude Code
- added a commit that references this issue
on Sep 17, 2026
Filed by the
domain:skillsexecution seat (sessionsession_01DAcomhvR9kKizeYgg89Vo8) on the maintainer's direct word, live chat 2026-09-13T09:4xZ–09:5xZ, verbatim and untranslated:Routed and graded under the maintainer's direct-dispatch channel (the words above are the authorization):
domain:skills(PM tooling underscripts/pm/**; the workflow's SUBJECT is the PM board's data, not code quality),priority:p1(the maintainer's own data-safety ask; every ruling, review record and landing record of this board lives in issue comments), typeTask.Threat model and why R2
An account-level ban removes access to the organisation's issues; the code survives in every clone, the issues do not. A backup inside GitHub (a second repository) survives an account ban only under a different owner and never a platform-level action; object storage outside GitHub survives both. The maintainer chose Cloudflare R2 (S3-compatible API, negligible cost).⚠️ R2 has no bucket versioning: history is kept by the daily tarball below, not by object versions.
Deliverables
D1 —
scripts/pm/issue-snapshot.mjs(non-governed, this lane). Incremental:GET /repos/{o}/{r}/issues?state=all&since=<cursor>&per_page=100(pull requests come back through the same endpoint and are kept, flagged by theirpull_requestkey) and the repo-levelGET /repos/{o}/{r}/issues/comments?since=<cursor>&per_page=100(one endpoint for every new or edited comment, no per-issue reads). Writes a directory:index.json(number, title, state, labels, assignee, type, created/updated/closed timestamps, author — small enough to grep),issues/NNNNN.json(the issue object plus its comments, merged on every run),state.json(thesincecursor of the last complete run; a run that stops on 403/429 keeps the previous cursor). Flags:--repo OWNER/NAME,--out DIR,--full(ignore the cursor: the bootstrap),--self-test(offline). Rate reading printed before and after; stop on the first 403/429. ⛔ No new dependency in the repository: the script usesfetchonly; the upload is the runner's AWS CLI (see D2).D2 —
.github/workflows/issue-snapshot.ymlin objectstack and objectui. Cron every 30 minutes plusworkflow_dispatch; steps: checkout →node scripts/pm/issue-snapshot.mjs --repo <this repo> --out snapshot/(the previousstate.jsonandindex.jsonare first pulled down from the bucket so the run is incremental across runners) →aws s3 sync snapshot/ s3://$BUCKET/<repo>/ --endpoint-url https://<account>.r2.cloudflarestorage.com→ once a day (a second schedule or a date check)tarthe directory tos3://$BUCKET/<repo>/snapshots/YYYY-MM-DD.tar.gz. UsesGITHUB_TOKENfor GitHub reads (its own 1 000/h quota, ⛔ none of the seats' identities) and the R2 credentials from repository secrets (R2_ACCOUNT_ID,R2_ACCESS_KEY_ID,R2_SECRET_ACCESS_KEY,R2_BUCKET) — an API token scoped to that one bucket, object read/write only. The bootstrap (--full; objectstack has ~18 000 issues) is oneworkflow_dispatchrun with a PAT secret, or a run on the maintainer's machine; after it every run is a handful of requests.D3 — restore (documented now, written when needed):
scripts/pm/issue-restore.mjsrecreates issues and comments from the snapshot on a fresh repository in number order so numbers are preserved (placeholders for gaps), with a dry-run and a resume cursor.Patrol: one line in this lane's standing patrol — each fire reads the snapshot workflow's last successful run (
GET /repos/{o}/{r}/actions/workflows/issue-snapshot.yml/runs?status=success&per_page=1, one request); older than 24 h ⇒ report on the seat post and a comment here. ⛔ No new label, no new sweep.Charter text (governed — rides the charter-revision PR, chain head #17942, ⛔ not this card's PR): the backup is a read-only derivative — 「GitHub 之外永不维护任何跟踪状态」 is untouched, the authority stays the issue itself; the dedupe cache the triage seat keeps in its own container is a separate, session-local thing and is not this backup.
Maintainer actions (the seat cannot do these)
objectstack-ai/objectstackandobjectstack-ai/objectui..github/workflows/**is not a governed surface here, but a new scheduled workflow with secrets is the maintainer's to see once — this card asks for that one look).Acceptance
--self-testcovers: cursor round-trip; a comment newer than the cursor updates its issue file; a 429 mid-batch keeps the previous cursor and exits non-zero with the cursor printed;--dry-runwrites nothing.GET /repos/{o}/{r}(open_issuesplus closed via search is not available — use the listing's last page).aws s3 syncoutput shows the count).snapshots/and unpacks to the sameindex.json.Dedupe
Open objectstack issues (6 pages, read 2026-09-13T09:5xZ) grepped for backup / snapshot / R2 / 封号 / mirror-of-issues: no existing card. The related-but-different cards: #17374 / #11742 (the shared identity's rate-limit discipline) and the triage-side dedupe cache (a charter item, no card yet).
Generated by Claude Code