mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 11:31:08 +00:00
A second sink module: connects to the relay's socket (lazily, the relay comes up after the daemon; a clocked null sink until then), sends the hello, maps the render ring it gets back and keeps it topped up to the relay's target with a futex wait per burst. Every AAudio call, the adaptive buffer and the route handling stay in the relay, outside proot. With DirectAudio on the daemon loads this sink instead of module-aaudio-sink; the bundle carries both.
427 lines
22 KiB
YAML
427 lines
22 KiB
YAML
name: Build APK
|
|
|
|
# One job: the two glibc preload libraries the session needs, then the app around them.
|
|
# Nothing is built on a developer machine - this workflow is the only thing that produces an apk.
|
|
#
|
|
# This repo is private, so every minute here is billed. Three caches carry the cost of a rebuild
|
|
# down to what actually changed:
|
|
# • Gradle's own (dependencies, wrapper, build cache, configuration cache) via setup-gradle
|
|
# • the CMake/ninja tree, so a Kotlin-only change does not recompile the compositor
|
|
# • the two cross-compiled preloads, which also skips installing the cross toolchain
|
|
# A run that changes only Kotlin should not touch the C compiler at all.
|
|
|
|
on:
|
|
push:
|
|
branches: [ main ]
|
|
# Documentation changes cannot alter the apk, so they do not buy a build.
|
|
paths-ignore:
|
|
- '**/*.md'
|
|
- 'LICENSE'
|
|
- '.gitignore'
|
|
# A pull request against main builds by itself: the contributor's branch, as it would be once
|
|
# merged onto main, comes back as an apk on the run without anyone here pressing a button.
|
|
# Every push to the pull request rebuilds it and cancels the build it replaces; a draft waits
|
|
# until it is marked ready. pull_request_target rather than pull_request: GitHub does not run
|
|
# the latter at all while a pull request conflicts with main, and saying what conflicts is the
|
|
# point. The workflow file is always main's; the pull request's own code is checked out below.
|
|
pull_request_target:
|
|
branches: [ main ]
|
|
types: [ opened, synchronize, reopened, ready_for_review ]
|
|
paths-ignore:
|
|
- '**/*.md'
|
|
- 'LICENSE'
|
|
- '.gitignore'
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
# The pull-request report below is one comment on the pull request, kept up to date.
|
|
pull-requests: write
|
|
|
|
# A newer push makes the running build pointless; stop paying for it.
|
|
concurrency:
|
|
group: build-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
# Bump to force the preload cache to miss (e.g. after changing a compiler flag below).
|
|
PRELOAD_CACHE_VERSION: v1
|
|
|
|
jobs:
|
|
build:
|
|
if: ${{ github.event_name != 'pull_request_target' || !github.event.pull_request.draft }}
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 45
|
|
steps:
|
|
# A pull request is checked out at its own head with full history, not at GitHub's merge
|
|
# ref: that ref does not exist while the pull request conflicts with main, and the point of
|
|
# the next step is to say so and show where.
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
|
|
# Full history: the merge below needs it (a shallow clone refuses to merge at all).
|
|
fetch-depth: 0
|
|
|
|
# The pull request as it would be once merged onto main is what gets built. When it cannot
|
|
# be merged, the conflicting files and the conflicting hunks go into the run summary and the
|
|
# pull-request comment, and the job stops here: there is nothing coherent to build.
|
|
- name: Merge onto main, or say what conflicts
|
|
if: github.event_name == 'pull_request_target'
|
|
run: |
|
|
set -uo pipefail
|
|
git config user.name "ci"
|
|
git config user.email "ci@users.noreply.github.com"
|
|
git fetch --no-tags origin main
|
|
BASE=$(git rev-parse --short origin/main)
|
|
HEAD=$(git rev-parse --short HEAD)
|
|
if git merge --no-ff --no-edit origin/main >/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
|
|
# The merge did not even start (git said why): quote it rather than list nothing.
|
|
{
|
|
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
|
|
|
|
- uses: actions/setup-java@v4
|
|
with:
|
|
distribution: temurin
|
|
java-version: '17'
|
|
|
|
# Caches ~/.gradle (dependencies, wrapper distribution, build cache, configuration cache)
|
|
# and prunes what the run did not use, so the entry cannot grow without bound.
|
|
- uses: gradle/actions/setup-gradle@v4
|
|
with:
|
|
cache-cleanup: on-success
|
|
|
|
- name: Resolve the runner's NDK
|
|
run: |
|
|
NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
|
|
VERSION=$(sed -n 's/^Pkg.Revision *= *//p' "$NDK/source.properties")
|
|
echo "NDK $VERSION at $NDK"
|
|
echo "NDK_VERSION=$VERSION" >> "$GITHUB_ENV"
|
|
|
|
# ── The session's preloads ───────────────────────────────────────────────────────────
|
|
# Both are glibc aarch64 libraries that live inside the rootfs, so they are cross-compiled
|
|
# here rather than by the NDK. They ship in assets (not in the runtime tarball) so a fix in
|
|
# either reaches an already-installed runtime on the next launch instead of waiting on a
|
|
# ~790 MB runtime re-host. Their inputs change rarely, so most runs restore them.
|
|
- name: Cache the preloads
|
|
id: preloads
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: app/src/main/assets/linuxfs
|
|
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env') }}
|
|
|
|
- name: Build libfakeinput.so and libblsession.so (glibc aarch64)
|
|
if: steps.preloads.outputs.cache-hit != 'true'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -y -qq gcc-aarch64-linux-gnu g++-aarch64-linux-gnu >/dev/null
|
|
D=app/src/main/assets/linuxfs
|
|
mkdir -p "$D"
|
|
# Statically linked C++ runtime: this is preloaded into every Steam process, and Steam
|
|
# ships its own libstdc++ while rewriting LD_LIBRARY_PATH. Pulling the system one in
|
|
# ahead of it is a version conflict waiting to happen. Only libc is left as a dependency.
|
|
aarch64-linux-gnu-g++ -shared -fPIC -O2 -Wall -Wno-attributes -Wno-nonnull-compare \
|
|
-pthread -std=c++17 -static-libstdc++ -static-libgcc \
|
|
-o "$D/libfakeinput.so" app/src/main/cpp/fakeinput_steam.cpp -ldl
|
|
aarch64-linux-gnu-strip --strip-unneeded "$D/libfakeinput.so"
|
|
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
|
|
-o "$D/libblsession.so" tools/linuxfs/preload/*.c -ldl
|
|
aarch64-linux-gnu-strip --strip-unneeded "$D/libblsession.so"
|
|
# The session's scripts travel the same way, for the same reason.
|
|
for script in tools/linuxfs/overlay/usr/local/bin/bannerlator-*; do
|
|
install -Dm644 "$script" "$D/usr/local/bin/$(basename "$script")"
|
|
done
|
|
# The SteamOS helpers the client calls in Deck mode, as no-op stubs (see each file).
|
|
for f in tools/linuxfs/overlay/usr/bin/* tools/linuxfs/overlay/usr/bin/steamos-polkit-helpers/*; do
|
|
[ -f "$f" ] && install -Dm644 "$f" "$D/${f#tools/linuxfs/overlay/}"
|
|
done
|
|
# The patched gamescope, when a build of it has been published (tools/gamescope/release.env
|
|
# names the release and the archive's sha256); see tools/gamescope/PATCHES.md.
|
|
if [ -f tools/gamescope/release.env ]; then
|
|
. tools/gamescope/release.env
|
|
# Through the API: the repository is private, so the plain download URL answers 404.
|
|
gh release download "${GAMESCOPE_TAG}" -R "${GITHUB_REPOSITORY}" -p gamescope.tzst -O gamescope.tzst
|
|
echo "${GAMESCOPE_SHA256} gamescope.tzst" | sha256sum -c -
|
|
tar --use-compress-program=unzstd -xf gamescope.tzst -C "$D"
|
|
test -f "$D/usr/local/bin/gamescope"
|
|
rm gamescope.tzst
|
|
fi
|
|
# And the desktop's launcher and labwc defaults, staged over the desktop package's copies.
|
|
install -Dm644 tools/linuxfs/desktop/steamdeck-desktop "$D/usr/local/bin/steamdeck-desktop"
|
|
install -Dm644 tools/linuxfs/desktop/autostart "$D/etc/xdg/labwc/autostart"
|
|
install -Dm644 tools/linuxfs/desktop/rc.xml "$D/etc/xdg/labwc/rc.xml"
|
|
install -Dm644 tools/linuxfs/desktop/panel.conf "$D/etc/xdg/lxqt/panel.conf"
|
|
install -Dm644 tools/linuxfs/desktop/firefox-steamdeck.js "$D/usr/lib/firefox/defaults/pref/steamdeck.js"
|
|
ls -lR "$D"
|
|
|
|
# The daemon's AAudio sink (tools/aaudio-sink), compiled here against upstream PulseAudio 13
|
|
# headers and the app's own libpulse*, then put into the audio bundle the apk carries. The
|
|
# committed bundle holds every other module and no sink, so a build without this step would
|
|
# have no audio - the unpack check in PulseAudioComponent would then fail loudly, not quietly.
|
|
- name: Cache PulseAudio 13.0 source
|
|
id: pa-src
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: pa13
|
|
key: pulseaudio-src-13.0
|
|
- name: Fetch PulseAudio 13.0 source
|
|
if: steps.pa-src.outputs.cache-hit != 'true'
|
|
run: |
|
|
set -euo pipefail
|
|
curl -fsSL -o pa.tar.gz https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz
|
|
mkdir -p pa13 && tar -xzf pa.tar.gz -C pa13 --strip-components=1 && rm pa.tar.gz
|
|
- name: Build the AAudio sink into the audio bundle
|
|
run: |
|
|
set -euo pipefail
|
|
export NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"
|
|
tools/aaudio-sink/build.sh "$PWD/pa13" "$PWD/sink-out"
|
|
rm -rf pa-bundle && mkdir -p pa-bundle
|
|
tar --use-compress-program=unzstd -xf app/src/main/assets/pulseaudio.tzst -C pa-bundle
|
|
test ! -f pa-bundle/modules/arm64/module-aaudio-sink.so
|
|
install -m755 sink-out/module-aaudio-sink.so pa-bundle/modules/arm64/module-aaudio-sink.so
|
|
install -m755 sink-out/module-directaudio-sink.so pa-bundle/modules/arm64/module-directaudio-sink.so
|
|
(cd pa-bundle && tar --use-compress-program='zstd -19' -cf ../pulseaudio.tzst .)
|
|
mv pulseaudio.tzst app/src/main/assets/pulseaudio.tzst
|
|
tar --use-compress-program=unzstd -tf app/src/main/assets/pulseaudio.tzst | grep -c '\.so$'
|
|
rm -rf pa-bundle sink-out
|
|
|
|
# Runs whether the preloads were built or restored: readelf reads any ELF whatever the host,
|
|
# so these assertions cost nothing and need no cross toolchain.
|
|
- name: Check the preloads
|
|
run: |
|
|
set -euo pipefail
|
|
D=app/src/main/assets/linuxfs
|
|
NEED=$(readelf -d "$D/libfakeinput.so" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p')
|
|
echo "libfakeinput NEEDED: $NEED"
|
|
for bad in libstdc++.so.6 libgcc_s.so.1; do
|
|
if printf '%s\n' "$NEED" | grep -qx "$bad"; then
|
|
echo "::error::libfakeinput.so links $bad - the C++ runtime must stay static"; exit 1
|
|
fi
|
|
done
|
|
syms() { readelf -Ws "$1" | awk '$4 == "FUNC" && $5 == "GLOBAL" {sub(/@.*/, "", $8); print $8}'; }
|
|
# The interposers are the whole point: if one is missing the client sees no controller.
|
|
FAKE=$(syms "$D/libfakeinput.so")
|
|
for sym in open openat ioctl read close poll ppoll select stat fstat access scandir; do
|
|
grep -qx "$sym" <<<"$FAKE" || { echo "::error::libfakeinput.so does not export $sym"; exit 1; }
|
|
done
|
|
SESSION=$(syms "$D/libblsession.so")
|
|
for sym in socket bind getsockname setsockopt statfs statvfs; do
|
|
grep -qx "$sym" <<<"$SESSION" || { echo "::error::libblsession.so does not export $sym"; exit 1; }
|
|
done
|
|
test -f "$D/usr/local/bin/bannerlator-session"
|
|
test -f "$D/usr/local/bin/bannerlator-proton-extra"
|
|
echo "preloads check out"
|
|
|
|
# ── The native build ─────────────────────────────────────────────────────────────────
|
|
# The compositor and adrenotools are the slowest part of a build and change least often.
|
|
# The key pins the C sources and the NDK; the restore-key still hands an older tree to
|
|
# ninja, which then rebuilds only the files that differ.
|
|
- 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/**') }}
|
|
restore-keys: |
|
|
cxx-${{ env.NDK_VERSION }}-
|
|
|
|
# Release, not debug: AGP quietly drops v1 (JAR) signing from a debug build once minSdk is
|
|
# 24 or higher, and the apk has to carry all three schemes. Nothing is minified either way.
|
|
# The whole log is kept so a failed build's errors can be quoted back on a pull request.
|
|
- name: Build
|
|
id: build
|
|
run: |
|
|
set -o pipefail
|
|
./gradlew assembleRelease --console=plain -PndkVersion="$NDK_VERSION" 2>&1 | tee /tmp/build.log
|
|
|
|
- name: Describe the apk
|
|
run: |
|
|
set -euo pipefail
|
|
APK=$(find app/build/outputs/apk -name '*.apk' | head -1)
|
|
echo "apk: $APK"
|
|
ls -l "$APK"
|
|
sha256sum "$APK"
|
|
unzip -l "$APK" | grep -E 'lib/arm64-v8a|assets/linuxfs' | head -30
|
|
|
|
# Every NEEDED of every library we ship must resolve inside the apk or on the device. The
|
|
# first build shipped libwayland-server.so without libffi.so, and the app died on
|
|
# System.loadLibrary the moment a session started - a check this step would have caught.
|
|
- name: Check the native dependency closure
|
|
run: |
|
|
set -euo pipefail
|
|
APK=$(find app/build/outputs/apk -name '*.apk' | head -1)
|
|
WORK=$(mktemp -d)
|
|
unzip -q "$APK" 'lib/arm64-v8a/*' -d "$WORK"
|
|
cd "$WORK/lib/arm64-v8a"
|
|
# Provided by the platform, so never packaged.
|
|
SYSTEM="libc.so libm.so libdl.so liblog.so libandroid.so libz.so libvulkan.so
|
|
libGLESv2.so libEGL.so libnativewindow.so libjnigraphics.so libaaudio.so
|
|
libOpenSLES.so libmediandk.so libcamera2ndk.so libsync.so libneuralnetworks.so"
|
|
fail=0
|
|
for so in *.so; do
|
|
for need in $(readelf -d "$so" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p'); do
|
|
[ -f "$need" ] && continue
|
|
# $SYSTEM spans lines; unquoted echo folds its newlines to spaces, or the last
|
|
# name on each line could never match (libaaudio.so did not, for the relay helper).
|
|
case " $(echo $SYSTEM) " in *" $need "*) continue ;; esac
|
|
echo "::error::$so needs $need, which is neither in the apk nor a platform library"
|
|
fail=1
|
|
done
|
|
done
|
|
[ "$fail" = 0 ] && echo "every NEEDED resolves"
|
|
exit $fail
|
|
|
|
# AGP signs with v2+v3 and drops v1 whenever minSdk is 24 or higher, and setting
|
|
# enableV1Signing in the DSL does not move it. Re-signing here with apksigner is the one way
|
|
# to be sure of what the apk carries: zipalign first, then all three schemes explicitly.
|
|
- 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"
|
|
|
|
# The apk must stay installable over the last one, and on every Android version: that means
|
|
# one stable key and all three signature schemes. Cheap to assert, easy to lose silently.
|
|
- 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)
|
|
# --min-sdk-version 21: apksigner only verifies the schemes an apk's own minSdk requires,
|
|
# so at minSdk 26 it reports v1 as false whether or not the JAR signature is there.
|
|
# Asking it to verify from 21 makes it actually look.
|
|
"$APKSIGNER" verify --min-sdk-version 21 --verbose --print-certs "$APK" | tee /tmp/sig.txt
|
|
# And the files themselves, which is what an old installer reads.
|
|
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; }
|
|
|
|
# ── The pull-request report ──────────────────────────────────────────────────────────
|
|
# One comment per pull request, rewritten on every push: whether it merges, and when the
|
|
# build failed, the compiler's own errors with paths made short. Fork pull requests carry a
|
|
# read-only token and cannot comment; the same text is in the run summary either way.
|
|
- name: Report on the pull request
|
|
if: always() && github.event_name == 'pull_request_target'
|
|
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: ${{ steps.build.outcome }}
|
|
run: |
|
|
set -uo pipefail
|
|
MARK="<!-- steamdeck-pr-build -->"
|
|
{
|
|
echo "$MARK"
|
|
echo "## Build of this pull request"
|
|
echo
|
|
[ -f /tmp/pr-merge.md ] && cat /tmp/pr-merge.md
|
|
echo
|
|
case "$BUILD_OUTCOME" in
|
|
success)
|
|
echo "### Build passed"
|
|
echo
|
|
echo "The apk is the \`steamdeck-apk\` artifact on [this run]($RUN_URL) (signed with the same test key as every build, so it installs over any other)."
|
|
;;
|
|
failure)
|
|
echo "### ERROR: Build failed"
|
|
echo
|
|
echo "The compiler's errors ([full log]($RUN_URL)):"
|
|
echo
|
|
echo '```'
|
|
{ grep -E '^e: |^w: .*error|error:|CMake Error|fatal error|ninja: error|FAILED|What went wrong|Execution failed for task|Unresolved reference|Type mismatch|Unresolved' /tmp/build.log 2>/dev/null \
|
|
| sed -e 's#file:///home/runner/work/[^/]*/[^/]*/##' -e 's#/home/runner/work/[^/]*/[^/]*/##' \
|
|
| grep -v '^w: ' | awk '!seen[$0]++' | head -80; } || true
|
|
echo '```'
|
|
echo
|
|
echo "<details><summary>What went wrong (Gradle)</summary>"
|
|
echo
|
|
echo '```'
|
|
awk '/^\* What went wrong/{p=1} /^\* Try:/{p=0} p{print}' /tmp/build.log 2>/dev/null | head -60
|
|
echo '```'
|
|
echo "</details>"
|
|
;;
|
|
skipped|cancelled)
|
|
echo "### Not built"
|
|
echo
|
|
echo "The build did not run (see above, or [the run]($RUN_URL))."
|
|
;;
|
|
esac
|
|
echo
|
|
echo "_Updated on every push to this pull request._"
|
|
} > /tmp/pr-report.md
|
|
cat /tmp/pr-report.md >> "$GITHUB_STEP_SUMMARY"
|
|
ID=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" --paginate --jq ".[] | select(.body | startswith(\"$MARK\")) | .id" | head -1 || true)
|
|
if [ -n "$ID" ]; then
|
|
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${ID}" -F body=@/tmp/pr-report.md >/dev/null
|
|
else
|
|
gh api -X POST "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" -F body=@/tmp/pr-report.md >/dev/null
|
|
fi
|
|
|
|
- uses: actions/upload-artifact@v4
|
|
with:
|
|
name: steamdeck-apk
|
|
path: app/build/outputs/apk/release/*.apk
|
|
if-no-files-found: error
|
|
retention-days: 30
|
|
# An apk is a zip already; compressing it again buys nothing and costs seconds.
|
|
compression-level: 0
|