Repository navigation
DEV_GUIDE
How to add QUpdateTool to your app: build a branded updater and wire it into CI/CD.
See also: README
for flags and config.yaml, and
SECURITY.md
for why each step below matters.
Your app does not import this package. It ships a branded updater.exe
and runs it as a separate process:
1. App startup updater --check-only --json -> "an update is available"
2. Help > Check updater --gui -> progress window
3. Updater stops the app, installs, relaunches it
This guide covers building that binary and calling it. Full flag and
config.yaml reference:
README quick start
and
configuration file.
A brand file is a YAML file describing your app: name, release repo, signing
key, install behaviour. It gets compiled into the binary at build time, so
the branded updater.exe needs no flags or config file.
Copy examples/qsnippet-brand.yaml
as a starting point - it's a full, working example. You need to set:
-
app.name,app.process_names- what to look for and stop before install -
source.provider/source.repo- where releases are published -
security.allowed_fingerprints- which signing key(s) are trusted (see SECURITY.md §1) -
install.installer_args- silent-install flags for your installer (Inno Setup, WiX/MSI, etc.)
Keep the brand file in your app's repo, not this one.
tools/build_branded.py reads it at build time.
python tools/build_branded.py \
--brand ../MyApp/updater-brand.yaml \
--icon ../MyApp/assets/MyApp.ico \
--key ../MyApp/release-key.asc \
--out ../MyApp/output/updater.exe \
--clean--key pins the OpenPGP public key used to check release signatures. It
overrides anything inlined in the brand file, so CI keeps the real key in
your app's repo. Add --gui to bundle PySide6 so updater --gui works; skip
it for a smaller, headless-only binary.
This produces two files:
-
updater.exe- the branded binary -
updater.exe.sha256- its hash, needed in step 4
Or use the Makefile wrapper: make brand BRAND=... ICON=... KEY=... OUT=...
(make help lists every target).
Run this build in your app's own release pipeline, since it needs your repo's brand file, icon, and signing key. Example GitHub Actions job, triggered on the same tag that cuts your app release:
build-updater:
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
with:
repository: your-org/QUpdateTool2.0
path: qupdatetool
- uses: actions/setup-python@v7
with:
python-version: "3.13"
- run: pip install -r qupdatetool/requirements.txt -r qupdatetool/requirements-dev.txt
- name: Write signing key
shell: bash
run: echo "${{ secrets.RELEASE_PUBLIC_KEY }}" > release-key.asc
- name: Build branded updater
run: |
python qupdatetool/tools/build_branded.py \
--brand updater-brand.yaml \
--icon assets/MyApp.ico \
--key release-key.asc \
--out dist/updater.exe \
--sign-command "azuresigntool sign -kvu ... -kvc ... {file}" \
--clean
- uses: actions/upload-artifact@v4
with:
name: updater
path: |
dist/updater.exe
dist/updater.exe.sha256Notes:
-
--keyis the public signing key. It's safe to embed, but keep it in a secret/variable anyway so the brand file stays generic across environments. -
--sign-commandcode-signs the binary as part of the build. This matters more than any check the binary could do on itself - see SECURITY.md §3.2. - Publish
updater.exe.sha256as a build artifact or attach it to the release. Your app needs that value too, in step 4. -
--cleanclears oldbuild//dist/output first, so a reused CI runner can't ship a stale binary.
A binary can't prove its own integrity - only an outside check can.
updater.exe.sha256 exists so your app can be that check. Wire the hash
from step 2/3 into your app's build info:
# build_info.py, written by your app's CI, not by hand
UPDATER_SHA256 = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"And check it before ever launching the updater:
from qupdatetool.integrity import verify_file_hash
if not verify_file_hash(updater_path, UPDATER_SHA256):
logger.error("Updater binary failed its integrity check; refusing to run it")
returnWhy this is the important check, and what code signing and install permissions add on top of it: SECURITY.md §3.
Two calls cover the whole lifecycle:
# Silent check, e.g. on app startup - parse the JSON, show a badge/toast
updater --check-only --json
# User-initiated update, e.g. from a Help > Check for Updates menu item
updater --guiBranch on the exit code, not stdout. Full table: README. Most common:
| Code | What your app should do |
|---|---|
| 10 |
--check-only: update available - show the badge/prompt |
| 11 |
--check-only: already up to date - do nothing |
| 4 | Verification failed - tell the user, do not retry silently |
Pass --calling-pid <your PID> on a real (non-check-only) update, so the
updater waits for your process to exit before overwriting files. Pass
--executable so it knows what to relaunch.
Point the updater at your app's own log directory, so support bundles interleave cleanly by timestamp:
updater --log-file "C:/Users/me/AppData/Local/MyApp/logs"Format and rotation already match a typical app logger. Details: README Logging section.
make check REPO=your-org/your-app # live --check-only against your real repo
updater --self-check # hash, brand, signature status, install
# permissions, all in one reportThen run the SECURITY.md checklist once before your first real release. It's short, and each item maps to a step above.
-
README -
full flag and
config.yamlreference - SECURITY.md - the threat model behind every step above
-
examples/qsnippet-brand.yaml- a complete, real brand file -
examples/config.yaml- every runtime setting, commented
QUpdateTool2.0 - see LICENSE.txt
Get started
Features
Security