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/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
run: uv run python scripts/check_dep_age.py

- name: Export lockfile for audit
run: uv export --frozen --no-emit-project --output-file requirements-audit.txt
run: uv export --frozen --all-groups --no-emit-project --output-file requirements-audit.txt

- name: pip-audit (known CVEs)
run: uv run pip-audit --disable-pip --requirement requirements-audit.txt
Expand Down
96 changes: 90 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Publishes a release when a vX.Y.Z tag is pushed: the sdist/wheel are
# built and smoke-tested in an unprivileged job, published to PyPI via
# Publishes a release when a vX.Y.Z tag is pushed: the sdist/wheel and
# Windows executable are built and smoke-tested in unprivileged jobs.
# Python packages are published to PyPI via
# trusted publishing (OIDC — no stored tokens) with PEP 740 attestations,
# and only then announced as a GitHub Release with the artifacts
# attached, per the release process in docs/planning.md §4.10. The
Expand Down Expand Up @@ -73,6 +74,80 @@ jobs:
release-notes.md
if-no-files-found: error

# Compilation only runs for a release tag, after build validates its
# ancestry, version and changelog. No publish permissions or secrets.
build-windows:
needs: build
runs-on: windows-2025
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: "0.11.32"
enable-cache: true

- name: Install locked build dependencies
run: uv sync --locked --no-dev --group binary

- name: Build folder bundle
run: uv run --locked --no-dev --group binary python scripts/build_binary.py

- name: Test relocated folder bundle without Python on PATH
run: >-
pwsh -NoProfile -File scripts/test_binary.ps1
-Executable build/nuitka/glidepath_binary.dist/glidepath.exe
-Report build/nuitka/smoke-standalone.json

- name: Build single executable
run: uv run --locked --no-dev --group binary python scripts/build_binary.py --mode onefile

- name: Stage the versioned executable
run: |
$version = $env:GITHUB_REF_NAME.Substring(1)
New-Item -ItemType Directory -Path windows-release | Out-Null
Copy-Item build/nuitka/glidepath.exe "windows-release/glidepath-$version-windows-x64.exe"

- name: Stage readable runtime licence notices
run: |
$version = $env:GITHUB_REF_NAME.Substring(1)
Compress-Archive -Path third-party-licences `
-DestinationPath "windows-release/glidepath-$version-third-party-licences.zip"

- name: Test the release executable and verify its version
run: |
$version = $env:GITHUB_REF_NAME.Substring(1)
pwsh -NoProfile -File scripts/test_binary.ps1 `
-Executable "windows-release/glidepath-$version-windows-x64.exe" `
-Report build/nuitka/smoke-onefile.json
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$report = Get-Content build/nuitka/smoke-onefile.json -Raw | ConvertFrom-Json
if ($report.version -ne $version) {
throw "Executable version $($report.version) does not match release $version"
}

- name: Upload tested Windows executable and licence notices
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: windows-release-assets
path: windows-release/*
if-no-files-found: error

- name: Upload build diagnostics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: windows-build-diagnostics
path: |
build/nuitka/compilation-report.xml
build/nuitka/smoke-*.json
if-no-files-found: ignore

# Publishes to PyPI via trusted publishing: the job's OIDC token is
# exchanged for a short-lived upload token, so no PyPI credential is
# stored in the repo. This job checks out nothing and runs no project
Expand All @@ -82,7 +157,7 @@ jobs:
# contract registered on PyPI; its protection rules (required
# reviewer, v* tags only) gate this job.
publish-pypi:
needs: build
needs: [build, build-windows]
runs-on: ubuntu-latest
environment:
name: pypi
Expand All @@ -100,8 +175,8 @@ jobs:

# The GitHub Release is created only after PyPI publication succeeds,
# so a failed upload never leaves a public release advertising a
# package that is not there. The sdist/wheel are attached so the
# release carries the exact published artifacts, and signed build
# package that is not there. The sdist/wheel and Windows exe are attached
# so the release carries the exact published artifacts, and signed build
# provenance is attested for them first — the GitHub-side mirror of
# the PEP 740 attestations PyPI holds, so a download from the release
# page verifies with `gh attestation verify <file> -R <repo>`. Like
Expand All @@ -119,11 +194,19 @@ jobs:
with:
name: release-assets

- name: Download the tested Windows executable and licence notices
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: windows-release-assets
path: windows

- name: Attest build provenance for the release artifacts
id: attest
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: dist/*
subject-path: |
dist/*
windows/*

# The signed Sigstore bundle lives in GitHub's attestation store,
# which is invisible on the release page (and to Scorecard's
Expand All @@ -145,4 +228,5 @@ jobs:
--title "glidepath $GITHUB_REF_NAME" \
--notes-file release-notes.md \
dist/* \
windows/* \
glidepath-*-provenance.intoto.jsonl
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,40 @@ recent release first. The format follows
process in `docs/planning.md` §4.10). Each release's section is curated
in the release PR and becomes the GitHub Release notes verbatim.

## [1.1.0] - 2026-09-05

The first Windows executable release: download one file and launch
Glidepath without installing Python or uv. The executable is currently
unsigned, so Windows may show an unrecognised-app warning. GitHub build
provenance is included; Windows publisher signing is separate follow-up work.

### Added

- The Windows executable embeds the Glidepath icon at seven sizes, so
Explorer and shortcuts show the app's artwork instead of a generic icon.
- Windows x64 Nuitka packaging: a locked optional build
dependency group, `make binary` for a folder bundle, and
`make binary BINARY_MODE=onefile` for a single executable. The
release workflow builds and checks the executable only for version
tags, then attaches `glidepath-X.Y.Z-windows-x64.exe` to the GitHub
Release with build provenance. A failed Windows build blocks
publication. Branch pushes and PRs do not compile binaries.
- A compiled-app self-test covering packaged data/artwork, projection,
save/load, spawned Monte Carlo workers and GUI rendering, plus
`docs/packaging.md` with local build and validation instructions.

### Changed

- Nuitka's source-build requirements are locked and included in the dependency
audit. Windows bundles carry the upstream runtime licences and attribution
notices, also attached to releases as a readable ZIP with build provenance.
- README, getting-started guide and GitHub Pages now explain the Windows
download, launching and upgrading it, and the unsigned-app warning,
alongside the existing uv installation route.
- Dependency tooling refreshed through `make deps` under the seven-day
cooldown while adding Nuitka 4.2 and zstandard. The PySide6 runtime
pin remains 6.11.2.

## [1.0.2] - 2026-08-29

### Added
Expand Down
16 changes: 12 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ PySide6 shell (`glidepath/gui/`) over the UI-agnostic app layer
- `make audit` — pip-audit the lockfile for known CVEs.
- `make sonar` — tests + local SonarQube scan.
- `make hooks` — install pre-commit hooks.
- `make binary` — Nuitka folder build; `BINARY_MODE=onefile` for one executable.

## Supply-chain policy (permanent, all sessions)

Expand Down Expand Up @@ -98,18 +99,25 @@ releases). Rules:

- SemVer; the version lives in `[project] version`, released as a
`vX.Y.Z` tag on `main`. Releases are tag + GitHub Release + PyPI
(sdist/wheel via trusted publishing) — no binary artifacts yet
(§4.10 records why).
(sdist/wheel via trusted publishing). Only release tags trigger the
Windows Nuitka build in `release.yml`; its tested single executable is
attached to the GitHub Release (planning §4.10, `docs/packaging.md`).
- `CHANGELOG.md` (Keep a Changelog format) is curated in the release PR;
the tagged version's section becomes the GitHub Release notes.
- To cut a release, on an up-to-date `dev`: `make bump V=X.Y.Z`, move the
- To cut a release, on a release branch based on up-to-date `main`:
`make bump V=X.Y.Z`, move the
Unreleased items into a dated `## [X.Y.Z]` section, `make check`, PR to
`main`. After the merge, tag the merge commit `vX.Y.Z` and push the
tag; `release.yml` validates it (tag on main, version match, changelog
section present), builds and smoke-tests the sdist/wheel, publishes
them to PyPI with PEP 740 attestations once the `pypi` environment
deployment is manually approved (GitHub → the run's review prompt),
then creates the GitHub Release with the artifacts attached.
then creates the GitHub Release with the artifacts attached. The
Windows build must also pass before PyPI publication; only Python
packages are uploaded to PyPI. The runtime licence ZIP is attached alongside
the executable; all release artifacts carry build provenance. The executable
is not yet Authenticode-signed. Refresh `third-party-licences/` when changing
the bundled Python, PySide6 or Nuitka versions.

## Coding conventions

Expand Down
7 changes: 5 additions & 2 deletions LICENSE-DATA
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
Data licence notice
===================

Everything in this repository is MIT-licensed (see LICENSE) EXCEPT the
following bundled data file:
Glidepath's own code is MIT-licensed (see LICENSE). The following bundled
data file has a separate licence:

src/glidepath/regions/uk/data/returns_history.toml

Expand All @@ -26,3 +26,6 @@ The MIT licence grant does not extend to that file. To obtain a
distribution free of CC BY-NC-SA material, delete the file and
regenerate a replacement from a source of your choosing with
scripts/build_returns_history.py as a starting point.

Third-party runtime licences and notices in third-party-licences/ retain
their original terms; Glidepath's MIT licence does not replace them.
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Canonical developer commands for glidepath. See CLAUDE.md.

.DEFAULT_GOAL := help
.PHONY: help check fix test deps bump audit sonar hooks
.PHONY: help check fix test deps bump audit sonar hooks binary

BINARY_MODE ?= standalone

# Per-machine venv naming: Windows and WSL share this checkout, so each
# platform gets its own environment (see CLAUDE.md).
Expand Down Expand Up @@ -33,12 +35,16 @@ help:
@echo "make audit - pip-audit the lockfile for known CVEs"
@echo "make sonar - run tests then a local SonarQube scan (needs sonar-scanner)"
@echo "make hooks - install pre-commit hooks"
@echo "make binary - compile with Nuitka (BINARY_MODE=standalone or onefile)"

# All non-dependency commands use `uv run --locked` so they can never
# silently re-resolve the lockfile — only `make deps` may change it.
sync:
uv sync --locked

binary:
uv run --locked --group binary python scripts/build_binary.py --mode $(BINARY_MODE)

check:
uv run --locked ruff check .
uv run --locked ruff format --check .
Expand Down Expand Up @@ -75,7 +81,7 @@ bump:
uv run --locked python scripts/check_dep_age.py

audit:
uv export --frozen --no-emit-project --output-file requirements-audit.txt
uv export --frozen --all-groups --no-emit-project --output-file requirements-audit.txt
uv run --locked pip-audit --disable-pip --requirement requirements-audit.txt

sonar:
Expand Down
45 changes: 40 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,32 @@ based solely on this tool.

## Install

Glidepath is installed from [PyPI](https://pypi.org/project/glidepath/)
### Windows download

From **v1.1.0**, Windows x64 users can run Glidepath without Python, uv
or terminal commands:

1. Open the [latest GitHub Release](https://github.com/williajm/glidepath/releases/latest).
2. Under **Assets**, download `glidepath-X.Y.Z-windows-x64.exe`.
3. Keep it in a folder of your choice and double-click it to launch.

This is a runnable app, not an installer; it does not add shortcuts or
update itself. For a newer version, close Glidepath and download the new
executable. Your saved `.glidepath.json` plans stay where you put them.

The executable is **not yet signed with a Windows publisher certificate**.
Windows may show “Windows protected your PC” or “unrecognised app”. Check
that the file came from this repository's release before deciding whether
to proceed. GitHub build provenance does not remove that warning.
If the latest release has no `.exe` yet, use the uv installation below.

Runtime licences and attribution notices are included in the executable
and in the release's `glidepath-X.Y.Z-third-party-licences.zip` download.
That archive also lists the corresponding upstream sources.

### Install with uv

Alternatively, install Glidepath from [PyPI](https://pypi.org/project/glidepath/)
with [uv](https://docs.astral.sh/uv/), which also fetches the Python it
needs — you do not need Python installed first.

Expand Down Expand Up @@ -210,14 +235,24 @@ Releases are `vX.Y.Z` tags on `main`. Each one is published to
trusted publishing with PEP 740 attestations, and as a GitHub Release
carrying its notes from
[`CHANGELOG.md`](https://github.com/williajm/glidepath/blob/main/CHANGELOG.md)
with the same artifacts attached, each carrying signed build
with the same Python artifacts and a tested Windows x64 executable attached,
each carrying signed build
provenance — verify a downloaded file with
`gh attestation verify <file> -R williajm/glidepath`. The signed
provenance bundle is attached to each release too
(`glidepath-X.Y.Z-provenance.intoto.jsonl`), so verification also
works offline with `--bundle`. There are no
packaged binary builds (installer/exe) yet — install from PyPI as
above.
works offline with `--bundle`.

From v1.1.0, the tag-triggered workflow also builds
`glidepath-X.Y.Z-windows-x64.exe` with Nuitka. Download it from the
GitHub Release and double-click to run; Python and uv are not required.
The Windows build runs only for release tags, after tag validation, and
must pass its compiled-app checks before publication. Ordinary branch
pushes and PRs run the normal CI checks without compiling an executable.
The executable is not yet Authenticode-signed, so Windows may show a
SmartScreen warning; the build provenance above is a separate mechanism.
See [`docs/packaging.md`](https://github.com/williajm/glidepath/blob/main/docs/packaging.md)
for local builds and the release workflow.

## Data licences

Expand Down
25 changes: 24 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,30 @@ glidepath".

## Step 1 — Install

Glidepath is installed with [uv](https://docs.astral.sh/uv/), which also
### Windows: download and run

From **v1.1.0**, a single executable is available for Windows x64.
You do not need Python, uv or a terminal.

1. Open the [latest GitHub Release](https://github.com/williajm/glidepath/releases/latest).
2. Expand **Assets** if needed and download `glidepath-X.Y.Z-windows-x64.exe`.
3. Move it to a folder where you want to keep the app, then double-click it.

This is the app itself, so there is no installation wizard. You can create
a shortcut yourself. When a new version is released, close the app and
download the newer executable; keep your saved `.glidepath.json` plans.

The executable is not yet signed with a Windows publisher certificate.
Windows may show **“Windows protected your PC”** or **“unrecognised app”**.
Check that you downloaded it from this repository's release before deciding
whether to proceed. Windows may remember that choice for the same file.

If the latest release does not list an `.exe` yet, use the uv route below.
Otherwise, continue to **Step 2 — First launch**.

### Install with uv

You can also install Glidepath with [uv](https://docs.astral.sh/uv/), which
fetches the Python it needs — you do not need Python installed first.

1. **Install uv.** Open a terminal (Windows: PowerShell) and run one
Expand Down
Loading