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
14 changes: 10 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
name: Build executables

# Builds both executables (windowed cachet + console cachet-cli) on
# CI: builds both executables (windowed cachet + console cachet-cli) on
# Windows AND Linux using the shared cachet.spec, then publishes the
# downloadable artifacts. Triggered on push/tag/manual.
# downloadable artifacts. Runs on develop and on pull requests; pushes to
# main are handled by release.yml, which builds AND publishes a Release.
on:
push:
branches: [main]
tags: ["v*"]
branches: [develop]
pull_request:
workflow_dispatch:

Expand Down Expand Up @@ -44,6 +44,12 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements-build.txt

# Headless: the GUI tests skip without a display. Linux only — on the
# Windows runner Tk could open real windows and hang the job.
- name: Unit tests
if: runner.os == 'Linux'
run: python -m unittest -v

- name: Build with PyInstaller
run: pyinstaller --noconfirm --clean cachet.spec

Expand Down
153 changes: 153 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
name: Release

# Publishes a GitHub Release when `develop` is merged into `main`.
#
# Flow:
# 1. Bump __version__ in sign_pdfs_beid.py on develop (single source of
# truth), merge develop -> main.
# 2. This workflow reads the version; if the tag v{version} does not exist
# yet, it builds both executables on Windows AND Linux (same recipe as
# ci/build.yml), packages them, tags v{version} and publishes a Release
# with auto-generated notes and the binaries attached.
# 3. If the tag already exists (merge without a version bump), the run
# exits early with a notice — nothing is republished.
on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: write # create tags + releases

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

env:
PYTHON_VERSION: "3.13"

jobs:
version:
name: Resolve version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.v.outputs.version }}
should_release: ${{ steps.v.outputs.should_release }}
steps:
- uses: actions/checkout@v4

- name: Read __version__ and check the tag
id: v
run: |
VERSION=$(python3 -c "import re, pathlib; print(re.search(r'__version__ = \"([^\"]+)\"', pathlib.Path('sign_pdfs_beid.py').read_text(encoding='utf-8')).group(1))")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if git ls-remote --exit-code origin "refs/tags/v${VERSION}" >/dev/null 2>&1; then
echo "::notice::Tag v${VERSION} already exists - skipping release. Bump __version__ in sign_pdfs_beid.py on develop to publish."
echo "should_release=false" >> "$GITHUB_OUTPUT"
else
echo "::notice::Will release v${VERSION}."
echo "should_release=true" >> "$GITHUB_OUTPUT"
fi

build:
name: Build (${{ matrix.os }})
needs: version
if: needs.version.outputs.should_release == 'true'
strategy:
fail-fast: true # a broken build on either OS must block the release
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: pip

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements-build.txt

# Gate the release on the test suite (Linux only: headless, the GUI
# tests skip without a display; on the Windows runner Tk could open
# real windows and hang the job).
- name: Unit tests
if: runner.os == 'Linux'
run: python -m unittest -v

- name: Build with PyInstaller
run: pyinstaller --noconfirm --clean cachet.spec

- name: Smoke test (CLI image mode, no eID hardware)
shell: bash
run: |
python - <<'PY'
from test_sign_pdfs_beid import make_pdf, make_png
make_pdf("smoke.pdf", [(595, 842)])
make_png("sig.png")
print("fixtures written")
PY
CLI=$(ls dist/cachet-cli* | head -1)
"$CLI" --version
"$CLI" --mode image --image-path sig.png --input smoke.pdf \
--output smoke_out --page 1 --x 100 --y 100
test -n "$(ls smoke_out/*.pdf 2>/dev/null)" || { echo "FAIL: no output PDF"; exit 1; }
echo "OK: image mode works in the frozen binary."

- name: Package (Linux)
if: runner.os == 'Linux'
run: |
V=${{ needs.version.outputs.version }}
tar -czf "cachet-${V}-linux-x86_64.tar.gz" -C dist cachet cachet-cli

- name: Package (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
V=${{ needs.version.outputs.version }}
7z a "cachet-${V}-windows-x86_64.zip" ./dist/cachet.exe ./dist/cachet-cli.exe

- name: Upload package
uses: actions/upload-artifact@v4
with:
name: release-${{ matrix.os }}
path: |
cachet-*-linux-x86_64.tar.gz
cachet-*-windows-x86_64.zip
if-no-files-found: error
retention-days: 7

release:
name: Publish v${{ needs.version.outputs.version }}
needs: [version, build]
if: needs.version.outputs.should_release == 'true'
runs-on: ubuntu-latest
steps:
- name: Download packages
uses: actions/download-artifact@v4
with:
pattern: release-*
merge-multiple: true
path: packages

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.version.outputs.version }}
name: Cachet v${{ needs.version.outputs.version }}
generate_release_notes: true
make_latest: true
files: packages/*
fail_on_unmatched_files: true
body: |
Standalone binaries (no Python required):
- `cachet-…-linux-x86_64.tar.gz` — `cachet` (GUI) + `cachet-cli` (console)
- `cachet-…-windows-x86_64.zip` — `cachet.exe` (GUI) + `cachet-cli.exe` (console)

Runtime requirements (beid mode): Belgian eID middleware + reader.
Levels ≥ b-t need network (TSA / trust lists / OCSP-CRL) — see BUILD.md.
22 changes: 22 additions & 0 deletions BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,28 @@ git push -u origin main
Then: **Actions** tab → run → **Artifacts** → `cachet-windows-latest` /
`cachet-ubuntu-latest`. Can also be triggered manually (*workflow_dispatch*).

## Release process (develop → main)

Branch model: day-to-day work lands on **`develop`** (CI builds + tests every
push, `.github/workflows/build.yml`); **merging `develop` into `main`
publishes a release** (`.github/workflows/release.yml`):

1. On `develop`, bump `__version__` in `sign_pdfs_beid.py` (single source of
truth — the CLI `--version` and the GUI title read it).
2. Open a PR `develop` → `main` and merge it.
3. The release workflow then: reads the version → checks the tag `v{version}`
does not already exist → runs the unit tests → builds **both** executables
on Windows AND Linux → smoke-tests the frozen CLI → packages
`cachet-{v}-linux-x86_64.tar.gz` + `cachet-{v}-windows-x86_64.zip` →
creates the tag and a **GitHub Release** with auto-generated notes and the
two archives attached.
4. Merging without bumping `__version__` is safe: the workflow detects the
existing tag and skips publishing (notice in the run log).

A failed build/test on either OS blocks the release (`fail-fast`). The
manual eID acceptance test (above) is NOT gated by CI — run it before
merging when the signing path changed.

The CI's Python version is `3.13` (variable `PYTHON_VERSION` at the top of the
workflow; `3.14` works too).

Expand Down
7 changes: 6 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,12 @@ must stay available in the **CLI** binary (do NOT add azure to CLI_EXCLUDES).

Build routes: `./build_linux.sh` (native), `build_windows.bat` (real Windows),
`./build_windows_wine.sh` (Linux→Windows via Wine, best-effort), and
`.github/workflows/build.yml` (CI matrix, windows+linux, artifacts). Verify
`.github/workflows/build.yml` (CI matrix, windows+linux, artifacts — runs on
`develop` pushes and PRs). **Releases**: merging `develop` into `main` runs
`.github/workflows/release.yml`, which tags `v{__version__}` (read from
`sign_pdfs_beid.py` — bump it on develop, it is the single source of truth)
and publishes a GitHub Release with both packaged binaries; an existing tag
makes the workflow skip gracefully. See BUILD.md "Release process". Verify
headlessly: CLI `--help`, image-mode end-to-end, a PKCS#11 native-load canary
(`--lib` at a dummy `.so` → expect a PKCS#11 error, not `ImportError`), and the
GUI binary launched on `DISPLAY=:0` + screenshot. Real eID signing needs
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,9 @@ place) → **7.** launch → **8.** per-document summary.

The project compiles into **two standalone binaries** per OS (a windowed GUI
`cachet`, a console CLI `cachet-cli`) — no Python required on the target
machine. See **[BUILD.md](BUILD.md)** for all the routes (native Linux, native
machine. Official builds are published as **GitHub Releases**: merging
`develop` into `main` automatically tags `v{version}` and attaches the
Linux/Windows packages (see *Release process* in BUILD.md). See **[BUILD.md](BUILD.md)** for all the routes (native Linux, native
Windows, Wine, and GitHub Actions CI).

```bash
Expand Down
Loading
Loading