Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 34 additions & 12 deletions .github/workflows/beta-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-22.04, macos-latest]
os: [windows-latest, macos-latest]

steps:
- name: Checkout Code
Expand All @@ -33,21 +33,24 @@ jobs:
python-version: '3.13'
cache: 'pip'

- name: Install System Dependencies (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y rpm dpkg binutils libxcb-cursor0 libxkbcommon-x11-0 libegl1 desktop-file-utils

- name: Install Python Dependencies (includes spaCy models)
shell: bash
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
# The spaCy models come from github.com release assets, which
# occasionally answer 504: retry instead of failing the release.
for attempt in 1 2 3 4 5; do
pip install -r requirements.txt && exit 0
echo "Attempt $attempt failed, retrying in 30s..."; sleep 30
done
exit 1

- name: Run Smoke Tests
run: pytest tests/ -v

- name: Type Check (mypy)
shell: bash
run: mypy NullifyPDF.py pii_detection.py privacy_core.py PDF_Checker.py
run: mypy NullifyPDF.py pii_detection.py privacy_core.py update_check.py PDF_Checker.py export_engine.py pdf_sanitize.py review_dialog.py lexicon_it.py secure_store.py word_lists.py dictionary_dialog.py help_guide.py spacy_models.py licences_info.py

- name: Resolve beta suffix from tag
if: startsWith(github.ref, 'refs/tags/')
Expand All @@ -67,15 +70,24 @@ jobs:
path: |
dist/NullifyPDF_v*_Windows.exe
dist/NullifyPDF_v*_macOS.zip
dist/NullifyPDF_v*_Ubuntu.deb
dist/NullifyPDF_v*_Fedora.rpm

linux:
name: Linux packages
uses: ./.github/workflows/build-linux.yml
with:
artifact_prefix: beta-build
is_beta: true
retention_days: 5

create_beta_release:
name: Create GitHub Beta Release
needs: build
needs: [build, linux]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Checkout Code
uses: actions/checkout@v7

- name: Download All Artifacts
uses: actions/download-artifact@v8
with:
Expand All @@ -91,12 +103,17 @@ jobs:
set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}"
REPO="${{ github.repository }}"
NOTES="NullifyPDF beta release (build from develop). Do not use in production."
# Release body = the matching CHANGELOG section (shown by the in-app
# update check); fall back to a generic text if it is empty.
NOTES_FILE="$(mktemp)"
if ! python3 scripts/extract_release_notes.py "$TAG" > "$NOTES_FILE"; then
echo "NullifyPDF beta release (build from develop). Do not use in production." > "$NOTES_FILE"
fi

if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then
echo "Release $TAG already exists, uploading/replacing assets."
else
gh release create "$TAG" --repo "$REPO" --title "$TAG" --prerelease --notes "$NOTES"
gh release create "$TAG" --repo "$REPO" --title "$TAG" --prerelease --notes-file "$NOTES_FILE"
fi

# Uploaded one at a time (not via softprops/action-gh-release's
Expand All @@ -116,6 +133,11 @@ jobs:
exit 1
fi

# A release must carry every platform package; never ship a partial one.
for suffix in _Windows.exe _macOS.zip _Ubuntu.deb _Fedora.rpm; do
ls all-builds/NullifyPDF_v*"$suffix" >/dev/null 2>&1 || { echo "Missing release asset *$suffix" >&2; exit 1; }
done

for f in "${files[@]}"; do
echo "Uploading $f ..."
attempt=1
Expand Down
230 changes: 230 additions & 0 deletions .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
name: Build Linux packages (reusable)

# Shared by the stable release, the beta release and the test build.
#
# The PyInstaller binary embeds the glibc of the machine that builds it, so it
# is built ONCE inside a RHEL 9 container (glibc 2.34, rpm 4.16). The result
# runs on every distribution with glibc >= 2.34 (Ubuntu 22.04+, Debian 12+,
# RHEL/Rocky/Alma 9-10, current Fedora). Do NOT build it on ubuntu-latest:
# ubuntu-24.04/26.04 produce a binary that does not start on older systems.
#
# build-linux : tests + type check + PyInstaller + .rpm (rockylinux:9)
# build-deb : wraps the same binary into a .deb (needs dpkg-deb)
# smoke : installs each package in a clean container of every
# supported distribution and starts the application there.
#
# Runners are pinned (not ubuntu-latest) so the GitHub migration of
# ubuntu-latest to 26.04 cannot change the build environment.

on:
workflow_call:
inputs:
artifact_prefix:
description: Prefix of the uploaded artifacts (stable-build, beta-build, test-build)
required: true
type: string
is_beta:
description: Derive NULLIFYPDF_BETA_SUFFIX from the pushed beta tag
required: false
default: false
type: boolean
retention_days:
required: false
default: 5
type: number

permissions:
contents: read

jobs:
build-linux:
name: Build binary and .rpm (RHEL 9)
runs-on: ubuntu-24.04
container: rockylinux:9
env:
QT_QPA_PLATFORM: offscreen
steps:
- name: Install system dependencies
# Build tools + the libraries PySide6 needs to import under pytest.
run: |
dnf install -y git tar gzip which findutils binutils python3-pip rpm-build \
xcb-util-cursor xcb-util-image xcb-util-keysyms xcb-util-renderutil xcb-util-wm \
libxkbcommon libxkbcommon-x11 mesa-libEGL mesa-libGL fontconfig dbus-libs glib2

- uses: actions/checkout@v7

- name: Set up Python 3.13 (uv)
run: |
# RHEL 9 AppStream has no python3.13; uv installs a python-build-standalone one.
python3 -m pip install uv==0.8.0
uv python install 3.13
uv venv --python 3.13 /opt/venv
echo "/opt/venv/bin" >> "$GITHUB_PATH"
echo "VIRTUAL_ENV=/opt/venv" >> "$GITHUB_ENV"

- name: Show toolchain
run: |
python --version
ldd --version | head -1
rpmbuild --version

- name: Install Python dependencies (includes spaCy models)
run: |
# The spaCy models come from github.com release assets, which
# occasionally answer 504: retry instead of failing the release.
for attempt in 1 2 3 4 5; do
uv pip install -r requirements.txt && exit 0
echo "Attempt $attempt failed, retrying in 30s..."; sleep 30
done
exit 1

- name: Run tests
run: pytest tests/ -v

- name: Type check (mypy)
run: mypy NullifyPDF.py pii_detection.py privacy_core.py update_check.py PDF_Checker.py export_engine.py pdf_sanitize.py review_dialog.py lexicon_it.py secure_store.py word_lists.py dictionary_dialog.py help_guide.py spacy_models.py licences_info.py

- name: Resolve beta suffix from tag
if: inputs.is_beta && startsWith(github.ref, 'refs/tags/')
run: |
TAG="${GITHUB_REF#refs/tags/}"
echo "NULLIFYPDF_BETA_SUFFIX=${TAG#*-}" >> "$GITHUB_ENV"

- name: Build
run: python build_local.py

- name: Check the build output
run: |
ls -l dist
ls dist/NullifyPDF_v*_Linux_Portable dist/NullifyPDF_v*_Fedora.rpm

- name: Upload binary (input of the .deb job)
uses: actions/upload-artifact@v7
with:
name: linux-portable
path: dist/NullifyPDF_v*_Linux_Portable
retention-days: 1
if-no-files-found: error

- name: Upload .rpm
uses: actions/upload-artifact@v7
with:
name: ${{ inputs.artifact_prefix }}-Linux-rpm
path: dist/NullifyPDF_v*_Fedora.rpm
retention-days: ${{ inputs.retention_days }}
if-no-files-found: error

build-deb:
name: Build .deb
needs: build-linux
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7

- uses: actions/download-artifact@v8
with:
name: linux-portable
path: dist

- name: Resolve beta suffix from tag
if: inputs.is_beta && startsWith(github.ref, 'refs/tags/')
run: |
TAG="${GITHUB_REF#refs/tags/}"
echo "NULLIFYPDF_BETA_SUFFIX=${TAG#*-}" >> "$GITHUB_ENV"

- name: Package .deb
run: |
chmod +x dist/NullifyPDF_v*_Linux_Portable
python3 build_local.py --deb-only
ls -l dist
ls dist/NullifyPDF_v*_Ubuntu.deb
dpkg-deb --info dist/NullifyPDF_v*_Ubuntu.deb

- name: Upload .deb
uses: actions/upload-artifact@v7
with:
name: ${{ inputs.artifact_prefix }}-Linux-deb
path: dist/NullifyPDF_v*_Ubuntu.deb
retention-days: ${{ inputs.retention_days }}
if-no-files-found: error

smoke:
name: Smoke test ${{ matrix.name }}
needs: [build-linux, build-deb]
runs-on: ubuntu-24.04
container: ${{ matrix.image }}
# Supported LTS / long-lived distributions. Review this list when one
# approaches its end of support (same policy as MetaLens). Ubuntu 22.04 and
# Debian 12 still run the binary (glibc 2.34 baseline) but are not tested.
strategy:
fail-fast: false
matrix:
include:
- { name: ubuntu-24.04 deb, image: 'ubuntu:24.04', kind: deb }
- { name: ubuntu-26.04 deb, image: 'ubuntu:26.04', kind: deb }
- { name: debian-13 deb, image: 'debian:13', kind: deb }
- { name: rocky-9 rpm, image: 'rockylinux:9', kind: rpm }
- { name: rocky-10 rpm, image: 'rockylinux/rockylinux:10', kind: rpm }
- { name: fedora-latest rpm, image: 'fedora:latest', kind: rpm }
steps:
- name: Install the tools the smoke test needs
run: |
if command -v apt-get >/dev/null; then
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ca-certificates xvfb xauth >/dev/null
else
# RHEL 10 dropped the Xorg server (no Xvfb): there X11 clients run
# on Xwayland inside a headless mutter (xwfb-run).
dnf install -y -q xorg-x11-server-Xvfb xorg-x11-xauth --allowerasing >/dev/null || dnf install -y -q xwayland-run mutter dbus-daemon xorg-x11-xauth --allowerasing >/dev/null
fi

- uses: actions/download-artifact@v8
with:
pattern: ${{ inputs.artifact_prefix }}-Linux-*
merge-multiple: true
path: pkgs

- name: Install the package
run: |
ls -l pkgs
case "${{ matrix.kind }}" in
deb) DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ./pkgs/*.deb ;;
rpm) dnf install -y -q ./pkgs/*.rpm ;;
esac

- name: The package is installed correctly
run: |
test -x /usr/bin/nullify-pdf
test -f /usr/share/applications/nullify-pdf.desktop
case "${{ matrix.kind }}" in
deb) dpkg -s nullify-pdf | grep -E '^(Version|Depends)' ;;
rpm) rpm -q --qf '%{VERSION}-%{RELEASE}\n' nullify-pdf; rpm -q --requires nullify-pdf ;;
esac

- name: The application starts and stays up
run: |
# PyInstaller one-file: the first start unpacks Python, Qt and the
# spaCy models, so give it time. Exit status 124 = still running
# when the timeout stopped it = it started correctly.
# A real X server is mandatory: the offscreen Qt platform would not
# load the xcb plugin and could not detect a missing xcb-* dependency.
export QT_QPA_PLATFORM=xcb
if command -v xvfb-run >/dev/null; then
RUN="xvfb-run -a"
elif command -v xwfb-run >/dev/null; then
RUN="xwfb-run -c mutter --"
elif command -v Xvfb >/dev/null; then
Xvfb :99 -screen 0 1280x1024x24 >/tmp/xvfb.log 2>&1 &
sleep 3; export DISPLAY=:99; RUN=""
else
echo "::error::no Xvfb available in this image, cannot test the xcb platform"; exit 1
fi
set +e
timeout 60 $RUN /usr/bin/nullify-pdf > /tmp/app.log 2>&1
RC=$?
set -e
echo "exit status: $RC"; cat /tmp/app.log
if grep -qiE "could not (load|find) the Qt platform plugin|xcb-cursor|no Qt platform plugin" /tmp/app.log; then
echo "::error::Qt could not load the xcb platform plugin (missing system library?)"; exit 1
fi
[ "$RC" -eq 124 ] || { echo "::error::NullifyPDF did not stay running (exit $RC)"; exit 1; }
Loading
Loading