Skip to content

fix(linux): enforce reproducible AppImage build baseline #114

fix(linux): enforce reproducible AppImage build baseline

fix(linux): enforce reproducible AppImage build baseline #114

Workflow file for this run

name: CI
on:
push:
branches: [main, master, linux]
tags: ['v*']
pull_request:
branches: [main, master, linux]
# Keep CI read-only and free of secrets; release verification scans untrusted changes.
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# Python — the UI and its tests, on both platforms that ship a build.
python:
name: Python ${{ matrix.python-version }} (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
# 3.14 is the interpreter the alpha is actually built with (the shipped
# bundle contains python314.dll). 3.12 is the floor we claim to support.
python-version: ["3.12", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: |
requirements-alpha.txt
requirements-dev.txt
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update -q
sudo apt-get install -q -y \
cmake g++ pkg-config libpulse0 libpulse-dev libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 \
libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 \
libxcb-xinerama0 libxkbcommon-x11-0 libx11-dev libxext-dev libxfixes-dev \
libwayland-dev wayland-protocols libpipewire-0.3-dev libdbus-1-dev
- name: Install pinned dependencies
# The alpha lock is the ONLY install path. If it does not resolve on a
# supported interpreter, that is a release bug, not a CI inconvenience.
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements-alpha.txt -r requirements-dev.txt
- name: Assert the environment matches the lock
# Catches a transitive dependency that resolved to something other than
# what requirements-alpha.txt pins — the exact AUDIT-009 failure mode.
run: python -m pip check
- name: Assert imageio-ffmpeg is absent (AUDIT-005)
shell: bash
run: |
if python -m pip show imageio-ffmpeg > /dev/null 2>&1; then
echo "::error::imageio-ffmpeg is installed. It bundles a GPLv3 FFmpeg"
echo "::error::and must never enter a build environment."
exit 1
fi
echo "imageio-ffmpeg absent."
- name: Install Windows FFmpeg runtime for media tests
if: runner.os == 'Windows'
shell: pwsh
run: |
python tools/fetch_third_party.py --ffmpeg
$ffmpeg = "${{ github.workspace }}\\FTHRcapture\\FTHRclips\\third_party\\ffmpeg\\bin"
"Path=$ffmpeg;$env:Path" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8
- name: Prepare Linux test artifacts
if: runner.os == 'Linux'
run: |
sudo apt-get install -q -y cmake g++ pkg-config libpulse-dev libx11-dev libxext-dev libxfixes-dev libwayland-dev wayland-protocols libpipewire-0.3-dev libdbus-1-dev
python tools/fetch_third_party.py --ffmpeg-linux
echo "LD_LIBRARY_PATH=${{ github.workspace }}/FTHRcapture_linux/third_party/ffmpeg/lib:${LD_LIBRARY_PATH:-}" >> "$GITHUB_ENV"
echo "PATH=${{ github.workspace }}/FTHRcapture_linux/third_party/ffmpeg/bin:${PATH}" >> "$GITHUB_ENV"
cmake -B FTHRcapture_linux/build -S FTHRcapture_linux -DCMAKE_BUILD_TYPE=Release -DFTHR_FFMPEG_ROOT=${{ github.workspace }}/FTHRcapture_linux/third_party/ffmpeg
cmake --build FTHRcapture_linux/build -j"$(nproc)"
- name: Byte-compile all sources
# A syntax error in a rarely imported module otherwise only shows up at
# runtime, inside a frozen bundle, on a user's machine.
run: python -m compileall -q FTHR_UI tests tools
- name: Lint
run: python -m ruff check .
- name: Tests
shell: bash
env:
QT_QPA_PLATFORM: offscreen
run: |
if [ "${{ runner.os }}" = "Linux" ]; then
python -m pytest tests/ -v --tb=short \
--ignore=tests/test_linux_appimage_report.py \
--ignore=tests/test_linux_runtime.py \
--ignore=tests/test_linux_tools.py \
--ignore=tests/test_capture_backend_detection.py \
--ignore=tests/test_windows_monitor.py \
--ignore=tests/test_windows_native_hotkeys.py \
--ignore=tests/test_windows_autostart.py \
--ignore=tests/test_windows_installer_contract.py \
--ignore=tests/test_windows_lifecycle_contract.py \
--ignore=tests/test_windows_hardware_qualification_args.py \
--ignore=tests/test_windows_microphone_devices.py
else
python -m pytest tests/ -v --tb=short \
--ignore=tests/test_linux_appimage_report.py \
--ignore=tests/test_linux_runtime.py \
--ignore=tests/test_linux_tools.py \
--ignore=tests/test_capture_backend_detection.py \
--ignore=tests/test_license_gate_linux.py \
--ignore=tests/test_windows_monitor.py \
--ignore=tests/test_windows_native_hotkeys.py \
--ignore=tests/test_windows_autostart.py \
--ignore=tests/test_windows_installer_contract.py \
--ignore=tests/test_windows_lifecycle_contract.py \
--ignore=tests/test_windows_hardware_qualification_args.py \
--ignore=tests/test_windows_microphone_devices.py
fi
# Linux capture engine
linux-engine:
name: Linux engine (CMake)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Install build dependencies
run: |
sudo apt-get update -q
sudo apt-get install -q -y \
cmake g++ pkg-config \
libpulse-dev libx11-dev libxext-dev libxfixes-dev \
libwayland-dev wayland-protocols libwayland-bin \
libpipewire-0.3-dev libdbus-1-dev
- name: Fetch pinned Linux FFmpeg
run: python tools/fetch_third_party.py --ffmpeg-linux
- name: Configure
run: >
cmake -B FTHRcapture_linux/build -S FTHRcapture_linux
-DCMAKE_BUILD_TYPE=Release
-DFTHR_FFMPEG_ROOT=${{ github.workspace }}/FTHRcapture_linux/third_party/ffmpeg
- name: Build
# Warnings are surfaced but not fatal yet: the engine has never been
# built warning-clean and turning them into errors here would make the
# first green run impossible. Tighten once the backlog is cleared.
run: |
cmake --build FTHRcapture_linux/build -j"$(nproc)" 2>&1 | tee build.log
echo "--- warning summary ---"
grep -c "warning:" build.log || echo "0 warnings"
- name: Fail on compile errors
run: |
if grep -q "error:" build.log; then
echo "::error::compile errors in the Linux engine"
exit 1
fi
- name: Verify the binary exists
run: test -x FTHRcapture_linux/build/FTHRclips
- name: Native contract tests
run: ctest --test-dir FTHRcapture_linux/build --output-on-failure
- name: Verify licences and a clean source tree
run: |
python tools/verify_release_licenses.py --tree .
git diff --exit-code
# Deliberately NOT run: the engine's own capture path. A GitHub runner
# has no compositor, no DRM device and no audio server. Starting it here
# would test the failure path and prove nothing about capture.
# Windows capture engine
windows-engine:
name: Windows engine (MSBuild)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.14"
cache: pip
cache-dependency-path: |
requirements-alpha.txt
requirements-dev.txt
- uses: microsoft/setup-msbuild@v2
- name: Fetch FFmpeg runtime
# Headers and import libs are tracked; the 152 MB of DLLs are not.
# The fetch verifies every file against tools/ffmpeg_manifest.json.
run: python tools/fetch_third_party.py --ffmpeg
- name: Report missing third-party files clearly
shell: bash
run: |
missing=0
for f in include/libavcodec/avcodec.h lib/avcodec.lib bin/avcodec-62.dll; do
p="FTHRcapture/FTHRclips/third_party/ffmpeg/$f"
if [ ! -f "$p" ]; then
echo "::error::missing third-party file: $p"
missing=1
fi
done
[ "$missing" -eq 0 ] || {
echo "::error::Run: python tools/fetch_third_party.py --ffmpeg"
exit 1
}
- name: Build (Release | x64)
run: >
msbuild FTHRcapture\FTHRcapture.sln
-p:Configuration=Release
-p:Platform=x64
-m
-verbosity:minimal
- name: Verify the binary exists and is x64
shell: pwsh
run: |
$exe = "FTHRcapture\x64\Release\FTHRclips.exe"
if (-not (Test-Path $exe)) { throw "engine binary not produced" }
# PE header: 0x8664 = x86-64. A silent Win32 build would link but
# then fail to load the x64 FFmpeg DLLs at runtime.
$fs = [IO.File]::OpenRead($exe)
$br = New-Object IO.BinaryReader($fs)
$fs.Position = 0x3C
$fs.Position = $br.ReadInt32() + 4
$machine = $br.ReadUInt16()
$br.Close()
if ($machine -ne 0x8664) { throw ("wrong architecture: 0x{0:X}" -f $machine) }
"engine OK, x64"
- name: Install pinned bundle dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements-alpha.txt -r requirements-dev.txt
python -m pip check
- name: Build sealed optional upload packages
run: python tools/build_optional_uploaders.py
- name: Test built optional upload packages
run: >
python -m pytest tests/test_uploader_plugin_boundary.py
-k "test_built_" --tb=short
- name: Build and verify the Windows bundle
shell: pwsh
run: |
python -m PyInstaller FTHR.spec --clean --noconfirm --log-level WARN
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
if (-not (Test-Path "dist\FTHRClips\FTHRClips.exe")) { throw "UI binary not produced" }
if (-not (Test-Path "dist\FTHRClips\_internal\engine\FTHRClips.exe")) { throw "bundled engine not produced" }
python tools/verify_release_licenses.py --windows-dist dist\FTHRClips
# Release verification — the gates that decide whether a tag is allowed.
release-verification:
name: Release verification
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # the large-file check inspects history
- uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Licences (AUDIT-005)
# Refuses any tree that reintroduces GPL FFmpeg components or drops
# the third-party licence paperwork.
run: python tools/verify_release_licenses.py --tree .
- name: Version consistency (AUDIT-008)
run: python tools/verify_version_consistency.py
- name: Shared-memory contract
run: python tools/verify_shared_memory_contract.py
- name: Engine response contract
run: python tools/verify_engine_response_contract.py
- name: Exception-handling ratchet
run: python tools/verify_exception_handling.py
- name: Secrets, user data and oversized binaries
run: python tools/scan_repo_hygiene.py
- name: No large blobs in history
# A 150 MB DLL committed once stays in the pack forever, even after a
# later delete. Cheaper to catch it on the PR that introduces it.
run: |
limit=$((2 * 1024 * 1024))
bad=$(git rev-list --objects --all \
| git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
| awk -v lim="$limit" '$1 == "blob" && $3 > lim {print $3, $4}' \
| sort -rn | head -20)
if [ -n "$bad" ]; then
echo "::error::blobs above 2 MB found in history:"
echo "$bad"
exit 1
fi
echo "No oversized blobs in history."
- name: Licence claim sanity (AUDIT-013)
# FTHR's project licence is GPL-3.0-only. Generated assets may retain
# their explicit MIT grant, so only current application claims belong
# in this consistency check.
run: |
grep -q 'GNU GENERAL PUBLIC LICENSE' LICENSE
grep -q 'Version 3, 29 June 2007' LICENSE
if grep -nE "SOURCE_LICENSE = 'MIT'|FTHR Clips.+source.+MIT|application code remains MIT" \
README.md THIRD_PARTY_NOTICES.md FTHR_UI/version.py FTHR_UI/main.py; then
echo "::error::FTHR's application licence drifted back to MIT."
exit 1
fi
echo "GPL-3.0-only project licence declaration is consistent."
# Tag guard
tag-guard:
name: Tag guard
if: startsWith(github.ref, 'refs/tags/v')
needs: [python, linux-engine, linux-appimage-package, windows-engine, release-verification]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Tag must match FTHR_UI/version.py
run: |
tag="${GITHUB_REF#refs/tags/v}"
src="$(python -c "import sys; sys.path.insert(0,'FTHR_UI'); import version; print(version.__version__)")"
if [ "$tag" != "$src" ]; then
echo "::error::tag v$tag does not match FTHR_UI/version.py ($src)"
exit 1
fi
echo "Tag v$tag matches the source version."
# Publish artifacts manually after install, launch, and uninstall qualification.
# Linux AppImage compatibility check. Build on the oldest supported runner
# and validate the result, but never upload or publish the image from CI.
linux-appimage-package:
name: Linux AppImage (glibc baseline)
runs-on: ubuntu-22.04
needs: [linux-engine, release-verification]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.14"
cache: pip
cache-dependency-path: requirements-alpha.txt
- name: Install packaging dependencies
run: |
sudo apt-get update -q
sudo apt-get install -q -y \
build-essential cmake pkg-config patchelf curl file binutils \
libavcodec-dev libavformat-dev libavutil-dev libavdevice-dev \
libswscale-dev libswresample-dev libpulse-dev libportaudio2 libx11-dev \
libgl1 libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 \
libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 \
libxcb-shape0 libxcb-xinerama0 libxkbcommon0 libxkbcommon-x11-0 \
libxext-dev libxfixes-dev libwayland-dev wayland-protocols libwayland-bin \
libpipewire-0.3-dev libdbus-1-dev
python -m pip install --upgrade pip
python -m pip install -r requirements-alpha.txt -r requirements-dev.txt
python -m pip check
- name: Build AppImage
run: bash build_linux.sh
- name: Verify AppImage artifact and checksum
run: |
image=(build_output/FTHRClips-*-x86_64.AppImage)
[ "${#image[@]}" -eq 1 ]
checksum="${image[0]}.sha256"
test -f "$checksum"
(cd "$(dirname "${image[0]}")" && sha256sum --check "$(basename "$checksum")")
python tools/verify_linux_glibc.py \
--manifest tools/linux_release_baseline.json \
--target build_output/AppDir \
--target "${image[0]}"
# The artifact remains a CI-local compatibility result. Users build
# and verify their own AppImage; GitHub does not publish this file.