Add channel-aware release tooling and updater recovery
This commit is contained in:
216
release/README.md
Normal file
216
release/README.md
Normal 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.
|
||||
35
release/SIGNING_MIGRATION.md
Normal file
35
release/SIGNING_MIGRATION.md
Normal 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
140
release/config.json
Normal 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
4
release/statuses.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"releases": {},
|
||||
"schema": 1
|
||||
}
|
||||
Reference in New Issue
Block a user