Prepare v1.0.0 with rebuilt runtimes and HD coverage note

This commit is contained in:
raptorreplica committed 2026-10-21 18:30:15 +11:00
1 parent ea6e937b91
commit cd31949383
18 files changed
+357 -42

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
name: Upload frozen release to existing draft
on:
workflow_dispatch:
inputs:
commit:
description: Exact tested 40-character game commit
required: true
type: string
tag:
description: Exact existing game tag
required: true
type: string
release_id:
description: Exact existing empty draft release ID
required: true
type: string
prerelease:
description: Expected prerelease flag on the existing draft
required: true
default: false
type: boolean
permissions: {}
concurrency:
group: frozen-draft-release-${{ inputs.release_id }}
cancel-in-progress: false
jobs:
upload-draft:
if: github.repository == 'Quintine/RustRaptor'
runs-on: ubuntu-latest
timeout-minutes: 180
permissions:
contents: write
env:
PYTHONDONTWRITEBYTECODE: '1'
FROZEN_COMMIT: ${{ inputs.commit }}
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_ID: ${{ inputs.release_id }}
EXPECT_PRERELEASE: ${{ inputs.prerelease }}
steps:
- name: Validate exact release inputs before any checkout
shell: bash
run: |
python3 - <<'PYTHON'
import os, re
assert re.fullmatch('[0-9a-f]{40}', os.environ['FROZEN_COMMIT']), 'Exact commit required'
assert re.fullmatch(r'v[0-9A-Za-z._-]+', os.environ['RELEASE_TAG']), 'Safe exact tag required'
assert re.fullmatch('[1-9][0-9]*', os.environ['RELEASE_ID']), 'Positive release ID required'
assert os.environ['EXPECT_PRERELEASE'] in ('true', 'false'), 'Boolean prerelease expectation required'
PYTHON
- name: Make room before any source checkout
shell: bash
run: |
set -euo pipefail
minimum=25000000000
available() { df --output=avail -B1 "$GITHUB_WORKSPACE" | tail -1 | tr -d ' '; }
for directory in /usr/local/lib/android /usr/share/dotnet /opt/ghc; do
if (( $(available) >= minimum )); then break; fi
sudo rm -rf -- "$directory"
done
(( $(available) >= minimum )) || { echo 'Need at least 25GB free before source checkout.' >&2; exit 1; }
df -h "$GITHUB_WORKSPACE"
- name: Fetch only the publishing controller from this maintenance commit
uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
path: controller
filter: blob:none
sparse-checkout-cone-mode: false
sparse-checkout: |
/tools/upload_draft_release.py
/tools/test_upload_draft_release.py
persist-credentials: false
- name: Fetch the immutable tested game checkpoint
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit }}
path: frozen
fetch-depth: 1
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Test the existing exporter and publishing guards
shell: bash
run: |
set -euo pipefail
python -B -m unittest discover -s frozen/tools -p test_export_release.py -v
python -B -m unittest discover -s controller/tools -p test_upload_draft_release.py -v
- name: Export and verify every asset on the existing draft
env:
GH_TOKEN: ${{ github.token }}
run: >-
python -B controller/tools/upload_draft_release.py
--package "$GITHUB_WORKSPACE/frozen"
--state "$RUNNER_TEMP/RustRaptor-draft-export-state"
--temporary "$RUNNER_TEMP/RustRaptor-single-export"
--commit "$FROZEN_COMMIT"
--tag "$RELEASE_TAG"
--release-id "$RELEASE_ID"
--prerelease "$EXPECT_PRERELEASE"
- name: Retain frozen plan, local receipts, remote proofs and logs
if: always()
uses: actions/upload-artifact@v4
with:
name: frozen-release-draft-export-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/RustRaptor-draft-export-state
if-no-files-found: warn
retention-days: 30
Generated
+1 -1
View File
@@ -449,7 +449,7 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]] [[package]]
name = "raptor-replica" name = "raptor-replica"
version = "0.1.0" version = "1.0.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"image", "image",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "raptor-replica" name = "raptor-replica"
version = "0.1.0" version = "1.0.0"
edition = "2024" edition = "2024"
rust-version = "1.88" rust-version = "1.88"
license = "GPL-2.0-or-later" license = "GPL-2.0-or-later"
+2
View File
@@ -1,5 +1,7 @@
# RustRaptor # RustRaptor
**v1.0.0 artwork note:** HD coverage is **5,743 of 5,753 original pictures**. Ten pictures retain their original graphics; HD coverage is not complete. All original game data and the 45 bundled mods remain included.
The independent Linux and Windows Rust game and its complete local mod packages live here. The independent Linux and Windows Rust game and its complete local mod packages live here.
SDL2 handles platform services; gameplay, asset decoding, rendering, audio, SDL2 handles platform services; gameplay, asset decoding, rendering, audio,
saving and mod scripting run in Rust. saving and mod scripting run in Rust.
BIN
View File
Binary file not shown.
Binary file not shown.
+15 -11
View File
@@ -1,6 +1,8 @@
# Downloadable releases # Downloadable releases
Current release: [0.1.0 Preview — Linux and Windows](https://github.com/Quintine/RustRaptor/releases/tag/v0.1.0-preview.20261020). Current release: [v1.0.0 — Linux and Windows](https://github.com/Quintine/RustRaptor/releases/tag/v1.0.0).
**Artwork note:** 5,743 of 5,753 original pictures have approved HD remasters. Ten retain original-art fallback; v1.0.0 does not claim complete HD coverage.
The GitHub release provides playable Linux x86_64 and Windows x86_64 packages. The GitHub release provides playable Linux x86_64 and Windows x86_64 packages.
Cargo, Python, the old engine and a Git checkout are unnecessary for play. Cargo, Python, the old engine and a Git checkout are unnecessary for play.
@@ -79,7 +81,7 @@ export copy before producing the next. Keep receipts. After all assets exist:
```sh ```sh
python3 tools/export_release.py checksums --plan /tmp/release-plan.json \ python3 tools/export_release.py checksums --plan /tmp/release-plan.json \
--receipts /path/to/receipts --output /tmp/SHA256SUMS \ --receipts /path/to/receipts --output /tmp/SHA256SUMS \
--manifest-output /tmp/release-manifest.json --tag v0.1.0-preview.20261020 --manifest-output /tmp/release-manifest.json --tag v1.0.0
``` ```
Upload the versioned release manifest, checksum list and installers, check complete remote membership, and Upload the versioned release manifest, checksum list and installers, check complete remote membership, and
@@ -87,13 +89,15 @@ publish the draft only after all checks pass. Native Windows CI and local
representative rendering/audio evidence retain their actual commit and scope; representative rendering/audio evidence retain their actual commit and scope;
archive integrity checks do not imply a fresh full playthrough. archive integrity checks do not imply a fresh full playthrough.
## Hosted export for this preview ## Hosted export
The manually dispatched **Upload frozen preview to existing draft** workflow The manually dispatched **Upload frozen release to existing draft** workflow
packages the immutable `7ecaa42b29ff0db5d2f7cdfe6da0a3221dfea555` game accepts the exact tested 40-character commit, existing version tag, empty draft
checkpoint on a GitHub runner. Its publishing controller is checked out release ID and expected prerelease flag. For v1.0.0 select `false` for prerelease.
separately from the later maintenance commit; the release tag stays fixed. The publishing controller is checked out separately from the immutable game
The controller requires the existing empty preview draft, verifies every local checkpoint. Every source and remote release identity is checked, all archives
and remote asset, and retains the frozen plan and receipts as an Actions are verified, and the frozen plan and receipts are retained as an Actions
artifact. It leaves the draft unpublished for final review. It cannot be reused artifact. The job leaves the draft unpublished for final review.
for a different release without explicitly updating its fixed identity guards.
The older preview workflow retains its fixed 20 October checkpoint and cannot
modify its already-published release. Neither workflow publishes automatically.
+84
View File
@@ -4122,5 +4122,89 @@
"new_World_intrinsicQA_imagegen_game_build_calls_during_assembly": 0, "new_World_intrinsicQA_imagegen_game_build_calls_during_assembly": 0,
"native_Windows_scope": "Dated native5654 SUCCESS39229967668 for126d28ba; no native 5743 claim.", "native_Windows_scope": "Dated native5654 SUCCESS39229967668 for126d28ba; no native 5743 claim.",
"scope": "Exact50 Terrain pictures:32 Source-guided derived, nine untouched genuine-atlas references, three references to a partly derived shared atlas and six lossless genuine-atlas crops.304fb alone is patched;304fa/304fc cell pixels are unchanged inside that derived file.211 retained Original cells and all Source construction get zero generated detail credit.2043f/20458 are unused catalogue-only, not runtime-placed. Literal original measurements and separate joint35 H5654+89 overlay remain distinct; the overlay is not a full5743 measured bank. Different peer checked all 560 saved native roles; Root all 35 native4. Static finite controls do not claim every caller, World, complete missions, audio or performance. Ten provider-refused originals remain unapproved." "scope": "Exact50 Terrain pictures:32 Source-guided derived, nine untouched genuine-atlas references, three references to a partly derived shared atlas and six lossless genuine-atlas crops.304fb alone is patched;304fa/304fc cell pixels are unchanged inside that derived file.211 retained Original cells and all Source construction get zero generated detail credit.2043f/20458 are unused catalogue-only, not runtime-placed. Literal original measurements and separate joint35 H5654+89 overlay remain distinct; the overlay is not a full5743 measured bank. Different peer checked all 560 saved native roles; Root all 35 native4. Static finite controls do not claim every caller, World, complete missions, audio or performance. Ten provider-refused originals remain unapproved."
},
"v1_0_0_distribution": {
"status": "v1_0_0_fresh_binaries_local_CLI_and_SDL_pass_native_Windows_and_export_pending",
"version": "1.0.0",
"approved_hd_frames": 5743,
"source_picture_targets": 5753,
"remaining_original_pictures": 10,
"complete_remaster": false,
"remaining_ids": [
"1035b",
"1035c",
"1035d",
"1035e",
"300a1",
"301ae",
"40374",
"404d2",
"4050b",
"40512"
],
"engine_scope": "Same18Rustsourcefiles as SourceG2; Cargo package version only changed. No new artwork, simulation, mod or data changes. Earlier receipts retain their actual binaries and scope.",
"linux_binary_sha256": "14d85a8377078cf6abba44d72bc4eb8ef2760c6a3c810dd51e2d55d4ff91a5af",
"windows_binary_sha256": "187a927767c7042c6c54f0307ac30c08504b1bd46c3b742e34617afa798d93f0",
"data_files": 32373,
"data_bytes": 10376039515,
"data_manifest_map_unchanged": true,
"installed_cli": [
{
"args": [
"--verify-assets"
],
"exit": 0,
"stdout": "Verified 6449 assets; 27 missions, 528 enemies, 16 weapons, 24 items\n",
"stderr": "",
"elapsed_seconds": 0.5691442220122553
},
{
"args": [
"--validate-hd"
],
"exit": 0,
"stdout": "HD pack: 5743 registered, 5743 validated, 0 rejected supplied frames\n",
"stderr": "",
"elapsed_seconds": 160.12859854800627
},
{
"args": [
"--mods",
"/home/quintine/Projects/RaptorReplica/RustRaptor/mods",
"--profile-root",
"/home/quintine/Projects/RaptorReplica/RustRaptor/.reference/v1-release-prep-20261021/root-state/validation-profile",
"--validate-mods",
"aurora,ion_lance,iron_coast_boss,iron_coast_boss_2,iron_coast_boss_3,iron_coast_boss_4,iron_coast_boss_5,iron_coast_boss_6,iron_coast_campaign,iron_coast_depth_charge,iron_coast_mission_1,iron_coast_mission_2,iron_coast_mission_3,iron_coast_mission_4,iron_coast_mission_5,iron_coast_mission_6,iron_coast_music,iron_coast_railburst,iron_coast_scout,iron_coast_stage2_boss_1,iron_coast_stage2_boss_2,iron_coast_stage2_boss_3,iron_coast_stage2_boss_4,iron_coast_stage2_boss_5,iron_coast_stage2_boss_6,iron_coast_stage2_boss_7,iron_coast_stage2_boss_8,iron_coast_stage2_campaign,iron_coast_stage2_interceptor,iron_coast_stage2_mission_1,iron_coast_stage2_mission_2,iron_coast_stage2_mission_3,iron_coast_stage2_mission_4,iron_coast_stage2_mission_5,iron_coast_stage2_mission_6,iron_coast_stage2_mission_7,iron_coast_stage2_mission_8,iron_coast_stage2_music,iron_coast_stage2_siege,iron_coast_stage2_skimmer,iron_coast_turret,lightning_cannon,lightning_shield,pulse_cannon,seeker_swarm"
],
"exit": 0,
"stdout": "Validated 45 mods; 1 override(s)\n",
"stderr": "",
"elapsed_seconds": 8.680746864993125
}
],
"linux_SDL_smoke": {
"frames": 240,
"resolution": [
1920,
1080
],
"HD_mode": true,
"mission": "base:sector_1_wave_1",
"audio": {
"frames": 132300,
"rate": 44100,
"channels": 2,
"nonzero_samples": 260239
},
"screenshot_sha256": "65a65351583a1f4602268ca67280e8f7e6f2e2904d5e09fd0db508a24da00ab0",
"audio_sha256": "fe037b25fc2f0267b3ed151ed20a1502070433765a1ae173855402010c3bdefe",
"scope": "240 render frames of base sector1wave1, software SDL/dummy audio capture; not hardware audio or complete mission playthrough"
},
"packaging_tests": {
"exporter": 22,
"publisher_guards": 10
},
"native_windows_execution": "pending new1.0.0 CI; prior7ec result is historical",
"distribution_note": "Ten pictures retain original graphics. v1.0.0 does not claim complete HD coverage."
} }
} }
+2
View File
@@ -1,5 +1,7 @@
# Current approved-artwork checkpoint — 20 October 2026 # Current approved-artwork checkpoint — 20 October 2026
v1.0.0 preparation — 21 October 2026: the user approved release with a prominent **5,743/5,753 HD; ten original-art fallbacks** note. Both executables were rebuilt with Cargo package version 1.0.0 using the existing Rust 1.99 toolchains. Engine source, mods and all 32,373 data-file hashes are unchanged. Fresh installed CLI passed catalog, all HD frames and 45 mods; one 240-frame 1080p HD SDL capture has non-silent stereo audio. Exporter 22 and publisher 10 checks pass. Native Windows CI, frozen-tag export and public v1.0.0 publication remain pending at this source checkpoint. The tracked release note is `release/notes-v1.0.0.md`.
Published release checkpoint — 20 October 2026: [RustRaptor 0.1.0 Preview](https://github.com/Quintine/RustRaptor/releases/tag/v0.1.0-preview.20261020) is public, with all 13 assets verified: seven complete shared-data ZIPs, Linux and Windows platform ZIPs, two installers, the versioned manifest and checksums. The helpers assemble one self-contained folder with original data, 45 mods and their artwork/audio. Published release checkpoint — 20 October 2026: [RustRaptor 0.1.0 Preview](https://github.com/Quintine/RustRaptor/releases/tag/v0.1.0-preview.20261020) is public, with all 13 assets verified: seven complete shared-data ZIPs, Linux and Windows platform ZIPs, two installers, the versioned manifest and checksums. The helpers assemble one self-contained folder with original data, 45 mods and their artwork/audio.
The release tag is fixed at `7ecaa42b29ff0db5d2f7cdfe6da0a3221dfea555`. [Native Windows game validation](https://github.com/Quintine/RustRaptor/actions/runs/39410204613) passed at that exact commit. [Hosted export](https://github.com/Quintine/RustRaptor/actions/runs/39411973689) checked every archive and all remote hashes; final independent review matched all 48,209 archive payload rows and 13 remote assets. The later maintenance commit `991b471f` adds the hosted publishing controller without moving the release tag or changing game data. The release tag is fixed at `7ecaa42b29ff0db5d2f7cdfe6da0a3221dfea555`. [Native Windows game validation](https://github.com/Quintine/RustRaptor/actions/runs/39410204613) passed at that exact commit. [Hosted export](https://github.com/Quintine/RustRaptor/actions/runs/39411973689) checked every archive and all remote hashes; final independent review matched all 48,209 archive payload rows and 13 remote assets. The later maintenance commit `991b471f` adds the hosted publishing controller without moving the release tag or changing game data.
+12 -1
View File
@@ -57,7 +57,18 @@ The [Windows workflow](../.github/workflows/windows.yml) runs simulation,
modding and save tests on Windows, validates packaged assets and all local modding and save tests on Windows, validates packaged assets and all local
mods, captures SDL gameplay and publishes the zip as a workflow artifact. mods, captures SDL gameplay and publishes the zip as a workflow artifact.
## Current runtime and validation ## Current v1.0.0 runtime
The included version 1.0.0 GNU executable has SHA-256
`187a927767c7042c6c54f0307ac30c08504b1bd46c3b742e34617afa798d93f0`.
It shares **5,743 approved HD frames**, **32,373 data files / 10,376,039,515 bytes**
and **45 mods** with Linux. Ten original pictures retain original-art fallback.
The release does not claim complete HD coverage. PE imports and SDL dependencies
are unchanged from SourceG2; the runtime manifests contain the exact current hashes.
Fresh native Windows execution for this rebuilt version is pending its CI run.
Earlier CI and exported-folder observations below retain their actual snapshots.
## Historical runtime and validation
The included GNU executable uses the reviewed **2,935-frame HD snapshot**; The included GNU executable uses the reviewed **2,935-frame HD snapshot**;
the shared manifest binds **7,816 data files** and the package contains the shared manifest binds **7,816 data files** and the package contains
+1 -1
View File
@@ -16,7 +16,7 @@ import tempfile
import urllib.request import urllib.request
import zipfile import zipfile
TAG = 'v0.1.0-preview.20261020' TAG = 'v1.0.0'
BASE = 'https://github.com/Quintine/RustRaptor/releases/download/' + TAG BASE = 'https://github.com/Quintine/RustRaptor/releases/download/' + TAG
+1 -1
View File
@@ -1,6 +1,6 @@
param([string]$Destination = (Get-Location).Path) param([string]$Destination = (Get-Location).Path)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$tag = 'v0.1.0-preview.20261020' $tag = 'v1.0.0'
$base = "https://github.com/Quintine/RustRaptor/releases/download/$tag" $base = "https://github.com/Quintine/RustRaptor/releases/download/$tag"
if (-not [Environment]::Is64BitOperatingSystem) { throw 'RustRaptor requires 64-bit Windows.' } if (-not [Environment]::Is64BitOperatingSystem) { throw 'RustRaptor requires 64-bit Windows.' }
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+1 -1
View File
@@ -34,7 +34,7 @@ try {
$longMember = 'data/' + (('long-proof-component/' * 20)) + 'accepted.json' $longMember = 'data/' + (('long-proof-component/' * 20)) + 'accepted.json'
$data = New-FixtureArchive 'RustRaptor-data-01-of-01.zip' 'shared-data' $longMember $data = New-FixtureArchive 'RustRaptor-data-01-of-01.zip' 'shared-data' $longMember
$core = New-FixtureArchive 'RustRaptor-windows-x86_64.zip' 'windows' 'run.cmd' $core = New-FixtureArchive 'RustRaptor-windows-x86_64.zip' 'windows' 'run.cmd'
$manifest = @{format_version=1; tag='v0.1.0-preview.20261020'; assets=@($data, $core)} $manifest = @{format_version=1; tag='v1.0.0'; assets=@($data, $core)}
$manifestPath = Join-Path $global:RaptorInstallerFixtureDownloads 'release-manifest.json' $manifestPath = Join-Path $global:RaptorInstallerFixtureDownloads 'release-manifest.json'
$manifest | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $manifestPath -Encoding UTF8 $manifest | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
$destination = Join-Path $temp 'portable folder' $destination = Join-Path $temp 'portable folder'
+22
View File
@@ -0,0 +1,22 @@
# RustRaptor v1.0.0
Self-contained Linux x86_64 and Windows x86_64 packages, with original game data, the classic intro and sound, the in-game mod manager and all 45 bundled mods. Includes both Iron Coast stages, their 14 missions, bosses, weapons and original music, plus SD/HD mission briefings.
**HD artwork note: 5,743 of 5,753 original pictures have approved HD remasters. Ten pictures retain their original graphics. This release does not claim complete HD coverage.** The remaining IDs are `1035b`, `1035c`, `1035d`, `1035e`, `300a1`, `301ae`, `40374`, `404d2`, `4050b` and `40512`. Original and crisp scaling modes remain available; HD art does not alter collision dimensions or simulation.
This release keeps the gameplay, mod content and artwork from the tested 20 October preview, with freshly rebuilt version 1.0.0 Linux and Windows executables. No new artwork was generated for this release.
## Download and install
Download `Install-Windows.ps1` or `Install-Linux.sh` from this release and run it in a directory without an existing `RustRaptor` folder:
- Windows: `powershell -NoProfile -ExecutionPolicy Bypass -File .\Install-Windows.ps1`
- Linux: `bash ./Install-Linux.sh` (the optional download helper needs Bash and Python 3.8+; the game does not require Python).
The helper downloads your platform ZIP and all seven shared-data ZIPs, checks archive and payload SHA-256 hashes, and assembles one portable folder. Start `RustRaptor/run.cmd` or `RustRaptor/run.sh`. Saves and settings stay in the separate `RaptorReplica` user profile directories.
For manual installation, download one platform ZIP plus every `RustRaptor-data-NN-of-07.zip` from this release. Compare with `SHA256SUMS`, then extract all eight archives into the same parent directory. Every ZIP starts with `RustRaptor/`. Use the Windows helper for reliable long-path extraction of retained artwork-review files.
Allow roughly 13 GB free for the installed folder and one temporary download. The complete shared data contains 32,373 files / 10,376,039,515 bytes. Original assets, accepted HD artwork and bundled mod graphics, sound and music are included. No compiler or old C++ engine is required.
Broader playtesting remains useful; the included validation covers its documented scenarios and is not a new full playthrough of every mission.
+1 -1
View File
@@ -3,7 +3,7 @@
"platform": "Linux x86_64", "platform": "Linux x86_64",
"catalog_entries": 6449, "catalog_entries": 6449,
"approved_hd_frames": 5743, "approved_hd_frames": 5743,
"binary_sha256": "e9cbc9f987bb60e9533a8e2d7ab216f6ecccc0848249354ded22abd513a3f255", "binary_sha256": "14d85a8377078cf6abba44d72bc4eb8ef2760c6a3c810dd51e2d55d4ff91a5af",
"data_files": 32373, "data_files": 32373,
"data_bytes": 10376039515, "data_bytes": 10376039515,
"files": { "files": {
+1 -1
View File
@@ -37,7 +37,7 @@
] ]
}, },
"files": { "files": {
"bin/raptor-rust.exe": "a5a82c7942ac07e11ef00219218c3f7eadfa9eff51ee22c44d16f7f54ebaec53", "bin/raptor-rust.exe": "187a927767c7042c6c54f0307ac30c08504b1bd46c3b742e34617afa798d93f0",
"bin/SDL2.dll": "53e8fa7e9ed43c30bd650c39396f0d9d2067d2fff0b926c9433b75202592e4aa", "bin/SDL2.dll": "53e8fa7e9ed43c30bd650c39396f0d9d2067d2fff0b926c9433b75202592e4aa",
"bin/SDL2-WINDOWS-LICENSE.txt": "97f35b302b361680ec1e891e95d2d52097bb95abff361434916d99dc1305f127", "bin/SDL2-WINDOWS-LICENSE.txt": "97f35b302b361680ec1e891e95d2d52097bb95abff361434916d99dc1305f127",
"run.cmd": "b007382d9ed3560191856514a9ebd4e429801fc25428840e304bb297f3553269" "run.cmd": "b007382d9ed3560191856514a9ebd4e429801fc25428840e304bb297f3553269"
+51
View File
@@ -97,5 +97,56 @@ class FrozenDraftUploadTests(unittest.TestCase):
upload.DraftExporter(package, state, root / 'single') upload.DraftExporter(package, state, root / 'single')
class ConfigurableReleaseTargetTests(unittest.TestCase):
def test_stable_and_preview_guards_require_the_explicit_expected_state(self):
stable = upload.ReleaseTarget('c' * 40, 'v1.0.0', 12345, False)
release = {'id': 12345, 'tag_name': 'v1.0.0', 'target_commitish': 'c' * 40,
'draft': True, 'prerelease': False}
upload.checked_release(release, stable)
with self.assertRaises(ValueError):
upload.checked_release(dict(release, prerelease=True), stable)
with self.assertRaises(ValueError):
upload.checked_release(release) # Legacy preview defaults cannot match stable.
reference = {'ref': 'refs/tags/v1.0.0', 'object': {'type': 'commit', 'sha': 'c' * 40}}
upload.checked_tag(reference, target=stable)
with self.assertRaises(ValueError):
upload.checked_tag(dict(reference, object={'type': 'commit', 'sha': upload.COMMIT}), target=stable)
def test_invalid_exact_release_inputs_fail_before_any_directory_or_upload(self):
for values in (("main", 'v1.0.0', 123, False), ('A' * 40, 'v1.0.0', 123, False),
('c' * 40, '../v1', 123, False), ('c' * 40, 'v1/other', 123, False),
('c' * 40, 'v1.0.0', 0, False), ('c' * 40, 'v1.0.0', True, False),
('c' * 40, 'v1.0.0', 123, 'false')):
with self.subTest(values=values), self.assertRaises(ValueError):
upload.ReleaseTarget(*values)
def test_explicit_release_routes_api_and_upload_to_its_exact_id_and_tag(self):
target = upload.ReleaseTarget('c' * 40, 'v1.0.0', 12345, False)
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
package = root / 'frozen'
package.mkdir()
controller = upload.DraftExporter(package, root / 'state', root / 'single', target)
reference = {'ref': 'refs/tags/v1.0.0', 'object': {'type': 'commit', 'sha': 'c' * 40}}
release = {'id': 12345, 'tag_name': 'v1.0.0', 'target_commitish': 'c' * 40,
'draft': True, 'prerelease': False, 'assets': []}
with patch.object(controller, 'api', side_effect=[reference, release]) as api:
self.assertEqual(controller.release(), release)
self.assertEqual(api.call_args_list[0].args[0], 'repos/Quintine/RustRaptor/git/ref/tags/v1.0.0')
self.assertEqual(api.call_args_list[1].args[0], 'repos/Quintine/RustRaptor/releases/12345')
archive = root / 'single/one.zip'
archive.write_bytes(b'fixture')
wrong = {'name': 'one.zip', 'size': archive.stat().st_size, 'state': 'uploaded', 'digest': 'sha256:' + '0' * 64}
with patch.object(controller, 'snapshot'), patch.object(controller, 'release', return_value=release), \
patch.object(controller, 'log'), patch.object(upload.subprocess, 'run') as run:
run.return_value.returncode = 0
run.return_value.stdout = json.dumps(wrong)
run.return_value.stderr = ''
with self.assertRaises(ValueError):
controller.upload(archive, 'one.zip', upload.digest(archive), archive.stat().st_size)
self.assertIn('/releases/12345/assets?name=one.zip', run.call_args.args[0][4])
self.assertTrue(archive.exists())
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
+50 -23
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Export the frozen preview to its existing empty draft; never publish it.""" """Export an exact frozen release to its existing empty draft; never publish it."""
import argparse import argparse
import hashlib import hashlib
import json import json
@@ -9,16 +9,37 @@ import subprocess
import sys import sys
import time import time
from pathlib import Path from pathlib import Path
from dataclasses import dataclass
from urllib.parse import quote from urllib.parse import quote
REPO = 'Quintine/RustRaptor' REPO = 'Quintine/RustRaptor'
TAG = 'v0.1.0-preview.20261020' TAG = 'v0.1.0-preview.20261020'
COMMIT = '7ecaa42b29ff0db5d2f7cdfe6da0a3221dfea555' COMMIT = '7ecaa42b29ff0db5d2f7cdfe6da0a3221dfea555'
RELEASE_ID = 418286902 RELEASE_ID = 418286902
UPLOAD_URL = f'https://uploads.github.com/repos/{REPO}/releases/{RELEASE_ID}/assets'
FLOOR = 1_800_000_000 FLOOR = 1_800_000_000
@dataclass(frozen=True)
class ReleaseTarget:
commit: str = COMMIT
tag: str = TAG
release_id: int = RELEASE_ID
prerelease: bool = True
def __post_init__(self):
if not isinstance(self.commit, str) or not re.fullmatch('[0-9a-f]{40}', self.commit):
raise ValueError('release commit must be an exact lowercase 40-character SHA')
if not isinstance(self.tag, str) or not re.fullmatch(r'v[0-9A-Za-z._-]+', self.tag):
raise ValueError('release tag must be a safe exact version tag')
if type(self.release_id) is not int or self.release_id <= 0:
raise ValueError('release ID must be a positive integer')
if type(self.prerelease) is not bool:
raise ValueError('prerelease expectation must be a boolean')
PREVIEW_TARGET = ReleaseTarget()
def digest(path): def digest(path):
with Path(path).open('rb') as stream: with Path(path).open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest() return hashlib.file_digest(stream, 'sha256').hexdigest()
@@ -30,24 +51,24 @@ def write_json(path, value):
stream.write('\n') stream.write('\n')
def checked_release(release): def checked_release(release, target=PREVIEW_TARGET):
if (release.get('id') != RELEASE_ID or release.get('tag_name') != TAG if (release.get('id') != target.release_id or release.get('tag_name') != target.tag
or release.get('target_commitish') != COMMIT or release.get('draft') is not True or release.get('target_commitish') != target.commit or release.get('draft') is not True
or release.get('prerelease') is not True): or release.get('prerelease') is not target.prerelease):
raise ValueError('existing release ID/tag/target/draft state differs') raise ValueError('existing release ID/tag/target/draft state differs')
return release return release
def checked_tag(reference, annotated=None): def checked_tag(reference, annotated=None, target=PREVIEW_TARGET):
if reference.get('ref') != 'refs/tags/' + TAG: if reference.get('ref') != 'refs/tags/' + target.tag:
raise ValueError('wrong frozen tag reference') raise ValueError('wrong frozen tag reference')
obj = reference.get('object', {}) obj = reference.get('object', {})
if obj.get('type') == 'tag': if obj.get('type') == 'tag':
if (not isinstance(annotated, dict) or annotated.get('sha') != obj.get('sha') if (not isinstance(annotated, dict) or annotated.get('sha') != obj.get('sha')
or annotated.get('tag') != TAG): or annotated.get('tag') != target.tag):
raise ValueError('annotated tag identity differs') raise ValueError('annotated tag identity differs')
obj = annotated.get('object', {}) obj = annotated.get('object', {})
if obj.get('type') != 'commit' or obj.get('sha') != COMMIT: if obj.get('type') != 'commit' or obj.get('sha') != target.commit:
raise ValueError('frozen tag no longer points to the tested commit') raise ValueError('frozen tag no longer points to the tested commit')
@@ -70,7 +91,8 @@ def checked_receipt(plan, plan_sha, asset, receipt):
class DraftExporter: class DraftExporter:
def __init__(self, package, state, temporary): def __init__(self, package, state, temporary, target=PREVIEW_TARGET):
self.target = target
self.package = package.resolve(strict=True) self.package = package.resolve(strict=True)
self.state = state.absolute() self.state = state.absolute()
self.temporary = temporary.absolute() self.temporary = temporary.absolute()
@@ -98,7 +120,7 @@ class DraftExporter:
return json.loads(subprocess.check_output(['gh', 'api', endpoint], cwd=self.package)) return json.loads(subprocess.check_output(['gh', 'api', endpoint], cwd=self.package))
def release(self): def release(self):
reference = self.api(f'repos/{REPO}/git/ref/tags/{TAG}') reference = self.api(f'repos/{REPO}/git/ref/tags/{self.target.tag}')
obj = reference.get('object', {}) obj = reference.get('object', {})
annotated = None annotated = None
if obj.get('type') == 'tag': if obj.get('type') == 'tag':
@@ -106,14 +128,14 @@ class DraftExporter:
if not re.fullmatch('[0-9a-f]{40}', sha): if not re.fullmatch('[0-9a-f]{40}', sha):
raise ValueError('unsafe annotated tag SHA') raise ValueError('unsafe annotated tag SHA')
annotated = self.api(f'repos/{REPO}/git/tags/{sha}') annotated = self.api(f'repos/{REPO}/git/tags/{sha}')
checked_tag(reference, annotated) checked_tag(reference, annotated, self.target)
return checked_release(self.api(f'repos/{REPO}/releases/{RELEASE_ID}')) return checked_release(self.api(f'repos/{REPO}/releases/{self.target.release_id}'), self.target)
def snapshot(self): def snapshot(self):
actual = {'commit': subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=self.package, text=True).strip(), actual = {'commit': subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=self.package, text=True).strip(),
'runtime_manifest_sha256': digest(self.package / 'runtime-manifest.json'), 'runtime_manifest_sha256': digest(self.package / 'runtime-manifest.json'),
'windows_manifest_sha256': digest(self.package / 'runtime-windows-manifest.json')} 'windows_manifest_sha256': digest(self.package / 'runtime-windows-manifest.json')}
if actual['commit'] != COMMIT or subprocess.check_output( if actual['commit'] != self.target.commit or subprocess.check_output(
['git', 'status', '--porcelain', '--untracked-files=all'], cwd=self.package): ['git', 'status', '--porcelain', '--untracked-files=all'], cwd=self.package):
raise ValueError('frozen source commit or cleanliness differs') raise ValueError('frozen source commit or cleanliness differs')
if hasattr(self, 'plan') and actual != self.plan['snapshot']: if hasattr(self, 'plan') and actual != self.plan['snapshot']:
@@ -138,7 +160,7 @@ class DraftExporter:
or digest(path) != expected_sha): or digest(path) != expected_sha):
raise ValueError('local upload no longer matches authenticated receipt') raise ValueError('local upload no longer matches authenticated receipt')
self.log('uploading', asset=name, bytes=expected_size, sha256=expected_sha) self.log('uploading', asset=name, bytes=expected_size, sha256=expected_sha)
result = subprocess.run(['gh', 'api', '--method', 'POST', UPLOAD_URL + '?name=' + quote(name, safe=''), result = subprocess.run(['gh', 'api', '--method', 'POST', f'https://uploads.github.com/repos/{REPO}/releases/{self.target.release_id}/assets?name=' + quote(name, safe=''),
'--input', str(path), '-H', 'Content-Type: application/octet-stream'], '--input', str(path), '-H', 'Content-Type: application/octet-stream'],
cwd=self.package, capture_output=True, text=True) cwd=self.package, capture_output=True, text=True)
(self.state / (name + '.upload.stdout')).write_text(result.stdout) (self.state / (name + '.upload.stdout')).write_text(result.stdout)
@@ -163,12 +185,12 @@ class DraftExporter:
raise ValueError('draft must be completely empty before this one-pass export') raise ValueError('draft must be completely empty before this one-pass export')
write_json(self.state / 'initial-empty-draft.json', initial) write_json(self.state / 'initial-empty-draft.json', initial)
subprocess.run([sys.executable, '-B', str(self.exporter), 'plan', '--package', str(self.package), subprocess.run([sys.executable, '-B', str(self.exporter), 'plan', '--package', str(self.package),
'--expected-commit', COMMIT, '--output', str(self.plan_path)], check=True, cwd=self.package) '--expected-commit', self.target.commit, '--output', str(self.plan_path)], check=True, cwd=self.package)
self.plan = json.loads(self.plan_path.read_text()) self.plan = json.loads(self.plan_path.read_text())
plan_sha = digest(self.plan_path) plan_sha = digest(self.plan_path)
if (len(self.plan['assets']) != 9 or self.plan['snapshot']['commit'] != COMMIT if (len(self.plan['assets']) != 9 or self.plan['snapshot']['commit'] != self.target.commit
or sorted(asset['kind'] for asset in self.plan['assets']) != ['linux'] + ['shared-data'] * 7 + ['windows']): or sorted(asset['kind'] for asset in self.plan['assets']) != ['linux'] + ['shared-data'] * 7 + ['windows']):
raise ValueError('frozen preview no longer has exactly the expected nine ZIPs') raise ValueError('frozen release no longer has exactly the expected nine ZIPs')
for asset in self.plan['assets']: for asset in self.plan['assets']:
self.snapshot() self.snapshot()
self.remote_members(self.release()) self.remote_members(self.release())
@@ -188,15 +210,15 @@ class DraftExporter:
manifest = self.state / 'release-manifest.json' manifest = self.state / 'release-manifest.json'
subprocess.run([sys.executable, '-B', str(self.exporter), 'checksums', '--plan', str(self.plan_path), subprocess.run([sys.executable, '-B', str(self.exporter), 'checksums', '--plan', str(self.plan_path),
'--receipts', str(self.receipts), '--output', str(sums), '--receipts', str(self.receipts), '--output', str(sums),
'--manifest-output', str(manifest), '--tag', TAG], check=True, cwd=self.package) '--manifest-output', str(manifest), '--tag', self.target.tag], check=True, cwd=self.package)
for path in (sums, manifest, self.package / 'release/Install-Linux.sh', self.package / 'release/Install-Windows.ps1'): for path in (sums, manifest, self.package / 'release/Install-Linux.sh', self.package / 'release/Install-Windows.ps1'):
self.upload(path, path.name, digest(path), path.stat().st_size) self.upload(path, path.name, digest(path), path.stat().st_size)
final = self.release() final = self.release()
self.remote_members(final) self.remote_members(final)
if len(final['assets']) != 13: if len(final['assets']) != 13:
raise ValueError('complete preview must contain nine ZIPs plus four companion assets') raise ValueError('complete release must contain nine ZIPs plus four companion assets')
write_json(self.state / 'final-13-assets-draft-still-unpublished.json', final) write_json(self.state / 'final-13-assets-draft-still-unpublished.json', final)
self.log('COMPLETE_THIRTEEN_REMOTE_ASSETS_VERIFIED_DRAFT_NOT_PUBLISHED', checkpoint=COMMIT) self.log('COMPLETE_THIRTEEN_REMOTE_ASSETS_VERIFIED_DRAFT_NOT_PUBLISHED', checkpoint=self.target.commit)
def main(): def main():
@@ -204,8 +226,13 @@ def main():
parser.add_argument('--package', type=Path, required=True) parser.add_argument('--package', type=Path, required=True)
parser.add_argument('--state', type=Path, required=True) parser.add_argument('--state', type=Path, required=True)
parser.add_argument('--temporary', type=Path, required=True) parser.add_argument('--temporary', type=Path, required=True)
parser.add_argument('--commit', default=COMMIT)
parser.add_argument('--tag', default=TAG)
parser.add_argument('--release-id', type=int, default=RELEASE_ID)
parser.add_argument('--prerelease', choices=('true', 'false'), default='true')
args = parser.parse_args() args = parser.parse_args()
DraftExporter(args.package, args.state, args.temporary).run() target = ReleaseTarget(args.commit, args.tag, args.release_id, args.prerelease == 'true')
DraftExporter(args.package, args.state, args.temporary, target).run()
if __name__ == '__main__': if __name__ == '__main__':