diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83aa84ad..d53d00ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,10 @@ env: # unreliable on GitHub-hosted runners. # 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 # parity guard compares through that shape rather than around it: chaos legs # are compared per scenario (and per flag) via their `scenario:` field, @@ -787,11 +791,24 @@ jobs: # floor violation stops the run. # ───────────────────────────────────────────────────────────────────────────── deb-package: - name: Build .deb - runs-on: ubuntu-latest + name: Build .deb${{ matrix.deb_arch == 'arm64' && ' (arm64)' || '' }} + runs-on: ${{ matrix.os }} needs: [build, test] 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: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 @@ -824,11 +841,23 @@ jobs: path: ${{ runner.temp }}/deb-builder-image.tar 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 timeout-minutes: 30 + shell: bash run: | + set -euo pipefail 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 # and saved it, so its presence is what says there is something to save. @@ -856,8 +885,8 @@ jobs: - name: Upload the .deb uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: fips-deb - path: deploy/fips_*.deb + name: fips-deb-${{ matrix.deb_arch }} + path: deploy/fips_*_${{ matrix.deb_arch }}.deb if-no-files-found: error retention-days: 1 @@ -901,7 +930,7 @@ jobs: - name: Download the .deb uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: fips-deb + name: fips-deb-amd64 path: _deb - name: Run dns-resolver test @@ -944,11 +973,12 @@ jobs: # # The legs keep `type: deb-install` and `scenario:` because # 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: - name: Deb install (${{ matrix.scenario }}) - runs-on: ubuntu-latest + name: Deb install (${{ matrix.scenario }}${{ matrix.arch == 'arm64' && ' arm64' || '' }}) + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} needs: [deb-package] if: ${{ !inputs.skip_integration }} @@ -958,14 +988,27 @@ jobs: include: - type: deb-install scenario: debian12 + arch: amd64 - type: deb-install scenario: debian13 + arch: amd64 - type: deb-install scenario: ubuntu22 + arch: amd64 - type: deb-install scenario: ubuntu24 + arch: amd64 - type: deb-install 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: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 @@ -973,7 +1016,7 @@ jobs: - name: Download the .deb uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: - name: fips-deb + name: fips-deb-${{ matrix.arch }} path: _deb - name: Run deb-install scenario diff --git a/CHANGELOG.md b/CHANGELOG.md index c2ad5efe..24b0e88c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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. +- 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 diff --git a/README.md b/README.md index 6c26c371..1af760f1 100644 --- a/README.md +++ b/README.md @@ -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 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 -pinned container as the released one. It does not run at a tag, and the -arm64 package is install-tested by nothing: no workflow installs a published +pinned container as the released one. The arm64 package, built the same way +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 hand. OpenWrt is a musl target rather than glibc, and it takes an `.ipk` on 24.x and earlier or diff --git a/testing/README.md b/testing/README.md index aeb6aec2..2c006fac 100644 --- a/testing/README.md +++ b/testing/README.md @@ -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 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 diff --git a/testing/check-ci-parity.sh b/testing/check-ci-parity.sh index d0b029f5..d69eeaad 100755 --- a/testing/check-ci-parity.sh +++ b/testing/check-ci-parity.sh @@ -11,6 +11,11 @@ # unreliable on GitHub-hosted runners. # 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: # chaos — per scenario, plus its flags. GitHub fans each scenario # into its own matrix leg carrying `scenario:` (and @@ -21,7 +26,11 @@ # deb-install — per distro. GitHub splits into per-distro legs carrying # `scenario:`, in a job of their own; local runs the same # 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. # # 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) sys.exit(2) 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 = [] for leg in include: if "suite" not in leg and "scenario" not in leg: @@ -166,8 +179,15 @@ for leg in include: continue if kind == "chaos": 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"])) + 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: github.add(str(leg["suite"])) else: @@ -233,6 +253,12 @@ chaos_flag_drift = sorted( ) deb_local_only = sorted(local_deb - github_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 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 " f"(flags compared), {len(github_deb)} deb-install distros " 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) PY diff --git a/testing/ci-local.sh b/testing/ci-local.sh index daaa7de3..ecab9d5e 100755 --- a/testing/ci-local.sh +++ b/testing/ci-local.sh @@ -114,6 +114,11 @@ # unreliable on GitHub-hosted runners. # 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 # 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