Native smoke #26
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: Native smoke | |
| # Do games packaged from the runtime templates actually start? | |
| # | |
| # `verify-template` reads what an archive carries, which is a weaker claim than it | |
| # sounds: v0.36.0's Android template carried a host that ran, and every game made | |
| # from it still opened on a black screen. This packages every example around the | |
| # template the same way the editor's Package dialog does, installs each on a | |
| # simulator, and asks the two questions a completeness check cannot — did the boot | |
| # record reach "ready", and is the frame more than one flat color. | |
| # | |
| # Booting a device is minutes and each app is seconds, so one device runs many | |
| # examples and the SET is sharded across jobs. A job per example would spend its | |
| # whole budget starting emulators. | |
| # | |
| # A simulator is not a phone. Real GPU drivers, real compressed textures, thermal | |
| # throttling and memory pressure are all outside what this can see, and the two | |
| # Android rendering bugs this project has actually shipped were of that kind. What | |
| # this catches is the layer under them: templates that cannot produce a launching | |
| # app at all, on any device. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| template-tag: | |
| description: "Release tag to take the templates from (blank = this run's own artifacts, or the latest release when run by hand)." | |
| required: false | |
| type: string | |
| examples: | |
| description: 'Comma-separated example names, or blank for every one.' | |
| required: false | |
| default: '' | |
| type: string | |
| template-source: | |
| description: 'release = the newest published templates; head = build this branch first (~40 min).' | |
| required: false | |
| default: release | |
| type: choice | |
| options: [release, head] | |
| workflow_call: | |
| inputs: | |
| template-tag: | |
| required: false | |
| default: '' | |
| type: string | |
| examples: | |
| required: false | |
| default: '' | |
| type: string | |
| # Blank from a caller: the templates are that run's own artifacts, which is | |
| # what the release pipeline hands over. Only a hand run has to choose. | |
| template-source: | |
| required: false | |
| default: '' | |
| type: string | |
| permissions: | |
| contents: read | |
| env: | |
| # Pinned, because "whatever the runner listed first" is a device nobody chose: | |
| # a runner image update would silently move the test to another phone. A name | |
| # that stops existing fails loudly, listing what the image does have. | |
| IOS_DEVICE: iPhone 17 Pro | |
| # The action's default profile is 640x320 — enough to prove a launch, useless | |
| # for looking at. | |
| ANDROID_PROFILE: pixel_6 | |
| ANDROID_API: '34' | |
| jobs: | |
| # Only for a hand run that asks for `head`. A caller (the release pipeline) has | |
| # already built the templates this run will smoke, and a tag names published | |
| # ones — in both cases there is nothing to build here and this is skipped. | |
| # | |
| # The build itself is one `cli.js native` call per ABI; what repeats around it is | |
| # runner setup, which differs per target anyway. The same pair of calls appears | |
| # in release-desktop's template job and in android-canary-apk, each with the | |
| # setup its own runner needs. | |
| templates: | |
| name: Build runtime template (${{ matrix.name }}) | |
| if: inputs.template-source == 'head' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: android | |
| os: ubuntu-latest | |
| - name: ios | |
| os: macos-latest | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 120 | |
| steps: | |
| # With submodules: the host links box2d, spine and glm from third_party. | |
| - uses: actions/checkout@v7 | |
| with: | |
| submodules: recursive | |
| - uses: ./.github/actions/setup | |
| # The SDK bundle is compiled INTO the host binary, which is why a template | |
| # is matched against the editor version that ships with it. | |
| - name: Build SDK | |
| run: pnpm --filter ./sdk build | |
| - name: Read the native dependency pins | |
| id: pins | |
| shell: bash | |
| run: node build-tools/cli.js native --deps-cache-key ${{ matrix.name }} >> "$GITHUB_OUTPUT" | |
| - name: Cache the pinned checkouts + Dawn builds | |
| uses: actions/cache@v4 | |
| with: | |
| path: build/native-deps | |
| key: ${{ steps.pins.outputs.key }} | |
| - name: Set up CMake + Ninja | |
| if: startsWith(matrix.os, 'macos') | |
| run: brew install cmake ninja | |
| - name: Set up the Android NDK | |
| if: matrix.name == 'android' | |
| uses: nttld/setup-ndk@v1 | |
| id: ndk | |
| with: | |
| ndk-version: r28 | |
| - name: Fetch Dawn + QuickJS at their pinned commits | |
| run: node build-tools/cli.js native --fetch-deps | |
| - name: Build the iOS template (device + simulator) | |
| if: matrix.name == 'ios' | |
| run: | | |
| node build-tools/cli.js native --target ios --simulator --no-template | |
| node build-tools/cli.js native --target ios --template-out artifacts | |
| # Both architectures into ONE template: the emitter refuses an incomplete | |
| # one, and an x86_64-only build fails at the emit rather than the compile. | |
| - name: Build the Android template (arm64 + x86_64) | |
| if: matrix.name == 'android' | |
| env: | |
| ANDROID_NDK_HOME: ${{ steps.ndk.outputs.ndk-path }} | |
| run: | | |
| node build-tools/cli.js native --abi arm64-v8a | |
| node build-tools/cli.js native --abi x86_64 --template-out artifacts | |
| # The name the shards below download when no tag names a release. | |
| - name: Upload template artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: native-template-${{ matrix.name }} | |
| path: artifacts/*.zip | |
| android: | |
| name: Android ${{ matrix.shard }}/3 | |
| needs: templates | |
| # `templates` is skipped unless this run builds them, and a skipped dependency | |
| # would otherwise skip this too. | |
| if: always() && needs.templates.result != 'failure' && needs.templates.result != 'cancelled' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [0, 1, 2] | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: ./.github/actions/setup | |
| # Called from the release pipeline the templates are this run's artifacts; | |
| # run by hand there are none, so a blank tag means the newest release rather | |
| # than a download that fails saying nothing was uploaded. | |
| - name: Decide where the template comes from | |
| id: src | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ inputs.template-tag }} | |
| SOURCE: ${{ inputs.template-source }} | |
| run: | | |
| if [ -z "$TAG" ] && [ "$GITHUB_EVENT_NAME" = 'workflow_dispatch' ] && [ "$SOURCE" != 'head' ]; then | |
| TAG=$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName -q .tagName) | |
| echo "no tag given — taking the templates from the latest release, $TAG" | |
| fi | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| - name: Take the template from the release | |
| if: steps.src.outputs.tag != '' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh release download "${{ steps.src.outputs.tag }}" --repo "$GITHUB_REPOSITORY" \ | |
| -p 'estella-native-android-*.zip' -D templates | |
| - name: Take the template from this run | |
| if: steps.src.outputs.tag == '' | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: native-template-android | |
| path: templates | |
| # Under release strictness, so this never packages around an archive the | |
| # publish gate would reject anyway. | |
| - name: Unpack the template | |
| run: | | |
| node build-tools/cli.js verify-template templates/*.zip | |
| mkdir -p template | |
| unzip -q templates/*.zip -d template | |
| - name: Build the SDK the games are bundled against | |
| run: pnpm --filter ./sdk build | |
| # Without this the emulator falls back to software CPU emulation and the | |
| # boot alone outlasts the job. | |
| - name: Enable KVM | |
| run: | | |
| echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ | |
| | sudo tee /etc/udev/rules.d/99-kvm4all.rules | |
| sudo udevadm control --reload-rules | |
| sudo udevadm trigger --name-match=kvm | |
| # The host is Dawn on Vulkan (the manifest requires it), which on a runner | |
| # with no GPU means the emulator's bundled SwiftShader Vulkan. | |
| - name: Package and boot every example in this shard | |
| uses: reactivecircus/android-emulator-runner@v2 | |
| with: | |
| api-level: ${{ env.ANDROID_API }} | |
| target: google_apis | |
| arch: x86_64 | |
| profile: ${{ env.ANDROID_PROFILE }} | |
| # Fourteen installs and launches in a row starve a default-sized AVD | |
| # until its launcher stops responding, which is noise this has already | |
| # had to filter once. | |
| emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none -memory 4096 -cores 2 | |
| # --recreate only when the template is this run's own build: the probe | |
| # requires the stack-anchor fix (jsEntry), and pointing it at an older | |
| # release's template would red the run on a bug that release is known | |
| # to carry. | |
| script: >- | |
| node tools/verify-native-boot.mjs --platform android --examples all | |
| --shard ${{ matrix.shard }}/3 | |
| ${{ steps.src.outputs.tag == '' && '--recreate' || '' }} | |
| ${{ inputs.examples && format('--only {0}', inputs.examples) || '' }} | |
| - name: Keep the frames and the records | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: android-shots-${{ matrix.shard }} | |
| path: build/native-boot | |
| if-no-files-found: warn | |
| ios: | |
| name: iOS ${{ matrix.shard }}/3 | |
| needs: templates | |
| if: always() && needs.templates.result != 'failure' && needs.templates.result != 'cancelled' | |
| runs-on: macos-latest | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [0, 1, 2] | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: ./.github/actions/setup | |
| - name: Decide where the template comes from | |
| id: src | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ inputs.template-tag }} | |
| SOURCE: ${{ inputs.template-source }} | |
| run: | | |
| if [ -z "$TAG" ] && [ "$GITHUB_EVENT_NAME" = 'workflow_dispatch' ] && [ "$SOURCE" != 'head' ]; then | |
| TAG=$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName -q .tagName) | |
| echo "no tag given — taking the templates from the latest release, $TAG" | |
| fi | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| - name: Take the template from the release | |
| if: steps.src.outputs.tag != '' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh release download "${{ steps.src.outputs.tag }}" --repo "$GITHUB_REPOSITORY" \ | |
| -p 'estella-native-ios-*.zip' -D templates | |
| - name: Take the template from this run | |
| if: steps.src.outputs.tag == '' | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: native-template-ios | |
| path: templates | |
| - name: Unpack the template | |
| run: | | |
| node build-tools/cli.js verify-template templates/*.zip | |
| mkdir -p template | |
| unzip -q templates/*.zip -d template | |
| - name: Build the SDK the games are bundled against | |
| run: pnpm --filter ./sdk build | |
| # Simulator slices need no signing identity, which is the whole reason this | |
| # can run on a hosted runner at all. | |
| - name: Package and boot every example in this shard | |
| run: >- | |
| node tools/verify-native-boot.mjs --platform ios --examples all | |
| --device "$IOS_DEVICE" --shard ${{ matrix.shard }}/3 | |
| ${{ inputs.examples && format('--only {0}', inputs.examples) || '' }} | |
| - name: Keep the frames and the records | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ios-shots-${{ matrix.shard }} | |
| path: build/native-boot | |
| if-no-files-found: warn |