Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0d664fa
ci(web): single-source toolchain versions, patch-level drift guard, 4…
claude Jul 25, 2026
761e70f
feat(test): seed-deterministic generation, regression foundation, vis…
claude Jul 25, 2026
48c6e45
fix(test): narrow PerfectAgent sample to stop the suite hanging in CI
claude Jul 25, 2026
4fd8479
feat(test): in-app test bridge, headless scene tests, Playwright func…
claude Jul 25, 2026
9c2cf69
refactor: extract MazeSession from GameState, cover the newly testabl…
claude Jul 25, 2026
261e842
fix: bound end-point selection so a 1x1x1 maze cannot hang generation
claude Jul 25, 2026
49b07b4
perf(agent): make PerfectAgent linear instead of exponential
claude Jul 25, 2026
c6d165f
ci: auto-detect the test bridge on deploy, add editor mirror workflow
claude Jul 25, 2026
0b3a5c9
fix(test): run one fixture instance per test case, fixing a paralleli…
claude Jul 25, 2026
68518c5
Merge branch 'claude/split-2-determinism-3a6h5m' into claude/split-3-…
claude Jul 25, 2026
8dabeec
Merge branch 'claude/split-1-web-toolchain-3a6h5m' into claude/split-…
claude Jul 25, 2026
9064d02
Merge branch 'claude/split-3-testability-3a6h5m' into claude/godot-wa…
claude Jul 25, 2026
1000510
docs(test): record the fixture-lifecycle requirement and how it was d…
claude Jul 25, 2026
4dad361
Merge branch 'claude/split-3-testability-3a6h5m' into claude/godot-wa…
claude Jul 25, 2026
9f1e935
docs: refresh unit-test count for the lifecycle guard test
claude Jul 25, 2026
1912c4f
Merge branch 'claude/split-3-testability-3a6h5m' into claude/godot-wa…
claude Jul 25, 2026
13e49ae
chore: gitignore node_modules and Playwright run output
claude Jul 26, 2026
0e25490
Merge branch 'claude/split-1-web-toolchain-3a6h5m' into claude/split-…
claude Jul 26, 2026
1266a2c
Merge branch 'claude/split-2-determinism-3a6h5m' into claude/split-3-…
claude Jul 26, 2026
244695d
Merge branch 'claude/split-3-testability-3a6h5m' into claude/godot-wa…
claude Jul 26, 2026
5646ef4
Merge remote-tracking branch 'origin/main' into claude/split-1-web-to…
claude Jul 26, 2026
e4aaeec
Merge branch 'claude/split-1-web-toolchain-3a6h5m' into claude/split-…
claude Jul 26, 2026
9083e08
Merge branch 'claude/split-2-determinism-3a6h5m' into claude/split-3-…
claude Jul 26, 2026
f634ed7
Merge branch 'claude/split-3-testability-3a6h5m' into claude/godot-wa…
claude Jul 26, 2026
2df9248
ci: pin the actions used by this stack's new jobs
claude Jul 26, 2026
7550f49
feat: About screen reporting the build hash, and verify it end to end
claude Jul 26, 2026
195de57
fix(ci): self-derive the build stamp, and make the smoke test's boot …
claude Jul 26, 2026
b3995ab
ci: let an automated caller issue a preview deploy via workflow_dispatch
claude Jul 26, 2026
2bc91d9
test(web): enable the functional browser suite; gate visual on baseli…
claude Jul 26, 2026
10d49bf
refactor(ci): move step bodies out of YAML into script files
claude Jul 26, 2026
fdff4cb
Merge main (squashes of #8-#10) into claude/godot-wasm-hosting-eval-3…
rtkelly13 Jul 27, 2026
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
105 changes: 92 additions & 13 deletions .github/actions/export-web/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ name: "Export Godot C# project to Web (WASM)"
description: >
Builds an experimental C#/.NET WebAssembly export using ComplexRobot's patched
Windows editor build. Must run on a windows-latest runner. Produces build/web.
Step logic lives in ./scripts/*.ps1.

inputs:
# These three intentionally default to EMPTY. When empty they are resolved from
Expand All @@ -26,6 +25,30 @@ inputs:
description: "Token for downloading the fork release via gh"
required: true

# Build stamp. Compiled into the binary so the About screen and the test bridge can report
# which commit produced the running build. Blank is legitimate — it produces a build that
# honestly reports itself as untraceable rather than one carrying a misleading hash.
build_commit:
description: "Full commit SHA this build is made from (blank = unstamped)"
required: false
default: ""
build_branch:
description: "Branch or tag name, without any refs/heads/ prefix"
required: false
default: ""
build_repository:
description: "owner/repo, used to build commit and run links"
required: false
default: ""
build_run_id:
description: "GitHub Actions run id that produced this build"
required: false
default: ""
build_run_attempt:
description: "Run attempt — a re-run of the same commit is a different artefact"
required: false
default: ""

outputs:
web_dir:
description: "Path to the exported web output"
Expand All @@ -36,6 +59,12 @@ outputs:
template_version:
description: "The export-template version actually used for this export"
value: ${{ steps.toolchain.outputs.template_version }}
build_commit:
description: >
The commit this export was actually stamped with. Prefer this over github.sha when
asserting what a deploy serves: for issue_comment-triggered runs github.sha is the
default branch, not the PR head that was built.
value: ${{ steps.stamp.outputs.commit }}

runs:
using: composite
Expand All @@ -47,15 +76,29 @@ runs:
IN_FORK_REPO: ${{ inputs.fork_repo }}
IN_FORK_TAG: ${{ inputs.godot_fork_tag }}
IN_TEMPLATE_VERSION: ${{ inputs.template_version }}
run: '& "${{ github.action_path }}/scripts/resolve-toolchain.ps1"'
run: '& "$env:GITHUB_ACTION_PATH/scripts/resolve-toolchain.ps1"'

- name: Resolve build stamp
id: stamp
shell: pwsh
env:
IN_COMMIT: ${{ inputs.build_commit }}
IN_BRANCH: ${{ inputs.build_branch }}
IN_REPOSITORY: ${{ inputs.build_repository }}
IN_RUN_ID: ${{ inputs.build_run_id }}
IN_RUN_ATTEMPT: ${{ inputs.build_run_attempt }}
CTX_REPOSITORY: ${{ github.repository }}
CTX_RUN_ID: ${{ github.run_id }}
CTX_RUN_ATTEMPT: ${{ github.run_attempt }}
run: '& "$env:GITHUB_ACTION_PATH/scripts/resolve-build-stamp.ps1"'

- name: Guard against version drift
shell: pwsh
env:
TEMPLATE_VERSION: ${{ steps.toolchain.outputs.template_version }}
FORK_TAG: ${{ steps.toolchain.outputs.fork_tag }}
ASSET: ${{ steps.toolchain.outputs.asset }}
run: '& "${{ github.action_path }}/scripts/check-version-drift.ps1"'
run: '& "$env:GITHUB_ACTION_PATH/scripts/check-version-drift.ps1"'

- name: Set up .NET 9 SDK
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
Expand All @@ -81,13 +124,16 @@ runs:
FORK_REPO: ${{ steps.toolchain.outputs.fork_repo }}
FORK_TAG: ${{ steps.toolchain.outputs.fork_tag }}
ASSET: ${{ steps.toolchain.outputs.asset }}
run: '& "${{ github.action_path }}/scripts/download-editor.ps1"'
run: |
New-Item -ItemType Directory -Force -Path "${env:RUNNER_TEMP}/godot-zip" | Out-Null
Write-Host "Downloading $env:ASSET from $env:FORK_REPO@$env:FORK_TAG"
gh release download "$env:FORK_TAG" --repo "$env:FORK_REPO" --pattern "$env:ASSET" --dir "${env:RUNNER_TEMP}/godot-zip"

- name: Verify editor checksum (supply-chain guard)
shell: pwsh
env:
ASSET: ${{ steps.toolchain.outputs.asset }}
run: '& "${{ github.action_path }}/scripts/verify-editor-checksum.ps1"'
run: '& "$env:GITHUB_ACTION_PATH/scripts/verify-editor-checksum.ps1"'

- name: Extract editor
shell: pwsh
Expand All @@ -98,34 +144,67 @@ runs:
- name: Locate editor executable and bundle contents
id: locate
shell: pwsh
run: '& "${{ github.action_path }}/scripts/locate-editor.ps1"'
run: '& "$env:GITHUB_ACTION_PATH/scripts/locate-editor.ps1"'

- name: Install web export templates (self-contained mode)
shell: pwsh
env:
BUNDLE_DIR: ${{ steps.locate.outputs.bundle_dir }}
TEMPLATE_VERSION: ${{ steps.toolchain.outputs.template_version }}
run: '& "${{ github.action_path }}/scripts/install-templates.ps1"'
BUNDLE_DIR: ${{ steps.locate.outputs.bundle_dir }}
run: '& "$env:GITHUB_ACTION_PATH/scripts/install-templates.ps1"'

- name: Register bundled NuGet source (if present)
shell: pwsh
run: '& "${{ github.action_path }}/scripts/register-nuget-source.ps1"'

run: '& "$env:GITHUB_ACTION_PATH/scripts/register-nuget-source.ps1"'
# Build-stamp variables are set as ENVIRONMENT variables on the build and export steps
# rather than passed with -p:. The export step re-enters MSBuild through the Godot editor,
# whose command line we do not control; MSBuild reads environment variables as properties,
# so this is the one channel that reaches both. Consumed by the GenerateBuildStamp target
# in ProceduralGeneration3DMazes.csproj. See docs/BUILD_VERIFICATION.md.
- name: Restore / build (net9.0)
shell: pwsh
env:
BuildCommit: ${{ steps.stamp.outputs.commit }}
BuildBranch: ${{ steps.stamp.outputs.branch }}
BuildRepository: ${{ steps.stamp.outputs.repository }}
BuildRunId: ${{ steps.stamp.outputs.run_id }}
BuildRunAttempt: ${{ steps.stamp.outputs.run_attempt }}
BuildTimestampUtc: ${{ steps.stamp.outputs.timestamp }}
run: dotnet build ProceduralGeneration3DMazes.csproj -c ExportRelease /p:GodotTargetPlatform=web

- name: Import resources (headless)
shell: pwsh
continue-on-error: true
run: |
& "${{ steps.locate.outputs.godot_exe }}" --headless --path . --import 2>&1 | Tee-Object -FilePath import.log
env:
GODOT_EXE: ${{ steps.locate.outputs.godot_exe }}
run: '& "$env:GITHUB_ACTION_PATH/scripts/import-project.ps1"'

- name: Export Web (headless CLI)
shell: pwsh
env:
GODOT_EXE: ${{ steps.locate.outputs.godot_exe }}
run: '& "${{ github.action_path }}/scripts/export-web.ps1"'
BuildCommit: ${{ steps.stamp.outputs.commit }}
BuildBranch: ${{ steps.stamp.outputs.branch }}
BuildRepository: ${{ steps.stamp.outputs.repository }}
BuildRunId: ${{ steps.stamp.outputs.run_id }}
BuildRunAttempt: ${{ steps.stamp.outputs.run_attempt }}
BuildTimestampUtc: ${{ steps.stamp.outputs.timestamp }}
run: '& "$env:GITHUB_ACTION_PATH/scripts/export-web.ps1"'
# A sibling of index.html, so the served build can be identified with a plain curl without
# booting a 96 MB WASM payload in a browser. The About screen reads its values from the
# compiled-in stamp, not from this file — they come from the same inputs, so a mismatch
# between the two would itself be a signal that something repackaged the deploy.
- name: Write build-info.json
shell: pwsh
env:
BUILD_COMMIT: ${{ steps.stamp.outputs.commit }}
BUILD_BRANCH: ${{ steps.stamp.outputs.branch }}
BUILD_REPOSITORY: ${{ steps.stamp.outputs.repository }}
BUILD_RUN_ID: ${{ steps.stamp.outputs.run_id }}
BUILD_RUN_ATTEMPT: ${{ steps.stamp.outputs.run_attempt }}
BUILD_TIMESTAMP: ${{ steps.stamp.outputs.timestamp }}
FORK_TAG: ${{ steps.toolchain.outputs.fork_tag }}
run: '& "$env:GITHUB_ACTION_PATH/scripts/write-build-info.ps1"'

- name: Upload web export artifact
if: always()
Expand Down
58 changes: 39 additions & 19 deletions .github/actions/export-web/scripts/check-version-drift.ps1
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
# Fail fast (before the 165 MB editor download) if the Godot.NET.Sdk version (csproj),
# the patched-editor tag, and the export-template folder disagree at PATCH level, or if
# the target editor asset has no pinned checksum.
# Fail fast if the Godot SDK, export templates and editor tag disagree.
#
# The three values spell the same version differently; normalise to bare x.y[.z]:
# csproj "4.7.1" -> 4.7.1
# template "4.7.1.stable.mono" -> 4.7.1
# fork tag "4.7.1-stable" -> 4.7.1
# Godot names x.y releases without a patch component (template "4.7.stable.mono"),
# so a csproj SDK of "4.7.0" normalises to "4.7".
# Compares at PATCH level -- major.minor alone lets a 4.7.0-vs-4.7.1 mismatch through, which is the most common kind of bump.
#
# Extracted from action.yml rather than inlined there: a 40-line script inside a YAML block
# scalar cannot be linted, cannot be run outside CI, and silently breaks on constructs the
# block scalar swallows -- a PowerShell here-string (@"..."@) needs its terminator at column 0,
# which ends the YAML block. See docs/WEB_EXPORT.md.
#
# Env in: TEMPLATE_VERSION, FORK_TAG, ASSET
# Inputs (environment):
# TEMPLATE_VERSION, FORK_TAG, ASSET

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Actions' pwsh wrapper sets these; set them here too so the script behaves identically when
# run by hand, and so a failing native command (git/gh/dotnet/godot) is not silently ignored.
$PSNativeCommandUseErrorActionPreference = $true

# The Godot.NET.Sdk version (csproj), the patched-editor tag, and the
# export-template folder must agree at PATCH level — comparing only
# major.minor lets a 4.7.0-vs-4.7.1 mismatch through, which is exactly the
# class of bump that happens most often. Catch it in seconds, before we
# download a 165 MB editor.
#
# Normalise the three spellings of the same version to a bare x.y[.z]:
# csproj "4.7.1" -> 4.7.1 ("4.7.0" -> 4.7, see below)
# template "4.7.1.stable.mono" -> 4.7.1
# fork tag "4.7.1-stable" -> 4.7.1
# Godot names x.y releases without a patch component, so an SDK version of
# "4.7.0" corresponds to template "4.7.stable.mono": strip a trailing ".0".
function Get-BareVersion([string]$s) {
return ($s -replace '[-.](stable|beta|rc|dev)[-.0-9]*.*$', '')
}
Expand All @@ -23,17 +38,22 @@ $sdkVersion = $Matches[1] -replace '\.0$', ''
$tplVersion = Get-BareVersion $env:TEMPLATE_VERSION
$tagVersion = Get-BareVersion $env:FORK_TAG

if (($sdkVersion -ne $tplVersion) -or ($sdkVersion -ne $tagVersion)) {
throw @"
Version drift - these must all describe the same Godot version:
Godot.NET.Sdk (csproj) -> $sdkVersion
template_version -> $tplVersion (raw: $env:TEMPLATE_VERSION)
godot_fork_tag -> $tagVersion (raw: $env:FORK_TAG)
Align them in ProceduralGeneration3DMazes.csproj and .github/web-toolchain.env.
See docs/WEB_EXPORT.md -> 'Updating the pinned editor'.
"@
$mismatch = ($sdkVersion -ne $tplVersion) -or ($sdkVersion -ne $tagVersion)
if ($mismatch) {
# Built as an array rather than a here-string: a PowerShell here-string needs
# its closing "@ at column 0, which would terminate this YAML block scalar.
throw (@(
"Version drift - these must all describe the same Godot version:",
" Godot.NET.Sdk (csproj) -> $sdkVersion",
" template_version -> $tplVersion (raw: $env:TEMPLATE_VERSION)",
" godot_fork_tag -> $tagVersion (raw: $env:FORK_TAG)",
"Align them in ProceduralGeneration3DMazes.csproj and .github/web-toolchain.env.",
"See docs/WEB_EXPORT.md -> 'Updating the pinned editor'."
) -join "`n")
}

# Fail fast if the editor we're about to fetch has no pinned checksum, rather
# than after spending a runner-minute downloading it.
$pinned = Get-Content .github/editor-checksums.txt |
Where-Object { $_ -notmatch '^\s*#' -and $_ -match [regex]::Escape($env:ASSET) }
if (-not $pinned) {
Expand Down
9 changes: 0 additions & 9 deletions .github/actions/export-web/scripts/download-editor.ps1

This file was deleted.

17 changes: 15 additions & 2 deletions .github/actions/export-web/scripts/export-web.ps1
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
# Run the headless web export and fail loudly if no output was produced.
# Env in: GODOT_EXE
# Run the headless web export and confirm it produced output.
#
# The export can exit 0 while producing nothing, so index.html is checked explicitly.
#
# Extracted from action.yml rather than inlined there: a 40-line script inside a YAML block
# scalar cannot be linted, cannot be run outside CI, and silently breaks on constructs the
# block scalar swallows -- a PowerShell here-string (@"..."@) needs its terminator at column 0,
# which ends the YAML block. See docs/WEB_EXPORT.md.
#
# Inputs (environment):
# GODOT_EXE

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Actions' pwsh wrapper sets these; set them here too so the script behaves identically when
# run by hand, and so a failing native command (git/gh/dotnet/godot) is not silently ignored.
$PSNativeCommandUseErrorActionPreference = $true

New-Item -ItemType Directory -Force -Path "build/web" | Out-Null
& "$env:GODOT_EXE" --headless --path . --export-release "Web" "build/web/index.html" 2>&1 | Tee-Object -FilePath export.log
Expand Down
19 changes: 19 additions & 0 deletions .github/actions/export-web/scripts/import-project.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Import the project's resources headlessly before exporting.
#
# Godot must import once or the export produces an incomplete PCK.
#
# Extracted from action.yml rather than inlined there: a 40-line script inside a YAML block
# scalar cannot be linted, cannot be run outside CI, and silently breaks on constructs the
# block scalar swallows -- a PowerShell here-string (@"..."@) needs its terminator at column 0,
# which ends the YAML block. See docs/WEB_EXPORT.md.
#
# Inputs (environment):
# GODOT_EXE

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Actions' pwsh wrapper sets these; set them here too so the script behaves identically when
# run by hand, and so a failing native command (git/gh/dotnet/godot) is not silently ignored.
$PSNativeCommandUseErrorActionPreference = $true

& "$env:GODOT_EXE" --headless --path . --import 2>&1 | Tee-Object -FilePath import.log
21 changes: 16 additions & 5 deletions .github/actions/export-web/scripts/install-templates.ps1
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
# Install the bundled web export templates in self-contained mode (next to the
# editor, via a "._sc_" marker — no AppData).
# Env in: BUNDLE_DIR, TEMPLATE_VERSION
# Install the web export templates into the editor bundle (self-contained mode).
#
# A `._sc_` marker makes the editor look for templates beside itself instead of in the user profile.
#
# Extracted from action.yml rather than inlined there: a 40-line script inside a YAML block
# scalar cannot be linted, cannot be run outside CI, and silently breaks on constructs the
# block scalar swallows -- a PowerShell here-string (@"..."@) needs its terminator at column 0,
# which ends the YAML block. See docs/WEB_EXPORT.md.
#
# Inputs (environment):
# TEMPLATE_VERSION, BUNDLE_DIR

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Actions' pwsh wrapper sets these; set them here too so the script behaves identically when
# run by hand, and so a failing native command (git/gh/dotnet/godot) is not silently ignored.
$PSNativeCommandUseErrorActionPreference = $true

$bundle = $env:BUNDLE_DIR
$bundle = "$env:BUNDLE_DIR"
New-Item -ItemType File -Force -Path (Join-Path $bundle "._sc_") | Out-Null
$tplDir = Join-Path $bundle "editor_data/export_templates/${env:TEMPLATE_VERSION}"
New-Item -ItemType Directory -Force -Path $tplDir | Out-Null

$templates = Get-ChildItem -Path "$env:RUNNER_TEMP/godot" -Recurse -Include "web_release.zip","web_debug.zip"
if (-not $templates) {
Get-ChildItem -Path "$env:RUNNER_TEMP/godot" -Recurse -File | Select-Object -ExpandProperty FullName
Expand Down
15 changes: 12 additions & 3 deletions .github/actions/export-web/scripts/locate-editor.ps1
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
# Find the Godot editor executable inside the extracted fork bundle (prefer the
# console build) and report it plus its directory for later steps.
# Outputs: godot_exe, bundle_dir
# Locate the editor executable inside the extracted fork bundle.
#
# The fork's zip layout is not guaranteed stable, so the exe is discovered rather than assumed.
#
# Extracted from action.yml rather than inlined there: a 40-line script inside a YAML block
# scalar cannot be linted, cannot be run outside CI, and silently breaks on constructs the
# block scalar swallows -- a PowerShell here-string (@"..."@) needs its terminator at column 0,
# which ends the YAML block. See docs/WEB_EXPORT.md.

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Actions' pwsh wrapper sets these; set them here too so the script behaves identically when
# run by hand, and so a failing native command (git/gh/dotnet/godot) is not silently ignored.
$PSNativeCommandUseErrorActionPreference = $true

$root = "$env:RUNNER_TEMP/godot"
$exe = Get-ChildItem -Path $root -Recurse -Filter "*.exe" |
Expand Down
13 changes: 12 additions & 1 deletion .github/actions/export-web/scripts/register-nuget-source.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
# Register the fork's bundled local NuGet source, if the bundle ships one.
# Register the fork's bundled NuGet source, when it ships one.
#
# The patched editor may carry Godot NuGet packages that are not on nuget.org.
#
# Extracted from action.yml rather than inlined there: a 40-line script inside a YAML block
# scalar cannot be linted, cannot be run outside CI, and silently breaks on constructs the
# block scalar swallows -- a PowerShell here-string (@"..."@) needs its terminator at column 0,
# which ends the YAML block. See docs/WEB_EXPORT.md.

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Actions' pwsh wrapper sets these; set them here too so the script behaves identically when
# run by hand, and so a failing native command (git/gh/dotnet/godot) is not silently ignored.
$PSNativeCommandUseErrorActionPreference = $true

$nuget = Get-ChildItem -Path "$env:RUNNER_TEMP/godot" -Recurse -Directory -Filter "nuget" | Select-Object -First 1
if ($nuget) {
Expand Down
Loading
Loading