Packaging and releases

One repository, two distributions on PyPI, one tag.

Distribution

Contains

Built by

Installed with

plesty-bench

plesty/bench — the client

the project’s own pyproject.toml (hub-release of plesty-standard-ci)

uv tool install plesty-bench (developer machine)

plesty-server

plesty/server — the bench application (CLI, console, agent); depends on plesty-bench==<same version>, plesty-lib; extra gui = plesty-lib[gui]

packaging/server_dist.py in release:server-wheel

uv tool install "plesty-server[gui]" (bench)

Tagging vX.Y.Z on main publishes both through PyPI trusted publishing (OIDC — GitLab namespace plesty/core, project plesty-bench, .gitlab-ci.yml), one publisher per distribution told apart by the job’s GitLab environment (pypi-bench, pypi-server), and creates the GitLab Release.

Building the Windows installer

packaging/windows/build-installer.ps1 has two modes, and the difference is what the wizard installs:

powershell -ExecutionPolicy Bypass -File .\packaging\windows\build-installer.ps1 -Mode Test
powershell -ExecutionPolicy Bypass -File .\packaging\windows\build-installer.ps1 -Mode Release -Version 0.2.5

The -ExecutionPolicy Bypass is not optional on a default box: an unsigned .ps1 will not run otherwise. And every .ps1 here is ASCII only — Windows PowerShell 5.1 reads a file without a BOM as cp1252, where the third byte of an em-dash decodes to a smart quote, which PowerShell accepts as a string delimiter. One em-dash in a comment silently opens a string and the file stops parsing.

Release is what ships: the wizard runs uv tool install "plesty-server[gui]==<version>" against PyPI, so the version must be a real release — the Inno script refuses a .dev or +local version outright.

install.sh has the same two modes on macOS and Linux — --mode=test builds both wheels from the checkout and installs those, --mode=release (the default) installs from PyPI — plus --launcher-only for a build that is already installed some other way.

Test is how a branch is put through the real wizard: the version is the working tree’s (0.3.0.dev3+0ca7c6c), both distributions are built from the tree, and the wheels are carried inside the installer, so what gets installed is the code in front of you. The output is named …-TEST-… and every wizard page says so.

That distinction is not cosmetic. The wizard is a process context of its own — a process descended from setup.exe cannot traverse a directory junction, so uv behaves differently under it than under a shell — and a defect of that kind can only be reproduced by running the real installer. Without a test mode the only way to exercise the wizard was to ship a release first.

The light installers

The installers carry no application code. They install uv if it is missing and run uv tool install "plesty-server[gui]==<version>", which downloads the application and its dependencies from PyPI into uv’s tool environment; then they make the application visible:

Platform

Installer

What it adds

Windows

plesty-server-<tag>-windows-setup.exe — Inno Setup (packaging/windows/plesty-server.iss), built by packaging/windows/build-installer.ps1, ~2 MB, per-user, no admin

Start-menu entries (PLESTY Bench, console, Update, Uninstall), optional desktop shortcut, uninstaller in “Apps & features”

macOS

curl -LsSf <install.sh> | sh (packaging/install.sh; --launcher-only makes just the entry, for a build installed some other way)

~/Applications/PLESTY Bench.app — a launcher with the mark as icon, so it appears in Launchpad

Linux

the same install.sh

a .desktop entry in the application menu

plesty-server / plesty-server-gui land on uv’s bin dir (~/.local/bin); the console remembers the last fleet it used (<home>/last-fleet) so a double-click reopens it. Updating a bench is uv tool upgrade plesty-server (Start-menu entry Update PLESTY Bench on Windows).

The pinned version

Both installers install the release they were built for, never latest and never a development version — a bench is exactly the release it downloaded, and uv tool upgrade plesty-server is the only thing that moves it forward.

The pin must be a version that exists on PyPI, so it comes from the tag, not from the working tree: installer:windows derives it from $CI_COMMIT_TAG, and plesty-server.iss refuses to compile when it carries .dev or a + local segment (what versioningit yields off a tag — 0.2.3.dev7+abc1234, 0.2.2+modified — none of which are published). release:assets stamps the same version into install.sh in place of @PLESTY_VERSION@; run from a checkout the placeholder survives and the install is unpinned. PLESTY_SERVER_VERSION overrides it, and release:server-wheel fails if the version it publishes is not the tag.

An installer/<name> branch builds a test installer against the last release (or INSTALLER_VERSION), so the wizard can be tried end to end before tagging.

Signing

Unsigned installers are refused by name: Windows SmartScreen says “Windows protected your PC — Unknown publisher”, macOS Gatekeeper “unidentified developer”. Both need a paid identity; the build is wired for it but ships unsigned until one exists.

What removes the warning

Wired

Windows

An Authenticode certificate. installer:windows passes Inno’s SignTool when WINDOWS_SIGN_COMMAND (protected CI variable, a signtool command line with Inno’s $f placeholder) is set, and signs the uninstaller too. OV certificates still accumulate SmartScreen reputation over the first downloads; EV is trusted immediately.

yes — set the variable

macOS

A Developer ID Application certificate (Apple Developer Program) plus notarization with notarytool and a stapled ticket. This needs a real signed bundle shipped from CI rather than the launcher install.sh writes on the machine.

no

install.sh does clear com.apple.quarantine from the launcher it writes and ad-hoc signs it, so a locally built bundle opens by double-click — the bundle is created on the machine at the user’s request, and the flag it inherits from the downloaded script is what Gatekeeper would otherwise object to. That is not a substitute for Developer ID on anything downloaded as an app.

The CI jobs on a tag

Job

Does

hub-release (template)

publishes plesty-bench

release:server-wheel

server_dist.py → publishes plesty-server (same version, same trusted publisher)

installer:windows

saas-windows-medium-amd64: .ico from the mark (Pillow), Inno Setup via chocolatey → setup.exe

release:assets

stamps the tag into install.sh, uploads it and setup.exe to the generic package registry (permanent) and creates the Release: installer, install script, both PyPI links

What the bench still needs

uv (the installers install it) and git for modules installed from a repository; Python is not needed — uv manages interpreters for the tool environment and for every device environment.

Re-executing itself — plesty/server/reexec.py

Jobs and the detached launcher are plesty-server processes too: from a tool environment or a checkout that is python -m plesty.server / python -m plesty.server.services.launch with the environment’s interpreter. reexec is the one module that would also know a frozen bundle, should one ever be built again.

Headless bench: fleet run

plesty-server fleet run --agent            # bring the fleet up, keep it up, serve plesty-bench clients

Bench.run() brings the autostart devices up, then sweeps every 5 s: a device found exited is started again after a backoff that doubles per consecutive crash (1, 2, 4 … 60 s) and resets once it answers a probe; devices stopped on purpose are left alone. SIGINT/SIGTERM/SIGHUP (Ctrl-C, a service stop, the terminal closing) end the loop and — unless --leave-running — bring the servers down, so a service manager stopping fleet run stops the bench cleanly. --agent serves plesty-bench clients from the same process.