Add channel-aware release tooling and updater recovery

This commit is contained in:
2026-09-10 00:39:37 +02:00
parent cc48bf45d2
commit 0a89323533
42 changed files with 7840 additions and 0 deletions

216
release/README.md Normal file
View File

@@ -0,0 +1,216 @@
# Manual Gitea launcher releases
Manual publication mode is currently active. Build a complete local package with:
```text
./scripts/release.sh stable 1.8.0
./scripts/release.sh prerelease 1.9.0-beta.1
```
The command creates `dist/v<VERSION>/`, validates the channel/version pair,
builds and previews release notes, runs the configured preflight, creates a
deterministic archive, signs the manifest, and verifies the complete result. It
changes the requested version only inside the staged package, so the source
checkout is unchanged. It does not commit, tag, push, create a Gitea release,
or upload anything.
The default archive layout is flat: extracting the tar directly produces
`ptlinux/`, `ptdiag/`, and `release.json`. Versions `0.9.10` and `0.9.11` form a
short compatibility window and use the legacy enclosing directory. That lets
deployed `0.9.8` launchers install either release automatically, even if they
missed `0.9.10`. The updater inside both releases accepts both layouts, so
releases after `0.9.11` return to the flat layout.
Use `--description` for short Markdown or `--description-file` for a prepared
Markdown document. Generated commit notes are appended by default. Run
`./scripts/release.sh --help` for dry-run, editor, changelog-only, signing-key,
and test options.
After a successful build, create a new release in Gitea with tag `v<VERSION>`.
Leave **Pre-release** clear for `stable`; select it for `prerelease`. Use
`dist/v<VERSION>/release-notes.md` as the description and upload exactly these
five files from that version directory:
- `PristonTale-EU-<VERSION>.tar.gz`
- `PristonTale-EU-<VERSION>.tar.gz.sha256`
- `manifest.json`
- `manifest.json.sig`
- `release-notes.md`
Uploading a release does not by itself move the launcher's channel pointer.
Upload and save all five assets before publishing the channel pointer. The
signed archive URL includes its SHA-256 as a query parameter so Cloudflare and
other CDNs cannot reuse an empty response cached before the attachment existed.
After the Gitea release exists and each attachment has a nonzero size, publish
the matching files generated under
`dist/v<VERSION>/channel/` to the repository:
```text
dist/v<VERSION>/channel/stable.json -> updates/stable.json
dist/v<VERSION>/channel/stable.json.sig -> updates/stable.json.sig
dist/v<VERSION>/channel/prerelease.json -> updates/prerelease.json
dist/v<VERSION>/channel/prerelease.json.sig -> updates/prerelease.json.sig
```
Commit and push only the selected channel pair. Publishing a prerelease must not
change `updates/stable.json`.
Manual mode does not rebuild `updates/history.json`. The uploaded version can be
discovered through its channel pointer, but it will not appear as a remote
rollback candidate until signed history publication is run manually or the
automatic publisher is re-enabled.
The published `0.9.9` release is flat and therefore correctly asks deployed
`0.9.8` launchers for a manual update. Releases `0.9.10` and `0.9.11` use the
wrapped bridge layout and minimum updater `0.5.0`, so those launchers can install
either one automatically. Flat manifests after this compatibility window declare
`0.9.9` as their minimum updater version.
For direct artifact tooling without release notes or manual-upload guidance:
```text
PTLINUX_PYTHON=/path/to/python \
./scripts/build-release.sh 0.9.11 stable ./dist/v0.9.11
./scripts/verify-release.sh 0.9.11 stable ./dist/v0.9.11
```
The builder selects the runtime payload declared in `config.json`; it does not
archive the repository. It emits a deterministic versioned archive, checksum,
and external per-file manifest. When `PTLINUX_SIGNING_KEY` is set, it also
emits and immediately verifies `manifest.json.sig`.
`repository_url` is the public HTTPS base used in signed download links.
`allowed_git_remotes` is retained for the deferred automatic release validation;
it is not consulted by the current local packaging command.
## Deferred automatic publication
The automatic publisher and status publisher remain in the repository for a
later migration, but their push triggers are disabled. Both workflows are
manual-dispatch only, so creating a tag or Gitea release cannot race the manual
upload process. The remainder of this section applies only when automatic
publication is re-enabled.
## Repository and runner setup
Enable Gitea Actions for the instance and repository, then register an isolated
Docker-backed `act_runner` for trusted repository workflows. The runner image
must provide Git, Bash, curl, jq, Python 3.11 or newer, tar, gzip, coreutils
(`base64`, `realpath`, `sha256sum`, and `stat`), and OpenSSL. The release job
uses `actions/checkout@v4` and checks that the checked-out commit is the exact
commit referenced by the annotated tag.
Configure these protected Actions secrets:
- `RELEASE_TOKEN`: a dedicated token with only the repository release and
repository-content write permissions needed to create releases, upload
attachments, and update signed channel metadata.
- `MANIFEST_SIGNING_KEY`: the private key corresponding to
`ptlinux/launcher-update-public.pem`.
Secrets are available only to trusted tag and release-status workflows. Do not
add the release secrets to pull-request workflows. The signing key is written under
`RUNNER_TEMP` with mode `0600`, used for signing, and removed by an `always()`
cleanup step. Shell tracing is intentionally disabled around all secret-bearing
commands.
The current installed population verifies the deployed RSA/SHA-256 public key.
Release tooling can use OpenSSL Ed25519 signing, but changing the production
key or algorithm requires a staged key-rotation release first. Until that
migration is deployed, configure the matching existing RSA private key.
## Automatic publication behavior
The workflow validates the tag in CI, runs tests, builds and verifies every
artifact, creates a non-draft Gitea release with the derived prerelease flag,
and uploads these immutable assets separately:
- `PristonTale-EU-<version>.tar.gz`
- `PristonTale-EU-<version>.tar.gz.sha256`
- `manifest.json`
- `manifest.json.sig`
- `release-notes.md`
It re-queries the release and checks every attachment name and size before
updating metadata. A stable tag updates only `updates/stable.json` and its
signature. A beta or RC tag updates only `updates/prerelease.json` and its
signature. Both rebuild `updates/history.json` and its signature from real
Gitea releases, following API pagination so older rollback entries are retained.
The four metadata files are committed atomically through the Gitea contents API.
Existing release detection is fail-closed. Never delete, move, or reuse a tag
after publication. Fix a failed release with a new patch, beta, or RC number.
## Withdrawing or blocking a release
Edit `release/statuses.json` on `main` and set the immutable tag to
`withdrawn` or `blocked`, for example:
```json
{
"releases": {
"v1.9.0-beta.2": "withdrawn"
},
"schema": 1
}
```
The trusted `release-status.yml` workflow rebuilds and signs `updates/history.json`
from real Gitea releases. It also rebuilds both channel manifests: each channel
points to its greatest active release, or carries an authenticated inactive
status when that channel has no active release. The workflow verifies every
selected release manifest before signing and atomically publishing the channel
and history files. This prevents a withdrawn channel head from continuing to be
offered by launchers that read `updates/stable.json` or
`updates/prerelease.json` directly.
The release assets remain available for support, while new automatic update and
rollback selection excludes the inactive release. Deleting the tag's override
returns it to `active`. A status entry can also declare compatibility when a
future release changes the persistent settings schema:
```json
{
"releases": {
"v2.0.0": {
"status": "active",
"config_schema": 2,
"minimum_config_schema": 1
}
},
"schema": 1
}
```
The workflow can be run manually after a runner or API outage.
## Launcher history and rollback
The launcher fetches and verifies `updates/history.json` and its detached
signature for the version-history window. An explicit historical rollback uses
that release's immutable `manifest.json`, `manifest.json.sig`, and versioned
archive:
```text
pristontale-eu launcher-update --rollback 1.8.0
```
Running `--rollback` without a version keeps the original fast path to the
locally retained previous release. Every remote target is signature-, size-,
archive-hash-, and per-file-hash checked before it enters the immutable release
store. The launcher also persists that authenticated file inventory and verifies
it again before reactivating a locally retained release. Releases installed by an
older launcher remain usable through the compatibility path when no persisted
inventory exists. Complete release directories make reconciliation exact: files
managed by the current release but absent from the target disappear when
`current` changes, while settings, logs, saves, and downloads remain outside the
release tree.
Activation records the original `current` and `previous` links in a durable
transaction journal. Link or post-activation validation failure restores those
links, and the next launcher run recovers an interrupted transaction before any
new mutation. The local operation history retains the latest 30 update and
rollback results.
See `SIGNING_MIGRATION.md` before rotating the deployed manifest key.

View File

@@ -0,0 +1,35 @@
# Manifest signing migration
The installed launcher population currently pins the 3072-bit RSA public key in
`ptlinux/launcher-update-public.pem`. Release scripts also support OpenSSL
Ed25519 keys, but changing the Gitea secret before clients receive the new
public key would make all metadata unverifiable.
Use this staged migration when Ed25519 is adopted:
1. Generate the Ed25519 key pair offline. Keep the private key out of the
repository and record the public-key fingerprint through the normal release
review process.
2. Publish a normal RSA-signed transition release. That immutable archive adds
the Ed25519 public key and updater support, while its channel manifest still
verifies with the RSA key already installed on clients.
3. Verify the transition release from a clean pre-transition installation on
both stable and prerelease channel paths. Confirm rollback still verifies old
RSA-signed historical manifests.
4. Keep `manifest.json.sig` RSA-signed while enough installations move to the
transition release. If dual signatures are introduced, upload the Ed25519
signature under a separate filename; never change the meaning of the legacy
`.sig` asset for old clients.
5. Publish a second RSA-signed release that makes the updater choose signatures
by an authenticated algorithm and key identifier, and bundles a keyring with
both public keys. Its minimum updater compatibility must prevent an older
verifier from trusting metadata it cannot authenticate.
6. Replace the protected Actions signing secret only after the keyring release
is deployed and measured. Exercise stable discovery, prerelease discovery,
remote rollback, withdrawal publication, and tamper rejection with Ed25519.
7. Retire RSA verification in a later release. Retain the old public key for
immutable historical rollback metadata until those releases are outside the
supported rollback window.
At every stage, a failed or incorrectly signed tag remains immutable. Correct
the pipeline and publish the next semantic version rather than replacing assets.

140
release/config.json Normal file
View File

@@ -0,0 +1,140 @@
{
"application": "PristonTale-EU",
"archive_layout": "flat",
"archive_prefix": "PristonTale-EU",
"legacy_archive_root_prefix": "pristontale-eu-linux-update-v",
"legacy_minimum_updater_version": "0.5.0",
"wrapped_bridge_versions": [
"0.9.10",
"0.9.11"
],
"channels": [
"stable",
"prerelease"
],
"manifest_version": 1,
"config_schema": 1,
"minimum_config_schema": 1,
"minimum_updater_version": "0.9.9",
"release_branch": "main",
"release_status_file": "release/statuses.json",
"remote": "origin",
"repository_url": "https://git.m4ko.net/m4ko/pristontale-eu-linux",
"allowed_git_remotes": [
"https://git.m4ko.net/m4ko/pristontale-eu-linux",
"ssh://git@192.168.0.110:222/m4ko/pristontale-eu-linux.git"
],
"version_mirrors": [
"pyproject.toml"
],
"version_source": "ptlinux/__init__.py",
"payload": {
"python_trees": [
{
"exclude": [
"desktop_entry.py",
"launcher_wrapper.py"
],
"source": "ptlinux"
}
],
"files": [
{
"mode": "0644",
"path": "ptlinux/launcher-update-public.pem"
},
{
"mode": "0644",
"path": "ptdiag/__init__.py"
},
{
"mode": "0644",
"path": "ptdiag/assbin.py"
},
{
"mode": "0644",
"path": "ptdiag/logs.py"
},
{
"mode": "0644",
"path": "ptlinux/native/pt-identity-probe.exe"
},
{
"mode": "0755",
"path": "ptlinux/native/gamescope-3.16.20-ubuntu26.04-amd64"
},
{
"mode": "0644",
"path": "ptlinux/native/gamescope-compat.README"
},
{
"mode": "0644",
"path": "ptlinux/native/gamescope-compat.copyright"
},
{
"mode": "0644",
"path": "ptlinux/native/gamescope-lazy-output-images.patch"
},
{
"mode": "0644",
"path": "ptlinux/native/gamescope-wayland-live-resize.patch"
},
{
"mode": "0644",
"path": "ptlinux/native/gamescope-wayland-letterbox-backing.patch"
},
{
"mode": "0644",
"path": "ptlinux/native/gamescope-wayland-libdecor-input-scope.patch"
},
{
"mode": "0644",
"path": "ptlinux/native/gamescope-keep-nested-windows-visible.patch"
}
]
},
"required_files": [
"release.json",
"ptlinux/__init__.py",
"ptlinux/__main__.py",
"ptlinux/cli.py",
"ptlinux/self_update.py",
"ptlinux/update_resolver.py",
"ptlinux/launcher-update-public.pem"
],
"exclusions": [
".git",
".gitea",
".idea",
".run",
".venv",
".vscode",
"dist",
"logs",
"results",
"test-output",
"tests",
"testcases",
"tools",
"__pycache__",
"*.pyc",
"*.pyo",
".env",
".env.*",
"*.pem",
"*.key",
"id_rsa",
"id_ed25519",
"*.part",
"*.tmp",
"downloads"
],
"allowed_pem_files": [
"ptlinux/launcher-update-public.pem"
],
"preflight": [
"\"$PTLINUX_PYTHON\" -m compileall -q ptlinux ptdiag scripts release_tests",
"\"$PTLINUX_PYTHON\" -m unittest discover -s release_tests -p 'test_*.py'",
"\"$PTLINUX_PYTHON\" -m unittest discover -s tests -p 'test_*.py'"
]
}

4
release/statuses.json Normal file
View File

@@ -0,0 +1,4 @@
{
"releases": {},
"schema": 1
}