Merge feature/29 into integration/spec16

This commit is contained in:
soubarnak
2026-11-06 09:44:24 +05:30
6 changed files with 289 additions and 5 deletions
+113 -3
View File
@@ -289,6 +289,21 @@ jobs:
`--dangerous` is required because this file is not a Store revision. It will not
refresh from the Store. amd64 only.
### Void Linux (glibc x86_64, signed xbps repo)
Also attached below: the signed repository files (`*.x86_64.xbps`
with signatures, `x86_64-repodata`, and the client key
`FINGERPRINT` plus the fingerprint-named `.plist`). This is a
third-party repository published by this project, not upstream
Void Linux:
```bash
# as root (the files come from this release's assets):
cp <fingerprint>.plist /var/db/xbps/keys/
echo 'repository=https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/' > /etc/xbps.d/voculinux.conf
sudo xbps-install -Sy voculinux
```
Answer `Y` at the fingerprint prompt only after checking it
matches the attached `FINGERPRINT` file.
### Verifying what you downloaded
`SHA256SUMS` covers every file here and is attached once the aarch64 AppImage,
both Flatpak builds, and the snap finish, a few minutes after this release appears.
@@ -789,7 +804,7 @@ jobs:
# missing one looks like the tampered one.
publish-checksums:
name: Checksums and Provenance
needs: [build-and-release, build-appimage-arm64, build-native, build-native-arm64, build-flatpak-amd64, build-flatpak-arm64, attach-flatpak, attach-snap]
needs: [build-and-release, build-appimage-arm64, build-native, build-native-arm64, build-flatpak-amd64, build-flatpak-arm64, attach-flatpak, attach-snap, build-xbps]
runs-on: ubuntu-latest
# An attach-job failure must not skip checksums: this job hashes the build
# artifacts, which exist regardless of whether an upload to the release
@@ -805,7 +820,7 @@ jobs:
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
# Verification-only artifacts must never enter the release manifest.
pattern: '@(python-dist|appimage-*|native-*|flatpak-*|snap-amd64)'
pattern: '@(python-dist|appimage-*|native-*|flatpak-*|snap-amd64|xbps-repo)'
merge-multiple: true
path: dist
@@ -816,6 +831,9 @@ jobs:
for pat in '*.whl' '*.tar.gz' '*.AppImage' '*.deb' '*.rpm' '*.flatpak' '*.snap'; do
compgen -G "$pat" > /dev/null || { echo "missing artifact: $pat"; exit 1; }
done
for pat in 'repo/*.x86_64.xbps' 'repo/*.sig2' 'repo/*-repodata*' 'keys/FINGERPRINT' 'keys/*.plist'; do
compgen -G "$pat" > /dev/null || { echo "missing artifact: $pat"; exit 1; }
done
test "$(compgen -G '*.AppImage' | wc -l)" -eq 2
test "$(compgen -G '*.deb' | wc -l)" -eq 2
test "$(compgen -G '*.rpm' | wc -l)" -eq 2
@@ -827,8 +845,12 @@ jobs:
set -euo pipefail
ls -la
# Bare names, so `sha256sum -c` works in whatever directory the user
# downloaded the release into.
# downloaded the release into. The xbps repo files hash from their
# own directories for the same reason: the release attaches them
# flat, and RSA signatures stay the primary trust root.
sha256sum -- *.whl *.tar.gz *.AppImage *.deb *.rpm *.flatpak *.snap | tee SHA256SUMS
(cd repo && sha256sum -- *.x86_64.xbps *.sig2 *-repodata*) >> SHA256SUMS
(cd keys && sha256sum -- FINGERPRINT *.plist) >> SHA256SUMS
# Attesting from the manifest rather than a second glob: the file we
# publish and the set we attest cannot drift apart.
@@ -1021,6 +1043,94 @@ jobs:
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
commit_message: 'Release ${{ github.ref_name }}'
# The signed Void xbps repository (ticket #29): every template builds in
# smoke-gate order in the pinned Void container via podman, the full
# repository index is signed with the RSA4096 repository key, and the
# packages, signatures, repodata, and client key land on this release.
# XBPS_SIGNING_KEY is required — an unsigned repository can never
# publish, so a missing key fails the job instead of skipping it (unlike
# the opt-in Flatpak remote and AUR publishes). The voculinux template
# builds the canonical release sdist, so this waits for build-and-release,
# and the tag must match src/vocalinux/version.py or the job aborts.
build-xbps:
name: Build signed xbps repository
needs: build-and-release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Verify tag matches the version source of truth
run: |
set -euo pipefail
TAG="${GITHUB_REF#refs/tags/v}"
PACKAGE_VERSION=$(grep -oP '__version__\s*=\s*"\K[^"]+' src/vocalinux/version.py)
echo "Tag version: $TAG"
echo "Package version: $PACKAGE_VERSION"
if [[ "$PACKAGE_VERSION" != *"$TAG"* ]] && [[ "$TAG" != *"$PACKAGE_VERSION"* ]]; then
echo "::error::Tag v$TAG does not match src/vocalinux/version.py ($PACKAGE_VERSION); refusing to ship the signed repo"
exit 1
fi
# Fail-closed: no key, no repo, no publish — never a silent skip.
- name: Require the repository signing key
env:
XBPS_SIGNING_KEY: ${{ secrets.XBPS_SIGNING_KEY }}
run: |
if [ -z "$XBPS_SIGNING_KEY" ]; then
echo "::error::XBPS_SIGNING_KEY is unset; refusing to publish an unsigned repository (packaging/void/README.md)"
exit 1
fi
- name: Install podman
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends podman
- name: Build and sign the repository
env:
XBPS_SIGNING_KEY: ${{ secrets.XBPS_SIGNING_KEY }}
run: |
set -euo pipefail
printf '%s' "$XBPS_SIGNING_KEY" > /tmp/xbps-sign-key.pem
chmod 600 /tmp/xbps-sign-key.pem
XBPS_SIGNING_KEY_FILE=/tmp/xbps-sign-key.pem bash packaging/void/repo.sh
rm -f /tmp/xbps-sign-key.pem
ls -la publish/xbps/repo publish/xbps/keys
- name: Upload the signed repository
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: xbps-repo
path: |
publish/xbps/repo/*
publish/xbps/keys/*
if-no-files-found: error
retention-days: 7
- name: Attach the signed repository to the release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
cd publish/xbps
# Full signed repo or nothing: binary packages (*.x86_64.xbps)
# with their signatures, the signed repository metadata
# (*-repodata), and the client key files (FINGERPRINT plus the
# fingerprint-named .plist). Each class is required below, so a
# partial repo can never attach.
compgen -G 'repo/*.x86_64.xbps' > /dev/null || { echo "missing packages"; exit 1; }
compgen -G 'repo/*.sig2' > /dev/null || { echo "missing signatures"; exit 1; }
compgen -G 'repo/*-repodata*' > /dev/null || { echo "missing repodata"; exit 1; }
compgen -G 'keys/FINGERPRINT' > /dev/null || { echo "missing FINGERPRINT"; exit 1; }
compgen -G 'keys/*.plist' > /dev/null || { echo "missing client key"; exit 1; }
gh release upload "${{ github.ref_name }}" \
repo/*.x86_64.xbps repo/*.sig2 repo/*-repodata* keys/FINGERPRINT keys/*.plist \
--repo ${{ github.repository }} \
--clobber
# GitHub Release first, Store second. A super-privileged plug can still
# queue a human review and refuse edge/candidate; the .snap still
# has to land on the tag for sideload. grade: stable allows candidate; do
+26
View File
@@ -236,6 +236,32 @@ nix profile install github:VocaHQ/vocalinux
The flake also ships `nixosModules.voculinux` and `homeManagerModules.voculinux` for declarative installs (`programs.voculinux.enable = true`). The bundled pywhispercpp builds with Vulkan GPU inference. Details: [packaging/nix/README.md](packaging/nix/README.md).
### Void Linux (signed xbps repo)
Void Linux **glibc `x86_64` only**. Each tagged release attaches a signed
personal xbps repository: binary packages (`*.x86_64.xbps`), signatures,
repository metadata (`x86_64-repodata`), and the client key
(`FINGERPRINT` plus the fingerprint-named `.plist`).
This is a **third-party repository** — it is published by this project,
not by upstream Void Linux, and its packages are built from the
templates in `packaging/void/srcpkgs/`. Details: [packaging/void/README.md](packaging/void/README.md).
```bash
# as root on the Void system (the files come from the release assets):
cp <fingerprint>.plist /var/db/xbps/keys/
TAG=v0.18.5 # match the release you install from
echo "repository=https://github.com/VocaHQ/vocalinux/releases/download/${TAG}/" > /etc/xbps.d/voculinux.conf
```
Prefer the release notes for the exact repository URL. On first sync,
answer `Y` at the fingerprint prompt only after checking it matches the
`FINGERPRINT` file attached to the same release:
```bash
sudo xbps-install -Sy voculinux
```
### From source
```bash
+14
View File
@@ -54,6 +54,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`)
### Website
- [ ] `web/src/app/page.tsx` - Update `softwareVersion` in schema
@@ -310,6 +311,19 @@ Update the supported versions table:
| < 0.4 | :x: |
```
#### 3.5 Update Void templates (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). 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, and the signed xbps
repository for the *next* release builds from these templates.
### Step 4: Update Website
#### 4.1 Update `web/src/app/page.tsx`
+2 -1
View File
@@ -172,7 +172,8 @@ openssl genrsa -out xbps-sign-key.pem 4096
```sh
# as root on the Void system (the release notes carry the repo URL):
cp <fingerprint>.plist /var/db/xbps/keys/
echo 'repository=<repo-url-from-release-notes>' > /etc/xbps.d/voculinux.conf
TAG=v0.18.5 # match the release you install from
echo "repository=https://github.com/VocaHQ/vocalinux/releases/download/${TAG}/" > /etc/xbps.d/voculinux.conf
xbps-install -Sy voculinux
```
+7 -1
View File
@@ -118,11 +118,13 @@ def test_release_publication_cannot_bypass_the_wheel_gate():
"publish-aur",
"publish-aur-bin",
"publish-flatpak-remote",
"build-xbps",
):
assert "build-and-release" in _needs(jobs[name]), name
checksums = jobs["publish-checksums"]
assert "needs.build-and-release.result == 'success'" in checksums
assert "pattern: '@(python-dist|appimage-*|native-*|flatpak-*|snap-amd64)'" in checksums
pattern = "'@(python-dist|appimage-*|native-*|flatpak-*|snap-amd64|xbps-repo)'"
assert f"pattern: {pattern}" in checksums
def test_aur_bin_publication_depends_only_on_its_appimage_inputs() -> None:
@@ -177,6 +179,10 @@ def test_the_manifest_covers_every_kind_of_artifact_we_publish():
assert checksum_line, "publish-checksums does not generate SHA256SUMS"
for pattern in ("*.whl", "*.tar.gz", "*.AppImage", "*.flatpak", "*.snap"):
assert pattern in checksum_line.group(1), f"{pattern} is published but unchecksummed"
# The signed xbps repo hashes from its own directories (bare names, same
# as the flat files the release attaches), alongside the client key.
for pattern in ("*.x86_64.xbps", "*-repodata*", "FINGERPRINT", "*.plist"):
assert pattern in block, f"{pattern} is published but unchecksummed"
def test_provenance_is_generated_from_the_published_manifest():
+127
View File
@@ -33,6 +33,9 @@ import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
RELEASE_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "release.yml"
ROOT_README = REPO_ROOT / "README.md"
RELEASE_PROCESS = REPO_ROOT / "docs" / "RELEASE_PROCESS.md"
VOID = REPO_ROOT / "packaging" / "void"
SRC = VOID / "srcpkgs"
REPO_SH = VOID / "repo.sh"
@@ -198,3 +201,127 @@ def test_readme_documents_the_signed_repo_flow() -> None:
"fail" in text.lower() and "closed" in text.lower()
), "README must state the fail-closed rule: no key, no repo"
assert ".plist" in text, "README must document the client key install"
def _release_workflow_text() -> str:
assert RELEASE_WORKFLOW.is_file(), f"missing release workflow: {RELEASE_WORKFLOW}"
return RELEASE_WORKFLOW.read_text(encoding="utf-8")
def _xbps_job_text() -> str:
"""The release xbps job block only — other jobs have their own runtimes."""
text = _release_workflow_text()
lines = text.splitlines(keepends=True)
start = next(
(i for i, line in enumerate(lines) if re.match(r"^ build-xbps:", line)),
None,
)
assert start is not None, "release.yml must define a build-xbps job"
end = next(
(
i
for i, line in enumerate(lines[start + 1 :], start + 1)
if re.match(r"^ [a-z][a-z0-9-]*:", line)
),
len(lines),
)
return "".join(lines[start:end])
def test_release_workflow_builds_signed_xbps_on_tag() -> None:
"""The tag pipeline builds the signed repo and attaches it to the release."""
text = _xbps_job_text()
assert (
"packaging/void/repo.sh" in text
), "the xbps job must run the signed-repo pipeline, not a second inline flow"
assert "podman" in text, "the release xbps job must install podman (never Docker)"
assert "docker" not in text.lower().replace(
"never docker", ""
), "the release xbps job must never invoke Docker"
def test_release_workflow_xbps_job_fails_closed_without_key() -> None:
"""An unset XBPS_SIGNING_KEY fails the xbps job — never a silent skip."""
text = _xbps_job_text()
assert SIGNING_SECRET in text, "the xbps job must read the XBPS_SIGNING_KEY secret"
assert re.search(
r"XBPS_SIGNING_KEY.{0,400}?exit 1", text, re.DOTALL
), "an empty XBPS_SIGNING_KEY must exit 1, not skip the publish"
def test_release_workflow_xbps_job_gates_tag_against_version() -> None:
"""The xbps job only ships the repo when the tag matches version.py."""
text = _xbps_job_text()
assert "version.py" in text, "the xbps job must gate on version.py"
assert re.search(
r"[Tt]ag.{0,200}?[Vv]ersion|[Vv]ersion.{0,200}?[Tt]ag", text
), "release.yml must compare the tag against the version source of truth"
def test_release_workflow_attaches_signed_repo_to_release() -> None:
"""Packages, signatures, repodata, and the client key land on the release."""
text = _xbps_job_text()
assert "gh release upload" in text, "the xbps job must attach its output to the release"
for marker in ("*.x86_64.xbps", "repodata", "FINGERPRINT"):
assert marker in text, f"the xbps job must attach {marker} to the release"
#: Concrete release-download prefix every client snippet must name. The
#: xbps job attaches the signed repo flat to the tag release with
#: ``gh release upload "${{ github.ref_name }}"``, so the repository URL is
#: the per-tag download prefix below.
CONCRETE_REPO_DOWNLOAD = "https://github.com/VocaHQ/vocalinux/releases/download/"
def test_client_registration_names_concrete_repo_url() -> None:
"""No self-referential placeholder: every client snippet names the
concrete release-download URL the xbps job attaches the repo to."""
workflow = _release_workflow_text()
root = ROOT_README.read_text(encoding="utf-8")
void_readme = README.read_text(encoding="utf-8")
for name, text in (
("release.yml", workflow),
("README.md", root),
("packaging/void/README.md", void_readme),
):
assert (
"<repo-url-from-release-notes>" not in text
), f"{name} still carries the self-referential repo-URL placeholder"
assert "repository=<" not in text, f"{name} still carries a placeholder repository URL"
assert CONCRETE_REPO_DOWNLOAD in root, "root README must name the concrete release download URL"
assert (
CONCRETE_REPO_DOWNLOAD in void_readme
), "void README must name the concrete release download URL"
assert (
"releases/download/${{ github.ref_name }}" in workflow
), "release.yml must render the concrete per-tag download URL at publish time"
def test_root_readme_registers_the_void_repo() -> None:
"""Root README: Void install from the signed repo, not just the track README."""
text = ROOT_README.read_text(encoding="utf-8")
assert "xbps" in text.lower(), "root README must name xbps"
for marker in ("FINGERPRINT", ".plist", "repository="):
assert marker in text, f"root README must document client registration ({marker})"
assert "xbps-install" in text, "root README must give the install command"
def test_root_readme_states_void_scope_and_third_party_caveat() -> None:
"""glibc x86_64 only, and this is a third-party repo outside upstream Void."""
text = ROOT_README.read_text(encoding="utf-8").lower()
assert "x86_64" in text, "root README must scope the repo to x86_64"
assert "glibc" in text, "root README must scope the repo to glibc"
assert (
"third-party" in text or "third party" in text
), "root README must say the repo is third-party, not upstream Void"
def test_release_process_documents_void_template_bump_as_pr() -> None:
"""Template version/checksum bumps land as a pull request, never direct to main."""
text = RELEASE_PROCESS.read_text(encoding="utf-8")
assert (
"srcpkgs" in text or "xbps" in text.lower()
), "docs/RELEASE_PROCESS.md must cover the Void template bump"
assert re.search(
r"[Pp]ull request", text
), "docs/RELEASE_PROCESS.md must require the bump as a pull request"