Skip to content

Fix JavaScript text selection and Android startup #12203

Fix JavaScript text selection and Android startup

Fix JavaScript text selection and Android startup #12203

Workflow file for this run

name: Test iOS UI build scripts
on:
workflow_dispatch:
inputs:
watch_only:
description: Run only the watchOS evidence job after the shared port build
required: false
default: false
type: boolean
schedule:
- cron: 15 1 * * *
pull_request:
paths:
- .github/workflows/scripts-ios.yml
- .github/workflows/_build-ios-port.yml
- scripts/setup-workspace.sh
- scripts/build-ios-port.sh
- scripts/build-ios-app.sh
- scripts/check-ios-framework-links.py
- scripts/check-ios-sdk-deltas.py
- scripts/check-ios-private-api.py
- scripts/lib/xcode.sh
- scripts/run-ios-ui-tests.sh
- scripts/ci/boot-ios-simulator.sh
- scripts/run-watch-ui-tests.sh
- scripts/run-tv-ui-tests.sh
- scripts/run-ios-native-tests.sh
- scripts/ios/notification-tests/native-tests/**
- scripts/ios/notification-tests/install-native-notification-tests.sh
- scripts/ios/notification-tests/**
- scripts/hellocodenameone/**
- scripts/ios/tests/**
- scripts/ios/screenshots-metal/**
- scripts/ios/screenshots-metal-27/**
- scripts/ios/screenshots-watch/**
- scripts/ios/screenshots-tv/**
- scripts/templates/**
- CodenameOne/src/**
- Ports/iOSPort/**
- native-themes/ios-modern/**
- vm/**
- tests/**
- maven/**
- scripts/hellocodenameone/backend/**
- scripts/lib/cn1ss.sh
- .github/workflows/scripts-ios-native.yml
- scripts/ios/create-shared-scheme.py
- .github/workflows/ios-packaging.yml
- maven/codenameone-maven-plugin/**
- vm/ByteCodeTranslator/**
- scripts/run-ios-device-release-build.sh
- scripts/ios/**
- .github/workflows/scripts-mac-catalyst.yml
- scripts/build-mac-catalyst-app.sh
- scripts/run-mac-catalyst-ui-tests.sh
- scripts/mac-catalyst/**
- scripts/common/java/**
- '!**/*.md'
- '!maven/cn1-ai-*/**'
- '!maven/cn1-admob/**'
- '!maven/cn1-applovin/**'
- '!maven/cn1-unity-levelplay/**'
- .github/ci/apple-checks.json
- scripts/ci/select-apple-checks.py
- scripts/ci/select-cn1lib-checks.py
push:
branches:
- master
paths:
- .github/workflows/scripts-ios.yml
- .github/workflows/_build-ios-port.yml
- scripts/setup-workspace.sh
- scripts/build-ios-port.sh
- scripts/build-ios-app.sh
- scripts/check-ios-framework-links.py
- scripts/check-ios-sdk-deltas.py
- scripts/check-ios-private-api.py
- scripts/lib/xcode.sh
- scripts/run-ios-ui-tests.sh
- scripts/ci/boot-ios-simulator.sh
- scripts/run-watch-ui-tests.sh
- scripts/run-tv-ui-tests.sh
- scripts/run-ios-native-tests.sh
- scripts/ios/notification-tests/native-tests/**
- scripts/ios/notification-tests/install-native-notification-tests.sh
- scripts/ios/notification-tests/**
- scripts/hellocodenameone/**
- scripts/ios/tests/**
- scripts/ios/screenshots-metal/**
- scripts/ios/screenshots-metal-27/**
- scripts/ios/screenshots-watch/**
- scripts/ios/screenshots-tv/**
- scripts/templates/**
- CodenameOne/src/**
- Ports/iOSPort/**
- native-themes/ios-modern/**
- vm/**
- tests/**
- maven/**
- scripts/hellocodenameone/backend/**
- scripts/lib/cn1ss.sh
- .github/workflows/scripts-ios-native.yml
- scripts/ios/create-shared-scheme.py
- .github/workflows/ios-packaging.yml
- maven/codenameone-maven-plugin/**
- vm/ByteCodeTranslator/**
- scripts/run-ios-device-release-build.sh
- scripts/ios/**
- .github/workflows/scripts-mac-catalyst.yml
- scripts/build-mac-catalyst-app.sh
- scripts/run-mac-catalyst-ui-tests.sh
- scripts/mac-catalyst/**
- scripts/common/java/**
- '!**/*.md'
- '!maven/cn1-ai-*/**'
- '!maven/cn1-admob/**'
- '!maven/cn1-applovin/**'
- '!maven/cn1-unity-levelplay/**'
- .github/ci/apple-checks.json
- scripts/ci/select-apple-checks.py
- scripts/ci/select-cn1lib-checks.py
concurrency:
# Only the latest revision needs validation. PR numbers keep forks isolated;
# event names keep a manual/scheduled run from cancelling a push or PR run.
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
select:
runs-on: ubuntu-24.04
outputs:
ios: ${{ steps.plan.outputs.ios }}
native: ${{ steps.plan.outputs.native }}
packaging: ${{ steps.plan.outputs.packaging }}
catalyst: ${{ steps.plan.outputs.catalyst }}
any: ${{ steps.plan.outputs.any }}
steps:
- uses: actions/checkout@v6
- id: plan
env:
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
run: python3 scripts/ci/select-apple-checks.py
build-port:
needs: select
if: needs.select.outputs.any == 'true'
uses: ./.github/workflows/_build-ios-port.yml
build-ios-metal:
if: ${{ !inputs.watch_only && needs.select.outputs.ios == 'true' }}
# The iOS renderer job. Metal is the only iOS rendering backend -- the
# OpenGL ES 2 pipeline and the job that exercised it are gone -- so this
# runs on every pull request rather than only on the nightly schedule.
needs: [select, build-port]
permissions:
contents: read
pull-requests: write
issues: write
runs-on: macos-15
timeout-minutes: 75
env:
GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }}
# The device runner reports logical test failures through CN1SS log
# markers, not through the build or the screenshot comparison. Make the
# normalized report authoritative so a failing or never-run compliance
# test cannot leave this workflow green and then be published from master.
CN1SS_FAIL_ON_TEST_PROBLEMS: '1'
GH_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }}
# Optional: when set, build-ios-app.sh writes it as a bundled resource so
# the GoogleWebMap screenshot test renders a live Google map; absent (e.g.
# fork PRs with no access to the secret) the test skips.
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
steps:
- uses: actions/checkout@v6
- name: Cache CocoaPods and user gems
uses: actions/cache@v5
with:
path: |
~/.gem
~/Library/Caches/CocoaPods
~/.cocoapods/repos
key: ${{ runner.os }}-pods-v1-${{ hashFiles('scripts/setup-workspace.sh') }}
restore-keys: |
${{ runner.os }}-pods-v1-
- name: Ensure CocoaPods tooling
run: |
mkdir -p ~/.codenameone
cp maven/UpdateCodenameOne.jar ~/.codenameone/
set -euo pipefail
if ! command -v ruby >/dev/null; then
echo "ruby not found"; exit 1
fi
GEM_USER_DIR="$(ruby -e 'print Gem.user_dir')"
export PATH="$GEM_USER_DIR/bin:$PATH"
if ! command -v pod >/dev/null 2>&1; then
gem install cocoapods xcodeproj --no-document --user-install
fi
pod --version
- name: Compute setup-workspace hash
id: setup_hash
run: |
set -euo pipefail
echo "hash=$(shasum -a 256 scripts/setup-workspace.sh | awk '{print $1}')" >> "$GITHUB_OUTPUT"
- name: Compute CN1 source hash
id: src_hash
run: |
set -euo pipefail
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes native-themes \
maven/codenameone-maven-plugin/src/main \
maven/build-engine/src/main \
maven/project-model/src/main \
-type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
SCRIPT_HASH=$(shasum -a 256 \
scripts/setup-workspace.sh \
scripts/build-ios-port.sh \
scripts/build-native-themes.sh \
.github/workflows/_build-ios-port.yml \
| shasum -a 256 | awk '{print $1}')
echo "hash=${SRC_HASH:0:16}-${POM_HASH:0:16}-${SCRIPT_HASH:0:16}" >> "$GITHUB_OUTPUT"
- name: Set TMPDIR
run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV
- name: Cache Maven repository
uses: actions/cache@v5
with:
path: ~/.m2/repository
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-m2-
- name: Restore cn1-binaries cache
uses: actions/cache@v5
with:
path: ../cn1-binaries
key: cn1-binaries-${{ runner.os }}-${{ steps.setup_hash.outputs.hash }}
restore-keys: |
cn1-binaries-${{ runner.os }}-
- name: Download built CN1 + iOS port bundle
uses: actions/download-artifact@v4
with:
name: cn1-built-bundle
path: ${{ runner.temp }}
- name: Restore built CN1 + iOS port tree
run: |
set -euo pipefail
# Replaces the evictable cn1-built cache restore with the artifact
# build-port handed off (not LRU-evicted, survives --failed re-runs).
# The tar was created relative to / so this restores
# ~/.m2/com/codenameone + Themes + Ports/iOSPort/nativeSources to
# their original absolute paths.
tar -C / -xf "$RUNNER_TEMP/cn1-built-bundle.tar"
- name: Install Metal Toolchain
# Xcode 26+ requires the Metal Toolchain component to be downloaded
# explicitly before .metal files can compile. The runner's default
# xcodebuild is often an older Xcode whose -downloadComponent flag
# doesn't exist, so we point DEVELOPER_DIR at Xcode 26 first (same
# selection logic build-ios-app.sh uses). If the component is
# already cached on the runner image, xcodebuild exits quickly
# without re-downloading.
run: |
set -euo pipefail
# Same selection the build scripts use, so this cannot install the
# component into one Xcode and then build with another.
# shellcheck source=/dev/null
source scripts/lib/xcode.sh
cn1_select_xcode || exit 1
"$XCODEBUILD" -downloadComponent MetalToolchain
timeout-minutes: 10
- name: Build sample iOS app and compile workspace (Metal)
id: build-ios-app
run: ./scripts/build-ios-app.sh -q -DskipTests
timeout-minutes: 30
# Every framework the port's natives need must be one the project DECLARES.
# A framework that only reaches the link through an autolink hint -- a Swift
# file's `import SwiftUI`, a CocoaPods dependency, `@import` under the Metal
# build's CLANG_ENABLE_MODULES -- links this sample and no plain Objective-C
# application. That is how `_OBJC_CLASS_$_UTType` stayed undefined in every
# customer archive while this job was green for five days.
#
# Runs on the project the step above already generated, so it costs seconds
# and it never links a Swift object, a pod or an extension.
- name: Check the port links only frameworks the project declares (Metal)
run: |
set -euo pipefail
# Resolve against the SAME Xcode build-ios-app.sh used. This step used
# to shell out to the runner's ambient xcrun, so it answered "is this
# symbol declared?" against a different SDK than the one that produced
# the project it was checking.
# shellcheck source=/dev/null
source scripts/lib/xcode.sh
cn1_select_xcode || exit 1
scripts/check-ios-framework-links.py \
--developer-dir "$DEVELOPER_DIR" \
"$(dirname "${{ steps.build-ios-app.outputs.workspace }}")"
timeout-minutes: 10
# App Store Connect rejects an upload that imports any SDK symbol no public header
# declares ("Validation failed (409) The app references non-public symbols"), and
# nothing else here can see one: a private symbol compiles and links like any other.
# Customer uploads were rejected for CommonCrypto's private AES-GCM calls while every
# job stayed green, because the calls sat behind a feature gate this sample never
# turns on. So this compiles EVERY port native with EVERY gate on, from pristine
# sources, and fails on any import that is exported but undeclared. The linked
# binary is checked the same way in ios-packaging.yml.
- name: Check the port references no private Apple API
run: |
set -euo pipefail
# shellcheck source=/dev/null
source scripts/lib/xcode.sh
cn1_select_xcode || exit 1
scripts/check-ios-private-api.py \
--developer-dir "$DEVELOPER_DIR" \
--project-dir "$(dirname "${{ steps.build-ios-app.outputs.workspace }}")"/*-src
timeout-minutes: 15
# Apple can deprecate an API, or change a behaviour for anything LINKED
# against the new SDK, and nothing else here would notice: every other
# check compiles against one SDK and has nothing to compare it to. This one
# compiles the port twice, varying only -isysroot, so what it reports is
# caused by the SDK and not by the command line.
#
# It sweeps the renderer and on-device-debug configurations itself, from
# pristine port sources, so hosting it in this job costs one build.
#
# It is a no-op on a runner that carries a single Xcode, which is the case
# today -- and it starts working, without another edit, on the first image
# that ships two. That is deliberate: the alternative is remembering to add
# it at exactly the moment a toolchain moves, which is the moment nobody
# has spare attention.
- name: Compare the port against the next iOS SDK
run: |
set -euo pipefail
scripts/check-ios-sdk-deltas.py \
--skip-if-single-sdk \
--project-dir "$(dirname "${{ steps.build-ios-app.outputs.workspace }}")"/*-src
timeout-minutes: 20
- name: Run iOS UI screenshot tests (Metal)
env:
ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/ios-ui-tests-metal
# Golden set for the iOS simulator. See
# scripts/ios/screenshots-metal/README.md.
SCREENSHOT_REF_DIR: ${{ github.workspace }}/scripts/ios/screenshots-metal
# Override the PR comment marker / preview path / title so this
# job posts its own comment instead of overwriting the GL job's
# comment (both jobs ran with the same default marker before, so
# whichever finished last clobbered the other's results).
CN1SS_COMMENT_MARKER: '<!-- CN1SS_IOS_METAL_COMMENT -->'
CN1SS_PREVIEW_SUBDIR: ios-metal
CN1SS_REPORT_TITLE: 'iOS Metal screenshot updates'
CN1SS_SUCCESS_MESSAGE: '✅ Native iOS Metal screenshot tests passed.'
CN1SS_COMMENT_LOG_PREFIX: '[run-ios-device-tests-metal]'
# Strict screenshot gating: fail on any pixel mismatch and on any
# missing screenshot. The Metal backend must produce the full
# baseline set; a test that hangs (e.g. the DialogTheme texture
# backdrop) drops every later capture and silently shrinks the
# suite from 122 to 107 - that now fails the job. No missing
# screenshots are tolerated. Enforced in scripts/lib/cn1ss.sh.
CN1SS_FAIL_ON_MISMATCH: '1'
# Enable Apple's Metal API Validation in the simulator. Catches
# render-pass / pipeline-state mismatches at the moment they
# happen (issue #5103: stencil pixel-format mismatch shipped in
# four consecutive 7.0.x releases because no test exercised the
# validation layer). assert mode crashes the app on the first
# validation error -- the missed CN1SS:SUITE:FINISHED marker
# then fails this step. run-ios-ui-tests.sh forwards both vars
# to the launched app via simctl --setenv.
MTL_DEBUG_LAYER: '1'
MTL_DEBUG_LAYER_ERROR_MODE: 'assert'
CN1SS_PORT_ID: ios-metal
# Count this build's compiler warnings and attribute each to whoever
# owns the code. This used to ride on the GL leg; that leg is gone, so
# it moves here -- and this is now the ONLY leg that runs the census,
# which means the baseline comparison and its injected-warning probe
# exist nowhere else. pr.yml only runs the parser self-test, which
# censuses nothing.
#
# This leg can host it for the same reason the old one could: it sets
# no CN1_IOS_DERIVED_DATA, so run-ios-ui-tests.sh wipes its derived
# data and compiles every translation unit from cold. An incremental
# build reports no warnings and is indistinguishable from a clean
# codebase, which is why the tool exits 2 rather than passing when the
# compile count falls short of the manifest.
CN1_WARNING_CENSUS: '1'
CN1_WARNING_LEG: ios-sim-debug
# build-ios-app.sh stages the manifest under the default artifacts dir,
# which is not the per-step one above.
CN1_WARNING_MANIFEST: ${{ github.workspace }}/artifacts/cn1-source-manifest.txt
run: |
set -euo pipefail
mkdir -p "${ARTIFACTS_DIR}"
echo "workspace='${{ steps.build-ios-app.outputs.workspace }}'"
echo "scheme='${{ steps.build-ios-app.outputs.scheme }}'"
echo "reference dir='${SCREENSHOT_REF_DIR}'"
./scripts/run-ios-ui-tests.sh \
"${{ steps.build-ios-app.outputs.workspace }}" \
"" \
"${{ steps.build-ios-app.outputs.scheme }}"
# Match the GL envelope; run-ios-ui-tests.sh still fails a stalled
# DeviceRunner after 12 minutes without CN1SS progress.
timeout-minutes: 65
- name: Upload iOS Metal port status
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: port-status-ios-metal
path: artifacts/ios-ui-tests-metal/port-status-ios-metal.json
if-no-files-found: warn
retention-days: 14
- name: The suite must have compared screenshots
# Runs in BOTH iOS Metal legs, and is the control that was missing.
#
# The screenshot step can be tolerated while a baseline set is being
# seeded -- there is nothing to compare against yet -- but "tolerated"
# kept turning into "produced nothing and said nothing". On the iOS 27
# leg that concealed three separate defects in a row: a warning census
# keyed to the wrong toolchain, a simulator destination that fell back to
# an iPad, and before those a reference directory that did not exist.
# Every one of them reported the job green with zero PNGs in the
# artifact, and each was only found by downloading that artifact by hand.
#
# A run that captures nothing is never a pass, whatever else is tolerated.
# Deliberately NOT expressed as a count against the reference directory:
# that guard exists inside run-ios-ui-tests.sh and is disabled by an empty
# reference set, which is exactly the situation a seeding run is in.
if: ${{ !cancelled() }}
env:
# Declared HERE rather than inherited: ARTIFACTS_DIR is set in the
# screenshot step's own env block, and step env does not reach other
# steps. Without this the check reads an empty path, finds nothing and
# fails a leg that captured a full set -- which is exactly what it did
# on its first run, the same scope mistake as the warning leg above.
ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/ios-ui-tests-metal
#
# Counted from the COMPARISON REPORT, not from PNGs on disk. The runner
# keeps only the captures that failed comparison, so a run in which all
# 155 matched leaves zero PNGs behind -- indistinguishable, by file
# count, from a run that never started. That was this check's first
# version, and it failed the iOS 27 leg on the very run where every
# screenshot matched its baseline. The report exists only once the suite
# reached the screenshot stage, and lists every screenshot it compared.
run: |
set -euo pipefail
report="${ARTIFACTS_DIR}/screenshot-compare.json"
if [ ! -f "${report}" ]; then
echo "::error title=No screenshots::no comparison report at ${report}; the suite never reached the screenshot stage"
exit 1
fi
count=$(python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1])).get("results") or []))' "${report}")
echo "screenshots compared: ${count}"
if [ "${count}" -eq 0 ]; then
echo "::error title=No screenshots::the suite compared 0 screenshots; the leg validated nothing"
exit 1
fi
- name: Publish Metal screenshot summary
# Surfaces run-ios-ui-tests.sh's comparison result in the job's
# GitHub Actions summary page so the Metal port status is visible
# at a glance without digging into the artifact zip. Always runs
# so a failed or cancelled tests step still reports whatever got
# captured. The Python helper lives in scripts/ci/ because
# embedding Python heredocs inside a YAML "run: |" block is
# fragile -- unindented Python breaks the block scalar.
if: ${{ !cancelled() }}
env:
COMPARE_JSON: ${{ github.workspace }}/artifacts/ios-ui-tests-metal/screenshot-compare.json
COMMENT_MD: ${{ github.workspace }}/artifacts/ios-ui-tests-metal/screenshot-comment.md
run: |
set -eu
{
echo "## iOS Metal screenshot comparison"
echo
echo "Ran against \`scripts/hellocodenameone\` on the iOS simulator."
echo "Golden images: \`scripts/ios/screenshots-metal/\`."
echo
if [ -s "$COMPARE_JSON" ]; then
python3 scripts/ci/metal-screenshot-summary.py --markdown "$COMPARE_JSON"
elif [ -s "$COMMENT_MD" ]; then
cat "$COMMENT_MD"
else
echo "_No screenshot comparison artifact was produced. See the upload step output for details._"
fi
} >> "$GITHUB_STEP_SUMMARY"
if [ -s "$COMPARE_JSON" ]; then
NOTICE="$(python3 scripts/ci/metal-screenshot-summary.py --headline "$COMPARE_JSON" || true)"
if [ -n "$NOTICE" ]; then
echo "::notice title=Metal screenshot comparison::${NOTICE}"
fi
fi
- name: Upload iOS Metal artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: ios-ui-tests-metal
path: |
artifacts/ios-ui-tests-metal
artifacts/*.log
artifacts/*-stats.txt
artifacts/vm_time.txt
artifacts/xcodebuild-list.txt
artifacts/cn1-source-manifest.txt
artifacts/ios-ui-tests-metal/native-warnings.json
if-no-files-found: warn
retention-days: 14
build-ios-metal-27:
if: ${{ !inputs.watch_only && needs.select.outputs.ios == 'true' }}
# The SAME suite as build-ios-metal, on the Xcode 27 / iOS 27 hosted image and
# against its own baselines. A copy rather than a move: build-ios-metal keeps
# gating the toolchain we actually pin (CN1_XCODE_MAJOR=26), which is what
# applications are built with today, and this leg is what stops iOS 27
# breaking us in the gap before that pin moves.
#
# Everything below is deliberately identical to build-ios-metal except the
# runner, the toolchain pin, the theme generation, the baseline directory and
# the artifact/comment names. scripts/check-ios-metal-legs-match.py holds the
# two to that, because a copied 320-line job drifts otherwise.
#
# `xcode-27` is a PUBLIC PREVIEW image (actions/runner-images#14404) whose
# capacity is still being balanced, so read a queue here as the image rather
# than as a fault in the change under test.
needs: [select, build-port]
permissions:
contents: read
pull-requests: write
issues: write
runs-on: xcode-27
timeout-minutes: 75
env:
GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }}
# The device runner reports logical test failures through CN1SS log
# markers, not through the build or the screenshot comparison. Make the
# normalized report authoritative so a failing or never-run compliance
# test cannot leave this workflow green and then be published from master.
CN1SS_FAIL_ON_TEST_PROBLEMS: '1'
GH_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }}
# Pin the toolchain rather than taking the image default: scripts/lib/
# xcode.sh resolves CN1_XCODE_MAJOR, and leaving it at 26 on an image that
# ships only Xcode 27 fails resolution outright instead of quietly using
# the wrong one.
CN1_XCODE_MAJOR: '27'
# Build the sample against the iOS 27 theme generation. helloworld installs
# the modern theme itself (DualAppearanceBaseTest.pickModernThemeResource)
# and now asks the port which generation this build carries, so this hint is
# what makes the 27 baselines render the iOS 27 theme instead of scoring the
# iOS 26 one against iOS 27 goldens.
IOS_DEPENDENCY_ARGS: '-Dcodename1.arg.ios.themeGeneration=27'
# Optional: when set, build-ios-app.sh writes it as a bundled resource so
# the GoogleWebMap screenshot test renders a live Google map; absent (e.g.
# fork PRs with no access to the secret) the test skips.
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
steps:
- uses: actions/checkout@v6
- name: Cache CocoaPods and user gems
uses: actions/cache@v5
with:
path: |
~/.gem
~/Library/Caches/CocoaPods
~/.cocoapods/repos
key: ${{ runner.os }}-pods-v1-${{ hashFiles('scripts/setup-workspace.sh') }}
restore-keys: |
${{ runner.os }}-pods-v1-
- name: Ensure CocoaPods tooling
run: |
mkdir -p ~/.codenameone
cp maven/UpdateCodenameOne.jar ~/.codenameone/
set -euo pipefail
if ! command -v ruby >/dev/null; then
echo "ruby not found"; exit 1
fi
GEM_USER_DIR="$(ruby -e 'print Gem.user_dir')"
export PATH="$GEM_USER_DIR/bin:$PATH"
if ! command -v pod >/dev/null 2>&1; then
gem install cocoapods xcodeproj --no-document --user-install
fi
pod --version
- name: Compute setup-workspace hash
id: setup_hash
run: |
set -euo pipefail
echo "hash=$(shasum -a 256 scripts/setup-workspace.sh | awk '{print $1}')" >> "$GITHUB_OUTPUT"
- name: Compute CN1 source hash
id: src_hash
run: |
set -euo pipefail
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes native-themes \
maven/codenameone-maven-plugin/src/main \
maven/build-engine/src/main \
maven/project-model/src/main \
-type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
SCRIPT_HASH=$(shasum -a 256 \
scripts/setup-workspace.sh \
scripts/build-ios-port.sh \
scripts/build-native-themes.sh \
.github/workflows/_build-ios-port.yml \
| shasum -a 256 | awk '{print $1}')
echo "hash=${SRC_HASH:0:16}-${POM_HASH:0:16}-${SCRIPT_HASH:0:16}" >> "$GITHUB_OUTPUT"
- name: Set TMPDIR
run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV
- name: Cache Maven repository
uses: actions/cache@v5
with:
path: ~/.m2/repository
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-m2-
- name: Restore cn1-binaries cache
uses: actions/cache@v5
with:
path: ../cn1-binaries
key: cn1-binaries-${{ runner.os }}-${{ steps.setup_hash.outputs.hash }}
restore-keys: |
cn1-binaries-${{ runner.os }}-
- name: Download built CN1 + iOS port bundle
uses: actions/download-artifact@v4
with:
name: cn1-built-bundle
path: ${{ runner.temp }}
- name: Restore built CN1 + iOS port tree
run: |
set -euo pipefail
# Replaces the evictable cn1-built cache restore with the artifact
# build-port handed off (not LRU-evicted, survives --failed re-runs).
# The tar was created relative to / so this restores
# ~/.m2/com/codenameone + Themes + Ports/iOSPort/nativeSources to
# their original absolute paths.
tar -C / -xf "$RUNNER_TEMP/cn1-built-bundle.tar"
- name: Install Metal Toolchain
# Xcode 26+ requires the Metal Toolchain component to be downloaded
# explicitly before .metal files can compile. The runner's default
# xcodebuild is often an older Xcode whose -downloadComponent flag
# doesn't exist, so we point DEVELOPER_DIR at Xcode 26 first (same
# selection logic build-ios-app.sh uses). If the component is
# already cached on the runner image, xcodebuild exits quickly
# without re-downloading.
run: |
set -euo pipefail
# Same selection the build scripts use, so this cannot install the
# component into one Xcode and then build with another.
# shellcheck source=/dev/null
source scripts/lib/xcode.sh
cn1_select_xcode || exit 1
"$XCODEBUILD" -downloadComponent MetalToolchain
timeout-minutes: 10
- name: Build sample iOS app and compile workspace (Metal)
id: build-ios-app
run: ./scripts/build-ios-app.sh -q -DskipTests
timeout-minutes: 30
# Every framework the port's natives need must be one the project DECLARES.
# A framework that only reaches the link through an autolink hint -- a Swift
# file's `import SwiftUI`, a CocoaPods dependency, `@import` under the Metal
# build's CLANG_ENABLE_MODULES -- links this sample and no plain Objective-C
# application. That is how `_OBJC_CLASS_$_UTType` stayed undefined in every
# customer archive while this job was green for five days.
#
# Runs on the project the step above already generated, so it costs seconds
# and it never links a Swift object, a pod or an extension.
- name: Check the port links only frameworks the project declares (Metal)
run: |
set -euo pipefail
# Resolve against the SAME Xcode build-ios-app.sh used. This step used
# to shell out to the runner's ambient xcrun, so it answered "is this
# symbol declared?" against a different SDK than the one that produced
# the project it was checking.
# shellcheck source=/dev/null
source scripts/lib/xcode.sh
cn1_select_xcode || exit 1
scripts/check-ios-framework-links.py \
--developer-dir "$DEVELOPER_DIR" \
"$(dirname "${{ steps.build-ios-app.outputs.workspace }}")"
timeout-minutes: 10
# App Store Connect rejects an upload that imports any SDK symbol no public header
# declares ("Validation failed (409) The app references non-public symbols"), and
# nothing else here can see one: a private symbol compiles and links like any other.
# Customer uploads were rejected for CommonCrypto's private AES-GCM calls while every
# job stayed green, because the calls sat behind a feature gate this sample never
# turns on. So this compiles EVERY port native with EVERY gate on, from pristine
# sources, and fails on any import that is exported but undeclared. The linked
# binary is checked the same way in ios-packaging.yml.
- name: Check the port references no private Apple API
run: |
set -euo pipefail
# shellcheck source=/dev/null
source scripts/lib/xcode.sh
cn1_select_xcode || exit 1
scripts/check-ios-private-api.py \
--developer-dir "$DEVELOPER_DIR" \
--project-dir "$(dirname "${{ steps.build-ios-app.outputs.workspace }}")"/*-src
timeout-minutes: 15
# Apple can deprecate an API, or change a behaviour for anything LINKED
# against the new SDK, and nothing else here would notice: every other
# check compiles against one SDK and has nothing to compare it to. This one
# compiles the port twice, varying only -isysroot, so what it reports is
# caused by the SDK and not by the command line.
#
# It sweeps the renderer and on-device-debug configurations itself, from
# pristine port sources, so hosting it in this job costs one build.
#
# It is a no-op on a runner that carries a single Xcode, which is the case
# today -- and it starts working, without another edit, on the first image
# that ships two. That is deliberate: the alternative is remembering to add
# it at exactly the moment a toolchain moves, which is the moment nobody
# has spare attention.
- name: Compare the port against the next iOS SDK
run: |
set -euo pipefail
scripts/check-ios-sdk-deltas.py \
--skip-if-single-sdk \
--project-dir "$(dirname "${{ steps.build-ios-app.outputs.workspace }}")"/*-src
timeout-minutes: 20
- name: Boot an iPhone 16 on iOS 27
id: sim27
# run-ios-ui-tests.sh asks xcodebuild for a destination named "iPhone 16",
# and -showdestinations lists only simulators that ALREADY EXIST. This
# image pre-creates iPhone 17 / 17e / 18 Pro / 18 Pro Max / Air and no
# iPhone 16, so the suite logged "Simulator auto-selection did not return
# a destination" and captured on whatever fallback_sim_destination
# happened to return -- an iPad, on the run that exposed this. A baseline
# taken on an arbitrary device is one nobody can reason about, and it
# moves the day the image's device list changes.
#
# The device TYPE is available even though no instance is, so create one.
# Same model as the iOS 26 leg on purpose: the two sets then differ by
# runtime and theme, which is what they are for, rather than by hardware.
run: ./scripts/ci/boot-ios-simulator.sh 'iOS-27[0-9-]*' 'iPhone 16' iPhone16-metal27
- name: Run iOS UI screenshot tests (Metal)
env:
# The simulator created by the step above. Without it the suite asks
# xcodebuild for a destination named "iPhone 16", finds none on this
# image, and silently captures on a fallback device.
IOS_SIM_DESTINATION: 'platform=iOS Simulator,id=${{ steps.sim27.outputs.udid }}'
ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/ios-ui-tests-metal-27
# Golden set for the iOS simulator. See
# scripts/ios/screenshots-metal/README.md.
SCREENSHOT_REF_DIR: ${{ github.workspace }}/scripts/ios/screenshots-metal-27
# Override the PR comment marker / preview path / title so this
# job posts its own comment instead of overwriting the GL job's
# comment (both jobs ran with the same default marker before, so
# whichever finished last clobbered the other's results).
CN1SS_COMMENT_MARKER: '<!-- CN1SS_IOS_METAL27_COMMENT -->'
CN1SS_PREVIEW_SUBDIR: ios-metal-27
CN1SS_REPORT_TITLE: 'iOS Metal screenshot updates'
CN1SS_SUCCESS_MESSAGE: '✅ Native iOS Metal screenshot tests passed.'
CN1SS_COMMENT_LOG_PREFIX: '[run-ios-device-tests-metal]'
# Strict screenshot gating: fail on any pixel mismatch and on any
# missing screenshot. The Metal backend must produce the full
# baseline set; a test that hangs (e.g. the DialogTheme texture
# backdrop) drops every later capture and silently shrinks the
# suite from 122 to 107 - that now fails the job. No missing
# screenshots are tolerated. Enforced in scripts/lib/cn1ss.sh.
CN1SS_FAIL_ON_MISMATCH: '1'
# Enable Apple's Metal API Validation in the simulator. Catches
# render-pass / pipeline-state mismatches at the moment they
# happen (issue #5103: stencil pixel-format mismatch shipped in
# four consecutive 7.0.x releases because no test exercised the
# validation layer). assert mode crashes the app on the first
# validation error -- the missed CN1SS:SUITE:FINISHED marker
# then fails this step. run-ios-ui-tests.sh forwards both vars
# to the launched app via simctl --setenv.
MTL_DEBUG_LAYER: '1'
MTL_DEBUG_LAYER_ERROR_MODE: 'assert'
CN1SS_PORT_ID: ios-metal
# Count this build's compiler warnings and attribute each to whoever
# owns the code. This used to ride on the GL leg; that leg is gone, so
# it moves here -- and this is now the ONLY leg that runs the census,
# which means the baseline comparison and its injected-warning probe
# exist nowhere else. pr.yml only runs the parser self-test, which
# censuses nothing.
#
# This leg can host it for the same reason the old one could: it sets
# no CN1_IOS_DERIVED_DATA, so run-ios-ui-tests.sh wipes its derived
# data and compiles every translation unit from cold. An incremental
# build reports no warnings and is indistinguishable from a clean
# codebase, which is why the tool exits 2 rather than passing when the
# compile count falls short of the manifest.
CN1_WARNING_CENSUS: '1'
CN1_WARNING_LEG: ios-sim-debug-xcode27
# build-ios-app.sh stages the manifest under the default artifacts dir,
# which is not the per-step one above.
CN1_WARNING_MANIFEST: ${{ github.workspace }}/artifacts/cn1-source-manifest.txt
run: |
set -euo pipefail
mkdir -p "${ARTIFACTS_DIR}"
echo "workspace='${{ steps.build-ios-app.outputs.workspace }}'"
echo "scheme='${{ steps.build-ios-app.outputs.scheme }}'"
echo "reference dir='${SCREENSHOT_REF_DIR}'"
./scripts/run-ios-ui-tests.sh \
"${{ steps.build-ios-app.outputs.workspace }}" \
"" \
"${{ steps.build-ios-app.outputs.scheme }}"
# Match the GL envelope; run-ios-ui-tests.sh still fails a stalled
# DeviceRunner after 12 minutes without CN1SS progress.
timeout-minutes: 65
# NO port-status upload here, deliberately. The published port report is
# keyed on the port id `ios-metal`, and there is only one iOS port -- this
# leg differs by TOOLCHAIN, not by port. A second upload under that id
# would publish whichever leg finished last, the same clobber the comment
# marker above already had to be split to avoid.
- name: The suite must have compared screenshots
# Runs in BOTH iOS Metal legs, and is the control that was missing.
#
# The screenshot step can be tolerated while a baseline set is being
# seeded -- there is nothing to compare against yet -- but "tolerated"
# kept turning into "produced nothing and said nothing". On the iOS 27
# leg that concealed three separate defects in a row: a warning census
# keyed to the wrong toolchain, a simulator destination that fell back to
# an iPad, and before those a reference directory that did not exist.
# Every one of them reported the job green with zero PNGs in the
# artifact, and each was only found by downloading that artifact by hand.
#
# A run that captures nothing is never a pass, whatever else is tolerated.
# Deliberately NOT expressed as a count against the reference directory:
# that guard exists inside run-ios-ui-tests.sh and is disabled by an empty
# reference set, which is exactly the situation a seeding run is in.
if: ${{ !cancelled() }}
env:
# Declared HERE rather than inherited: ARTIFACTS_DIR is set in the
# screenshot step's own env block, and step env does not reach other
# steps. Without this the check reads an empty path, finds nothing and
# fails a leg that captured a full set -- which is exactly what it did
# on its first run, the same scope mistake as the warning leg above.
ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/ios-ui-tests-metal-27
#
# Counted from the COMPARISON REPORT, not from PNGs on disk. The runner
# keeps only the captures that failed comparison, so a run in which all
# 155 matched leaves zero PNGs behind -- indistinguishable, by file
# count, from a run that never started. That was this check's first
# version, and it failed the iOS 27 leg on the very run where every
# screenshot matched its baseline. The report exists only once the suite
# reached the screenshot stage, and lists every screenshot it compared.
run: |
set -euo pipefail
report="${ARTIFACTS_DIR}/screenshot-compare.json"
if [ ! -f "${report}" ]; then
echo "::error title=No screenshots::no comparison report at ${report}; the suite never reached the screenshot stage"
exit 1
fi
count=$(python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1])).get("results") or []))' "${report}")
echo "screenshots compared: ${count}"
if [ "${count}" -eq 0 ]; then
echo "::error title=No screenshots::the suite compared 0 screenshots; the leg validated nothing"
exit 1
fi
- name: Publish Metal screenshot summary
# Surfaces run-ios-ui-tests.sh's comparison result in the job's
# GitHub Actions summary page so the Metal port status is visible
# at a glance without digging into the artifact zip. Always runs
# so a failed or cancelled tests step still reports whatever got
# captured. The Python helper lives in scripts/ci/ because
# embedding Python heredocs inside a YAML "run: |" block is
# fragile -- unindented Python breaks the block scalar.
if: ${{ !cancelled() }}
env:
COMPARE_JSON: ${{ github.workspace }}/artifacts/ios-ui-tests-metal-27/screenshot-compare.json
COMMENT_MD: ${{ github.workspace }}/artifacts/ios-ui-tests-metal-27/screenshot-comment.md
run: |
set -eu
{
echo "## iOS Metal screenshot comparison"
echo
echo "Ran against \`scripts/hellocodenameone\` on the iOS simulator."
echo "Golden images: \`scripts/ios/screenshots-metal/\`."
echo
if [ -s "$COMPARE_JSON" ]; then
python3 scripts/ci/metal-screenshot-summary.py --markdown "$COMPARE_JSON"
elif [ -s "$COMMENT_MD" ]; then
cat "$COMMENT_MD"
else
echo "_No screenshot comparison artifact was produced. See the upload step output for details._"
fi
} >> "$GITHUB_STEP_SUMMARY"
if [ -s "$COMPARE_JSON" ]; then
NOTICE="$(python3 scripts/ci/metal-screenshot-summary.py --headline "$COMPARE_JSON" || true)"
if [ -n "$NOTICE" ]; then
echo "::notice title=Metal screenshot comparison::${NOTICE}"
fi
fi
- name: Upload iOS Metal artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: ios-ui-tests-metal-27
path: |
artifacts/ios-ui-tests-metal-27
artifacts/*.log
artifacts/*-stats.txt
artifacts/vm_time.txt
artifacts/xcodebuild-list.txt
artifacts/cn1-source-manifest.txt
artifacts/ios-ui-tests-metal-27/native-warnings.json
if-no-files-found: warn
retention-days: 14
build-ios-watch:
if: needs.select.outputs.ios == 'true'
# Native Apple Watch (watchOS) screenshot pipeline. The watch slice
# auto-enables from codename1.watchMain in the sample, so build-ios-app.sh
# generates the <Main>Watch target alongside the iOS app. This job renders
# the cn1ss suite on the watch simulator through the Core Graphics backend
# (no GL/Metal on watchOS), streams frames to the same cn1ss test server
# WS sink the iOS jobs use, and compares against scripts/ios/screenshots-watch.
# Isolated in its own job (like build-ios-metal) so the watch result is a
# distinct check and a regression there doesn't mask the iOS path.
needs: [select, build-port]
permissions:
contents: read
pull-requests: write
issues: write
runs-on: macos-15
timeout-minutes: 60
env:
GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }}
# The device runner reports logical test failures through CN1SS log
# markers, not through the build or the screenshot comparison. Make the
# normalized report authoritative so a failing or never-run compliance
# test cannot leave this workflow green and then be published from master.
CN1SS_FAIL_ON_TEST_PROBLEMS: '1'
GH_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }}
# Optional: when set, build-ios-app.sh writes it as a bundled resource so
# the GoogleWebMap screenshot test renders a live Google map; absent (e.g.
# fork PRs with no access to the secret) the test skips.
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
steps:
- uses: actions/checkout@v6
- name: Cache CocoaPods and user gems
uses: actions/cache@v5
with:
path: |
~/.gem
~/Library/Caches/CocoaPods
~/.cocoapods/repos
key: ${{ runner.os }}-pods-v1-${{ hashFiles('scripts/setup-workspace.sh') }}
restore-keys: |
${{ runner.os }}-pods-v1-
- name: Ensure CocoaPods tooling
run: |
mkdir -p ~/.codenameone
cp maven/UpdateCodenameOne.jar ~/.codenameone/
set -euo pipefail
if ! command -v ruby >/dev/null; then
echo "ruby not found"; exit 1
fi
GEM_USER_DIR="$(ruby -e 'print Gem.user_dir')"
export PATH="$GEM_USER_DIR/bin:$PATH"
if ! command -v pod >/dev/null 2>&1; then
gem install cocoapods xcodeproj --no-document --user-install
fi
pod --version
- name: Compute setup-workspace hash
id: setup_hash
run: |
set -euo pipefail
echo "hash=$(shasum -a 256 scripts/setup-workspace.sh | awk '{print $1}')" >> "$GITHUB_OUTPUT"
- name: Set TMPDIR
run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV
- name: Cache Maven repository
uses: actions/cache@v5
with:
path: ~/.m2/repository
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-m2-
- name: Restore cn1-binaries cache
uses: actions/cache@v5
with:
path: ../cn1-binaries
key: cn1-binaries-${{ runner.os }}-${{ steps.setup_hash.outputs.hash }}
restore-keys: |
cn1-binaries-${{ runner.os }}-
- name: Download built CN1 + iOS port bundle
uses: actions/download-artifact@v4
with:
name: cn1-built-bundle
path: ${{ runner.temp }}
- name: Restore built CN1 + iOS port tree
run: |
set -euo pipefail
# Replaces the evictable cn1-built cache restore with the artifact
# build-port handed off (not LRU-evicted, survives --failed re-runs).
tar -C / -xf "$RUNNER_TEMP/cn1-built-bundle.tar"
- name: Build sample iOS app (generates the watch target)
id: build-ios-app
run: ./scripts/build-ios-app.sh -q -DskipTests
timeout-minutes: 30
- name: Ensure watchOS simulator runtime
run: |
set -euo pipefail
if ! xcrun simctl list runtimes available 2>/dev/null | grep -qi watchOS; then
echo "No watchOS runtime found; attempting download"
xcodebuild -downloadPlatform watchOS || true
fi
xcrun simctl list runtimes available 2>/dev/null | grep -i watchOS || true
xcrun simctl list devices available 2>/dev/null | grep -i "Apple Watch" || true
- name: Run watchOS UI screenshot tests
env:
ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/watch-ui-tests
SCREENSHOT_REF_DIR: ${{ github.workspace }}/scripts/ios/screenshots-watch
CN1SS_COMMENT_MARKER: '<!-- CN1SS_IOS_WATCH_COMMENT -->'
CN1SS_PREVIEW_SUBDIR: ios-watch
CN1SS_REPORT_TITLE: 'Apple Watch (watchOS) screenshot updates'
CN1SS_SUCCESS_MESSAGE: '✅ Native Apple Watch (watchOS, Core Graphics) screenshot tests passed.'
CN1SS_COMMENT_LOG_PREFIX: '[run-watch-ui-tests]'
CN1SS_FAIL_ON_MISMATCH: '1'
CN1SS_WATCH_TIMEOUT: '2400'
# A missing frame is an incomplete run, not an allowed result.
CN1SS_ALLOWED_MISSING: '0'
run: |
set -euo pipefail
mkdir -p "${ARTIFACTS_DIR}"
echo "workspace='${{ steps.build-ios-app.outputs.workspace }}'"
echo "scheme='${{ steps.build-ios-app.outputs.scheme }}'"
./scripts/run-watch-ui-tests.sh \
"${{ steps.build-ios-app.outputs.workspace }}" \
"${{ steps.build-ios-app.outputs.scheme }}"
timeout-minutes: 90
- name: Upload watchOS port status
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: port-status-watchos
path: artifacts/watch-ui-tests/port-status-watchos.json
if-no-files-found: warn
retention-days: 14
- name: Upload watch artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: watch-ui-tests
path: |
artifacts/watch-ui-tests
artifacts/*.log
artifacts/*-stats.txt
artifacts/vm_time.txt
artifacts/xcodebuild-list.txt
if-no-files-found: warn
retention-days: 14
build-ios-tv:
if: ${{ !inputs.watch_only && needs.select.outputs.ios == 'true' }}
# Native Apple TV (tvOS) screenshot pipeline. The tvOS slice auto-enables
# from codename1.tvMain in the sample, so build-ios-app.sh generates the
# <Main>TV target alongside the iOS app. tvOS reuses the iOS UIApplicationMain
# entry and the Metal renderer (no OpenGL ES on tvOS); this job builds the TV
# target for the appletvsimulator, renders the cn1ss suite and streams frames
# to the same cn1ss test server the iOS/watch jobs use, comparing against
# scripts/ios/screenshots-tv.
#
# BLOCKING (a hard golden gate, like build-ios-watch): the tvOS slice
# compiles end-to-end and the golden set is seeded from a CI capture (see
# Ports/iOSPort/nativeSources/TVOS_PORT.md). A mismatch fails the job.
needs: [select, build-port]
permissions:
contents: read
pull-requests: write
issues: write
runs-on: macos-15
timeout-minutes: 60
env:
GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }}
# The device runner reports logical test failures through CN1SS log
# markers, not through the build or the screenshot comparison. Make the
# normalized report authoritative so a failing or never-run compliance
# test cannot leave this workflow green and then be published from master.
CN1SS_FAIL_ON_TEST_PROBLEMS: '1'
GH_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }}
steps:
- uses: actions/checkout@v6
- name: Cache CocoaPods and user gems
uses: actions/cache@v5
with:
path: |
~/.gem
~/Library/Caches/CocoaPods
~/.cocoapods/repos
key: ${{ runner.os }}-pods-v1-${{ hashFiles('scripts/setup-workspace.sh') }}
restore-keys: |
${{ runner.os }}-pods-v1-
- name: Ensure CocoaPods tooling
run: |
mkdir -p ~/.codenameone
cp maven/UpdateCodenameOne.jar ~/.codenameone/
set -euo pipefail
if ! command -v ruby >/dev/null; then
echo "ruby not found"; exit 1
fi
GEM_USER_DIR="$(ruby -e 'print Gem.user_dir')"
export PATH="$GEM_USER_DIR/bin:$PATH"
if ! command -v pod >/dev/null 2>&1; then
gem install cocoapods xcodeproj --no-document --user-install
fi
pod --version
- name: Compute setup-workspace hash
id: setup_hash
run: |
set -euo pipefail
echo "hash=$(shasum -a 256 scripts/setup-workspace.sh | awk '{print $1}')" >> "$GITHUB_OUTPUT"
- name: Set TMPDIR
run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV
- name: Cache Maven repository
uses: actions/cache@v5
with:
path: ~/.m2/repository
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-m2-
- name: Restore cn1-binaries cache
uses: actions/cache@v5
with:
path: ../cn1-binaries
key: cn1-binaries-${{ runner.os }}-${{ steps.setup_hash.outputs.hash }}
restore-keys: |
cn1-binaries-${{ runner.os }}-
- name: Download built CN1 + iOS port bundle
uses: actions/download-artifact@v4
with:
name: cn1-built-bundle
path: ${{ runner.temp }}
- name: Restore built CN1 + iOS port tree
run: |
set -euo pipefail
# Replaces the evictable cn1-built cache restore with the artifact
# build-port handed off (not LRU-evicted, survives --failed re-runs).
tar -C / -xf "$RUNNER_TEMP/cn1-built-bundle.tar"
- name: Build sample iOS app (generates the tvOS target)
id: build-ios-app
run: ./scripts/build-ios-app.sh -q -DskipTests
timeout-minutes: 30
# The tvOS half of the private-API gate, and the tvOS compile gate. A tvOS slice compiles
# the port with whatever switches the iOS app turned on, and the iOS jobs compile it only
# for iOS -- where every `#if TARGET_OS_TV` branch is preprocessed away -- so a feature
# whose code tvOS cannot compile (contacts, NFC, the photo picker, StoreKit's identifier
# payments ...) broke every TV build of an app using it, unseen. This compiles every
# native with every gate on against BOTH tvOS SDKs: the device SDK differs (it has no
# LocalAuthentication) and no job builds it. The tvOS app itself is binary-checked by
# run-tv-ui-tests.sh after its build.
- name: Check the port compiles for tvOS and references no private Apple API
run: |
set -euo pipefail
# shellcheck source=/dev/null
source scripts/lib/xcode.sh
cn1_select_xcode || exit 1
for sdk in appletvos appletvsimulator; do
scripts/check-ios-private-api.py \
--developer-dir "$DEVELOPER_DIR" --sdk "$sdk" \
--project-dir "$(dirname "${{ steps.build-ios-app.outputs.workspace }}")"/*-src
done
timeout-minutes: 20
- name: Ensure tvOS simulator runtime
run: |
set -euo pipefail
if ! xcrun simctl list runtimes available 2>/dev/null | grep -qi tvOS; then
echo "No tvOS runtime found; attempting download"
xcodebuild -downloadPlatform tvOS || true
fi
xcrun simctl list runtimes available 2>/dev/null | grep -i tvOS || true
xcrun simctl list devices available 2>/dev/null | grep -i "Apple TV" || true
- name: Run tvOS UI screenshot tests
env:
ARTIFACTS_DIR: ${{ github.workspace }}/artifacts/tv-ui-tests
SCREENSHOT_REF_DIR: ${{ github.workspace }}/scripts/ios/screenshots-tv
CN1SS_COMMENT_MARKER: '<!-- CN1SS_IOS_TV_COMMENT -->'
CN1SS_PREVIEW_SUBDIR: ios-tv
CN1SS_REPORT_TITLE: 'Apple TV (tvOS) screenshot updates'
CN1SS_SUCCESS_MESSAGE: '✅ Native Apple TV (tvOS, Metal) screenshot tests passed.'
CN1SS_COMMENT_LOG_PREFIX: '[run-tv-ui-tests]'
CN1SS_FAIL_ON_MISMATCH: '1'
# A missing frame is an incomplete run, not an allowed result.
CN1SS_ALLOWED_MISSING: '0'
run: |
set -euo pipefail
mkdir -p "${ARTIFACTS_DIR}"
echo "workspace='${{ steps.build-ios-app.outputs.workspace }}'"
echo "scheme='${{ steps.build-ios-app.outputs.scheme }}'"
./scripts/run-tv-ui-tests.sh \
"${{ steps.build-ios-app.outputs.workspace }}" \
"${{ steps.build-ios-app.outputs.scheme }}"
timeout-minutes: 45
- name: Upload tvOS port status
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: port-status-tvos
path: artifacts/tv-ui-tests/port-status-tvos.json
if-no-files-found: warn
retention-days: 14
- name: Upload tv artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: tv-ui-tests
path: |
artifacts/tv-ui-tests
artifacts/*.log
artifacts/*-stats.txt
artifacts/vm_time.txt
artifacts/xcodebuild-list.txt
if-no-files-found: warn
retention-days: 14
native:
needs: [select, build-port]
if: ${{ !inputs.watch_only && needs.select.outputs.native == 'true' }}
uses: ./.github/workflows/scripts-ios-native.yml
with:
port_prepared: true
secrets: inherit
packaging:
needs: [select, build-port]
if: ${{ !inputs.watch_only && needs.select.outputs.packaging == 'true' }}
uses: ./.github/workflows/ios-packaging.yml
with:
port_prepared: true
secrets: inherit
catalyst:
permissions:
contents: read
pull-requests: write
issues: write
needs: [select, build-port]
if: ${{ !inputs.watch_only && needs.select.outputs.catalyst == 'true' }}
uses: ./.github/workflows/scripts-mac-catalyst.yml
with:
port_prepared: true
secrets: inherit