From 9d3ab30bbb70842b4a54f62b448eaeb54eff7a43 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 3 Aug 2026 22:16:35 -0400 Subject: [PATCH] Build Windows release artifact in Gitea --- .gitea/workflows/linux-release-build.yml | 91 +++++++++++++++++++++++- docs/PROJECT-STRUCTURE.md | 3 +- docs/platform-portability.md | 21 ++++-- docs/remake-architecture-and-roadmap.md | 10 +++ docs/tools-reference.md | 18 +++-- tools/test_release_workflow.py | 70 ++++++++++++++++++ tools/validate.py | 1 + 7 files changed, 197 insertions(+), 17 deletions(-) create mode 100755 tools/test_release_workflow.py diff --git a/.gitea/workflows/linux-release-build.yml b/.gitea/workflows/linux-release-build.yml index d4909c4..ba2582c 100644 --- a/.gitea/workflows/linux-release-build.yml +++ b/.gitea/workflows/linux-release-build.yml @@ -1,4 +1,4 @@ -name: Linux release build +name: Release builds on: push: @@ -12,7 +12,7 @@ permissions: contents: read concurrency: - group: linux-release-${{ gitea.ref }} + group: release-builds-${{ gitea.ref }} cancel-in-progress: true jobs: @@ -95,6 +95,93 @@ jobs: if-no-files-found: ignore retention-days: 7 + windows-release: + name: Windows x64 artifact + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + DOTNET_CLI_TELEMETRY_OPTOUT: "1" + DOTNET_NOLOGO: "true" + NUGET_XMLDOC_MODE: skip + PYTHONUTF8: "1" + MINGW_CC: x86_64-w64-mingw32-gcc-posix + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Set up Python 3.11 + uses: actions/setup-python@v6 + with: + python-version: "3.11" + architecture: x64 + + - name: Set up .NET SDK + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Install MinGW cross-toolchain + run: | + sudo apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends \ + gcc-mingw-w64-x86-64 binutils-mingw-w64-x86-64 + + - name: Restore immutable Windows inputs + uses: actions/cache@v4 + with: + path: | + build/downloads/Godot_*_mono_linux_x86_64.zip + build/downloads/ffmpeg-*-win64-lgpl-shared-*.zip + build/toolchains/godot-*/xdg-data/godot/export_templates/*/windows_release_x86_64.exe + key: windows-release-inputs-${{ hashFiles('tools/godot-linux-x64.json', 'native/age_movie_ffmpeg/dependency-win64.json') }} + + - name: Report toolchain + run: | + python --version + dotnet --version + "$MINGW_CC" --version + x86_64-w64-mingw32-objdump --version + free -h + if [ -r /sys/fs/cgroup/memory.max ]; then + echo "cgroup memory.max=$(cat /sys/fs/cgroup/memory.max)" + echo "cgroup memory.current=$(cat /sys/fs/cgroup/memory.current)" + fi + + - name: Cross-build and verify Windows package + run: ./tools/build-windows-x64.sh + + - name: Stage verified workflow artifact + run: | + mkdir -p build/workflow-artifact-windows + cp build/package/windows-x64/OpenMaidEngine-Himegari-windows-x64.zip build/workflow-artifact-windows/ + cp build/package/windows-x64/staging/OpenMaidEngine-Himegari-windows-x64/BUILD-INFO.json build/workflow-artifact-windows/ + cp build/package/windows-x64/staging/OpenMaidEngine-Himegari-windows-x64/SHA256SUMS build/workflow-artifact-windows/ + cp build/package/windows-x64/staging/OpenMaidEngine-Himegari-windows-x64/WINDOWS-VERIFICATION.json build/workflow-artifact-windows/ + cd build/workflow-artifact-windows + sha256sum OpenMaidEngine-Himegari-windows-x64.zip > OpenMaidEngine-Himegari-windows-x64.zip.sha256 + + - name: Upload verified Windows package + uses: christopherhx/gitea-upload-artifact@v4 + with: + name: OpenMaidEngine-Himegari-windows-x64-${{ gitea.sha }} + path: build/workflow-artifact-windows/* + if-no-files-found: error + retention-days: 30 + + - name: Upload failure evidence + if: failure() + uses: christopherhx/gitea-upload-artifact@v4 + with: + name: windows-release-failure-${{ gitea.run_id }}-${{ gitea.run_attempt }} + path: | + build/native/win-x64/verification.json + build/package/windows-x64/staging/OpenMaidEngine-Himegari-windows-x64/WINDOWS-VERIFICATION.json + build/validation/** + if-no-files-found: ignore + retention-days: 7 + publish-release: name: Publish tagged Gitea release if: startsWith(gitea.ref, 'refs/tags/v') diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index 587d45b..2e27d55 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -36,7 +36,7 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings) │ tracked source and generated references use canonical LF ├── .gitea/workflows/core-validation.yml read-only Ubuntu source-only core gate for the private Gitea remote ├── .gitea/workflows/linux-release-build.yml - │ pinned Linux export/package/smoke artifact job plus tag-only promotion + │ independent Linux/Windows x64 artifact jobs plus Linux-only tag promotion ├── run-godot.ps1 / run-godot.cmd tracked development launcher + Windows wrapper; │ resolves Godot/game-root from parameters, environment, │ PATH, and the conventional sibling install @@ -60,6 +60,7 @@ S:\Game Hacking\Eushully\Himegari\ ← workspace root (three siblings) │ │ test_package_windows_x64.py, │ │ test_dotnet_publish_proxy.py, │ │ test_publish_gitea_release.py, + │ │ test_release_workflow.py, │ │ test_verify_windows_native.py source-only bootstrap/package/publish/native regressions │ ├── sys4load.py loader + disassembler (opcode-decoding) │ ├── age_opcodes.py GENERATED complete Python ABI view (do not hand-edit) diff --git a/docs/platform-portability.md b/docs/platform-portability.md index d661881..b811872 100644 --- a/docs/platform-portability.md +++ b/docs/platform-portability.md @@ -124,17 +124,24 @@ bootstrap/hash-checks the pinned Windows FFmpeg SDK, cross-builds and inspects ` exact self-contained `ExportRelease/win-x64` publish, and runs the real Godot Windows export. The accepted Linux path retains its target selection and dynamic package-smoke behavior. -CI acceptance is structural because the produced executable will not run on the Linux host. The verifier must -require the Windows executable/PCK/self-contained managed directory, `Age.Engine.Text.Windows.dll`, the project -shim, all five pinned FFmpeg DLLs, and the FFmpeg license; reject Linux shared objects and incomplete/cross-target -payloads; and inspect every project-owned PE image as AMD64. The portable ZIP will carry the project license, +CI acceptance is structural because the produced executable does not run on the Linux host. The verifier +requires the Windows executable/PCK/self-contained managed directory, `Age.Engine.Text.Windows.dll`, the project +shim, all five pinned FFmpeg DLLs, and the FFmpeg license; rejects Linux shared objects and incomplete/cross-target +payloads; and inspects the executable plus native DLLs as AMD64. The portable ZIP carries the project license, third-party notices, clean source/dependency metadata, and a complete payload checksum ledger. A manual packaged `--package-smoke` command remains available for real-Windows acceptance, but its result is not fabricated or reported as a hosted CI gate. -Develop, manual, and tag events will run independent Linux and Windows build jobs, each with read-only repository -permission and its own manifest-keyed cache/workflow artifact. Tag promotion will require both jobs, download -both already-built archives, verify their commit/build evidence, and publish two distributable release assets: +The hosted Windows artifact job was added on 2026-08-03 beside the accepted Linux job. Develop, manual, and tag +events now run independent `ubuntu-latest` builds, each with inherited read-only repository permission and its +own manifest-keyed cache/workflow artifact. Windows installs Ubuntu MinGW-w64 GCC/binutils, reports the compiler +and PE inspector, calls only `tools/build-windows-x64.sh`, and retains the ZIP plus its external hash, build +metadata, payload ledger, and static verification report for 30 days. It has no secrets, Wine, EXE execution, or +release authority. Source-only workflow regressions pin those rules and preserve the current Linux-only +`publish-release` dependency; the first hosted dual-job develop result remains the acceptance gate. + +The next promotion slice will require both jobs, download both already-built archives, verify their commit/build +evidence, and publish two distributable release assets: `OpenMaidEngine-Himegari-linux-x64.tar.gz` and `OpenMaidEngine-Himegari-windows-x64.zip`. A third small `RELEASE-SHA256SUMS` attachment will authenticate both archives while keeping the release below Gitea's observed five-attachment boundary; detailed build metadata, payload ledgers, and logs remain inside each archive and in diff --git a/docs/remake-architecture-and-roadmap.md b/docs/remake-architecture-and-roadmap.md index 007c100..6fb21d5 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -1073,6 +1073,16 @@ do not mix mechanical moves with semantic changes. stable, and an optional Windows-host run of the packaged EXE reported the 548-opcode/FFmpeg-ABI-3 smoke marker. Slice 3 is now active: add the read-only hosted Windows artifact job while retaining the accepted Linux job. + **Slice 3 implementation completed 2026-08-03; hosted acceptance pending:** the existing artifact workflow + now has independent `ubuntu-latest` Linux and Windows jobs under inherited read-only contents permission. + Windows provisions MinGW-w64 GCC/binutils, restores only its manifest-keyed editor/FFmpeg/template inputs, + calls the accepted `tools/build-windows-x64.sh`, and uploads the ZIP, external archive hash, build metadata, + payload ledger, and static verification report for 30 days. It has no secrets, Wine, EXE execution, or release + authority. A core-gated source-only workflow regression pins that boundary and deliberately proves that + `publish-release` still needs only `linux-release` during this slice. Acceptance requires the first hosted + develop run to leave promotion skipped and complete both platform jobs; slice 4 then replaces the Linux-only + promotion contract with dual-archive verification and publication. + **Completion gate:** one deliberately selected future tag produces a single Gitea release containing the Linux `.tar.gz`, Windows `.zip`, and combined archive checksums; both archives bind to the tag commit, Linux retains its dynamic packaged smoke, Windows passes all structural PE/payload gates, and neither platform job diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 4514136..7781ba5 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -48,13 +48,16 @@ its tree-equivalent rewritten commit is `524ea74`. The hosted gate also succeede `d673652` on 2026-08-03. The artifact wrapper is `.gitea/workflows/linux-release-build.yml`. It runs independently from the fast core -gate on `develop` pushes, manual dispatch, and `v*` tag pushes. The build job uses the same Python 3.11 and -`global.json` .NET setup actions, then calls `./tools/build-linux-x64.sh` unchanged. `actions/cache@v4` retains -only the two hash-verified dependency archives and the independently hash-verified Linux release template; its -key is the combined Godot/FFmpeg manifest hash and deliberately has no prefix fallback. Successful runs upload -one flat artifact containing the `.tar.gz`, its external SHA-256 file, `BUILD-INFO.json`, `SHA256SUMS`, and packaged-runtime smoke log for -30 days. Failed runs retain any smoke/validation evidence for seven days. The build job has read-only repository -permission, no secrets or private game corpus, and no release/package publication authority. +gate on `develop` pushes, manual dispatch, and `v*` tag pushes. Independent Linux and Windows jobs use the same +Python 3.11 and `global.json` .NET setup actions and call their locally accepted build commands unchanged. The +Windows job adds Ubuntu's MinGW-w64 GCC/binutils and never installs Wine. Each job has its own manifest-keyed +`actions/cache@v4` entry with no prefix fallback: shared Linux Godot editor plus only that target's hash-verified +FFmpeg archive and selectively installed release template. Linux uploads its `.tar.gz`, external SHA-256, +`BUILD-INFO.json`, `SHA256SUMS`, and dynamic package-smoke log. Windows uploads its ZIP, external SHA-256, +`BUILD-INFO.json`, `SHA256SUMS`, and `WINDOWS-VERIFICATION.json`; it does not execute the EXE. Both artifacts are +retained for 30 days and failures retain bounded evidence for seven days. Both build jobs inherit read-only +repository permission and have no secrets, private game corpus, or release/package publication authority. The +first hosted dual-job result remains pending until this workflow change reaches `develop`. Only a successful `v*` tag run enables the dependent promotion job. It downloads that same workflow artifact rather than rebuilding, checks out only the release tool, and receives job-local `releases: write` plus @@ -69,6 +72,7 @@ refuses mismatched releases or same-name/different-size assets instead of editin |---|---|---|---| | `publish_gitea_release.py` | Tag-only, retry-safe promotion of one verified Linux workflow artifact through Gitea's native release API. Requires `GITEA_TOKEN`; the token is passed to upload `curl` through standard input rather than its argument list. | `publish_gitea_release.py --server --repository --tag --target --asset ` (exactly the five expected `--asset` names) | verified downloaded workflow artifact + tag context → matching Gitea release and five attachments | | `test_publish_gitea_release.py` | Pure synthetic creation, retry/resume, collision, and mismatch regressions. | `test_publish_gitea_release.py` | temporary files only | +| `test_release_workflow.py` | Source-only policy regression for the independent read-only Linux/Windows jobs, target-specific MinGW/cache/artifact boundary, no-Wine/no-Windows-smoke rule, and deliberately unchanged Linux-only tag promotion dependency during Windows CI/CD slice 3. | `test_release_workflow.py` | `.gitea/workflows/linux-release-build.yml` → assertions only | ## Optional local binary tools diff --git a/tools/test_release_workflow.py b/tools/test_release_workflow.py new file mode 100755 index 0000000..18fa753 --- /dev/null +++ b/tools/test_release_workflow.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import re +import unittest +from pathlib import Path + + +REPO = Path(__file__).resolve().parent.parent +WORKFLOW = REPO / ".gitea/workflows/linux-release-build.yml" + + +def job(text: str, name: str, next_name: str | None) -> str: + start = text.index(f" {name}:\n") + end = len(text) if next_name is None else text.index(f" {next_name}:\n", start + 1) + return text[start:end] + + +class ReleaseWorkflowTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.text = WORKFLOW.read_text(encoding="utf-8") + cls.linux = job(cls.text, "linux-release", "windows-release") + cls.windows = job(cls.text, "windows-release", "publish-release") + cls.publish = job(cls.text, "publish-release", None) + + def test_build_jobs_are_linux_hosted_read_only_and_target_separate(self) -> None: + self.assertRegex(self.text, r"(?m)^permissions:\n contents: read$") + self.assertIn("runs-on: ubuntu-latest", self.linux) + self.assertIn("run: ./tools/build-linux-x64.sh", self.linux) + self.assertIn("runs-on: ubuntu-latest", self.windows) + self.assertIn("run: ./tools/build-windows-x64.sh", self.windows) + self.assertNotIn("releases: write", self.linux + self.windows) + self.assertNotIn("secrets.", self.linux + self.windows) + + def test_windows_job_provisions_and_caches_only_its_cross_inputs(self) -> None: + self.assertRegex( + self.windows, + r"gcc-mingw-w64-x86-64 binutils-mingw-w64-x86-64", + ) + self.assertIn("binutils-mingw-w64-x86-64", self.windows) + self.assertIn("ffmpeg-*-win64-lgpl-shared-*.zip", self.windows) + self.assertIn("windows_release_x86_64.exe", self.windows) + self.assertIn("dependency-win64.json", self.windows) + self.assertNotIn("linux_release.x86_64", self.windows) + self.assertNotIn("dependency-linux-x64.json", self.windows) + self.assertNotRegex(self.windows.lower(), r"\bwine(?:32|64)?\b") + self.assertNotIn("--package-smoke", self.windows) + + def test_windows_artifact_is_archive_plus_structural_evidence(self) -> None: + for name in ( + "OpenMaidEngine-Himegari-windows-x64.zip", + "OpenMaidEngine-Himegari-windows-x64.zip.sha256", + "BUILD-INFO.json", + "SHA256SUMS", + "WINDOWS-VERIFICATION.json", + ): + self.assertIn(name, self.windows) + self.assertIn("if-no-files-found: error", self.windows) + self.assertIn("retention-days: 30", self.windows) + + def test_slice_three_keeps_linux_only_tag_promotion_boundary(self) -> None: + self.assertIn("if: startsWith(gitea.ref, 'refs/tags/v')", self.publish) + self.assertRegex(self.publish, r"(?m)^ needs: linux-release$") + self.assertIn("releases: write", self.publish) + self.assertNotIn("windows-release", self.publish) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/validate.py b/tools/validate.py index 6ff46f1..1907f05 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -35,6 +35,7 @@ CORE_TESTS = ( "test_package_windows_x64.py", "test_dotnet_publish_proxy.py", "test_publish_gitea_release.py", + "test_release_workflow.py", "test_verify_windows_native.py", "test_diff_optrace.py", "test_engine_ctx.py",