Files
horizon/docs/api/owner-update-v2.md
T
yahya-nusseibeh 7aceb1cbf9 Mod registry: decide project ownership by numeric GitHub ID
Owners were GitHub logins, and a login can be claimed by someone else once
its owner renames or deletes the account. For a project without a key, a
reclaimed login could publish an owner update that merges on its own.

- Entry gains `owner_ids` (#[serde(default)], omitted when empty). The
  form records the lister's ID for new projects (file and repository
  listings). The registry review notes any change to it.
- ownership(): when an entry has owner_ids, only an ID in them is an
  owner, so a matching login on another account is refused. Without
  owner_ids (older entries) the login matches, but the update always waits
  for a reviewer and the pull request says so.
- The automatic owner update requires an ID match.
- update_trailer is v2: `owner-update-v2` message with the submitter's
  numeric ID, and `format: v2` / `submitter_id:` lines in the trailer.
- tools/registry_backfill_owner_ids.py: one-off, dry run by default,
  resolves owners' logins to IDs, cross-checks them against the IDs the
  form recorded in the entry's commits, and leaves alone (and prints)
  any entry it can't resolve completely.
- docs/api/owner-update-v2.md: what the registry repository's
  owner-update workflow must check, and the deployment order.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stH5q6PgMAVUSmJqKDe
(cherry picked from commit 526bfcbbe1)
2026-11-05 06:59:35 +02:00

3.3 KiB

What the registry's owner-update workflow must check (trailer v2)

The registry repository (horizon-rust/horizon-mods) merges an owner's update on its own when the pull request carries an owner-update trailer that the submission service (crates/horizon-mod-registry, update_trailer) signed with REGISTRY_UPDATE_KEY. Its workflow and scripts/owner_update.py live in that repository, not here. Since owner IDs, the trailer is v2, and the workflow must check all of the following before it merges. Any check that fails leaves the pull request to a reviewer (remove the label, comment why); it never merges partly.

The trailer

The last HTML comment of the pull request body:

<!-- owner-update
format: v2
id: <project id>
version: <version>
sha256: <hex>
file: <id>-<version>.<extension>
submitter: <login>
submitter_id: <numeric GitHub ID>
hmac: <hex>
-->

hmac is HMAC-SHA256 with REGISTRY_UPDATE_KEY, lowercase hex, over exactly these bytes (\n between fields, no trailing newline):

owner-update-v2\n<id>\n<version>\n<sha256>\n<file>\n<submitter>\n<submitter_id>

The checks

  1. Format. format: v2 is present and every field above is there once. Refuse v1 trailers (no format line) once the service is deployed: a v1 trailer names the submitter by login only.
  2. HMAC. Recompute it over the v2 message and compare in constant time (hmac.compare_digest).
  3. The submitter is the pull request's. submitter_id equals the numeric ID in the pull request head commit's Co-authored-by: <login> <<id>+<login>@users.noreply.github.com> line, which the service writes. Compare the IDs, not the logins.
  4. The submitter owns the project, by ID. Read mods/<id>.toml on main (not the pull request's copy): submitter_id must be in its owner_ids. An entry without owner_ids never merges on its own (the service doesn't sign those, so a trailer on one is forged or stale).
  5. Nothing else changes. The pull request touches only mods/<id>.toml. Its new entry equals main's except for one added [[versions]] entry, first, whose version, sha256 and file are the trailer's. owners, owner_ids, key, kind, repo and the page fields are identical (the service already only signs such updates; check again, since the pull request branch can be pushed to after it opened).
  6. The file. staging holds <sha256>.<extension> and its SHA-256 is sha256; the usual CI checks (cargo horizon-mod registry check, the trial run, a signature when the entry has a key) pass at the head that is merged.
  7. The head merged is the head checked. Merge with the head SHA the checks ran on (gh pr merge --match-head-commit <sha>), so a push between check and merge can't slip in.

Order of deployment

  1. In the registry repository: update scripts/owner_update.py and the workflow to the checks above, accepting v2 only; deploy the CI's cargo horizon-mod from a Horizon commit that knows owner_ids (older ones refuse the field).
  2. Run tools/registry_backfill_owner_ids.py <checkout> (dry run, then --write) and merge its pull request by hand.
  3. Deploy the submission service with this change. Until step 1 is live, its v2 trailers fail verification and the updates simply wait for a reviewer.