Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd31949383 | ||
|
|
ea6e937b91 | ||
|
|
991b471fdb |
No files matched your search
@@ -0,0 +1,76 @@
|
|||||||
|
name: Upload frozen preview to existing draft
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: frozen-preview-v0.1.0-preview.20261020
|
||||||
|
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'
|
||||||
|
steps:
|
||||||
|
- 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: 7ecaa42b29ff0db5d2f7cdfe6da0a3221dfea555
|
||||||
|
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"
|
||||||
|
- name: Retain frozen plan, local receipts, remote proofs and logs
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: frozen-preview-draft-export-${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
|
path: ${{ runner.temp }}/RustRaptor-draft-export-state
|
||||||
|
if-no-files-found: warn
|
||||||
|
retention-days: 30
|
||||||
@@ -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
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+18
-1
@@ -1,5 +1,9 @@
|
|||||||
# Downloadable releases
|
# Downloadable releases
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
@@ -77,10 +81,23 @@ 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
|
||||||
publish the draft only after all checks pass. Native Windows CI and local
|
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
|
||||||
|
|
||||||
|
The manually dispatched **Upload frozen release to existing draft** workflow
|
||||||
|
accepts the exact tested 40-character commit, existing version tag, empty draft
|
||||||
|
release ID and expected prerelease flag. For v1.0.0 select `false` for prerelease.
|
||||||
|
The publishing controller is checked out separately from the immutable game
|
||||||
|
checkpoint. Every source and remote release identity is checked, all archives
|
||||||
|
are verified, and the frozen plan and receipts are retained as an Actions
|
||||||
|
artifact. The job leaves the draft unpublished for final review.
|
||||||
|
|
||||||
|
The older preview workflow retains its fixed 20 October checkpoint and cannot
|
||||||
|
modify its already-published release. Neither workflow publishes automatically.
|
||||||
@@ -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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Current HD coverage is **5,743 / 5,753 original pictures**. Ten remain unapproved and retain original-art fallback. The reviewed 50-picture append was published in `443d7785`; no further artwork was generated for the release.
|
||||||
|
|
||||||
Current artwork checkpoint — 20 October 2026: **5,743 reviewed pictures / 3,830 complete sequences**, including **4,530 Terrain pictures**. **Ten originals remain unapproved**: five Terrain, four enemies/ground objects and one UI/other picture. Generation was declined for those ten; they are not counted as remastered.
|
Current artwork checkpoint — 20 October 2026: **5,743 reviewed pictures / 3,830 complete sequences**, including **4,530 Terrain pictures**. **Ten originals remain unapproved**: five Terrain, four enemies/ground objects and one UI/other picture. Generation was declined for those ten; they are not counted as remastered.
|
||||||
|
|
||||||
This 50-picture Terrain append covers dark/night material, coastlines, industrial structures and the remaining first/late-campaign terrain. Its PNG provenance is 32 Source-guided derived pictures, nine selections from untouched genuine atlases, three pictures sharing one partially derived atlas, and six lossless genuine-atlas crops. Only 304fb is patched in the shared atlas; 304fa/304fc cells remain unchanged genuine pixels. All 211 retained Original owners, Source geometry and tonal backing earn no generated-HD-detail credit. Stepped joins, dark material and native-scale detail limits remain documented. Two unused assets, 2043f/20458, have catalogue/intrinsic approval without an invented runtime placement.
|
This 50-picture Terrain append covers dark/night material, coastlines, industrial structures and the remaining first/late-campaign terrain. Its PNG provenance is 32 Source-guided derived pictures, nine selections from untouched genuine atlases, three pictures sharing one partially derived atlas, and six lossless genuine-atlas crops. Only 304fb is patched in the shared atlas; 304fa/304fc cells remain unchanged genuine pixels. All 211 retained Original owners, Source geometry and tonal backing earn no generated-HD-detail credit. Stepped joins, dark material and native-scale detail limits remain documented. Two unused assets, 2043f/20458, have catalogue/intrinsic approval without an invented runtime placement.
|
||||||
|
|||||||
+12
-1
@@ -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
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
"""Synthetic guards for this one fixed preview upload; no GitHub mutations."""
|
||||||
|
from copy import deepcopy
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import upload_draft_release as upload
|
||||||
|
|
||||||
|
|
||||||
|
class FrozenDraftUploadTests(unittest.TestCase):
|
||||||
|
def test_release_guard_rejects_any_changed_identity_or_published_state(self):
|
||||||
|
good = {'id': upload.RELEASE_ID, 'tag_name': upload.TAG,
|
||||||
|
'target_commitish': upload.COMMIT, 'draft': True, 'prerelease': True}
|
||||||
|
self.assertEqual(upload.checked_release(good), good)
|
||||||
|
for field, value in (('id', 1), ('tag_name', 'wrong'), ('target_commitish', 'main'), ('draft', False), ('prerelease', False)):
|
||||||
|
bad = dict(good)
|
||||||
|
bad[field] = value
|
||||||
|
with self.subTest(field=field), self.assertRaises(ValueError):
|
||||||
|
upload.checked_release(bad)
|
||||||
|
|
||||||
|
def test_direct_and_annotated_tag_must_peel_to_exact_commit(self):
|
||||||
|
direct = {'ref': 'refs/tags/' + upload.TAG, 'object': {'type': 'commit', 'sha': upload.COMMIT}}
|
||||||
|
upload.checked_tag(direct)
|
||||||
|
reference = {'ref': direct['ref'], 'object': {'type': 'tag', 'sha': 'a' * 40}}
|
||||||
|
annotation = {'sha': 'a' * 40, 'tag': upload.TAG, 'object': direct['object']}
|
||||||
|
upload.checked_tag(reference, annotation)
|
||||||
|
for bad in (None, dict(annotation, sha='b' * 40), dict(annotation, object={'type': 'commit', 'sha': 'b' * 40})):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
upload.checked_tag(reference, bad)
|
||||||
|
|
||||||
|
def test_remote_asset_requires_exact_hash_size_name_and_uploaded_state(self):
|
||||||
|
good = {'name': 'one.zip', 'size': 42, 'state': 'uploaded', 'digest': 'sha256:' + 'a' * 64}
|
||||||
|
upload.checked_remote(good, 'one.zip', 42, 'a' * 64)
|
||||||
|
for field, value in (('name', 'two.zip'), ('size', 41), ('state', 'starter'), ('digest', None)):
|
||||||
|
with self.subTest(field=field), self.assertRaises(ValueError):
|
||||||
|
upload.checked_remote(dict(good, **{field: value}), 'one.zip', 42, 'a' * 64)
|
||||||
|
|
||||||
|
def test_receipt_must_bind_same_plan_checkpoint_kind_and_every_payload(self):
|
||||||
|
plan = {'snapshot': {'commit': upload.COMMIT}}
|
||||||
|
asset = {'name': 'one.zip', 'kind': 'shared-data', 'files': [{'name': 'data/pic', 'sha256': 'b' * 64}]}
|
||||||
|
good = {'status': 'ZIP_CRC_AND_COMPLETE_PAYLOAD_SHA256_PASS', 'asset': 'one.zip', 'kind': 'shared-data',
|
||||||
|
'checkpoint': plan['snapshot'], 'plan_sha256': 'c' * 64, 'payload_files': 1,
|
||||||
|
'payload_sha256': {'data/pic': 'b' * 64}, 'bytes': 42, 'sha256': 'd' * 64}
|
||||||
|
upload.checked_receipt(plan, 'c' * 64, asset, good)
|
||||||
|
for field, value in (('checkpoint', {}), ('plan_sha256', 'a' * 64), ('kind', 'linux'),
|
||||||
|
('payload_files', 0), ('payload_sha256', {}), ('bytes', 2 * 1024**3), ('sha256', 'x')):
|
||||||
|
with self.subTest(field=field), self.assertRaises(ValueError):
|
||||||
|
upload.checked_receipt(plan, 'c' * 64, asset, dict(good, **{field: value}))
|
||||||
|
|
||||||
|
def test_remote_members_rejects_unplanned_missing_or_duplicate_assets(self):
|
||||||
|
controller = object.__new__(upload.DraftExporter)
|
||||||
|
controller.verified = {'one.zip': {'bytes': 42, 'sha256': 'a' * 64}}
|
||||||
|
asset = {'name': 'one.zip', 'size': 42, 'state': 'uploaded', 'digest': 'sha256:' + 'a' * 64}
|
||||||
|
controller.remote_members({'assets': [asset]})
|
||||||
|
for assets in ([], [asset, asset], [dict(asset, name='extra.zip')]):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
controller.remote_members({'assets': assets})
|
||||||
|
|
||||||
|
def test_upload_failure_or_remote_digest_mismatch_never_deletes_local_zip(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
|
root = Path(temp)
|
||||||
|
package = root / 'frozen'
|
||||||
|
package.mkdir()
|
||||||
|
controller = upload.DraftExporter(package, root / 'state', root / 'single')
|
||||||
|
archive = root / 'single/one.zip'
|
||||||
|
archive.write_bytes(b'complete ZIP fixture')
|
||||||
|
expected = upload.digest(archive)
|
||||||
|
before = {'assets': []}
|
||||||
|
wrong = {'id': 123, '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=before), \
|
||||||
|
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', expected, archive.stat().st_size)
|
||||||
|
self.assertTrue(archive.exists())
|
||||||
|
self.assertEqual(controller.verified, {})
|
||||||
|
run.return_value.returncode = 1
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
controller.upload(archive, 'one.zip', expected, archive.stat().st_size)
|
||||||
|
self.assertTrue(archive.exists())
|
||||||
|
|
||||||
|
def test_output_directories_must_be_new_and_outside_frozen_tree(self):
|
||||||
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
|
root = Path(temp)
|
||||||
|
package = root / 'frozen'
|
||||||
|
package.mkdir()
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
upload.DraftExporter(package, package / 'state', root / 'single')
|
||||||
|
state = root / 'already-existing-state'
|
||||||
|
state.mkdir()
|
||||||
|
with self.assertRaises(FileExistsError):
|
||||||
|
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__':
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Export an exact frozen release to its existing empty draft; never publish it."""
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
REPO = 'Quintine/RustRaptor'
|
||||||
|
TAG = 'v0.1.0-preview.20261020'
|
||||||
|
COMMIT = '7ecaa42b29ff0db5d2f7cdfe6da0a3221dfea555'
|
||||||
|
RELEASE_ID = 418286902
|
||||||
|
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):
|
||||||
|
with Path(path).open('rb') as stream:
|
||||||
|
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(path, value):
|
||||||
|
with Path(path).open('x', encoding='utf-8') as stream:
|
||||||
|
json.dump(value, stream, indent=2)
|
||||||
|
stream.write('\n')
|
||||||
|
|
||||||
|
|
||||||
|
def checked_release(release, target=PREVIEW_TARGET):
|
||||||
|
if (release.get('id') != target.release_id or release.get('tag_name') != target.tag
|
||||||
|
or release.get('target_commitish') != target.commit or release.get('draft') is not True
|
||||||
|
or release.get('prerelease') is not target.prerelease):
|
||||||
|
raise ValueError('existing release ID/tag/target/draft state differs')
|
||||||
|
return release
|
||||||
|
|
||||||
|
|
||||||
|
def checked_tag(reference, annotated=None, target=PREVIEW_TARGET):
|
||||||
|
if reference.get('ref') != 'refs/tags/' + target.tag:
|
||||||
|
raise ValueError('wrong frozen tag reference')
|
||||||
|
obj = reference.get('object', {})
|
||||||
|
if obj.get('type') == 'tag':
|
||||||
|
if (not isinstance(annotated, dict) or annotated.get('sha') != obj.get('sha')
|
||||||
|
or annotated.get('tag') != target.tag):
|
||||||
|
raise ValueError('annotated tag identity differs')
|
||||||
|
obj = annotated.get('object', {})
|
||||||
|
if obj.get('type') != 'commit' or obj.get('sha') != target.commit:
|
||||||
|
raise ValueError('frozen tag no longer points to the tested commit')
|
||||||
|
|
||||||
|
|
||||||
|
def checked_remote(remote, name, size, sha256):
|
||||||
|
if (remote.get('name') != name or remote.get('size') != size
|
||||||
|
or remote.get('state') != 'uploaded' or remote.get('digest') != 'sha256:' + sha256):
|
||||||
|
raise ValueError('remote asset name/size/state/SHA-256 differs; retain local export')
|
||||||
|
return remote
|
||||||
|
|
||||||
|
|
||||||
|
def checked_receipt(plan, plan_sha, asset, receipt):
|
||||||
|
if (receipt.get('status') != 'ZIP_CRC_AND_COMPLETE_PAYLOAD_SHA256_PASS'
|
||||||
|
or receipt.get('asset') != asset['name'] or receipt.get('kind') != asset['kind']
|
||||||
|
or receipt.get('checkpoint') != plan['snapshot'] or receipt.get('plan_sha256') != plan_sha
|
||||||
|
or receipt.get('payload_files') != len(asset['files'])
|
||||||
|
or receipt.get('payload_sha256') != {r['name']: r['sha256'] for r in asset['files']}
|
||||||
|
or not re.fullmatch('[0-9a-f]{64}', receipt.get('sha256', ''))
|
||||||
|
or not 0 < receipt.get('bytes', 0) < 2 * 1024**3):
|
||||||
|
raise ValueError('local receipt differs from the one frozen runner plan')
|
||||||
|
|
||||||
|
|
||||||
|
class DraftExporter:
|
||||||
|
def __init__(self, package, state, temporary, target=PREVIEW_TARGET):
|
||||||
|
self.target = target
|
||||||
|
self.package = package.resolve(strict=True)
|
||||||
|
self.state = state.absolute()
|
||||||
|
self.temporary = temporary.absolute()
|
||||||
|
if any(directory.resolve() != directory for directory in (self.state, self.temporary)):
|
||||||
|
raise ValueError('state and temporary paths must be literal paths without symlink ancestors')
|
||||||
|
for directory in (self.state, self.temporary):
|
||||||
|
if directory.is_relative_to(self.package):
|
||||||
|
raise ValueError('release outputs must remain outside the frozen source tree')
|
||||||
|
directory.mkdir(parents=True, exist_ok=False)
|
||||||
|
self.receipts = self.state / 'receipts'
|
||||||
|
self.remote = self.state / 'remote-receipts'
|
||||||
|
self.receipts.mkdir()
|
||||||
|
self.remote.mkdir()
|
||||||
|
self.plan_path = self.state / 'release-plan.json'
|
||||||
|
self.exporter = self.package / 'tools/export_release.py'
|
||||||
|
self.verified = {}
|
||||||
|
|
||||||
|
def log(self, event, **details):
|
||||||
|
row = {'time_utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), 'event': event, **details}
|
||||||
|
print(json.dumps(row), flush=True)
|
||||||
|
with (self.state / 'progress.jsonl').open('a') as stream:
|
||||||
|
stream.write(json.dumps(row) + '\n')
|
||||||
|
|
||||||
|
def api(self, endpoint):
|
||||||
|
return json.loads(subprocess.check_output(['gh', 'api', endpoint], cwd=self.package))
|
||||||
|
|
||||||
|
def release(self):
|
||||||
|
reference = self.api(f'repos/{REPO}/git/ref/tags/{self.target.tag}')
|
||||||
|
obj = reference.get('object', {})
|
||||||
|
annotated = None
|
||||||
|
if obj.get('type') == 'tag':
|
||||||
|
sha = obj.get('sha', '')
|
||||||
|
if not re.fullmatch('[0-9a-f]{40}', sha):
|
||||||
|
raise ValueError('unsafe annotated tag SHA')
|
||||||
|
annotated = self.api(f'repos/{REPO}/git/tags/{sha}')
|
||||||
|
checked_tag(reference, annotated, self.target)
|
||||||
|
return checked_release(self.api(f'repos/{REPO}/releases/{self.target.release_id}'), self.target)
|
||||||
|
|
||||||
|
def snapshot(self):
|
||||||
|
actual = {'commit': subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=self.package, text=True).strip(),
|
||||||
|
'runtime_manifest_sha256': digest(self.package / 'runtime-manifest.json'),
|
||||||
|
'windows_manifest_sha256': digest(self.package / 'runtime-windows-manifest.json')}
|
||||||
|
if actual['commit'] != self.target.commit or subprocess.check_output(
|
||||||
|
['git', 'status', '--porcelain', '--untracked-files=all'], cwd=self.package):
|
||||||
|
raise ValueError('frozen source commit or cleanliness differs')
|
||||||
|
if hasattr(self, 'plan') and actual != self.plan['snapshot']:
|
||||||
|
raise ValueError('frozen runtime manifests changed')
|
||||||
|
if shutil.disk_usage(self.package).free < FLOOR:
|
||||||
|
raise ValueError('runner workspace crossed the 1.8GB free-space floor')
|
||||||
|
|
||||||
|
def remote_members(self, release):
|
||||||
|
assets = release.get('assets', [])
|
||||||
|
names = [asset.get('name') for asset in assets]
|
||||||
|
if len(names) != len(set(names)) or set(names) != set(self.verified):
|
||||||
|
raise ValueError('unexpected, missing or duplicate remote assets; no clobber allowed')
|
||||||
|
for asset in assets:
|
||||||
|
receipt = self.verified[asset['name']]
|
||||||
|
checked_remote(asset, asset['name'], receipt['bytes'], receipt['sha256'])
|
||||||
|
return assets
|
||||||
|
|
||||||
|
def upload(self, path, name, expected_sha, expected_size):
|
||||||
|
self.snapshot()
|
||||||
|
self.remote_members(self.release())
|
||||||
|
if (path.is_symlink() or not path.is_file() or path.stat().st_size != expected_size
|
||||||
|
or digest(path) != expected_sha):
|
||||||
|
raise ValueError('local upload no longer matches authenticated receipt')
|
||||||
|
self.log('uploading', asset=name, bytes=expected_size, sha256=expected_sha)
|
||||||
|
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'],
|
||||||
|
cwd=self.package, capture_output=True, text=True)
|
||||||
|
(self.state / (name + '.upload.stdout')).write_text(result.stdout)
|
||||||
|
(self.state / (name + '.upload.stderr')).write_text(result.stderr)
|
||||||
|
if result.returncode:
|
||||||
|
raise RuntimeError('upload returned nonzero; retain local file and inspect remote draft before retry')
|
||||||
|
uploaded = checked_remote(json.loads(result.stdout), name, expected_size, expected_sha)
|
||||||
|
release = self.release()
|
||||||
|
matches = [asset for asset in release['assets'] if asset.get('name') == name]
|
||||||
|
if len(matches) != 1 or matches[0].get('id') != uploaded.get('id'):
|
||||||
|
raise ValueError('uploaded asset is not uniquely attached to the guarded draft')
|
||||||
|
checked_remote(matches[0], name, expected_size, expected_sha)
|
||||||
|
self.verified[name] = {'bytes': expected_size, 'sha256': expected_sha}
|
||||||
|
self.remote_members(release)
|
||||||
|
write_json(self.remote / (name + '.json'), matches[0])
|
||||||
|
self.log('remote_sha256_verified', asset=name, bytes=expected_size, sha256=expected_sha)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.snapshot()
|
||||||
|
initial = self.release()
|
||||||
|
if initial.get('assets'):
|
||||||
|
raise ValueError('draft must be completely empty before this one-pass export')
|
||||||
|
write_json(self.state / 'initial-empty-draft.json', initial)
|
||||||
|
subprocess.run([sys.executable, '-B', str(self.exporter), 'plan', '--package', str(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())
|
||||||
|
plan_sha = digest(self.plan_path)
|
||||||
|
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']):
|
||||||
|
raise ValueError('frozen release no longer has exactly the expected nine ZIPs')
|
||||||
|
for asset in self.plan['assets']:
|
||||||
|
self.snapshot()
|
||||||
|
self.remote_members(self.release())
|
||||||
|
name = asset['name']
|
||||||
|
path = self.temporary / name
|
||||||
|
receipt_path = self.receipts / (name + '.json')
|
||||||
|
self.log('building', asset=name, uncompressed_bytes=asset['uncompressed_bytes'])
|
||||||
|
subprocess.run([sys.executable, '-B', str(self.exporter), 'archive', '--plan', str(self.plan_path),
|
||||||
|
'--asset', name, '--output', str(path), '--receipt', str(receipt_path)],
|
||||||
|
check=True, cwd=self.package)
|
||||||
|
receipt = json.loads(receipt_path.read_text())
|
||||||
|
checked_receipt(self.plan, plan_sha, asset, receipt)
|
||||||
|
self.upload(path, name, receipt['sha256'], receipt['bytes'])
|
||||||
|
path.unlink() # Only this just-authenticated disposable archive, after remote proof.
|
||||||
|
self.log('temporary_export_removed', asset=name)
|
||||||
|
sums = self.state / 'SHA256SUMS'
|
||||||
|
manifest = self.state / 'release-manifest.json'
|
||||||
|
subprocess.run([sys.executable, '-B', str(self.exporter), 'checksums', '--plan', str(self.plan_path),
|
||||||
|
'--receipts', str(self.receipts), '--output', str(sums),
|
||||||
|
'--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'):
|
||||||
|
self.upload(path, path.name, digest(path), path.stat().st_size)
|
||||||
|
final = self.release()
|
||||||
|
self.remote_members(final)
|
||||||
|
if len(final['assets']) != 13:
|
||||||
|
raise ValueError('complete release must contain nine ZIPs plus four companion assets')
|
||||||
|
write_json(self.state / 'final-13-assets-draft-still-unpublished.json', final)
|
||||||
|
self.log('COMPLETE_THIRTEEN_REMOTE_ASSETS_VERIFIED_DRAFT_NOT_PUBLISHED', checkpoint=self.target.commit)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('--package', type=Path, required=True)
|
||||||
|
parser.add_argument('--state', 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()
|
||||||
|
target = ReleaseTarget(args.commit, args.tag, args.release_id, args.prerelease == 'true')
|
||||||
|
DraftExporter(args.package, args.state, args.temporary, target).run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in new issue
Block a user