Package Build & Deployment Infrastructure for packages.netlinux.co.uk
Overview
packages.netlinux.co.uk is a Debian APT repository serving custom-built amd64
packages. Source code lives in GitHub repos under the
netlinux-ai organisation.
Packages are built automatically by GitHub Actions on every push to main,
published as GitHub Releases, then pulled into the server's reprepro repository
via a webhook.
Suites
The repository serves two suites with distinct target distributions:
Codename
Target
apt line
stable
Debian 12 (bookworm) — used by NetLinux Desktop / Server
deb https://packages.netlinux.co.uk/debian stable main
resolute
Ubuntu 26.04 LTS (“Resolute Raccoon”)
deb https://packages.netlinux.co.uk/debian resolute main
Packages reach these suites by two complementary pipelines:
Nightly dual-build (dev2, 03:00 UTC) — clones every repo, runs brain-code-agent inside a debian:bookworm container and an ubuntu:26.04 container, publishes each resulting .deb to the matching suite. This is the only path that populates resolute. See Nightly dual-build pipeline below.
Both suites share the
same pool/ directory on disk. Because bookworm and resolute builds use
different Debian version suffixes (~bookworm1 / ~resolute1), two targeted
builds of the same upstream coexist in the pool without collision.
The ISO is built with live-build using the scripts in the
netlinux-desktop repo. It pulls packages from both the Debian
bookworm repos and the NetLinux APT repository. APT pinning ensures NetLinux
packages take priority where available.
No desktop environment, no GUI — just a hardened server baseline with
SSH, Docker, and firewall enabled out of the box. The netlinux-server
meta-package can also be installed on any Debian bookworm system via apt.
Every repo under netlinux-ai that produces a .deb has a workflow at
.github/workflows/release-deb.yml. The structure is the same across all repos,
with build steps varying per project.
Signature verified by computing HMAC-SHA256(request_body, WEBHOOK_SECRET)
and comparing against the X-Webhook-Signature: sha256=<hex> header.
Logs to /var/log/repo-webhook.log.
update-repo.sh
The main deployment script. Called as: update-repo.sh <tag> <github-repo>
Maps github-repo to PKG_NAME and POOL_DIR via a case statement
Fetches release metadata from GitHub API to find the .deb asset URL
Downloads the .deb
If the .deb contains zstd members, repacks to xz (safety net)
Removes any old version: reprepro remove stable <pkg>
Adds the new version: reprepro includedeb stable <deb>
Looks up the actual filename in the pool (handles checkinstall's -1 suffix)
Updates the pool's index.html with the new version/filename
Updates the main /Sites/netlinux/packages/index.html
Kernel packages (netlinux-ai/linux) are handled specially because they produce
multiple .deb files (linux-image, linux-headers, linux-libc-dev) with version
numbers embedded in the package name.
📝
Note on suites:update-repo.sh currently publishes every
webhook-triggered build into the stable suite only. resolute is
populated by the nightly pipeline below. If you need a just-pushed package
in resolute before the next nightly, either re-run the nightly manually
on dev2 or run reprepro -b /Sites/netlinux/packages/debian copy resolute
stable <pkg> to copy the existing bookworm-compat .deb into
resolute as a temporary stand-in.
Nightly Dual-Build Pipeline
A second pipeline runs on dev2 (147.182.205.211) at 03:00 UTC via cron
and is the only source of resolute suite builds. It lives at
/home/graham/nightly/ (see README.md there for the full runbook).
Flow
Developer pushes to main
|
└─────> GitHub Actions (as documented above) → stable suite
dev2 cron @ 03:00 UTC
|
v
nightly-packages.sh
|
├──> build-images.sh (refresh netlinux-build:{bookworm,resolute}
| Docker images if >14 days old)
|
v
For each repo in repos.conf (if GitHub has new commits):
|
├──> Clone once into build/<repo>/src/
|
├──> docker run --rm --network host \
| -v src:/src -v output:/output \
| netlinux-build:bookworm \
| brain-code-agent --prompt "..."
| → .deb tagged ~bookworm1
| → publish_deb to suite=stable
|
└──> docker run --rm --network host \
-v src:/src -v output:/output \
netlinux-build:resolute \
brain-code-agent --prompt "..."
→ .deb tagged ~resolute1
→ publish_deb to suite=resolute
Key properties
Build-env isolation: each target builds inside its own Docker container, so checkinstall/apt link against the correct distro's libraries. Base images are debian:bookworm and ubuntu:26.04.
Version tagging: the nightly agent emits versions suffixed -<build_num>netlinux1~<target>1 so both .debs coexist in the shared pool and clients on each suite resolve the correct artefact.
Last-commit advancement: a repo's stored SHA is only updated when at least one target publishes successfully. A both-targets failure keeps the repo in the retry set for the next nightly run.
Agent:brain-code-agent is a bash-based LLM agent backed by a locally hosted Qwen2.5-Coder-14B via llama-server (SSH-tunnelled to localhost:8090). The agent reads each repo's .github/workflows/release-deb.yml for build instructions, runs them inside the container, then repacks zstd→xz.
Smoke tests: after all builds, test-packages.sh boots three VMs (NetLinux Server ISO, Debian bookworm, Ubuntu Resolute) and runs per-package smoke tests. The Resolute VM points at the resolute suite; the other two point at stable.
Configuring the server side (one-time)
On dev2 (in /home/graham/nightly):
bash./add-resolute-suite.sh # ssh's to packages server, appends resolute
# codename to conf/distributions, reprepro export
./build-images.sh # docker builds netlinux-build:{bookworm,resolute}
./prepare-resolute-vm.sh # downloads + customises the test VM image
After these, ./nightly-packages.sh dual-builds and dual-publishes.
Add source code and .github/workflows/release-deb.yml following the template.
Customise: build dependencies, build commands, --pkgname, --requires,
install command, version base string, and repo name in the webhook payload.
Set the webhook secret on the repo:
gh secret set WEBHOOK_SECRET --repo netlinux-ai/<name>
Use the same shared secret as other repos.
On the server, add the new package to update-repo.sh:
ssh root@packages.netlinux.co.uk
vi /Sites/netlinux/packages/webhook/update-repo.sh
# Add a new case entry:
netlinux-ai/<name>)
PKG_NAME="<package-name>"
POOL_DIR="<first-letter>/<package-name>"
;;
Create the pool directory and index page on the server:
Create an index.html in that directory. Every pool index page must
include the following sections:
Breadcrumb — link back to the main packages page
Package name and description — what this package is
“Why this build?” box — a green highlighted box
(.why class) explaining why someone should use the NetLinux version
instead of the distro package. This is the most important section.
It should include:
What version the distro ships and what version NetLinux provides
Specific improvements: security fixes, new features, bug fixes,
or patches unique to the NetLinux build
For entirely new packages (no distro equivalent), explain what
gap this fills and what capabilities it provides
Comparison table (where applicable) — a side-by-side
feature comparison between the distro version and the NetLinux version,
using the .compare class
Install instructions — sudo apt install
command and link to the main page for repository setup
Use an existing pool page (e.g.
rsync)
as a template. The .why box uses
background: #e8f5e9; border: 1px solid #a5d6a7 styling and a
<h3>Why this build?</h3> heading in color: #2e7d32.
The version and download filename will be updated automatically by
update-repo.sh on every successful build.
Add a row to the main index.html at /Sites/netlinux/packages/index.html.
The main index page has the following structure (in order):
Title and description — “NetLinux Packages”
heading with tagline
“Why NetLinux?” box — green .why
box explaining the value of the repository: upstream tracking,
security-first approach, restored packages, and the AI-assisted
development model with security/stability guardrails
Quick setup — curl + apt
commands to add the repo and install
Available packages table — one row per package with
columns: Package (linking to pool index page), Version (linking to
.deb), Description, Source (linking to GitHub repo)
Disclaimer note — packages provided as-is, not
affiliated with Debian
Add a new row to the “Available packages” table. The version and
filename will be updated automatically by update-repo.sh on the
first successful build.
Push to main — the GitHub Action will build, release, and trigger
the webhook. The package will appear in the repo within seconds.
APT source line:deb https://packages.netlinux.co.uk/debian resolute main
Other distributions (Debian 13, Ubuntu 24.04, etc.)
Start with the stable suite — most packages link only against Debian
bookworm's libraries and will install on Debian 13 / recent Ubuntu releases
without issue. If you hit a dependency version mismatch, file an issue; the
nightly pipeline can be extended with an additional target.
Useful Commands
On the server
bash# List all packages in a suite (swap 'stable' for 'resolute' as needed)
reprepro -b /Sites/netlinux/packages/debian list stable
reprepro -b /Sites/netlinux/packages/debian list resolute
# Manually add a .deb to a specific suite
reprepro -b /Sites/netlinux/packages/debian includedeb stable /path/to/file.deb
reprepro -b /Sites/netlinux/packages/debian includedeb resolute /path/to/file.deb
# Remove a package from a specific suite
reprepro -b /Sites/netlinux/packages/debian remove stable <package-name>
reprepro -b /Sites/netlinux/packages/debian remove resolute <package-name>
# Copy a package version across suites (e.g. promote stable → resolute as stand-in)
reprepro -b /Sites/netlinux/packages/debian copy resolute stable <package-name>
# Regenerate Release/InRelease for both suites
reprepro -b /Sites/netlinux/packages/debian export
# Check webhook service
systemctl status repo-webhook
journalctl -u repo-webhook -f
# View webhook log
tail -f /var/log/repo-webhook.log
# Manually trigger an update (from anywhere) — publishes to 'stable' only
curl -sf -X POST \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: sha256=$(echo -n '{"repo":"netlinux-ai/<repo>","tag":"latest"}' \
| openssl dgst -sha256 -hmac '<secret>' -binary | xxd -p -c 256)" \
-d '{"repo":"netlinux-ai/<repo>","tag":"latest"}' \
https://packages.netlinux.co.uk/webhook/update-repo
On dev2 (nightly pipeline)
bash# Run the nightly dual-build immediately
/home/graham/nightly/nightly-packages.sh
# Just refresh the build-env containers
/home/graham/nightly/build-images.sh
# Smoke-test only (without rebuilding)
/home/graham/nightly/test-packages.sh \
--build-num N --repos-conf /home/graham/nightly/repos.conf \
--output /tmp/test.json --log-dir /tmp/logs
On GitHub
bash# Check latest release
gh release list --repo netlinux-ai/<repo>
# View workflow runs
gh run list --repo netlinux-ai/<repo>
# Set webhook secret on a new repo
gh secret set WEBHOOK_SECRET --repo netlinux-ai/<repo>
Known Issues & Gotchas
zstd vs xz
Ubuntu 24.04's checkinstall produces zstd-compressed .deb files.
The server's reprepro requires xz. Both the GitHub Actions workflow and
update-repo.sh include a repack step, but if you're manually adding a
.deb, you must repack it first.
checkinstall -1 suffix
checkinstall appends -1 as a Debian release number to the version, so
1.0-2netlinux1 becomes 1.0-2netlinux1-1 in the filename.
update-repo.sh handles this by looking up the actual filename in the pool
with ls rather than constructing it.
First push after creating a repo
The WEBHOOK_SECRET must be set before the first push, otherwise the webhook
notification step will fail (the build and release still succeed). If this happens,
push again or manually trigger the webhook.
Pool directory naming
The pool directory path uses the package name
(e.g. s/simplescreenrecorder), not the GitHub repo name (which may differ,
e.g. ssr). The POOL_DIR in the case statement must match exactly.
Dual-build version collision
bookworm and resolute builds of the same upstream must carry different
Debian version suffixes (~bookworm1 vs ~resolute1) so both
.debs coexist in the shared pool. The nightly pipeline enforces
this via the build prompt; a manual dpkg-deb invocation must do the
same.
GHA-only packages won't reach resolute
Webhook-triggered builds only publish to stable. Until the nightly
runs, apt update on a resolute client won't see a just-pushed upstream
change. Run the nightly manually or
reprepro copy resolute stable <pkg> to bridge.
Architecture Diagram
Two pipelines feed the
same reprepro repo: GitHub Actions (push-triggered, publishes
to stable) and the dev2 nightly (cron, dual-builds
inside per-distro containers and publishes to both stable and
resolute). Clients pick the suite that matches their host distro.