refactor: bump gh actions versions #72
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Linux CI | |
| # Builds the terminal client AT THE GLIBC FLOOR (Debian 11 / glibc 2.31, the same | |
| # base the release pipeline ships from), packages it (deb/rpm/arch), then INSTALLS | |
| # each package in a clean distro/arch matrix and smoke-tests it. The matrix is the | |
| # point: a successful install+run on, say, ubuntu:20.04 proves the glibc-2.31 floor | |
| # holds AND that the package's Avahi dependency resolves -- both of which the raw | |
| # build can't show (the build runner already has libavahi-client-dev, and a | |
| # modern-glibc build would silently raise the floor). Build here == build at release, | |
| # so green CI means the release artifacts will install the same way. | |
| on: | |
| push: | |
| branches: | |
| - master | |
| pull_request: | |
| branches: | |
| - master | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: linux-ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build: | |
| name: Build + package (${{ matrix.arch }}) | |
| runs-on: ${{ matrix.runner }} | |
| # Same old-glibc base as the release build-linux job (glibc 2.31 floor). Native | |
| # arm64 runner -- no QEMU; both arches run the identical container. | |
| container: | |
| image: debian:11 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - arch: amd64 | |
| runner: ubuntu-latest | |
| - arch: arm64 | |
| runner: ubuntu-24.04-arm | |
| defaults: | |
| run: | |
| shell: bash | |
| env: | |
| CLIPP_CACHE_DIR: ${{ github.workspace }}/clipp-cache | |
| VCPKG_ROOT: ${{ github.workspace }}/v | |
| VCPKG_DEFAULT_BINARY_CACHE: ${{ github.workspace }}/vcpkg-cache | |
| VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/vcpkg-cache,readwrite | |
| steps: | |
| # debian:11 is bare -- install the toolchain build_linux.sh expects plus the bits | |
| # vcpkg needs to build libsodium, the Avahi client headers, and the packaging | |
| # tools (gettext-base = envsubst for package_linux.sh; rpm + zstd let nfpm emit | |
| # .rpm / Arch packages and let the assertion below read them). | |
| - name: Install toolchain | |
| run: | | |
| apt-get update | |
| # NOT installing Debian 11's cmake (3.18.4 < our 3.20 floor); a recent Kitware | |
| # build is dropped on PATH in the next step. | |
| apt-get install -y --no-install-recommends \ | |
| build-essential ninja-build pkg-config git curl ca-certificates \ | |
| autoconf autoconf-archive automake libtool \ | |
| libavahi-client-dev libxcb1-dev \ | |
| gettext-base rpm zstd \ | |
| zip unzip tar | |
| - name: Install recent CMake | |
| run: | | |
| set -euo pipefail | |
| cmake_version=3.30.5 | |
| case "${{ matrix.arch }}" in | |
| amd64) cmake_arch=x86_64 ;; | |
| arm64) cmake_arch=aarch64 ;; | |
| *) echo "Unhandled arch ${{ matrix.arch }}" >&2; exit 1 ;; | |
| esac | |
| url="https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-linux-${cmake_arch}.tar.gz" | |
| mkdir -p /opt/cmake | |
| curl -fsSL "$url" | tar -xz --strip-components=1 -C /opt/cmake | |
| echo "/opt/cmake/bin" >> "$GITHUB_PATH" | |
| /opt/cmake/bin/cmake --version | |
| - name: Checkout clipp | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Read vcpkg baseline | |
| id: vcpkg-baseline | |
| run: | | |
| # Grep the 40-hex baseline straight out of the manifest -- the bare debian:11 | |
| # base has no python3, and a grep is sufficient for a single well-formed field. | |
| baseline="$(grep -oE '"builtin-baseline"[[:space:]]*:[[:space:]]*"[0-9a-f]+"' src/vcpkg.json | grep -oE '[0-9a-f]{40}')" | |
| if [[ -z "$baseline" ]]; then | |
| echo "src/vcpkg.json must define builtin-baseline for CI." >&2 | |
| exit 1 | |
| fi | |
| echo "ref=$baseline" >> "$GITHUB_OUTPUT" | |
| - name: Checkout vcpkg | |
| run: | | |
| git clone --no-tags https://github.com/microsoft/vcpkg.git "$VCPKG_ROOT" | |
| git -C "$VCPKG_ROOT" checkout --detach "${{ steps.vcpkg-baseline.outputs.ref }}" | |
| - name: Bootstrap vcpkg | |
| run: | | |
| "$VCPKG_ROOT/bootstrap-vcpkg.sh" -disableMetrics | |
| - name: Prepare vcpkg binary cache | |
| run: mkdir -p "$VCPKG_DEFAULT_BINARY_CACHE" | |
| # Restore/save split (see windows-ci.yml): a combined actions/cache step | |
| # deadlocks into a permanent rebuild loop after any toolchain drift. | |
| - name: Restore vcpkg binary cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| # Same key scheme as the release build-linux job, so CI and release share the | |
| # warm cache instead of each rebuilding the dependency tree from source. | |
| path: vcpkg-cache | |
| key: linux-${{ matrix.arch }}-glibc231-vcpkg-${{ hashFiles('src/vcpkg.json', 'src/vcpkg-triplets/**') }}-ci-${{ github.run_id }} | |
| restore-keys: | | |
| linux-${{ matrix.arch }}-glibc231-vcpkg-${{ hashFiles('src/vcpkg.json', 'src/vcpkg-triplets/**') }}- | |
| linux-${{ matrix.arch }}-glibc231-vcpkg- | |
| - name: Build Release | |
| # A synthetic 0.0.0.0 version: nothing in the build/package/smoke path depends on | |
| # the value (the release pipeline passes the real tag). | |
| run: bash ./scripts/build_linux.sh --release --version 0.0.0.0 | |
| - name: Save vcpkg binary cache | |
| uses: actions/cache/save@v5 | |
| with: | |
| path: vcpkg-cache | |
| key: linux-${{ matrix.arch }}-glibc231-vcpkg-${{ hashFiles('src/vcpkg.json', 'src/vcpkg-triplets/**') }}-ci-${{ github.run_id }} | |
| - name: Unit tests (doctest) | |
| # The engine + wire tests are pure, portable C++ (no platform code), so one arch | |
| # guards the logic for all of them; the arm64 build still gives cross-compiler | |
| # COMPILE coverage. Reuses the vcpkg tree the Release build just populated. | |
| if: matrix.arch == 'amd64' | |
| run: | | |
| set -euo pipefail | |
| cmake -S . -B build/tests -G Ninja \ | |
| -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" \ | |
| -DVCPKG_MANIFEST_DIR=src \ | |
| -DVCPKG_INSTALLED_DIR="$CLIPP_CACHE_DIR/vcpkg-installed" \ | |
| -DVCPKG_MANIFEST_FEATURES=tests \ | |
| -DCLIPP_BUILD_TESTS=ON \ | |
| -DCMAKE_BUILD_TYPE=Release | |
| cmake --build build/tests --target clipp-tests | |
| ctest --test-dir build/tests --output-on-failure | |
| - name: Verify dependency closure | |
| run: | | |
| set -euo pipefail | |
| binary="build/linux-release/clipp" | |
| if [[ ! -x "$binary" ]]; then | |
| echo "Missing expected executable: $binary" >&2 | |
| exit 1 | |
| fi | |
| echo "=== readelf -d NEEDED ===" | |
| readelf -d "$binary" | grep NEEDED || true | |
| # Static-runtime invariant: libstdc++/libgcc_s must NOT be dynamic deps, and the | |
| # vcpkg libs must be statically baked in. If any reappear, the package's declared | |
| # deps would be wrong (only avahi + libc may be dynamic). | |
| needed="$(readelf -d "$binary" | grep NEEDED || true)" | |
| for forbidden in libstdc++ libgcc_s libsodium libxxhash libzstd libutf8proc; do | |
| if echo "$needed" | grep -q "$forbidden"; then | |
| echo "Unexpected dynamic dependency on $forbidden -- static-link invariant broken." >&2 | |
| exit 1 | |
| fi | |
| done | |
| echo "Static-runtime invariant holds (only avahi + libc are dynamic)." | |
| - name: Smoke test the raw binary | |
| run: | | |
| set -euo pipefail | |
| binary="build/linux-release/clipp" | |
| # Isolate settings under a temp XDG dir so the file backend writes somewhere | |
| # inspectable and disposable. | |
| export XDG_CONFIG_HOME="$(mktemp -d)" | |
| echo "::group::clipp --help (headless bare-launch path)" | |
| help_out="$("$binary" --help)" | |
| echo "$help_out" | |
| for verb in copy paste key hostid; do | |
| echo "$help_out" | grep -qw "$verb" || { echo "help missing verb: $verb" >&2; exit 1; } | |
| done | |
| if echo "$help_out" | grep -qw gui; then | |
| echo "headless help unexpectedly lists a 'gui' subcommand" >&2 | |
| exit 1 | |
| fi | |
| echo "::endgroup::" | |
| echo "::group::hostid round-trip (Settings file backend)" | |
| "$binary" hostid reset | |
| id1="$("$binary" hostid show | sed -n 's/^hostid: //p')" | |
| id2="$("$binary" hostid show | sed -n 's/^hostid: //p')" | |
| if [[ -z "$id1" || "$id1" == "(none)" || "$id1" != "$id2" ]]; then | |
| echo "hostid not stable/persisted: '$id1' vs '$id2'" >&2 | |
| exit 1 | |
| fi | |
| echo "hostid: $id1" | |
| echo "::endgroup::" | |
| - name: Package (deb, rpm, Arch) + stage canonical names | |
| run: | | |
| set -euo pipefail | |
| arch="${{ matrix.arch }}" | |
| bash ./scripts/package_linux.sh \ | |
| --version 0.0.0.0 \ | |
| --binary build/linux-release/clipp \ | |
| --arch "$arch" \ | |
| --outdir dist | |
| mkdir -p packages | |
| # Version-LESS names matching the release assets (and what package-test expects). | |
| cp "$(ls dist/*.deb)" "packages/clipp-linux-$arch.deb" | |
| cp "$(ls dist/*.rpm)" "packages/clipp-linux-$arch.rpm" | |
| cp "$(ls dist/*.pkg.tar.zst)" "packages/clipp-linux-$arch.pkg.tar.zst" | |
| # Raw binary too, for the curl-and-chmod path that the install matrix doesn't cover. | |
| cp build/linux-release/clipp "packages/clipp-linux-$arch" | |
| ls -la packages/ | |
| - name: Assert each package ships /usr/bin/clipp | |
| run: | | |
| set -euo pipefail | |
| arch="${{ matrix.arch }}" | |
| # Capture listings into vars and match with a pipe-free `case`: a | |
| # `<lister> | grep -q` would SIGPIPE the lister under `pipefail` (grep -q | |
| # closes the pipe at first match) and falsely report "missing". | |
| has() { case "$1" in *"$2"*) return 0 ;; *) return 1 ;; esac; } | |
| deb_list="$(dpkg-deb -c "packages/clipp-linux-$arch.deb")" | |
| rpm_list="$(rpm -qlp "packages/clipp-linux-$arch.rpm")" | |
| arch_list="$(tar --zstd -tf "packages/clipp-linux-$arch.pkg.tar.zst")" | |
| has "$deb_list" './usr/bin/clipp' || has "$deb_list" 'usr/bin/clipp' || { echo "::error::deb ($arch) missing /usr/bin/clipp"; exit 1; } | |
| has "$rpm_list" '/usr/bin/clipp' || { echo "::error::rpm ($arch) missing /usr/bin/clipp"; exit 1; } | |
| has "$arch_list" 'usr/bin/clipp' || { echo "::error::arch ($arch) missing /usr/bin/clipp"; exit 1; } | |
| echo "All packages ship /usr/bin/clipp." | |
| - name: Upload packages | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: linux-packages-${{ matrix.arch }} | |
| path: packages/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| package-test: | |
| name: Install ${{ matrix.kind }} on ${{ matrix.image }} (${{ matrix.arch }}) | |
| needs: build | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # ubuntu:20.04 is the glibc-2.31 FLOOR guard -- a debian:11-built clipp must | |
| # install and RUN here, or the floor has been raised. The rest cover newer | |
| # glibc + the three package families (deb/rpm/arch). | |
| - { image: "ubuntu:20.04", arch: amd64, kind: deb, runner: ubuntu-latest } | |
| - { image: "debian:12", arch: amd64, kind: deb, runner: ubuntu-latest } | |
| - { image: "fedora:latest", arch: amd64, kind: rpm, runner: ubuntu-latest } | |
| - { image: "almalinux:9", arch: amd64, kind: rpm, runner: ubuntu-latest } | |
| - { image: "archlinux:latest", arch: amd64, kind: arch, runner: ubuntu-latest } | |
| # arm64: lighter smoke (one deb + one rpm) on a native ARM runner, no QEMU. | |
| - { image: "debian:12", arch: arm64, kind: deb, runner: ubuntu-24.04-arm } | |
| - { image: "fedora:latest", arch: arm64, kind: rpm, runner: ubuntu-24.04-arm } | |
| steps: | |
| - name: Download packages | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: linux-packages-${{ matrix.arch }} | |
| path: packages | |
| - name: Install and smoke-test in a clean container | |
| env: | |
| IMAGE: ${{ matrix.image }} | |
| ARCH: ${{ matrix.arch }} | |
| KIND: ${{ matrix.kind }} | |
| run: | | |
| set -euo pipefail | |
| # Install via the REAL package manager (not dpkg -i / rpm -i) so clipp's Avahi | |
| # dependency resolves from the distro repos -- the whole point of the matrix. | |
| # The smoke then runs clipp, which dynamically links libavahi-client.so.3: any | |
| # invocation (even --help) load-fails if the dep didn't resolve OR if the | |
| # binary's glibc floor is above the container's -- so one run guards both. | |
| docker run --rm -e IMAGE -e ARCH -e KIND -v "$PWD/packages:/pkg:ro" "$IMAGE" bash -c ' | |
| set -eu | |
| echo "=== install ($KIND, $ARCH) ===" | |
| case "$KIND" in | |
| deb) | |
| export DEBIAN_FRONTEND=noninteractive | |
| apt-get update -qq | |
| apt-get install -y "/pkg/clipp-linux-$ARCH.deb" | |
| ;; | |
| rpm) | |
| dnf install -y "/pkg/clipp-linux-$ARCH.rpm" | |
| ;; | |
| arch) | |
| pacman -Sy --noconfirm | |
| pacman -U --noconfirm "/pkg/clipp-linux-$ARCH.pkg.tar.zst" | |
| ;; | |
| *) echo "unknown kind: $KIND" >&2; exit 1 ;; | |
| esac | |
| echo "=== smoke ===" | |
| export XDG_CONFIG_HOME="$(mktemp -d)" | |
| fail=0 | |
| chk() { if eval "$2"; then echo " PASS: $1"; else echo " FAIL: $1"; fail=1; fi; } | |
| chk "clipp resolves on PATH" "command -v clipp >/dev/null" | |
| chk "clipp --help loads + runs" "clipp --help >/dev/null" | |
| chk "hostid reset/show persists" "clipp hostid reset && clipp hostid show | grep -vq \"(none)\"" | |
| chk "key show with no key" "clipp key show | grep -q \"fingerprint: (none)\"" | |
| if [ "$fail" -ne 0 ]; then echo "SMOKE FAILED: $IMAGE ($ARCH/$KIND)"; exit 1; fi | |
| echo "SMOKE OK: $IMAGE ($ARCH/$KIND)" | |
| ' |