Add CI
All checks were successful
release / release (push) Successful in 12m40s

This commit is contained in:
2026-10-27 17:38:55 +03:00
parent 2f38d27b3f
commit 3ef78bd49c
12 changed files with 363 additions and 10 deletions

26
tools/ci/Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
# Build toolchain of CI (.gitea/workflows/release.yaml, docs/development.md «CI»): JDK 21 (Gradle, the desktop app's
# toolchain and its jpackage) and the Android SDK the TV app compiles against. dpkg-deb for the .deb comes with the base
# image. Gradle itself comes through the wrapper and, like every dependency, lands in /gradle — a BuildKit cache mount of
# release.Dockerfile. Bump the platform together with compileSdk in app/build.gradle.kts.
FROM eclipse-temurin:21-jdk-noble
ARG CMDLINE_TOOLS=16111833
ARG SDK_PACKAGES="platforms;android-37.0 build-tools;36.0.0"
ENV ANDROID_HOME=/opt/android-sdk
ENV PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$PATH
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl unzip \
&& rm -rf /var/lib/apt/lists/* \
&& curl -fsSL -o /tmp/tools.zip https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS}_latest.zip \
&& mkdir -p $ANDROID_HOME/cmdline-tools \
&& unzip -q /tmp/tools.zip -d $ANDROID_HOME/cmdline-tools \
&& mv $ANDROID_HOME/cmdline-tools/cmdline-tools $ANDROID_HOME/cmdline-tools/latest \
&& rm /tmp/tools.zip \
&& (yes | sdkmanager --licenses > /dev/null) \
&& sdkmanager $SDK_PACKAGES
ENV GRADLE_USER_HOME=/gradle \
ANDROID_USER_HOME=/gradle/android \
GRADLE_OPTS=-Dorg.gradle.daemon=false

View File

@@ -0,0 +1,26 @@
# The release build of CI (.gitea/workflows/release.yaml) on the toolchain of ./Dockerfile. BuildKit does it all, so
# the job needs no bind mounts (act_runner's docker resolves those on the host, not in the job): the sources go in as
# the build context, the signing key as secrets that never land in a layer, Gradle's caches in a cache mount that
# outlives the build, and the result comes out with --output.
#
# docker build -t sleepyowl-ci tools/ci
# docker build --no-cache -f tools/ci/release.Dockerfile \
# --secret id=keystore,src=release.jks --secret id=keystore_password,src=password.txt \
# --secret id=key_alias,src=alias.txt --build-arg VERSION_NAME=0.1.1 -o out .
#
# out/ then holds what tools/ci/release.sh lists. --no-cache because secrets are not in the layer cache key: a cached
# layer would return an APK signed with whatever key built it; the Gradle cache mount survives it.
ARG CI_IMAGE=sleepyowl-ci
FROM ${CI_IMAGE} AS build
ARG VERSION_NAME=
WORKDIR /work
COPY . .
RUN --mount=type=cache,id=sleepyowl-gradle,target=/gradle,sharing=locked \
--mount=type=secret,id=keystore \
--mount=type=secret,id=keystore_password \
--mount=type=secret,id=key_alias \
sh tools/ci/release.sh /out
FROM scratch
COPY --from=build /out/ /

41
tools/ci/release.sh Normal file
View File

@@ -0,0 +1,41 @@
#!/bin/sh
# The release build of CI (release.Dockerfile): the TV's APK on the release key, which BuildKit hands in as secrets,
# and the desktop's .deb.
# $1/sleepyowl.apk the APK (R8 without obfuscation: no mapping needed for stack traces)
# $1/sleepyowl_*_amd64.deb the .deb
# $1/cert-sha256.txt the APK's signing certificate, SHA-256 (as apksigner prints it)
# $1/version.txt "<versionName> <versionCode>", as the APK has them
# VERSION_NAME overrides gradle.properties' sleepyowl.versionName.
set -eu
OUT=$1
APK=app/build/outputs/apk/release/app-release.apk
# Without the key Gradle signs with a debug key made up in the container: such an APK installs over nothing.
[ -s /run/secrets/keystore ] || { echo "No keystore secret: the release key is required." >&2; exit 1; }
ANDROID_KEYSTORE=/run/secrets/keystore
ANDROID_KEYSTORE_PASSWORD=$(cat /run/secrets/keystore_password)
ANDROID_KEY_ALIAS=$(cat /run/secrets/key_alias 2>/dev/null || true)
export ANDROID_KEYSTORE ANDROID_KEYSTORE_PASSWORD ANDROID_KEY_ALIAS
./gradlew :app:assembleRelease :desktop:packageDeb \
${VERSION_NAME:+"-Psleepyowl.versionName=$VERSION_NAME"}
buildtools=$(ls -d "$ANDROID_HOME"/build-tools/* | tail -n 1)
digest=$("$buildtools/apksigner" verify --print-certs "$APK" | sed -n 's/^Signer #1 certificate SHA-256 digest: //p')
[ -n "$digest" ] || { echo "apksigner printed no certificate digest." >&2; exit 1; }
line=$("$buildtools/aapt2" dump badging "$APK" | sed -n '1p')
name=$(printf '%s' "$line" | sed -n "s/.* versionName='\([^']*\)'.*/\1/p")
code=$(printf '%s' "$line" | sed -n "s/.* versionCode='\([0-9]*\)'.*/\1/p")
[ -n "$name" ] && [ -n "$code" ] || { echo "No version in the APK's manifest: $line" >&2; exit 1; }
mkdir -p "$OUT"
cp "$APK" "$OUT/sleepyowl.apk"
cp desktop/build/deb/sleepyowl_*_amd64.deb "$OUT/"
printf '%s\n' "$digest" > "$OUT/cert-sha256.txt"
printf '%s %s\n' "$name" "$code" > "$OUT/version.txt"
# What the build left in /work would land in this RUN's layer, and BuildKit keeps such a layer from every run
# (--no-cache): only the sources stay. (.dockerignore keeps every build/ out of the context, so these are all Gradle's.)
find . -type d -name build -prune -exec rm -rf {} +
rm -rf .gradle .kotlin