ci(release): release candidates from release-please, Promote to release, and a nightly of main #2018

Merged
hckrman101 merged 15 commits from ci-release-candidates into main 2026-11-11 00:35:09 +00:00
hckrman101 commented 2026-11-10 23:48:59 +00:00 (Migrated from github.com)

#1059 releases straight from release-please's PR: whatever lands in a release PR goes to every player as "latest" with no test build in between. This puts a tested release candidate in front of every release. release-please proposes the candidate in its PR, merging that PR publishes it as a prerelease, and a manual Promote publishes a good one as the release. It also brings in #1062, the rolling nightly prerelease of main (closed in favor of this PR).

Overlap with #2010: #2010 (one-click pre-releases, docs/releasing.md) solves the same problem a different way: a button that tags the next vX.Y.Z-rc.N itself, then a stable release from release-please's PR. The two do not conflict as text, since #2010 only adds new files. They do conflict in what they do: both define how RC tags are numbered and how a stable release is made, and #2010's optional cron nightly overlaps the nightly here. Merge one of them. If this one goes in, #2010's button would make RC tags that release-please does not know about (they are not in the manifest), and Promote's "newest RC" default could pick one of them.

The flow

When What happens
Push to main The nightly prerelease is rebuilt from that commit. release-please opens or updates its release PR, chore: release X.Y.Z-rc.N.
Merge the release PR vX.Y.Z-rc.N is tagged on the merge commit and built like a release. Once every file is there, it is published as a GitHub prerelease, never "latest". The next release PR proposes rc.N+1 of the same version, with the changes since rc.N.
Promote (Actions > Release > Run workflow, input: an RC tag, empty for the newest) vX.Y.Z is tagged on that RC's exact commit, built from it, and published as "latest". Its notes are those of its RCs, newest first. Then .release-please-manifest.json is set to X.Y.Z, so the next release PR proposes the next version's rc.1.

There is no "start a series" step: the first fix after a release proposes the next version's rc.1.

release-please's prerelease versioning, as it behaves

I checked PrereleaseVersioningStrategy in release-please 17.6.0, the version release-please-action v5.0.0 bundles:

  • After a release 2.0.14: fix gives 2.0.15-rc.1, feat gives 2.1.0-rc.1, and a breaking change gives 3.0.0-rc.1.
  • After an RC: fix (and perf, revert) bumps the RC number, 2.0.15-rc.1 -> 2.0.15-rc.2. On a minor RC (2.1.0-rc.1), feat also only bumps the RC number. One quirk: a feat during a patch series moves to the next minor and keeps the RC number (2.0.15-rc.2 -> 2.1.0-rc.2). docs/versioning.md says how to restart it at rc.1 with Release-As:.
  • prerelease-type is rc.1, not rc. With rc, the first one would be 2.0.15-rc, and the one after it 2.0.15-rc.1.

Promote rebuilds the files instead of copying the RC's

The plan was to publish the stable version with the RC's files, byte for byte. That would break updating for every player. Each file records the version it was built as: the tools' version.txt inside the app part, the AppImage's VERSION and file name, and the tag line of hookshot-setup.txt and hookshot-linux.txt. The game (host/hos_update.c), Hookshot Builder (Updater.cs) and the Linux updater compare that version with the latest release's tag. A v2.1.0 release holding 2.1.0-rc.2 files would offer itself as an update forever, and the builder would stop with "The update installed v2.1.0-rc.2, not v2.1.0". Clients already installed have that logic, so newer client code cannot fix it either.

So Promote tags vX.Y.Z on the RC's commit and calls the same build workflows as for an RC. The source is the same commit, the dependencies are the same checked downloads, and the previous stable release supplies the same reused parts. Update parts that did not change are the same files on builder-parts/linux-parts, so an update from the RC downloads little more than what names the version. The AppImage is named after the release's tag (HOOKSHOT_VERSION), not git describe, since that commit now carries two tags.

Details

  • .github/workflows/release.yml is one workflow with three entry points: push to main, the release PR merged (also a push), and workflow_dispatch for Promote. Jobs: release-please (push), promote (dispatch, main only), linux and windows (call linux-appimage.yml and windows-builder.yml with the tag, for an RC or a promoted version), publish (SHA256SUMS, the files table, checks every file is there, then publishes as a prerelease or as "latest"), and record (after a promote).
  • scripts/release_promote.sh prepare checks that the RC is a published prerelease on main, creates the version's tag on its commit through the refs API, and creates the draft release with the RC notes combined (the "Which file do I download?" table is removed from each). Running it again for the same RC continues with the same tag and draft. It refuses an RC tag that is a draft or not on main, and a version that already exists elsewhere or is already released.
  • record runs scripts/release_promote.sh record, which sets the manifest (only while it still holds one of that version's RCs) and .release-please-version, and adds a short CHANGELOG.md entry for the version that points to its RC entries. It pushes this to main with GITHUB_TOKEN, then has release-please rebuild the release PR, because that push starts no workflow. If main does not accept the push (branch protection or a ruleset), it opens a small PR chore(release): record X.Y.Z as released instead. Merge it before the release PR.
  • Configuration: versioning: prerelease and prerelease-type: rc.1 are added. The manifest (2.0.14) and last-release-sha: b2e1c5a6 from #1059 are kept. The release-as: 2.1.0 pin from #1059 is removed. It would have proposed 2.1.0 as a release, with no RC, and then the same version again. Without it, main's feat commits since b2e1c5a6 make the first release PR propose 2.1.0-rc.1 on their own, and promoting that gives 2.1.0. A dry run of release-please 17.6.0 on the fork gave release 2.1.0 with the pin and release 2.1.0-rc.1 without.
  • release-please's version file is .release-please-version. The simple strategy writes version.txt at the root by default, and nx2/version.py reads that file before git describe as the release name of every build from a checkout, the nightly included.
  • Concurrency is on the release-please job only, so the 20+ minute builds of one release no longer hold up the next push. A push waiting behind another one is replaced by the newer push, which reads the same commits and makes the same release (the review nit). Promote has its own group, so a push to main cannot cancel a waiting Promote.
  • The fix from the #1062 review: the Windows nightly clause also matched the Release workflow's workflow_call on a push to main, because github.event_name/github.ref are the caller's. That defeated CI_RELEASE_WINDOWS=false. It is now scoped with !inputs.tag.

Unchanged from #1059, or brought in from #1062:

  • From #1059: SHA256SUMS, build provenance attestations, zsync, the "Which file do I download?" table, and the PR title check.
  • The nightly: on every push to main that is not a release, the build workflows' release jobs move the nightly tag and replace its files (scripts/release_upload.sh nightly). An older run never overwrites a newer commit. The nightly AppImage updates to the next nightly. The readme gets a "Latest development build" section (Windows: HookshotBuilder.exe --tag nightly).
  • docs/versioning.md is rewritten for release candidates and Promote.

Tested on the fork

Actions are currently disabled on hckrman101/Hookshot ("Actions has been disabled for this repository", even for workflow_dispatch), so no workflow could run there this time. Instead, I ran each step the workflow runs from this machine against the fork, on a test branch rc-test: release-please 17.6.0 (release-pr/github-release, the bundled version), the release jobs' scripts/release_upload.sh, the publish job's steps, and scripts/release_promote.sh. Stub files stood in for the builds, named like the real ones and holding the version and the commit. The manifest started at 0.0.1, seeded with a v0.0.1 release.

  1. fix: change one gave release PR chore(rc-test): release 0.0.2-rc.1. Merging it made draft v0.0.2-rc.1 on the merge commit. Uploading and publishing made it a prerelease with all files, SHA256SUMS passed sha256sum -c, and v0.0.1 stayed "latest".
  2. fix: change two gave release 0.0.2-rc.2, with only change two and the compare link v0.0.2-rc.1...v0.0.2-rc.2. Merged and published as a prerelease.
  3. fix: change three gave an open PR for 0.0.2-rc.3, left open.
  4. Promote with an empty input picked v0.0.2-rc.2. v0.0.2 was tagged on rc.2's commit (6d860d5, the same SHA as the rc.2 tag). The draft's notes were the rc.2 and rc.1 entries, newest first, without their tables. After the build, v0.0.2 was published as "latest", not a prerelease, and its SHA256SUMS passed. Its files were the rebuilt ones, carrying v0.0.2 (see above for why the bytes differ from the RC's on purpose). Running prepare again for rc.2 continued with the same draft. It refused v0.0.2-rc.1 (v0.0.2 is already on another commit), v0.0.1 (not an RC tag), and an RC tag that was not a published prerelease.
  5. Record with rc-test protected (PR required, admins included): the direct push was refused, so the PR chore(release): record 0.0.2 as released was opened (fork PR 19). Merging it set the manifest to 0.0.2 and added the 0.0.2 changelog entry. The next release-pr then proposed 0.0.3-rc.1, with change three and the compare link v0.0.2...v0.0.3-rc.1.

Afterwards I deleted every test release, tag and branch on the fork, removed the branch protection, and put back the fork's Actions setting that I had changed briefly.

Also: actionlint 1.7.7 with shellcheck 0.10.0 passes on every workflow, and shellcheck passes on release_promote.sh, release_upload.sh and release_files.sh. The tests that read the build workflows pass: test_appimage_player_build, test_appimage_package_retry, test_recomplauncher_json, test_appimage_scripts.

Not tested:

  • The workflow wiring itself: job conditions with skipped needs, outputs between jobs, the workflow_dispatch input, concurrency, the record job's own git push and gh pr create from the runner (step 5 ran the same commands from here), and the release-please refresh with skip-github-release. These need Actions back on the fork, or the first real run.
  • Real builds from Promote. The build workflows were already tested as reusable workflows in the earlier fork test, and Promote calls them the same way. Not tested: the new HOOKSHOT_VERSION naming of a release AppImage.
  • The CI_RELEASE_WINDOWS=false path after a promote. record then does not run, and docs/versioning.md gives the manual command.

What a maintainer has to do

  1. As for #1059: Settings > Actions > General > "Allow GitHub Actions to create and approve pull requests" (the release PR, and record's fallback PR), and squash merging with the PR title as the commit message.
  2. If main is protected: either allow the Actions bot to push to it, or merge record's small PR after each Promote (before the next release PR).
  3. Decide between this PR and #2010 (above).
  4. Optional: CI_RELEASE_WINDOWS=false to upload the Windows files by hand.
#1059 releases straight from release-please's PR: whatever lands in a release PR goes to every player as "latest" with no test build in between. This puts a tested release candidate in front of every release. release-please proposes the candidate in its PR, merging that PR publishes it as a prerelease, and a manual Promote publishes a good one as the release. It also brings in #1062, the rolling `nightly` prerelease of main (closed in favor of this PR). **Overlap with #2010:** #2010 (one-click pre-releases, `docs/releasing.md`) solves the same problem a different way: a button that tags the next `vX.Y.Z-rc.N` itself, then a stable release from release-please's PR. The two do not conflict as text, since #2010 only adds new files. They do conflict in what they do: both define how RC tags are numbered and how a stable release is made, and #2010's optional cron nightly overlaps the `nightly` here. Merge one of them. If this one goes in, #2010's button would make RC tags that release-please does not know about (they are not in the manifest), and Promote's "newest RC" default could pick one of them. ## The flow | When | What happens | |---|---| | Push to main | The `nightly` prerelease is rebuilt from that commit. release-please opens or updates its release PR, `chore: release X.Y.Z-rc.N`. | | Merge the release PR | `vX.Y.Z-rc.N` is tagged on the merge commit and built like a release. Once every file is there, it is published as a GitHub prerelease, never "latest". The next release PR proposes `rc.N+1` of the same version, with the changes since `rc.N`. | | Promote (Actions > Release > Run workflow, input: an RC tag, empty for the newest) | `vX.Y.Z` is tagged on that RC's exact commit, built from it, and published as "latest". Its notes are those of its RCs, newest first. Then `.release-please-manifest.json` is set to `X.Y.Z`, so the next release PR proposes the next version's `rc.1`. | There is no "start a series" step: the first fix after a release proposes the next version's `rc.1`. ### release-please's prerelease versioning, as it behaves I checked `PrereleaseVersioningStrategy` in release-please 17.6.0, the version `release-please-action` v5.0.0 bundles: - After a release `2.0.14`: `fix` gives `2.0.15-rc.1`, `feat` gives `2.1.0-rc.1`, and a breaking change gives `3.0.0-rc.1`. - After an RC: `fix` (and `perf`, `revert`) bumps the RC number, `2.0.15-rc.1` -> `2.0.15-rc.2`. On a minor RC (`2.1.0-rc.1`), `feat` also only bumps the RC number. One quirk: a `feat` during a patch series moves to the next minor and keeps the RC number (`2.0.15-rc.2` -> `2.1.0-rc.2`). docs/versioning.md says how to restart it at `rc.1` with `Release-As:`. - `prerelease-type` is `rc.1`, not `rc`. With `rc`, the first one would be `2.0.15-rc`, and the one after it `2.0.15-rc.1`. ### Promote rebuilds the files instead of copying the RC's The plan was to publish the stable version with the RC's files, byte for byte. That would break updating for every player. Each file records the version it was built as: the tools' `version.txt` inside the app part, the AppImage's `VERSION` and file name, and the `tag` line of `hookshot-setup.txt` and `hookshot-linux.txt`. The game (`host/hos_update.c`), Hookshot Builder (`Updater.cs`) and the Linux updater compare that version with the latest release's tag. A `v2.1.0` release holding `2.1.0-rc.2` files would offer itself as an update forever, and the builder would stop with "The update installed v2.1.0-rc.2, not v2.1.0". Clients already installed have that logic, so newer client code cannot fix it either. So Promote tags `vX.Y.Z` on the RC's commit and calls the same build workflows as for an RC. The source is the same commit, the dependencies are the same checked downloads, and the previous stable release supplies the same reused parts. Update parts that did not change are the same files on `builder-parts`/`linux-parts`, so an update from the RC downloads little more than what names the version. The AppImage is named after the release's tag (`HOOKSHOT_VERSION`), not `git describe`, since that commit now carries two tags. ## Details - `.github/workflows/release.yml` is one workflow with three entry points: push to main, the release PR merged (also a push), and `workflow_dispatch` for Promote. Jobs: `release-please` (push), `promote` (dispatch, main only), `linux` and `windows` (call `linux-appimage.yml` and `windows-builder.yml` with the tag, for an RC or a promoted version), `publish` (SHA256SUMS, the files table, checks every file is there, then publishes as a prerelease or as "latest"), and `record` (after a promote). - `scripts/release_promote.sh prepare` checks that the RC is a published prerelease on main, creates the version's tag on its commit through the refs API, and creates the draft release with the RC notes combined (the "Which file do I download?" table is removed from each). Running it again for the same RC continues with the same tag and draft. It refuses an RC tag that is a draft or not on main, and a version that already exists elsewhere or is already released. - `record` runs `scripts/release_promote.sh record`, which sets the manifest (only while it still holds one of that version's RCs) and `.release-please-version`, and adds a short `CHANGELOG.md` entry for the version that points to its RC entries. It pushes this to main with `GITHUB_TOKEN`, then has release-please rebuild the release PR, because that push starts no workflow. If main does not accept the push (branch protection or a ruleset), it opens a small PR `chore(release): record X.Y.Z as released` instead. Merge it before the release PR. - Configuration: `versioning: prerelease` and `prerelease-type: rc.1` are added. The manifest (2.0.14) and `last-release-sha: b2e1c5a6` from #1059 are kept. The `release-as: 2.1.0` pin from #1059 is removed. It would have proposed 2.1.0 as a release, with no RC, and then the same version again. Without it, main's `feat` commits since b2e1c5a6 make the first release PR propose `2.1.0-rc.1` on their own, and promoting that gives 2.1.0. A dry run of release-please 17.6.0 on the fork gave `release 2.1.0` with the pin and `release 2.1.0-rc.1` without. - release-please's version file is `.release-please-version`. The simple strategy writes `version.txt` at the root by default, and `nx2/version.py` reads that file before `git describe` as the release name of every build from a checkout, the nightly included. - Concurrency is on the release-please job only, so the 20+ minute builds of one release no longer hold up the next push. A push waiting behind another one is replaced by the newer push, which reads the same commits and makes the same release (the review nit). Promote has its own group, so a push to main cannot cancel a waiting Promote. - The fix from the #1062 review: the Windows nightly clause also matched the Release workflow's `workflow_call` on a push to main, because `github.event_name`/`github.ref` are the caller's. That defeated `CI_RELEASE_WINDOWS=false`. It is now scoped with `!inputs.tag`. Unchanged from #1059, or brought in from #1062: - From #1059: SHA256SUMS, build provenance attestations, zsync, the "Which file do I download?" table, and the PR title check. - The nightly: on every push to main that is not a release, the build workflows' release jobs move the `nightly` tag and replace its files (`scripts/release_upload.sh nightly`). An older run never overwrites a newer commit. The nightly AppImage updates to the next nightly. The readme gets a "Latest development build" section (Windows: `HookshotBuilder.exe --tag nightly`). - docs/versioning.md is rewritten for release candidates and Promote. ## Tested on the fork Actions are currently disabled on hckrman101/Hookshot ("Actions has been disabled for this repository", even for `workflow_dispatch`), so no workflow could run there this time. Instead, I ran each step the workflow runs from this machine against the fork, on a test branch `rc-test`: release-please 17.6.0 (`release-pr`/`github-release`, the bundled version), the release jobs' `scripts/release_upload.sh`, the publish job's steps, and `scripts/release_promote.sh`. Stub files stood in for the builds, named like the real ones and holding the version and the commit. The manifest started at 0.0.1, seeded with a v0.0.1 release. 1. `fix: change one` gave release PR `chore(rc-test): release 0.0.2-rc.1`. Merging it made draft `v0.0.2-rc.1` on the merge commit. Uploading and publishing made it a prerelease with all files, `SHA256SUMS` passed `sha256sum -c`, and v0.0.1 stayed "latest". 2. `fix: change two` gave `release 0.0.2-rc.2`, with only change two and the compare link `v0.0.2-rc.1...v0.0.2-rc.2`. Merged and published as a prerelease. 3. `fix: change three` gave an open PR for `0.0.2-rc.3`, left open. 4. Promote with an empty input picked `v0.0.2-rc.2`. `v0.0.2` was tagged on rc.2's commit (`6d860d5`, the same SHA as the rc.2 tag). The draft's notes were the rc.2 and rc.1 entries, newest first, without their tables. After the build, `v0.0.2` was published as "latest", not a prerelease, and its `SHA256SUMS` passed. Its files were the rebuilt ones, carrying `v0.0.2` (see above for why the bytes differ from the RC's on purpose). Running prepare again for rc.2 continued with the same draft. It refused `v0.0.2-rc.1` (v0.0.2 is already on another commit), `v0.0.1` (not an RC tag), and an RC tag that was not a published prerelease. 5. Record with rc-test protected (PR required, admins included): the direct push was refused, so the PR `chore(release): record 0.0.2 as released` was opened (fork PR 19). Merging it set the manifest to 0.0.2 and added the 0.0.2 changelog entry. The next `release-pr` then proposed `0.0.3-rc.1`, with change three and the compare link `v0.0.2...v0.0.3-rc.1`. Afterwards I deleted every test release, tag and branch on the fork, removed the branch protection, and put back the fork's Actions setting that I had changed briefly. Also: actionlint 1.7.7 with shellcheck 0.10.0 passes on every workflow, and shellcheck passes on `release_promote.sh`, `release_upload.sh` and `release_files.sh`. The tests that read the build workflows pass: `test_appimage_player_build`, `test_appimage_package_retry`, `test_recomplauncher_json`, `test_appimage_scripts`. Not tested: - The workflow wiring itself: job conditions with skipped needs, outputs between jobs, the `workflow_dispatch` input, concurrency, the `record` job's own `git push` and `gh pr create` from the runner (step 5 ran the same commands from here), and the release-please refresh with `skip-github-release`. These need Actions back on the fork, or the first real run. - Real builds from Promote. The build workflows were already tested as reusable workflows in the earlier fork test, and Promote calls them the same way. Not tested: the new `HOOKSHOT_VERSION` naming of a release AppImage. - The `CI_RELEASE_WINDOWS=false` path after a promote. `record` then does not run, and docs/versioning.md gives the manual command. ## What a maintainer has to do 1. As for #1059: Settings > Actions > General > "Allow GitHub Actions to create and approve pull requests" (the release PR, and `record`'s fallback PR), and squash merging with the PR title as the commit message. 2. If main is protected: either allow the Actions bot to push to it, or merge `record`'s small PR after each Promote (before the next release PR). 3. Decide between this PR and #2010 (above). 4. Optional: `CI_RELEASE_WINDOWS=false` to upload the Windows files by hand.
Commenting is not possible because the repository is archived.
No description provided.