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
2 changes: 1 addition & 1 deletion .github/workflows/beta-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ jobs:
set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}"
REPO="${{ github.repository }}"
NOTES="NullifyPDF beta release (build from develop). Do not use in production. If validation succeeds, this build will be promoted to stable without rebuilding."
NOTES="NullifyPDF beta release (build from develop). Do not use in production."

if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then
echo "Release $TAG already exists, uploading/replacing assets."
Expand Down
143 changes: 85 additions & 58 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
name: Promote Beta to Stable Release
name: Build and Release NullifyPDF (Stable)

# The stable release is compiled from its own tag (not promoted from a beta):
# the app embeds its version string at build time, so re-labelling a beta
# build would leave "-beta.N" in the About dialog.

on:
push:
tags:
- 'v*'
- '!v*-beta.*'
workflow_dispatch:
inputs:
beta_tag:
description: 'Beta tag to promote (leave empty to auto-select the latest beta)'
required: false
type: string

permissions:
contents: write
actions: read

concurrency:
group: stable-release-${{ github.ref }}
cancel-in-progress: false

jobs:
promote:
name: Promote latest beta artifacts to stable
changelog:
name: Verify CHANGELOG entry
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Checkout Code
uses: actions/checkout@v7

- name: Verify CHANGELOG has an entry for this version
if: startsWith(github.ref, 'refs/tags/')
run: |
TAG="${GITHUB_REF#refs/tags/}"
VERSION="${TAG#v}"
Expand All @@ -46,68 +46,90 @@ jobs:

echo "[OK] CHANGELOG.md has a clean [$VERSION] entry and an empty [Unreleased] section."

- name: Resolve target beta release
id: beta
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BETA_TAG_INPUT: ${{ github.event.inputs.beta_tag }}
- name: Verify the app version matches the tag
run: |
if [ -n "$BETA_TAG_INPUT" ]; then
TAG="$BETA_TAG_INPUT"
else
TAG=$(gh release list --repo "${{ github.repository }}" --limit 50 --json tagName,isPrerelease,createdAt \
--jq '[.[] | select(.isPrerelease == true)] | sort_by(.createdAt) | last | .tagName // empty')
fi

if [ -z "$TAG" ]; then
echo "::error::No beta release found to promote. Publish a beta release first (tag v*-beta.*) or pass beta_tag explicitly."
TAG="${GITHUB_REF#refs/tags/}"
VERSION="${TAG#v}"
BASE=$(sed -nE 's/^__version__ = "([^"]+)".*/\1/p' NullifyPDF.py)
PRE=$(sed -nE 's/^__version_prerelease__ = "([^"]*)".*/\1/p' NullifyPDF.py)
if [ "$BASE" != "$VERSION" ] || [ -n "$PRE" ]; then
echo "::error::NullifyPDF.py declares version '$BASE' with prerelease '$PRE', but the stable tag is '$VERSION'. Set __version__ to $VERSION and __version_prerelease__ to \"\" before tagging."
exit 1
fi
echo "[OK] NullifyPDF.py declares stable version $VERSION."

build:
name: Build for ${{ matrix.os }}
needs: changelog
if: ${{ !failure() && !cancelled() }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-22.04, macos-latest]

echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "Promoting beta release: $TAG"
steps:
- name: Checkout Code
uses: actions/checkout@v7

- name: Download beta release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p stable-assets
# Patterns are deliberately tolerant of a trailing "_Full"/"_Lite"
# suffix (no underscore pinned before the extension) so a beta
# built before the lite/full split was removed can still be
# promoted to stable.
gh release download "${{ steps.beta.outputs.tag }}" --repo "${{ github.repository }}" --dir stable-assets \
--pattern "NullifyPDF_v*_Windows*.exe" \
--pattern "NullifyPDF_v*_macOS*.zip" \
--pattern "NullifyPDF_v*_Ubuntu*.deb" \
--pattern "NullifyPDF_v*_Fedora*.rpm"

- name: Strip beta suffix from filenames
- name: Set up Python
uses: actions/setup-python@v7
with:
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)
run: |
cd stable-assets
# Drop any leftover "_Lite" assets from betas built before the
# lite/full split was removed: only the full variant is promoted.
rm -f *_Lite.* *_Lite
for f in *; do
# Also drops the now-redundant "_Full" variant tag.
new="$(echo "$f" | sed -E 's/-beta\.[0-9]+//; s/_Full//')"
if [ "$f" != "$new" ]; then
mv "$f" "$new"
fi
done
ls -la
python -m pip install --upgrade pip
pip install -r requirements.txt

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

- name: Create stable release and upload assets sequentially
- name: Type Check (mypy)
shell: bash
run: mypy NullifyPDF.py pii_detection.py privacy_core.py PDF_Checker.py

- name: Build
run: python build_local.py

- name: Upload Artifact
uses: actions/upload-artifact@v7
with:
name: stable-build-${{ runner.os }}
retention-days: 5
path: |
dist/NullifyPDF_v*_Windows.exe
dist/NullifyPDF_v*_macOS.zip
dist/NullifyPDF_v*_Ubuntu.deb
dist/NullifyPDF_v*_Fedora.rpm

create_release:
name: Create GitHub Stable Release
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Download All Artifacts
uses: actions/download-artifact@v8
with:
path: all-builds
pattern: stable-build-*
merge-multiple: true

- name: Create release and upload assets sequentially
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}"
REPO="${{ github.repository }}"
NOTES="NullifyPDF stable release.
Promoted from beta \`${{ steps.beta.outputs.tag }}\` (already verified build, no rebuild).
Available for Windows, macOS, and Linux."
NOTES="NullifyPDF stable release. Available for Windows, macOS, and Linux. See CHANGELOG.md for the changes in this version."

if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then
echo "Release $TAG already exists, uploading/replacing assets."
Expand All @@ -120,7 +142,12 @@ jobs:
# (100-300MB each) were timing out with an opaque "Error saving
# asset" and no per-file retry (see beta-release.yml).
shopt -s nullglob
files=(stable-assets/*)
files=(
all-builds/NullifyPDF_v*_Windows.exe
all-builds/NullifyPDF_v*_macOS.zip
all-builds/NullifyPDF_v*_Ubuntu.deb
all-builds/NullifyPDF_v*_Fedora.rpm
)

if [ ${#files[@]} -eq 0 ]; then
echo "No release assets found to upload" >&2
Expand Down
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ GitHub Actions build one artifact per supported OS:
- Ubuntu
- Fedora

Beta releases build and upload all artifacts. Stable releases promote the latest verified beta artifacts without recompilation.
Beta releases (`v*-beta.N` tags) and stable releases (`vX.Y.Z` tags) are each compiled from their own tag, because the application embeds its version string at build time. The stable workflow first checks that `CHANGELOG.md` has a `[X.Y.Z]` entry with an empty `[Unreleased]` section and that `NullifyPDF.py` declares that exact version with no prerelease suffix.

Expected artifact names include:

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Validated through the 2.3.0-beta.1/beta.2/beta.3 prereleases before this stable

### 🐛 Fixed

- **Stable build showed the beta version**: the stable release was promoted from a beta build, which embeds its own version string, so the About dialog read `2.3.0-beta.3` and "AI Privacy Beta". Stable releases are now compiled from their own tag, the workflow refuses a tag whose `NullifyPDF.py` version does not match, and the "Beta" label appears only for prereleases.
- **AI scan flagged labels and headings as names or places** ("Telefono:", "- Presente", "Configurazione"): NER hits are now judged line by line (a span no longer crosses a line break, and bullets/dashes are ignored), and a single-word PERSON/LOCATION hit is dropped when it is a field label followed by a colon, a list item, a lone heading line, a word also written in lowercase elsewhere on the page, or a capitalized line start followed by a lowercase word. A list of common label, section and job-title words (IT/EN) is also ignored. Multi-word names such as "Mario Rossi" are unaffected.
- **AI scan crashed in packaged builds when an email was found**: Presidio's email recognizer needs `tldextract`'s `.tld_set_snapshot` data file, which PyInstaller did not bundle, so the scan aborted with `FileNotFoundError` under `_MEI... ldextract`. The package is now collected by the spec and by `build_local.py`.
- **AI scan selected ordinary text and parts of words**: detections were reduced to bare strings and then re-searched page-wide with a case-insensitive substring search, so one NER hit on "Rossi" also redacted "Rossini", and a stray tag spread to every occurrence on the page. Detections now use Presidio's exact character offsets mapped to word boxes, other occurrences are matched as whole words only, weak recognizers are filtered by per-type confidence thresholds, and generic PERSON/LOCATION hits (titles, months, salutations, lowercase text) are dropped. Blocklist and allowlist matching is whole-word too. A name that the NER model glues to the following address ("Rossini Marco Via Garibaldi") is now cut at the street word or number; the address part is kept as a location only when that type is enabled.
Expand Down
8 changes: 7 additions & 1 deletion NullifyPDF.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@
)


def about_version_text() -> str:
"""Version line of the About dialog; says "Beta" only for prereleases."""
suffix = " Beta" if __version_prerelease__ else ""
return f"v{APP_VERSION} AI Privacy{suffix}"


class DetectionDict(TypedDict):
text: str
entity_type: str
Expand Down Expand Up @@ -1645,7 +1651,7 @@ def cmd_about(self) -> None:
lbl_title.setStyleSheet("font-size: 24px; font-weight: bold;")
lbl_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
lay.addWidget(lbl_title)
lbl_ver = QLabel(f"v{APP_VERSION} AI Privacy Beta")
lbl_ver = QLabel(about_version_text())
lbl_ver.setStyleSheet("color: #0ea5e9; font-weight: bold;")
lbl_ver.setAlignment(Qt.AlignmentFlag.AlignCenter)
lay.addWidget(lbl_ver)
Expand Down
11 changes: 11 additions & 0 deletions tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,14 @@ def test_list_manager_handles_invalid_paths(invalid_input):

if __name__ == "__main__":
pytest.main([__file__, "-v"])


def test_about_version_text_mentions_beta_only_for_prereleases(monkeypatch):
import NullifyPDF

monkeypatch.setattr(NullifyPDF, "APP_VERSION", "2.3.0")
monkeypatch.setattr(NullifyPDF, "__version_prerelease__", "")
assert NullifyPDF.about_version_text() == "v2.3.0 AI Privacy"
monkeypatch.setattr(NullifyPDF, "APP_VERSION", "2.3.0-beta.4")
monkeypatch.setattr(NullifyPDF, "__version_prerelease__", "beta.4")
assert NullifyPDF.about_version_text() == "v2.3.0-beta.4 AI Privacy Beta"
Loading