Skip to content

Native smoke

Native smoke #28

Workflow file for this run

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