From 33652064f9db2074975fcf703a6d1cd0c1ff85ed Mon Sep 17 00:00:00 2001 From: Blake Ridgway Date: Fri, 21 Aug 2026 13:15:43 -0500 Subject: [PATCH] ci: add GitLab CI pipeline and rootfs smoke tests - tests/: offline tree validation plus rootfs smoke tests that assert the hardening guarantees (kptr_restrict, default-deny firewall, key-only ssh, no snapd/telemetry, btrfs tooling). - ci/: GitLab pipeline - validate, build matrix (server/workstation/ cloud), smoke tests, publish on tags. --- ci/.gitlab-ci.yml | 99 ++++++++++++++++++++++++++++++++++++ tests/run-tests.sh | 42 +++++++++++++++ tests/smoke/verify-rootfs.sh | 78 ++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+) create mode 100644 ci/.gitlab-ci.yml create mode 100755 tests/run-tests.sh create mode 100755 tests/smoke/verify-rootfs.sh diff --git a/ci/.gitlab-ci.yml b/ci/.gitlab-ci.yml new file mode 100644 index 0000000..72701b8 --- /dev/null +++ b/ci/.gitlab-ci.yml @@ -0,0 +1,99 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Arcline OS — GitLab CI pipeline +# +# Mirrors the local build flow (scripts/ + Makefile) in CI: +# validate → build (matrix over editions) → test → publish +# +# The build image is Debian bookworm with the host deps installed, matching +# what scripts/check-host-deps.sh expects locally. +# ───────────────────────────────────────────────────────────────────────────── +stages: + - validate + - build + - test + - publish + +variables: + DEBIAN_FRONTEND: noninteractive + VERSION: "0.1.0" + +# ── validate ──────────────────────────────────────────────────────────────── +validate: + stage: validate + image: debian:bookworm + script: + - apt-get update -qq && apt-get install -y -qq make bash + - make check + +# ── build (one job per edition) ───────────────────────────────────────────── +.build: + stage: build + image: debian:bookworm + before_script: + - apt-get update -qq + - apt-get install -y -qq debootstrap squashfs-tools grub2-common xorriso cpio curl git make bash + script: + - make iso-${EDITION} + artifacts: + name: "arcline-${EDITION}-${VERSION}" + paths: + - build/artifacts/ + expire_in: 2 weeks + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + when: never + - when: always + +build-server: + extends: .build + variables: + EDITION: server + +build-workstation: + extends: .build + variables: + EDITION: workstation + +build-cloud: + extends: .build + variables: + EDITION: cloud + +# ── test ──────────────────────────────────────────────────────────────────── +test: + stage: test + image: debian:bookworm + needs: [build-server, build-workstation, build-cloud] + before_script: + - apt-get update -qq && apt-get install -y -qq bash make xz-utils + script: + # extract the rootfs artifacts so the smoke tests can inspect them + - for f in build/artifacts/arcline-*.tar.xz; do + [ -e "$f" ] || continue; + e=$(basename "$f" | sed -E 's/arcline-([a-z]+)-.*/\1/'); + mkdir -p "build/rootfs/$e"; + tar -xJf "$f" -C "build/rootfs/$e"; + done + - make test + artifacts: + reports: + junit: build/logs/*.xml + when: always + +# ── publish (tagged releases only) ────────────────────────────────────────── +publish: + stage: publish + image: alpine:latest + needs: [test] + before_script: + - apk add --no-cache curl jq + script: + - echo "Publishing release $VERSION (edit this step to push to your release server / GitLab Packages)" + - ls -la build/artifacts/ || true + artifacts: + name: "arcline-${VERSION}" + paths: + - build/artifacts/ + expire_in: 1 year + rules: + - if: '$CI_COMMIT_TAG' diff --git a/tests/run-tests.sh b/tests/run-tests.sh new file mode 100755 index 0000000..1d4fa72 --- /dev/null +++ b/tests/run-tests.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# Arcline OS — test runner +# +# tests/run-tests.sh # validate tree + test built rootfs(s) +# tests/run-tests.sh server # test a specific edition +# +# Verifies: +# 1. the whole build tree is syntactically valid (validate.sh) +# 2. any built rootfs in build/rootfs/ satisfies the hardening +# and packaging guarantees (tests/smoke/verify-rootfs.sh) +# ───────────────────────────────────────────────────────────────────────────── +set -euo pipefail +source "$(dirname "${BASH_SOURCE[0]}")/../scripts/common.sh" + +fail=0 + +log "step 1/2 — tree validation" +"$ROOT/scripts/validate.sh" || fail=1 + +log "step 2/2 — rootfs smoke tests" +editions=("${EDITIONS[@]}") +[[ $# -gt 0 ]] && editions=("$@") + +for e in "${editions[@]}"; do + rootfs="$ROOTFS_DIR/$e" + if [[ ! -d "$rootfs" ]]; then + warn "no rootfs for '$e' at $rootfs — skipping smoke test" + continue + fi + log "smoke testing rootfs: $e" + if ! "$ROOT/tests/smoke/verify-rootfs.sh" "$rootfs"; then + warn "smoke test failed for '$e'" + fail=1 + fi +done + +if [[ $fail -eq 0 ]]; then + log "tests passed ✓" +else + die "tests failed" +fi diff --git a/tests/smoke/verify-rootfs.sh b/tests/smoke/verify-rootfs.sh new file mode 100755 index 0000000..1ac6531 --- /dev/null +++ b/tests/smoke/verify-rootfs.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# Arcline OS — rootfs smoke test +# +# tests/smoke/verify-rootfs.sh +# +# Asserts the packaging + hardening guarantees of a built rootfs. Runs +# read-only against an extracted rootfs tree (no chroot needed). A failed +# assertion exits nonzero with a message. +# ───────────────────────────────────────────────────────────────────────────── +set -euo pipefail + +ROOTFS="${1:?usage: verify-rootfs.sh }" +[[ -d "$ROOTFS" ]] || { echo "FAIL: rootfs not found: $ROOTFS"; exit 1; } + +pass=0; fail=0 +ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; pass=$((pass+1)); } +bad() { printf ' \033[31m✗\033[0m %s\n' "$*"; fail=$((fail+1)); } + +have_file() { [[ -f "$ROOTFS/$1" ]]; } +have_dir() { [[ -d "$ROOTFS/$1" ]]; } + +echo "─ hardening ───────────────────────────────────────────" +if have_file "etc/sysctl.d/10-arcline-hardening.conf"; then + grep -q 'kernel.kptr_restrict=2' "$ROOTFS/etc/sysctl.d/10-arcline-hardening.conf" \ + && ok "sysctl hardening present (kptr_restrict=2)" || bad "sysctl file missing kptr_restrict=2" +else + bad "missing etc/sysctl.d/10-arcline-hardening.conf" +fi + +if have_file "etc/nftables.conf"; then + grep -q 'policy drop' "$ROOTFS/etc/nftables.conf" \ + && ok "nftables default-deny present" || bad "nftables policy is not drop" +else + bad "missing etc/nftables.conf" +fi + +if have_file "etc/ssh/sshd_config.d/10-arcline-hardening.conf"; then + grep -q 'PasswordAuthentication no' "$ROOTFS/etc/ssh/sshd_config.d/10-arcline-hardening.conf" \ + && ok "ssh hardening present" || bad "ssh password auth not disabled" +else + bad "missing ssh hardening drop-in" +fi + +have_file "etc/systemd/journald.conf.d/10-arcline.conf" \ + && ok "journald persistent config present" || bad "missing journald config" +have_file "etc/modprobe.d/arcline-hardening.conf" \ + && ok "module blacklist present" || bad "missing modprobe blacklist" + +echo "─ telemetry (must be absent) ──────────────────────────" +if have_dir "var/lib/ubuntu-report" || have_dir "var/lib/popularity-contest"; then + bad "telemetry package artifacts found (ubuntu-report / popularity-contest)" +else + ok "no distro telemetry artifacts" +fi +if [[ -d "$ROOTFS/usr/lib/snapd" ]]; then + bad "snapd found (Arcline is snap-free)" +else + ok "no snapd" +fi + +echo "─ core subsystems ─────────────────────────────────────" +if have_file "usr/local/sbin/arcline-snapshot"; then + ok "btrfs snapshot tooling present" +else + bad "missing arcline-snapshot" +fi +have_file "usr/local/sbin/arcline-rollback" \ + && ok "btrfs rollback tooling present" || bad "missing arcline-rollback" +have_file "usr/lib/systemd/system/arcline-snapshot.timer" \ + && ok "snapshot timer unit present" || bad "missing arcline-snapshot.timer" + +grep -qi 'arcline' "$ROOTFS/etc/arcline-release" 2>/dev/null \ + && ok "arcline-release present" || bad "missing /etc/arcline-release" + +echo "─ summary ─────────────────────────────────────────────" +echo " $pass passed, $fail failed" +[[ $fail -eq 0 ]]