Cross-Platform Build #47
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Cross-Platform Build | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| reason: | |
| description: 'Reason for running this build' | |
| required: false | |
| default: 'Manual verification' | |
| issue_comment: | |
| types: [created] | |
| milestone: | |
| types: [closed] | |
| schedule: | |
| - cron: '0 4 * * 1' # Every Monday 4:00 UTC | |
| jobs: | |
| # Post "building..." comment immediately | |
| notify-start: | |
| if: >- | |
| github.event_name != 'issue_comment' || | |
| (github.event.comment.body == '/build' && | |
| github.event.issue.state == 'open') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| outputs: | |
| comment-id: ${{ steps.comment.outputs.comment-id }} | |
| steps: | |
| - name: React to comment | |
| if: github.event_name == 'issue_comment' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| await github.rest.reactions.createForIssueComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: context.payload.comment.id, | |
| content: 'rocket' | |
| }); | |
| - name: Post progress comment | |
| id: comment | |
| if: github.event_name == 'issue_comment' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const { data } = await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.issue.number, | |
| body: `⏳ **Cross-Platform Build started...**\n\n| Platform | Status |\n|----------|--------|\n| macOS | 🔄 building... |\n| Windows | 🔄 building... |\n\n[View live logs](${runUrl})`, | |
| }); | |
| core.setOutput('comment-id', data.id); | |
| build: | |
| needs: notify-start | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: macos-latest | |
| name: macOS | |
| - platform: windows-latest | |
| name: Windows | |
| name: ${{ matrix.name }} Build | |
| runs-on: ${{ matrix.platform }} | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| - uses: pnpm/action-setup@v4 | |
| with: | |
| version: 10 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - name: Add macOS universal target | |
| if: matrix.platform == 'macos-latest' | |
| run: rustup target add x86_64-apple-darwin | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: desktop/src-tauri -> target | |
| - name: Setup Emscripten | |
| uses: mymindstorm/setup-emsdk@v14 | |
| with: | |
| version: 5.0.0 | |
| - name: Setup ccache (non-Windows) | |
| if: matrix.platform != 'windows-latest' | |
| uses: hendrikmuhs/ccache-action@v1.2.20 | |
| with: | |
| key: ${{ matrix.platform }}-cross | |
| max-size: 1G | |
| - name: Install dependencies | |
| run: pnpm install | |
| # Native ECS release-safety tests (ES_VERIFY guards + PackedId packing). | |
| # Compiled WITHOUT -DES_DEBUG to mirror the shipped Release config (where | |
| # ES_ASSERT is stripped) and under ASAN, so the OOB write / index-alias | |
| # these guards prevent are caught deterministically. Header-only: needs | |
| # only a C++20 compiler, no CMake/emsdk. MSVC has different sanitizer flags. | |
| - name: Run ECS release-safety tests (ASAN, release-mirror) | |
| if: matrix.platform != 'windows-latest' | |
| shell: bash | |
| run: | | |
| c++ -std=c++20 -fsanitize=address -I src \ | |
| tests/ecs/test_registry_safety.cpp src/esengine/core/Log.cpp \ | |
| -o test_registry_safety | |
| ./test_registry_safety | |
| # Type-check & test SDK first (editor depends on it) | |
| - name: Type-check SDK | |
| run: pnpm --filter ./sdk exec tsc --noEmit | |
| - name: Run SDK tests | |
| run: pnpm --filter ./sdk exec vitest run | |
| - name: Build SDK | |
| run: pnpm --filter ./sdk build | |
| # Now editor (depends on SDK build output). The editor package is | |
| # @estella/editor at ./desktop — the old ./editor filter matched no | |
| # package, so these steps silently no-op'd and never ran in CI. | |
| - name: Type-check Editor | |
| run: pnpm --filter ./desktop run typecheck | |
| - name: Run Editor tests | |
| run: pnpm --filter ./desktop run test | |
| - name: Build Editor | |
| run: pnpm --filter ./desktop run build | |
| # Build WASM targets individually and verify outputs | |
| - name: Build Web target | |
| run: node build-tools/cli.js build -t web --no-sync | |
| - name: Verify Web output | |
| shell: bash | |
| run: | | |
| test -f build/wasm/web/esengine.js || (echo "FAIL: esengine.js not found" && exit 1) | |
| test -f build/wasm/web/esengine.wasm || (echo "FAIL: esengine.wasm not found" && exit 1) | |
| echo "Web build OK" | |
| - name: Build Playable target | |
| run: node build-tools/cli.js build -t playable --no-sync | |
| - name: Verify Playable output | |
| shell: bash | |
| run: | | |
| test -f build/wasm/playable/esengine.single.js || (echo "FAIL: esengine.single.js not found" && exit 1) | |
| echo "Playable build OK" | |
| - name: Build WeChat target | |
| run: node build-tools/cli.js build -t wechat --no-sync | |
| - name: Verify WeChat output | |
| shell: bash | |
| run: | | |
| test -f build/wasm/wechat/esengine.wxgame.js || (echo "FAIL: esengine.wxgame.js not found" && exit 1) | |
| test -f build/wasm/wechat/esengine.wxgame.wasm || (echo "FAIL: esengine.wxgame.wasm not found" && exit 1) | |
| echo "WeChat build OK" | |
| - name: Build Spine targets | |
| run: | | |
| node build-tools/cli.js build -t spine --no-sync | |
| node build-tools/cli.js build -t spine38 --no-sync | |
| node build-tools/cli.js build -t spine41 --no-sync | |
| - name: Build Physics targets | |
| run: | | |
| node build-tools/cli.js build -t physics --no-sync | |
| node build-tools/cli.js build -t physics-playable --no-sync | |
| - name: Build SDK | |
| run: node build-tools/cli.js sdk --no-sync | |
| # Toolchain packaging | |
| - name: Package toolchain | |
| run: node build-tools/cli.js toolchain | |
| # Rust tests (Tauri app) | |
| - name: Fix broken symlinks in toolchain | |
| shell: bash | |
| run: find desktop/src-tauri/toolchain -xtype l -delete 2>/dev/null || true | |
| - name: Tauri build check | |
| shell: bash | |
| run: cd desktop/src-tauri && cargo check | |
| - name: Tauri unit tests | |
| shell: bash | |
| run: cd desktop/src-tauri && cargo test | |
| # Artifact size report | |
| - name: Collect artifact sizes | |
| id: sizes | |
| shell: bash | |
| run: | | |
| report="" | |
| if [ -f sdk/dist/index.bundled.js ]; then | |
| size=$(du -sh sdk/dist/index.bundled.js | cut -f1) | |
| report+="| SDK bundle | $size |"$'\n' | |
| fi | |
| for f in desktop/public/*.wasm; do | |
| if [ -f "$f" ]; then | |
| name=$(basename "$f") | |
| size=$(du -sh "$f" | cut -f1) | |
| report+="| WASM ($name) | $size |"$'\n' | |
| fi | |
| done | |
| if [ -d desktop/src-tauri/toolchain ]; then | |
| total=$(du -sh desktop/src-tauri/toolchain | cut -f1) | |
| report+="| Toolchain (total) | $total |"$'\n' | |
| if [ -d desktop/src-tauri/toolchain/engine-src ]; then | |
| size=$(du -sh desktop/src-tauri/toolchain/engine-src | cut -f1) | |
| report+="| - engine-src | $size |"$'\n' | |
| fi | |
| if [ -d desktop/src-tauri/toolchain/cmake ]; then | |
| size=$(du -sh desktop/src-tauri/toolchain/cmake | cut -f1) | |
| report+="| - cmake | $size |"$'\n' | |
| fi | |
| fi | |
| if [ -f desktop/public/esbuild.wasm ]; then | |
| size=$(du -sh desktop/public/esbuild.wasm | cut -f1) | |
| report+="| esbuild.wasm | $size |"$'\n' | |
| fi | |
| echo "## Artifact Size Report (${{ matrix.name }})" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Artifact | Size |" >> $GITHUB_STEP_SUMMARY | |
| echo "|----------|------|" >> $GITHUB_STEP_SUMMARY | |
| echo "$report" >> $GITHUB_STEP_SUMMARY | |
| # Save for final report | |
| EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) | |
| echo "report<<$EOF" >> $GITHUB_OUTPUT | |
| echo "$report" >> $GITHUB_OUTPUT | |
| echo "$EOF" >> $GITHUB_OUTPUT | |
| # Update the progress comment with final results | |
| report: | |
| needs: [notify-start, build] | |
| if: always() && github.event_name == 'issue_comment' && needs.notify-start.outputs.comment-id | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| steps: | |
| - name: Update progress comment with results | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const jobs = await github.rest.actions.listJobsForWorkflowRun({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: context.runId, | |
| }); | |
| const buildJobs = jobs.data.jobs.filter(j => | |
| j.name.includes('Build') && !j.name.includes('notify') && !j.name.includes('report') | |
| ); | |
| const allPassed = buildJobs.every(j => j.conclusion === 'success'); | |
| const header = allPassed | |
| ? '✅ **Cross-Platform Build passed!**' | |
| : '❌ **Cross-Platform Build failed**'; | |
| const lines = buildJobs.map(j => { | |
| const icon = j.conclusion === 'success' ? '✅' : '❌'; | |
| const dur = j.completed_at && j.started_at | |
| ? Math.round((new Date(j.completed_at) - new Date(j.started_at)) / 1000) | |
| : 0; | |
| const mins = Math.floor(dur / 60); | |
| const secs = dur % 60; | |
| const time = mins > 0 ? `${mins}m${secs}s` : `${secs}s`; | |
| return `| ${j.name} | ${icon} ${j.conclusion || 'unknown'} | ${time} |`; | |
| }); | |
| const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const body = [ | |
| header, | |
| '', | |
| '| Platform | Status | Duration |', | |
| '|----------|--------|----------|', | |
| ...lines, | |
| '', | |
| `[View full run](${runUrl})`, | |
| ].join('\n'); | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: ${{ needs.notify-start.outputs.comment-id }}, | |
| body, | |
| }); |