Skip to content

Increase test coverage #275

Increase test coverage

Increase test coverage #275

Workflow file for this run

name: android
on:
push:
branches: ["main"]
tags: ["*"]
pull_request:
workflow_dispatch:
concurrency:
group: android-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
env:
CI_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
jobs:
notices:
name: Notices | Generation + drift
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.CI_SHA }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- name: Install dependencies
run: npm ci
- name: Generate notices
run: npm run notices:generate
- name: Verify notice drift
run: npm run notices:check
- name: Build packaged app assets
run: npm run build
- name: Verify packaged notice drift
run: bash scripts/package-third-party-notice.sh --check
- name: Ensure compliance artifacts are committed
run: |
set -euo pipefail
git diff --exit-code -- \
THIRD_PARTY_NOTICES.md
- name: Upload notice artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: third-party-notices
path: |
THIRD_PARTY_NOTICES.md
dist/THIRD_PARTY_NOTICES.md
if-no-files-found: warn
web-unit:
name: Web | Unit tests (coverage)
runs-on: ubuntu-latest
env:
HVSC_UPDATE_84_CACHE: ${{ github.workspace }}/.cache/hvsc
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.CI_SHA }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- name: Install dependencies
run: |
start=$(date +%s)
npm ci
end=$(date +%s)
echo "- deps install (npm): $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Cache HVSC Update 84 archive
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/.cache/hvsc
key: hvsc-update-84-${{ runner.os }}
- name: Run unit tests with coverage
run: |
start=$(date +%s)
npm run test:coverage
end=$(date +%s)
echo "- unit tests: $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Upload unit coverage
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-unit
path: coverage/lcov.info
web-screenshots:
name: Web | Screenshots
runs-on: ubuntu-latest
needs: web-build-coverage
env:
HVSC_UPDATE_84_CACHE: ${{ github.workspace }}/.cache/hvsc
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.CI_SHA }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- name: Install dependencies
run: |
start=$(date +%s)
npm ci
end=$(date +%s)
echo "- deps install (npm): $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Cache HVSC Update 84 archive
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/.cache/hvsc
key: hvsc-update-84-${{ runner.os }}
- name: Fix broken apt repos
run: |
sudo rm -f /etc/apt/sources.list.d/azure-cli.sources /etc/apt/sources.list.d/azure-cli.list || true
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json', 'bun.lockb') }}
- name: Install Playwright browsers
run: |
start=$(date +%s)
npx playwright install --with-deps
end=$(date +%s)
echo "- playwright install: $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Download coverage build output
uses: actions/download-artifact@v4
with:
name: web-dist-coverage
path: dist
- name: Ensure build output exists
run: |
if [[ ! -f dist/index.html ]]; then
echo "dist/index.html missing; rebuilding"
npm run build
fi
- name: Run screenshot tests
env:
PLAYWRIGHT_SKIP_BUILD: "1"
VITE_GIT_SHA: "ci"
VITE_BUILD_TIME: "1970-01-01T00:00:00Z"
SOURCE_DATE_EPOCH: "0"
run: |
start=$(date +%s)
npm run screenshots
end=$(date +%s)
echo "- screenshots: $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Upload screenshot evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-evidence-screenshots
path: test-results/evidence/**
web-build-coverage:
name: Web | Build (coverage)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.CI_SHA }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- name: Install dependencies
run: |
start=$(date +%s)
npm ci
end=$(date +%s)
echo "- deps install (npm): $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Build web (coverage probes)
run: |
start=$(date +%s)
VITE_COVERAGE=true VITE_ENABLE_TEST_PROBES=1 npm run build
end=$(date +%s)
echo "- build (coverage): $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Upload coverage build output
uses: actions/upload-artifact@v4
with:
name: web-dist-coverage
path: dist
web-e2e:
name: Web | E2E (sharded)
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.57.0-noble
defaults:
run:
shell: bash
needs: web-build-coverage
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
shardTotal: [12]
env:
VITE_COVERAGE: "true"
VITE_ENABLE_TEST_PROBES: "1"
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.CI_SHA }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- name: Install dependencies
run: |
start=$(date +%s)
npm ci
end=$(date +%s)
echo "- deps install (npm): $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Download coverage build output
uses: actions/download-artifact@v4
with:
name: web-dist-coverage
path: dist
- name: Ensure coverage build exists
run: |
if [[ ! -f dist/index.html ]]; then
echo "dist/index.html missing; rebuilding with coverage instrumentation"
VITE_COVERAGE=true VITE_ENABLE_TEST_PROBES=1 npm run build
fi
if ! grep -qr "__coverage__" dist/assets 2>/dev/null; then
echo "coverage instrumentation missing; rebuilding with coverage instrumentation"
VITE_COVERAGE=true VITE_ENABLE_TEST_PROBES=1 npm run build
fi
- name: Prepare coverage output
run: |
mkdir -p .nyc_output
- name: Select Playwright shard files
run: |
node scripts/get-playwright-shard-files.mjs \
--shard=${{ matrix.shard }} \
--total=${{ matrix.shardTotal }} \
> shard-files.txt
echo "Shard files:" >> $GITHUB_STEP_SUMMARY
sed 's/^/- /' shard-files.txt >> $GITHUB_STEP_SUMMARY
- name: Run Playwright e2e tests (shard ${{ matrix.shard }}/${{ matrix.shardTotal }})
env:
PLAYWRIGHT_SKIP_BUILD: "1"
VITE_GIT_SHA: "ci"
VITE_BUILD_TIME: "1970-01-01T00:00:00Z"
SOURCE_DATE_EPOCH: "0"
run: |
start=$(date +%s)
mapfile -t SHARD_FILES < shard-files.txt
npm run test:e2e -- "${SHARD_FILES[@]}"
end=$(date +%s)
echo "- e2e shard ${{ matrix.shard }}/${{ matrix.shardTotal }}: $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Verify nyc output
if: always()
run: |
if ! find .nyc_output -type f -name "*.json" | grep -q .; then
echo "No nyc coverage output found in shard ${{ matrix.shard }}/${{ matrix.shardTotal }}"
exit 1
fi
- name: Upload e2e coverage shard
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-nyc-output-${{ matrix.shard }}
path: .nyc_output/**
if-no-files-found: ignore
include-hidden-files: true
- name: Upload e2e evidence shard
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-evidence-e2e-${{ matrix.shard }}
path: test-results/evidence/**
web-coverage-merge:
name: Web | Coverage + evidence merge
runs-on: ubuntu-latest
needs: [web-unit, web-screenshots, web-e2e]
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.CI_SHA }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- name: Install dependencies (npm)
run: |
start=$(date +%s)
npm ci
end=$(date +%s)
echo "- deps install (npm): $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Download unit coverage
uses: actions/download-artifact@v4
with:
name: coverage-unit
path: coverage
- name: Download e2e nyc outputs
uses: actions/download-artifact@v4
with:
pattern: e2e-nyc-output-*
path: artifacts/nyc
merge-multiple: true
- name: Download Playwright evidence
uses: actions/download-artifact@v4
with:
pattern: playwright-evidence-*
path: test-results/evidence
merge-multiple: true
- name: Collect nyc coverage
run: |
mkdir -p .nyc_output
mkdir -p artifacts/nyc
if find artifacts/nyc -type f -name "*.json" | grep -q .; then
find artifacts/nyc -type f -name "*.json" -exec cp -f {} .nyc_output/ \;
fi
- name: Validate Playwright evidence
run: |
start=$(date +%s)
npm run validate:evidence
end=$(date +%s)
echo "- evidence validation: $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Merge coverage reports
run: |
start=$(date +%s)
echo "==> Generating E2E coverage report..."
npx nyc report \
--temp-dir .nyc_output \
--report-dir coverage/e2e \
--reporter=lcov \
--reporter=text-summary
echo "==> Merging unit + E2E LCOV for Codecov..."
npx lcov-result-merger \
"coverage/{lcov.info,e2e/lcov.info}" \
coverage/lcov-merged.info
end=$(date +%s)
echo "- coverage merge: $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: List web coverage artifacts
run: |
echo "Coverage files:";
find coverage -maxdepth 3 -type f \( -name "*.info" -o -name "*.json" -o -name "*.html" \) | sort
- name: Verify coverage artifacts
env:
EXPECT_WEB_COVERAGE: "1"
run: node scripts/verify-coverage-artifacts.mjs
- name: Enforce coverage threshold
env:
COVERAGE_MIN: "90"
COVERAGE_FILE: "coverage/lcov-merged.info"
run: node scripts/check-coverage-threshold.mjs
- name: Enforce coverage paths
env:
MIN_SRC_FILES: "50"
MIN_SRC_LINES: "2000"
run: node scripts/check-lcov-paths.mjs
- name: Upload web coverage to Codecov
uses: codecov/codecov-action@v5
env:
CODECOV_SHA: ${{ env.CI_SHA }}
CC_SHA: ${{ env.CI_SHA }}
with:
files: ./coverage/lcov-merged.info
disable_search: true
flags: unittests,e2etests
name: c64-commander-web-coverage
use_pypi: true
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload Playwright evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-evidence
path: test-results/evidence/**
android-tests:
name: Android | Tests + Coverage
runs-on: ubuntu-latest
env:
GRADLE_MAX_WORKERS: "6"
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.CI_SHA }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- name: Resolve app version
run: |
if [[ "${GITHUB_REF_TYPE}" == "tag" && -n "${GITHUB_REF_NAME}" ]]; then
APP_VERSION_RAW="${GITHUB_REF_NAME}"
else
APP_VERSION_RAW="$(git describe --tags --exact-match 2>/dev/null || true)"
if [[ -z "$APP_VERSION_RAW" ]]; then
APP_VERSION_RAW="$(node -p "require('./package.json').version" 2>/dev/null || true)"
fi
fi
APP_VERSION="${APP_VERSION_RAW#v}"
VERSION_CODE=$((1600 + GITHUB_RUN_NUMBER))
echo "APP_VERSION=$APP_VERSION" >> $GITHUB_ENV
echo "VERSION_NAME=$APP_VERSION" >> $GITHUB_ENV
echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_ENV
- name: Install dependencies
run: |
start=$(date +%s)
npm ci
end=$(date +%s)
echo "- deps install (npm): $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
cache: gradle
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Export Android SDK env
run: |
if [[ -n "${ANDROID_SDK_ROOT:-}" ]]; then
echo "ANDROID_HOME=$ANDROID_SDK_ROOT" >> $GITHUB_ENV
elif [[ -n "${ANDROID_HOME:-}" ]]; then
echo "ANDROID_SDK_ROOT=$ANDROID_HOME" >> $GITHUB_ENV
fi
- name: Cache Android SDK system images
uses: actions/cache@v4
with:
path: |
${{ env.ANDROID_SDK_ROOT }}/system-images
${{ env.ANDROID_SDK_ROOT }}/emulator
${{ env.ANDROID_SDK_ROOT }}/platform-tools
${{ env.ANDROID_SDK_ROOT }}/platforms/android-${{ env.ANDROID_API_LEVEL }}
~/.android/avd
~/.android/adb*
key: android-sdk-${{ runner.os }}-api${{ env.ANDROID_API_LEVEL }}-${{ env.ANDROID_SYSTEM_IMAGE }}
- name: Decode keystore
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
if [ -n "$ANDROID_KEYSTORE_BASE64" ]; then
mkdir -p android/keystore
echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/keystore/release.keystore
else
echo "ANDROID_KEYSTORE_BASE64 is not set. Skipping keystore setup."
fi
- name: Export signing env
env:
KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }}
KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }}
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
if [ -n "$ANDROID_KEYSTORE_BASE64" ]; then
echo "KEYSTORE_STORE_PASSWORD=$KEYSTORE_STORE_PASSWORD" >> $GITHUB_ENV
echo "KEYSTORE_KEY_PASSWORD=$KEYSTORE_KEY_PASSWORD" >> $GITHUB_ENV
echo "KEYSTORE_KEY_ALIAS=c64commander" >> $GITHUB_ENV
echo "KEYSTORE_STORE_FILE=${{ github.workspace }}/android/keystore/release.keystore" >> $GITHUB_ENV
echo "HAS_KEYSTORE=true" >> $GITHUB_ENV
else
echo "Signing secrets not set. Skipping signing env export."
echo "HAS_KEYSTORE=false" >> $GITHUB_ENV
fi
- name: Accept licenses
run: yes | sdkmanager --licenses
- name: Install SDK components
run: |
sdkmanager \
"platform-tools" \
"platforms;android-34" \
"build-tools;34.0.0"
- name: Build web + sync
run: |
start=$(date +%s)
npm run cap:build
end=$(date +%s)
echo "- cap build: $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Verify Android assemble path (dry-run)
run: |
set -e
cd android
./gradlew -m :app:assembleDebug
- name: Inspect SevenZ/XZ dependency graph
run: |
set -e
cd android
./gradlew :app:dependencies --configuration debugRuntimeClasspath
./gradlew :app:dependencyInsight --dependency xz --configuration debugRuntimeClasspath
- name: Build debug + minifiedDebug APKs
run: |
set -e
cd android
./gradlew :app:assembleDebug :app:assembleMinifiedDebug
- name: Verify SevenZ/XZ runtime classes in APK DEX
run: |
set -euo pipefail
DEBUG_APK=$(ls android/app/build/outputs/apk/debug/*.apk | head -n 1)
MINIFIED_DEBUG_APK=$(ls android/app/build/outputs/apk/minifiedDebug/*.apk | head -n 1)
bash scripts/verify-android-apk-lzma2.sh "$DEBUG_APK" "$MINIFIED_DEBUG_APK"
- name: Run Android tests with coverage
run: |
start=$(date +%s)
cd android
./gradlew testDebugUnitTest jacocoTestReport
end=$(date +%s)
echo "- android tests: $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: List Android coverage artifacts
if: always()
run: |
echo "Android coverage files:";
find android/app/build/reports -type f -name "jacocoTestReport.xml" | sort
- name: Normalize Android coverage report path
if: always()
id: normalize_android_coverage
run: |
set -e
report=$(find android/app/build/reports -type f -name "*.xml" | grep -i jacoco | head -n 1)
if [ -z "$report" ]; then
echo "No JaCoCo XML report found under android/app/build/reports." >&2
exit 1
fi
target="android/app/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml"
mkdir -p "$(dirname "$target")"
if [ "$report" != "$target" ]; then
cp "$report" "$target"
fi
echo "report=$target" >> $GITHUB_OUTPUT
- name: Verify coverage artifacts
if: always()
env:
EXPECT_ANDROID_COVERAGE: "1"
run: node scripts/verify-coverage-artifacts.mjs
- name: Upload Android coverage to Codecov
if: always()
uses: codecov/codecov-action@v5
env:
CODECOV_SHA: ${{ env.CI_SHA }}
CC_SHA: ${{ env.CI_SHA }}
with:
files: ${{ steps.normalize_android_coverage.outputs.report }}
disable_search: true
flags: android
name: c64-commander-android-coverage
use_pypi: true
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
android-maestro:
name: Android | Maestro gating
runs-on: ubuntu-latest
env:
GRADLE_MAX_WORKERS: "6"
ANDROID_API_LEVEL: "34"
ANDROID_SYSTEM_IMAGE: "system-images;android-34;google_apis;x86_64"
ANDROID_DEVICE_PROFILE: "pixel_6"
ANDROID_AVD_NAME: "c64-ci-constrained-3gb"
ANDROID_AVD_RAM_MB: "3072"
ANDROID_AVD_HEAP_MB: "512"
ANDROID_AVD_CPU_CORES: "2"
ANDROID_AVD_CPU_FREQ_MHZ: "2000"
ANDROID_AVD_LOW_RAM: "no"
EMULATOR_HEADLESS: "1"
BOOT_TIMEOUT_SECS: "180"
MAESTRO_TIMEOUT_SECS: "1800"
MAESTRO_CLI_NO_ANALYTICS: "1"
ANDROID_AVD_HOME: /home/runner/.android/avd
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.CI_SHA }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
cache: gradle
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Cache Maestro CLI
id: maestro-cache
uses: actions/cache@v4
with:
path: ~/.maestro
key: maestro-cli-${{ runner.os }}-v1
- name: Export Android SDK env
run: |
if [[ -n "${ANDROID_SDK_ROOT:-}" ]]; then
echo "ANDROID_HOME=$ANDROID_SDK_ROOT" >> $GITHUB_ENV
elif [[ -n "${ANDROID_HOME:-}" ]]; then
echo "ANDROID_SDK_ROOT=$ANDROID_HOME" >> $GITHUB_ENV
fi
- 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: Start emulator (background)
run: |
set -ex
export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
export PATH="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools:$ANDROID_SDK_ROOT/emulator:$PATH"
echo "=== Restarting adb daemon ==="
adb kill-server || true
adb start-server
# Ensure AVD directory exists
mkdir -p "$ANDROID_AVD_HOME"
echo "ANDROID_AVD_HOME=$ANDROID_AVD_HOME"
echo "=== Installing Android SDK components ==="
sdkmanager "platform-tools" "emulator" "platforms;android-${ANDROID_API_LEVEL}" "${ANDROID_SYSTEM_IMAGE}"
echo "=== Listing existing AVDs ==="
avdmanager list avd || true
echo "=== Creating AVD if needed ==="
if ! avdmanager list avd | grep -q "Name: ${ANDROID_AVD_NAME}"; then
echo "Creating AVD: ${ANDROID_AVD_NAME}"
echo "no" | avdmanager create avd -n "${ANDROID_AVD_NAME}" -k "${ANDROID_SYSTEM_IMAGE}" -d "${ANDROID_DEVICE_PROFILE}" --force
echo "AVD creation completed"
else
echo "AVD ${ANDROID_AVD_NAME} already exists"
fi
echo "=== Verifying AVD exists ==="
avdmanager list avd
ls -la "$ANDROID_AVD_HOME" || true
if ! avdmanager list avd | grep -q "Name: ${ANDROID_AVD_NAME}"; then
echo "ERROR: AVD ${ANDROID_AVD_NAME} was not created!"
exit 1
fi
AVD_CONFIG="$ANDROID_AVD_HOME/${ANDROID_AVD_NAME}.avd/config.ini"
if [[ -f "$AVD_CONFIG" ]]; then
sed -i "s/^hw.ramSize=.*/hw.ramSize=${ANDROID_AVD_RAM_MB}/" "$AVD_CONFIG" || true
sed -i "s/^vm.heapSize=.*/vm.heapSize=${ANDROID_AVD_HEAP_MB}/" "$AVD_CONFIG" || true
sed -i "s/^hw.cpu.ncore=.*/hw.cpu.ncore=${ANDROID_AVD_CPU_CORES}/" "$AVD_CONFIG" || true
sed -i "s/^hw.cpu.speed=.*/hw.cpu.speed=${ANDROID_AVD_CPU_FREQ_MHZ}/" "$AVD_CONFIG" || true
sed -i "s/^hw.device.lowram=.*/hw.device.lowram=${ANDROID_AVD_LOW_RAM}/" "$AVD_CONFIG" || true
grep -q '^hw.ramSize=' "$AVD_CONFIG" || echo "hw.ramSize=${ANDROID_AVD_RAM_MB}" >> "$AVD_CONFIG"
grep -q '^vm.heapSize=' "$AVD_CONFIG" || echo "vm.heapSize=${ANDROID_AVD_HEAP_MB}" >> "$AVD_CONFIG"
grep -q '^hw.cpu.ncore=' "$AVD_CONFIG" || echo "hw.cpu.ncore=${ANDROID_AVD_CPU_CORES}" >> "$AVD_CONFIG"
grep -q '^hw.cpu.speed=' "$AVD_CONFIG" || echo "hw.cpu.speed=${ANDROID_AVD_CPU_FREQ_MHZ}" >> "$AVD_CONFIG"
grep -q '^hw.device.lowram=' "$AVD_CONFIG" || echo "hw.device.lowram=${ANDROID_AVD_LOW_RAM}" >> "$AVD_CONFIG"
echo "=== AVD config readback ==="
grep -E '^(hw\.ramSize|vm\.heapSize|hw\.cpu\.ncore|hw\.cpu\.speed|hw\.device\.lowram)=' "$AVD_CONFIG" | sort
fi
echo "=== Starting emulator in background ==="
mkdir -p test-results/maestro
nohup "$ANDROID_SDK_ROOT/emulator/emulator" -avd "${ANDROID_AVD_NAME}" -no-window -no-audio -no-metrics -no-boot-anim -no-snapshot -no-snapshot-load -no-snapshot-save -gpu swiftshader_indirect -netdelay none -netspeed full -memory "${ANDROID_AVD_RAM_MB}" -cores "${ANDROID_AVD_CPU_CORES}" > test-results/maestro/emulator.log 2>&1 &
EMULATOR_PID=$!
echo $EMULATOR_PID > test-results/maestro/emulator.pid
echo "Emulator started with PID: $EMULATOR_PID"
# Give emulator a moment to start and check if it's still running
sleep 3
if ! kill -0 $EMULATOR_PID 2>/dev/null; then
echo "ERROR: Emulator process died immediately"
cat test-results/maestro/emulator.log || true
exit 1
fi
echo "Emulator process is running"
- name: Wait for emulator boot
run: |
set -e
export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
export PATH="$ANDROID_SDK_ROOT/platform-tools:$PATH"
adb start-server
boot_start=$(date +%s)
echo "Waiting for emulator to appear in adb devices..."
timeout 240 bash -c 'until adb devices | grep -q "emulator-"; do sleep 2; done' || {
echo "Emulator did not appear on first attempt; restarting adb server and retrying"
adb kill-server || true
adb start-server
timeout 180 bash -c 'until adb devices | grep -q "emulator-"; do sleep 2; done' || {
echo "Emulator did not appear"
adb devices
cat test-results/maestro/emulator.log || true
exit 1
}
}
SERIAL=$(adb devices | awk '/emulator-/ {print $1; exit}')
echo "Emulator serial: $SERIAL"
timeout 240 adb -s "$SERIAL" wait-for-device || { echo "Device wait-for-device failed"; adb devices -l; exit 1; }
echo "Waiting for boot completion..."
timeout 420 bash -c "until adb -s $SERIAL shell getprop sys.boot_completed 2>/dev/null | tr -d '\r' | grep -q '1'; do sleep 3; done" || { echo "Boot did not complete"; adb -s $SERIAL shell getprop sys.boot_completed || true; cat test-results/maestro/emulator.log || true; exit 1; }
echo "Waiting for boot animation to stop..."
timeout 120 bash -c "until adb -s $SERIAL shell getprop init.svc.bootanim 2>/dev/null | tr -d '\r' | grep -q 'stopped'; do sleep 2; done" || true
CPU_COUNT=$(adb -s "$SERIAL" shell "grep -c '^processor' /proc/cpuinfo" | tr -d '\r' || true)
MEM_TOTAL_KB=$(adb -s "$SERIAL" shell "awk '/MemTotal/ {print \$2; exit}' /proc/meminfo" | tr -d '\r' || true)
echo "runtime cpu_count=$CPU_COUNT"
echo "runtime mem_total_kb=$MEM_TOTAL_KB"
if [[ ! "$CPU_COUNT" =~ ^[0-9]+$ || "$CPU_COUNT" -lt 2 ]]; then
echo "Emulator CPU core count is below expected profile (>=2)"
exit 1
fi
if [[ -z "$MEM_TOTAL_KB" || "$MEM_TOTAL_KB" -lt 2500000 ]]; then
echo "Emulator memory is below expected profile (~3GB)"
exit 1
fi
echo "Assuming 2GHz/core profile as host-backed emulator CPU characteristic"
boot_end=$(date +%s)
echo "- emulator boot: $((boot_end-boot_start))s" >> $GITHUB_STEP_SUMMARY
echo "- emulator profile: RAM=${ANDROID_AVD_RAM_MB}MB CORES=${ANDROID_AVD_CPU_CORES} CPU_FREQ=assumed-2GHz" >> $GITHUB_STEP_SUMMARY
echo "emulator_boot_seconds=$((boot_end-boot_start))" >> test-results/maestro/timings.txt
echo "Emulator is ready"
adb devices -l
- name: Install dependencies
run: |
start=$(date +%s)
npm ci
end=$(date +%s)
echo "- deps install (npm): $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Install Maestro CLI
if: steps.maestro-cache.outputs.cache-hit != 'true'
run: |
set -euo pipefail
MAESTRO_VERSION="cli-2.2.0"
MAESTRO_SHA256="6de501d2e8adf2d60f4b6b3174dc4b5e393f2f2617245d350a659627dccb0922"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL "https://github.com/mobile-dev-inc/Maestro/releases/download/${MAESTRO_VERSION}/maestro.zip" -o "$tmpdir/maestro.zip"
echo "${MAESTRO_SHA256} $tmpdir/maestro.zip" | sha256sum -c -
mkdir -p "$HOME/.maestro"
mkdir -p "$HOME/.maestro/bin"
unzip -q "$tmpdir/maestro.zip" -d "$HOME/.maestro"
chmod +x "$HOME/.maestro/maestro/bin/maestro"
ln -sf "$HOME/.maestro/maestro/bin/maestro" "$HOME/.maestro/bin/maestro"
"$HOME/.maestro/bin/maestro" --version
echo "$HOME/.maestro/bin" >> $GITHUB_PATH
- name: Export Maestro CLI path
run: |
echo "$HOME/.maestro/bin" >> $GITHUB_PATH
- name: Build web + sync
run: |
start=$(date +%s)
VITE_ENABLE_TEST_PROBES=1 npm run cap:build
end=$(date +%s)
echo "- cap build: $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Build APK (debug)
run: |
start=$(date +%s)
VITE_ENABLE_TEST_PROBES=1 npm run android:apk
end=$(date +%s)
echo "- apk debug: $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Start Android telemetry monitor
run: |
set -euo pipefail
mkdir -p ci-artifacts/telemetry/android
(
status=0
code_file="ci-artifacts/telemetry/android/monitor.exitcode"
trap 'echo "${status:-1}" > "$code_file"' EXIT
TELEMETRY_INTERVAL_SEC=1 \
TELEMETRY_OUTPUT_DIR=ci-artifacts/telemetry/android \
ANDROID_PACKAGE_NAME=uk.gleissner.c64commander \
TELEMETRY_DEVICE_NAME="${ANDROID_AVD_NAME}" \
ci/telemetry/android/monitor_android.sh &
monitor_child_pid=$!
echo "$monitor_child_pid" > ci-artifacts/telemetry/android/monitor.child.pid
wait "$monitor_child_pid" || status=$?
) > ci-artifacts/telemetry/android/monitor-run.log 2>&1 &
echo $! > ci-artifacts/telemetry/android/monitor.pid
sleep 2
if ! kill -0 "$(cat ci-artifacts/telemetry/android/monitor.pid)" 2>/dev/null; then
echo "Android telemetry monitor failed to start"
cat ci-artifacts/telemetry/android/monitor-run.log || true
exit 1
fi
- name: Install APK and prime telemetry
run: |
set -euo pipefail
export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
export PATH="$ANDROID_SDK_ROOT/platform-tools:$PATH"
package_name="uk.gleissner.c64commander"
SERIAL=$(adb devices | awk '/emulator-/ {print $1; exit}')
if [[ -z "$SERIAL" ]]; then
echo "::error::No emulator serial found"
adb devices -l || true
exit 1
fi
echo "Emulator serial: $SERIAL"
# Install APK so the app can be launched for telemetry priming.
# run-maestro-gating.sh will reinstall with -r (harmless).
debug_apk_dir="android/app/build/outputs/apk/debug"
apk_path="$debug_apk_dir/app-debug.apk"
if [[ ! -f "$apk_path" ]]; then
apk_path="$(find "$debug_apk_dir" -maxdepth 1 -type f -name '*-debug.apk' ! -name '*androidTest*.apk' | sort | head -n 1 || true)"
fi
if [[ -z "$apk_path" || ! -f "$apk_path" ]]; then
echo "::error::APK not found under $debug_apk_dir"
ls -la "$debug_apk_dir" || true
exit 1
fi
echo "Using APK: $apk_path"
adb -s "$SERIAL" install -r -t -d "$apk_path" || {
echo "APK install failed; trying uninstall + reinstall"
adb -s "$SERIAL" uninstall "$package_name" 2>/dev/null || true
adb -s "$SERIAL" install -t -d "$apk_path"
}
echo "APK installed, verifying package..."
timeout 15 bash -c "until adb -s '$SERIAL' shell pm list packages '$package_name' | tr -d '\r' | grep -q '$package_name'; do sleep 1; done" || {
echo "::error::Package not discoverable after install"
exit 1
}
# Launch app to prime the telemetry monitor with initial samples.
# Uses -W (wait) for synchronous launch confirmation.
echo "Launching app for telemetry priming..."
adb -s "$SERIAL" shell am start -W -n "${package_name}/.MainActivity" 2>&1 || true
# Wait for telemetry monitor to capture at least one sample row.
csv="ci-artifacts/telemetry/android/metrics.csv"
preflight_timeout="${TELEMETRY_PREFLIGHT_TIMEOUT_SEC:-30}"
if timeout "$preflight_timeout" bash -c "until [[ -f '$csv' ]] && [[ \$(wc -l < '$csv' 2>/dev/null || echo 0) -gt 1 ]]; do sleep 1; done"; then
echo "Telemetry priming OK: $(wc -l < "$csv") lines in metrics.csv"
else
echo "::warning::Telemetry priming: monitor did not capture samples within ${preflight_timeout}s"
echo "=== monitor diagnostics ==="
ls -la ci-artifacts/telemetry/android/ || true
tail -n 30 ci-artifacts/telemetry/android/monitor-run.log || true
tail -n 20 ci-artifacts/telemetry/android/events.log || true
adb -s "$SERIAL" shell "pidof $package_name" || echo "(pidof returned empty)"
adb -s "$SERIAL" shell "ps -A | grep -i c64" || echo "(ps grep returned empty)"
# Non-fatal: Maestro will relaunch the app and monitor should capture then.
fi
- name: Run Maestro gating flows
env:
CI: "true"
CI_RUN_LOWRAM_FLOW: "false"
MAESTRO_LOG_LEVEL: "debug"
MAESTRO_CLI_LOG_LEVEL: "debug"
run: |
set -euo pipefail
export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
export PATH="$ANDROID_SDK_ROOT/platform-tools:$HOME/.maestro/bin:$PATH"
start=$(date +%s)
# Verify emulator is still available
adb devices -l
maestro_status=0
bash scripts/run-maestro-gating.sh --skip-emulator-start --skip-build || maestro_status=$?
if [[ "$maestro_status" -ne 0 ]]; then
echo "=== Emulator log ===" >&2
cat test-results/maestro/emulator.log || true
echo "=== adb devices ===" >&2
adb devices -l || true
exit "$maestro_status"
fi
end=$(date +%s)
echo "- maestro gating: $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Assert memory class logging
if: success()
run: |
set -euo pipefail
export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}"
export PATH="$ANDROID_SDK_ROOT/platform-tools:$PATH"
SERIAL=$(adb devices | awk '/emulator-/ {print $1; exit}')
if [[ -z "$SERIAL" ]]; then
echo "::warning::No emulator serial found for memory class assertion; skipping check"
exit 0
fi
adb -s "$SERIAL" logcat -c || true
adb -s "$SERIAL" shell am start -n uk.gleissner.c64commander/.MainActivity >/dev/null 2>&1 || true
if ! timeout 45 bash -c 'until adb -s "$0" logcat -d | grep -q "Android memory class detected:"; do sleep 2; done' "$SERIAL"; then
echo "::warning::MainActivity memory class log not found after launch+retry"
adb -s "$SERIAL" logcat -d | tail -n 200 || true
fi
- name: Stop Android telemetry monitor
if: always()
run: |
set -euo pipefail
code_file="ci-artifacts/telemetry/android/monitor.exitcode"
child_pid=""
if [[ -f ci-artifacts/telemetry/android/monitor.child.pid ]]; then
child_pid="$(cat ci-artifacts/telemetry/android/monitor.child.pid)"
fi
if [[ -f ci-artifacts/telemetry/android/monitor.pid ]]; then
pid="$(cat ci-artifacts/telemetry/android/monitor.pid)"
if [[ -n "$child_pid" ]]; then
kill -TERM "$child_pid" 2>/dev/null || true
else
kill -TERM "$pid" 2>/dev/null || true
fi
for _ in $(seq 1 20); do
if [[ -f ci-artifacts/telemetry/android/monitor.exitcode ]]; then
break
fi
sleep 1
done
if [[ -n "$child_pid" ]] && kill -0 "$child_pid" 2>/dev/null; then
kill -KILL "$child_pid" 2>/dev/null || true
fi
for _ in $(seq 1 10); do
if [[ -f "$code_file" ]]; then
break
fi
sleep 1
done
if kill -0 "$pid" 2>/dev/null; then
kill -KILL "$pid" 2>/dev/null || true
fi
for _ in $(seq 1 5); do
if [[ -f "$code_file" ]]; then
break
fi
sleep 1
done
if [[ ! -f "$code_file" ]]; then
echo "1" > "$code_file"
echo "telemetry(android): synthesized monitor.exitcode=1 because wrapper exited before writing status" >&2
fi
fi
cat ci-artifacts/telemetry/android/monitor-run.log || true
- name: Diagnose Android telemetry inputs
if: always()
run: |
set -euo pipefail
telemetry_dir="ci-artifacts/telemetry/android"
echo "=== telemetry dir tree ==="
ls -la "$telemetry_dir" || true
csv="$telemetry_dir/metrics.csv"
if [[ -f "$csv" ]]; then
echo "=== metrics.csv lines ==="
wc -l "$csv" || true
echo "=== metrics.csv head ==="
head -n 5 "$csv" || true
echo "=== metrics.csv tail ==="
tail -n 5 "$csv" || true
fi
echo "=== monitor-run.log tail ==="
tail -n 80 "$telemetry_dir/monitor-run.log" || true
echo "=== events.log tail ==="
tail -n 80 "$telemetry_dir/events.log" || true
- name: Summarize telemetry
if: always()
run: |
set -euo pipefail
TELEMETRY_SUMMARY_DIR=ci-artifacts/telemetry \
TELEMETRY_INPUT_CSVS=ci-artifacts/telemetry/android/metrics.csv \
python3 ci/telemetry/summarize_metrics.py
- name: Render telemetry charts
if: always()
run: |
set -euo pipefail
TELEMETRY_SUMMARY_DIR=ci-artifacts/telemetry python3 ci/telemetry/render_charts.py
- name: Upload Maestro evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: maestro-evidence
path: |
test-results/maestro/**
test-results/evidence/maestro/**
ci-artifacts/telemetry/android/**
ci-artifacts/telemetry/summary.json
ci-artifacts/telemetry/summary.md
ci-artifacts/telemetry/charts/**
- name: Enforce Android telemetry gates
if: always()
run: |
set -euo pipefail
expected_header="timestamp,platform,device,process_name,pid,cpu_percent,rss_kb,threads,pss_kb,dalvik_pss_kb,native_pss_kb,total_pss_kb"
debug_tail_lines="${TELEMETRY_GATE_DEBUG_TAIL_LINES:-80}"
csv="ci-artifacts/telemetry/android/metrics.csv"
if [[ ! -f "$csv" ]]; then
echo "telemetry gate failed: missing $csv"
exit 1
fi
header=$(head -n 1 "$csv")
if [[ "$header" != "$expected_header" ]]; then
echo "telemetry gate failed: unexpected CSV header"
echo "expected: $expected_header"
echo "actual: $header"
exit 1
fi
line_count=$(wc -l < "$csv")
data_rows=$((line_count - 1))
if [[ "$data_rows" -lt 2 ]]; then
echo "telemetry gate failed: expected multiple data rows, found ${data_rows}"
echo "telemetry gate debug: csv=${csv} line_count=${line_count} data_rows=${data_rows}"
tail -n "$debug_tail_lines" ci-artifacts/telemetry/android/monitor-run.log || true
tail -n "$debug_tail_lines" ci-artifacts/telemetry/android/events.log || true
if [[ -f ci-artifacts/telemetry/android/metadata.json ]]; then
echo "telemetry gate debug: metadata.json"
cat ci-artifacts/telemetry/android/metadata.json || true
fi
exit 1
fi
code_file="ci-artifacts/telemetry/android/monitor.exitcode"
if [[ ! -f "$code_file" ]]; then
echo "telemetry gate failed: monitor exit code missing"
exit 1
fi
code=$(cat "$code_file")
if [[ "$code" == "3" ]]; then
if [[ "${GITHUB_REF_TYPE}" == "tag" || "${GITHUB_REF_NAME}" == release/* ]]; then
echo "telemetry gate failed: main process disappearance/restart detected on release flow; see ci-artifacts/telemetry/android/events.log"
exit 1
fi
echo "telemetry gate warning: main process disappearance/restart detected (non-release flow); see ci-artifacts/telemetry/android/events.log"
exit 0
fi
if [[ "$code" == "137" ]]; then
echo "telemetry gate warning: monitor exited with code 137 (likely infra resource kill); see ci-artifacts/telemetry/android/monitor-run.log"
exit 0
fi
if [[ "$code" != "0" ]]; then
echo "telemetry gate failed: monitor exited with code $code"
exit 1
fi
android-packaging:
name: Android | Packaging
runs-on: ubuntu-latest
needs: [android-maestro]
env:
GRADLE_MAX_WORKERS: "6"
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.CI_SHA }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- name: Resolve app version
run: |
if [[ "${GITHUB_REF_TYPE}" == "tag" && -n "${GITHUB_REF_NAME}" ]]; then
APP_VERSION_RAW="${GITHUB_REF_NAME}"
else
APP_VERSION_RAW="$(git describe --tags --exact-match 2>/dev/null || true)"
if [[ -z "$APP_VERSION_RAW" ]]; then
APP_VERSION_RAW="$(node -p "require('./package.json').version" 2>/dev/null || true)"
fi
fi
APP_VERSION="${APP_VERSION_RAW#v}"
VERSION_CODE=$((1600 + GITHUB_RUN_NUMBER))
echo "APP_VERSION=$APP_VERSION" >> $GITHUB_ENV
echo "VERSION_NAME=$APP_VERSION" >> $GITHUB_ENV
echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_ENV
- name: Install dependencies
run: |
start=$(date +%s)
npm ci
end=$(date +%s)
echo "- deps install (npm): $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
cache: gradle
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Export Android SDK env
run: |
if [[ -n "${ANDROID_SDK_ROOT:-}" ]]; then
echo "ANDROID_HOME=$ANDROID_SDK_ROOT" >> $GITHUB_ENV
elif [[ -n "${ANDROID_HOME:-}" ]]; then
echo "ANDROID_SDK_ROOT=$ANDROID_HOME" >> $GITHUB_ENV
fi
- name: Decode keystore
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
if [ -n "$ANDROID_KEYSTORE_BASE64" ]; then
mkdir -p android/keystore
echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/keystore/release.keystore
else
echo "ANDROID_KEYSTORE_BASE64 is not set. Skipping keystore setup."
fi
- name: Export signing env
env:
KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }}
KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }}
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
if [ -n "$ANDROID_KEYSTORE_BASE64" ]; then
echo "KEYSTORE_STORE_PASSWORD=$KEYSTORE_STORE_PASSWORD" >> $GITHUB_ENV
echo "KEYSTORE_KEY_PASSWORD=$KEYSTORE_KEY_PASSWORD" >> $GITHUB_ENV
echo "KEYSTORE_KEY_ALIAS=c64commander" >> $GITHUB_ENV
echo "KEYSTORE_STORE_FILE=${{ github.workspace }}/android/keystore/release.keystore" >> $GITHUB_ENV
echo "HAS_KEYSTORE=true" >> $GITHUB_ENV
else
echo "Signing secrets not set. Skipping signing env export."
echo "HAS_KEYSTORE=false" >> $GITHUB_ENV
fi
- name: Accept licenses
run: yes | sdkmanager --licenses
- name: Install SDK components
run: |
sdkmanager \
"platform-tools" \
"platforms;android-34" \
"build-tools;34.0.0"
- name: Build web + sync
run: |
start=$(date +%s)
npm run cap:build
end=$(date +%s)
echo "- cap build: $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Build APK (debug)
run: |
start=$(date +%s)
npm run android:apk
end=$(date +%s)
echo "- apk debug: $((end-start))s" >> $GITHUB_STEP_SUMMARY
- name: Build APK (release)
if: startsWith(github.ref, 'refs/tags/') && env.HAS_KEYSTORE == 'true'
run: cd android && ./gradlew assembleRelease
- name: Build App Bundle (release)
if: startsWith(github.ref, 'refs/tags/') && env.HAS_KEYSTORE == 'true'
run: cd android && ./gradlew bundleRelease
- name: Rename release APK for publishing
if: startsWith(github.ref, 'refs/tags/') && env.HAS_KEYSTORE == 'true'
run: |
mv android/app/build/outputs/apk/release/c64commander-${{ env.APP_VERSION }}.apk \
android/app/build/outputs/apk/release/c64commander-${{ env.APP_VERSION }}-android.apk
- name: Upload APK artifact (release)
if: startsWith(github.ref, 'refs/tags/') && env.HAS_KEYSTORE == 'true'
uses: actions/upload-artifact@v4
with:
name: c64commander-release-apk
path: |
android/app/build/outputs/apk/release/c64commander-${{ env.APP_VERSION }}-android.apk
- name: Rename AAB for publishing
if: startsWith(github.ref, 'refs/tags/') && env.HAS_KEYSTORE == 'true'
run: |
mv android/app/build/outputs/bundle/release/app-release.aab \
android/app/build/outputs/bundle/release/c64commander-${{ env.APP_VERSION }}-android-play.aab
- name: Upload AAB artifact (release)
if: startsWith(github.ref, 'refs/tags/') && env.HAS_KEYSTORE == 'true'
uses: actions/upload-artifact@v4
with:
name: c64commander-release-aab
path: android/app/build/outputs/bundle/release/c64commander-${{ env.APP_VERSION }}-android-play.aab
release-artifacts:
name: Release | Attach APK/AAB
runs-on: ubuntu-latest
needs: [web-coverage-merge, android-tests, android-packaging]
permissions:
contents: write
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.CI_SHA }}
- name: Resolve app version
run: |
if [[ "${GITHUB_REF_TYPE}" == "tag" && -n "${GITHUB_REF_NAME}" ]]; then
APP_VERSION_RAW="${GITHUB_REF_NAME}"
else
APP_VERSION_RAW="$(git describe --tags --exact-match 2>/dev/null || true)"
if [[ -z "$APP_VERSION_RAW" ]]; then
APP_VERSION_RAW="$(node -p "require('./package.json').version" 2>/dev/null || true)"
fi
fi
APP_VERSION="${APP_VERSION_RAW#v}"
echo "APP_VERSION=$APP_VERSION" >> $GITHUB_ENV
- name: Resolve signing availability
run: |
if [ -n "$ANDROID_KEYSTORE_BASE64" ]; then
echo "HAS_KEYSTORE=true" >> $GITHUB_ENV
else
echo "HAS_KEYSTORE=false" >> $GITHUB_ENV
fi
- name: Download release APK artifact
if: startsWith(github.ref, 'refs/tags/') && env.HAS_KEYSTORE == 'true'
uses: actions/download-artifact@v4
with:
name: c64commander-release-apk
path: artifacts/release-apk
- name: Download release AAB artifact
if: startsWith(github.ref, 'refs/tags/') && env.HAS_KEYSTORE == 'true'
uses: actions/download-artifact@v4
with:
name: c64commander-release-aab
path: artifacts/release-aab
- name: Validate release artifact filenames
if: startsWith(github.ref, 'refs/tags/') && env.HAS_KEYSTORE == 'true'
run: |
set -euo pipefail
test -f "artifacts/release-apk/c64commander-${APP_VERSION}-android.apk"
test -f "artifacts/release-aab/c64commander-${APP_VERSION}-android-play.aab"
! find artifacts -type f | grep -Ei '(debug|unsigned|altstore|app-release\.aab)'
- name: Ensure GitHub release exists
if: startsWith(github.ref, 'refs/tags/') && env.HAS_KEYSTORE == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
TAG="${GITHUB_REF_NAME}"
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release already exists for $TAG"
else
if [[ "$TAG" == *-rc* ]]; then
gh release create "$TAG" --target "$GITHUB_SHA" --verify-tag --prerelease --generate-notes
else
gh release create "$TAG" --target "$GITHUB_SHA" --verify-tag --generate-notes
fi
fi
- name: Upload release artifacts to GitHub release
if: startsWith(github.ref, 'refs/tags/') && env.HAS_KEYSTORE == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh release upload "${GITHUB_REF_NAME}" \
"artifacts/release-apk/c64commander-${APP_VERSION}-android.apk" \
"artifacts/release-aab/c64commander-${APP_VERSION}-android-play.aab" \
--clobber
- name: Verify Android release assets are attached
if: startsWith(github.ref, 'refs/tags/') && env.HAS_KEYSTORE == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
assets_json="$(gh release view "${GITHUB_REF_NAME}" --json assets --jq '.assets[].name')"
echo "Release assets:"
echo "$assets_json"
grep -Fx "c64commander-${APP_VERSION}-android.apk" <<<"$assets_json" >/dev/null
grep -Fx "c64commander-${APP_VERSION}-android-play.aab" <<<"$assets_json" >/dev/null
- name: Upload AAB to Google Play (internal)
if: startsWith(github.ref, 'refs/tags/') && env.HAS_KEYSTORE == 'true'
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
packageName: uk.gleissner.c64commander
releaseFiles: artifacts/release-aab/c64commander-${{ env.APP_VERSION }}-android-play.aab
track: internal
# TODO Change to 'completed' once all requirements met and app submitted once manually
status: draft