feat(release)!: releases on Forgejo, signed by one key; tools.lock; Forgejo Actions only #13

Merged
HumanBean merged 6 commits from HumanBean/Longshot:feat/forgejo-releases into main 2026-11-13 02:07:47 +00:00
Owner

Why

Longshot can't release anything yet: its release system, update checks and CI still point at GitHub, at a repository that isn't ours. This PR replaces all of that with plain Forgejo releases signed by one key, so the Longshot Launcher (MagicBeanDev/LongshotLauncher#1) can install and update the builder safely.

What changed

The design is in docs/releases.md (with how the launcher's setup exe installs itself or runs portable); the maintainer's step-by-step guide is the new RELEASING.md. Both, with scripts/release.py, tests/test_release.py and cliff.toml, are byte-identical in the launcher's repository.

  • A release is a Forgejo release, with no generic packages, pointer files or serials. Each version ships:

    • LongshotBuilder-<v>-windows-x64.zip: the setup as LongshotBuilder.exe, the builder and Longshot's sources, tools.lock, VERSION, and recomplauncher.json at the top;
    • Longshot-<v>-x86_64.AppImage, with recomplauncher.json inside;
    • tools.lock;
    • SHA256SUMS and SHA256SUMS.sig.
  • How CI publishes (.forgejo/workflows/release.yml): the first job checks tools.lock (tools.py check --require) and the tag, before any build. Then both builders are built, and the release is created as a draft, every file uploaded, and published last. Nightly runs then prune the nightlies past the newest 5.

  • Channels come from the release list (scripts/release.py releases() reads every release, page by page until an empty page, drafts and pre-releases told apart by their fields; newest()):

    • stable: the newest published, non-pre-release vX.Y.Z;
    • rc: the newest vX.Y.Z-rc.N or vX.Y.Z;
    • nightly: the newest nightly-<n>-<hash>, counted from the fork commit e2a4d45.

    Pulling a version means deleting it or setting it back to a draft; its channel then falls back by itself.

  • No bot account or release token. CI makes the release with the job's own token (forgejo.token, passed to release.py as RELEASE_TOKEN, as in the launcher's release.yml): on a push or tag in this repository Forgejo 17 gives it write access to this repository only, which covers every call release.py makes. tools.py run by hand takes the maintainer's own token as RELEASE_TOKEN.

  • One key. CI signs SHA256SUMS with the RELEASE_SIGNING_KEY secret (ECDSA P-256, via openssl). scripts/release_keys.json lists the public key the launcher trusts (id 5e18330371d7f5aa). keys_from_cs() reads the launcher's Core/ReleaseTrust.cs with its comments left out, inside the BuiltIn initializer's braces.

  • tools.lock replaces the builder parts. It is a line per tool: name, version, system, sha256, size, dest, prefix, and URLs: the copy in Longshot's long-lived tools release first, then where it came from (upstream, or NotRexed/Hookshot's builder-parts) as the fallback.

    • Who uses it: builder/setup/setup.c and the launcher download each missing tool, verify it, share it across versions, and unpack its prefix as its dest.
    • scripts/tools.py has check, fetch, install, mirror (puts the copy first), add, and assemble python|cpm|third-party|profile.
    • The release workflow stops in its first job until every tool the Windows builder needs is in the lock: "the tools release is missing: …; RELEASING.md, One-time setup".
  • Tags, not release PRs; no release-please. A maintainer pushes vX.Y.Z-rc.N or vX.Y.Z, or uses Create tag in the web UI.

    • What scripts/release.py check-tag refuses: a tag that is not on main, not above every published release, or lower than the bump the commits since the last stable call for (feat → minor, fix/perf → patch, ! or BREAKING CHANGE: → major).
    • Notes: git-cliff writes them at release time (cliff.toml, version pinned and checked by SHA-256). They go on the release page; nothing is committed back.
    • Preview: scripts/release.py next prints the version and the notes locally.
  • Removed:

    • the parts store, release_parts.py (its package step is now scripts/builder_package.py), make_setup_parts.py, release_parts.py previous, and the seed list;
    • the release-channels pointers, serials, and the nightly/release key split with offline signing;
    • the release-please workflow and scripts;
    • AppImage zsync updates;
    • tests/test_release_cpm_add.py and tests/test_release_prompt_packages.py (tests/test_tools_lock.py covers that code now), and tests/test_updater_asset_digest.py with its fixture (they tested the updater's downloads, which Updater.cs no longer has);
    • .github/ (only dependabot.yml was left).
    • Kept: scripts/linux/make_parts.py. The Linux builder's local-list installer (LONGSHOT_LINUX_LIST) and its tests use it, but nothing in the release produces parts any more.
  • The builders don't update themselves. host/hos_update.c is deleted. The WPF, Linux and Python builders and the setup look up no release.

  • RecompLauncher:

    • recomplauncher.json ships in both builders.
    • The Windows zip's setup installs the tools.lock beside it, with no release lookup.
    • RecompLauncher still only reads GitHub releases (see docs/releases.md, RecompLauncher).
  • CI is Forgejo Actions only.

    • Renamed: source-regressions.yml is now ci.yml, and the PR title check is a job inside it; it takes the WIP prefix off with #48's strip_wip.
    • Release workflow: release.yml calls build-linux.yml and build-windows.yml (formerly linux-appimage.yml and windows-builder.yml). Every action is pinned by commit, Forgejo's upload/download-artifact too.
    • Unchanged: vulkan-dlss.yml, third-party.yml, and #48's pr-description.yml and pull_request_template.md.
  • Kept from the previous rounds: the third_party check and the corrected notes; hardware_report.py ported to the Forgejo API; no NotRexed/Longshot references.

  • Docs updated: docs/versioning.md (the bump rule, no release-please), CONTRIBUTING's Pull request titles, the README's install, update and network sections, docs/forgejo-runners.md (the workflow names and the Cloudflare upload limit).

  • Rebased on main after #48 (d725b363).

Forgejo limits, checked on git.magicbean.dev (Forgejo 17.0.1): [attachment] MAX_SIZE is 2048 MB; Cloudflare in front cuts uploads at 100 MB, so CI uploads to FORGEJO_UPLOAD_API (the instance without Cloudflare) when it is set; [repository.release] ALLOWED_TYPES must stay empty; GET /repos/{owner}/{repo}/releases gives at most 50 per page (max_response_items), and its draft=/pre-release= parameters are filters. All in RELEASING.md, One-time setup.

What tools.lock holds now (each line's first URL is the tools release, which is still to fill):

Tool Fallback source
llvm-mingw 20261104 upstream (mstorsjo)
python 3.14 (with the prompt packages) Hookshot v2.1.0's archive (NotRexed/Hookshot builder-parts)
emsdk Hookshot v2.1.0's archive
cpm Hookshot v2.1.0's archive
wgpu-native 29.0.1.1 Hookshot v2.1.0's archive

Still to put in the tools release before the first release: tools.py mirror the five above, and assemble + add cpm with dav1d, third-party (the prebuilt a64emit.dll, lld-link.exe, SDL2/SDL3 and zbic.dll only) and profile (the play profile).

Flags and settings

Flag New or changed What it does Default now Goal: on by default? Helps on Doesn't help on Untested on Untested, but should help on
LONGSHOT_LAUNCHER_TRUST new a launcher also trusts the release key in this .pub.pem (to try releases signed with a test key) unset no: testing only trying a release on a fork players - -

RELEASE_SIGNING_KEY (secret), FORGEJO_UPLOAD_API (variable) and RELEASE_TOKEN (the job token, or a maintainer's own for tools.py by hand) are CI's and the scripts', in RELEASING.md, One-time setup.

Measurements

No game or GPU behaviour changes. The measurements are the test suites and the builds.

main: d725b363 (2026-11-13)
this PR: feat/forgejo-releases @ 5171786f
Hardware: Linux (CachyOS), Python 3.14, .NET 10 (~/.dotnet)
Scene: none (game-free)
Method: LONGSHOT_VULKAN_HEADERS=/usr/include python -I -B tests/run_all.py, 2 interleaved runs each (main, PR, main, PR); tests/test_release.py against the code before the fix; the WPF builder with dotnet build -p:EnableWindowsTargeting=true
How to reproduce: the commands above, in a checkout of each commit

main @ d725b363 this PR @ 5171786f
tests/run_all.py, run 1 491 passed, 3 failed, 37 skipped (93 s) 490 passed, 2 failed, 36 skipped (90 s)
tests/run_all.py, run 2 491 passed, 3 failed, 37 skipped (90 s) 490 passed, 2 failed, 36 skipped (97 s)
failures test_bake_portability (no zstandard), test_builder_log_read_only (no Tk), test_updater_asset_digest (built for net8.0, no .NET 8 runtime here) test_bake_portability, test_builder_log_read_only (the same two)
tests/test_release.py 6 of 18 fail (the old release.py with the new tests, at 22a280fa) 18 passed
tests/test_tools_lock.py (not on main) 10 passed
dotnet build LongshotBuilder - 0 warnings, 0 errors
main 1: 491 passed, 3 failed, 37 skipped in 93 s (linux, 24 at once)
PR   1: 490 passed, 2 failed, 36 skipped in 90 s (linux, 24 at once)
main 2: 491 passed, 3 failed, 37 skipped in 90 s (linux, 24 at once)
PR   2: 490 passed, 2 failed, 36 skipped in 97 s (linux, 24 at once)
(the same at c9a3b0dc and 2b8af4ec, two interleaved pairs each)
old release.py: FAIL test_the_trusted_keys, test_a_draft_then_the_files_then_published,
  test_check_tag_against_the_published_releases, test_every_release_on_every_page,
  test_prune_keeps_the_newest_nightlies_and_everything_else, test_publish_refuses_a_published_tag_on_a_later_page

Testing

Tested: on Linux (this PC), the game-free checks (tests/run_all.py, interleaved against main, in Measurements); tests/test_release.py against a fake Forgejo that filters draft, pre-release, limit and page as Forgejo 17 does (publish, prune, check-tag, paging); dotnet build of the Windows builder (0 warnings, 0 errors); builder/setup/setup.c with llvm-mingw -Wall -Werror; release.py sums/sign/verify with the real release key on real packaged files (in LongshotLauncher#1). After the rebase onto main @ ef8b784f (#39, #72; no conflicts), on the merged tree: 489 passed, 3 failed, 36 skipped: the two known ones, plus "byte-compile every tracked .py", which needs a git checkout (the tree was extracted, "not a git repository"); every .py in the tree parses. The new Forgejo runner (unraid-linux-1) runs LongshotLauncher#1's CI green, with the same job image as these workflows. Tested but not measured: nothing.

Not tested: a workflow run on Longshot (Actions is off on this repo until this merges; the workflows were parsed, not run); a real release, publish and prune against git.magicbean.dev; the tools release (not filled yet: release.yml refuses to publish until it is); a game build.

Needs other testers:

  • Windows 10/11: the Windows builder zip from the first v3.0.0-rc.1 (installs its tools from tools.lock, then builds the game), expecting a working game folder and no release lookup (request to open once rc.1 is built)

Screenshots

Nothing on screen. The release page is Forgejo's own.

Risk and rollback

Until the tools release is filled, every push to main stops in release.yml's first job (by design: no nightly without the tools). A bad release is pulled by deleting it or setting it back to a draft; the channel falls back by itself. The PR as a whole is reverted by reverting its commits; nothing it publishes is committed back to the repository.

Reviewer notes

Start with docs/releases.md, then scripts/release.py (releases, check_tag, publish, prune, keys_from_cs) and .forgejo/workflows/release.yml. The shared files must stay byte-identical with LongshotLauncher#1. Left for later on purpose: release.py's TITLE takes any scope where forge.py's takes [\w./-]+ (no effect on the bump).

Issues

See MagicBeanDev/LongshotLauncher#1 (the launcher's side).

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead). Only the rename's edits are undone, and the notes are corrected.
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers. None in the commits, at the maintainer's request.
  • Flags table filled in; README / docs/development.md switch reference updated.
  • Screenshots for a change that shows on screen. Nothing on screen.
  • A row in docs/tested-configurations.md for each measured run. No game run.
  • MKWVK_API_VERSION bumped. Not changed.
  • The SPIR-V headers regenerated with glslang. No shader changed.
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened (test_setup_tar_diagnostics, test_ship_state_list, test_recomplauncher_json, test_tools_lock).
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths.
  • Docs updated.

Manual steps (none done)

  1. The key is made (id 5e18330371d7f5aa, in scripts/release_keys.json and the launcher's Core/ReleaseTrust.cs).
  2. Org secret: RELEASE_SIGNING_KEY (the private PEM). No token: the job's own is used. Leave nightly-* tags unprotected.
  3. Org variable: FORGEJO_UPLOAD_API, the API address that bypasses Cloudflare.
  4. app.ini: [attachment] MAX_SIZE at least the AppImage's size, and [repository.release] ALLOWED_TYPES left empty.
  5. Actions and runners: enable Actions and register runners per docs/forgejo-runners.md. Longshot needs Linux and Windows.
  6. The tools release: tools.py mirror the five tools, then assemble + add cpm (+dav1d), third-party and profile; tools.py check --require must pass.
  7. RecompLauncher: choose between a GitHub mirror and a Forgejo feature request.
  8. First releases: v3.0.0-rc.1, then v3.0.0.
## Why Longshot can't release anything yet: its release system, update checks and CI still point at GitHub, at a repository that isn't ours. This PR replaces all of that with plain Forgejo releases signed by one key, so the Longshot Launcher (MagicBeanDev/LongshotLauncher#1) can install and update the builder safely. ## What changed **The design** is in [docs/releases.md](docs/releases.md) (with how the launcher's setup exe installs itself or runs portable); the maintainer's step-by-step guide is the new [RELEASING.md](RELEASING.md). Both, with `scripts/release.py`, `tests/test_release.py` and `cliff.toml`, are byte-identical in the launcher's repository. - **A release is a Forgejo release**, with no generic packages, pointer files or serials. Each version ships: - `LongshotBuilder-<v>-windows-x64.zip`: the setup as `LongshotBuilder.exe`, the builder and Longshot's sources, `tools.lock`, `VERSION`, and `recomplauncher.json` at the top; - `Longshot-<v>-x86_64.AppImage`, with `recomplauncher.json` inside; - `tools.lock`; - `SHA256SUMS` and `SHA256SUMS.sig`. - **How CI publishes** (`.forgejo/workflows/release.yml`): the first job checks `tools.lock` (`tools.py check --require`) and the tag, before any build. Then both builders are built, and the release is created as a draft, every file uploaded, and published last. Nightly runs then prune the nightlies past the newest 5. - **Channels come from the release list** (`scripts/release.py` `releases()` reads every release, page by page until an empty page, drafts and pre-releases told apart by their fields; `newest()`): - stable: the newest published, non-pre-release `vX.Y.Z`; - rc: the newest `vX.Y.Z-rc.N` or `vX.Y.Z`; - nightly: the newest `nightly-<n>-<hash>`, counted from the fork commit e2a4d45. Pulling a version means deleting it or setting it back to a draft; its channel then falls back by itself. - **No bot account or release token.** CI makes the release with the job's own token (`forgejo.token`, passed to `release.py` as `RELEASE_TOKEN`, as in the launcher's `release.yml`): on a push or tag in this repository Forgejo 17 gives it write access to this repository only, which covers every call `release.py` makes. `tools.py` run by hand takes the maintainer's own token as `RELEASE_TOKEN`. - **One key.** CI signs `SHA256SUMS` with the `RELEASE_SIGNING_KEY` secret (ECDSA P-256, via openssl). `scripts/release_keys.json` lists the public key the launcher trusts (id `5e18330371d7f5aa`). `keys_from_cs()` reads the launcher's `Core/ReleaseTrust.cs` with its comments left out, inside the `BuiltIn` initializer's braces. - **`tools.lock`** replaces the builder parts. It is a line per tool: name, version, system, sha256, size, dest, prefix, and URLs: the copy in Longshot's long-lived `tools` release first, then where it came from (upstream, or NotRexed/Hookshot's `builder-parts`) as the fallback. - **Who uses it:** `builder/setup/setup.c` and the launcher download each missing tool, verify it, share it across versions, and unpack its prefix as its dest. - **`scripts/tools.py`** has `check`, `fetch`, `install`, `mirror` (puts the copy first), `add`, and `assemble python|cpm|third-party|profile`. - **The release workflow** stops in its first job until every tool the Windows builder needs is in the lock: "the tools release is missing: …; RELEASING.md, One-time setup". - **Tags, not release PRs; no release-please.** A maintainer pushes `vX.Y.Z-rc.N` or `vX.Y.Z`, or uses *Create tag* in the web UI. - **What `scripts/release.py check-tag` refuses:** a tag that is not on main, not above every published release, or lower than the bump the commits since the last stable call for (feat → minor, fix/perf → patch, `!` or `BREAKING CHANGE:` → major). - **Notes:** git-cliff writes them at release time (`cliff.toml`, version pinned and checked by SHA-256). They go on the release page; nothing is committed back. - **Preview:** `scripts/release.py next` prints the version and the notes locally. - **Removed:** - the parts store, `release_parts.py` (its `package` step is now `scripts/builder_package.py`), `make_setup_parts.py`, `release_parts.py previous`, and the seed list; - the `release-channels` pointers, serials, and the nightly/release key split with offline signing; - the release-please workflow and scripts; - AppImage zsync updates; - `tests/test_release_cpm_add.py` and `tests/test_release_prompt_packages.py` (`tests/test_tools_lock.py` covers that code now), and `tests/test_updater_asset_digest.py` with its fixture (they tested the updater's downloads, which `Updater.cs` no longer has); - `.github/` (only `dependabot.yml` was left). - **Kept:** `scripts/linux/make_parts.py`. The Linux builder's local-list installer (`LONGSHOT_LINUX_LIST`) and its tests use it, but nothing in the release produces parts any more. - **The builders don't update themselves.** `host/hos_update.c` is deleted. The WPF, Linux and Python builders and the setup look up no release. - **RecompLauncher:** - `recomplauncher.json` ships in both builders. - The Windows zip's setup installs the `tools.lock` beside it, with no release lookup. - RecompLauncher still only reads GitHub releases (see docs/releases.md, *RecompLauncher*). - **CI is Forgejo Actions only.** - **Renamed:** `source-regressions.yml` is now `ci.yml`, and the PR title check is a job inside it; it takes the WIP prefix off with #48's `strip_wip`. - **Release workflow:** `release.yml` calls `build-linux.yml` and `build-windows.yml` (formerly `linux-appimage.yml` and `windows-builder.yml`). Every action is pinned by commit, Forgejo's upload/download-artifact too. - **Unchanged:** `vulkan-dlss.yml`, `third-party.yml`, and #48's `pr-description.yml` and `pull_request_template.md`. - **Kept from the previous rounds:** the third_party check and the corrected notes; `hardware_report.py` ported to the Forgejo API; no `NotRexed/Longshot` references. - **Docs updated:** `docs/versioning.md` (the bump rule, no release-please), CONTRIBUTING's *Pull request titles*, the README's install, update and network sections, `docs/forgejo-runners.md` (the workflow names and the Cloudflare upload limit). - Rebased on main after #48 (d725b363). **Forgejo limits, checked on git.magicbean.dev (Forgejo 17.0.1):** `[attachment] MAX_SIZE` is 2048 MB; Cloudflare in front cuts uploads at 100 MB, so CI uploads to `FORGEJO_UPLOAD_API` (the instance without Cloudflare) when it is set; `[repository.release] ALLOWED_TYPES` must stay empty; `GET /repos/{owner}/{repo}/releases` gives at most 50 per page (`max_response_items`), and its `draft=`/`pre-release=` parameters are filters. All in RELEASING.md, *One-time setup*. **What `tools.lock` holds now** (each line's first URL is the `tools` release, which is still to fill): | Tool | Fallback source | |---|---| | `llvm-mingw` 20261104 | upstream (mstorsjo) | | `python` 3.14 (with the prompt packages) | Hookshot v2.1.0's archive (NotRexed/Hookshot `builder-parts`) | | `emsdk` | Hookshot v2.1.0's archive | | `cpm` | Hookshot v2.1.0's archive | | `wgpu-native` 29.0.1.1 | Hookshot v2.1.0's archive | **Still to put in the `tools` release before the first release:** `tools.py mirror` the five above, and `assemble` + `add` `cpm` with dav1d, `third-party` (the prebuilt a64emit.dll, lld-link.exe, SDL2/SDL3 and zbic.dll only) and `profile` (the play profile). ## Flags and settings | Flag | New or changed | What it does | Default now | Goal: on by default? | Helps on | Doesn't help on | Untested on | Untested, but should help on | |---|---|---|---|---|---|---|---|---| | `LONGSHOT_LAUNCHER_TRUST` | new | a launcher also trusts the release key in this .pub.pem (to try releases signed with a test key) | unset | no: testing only | trying a release on a fork | players | - | - | `RELEASE_SIGNING_KEY` (secret), `FORGEJO_UPLOAD_API` (variable) and `RELEASE_TOKEN` (the job token, or a maintainer's own for `tools.py` by hand) are CI's and the scripts', in RELEASING.md, *One-time setup*. ## Measurements No game or GPU behaviour changes. The measurements are the test suites and the builds. main: d725b363 (2026-11-13) this PR: feat/forgejo-releases @ 5171786f Hardware: Linux (CachyOS), Python 3.14, .NET 10 (`~/.dotnet`) Scene: none (game-free) Method: `LONGSHOT_VULKAN_HEADERS=/usr/include python -I -B tests/run_all.py`, 2 interleaved runs each (main, PR, main, PR); `tests/test_release.py` against the code before the fix; the WPF builder with `dotnet build -p:EnableWindowsTargeting=true` How to reproduce: the commands above, in a checkout of each commit | | main @ d725b363 | this PR @ 5171786f | |---|---|---| | `tests/run_all.py`, run 1 | 491 passed, 3 failed, 37 skipped (93 s) | 490 passed, 2 failed, 36 skipped (90 s) | | `tests/run_all.py`, run 2 | 491 passed, 3 failed, 37 skipped (90 s) | 490 passed, 2 failed, 36 skipped (97 s) | | failures | `test_bake_portability` (no zstandard), `test_builder_log_read_only` (no Tk), `test_updater_asset_digest` (built for net8.0, no .NET 8 runtime here) | `test_bake_portability`, `test_builder_log_read_only` (the same two) | | `tests/test_release.py` | 6 of 18 fail (the old `release.py` with the new tests, at 22a280fa) | 18 passed | | `tests/test_tools_lock.py` | (not on main) | 10 passed | | `dotnet build` LongshotBuilder | - | 0 warnings, 0 errors | ``` main 1: 491 passed, 3 failed, 37 skipped in 93 s (linux, 24 at once) PR 1: 490 passed, 2 failed, 36 skipped in 90 s (linux, 24 at once) main 2: 491 passed, 3 failed, 37 skipped in 90 s (linux, 24 at once) PR 2: 490 passed, 2 failed, 36 skipped in 97 s (linux, 24 at once) (the same at c9a3b0dc and 2b8af4ec, two interleaved pairs each) old release.py: FAIL test_the_trusted_keys, test_a_draft_then_the_files_then_published, test_check_tag_against_the_published_releases, test_every_release_on_every_page, test_prune_keeps_the_newest_nightlies_and_everything_else, test_publish_refuses_a_published_tag_on_a_later_page ``` ## Testing **Tested:** on Linux (this PC), the game-free checks (`tests/run_all.py`, interleaved against main, in Measurements); `tests/test_release.py` against a fake Forgejo that filters `draft`, `pre-release`, `limit` and `page` as Forgejo 17 does (publish, prune, check-tag, paging); `dotnet build` of the Windows builder (0 warnings, 0 errors); `builder/setup/setup.c` with llvm-mingw `-Wall -Werror`; `release.py sums`/`sign`/`verify` with the real release key on real packaged files (in LongshotLauncher#1). After the rebase onto main @ ef8b784f (#39, #72; no conflicts), on the merged tree: 489 passed, 3 failed, 36 skipped: the two known ones, plus "byte-compile every tracked .py", which needs a git checkout (the tree was extracted, "not a git repository"); every `.py` in the tree parses. The new Forgejo runner (unraid-linux-1) runs LongshotLauncher#1's CI green, with the same job image as these workflows. Tested but not measured: nothing. **Not tested:** a workflow run on Longshot (Actions is off on this repo until this merges; the workflows were parsed, not run); a real release, `publish` and `prune` against git.magicbean.dev; the `tools` release (not filled yet: `release.yml` refuses to publish until it is); a game build. **Needs other testers:** - Windows 10/11: the Windows builder zip from the first `v3.0.0-rc.1` (installs its tools from `tools.lock`, then builds the game), expecting a working game folder and no release lookup (request to open once rc.1 is built) ## Screenshots Nothing on screen. The release page is Forgejo's own. ## Risk and rollback Until the `tools` release is filled, every push to main stops in `release.yml`'s first job (by design: no nightly without the tools). A bad release is pulled by deleting it or setting it back to a draft; the channel falls back by itself. The PR as a whole is reverted by reverting its commits; nothing it publishes is committed back to the repository. ## Reviewer notes Start with docs/releases.md, then `scripts/release.py` (`releases`, `check_tag`, `publish`, `prune`, `keys_from_cs`) and `.forgejo/workflows/release.yml`. The shared files must stay byte-identical with LongshotLauncher#1. Left for later on purpose: release.py's `TITLE` takes any scope where forge.py's takes `[\w./-]+` (no effect on the bump). ## Issues See MagicBeanDev/LongshotLauncher#1 (the launcher's side). ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). Only the rename's edits are undone, and the notes are corrected. - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [ ] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. None in the commits, at the maintainer's request. - [x] Flags table filled in; README / docs/development.md switch reference updated. - [ ] Screenshots for a change that shows on screen. Nothing on screen. - [ ] A row in `docs/tested-configurations.md` for each measured run. No game run. - [ ] `MKWVK_API_VERSION` bumped. Not changed. - [ ] The SPIR-V headers regenerated with glslang. No shader changed. - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened (`test_setup_tar_diagnostics`, `test_ship_state_list`, `test_recomplauncher_json`, `test_tools_lock`). - [x] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. - [x] Docs updated. ## Manual steps (none done) 1. **The key** is made (id `5e18330371d7f5aa`, in `scripts/release_keys.json` and the launcher's `Core/ReleaseTrust.cs`). 2. **Org secret:** `RELEASE_SIGNING_KEY` (the private PEM). No token: the job's own is used. Leave `nightly-*` tags unprotected. 3. **Org variable:** `FORGEJO_UPLOAD_API`, the API address that bypasses Cloudflare. 4. **`app.ini`:** `[attachment] MAX_SIZE` at least the AppImage's size, and `[repository.release] ALLOWED_TYPES` left empty. 5. **Actions and runners:** enable Actions and register runners per `docs/forgejo-runners.md`. Longshot needs Linux and Windows. 6. **The `tools` release:** `tools.py mirror` the five tools, then `assemble` + `add` `cpm` (+dav1d), `third-party` and `profile`; `tools.py check --require` must pass. 7. **RecompLauncher:** choose between a GitHub mirror and a Forgejo feature request. 8. **First releases:** `v3.0.0-rc.1`, then `v3.0.0`.
The launcher (launcher/Core, launcher/App, Avalonia, .NET 10) from Hookshot's
feat/hookshot-launcher branch, its core tests (tests/launcher_core,
tests/test_launcher_core.py) and the hand-over contract (docs/launcher-upgrade.md,
tests/fixtures/launcher), renamed to Longshot: the program LongshotLauncher(.exe),
the projects and namespaces, the window title, the home folders
(%LOCALAPPDATA%\LongshotLauncher, ~/.local/share/longshot/launcher), ~/Games/Longshot,
the LONGSHOT_LAUNCHER_* variables, the shortcuts and the AppImage name. The logo is
assets/longshot.png, and the Windows program's icon builder/setup/longshot.ico.

The version vectors the core is checked against are now the test's own: Longshot's
tests/test_update_tracks.py has no nightly-<count>-<commit> names. The release check
asks git.magicbean.dev's releases until the release and update redesign replaces it.
CI compiles the launcher and runs its core tests with .NET 10.
Hookshot's release process was GitHub's: release-please, GitHub releases with rolling
nightly and rc releases, AppImageUpdate, RecompLauncher and build attestations. None of
it works for Longshot. This replaces it from scratch (docs/releases.md):

- A release is a set of immutable files in the generic package longshot/<version> on
  git.magicbean.dev: the two launchers, the two tools lists, and longshot-release.json,
  a signed file naming each with its size and SHA-256. Tools parts go to one
  content-addressed store, longshot-parts/store.
- Each channel (release, rc, nightly) is a signed pointer in MagicBeanDev/longshot-channels,
  committed last, with a serial that only goes up (the launcher refuses replays).
- Signatures are ECDSA P-256/SHA-256: .NET's own crypto in the launcher, openssl in
  scripts/release.py. The nightly key (CI's) signs nightlies only; the release key
  signs release candidates and releases and can stay offline.
- The launcher checks every channel, release and download, installs a version's tools
  atomically, and replaces itself only with a file that matched its hash and answered
  --self-check; --rollback puts the previous launcher back.
- .github/workflows/release.yml (Forgejo Actions) makes the nightly from main and a
  release candidate or release from a v* tag; the Linux and Windows build workflows
  upload their parts when it calls them, and build on pull requests otherwise.

Removed: release-please's config and manifest, recomplauncher.json, the GitHub release
scripts and their tests, the release jobs, attestations, the rolling releases and the
AppImage's update information. The keys, the channels repository, the bot token, the
secrets and the runners are manual steps (docs/releases.md, Setting it up).
HumanBean force-pushed feat/forgejo-releases from 87d2c59a6f to 9d2989d6b5 2026-11-12 23:24:42 +00:00 Compare
HumanBean force-pushed feat/forgejo-releases from 9d2989d6b5 to eda1e14081 2026-11-12 23:24:49 +00:00 Compare
HumanBean changed title from feat(release)!: signed releases on git.magicbean.dev, and the launcher updating from them to feat(release)!: signed builder releases on git.magicbean.dev, Forgejo Actions only 2026-11-12 23:25:40 +00:00
HumanBean force-pushed feat/forgejo-releases from eda1e14081 to a5fd9c088c 2026-11-13 00:09:47 +00:00 Compare
HumanBean changed title from feat(release)!: signed builder releases on git.magicbean.dev, Forgejo Actions only to feat(release)!: releases on Forgejo, signed by one key; tools.lock; Forgejo Actions only 2026-11-13 00:12:54 +00:00
HumanBean force-pushed feat/forgejo-releases from a5fd9c088c to f327d1018b 2026-11-13 00:18:14 +00:00 Compare
Author
Owner

Pushed f327d101: the PR title check in .forgejo/workflows/ci.yml now strips both of Forgejo's work-in-progress prefixes (WIP: and [WIP], any case) before checking the Conventional Commit, not only WIP: , so a [WIP] fix: ... title passes like a WIP: fix: ... one (#48 opens PRs as WIP).

Pushed f327d101: the PR title check in .forgejo/workflows/ci.yml now strips both of Forgejo's work-in-progress prefixes (`WIP:` and `[WIP]`, any case) before checking the Conventional Commit, not only `WIP: `, so a `[WIP] fix: ...` title passes like a `WIP: fix: ...` one (#48 opens PRs as WIP).
Author
Owner

Pushed a82a2230: the release key is in scripts/release_keys.json (ECDSA P-256, key id 5e18330371d7f5aa). Its private half is the organization secret RELEASE_SIGNING_KEY (set 2026-11-13). The launcher has the same key built in (LongshotLauncher#1). tests/test_release.py passes.

Pushed a82a2230: the release key is in `scripts/release_keys.json` (ECDSA P-256, key id `5e18330371d7f5aa`). Its private half is the organization secret `RELEASE_SIGNING_KEY` (set 2026-11-13). The launcher has the same key built in (LongshotLauncher#1). `tests/test_release.py` passes.
HumanBean left a comment

Reviewed the code at a82a2230. The design holds up and the signing contract with the launcher matches. Two bugs need fixing before the first release, though, and one test no longer compiles.

Bugs

  1. releases() sends draft=true, so it only ever sees drafts (scripts/release.py:396). In Forgejo 17, draft is a filter (is_draft = value), not "include drafts". ?draft=true on codeberg's forgejo/forgejo returns [], and without it the published releases come back. A writer token already sees drafts without the flag. As written:

    • check-tag's "above every release" check is empty, because existing (non-drafts of a drafts-only list) is always [];
    • prune never deletes a published nightly;
    • the "already published" guard in publish never fires;
    • channels and next see nothing.

    The fake Forgejo in test_release.py ignores the parameter, so the tests pass anyway. Fix: drop draft=true. Also make prune ignore a 404 on the tag delete: a draft nightly left by a failed run has no tag yet.

  2. keys_from_cs reads no key from the launcher's ReleaseTrust.cs as it now is (scripts/release.py:311). It cuts the text at the first ; after BuiltIn =. The new comment line, ... keygen, 2026-11-13; RELEASING.md ..., has a ; before the FromSpki line. The result is [], so the launcher's release workflow stops at verify ("no key to check with"). This file is shared, so the fix belongs here and in LongshotLauncher#1. Either cut at };, or drop the // comments before the search.

  3. tests/test_updater_asset_digest.py and tests/updater_asset_digest_test.cs test the old updater. The fixture calls Updater.RunLatestSetup, UpdateFolder(w, folder, tag, fetched:) and DownloadOnce, which the 34-line Updater.cs no longer has. With a .NET SDK present (ci.yml installs one), the check can't compile. Delete both, or rewrite them for what is left.

Worth changing

  1. tools.py check --require runs in the publish job, after both builds. The lock has no third-party or profile yet, so every push to main builds Windows and Linux in full, then fails. Moving the check into the version job fails fast with the same message.
  2. tools.lock: python, emsdk, cpm and wgpu-native are tried first from NotRexed/Hookshot's builder-parts release. Their second URL, the tools release, doesn't exist yet. Until tools.py mirror has run, one repository we don't control is the only source, for every Windows install. These are Hookshot's own archives, not upstream, so put the forge copy first once it is mirrored, and make mirroring part of the first-release checklist.

Nits

  • download-artifact@v4 and upload-artifact@v4 (data.forgejo.org) are pinned by tag; every other action is pinned by SHA.
  • ci.yml's title job copies the WIP-stripping regex. main now has strip_wip in check_pr_description.py (#48), and forge.py already imports it, so use that one. Otherwise ci.yml and #48 don't conflict: #13 merges cleanly onto d725b363, and the title job reads forge.TITLE/TITLE_TYPES, which main has.
  • release.py's TITLE takes any scope ([^)]*); forge.py's takes [\w./-]+. Not a bug, but the bump rule and the title check can drift apart.
  • Comments left pointing at the deleted host/hos_update.c: builder/LongshotBuilder/Runner.cs:116, builder/py/longshot_builder/core.py:72. Also, .github/dependabot.yml is GitHub-only.

Checked and fine:

  • secrets only on push (no pull_request_target);
  • draft, then upload, then publish;
  • prune limited to nightly-<n>-<hash>, so it never touches stables or tools;
  • a failed run's draft is reused;
  • concurrency on the ref, without cancelling;
  • no NotRexed/Longshot or release-please left in code.

Works with the launcher (MagicBeanDev/LongshotLauncher#1)

Compared field by field:

  • Same in both repos: scripts/release.py, docs/releases.md, RELEASING.md, cliff.toml and tests/test_release.py are byte-identical.
  • The key: identical on both sides. release_keys.json and ReleaseTrust.cs hold the same SPKI, a P-256 key with id 5e18330371d7f5aa. The launcher's 8-byte hex id equals key_id's 16 hex.
  • Signature: openssl dgst -sha256 -sign writes DER over the exact bytes of SHA256SUMS. The launcher checks it with VerifyData(..., SHA256, Rfc3279DerSequence) on those same bytes.
  • SHA256SUMS: the <hex> <name> lines are written and read by the same regex.
  • Assets and channels: the file names (LongshotBuilder-<v>-windows-x64.zip, Longshot-<v>-x86_64.AppImage, tools.lock, SHA256SUMS, SHA256SUMS.sig) match BuilderFile. The channel rules match ReleaseManifest.Newest: stable needs !prerelease, and tools sits on no channel.
  • Nightly order: both sides order nightlies by their count.
  • tools.lock: both sides use the same schema. The launcher, like setup.c, takes only the <prefix> folder.
  • Zip layout: the zip has LongshotBuilder/ and the launcher runs LongshotBuilder/LongshotBuilder.exe. It then starts tools/python/python.exe on tools/nx2/make_game.py, which builder_package.py and the lock's dests provide. The AppImage's usr/share/longshot and usr/python are where Toolchain looks.
  • Release list: the launcher reads up to 20 pages of 50 and drops drafts itself, so it does not have bug 1.

The mismatches are bug 2 (the key parser breaks on the launcher's own file) and bug 1 (it also breaks check-tag and prune in the launcher's release workflow, since the file is shared). Both are fixed in release.py, in both repos at once.

Reviewed the code at a82a2230. The design holds up and the signing contract with the launcher matches. Two bugs need fixing before the first release, though, and one test no longer compiles. **Bugs** 1. **`releases()` sends `draft=true`, so it only ever sees drafts** (scripts/release.py:396). In Forgejo 17, `draft` is a filter (`is_draft = value`), not "include drafts". `?draft=true` on codeberg's forgejo/forgejo returns `[]`, and without it the published releases come back. A writer token already sees drafts without the flag. As written: - `check-tag`'s "above every release" check is empty, because `existing` (non-drafts of a drafts-only list) is always `[]`; - `prune` never deletes a published nightly; - the "already published" guard in `publish` never fires; - `channels` and `next` see nothing. The fake Forgejo in test_release.py ignores the parameter, so the tests pass anyway. Fix: drop `draft=true`. Also make `prune` ignore a 404 on the tag delete: a draft nightly left by a failed run has no tag yet. 2. **`keys_from_cs` reads no key from the launcher's ReleaseTrust.cs as it now is** (scripts/release.py:311). It cuts the text at the first `;` after `BuiltIn =`. The new comment line, `... keygen, 2026-11-13; RELEASING.md ...`, has a `;` before the `FromSpki` line. The result is `[]`, so the launcher's release workflow stops at `verify` ("no key to check with"). This file is shared, so the fix belongs here and in LongshotLauncher#1. Either cut at `};`, or drop the `//` comments before the search. 3. **tests/test_updater_asset_digest.py and tests/updater_asset_digest_test.cs test the old updater.** The fixture calls `Updater.RunLatestSetup`, `UpdateFolder(w, folder, tag, fetched:)` and `DownloadOnce`, which the 34-line Updater.cs no longer has. With a .NET SDK present (ci.yml installs one), the check can't compile. Delete both, or rewrite them for what is left. **Worth changing** 4. `tools.py check --require` runs in the publish job, after both builds. The lock has no `third-party` or `profile` yet, so every push to main builds Windows and Linux in full, then fails. Moving the check into the `version` job fails fast with the same message. 5. tools.lock: python, emsdk, cpm and wgpu-native are tried first from NotRexed/Hookshot's `builder-parts` release. Their second URL, the `tools` release, doesn't exist yet. Until `tools.py mirror` has run, one repository we don't control is the only source, for every Windows install. These are Hookshot's own archives, not upstream, so put the forge copy first once it is mirrored, and make mirroring part of the first-release checklist. **Nits** - `download-artifact@v4` and `upload-artifact@v4` (data.forgejo.org) are pinned by tag; every other action is pinned by SHA. - ci.yml's title job copies the WIP-stripping regex. main now has `strip_wip` in check_pr_description.py (#48), and forge.py already imports it, so use that one. Otherwise ci.yml and #48 don't conflict: #13 merges cleanly onto d725b363, and the title job reads `forge.TITLE`/`TITLE_TYPES`, which main has. - release.py's `TITLE` takes any scope (`[^)]*`); forge.py's takes `[\w./-]+`. Not a bug, but the bump rule and the title check can drift apart. - Comments left pointing at the deleted host/hos_update.c: builder/LongshotBuilder/Runner.cs:116, builder/py/longshot_builder/core.py:72. Also, `.github/dependabot.yml` is GitHub-only. Checked and fine: - secrets only on `push` (no `pull_request_target`); - draft, then upload, then publish; - prune limited to `nightly-<n>-<hash>`, so it never touches stables or `tools`; - a failed run's draft is reused; - `concurrency` on the ref, without cancelling; - no `NotRexed/Longshot` or release-please left in code. **Works with the launcher (MagicBeanDev/LongshotLauncher#1)** Compared field by field: - **Same in both repos:** scripts/release.py, docs/releases.md, RELEASING.md, cliff.toml and tests/test_release.py are byte-identical. - **The key:** identical on both sides. `release_keys.json` and `ReleaseTrust.cs` hold the same SPKI, a P-256 key with id `5e18330371d7f5aa`. The launcher's 8-byte hex id equals `key_id`'s 16 hex. - **Signature:** `openssl dgst -sha256 -sign` writes DER over the exact bytes of SHA256SUMS. The launcher checks it with `VerifyData(..., SHA256, Rfc3279DerSequence)` on those same bytes. - **SHA256SUMS:** the `<hex> <name>` lines are written and read by the same regex. - **Assets and channels:** the file names (`LongshotBuilder-<v>-windows-x64.zip`, `Longshot-<v>-x86_64.AppImage`, `tools.lock`, `SHA256SUMS`, `SHA256SUMS.sig`) match `BuilderFile`. The channel rules match `ReleaseManifest.Newest`: stable needs `!prerelease`, and `tools` sits on no channel. - **Nightly order:** both sides order nightlies by their count. - **tools.lock:** both sides use the same schema. The launcher, like setup.c, takes only the `<prefix>` folder. - **Zip layout:** the zip has `LongshotBuilder/` and the launcher runs `LongshotBuilder/LongshotBuilder.exe`. It then starts `tools/python/python.exe` on `tools/nx2/make_game.py`, which builder_package.py and the lock's dests provide. The AppImage's `usr/share/longshot` and `usr/python` are where Toolchain looks. - **Release list:** the launcher reads up to 20 pages of 50 and drops drafts itself, so it does not have bug 1. The mismatches are bug 2 (the key parser breaks on the launcher's own file) and bug 1 (it also breaks check-tag and prune in the launcher's release workflow, since the file is shared). Both are fixed in release.py, in both repos at once.
@ -0,0 +102,4 @@
run: |
git fetch -q --tags origin
# (every tool the Windows builder needs is in tools.lock: RELEASING.md, One-time setup)
python3 -I scripts/tools.py check --require
Author
Owner

This fails every nightly until third-party and profile are in the lock, but only after both full builds. Running it in the version job fails before any build.

This fails every nightly until `third-party` and `profile` are in the lock, but only after both full builds. Running it in the `version` job fails before any build.
@ -0,0 +308,4 @@
def keys_from_cs(text):
"""[spki] of the launcher's ReleaseKeys.BuiltIn: the ReleaseKey.FromSpki("...") entries of its initializer"""
start = text.index("BuiltIn =")
return [base64.b64decode(k) for k in re.findall(r'ReleaseKey\.FromSpki\("([A-Za-z0-9+/=]+)"\)', text[start:text.index(";", start)])]
Author
Owner

Cuts at the first ;. The launcher's new comment above the key (2026-11-13; RELEASING.md) has one, so this returns [] there and the launcher's verify step fails. Cut at };, or skip // lines.

Cuts at the first `;`. The launcher's new comment above the key (`2026-11-13; RELEASING.md`) has one, so this returns `[]` there and the launcher's `verify` step fails. Cut at `};`, or skip `//` lines.
@ -0,0 +393,4 @@
def releases(repo):
out, page = [], 1
while True:
items = api("GET", "/repos/%s/releases?draft=true&limit=50&page=%d" % (repo, page)) or []
Author
Owner

draft=true filters to drafts only on Forgejo 17 (cond.And(builder.Eq{"is_draft": value})). Drop it: a writer token gets drafts and published releases together. As written, check-tag's 'above every release', prune and the already-published guard see nothing.

`draft=true` filters to drafts only on Forgejo 17 (`cond.And(builder.Eq{"is_draft": value})`). Drop it: a writer token gets drafts and published releases together. As written, check-tag's 'above every release', prune and the already-published guard see nothing.
tools.lock Outdated
@ -0,0 +13,4 @@
# Taken as they are from Hookshot v2.1.0's Windows builder (NotRexed/Hookshot, release builder-parts), until
# Longshot assembles its own: python, emsdk, cpm and wgpu-native.
tool llvm-mingw 20261104 windows-x64 552191b5754c49a5a20723276a6fc56541ea1922c4c50b7b669022333fc1b40f 191037046 LongshotBuilder/tools/llvm-mingw llvm-mingw-20261104-ucrt-x86_64 https://github.com/mstorsjo/llvm-mingw/releases/download/20261104/llvm-mingw-20261104-ucrt-x86_64.zip https://git.magicbean.dev/MagicBeanDev/Longshot/releases/download/tools/552191b5754c49a5a20723276a6fc56541ea1922c4c50b7b669022333fc1b40f-llvm-mingw.zip
tool python 3.14-hookshot-v2.1.0 windows-x64 c0eb2e3fbabae82cb7d3dc61ec4b0e9b4b854269d9c38c3bbcc5a08e4497bd12 55961280 LongshotBuilder/tools/python HookshotBuilder/tools/python https://github.com/NotRexed/Hookshot/releases/download/builder-parts/HookshotBuilder-python-c0eb2e3fbaba.zip https://git.magicbean.dev/MagicBeanDev/Longshot/releases/download/tools/c0eb2e3fbabae82cb7d3dc61ec4b0e9b4b854269d9c38c3bbcc5a08e4497bd12-python.zip
Author
Owner

The first URL is a repository we don't control, and the second doesn't exist yet (no tools release). Mirror before the first release, and once mirrored, put the forge copy first for these four: they are Hookshot's archives, not upstream.

The first URL is a repository we don't control, and the second doesn't exist yet (no `tools` release). Mirror before the first release, and once mirrored, put the forge copy first for these four: they are Hookshot's archives, not upstream.
HumanBean force-pushed feat/forgejo-releases from a82a2230be to 22a280fab5 2026-11-13 01:22:43 +00:00 Compare
- release.yml checks tools.lock (tools.py check --require) in its first job, so a lock
  without third-party or profile stops before both builds: "the tools release is missing: ...;
  RELEASING.md, One-time setup".
- tools.lock: every tool's first URL is its copy in the tools release, where it came from
  (upstream, NotRexed/Hookshot's builder-parts) the fallback; tools.py mirror puts the copy
  first. RELEASING.md: the tools release is filled before the first release.
- tests/test_updater_asset_digest.py and its fixture are gone: they tested the updater's
  downloads, which Updater.cs no longer has.
- Forgejo's upload-artifact and download-artifact pinned by commit; ci.yml's title check
  takes the WIP prefix off with check_pr_description.strip_wip; no host/hos_update.c in
  comments; .github/ (dependabot.yml) removed.
On a push or tag in its repository, Forgejo 17 gives the Actions job token write access to every
unit of that repository and no other (routers/api/v1/permissions/repo_access.go): it may create,
upload to, edit, publish and delete releases and delete tags there, which is all release.py does.
release.yml passes secrets.GITHUB_TOKEN as FORGEJO_TOKEN; release.py and tools.py read
FORGEJO_TOKEN (a maintainer's own token when run by hand). No permissions: key, since Forgejo 17
ignores it with a warning. RELEASING.md and docs/releases.md: one secret, and nightly-* tags
left unprotected so the prune can delete them.
The shared files follow LongshotLauncher#1: its Windows release is a self-installing
LongshotLauncher-<v>-windows-x64-setup.exe and a -portable.zip, and docs/releases.md says how the
launcher installs itself (Install or Run portable, --install, --uninstall, the portable marker,
the Linux applications menu entry and <AppImage>.home). release.py and tools.py read the job
token as RELEASE_TOKEN again, the name the launcher's release.yml passes forgejo.token under,
and Longshot's release.yml passes forgejo.token the same way: still no secret or bot account.
Author
Owner

Review fixes (HumanBean's review at a82a2230), rebased on main @ d725b363 (#48; its pr-description.yml and pull_request_template.md kept). Four new commits:

  • 22a280fa fix(release): list every release, page by page; read the launcher's keys past its comments: scripts/release.py and tests/test_release.py only.
  • c9a3b0dc fix(release): the tools check before any build, the forge's copies first; review nits.
  • 2b8af4ec fix(release): releases with the job's own token, no RELEASE_TOKEN or bot account (the maintainer's request, after the review).
  • 5171786f docs(release): the launcher's setup exe and portable zip; the job token as RELEASE_TOKEN: the shared docs follow LongshotLauncher#1 (LongshotLauncher-<v>-windows-x64-setup.exe and -portable.zip, and a section on how the launcher installs itself: Install / Run portable, --install, --uninstall, the portable marker, Add to applications menu, <AppImage>.home), and the env var is RELEASE_TOKEN again, the name the launcher's workflow uses.

Bugs

  1. releases() and draft=true. It now sends no draft=/pre-release= filter (on Forgejo 17 they keep only the releases whose field equals the value) and reads every page (limit=50&page=N until an empty page, a page repeated by the server stops it too); draft and prerelease come from each release. prune deletes the published nightlies past the newest 5, and drafts of nightlies older than the newest published one (left by failed runs), and takes a 404 on a draft's missing tag. Never a stable, an rc or tools. The fake Forgejo in test_release.py now does what Forgejo's ListReleases does: drafts only with a token, draft=/pre-release= as filters, limit (default 30, at most max_response_items) and page, newest first, and a 404 on a tag that isn't there. New tests: check-tag against published releases (refused below a published v3.0.0, allowed once it is a draft again), prune over published and draft nightlies plus tools/stable/rc/2.x (only nightlies go), every release over 3 pages at a 20-item server limit, and publish's "already published" guard for a tag on a later page. On the old release.py 6 of 18 tests fail; on the new one all 18 pass.
  2. keys_from_cs. It blanks // and /* */ comments first (strings kept), finds BuiltIn ='s initializer {, reads to its matching }, and takes the ReleaseKey.FromSpki("…") entries inside. Test: comments with ;, { and } before, inside and after the initializer, a commented-out key, and a FromSpki in a string outside it.
  3. tests/test_updater_asset_digest.py and tests/updater_asset_digest_test.cs deleted (and their run_all_times.json entries). They tested the updater's downloads (RunLatestSetup, DownloadOnce, the asset digests), and Updater.cs no longer downloads anything: there is nothing of it left for them to test. The builder builds (dotnet build -p:EnableWindowsTargeting=true, 0 warnings).

Worth changing

  1. The tools check is the first step of release.yml's first job (version, before the tag check and any build). It fails with the tools release is missing: third-party, profile (…); RELEASING.md, One-time setup and an ::error:: line. The publish job no longer runs it. RELEASING.md's troubleshooting row is moved to The version.
  2. tools.lock: the tools release URL is first for all five tools, the upstream (llvm-mingw) or NotRexed/Hookshot builder-parts URL the fallback, sha256s and sizes unchanged. tools.py mirror now puts the copy first. test_tools_lock checks that every line's first URL is …/releases/download/tools/<sha256>-<name>.zip. RELEASING.md, One-time setup step 6: the tools release must be filled before the first release (tools.py mirror the five, assemble + add cpm, third-party, profile); docs/releases.md says the same.

Nits

  • forgejo/upload-artifact pinned to 16871d9e and download-artifact to d8d0a990: the commits v4 points to now (4.3.1 and 4.1.4 with the GHES check off), so nothing else changes.
  • ci.yml's title job uses forge.desc.strip_wip (scripts/check_pr_description.py, which forge.py imports); the sparse checkout is the whole scripts/ folder, so nothing to add. Checked with [WIP] feat(release): x, WIP: fix: y and nope.
  • host/hos_update.c comments removed from Runner.cs, core.py (and one in tests/test_linux_update.py).
  • .github/dependabot.yml deleted, so .github/ is gone (the testing skill's .github/ is now .forgejo/).
  • release.py's TITLE scope vs forge.py's: left as it is (no effect on the bump), noted in the description.

The job's token instead of a RELEASE_TOKEN secret and a bot (2b8af4ec). From Forgejo 17.0.1's source: the task token authenticates as the Actions user (services/auth/method/action_task_token.go), reqToken accepts it, and routers/api/v1/permissions/repo_access.go gives it AccessModeWrite on every unit of the task's own repository (read for a fork's pull request, 404 for any other repository). Every endpoint release.py calls needs only reqRepoWriter(TypeReleases) (create, edit/publish, delete a release, upload and delete assets) or reqRepoWriter(TypeCode) (delete a tag), so the job token covers all of it, no fallback needed. The one limit: a protected tag can't be deleted by it (422), so RELEASING.md says to leave nightly-* unprotected. permissions: isn't set: Forgejo 17 ignores the key and shows a warning (calculateWarnings in services/actions/notifier.go). release.yml passes forgejo.token as the env var RELEASE_TOKEN (no secret of that name any more); release.py and tools.py read RELEASE_TOKEN (a maintainer's own token for tools.py mirror/add by hand). RELEASING.md's one-time setup now has one secret, RELEASE_SIGNING_KEY; docs/releases.md says what the token may do.

Shared files. scripts/release.py, tests/test_release.py (22a280fa), RELEASING.md and docs/releases.md (c9a3b0dc) changed (and again in 2b8af4ec and 5171786f); LongshotLauncher#1 takes all five shared files as they are at 5171786f. cliff.toml is unchanged.

Tests (LONGSHOT_VULKAN_HEADERS=/usr/include python -I -B tests/run_all.py, interleaved main, PR, main, PR):

main @ d725b363 this PR @ 5171786f
tests/run_all.py, run 1 491 passed, 3 failed, 37 skipped (93 s) 490 passed, 2 failed, 36 skipped (90 s)
tests/run_all.py, run 2 491 passed, 3 failed, 37 skipped (90 s) 490 passed, 2 failed, 36 skipped (97 s)
failures test_bake_portability (no zstandard), test_builder_log_read_only (no Tk), test_updater_asset_digest (built for net8.0, no .NET 8 runtime here) test_bake_portability, test_builder_log_read_only (the same two)
tests/test_release.py 6 of 18 fail (the old release.py with the new tests, at 22a280fa) 18 passed
tests/test_tools_lock.py (not on main) 10 passed
dotnet build LongshotBuilder - 0 warnings, 0 errors
main 1: 491 passed, 3 failed, 37 skipped in 93 s (linux, 24 at once)
PR   1: 490 passed, 2 failed, 36 skipped in 90 s (linux, 24 at once)
main 2: 491 passed, 3 failed, 37 skipped in 90 s (linux, 24 at once)
PR   2: 490 passed, 2 failed, 36 skipped in 97 s (linux, 24 at once)
(the same at c9a3b0dc and 2b8af4ec, two interleaved pairs each)
old release.py: FAIL test_the_trusted_keys, test_a_draft_then_the_files_then_published,
  test_check_tag_against_the_published_releases, test_every_release_on_every_page,
  test_prune_keeps_the_newest_nightlies_and_everything_else, test_publish_refuses_a_published_tag_on_a_later_page
Review fixes (HumanBean's review at a82a2230), rebased on main @ d725b363 (#48; its `pr-description.yml` and `pull_request_template.md` kept). Four new commits: - **22a280fa** `fix(release): list every release, page by page; read the launcher's keys past its comments`: `scripts/release.py` and `tests/test_release.py` only. - **c9a3b0dc** `fix(release): the tools check before any build, the forge's copies first; review nits`. - **2b8af4ec** `fix(release): releases with the job's own token, no RELEASE_TOKEN or bot account` (the maintainer's request, after the review). - **5171786f** `docs(release): the launcher's setup exe and portable zip; the job token as RELEASE_TOKEN`: the shared docs follow LongshotLauncher#1 (`LongshotLauncher-<v>-windows-x64-setup.exe` and `-portable.zip`, and a section on how the launcher installs itself: Install / Run portable, `--install`, `--uninstall`, the `portable` marker, *Add to applications menu*, `<AppImage>.home`), and the env var is `RELEASE_TOKEN` again, the name the launcher's workflow uses. **Bugs** 1. **`releases()` and `draft=true`.** It now sends no `draft=`/`pre-release=` filter (on Forgejo 17 they keep only the releases whose field equals the value) and reads every page (`limit=50&page=N` until an empty page, a page repeated by the server stops it too); `draft` and `prerelease` come from each release. `prune` deletes the published nightlies past the newest 5, and drafts of nightlies older than the newest published one (left by failed runs), and takes a 404 on a draft's missing tag. Never a stable, an rc or `tools`. The fake Forgejo in `test_release.py` now does what Forgejo's `ListReleases` does: drafts only with a token, `draft=`/`pre-release=` as filters, `limit` (default 30, at most `max_response_items`) and `page`, newest first, and a 404 on a tag that isn't there. New tests: check-tag against published releases (refused below a published `v3.0.0`, allowed once it is a draft again), prune over published and draft nightlies plus `tools`/stable/rc/2.x (only nightlies go), every release over 3 pages at a 20-item server limit, and publish's "already published" guard for a tag on a later page. **On the old `release.py` 6 of 18 tests fail; on the new one all 18 pass.** 2. **`keys_from_cs`.** It blanks `//` and `/* */` comments first (strings kept), finds `BuiltIn =`'s initializer `{`, reads to its matching `}`, and takes the `ReleaseKey.FromSpki("…")` entries inside. Test: comments with `;`, `{` and `}` before, inside and after the initializer, a commented-out key, and a `FromSpki` in a string outside it. 3. **`tests/test_updater_asset_digest.py` and `tests/updater_asset_digest_test.cs` deleted** (and their `run_all_times.json` entries). They tested the updater's downloads (`RunLatestSetup`, `DownloadOnce`, the asset digests), and `Updater.cs` no longer downloads anything: there is nothing of it left for them to test. The builder builds (`dotnet build -p:EnableWindowsTargeting=true`, 0 warnings). **Worth changing** 4. **The tools check is the first step of `release.yml`'s first job** (`version`, before the tag check and any build). It fails with `the tools release is missing: third-party, profile (…); RELEASING.md, One-time setup` and an `::error::` line. The publish job no longer runs it. RELEASING.md's troubleshooting row is moved to *The version*. 5. **`tools.lock`: the `tools` release URL is first for all five tools**, the upstream (llvm-mingw) or NotRexed/Hookshot `builder-parts` URL the fallback, sha256s and sizes unchanged. `tools.py mirror` now puts the copy first. `test_tools_lock` checks that every line's first URL is `…/releases/download/tools/<sha256>-<name>.zip`. RELEASING.md, One-time setup step 6: **the `tools` release must be filled before the first release** (`tools.py mirror` the five, `assemble` + `add` cpm, third-party, profile); docs/releases.md says the same. **Nits** - `forgejo/upload-artifact` pinned to `16871d9e` and `download-artifact` to `d8d0a990`: the commits `v4` points to now (4.3.1 and 4.1.4 with the GHES check off), so nothing else changes. - ci.yml's title job uses `forge.desc.strip_wip` (`scripts/check_pr_description.py`, which forge.py imports); the sparse checkout is the whole `scripts/` folder, so nothing to add. Checked with `[WIP] feat(release): x`, `WIP: fix: y` and `nope`. - `host/hos_update.c` comments removed from `Runner.cs`, `core.py` (and one in `tests/test_linux_update.py`). - `.github/dependabot.yml` deleted, so `.github/` is gone (the testing skill's `.github/` is now `.forgejo/`). - release.py's `TITLE` scope vs forge.py's: left as it is (no effect on the bump), noted in the description. **The job's token instead of a `RELEASE_TOKEN` secret and a bot** (2b8af4ec). From Forgejo 17.0.1's source: the task token authenticates as the Actions user (`services/auth/method/action_task_token.go`), `reqToken` accepts it, and `routers/api/v1/permissions/repo_access.go` gives it `AccessModeWrite` on every unit of the task's own repository (read for a fork's pull request, 404 for any other repository). Every endpoint `release.py` calls needs only `reqRepoWriter(TypeReleases)` (create, edit/publish, delete a release, upload and delete assets) or `reqRepoWriter(TypeCode)` (delete a tag), so the job token covers all of it, no fallback needed. The one limit: a **protected** tag can't be deleted by it (422), so RELEASING.md says to leave `nightly-*` unprotected. `permissions:` isn't set: Forgejo 17 ignores the key and shows a warning (`calculateWarnings` in `services/actions/notifier.go`). `release.yml` passes `forgejo.token` as the env var `RELEASE_TOKEN` (no secret of that name any more); `release.py` and `tools.py` read `RELEASE_TOKEN` (a maintainer's own token for `tools.py mirror`/`add` by hand). RELEASING.md's one-time setup now has one secret, `RELEASE_SIGNING_KEY`; docs/releases.md says what the token may do. **Shared files.** `scripts/release.py`, `tests/test_release.py` (22a280fa), `RELEASING.md` and `docs/releases.md` (c9a3b0dc) changed (and again in 2b8af4ec and 5171786f); LongshotLauncher#1 takes all five shared files as they are at **5171786f**. `cliff.toml` is unchanged. **Tests** (`LONGSHOT_VULKAN_HEADERS=/usr/include python -I -B tests/run_all.py`, interleaved main, PR, main, PR): | | main @ d725b363 | this PR @ 5171786f | |---|---|---| | `tests/run_all.py`, run 1 | 491 passed, 3 failed, 37 skipped (93 s) | 490 passed, 2 failed, 36 skipped (90 s) | | `tests/run_all.py`, run 2 | 491 passed, 3 failed, 37 skipped (90 s) | 490 passed, 2 failed, 36 skipped (97 s) | | failures | `test_bake_portability` (no zstandard), `test_builder_log_read_only` (no Tk), `test_updater_asset_digest` (built for net8.0, no .NET 8 runtime here) | `test_bake_portability`, `test_builder_log_read_only` (the same two) | | `tests/test_release.py` | 6 of 18 fail (the old `release.py` with the new tests, at 22a280fa) | 18 passed | | `tests/test_tools_lock.py` | (not on main) | 10 passed | | `dotnet build` LongshotBuilder | - | 0 warnings, 0 errors | ``` main 1: 491 passed, 3 failed, 37 skipped in 93 s (linux, 24 at once) PR 1: 490 passed, 2 failed, 36 skipped in 90 s (linux, 24 at once) main 2: 491 passed, 3 failed, 37 skipped in 90 s (linux, 24 at once) PR 2: 490 passed, 2 failed, 36 skipped in 97 s (linux, 24 at once) (the same at c9a3b0dc and 2b8af4ec, two interleaved pairs each) old release.py: FAIL test_the_trusted_keys, test_a_draft_then_the_files_then_published, test_check_tag_against_the_published_releases, test_every_release_on_every_page, test_prune_keeps_the_newest_nightlies_and_everything_else, test_publish_refuses_a_published_tag_on_a_later_page ```
HumanBean force-pushed feat/forgejo-releases from 5171786f02 to a2f2accc84 2026-11-13 01:58:40 +00:00 Compare
HumanBean force-pushed feat/forgejo-releases from a2f2accc84 to ffc0dfa0b0 2026-11-13 01:58:55 +00:00 Compare
Author
Owner

Rebased onto main @ ef8b784f (#39 and #72; no conflicts), now ffc0dfa0, and every commit is authored HumanBean (two still said hckrman101). The description follows #72: a Testing section (Tested / Not tested / Needs other testers) replaces Not tested, and the For everyone box "Testing filled in" is ticked; main's check_pr_description.py passes it, with one warning: the Windows test request opens once v3.0.0-rc.1 exists. On the merged tree run_all.py: 489 passed, 3 failed, 36 skipped: the two known failures, plus the byte-compile check, which needs a git checkout (every .py parses).

Rebased onto main @ ef8b784f (#39 and #72; no conflicts), now ffc0dfa0, and every commit is authored HumanBean (two still said hckrman101). The description follows #72: a **Testing** section (Tested / Not tested / Needs other testers) replaces Not tested, and the For everyone box "Testing filled in" is ticked; main's `check_pr_description.py` passes it, with one warning: the Windows test request opens once `v3.0.0-rc.1` exists. On the merged tree `run_all.py`: 489 passed, 3 failed, 36 skipped: the two known failures, plus the byte-compile check, which needs a git checkout (every `.py` parses).
HumanBean deleted branch feat/forgejo-releases 2026-11-13 02:07:47 +00:00
Sign in to join this conversation.
No description provided.