Repository navigation
feat(analytics): restore from an S3-compatible bucket's dumps - #214
Merged
Merged
Conversation
The analytics workflow can now take its dump from an S3-compatible bucket instead of Heroku PGBackups, switched by the repository variable BACKUP_SOURCE: unset or "heroku" does exactly what it did, "s3" takes the bucket's dump. Switching is setting the variable; unsetting it switches back. scripts/download_s3_dump.py lists keys named <prefix>-YYYYMMDDTHHMMSSZ.dump, which sort by time, and takes the newest whose `pg_restore --list` shows TABLE DATA entries, so a dump of a database with no tables yet (a new, empty volume) is skipped for an older one. It gives up after 48 such dumps, and stops at once on a dump pg_restore can't read. A manual run's new dump_key input names a dump instead; backup_id stays the Heroku source's, and each source refuses the other's input. The connection comes from four secrets (BACKUP_S3_ENDPOINT, BACKUP_S3_BUCKET, BACKUP_S3_ACCESS_KEY_ID, BACKUP_S3_SECRET_ACCESS_KEY), with optional variables for the key prefix and region. Nothing reads them while the variable is unset, so this is safe to merge before they exist. The metadata artifact names the dump, its size, time and archive header, never the endpoint or bucket. The service and client move to Postgres 18: pg_restore reads its own major version and older ones, so it takes Heroku's 17 dumps too. The published sanitized dump now comes from pg_dump 18. local_sanitize_backup.sh defaults to postgres:18-alpine to match. scripts/test_download_s3_dump.sh tests the S3 path against a local S3 server (versitygw) with dumps from Postgres 18, restoring with the workflow's flags, and restores a Postgres 17 dump with pg_restore 18. It runs in CI as the backup-download job, without credentials. The workflow's Python pins move to scripts/requirements-analytics.txt, which that job reads boto3's from. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QoXLf4KSfqzMFrdZGQG9Up
- Run the installed Postgres 18 client, not the runner's own. The runner image has an older Postgres cluster, and Debian's pg_wrapper picks its version for a command that names no host, so the script's `pg_restore --list` would have refused every newer dump. scripts/install_postgres_client.sh installs the client and puts its bin directory first on PATH. The workflow and the CI job both use it. The CI job now runs the test against that pg_restore (REQUIRE_HOST_PG_RESTORE=1) instead of a container's, and the script logs which pg_restore it runs. - Fail the restore step when the restored database holds no pull requests. A dump of a freshly migrated database lists TABLE DATA for its empty tables, and would otherwise publish an empty snapshot. - Anchor the TABLE DATA match to the TOC line's shape, so an object named like one can't pass for table data. - Publish only the archive's format and versions from its header, not when or from which database it was made. - A missing secret fails the first step; an endpoint without a scheme and boto3's own errors end in one line rather than a traceback, and never repeat the endpoint. - Test: restores now fail the checks when pg_restore reports errors (they passed before); one dump is over boto3's multipart threshold; the empty database holds a sequence named like a TABLE DATA entry; unset variables are passed empty, as the workflow passes them; the bucket and credentials are distinctive, so the leak check finds bare values. - Example key times and wording no longer suggest a dump schedule. Doc 016: the restore guard, the client's PATH, and restoring the sanitized dump with pg_restore 18. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QoXLf4KSfqzMFrdZGQG9Up
bryangingechen
marked this pull request as ready for review
October 11, 2026 22:09
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.
Motivation
The analytics workflow (
upload_backup.yaml) can only read Heroku PGBackups. This lets it read the database'spg_dumparchives from an S3-compatible bucket instead. Switching sources is a single repository variable, and with the variable unset the workflow does what it does today.What changes
BACKUP_SOURCErepository variable. Unset orheroku: the Heroku path, unchanged.s3: the bucket. Any other value fails the run's first step.scripts/download_s3_dump.pylists keys named<prefix>-YYYYMMDDTHHMMSSZ.dump, which sort by time, and ignores any other key.pg_restore --listshows TABLE DATA entries, matched on the TOC line's shape. A dump of a database with no tables yet is skipped for an older one.pg_restorecan't read.pg_restoreit runs.dump_keyinput names a dump for the S3 source;backup_idstays the Heroku source's. Each source refuses the other's input.BACKUP_S3_ENDPOINT,BACKUP_S3_BUCKET,BACKUP_S3_ACCESS_KEY_ID,BACKUP_S3_SECRET_ACCESS_KEY. Read-only credentials suffice. With the S3 source, a missing one fails the first step.BACKUP_S3_KEY_PREFIX(defaultqueueboard) andBACKUP_S3_REGION(defaultus-east-1).backup-metadataartifact names the dump, its size, its upload time and the archive's format and versions. It never names the endpoint or the bucket, nor when or from which database the archive was made.pg_restorereads its own major version and older ones, so it still takes Heroku's 17 dumps. The publishedsanitized-pg-dumpis now made by pg_dump 18; restore it with pg_restore 18.scripts/local_sanitize_backup.shdefaults topostgres:18-alpine.scripts/install_postgres_client.shinstalls the client and puts itsbindirectory first onPATH. The runner image has its own Postgres 16 cluster, and Debian's pg_wrapper picks that version for a command that names no host. So without the pin,pg_restore --listruns version 16, which refuses newer dumps (unsupported version (1.16) in file header). The workflow and the CI job both use the script.scripts/requirements-analytics.txt(addingboto3). The Heroku CLI is installed only for the Heroku source. Design doc 016 gets the operational notes.Safe to merge before the bucket exists
With
BACKUP_SOURCEunset, nothing reads the new secrets (beyond the first step testing them for emptiness, which it does only for the S3 source). The S3 step is skipped. The other differences on the Heroku path are Postgres 18, the client install script, the extraboto3install, and the empty-database guard.Switching
BACKUP_SOURCE=s3. Unsetting it switches back.Testing
scripts/test_download_s3_dump.sh, CI jobbackup-download. It runs versitygw as a local S3 server, with Postgres 18 and 17 containers, and needs no credentials.pg_restoreas the workflow sets it up (REQUIRE_HOST_PG_RESTORE=1). Locally, without Postgres 18, it falls back to the container's.--max-candidates, and at an unreadable dump;pg_stat_statements, as Heroku's has) using pg_restore 18.pg_restorereports errors.pg_restore16.15 through pg_wrapper with no host, and 18.6 installed. The test then ran against 18.6, and all eleven checks passed.ba809eab):postgresql-client-18and thepostgres:18-alpineservice restored the day's Heroku backup (a Postgres 17 database, 172.9 MB), with data for all 54 tables and no errors.24bc7d6b): the same, with the install script pinning 18.6 over the runner's 16.15, and the guard passing (44,079 pull requests restored). Sanitize, export and uploads passed in both.actionlinton both workflows reports only the existingfor iloop in the restore step.shellcheckis clean on the new scripts.ruff checkandruff format --checkpass.24bc7d6b. Not changed here: the existing pandas/pyarrow pins and readiness loop inlocal_sanitize_backup.sh.🤖 Generated with Claude Code
https://claude.ai/code/session_01QoXLf4KSfqzMFrdZGQG9Up