~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
85 lines · 4.0 KB Raw History

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.