From 4a280f65516d15ee0968b14a86bd8e6e39c47487 Mon Sep 17 00:00:00 2001 From: 0verwrite <31691645+overwrite00@users.noreply.github.com> Date: Sat, 19 Sep 2026 12:32:02 +0200 Subject: [PATCH] fix: build stable releases from their own tag and drop the Beta label (#121) Compile stable from its own tag, verify the tag matches NullifyPDF.py, and show Beta only for prereleases. --- .github/workflows/beta-release.yml | 2 +- .github/workflows/release.yml | 143 +++++++++++++++++------------ ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + NullifyPDF.py | 8 +- tests/test_validation.py | 11 +++ 6 files changed, 106 insertions(+), 61 deletions(-) diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml index 5e0140c..c082ed6 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-release.yml @@ -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." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4342a16..28d9823 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,8 @@ -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: @@ -6,29 +10,25 @@ on: - '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}" @@ -46,58 +46,82 @@ 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 }} @@ -105,9 +129,7 @@ jobs: 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." @@ -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 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2147510..2a4b77d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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: diff --git a/CHANGELOG.md b/CHANGELOG.md index be0177a..3f695f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/NullifyPDF.py b/NullifyPDF.py index d319b12..e4f599a 100644 --- a/NullifyPDF.py +++ b/NullifyPDF.py @@ -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 @@ -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) diff --git a/tests/test_validation.py b/tests/test_validation.py index 80967a6..f70088d 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -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"