Build Windows release artifact in Gitea
All checks were successful
All checks were successful
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <url> --repository <owner/name> --tag <v...> --target <sha> --asset <path>` (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
|
||||
|
||||
|
||||
70
tools/test_release_workflow.py
Executable file
70
tools/test_release_workflow.py
Executable file
@@ -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()
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user