From c8bbacb47226902695cf9f8b56202f9d4fd88b8e Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:38:57 +0300 Subject: [PATCH 1/2] Cancel obsolete CI runs and consolidate Apple validation --- .github/ci/apple-checks.json | 264 +++++++++++++ .github/workflows/_build-ios-port.yml | 20 +- .github/workflows/_build-mac-port.yml | 3 - .../workflows/ad-cn1lib-ios-native-check.yml | 212 +++-------- .../workflows/admob-android-runtime-check.yml | 4 +- .github/workflows/ai-cn1lib-android-check.yml | 6 + .github/workflows/ai-cn1lib-native-check.yml | 178 ++------- .github/workflows/ant.yml | 8 +- .github/workflows/archetype-smoke.yml | 8 +- .github/workflows/blog-prose.yml | 12 +- .github/workflows/call-vpn-native.yml | 4 +- .github/workflows/cancel-stale-ci.yml | 35 ++ .github/workflows/car-android.yml | 8 +- .github/workflows/car-ios.yml | 9 +- .../workflows/check-16k-page-alignment.yml | 6 + .github/workflows/check-author-tags.yml | 6 + .../workflows/check-control-characters.yml | 6 + .github/workflows/check-copyright-headers.yml | 6 + .github/workflows/check-since-tags.yml | 6 + .github/workflows/check-workflows.yml | 12 + .github/workflows/cn1playground-language.yml | 6 + .github/workflows/codeql.yml | 8 +- .github/workflows/designer.yml | 8 +- .github/workflows/developer-guide-docs.yml | 34 +- .github/workflows/errorprone.yml | 6 + .github/workflows/guibuilder.yml | 6 +- .github/workflows/health-android.yml | 8 +- .github/workflows/identity-stack.yml | 4 +- .github/workflows/input-validation.yml | 11 +- .../workflows/ios-accessibility-thread.yml | 4 +- .github/workflows/ios-packaging.yml | 62 +-- .github/workflows/ios-push-completion.yml | 4 +- .github/workflows/java-snippet-validation.yml | 6 + .github/workflows/javadocs.yml | 6 + .github/workflows/javase-cef-ffmpeg-smoke.yml | 6 +- .github/workflows/jsport-barrier-lint.yml | 6 + .github/workflows/linux-browser-link.yml | 6 + .github/workflows/linux-build-run.yml | 20 +- .github/workflows/material-icons-update.yml | 6 + .github/workflows/parparvm-parallel-mark.yml | 6 +- .github/workflows/parparvm-selfhost.yml | 4 +- .github/workflows/parparvm-tests-windows.yml | 26 +- .github/workflows/parparvm-tests.yml | 14 +- .github/workflows/port-status-contract.yml | 6 + .github/workflows/pr.yml | 140 ++----- .github/workflows/protocol-e2e.yml | 8 +- .github/workflows/purchase-e2e.yml | 11 +- .github/workflows/scaffolding-parity.yml | 4 +- .github/workflows/scripts-android.yml | 24 +- .github/workflows/scripts-fidelity.yml | 10 +- .github/workflows/scripts-ios-native.yml | 88 +---- .github/workflows/scripts-ios.yml | 354 +++++++++--------- .github/workflows/scripts-javascript.yml | 12 +- .github/workflows/scripts-javase.yml | 8 +- .github/workflows/scripts-mac-catalyst.yml | 112 +----- .github/workflows/scripts-macos.yml | 37 +- .github/workflows/starter-launchers.yml | 6 + .github/workflows/website-docs.yml | 8 +- .github/workflows/windows-cross-build-run.yml | 12 +- .github/workflows/windows-cross-compile.yml | 4 +- .github/workflows/windows-tooling.yml | 6 +- docs/website/data/port_status.json | 2 +- scripts/check-cn1lib-native-coverage.py | 7 +- scripts/check-cn1lib-native-sources.py | 9 +- scripts/ci/cancel-stale-pr-runs.js | 79 ++++ scripts/ci/check-ios-cn1lib.py | 99 +++++ scripts/ci/select-apple-checks.py | 65 ++++ scripts/ci/select-cn1lib-checks.py | 102 +++++ scripts/ci/test-cancel-stale-pr-runs.js | 89 +++++ scripts/ci/test_select_apple_checks.py | 47 +++ scripts/ci/test_select_cn1lib_checks.py | 102 +++++ .../conformance/test_port_status.py | 6 +- 72 files changed, 1548 insertions(+), 989 deletions(-) create mode 100644 .github/ci/apple-checks.json create mode 100644 .github/workflows/cancel-stale-ci.yml create mode 100644 scripts/ci/cancel-stale-pr-runs.js create mode 100644 scripts/ci/check-ios-cn1lib.py create mode 100644 scripts/ci/select-apple-checks.py create mode 100644 scripts/ci/select-cn1lib-checks.py create mode 100644 scripts/ci/test-cancel-stale-pr-runs.js create mode 100644 scripts/ci/test_select_apple_checks.py create mode 100644 scripts/ci/test_select_cn1lib_checks.py diff --git a/.github/ci/apple-checks.json b/.github/ci/apple-checks.json new file mode 100644 index 00000000000..51351a973cf --- /dev/null +++ b/.github/ci/apple-checks.json @@ -0,0 +1,264 @@ +{ + "ios": { + "pull_request": [ + ".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/**", + "!scripts/templates/**/*.md", + "CodenameOne/src/**", + "!CodenameOne/src/**/*.md", + "Ports/iOSPort/**", + "!Ports/iOSPort/**/*.md", + "native-themes/ios-modern/**", + "!native-themes/ios-modern/**/*.md", + "vm/**", + "!vm/**/*.md", + "tests/**", + "!tests/**/*.md", + "!docs/**", + "maven/**", + "!maven/cn1-ai-*/**", + "!maven/cn1-admob/**", + "!maven/cn1-applovin/**", + "!maven/cn1-unity-levelplay/**", + "!maven/core-unittests/**", + "vm/backend/demo/cn1ss/**", + "scripts/lib/cn1ss.sh" + ], + "push": [ + ".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/**", + "!scripts/templates/**/*.md", + "CodenameOne/src/**", + "!CodenameOne/src/**/*.md", + "Ports/iOSPort/**", + "!Ports/iOSPort/**/*.md", + "native-themes/ios-modern/**", + "!native-themes/ios-modern/**/*.md", + "vm/**", + "!vm/**/*.md", + "tests/**", + "!tests/**/*.md", + "!docs/**", + "maven/**", + "!maven/cn1-ai-*/**", + "!maven/cn1-admob/**", + "!maven/cn1-applovin/**", + "!maven/cn1-unity-levelplay/**", + "!maven/core-unittests/**", + "vm/backend/demo/cn1ss/**", + "scripts/lib/cn1ss.sh" + ] + }, + "native": { + "pull_request": [ + ".github/workflows/scripts-ios-native.yml", + ".github/workflows/_build-ios-port.yml", + "scripts/setup-workspace.sh", + "scripts/build-ios-port.sh", + "scripts/build-ios-app.sh", + "scripts/run-ios-native-tests.sh", + "scripts/lib/xcode.sh", + "scripts/ios/create-shared-scheme.py", + "scripts/ios/notification-tests/native-tests/**", + "scripts/ios/notification-tests/install-native-notification-tests.sh", + "scripts/ios/notification-tests/**", + "scripts/hellocodenameone/**", + "scripts/templates/**", + "!scripts/templates/**/*.md", + "CodenameOne/src/**", + "!CodenameOne/src/**/*.md", + "Ports/iOSPort/**", + "!Ports/iOSPort/**/*.md", + "native-themes/ios-modern/**", + "!native-themes/ios-modern/**/*.md", + "vm/**", + "!vm/**/*.md", + "tests/**", + "!tests/**/*.md", + "!docs/**", + "maven/**", + "!maven/cn1-ai-*/**", + "!maven/cn1-admob/**", + "!maven/cn1-applovin/**", + "!maven/cn1-unity-levelplay/**", + "!maven/core-unittests/**" + ], + "push": [ + ".github/workflows/scripts-ios-native.yml", + ".github/workflows/_build-ios-port.yml", + "scripts/setup-workspace.sh", + "scripts/build-ios-port.sh", + "scripts/build-ios-app.sh", + "scripts/run-ios-native-tests.sh", + "scripts/lib/xcode.sh", + "scripts/ios/create-shared-scheme.py", + "scripts/ios/notification-tests/native-tests/**", + "scripts/ios/notification-tests/install-native-notification-tests.sh", + "scripts/ios/notification-tests/**", + "scripts/hellocodenameone/**", + "scripts/templates/**", + "!scripts/templates/**/*.md", + "CodenameOne/src/**", + "!CodenameOne/src/**/*.md", + "Ports/iOSPort/**", + "!Ports/iOSPort/**/*.md", + "native-themes/ios-modern/**", + "!native-themes/ios-modern/**/*.md", + "vm/**", + "!vm/**/*.md", + "tests/**", + "!tests/**/*.md", + "!docs/**", + "maven/**", + "!maven/cn1-ai-*/**", + "!maven/cn1-admob/**", + "!maven/cn1-applovin/**", + "!maven/cn1-unity-levelplay/**", + "!maven/core-unittests/**" + ] + }, + "packaging": { + "pull_request": [ + ".github/workflows/ios-packaging.yml", + ".github/workflows/_build-ios-port.yml", + "maven/codenameone-maven-plugin/**", + "vm/ByteCodeTranslator/**", + "Ports/iOSPort/**", + "scripts/build-ios-app.sh", + "scripts/lib/xcode.sh", + "scripts/run-ios-device-release-build.sh", + "scripts/check-ios-private-api.py", + "scripts/check-ios-sdk-deltas.py", + "scripts/run-ios-ui-tests.sh", + "scripts/run-ios-native-tests.sh", + "scripts/ios/**", + "scripts/hellocodenameone/**", + "!docs/**" + ], + "push": [ + ".github/workflows/ios-packaging.yml", + ".github/workflows/_build-ios-port.yml", + "maven/codenameone-maven-plugin/**", + "vm/ByteCodeTranslator/**", + "Ports/iOSPort/**", + "scripts/build-ios-app.sh", + "scripts/lib/xcode.sh", + "scripts/run-ios-device-release-build.sh", + "scripts/check-ios-private-api.py", + "scripts/check-ios-sdk-deltas.py", + "scripts/run-ios-ui-tests.sh", + "scripts/run-ios-native-tests.sh", + "scripts/ios/**", + "scripts/hellocodenameone/**", + "!docs/**" + ] + }, + "catalyst": { + "pull_request": [ + ".github/workflows/scripts-mac-catalyst.yml", + ".github/workflows/_build-ios-port.yml", + "scripts/setup-workspace.sh", + "scripts/build-ios-port.sh", + "scripts/build-mac-catalyst-app.sh", + "scripts/run-mac-catalyst-ui-tests.sh", + "scripts/hellocodenameone/**", + "scripts/ios/tests/**", + "scripts/mac-catalyst/**", + "scripts/templates/**", + "!scripts/templates/**/*.md", + "scripts/common/java/**", + "scripts/lib/cn1ss.sh", + "scripts/lib/xcode.sh", + "CodenameOne/src/**", + "!CodenameOne/src/**/*.md", + "Ports/iOSPort/**", + "!Ports/iOSPort/**/*.md", + "native-themes/ios-modern/**", + "!native-themes/ios-modern/**/*.md", + "vm/**", + "!vm/**/*.md", + "tests/**", + "!tests/**/*.md", + "maven/**", + "!maven/cn1-ai-*/**", + "!maven/cn1-admob/**", + "!maven/cn1-applovin/**", + "!maven/cn1-unity-levelplay/**", + "!maven/core-unittests/**", + "!docs/**", + "vm/backend/demo/cn1ss/**" + ], + "push": [ + ".github/workflows/scripts-mac-catalyst.yml", + ".github/workflows/_build-ios-port.yml", + "scripts/setup-workspace.sh", + "scripts/build-ios-port.sh", + "scripts/build-mac-catalyst-app.sh", + "scripts/run-mac-catalyst-ui-tests.sh", + "scripts/hellocodenameone/**", + "scripts/ios/tests/**", + "scripts/mac-catalyst/**", + "scripts/templates/**", + "!scripts/templates/**/*.md", + "scripts/common/java/**", + "scripts/lib/cn1ss.sh", + "scripts/lib/xcode.sh", + "CodenameOne/src/**", + "Ports/iOSPort/**", + "native-themes/ios-modern/**", + "vm/**", + "tests/**", + "maven/**", + "!maven/cn1-ai-*/**", + "!maven/cn1-admob/**", + "!maven/cn1-applovin/**", + "!maven/cn1-unity-levelplay/**", + "!maven/core-unittests/**", + "vm/backend/demo/cn1ss/**" + ] + } +} diff --git a/.github/workflows/_build-ios-port.yml b/.github/workflows/_build-ios-port.yml index 1dbca94d61c..08d8389ed25 100644 --- a/.github/workflows/_build-ios-port.yml +++ b/.github/workflows/_build-ios-port.yml @@ -1,17 +1,10 @@ name: _Build iOS port (reusable) -# Reusable workflow that runs scripts/setup-workspace.sh + scripts/build-ios-port.sh -# once per workflow run, populating shared caches that downstream test jobs (in -# scripts-ios.yml, scripts-ios-native.yml, ios-packaging.yml) restore via the -# same cache keys. -# -# A separate "cn1-built" cache is keyed on the CN1 source hash with no restore- -# keys (exact match only). On hit, the entire setup-workspace + build-ios-port -# sequence is skipped — the iOS port artifact in ~/.m2/repository/com/codenameone -# is already correct for the current source state. -# -# This cache is shared across all three iOS workflows on the same branch, so -# whichever workflow runs first populates it and the others skip the rebuild. +# The iOS coordinator calls this once and shares its artifact with UI, +# notification, packaging and Catalyst consumers in the SAME run. Standalone +# diagnostic dispatches can still call it themselves. Caches only accelerate +# later runs; an exact artifact remains the required handoff, even on a cold +# cache or when entries are evicted while consumers wait for a runner. on: workflow_call: @@ -26,9 +19,6 @@ jobs: timeout-minutes: 60 outputs: cn1_built_cache_key: ${{ steps.cn1_built_key.outputs.key }} - concurrency: - group: mac-ios-port-${{ github.workflow }}-${{ github.ref_name }} - cancel-in-progress: true steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/_build-mac-port.yml b/.github/workflows/_build-mac-port.yml index 4c8dd56047b..084963a7a65 100644 --- a/.github/workflows/_build-mac-port.yml +++ b/.github/workflows/_build-mac-port.yml @@ -27,9 +27,6 @@ jobs: timeout-minutes: 60 outputs: cn1_built_mac_cache_key: ${{ steps.cn1_built_mac_key.outputs.key }} - concurrency: - group: mac-port-${{ github.workflow }}-${{ github.ref_name }} - cancel-in-progress: true steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/ad-cn1lib-ios-native-check.yml b/.github/workflows/ad-cn1lib-ios-native-check.yml index d1d282330dc..ffa7e15ab84 100644 --- a/.github/workflows/ad-cn1lib-ios-native-check.yml +++ b/.github/workflows/ad-cn1lib-ios-native-check.yml @@ -24,6 +24,8 @@ on: - 'vm/ByteCodeTranslator/**' - 'vm/pom.xml' - '.github/workflows/ad-cn1lib-ios-native-check.yml' + - 'scripts/ci/select-cn1lib-checks.py' + - 'scripts/ci/check-ios-cn1lib.py' push: branches: [master] paths: @@ -34,188 +36,58 @@ on: - 'vm/ByteCodeTranslator/**' - 'vm/pom.xml' - '.github/workflows/ad-cn1lib-ios-native-check.yml' + - 'scripts/ci/select-cn1lib-checks.py' + - 'scripts/ci/check-ios-cn1lib.py' concurrency: - # These jobs run on macOS, where the runner pool is small and a queued run - # holds its slots until it finishes. Without this, a branch that is pushed - # several times queues every superseded run behind the current one -- five - # obsolete runs of this workflow, 6 macOS jobs each, sat queued on this - # branch before it was added. - # - # Keyed on the pull request number rather than head_ref, which is the source - # branch name with no fork identity in it: two pull requests opened from - # different forks that both use a common branch name -- master, patch-1 -- - # would share a group and cancel each other's check. On push to master there - # is no pull request, so the group falls back to the unique run_id and every - # master commit is still checked in full. - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + # 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: - compile-native-sources: - name: clang ${{ matrix.lib }} (ARC ${{ matrix.arc }}) - runs-on: macos-15 - strategy: - fail-fast: false - matrix: - lib: [cn1-admob, cn1-applovin, cn1-unity-levelplay] - # Both memory models, because which one applies is not ours to assume. - # The generated app target is manual-retain-release - # (CLANG_ENABLE_OBJC_ARC = NO in - # vm/ByteCodeTranslator/src/template/template.xcodeproj), and that is - # what compiles a cn1lib's sources today -- but IPhoneBuilder also adds - # -fobjc-arc to individual files (arcPhaseFixScript does it for - # CN1Vision.m, CN1Language.m and CN1Inference.m), so ARC is reachable - # too. Checking only one model would accept, for instance, a __bridge - # cast that is an error under MRR, or a missing release that ARC hides. - arc: [NO, YES] + select: + runs-on: ubuntu-24.04 + outputs: + libraries: ${{ steps.plan.outputs.ios_ads }} + link: ${{ steps.plan.outputs.admob_link }} steps: - uses: actions/checkout@v6 + - name: Select impacted libraries before allocating a Mac + id: plan + env: + BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} + run: python3 scripts/ci/select-cn1lib-checks.py - - name: Resolve the pod this library pins - id: pod - # Read it out of codenameone_library_required.properties rather than - # repeating it here, so the version the check compiles against is the - # version apps build against, by construction. - run: | - set -euo pipefail - props="maven/${{ matrix.lib }}/common/codenameone_library_required.properties" - line="$(sed -n 's/^codename1\.arg\.ios\.pods=//p' "$props" | head -1)" - if [ -z "$line" ]; then - echo "::error::No codename1.arg.ios.pods in $props"; exit 1 - fi - name="${line%% *}" - version="" - if [ "$name" != "$line" ]; then - version="${line#* }" - fi - echo "name=$name" >> "$GITHUB_OUTPUT" - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "Pod: $name ${version:-(unpinned)}" - - - name: Stage the probe sources - run: | - set -euxo pipefail - PROBE=/tmp/probe-${{ matrix.lib }}-arc${{ matrix.arc }} - mkdir -p "$PROBE/CN1AdProbe" - # Objective-C and Objective-C++ alike. check-cn1lib-native-coverage.py - # counts a .mm file as an iOS native source, so staging only .m would - # let a broken Objective-C++ bridge be reported as covered while - # nothing ever compiled it. - cp maven/${{ matrix.lib }}/ios/src/main/objectivec/*.h "$PROBE/CN1AdProbe/" 2>/dev/null || true - cp maven/${{ matrix.lib }}/ios/src/main/objectivec/*.m "$PROBE/CN1AdProbe/" 2>/dev/null || true - cp maven/${{ matrix.lib }}/ios/src/main/objectivec/*.mm "$PROBE/CN1AdProbe/" 2>/dev/null || true - # An empty probe would build green and check nothing. find is used - # rather than ls because ls reports failure when either glob is - # unmatched, which is the normal case for a library with no .mm. - staged=$(find "$PROBE/CN1AdProbe" -maxdepth 1 \( -name '*.m' -o -name '*.mm' \) | wc -l) - if [ "$staged" -eq 0 ]; then - echo "::error::No Objective-C sources staged for ${{ matrix.lib }}"; exit 1 - fi - - # These sources call back into Java through the generated entry point - # and use JAVA_INT / JAVA_OBJECT / fromNSString without importing - # anything: in a real build the translator's -Prefix.pch pulls in - # cn1_globals.h for them. Reproduce that here, using the port's own - # header so a change to those macros is caught too. - cp vm/ByteCodeTranslator/src/cn1_globals.h "$PROBE/" - # cn1_globals.h includes this one (CN1_RESUME_THREAD yields a virtual - # thread rather than sleeping the carrier it runs on), so staging the - # first without the second stops the probe at "file not found" before it - # compiles a single line of the cn1lib. - cp vm/ByteCodeTranslator/src/cn1_virtual_thread.h "$PROBE/" - # Generated per translation from the app's class list; nothing in the - # ad bridges reads it, so an empty stand-in is enough to let - # cn1_globals.h parse on its own. - printf '#pragma once\n' > "$PROBE/cn1_class_method_index.h" - cat > "$PROBE/CN1AdProbe-Prefix.pch" <<'PCH' - #ifdef __OBJC__ - #import - #import - #endif - #include "cn1_globals.h" - PCH - ls -la "$PROBE" "$PROBE/CN1AdProbe" - - - name: Install xcodegen - run: brew install xcodegen - - - name: Synthesise the Xcode project - run: | - set -euxo pipefail - PROBE=/tmp/probe-${{ matrix.lib }}-arc${{ matrix.arc }} - # The heredoc is quoted so the shell leaves it alone; the matrix - # value below is substituted by Actions before this script runs, so - # the generated project carries a literal YES or NO. - cat > "$PROBE/project.yml" <<'YML' - name: CN1AdProbe - options: - bundleIdPrefix: com.codenameone.cn1ads - deploymentTarget: - iOS: "15.0" - targets: - CN1AdProbe: - type: library.static - platform: iOS - sources: - - path: CN1AdProbe - settings: - base: - CLANG_ENABLE_MODULES: YES - CLANG_ENABLE_OBJC_ARC: ${{ matrix.arc }} - CODE_SIGNING_ALLOWED: NO - GCC_PREFIX_HEADER: CN1AdProbe-Prefix.pch - GCC_PRECOMPILE_PREFIX_HEADER: NO - HEADER_SEARCH_PATHS: $(inherited) $(SRCROOT) - YML - cd "$PROBE" - xcodegen generate --spec project.yml - - - name: pod install - run: | - set -euxo pipefail - PROBE=/tmp/probe-${{ matrix.lib }}-arc${{ matrix.arc }} - cd "$PROBE" - { - echo "platform :ios, '15.0'" - echo "target 'CN1AdProbe' do" - echo " use_frameworks!" - if [ -n "${{ steps.pod.outputs.version }}" ]; then - echo " pod '${{ steps.pod.outputs.name }}', '${{ steps.pod.outputs.version }}'" - else - echo " pod '${{ steps.pod.outputs.name }}'" - fi - echo "end" - } > Podfile - cat Podfile - pod install --repo-update - - - name: xcodebuild - run: | - set -euxo pipefail - PROBE=/tmp/probe-${{ matrix.lib }}-arc${{ matrix.arc }} - cd "$PROBE" - xcodebuild -workspace CN1AdProbe.xcworkspace \ - -scheme CN1AdProbe \ - -configuration Debug \ - -sdk iphonesimulator \ - -destination 'generic/platform=iOS Simulator' \ - CODE_SIGNING_ALLOWED=NO \ - build - - link-admob-app: - name: Link AdMob app (${{ matrix.sdk }}) + native-checks: + # One runner, one checkout/tool installation, and one pod resolution per + # library. Keep both ARC modes and both AdMob link destinations. + needs: select + if: ${{ needs.select.outputs.libraries != '' || needs.select.outputs.link == 'true' }} runs-on: macos-15 - strategy: - fail-fast: false - matrix: - sdk: [iphoneos, iphonesimulator] + timeout-minutes: 120 steps: - uses: actions/checkout@v6 - # Temurin 8 is unavailable on macOS ARM64; Zulu supplies native Java 8. - uses: actions/setup-java@v5 + if: needs.select.outputs.link == 'true' with: distribution: zulu java-version: '8' - - name: Link the native bridge and SDK into an app - run: scripts/check-admob-ios-link.sh "$RUNNER_TEMP/admob-link" "${{ matrix.sdk }}" + - name: Install xcodegen once + if: needs.select.outputs.libraries != '' + run: command -v xcodegen || brew install xcodegen + - name: Compile cn1-admob (MRR and ARC) + if: ${{ !cancelled() && contains(needs.select.outputs.libraries, 'cn1-admob') }} + run: python3 scripts/ci/check-ios-cn1lib.py cn1-admob + - name: Compile cn1-applovin (MRR and ARC) + if: ${{ !cancelled() && contains(needs.select.outputs.libraries, 'cn1-applovin') }} + run: python3 scripts/ci/check-ios-cn1lib.py cn1-applovin + - name: Compile cn1-unity-levelplay (MRR and ARC) + if: ${{ !cancelled() && contains(needs.select.outputs.libraries, 'cn1-unity-levelplay') }} + run: python3 scripts/ci/check-ios-cn1lib.py cn1-unity-levelplay + - name: Link AdMob app (iphoneos) + if: ${{ !cancelled() && needs.select.outputs.link == 'true' }} + run: scripts/check-admob-ios-link.sh "$RUNNER_TEMP/admob-link-iphoneos" "iphoneos" + - name: Link AdMob app (iphonesimulator) + if: ${{ !cancelled() && needs.select.outputs.link == 'true' }} + run: scripts/check-admob-ios-link.sh "$RUNNER_TEMP/admob-link-iphonesimulator" "iphonesimulator" diff --git a/.github/workflows/admob-android-runtime-check.yml b/.github/workflows/admob-android-runtime-check.yml index 8d331069f4e..3f261bd89e3 100644 --- a/.github/workflows/admob-android-runtime-check.yml +++ b/.github/workflows/admob-android-runtime-check.yml @@ -16,7 +16,9 @@ on: - '.github/workflows/admob-android-runtime-check.yml' concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + # 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: diff --git a/.github/workflows/ai-cn1lib-android-check.yml b/.github/workflows/ai-cn1lib-android-check.yml index cb4718ac50d..89a3c3d721b 100644 --- a/.github/workflows/ai-cn1lib-android-check.yml +++ b/.github/workflows/ai-cn1lib-android-check.yml @@ -20,6 +20,12 @@ on: - 'maven/cn1-ai-**' - 'scripts/gen-ai-cn1libs.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: android-native-check: runs-on: ubuntu-24.04 diff --git a/.github/workflows/ai-cn1lib-native-check.yml b/.github/workflows/ai-cn1lib-native-check.yml index e2bafc10b9d..cbfbad00a95 100644 --- a/.github/workflows/ai-cn1lib-native-check.yml +++ b/.github/workflows/ai-cn1lib-native-check.yml @@ -13,160 +13,60 @@ on: - 'maven/cn1-ai-stablediffusion/**' - 'scripts/gen-ai-cn1libs.py' - '.github/workflows/ai-cn1lib-native-check.yml' + - 'scripts/ci/select-cn1lib-checks.py' + - 'scripts/ci/check-ios-cn1lib.py' push: branches: [master] paths: - 'maven/cn1-ai-whisper/**' - 'maven/cn1-ai-stablediffusion/**' - 'scripts/gen-ai-cn1libs.py' + - '.github/workflows/ai-cn1lib-native-check.yml' + - 'scripts/ci/select-cn1lib-checks.py' + - 'scripts/ci/check-ios-cn1lib.py' concurrency: - # These jobs run on macOS, where the runner pool is small and a queued run - # holds its slots until it finishes. Without this, a branch that is pushed - # several times queues every superseded run behind the current one -- five - # obsolete runs of this workflow, 4 macOS jobs each, sat queued on this - # branch before it was added. - # - # Keyed on the pull request number rather than head_ref, which is the source - # branch name with no fork identity in it: two pull requests opened from - # different forks that both use a common branch name -- master, patch-1 -- - # would share a group and cancel each other's check. On push to master there - # is no pull request, so the group falls back to the unique run_id and every - # master commit is still checked in full. - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + # 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: - xcodebuild-cn1libs: - name: xcodebuild ${{ matrix.lib }} (ARC ${{ matrix.arc }}) - # macos-14 ships Xcode 15.4 by default but also has Xcode 16.x - # under /Applications/Xcode_16.X.app. xcodegen 2.45.4 emits - # objectVersion=77 (Xcode 16-format) projects, so we explicitly - # select Xcode 16 via xcode-select in a setup step below. - runs-on: macos-14 - strategy: - fail-fast: false - matrix: - # Each library is compiled under both memory models. The generated app - # target is manual-retain-release (CLANG_ENABLE_OBJC_ARC = NO in - # vm/ByteCodeTranslator/src/template/template.xcodeproj) and that is - # what compiles a cn1lib's sources today, while IPhoneBuilder can add - # -fobjc-arc to an individual file, so ARC is reachable too. An - # ARC-only check accepts code that does not build for customers. - include: - - { lib: cn1-ai-whisper, pod: '', arc: NO } # links static libwhisper.a - - { lib: cn1-ai-whisper, pod: '', arc: YES } - - { lib: cn1-ai-stablediffusion, pod: '', arc: NO } # links Swift runner - - { lib: cn1-ai-stablediffusion, pod: '', arc: YES } + select: + runs-on: ubuntu-24.04 + outputs: + libraries: ${{ steps.plan.outputs.ios_ai }} + link: ${{ steps.plan.outputs.admob_link }} steps: - uses: actions/checkout@v6 + - name: Select impacted libraries before allocating a Mac + id: plan + env: + BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} + run: python3 scripts/ci/select-cn1lib-checks.py - - name: Select Xcode 16 (xcodegen emits Xcode-16-format projects) - # The default xcode-select on macos-14 is Xcode 15.4; that can't - # read xcodegen's objectVersion=77 projects ("future project file - # format"). Pick the highest-version Xcode 16.* available on this - # runner image. + native-checks: + # One runner, one checkout/tool installation, and one pod resolution per + # library. Keep both ARC modes and both AdMob link destinations. + needs: select + if: ${{ needs.select.outputs.libraries != '' }} + runs-on: macos-14 + timeout-minutes: 120 + steps: + - uses: actions/checkout@v6 + - name: Select Xcode 16 run: | - set -euxo pipefail + set -euo pipefail XCODE_PATH=$(ls -d /Applications/Xcode_16*.app 2>/dev/null | sort -V | tail -1) - if [ -z "$XCODE_PATH" ]; then - echo "::error::No Xcode 16.* found in /Applications"; ls /Applications/Xcode_*.app; exit 1 - fi + test -n "$XCODE_PATH" sudo xcode-select -s "$XCODE_PATH/Contents/Developer" xcodebuild -version - - - name: Install xcodegen - # xcodegen produces a full-featured pbxproj from a YAML spec. We need - # a real Xcode project (not a hand-rolled minimal pbxproj) so the - # CocoaPods integration step can inject baseConfigurationReference - # entries and the xcframework -> framework extraction script phase. - run: brew install xcodegen - - - name: Synthesise Xcode project via xcodegen - run: | - set -euxo pipefail - PROBE=/tmp/probe-${{ matrix.lib }}-arc${{ matrix.arc }} - mkdir -p "$PROBE/CN1AIProbe" - # Objective-C and Objective-C++ alike. check-cn1lib-native-coverage.py - # counts a .mm file as an iOS native source, so staging only .m would - # let a broken Objective-C++ bridge be reported as covered while - # nothing ever compiled it. - cp maven/${{ matrix.lib }}/ios/src/main/objectivec/*.h "$PROBE/CN1AIProbe/" 2>/dev/null || true - cp maven/${{ matrix.lib }}/ios/src/main/objectivec/*.m "$PROBE/CN1AIProbe/" 2>/dev/null || true - cp maven/${{ matrix.lib }}/ios/src/main/objectivec/*.mm "$PROBE/CN1AIProbe/" 2>/dev/null || true - # An empty probe would build green and check nothing. find is used - # rather than ls because ls reports failure when either glob is - # unmatched, which is the normal case for a library with no .mm. - staged=$(find "$PROBE/CN1AIProbe" -maxdepth 1 \( -name '*.m' -o -name '*.mm' \) | wc -l) - if [ "$staged" -eq 0 ]; then - echo "::error::No Objective-C sources staged for ${{ matrix.lib }}"; exit 1 - fi - ls -la "$PROBE/CN1AIProbe/" - - cat > "$PROBE/project.yml" <<'YAML' - name: CN1AIProbe - options: - bundleIdPrefix: com.codenameone.cn1ai - deploymentTarget: - iOS: "14.0" - targets: - CN1AIProbe: - # Static library: archives .o files without linking, so cn1libs - # that reference externs supplied by the real build server - # (libwhisper.a for whisper, cn1_sd_generate for stable - # diffusion, the actual ML Kit framework binaries) still - # compile-check successfully. - type: library.static - platform: iOS - sources: - - path: CN1AIProbe - settings: - base: - CLANG_ENABLE_MODULES: YES - CLANG_ENABLE_OBJC_ARC: ${{ matrix.arc }} - CODE_SIGNING_ALLOWED: NO - YAML - cd "$PROBE" - xcodegen generate --spec project.yml - - - name: pod install (with integration) - if: ${{ matrix.pod != '' }} - # With a proper xcodegen-generated pbxproj, CocoaPods can integrate - # normally -- it injects the baseConfigurationReference, sets up the - # framework-extraction script phase, and produces a workspace that - # xcodebuild can build directly. - run: | - set -euxo pipefail - PROBE=/tmp/probe-${{ matrix.lib }}-arc${{ matrix.arc }} - cd "$PROBE" - cat > Podfile <> $GITHUB_ENV - - name: Cache codenameone-tools - uses: actions/cache@v5 - with: - path: ${{ runner.temp }}/codenameone-tools - key: ${{ runner.os }}-cn1-tools-${{ steps.setup_hash.outputs.hash }} - restore-keys: | - ${{ runner.os }}-cn1-tools- - - name: Cache Maven repository uses: actions/cache@v5 with: @@ -283,7 +247,7 @@ jobs: timeout-minutes: 45 - name: Upload packaging artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: ios-packaging diff --git a/.github/workflows/ios-push-completion.yml b/.github/workflows/ios-push-completion.yml index b61feee7c1a..9f249bf5a31 100644 --- a/.github/workflows/ios-push-completion.yml +++ b/.github/workflows/ios-push-completion.yml @@ -23,7 +23,9 @@ permissions: contents: read concurrency: - group: ios-push-completion-${{ github.ref }} + # 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: diff --git a/.github/workflows/java-snippet-validation.yml b/.github/workflows/java-snippet-validation.yml index 65b36029cae..48ea99c3339 100644 --- a/.github/workflows/java-snippet-validation.yml +++ b/.github/workflows/java-snippet-validation.yml @@ -12,6 +12,12 @@ on: - 'scripts/java-snippet-validation-exclusions.jsonl' - '.github/workflows/java-snippet-validation.yml' +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: validate-snippets: runs-on: ubuntu-24.04 diff --git a/.github/workflows/javadocs.yml b/.github/workflows/javadocs.yml index e6315212ce2..e9d5129d402 100644 --- a/.github/workflows/javadocs.yml +++ b/.github/workflows/javadocs.yml @@ -22,6 +22,12 @@ on: - published workflow_dispatch: +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: ${{ github.event_name == 'pull_request' }} + jobs: build-javadocs: runs-on: ubuntu-24.04 diff --git a/.github/workflows/javase-cef-ffmpeg-smoke.yml b/.github/workflows/javase-cef-ffmpeg-smoke.yml index 3aa47845723..197b2b4113b 100644 --- a/.github/workflows/javase-cef-ffmpeg-smoke.yml +++ b/.github/workflows/javase-cef-ffmpeg-smoke.yml @@ -13,7 +13,9 @@ permissions: # each master run is its own group -- superseding applies to PR iteration only, never to # the branch's own history. concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + # 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 on: @@ -127,7 +129,7 @@ jobs: run: python scripts/run-javase-cef-ffmpeg-smoke.py - name: Upload smoke artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: javase-cef-ffmpeg-smoke-${{ runner.os }} diff --git a/.github/workflows/jsport-barrier-lint.yml b/.github/workflows/jsport-barrier-lint.yml index c545593ae42..a66d00ad61a 100644 --- a/.github/workflows/jsport-barrier-lint.yml +++ b/.github/workflows/jsport-barrier-lint.yml @@ -22,6 +22,12 @@ name: JS port barrier-read lint - 'scripts/lint/jsport-canvas-barrier-reads.py' - '.github/workflows/jsport-barrier-lint.yml' +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: barrier-lint: name: JS port canvas barrier-read lint diff --git a/.github/workflows/linux-browser-link.yml b/.github/workflows/linux-browser-link.yml index 0d6e7635153..c49ecafac8d 100644 --- a/.github/workflows/linux-browser-link.yml +++ b/.github/workflows/linux-browser-link.yml @@ -5,6 +5,12 @@ on: workflow_dispatch: permissions: contents: read +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: browser-link: strategy: diff --git a/.github/workflows/linux-build-run.yml b/.github/workflows/linux-build-run.yml index 85fa1babf04..c1e86edfab0 100644 --- a/.github/workflows/linux-build-run.yml +++ b/.github/workflows/linux-build-run.yml @@ -82,7 +82,9 @@ on: - '!native-themes/gnome-adwaita/**/*.md' concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + # 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: @@ -400,7 +402,7 @@ jobs: --hello-app LinuxHelloMain - name: Upload screenshot artifact (${{ matrix.arch }}) - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: linux-screenshot-raw-${{ matrix.arch }} @@ -416,7 +418,7 @@ jobs: # they are compressed, capped, and collected ONLY when one exists, which means # only when the suite actually crashed. - name: Package core dump for offline autopsy - if: always() + if: ${{ !cancelled() }} run: | set -u shopt -s nullglob @@ -457,7 +459,7 @@ jobs: TXT - name: Upload core dump - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: linux-core-${{ matrix.arch }} @@ -469,7 +471,7 @@ jobs: # GTK window showing a Form -- not the headless suite). Download and run it # on a Linux desktop to smoke-test the native port on real hardware. - name: Upload windowed demo binary (${{ matrix.arch }}) - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: linux-demo-${{ matrix.arch }} @@ -548,7 +550,7 @@ jobs: -Dsurefire.failIfNoSpecifiedTests=false ' - name: Upload musl screenshots - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: linux-screenshot-raw-musl @@ -707,7 +709,7 @@ jobs: if [ "$fail" -ne 0 ]; then echo "Linux screenshot gate failed."; exit 1; fi - name: Upload Linux port status - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: port-status-linux @@ -725,11 +727,11 @@ jobs: # columns until it aged out. The report is already on disk in this job, so # publish it directly and depend on no event. # - # if: always() for the same reason the job itself runs on failure -- a + # if: ${{ !cancelled() }} for the same reason the job itself runs on failure -- a # report recording real failures is the one the table most needs. - name: Publish the Linux reports to the data branch if: >- - always() && github.ref == 'refs/heads/master' && + !cancelled() && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') env: diff --git a/.github/workflows/material-icons-update.yml b/.github/workflows/material-icons-update.yml index ed74d12b329..8ef57a01f26 100644 --- a/.github/workflows/material-icons-update.yml +++ b/.github/workflows/material-icons-update.yml @@ -15,6 +15,12 @@ permissions: contents: write pull-requests: write +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: ${{ github.event_name == 'pull_request' }} + jobs: update-material-icons: runs-on: ubuntu-24.04 diff --git a/.github/workflows/parparvm-parallel-mark.yml b/.github/workflows/parparvm-parallel-mark.yml index 38970bc3614..a974516ef4e 100644 --- a/.github/workflows/parparvm-parallel-mark.yml +++ b/.github/workflows/parparvm-parallel-mark.yml @@ -86,7 +86,9 @@ on: - '.github/workflows/parparvm-parallel-mark.yml' concurrency: - group: parparvm-parallel-mark-${{ github.ref }} + # 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: @@ -147,7 +149,7 @@ jobs: working-directory: vm - name: Surefire reports - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: gc-suite-${{ matrix.arch }}-${{ matrix.markers }}marker diff --git a/.github/workflows/parparvm-selfhost.yml b/.github/workflows/parparvm-selfhost.yml index fbf19f74438..6818d5577d0 100644 --- a/.github/workflows/parparvm-selfhost.yml +++ b/.github/workflows/parparvm-selfhost.yml @@ -37,7 +37,9 @@ on: - '!vm/**/docs/**' concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + # 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 env: diff --git a/.github/workflows/parparvm-tests-windows.yml b/.github/workflows/parparvm-tests-windows.yml index f2f83ffdf17..0bd6e65bc04 100644 --- a/.github/workflows/parparvm-tests-windows.yml +++ b/.github/workflows/parparvm-tests-windows.yml @@ -70,11 +70,9 @@ on: - '!vm/**/docs/**' concurrency: - # Cancel superseded runs of this workflow for the same PR branch - # (github.head_ref is set on pull_request events). On push to master - # head_ref is empty, so the group falls back to the unique run_id and - # every master commit is still tested in full -- no coverage lost. - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + # 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 env: @@ -540,7 +538,7 @@ jobs: # Preserve the native cn1WindowsLog output so the offscreen-capture path is # confirmable on the green x64 gate too (compare against the arm64 leg). - name: Collect native windows log (x64) - if: always() + if: ${{ !cancelled() }} shell: pwsh run: | New-Item -ItemType Directory -Force -Path artifacts/windows-port/raw | Out-Null @@ -568,7 +566,7 @@ jobs: --hello-app WinHelloMain - name: Upload screenshot artifact (x64) - if: always() + if: ${{ !cancelled() }} uses: ./.github/actions/upload-artifact-with-retry with: name: windows-port-screenshot-raw-x64 @@ -745,7 +743,7 @@ jobs: # so the raw dir is never empty (the arm64 artifact would otherwise not be # produced when the suite stalls before writing any PNG). - name: Collect native windows log (arm64) - if: always() + if: ${{ !cancelled() }} shell: pwsh run: | New-Item -ItemType Directory -Force -Path artifacts/windows-port/raw | Out-Null @@ -773,7 +771,7 @@ jobs: --hello-app WinHelloMain - name: Upload screenshot artifact (arm64) - if: always() + if: ${{ !cancelled() }} uses: ./.github/actions/upload-artifact-with-retry with: name: windows-port-screenshot-raw-arm64 @@ -796,7 +794,7 @@ jobs: name: screenshot-comment (x64) needs: windows-port-screenshot if: >- - always() && + !cancelled() && needs.windows-port-screenshot.result != 'cancelled' && needs.windows-port-screenshot.result != 'skipped' && (github.event_name == 'pull_request' || github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') @@ -884,7 +882,7 @@ jobs: # native-build diagnostics out of the publisher/backfill port-status-* # selection so a green native run cannot hide a shipping-pipeline failure. - name: Upload Windows native x64 diagnostics - if: always() + if: ${{ !cancelled() }} uses: ./.github/actions/upload-artifact-with-retry with: name: windows-native-x64-diagnostics @@ -895,11 +893,11 @@ jobs: windows-port-screenshot-comment-arm64: name: screenshot-comment (arm64) needs: windows-port-screenshot-arm64 - # The capture job uploads its raw evidence with if: always(). Normalize it + # The capture job uploads its raw evidence with if: ${{ !cancelled() }}. Normalize it # even when capture failed so diagnostics remain available. The failed # capture still remains a blocking red check. if: >- - always() && + !cancelled() && needs.windows-port-screenshot-arm64.result != 'cancelled' && needs.windows-port-screenshot-arm64.result != 'skipped' && (github.event_name == 'pull_request' || github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') @@ -981,7 +979,7 @@ jobs: if [ "$fail" -ne 0 ]; then echo "Windows arm64 screenshot gate failed."; exit 1; fi - name: Upload Windows ARM64 port status - if: always() + if: ${{ !cancelled() }} uses: ./.github/actions/upload-artifact-with-retry with: name: port-status-windows-arm64 diff --git a/.github/workflows/parparvm-tests.yml b/.github/workflows/parparvm-tests.yml index 0e35ae795d9..ba9944b52ff 100644 --- a/.github/workflows/parparvm-tests.yml +++ b/.github/workflows/parparvm-tests.yml @@ -77,11 +77,9 @@ on: - published concurrency: - # Cancel superseded runs of this workflow for the same PR branch - # (github.head_ref is set on pull_request events). On push to master - # head_ref is empty, so the group falls back to the unique run_id and - # every master commit is still tested in full -- no coverage lost. - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + # 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 env: @@ -382,7 +380,7 @@ jobs: JDK_25_HOME: ${{ env.JDK_25_HOME }} - name: Publish ByteCodeTranslator quality previews - if: ${{ always() && github.server_url == 'https://github.com' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + if: ${{ !cancelled() && github.server_url == 'https://github.com' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} id: publish-bytecode-quality-previews env: GITHUB_TOKEN: ${{ github.token }} @@ -440,7 +438,7 @@ jobs: echo "jacoco_url=${preview_base}/coverage/index.html" >> "$GITHUB_OUTPUT" - name: Generate ByteCodeTranslator quality report - if: ${{ always() }} + if: ${{ !cancelled() }} env: QUALITY_REPORT_TARGET_DIRS: vm/tests/target QUALITY_REPORT_SERVER_URL: ${{ github.server_url }} @@ -519,7 +517,7 @@ jobs: run: vm/backend/ws-conformance.sh --arm javase - name: Upload the Autobahn report - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: autobahn-report diff --git a/.github/workflows/port-status-contract.yml b/.github/workflows/port-status-contract.yml index 1601cf301fc..1b67ff0229d 100644 --- a/.github/workflows/port-status-contract.yml +++ b/.github/workflows/port-status-contract.yml @@ -69,6 +69,12 @@ on: permissions: contents: read +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: validate: runs-on: ubuntu-24.04 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 14370afe36e..e44a0476f98 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -57,6 +57,7 @@ on: - 'scripts/check-native-peer-returns.py' - 'scripts/check-cn1lib-android-api.py' - 'scripts/check-cn1lib-native-coverage.py' + - 'scripts/ci/select-cn1lib-checks.py' - 'scripts/check-cn1lib-native-sources.py' - 'scripts/cn1lib-api-check/**' # The certificate wizard's own tests run in this workflow (see "Run Certificate Wizard @@ -124,6 +125,7 @@ on: - 'scripts/check-native-peer-returns.py' - 'scripts/check-cn1lib-android-api.py' - 'scripts/check-cn1lib-native-coverage.py' + - 'scripts/ci/select-cn1lib-checks.py' - 'scripts/check-cn1lib-native-sources.py' - 'scripts/cn1lib-api-check/**' # The certificate wizard's own tests run in this workflow (see "Run Certificate Wizard @@ -153,11 +155,9 @@ env: CN1_NATIVE_VERIFY: strict concurrency: - # Cancel superseded runs of this workflow for the same PR branch - # (github.head_ref is set on pull_request events). On push to master - # head_ref is empty, so the group falls back to the unique run_id and - # every master commit is still tested in full -- no coverage lost. - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + # 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: @@ -321,104 +321,8 @@ jobs: env: BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} run: | - set -euo pipefail - # This job runs inside the pr-ci-container, where GitHub's - # checkout safe.directory entry can be written under a different - # HOME than this shell step uses. Mark the workspace safe before - # diffing the PR so unrelated cn1libs are not rebuilt. git config --global --add safe.directory "$GITHUB_WORKSPACE" - - all_libs=( - cn1-ai-whisper - cn1-ai-stablediffusion - cn1-admob - cn1-applovin - cn1-unity-levelplay - cn1-ads-mock - ) - ai_libs=( - cn1-ai-whisper - cn1-ai-stablediffusion - ) - - selected_libs=() - add_lib() { - local lib="$1" - local selected_lib - [ -d "maven/$lib" ] || return 0 - for selected_lib in "${selected_libs[@]}"; do - [ "$selected_lib" = "$lib" ] && return 0 - done - selected_libs+=("$lib") - } - add_all() { - local lib - for lib in "${all_libs[@]}"; do add_lib "$lib"; done - } - add_ai() { - local lib - for lib in "${ai_libs[@]}"; do add_lib "$lib"; done - } - - changed_files="" - if [ -n "${BASE_SHA:-}" ] && [[ "$BASE_SHA" != 0000000000000000000000000000000000000000 ]]; then - git fetch --no-tags --depth=1 origin "$BASE_SHA" || true - changed_files="$(git diff --name-only "$BASE_SHA" HEAD || true)" - fi - if [ -z "$changed_files" ]; then - echo "Unable to determine changed files; building all AI/ad cn1libs." - add_all - else - while IFS= read -r file; do - case "$file" in - maven/pom.xml|maven/codenameone-maven-plugin/*) - add_all - ;; - scripts/gen-ai-cn1libs.py) - add_ai - ;; - maven/cn1-ai-*/*|maven/cn1-admob/*|maven/cn1-applovin/*|maven/cn1-unity-levelplay/*|maven/cn1-ads-mock/*|maven/cn1-ads-mock) - lib="${file#maven/}" - lib="${lib%%/*}" - add_lib "$lib" - ;; - esac - done <<< "$changed_files" - fi - - libs=() - modules=() - for lib in "${all_libs[@]}"; do - selected="false" - for selected_lib in "${selected_libs[@]}"; do - if [ "$selected_lib" = "$lib" ]; then - selected="true" - break - fi - done - if [ "$selected" = "true" ]; then - libs+=("$lib") - if [ "$lib" = "cn1-ads-mock" ]; then - modules+=("$lib") - else - modules+=("$lib/common") - fi - fi - done - - { - printf 'libs=' - (IFS=,; printf '%s' "${libs[*]:-}") - printf '\nmodules=' - (IFS=,; printf '%s' "${modules[*]:-}") - printf '\n' - } >> "$GITHUB_OUTPUT" - - if [ ${#libs[@]} -eq 0 ]; then - echo "No AI/ad cn1lib changes detected; skipping cn1lib packaging step." - else - echo "AI/ad cn1libs selected for packaging: ${libs[*]}" - fi + python3 scripts/ci/select-cn1lib-checks.py - name: Build changed/impacted AI and ad cn1libs if: ${{ matrix.java-version == 8 && steps.cn1libs.outputs.modules != '' }} # Exercises every cn1-ai-* and ad-provider multi-module project: @@ -557,7 +461,7 @@ jobs: if: ${{ matrix.java-version == 8 }} run: python3 scripts/check-cn1lib-native-coverage.py - name: Install C compilers for the cn1lib native source check - if: ${{ matrix.java-version == 8 }} + if: ${{ matrix.java-version == 8 && steps.cn1libs.outputs.desktop != '' }} # gcc for the Linux glue and the mingw-w64 cross compiler for the # Windows glue, whose _WIN32 half (windows.h, LoadLibraryA, # GetProcAddress) does not exist for a host target -- compiled as host @@ -571,19 +475,25 @@ jobs: # glue the native win32/Linux ports compile into the app, and the # JavaScript port implementations, which have no build step at all. - name: Check cn1lib desktop and JavaScript native sources - if: ${{ matrix.java-version == 8 }} - run: python3 scripts/check-cn1lib-native-sources.py --require-all + if: ${{ matrix.java-version == 8 && steps.cn1libs.outputs.desktop != '' }} + env: + SELECTED_LIBS: ${{ steps.cn1libs.outputs.desktop }} + run: | + IFS=',' read -r -a libs <<< "$SELECTED_LIBS" + python3 scripts/check-cn1lib-native-sources.py --require-all "${libs[@]}" # The Android half of the same gap: compile every cn1lib's Android # sources against the artifacts its android.gradleDep pins. Runs on the # JDK the Android build uses, because the pinned SDKs ship class files # javac 8 cannot read. - name: Check cn1lib Android sources against pinned SDKs - if: ${{ matrix.java-version == 8 }} + if: ${{ matrix.java-version == 8 && steps.cn1libs.outputs.android != '' }} env: + SELECTED_LIBS: ${{ steps.cn1libs.outputs.android }} CN1_BINARIES: ${{ github.workspace }}/maven/target/cn1-binaries run: | + IFS=',' read -r -a libs <<< "$SELECTED_LIBS" JAVA17_HOME="${JAVA_HOME_17}" \ - python3 scripts/check-cn1lib-android-api.py --require-all + python3 scripts/check-cn1lib-android-api.py --require-all "${libs[@]}" - name: Check native method signatures if: ${{ matrix.java-version == 8 }} env: @@ -690,7 +600,7 @@ jobs: JAVA_HOME="${JAVA_HOME_17}" xvfb-run bash ../../scripts/ci/retry.sh \ mvn -B -pl common test -Dcn1.certificatewizard.skipTests=false - name: Generate static analysis HTML summaries - if: ${{ always() && matrix.java-version == 8 }} + if: ${{ !cancelled() && matrix.java-version == 8 }} env: QUALITY_REPORT_TARGET_DIRS: maven/core-unittests/target:maven/android/target:maven/ios/target:vm/ByteCodeTranslator/target:maven/codenameone-maven-plugin/target:maven/build-hint-catalog/target:maven/build-hint-tools/target:maven/backend/target:maven/backend-parparvm/target QUALITY_REPORT_SERVER_URL: ${{ github.server_url }} @@ -699,7 +609,7 @@ jobs: QUALITY_REPORT_GENERATE_HTML_ONLY: "1" run: python3 .github/scripts/generate-quality-report.py - name: Collect quality artifacts - if: ${{ always() && matrix.java-version == 8 }} + if: ${{ !cancelled() && matrix.java-version == 8 }} run: | set -euo pipefail mkdir -p quality-artifacts/static-analysis @@ -747,14 +657,14 @@ jobs: echo "No quality artifacts were generated." > quality-artifacts/README.txt fi - name: Upload quality artifacts - if: ${{ always() && matrix.java-version == 8 }} + if: ${{ !cancelled() && matrix.java-version == 8 }} id: upload-quality-artifacts uses: actions/upload-artifact@v7 with: name: quality-artifacts path: quality-artifacts - name: Publish quality report previews - if: ${{ always() && matrix.java-version == 8 && github.server_url == 'https://github.com' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + if: ${{ !cancelled() && matrix.java-version == 8 && github.server_url == 'https://github.com' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} id: publish-quality-previews env: GITHUB_TOKEN: ${{ github.token }} @@ -829,7 +739,7 @@ jobs: echo "jacoco_url=${preview_base}/coverage/index.html" >> "$GITHUB_OUTPUT" fi - name: Generate quality report summary - if: ${{ always() && matrix.java-version == 8 }} + if: ${{ !cancelled() && matrix.java-version == 8 }} env: QUALITY_REPORT_TARGET_DIRS: maven/core-unittests/target:maven/android/target:maven/ios/target:vm/ByteCodeTranslator/target:maven/codenameone-maven-plugin/target:maven/build-hint-catalog/target:maven/build-hint-tools/target:maven/backend/target:maven/backend-parparvm/target # Every project listed here must produce a SpotBugs report and that @@ -854,7 +764,7 @@ jobs: JACOCO_HTML_URL: ${{ steps.publish-quality-previews.outputs.jacoco_url }} run: python3 .github/scripts/generate-quality-report.py - name: Upload quality report summary - if: ${{ always() && matrix.java-version == 8 }} + if: ${{ !cancelled() && matrix.java-version == 8 }} uses: actions/upload-artifact@v7 with: name: quality-report @@ -862,10 +772,10 @@ jobs: # Guards the property that the step below cannot fail this build. It # could, and did: a 504 publishing a green report failed the job. - name: Check the quality comment publisher cannot fail a build - if: ${{ always() && matrix.java-version == 8 }} + if: ${{ !cancelled() && matrix.java-version == 8 }} run: node .github/scripts/test-publish-quality-comment.mjs - name: Publish quality report comment - if: ${{ always() && github.event_name == 'pull_request' && matrix.java-version == 8 && hashFiles('quality-report.md') != '' }} + if: ${{ !cancelled() && github.event_name == 'pull_request' && matrix.java-version == 8 && hashFiles('quality-report.md') != '' }} uses: actions/github-script@v9 with: # Retried rather than fatal on a transient 5xx. The publisher diff --git a/.github/workflows/protocol-e2e.yml b/.github/workflows/protocol-e2e.yml index 22a89c38613..c3edc3f5a86 100644 --- a/.github/workflows/protocol-e2e.yml +++ b/.github/workflows/protocol-e2e.yml @@ -26,6 +26,12 @@ on: - '.github/workflows/protocol-e2e.yml' workflow_dispatch: +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: protocol-e2e: runs-on: ubuntu-24.04 @@ -69,7 +75,7 @@ jobs: scripts/protocol-e2e/run-protocol-e2e.sh - name: Upload server log - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: protocol-e2e-server-log diff --git a/.github/workflows/purchase-e2e.yml b/.github/workflows/purchase-e2e.yml index 1e1caec21f6..bcf5120bd0b 100644 --- a/.github/workflows/purchase-e2e.yml +++ b/.github/workflows/purchase-e2e.yml @@ -69,7 +69,9 @@ permissions: packages: read concurrency: - group: purchase-e2e-${{ github.workflow }}-${{ github.ref_name }} + # 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: @@ -134,9 +136,6 @@ jobs: contents: read runs-on: macos-15 timeout-minutes: 45 - concurrency: - group: mac-ci-${{ github.workflow }}-${{ github.ref_name }} - cancel-in-progress: true steps: - uses: actions/checkout@v6 @@ -225,7 +224,7 @@ jobs: timeout-minutes: 25 - name: Upload native iOS purchase artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: ios-purchase-tests @@ -356,7 +355,7 @@ jobs: force-avd-creation: true script: cd "${{ steps.build-android-app.outputs.gradle_project_dir }}" && ./gradlew --no-daemon --stacktrace connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.codenameone.examples.purchasetest.PurchaseBillingInstrumentationTest - name: Upload Android purchase test report - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: android-purchase-tests diff --git a/.github/workflows/scaffolding-parity.yml b/.github/workflows/scaffolding-parity.yml index 9803519eb0c..79b3b8643c0 100644 --- a/.github/workflows/scaffolding-parity.yml +++ b/.github/workflows/scaffolding-parity.yml @@ -43,7 +43,9 @@ on: - '.github/workflows/scaffolding-parity.yml' concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + # 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: diff --git a/.github/workflows/scripts-android.yml b/.github/workflows/scripts-android.yml index 87eefecd4d0..3fee594bff4 100644 --- a/.github/workflows/scripts-android.yml +++ b/.github/workflows/scripts-android.yml @@ -115,11 +115,9 @@ name: Test Android build scripts # and edit the CN1SS_GH_TOKEN to use the new token concurrency: - # Cancel superseded runs of this workflow for the same PR branch - # (github.head_ref is set on pull_request events). On push to master - # head_ref is empty, so the group falls back to the unique run_id and - # every master commit is still tested in full -- no coverage lost. - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + # 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: @@ -298,7 +296,7 @@ jobs: if: matrix.id == 'default' run: ./scripts/verify-android-app-compile-sdk.sh "${{ steps.build-android-app.outputs.gradle_project_dir }}" 37 - name: Upload Android port status - if: always() && matrix.id == 'default' + if: ${{ !cancelled() && matrix.id == 'default' }} uses: actions/upload-artifact@v7 with: name: port-status-android @@ -306,7 +304,7 @@ jobs: if-no-files-found: warn retention-days: 14 - name: Upload emulator screenshot - if: always() && matrix.id == 'default' + if: ${{ !cancelled() && matrix.id == 'default' }} uses: actions/upload-artifact@v7 with: name: emulator-screenshot @@ -315,7 +313,7 @@ jobs: retention-days: 14 compression-level: 6 - name: Upload emulator screenshot for JDK matrix - if: always() && matrix.id != 'default' + if: ${{ !cancelled() && matrix.id != 'default' }} uses: actions/upload-artifact@v7 with: name: emulator-screenshot-${{ matrix.id }} @@ -324,7 +322,7 @@ jobs: retention-days: 14 compression-level: 6 - name: Upload Android Gradle logs - if: always() && matrix.id == 'default' + if: ${{ !cancelled() && matrix.id == 'default' }} uses: actions/upload-artifact@v7 with: name: android-gradle-logs @@ -339,7 +337,7 @@ jobs: # this upload step disappears, the next decode flake will be # un-debuggable: keep it. - name: Upload Android instrumentation logs - if: always() && matrix.id == 'default' + if: ${{ !cancelled() && matrix.id == 'default' }} uses: actions/upload-artifact@v7 with: name: android-instrumentation-logs @@ -355,7 +353,7 @@ jobs: retention-days: 14 compression-level: 6 - name: Upload Android instrumentation logs for JDK matrix - if: always() && matrix.id != 'default' + if: ${{ !cancelled() && matrix.id != 'default' }} uses: actions/upload-artifact@v7 with: name: android-instrumentation-logs-${{ matrix.id }} @@ -367,7 +365,7 @@ jobs: retention-days: 14 compression-level: 6 - name: Upload Android test report - if: always() && matrix.id == 'default' + if: ${{ !cancelled() && matrix.id == 'default' }} uses: actions/upload-artifact@v7 with: name: android-test-report @@ -376,7 +374,7 @@ jobs: retention-days: 14 compression-level: 6 - name: Upload Android Jacoco coverage report - if: always() && matrix.id == 'default' + if: ${{ !cancelled() && matrix.id == 'default' }} uses: actions/upload-artifact@v7 with: name: android-jacoco-coverage diff --git a/.github/workflows/scripts-fidelity.yml b/.github/workflows/scripts-fidelity.yml index dba14c8a020..829f7597caa 100644 --- a/.github/workflows/scripts-fidelity.yml +++ b/.github/workflows/scripts-fidelity.yml @@ -102,7 +102,9 @@ name: Native theme fidelity - 'vm/backend/demo/cn1ss/**' concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + # 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: @@ -196,7 +198,7 @@ jobs: # FIDELITY_UPDATE_BASELINE=1 and commit the updated baseline JSON. script: adb shell wm size 480x800 && adb shell wm density 160 && ./scripts/run-android-fidelity-tests.sh "${{ steps.build.outputs.gradle_project_dir }}" - name: Upload fidelity artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: android-fidelity @@ -308,7 +310,7 @@ jobs: # FIDELITY_UPDATE_BASELINE=1 and commit the updated baseline JSON. run: ./scripts/run-ios-fidelity-tests.sh "${{ steps.simapp.outputs.app_path }}" "${{ steps.sim.outputs.udid }}" - name: Upload fidelity artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: ios-fidelity @@ -441,7 +443,7 @@ jobs: # FIDELITY_UPDATE_BASELINE=1 and commit the updated baseline JSON. run: ./scripts/run-ios-fidelity-tests.sh "${{ steps.simapp.outputs.app_path }}" "${{ steps.sim.outputs.udid }}" - name: Upload fidelity artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: ios-fidelity-27 diff --git a/.github/workflows/scripts-ios-native.yml b/.github/workflows/scripts-ios-native.yml index 128d51d2f3a..73f88e69c78 100644 --- a/.github/workflows/scripts-ios-native.yml +++ b/.github/workflows/scripts-ios-native.yml @@ -1,80 +1,32 @@ name: Test iOS native test scripts +# Automatic triggers and changed-file selection are owned by scripts-ios.yml. on: - pull_request: - paths: - - '.github/workflows/scripts-ios-native.yml' - - '.github/workflows/_build-ios-port.yml' - - 'scripts/setup-workspace.sh' - - 'scripts/build-ios-port.sh' - - 'scripts/build-ios-app.sh' - - 'scripts/run-ios-native-tests.sh' - - 'scripts/lib/xcode.sh' - - 'scripts/ios/create-shared-scheme.py' - - 'scripts/ios/notification-tests/native-tests/**' - - 'scripts/ios/notification-tests/install-native-notification-tests.sh' - - 'scripts/ios/notification-tests/**' - - 'scripts/hellocodenameone/**' - - 'scripts/templates/**' - - '!scripts/templates/**/*.md' - - 'CodenameOne/src/**' - - '!CodenameOne/src/**/*.md' - - 'Ports/iOSPort/**' - - '!Ports/iOSPort/**/*.md' - - 'native-themes/ios-modern/**' - - '!native-themes/ios-modern/**/*.md' - - 'vm/**' - - '!vm/**/*.md' - - 'tests/**' - - '!tests/**/*.md' - - '!docs/**' - - 'maven/**' - - '!maven/core-unittests/**' - push: - branches: [ master ] - paths: - - '.github/workflows/scripts-ios-native.yml' - - '.github/workflows/_build-ios-port.yml' - - 'scripts/setup-workspace.sh' - - 'scripts/build-ios-port.sh' - - 'scripts/build-ios-app.sh' - - 'scripts/run-ios-native-tests.sh' - - 'scripts/lib/xcode.sh' - - 'scripts/ios/create-shared-scheme.py' - - 'scripts/ios/notification-tests/native-tests/**' - - 'scripts/ios/notification-tests/install-native-notification-tests.sh' - - 'scripts/ios/notification-tests/**' - - 'scripts/hellocodenameone/**' - - 'scripts/templates/**' - - '!scripts/templates/**/*.md' - - 'CodenameOne/src/**' - - '!CodenameOne/src/**/*.md' - - 'Ports/iOSPort/**' - - '!Ports/iOSPort/**/*.md' - - 'native-themes/ios-modern/**' - - '!native-themes/ios-modern/**/*.md' - - 'vm/**' - - '!vm/**/*.md' - - 'tests/**' - - '!tests/**/*.md' - - '!docs/**' - - 'maven/**' - - '!maven/core-unittests/**' + workflow_dispatch: + workflow_call: + inputs: + port_prepared: + type: boolean + default: false + +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: native-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: build-port: + if: ${{ !inputs.port_prepared }} uses: ./.github/workflows/_build-ios-port.yml native-ios: + if: ${{ !cancelled() && (inputs.port_prepared || needs.build-port.result == 'success') }} needs: build-port permissions: contents: read runs-on: macos-15 timeout-minutes: 45 - concurrency: - group: mac-ci-${{ github.workflow }}-${{ github.ref_name }} - cancel-in-progress: true - steps: - uses: actions/checkout@v6 @@ -131,14 +83,6 @@ jobs: - name: Set TMPDIR run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV - - name: Cache codenameone-tools - uses: actions/cache@v5 - with: - path: ${{ runner.temp }}/codenameone-tools - key: ${{ runner.os }}-cn1-tools-${{ steps.setup_hash.outputs.hash }} - restore-keys: | - ${{ runner.os }}-cn1-tools- - - name: Cache Maven repository uses: actions/cache@v5 with: @@ -188,7 +132,7 @@ jobs: timeout-minutes: 45 - name: Upload native iOS artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: ios-native-tests diff --git a/.github/workflows/scripts-ios.yml b/.github/workflows/scripts-ios.yml index 24747182c68..307d1a58021 100644 --- a/.github/workflows/scripts-ios.yml +++ b/.github/workflows/scripts-ios.yml @@ -1,9 +1,6 @@ name: Test iOS UI build scripts on: - # Manual dispatch: this PR's changed-file count exceeds GitHub's paths-filter - # diff limit, so pull_request triggers stopped firing -- dispatch with - # gh workflow run --ref workflow_dispatch: inputs: watch_only: @@ -12,139 +9,157 @@ on: default: false type: boolean schedule: - - cron: '15 1 * * *' + - 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' - # The iOS legs boot their simulator through this helper and read its - # `udid` output, so a change to it alone must exercise them. - - '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/**' - - '!scripts/templates/**/*.md' - - 'CodenameOne/src/**' - - '!CodenameOne/src/**/*.md' - - 'Ports/iOSPort/**' - - '!Ports/iOSPort/**/*.md' - - 'native-themes/ios-modern/**' - - '!native-themes/ios-modern/**/*.md' - - 'vm/**' - - '!vm/**/*.md' - - 'tests/**' - - '!tests/**/*.md' - - '!docs/**' - - 'maven/**' - - '!maven/core-unittests/**' - # The screenshot transport is now a Codename One backend application, so the - # two files that are ONLY exercised here have to trigger this leg: the - # server itself and the script that launches it. - # - # Deliberately NOT the rest of vm/backend. The websocket protocol code is - # covered by maven/backend's unit tests, by vm/tests against the translated - # binary, and by the Autobahn conformance job -- all of which already - # trigger on vm/**. Fanning every backend change out to the macOS and - # emulator legs as well would buy nothing and cost a great deal: a change to - # the ORM, the database layer or the HTTP parser cannot reach this - # transport. - - 'vm/backend/demo/cn1ss/**' - - 'scripts/lib/cn1ss.sh' + - .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/** + - vm/backend/demo/cn1ss/** + - 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 ] + 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' - # The iOS legs boot their simulator through this helper and read its - # `udid` output, so a change to it alone must exercise them. - - '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/**' - - '!scripts/templates/**/*.md' - - 'CodenameOne/src/**' - - '!CodenameOne/src/**/*.md' - - 'Ports/iOSPort/**' - - '!Ports/iOSPort/**/*.md' - - 'native-themes/ios-modern/**' - - '!native-themes/ios-modern/**/*.md' - - 'vm/**' - - '!vm/**/*.md' - - 'tests/**' - - '!tests/**/*.md' - - '!docs/**' - - 'maven/**' - - '!maven/core-unittests/**' - # The screenshot transport is now a Codename One backend application, so the - # two files that are ONLY exercised here have to trigger this leg: the - # server itself and the script that launches it. - # - # Deliberately NOT the rest of vm/backend. The websocket protocol code is - # covered by maven/backend's unit tests, by vm/tests against the translated - # binary, and by the Autobahn conformance job -- all of which already - # trigger on vm/**. Fanning every backend change out to the macOS and - # emulator legs as well would buy nothing and cost a great deal: a change to - # the ORM, the database layer or the HTTP parser cannot reach this - # transport. - - 'vm/backend/demo/cn1ss/**' - - 'scripts/lib/cn1ss.sh' + - .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/** + - vm/backend/demo/cn1ss/** + - 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 }} + 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: build-port + needs: [select, build-port] permissions: contents: read pull-requests: write issues: write runs-on: macos-15 timeout-minutes: 75 - concurrency: - group: mac-ci-${{ github.workflow }}-metal-${{ github.ref_name }} - cancel-in-progress: true - env: GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} # The device runner reports logical test failures through CN1SS log @@ -214,14 +229,6 @@ jobs: - name: Set TMPDIR run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV - - name: Cache codenameone-tools - uses: actions/cache@v5 - with: - path: ${{ runner.temp }}/codenameone-tools - key: ${{ runner.os }}-cn1-tools-${{ steps.setup_hash.outputs.hash }} - restore-keys: | - ${{ runner.os }}-cn1-tools- - - name: Cache Maven repository uses: actions/cache@v5 with: @@ -409,7 +416,7 @@ jobs: timeout-minutes: 65 - name: Upload iOS Metal port status - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: port-status-ios-metal @@ -433,7 +440,7 @@ jobs: # 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: always() + 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 @@ -470,7 +477,7 @@ jobs: # 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: always() + 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 @@ -498,7 +505,7 @@ jobs: fi - name: Upload iOS Metal artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: ios-ui-tests-metal @@ -515,7 +522,7 @@ jobs: build-ios-metal-27: - if: ${{ !inputs.watch_only }} + 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 @@ -530,17 +537,13 @@ jobs: # `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: build-port + needs: [select, build-port] permissions: contents: read pull-requests: write issues: write runs-on: xcode-27 timeout-minutes: 75 - concurrency: - group: mac-ci-${{ github.workflow }}-metal27-${{ github.ref_name }} - cancel-in-progress: true - env: GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} # The device runner reports logical test failures through CN1SS log @@ -621,14 +624,6 @@ jobs: - name: Set TMPDIR run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV - - name: Cache codenameone-tools - uses: actions/cache@v5 - with: - path: ${{ runner.temp }}/codenameone-tools - key: ${{ runner.os }}-cn1-tools-${{ steps.setup_hash.outputs.hash }} - restore-keys: | - ${{ runner.os }}-cn1-tools- - - name: Cache Maven repository uses: actions/cache@v5 with: @@ -856,7 +851,7 @@ jobs: # 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: always() + 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 @@ -893,7 +888,7 @@ jobs: # 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: always() + 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 @@ -921,7 +916,7 @@ jobs: fi - name: Upload iOS Metal artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: ios-ui-tests-metal-27 @@ -936,6 +931,7 @@ jobs: 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
Watch target alongside the iOS app. This job renders @@ -944,17 +940,13 @@ jobs: # 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: build-port + needs: [select, build-port] permissions: contents: read pull-requests: write issues: write runs-on: macos-15 timeout-minutes: 60 - concurrency: - group: mac-ci-${{ github.workflow }}-watch-${{ github.ref_name }} - cancel-in-progress: true - env: GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} # The device runner reports logical test failures through CN1SS log @@ -1006,14 +998,6 @@ jobs: - name: Set TMPDIR run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV - - name: Cache codenameone-tools - uses: actions/cache@v5 - with: - path: ${{ runner.temp }}/codenameone-tools - key: ${{ runner.os }}-cn1-tools-${{ steps.setup_hash.outputs.hash }} - restore-keys: | - ${{ runner.os }}-cn1-tools- - - name: Cache Maven repository uses: actions/cache@v5 with: @@ -1081,7 +1065,7 @@ jobs: timeout-minutes: 90 - name: Upload watchOS port status - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: port-status-watchos @@ -1090,7 +1074,7 @@ jobs: retention-days: 14 - name: Upload watch artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: watch-ui-tests @@ -1104,7 +1088,7 @@ jobs: retention-days: 14 build-ios-tv: - if: ${{ !inputs.watch_only }} + 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 #
TV target alongside the iOS app. tvOS reuses the iOS UIApplicationMain @@ -1116,17 +1100,13 @@ jobs: # 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: build-port + needs: [select, build-port] permissions: contents: read pull-requests: write issues: write runs-on: macos-15 timeout-minutes: 60 - concurrency: - group: mac-ci-${{ github.workflow }}-tv-${{ github.ref_name }} - cancel-in-progress: true - env: GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} # The device runner reports logical test failures through CN1SS log @@ -1174,14 +1154,6 @@ jobs: - name: Set TMPDIR run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV - - name: Cache codenameone-tools - uses: actions/cache@v5 - with: - path: ${{ runner.temp }}/codenameone-tools - key: ${{ runner.os }}-cn1-tools-${{ steps.setup_hash.outputs.hash }} - restore-keys: | - ${{ runner.os }}-cn1-tools- - - name: Cache Maven repository uses: actions/cache@v5 with: @@ -1269,7 +1241,7 @@ jobs: timeout-minutes: 45 - name: Upload tvOS port status - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: port-status-tvos @@ -1278,7 +1250,7 @@ jobs: retention-days: 14 - name: Upload tv artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: tv-ui-tests @@ -1290,3 +1262,31 @@ jobs: 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 diff --git a/.github/workflows/scripts-javascript.yml b/.github/workflows/scripts-javascript.yml index ae88e35d54d..eb3b2f4ca00 100644 --- a/.github/workflows/scripts-javascript.yml +++ b/.github/workflows/scripts-javascript.yml @@ -96,11 +96,9 @@ on: - 'vm/backend/demo/cn1ss/**' concurrency: - # Cancel superseded runs of this workflow for the same PR branch - # (github.head_ref is set on pull_request events). On push to master - # head_ref is empty, so the group falls back to the unique run_id and - # every master commit is still tested in full -- no coverage lost. - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + # 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: @@ -373,7 +371,7 @@ jobs: ./scripts/run-javascript-browser-tests.sh "${{ steps.locate_bundle.outputs.bundle }}" "${GITHUB_WORKSPACE}/scripts/javascript/screenshots" - name: Upload JavaScript port status - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: port-status-javascript @@ -382,7 +380,7 @@ jobs: retention-days: 14 - name: Upload JavaScript screenshot artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: javascript-ui-tests diff --git a/.github/workflows/scripts-javase.yml b/.github/workflows/scripts-javase.yml index 84633599d0c..eddfbc082df 100644 --- a/.github/workflows/scripts-javase.yml +++ b/.github/workflows/scripts-javase.yml @@ -27,6 +27,12 @@ on: - 'Ports/JavaSE/**' - '!Ports/JavaSE/**/*.md' +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: javase-simulator-tests: permissions: @@ -71,7 +77,7 @@ jobs: run: ./scripts/run-javase-simulator-integration-tests.sh - name: Upload JavaSE simulator artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: javase-simulator-tests diff --git a/.github/workflows/scripts-mac-catalyst.yml b/.github/workflows/scripts-mac-catalyst.yml index 2979915f4ce..bec32802ab8 100644 --- a/.github/workflows/scripts-mac-catalyst.yml +++ b/.github/workflows/scripts-mac-catalyst.yml @@ -14,95 +14,31 @@ name: Test Mac Catalyst UI build scripts # reusable _build-ios-port.yml workflow) so cache hits across the three # Mac/iOS workflows on the same SHA stay fast. +# Automatic triggers and changed-file selection are owned by scripts-ios.yml. on: workflow_dispatch: - schedule: - - cron: '55 1 * * *' - pull_request: - paths: - - '.github/workflows/scripts-mac-catalyst.yml' - - '.github/workflows/_build-ios-port.yml' - - 'scripts/setup-workspace.sh' - - 'scripts/build-ios-port.sh' - - 'scripts/build-mac-catalyst-app.sh' - - 'scripts/run-mac-catalyst-ui-tests.sh' - - 'scripts/hellocodenameone/**' - - 'scripts/ios/tests/**' - - 'scripts/mac-catalyst/**' - - 'scripts/templates/**' - - '!scripts/templates/**/*.md' - - 'scripts/common/java/**' - - 'scripts/lib/cn1ss.sh' - - 'scripts/lib/xcode.sh' - - 'CodenameOne/src/**' - - '!CodenameOne/src/**/*.md' - - 'Ports/iOSPort/**' - - '!Ports/iOSPort/**/*.md' - - 'native-themes/ios-modern/**' - - '!native-themes/ios-modern/**/*.md' - - 'vm/**' - - '!vm/**/*.md' - - 'tests/**' - - '!tests/**/*.md' - - 'maven/**' - - '!maven/core-unittests/**' - - '!docs/**' - # The screenshot transport is now a Codename One backend application, so the - # two files that are ONLY exercised here have to trigger this leg: the - # server itself and the script that launches it. - # - # Deliberately NOT the rest of vm/backend. The websocket protocol code is - # covered by maven/backend's unit tests, by vm/tests against the translated - # binary, and by the Autobahn conformance job -- all of which already - # trigger on vm/**. Fanning every backend change out to the macOS and - # emulator legs as well would buy nothing and cost a great deal: a change to - # the ORM, the database layer or the HTTP parser cannot reach this - # transport. - - 'vm/backend/demo/cn1ss/**' - push: - branches: [ master ] - paths: - - '.github/workflows/scripts-mac-catalyst.yml' - - '.github/workflows/_build-ios-port.yml' - - 'scripts/setup-workspace.sh' - - 'scripts/build-ios-port.sh' - - 'scripts/build-mac-catalyst-app.sh' - - 'scripts/run-mac-catalyst-ui-tests.sh' - - 'scripts/hellocodenameone/**' - - 'scripts/ios/tests/**' - - 'scripts/mac-catalyst/**' - - 'scripts/templates/**' - - '!scripts/templates/**/*.md' - - 'scripts/common/java/**' - - 'scripts/lib/cn1ss.sh' - - 'scripts/lib/xcode.sh' - - 'CodenameOne/src/**' - - 'Ports/iOSPort/**' - - 'native-themes/ios-modern/**' - - 'vm/**' - - 'tests/**' - - 'maven/**' - - '!maven/core-unittests/**' - # The screenshot transport is now a Codename One backend application, so the - # two files that are ONLY exercised here have to trigger this leg: the - # server itself and the script that launches it. - # - # Deliberately NOT the rest of vm/backend. The websocket protocol code is - # covered by maven/backend's unit tests, by vm/tests against the translated - # binary, and by the Autobahn conformance job -- all of which already - # trigger on vm/**. Fanning every backend change out to the macOS and - # emulator legs as well would buy nothing and cost a great deal: a change to - # the ORM, the database layer or the HTTP parser cannot reach this - # transport. - - 'vm/backend/demo/cn1ss/**' + workflow_call: + inputs: + port_prepared: + type: boolean + default: false + +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: catalyst-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build-port: + if: ${{ !inputs.port_prepared }} # Shared with scripts-ios.yml / scripts-ios-native.yml / ios-packaging.yml # via the cn1-built cache; first runner to land a fresh SHA populates it # and the others skip the rebuild. uses: ./.github/workflows/_build-ios-port.yml build-mac-catalyst: + if: ${{ !cancelled() && (inputs.port_prepared || needs.build-port.result == 'success') }} needs: build-port permissions: contents: read @@ -113,10 +49,6 @@ jobs: # iOS Metal job headroom bump (a08e87b32). The 45-min job cap used to # SIGKILL the screenshot run mid-post-processing on slow runners. timeout-minutes: 90 - concurrency: - group: mac-ci-${{ github.workflow }}-mac-catalyst-${{ github.ref_name }} - cancel-in-progress: true - env: GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} # The device runner reports logical test failures through CN1SS log @@ -176,14 +108,6 @@ jobs: - name: Set TMPDIR run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV - - name: Cache codenameone-tools - uses: actions/cache@v5 - with: - path: ${{ runner.temp }}/codenameone-tools - key: ${{ runner.os }}-cn1-tools-${{ steps.setup_hash.outputs.hash }} - restore-keys: | - ${{ runner.os }}-cn1-tools- - - name: Cache Maven repository uses: actions/cache@v5 with: @@ -258,7 +182,7 @@ jobs: timeout-minutes: 45 - name: Upload Mac Catalyst port status - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: port-status-mac-catalyst @@ -273,7 +197,7 @@ jobs: # Reuses the existing metal-screenshot-summary.py helper because # the JSON schema is identical -- the summary text says "iOS # Metal" so the wrapper here overrides the headline manually. - if: always() + if: ${{ !cancelled() }} env: COMPARE_JSON: ${{ github.workspace }}/artifacts/mac-catalyst-ui-tests/screenshot-compare.json COMMENT_MD: ${{ github.workspace }}/artifacts/mac-catalyst-ui-tests/screenshot-comment.md @@ -301,7 +225,7 @@ jobs: fi - name: Upload Mac Catalyst artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: mac-catalyst-ui-tests diff --git a/.github/workflows/scripts-macos.yml b/.github/workflows/scripts-macos.yml index a87c43864b2..efa6a5794c9 100644 --- a/.github/workflows/scripts-macos.yml +++ b/.github/workflows/scripts-macos.yml @@ -55,6 +55,10 @@ on: - 'tests/**' - '!tests/**/*.md' - 'maven/**' + - '!maven/cn1-ai-*/**' + - '!maven/cn1-admob/**' + - '!maven/cn1-applovin/**' + - '!maven/cn1-unity-levelplay/**' - '!maven/core-unittests/**' - '!docs/**' # The screenshot transport is now a Codename One backend application, so the @@ -93,6 +97,10 @@ on: - 'vm/**' - 'tests/**' - 'maven/**' + - '!maven/cn1-ai-*/**' + - '!maven/cn1-admob/**' + - '!maven/cn1-applovin/**' + - '!maven/cn1-unity-levelplay/**' - '!maven/core-unittests/**' # The screenshot transport is now a Codename One backend application, so the # two files that are ONLY exercised here have to trigger this leg: the @@ -106,11 +114,16 @@ on: # the ORM, the database layer or the HTTP parser cannot reach this # transport. - 'vm/backend/demo/cn1ss/**' +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: build-port: - # Shared with scripts-ios.yml / scripts-ios-native.yml / ios-packaging.yml - # via the cn1-built cache; first runner to land a fresh SHA populates it - # and the others skip the rebuild. + # AppKit has its own source hash and producer; it does not invalidate + # the iOS coordinator's bundle. uses: ./.github/workflows/_build-mac-port.yml build-macos: @@ -125,10 +138,6 @@ jobs: # SIGKILL the screenshot run mid-post-processing on slow runners. Plus 50 for the # ParparVM vs JDK 25 performance gate. timeout-minutes: 140 - concurrency: - group: mac-ci-${{ github.workflow }}-macos-${{ github.ref_name }} - cancel-in-progress: true - env: GITHUB_TOKEN: ${{ secrets.CN1SS_GH_TOKEN }} # The device runner reports logical test failures through CN1SS log @@ -180,14 +189,6 @@ jobs: - name: Set TMPDIR run: echo "TMPDIR=${{ runner.temp }}" >> $GITHUB_ENV - - name: Cache codenameone-tools - uses: actions/cache@v5 - with: - path: ${{ runner.temp }}/codenameone-tools - key: ${{ runner.os }}-cn1-tools-${{ steps.setup_hash.outputs.hash }} - restore-keys: | - ${{ runner.os }}-cn1-tools- - - name: Cache Maven repository uses: actions/cache@v5 with: @@ -281,7 +282,7 @@ jobs: timeout-minutes: 45 - name: Upload macOS port status - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: port-status-macos @@ -296,7 +297,7 @@ jobs: # Reuses the existing metal-screenshot-summary.py helper because # the JSON schema is identical -- the summary text says "iOS # Metal" so the wrapper here overrides the headline manually. - if: always() + if: ${{ !cancelled() }} env: COMPARE_JSON: ${{ github.workspace }}/artifacts/macos-ui-tests/screenshot-compare.json COMMENT_MD: ${{ github.workspace }}/artifacts/macos-ui-tests/screenshot-comment.md @@ -324,7 +325,7 @@ jobs: fi - name: Upload macOS artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: macos-ui-tests diff --git a/.github/workflows/starter-launchers.yml b/.github/workflows/starter-launchers.yml index 8a414a9ef7a..9e33dd989c3 100644 --- a/.github/workflows/starter-launchers.yml +++ b/.github/workflows/starter-launchers.yml @@ -5,6 +5,12 @@ on: workflow_dispatch: permissions: contents: read +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: launchers: strategy: diff --git a/.github/workflows/website-docs.yml b/.github/workflows/website-docs.yml index 34ba21add86..8b1353f71cb 100644 --- a/.github/workflows/website-docs.yml +++ b/.github/workflows/website-docs.yml @@ -98,6 +98,12 @@ permissions: contents: read pull-requests: write +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: ${{ github.event_name == 'pull_request' }} + jobs: build: runs-on: ubuntu-24.04 @@ -412,7 +418,7 @@ jobs: fi - name: Upload codenameone link policy report - if: ${{ always() }} + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: codenameone-link-policy-report diff --git a/.github/workflows/windows-cross-build-run.yml b/.github/workflows/windows-cross-build-run.yml index f908beacb0d..368b4b483ec 100644 --- a/.github/workflows/windows-cross-build-run.yml +++ b/.github/workflows/windows-cross-build-run.yml @@ -53,7 +53,9 @@ on: - 'scripts/ci/retry.sh' concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + # 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: @@ -298,7 +300,7 @@ jobs: mvn -B test -pl tests -am '-Dtest=CleanTargetIntegrationTest#capturesHelloSuiteOverWebSocket' '-Dsurefire.failIfNoSpecifiedTests=false' - name: Upload screenshot artifact (cross-compiled, x64) - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: windows-cross-screenshot-raw @@ -432,7 +434,7 @@ jobs: if [ "$fail" -ne 0 ]; then echo "Windows cross screenshot gate failed."; exit 1; fi - name: Upload Windows port status - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: port-status-windows-x64 @@ -447,11 +449,11 @@ jobs: # fallback for the Windows x64 column. The report is already on disk in # this job, so publish it directly and depend on no event. # - # if: always() for the same reason the job itself runs on failure -- a + # if: ${{ !cancelled() }} for the same reason the job itself runs on failure -- a # report recording real failures is the one the table most needs. - name: Publish the Windows report to the data branch if: >- - always() && github.ref == 'refs/heads/master' && + !cancelled() && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') env: diff --git a/.github/workflows/windows-cross-compile.yml b/.github/workflows/windows-cross-compile.yml index f84fa8328ce..26f80beda12 100644 --- a/.github/workflows/windows-cross-compile.yml +++ b/.github/workflows/windows-cross-compile.yml @@ -27,7 +27,9 @@ on: - 'maven/windows/**' concurrency: - group: windows-cross-compile-${{ github.ref }} + # 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: diff --git a/.github/workflows/windows-tooling.yml b/.github/workflows/windows-tooling.yml index 27fea0b7dae..a2acb0953ae 100644 --- a/.github/workflows/windows-tooling.yml +++ b/.github/workflows/windows-tooling.yml @@ -43,7 +43,9 @@ on: - 'maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/OpenCertificateWizardMojo.java' concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + # 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: @@ -110,7 +112,7 @@ jobs: run: ./scripts/run-windows-tooling-tests.sh - name: Upload Windows tooling artifacts - if: always() + if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: windows-tooling-tests diff --git a/docs/website/data/port_status.json b/docs/website/data/port_status.json index 3a50cd2bc50..e4388d3a898 100644 --- a/docs/website/data/port_status.json +++ b/docs/website/data/port_status.json @@ -52,7 +52,7 @@ "id": "mac-catalyst", "name": "macOS (Mac Catalyst)", "detail": "Legacy Mac Catalyst build", - "workflow": "scripts-mac-catalyst.yml" + "workflow": "scripts-ios.yml" }, { "id": "javascript", diff --git a/scripts/check-cn1lib-native-coverage.py b/scripts/check-cn1lib-native-coverage.py index 3a28c4b5be9..d3ec74140e9 100755 --- a/scripts/check-cn1lib-native-coverage.py +++ b/scripts/check-cn1lib-native-coverage.py @@ -4,7 +4,7 @@ Native sources in a cn1lib are shipped, never built by us. Two workflows close that gap by compiling them -- ai-cn1lib-native-check.yml and ad-cn1lib-ios-native-check.yml -- but a workflow only covers the libraries -named in its matrix, so a new cn1lib is uncovered by default and nothing says +named in its matrix or explicit probe steps, so a new cn1lib is uncovered by default and nothing says so. That is exactly how cn1-admob and cn1-unity-levelplay shipped Objective-C that had never been through a compiler. @@ -131,6 +131,10 @@ def compiles_cn1lib_natives(body): are what separate a native check from any other job that happens to loop over libraries. Applied per job, because one file can hold both. """ + if 'python3 scripts/ci/check-ios-cn1lib.py ' in body: + with open(os.path.join(REPO, 'scripts/ci/check-ios-cn1lib.py'), encoding='utf-8') as probe: + driver = probe.read() + return 'ios/src/main/objectivec' in driver and "'xcodebuild'" in driver return 'ios/src/main/objectivec' in body and 'xcodebuild' in body @@ -150,6 +154,7 @@ def covered_libraries(): for body in jobs(text): if not compiles_cn1lib_natives(body): continue + found.update(re.findall(r'run: python3 scripts/ci/check-ios-cn1lib\.py (cn1-[\w-]+)\s*$', body, re.M)) found.update(MATRIX_ENTRY.findall(body)) for group in MATRIX_LIST.findall(body): found.update(part.strip() for part in group.split(',')) diff --git a/scripts/check-cn1lib-native-sources.py b/scripts/check-cn1lib-native-sources.py index 460ec48030f..894dfa93347 100755 --- a/scripts/check-cn1lib-native-sources.py +++ b/scripts/check-cn1lib-native-sources.py @@ -20,7 +20,7 @@ the port itself is built with clang-cl, so this gate is about the API existing and the syntax parsing, not about matching that ABI. - scripts/check-cn1lib-native-sources.py [--require-all] + scripts/check-cn1lib-native-sources.py [--require-all] [lib ...] Not covered here, deliberately: cn1-ai-whisper's android-aar JNI sources. They need the NDK and a whisper.cpp checkout, and unlike everything above they are @@ -103,6 +103,11 @@ def prepare_headers(work): def main(argv): require_all = '--require-all' in argv + wanted = set(arg for arg in argv if arg != '--require-all') + unknown = wanted - set(libraries()) + if unknown: + print('Unknown cn1libs: ' + ', '.join(sorted(unknown)), file=sys.stderr) + return 1 findings = [] skipped = [] @@ -114,6 +119,8 @@ def main(argv): checked = 0 for lib in libraries(): + if wanted and lib not in wanted: + continue for platform, path in c_sources(lib): rel = os.path.relpath(path, REPO) compiler = win_cc if platform == 'win' else cc diff --git a/scripts/ci/cancel-stale-pr-runs.js b/scripts/ci/cancel-stale-pr-runs.js new file mode 100644 index 00000000000..466ae136a68 --- /dev/null +++ b/scripts/ci/cancel-stale-pr-runs.js @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +// Only PR validation runs are eligible. No checkout of PR code is needed by +// the caller, and push/release/dispatch runs are never cancellation targets. +const ACTIVE = new Set(['queued', 'in_progress', 'waiting', 'pending', 'requested']); + +function belongsTo(run, pr) { + if (run.event !== 'pull_request') return false; + const refs = run.pull_requests || []; + if (refs.length) return refs.some(ref => ref.number === pr.number); + // GitHub can return an empty association list for fork PR runs. Match BOTH + // repository identity and branch, never the branch alone (e.g. patch-1). + return !!pr.head.repo && run.head_repository?.id === pr.head.repo.id && + run.head_branch === pr.head.ref; +} + +function stale(run, pr) { + if (!ACTIVE.has(run.status) || !belongsTo(run, pr)) return false; + if (pr.state === 'closed') return true; + // Preserve a run testing the head or current synthetic merge commit. + return !!pr.head.sha && !!run.head_sha && run.head_sha !== pr.head.sha && + run.head_sha !== pr.merge_commit_sha; +} + +async function cancelStale({github, context, core, dryRun = false}) { + const repo = context.repo; + const number = context.payload.pull_request.number; + let pr = (await github.rest.pulls.get({...repo, pull_number: number})).data; + let cancelled = 0; + // Gather before cancelling: mutating while paging can skip runs as the list + // shrinks. Broad active statuses include queued dependencies and approvals. + const runs = new Map(); + for (const status of ACTIVE) { + const page = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, + {...repo, event: 'pull_request', status, per_page: 100}); + for (const run of page) if (belongsTo(run, pr)) runs.set(run.id, run); + } + for (const candidate of runs.values()) { + // Re-read both immediately before mutation: a delayed synchronize/closed + // event must not kill a new head or a PR that has since been reopened. + pr = (await github.rest.pulls.get({...repo, pull_number: number})).data; + const run = (await github.rest.actions.getWorkflowRun({...repo, run_id: candidate.id})).data; + if (!stale(run, pr)) continue; + core.info(`${dryRun ? 'Would cancel' : 'Cancelling'} obsolete PR #${number} run ${run.id}: ${run.name}`); + if (!dryRun) { + try { + await github.rest.actions.cancelWorkflowRun({...repo, run_id: run.id}); + } catch (error) { + // A run can finish between GET and POST; other errors remain failures. + if (error.status !== 409) throw error; + } + } + cancelled++; + } + core.info(`${cancelled} obsolete run(s) ${dryRun ? 'identified' : 'cancelled'}`); + return cancelled; +} +module.exports = {belongsTo, stale, cancelStale}; diff --git a/scripts/ci/check-ios-cn1lib.py b/scripts/ci/check-ios-cn1lib.py new file mode 100644 index 00000000000..b6d16eab859 --- /dev/null +++ b/scripts/ci/check-ios-cn1lib.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Compile one shipped iOS bridge under MRR and ARC, sharing SDK/pod setup. + +Keep both compilations even when one fails. Link coverage is a separate step +using check-admob-ios-link.sh, since these targets are static libraries. +""" +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile + +ROOT = Path(__file__).resolve().parents[2] +LIBRARIES = ('cn1-admob', 'cn1-applovin', 'cn1-unity-levelplay', + 'cn1-ai-whisper', 'cn1-ai-stablediffusion') + + +def project_spec(ad): + header = ''' GCC_PREFIX_HEADER: CN1Probe-Prefix.pch + GCC_PRECOMPILE_PREFIX_HEADER: NO + HEADER_SEARCH_PATHS: $(inherited) $(SRCROOT) +''' if ad else '' + return '''name: CN1Probe +options: + bundleIdPrefix: com.codenameone.ciprobe + deploymentTarget: + iOS: "DEPLOYMENT" +configs: + DebugMRR: debug + DebugARC: debug +targets: + CN1Probe: + type: library.static + platform: iOS + sources: + - path: Sources + settings: + base: + CLANG_ENABLE_MODULES: YES + CODE_SIGNING_ALLOWED: NO +'''.replace('DEPLOYMENT', '15.0' if ad else '14.0') + header + ''' configs: + DebugMRR: + CLANG_ENABLE_OBJC_ARC: NO + DebugARC: + CLANG_ENABLE_OBJC_ARC: YES +''' + + +def check(lib, work): + if lib not in LIBRARIES: + raise ValueError('Unsupported cn1lib: ' + lib) + work.mkdir(parents=True, exist_ok=True) + sources = work / 'Sources' + sources.mkdir(exist_ok=True) + for source in (ROOT / 'maven' / lib / 'ios/src/main/objectivec').iterdir(): + if source.suffix in ('.h', '.m', '.mm'): + shutil.copy2(source, sources) + if not any(source.suffix in ('.m', '.mm') for source in sources.iterdir()): + raise ValueError('No Objective-C sources staged for ' + lib) + ad = not lib.startswith('cn1-ai-') + if ad: + for header in ('cn1_globals.h', 'cn1_virtual_thread.h'): + shutil.copy2(ROOT / 'vm/ByteCodeTranslator/src' / header, work) + (work / 'cn1_class_method_index.h').write_text('#pragma once\n') + (work / 'CN1Probe-Prefix.pch').write_text( + '#ifdef __OBJC__\n#import \n#import \n#endif\n' + '#include "cn1_globals.h"\n') + (work / 'project.yml').write_text(project_spec(ad)) + subprocess.run(['xcodegen', 'generate', '--spec', 'project.yml'], cwd=work, check=True) + if ad: + props = ROOT / 'maven' / lib / 'common/codenameone_library_required.properties' + pod = next(line.split('=', 1)[1] for line in props.read_text().splitlines() + if line.startswith('codename1.arg.ios.pods=')) + name, version = pod.split(' ', 1) + (work / 'Podfile').write_text( + "platform :ios, '15.0'\n" + "project 'CN1Probe', 'DebugMRR' => :debug, 'DebugARC' => :debug\n" + "target 'CN1Probe' do\n use_frameworks!\n pod '%s', '%s'\nend\n" % (name, version)) + subprocess.run(['pod', 'install', '--repo-update'], cwd=work, check=True) + project = ['-workspace', 'CN1Probe.xcworkspace'] if ad else ['-project', 'CN1Probe.xcodeproj'] + failed = False + for config in ('DebugMRR', 'DebugARC'): + print('::group::' + lib + ' ' + config, flush=True) + rc = subprocess.run(['xcodebuild', *project, '-scheme', 'CN1Probe', '-configuration', config, + '-derivedDataPath', str(work / 'DerivedData'), + '-sdk', 'iphonesimulator', '-destination', 'generic/platform=iOS Simulator', + 'CODE_SIGNING_ALLOWED=NO', 'build'], cwd=work).returncode + print('::endgroup::', flush=True) + if rc: + print('::error::' + lib + ' failed ' + config, flush=True) + failed = True + return int(failed) + + +if __name__ == '__main__': + lib = sys.argv[1] + work = Path(tempfile.mkdtemp(prefix=lib + '-', dir=os.environ.get('RUNNER_TEMP'))) + sys.exit(check(lib, work)) diff --git a/scripts/ci/select-apple-checks.py b/scripts/ci/select-apple-checks.py new file mode 100644 index 00000000000..7142b3d5582 --- /dev/null +++ b/scripts/ci/select-apple-checks.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Select Apple suites before allocating runners; preserve ordered path filters.""" +import importlib.util +import json +import os +from pathlib import Path +import re + +ROOT = Path(__file__).resolve().parents[2] +spec = importlib.util.spec_from_file_location('cn1lib_selection', Path(__file__).with_name('select-cn1lib-checks.py')) +shared = importlib.util.module_from_spec(spec) +spec.loader.exec_module(shared) + + +def matches(path, pattern): + # These checked-in filters use literals, *, ** and ?. Reject an unfamiliar + # pattern rather than silently narrowing coverage when a filter is edited. + if any(char in pattern for char in '[]+{}'): + raise ValueError('Unsupported path filter: ' + pattern) + regex = '' + index = 0 + while index < len(pattern): + if pattern[index:index+3] == '**/': + regex += '(?:.*/)?' + index += 3 + elif pattern[index:index+2] == '**': + regex += '.*' + index += 2 + else: + char = pattern[index] + regex += '[^/]*' if char == '*' else '[^/]' if char == '?' else re.escape(char) + index += 1 + return re.fullmatch(regex, path) is not None + + +def affected(paths, patterns): + for path in paths: + included = False + for pattern in patterns: + negative = pattern.startswith('!') + if matches(path, pattern[1:] if negative else pattern): + included = not negative + if included: return True + return False + + +def select(paths, event='pull_request'): + filters = json.loads((ROOT / '.github/ci/apple-checks.json').read_text()) + # Planner changes must exercise every consumer. Missing diffs, schedules and + # manual dispatches retain full coverage; a known empty diff selects none. + full = paths is None or any(path in ( + '.github/workflows/scripts-ios.yml', '.github/ci/apple-checks.json', + 'scripts/ci/select-apple-checks.py', + 'scripts/ci/select-cn1lib-checks.py') for path in paths) + result = {suite: full or affected(paths, events[event]) for suite, events in filters.items()} + result['any'] = any(result.values()) + return {key: str(value).lower() for key, value in result.items()} + + +if __name__ == '__main__': + result = select(shared.changed_files(), os.environ.get('GITHUB_EVENT_NAME', 'pull_request') + if os.environ.get('GITHUB_EVENT_NAME') in ('pull_request', 'push') else 'pull_request') + print(json.dumps(result, indent=2)) + with open(os.environ['GITHUB_OUTPUT'], 'a') as output: + for key, value in result.items(): output.write(key + '=' + value + '\n') diff --git a/scripts/ci/select-cn1lib-checks.py b/scripts/ci/select-cn1lib-checks.py new file mode 100644 index 00000000000..e14ed35afdf --- /dev/null +++ b/scripts/ci/select-cn1lib-checks.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Select cn1lib checks from a verified diff; an unavailable diff runs all checks. + +An empty successful diff is different from an unavailable base. BASE_SHA is a +commit from the event payload, never a branch name or a shell expression. +""" +import json +import os +from pathlib import Path +import re +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[2] +AI = ('cn1-ai-whisper', 'cn1-ai-stablediffusion') +ADS = ('cn1-admob', 'cn1-applovin', 'cn1-unity-levelplay') +PACKAGES = AI + ADS + ('cn1-ads-mock',) + + +def select(paths, libraries=None): + libraries = libraries if libraries is not None else sorted( + p.name for p in (ROOT / 'maven').glob('cn1-*') if p.is_dir()) + result = {key: set() for key in ('package', 'android', 'desktop', 'ios_ads', 'ios_ai')} + link = paths is None + if paths is None: + result.update(package=set(PACKAGES), android=set(libraries), desktop=set(libraries), + ios_ads=set(ADS), ios_ai=set(AI)) + for path in paths or []: + if path.endswith('.md'): + continue + parts = path.split('/') + if len(parts) > 2 and parts[0] == 'maven' and parts[1] in libraries: + lib = parts[1] + if lib in PACKAGES: result['package'].add(lib) + # Library build hints and common interfaces can affect every platform. + if parts[2] in ('common', 'pom.xml'): + result['android'].add(lib) + result['desktop'].add(lib) + if lib in ADS: result['ios_ads'].add(lib) + if lib in AI: result['ios_ai'].add(lib) + if parts[2] == 'android': result['android'].add(lib) + if parts[2] in ('linux', 'win', 'javascript'): result['desktop'].add(lib) + if parts[2] == 'ios': + if lib in ADS: result['ios_ads'].add(lib) + if lib in AI: result['ios_ai'].add(lib) + if lib == 'cn1-admob' and parts[2] in ('ios', 'common', 'pom.xml'): link = True + if path == 'scripts/gen-ai-cn1libs.py': + result['package'].update(AI) + result['android'].update(AI) + result['desktop'].update(AI) + result['ios_ai'].update(AI) + if path == 'maven/pom.xml' or path.startswith('maven/codenameone-maven-plugin/'): + result['package'].update(PACKAGES) + if path.startswith(('CodenameOne/src/', 'Ports/Android/', 'scripts/cn1lib-api-check/')) or path in ( + 'scripts/check-cn1lib-android-api.py', '.ci/container/Dockerfile'): + result['android'].update(libraries) + if path.startswith('vm/ByteCodeTranslator/src/') and path.endswith('.h'): + result['desktop'].update(libraries) + result['ios_ads'].update(ADS) + if path == 'scripts/check-cn1lib-native-sources.py' or path == '.ci/container/Dockerfile': + result['desktop'].update(libraries) + if path.startswith('vm/ByteCodeTranslator/') or path in ('vm/pom.xml', 'scripts/check-admob-ios-link.sh'): + link = True + if path in ('scripts/ci/check-ios-cn1lib.py', '.github/workflows/ad-cn1lib-ios-native-check.yml'): + result['ios_ads'].update(ADS) + link = True + if path in ('scripts/ci/check-ios-cn1lib.py', '.github/workflows/ai-cn1lib-native-check.yml'): + result['ios_ai'].update(AI) + if path in ('scripts/ci/select-cn1lib-checks.py', '.github/workflows/pr.yml'): + return select(None, libraries) + # Only pass libraries with native sources to checkers that reject unknown names. + result['android'] = {lib for lib in result['android'] if + any((ROOT / 'maven' / lib / 'android/src/main/java').rglob('*.java'))} + out = {key: ','.join(sorted(value)) for key, value in result.items()} + out['modules'] = ','.join(lib if lib == 'cn1-ads-mock' else lib + '/common' + for lib in sorted(result['package'])) + out['libs'] = out['package'] + out['admob_link'] = str(link).lower() + return out + + +def changed_files(): + base = os.environ.get('BASE_SHA', '') + if not re.fullmatch(r'[0-9a-fA-F]{40}', base) or set(base) == {'0'}: + return None + try: + # Check first: local validation can supply an existing base without network. + if subprocess.run(['git', 'cat-file', '-e', base + '^{commit}'], cwd=ROOT, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode: + subprocess.run(['git', 'fetch', '--no-tags', '--depth=1', 'origin', base], cwd=ROOT, check=True) + return subprocess.check_output(['git', 'diff', '--name-only', '-z', base, 'HEAD'], cwd=ROOT).decode().split('\0')[:-1] + except (subprocess.CalledProcessError, UnicodeDecodeError): + print('::warning::Diff unavailable; running all cn1lib checks', file=sys.stderr) + return None + + +if __name__ == '__main__': + selected = select(changed_files()) + print(json.dumps(selected, indent=2)) + if os.environ.get('GITHUB_OUTPUT'): + with open(os.environ['GITHUB_OUTPUT'], 'a') as output: + for key, value in selected.items(): output.write(key + '=' + value + '\n') diff --git a/scripts/ci/test-cancel-stale-pr-runs.js b/scripts/ci/test-cancel-stale-pr-runs.js new file mode 100644 index 00000000000..243609c0677 --- /dev/null +++ b/scripts/ci/test-cancel-stale-pr-runs.js @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const {stale, cancelStale} = require('./cancel-stale-pr-runs'); +const pr = {number: 7, state: 'open', head: {sha: 'new', ref: 'patch-1', repo: {id: 9}}, merge_commit_sha: 'merge'}; +const run = {id: 11, name: 'CI', event: 'pull_request', status: 'queued', head_sha: 'old', pull_requests: [{number: 7}]}; + +test('superseded queued and running runs are stale; current head and merge are kept', () => { + for (const status of ['queued', 'in_progress', 'waiting', 'pending', 'requested']) { + assert.equal(stale({...run, status}, pr), true); + for (const head_sha of ['new', 'merge']) assert.equal(stale({...run, status, head_sha}, pr), false); + } + assert.equal(stale({...run, status: 'completed'}, pr), false); +}); +test('closed PR cancels even its final head; another PR and non-PR runs survive', () => { + assert.equal(stale({...run, head_sha: 'new'}, {...pr, state: 'closed'}), true); + assert.equal(stale({...run, pull_requests: [{number: 8}]}, pr), false); + for (const event of ['push', 'workflow_dispatch', 'schedule', 'pull_request_target']) { + assert.equal(stale({...run, event}, {...pr, state: 'closed'}), false); + } +}); +test('empty fork associations require both repository identity and branch', () => { + const forkRun = {...run, pull_requests: [], head_branch: 'patch-1', head_repository: {id: 9}}; + assert.equal(stale(forkRun, pr), true); + assert.equal(stale({...forkRun, head_repository: {id: 10}}, pr), false); + assert.equal(stale({...forkRun, head_branch: 'other'}, pr), false); + assert.equal(stale(forkRun, {...pr, head: {...pr.head, repo: null}}), false); +}); +function api(livePr = pr, liveRun = run) { + const cancelled = []; + const github = { + paginate: async (_, params) => params.status === 'queued' ? [run] : [], + rest: { + pulls: {get: async () => ({data: livePr})}, + actions: { + listWorkflowRunsForRepo: () => {}, + getWorkflowRun: async () => ({data: liveRun}), + cancelWorkflowRun: async ({run_id}) => cancelled.push(run_id) + } + } + }; + return {github, cancelled, context: {repo: {owner: 'o', repo: 'r'}, payload: {pull_request: pr}}, core: {info: () => {}}}; +} +test('requests cancellation only after checking live state', async () => { + const client = api(); + assert.equal(await cancelStale(client), 1); + assert.deepEqual(client.cancelled, [11]); +}); +test('delayed events do not cancel a newer run or a reopened current head', async () => { + for (const client of [api(pr, {...run, head_sha: 'new'}), api({...pr, head: {...pr.head, sha: 'old'}}), api(pr, {...run, status: 'completed'})]) { + await cancelStale(client); + assert.deepEqual(client.cancelled, []); + } +}); +test('dry-run is read-only', async () => { + const client = api(); + assert.equal(await cancelStale({...client, dryRun: true}), 1); + assert.deepEqual(client.cancelled, []); +}); +test('409 finish race is tolerated, authorization failures are not hidden', async () => { + for (const status of [409, 403]) { + const client = api(); + client.github.rest.actions.cancelWorkflowRun = async () => { throw Object.assign(new Error('API'), {status}); }; + if (status === 409) await cancelStale(client); + else await assert.rejects(cancelStale(client), /API/); + } +}); diff --git a/scripts/ci/test_select_apple_checks.py b/scripts/ci/test_select_apple_checks.py new file mode 100644 index 00000000000..aafad5c073d --- /dev/null +++ b/scripts/ci/test_select_apple_checks.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +import importlib.util +from pathlib import Path +import unittest + +spec = importlib.util.spec_from_file_location('apple', Path(__file__).with_name('select-apple-checks.py')) +apple = importlib.util.module_from_spec(spec) +spec.loader.exec_module(apple) + +class AppleSelectionTest(unittest.TestCase): + def test_catalyst_only_does_not_expand_to_all_suites(self): + for event in ['push', 'pull_request']: + result = apple.select(['scripts/run-mac-catalyst-ui-tests.sh'], event) + self.assertEqual({'ios':'false', 'native':'false', 'packaging':'false', 'catalyst':'true', 'any':'true'}, result) + + def test_no_native_runner_for_independent_cn1lib_or_core_unit_test(self): + for event in ['push', 'pull_request']: + for path in ['maven/cn1-admob/common/pom.xml', 'maven/cn1-ai-whisper/ios/src/main/objectivec/Bridge.m', + 'maven/core-unittests/src/Test.java', 'docs/guide.md']: + self.assertEqual('false', apple.select([path], event)['any'], (path, event)) + + def test_mock_ad_library_is_a_real_sample_dependency(self): + self.assertEqual('true', apple.select(['maven/cn1-ads-mock/src/MockAds.java'])['ios']) + + def test_port_builder_changes_test_every_consumer(self): + for path in ['.github/workflows/_build-ios-port.yml', 'Ports/iOSPort/nativeSources/IOSNative.m']: + self.assertTrue(all(value == 'true' for value in apple.select([path]).values())) + + def test_unknown_diff_and_selection_changes_run_all(self): + for paths in [None, ['.github/workflows/scripts-ios.yml'], ['.github/ci/apple-checks.json'], ['scripts/ci/select-cn1lib-checks.py']]: + self.assertTrue(all(value == 'true' for value in apple.select(paths).values())) + self.assertEqual('false', apple.select([])['any']) + + def test_workflow_specific_changes_are_selected(self): + for key, name in [('native', 'scripts-ios-native.yml'), ('packaging', 'ios-packaging.yml'), ('catalyst','scripts-mac-catalyst.yml')]: + self.assertEqual('true', apple.select(['.github/workflows/' + name])[key]) + + def test_globs_match_github_path_semantics(self): + self.assertTrue(apple.matches('foo.java', '**/*.java')) + self.assertTrue(apple.matches('a/b/foo.java', '**/*.java')) + self.assertFalse(apple.matches('a/b/foo.java', '*.java')) + self.assertTrue(apple.matches('maven/cn1-ai-whisper/ios/Bridge.m', 'maven/cn1-ai-*/**')) + self.assertFalse(apple.matches('maven/cn1-admob/ios/Bridge.m', 'maven/cn1-ai-*/**')) + self.assertTrue(apple.affected(['a/b/keep.java'], ['a/**', '!a/b/**', 'a/b/keep.java'])) + with self.assertRaises(ValueError): apple.matches('x', '[xy]') + +if __name__ == '__main__': unittest.main() diff --git a/scripts/ci/test_select_cn1lib_checks.py b/scripts/ci/test_select_cn1lib_checks.py new file mode 100644 index 00000000000..c12c13c5342 --- /dev/null +++ b/scripts/ci/test_select_cn1lib_checks.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +import importlib.util +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[2] + +def load(name, path): + spec = importlib.util.spec_from_file_location(name, ROOT / path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + +selector = load('selector', 'scripts/ci/select-cn1lib-checks.py') +probe = load('probe', 'scripts/ci/check-ios-cn1lib.py') +coverage = load('coverage', 'scripts/check-cn1lib-native-coverage.py') + +class SelectionTest(unittest.TestCase): + def test_backend_and_docs_do_not_build_libraries(self): + result = selector.select(['vm/backend/src/Foo.java', 'docs/example.md']) + self.assertTrue(all(value == '' for key, value in result.items() if key != 'admob_link')) + self.assertEqual('false', result['admob_link']) + + def test_empty_diff_skips_but_unknown_diff_runs_all(self): + self.assertEqual('', selector.select([])['libs']) + self.assertEqual(set(selector.PACKAGES), set(selector.select(None)['libs'].split(','))) + self.assertEqual('true', selector.select(None)['admob_link']) + + def test_admob_change_does_not_compile_other_providers(self): + result = selector.select(['maven/cn1-admob/ios/src/main/objectivec/Bridge.m']) + self.assertEqual('cn1-admob', result['ios_ads']) + self.assertEqual('true', result['admob_link']) + self.assertEqual('', result['android']) + self.assertEqual('cn1-admob/common', result['modules']) + + def test_translator_java_only_runs_link_probe(self): + result = selector.select(['vm/ByteCodeTranslator/src/com/codename1/tools/ByteCodeMethod.java']) + self.assertEqual('', result['ios_ads']) + self.assertEqual('true', result['admob_link']) + self.assertEqual('', result['desktop']) + + def test_runtime_header_checks_all_ad_bridges(self): + result = selector.select(['vm/ByteCodeTranslator/src/cn1_globals.h']) + self.assertEqual(set(selector.ADS), set(result['ios_ads'].split(','))) + self.assertNotEqual('', result['desktop']) + + def test_android_api_change_keeps_coverage(self): + result = selector.select(['CodenameOne/src/com/codename1/ui/Component.java']) + self.assertIn('cn1-admob', result['android']) + self.assertEqual('', result['ios_ads']) + self.assertEqual('', result['libs']) + + def test_generator_checks_ai_not_ads(self): + result = selector.select(['scripts/gen-ai-cn1libs.py']) + self.assertEqual(set(selector.AI), set(result['ios_ai'].split(','))) + self.assertEqual('', result['ios_ads']) + + def test_packaging_plugin_keeps_common_tests(self): + result = selector.select(['maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/Cn1libMojo.java']) + self.assertEqual(set(selector.PACKAGES), set(result['libs'].split(','))) + + def test_checker_changes_run_corresponding_checks(self): + for file, key in [('scripts/check-cn1lib-native-sources.py', 'desktop'), + ('scripts/check-cn1lib-android-api.py', 'android')]: + self.assertNotEqual('', selector.select([file])[key]) + + def test_no_base_and_bad_base_are_conservative(self): + for base in ['', '0'*40, 'refs/heads/master', '$(echo unsafe)']: + with patch.dict('os.environ', {'BASE_SHA': base}): + self.assertIsNone(selector.changed_files()) + +class ProbeTest(unittest.TestCase): + def test_arc_still_runs_after_mrr_failure_and_pods_resolved_once(self): + calls = [] + def run(command, **kwargs): + calls.append(command) + class Result: + returncode = 1 if 'DebugMRR' in command else 0 + return Result() + with tempfile.TemporaryDirectory() as directory, patch.object(probe.subprocess, 'run', side_effect=run): + work = Path(directory) + self.assertEqual(1, probe.check('cn1-admob', work)) + self.assertTrue(list((work / 'Sources').glob('*.m'))) + self.assertIn('DebugARC', (work / 'project.yml').read_text()) + self.assertIn('DebugMRR', (work / 'Podfile').read_text()) + self.assertEqual(1, sum(c[0] == 'pod' for c in calls)) + self.assertEqual(2, sum(c[0] == 'xcodebuild' for c in calls)) + + def test_unknown_library_fails(self): + with tempfile.TemporaryDirectory() as directory: + with self.assertRaises(ValueError): probe.check('../../bad', Path(directory)) + + def test_native_coverage_reads_actual_probe_steps(self): + covered, untriggered = coverage.covered_libraries() + self.assertFalse(untriggered) + self.assertTrue(set(selector.AI + selector.ADS) <= set(covered)) + self.assertFalse(coverage.compiles_cn1lib_natives('run: echo cn1-admob')) + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/hellocodenameone/conformance/test_port_status.py b/scripts/hellocodenameone/conformance/test_port_status.py index f07a0655b65..430a1b9f794 100755 --- a/scripts/hellocodenameone/conformance/test_port_status.py +++ b/scripts/hellocodenameone/conformance/test_port_status.py @@ -1411,10 +1411,10 @@ def test_the_two_macos_ports_are_registered_separately(self): ) by_id = {port["id"]: port for port in self.manifest["ports"]} - # Each has its own workflow, because backfill attributes reports by the - # workflow file name. + # Backfill attributes reports to their automatic producer. Catalyst now + # shares the iOS coordinator; AppKit retains its performance-gated run. self.assertEqual("scripts-macos.yml", by_id["macos"]["workflow"]) - self.assertEqual("scripts-mac-catalyst.yml", by_id["mac-catalyst"]["workflow"]) + self.assertEqual("scripts-ios.yml", by_id["mac-catalyst"]["workflow"]) # And its own goldens. A Catalyst window is a UIWindowScene fed an # off-screen raster and an AppKit one owns a real CAMetalLayer, so the From 7620dcb7c691d047c7adf9c23db020e85a0a2c92 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 1 Oct 2026 06:08:56 +0300 Subject: [PATCH 2/2] Allow iOS packaging to finish uploads after cold builds --- .github/workflows/ios-packaging.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ios-packaging.yml b/.github/workflows/ios-packaging.yml index bbe0fefa427..99508001f3c 100644 --- a/.github/workflows/ios-packaging.yml +++ b/.github/workflows/ios-packaging.yml @@ -26,9 +26,11 @@ jobs: permissions: contents: read runs-on: macos-15 - # Headroom for the single native xcodebuild compile of the translated app, which grew with - # the gaming runtime added to core and is slower on the Xcode 26.3 / iOS 26.2 toolchain. - timeout-minutes: 120 + # This job runs device Release, simulator UI, and notification builds in + # sequence. On a cold runner all tests passed after 119m44s, leaving only + # 16s of the old two-hour budget for artifact upload and cache saves. Keep + # the individual step limits below and allow 30m of finalization headroom. + timeout-minutes: 150 # Exercises both CocoaPods and SPM in a single Xcode project. Catches # regressions in either dependency manager (each pathway runs end to end) # and additionally validates that they coexist correctly. Also turns on