Build and install-test the arm64 package in CI

The arm64 .deb that ships to single-board hosts was floor-checked and
never installed anywhere in the pipeline, so a packaging fault that only
appears on arm64 would reach a user first.

CI's package job becomes a two-leg matrix, building natively on an
ubuntu-24.04-arm runner beside the amd64 leg, and each leg fails unless
the package it produced is its own architecture. The install job gains
one arm64 leg on ubuntu22, the oldest supported distribution: a fresh
install and a daemon start. The upgrade, purge and conffile scenarios
stay amd64-only. The displayed names of the existing checks are
unchanged.

The parity guard now reads each install leg's arch. Only amd64 legs are
compared with the local distro list; an arm64 leg must name a known
distro and is reported as GitHub-only. Without this, deleting the amd64
ubuntu22 leg would have passed, because the arm64 leg still supplied
the distro name.
This commit is contained in:
Johnathan Corgan
2026-09-19 11:38:44 +00:00
parent 9a1797d3ed
commit db254f6d44
6 changed files with 104 additions and 15 deletions
+53 -10
View File
@@ -40,6 +40,10 @@ env:
# unreliable on GitHub-hosted runners. # unreliable on GitHub-hosted runners.
# tor-directory — same; live Tor dependency. # tor-directory — same; live Tor dependency.
# #
# Deliberate GitHub-only: the arm64 install leg (ubuntu22). The local host is
# x86_64 and has no arm64 execution; the leg is compared by distribution only
# and does not stand in for the amd64 leg of the same distribution.
#
# The two runners express the same work in different matrix shapes, and the # The two runners express the same work in different matrix shapes, and the
# parity guard compares through that shape rather than around it: chaos legs # parity guard compares through that shape rather than around it: chaos legs
# are compared per scenario (and per flag) via their `scenario:` field, # are compared per scenario (and per flag) via their `scenario:` field,
@@ -787,11 +791,24 @@ jobs:
# floor violation stops the run. # floor violation stops the run.
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
deb-package: deb-package:
name: Build .deb name: Build .deb${{ matrix.deb_arch == 'arm64' && ' (arm64)' || '' }}
runs-on: ubuntu-latest runs-on: ${{ matrix.os }}
needs: [build, test] needs: [build, test]
if: ${{ !inputs.skip_integration }} if: ${{ !inputs.skip_integration }}
# The arm64 leg builds natively on an arm runner so the arm64 package the
# release ships is install-tested too (job 5). Being one job, both legs
# gate job 5 and the dns-resolver job: an arm64 build failure skips the
# amd64 install legs on that run as well.
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
deb_arch: amd64
- os: ubuntu-24.04-arm
deb_arch: arm64
steps: steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
@@ -824,11 +841,23 @@ jobs:
path: ${{ runner.temp }}/deb-builder-image.tar path: ${{ runner.temp }}/deb-builder-image.tar
key: ${{ steps.builder.outputs.key }} key: ${{ steps.builder.outputs.key }}
# The package path is the script's last line of stdout. A leg that
# produced the other architecture's package goes red here rather than
# handing an amd64 package to the arm64 install leg.
- name: Build the .deb in the pinned build container - name: Build the .deb in the pinned build container
timeout-minutes: 30 timeout-minutes: 30
shell: bash
run: | run: |
set -euo pipefail
bash packaging/debian/build-deb-container.sh --output-dir deploy \ bash packaging/debian/build-deb-container.sh --output-dir deploy \
--image-archive "$RUNNER_TEMP/deb-builder-image.tar" --image-archive "$RUNNER_TEMP/deb-builder-image.tar" \
| tee "$RUNNER_TEMP/build-deb-container.log"
deb=$(tail -n 1 "$RUNNER_TEMP/build-deb-container.log")
[ -f "$deb" ] || { echo "build-deb-container.sh did not name a package: '$deb'" >&2; exit 1; }
case "$deb" in
*_${{ matrix.deb_arch }}.deb) ;;
*) echo "Package $deb is not ${{ matrix.deb_arch }}" >&2; exit 1 ;;
esac
# On a cache miss the archive exists only if the script built the image # On a cache miss the archive exists only if the script built the image
# and saved it, so its presence is what says there is something to save. # and saved it, so its presence is what says there is something to save.
@@ -856,8 +885,8 @@ jobs:
- name: Upload the .deb - name: Upload the .deb
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with: with:
name: fips-deb name: fips-deb-${{ matrix.deb_arch }}
path: deploy/fips_*.deb path: deploy/fips_*_${{ matrix.deb_arch }}.deb
if-no-files-found: error if-no-files-found: error
retention-days: 1 retention-days: 1
@@ -901,7 +930,7 @@ jobs:
- name: Download the .deb - name: Download the .deb
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with: with:
name: fips-deb name: fips-deb-amd64
path: _deb path: _deb
- name: Run dns-resolver test - name: Run dns-resolver test
@@ -944,11 +973,12 @@ jobs:
# #
# The legs keep `type: deb-install` and `scenario:` because # The legs keep `type: deb-install` and `scenario:` because
# testing/check-ci-parity.sh reads those to match this matrix against the local # testing/check-ci-parity.sh reads those to match this matrix against the local
# suite's distro list; the steps below use `scenario:` only. # suite's distro list; it reads `arch:` too, and compares only the amd64 legs
# with the local run. The steps below use `scenario:` and `arch:`.
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
deb-install: deb-install:
name: Deb install (${{ matrix.scenario }}) name: Deb install (${{ matrix.scenario }}${{ matrix.arch == 'arm64' && ' arm64' || '' }})
runs-on: ubuntu-latest runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
needs: [deb-package] needs: [deb-package]
if: ${{ !inputs.skip_integration }} if: ${{ !inputs.skip_integration }}
@@ -958,14 +988,27 @@ jobs:
include: include:
- type: deb-install - type: deb-install
scenario: debian12 scenario: debian12
arch: amd64
- type: deb-install - type: deb-install
scenario: debian13 scenario: debian13
arch: amd64
- type: deb-install - type: deb-install
scenario: ubuntu22 scenario: ubuntu22
arch: amd64
- type: deb-install - type: deb-install
scenario: ubuntu24 scenario: ubuntu24
arch: amd64
- type: deb-install - type: deb-install
scenario: ubuntu26 scenario: ubuntu26
arch: amd64
# The arm64 package on the oldest supported distribution: a fresh
# install and a daemon start. Deliberately GitHub-only (the local host
# is x86_64), and deliberately one leg: the upgrade, purge and
# conffile paths run under debian12 on amd64 only and stay
# unexercised on arm64.
- type: deb-install
scenario: ubuntu22
arch: arm64
steps: steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
@@ -973,7 +1016,7 @@ jobs:
- name: Download the .deb - name: Download the .deb
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with: with:
name: fips-deb name: fips-deb-${{ matrix.arch }}
path: _deb path: _deb
- name: Run deb-install scenario - name: Run deb-install scenario
+7
View File
@@ -283,6 +283,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
warning and a rebuild, never a failed build. A cached image is not refreshed warning and a rebuild, never a failed build. A cached image is not refreshed
from apt or the base image until the base image name, the toolchain or from apt or the base image until the base image name, the toolchain or
`Dockerfile.build` changes, as was already true of a developer's machine. `Dockerfile.build` changes, as was already true of a developer's machine.
- CI now builds the arm64 `.deb` on an arm64 runner and installs it on Ubuntu
22.04, the oldest supported distribution, starting the daemon, on every push
and pull request. Until now the arm64 package was floor-checked and never
installed anywhere in the pipeline. Its upgrade, purge and conffile paths
remain unexercised; those run on amd64 only. The parity check reads each
install leg's architecture, so the arm64 leg is reported as GitHub-only and
cannot stand in for a missing amd64 leg of the same distribution.
## [0.5.1] - 2026-09-06 ## [0.5.1] - 2026-09-06
+4 -2
View File
@@ -195,8 +195,10 @@ below. **Only the `.deb` is exercised by an install test**, by the
`deb-install` suite across debian12, debian13, ubuntu22, ubuntu24 and `deb-install` suite across debian12, debian13, ubuntu22, ubuntu24 and
ubuntu26; neither the AUR package nor the flake is. That suite runs on ubuntu26; neither the AUR package nor the flake is. That suite runs on
every push and pull request, on x86_64, against a `.deb` built by the same every push and pull request, on x86_64, against a `.deb` built by the same
pinned container as the released one. It does not run at a tag, and the pinned container as the released one. The arm64 package, built the same way
arm64 package is install-tested by nothing: no workflow installs a published on an arm64 runner, is installed and its daemon started on ubuntu22 on every
push and pull request as well; its upgrade, purge and conffile paths are not
exercised. The suite does not run at a tag: no workflow installs a published
artifact, so the released packages are checked by artifact, so the released packages are checked by
hand. OpenWrt is a musl hand. OpenWrt is a musl
target rather than glibc, and it takes an `.ipk` on 24.x and earlier or target rather than glibc, and it takes an `.ipk` on 24.x and earlier or
+3 -1
View File
@@ -110,7 +110,9 @@ it the suite builds one through `packaging/debian/build-deb-container.sh`.
Installs the built `.deb` in systemd containers for each Installs the built `.deb` in systemd containers for each
target distro and verifies unit enablement, conffile placement and target distro and verifies unit enablement, conffile placement and
end-to-end `.fips` resolution as a user would meet it. end-to-end `.fips` resolution as a user would meet it. GitHub CI also
installs the arm64 package on ubuntu22 on an arm64 runner, a leg the
local run cannot have and the parity check reports as GitHub-only.
### [boringtun/](boringtun/) -- WireGuard Throughput Baseline ### [boringtun/](boringtun/) -- WireGuard Throughput Baseline
+32 -2
View File
@@ -11,6 +11,11 @@
# unreliable on GitHub-hosted runners. # unreliable on GitHub-hosted runners.
# tor-directory — same; live Tor dependency. # tor-directory — same; live Tor dependency.
# #
# Deliberate GitHub-only (NOT in the local run), with reason:
# deb-install ubuntu22 on arm64 — the local host is x86_64 and cannot run an
# arm64 package. The GitHub leg installs the arm64 package
# and starts the daemon on an arm runner.
#
# What is compared, and at what granularity: # What is compared, and at what granularity:
# chaos — per scenario, plus its flags. GitHub fans each scenario # chaos — per scenario, plus its flags. GitHub fans each scenario
# into its own matrix leg carrying `scenario:` (and # into its own matrix leg carrying `scenario:` (and
@@ -21,7 +26,11 @@
# deb-install — per distro. GitHub splits into per-distro legs carrying # deb-install — per distro. GitHub splits into per-distro legs carrying
# `scenario:`, in a job of their own; local runs the same # `scenario:`, in a job of their own; local runs the same
# distro set in one suite, enumerated by ALL_SCENARIOS in # distro set in one suite, enumerated by ALL_SCENARIOS in
# deb-install/test.sh. # deb-install/test.sh. A leg's `arch:` defaults to amd64,
# and only amd64 legs are compared with the local set, so
# an arm64 leg cannot stand in for a missing amd64 leg of
# the same distro. An arm64 leg must name a distro the
# local suite knows; any other arch is unidentifiable.
# everything else — per suite name. # everything else — per suite name.
# #
# dns-resolver is the one leg still compared at leg granularity rather than # dns-resolver is the one leg still compared at leg granularity rather than
@@ -153,6 +162,10 @@ if not include:
f"{ci_yml_path}; cannot verify CI parity", file=sys.stderr) f"{ci_yml_path}; cannot verify CI parity", file=sys.stderr)
sys.exit(2) sys.exit(2)
github_chaos, github_deb, github = {}, set(), set() github_chaos, github_deb, github = {}, set(), set()
# Deliberate GitHub-only install legs on another architecture, by distro. Kept
# out of github_deb: were they in it, deleting the amd64 leg of a distro that
# also has an arm64 leg would leave the distro in the set and pass.
github_deb_extra = {}
malformed = [] malformed = []
for leg in include: for leg in include:
if "suite" not in leg and "scenario" not in leg: if "suite" not in leg and "scenario" not in leg:
@@ -166,8 +179,15 @@ for leg in include:
continue continue
if kind == "chaos": if kind == "chaos":
github_chaos[str(leg["scenario"])] = str(leg.get("chaos_flags", "")) github_chaos[str(leg["scenario"])] = str(leg.get("chaos_flags", ""))
else: continue
arch = str(leg.get("arch", "amd64"))
if arch == "amd64":
github_deb.add(str(leg["scenario"])) github_deb.add(str(leg["scenario"]))
elif arch == "arm64":
github_deb_extra.setdefault(arch, set()).add(str(leg["scenario"]))
else:
malformed.append(f"deb-install leg {leg['scenario']} has arch "
f"{arch}, which is neither amd64 nor arm64")
elif "suite" in leg: elif "suite" in leg:
github.add(str(leg["suite"])) github.add(str(leg["suite"]))
else: else:
@@ -233,6 +253,12 @@ chaos_flag_drift = sorted(
) )
deb_local_only = sorted(local_deb - github_deb) deb_local_only = sorted(local_deb - github_deb)
deb_github_only = sorted(github_deb - local_deb) deb_github_only = sorted(github_deb - local_deb)
# An extra-arch leg for a distro the local suite does not know is still drift.
deb_github_only += sorted(
f"{d} ({arch})"
for arch, distros in github_deb_extra.items()
for d in distros - local_deb
)
problems = (local_only or github_only or chaos_local_only or chaos_github_only problems = (local_only or github_only or chaos_local_only or chaos_github_only
or chaos_flag_drift or deb_local_only or deb_github_only or chaos_flag_drift or deb_local_only or deb_github_only
@@ -290,5 +316,9 @@ print("CI parity OK: both runners cover the same work "
print(f" {len(github)} suites, {len(github_chaos)} chaos scenarios " print(f" {len(github)} suites, {len(github_chaos)} chaos scenarios "
f"(flags compared), {len(github_deb)} deb-install distros " f"(flags compared), {len(github_deb)} deb-install distros "
f"— {total} legs on each side.") f"— {total} legs on each side.")
for arch, distros in sorted(github_deb_extra.items()):
legs = "leg" if len(distros) == 1 else "legs"
print(f" plus {len(distros)} GitHub-only {arch} install {legs} "
f"({', '.join(sorted(distros))}).")
sys.exit(0) sys.exit(0)
PY PY
+5
View File
@@ -114,6 +114,11 @@
# unreliable on GitHub-hosted runners. # unreliable on GitHub-hosted runners.
# tor-directory — same; live Tor dependency. # tor-directory — same; live Tor dependency.
# #
# Deliberate GitHub-only (NOT in this local run), with reason:
# deb-install ubuntu22 on arm64 — this host is x86_64 and cannot run an
# arm64 package. The guard compares it by distro only and
# does not let it stand in for the amd64 ubuntu22 leg.
#
# The two runners express the same work in different matrix shapes, and the # The two runners express the same work in different matrix shapes, and the
# guard compares through that shape rather than around it: chaos legs are # guard compares through that shape rather than around it: chaos legs are
# compared per scenario (and per flag), deb-install legs per distro. The one # compared per scenario (and per flag), deb-install legs per distro. The one