httpd: Register and attest releases

- Add build/register for a delegate to record each archive in the tag's
  release with radicle-artifact, from their own machine
- Build the explorer bundle with config/default.json in build/register,
  and register and seed it as a collection in the same release
- Record the CI build that fetch leaves in artifacts/; make build drop it
- Add build/attest for other delegates to attest a matching build
- Document registration, attestation and how to verify a download
This commit is contained in:
Daniel Norman
2026-11-06 10:45:37 +01:00
committed by Rūdolfs Ošiņš
parent 9a9a78b5a2
commit c13c7fa5d9
6 changed files with 195 additions and 6 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# Builds the radicle-httpd and radicle-search release archives from a
# `releases/httpd/<version>` tag, and keeps them as Buildkite artifacts. A
# delegate downloads them with explorer/crates/radicle-httpd/build/fetch, then
# signs and uploads them, see explorer/RELEASE.md.
# signs, uploads and registers them, see explorer/RELEASE.md.
#
# garden-broker builds a tag push like a branch push: BUILDKITE_BRANCH is the
# tag name and BUILDKITE_COMMIT the tag object. build/ci checks that the build
+50 -5
View File
@@ -15,7 +15,9 @@ The release tooling lives in [`crates/radicle-httpd/build/`][build].
Releases cover **only** the `radicle-httpd` and `radicle-search` binaries. The
web front-end (`radicle-explorer`) is not released this way: it is deployed
continuously (rolling release) and must always remain compatible with the latest
`radicle-httpd`/`radicle-search` release.
`radicle-httpd`/`radicle-search` release. A release also registers the
frontend bundle of the tagged commit with radicle-artifact, see
[Publish](#publish).
## Versioning
@@ -35,8 +37,11 @@ for which releases explorer supports.
git config set user.signingKey "$(rad self --ssh-key)"
```
- Working `podman` setup (see [Podman Setup](#podman-setup) below), or Docker
- Required tools: `podman` or `docker`, `rad`, `sha256sum`, `curl`, `jq`,
`ssh`, `scp`, `ssh-keygen`
- Required tools: `podman` or `docker`, `rad`, `rad-artifact`, `sha256sum`,
`curl`, `jq`, `ssh`, `scp`, `ssh-keygen`, `node`, `npm`. Install `rad-artifact` with:
```sh
curl -sSf https://files.radicle.dev/releases/radicle-artifact/install | sh
```
- To download the CI build: a Buildkite API token with `read_builds` and
`read_artifacts`
@@ -86,6 +91,13 @@ Normal patches add their highlights to `## Unreleased` in
rad sync --announce
```
5. Optional: create the release in radicle-artifact now, so that it is visible
before its archives are registered. `register` creates it otherwise:
```sh
rad-artifact create releases/httpd/<version>
```
## Build
Pushing the tag starts the `📦 httpd release build` step in Buildkite. It builds
@@ -154,6 +166,35 @@ the archives.
> **Warning:** Uploading immediately publishes the release.
3. Register the archives with radicle-artifact, in the release of the tag. This
records each archive's files.radicle.dev URL and `sha256`. If you publish the
archives that `fetch` downloaded into `artifacts/`, it also records their
Buildkite build. It also builds the explorer frontend bundle from the
checked out tag with `config/default.json`, registers it as the collection
`radicle-explorer` and seeds it from your local artifact node. This needs
`node` and `npm`, in the version of `.nvmrc`, and no untracked files in
`config/`, such as `local.json`. Running it again changes nothing:
```sh
crates/radicle-httpd/build/register
```
## Attest (recommended)
Other delegates check the release by building it themselves. `attest` compares
the CIDs of your build with the registered ones, and attests them only if all
match:
```sh
git switch --detach releases/httpd/<version>
crates/radicle-httpd/build/build
crates/radicle-httpd/build/attest
```
If your build differs, find out why before anyone uses the release. If the
release is wrong, withdraw its archives with
`rad-artifact redact releases/httpd/<version> --cid <cid> -m <reason>`.
## Announce
1. Mark the previous release as resolved in Zulip `#Announcements`
@@ -180,6 +221,10 @@ the archives.
- https://files.radicle.dev/releases/radicle-search/<version>
- https://radicle.dev/download
Check a download with
`rad-artifact verify --repo rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5 <archive>`,
after `rad seed rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5`.
## Changelog
<copy the <version> section from CHANGELOG.md>
@@ -190,8 +235,8 @@ the archives.
<copy output from crates/radicle-httpd/build/checksums>
```
:info: This release was built on <platform>. Build reproducibility across
platforms is unsupported; checksums may differ if rebuilt elsewhere.
Attested by: <the DIDs in `rad-artifact show releases/httpd/<version>`, if
any>
````
## Notes
+66
View File
@@ -0,0 +1,66 @@
#!/bin/sh
set -e
# Attests the registered archives of `releases/httpd/<version>` after checking
# that your own build in `artifacts/` gives the same CIDs. Run `build` first.
outdir="$(dirname "$0")/artifacts"
version="$("$(dirname "$0")/version")"
tag="releases/httpd/$version"
for tool in rad rad-artifact jq; do
if ! command -v "$tool" >/dev/null; then
echo "fatal: $tool is not installed" >&2
exit 1
fi
done
release="$(rad-artifact show "$tag" --json)"
me="$(rad self --did)"
# Registering already counts as the author's attestation.
if echo "$release" | jq -e --arg me "$me" 'any(.[].artifacts[]; .author == $me)' >/dev/null; then
echo "fatal: you registered $tag, so another delegate has to attest it" >&2
exit 1
fi
cids=""
mismatch=false
for target in $(cat "$(dirname "$0")/TARGETS"); do
for bin in radicle-httpd radicle-search; do
filename="$bin-$version-$target.tar.xz"
filepath="$outdir/$filename"
if [ ! -f "$filepath" ]; then
echo "fatal: $filepath is missing, run build first" >&2
exit 1
fi
cid="$(rad-artifact cid "$filepath")"
registered="$(echo "$release" | jq -r --arg name "$filename" \
'[.[].artifacts[] | select(.name == $name) | .cid] | first // empty')"
if [ "$cid" = "$registered" ]; then
echo "✓ $filename $cid"
cids="$cids $cid"
else
echo "✗ $filename: your build is $cid, the release has ${registered:-nothing}" >&2
mismatch=true
fi
done
done
if [ "$mismatch" = true ]; then
echo "fatal: your build differs from the release; nothing was attested" >&2
echo "If the release is wrong, withdraw it with 'rad-artifact redact $tag --cid <cid> -m <reason>'" >&2
exit 1
fi
rad auth
for cid in $cids; do
rad-artifact --no-announce --no-input attest "$tag" --cid "$cid"
done
rad sync --announce
echo "Attestation successful."
@@ -83,6 +83,8 @@ main() {
# Copy build artifacts to output folder.
outdir="$(dirname "$0")/artifacts"
mkdir -p "$outdir"
# These archives are not from a CI build, see `fetch`.
rm -f "$outdir/build-url"
$engine cp "$container:/builds/." "$outdir/"
for target in $(cat "$(dirname "$0")/TARGETS"); do
@@ -78,4 +78,6 @@ for target in $(cat "$(dirname "$0")/TARGETS"); do
done
done
# `register` records the CI build of these archives.
echo "$web_url" >"$outdir/build-url"
echo "Fetch successful."
+74
View File
@@ -0,0 +1,74 @@
#!/bin/sh
set -e
# Registers the archives in `artifacts/` with radicle-artifact, in the release
# of the tag `releases/httpd/<version>`, with their files.radicle.dev URLs and
# checksums. It also builds the explorer bundle from the checked out tag with
# config/default.json, registers it as a collection and seeds it from the local
# artifact node. Run it after `upload`. Running it again changes nothing.
outdir="$(dirname "$0")/artifacts"
explorer="$(cd "$(dirname "$0")/../../.." && pwd)"
version="$("$(dirname "$0")/version")"
tag="releases/httpd/$version"
for tool in git npm rad rad-artifact jq; do
if ! command -v "$tool" >/dev/null; then
echo "fatal: $tool is not installed" >&2
exit 1
fi
done
# node-config also loads untracked files such as config/local.json.
if [ -n "$(git -C "$explorer" ls-files --others config)" ]; then
echo "fatal: $explorer/config has untracked files, remove them" >&2
exit 1
fi
rad auth
artifact() {
rad-artifact --no-announce --no-input "$@"
}
for target in $(cat "$(dirname "$0")/TARGETS"); do
for bin in radicle-httpd radicle-search; do
filename="$bin-$version-$target.tar.xz"
filepath="$outdir/$filename"
if [ ! -f "$filepath" ]; then
echo "fatal: $filepath is missing" >&2
exit 1
fi
(cd "$outdir" && sha256sum --check --quiet "$filename.sha256")
sha256="$(cut -d' ' -f1 "$filepath.sha256")"
echo "Registering $filename.."
cid="$(artifact register "$filepath" --revision "$tag" -n "$filename" --json | jq -r .cid)"
artifact location add --revision "$tag" --cid "$cid" \
"https://files.radicle.dev/releases/$bin/$version/$filename"
artifact metadata set --revision "$tag" --cid "$cid" sha256 "$sha256"
if [ -f "$outdir/build-url" ]; then
artifact metadata set --revision "$tag" --cid "$cid" ci-build "$(cat "$outdir/build-url")"
fi
done
done
echo "Building the explorer bundle.."
(
cd "$explorer"
unset NODE_CONFIG NODE_CONFIG_ENV NODE_CONFIG_DIR VITE_RUNTIME_CONFIG
npm ci -w explorer --no-audit --no-fund
npm run build
)
if ! rad-artifact node status >/dev/null 2>&1; then
rad-artifact node start
fi
echo "Registering the explorer bundle.."
artifact register "$explorer/build" --collection -n radicle-explorer \
--revision "$tag" --seed >/dev/null
rad sync --announce
echo "Registration successful."