┌────────────────────────────┐ │ Automating the Void: │ │Continuous Package Building │ │ with xbps-src and GitHub │ │ Actions │ │ 2026-09-04 │ │ │ ├────────────────────────────┤ │ << Back to Blog │ └────────────────────────────┘
╔══════════════════════════════════════╗ ║ Automating the Void: Continuous ║ ║ Package Building with xbps-src and ║ ║ GitHub Actions ║ ║ 2026-09-04 ║ ║ ║ ╠══════════════════════════════════════╣ ║ << Back to Blog ║ ╚══════════════════════════════════════╝
╔══════════════════════════════════════════════════════════╗ ║ Automating the Void: Continuous Package Building with ║ ║ xbps-src and GitHub Actions ║ ║ 2026-09-04 ║ ║ ║ ╠══════════════════════════════════════════════════════════╣ ║ << Back to Blog ║ ╚══════════════════════════════════════════════════════════╝
╔══════════════════════════════════════════════════════════════════════════════╗ ║ Automating the Void: Continuous Package Building with xbps-src and GitHub ║ ║ Actions ║ ║ 2026-09-04 ║ ║ ║ ╠══════════════════════════════════════════════════════════════════════════════╣ ║ << Back to Blog ║ ╚══════════════════════════════════════════════════════════════════════════════╝
Automating the Void: Continuous Package Building with xbps-src and GitHub Actions
Table of Contents
- Why Void Linux?
- The Problem with Manual xbps-src Maintenance
- Building the Upstream Watcher
- Containerized xbps-src in GitHub Actions
- Automated Testing and Artifact Deployment
- Lessons Learned from Void Packaging
- References
Why Void Linux?
Void Linux is one of the most refreshing operating systems in existence. It has no systemd, boots in under 2 seconds with runit, uses the blindingly fast xbps package manager written in C, and supports both glibc and musl natively. We run Void across our personal workstations, edge routing nodes, and PowerEdge servers.
However, when you maintain private software or bleeding-edge packages on Void, you don't just download a .deb or .rpm. You write a template file in shell syntax and compile it from source using xbps-src:
# template excerpt for a custom tool pkgname=custom-route-daemon version=1.4.2 revision=1 build_style=cargo short_desc="High performance BGP helper daemon" maintainer="Zoa <zoa@zoa.sh>" license="MIT" homepage="https://github.com/vxfemboy/custom-route" distfiles="https://github.com/vxfemboy/custom-route/archive/v${version}.tar.gz" checksum="a84f39bc...982f"
Running xbps-src pkg custom-route-daemon builds the package inside a masterdir chroot and emits an architecture-specific .xbps binary ready for installation.
The Problem with Manual xbps-src Maintenance
Building by hand works fine when you have two packages. But when you maintain an ecosystem of private daemons, custom kernel modules, and specialized utilities, manual packaging collapses:
- Upstream developers release new versions. You have to notice, calculate new SHA256 checksums, edit the template, and test the build.
- Cross-compilation across architectures (
x86_64,x86_64-musl,aarch64) takes substantial local CPU time. - If an upstream dependency changes its ABI, silent breakage occurs at runtime.
We needed a fully automated continuous packaging pipeline that detects new upstream releases, bumps templates, compiles in clean chroots, and publishes them to our private repository.
Building the Upstream Watcher
We created a GitHub Actions workflow that executes on a schedule to monitor upstream git repositories. When a new tag or GitHub release appears:
name: Check Upstream Releases on: schedule: - cron: '0 4 * * *' # Daily at 4 AM UTC workflow_dispatch: jobs: check-versions: runs-on: ubuntu-latest steps: - name: Checkout custom-packages repo uses: actions/checkout@v4 - name: Check latest releases and bump templates run: | python3 scripts/update_templates.py
The script extracts the latest release tag from GitHub APIs, downloads the tarball into a scratch space, computes the SHA256 checksum using sha256sum, and updates the template:
# scripts/update_templates.py snippet import re, hashlib, urllib.request def bump_template(template_path, new_version, tarball_url): content = open(template_path).read() resp = urllib.request.urlopen(tarball_url) checksum = hashlib.sha256(resp.read()).hexdigest() content = re.sub(r'version=.*', f'version={new_version}', content) content = re.sub(r'revision=.*', 'revision=1', content) content = re.sub(r'checksum=.*', f'checksum={checksum}', content) with open(template_path, 'w') as f: f.write(content)
Containerized xbps-src in GitHub Actions
Because xbps-src requires chroot capabilities, we run the compilation inside official Void Linux Docker containers:
build-package: needs: check-versions runs-on: ubuntu-latest container: image: ghcr.io/void-linux/xbps-src-masterdir:latest options: --privileged steps: - name: Clone void-packages repo run: | git clone --depth=1 https://github.com/void-linux/void-packages.git /void-packages ./xbps-src binary-bootstrap - name: Build custom package run: | ./xbps-src -j$(nproc) -a x86_64 pkg custom-route-daemon ./xbps-src -j$(nproc) -a x86_64-musl pkg custom-route-daemon
Automated Testing and Artifact Deployment
Once compiled, GitHub Actions signs the .xbps files using our private RSA repository key (xbps-rindex --sign) and uploads them via S3 to our public repository host.
On our Void Linux machines, updating is as simple as adding our repository to /etc/xbps.d/:
repository=https://repo.zoa.sh/void/current
And running:
xbps-install -Su
No manual interventions. No broken chroots on local workstations.