Skip to content
This repository was archived by the owner on Aug 1, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,49 @@ jobs:
cache: npm
cache-dependency-path: tools/bfme-launcher-mcp/package-lock.json
- run: npm.cmd ci
- run: npm.cmd audit --audit-level=moderate
- name: Test environment-independent protocol handshake
run: node --test test/raw-protocol.test.mjs

engine:
name: Engine (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.0.0
with:
dotnet-version: |
8.0.x
10.0.100
- run: dotnet test engine/OpenBfme.Engine.sln --nologo --configuration Release

release-contracts:
name: Launcher and release contracts
runs-on: windows-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.0.0
with:
dotnet-version: "10.0.100"
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12.10"
- name: Install hash-pinned importer test dependencies
run: python -m pip install --require-hashes -r importer/requirements-win.txt
- name: Importer suite
env:
TEMP: ${{ runner.temp }}
TMP: ${{ runner.temp }}
TMPDIR: ${{ runner.temp }}
run: python -m pytest importer/tests -q
- name: Launcher security and rollback tests
run: dotnet run --project launcher/OpenBFME.Launcher.Tests/OpenBFME.Launcher.Tests.csproj --configuration Release
- name: Import reproducibility comparator tests
run: python -m unittest tools.release.test_compare_import_bundles -v
- name: Release firewall tests
shell: powershell
run: ./tools/release/Test-ReleaseTools.ps1
309 changes: 297 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,27 +1,312 @@
name: Release
name: windows release

on:
workflow_dispatch:
inputs:
version:
description: "Release version without a leading v (manual validation does not publish)"
required: true
default: "0.0.0-local"
channel:
description: "Update channel"
required: true
type: choice
options: [playtest, stable, nightly]
default: playtest
run_acceptance:
description: "Run packaged BFME2 acceptance on the dedicated Windows runner"
required: true
type: boolean
default: false
push:
tags: ["v*"]

permissions:
contents: write
contents: read

jobs:
source-prerelease:
name: Publish source prerelease
runs-on: ubuntu-latest
build:
name: deterministic Windows packages
runs-on: windows-latest
permissions:
contents: read
id-token: write
attestations: write
outputs:
version: ${{ steps.identity.outputs.version }}
env:
GODOT_VERSION: "4.7"
GODOT_RELEASE: "4.7-stable"
GODOT_EDITOR_SHA512: "41645a908eb3181d6f2d1201ed7b6d6f095f6a23aaed8903d5d255277cc8d142814f3e6817f865b3cac142c39b8aff99280091d3bbdaa301517730b3ba0522b9"
GODOT_TEMPLATES_SHA512: "1035dfde4edcc2472bb0c0b9610ce3ee9302642c2b9957e9066372f9f6bb759ab250c8887551a66f0bc5f51bbd9a58bb45e33a0f29844e97615a9b1138c1120e"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.0.2
with:
fetch-depth: 0
- name: Require a version tag on main history
shell: bash
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.0.0
with:
dotnet-version: |
8.0.x
10.0.100
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12.10"

- name: Resolve immutable release identity
id: identity
shell: powershell
run: |
$version = if ("${{ github.event_name }}" -eq "push") {
"${{ github.ref_name }}".Substring(1)
} else {
"${{ inputs.version }}"
}
if ($version -cnotmatch '^[0-9A-Za-z][0-9A-Za-z._-]{0,63}$') { throw "Unsafe version." }
if ("${{ github.event_name }}" -eq "push" -and
$version -cnotmatch '^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$') {
throw "Release tags must use SemVer."
}
$channel = if ("${{ github.event_name }}" -eq "push") {
if ($version.Contains("-")) { "playtest" } else { "stable" }
} else {
"${{ inputs.channel }}"
}
"version=$version" >> $env:GITHUB_OUTPUT
"channel=$channel" >> $env:GITHUB_OUTPUT

- name: Verify signed tag and protected branch ancestry
if: github.event_name == 'push'
env:
GH_TOKEN: ${{ github.token }}
shell: powershell
run: |
$ref = gh api "repos/$env:GITHUB_REPOSITORY/git/ref/tags/$env:GITHUB_REF_NAME" | ConvertFrom-Json
if ($ref.object.type -ne "tag") { throw "Release tag must be annotated and signed." }
$tag = gh api "repos/$env:GITHUB_REPOSITORY/git/tags/$($ref.object.sha)" | ConvertFrom-Json
if ($tag.verification.verified -ne $true) {
throw "GitHub did not verify the release tag signature: $($tag.verification.reason)"
}
git fetch origin main --no-tags
git merge-base --is-ancestor "$GITHUB_SHA" "origin/main"
- name: Create GitHub prerelease
git merge-base --is-ancestor $env:GITHUB_SHA origin/main
if ($LASTEXITCODE -ne 0) { throw "Release commit is not an ancestor of origin/main." }

- name: Run source gates
shell: powershell
run: |
python -m pip install --require-hashes -r importer/requirements-win.txt
python -m pytest importer/tests -q
dotnet test engine/OpenBfme.Engine.sln --nologo --configuration Release
dotnet run --project launcher/OpenBFME.Launcher.Tests/OpenBFME.Launcher.Tests.csproj --configuration Release
python -m unittest tools.release.test_compare_import_bundles -v
./tools/test-export-scan.ps1
./tools/release/Test-ReleaseTools.ps1
Push-Location tools/bfme-launcher-mcp
try {
npm.cmd ci
npm.cmd audit --audit-level=moderate
node --test test/raw-protocol.test.mjs
} finally { Pop-Location }

- name: Download and verify Godot
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: gh release create "$GITHUB_REF_NAME" --verify-tag --generate-notes --prerelease --title "OpenBFME $GITHUB_REF_NAME"
shell: powershell
run: |
$toolRoot = Join-Path $env:RUNNER_TEMP "godot"
New-Item -ItemType Directory -Path $toolRoot | Out-Null
$editor = Join-Path $toolRoot "godot-editor.zip"
$templates = Join-Path $toolRoot "godot-templates.zip"
$releaseJson = gh api "repos/godotengine/godot/releases/tags/$env:GODOT_RELEASE"
if ($LASTEXITCODE -ne 0) { throw "Godot release lookup failed." }
$release = $releaseJson | ConvertFrom-Json
function Get-GodotReleaseAsset {
param([Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string]$Destination)
$asset = @($release.assets | Where-Object name -CEQ $Name)
if ($asset.Count -ne 1 -or $asset[0].state -ne "uploaded") {
throw "Godot release asset identity is missing or ambiguous."
}
& curl.exe `
--fail `
--location `
--silent `
--show-error `
--retry 5 `
--retry-all-errors `
--retry-delay 2 `
--retry-max-time 120 `
--header "Accept: application/octet-stream" `
--header "Authorization: Bearer $env:GH_TOKEN" `
--header "X-GitHub-Api-Version: 2022-11-28" `
--output $Destination `
"https://api.github.com/repos/godotengine/godot/releases/assets/$($asset[0].id)"
if ($LASTEXITCODE -ne 0) { throw "Godot release asset download failed." }
}
Get-GodotReleaseAsset `
"Godot_v$($env:GODOT_RELEASE)_win64.exe.zip" `
$editor
Get-GodotReleaseAsset `
"Godot_v$($env:GODOT_RELEASE)_export_templates.tpz" `
$templates
if ((Get-FileHash $editor -Algorithm SHA512).Hash.ToLowerInvariant() -ne $env:GODOT_EDITOR_SHA512) { throw "Godot editor hash mismatch." }
if ((Get-FileHash $templates -Algorithm SHA512).Hash.ToLowerInvariant() -ne $env:GODOT_TEMPLATES_SHA512) { throw "Godot templates hash mismatch." }
Expand-Archive $editor -DestinationPath (Join-Path $toolRoot "editor")
Expand-Archive $templates -DestinationPath (Join-Path $toolRoot "templates")
$templateTarget = Join-Path $env:APPDATA "Godot\export_templates\$env:GODOT_VERSION.stable"
New-Item -ItemType Directory -Path (Split-Path $templateTarget -Parent) -Force | Out-Null
Move-Item (Join-Path $toolRoot "templates\templates") $templateTarget

- name: Export code-only game
shell: powershell
run: |
$stage = Join-Path $env:RUNNER_TEMP "openbfme-code-only"
$dist = Join-Path $env:RUNNER_TEMP "openbfme-release"
./tools/release/Build-CodeOnlyExport.ps1 -RepositoryRoot $PWD -Destination $stage
$godot = Get-ChildItem (Join-Path $env:RUNNER_TEMP "godot\editor") -Filter "*console.exe" | Select-Object -First 1 -ExpandProperty FullName
./tools/release/Invoke-GodotExport.ps1 -Godot $godot -Project (Join-Path $stage "game") -Output (Join-Path $dist "game\OpenBFME.exe") -LogRoot (Join-Path $dist "logs")
./tools/release/Test-WindowsExport.ps1 -Executable (Join-Path $dist "game\OpenBFME.exe") -LogRoot (Join-Path $dist "logs")

- name: Publish launcher and bundled importer source
shell: powershell
run: |
$dist = Join-Path $env:RUNNER_TEMP "openbfme-release"
$launcher = Join-Path $dist "launcher"
dotnet publish launcher/OpenBFME.Launcher/OpenBFME.Launcher.csproj --configuration Release --runtime win-x64 --self-contained true -p:PublishSingleFile=true -p:DebugType=None --output $launcher
$sourceArchive = Join-Path $env:RUNNER_TEMP "openbfme-bundled-source.zip"
git archive --format=zip --output $sourceArchive HEAD -- `
importer/openbfme_importer `
importer/blender `
importer/profiles `
importer/requirements-release-win.txt `
contracts `
tools/openbfme_import.py `
tools/bootstrap-importer-python.ps1
if ($LASTEXITCODE -ne 0) { throw "Failed to archive committed bundled source." }
Expand-Archive $sourceArchive -DestinationPath $launcher
$sourceIdentity = [ordered]@{
schema = "openbfme.bundled-source-identity"
schemaVersion = 1
commit = "${{ github.sha }}"
sourceClean = $true
}
[IO.File]::WriteAllText(
(Join-Path $launcher "release-identity.json"),
($sourceIdentity | ConvertTo-Json) + "`n",
[Text.UTF8Encoding]::new($false)
)
./tools/release/New-PinnedPythonRuntime.ps1 `
-SourcePython (Get-Command python).Source `
-Destination (Join-Path $launcher "python") `
-Requirements (Resolve-Path importer/requirements-release-win.txt) `
-ImporterRoot (Resolve-Path importer) `
-ImporterEntry (Join-Path $launcher "tools/openbfme_import.py") `
-BundleRoot $launcher
./tools/release/Test-LauncherHeadless.ps1 -Launcher (Join-Path $launcher "OpenBFME.Launcher.exe")

- name: Package, scan, and attest
env:
OPENBFME_RELEASE_SIGNING_KEY: ${{ secrets.OPENBFME_RELEASE_SIGNING_KEY }}
shell: powershell
run: |
$version = "${{ steps.identity.outputs.version }}"
$dist = Join-Path $env:RUNNER_TEMP "openbfme-release"
$gameZip = Join-Path $dist "OpenBFME-$version-windows-x64.zip"
$launcherZip = Join-Path $dist "OpenBFME-Launcher-$version-windows-x64.zip"
Compress-Archive -Path (Join-Path $dist "game\*") -DestinationPath $gameZip -CompressionLevel Optimal
Compress-Archive -Path (Join-Path $dist "launcher\*") -DestinationPath $launcherZip -CompressionLevel Optimal
./tools/release/Test-ReleaseArtifact.ps1 -Path $gameZip
./tools/release/Test-ReleaseArtifact.ps1 -Path $launcherZip
./tools/release/New-ReleaseManifest.ps1 -ReleaseRoot $dist -Version $version -Commit "${{ github.sha }}" -Channel "${{ steps.identity.outputs.channel }}" -Output (Join-Path $dist "release-manifest.json")
./tools/release/Sign-ReleaseManifest.ps1 -Manifest (Join-Path $dist "release-manifest.json") -Output (Join-Path $dist "release-manifest.json.sig")
Get-ChildItem $dist -Filter "*.zip" | ForEach-Object {
"$(($_ | Get-FileHash -Algorithm SHA256).Hash.ToLowerInvariant()) $($_.Name)"
} | Set-Content (Join-Path $dist "SHA256SUMS.txt") -Encoding ascii

- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: openbfme-${{ steps.identity.outputs.version }}-windows
path: |
${{ runner.temp }}/openbfme-release/*.zip
${{ runner.temp }}/openbfme-release/release-manifest.json
${{ runner.temp }}/openbfme-release/release-manifest.json.sig
${{ runner.temp }}/openbfme-release/SHA256SUMS.txt
if-no-files-found: error
retention-days: 14

- uses: actions/attest-build-provenance@43d14bc2b83dec42d39ecae14e916627a18bb661 # v3
with:
subject-path: |
${{ runner.temp }}/openbfme-release/*.zip
${{ runner.temp }}/openbfme-release/release-manifest.json
${{ runner.temp }}/openbfme-release/release-manifest.json.sig

windows-vm-acceptance:
name: clean Windows BFME2 VM acceptance
if: github.event_name == 'push' || inputs.run_acceptance
needs: build
runs-on: [self-hosted, windows, x64, openbfme-release-vm]
timeout-minutes: 120
permissions:
contents: read
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.0.2
with:
fetch-depth: 1
ref: ${{ github.sha }}
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12.10"
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openbfme-${{ needs.build.outputs.version }}-windows
path: release
- name: Run packaged launcher twice against BFME II
shell: powershell
run: |
if ([string]::IsNullOrWhiteSpace($env:BFME2_RETAIL_PATH)) {
throw "The Windows VM runner does not define BFME2_RETAIL_PATH."
}
./tools/release/Invoke-WindowsVmAcceptance.ps1 `
-ReleaseDirectory release `
-RetailPath $env:BFME2_RETAIL_PATH `
-RepositoryRoot $PWD `
-ExpectedCommit "${{ github.sha }}" `
-Receipt (Join-Path $env:RUNNER_TEMP "openbfme-vm-acceptance.json")
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: openbfme-${{ needs.build.outputs.version }}-vm-acceptance
path: ${{ runner.temp }}/openbfme-vm-acceptance.json
if-no-files-found: error
retention-days: 30

publish:
name: publish immutable GitHub release
if: github.event_name == 'push'
needs: [build, windows-vm-acceptance]
runs-on: windows-latest
environment: production-release
permissions:
contents: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: openbfme-${{ needs.build.outputs.version }}-windows
path: release
- name: Publish tag artifacts
env:
GH_TOKEN: ${{ github.token }}
shell: powershell
run: |
$version = "${{ needs.build.outputs.version }}"
$dist = Join-Path $PWD "release"
$args = @("release", "create", "${{ github.ref_name }}",
(Join-Path $dist "OpenBFME-$version-windows-x64.zip"),
(Join-Path $dist "OpenBFME-Launcher-$version-windows-x64.zip"),
(Join-Path $dist "release-manifest.json"),
(Join-Path $dist "release-manifest.json.sig"),
(Join-Path $dist "SHA256SUMS.txt"),
"--repo", "${{ github.repository }}",
"--verify-tag",
"--generate-notes",
"--title", "OpenBFME $version")
if ($version.Contains("-")) { $args += "--prerelease" }
gh @args
Loading
Loading