Skip to content

release(mobile): drop AAB from GitHub Release attachments #53

release(mobile): drop AAB from GitHub Release attachments

release(mobile): drop AAB from GitHub Release attachments #53

name: Build WASM Docker Image
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
name: shared :connectedAndroidDeviceTest
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '21'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v5
# Smoke-compile the test source set on the runner without an emulator.
# Fails fast on Gradle / source-set wiring issues before paying the
# ~5 min AVD boot. Task name comes from the new
# com.android.kotlin.multiplatform.library plugin (androidDeviceTest).
- name: Compile instrumented tests
run: ./gradlew :shared:compileAndroidDeviceTest --stacktrace
- name: AVD cache
id: avd-cache
uses: actions/cache@v6
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-api-34-x86_64-v1
- name: Create AVD and generate snapshot (cold)
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
target: default
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: echo "Generated AVD snapshot for caching."
- name: Run :shared:connectedAndroidDeviceTest
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
target: default
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew :shared:connectedAndroidDeviceTest --stacktrace
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v7
with:
name: shared-android-test-report
path: |
shared/build/reports/androidTests/
shared/build/outputs/androidTest-results/
# Fast JVM run of every `commonTest` case (~30s after warm cache). Runs in
# parallel with `test` to give PR authors near-immediate feedback if a common
# test breaks, without paying the Android emulator boot. Now part of the
# `build-wasm-image` deploy gate (see the `needs:` list on that job), so a
# Skia/AWT flake here must be treated as a real blocker that needs a rerun
# rather than silently ignored.
desktop-test:
name: shared :desktopTest
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '21'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v5
- name: Run :shared:desktopTest
run: ./gradlew :shared:desktopTest --stacktrace
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v7
with:
name: shared-desktop-test-report
path: |
shared/build/reports/tests/desktopTest/
shared/build/test-results/desktopTest/
# End-to-end slice against the published Octocon backend image
# (digest-pinned in shared/src/desktopIntegrationTest/.../BackendContainer.kt).
# Boots the image in Testcontainers, drives the OAuth-callback auth flow
# to obtain a real token, then exercises ApiInterfaceImpl through the
# Phoenix WebSocket + endpoint-proxy paths (LoadClient + CreateAlter).
#
# Part of the `build-wasm-image` deploy gate (see the `needs:` list on
# that job). A Testcontainers hiccup or a ghcr.io outage pulling the
# backend image therefore blocks WASM deploy until a rerun succeeds —
# that's the accepted trade-off for guaranteeing every WASM ship has
# green end-to-end signal.
# Approximate cost on a cold runner: ~30 s image pull + ~5 s tests.
#
# Requirements satisfied implicitly on `ubuntu-latest`:
# - Docker daemon pre-installed and reachable on the default Unix socket.
# - Bridge-network container IPs (172.17.x.y) are directly routable from
# the host JVM, which BackendContainer.baseUrl depends on so the
# backend's WebSocket endpoint-proxy loopback resolves correctly.
integration-test:
name: shared :desktopIntegrationTest
runs-on: ubuntu-latest
permissions:
contents: read
# Only required if ghcr.io/azyyyyyy/interfold-api ever flips private;
# harmless when it stays public, so granted defensively.
packages: read
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '21'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v5
- name: Run :shared:desktopIntegrationTest
run: ./gradlew :shared:desktopIntegrationTest --stacktrace
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v7
with:
name: shared-integration-test-report
path: |
shared/build/reports/tests/desktopIntegrationTest/
shared/build/test-results/desktopIntegrationTest/
# Quick iOS build + test pass. Deliberately mirrors the iOS half of
# mobile-release.yml but stops at `xcodebuild build` — no archive, no
# IPA pack, no attest, no upload. Goal is a ~10-15 min warm signal that
# a future mobile release will still compile, plus running the same
# `commonTest` corpus on iOS that `test` runs on Android.
#
# macos-26 for iOS 26 SDK parity with mobile-release (Compose
# Multiplatform 1.11.x emits `UIViewLayoutRegion` refs that only link
# against the iOS 26 SDK — see the runner comment on mobile-release).
# Cache keys deliberately match mobile-release.yml so PR runs warm the
# ~/.konan and CocoaPods caches that the release workflow will re-use.
ios-test:
name: shared :iosSimulatorArm64Test + xcodebuild build
runs-on: macos-26
timeout-minutes: 40
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '21'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v5
# Same key as mobile-release.yml so the two workflows share the
# ~/.konan cache slice (~1-2 GB of Kotlin/Native compiler +
# prebuilts). Adding iosSimulatorArm64 here grows the slice by a
# simulator-arch triple; still well under the 10 GB repo quota.
- name: Cache Kotlin/Native (~/.konan)
uses: actions/cache@v6
with:
path: ~/.konan
key: konan-${{ runner.os }}-${{ hashFiles('gradle.properties') }}-${{ hashFiles('**/*.gradle.kts', 'gradle/libs.versions.toml') }}
restore-keys: |
konan-${{ runner.os }}-${{ hashFiles('gradle.properties') }}-
konan-${{ runner.os }}-
# Same key as mobile-release.yml. The cache-hit output gates
# whether `pod install` needs `--repo-update`.
- name: Cache CocoaPods
id: pods-cache
uses: actions/cache@v6
with:
path: |
~/.cocoapods
~/Library/Caches/CocoaPods
iosApp/Pods
key: cocoapods-${{ runner.os }}-${{ hashFiles('iosApp/Podfile.lock') }}
restore-keys: |
cocoapods-${{ runner.os }}-
# Same prep as mobile-release but without `-PappVersion.*` flags —
# CI doesn't need release-shaped version stamps. writeIosVersionXcconfig
# is still needed so the Podfile's post_install hook can chain
# Version.generated.xcconfig into every Pods xcconfig at pod-install
# time; generateDummyFramework is required or pod install fails with
# "Kotlin framework 'shared' doesn't exist yet".
- name: Prepare KMP iOS artifacts
run: |
set -euo pipefail
./gradlew :shared:writeIosVersionXcconfig :shared:generateDummyFramework --stacktrace
- name: pod install
working-directory: iosApp
run: |
set -euo pipefail
if [ "${{ steps.pods-cache.outputs.cache-hit }}" = "true" ]; then
echo "CocoaPods cache hit — skipping --repo-update"
pod install
else
echo "CocoaPods cache miss — running pod install --repo-update"
pod install --repo-update
fi
# Fast smoke compile of the iOS test source set. Analog to the
# `Compile instrumented tests` step in the Android job — fails fast
# on source-set / cinterop / pod-drift issues before we pay
# simulator boot cost in the test step below.
#
# `-Pkotlin.incremental.native=false` avoids a known Kotlin/Native
# compiler assertion the linker hits on iosSimulatorArm64 Debug/Test
# klibs:
# Failed to build cache for [...]/multiplatform-markdown-renderer[...]
# java.lang.AssertionError: Lowering ReturnsInsertion: phases
# [Enums] are required, but not satisfied
# The Kotlin compiler itself suggests this flag as the workaround.
# Doesn't affect mobile-release.yml which uses linkReleaseFramework
# (a different phase pipeline).
- name: Compile iOS simulator tests
run: ./gradlew :shared:compileTestKotlinIosSimulatorArm64 -Pkotlin.incremental.native=false --stacktrace
# Build the Xcode target for Simulator arch. Skips archive, IPA
# pack, signing, and attestation — the pieces mobile-release adds
# on top. On macos-26 Apple Silicon the Simulator arch is
# `iosSimulatorArm64`, same LLVM target family as the `iosArm64`
# device build mobile-release uses, so Swift + KMP + linker errors
# surface identically. Difference vs mobile-release is limited to
# signing/entitlements (skipped) and thin-binary packaging (n/a
# for `build`).
#
# `ARCHS=arm64 ONLY_ACTIVE_ARCH=YES` pins the build to the
# iosSimulatorArm64 slice. Without them, `generic/platform=iOS
# Simulator` builds a fat slice on Apple Silicon (arm64 + x86_64)
# and Compose Multiplatform's `syncPodComposeResourcesForIos`
# aborts with "Unknown iOS simulator arch: 'x86_64'" — that arch
# slice isn't in the KMP plugin's whitelist because we don't
# declare an iosX64 target in shared/build.gradle.kts.
- name: xcodebuild build (Simulator)
run: |
set -euo pipefail
xcodebuild build \
-workspace iosApp/iosApp.xcworkspace \
-scheme iosApp \
-configuration Debug \
-sdk iphonesimulator \
-destination "generic/platform=iOS Simulator" \
ARCHS=arm64 \
ONLY_ACTIVE_ARCH=YES \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGN_IDENTITY="" \
CODE_SIGN_ENTITLEMENTS="" \
DEVELOPMENT_TEAM=""
# Direct KMP analog of :shared:connectedAndroidDeviceTest — same
# commonTest source set, compiled for iosSimulatorArm64, launched
# on a Kotlin/Native-managed simulator. Iterates the same test
# corpus across both mobile targets. Same -Pkotlin.incremental.native
# workaround as the smoke step; the linker (linkDebugTestIosSimulatorArm64)
# is what actually trips the [Enums] phase assertion.
- name: Run :shared:iosSimulatorArm64Test
run: ./gradlew :shared:iosSimulatorArm64Test -Pkotlin.incremental.native=false --stacktrace
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v7
with:
name: shared-ios-test-report
path: |
shared/build/reports/tests/iosSimulatorArm64Test/
shared/build/test-results/iosSimulatorArm64Test/
build-wasm-image:
needs: [test, desktop-test, integration-test, ios-test]
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v7
with:
# Need full history so the compute-version step below can walk back
# through commits with `git log --grep=^ReleaseName:` — the marker
# is intentionally sticky across commits until re-declared.
fetch-depth: 0
# Central AppVersion knobs threaded into Dockerfile.wasm as build-args
# and re-injected as -PappVersion.* flags on the Gradle command inside.
# TAG -> the git tag on HEAD (e.g. "v1.3.2"); empty for
# rolling builds. Presence flips versionName from
# the build-id to the semver.
# RELEASE_NAME -> value of the most recent `ReleaseName: <name>`
# line reachable from HEAD (sticky across commits).
# run_number is passed straight from github.run_number below and
# forms the build-id date-runNumber pair.
- name: Compute app version inputs
id: appversion
run: |
set -euo pipefail
TAG=$(git describe --tags --exact-match HEAD 2>/dev/null || true)
RELEASE_NAME=$(git log --grep='^ReleaseName:' -1 --pretty=%B \
| sed -n 's/^ReleaseName:[[:space:]]*//p' \
| head -n1 || true)
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "release_name=$RELEASE_NAME" >> "$GITHUB_OUTPUT"
echo "Resolved tag='$TAG' release_name='$RELEASE_NAME' run_number='${{ github.run_number }}'"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/octocon-wasm
tags: |
type=ref,event=branch
type=sha,prefix=sha-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile.wasm
push: true
# Multi-arch fan-out. Builder stage runs once on $BUILDPLATFORM
# (the GHA amd64 runner) — see Dockerfile.wasm header comment.
# Runtime stage is COPY-only, so no QEMU binfmt handler is needed.
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
APPVERSION_RUN_NUMBER=${{ github.run_number }}
APPVERSION_TAG=${{ steps.appversion.outputs.tag }}
APPVERSION_RELEASE_NAME=${{ steps.appversion.outputs.release_name }}
# Suppress the SBOM/provenance attestation manifests — they otherwise
# show up as `unknown/unknown` entries in the OCI index, which confuses
# the bootstrapper's manifest inspection.
provenance: false
sbom: false
cache-from: type=gha,scope=octocon-wasm
cache-to: type=gha,mode=max,scope=octocon-wasm