From 72f51299e5d86cb67e06e26c4b5d1aa6faafb4ca Mon Sep 17 00:00:00 2001 From: owenpkent <20529132+owenpkent@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:19:58 -0400 Subject: [PATCH 1/2] Bring the docs back in line with what the checkout does A sweep after the update client, the candidate workflow and the bill of materials landed. Every change here is either a claim that had stopped being true or a thing the checkout does that nothing documented. Two statements were actively wrong, both falsified by the SBOM work: that the build inventories are "not a complete third-party license inventory or SBOM", and that a release-ready inventory and SBOM "remain separate work". The release plan still said the in-app check was deferred, and its scope freeze still listed auto-update flatly. That one needed splitting rather than deleting: the check is automatic, installing never is, and a silent background update is still out of scope, which is a more useful sentence than either "deferred" or nothing. The README listed `update` in the commands table and explained it nowhere. It now has a section covering what is verified before anything runs and, more importantly, that an update never closes a running Offloader, which is the guarantee rather than a limitation. The documentation index gains updates.md. The agent guide's code map listed none of the installation or update modules, so a reader looking for where maintenance lives had to grep. It now names installation.py, installation_lock.py, update.py and gui/updates.py together with the rule that binds them: neither may force-close a running transfer. Its start-here list points at build-windows.md and updates.md, and says that a build being implemented is not a release being possible, since that is the distinction the plan keeps having to restate. The roadmap still listed a Windows installer and code signing as future work when the packaging is implemented. Replaced with what is actually left, none of which is code: the hardware token, a clean machine, and the Qt LGPL/GPL decision the notices file flags on every build. Added a dated source-validation record, labelled as exactly that. Nothing was frozen, signed or installed in it, so it establishes none of the gates it sits above. Checked rather than assumed: all 68 relative links across the markdown docs resolve, every CLI subcommand appears in the commands table, and every `offload` flag is mentioned in the README. --- AGENTS.md | 7 +++++++ README.md | 28 +++++++++++++++++++++++++++- ROADMAP.md | 9 +++++++-- docs/build-windows.md | 14 +++++++++++--- docs/release-plan.md | 19 ++++++++++++------- 5 files changed, 64 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 48bdb49..f2d8532 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,12 @@ takes priority over features, throughput, and presentation. [docs/release-plan.md](docs/release-plan.md) for the proposed Windows beta. The release plan describes future work; verify the checkout before claiming an installer, signing pipeline, or release gate exists. +- Packaging, signing, the bill of materials and the tag-triggered candidate + workflow are in [docs/build-windows.md](docs/build-windows.md); what an + update checks before it runs anything is in + [docs/updates.md](docs/updates.md). Hardware-key signing and clean-machine + qualification are still gates, so a build being implemented is not a + release being possible. - Inspect the working tree before edits. Preserve unrelated user changes. Keep this guide concise and link to detailed documentation rather than copying it wholesale. @@ -56,6 +62,7 @@ All module paths below are relative to `src/offloader/`. | Timeline import | `timeline.py`: optional OpenTimelineIO integration, media resolution, and ambiguity handling | | Desktop | `gui/main_window.py`, `gui/worker.py`, `gui/queue_view.py`, mode/editor widgets, and `gui/drives.py` | | Persistent state | `config.py`, `presets.py`, `history.py`; `volumes.py` discovers storage and `naming.py` handles naming | +| Installation and updates | `installation.py` and `installation_lock.py` for transactional maintenance and the shared installed-instance lock; `update.py` finds, verifies and hands over a release, wrapped for the app by `gui/updates.py`. Neither may force-close a running transfer; see [docs/updates.md](docs/updates.md) | Python 3.10+ is supported. Core dependencies are xxhash and ReportLab; PySide6 is the GUI extra. Timeline dependencies are separate extras. ffmpeg/ffprobe diff --git a/README.md b/README.md index ac1646d..321d8d4 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,31 @@ manifest lists and exits non-zero if anything is off, so a format script can gat on it. `--allow-cache` skips the page-cache eviction — faster, and may verify memory rather than the device. +### `update` + +```sh +offloader update # is there a newer release? +offloader update --install # download it, verify it, run the installer +``` + +GitHub Releases is the feed, so there is no manifest server and no second +place a version number is recorded. Before anything runs, the download has to +be served from an allowlisted host *after* redirects, hash to what was +computed while streaming, and carry a valid Authenticode signature with this +project's certificate thumbprint, its publisher name, and an embedded +`FileVersion` matching the release. That last check is what stops an older +but still validly signed installer being re-served under a newer asset name. + +**It never closes a running Offloader.** The installer refuses maintenance +while a transfer is in flight, which is the guarantee rather than a +limitation, so updating means finishing or cancelling the job first. The +command says so before the elevation prompt appears. + +The desktop app does the same thing from **Help, Check for updates now**, and +checks once on launch unless that is turned off in Options. Full detail, +including what is deliberately not copied from the reference implementation, +is in [`docs/updates.md`](docs/updates.md). + ### Verification modes | Mode | What it does | Catches | @@ -516,7 +541,8 @@ general-purpose tool reports a filename, a size, and a placeholder icon. | --- | --- | | [`ROADMAP.md`](ROADMAP.md) | What is next, why, and what this will not become | | [`docs/release-plan.md`](docs/release-plan.md) | Windows beta release sequence, packaging, signing, acceptance gates, and recovery | -| [`docs/build-windows.md`](docs/build-windows.md) | Build, sign, and check Windows desktop bundles and installers | +| [`docs/build-windows.md`](docs/build-windows.md) | Build, sign, and check Windows desktop bundles and installers; the bill of materials, and tagging a candidate | +| [`docs/updates.md`](docs/updates.md) | What an update checks before it runs anything, and why it never closes a running transfer | | [`docs/data-safety.md`](docs/data-safety.md) | Threat model: what is guaranteed, what is not, and the bugs behind each guarantee | | [`docs/report-layout.md`](docs/report-layout.md) | Every coordinate of the PDF, measured off the reference report | | [`docs/performance.md`](docs/performance.md) | Why not robocopy, with benchmarks and the confounds that made the first run worthless | diff --git a/ROADMAP.md b/ROADMAP.md index c3c13d9..587a297 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -127,8 +127,13 @@ already does the stem-matching this needs. it settles almost every case for a stat, then the checksum. What is missing is only the wiring into the engine's own skip, where the file being skipped is at the destination rather than under another search root. -- **Windows installer and code signing**, so it can be handed to someone who - does not have Python. +- **A signed, qualified Windows release.** The packaging is implemented: + frozen bundle, NSIS installer, transactional maintenance, signing hooks, a + bill of materials, an update client, and a tag-triggered candidate workflow. + What is left is not code. Signing needs the hardware token, the acceptance + matrix needs a clean machine, and Qt's LGPL/GPL terms against Offloader's + MIT licence need a redistribution decision, which the notices file flags on + every build. See [`docs/release-plan.md`](docs/release-plan.md). - **Per-job report templates and custom branding.** ## Not planned diff --git a/docs/build-windows.md b/docs/build-windows.md index 5a907d4..8653ded 100644 --- a/docs/build-windows.md +++ b/docs/build-windows.md @@ -54,7 +54,8 @@ The builder writes `.offloader-build.json` inside the bundle, an external coverage, and `SHA256SUMS.txt` for the final installer, ZIP, and inventory. A failed build leaves `.offloader-build-incomplete`; it must not be promoted. Source changes during a build invalidate the candidate. These inventories are -provenance and tamper checks, not a complete third-party license inventory or SBOM. +provenance and tamper checks. The third-party licence inventory and the SBOM +are separate outputs of the same build, described below. ## Signing @@ -128,8 +129,7 @@ PEP 440 forms are rejected rather than silently truncated. The spec excludes optional timeline import, disables UPX, and includes the project license and distribution metadata. ffmpeg and ffprobe remain external. -Missing media tools reduce metadata/thumbnails, not copy verification. A -release-ready third-party license inventory and SBOM remain separate work. +Missing media tools reduce metadata/thumbnails, not copy verification. ## Bill of materials and licences @@ -219,6 +219,14 @@ that no job in the workflow attaches what it built to a release. ## Check the artifact +Source validation on 2026-09-21 (Windows x64, Python 3.13.5): 855 tests passed +with 12 skips; lint passed over `src`, `tests`, `scripts` and `build/windows`. +The source archive was built and checked to contain the Windows builder. This +run covers the update client, the tag-triggered candidate workflow and the +bill of materials. **It is a source test run and nothing more:** no bundle was +frozen, nothing was signed, and no installer was executed, so it establishes +none of the gates below. + Installer implementation validation on 2026-09-10 (Windows x64, Python 3.12.10, NSIS 3.12): 727 tests passed with 5 skips and 85% line coverage. Lint passed. The unsigned installer and portable bundle built, and the frozen smoke checks diff --git a/docs/release-plan.md b/docs/release-plan.md index 422fab8..04b3cb5 100644 --- a/docs/release-plan.md +++ b/docs/release-plan.md @@ -112,13 +112,18 @@ credentials, update endpoints, or installation paths. - **Timeline support:** include and test OpenTimelineIO and the currently declared adapter in the desktop bundle if timeline import is advertised for that bundle. Otherwise mark that capability source-only for the beta. -- **Updates:** `offloader update` finds and verifies a release and runs the - signed installer; the in-app check remains deferred. Refuse replacement - while the app or CLI has an active job; never force-kill a copy to install - an update. -- **Scope freeze:** defer new media features, cloud services, notifications, - auto-update, and a marketing website. Fix integrity and packaging blockers - discovered during qualification. +- **Updates:** `offloader update` and the desktop app both find and verify a + release and run the signed installer. Refuse replacement while the app or + CLI has an active job; never force-kill a copy to install an update. See + [updates.md](updates.md). +- **Scope freeze:** defer new media features, cloud services, notifications + and a marketing website. Fix integrity and packaging blockers discovered + during qualification. + + Updating is inside the freeze only as far as it goes today: the *check* is + automatic, and installing never is. Nothing is downloaded, replaced or + restarted without someone asking for it, and a silent background update + stays out of scope. These are working defaults for implementation, not statements that the packaging or safeguards already exist. From 821cff2f6277e24833021aef11de88965e4b2543 Mon Sep 17 00:00:00 2001 From: owenpkent <20529132+owenpkent@users.noreply.github.com> Date: Mon, 21 Sep 2026 21:28:18 -0400 Subject: [PATCH 2/2] Say that the updater cannot see the releases it is documented as ordering This branch exists to make the docs describe the checkout, and one of the claims it added does not. README, the release plan and updates.md all present "offloader update finds a release" as working, and the version ordering section explains at length that prereleases are ordered rather than rejected. The endpoint underneath is /releases/latest, which GitHub documents as returning the newest published full release and excluding prereleases. The first packaged release is planned as 0.1.0b1, so an installed beta cannot discover its successor, and a repository holding only the betas the candidate workflow publishes with --prerelease answers with nothing at all. The ordering is right; the endpoint is wrong. All three places now say so and point at the correction pending on #13, and the release plan no longer counts discovery as a gate that can be signed off. Nothing here claims the fix: it states the limit as the checkout currently has it, which is what this branch is for. --- README.md | 9 +++++++++ docs/release-plan.md | 10 ++++++---- docs/updates.md | 11 +++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 321d8d4..68dd331 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,15 @@ project's certificate thumbprint, its publisher name, and an embedded `FileVersion` matching the release. That last check is what stops an older but still validly signed installer being re-served under a newer asset name. +One limit, stated because this page is meant to describe the checkout rather +than the plan: the feed is GitHub's `/releases/latest`, which is documented as +excluding prereleases. The first packaged release is planned as `0.1.0b1`, so +an installed beta cannot discover the next one through it, and a repository +holding only betas answers with nothing at all. The version grammar already +orders prereleases; it is the endpoint that does not carry them. Reading the +releases collection instead is the correction pending on +[#13](https://github.com/owenpkent/offloader/pull/13). + **It never closes a running Offloader.** The installer refuses maintenance while a transfer is in flight, which is the guarantee rather than a limitation, so updating means finishing or cancelling the job first. The diff --git a/docs/release-plan.md b/docs/release-plan.md index 04b3cb5..73cb4e5 100644 --- a/docs/release-plan.md +++ b/docs/release-plan.md @@ -112,10 +112,12 @@ credentials, update endpoints, or installation paths. - **Timeline support:** include and test OpenTimelineIO and the currently declared adapter in the desktop bundle if timeline import is advertised for that bundle. Otherwise mark that capability source-only for the beta. -- **Updates:** `offloader update` and the desktop app both find and verify a - release and run the signed installer. Refuse replacement while the app or - CLI has an active job; never force-kill a copy to install an update. See - [updates.md](updates.md). +- **Updates:** `offloader update` and the desktop app both check for a + release, verify it, and run the signed installer. Refuse replacement while + the app or CLI has an active job; never force-kill a copy to install an + update. See [updates.md](updates.md). Discovery is not yet a gate that can + be signed off: the feed is `/releases/latest`, which excludes prereleases, + so a beta cannot find its successor until that is corrected. - **Scope freeze:** defer new media features, cloud services, notifications and a marketing website. Fix integrity and packaging blockers discovered during qualification. diff --git a/docs/updates.md b/docs/updates.md index 827c8b6..21ae16c 100644 --- a/docs/updates.md +++ b/docs/updates.md @@ -44,6 +44,17 @@ build that Windows then considers older than the one it replaced. If either version is unreadable the answer is "not newer". String comparison is what makes `1.0.10` look older than `1.0.9`. +**The feed above does not yet carry the prereleases this grammar orders.** +GitHub documents `/releases/latest` as returning the newest published full +release and excluding prereleases, so an installed `0.1.0b1` cannot discover +`0.1.0b2` or `0.1.0rc1` through it, and a repository holding only the betas +the candidate workflow publishes with `--prerelease` answers with nothing at +all. The ordering here is right and the endpoint is wrong; reading the +releases collection instead is the correction pending on +[#13](https://github.com/owenpkent/offloader/pull/13). Until that lands, take +the ordering as describing what the updater would do with a release it can +see, not as evidence that it can see one. + ## What is checked before anything runs | Check | What it stops |