chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
Stable repository keys
A git repository is a dev.pdsjs.git.repo record. Its record key was the
repository's name, so the name was the identity and renaming one changed its
AT-URI. Seven things hang off that URI, and a rename broke all of them:
statement subjects on every review, pull request and issue; a fork's
upstream; check subjects and the Constellation reverse index; a
collaborator's copy, read at the same key in their own repo; the derived keys
of pull requests, check requests and latest-check pointers; and the clone
remotes.
The key is now minted at creation and name is an ordinary field. The URI
never contains the name, so everything above survives a rename, and
dev.pdsjs.git.renameRepo writes one field.
The rule: the name is an address, the key is an identity. Every public
surface still takes a name. resolveRepo in hosting.js is the one place
that turns one into the other, and everything below it works in keys.
What did not change
A repository in a space keeps its name as its key. The space is what makes it private and what an address resolves to; it is keyed by the repository's name, and a space key cannot move. Making the record inside it a minted key would buy nothing (nothing outside a space names one of its records) and would cost every reader a listing behind a credential. Renaming a private repository is refused, and says why.
Running the migration
scripts/migrate-git-repo-keys.mjs, in two passes, because statements about a
repository live in their authors' accounts rather than the repository's.
# 1. Move the records, on each account that holds repositories.
PDS_PASSWORD=… node scripts/migrate-git-repo-keys.mjs keys \
--service https://pds.example.com --out keys.json --dry-run
PDS_PASSWORD=… node scripts/migrate-git-repo-keys.mjs keys \
--service https://pds.example.com --out keys.json
# 2. Re-point what named the old address, on every account that has spoken
# about those repositories, including each runner's.
PDS_PASSWORD=… node scripts/migrate-git-repo-keys.mjs remap \
--service https://pds.example.com --map keys.json
Order matters inside a move, and the script does it: the config record moves to the new key in its own commit, before the repository record. The write guard reads a repository's config for the branches it protects and refuses to delete a record holding one; with the config already moved, the delete of the old repository record reads no config and stands. Without that ordering, every repository with a protected branch would refuse to migrate.
The blobs survive. record_blobs is many-to-many and unlinkBlobsFromRecord
is scoped to one URI, so writing the same value at a new key links the same
blobs again and deleting the old key drops only its own rows. The orphan
reaper cross-checks record content besides.
Deploy order
latestCheckKey and checkRequestKey are duplicated between @pdsjs/git-ci
and hosting.js and must agree, so the runner image is rebuilt and deployed
with the servers. Deploy first, migrate second: the resolver finds an
unmigrated repository unchanged, because its key still equals its name, so
there is no window where a repository is unreachable.
Run against chadtmiller.com, pdsjs.dev and grain.social.
Duplicates on a derived key
A pull request, a check request and a latest-check pointer are keyed by the
repository and the ref, so several records for one branch collapse onto one
key. remap keeps the newest by createdAt, which is the word a reader
already takes; the rest are dropped, and the run says how many.
Run --dry-run first and read that count. A migration that reports duplicates
is discarding superseded records, which is usually right and is worth seeing
before it happens.
What a migration cannot reach
A collaborator's copy and their pull request records live on their own server.
The remap pass covers any account whose password you hold; anything else
keeps naming the old address and reads as absent. For the deployments here
that is everything, because the same operator holds them all.