Files
fips/.github/workflows/package-freebsd.yml
T
fr34akyandJohnathan Corgan 66c9638632 ci: publish the pfSense packages as release assets
The pfSense packages were kept out of releases by design, as workflow
artifacts, until one had been installed on a real pfSense box. That bar
has been met on the two supported Plus bases: install, boot script
lifecycle, log rotation, DNS through unbound, the package reload path,
reboot, teardown and removal on Plus 26.03.1 and 26.07 in Netgate-installed
VMs, and mesh traffic between the two over the TUN interface under pf;
the README's test record has the details.

The release job now needs the pfsense job as well as build, downloads
its artifact next to the FreeBSD one, and requires all three packages
before publishing instead of failing if a pfSense package is present.
Their checksums join checksums-freebsd.txt and the upload glob already
covers them. A pfSense build failure therefore holds a release the way
a FreeBSD build failure does, which is what a release asset means; on
every other ref the pfsense job's output stays a 30-day artifact.

Not covered by the evidence: physical appliances and CE 2.9.0.
2026-09-18 13:14:02 +00:00

465 lines
20 KiB
YAML

name: FreeBSD Package
on:
push:
branches:
- master
- maint
- next
tags:
- "v*"
pull_request:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
determine-versioning:
runs-on: ubuntu-latest
outputs:
freebsd_package_version: ${{ steps.freebsd_version.outputs.freebsd_package_version }}
freebsd_pkg_file_version: ${{ steps.freebsd_version.outputs.freebsd_pkg_file_version }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
- name: Derive FreeBSD package version
id: freebsd_version
shell: bash
run: |
: ${GITHUB_OUTPUT:=/tmp/github_output}
BASE_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME#v}"
else
BRANCH=$(echo "$GITHUB_REF_NAME" | sed 's|[^A-Za-z0-9]|.|g; s/\.\{2,\}/./g; s/^\.//; s/\.$//')
HEIGHT=$(git rev-list --count HEAD)
HASH=$(git rev-parse --short HEAD)
if [[ -z "$BRANCH" ]]; then
BRANCH="ref"
fi
VERSION="${BASE_VERSION}+${BRANCH}.${HEIGHT}.${HASH}"
fi
# build-pkg.sh maps '-' and '+' to '.' (neither is allowed in a
# pkg version); derive the same mapping here so later steps can
# assert the exact artifact filename.
PKG_FILE_VERSION=$(printf '%s' "$VERSION" | tr -- '+-' '..')
echo "freebsd_package_version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "freebsd_pkg_file_version=${PKG_FILE_VERSION}" >> "$GITHUB_OUTPUT"
build:
name: Build FreeBSD package (x86_64)
# No GitHub-hosted FreeBSD runners exist; build inside a KVM-accelerated
# FreeBSD VM on the Linux runner. The release must track the .pkg ABI
# major (FreeBSD:15:amd64) — pkg on other majors refuses the package.
runs-on: ubuntu-latest
needs: determine-versioning
# Successful runs of this job take 8 to 11 minutes. Five consecutive runs
# in August 2026 instead sat in the VM step for 70, 190, 360, 360 and 360
# minutes and ended cancelled, the last three at GitHub's own six-hour job
# ceiling. Nothing here bounded them. This bound is deliberately loose
# enough that a slow-but-working run still passes, and tight enough that a
# stall fails in half an hour instead of burning a runner for six.
timeout-minutes: 30
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
- name: Set SOURCE_DATE_EPOCH from git
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"
- name: Build and smoke-install in FreeBSD VM
uses: vmactions/freebsd-vm@f0552d3b69211736abd97f02ff3d4674c56b73b1 # v1
env:
FREEBSD_PACKAGE_VERSION: ${{ needs.determine-versioning.outputs.freebsd_package_version }}
with:
release: "15.1"
usesh: true
sync: rsync
copyback: true
mem: 6144
envs: "SOURCE_DATE_EPOCH CARGO_TERM_COLOR FREEBSD_PACKAGE_VERSION"
prepare: |
pkg install -y curl
run: |
set -e
# rustup rather than the ports rust: rust-toolchain.toml pins
# the toolchain, and rustup honors the pin on first cargo use.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --default-toolchain none --profile minimal
. "$HOME/.cargo/env"
cargo build --release
# The only place the FreeBSD cfg arms' unit tests ever run in
# CI — the main CI matrix is Linux-only, and a release build
# compiles no #[cfg(test)] code (AF-prefix strip round-trips,
# platform module, config path gates).
#
# Bounded, because libtest has no per-test deadline and this job
# runs plain `cargo test` rather than nextest, so one test that
# blocks on a syscall holds the whole binary open with nothing to
# end it. Six runs in August 2026 did exactly that. The bound is on
# the suite rather than on the job so the failure is a named step
# failure at fifteen minutes instead of the job ceiling at thirty,
# and `timeout` leaves the test binary's stdout untouched up to the
# kill: that stdout is what carries libtest's "has been running for
# over N seconds" lines, which are what name the blocked test.
#
# This bounds the damage; it does not fix anything. A test that can
# block for ever is a defect at the test, and the ones this suite
# has are bounded where they are written.
if ! timeout -s KILL 900 cargo test; then
echo "FAIL: cargo test failed, or did not finish within its 900s bound." >&2
echo " If the output above stops mid-run, look for libtest's" >&2
echo " 'has been running for over' lines: they name the test" >&2
echo " that blocked, and the tests that never reported at all" >&2
echo " are the rest of the answer." >&2
exit 1
fi
packaging/freebsd/build-pkg.sh \
--version "$FREEBSD_PACKAGE_VERSION" \
--no-build
# Smoke-install the package in the VM: files land where the
# rc.d scripts and DNS integration expect them, and the
# binaries link against this release's base libraries.
PKG=$(ls deploy/fips-*-freebsd-*.pkg)
pkg add "$PKG"
for bin in fips fipsctl fipstop; do
test -x "/usr/local/bin/$bin" || { echo "FAIL: missing /usr/local/bin/$bin"; exit 1; }
if ldd "/usr/local/bin/$bin" | grep "not found"; then
echo "FAIL: unresolved shared libraries in $bin"; exit 1
fi
done
test -x /usr/local/etc/rc.d/fips
test -x /usr/local/etc/rc.d/fips_dns
test -f /usr/local/etc/fips/fips.yaml.sample
test -f /usr/local/etc/fips/hosts.sample
# The manifest post-install script must have copied the
# samples into place (install-if-absent semantics).
test -f /usr/local/etc/fips/fips.yaml
test -f /usr/local/etc/fips/hosts
# fips.yaml may hold a node private key (nsec:); it must not
# be world-readable — Debian and macOS both install it 0600.
for f in /usr/local/etc/fips/fips.yaml /usr/local/etc/fips/fips.yaml.sample; do
mode=$(stat -f %Lp "$f")
if [ "$mode" != "600" ]; then
echo "FAIL: $f mode is $mode, expected 600"; exit 1
fi
done
# post-install must create the control-socket access group.
pw groupshow fips >/dev/null || { echo "FAIL: fips group missing"; exit 1; }
test -x /usr/local/libexec/fips/fips-dns-setup
pkg info fips
echo "==> pkg smoke-install PASSED"
# SHA-256 sidecar computed inside the VM; the host verifies the
# bytes again after the rsync copyback, so corruption across
# the VM handoff is detected before upload.
( cd deploy && sha256 -q "$(basename "$PKG")" \
| { read -r h; printf '%s %s\n' "$h" "$(basename "$PKG")"; } \
> "$(basename "$PKG").sha256" )
# The whole workspace is rsynced back to the host; drop the
# build tree so the copyback moves megabytes, not gigabytes.
rm -rf target
- name: Resolve FreeBSD asset path
id: freebsd-assets
shell: bash
run: |
: ${GITHUB_OUTPUT:=/tmp/github_output}
set -euo pipefail
# build-pkg.sh names the package from the derived version and the
# pkg ABI arch; assert the exact name so a naming regression fails
# here instead of colliding on the release page.
EXPECTED="deploy/fips-${{ needs.determine-versioning.outputs.freebsd_pkg_file_version }}-freebsd-amd64.pkg"
if [[ ! -f "$EXPECTED" ]]; then
echo "Expected package $EXPECTED was not produced" >&2
echo "deploy/ contains:" >&2
ls -la deploy >&2 || true
exit 1
fi
echo "pkg=$EXPECTED" >> "$GITHUB_OUTPUT"
- name: Verify .pkg integrity across the VM handoff
shell: bash
run: |
set -euo pipefail
PKG="${{ steps.freebsd-assets.outputs.pkg }}"
sidecar="${PKG}.sha256"
if [[ ! -f "$sidecar" ]]; then
echo "FAIL: missing SHA-256 sidecar for $(basename "$PKG")" >&2
exit 1
fi
expected=$(awk '{print $1}' "$sidecar")
actual=$(sha256sum "$PKG" | awk '{print $1}')
if [[ "$expected" != "$actual" ]]; then
echo "FAIL: $(basename "$PKG") SHA-256 mismatch across the VM copyback" >&2
echo " expected (FreeBSD VM): $expected" >&2
echo " actual (host): $actual" >&2
exit 1
fi
echo "PASS: $(basename "$PKG") matches the in-VM SHA-256 ($actual)"
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: fips_${{ needs.determine-versioning.outputs.freebsd_package_version }}_x86_64_freebsd
path: |
${{ steps.freebsd-assets.outputs.pkg }}
${{ steps.freebsd-assets.outputs.pkg }}.sha256
retention-days: 30
- name: Build summary
run: |
echo "Build Summary for freebsd/x86_64:"
echo " Package: ${{ steps.freebsd-assets.outputs.pkg }}"
pfsense:
name: Build and check the pfSense package (x86_64)
# A job of its own, so a pfSense-only failure — a new key in the common
# dns: block, a dependency that stops linking statically, a checker
# regression — reds this check by name and can never hide behind the
# FreeBSD job's result. `release` needs both jobs: the packages this
# job builds are release assets next to the FreeBSD one, after being run
# on pfSense Plus 26.03.1 and 26.07 (see packaging/pfsense/README.md),
# so a pfSense failure holds the release the way a FreeBSD failure
# does. On every other ref the artifact is kept 30 days for anyone to
# test.
#
# This VM is FreeBSD 15.1. One build yields static FreeBSD 15 binaries,
# packaged twice: as FreeBSD:15:amd64 for pfSense CE 2.8.1, and relabelled
# as FreeBSD:16:amd64 for CE 2.9 and Plus 26.x on Intel. Older binaries on
# a newer kernel is the direction FreeBSD's binary compatibility supports;
# the relabelled package has been run on Plus 26.03.1 and 26.07 (see
# packaging/pfsense/README.md). No aarch64 package is built
# here or published anywhere: rustup ships no toolchain for
# aarch64-unknown-freebsd, so such a build cannot honour the
# rust-toolchain.toml pin every published artifact is built with. ARM is
# build-it-yourself, per the README.
runs-on: ubuntu-latest
needs: determine-versioning
# Its own full release build in an emulated FreeBSD VM, like the build
# job; the same generous bound applies.
timeout-minutes: 45
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
- name: Set SOURCE_DATE_EPOCH from git
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"
- name: Lint the install smoke test
# Same arrangement as package-openwrt.yml's install-nak.sh lint: the
# script does not ship in the package, so the guards for shipped sh
# scripts do not apply. It is plain sh because pfSense has no bash.
run: |
if ! command -v shellcheck >/dev/null 2>&1; then
sudo apt-get install -y --no-install-recommends shellcheck
fi
shellcheck --shell=sh testing/pfsense-install-smoke.sh
- name: Build and check the pfSense package in a FreeBSD VM
uses: vmactions/freebsd-vm@f0552d3b69211736abd97f02ff3d4674c56b73b1 # v1
env:
FREEBSD_PACKAGE_VERSION: ${{ needs.determine-versioning.outputs.freebsd_package_version }}
with:
release: "15.1"
usesh: true
sync: rsync
copyback: true
mem: 6144
envs: "SOURCE_DATE_EPOCH CARGO_TERM_COLOR FREEBSD_PACKAGE_VERSION"
prepare: |
# curl for rustup; bash and php for testing/check-pfsense-pkg.sh
# (the checker is bash, and it runs php -l plus a fips_strip_block
# unit test on the config.xml helper).
pkg install -y curl bash php85
run: |
set -e
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none --profile minimal
. "$HOME/.cargo/env"
# Builds the release binaries (static by default) and packages
# them; on a FreeBSD 15.1 VM this yields the FreeBSD:15:amd64
# package for pfSense CE 2.8.1.
packaging/pfsense/build-pkg.sh --version "$FREEBSD_PACKAGE_VERSION"
PKG15=$(ls deploy/fips-*-pfsense-ce2.8-amd64.pkg)
# The same binaries again, labelled for FreeBSD 16 (pfSense CE 2.9
# and Plus 26.x on Intel). No FreeBSD 16 host is needed for that:
# static binaries from 15.1 run on a 16 kernel, the direction
# FreeBSD supports, and pkg only checks the label.
packaging/pfsense/build-pkg.sh --no-build --abi FreeBSD:16:amd64 \
--version "$FREEBSD_PACKAGE_VERSION"
PKG16=$(ls deploy/fips-*-pfsense-ce2.9-plus26-amd64.pkg)
for PKG in "$PKG15" "$PKG16"; do
testing/check-pfsense-pkg.sh "$PKG"
( cd deploy && sha256 -q "$(basename "$PKG")" \
| { read -r h; printf '%s %s\n' "$h" "$(basename "$PKG")"; } \
> "$(basename "$PKG").sha256" )
done
php -l packaging/pfsense/fips-unbound-custom.php
# Install the FreeBSD 15 package and run the daemon through the
# boot script's life on this FreeBSD 15 kernel; see the script
# header for what that does and does not prove about pfSense
# itself. pkg refuses the FreeBSD 16 package on this host (ABI
# major mismatch); its binaries are the same bytes.
testing/pfsense-install-smoke.sh "$PKG15"
rm -rf target
- name: Resolve pfSense asset path
id: pfsense-asset
shell: bash
run: |
: ${GITHUB_OUTPUT:=/tmp/github_output}
set -euo pipefail
VER="${{ needs.determine-versioning.outputs.freebsd_pkg_file_version }}"
PKG15="deploy/fips-${VER}-pfsense-ce2.8-amd64.pkg"
PKG16="deploy/fips-${VER}-pfsense-ce2.9-plus26-amd64.pkg"
for p in "$PKG15" "$PKG16"; do
if [[ ! -f "$p" || ! -f "$p.sha256" ]]; then
echo "pfSense package or its checksum is missing: $p" >&2
ls -la deploy >&2 || true
exit 1
fi
done
echo "pkg15=$PKG15" >> "$GITHUB_OUTPUT"
echo "pkg16=$PKG16" >> "$GITHUB_OUTPUT"
- name: Upload pfSense artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: fips_${{ needs.determine-versioning.outputs.freebsd_package_version }}_x86_64_pfsense
path: |
${{ steps.pfsense-asset.outputs.pkg15 }}
${{ steps.pfsense-asset.outputs.pkg15 }}.sha256
${{ steps.pfsense-asset.outputs.pkg16 }}
${{ steps.pfsense-asset.outputs.pkg16 }}.sha256
retention-days: 30
release:
name: Publish FreeBSD assets to GitHub Release
runs-on: ubuntu-latest
needs: [build, pfsense]
if: startsWith(github.ref, 'refs/tags/')
permissions:
contents: write
steps:
- name: Download FreeBSD artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
# One named pattern per job: without a pattern this action downloads
# every artifact in the run, and `needs` only orders jobs; it does
# not scope this.
pattern: fips_*_x86_64_freebsd
path: dist
merge-multiple: true
- name: Download pfSense artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: fips_*_x86_64_pfsense
path: dist
merge-multiple: true
- name: Validate .pkg bytes before publishing
shell: bash
run: |
set -euo pipefail
cd dist
# Three packages are expected: the FreeBSD one and the two pfSense
# ones (each job wrote the sidecars inside its own VM). Missing one
# is a failure, not a smaller release.
for want in '*-freebsd-*.pkg' '*-pfsense-ce2.8-amd64.pkg' '*-pfsense-ce2.9-plus26-amd64.pkg'; do
if ! compgen -G "$want" >/dev/null; then
echo "FAIL: no package matching $want was downloaded" >&2
ls -la . >&2 || true
exit 1
fi
done
pkgs=$(find . -maxdepth 1 -type f -name '*.pkg' | LC_ALL=C sort)
if [[ -z "$pkgs" ]]; then
echo "FAIL: no .pkg artifacts were downloaded" >&2
exit 1
fi
fail=0
while IFS= read -r pkg; do
base=$(basename "$pkg")
sidecar="${pkg}.sha256"
if [[ ! -f "$sidecar" ]]; then
echo "FAIL: missing SHA-256 sidecar for $base" >&2
fail=1
continue
fi
expected=$(awk '{print $1}' "$sidecar")
actual=$(sha256sum "$pkg" | awk '{print $1}')
if [[ "$expected" != "$actual" ]]; then
echo "FAIL: $base SHA-256 mismatch on the bytes about to be published" >&2
echo " expected (FreeBSD VM): $expected" >&2
echo " actual (downloaded): $actual" >&2
fail=1
continue
fi
echo "PASS: $base matches the in-VM SHA-256 ($actual)"
done <<<"$pkgs"
if [[ "$fail" -ne 0 ]]; then
echo "==> pre-publish .pkg verification FAILED; not publishing" >&2
exit 1
fi
echo "==> pre-publish .pkg verification PASSED"
- name: Generate FreeBSD release checksums
run: |
cd dist
find . -maxdepth 1 -type f -name '*.pkg' -printf '%P\n' \
| LC_ALL=C sort \
| xargs sha256sum \
> checksums-freebsd.txt
- name: Wait for tag release
env:
GH_TOKEN: ${{ github.token }}
run: |
for attempt in $(seq 1 20); do
if gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
exit 0
fi
echo "Release ${GITHUB_REF_NAME} not available yet; waiting..."
sleep 15
done
echo "Timed out waiting for release ${GITHUB_REF_NAME}" >&2
exit 1
- name: Upload FreeBSD assets
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release upload "${GITHUB_REF_NAME}" \
dist/*.pkg \
dist/checksums-freebsd.txt \
--clobber \
--repo "${GITHUB_REPOSITORY}"