scripts: add make-release.sh #5

Open
jann wants to merge 2 commits from jann/m2dev-client:claude/make-release-script into main
Member

Summary

Adds scripts/make-release.sh — the single entry point for the v1 manual publishing flow described in docs/update-manager.md. The script wraps make-manifest.py + sign-manifest.py into one pipeline and produces a ready-to-rsync release tree.

What it does

  • Validates args, refuses to run without mode-600 signing key, without Metin2.exe in the source, or into a non-empty out dir (unless --force).
  • [1/6] runs make-manifest.py to produce manifest.json.
  • [2/6] runs sign-manifest.py to produce manifest.json.sig (verifies exactly 64 bytes).
  • [3/6] copies the signed manifest pair into manifests/<version>.json(.sig) for historical archive.
  • [4/6] walks manifest.launcher + manifest.files[] via jq, builds files/<hash[0:2]>/<hash> blobs, hardlinks when possible (cp -l with copy fallback), deduplicates by hash.
  • [5/6] prints the final layout summary.
  • [6/6] in non-dry-run mode: prints the rsync target, waits for y/N unless --yes, rsyncs blobs+archive first (excluding manifest.json*), then a second rsync for manifest.json + manifest.json.sig so the new release becomes visible atomically.

Under 170 lines, set -euo pipefail, small functions, meaningful [make-release] [N/6] ... prefixes.

Flags: --source, --version, --previous, --notes <file>, --key (default ~/.config/metin/launcher-signing-key), --out (default /tmp/release-<version>), --force, --dry-run, --yes, --rsync-target (default VPS path).

Local testing

Ran --dry-run against /home/jann/metin/runtime/client-wine, version 2026.04.14-dev-test:

[make-release] [1/6] building manifest
manifest: /tmp/release-test/manifest.json  files: 54440  total: 3259.3 MiB
[make-release] [2/6] signing manifest
signed /tmp/release-test/manifest.json -> /tmp/release-test/manifest.json.sig  (64 bytes, verify with public key 1d2b63751ea0e0354d28e7eb4ec175919a01518b0bcf5878f0a3aa8e7c6ce2bc)
[make-release] [3/6] archiving historical manifest -> manifests/2026.04.14-dev-test.json
[make-release] [4/6] building content-addressed blob tree
[make-release]   entries: 54440  unique blobs: 39159  deduped: 15281
[make-release]   bytes written: 3050482309
[make-release] [5/6] final layout:
  ./files
  ./files/aa
  ./files/ab
  ...
[make-release] [6/6] --dry-run set, skipping rsync. target would be: mt2.jakubkadlec.dev@mt2.jakubkadlec.dev:/var/www/updates.jakubkadlec.dev/
  • manifest.json is valid JSON (parsed by jq), 54439 files + launcher entry.
  • manifest.json.sig is exactly 64 bytes.
  • Python cryptography.hazmat...Ed25519PublicKey using the hex public key from LauncherConfig.PublicKeyHex verifies the signature: SIGNATURE VERIFIES OK against LauncherConfig.PublicKeyHex.
  • Content-addressed blob tree has 39159 unique blobs, 15281 entries deduplicated by matching hash — hardlinks keep disk usage down. Spot-checked 3 random blobs by re-hashing the file at files/<h[:2]>/<h> — all matched the manifest.
  • Launcher blob also present in its files/<h[:2]>/<h> location.

End-to-end local HTTP test

Served /tmp/release-test with python3 -m http.server 8765 --bind 127.0.0.1 and launched the built launcher DLL against a scratch client dir containing a dummy Metin2.exe and this .updates/launcher-settings.json:

{"Locale":"cs","DevMode":true,"ManifestUrlOverride":"http://127.0.0.1:8765/manifest.json"}

Launcher log (excerpt):

2026-04-14 11:50:40.599 INFO  metin2 launcher starting in /tmp/scratch-client
2026-04-14 11:50:40.802 INFO  manifest 2026.04.14-dev-test verified, 54439 files
2026-04-14 11:50:41.021 INFO  [Downloading] 0%
2026-04-14 11:50:41.027 INFO  [Downloading] 0% — README.md (1/54434)
...
2026-04-14 11:51:28.928 INFO  [Applying] 100% — 54434 files
2026-04-14 11:51:28.932 INFO  update complete: 54434 files applied
2026-04-14 11:51:28.932 INFO  [UpToDate] 100%

So the full fetch-verify-download-hash-apply-upToDate pipeline works against a locally-served signed manifest. Signature verification passes against the baked-in LauncherConfig.PublicKeyHex. No SSL failures, no network dependency.

Note: the first run produced an SSL failure because the local bin/Release/net8.0/Metin2Launcher.dll was stale (predated the LauncherSettings / EffectiveManifestUrl wiring). After dotnet build the retry succeeded. Not a script issue — flagging it so others who pull this don't get caught by a stale bin.

/tmp/release-test and /tmp/scratch-client cleaned up after testing.

Judgment calls

  • --notes <file> in the spec is a file path; make-manifest.py --notes takes a string. The wrapper reads the file contents and passes them through as the string arg.
  • On failure the script leaves the half-built out dir in place for inspection (explicit in spec). No trap handler to clean up.
  • Historical archive copy uses cp -f (not hardlink) because the archived manifest is tiny and having a separate inode is safer.
  • Blob copy prefers cp -l (hardlink) and falls back to cp if the out dir is on a different filesystem than the source.
## Summary Adds `scripts/make-release.sh` — the single entry point for the v1 manual publishing flow described in `docs/update-manager.md`. The script wraps `make-manifest.py` + `sign-manifest.py` into one pipeline and produces a ready-to-rsync release tree. ## What it does - Validates args, refuses to run without mode-600 signing key, without `Metin2.exe` in the source, or into a non-empty out dir (unless `--force`). - `[1/6]` runs `make-manifest.py` to produce `manifest.json`. - `[2/6]` runs `sign-manifest.py` to produce `manifest.json.sig` (verifies exactly 64 bytes). - `[3/6]` copies the signed manifest pair into `manifests/<version>.json(.sig)` for historical archive. - `[4/6]` walks `manifest.launcher + manifest.files[]` via `jq`, builds `files/<hash[0:2]>/<hash>` blobs, hardlinks when possible (`cp -l` with copy fallback), deduplicates by hash. - `[5/6]` prints the final layout summary. - `[6/6]` in non-dry-run mode: prints the rsync target, waits for y/N unless `--yes`, rsyncs blobs+archive first (excluding `manifest.json*`), then a second rsync for `manifest.json` + `manifest.json.sig` so the new release becomes visible atomically. Under 170 lines, `set -euo pipefail`, small functions, meaningful `[make-release] [N/6] ...` prefixes. Flags: `--source`, `--version`, `--previous`, `--notes <file>`, `--key` (default `~/.config/metin/launcher-signing-key`), `--out` (default `/tmp/release-<version>`), `--force`, `--dry-run`, `--yes`, `--rsync-target` (default VPS path). ## Local testing Ran `--dry-run` against `/home/jann/metin/runtime/client-wine`, version `2026.04.14-dev-test`: ``` [make-release] [1/6] building manifest manifest: /tmp/release-test/manifest.json files: 54440 total: 3259.3 MiB [make-release] [2/6] signing manifest signed /tmp/release-test/manifest.json -> /tmp/release-test/manifest.json.sig (64 bytes, verify with public key 1d2b63751ea0e0354d28e7eb4ec175919a01518b0bcf5878f0a3aa8e7c6ce2bc) [make-release] [3/6] archiving historical manifest -> manifests/2026.04.14-dev-test.json [make-release] [4/6] building content-addressed blob tree [make-release] entries: 54440 unique blobs: 39159 deduped: 15281 [make-release] bytes written: 3050482309 [make-release] [5/6] final layout: ./files ./files/aa ./files/ab ... [make-release] [6/6] --dry-run set, skipping rsync. target would be: mt2.jakubkadlec.dev@mt2.jakubkadlec.dev:/var/www/updates.jakubkadlec.dev/ ``` - `manifest.json` is valid JSON (parsed by `jq`), 54439 files + launcher entry. - `manifest.json.sig` is exactly 64 bytes. - Python `cryptography.hazmat...Ed25519PublicKey` using the hex public key from `LauncherConfig.PublicKeyHex` verifies the signature: **SIGNATURE VERIFIES OK against LauncherConfig.PublicKeyHex**. - Content-addressed blob tree has **39159 unique blobs**, **15281 entries deduplicated** by matching hash — hardlinks keep disk usage down. Spot-checked 3 random blobs by re-hashing the file at `files/<h[:2]>/<h>` — all matched the manifest. - Launcher blob also present in its `files/<h[:2]>/<h>` location. ## End-to-end local HTTP test Served `/tmp/release-test` with `python3 -m http.server 8765 --bind 127.0.0.1` and launched the built launcher DLL against a scratch client dir containing a dummy `Metin2.exe` and this `.updates/launcher-settings.json`: ```json {"Locale":"cs","DevMode":true,"ManifestUrlOverride":"http://127.0.0.1:8765/manifest.json"} ``` Launcher log (excerpt): ``` 2026-04-14 11:50:40.599 INFO metin2 launcher starting in /tmp/scratch-client 2026-04-14 11:50:40.802 INFO manifest 2026.04.14-dev-test verified, 54439 files 2026-04-14 11:50:41.021 INFO [Downloading] 0% 2026-04-14 11:50:41.027 INFO [Downloading] 0% — README.md (1/54434) ... 2026-04-14 11:51:28.928 INFO [Applying] 100% — 54434 files 2026-04-14 11:51:28.932 INFO update complete: 54434 files applied 2026-04-14 11:51:28.932 INFO [UpToDate] 100% ``` So the full fetch-verify-download-hash-apply-upToDate pipeline works against a locally-served signed manifest. Signature verification passes against the baked-in `LauncherConfig.PublicKeyHex`. No SSL failures, no network dependency. Note: the first run produced an SSL failure because the local `bin/Release/net8.0/Metin2Launcher.dll` was stale (predated the `LauncherSettings` / `EffectiveManifestUrl` wiring). After `dotnet build` the retry succeeded. Not a script issue — flagging it so others who pull this don't get caught by a stale bin. `/tmp/release-test` and `/tmp/scratch-client` cleaned up after testing. ## Judgment calls - `--notes <file>` in the spec is a file path; `make-manifest.py --notes` takes a string. The wrapper reads the file contents and passes them through as the string arg. - On failure the script leaves the half-built out dir in place for inspection (explicit in spec). No trap handler to clean up. - Historical archive copy uses `cp -f` (not hardlink) because the archived manifest is tiny and having a separate inode is safer. - Blob copy prefers `cp -l` (hardlink) and falls back to `cp` if the out dir is on a different filesystem than the source.
jann added 2 commits 2026-04-14 13:53:19 +02:00
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u claude/make-release-script:jann-claude/make-release-script
git checkout jann-claude/make-release-script
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: metin-server/m2dev-client#5