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:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -12,7 +12,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: linux-release-${{ gitea.ref }}
|
group: release-builds-${{ gitea.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -95,6 +95,93 @@ jobs:
|
|||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
retention-days: 7
|
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:
|
publish-release:
|
||||||
name: Publish tagged Gitea release
|
name: Publish tagged Gitea release
|
||||||
if: startsWith(gitea.ref, 'refs/tags/v')
|
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
|
│ 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/core-validation.yml read-only Ubuntu source-only core gate for the private Gitea remote
|
||||||
├── .gitea/workflows/linux-release-build.yml
|
├── .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;
|
├── run-godot.ps1 / run-godot.cmd tracked development launcher + Windows wrapper;
|
||||||
│ resolves Godot/game-root from parameters, environment,
|
│ resolves Godot/game-root from parameters, environment,
|
||||||
│ PATH, and the conventional sibling install
|
│ 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_package_windows_x64.py,
|
||||||
│ │ test_dotnet_publish_proxy.py,
|
│ │ test_dotnet_publish_proxy.py,
|
||||||
│ │ test_publish_gitea_release.py,
|
│ │ test_publish_gitea_release.py,
|
||||||
|
│ │ test_release_workflow.py,
|
||||||
│ │ test_verify_windows_native.py source-only bootstrap/package/publish/native regressions
|
│ │ test_verify_windows_native.py source-only bootstrap/package/publish/native regressions
|
||||||
│ ├── sys4load.py loader + disassembler (opcode-decoding)
|
│ ├── sys4load.py loader + disassembler (opcode-decoding)
|
||||||
│ ├── age_opcodes.py GENERATED complete Python ABI view (do not hand-edit)
|
│ ├── 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
|
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.
|
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
|
CI acceptance is structural because the produced executable does not run on the Linux host. The verifier
|
||||||
require the Windows executable/PCK/self-contained managed directory, `Age.Engine.Text.Windows.dll`, the project
|
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; reject Linux shared objects and incomplete/cross-target
|
shim, all five pinned FFmpeg DLLs, and the FFmpeg license; rejects 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,
|
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
|
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
|
`--package-smoke` command remains available for real-Windows acceptance, but its result is not fabricated or
|
||||||
reported as a hosted CI gate.
|
reported as a hosted CI gate.
|
||||||
|
|
||||||
Develop, manual, and tag events will run independent Linux and Windows build jobs, each with read-only repository
|
The hosted Windows artifact job was added on 2026-08-03 beside the accepted Linux job. Develop, manual, and tag
|
||||||
permission and its own manifest-keyed cache/workflow artifact. Tag promotion will require both jobs, download
|
events now run independent `ubuntu-latest` builds, each with inherited read-only repository permission and its
|
||||||
both already-built archives, verify their commit/build evidence, and publish two distributable release assets:
|
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
|
`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
|
`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
|
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.
|
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 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
|
**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
|
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
|
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.
|
`d673652` on 2026-08-03.
|
||||||
|
|
||||||
The artifact wrapper is `.gitea/workflows/linux-release-build.yml`. It runs independently from the fast core
|
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
|
gate on `develop` pushes, manual dispatch, and `v*` tag pushes. Independent Linux and Windows jobs use the same
|
||||||
`global.json` .NET setup actions, then calls `./tools/build-linux-x64.sh` unchanged. `actions/cache@v4` retains
|
Python 3.11 and `global.json` .NET setup actions and call their locally accepted build commands unchanged. The
|
||||||
only the two hash-verified dependency archives and the independently hash-verified Linux release template; its
|
Windows job adds Ubuntu's MinGW-w64 GCC/binutils and never installs Wine. Each job has its own manifest-keyed
|
||||||
key is the combined Godot/FFmpeg manifest hash and deliberately has no prefix fallback. Successful runs upload
|
`actions/cache@v4` entry with no prefix fallback: shared Linux Godot editor plus only that target's hash-verified
|
||||||
one flat artifact containing the `.tar.gz`, its external SHA-256 file, `BUILD-INFO.json`, `SHA256SUMS`, and packaged-runtime smoke log for
|
FFmpeg archive and selectively installed release template. Linux uploads its `.tar.gz`, external SHA-256,
|
||||||
30 days. Failed runs retain any smoke/validation evidence for seven days. The build job has read-only repository
|
`BUILD-INFO.json`, `SHA256SUMS`, and dynamic package-smoke log. Windows uploads its ZIP, external SHA-256,
|
||||||
permission, no secrets or private game corpus, and no release/package publication authority.
|
`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
|
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
|
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 |
|
| `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_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
|
## 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_package_windows_x64.py",
|
||||||
"test_dotnet_publish_proxy.py",
|
"test_dotnet_publish_proxy.py",
|
||||||
"test_publish_gitea_release.py",
|
"test_publish_gitea_release.py",
|
||||||
|
"test_release_workflow.py",
|
||||||
"test_verify_windows_native.py",
|
"test_verify_windows_native.py",
|
||||||
"test_diff_optrace.py",
|
"test_diff_optrace.py",
|
||||||
"test_engine_ctx.py",
|
"test_engine_ctx.py",
|
||||||
|
|||||||
Reference in New Issue
Block a user