diff --git a/docs/backup-and-recovery.md b/docs/backup-and-recovery.md index 99c07a8d21..5924c3eef7 100644 --- a/docs/backup-and-recovery.md +++ b/docs/backup-and-recovery.md @@ -32,8 +32,8 @@ It records, in order: 1. **Backup configuration as observed** — the source branch (cluster size, region, replica count) and the recent backups with state, size, completion and expiry, from which the actual cadence and retention are derived. This is the "backup settings" half of the evidence. -2. **The restore** — `pscale branch create restore-test- --restore `, then - waits for the branch to be ready. The source branch is only ever read (`branch show`, +2. **The restore** — `pscale branch create restore-test- --restore + --cluster-size PS_DEV`, then waits for the branch to be ready. The source branch is only ever read (`branch show`, `backup list`); the restore lands in a new branch on its own cluster. 3. **Verification** on the restored branch, each a pass/fail row in the report: - the Drizzle migrations journal is present; @@ -69,10 +69,18 @@ login` session. Takes 10–20 minutes, most of it PlanetScale provisioning the r - **Failure path without a restore:** `RESTORE_TEST_SOURCE_BRANCH=does-not-exist` fails at `branch show`, before anything is created. - **In CI:** the workflow only runs on its schedule or `gh workflow run backup-restore-test.yml` - (after merge — a PR's CI does not run it). The first dispatch validates the service token: it - needs `read_backups`, `restore_production_branch_backups` and branch create/delete on the - database; a token missing the restore permission fails at `branch create` with nothing to - clean up. + (after merge — a PR's CI does not run it). The first dispatch validates the service token. On + the database it needs `read_branch`, `create_branch`, `delete_branch`, `connect_branch`, + `delete_branch_password`, `read_backups` and `restore_production_branch_backup` — the + development-branch accesses only, never `delete_production_branch`. A token missing + `read_backups` fails at `backup list`, one missing the restore access fails at `branch +create`, both before anything is created; one missing the delete accesses restores fine and + then leaves the branch behind, which the drill reports as a failed check. Only the token's + creator or an organization administrator can change its accesses. +- **Why the restore is `--cluster-size PS_DEV`:** a restore without an explicit size becomes a + PS-10 branch flagged `production: true`, which only `delete_production_branch` can remove — + and PlanetScale cannot demote a Postgres restore branch afterwards. A PS_DEV restore is a + development branch, so the CI token never needs an access that could reach `main`. ### Schedule diff --git a/packages/db/scripts/restore-test.ts b/packages/db/scripts/restore-test.ts index 709dae9f0e..f3d7e8d3cc 100644 --- a/packages/db/scripts/restore-test.ts +++ b/packages/db/scripts/restore-test.ts @@ -12,8 +12,8 @@ * 1. Records the backup configuration as PlanetScale reports it: the source * branch (cluster, region, replicas) and its recent backups, from which * the observed cadence and retention are derived. - * 2. Restores the newest successful backup into a fresh, throwaway branch - * (`restore-test-`) with `pscale branch create --restore`. + * 2. Restores the newest successful backup into a fresh, throwaway PS_DEV + * branch (`restore-test-`) with `pscale branch create --restore`. * 3. Connects to the restored branch and verifies the data is usable: the * migrations journal is present, every critical table exists and holds * rows, the newest record sits inside the backup's window (so this is the @@ -504,7 +504,22 @@ const main = async (): Promise => { } }) - const create = runPscale(["branch", "create", database, branch, "--restore", latest.id, "--wait"]) + // PS_DEV is not only the cheapest size: a restore without an explicit size + // lands as a PS-10 branch flagged `production: true`, which the CI token can + // only delete with production-delete accesses — the same accesses that could + // delete `main`. A PS_DEV restore is a development branch, so `delete_branch` + // and `delete_branch_password` suffice and the token never holds more. + const create = runPscale([ + "branch", + "create", + database, + branch, + "--restore", + latest.id, + "--cluster-size", + "PS_DEV", + "--wait", + ]) cleanupOwed = true if (create.exitCode !== 0 && !/timed out/i.test(`${create.stdout}\n${create.stderr}`)) { fail(`Could not create ${branch} from backup ${latest.id}`)