Files
DroidDeck/.github/workflows/build.yml

898 lines
36 KiB
YAML

name: Build APK
run-name: ${{ github.event_name == 'pull_request' && format('building pr {0} on {1}', github.event.pull_request.number, github.event.pull_request.head.sha) || format('building {0} on {1}', github.ref_name, github.sha) }}
on:
push:
branches: [ main ]
paths-ignore:
- '**/*.md'
- 'LICENSE'
- '.gitignore'
- 'docs/**'
- '.github/ISSUE_TEMPLATE/**'
pull_request:
branches: [ main ]
types: [ opened, synchronize, reopened, ready_for_review ]
paths-ignore:
- '**/*.md'
- 'LICENSE'
- '.gitignore'
- 'docs/**'
- '.github/ISSUE_TEMPLATE/**'
workflow_dispatch:
inputs:
rebuild_preloads:
description: Rebuild preloads instead of reusing the approved binary snapshot
type: boolean
default: false
permissions:
contents: read
actions: read
pull-requests: write
concurrency:
group: build-${{ github.event.pull_request.number || (github.event_name == 'push' && 'main') || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
jobs:
source:
name: Pin source tree
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
base-sha: ${{ steps.merge.outputs.base-sha || steps.trusted.outputs.base-sha }}
tree-sha: ${{ steps.source.outputs.tree-sha }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Pin and merge the APK source
id: merge
if: github.event_name == 'pull_request'
run: |
set -euo pipefail
git fetch --no-tags origin main
echo "base-sha=$(git rev-parse origin/main)" >> "$GITHUB_OUTPUT"
git config user.name "ci"
git config user.email "ci@users.noreply.github.com"
git merge --no-ff --no-edit origin/main
- name: Resolve trusted manifest base
id: trusted
if: github.event_name != 'pull_request'
run: |
set -euo pipefail
git fetch --no-tags origin main
echo "base-sha=$(git rev-parse origin/main)" >> "$GITHUB_OUTPUT"
- name: Record pinned source tree
id: source
run: echo "tree-sha=$(git rev-parse 'HEAD^{tree}')" >> "$GITHUB_OUTPUT"
xbox:
name: Xbox ARM64 backend
needs: source
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ubuntu-24.04-arm
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Merge the pinned APK base
if: github.event_name == 'pull_request'
env:
BUILD_BASE_SHA: ${{ needs.source.outputs.base-sha }}
run: |
set -euo pipefail
git fetch --no-tags origin "$BUILD_BASE_SHA"
git config user.name "ci"
git config user.email "ci@users.noreply.github.com"
git merge --no-ff --no-edit FETCH_HEAD
- name: Verify pinned source tree
env:
SOURCE_TREE: ${{ needs.source.outputs.tree-sha }}
run: test "$(git rev-parse 'HEAD^{tree}')" = "$SOURCE_TREE"
- name: Plan component identities
id: plan
run: python3 tools/components/prepare.py plan
- uses: actions/cache@v4
id: xbox-cache
with:
path: |
app/src/main/assets/linuxfs/usr/local/lib/droiddeck-xbox
app/build/xbox/xodus-droiddeck-source.tar.xz
app/build/xbox/runtime/*-source.tar.xz
key: xbox-v2-${{ steps.plan.outputs.xbox }}
- name: Build Xbox backend
if: steps.xbox-cache.outputs.cache-hit != 'true'
run: python3 tools/components/prepare.py xbox
- uses: actions/upload-artifact@v4
with:
name: xbox-arm64
path: app/src/main/assets/linuxfs/usr/local/lib/droiddeck-xbox
retention-days: 14
- uses: actions/upload-artifact@v4
with:
name: xbox-corresponding-source
path: |
app/build/xbox/xodus-droiddeck-source.tar.xz
app/build/xbox/runtime/*-source.tar.xz
if-no-files-found: error
retention-days: 14
components:
name: Prepare verified components
needs: source
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
timeout-minutes: 40
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Merge the pinned APK base
if: github.event_name == 'pull_request'
env:
BUILD_BASE_SHA: ${{ needs.source.outputs.base-sha }}
run: |
set -euo pipefail
git fetch --no-tags origin "$BUILD_BASE_SHA"
git config user.name "ci"
git config user.email "ci@users.noreply.github.com"
git merge --no-ff --no-edit FETCH_HEAD
- name: Verify pinned source tree
env:
SOURCE_TREE: ${{ needs.source.outputs.tree-sha }}
run: test "$(git rev-parse 'HEAD^{tree}')" = "$SOURCE_TREE"
- name: Resolve the pinned Android NDK
run: bash tools/ci/resolve-ndk.sh
- name: Plan component identities
id: plan
run: python3 tools/components/prepare.py plan
- name: Cache remaining Linux runtime assets
id: preloads
uses: actions/cache@v4
with:
path: |
app/src/main/assets/linuxfs
!app/src/main/assets/linuxfs/usr/local/lib/droiddeck-comet
!app/src/main/assets/linuxfs/usr/local/lib/droiddeck-xbox
!app/src/main/assets/linuxfs/i386/libblsession.so
!app/src/main/assets/linuxfs/i386/libfakeinput.so
!app/src/main/assets/linuxfs/i386/libfaultreport.so
!app/src/main/assets/linuxfs/libblaudit.so
!app/src/main/assets/linuxfs/libblfastpath.so
!app/src/main/assets/linuxfs/libblsession.so
!app/src/main/assets/linuxfs/libfakeinput.so
!app/src/main/assets/linuxfs/libssbs.so
!app/src/main/assets/linuxfs/usr/local/bin/droiddeck-clipboard
!app/src/main/assets/linuxfs/x86_64/libblsession.so
!app/src/main/assets/linuxfs/x86_64/libfakeinput.so
!app/src/main/assets/linuxfs/x86_64/libfaultreport.so
!app/src/main/assets/linuxfs/x86_64/libvulkan-thunk.so
!app/src/main/assets/linuxfs/x86_64/libwebkitinput.so
key: runtime-v9-${{ steps.plan.outputs.runtime }}
- name: Cache PulseAudio 13.0 source
id: pa-src
uses: actions/cache@v4
with:
path: pa13
key: pulseaudio-src-${{ steps.plan.outputs.pulseaudio-source }}
- name: Cache proot
id: proot
uses: actions/cache@v4
with:
path: |
app/src/main/jniLibs/arm64-v8a/libproot.so
app/src/main/jniLibs/arm64-v8a/libproot-loader.so
key: proot-v2-${{ steps.plan.outputs.proot }}
- name: Cache GOG Galaxy companion
id: comet
uses: actions/cache@v4
with:
path: app/src/main/assets/linuxfs/usr/local/lib/droiddeck-comet
key: comet-v2-${{ steps.plan.outputs.comet }}
- name: Restore verified preload snapshots
id: component-preloads
env:
GH_TOKEN: ${{ github.token }}
FORCE_PRELOAD_REBUILD: ${{ inputs.rebuild_preloads || 'false' }}
COMPONENT_TRUSTED_BASE: ${{ needs.source.outputs.base-sha }}
run: python3 tools/components/prepare.py preload-snapshot
- name: Prepare required Linux build tools once
env:
COMPONENTS_TO_BUILD: >-
${{ steps.preloads.outputs.cache-hit != 'true' && 'runtime' || '' }}
${{ steps.component-preloads.outputs.arm64 != 'true' && 'arm64-preloads' || '' }}
${{ steps.comet.outputs.cache-hit != 'true' && 'comet' || '' }}
run: python3 tools/components/prepare.py tools
- name: Build changed ARM64 preloads
if: steps.component-preloads.outputs.arm64 != 'true'
run: python3 tools/components/prepare.py arm64-preloads
- name: Prepare remaining Linux runtime assets
if: steps.preloads.outputs.cache-hit != 'true'
run: python3 tools/components/prepare.py runtime
- name: Fetch the Debian image the preloads build in
if: steps.component-preloads.outputs.x86 != 'true'
env:
GH_TOKEN: ${{ github.token }}
run: python3 tools/components/prepare.py debian-image
- name: Build the x86 preloads FEX guests load (glibc 2.31)
if: steps.component-preloads.outputs.x86 != 'true'
run: python3 tools/components/prepare.py x86-preloads
- name: Bundle the droiddeck-esync packs
run: python3 tools/components/prepare.py esync
- name: Fetch PulseAudio 13.0 source
if: steps.pa-src.outputs.cache-hit != 'true'
run: python3 tools/components/prepare.py pulseaudio-source
- name: Fetch DirectAudio (tools/directaudio/release.env)
run: python3 tools/components/prepare.py directaudio
- name: Build the AAudio sink into the audio bundle
run: python3 tools/components/prepare.py audio
- name: Build proot
if: steps.proot.outputs.cache-hit != 'true'
run: python3 tools/components/prepare.py proot
- name: Check the preloads
run: |
set -euo pipefail
bash tools/linuxfs/check-preloads.sh app/src/main/assets/linuxfs
- name: Build GOG Galaxy companion
if: steps.comet.outputs.cache-hit != 'true'
run: python3 tools/components/prepare.py comet
- name: Prepare graphics driver variants
run: python3 tools/quest/build-driver.py "$ANDROID_HOME/ndk/$NDK_VERSION"
- name: Record and pack prepared components
run: python3 tools/components/prepare.py pack
- uses: actions/upload-artifact@v4
with:
name: prepared-components
path: app/build/prepared-components.tar.gz
if-no-files-found: error
compression-level: 0
retention-days: 14
build:
needs: [source, components, xbox]
name: Compile APK and Android tests
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
timeout-minutes: 45
outputs:
variants: ${{ steps.variants.outputs.list }}
build: ${{ steps.build.outcome }}
apk: ${{ steps.apk-upload.outcome }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Check human authorship
env:
EVENT_NAME: ${{ github.event_name }}
BEFORE: ${{ github.event.before }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" = pull_request ]; then
git fetch --no-tags origin main
base=$(git merge-base origin/main HEAD)
checker="$RUNNER_TEMP/droiddeck-check-authorship.py"
git show origin/main:tools/release/check_authorship.py > "$checker"
python3 "$checker" "$base..HEAD" --event "$GITHUB_EVENT_PATH"
elif [ "$EVENT_NAME" = push ] && [ "$BEFORE" != 0000000000000000000000000000000000000000 ]; then
python3 tools/release/check_authorship.py "$BEFORE..HEAD"
else
python3 tools/release/check_authorship.py HEAD
fi
- name: The packages a signed build ships as
id: variants
run: echo "list=$(awk '!/^#/ && NF { print $1 }' tools/release/variants.txt | jq -R . | jq -sc .)" >> "$GITHUB_OUTPUT"
- name: No private key in the tree
run: |
set -euo pipefail
keys=$(git ls-files | grep -iE '\.(jks|keystore|p12|pfx|pk8|pem|key)$' | grep -vx 'keystore/testkey.p12' || true)
if [ -n "$keys" ]; then
echo "::error::a private key is committed - remove it from the history and make a new one: $keys"
exit 1
fi
- name: Version bumps are isolated
if: github.event_name == 'pull_request'
run: |
set -euo pipefail
git fetch --no-tags origin main
base=$(git merge-base origin/main HEAD)
diff=$(git diff -U0 "$base..HEAD" -- app/build.gradle)
grep -Eq '^[+-][[:space:]]*version(Code|Name)[[:space:]]' <<<"$diff" || exit 0
other_files=$(git diff --name-only "$base..HEAD" | grep -v '^app/build.gradle$' || true)
other_gradle=$(grep -E '^[+-]' <<<"$diff" | grep -Ev '^(---|\+\+\+)' | grep -Ev '^[+-][[:space:]]*version(Code|Name)[[:space:]]' || true)
if [[ -n "$other_files" || -n "$other_gradle" ]]; then
echo "::error::versionCode/versionName changes must be isolated in their own PR"
echo "A version PR may only change the versionCode/versionName lines in app/build.gradle." >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
- name: Merge onto main, or say what conflicts
if: github.event_name == 'pull_request'
env:
BUILD_BASE_SHA: ${{ needs.source.outputs.base-sha }}
run: |
set -uo pipefail
git config user.name "ci"
git config user.email "ci@users.noreply.github.com"
git fetch --no-tags origin main
git fetch --no-tags origin "$BUILD_BASE_SHA"
BASE=$(git rev-parse --short FETCH_HEAD)
HEAD=$(git rev-parse --short HEAD)
if git merge --no-ff --no-edit FETCH_HEAD >/tmp/merge.out 2>&1; then
{
echo "### Mergeable"
echo
echo "\`$HEAD\` merges cleanly onto main (\`$BASE\`). What follows was built from the merged result."
} > /tmp/pr-merge.md
cat /tmp/pr-merge.md >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
FILES=$(git diff --name-only --diff-filter=U)
if [ -z "$FILES" ]; then
{
echo "### ERROR: Could not merge onto main (\`$BASE\`)"
echo
echo '```'
cat /tmp/merge.out
echo '```'
} > /tmp/pr-merge.md
cat /tmp/pr-merge.md >> "$GITHUB_STEP_SUMMARY"
echo "::error::git could not merge main: $(head -3 /tmp/merge.out | tr '\n' ' ')"
exit 1
fi
{
echo "### ERROR: Not mergeable - conflicts with main (\`$BASE\`)"
echo
echo "Files that conflict:"
echo
for f in $FILES; do echo "- \`$f\`"; done
echo
echo "Each conflicting hunk, as git leaves it: \`<<<<<<< HEAD\` is this pull request, \`>>>>>>> origin/main\` is main."
echo
for f in $FILES; do
echo "<details><summary><code>$f</code></summary>"
echo
echo '```'
awk '/^<<<<<<< /{p=1} p{print} /^>>>>>>> /{p=0}' "$f" | head -300
echo '```'
echo "</details>"
echo
done
echo "Not built: rebase or merge main into the branch and push again."
} > /tmp/pr-merge.md
cat /tmp/pr-merge.md >> "$GITHUB_STEP_SUMMARY"
git merge --abort || true
echo "::error::the pull request conflicts with main: $(echo $FILES | tr '\n' ' ')"
exit 1
- name: Verify backend and APK source match
env:
XBOX_SOURCE_TREE: ${{ needs.source.outputs.tree-sha }}
run: test "$(git rev-parse 'HEAD^{tree}')" = "$XBOX_SOURCE_TREE"
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
cache-cleanup: ${{ github.event_name == 'pull_request' && 'never' || 'on-success' }}
- name: Resolve the pinned Android NDK
run: bash tools/ci/resolve-ndk.sh
- name: Set the APK build label
id: build-label
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BUILD_REF: ${{ github.ref_name }}
BUILD_SHA: ${{ github.sha }}
run: |
if [[ -n "$(git status --porcelain)" ]]; then
tree_state=dirty
else
tree_state=clean
fi
echo "DROIDDECK_BUILD_TREE_STATE=$tree_state" >> "$GITHUB_ENV"
if [[ -n "$PR_NUMBER" ]]; then
label="pr${PR_NUMBER} at ${PR_HEAD_SHA:0:7}"
else
label="${BUILD_REF} at ${BUILD_SHA:0:7}"
fi
echo "label=$label" >> "$GITHUB_OUTPUT"
echo "DROIDDECK_SOURCE_COMMIT=${PR_HEAD_SHA:-$BUILD_SHA}" >> "$GITHUB_ENV"
echo "DROIDDECK_PR_NUMBER=${PR_NUMBER:-0}" >> "$GITHUB_ENV"
echo "Build: \`$label\`" >> "$GITHUB_STEP_SUMMARY"
- name: Rust toolchain for the store engines (aarch64-linux-android)
id: rust
run: |
set -euo pipefail
rustup target add aarch64-linux-android
echo "rustc=$(rustc -V | awk '{print $2}')" >> "$GITHUB_OUTPUT"
rustc -V; cargo -V
- name: Cache cargo registry and the store engines' target
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
~/.cargo/bin/cargo-ndk
app/src/main/rust/stores/target
key: stores-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.NDK_VERSION }}-${{ steps.rust.outputs.rustc }}-${{ hashFiles('app/src/main/rust/stores/Cargo.lock') }}-${{ hashFiles('app/src/main/rust/stores/src/**', 'app/src/main/rust/stores/Cargo.toml', 'app/src/main/rust/stores/build.rs', 'app/src/main/rust/stores/.cargo/**', 'app/build.gradle') }}
restore-keys: |
stores-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.NDK_VERSION }}-${{ steps.rust.outputs.rustc }}-${{ hashFiles('app/src/main/rust/stores/Cargo.lock') }}-
stores-v2-${{ runner.os }}-${{ runner.arch }}-${{ env.NDK_VERSION }}-${{ steps.rust.outputs.rustc }}-
- name: cargo-ndk
run: |
set -euo pipefail
command -v cargo-ndk >/dev/null || cargo install cargo-ndk --locked
cargo ndk --version
- name: Cache the CMake build tree
uses: actions/cache@v4
with:
path: |
app/.cxx
app/build/intermediates/cxx
key: cxx-${{ env.NDK_VERSION }}-${{ hashFiles('app/src/main/cpp/**', 'tools/vr/src/xr_*.h') }}
restore-keys: |
cxx-${{ env.NDK_VERSION }}-
- uses: actions/download-artifact@v4
with:
name: prepared-components
path: app/build
- name: Verify and unpack prepared components
env:
SOURCE_TREE: ${{ needs.source.outputs.tree-sha }}
run: python3 tools/components/prepare.py unpack --tree "$SOURCE_TREE"
- uses: actions/download-artifact@v4
with:
name: xbox-arm64
path: app/src/main/assets/linuxfs/usr/local/lib/droiddeck-xbox
- name: Cache checksum-pinned pack inputs
uses: actions/cache@v4
with:
path: ~/.cache/droiddeck-pack-inputs
key: pack-inputs-v1-${{ hashFiles('tools/packs/build.sh', 'tools/packs/fetch-verified.sh', 'tools/packs/tracking/release.env') }}
restore-keys: pack-inputs-v1-
- name: Build the add-on packs
run: bash tools/packs/build.sh app/build/packs
- name: Note when the proot pack pin is behind this build
run: |
set -euo pipefail
# shellcheck disable=SC1091
source tools/packs/proot/release.env
sha=$(sha256sum app/build/packs/proot.tar.zst | cut -d' ' -f1)
if [ "$sha" != "$PROOT_PACK_SHA256" ]; then
echo "::warning::built proot pack $sha does not match tools/packs/proot/release.env ($PROOT_PACK_SHA256). This build publishes the pack it just built. Source builds keep downloading the pinned pack until release.env is updated."
fi
- name: Build
id: build
env:
DROIDDECK_BUILD_LABEL: ${{ steps.build-label.outputs.label }}
SGDB_API_KEY: ${{ secrets.SGDB_API_KEY }}
run: |
set -o pipefail
python3 tools/release/runtime_inputs.py record
./gradlew assembleRelease --console=plain -PndkVersion="$NDK_VERSION" 2>&1 | tee /tmp/build.log
python3 tools/release/check_session_assets.py app/build/outputs/apk/release/app-release.apk
python3 tools/release/runtime_inputs.py apk app/build/outputs/apk/release/app-release.apk
- name: Check the store engines' library
run: |
set -euo pipefail
NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
python3 tools/release/check_android_libraries.py app/build/outputs/apk/release/app-release.apk \
--readelf "$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf"
git diff --stat -- app/src/main/rust/stores/Cargo.lock
git diff --exit-code -- app/src/main/rust/stores/Cargo.lock \
|| { echo "::error::Cargo.lock changed during the build; commit the regenerated lockfile (uploaded as stores-cargo-lock)"; exit 1; }
- name: Store engines host unit tests
timeout-minutes: 10
continue-on-error: true
run: |
cd app/src/main/rust/stores
cargo test --locked 2>&1 | tail -60
- name: Upload the regenerated Cargo.lock
if: failure()
uses: actions/upload-artifact@v4
with:
name: stores-cargo-lock
path: app/src/main/rust/stores/Cargo.lock
if-no-files-found: ignore
- name: Sign with the testkey (v1 + v2 + v3)
run: |
set -euo pipefail
APK=$(find app/build/outputs/apk -name '*.apk' | head -1)
TOOLS=$(find "$ANDROID_HOME/build-tools" -maxdepth 1 -type d | sort -V | tail -1)
"$TOOLS/zipalign" -p -f 4 "$APK" "$APK.aligned"
"$TOOLS/apksigner" sign \
--ks keystore/testkey.p12 --ks-type PKCS12 --ks-pass pass:android \
--ks-key-alias testkey --key-pass pass:android \
--v1-signing-enabled true --v2-signing-enabled true --v3-signing-enabled true \
--out "$APK" "$APK.aligned"
rm -f "$APK.aligned" "$APK.idsig"
- name: Check the signature
run: |
set -euo pipefail
APK=$(find app/build/outputs/apk -name '*.apk' | head -1)
APKSIGNER=$(find "$ANDROID_HOME/build-tools" -name apksigner | sort -V | tail -1)
"$APKSIGNER" verify --min-sdk-version 21 --verbose --print-certs "$APK" | tee /tmp/sig.txt
unzip -l "$APK" | grep -qE 'META-INF/.*\.(SF|RSA|DSA)$' \
|| { echo "::error::apk carries no JAR signature files"; exit 1; }
for scheme in "v1 scheme (JAR signing): true" "v2 scheme (APK Signature Scheme v2): true" \
"v3 scheme (APK Signature Scheme v3): true"; do
grep -qF "$scheme" /tmp/sig.txt || { echo "::error::apk is not signed with $scheme"; exit 1; }
done
grep -qi "CN=Android, OU=Android, O=Android" /tmp/sig.txt \
|| { echo "::error::apk is not signed with the AOSP testkey"; exit 1; }
- uses: actions/upload-artifact@v4
id: apk-upload
with:
name: droiddeck-apk
path: app/build/outputs/apk/release/*.apk
if-no-files-found: error
retention-days: 30
compression-level: 0
- uses: actions/upload-artifact@v4
with:
name: droiddeck-packs
path: app/build/packs/*.tar.zst
if-no-files-found: error
retention-days: 30
compression-level: 0
- name: Keep the build log for the pull request report
if: always() && github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: build-report
path: |
/tmp/build.log
/tmp/pr-merge.md
if-no-files-found: ignore
retention-days: 1
unit-tests:
needs: [source, components, xbox]
name: Android unit tests
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
timeout-minutes: 45
outputs:
outcome: ${{ steps.unit-tests.outcome }}
diagnostics: ${{ steps.unit-test-diagnostics.outputs.artifact-url }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Merge the pinned APK base
if: github.event_name == 'pull_request'
env:
BUILD_BASE_SHA: ${{ needs.source.outputs.base-sha }}
run: |
set -euo pipefail
git fetch --no-tags origin "$BUILD_BASE_SHA"
git config user.name "ci"
git config user.email "ci@users.noreply.github.com"
git merge --no-ff --no-edit FETCH_HEAD
- name: Verify backend and APK source match
env:
XBOX_SOURCE_TREE: ${{ needs.source.outputs.tree-sha }}
run: test "$(git rev-parse 'HEAD^{tree}')" = "$XBOX_SOURCE_TREE"
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }}
cache-cleanup: ${{ github.event_name == 'pull_request' && 'never' || 'on-success' }}
- name: Resolve the pinned Android NDK
run: bash tools/ci/resolve-ndk.sh
- name: Set the APK build label
id: build-label
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BUILD_REF: ${{ github.ref_name }}
BUILD_SHA: ${{ github.sha }}
run: |
if [[ -n "$(git status --porcelain)" ]]; then
tree_state=dirty
else
tree_state=clean
fi
echo "DROIDDECK_BUILD_TREE_STATE=$tree_state" >> "$GITHUB_ENV"
if [[ -n "$PR_NUMBER" ]]; then
label="pr${PR_NUMBER} at ${PR_HEAD_SHA:0:7}"
else
label="${BUILD_REF} at ${BUILD_SHA:0:7}"
fi
echo "label=$label" >> "$GITHUB_OUTPUT"
echo "DROIDDECK_SOURCE_COMMIT=${PR_HEAD_SHA:-$BUILD_SHA}" >> "$GITHUB_ENV"
echo "DROIDDECK_PR_NUMBER=${PR_NUMBER:-0}" >> "$GITHUB_ENV"
- uses: actions/download-artifact@v4
with:
name: prepared-components
path: app/build
- name: Verify and unpack prepared components
env:
SOURCE_TREE: ${{ needs.source.outputs.tree-sha }}
run: python3 tools/components/prepare.py unpack --tree "$SOURCE_TREE"
- uses: actions/download-artifact@v4
with:
name: xbox-arm64
path: app/src/main/assets/linuxfs/usr/local/lib/droiddeck-xbox
- name: Cache checksum-pinned pack inputs
uses: actions/cache@v4
with:
path: ~/.cache/droiddeck-pack-inputs
key: pack-inputs-v1-${{ hashFiles('tools/packs/build.sh', 'tools/packs/fetch-verified.sh', 'tools/packs/tracking/release.env') }}
restore-keys: pack-inputs-v1-
- name: Build the add-on packs
run: bash tools/packs/build.sh app/build/packs
- name: Unit tests
id: unit-tests
env:
DROIDDECK_BUILD_LABEL: ${{ steps.build-label.outputs.label }}
SGDB_API_KEY: ${{ secrets.SGDB_API_KEY }}
run: |
set -o pipefail
mkdir -p app/build
python3 tools/release/runtime_inputs.py record
./gradlew testReleaseUnitTest --console=plain --info --stacktrace -Drobolectric.logging=stderr -PskipRust=true -PndkVersion="$NDK_VERSION" 2>&1 | tee app/build/unit-tests.log
- name: Upload unit test diagnostics
id: unit-test-diagnostics
if: always() && steps.unit-tests.outcome != 'skipped'
uses: actions/upload-artifact@v4
with:
name: unit-test-results
path: |
app/build/unit-tests.log
app/build/test-results/testReleaseUnitTest/
app/build/reports/tests/testReleaseUnitTest/
if-no-files-found: ignore
retention-days: 14
report:
name: Report the build on the pull request
needs: [build, unit-tests]
if: ${{ always() && github.event_name == 'pull_request' && !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- uses: actions/download-artifact@v4
continue-on-error: true
with:
name: build-report
path: /tmp
- name: Report on the pull request
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
BUILD_OUTCOME: ${{ needs.build.outputs.build || needs.build.result }}
UNIT_TEST_OUTCOME: ${{ needs.unit-tests.result }}
TEST_DIAGNOSTICS_URL: ${{ needs.unit-tests.outputs.diagnostics }}
APK_OUTCOME: ${{ needs.build.result == 'success' && needs.build.outputs.apk || needs.build.result }}
run: bash tools/release/report-build.sh
python-tests:
name: Python and native regression tests
needs: source
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Merge the pinned APK base
if: github.event_name == 'pull_request'
env:
BUILD_BASE_SHA: ${{ needs.source.outputs.base-sha }}
run: |
set -euo pipefail
git fetch --no-tags origin "$BUILD_BASE_SHA"
git config user.name "ci"
git config user.email "ci@users.noreply.github.com"
git merge --no-ff --no-edit FETCH_HEAD
- name: Verify backend and test source match
env:
XBOX_SOURCE_TREE: ${{ needs.source.outputs.tree-sha }}
run: test "$(git rev-parse 'HEAD^{tree}')" = "$XBOX_SOURCE_TREE"
- name: Install Python test dependencies
timeout-minutes: 7
run: bash tools/ci/install-ubuntu-packages.sh python3-yaml gcc-aarch64-linux-gnu libc6-dev-arm64-cross qemu-user libvulkan-dev mesa-vulkan-drivers
- name: Resolve the pinned Android NDK
run: bash tools/ci/resolve-ndk.sh
- name: Check Quest tracking IPC
run: |
set -euo pipefail
NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
VK_INCLUDE=$(ls -d "$NDK"/toolchains/llvm/prebuilt/*/sysroot/usr/include | head -1)
mkdir -p build/vr/include
cp -R "$VK_INCLUDE/vulkan" build/vr/include/
if [ -d "$VK_INCLUDE/vk_video" ]; then cp -R "$VK_INCLUDE/vk_video" build/vr/include/; fi
g++ -std=c++17 -Wall -Wextra -Werror -pthread tools/vr/tests/test_xr_tracking.cpp -o /tmp/test-xr-tracking
/tmp/test-xr-tracking
g++ -std=c++17 -Wall -Wextra -Werror tools/vr/tests/test_xr_touch.cpp -o /tmp/test-xr-touch
/tmp/test-xr-touch
g++ -std=c++17 -Wall -Wextra -Werror tools/vr/tests/test_pad_controller_config.cpp -o /tmp/test-pad-controller-config
/tmp/test-pad-controller-config
g++ -std=c++17 -Wall -Wextra -Werror -pthread tools/vr/tests/test_xr_vsync.cpp -o /tmp/test-xr-vsync
/tmp/test-xr-vsync
g++ -std=c++17 -Wall -Wextra -Werror -pthread tools/vr/tests/test_xr_frames.cpp -o /tmp/test-xr-frames
/tmp/test-xr-frames
cc -Itools/vr/third_party/wayland -c tools/vr/src/wayland_client_stub.c -o /tmp/test-wayland-stub.o
cc -Itools/vr/third_party/wayland -c app/src/main/cpp/waylandcomp/generated/xdg-shell-protocol.c -o /tmp/test-xdg-shell.o
g++ -std=c++17 -Wall -Wextra -Werror -Wno-unused-parameter -Wno-missing-field-initializers -pthread -Ibuild/vr/include \
-Itools/vr/third_party/wayland -Iapp/src/main/cpp/waylandcomp/generated \
tools/vr/tests/test_presenter_shutdown.cpp tools/vr/src/presenter.cpp /tmp/test-wayland-stub.o /tmp/test-xdg-shell.o \
-ldl -o /tmp/test-presenter-shutdown
timeout 10 /tmp/test-presenter-shutdown
- name: Steam import and game environment tests
run: bash tools/ci/run-python-tests.sh
checks:
name: Compile and test APK
needs: [build, unit-tests, python-tests]
if: ${{ always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Require compilation and every test job
env:
BUILD_RESULT: ${{ needs.build.result }}
UNIT_RESULT: ${{ needs.unit-tests.result }}
PYTHON_RESULT: ${{ needs.python-tests.result }}
run: test "$BUILD_RESULT" = success && test "$UNIT_RESULT" = success && test "$PYTHON_RESULT" = success
sign:
needs: [build, checks]
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
permissions:
contents: read
actions: read
uses: ./.github/workflows/sign.yml
with:
environment: signing-main
variants: ${{ needs.build.outputs.variants }}
secrets: inherit
packs:
needs: [build, checks]
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
permissions:
contents: read
actions: read
uses: ./.github/workflows/publish-packs.yml
secrets: inherit
announce:
needs: [build, sign, packs]
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
pull-requests: read
env:
CI_REPO: Droid-Deck/DroidDeck-CI
KEEP: 10
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: actions/download-artifact@v4
with:
pattern: droiddeck-signed-*
merge-multiple: true
path: signed
- uses: actions/create-github-app-token@v2
id: ci-token
with:
app-id: ${{ vars.DROIDDECK_CI_APP_ID }}
private-key: ${{ secrets.DROIDDECK_CI_APP_KEY }}
owner: Droid-Deck
repositories: DroidDeck-CI
- name: Describe the change
env:
GH_TOKEN: ${{ github.token }}
run: bash tools/release/describe-build.sh
- name: Publish to DroidDeck-CI
id: publish
env:
GH_TOKEN: ${{ steps.ci-token.outputs.token }}
run: bash tools/release/publish-ci.sh
- name: Post the Preview build to Discord
env:
WEBHOOK: ${{ secrets.DISCORD_BUILDS_WEBHOOK }}
GH_TOKEN: ${{ github.token }}
RELEASE: ${{ steps.publish.outputs.release }}
APK: ${{ steps.publish.outputs.apk }}
run: bash tools/release/announce-preview.sh
catalog:
needs: announce
if: ${{ always() && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && needs.announce.result != 'skipped' }}
uses: ./.github/workflows/update-catalog.yml
secrets: inherit