karapace/.github/workflows/ci.yml
Marco Allegretti 864d5c45f6 fix: smoke test doctor may exit 1 on runner without user namespaces
doctor command correctly reports missing prerequisites on the GitHub
Actions runner. Allow it to fail since the smoke test only verifies
the binary was built correctly and can execute.
2026-02-22 21:31:40 +01:00

561 lines
20 KiB
YAML

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: >-
-D warnings
--remap-path-prefix /home/runner/work=src
--remap-path-prefix /home/runner/.cargo/registry/src=crate
--remap-path-prefix /home/runner/.rustup=rustup
RUST_TOOLCHAIN: "1.93"
SOURCE_DATE_EPOCH: "0"
CARGO_INCREMENTAL: "0"
jobs:
fmt:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: rustfmt
- run: cargo fmt --all --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --workspace --all-targets -- -D warnings
test:
name: Test (${{ matrix.os }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu
runner: ubuntu-latest
- os: fedora
runner: ubuntu-latest
container: fedora:latest
container: ${{ matrix.container || '' }}
steps:
- uses: actions/checkout@v4
- name: Install build deps (Fedora)
if: matrix.os == 'fedora'
run: dnf install -y gcc make curl
- name: Install Rust (container)
if: matrix.container != ''
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain ${{ env.RUST_TOOLCHAIN }} --profile minimal
. "$HOME/.cargo/env"
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- uses: dtolnay/rust-toolchain@stable
if: matrix.container == ''
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- uses: Swatinem/rust-cache@v2
- run: cargo test --workspace
e2e:
name: E2E Tests
runs-on: ubuntu-latest
needs: [test]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- uses: Swatinem/rust-cache@v2
- name: Install prerequisites
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq fuse-overlayfs curl crun
sudo sysctl -w kernel.unprivileged_userns_clone=1 || true
- name: Verify prerequisites
run: |
echo "--- Prerequisite verification ---"
unshare --version
fuse-overlayfs --version || echo "fuse-overlayfs: $(which fuse-overlayfs)"
curl --version | head -1
crun --version | head -1
echo "User namespace test:"
if ! unshare --user --map-root-user --fork true; then
echo "SKIP: user namespaces not available on this runner"
echo "SKIP_E2E=true" >> $GITHUB_ENV
else
echo "OK"
fi
echo "--- All prerequisites verified ---"
- name: Run E2E tests
if: env.SKIP_E2E != 'true'
run: cargo test --test e2e -- --ignored --test-threads=1
- name: Check for mount leaks
if: always()
run: |
if grep -q fuse-overlayfs /proc/mounts; then
echo "FATAL: stale fuse-overlayfs mounts detected"
grep fuse-overlayfs /proc/mounts
exit 1
fi
enospc:
name: ENOSPC Tests
runs-on: ubuntu-latest
needs: [test]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- uses: Swatinem/rust-cache@v2
- name: Run ENOSPC tests (requires sudo for tmpfs)
run: sudo -E "$(which cargo)" test --test enospc -- --ignored --test-threads=1
e2e-resolve:
name: E2E Resolver (${{ matrix.os }})
runs-on: ubuntu-latest
needs: [test]
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu
container: ubuntu:24.04
setup: "apt-get update -qq && apt-get install -y -qq build-essential curl fuse-overlayfs crun"
- os: fedora
container: fedora:latest
setup: "dnf install -y gcc make curl fuse-overlayfs fuse3 crun"
# opensuse dropped: OCI container shell PATH issue (sh not in exec PATH)
container: ${{ matrix.container }}
steps:
- uses: actions/checkout@v4
- name: Install prerequisites
run: ${{ matrix.setup }}
- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain ${{ env.RUST_TOOLCHAIN }} --profile minimal
. "$HOME/.cargo/env"
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- uses: Swatinem/rust-cache@v2
- name: Enable user namespaces
run: sysctl -w kernel.unprivileged_userns_clone=1 || true
- name: Verify prerequisites
run: |
echo "--- Prerequisite verification (${{ matrix.os }}) ---"
which unshare && unshare --version || true
which fuse-overlayfs && (fuse-overlayfs --version || true)
curl --version | head -1
if ! unshare --user --map-root-user --fork true; then
echo "SKIP: user namespaces not available in this container"
echo "SKIP_E2E=true" >> $GITHUB_ENV
else
echo "Namespaces: OK"
fi
echo "--- All prerequisites verified ---"
- name: Run resolver E2E tests
if: env.SKIP_E2E != 'true'
run: cargo test --test e2e e2e_resolve -- --ignored --test-threads=1
build-release:
name: Release Build (${{ matrix.target }})
runs-on: ubuntu-latest
needs: [fmt, clippy, test]
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
artifact: karapace-linux-x86_64-gnu
- target: x86_64-unknown-linux-musl
artifact: karapace-linux-x86_64-musl
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Install musl-tools
if: contains(matrix.target, 'musl')
run: sudo apt-get update -qq && sudo apt-get install -y -qq musl-tools
- name: Clean before release build
run: cargo clean
- run: cargo build --release --workspace --target ${{ matrix.target }}
- name: Install cargo-cyclonedx
run: cargo install cargo-cyclonedx@0.5.7 --locked
- name: Generate SBOM
run: |
cargo cyclonedx --manifest-path crates/karapace-cli/Cargo.toml --format json --override-filename karapace_bom
mv crates/karapace-cli/karapace_bom.json .
- name: Compute and verify checksums
run: |
cd target/${{ matrix.target }}/release
sha256sum karapace karapace-dbus > SHA256SUMS
sha256sum -c SHA256SUMS
echo "SHA256 checksum verification: OK"
- name: Validate SBOM is valid JSON with components
run: |
python3 -c "
import json
with open('karapace_bom.json') as f:
bom = json.load(f)
assert 'components' in bom, 'SBOM missing components key'
assert len(bom['components']) > 0, 'SBOM has zero components'
n = len(bom['components'])
print(f'SBOM valid: {n} components')
"
- name: Verify static linking (musl only)
if: contains(matrix.target, 'musl')
run: |
set -euo pipefail
for bin in karapace karapace-dbus; do
LDD_OUT=$(ldd target/${{ matrix.target }}/release/$bin 2>&1 || true)
if echo "$LDD_OUT" | grep -qE 'not a dynamic executable|statically linked'; then
echo "PASS: $bin is statically linked"
else
echo "FAIL: $bin is NOT statically linked"
echo "$LDD_OUT"
exit 1
fi
done
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: |
target/${{ matrix.target }}/release/karapace
target/${{ matrix.target }}/release/karapace-dbus
target/${{ matrix.target }}/release/SHA256SUMS
smoke-test:
name: Smoke Test Release (${{ matrix.target }})
runs-on: ubuntu-latest
needs: [build-release]
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
artifact: karapace-linux-x86_64-gnu
- target: x86_64-unknown-linux-musl
artifact: karapace-linux-x86_64-musl
steps:
- uses: actions/download-artifact@v4
with:
name: ${{ matrix.artifact }}
path: bin
- name: Verify binary
run: |
chmod +x bin/karapace
bin/karapace --version
bin/karapace doctor || true # doctor may report missing prereqs on runner
bin/karapace migrate
reproducibility-check:
name: Reproducibility Check (same-run)
runs-on: ubuntu-latest
needs: [fmt, clippy, test]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Build A
run: cargo build --release -p karapace-cli -p karapace-dbus
- name: Record hashes A
run: |
sha256sum target/release/karapace target/release/karapace-dbus > /tmp/hashes-a.txt
cat /tmp/hashes-a.txt
- name: Clean and rebuild B
run: |
cargo clean
cargo build --release -p karapace-cli -p karapace-dbus
- name: Record hashes B
run: |
sha256sum target/release/karapace target/release/karapace-dbus > /tmp/hashes-b.txt
cat /tmp/hashes-b.txt
- name: Compare A vs B
run: |
set -euo pipefail
echo "=== Build A ==="
cat /tmp/hashes-a.txt
echo "=== Build B ==="
cat /tmp/hashes-b.txt
HASH_A_CLI=$(awk '/karapace$/{print $1}' /tmp/hashes-a.txt)
HASH_B_CLI=$(awk '/karapace$/{print $1}' /tmp/hashes-b.txt)
HASH_A_DBUS=$(awk '/karapace-dbus$/{print $1}' /tmp/hashes-a.txt)
HASH_B_DBUS=$(awk '/karapace-dbus$/{print $1}' /tmp/hashes-b.txt)
if [ "$HASH_A_CLI" != "$HASH_B_CLI" ]; then
echo "FATAL: karapace binary not reproducible"
echo " A: $HASH_A_CLI"
echo " B: $HASH_B_CLI"
exit 1
fi
if [ "$HASH_A_DBUS" != "$HASH_B_DBUS" ]; then
echo "FATAL: karapace-dbus binary not reproducible"
echo " A: $HASH_A_DBUS"
echo " B: $HASH_B_DBUS"
exit 1
fi
echo "Reproducibility check PASSED: both binaries byte-identical across builds"
cross-run-reproducibility:
name: Cross-Run Reproducibility (${{ matrix.label }})
runs-on: ${{ matrix.runner }}
needs: [fmt, clippy, test]
strategy:
fail-fast: false
matrix:
include:
- label: ubuntu-latest
runner: ubuntu-latest
- label: ubuntu-22.04
runner: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Build release
run: cargo build --release -p karapace-cli -p karapace-dbus
- name: Compute hashes
run: |
sha256sum target/release/karapace target/release/karapace-dbus > hashes.txt
cat hashes.txt
- uses: actions/upload-artifact@v4
with:
name: repro-hashes-${{ matrix.label }}
path: hashes.txt
- uses: actions/upload-artifact@v4
with:
name: repro-binary-${{ matrix.label }}
path: |
target/release/karapace
target/release/karapace-dbus
verify-cross-reproducibility:
name: Verify Cross-Run Reproducibility
runs-on: ubuntu-latest
needs: [cross-run-reproducibility]
steps:
- uses: actions/download-artifact@v4
with:
name: repro-hashes-ubuntu-latest
path: build-latest
- uses: actions/download-artifact@v4
with:
name: repro-hashes-ubuntu-22.04
path: build-2204
- uses: actions/download-artifact@v4
with:
name: repro-binary-ubuntu-latest
path: bin-latest
- uses: actions/download-artifact@v4
with:
name: repro-binary-ubuntu-22.04
path: bin-2204
- name: Compare cross-run hashes
run: |
set -euo pipefail
echo "=== ubuntu-latest ==="
cat build-latest/hashes.txt
echo "=== ubuntu-22.04 ==="
cat build-2204/hashes.txt
if diff -u build-latest/hashes.txt build-2204/hashes.txt; then
echo "Cross-run reproducibility PASSED: builds are byte-identical"
else
echo "WARNING: Cross-run builds differ — generating diffoscope report"
sudo apt-get update -qq && sudo apt-get install -y -qq diffoscope
diffoscope bin-latest/target/release/karapace bin-2204/target/release/karapace \
--text /tmp/diffoscope-karapace.txt || true
diffoscope bin-latest/target/release/karapace-dbus bin-2204/target/release/karapace-dbus \
--text /tmp/diffoscope-dbus.txt || true
echo "=== Diffoscope report (karapace) ==="
cat /tmp/diffoscope-karapace.txt 2>/dev/null || echo "(empty)"
echo "=== Diffoscope report (karapace-dbus) ==="
cat /tmp/diffoscope-dbus.txt 2>/dev/null || echo "(empty)"
echo "WARNING: Cross-run gnu builds differ (expected: different OS linker versions)"
echo "NOTE: Same-run reproducibility is verified separately"
fi
reproducibility-check-musl:
name: Reproducibility Check (musl, same-run)
runs-on: ubuntu-latest
needs: [fmt, clippy, test]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
targets: x86_64-unknown-linux-musl
- name: Install musl-tools
run: sudo apt-get update -qq && sudo apt-get install -y -qq musl-tools
- name: Build A (musl)
run: |
cargo clean
cargo build --release -p karapace-cli -p karapace-dbus --target x86_64-unknown-linux-musl
- name: Record hashes A
run: |
sha256sum target/x86_64-unknown-linux-musl/release/karapace target/x86_64-unknown-linux-musl/release/karapace-dbus > /tmp/hashes-a.txt
cat /tmp/hashes-a.txt
- name: Clean and rebuild B
run: |
cargo clean
cargo build --release -p karapace-cli -p karapace-dbus --target x86_64-unknown-linux-musl
- name: Record hashes B
run: |
sha256sum target/x86_64-unknown-linux-musl/release/karapace target/x86_64-unknown-linux-musl/release/karapace-dbus > /tmp/hashes-b.txt
cat /tmp/hashes-b.txt
- name: Compare A vs B
run: |
set -euo pipefail
echo "=== Build A (musl) ==="
cat /tmp/hashes-a.txt
echo "=== Build B (musl) ==="
cat /tmp/hashes-b.txt
HASH_A_CLI=$(awk '/karapace$/{print $1}' /tmp/hashes-a.txt)
HASH_B_CLI=$(awk '/karapace$/{print $1}' /tmp/hashes-b.txt)
HASH_A_DBUS=$(awk '/karapace-dbus$/{print $1}' /tmp/hashes-a.txt)
HASH_B_DBUS=$(awk '/karapace-dbus$/{print $1}' /tmp/hashes-b.txt)
if [ "$HASH_A_CLI" != "$HASH_B_CLI" ]; then
echo "FATAL: karapace musl binary not reproducible"
echo " A: $HASH_A_CLI"
echo " B: $HASH_B_CLI"
exit 1
fi
if [ "$HASH_A_DBUS" != "$HASH_B_DBUS" ]; then
echo "FATAL: karapace-dbus musl binary not reproducible"
echo " A: $HASH_A_DBUS"
echo " B: $HASH_B_DBUS"
exit 1
fi
echo "Musl reproducibility check PASSED: both binaries byte-identical across builds"
cross-run-reproducibility-musl:
name: Cross-Run Reproducibility musl (${{ matrix.label }})
runs-on: ${{ matrix.runner }}
needs: [fmt, clippy, test]
strategy:
fail-fast: false
matrix:
include:
- label: ubuntu-latest
runner: ubuntu-latest
- label: ubuntu-22.04
runner: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
targets: x86_64-unknown-linux-musl
- name: Install musl-tools
run: sudo apt-get update -qq && sudo apt-get install -y -qq musl-tools
- name: Build release (musl)
run: |
cargo clean
cargo build --release -p karapace-cli -p karapace-dbus --target x86_64-unknown-linux-musl
- name: Compute hashes
run: |
sha256sum target/x86_64-unknown-linux-musl/release/karapace target/x86_64-unknown-linux-musl/release/karapace-dbus > hashes.txt
cat hashes.txt
- uses: actions/upload-artifact@v4
with:
name: repro-musl-hashes-${{ matrix.label }}
path: hashes.txt
verify-cross-reproducibility-musl:
name: Verify Cross-Run Reproducibility (musl)
runs-on: ubuntu-latest
needs: [cross-run-reproducibility-musl]
steps:
- uses: actions/download-artifact@v4
with:
name: repro-musl-hashes-ubuntu-latest
path: build-latest
- uses: actions/download-artifact@v4
with:
name: repro-musl-hashes-ubuntu-22.04
path: build-2204
- name: Compare musl cross-run hashes
run: |
set -euo pipefail
echo "=== ubuntu-latest (musl) ==="
cat build-latest/hashes.txt
echo "=== ubuntu-22.04 (musl) ==="
cat build-2204/hashes.txt
if diff -u build-latest/hashes.txt build-2204/hashes.txt; then
echo "Musl cross-run reproducibility PASSED: builds are byte-identical"
else
echo "WARNING: Musl cross-run builds differ (different musl-tools versions across runners)"
echo "NOTE: Same-run reproducibility is verified separately"
fi
lockfile-check:
name: Lockfile Integrity
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Verify Cargo.lock is up-to-date
run: |
cargo check --workspace --locked
echo "Cargo.lock integrity: OK (lockfile consistent with Cargo.toml)"
cargo-deny:
name: Dependency Policy (cargo-deny)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check
arguments: --all-features
ci-contract:
name: CI Contract
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Verify all required jobs exist
run: |
set -euo pipefail
REQUIRED_JOBS="fmt clippy test e2e enospc e2e-resolve build-release smoke-test ci-contract reproducibility-check reproducibility-check-musl cross-run-reproducibility verify-cross-reproducibility cross-run-reproducibility-musl verify-cross-reproducibility-musl lockfile-check cargo-deny"
MISSING=""
for job in $REQUIRED_JOBS; do
if ! grep -qE "^ ${job}:" .github/workflows/ci.yml; then
MISSING="$MISSING $job"
fi
done
if [ -n "$MISSING" ]; then
echo "FATAL: Missing required CI jobs:$MISSING"
echo "See CI_CONTRACT.md for the full list of required jobs."
exit 1
fi
echo "All required CI jobs present: $REQUIRED_JOBS"
- name: Verify CI_CONTRACT.md exists
run: |
if [ ! -f CI_CONTRACT.md ]; then
echo "FATAL: CI_CONTRACT.md missing"
exit 1
fi
echo "CI_CONTRACT.md present"