Files
fips/.github/workflows/package-freebsd.yml
T

522 lines
22 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
# The package's newsyslog entry must rotate the daemon log and
# make daemon(8) reopen it. The rc script starts daemon(8) with
# -H and records the supervisor's pid in daemon.pid, which the
# entry signals; without -H the supervisor keeps writing into
# the rotated file.
LOG=/var/log/fips.log
ENTRY=/usr/local/etc/newsyslog.conf.d/fips.conf
test -f "$ENTRY" || { echo "FAIL: missing $ENTRY"; exit 1; }
# Dry run of the stock configuration: the entry is reached only
# through newsyslog.conf's include of newsyslog.conf.d.
if ! newsyslog -nv 2>&1 | grep -q "$LOG"; then
echo "FAIL: the stock newsyslog configuration does not cover $LOG"
newsyslog -nv 2>&1 || true
exit 1
fi
service fips onestart
i=0
until [ -s /var/run/fips/daemon.pid ] && [ -s "$LOG" ]; do
i=$((i + 1))
if [ "$i" -gt 30 ]; then
echo "FAIL: no daemon.pid or empty $LOG 30s after start"
ls -l /var/run/fips "$LOG" 2>&1 || true
cat "$LOG" 2>&1 || true
exit 1
fi
sleep 1
done
sup_pid=$(cat /var/run/fips/daemon.pid)
# Inode numbers a process holds open, from fstat's INUM column.
open_inodes() { fstat -p "$1" 2>/dev/null | awk 'NR > 1 && $6 ~ /^[0-9]+$/ { print $6 }'; }
before=$(stat -f %i "$LOG")
# -F rotates regardless of size; -f reads the shipped entry alone.
newsyslog -F -f "$ENTRY"
after=$(stat -f %i "$LOG")
if [ "$after" = "$before" ]; then
echo "FAIL: newsyslog -F did not rotate $LOG"; ls -li "$LOG"*; exit 1
fi
if ! ls "$LOG".0* >/dev/null 2>&1; then
echo "FAIL: no rotated generation of $LOG"; ls -l "$LOG"*; exit 1
fi
i=0
until open_inodes "$sup_pid" | grep -qx "$after"; do
i=$((i + 1))
if [ "$i" -gt 10 ]; then
echo "FAIL: daemon(8) pid $sup_pid did not reopen $LOG after rotation"
fstat -p "$sup_pid" || true
exit 1
fi
sleep 1
done
if open_inodes "$sup_pid" | grep -qx "$before"; then
echo "FAIL: daemon(8) pid $sup_pid still holds the rotated $LOG open"; exit 1
fi
service fips onestop
echo "==> log rotation PASSED"
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}"