Maintain the registry¶
This page is the registry keeper's guide: listing a reviewed tool, building and signing a release, and getting a
tag into an APRScaching release. It is for the project registry's maintainer. At the end a signed tag is on main,
published as a GitHub Release, and bundled into the app.
How a release flows¶
dev is the working branch: every change reaches it by a squash-merged pull request, and a changed tool stays
unsigned there until the next release. main holds only fully signed states, is the default branch the address
github:apachler/aprscaching-tools reads, and changes only by pull request.
A release branch is cut from dev, signed, and merged into main as a merge commit. dev then catches up with
main: a fast-forward push when nothing has landed on dev since the release branch was cut, which is the one push
to dev outside a pull request, or a pull request from main merged as a merge commit when something has.
Rulesets hold both branches to this. main takes a pull request only as a merge commit, dev as a squash or a
merge commit, and both only with the check and verify checks passing. Repository admins bypass the dev
ruleset, which is what lets the fast-forward push through.
flowchart LR
F["Feature PRs<br/>squash-merged into dev<br/>(unsigned)"] --> R["release/vX.Y.Z from dev:<br/>sign-all, CHANGELOG"]
R --> M["PR into main,<br/>merge commit"]
M --> T["Tag vX.Y.Z on main:<br/>GitHub Release"]
T --> D["dev catches up:<br/>fast-forward to main"]
T --> A["APRScaching PR:<br/>bundle-registry.mjs vX.Y.Z"]
The keys¶
The private values never enter this repository, CI, a log or the shell history. Keep each one in a file of its own
on the computer that signs, holding the value node scripts/genkey.mjs --raw printed, and pass it to one command as
"$(cat <file>)". The files live in the key folder, outside every repository, so no git add can pick them up:
TOOL_KEYS_DIRwhen it is set;- otherwise
$XDG_CONFIG_HOME/aprscaching-tools/keys, which is~/.config/aprscaching-tools/keyswhenXDG_CONFIG_HOMEis unset or not an absolute path.
TOOL_AUTHOR_KEY_FILE and TOOL_AUTHORITY_KEY_FILE point at one file elsewhere. The folder has mode 700 and each
file mode 600:
mkdir -p ~/.config/aprscaching-tools/keys && chmod 700 ~/.config/aprscaching-tools/keys
chmod 600 ~/.config/aprscaching-tools/keys/*.key
| Key | File | Signs |
|---|---|---|
| The project's author key (OE8APR) | oe8apr-tool-author.key |
The project's own tools |
| The authority key | registry-authority.key |
registry.json |
The authority's public key is authority.pub:
Back up the keys¶
A key that is lost cannot be recovered, and the registry depends on both. Without the author key, every project tool
is signed again under a new key, and players who installed one see Author key CHANGED. Without the authority key,
nothing can sign registry.json, and every instance and player that pinned the key has to confirm a new one. Keep
an encrypted copy of the key folder offline, in a place apart from the computer that signs:
-
Write the encrypted archive straight to the removable medium, never into a git working tree or a folder a cloud service syncs.
gpgasks for a passphrase; keep it apart from the medium, such as in a password manager: -
Keep two copies, on two media in two places, and make a new one when a key changes.
-
Check a copy once a year, and after you make it: restore it into an empty key folder and run the doctor, which compares the authority key with
authority.puband the author key with the registry:
To restore for good, unpack into ~/.config/aprscaching-tools instead, then chmod 700 the folder and chmod 600
each file. When no copy is left, the steps for a changed key apply (Keys, rotation and release
practice).
Add or update an entry¶
- Review the pull request against what the review checks. Confirm the author key through the channel the author named.
-
Add the entry to
registry.jsonunderentries, in the order the Registry list shows them:{ "name": "station-log", "title": "Station log", "author": "OE8APR", "version": "1.0.1", "pubkey": "<the author's public key, as in tool.json>", "entry": "tools/station-log/tool.json", "description": "Lists the stations the Station DB tool hears, with /seen and /whois commands." }entryis relative toregistry.json, with no leading/, so it works bundled with an instance and underraw.githubusercontent.com/apachler/aprscaching-tools/<tag>/. A tool hosted by its author takes its absolutehttps://address. Keepname,title,authorandversionequal to the manifest. -
The entry is signed with the next release; on
devit shows as unsigned. The signature covers the registry'sformatandentries, their order included, so every change to an entry needs a new signature.
To remove a tool, delete its entry. To change an author's key, update the entry's pubkey once the author signs
with the new key.
Release a tag¶
Tags follow the APRScaching major: v1.<minor>.<patch> is a registry for APRScaching 1.x.
Check the computer¶
The doctor checks Node, pnpm through corepack, gh auth status, the origin remote, dev and main, the libraries
in vendor/, that the lockfile installs, and the key files: in a folder outside every git working tree (a failure
otherwise) with mode 700 (a warning otherwise), present, mode 600, readable, the authority key the one
authority.pub pins and the author key the one the registry lists for OE8APR. It prints each public key and its
fingerprint, never a private value, and one pass, warn or FAIL line per check.
The key files are read from the key folder (The keys); the check step of the release script refuses
them in the same cases.
Release with the script¶
Each pull request adds its own line under ## [Unreleased] in CHANGELOG.md. Before a release, check that section
against the pull requests merged since the last tag and fill any gap, then:
node scripts/release.mjs --dry-run # the next version and where the release stands; changes nothing but a git fetch
node scripts/release.mjs # every step that is not done, asking before each change
node scripts/release.mjs 1.4.0 # the same, for a version you name
Without a version, the script chooses it. A release branch already in progress keeps its version, so a rerun
finishes the release it started; --from vX.Y.Z makes a patch on that tag. Otherwise the commits on origin/dev
since the newest tag decide, by their Conventional Commit type: a breaking change (feat!: or a BREAKING CHANGE:
footer) makes the next major, a feat the next minor, anything else the next patch. A run with nothing new since
the newest tag says nothing to release.
| Step | What it does |
|---|---|
check |
A clean tree, gh signed in, a new X.Y.Z version, a non-empty Unreleased section, the key files and their folder outside every working tree, a registry entry for every tool folder |
prepare |
release/vX.Y.Z from origin/dev, or from --from <ref>; pnpm install --frozen-lockfile --ignore-scripts, fetch-libs (from the local clone when there is one, else from GitHub), build --check |
sign |
sign-all with the two key files, ending in verify --strict |
changelog |
Dates Unreleased as ## [X.Y.Z] - <date> under a new empty Unreleased, and sets package.json's version |
pr-main |
Commits with a sign-off, pushes, opens the PR into main, watches the strict checks, merges it as a merge commit |
tag |
Tags origin/main, pushes the tag, watches the Release workflow |
sync-dev |
Fast-forwards dev to main; when dev has moved on, opens a PR from main into dev and merges it as a merge commit |
handover |
Prints the APRScaching command that bundles the tag |
Each step checks its precondition, says what it will do and asks y/N before it changes git or GitHub. A no stops the
release where it is. A rerun finds the steps already done (the release branch, a strict verify, the dated section on
origin/main and on origin/dev, the tag) and continues from the first one that is not. --step <name> runs one
step; --yes answers that step's questions, for a step already confirmed. --source <dir> names the APRScaching
clone fetch-libs reads; it defaults to APRSCACHING_SOURCE, then ~/Development/github/aprscaching, and without a
clone there the libraries come from GitHub. Without a terminal every question is answered no, and the run exits 1.
The script reads the key files in the sign step only, and passes their values to sign-all in that one process's
environment. Every line it prints passes through a filter that removes them.
In Claude Code, /release (or /release 1.4.0) runs the whole release: asking for it is the go-ahead for every step, signing and
the merge into main included. It checks the Unreleased entry against the pull requests merged since the last tag,
runs the sign step, which reads the key files itself, and merges into main only once the pull request's checks
have passed. It stops for you only when a check or step fails, when it had to write a CHANGELOG line for a pull
request that brought none, or when something turns up you would want to see (.claude/skills/release/SKILL.md).
Hotfix¶
A fix that cannot wait for what is on dev starts from the tag it fixes. --from names it:
prepare cuts release/v1.3.1 from v1.3.0; write the fix and its Unreleased entry there, then go on as for any
release. sync-dev finds that dev has moved on and brings main in through a pull request. When CHANGELOG.md
conflicts there, resolve it on a branch from dev: merge origin/main into it with a merge commit, keep both the
dated hotfix section and dev's Unreleased lines, and open that as the pull request into dev.
Release by hand¶
The script runs these steps; when it cannot, run them yourself. The steps for v1.1.0:
-
Cut the release branch from
dev: -
Fetch the libraries from a local APRScaching clone that holds the
lib.lockcommit, so signing needs no network (git -C ~/Development/github/aprscaching fetch originfirst if the clone lacks it): -
Sign everything in one step:
AUTHOR_KEY="$(cat ~/.config/aprscaching-tools/keys/oe8apr-tool-author.key)" \ AUTHORITY_KEY="$(cat ~/.config/aprscaching-tools/keys/registry-authority.key)" \ node scripts/sign-all.mjssign-allchecks that everytool.jsmatches its sources; signs each listedtool.jsonwith the author key, including itsapiand the script'sentrySha256; copies each manifest's key, title, author, version and description into its registry entry; signs the registry'sformatandentrieswith the authority key; and runsverify.mjs --strict. A manifest signed by another author's key is left alone, andsign-allstops when that signature does not hold. The authority key must be the one inauthority.pub. -
In
CHANGELOG.md, rename## [Unreleased]to## [1.1.0] - <date>and open a new emptyUnreleasedsection, and setversioninpackage.jsonto1.1.0. -
Commit, push and open the pull request into
main; once its strict checks pass, merge it as a merge commit: -
Tag
mainand push the tag: -
Bring
devup tomain. Git refuses the push unless it is a fast-forward; then open a pull request frommainintodevinstead, and merge it as a merge commit:
The tag becomes a GitHub Release, with the CHANGELOG section as its notes (.github/workflows/release.yml, which
checks that the tag is on main and runs verify.mjs --strict). A tag never moves: a fix is a new tag. A ruleset
refuses to delete or move a v* tag, and releases are immutable, so a published release and its files stay as they
were published. Instances that follow github:apachler/aprscaching-tools@<tag> read the files at that tag.
The release files¶
Each release carries aprscaching-tools-vX.Y.Z.tar.gz (the registry, authority.pub, the licence and the tools at
the tag), its SHA-256, and a build-provenance attestation (.intoto.jsonl) that names the workflow run which made
the archive. Check a download with:
sha256sum -c aprscaching-tools-vX.Y.Z.tar.gz.sha256
gh attestation verify aprscaching-tools-vX.Y.Z.tar.gz -R apachler/aprscaching-tools
The archive is a convenience: instances read the files at the tag, and the registry and manifest signatures are what they check.
Verify¶
node scripts/verify.mjs checks the registry against the pinned authority (AUTHORITY, or else authority.pub)
and its format, then for each entry: that its tool.json exists and names a well-formed api, that its signature
verifies with its own pubkey, that its script matches entrySha256, and that the entry's pubkey equals the
manifest's.
A signature that is missing or does not hold is listed as UNSIG; on dev a changed tool stays unsigned until the
next release. --strict fails on those, and on an entry address that leaves the repository when the registry is
served from a subpath. CI runs the plain check on dev, and --strict on main, on pull requests into it and on
release tags.
Bundle a tag into an APRScaching release¶
Every APRScaching release ships a registry tag. In a checkout of
apachler/aprscaching, on a feature branch cut from dev:
The script fetches the tag from GitHub and checks the registry's signature against the project key pinned in the
app, each manifest's signature against the author key its entry lists, and each script against its manifest's
entrySha256. It writes nothing when any check fails. It copies registry.json and the listed tools into the web
app's public/tools/, keeping the repository's layout, so the instance serves them from its own address with the
same relative entries. Open the change as a pull request into dev; it ships with the next APRScaching release.
In Claude Code in the APRScaching repository, /bundle-tools v1.1.0 does this on a chore/bundle-tools-v1.1.0
branch: it reads the tag from a temporary worktree of this repository, runs the gate (pnpm run verify, the tool
sandbox end-to-end test and the visual run of the tools surfaces) and opens the pull request. It merges nothing.
When the new tag needs a newer tool API than the app implements, it waits for the app release that implements it (Tool API versions).
The documentation site¶
This site is built from docs/ with MkDocs (mkdocs.yml). Every pull request into dev or main that touches the
docs, the tools or the registry builds it with mkdocs build --strict, and a push to main publishes it to GitHub Pages (.github/workflows/docs.yml). A new tool
needs its catalogue page: the build fails for a tool folder without one.
The repository checks on dev and main¶
pnpm test checks every tool folder against its registry entry. Contributors open pull requests before the entry
exists, so on dev a folder without one is a warning. With STRICT=1, which CI sets on main, on pull requests into
it and on release tags, it fails: main holds only listed, signed tools. Add the entries before the release.
Next¶
- Contribute a tool: what a contributor sends you.
- Keys, rotation and release practice: when a key changes or leaks.