feat(void): publish built releases with publish-release.sh

The release is built on the maintainer's x86_64 Void host, not in CI. The
bongbetic-ci runner is ARM64 and cannot build the glibc x86_64 package. A tag
push no longer starts the xbps workflow: it is manual dispatch only, and its
header says why.

- build-release.sh release refuses an uncommitted or untracked checkout before
  any container starts. It writes BUILT_FROM (the tag and the commit it was
  built from), which nothing recorded before.
- publish-release.sh TAG --out-dir DIR publishes a built out-dir to the Gitea
  release API in the workflow's order. It refuses unless origin's tag peels to
  the BUILT_FROM commit, the sdist digest matches, and the client key is the
  committed one. --dry-run runs every check and prints the planned calls
  without sending one. The token comes from GITEA_TOKEN or the tea login and is
  never printed.
- Tests run both scripts offline against a local bare origin and a stateful
  Gitea stand-in.
- docs/RELEASE_PROCESS.md and packaging/void/README.md describe the local
  sequence: merge, annotated tag by ref, clean build, dry-run, publish, checksum.
This commit is contained in:
2026-11-07 01:46:38 +05:30
parent 64020a132d
commit dc7e964981
10 changed files with 1186 additions and 116 deletions
+52 -45
View File
@@ -39,23 +39,25 @@ 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`. 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:
The signed Void repository is built and published from the maintainer's own
x86_64 Void host, not from CI. The whole build is one script,
`packaging/void/build-release.sh`, and the publish is one more,
`packaging/void/publish-release.sh`. The Gitea workflow
`.gitea/workflows/xbps-release.yml` is manual dispatch only: a tag push does not
start it, because its only runner (label `bongbetic-ci`) is ARM64 and cannot build
the glibc x86_64 release. Set this up once on the release host:
| what | value |
|---|---|
| Secret | `XBPS_SIGNING_KEY`: the PEM-encoded RSA 4096 repository private key (Gitea repository settings, Actions, Secrets) |
| Signing key | the PEM-encoded RSA 4096 repository private key, kept in the maintainer's own backup. It is passed to `build-release.sh` as a file on tmpfs (`/dev/shm`) and removed after the run; it is never committed |
| 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`, 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 |
| Host | x86_64 Void with rootless podman (user namespaces), python3.11, uv 0.12/0.13 and tmpfs `/dev/shm`. Requirements: `packaging/void/README.md`, "Runner requirements". Never Docker |
| Gitea login | a `tea` login for `https://git.bongbetic.com` whose token may create releases and upload assets, or `GITEA_TOKEN` in the environment for `publish-release.sh` |
Without the secret the workflow fails and publishes nothing; it never signs
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`.
Without a valid key the build stops before any container starts. It never signs
with a different key: the release path refuses any key whose public half is not
`repository-key.plist`. Only the public `.plist` is committed. Key generation and the
check that a private key matches the committed public key are in `packaging/void/README.md`.
The carried Void templates in `packaging/void/srcpkgs/` track the project's own
pins. `python3-PyAudio` and `python3-pysocks` replace stock Void packages below
@@ -109,7 +111,7 @@ Install `just` from https://just.systems or the distro package `just`.
- [ ] Commit all version changes
- [ ] Push branch and create PR
- [ ] After PR merge, create tag: `git tag -a vX.Y.Z-PHASE -m "Release X.Y.Z-PHASE"`
- [ ] Push tag: `git push origin vX.Y.Z-PHASE` (this also starts the Gitea xbps workflow, see Step 8)
- [ ] Create and push the annotated tag on the verified `origin/main` commit: `git push origin refs/tags/vX.Y.Z-PHASE`, never `--tags`. Pushing a tag starts no workflow; the Void release is built and published by hand (Step 8)
### After the release run finishes
- [ ] Run `just verify-release` - the published release verifies as published
@@ -351,8 +353,8 @@ It has two parts, handled at two times, and both go through a normal pull reques
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
prints `checksum=<sha256>`, and the published release notes carry it as
`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.
@@ -515,10 +517,15 @@ Key changes since 0.4.1-alpha:
Full changelog: https://github.com/VocaHQ/vocalinux/releases"
# Push tag
git push origin v0.5.0-beta
# Push the one tag by its ref, never with --tags
git push origin refs/tags/v0.5.0-beta
```
**Void release (this fork):** tag the verified `origin/main` commit by its full SHA, then push
that one ref: `git tag -a v0.19.0 <sha> -m "Voculinux 0.19.0"` and
`git push origin refs/tags/v0.19.0`. Never `git push --tags`: this clone holds upstream tags
that must not reach the fork's Gitea host. Then follow the Void section of Step 8.
### Step 8: Monitor Release
After pushing the tag, the GitHub Actions workflow will automatically:
@@ -551,36 +558,35 @@ After pushing the tag, the GitHub Actions workflow will automatically:
Monitor at: https://github.com/VocaHQ/vocalinux/actions
#### Void xbps repository (Gitea tag workflow)
#### Void xbps repository (local release)
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.
Pushing the tag starts nothing. The Void release is run by hand on the release host,
from a clean checkout of the tag, in this order. Replace `vX.Y.Z` with the release.
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.
1. **Merge the release pull request** to `main`, then confirm which commit `origin/main` is:
`git fetch origin && git rev-parse origin/main`.
2. **Create and push the annotated tag** on that commit (Step 7).
3. **Check out the tag cleanly:** `git worktree add /tmp/voculinux-vX.Y.Z vX.Y.Z`. A worktree
at a tag creates no tag. Work only in that worktree.
4. **Build:** from that checkout, run
`packaging/void/build-release.sh release vX.Y.Z --key-file /dev/shm/xbps-sign-key.pem --out-dir /path/to/out`.
The out-dir must be absent or empty. The run refuses an uncommitted checkout and a key
that is not the committed one, both before any container starts. It writes `sdist/`,
`xbps/`, `SDIST_SHA256` and `BUILT_FROM` (the tag and the commit it was built from), and
prints `checksum=<sha256>`. Remove the key file afterwards.
5. **Dry-run the publish:** `packaging/void/publish-release.sh vX.Y.Z --out-dir /path/to/out --dry-run`.
It runs every check (the tag is on `origin` at the built commit, the digest, the key)
and prints the API calls it would make. It sends no request and reads no token.
6. **Publish:** `packaging/void/publish-release.sh vX.Y.Z --out-dir /path/to/out`. It attaches the
sdist, packages, signatures, `FINGERPRINT` and the client `.plist` to the tag's release, then
replaces the assets of the rolling `latest` release that the client registration URL serves.
The repodata is uploaded last. The token comes from `GITEA_TOKEN`, or from the `tea` login
for `https://git.bongbetic.com`.
7. **Record the checksum** in a post-release pull request (section 3.5).
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.
Neither script commits or pushes to any branch or tag, and nothing is built or uploaded by
hand. Publish tags in version order: publishing an older tag refreshes `latest` with the
older repository.
### Step 9: Post-Release Tasks
@@ -601,7 +607,7 @@ or pushes to any branch. The post-release checksum pull request is section 3.5.
- [ ] Verify PyPI package was published (if applicable), and that its wheel sha256
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
- [ ] Verify the Void publish succeeded, and that both the tag release and
`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
@@ -636,6 +642,7 @@ git push origin v0.5.1-beta
| Version | Date | Phase | Notes |
|---------|------|-------|-------|
| 0.19.0 | 2026-11-07 | Stable | First Voculinux release: a hard fork for Void Linux (glibc, x86_64) with a signed xbps repository, niri and Dank Material Shell support, a per-backend memory-aware model default, and PipeWire system audio that never falls back to the microphone |
| 0.2.0-alpha | 2024 | Alpha | Initial alpha |
| 0.3.0-alpha | 2024 | Alpha | - |
| 0.4.0-alpha | 2024 | Alpha | Multi-language support |