The challenge
Compiling and shipping software by hand across a fleet of machines running different operating systems is slow and error-prone. Each host needs its own native binary, builds must be reproducible, and a release should never depend on someone remembering the right sequence of commands.
Our approach
We use a controller-and-agent model: a single Jenkins controller orchestrates ten build agents spanning five OS families. A multibranch pipeline indexes the Git repository and runs the Jenkinsfile committed at that exact revision, so the build definition is always version-matched to the code.
The pipeline fans a build matrix across every node. A branch guard means pull requests compile and archive artifacts but never deploy, while a merge to main installs each freshly built binary. Installs are atomic — copy then move into place — which avoids the classic “text file busy” failure and guarantees a host is never left with a half-written binary.
Technical specifics
- One Jenkins LTS controller (Ubuntu, Java 21) driving 10 agents across Ubuntu, Fedora, FreeBSD, Solaris, and OpenBSD (two FreeBSD agents run as vnet jails).
- A private-LAN fleet with no inbound webhooks, so builds trigger on a 5-minute repository scan.
- Per-OS toolchains and package managers (apt / dnf / pkg) with C/C++, CMake, and C# via .NET and Mono.
- Per-node artifact names and toolchain-grouped checksums that prove each host received its correct native build.
- Releases cut with the GitHub CLI (gh release create) as a single step.
Example configuration
The Jenkinsfile fans the build across agents and only installs on main, with an atomic copy-then-move so a running binary is never overwritten in place.
pipeline {
agent none
triggers { pollSCM('H/5 * * * *') } // no webhooks on a private LAN
stages {
stage('Build matrix') {
matrix {
axes { axis { name 'NODE'; values 'ubuntu','fedora','freebsd','solaris','openbsd' } }
stages {
stage('compile') {
agent { label "${NODE}" }
steps {
sh 'cmake -B build && cmake --build build'
archiveArtifacts "build/ascii-monitor-${NODE}"
}
}
stage('install') {
when { branch 'main' }
agent { label "${NODE}" }
steps {
// atomic install avoids "text file busy"
sh 'cp build/ascii-monitor /usr/local/bin/.am.new && \
mv -f /usr/local/bin/.am.new /usr/local/bin/ascii-monitor'
}
}
}
}
}
stage('release') {
when { branch 'main' }
agent { label 'ubuntu' }
steps { sh 'gh release create "v${BUILD_NUMBER}" build/ascii-monitor-*' }
}
}
}Outcome
One push to main produces one native binary per operating system and a tagged GitHub release in under five minutes — with atomic installs preventing corruption and checksums proving correctness on every host.