fix(void): build the xbps release from the tag, not an attached sdist (#37)
CI / security (push) Successful in 1m15s
CI / lint (push) Successful in 23s
CI / ai-review (push) Skipped

Every release is built from its tag alone by packaging/void/build-release.sh, shared by the tag workflow and local dry runs. Signs in a network-less container with the key mounted read-only and publishes to the tag release and the rolling latest release.
This commit was merged in pull request #37.
This commit is contained in:
2026-11-06 15:31:47 +00:00
parent 2a76793109
commit b47a65c91e
23 changed files with 1918 additions and 1218 deletions
+60 -53
View File
@@ -40,19 +40,21 @@ publish succeeds.
### Void xbps repository (required for a Void release)
The signed Void repository is released by `.gitea/workflows/xbps-release.yml`
on the fork's Gitea host, not by `release.yml`. Set it up once:
on the fork's Gitea host, not by `release.yml`. The whole release is one script,
`packaging/void/build-release.sh`, which the workflow and a local dry run both
call. Set it up once:
| what | value |
|---|---|
| Secret | `XBPS_SIGNING_KEY`: the PEM-encoded RSA 4096 repository private key (Gitea repository settings, Actions, Secrets) |
| Public key | `packaging/void/repository-key.plist`, committed, fingerprint `fa:d1:00:32:ce:b1:b9:fd:73:05:05:76:75:54:37:25` |
| Runner | `bongbetic-ci` with podman (the workflow installs it with apt when absent). Never Docker |
| Runner | `bongbetic-ci`, a host job with podman (rootless, with user namespaces), python3.11, uv 0.12/0.13 and tmpfs `/dev/shm`. Requirements: `packaging/void/README.md`, "Runner requirements". Never Docker |
| Token | the job token must be allowed to create releases and upload release assets |
Without the secret the workflow fails and publishes nothing; it never signs
with a different key either, because the fingerprint of the key that signed
must equal the one above. The private key is never committed; only the public
`.plist` is. Key generation and the check that a private key matches the
with a different key either: the release path refuses any key whose public half
is not `repository-key.plist`. The private key is never committed; only the
public `.plist` is. Key generation and the check that a private key matches the
committed public key are in `packaging/void/README.md`.
## Quick Release Checklist
@@ -72,7 +74,7 @@ Use this checklist for every release:
- [ ] `docs/INSTALL.md` - Verify install examples use `main/install.sh` (not version-pinned raw URLs)
- [ ] `docs/UPDATE.md` - Add "What's New" section for new version
- [ ] `SECURITY.md` - Update supported versions table
- [ ] `packaging/void/srcpkgs/` - After the tag, bump template `version`/`checksum` to the new release as a pull request (never direct to `main`)
- [ ] `packaging/void/srcpkgs/voculinux/template` - set `version` (the xbps spelling, the hyphen removed) and `_tag` to this release, in the same release PR as `version.py`. The tag gate refuses a tag whose template names another release. The checksum is recorded after the release (section 3.5)
### Website
- [ ] `web/src/app/page.tsx` - Update `softwareVersion` in schema
@@ -329,26 +331,32 @@ Update the supported versions table:
| < 0.4 | :x: |
```
#### 3.5 Update Void templates (pull request)
#### 3.5 Void template: version in the release PR, checksum after the tag (pull request)
After the tag is cut, the Void templates in `packaging/void/srcpkgs/` must
point at the new release: bump `version` in the `voculinux` template (and
any template whose upstream moved) and refresh every pinned `checksum`
(`sha256` sums of the release sdist and distfiles). The `voculinux` template
builds the fork's release asset `voculinux-X.Y.Z.tar.gz`; its `checksum` is
the sha256 of that exact downloaded asset, never a value copied from
upstream, and stays `PENDING_FORK_RELEASE_SHA256` (which fails closed in
`xbps-src`) until the asset exists. Land the bump as a **pull request**
through the normal test, lint, and type gates, never as a direct or bot
commit to `main`, and never hand-edited onto the release itself (see
`just verify-release`). `tests/test_void_app_template.py` fails if the
template version or checksum drifts from `src/vocalinux/version.py` and the
published sdist.
The Void template `packaging/void/srcpkgs/voculinux/template` names one release.
It has two parts, handled at two times, and both go through a normal pull request:
Nothing automates this: the xbps workflow never commits or pushes. The
signed repository a tag publishes is built from the templates as committed
at that tag, so a bump merged after tag `vX.Y.Z` first ships with the next
tag push.
- **Version and tag: in the release pull request**, with `version.py`. Set
`version` to the xbps spelling of `__version__` (the hyphen removed:
`0.19.0-beta` becomes `0.19.0beta`) and `_tag` to the tag (`v0.19.0-beta`).
`build-release.sh check-tag` refuses any tag whose template names another
release, so a stale template stops the tag, before any build.
- **Checksum: after the tag, in a pull request.** The release build replaces
`checksum=` in its build tree only, with the sha256 of the sdist it builds, and
prints `checksum=<sha256>` in the workflow log and in the release notes
(`sdist sha256:`). Copy that value into the template in a pull request through
the normal test, lint and type gates. Never commit it directly to `main`, and
never hand-edit a release asset.
The committed template carries a 64-zero placeholder for `checksum=` until that
first pull request lands. The placeholder is never a real digest and is never
published: every release build replaces it. `tests/test_void_app_template.py`
fails if the template's version or `_tag` drifts from `src/vocalinux/version.py`.
The release build never commits or pushes. Its signed repository is built from
the templates as committed at the tag, so a checksum pull request merged after
tag `vX.Y.Z` is recorded for the next release, and the next tag builds with it
(the build tree always takes the digest of the sdist it builds).
### Step 4: Update Website
@@ -541,32 +549,30 @@ Pushing the tag to `origin` (the fork's Gitea repository) also starts
`.gitea/workflows/xbps-release.yml`. Pushing a tag is a human act and is
approved each time: the workflow has no schedule and no manual trigger.
1. **Tag equals version.** The tag must be exactly `v` plus `__version__` in
`src/vocalinux/version.py`. Any other tag, including one that merely
contains the version, exits 1 and publishes nothing.
2. **Signing key required.** An empty `XBPS_SIGNING_KEY` exits 1 before any
container runs. The key is written to a 0600 temp file, passed to
`packaging/void/repo.sh`, and removed by a step that always runs.
3. **Build and sign.** `repo.sh` is the only path that builds or signs: the
pinned Void glibc container under podman, every template in smoke-gate
order, the full `x86_64` index, every package and the metadata signed.
4. **Verify.** Packages, signatures, repodata, `FINGERPRINT` and the client
key must all be present, nothing but glibc `x86_64` data may be present,
every package must be signed, and the signing fingerprint must be the
pinned one. A partial or unsigned repository never attaches.
5. **Publish.** The complete signed set attaches to the tag's release on
Gitea and replaces the assets of the rolling `latest` release, which the
client registration URL
1. **Tag gate.** `build-release.sh check-tag`: the tag must be exactly `v` plus
`__version__`, and the committed template must name that release. Any other
tag exits 1 and publishes nothing.
2. **Key gate.** The `XBPS_SIGNING_KEY` secret must be non-empty. It is written
with noclobber to a `0600` file on tmpfs and removed by a final step that
always runs.
3. **Runner check.** podman, python3.11, uv 0.12/0.13, openssl, git and curl
must be on the runner, or the job names what is missing and stops.
4. **Release.** `build-release.sh release` builds the canonical sdist from the
tag, builds every template in the pinned Void glibc container
without the key, signs the repository in a second container with no network,
and installs `voculinux` from that repository in a clean image without the key.
A local dry run is `just void-release vX.Y.Z` with `XBPS_SIGNING_KEY_FILE`
set: the same script, the same containers, nothing published.
5. **Publish.** The staged set (sdist, packages, signatures, `x86_64-repodata`,
`FINGERPRINT`, the client `.plist`) attaches to the tag's release on Gitea and
replaces the assets of the rolling `latest` release, which the client
registration URL
(`https://git.bongbetic.com/xavierk/voculinux/releases/download/latest`)
serves. Push tags in version order: pushing an older tag refreshes
`latest` with the older repository.
serves. Push tags in version order: pushing an older tag refreshes `latest`
with the older repository.
The workflow never commits or pushes to any branch and never bumps a
template; that is the pull request in section 3.5. The `voculinux` template
fetches the fork's `vX.Y.Z` release asset, so that asset must exist and its
checksum must be in the template before the build can pass. A tag whose
templates still carry the pending checksum fails at the build step, by
design.
Nothing in this run is built or uploaded by hand, and the workflow never commits
or pushes to any branch. The post-release checksum pull request is section 3.5.
### Step 9: Post-Release Tasks
@@ -588,11 +594,12 @@ design.
matches the line for that wheel in `SHA256SUMS`
- [ ] Verify website was deployed (check voculinux.com)
- [ ] Verify the Gitea xbps run succeeded, and that both the tag release and
`latest` carry the full signed set (`*.x86_64.xbps`, `*.sig2`,
`x86_64-repodata`, `FINGERPRINT`, the `.plist`) and that the fingerprint
matches the one in `README.md`
- [ ] Open the Void template bump pull request (section 3.5); never commit
it directly to `main`
`latest` carry the full signed set (`sdist/voculinux-*.tar.gz`,
`*.x86_64.xbps`, `*.sig2`, `x86_64-repodata`, `FINGERPRINT`, the `.plist`),
that the fingerprint matches the one in `README.md`, and that the sdist
sha256 in the release notes matches the `checksum=` line the run printed
- [ ] Open the Void checksum pull request (section 3.5) with that `checksum=`
value; never commit it directly to `main`
- [ ] If the Flatpak remote is configured, verify it picked up the tag:
`flatpak remote-info vocahq com.voculinux.Voculinux` (docs/FLATPAK_REMOTE.md)
- [ ] Announce on social media/communities