ci(release): produce F-Droid reference APKs via fdroid build in buildserver image, then sign

This commit is contained in:
vjrj 2026-07-19 08:56:54 +02:00
parent 654ed1fe68
commit b906161d70
2 changed files with 194 additions and 133 deletions

View file

@ -1,19 +1,25 @@
# Release automation for git.comunes.org (Forgejo Actions).
#
# Password-free: pushing a tag `v*` builds a signed AAB + per-ABI APKs, uploads
# the AAB to Google Play's internal track via fastlane, and attaches the signed
# per-ABI APKs to the Forgejo release (the reference binaries F-Droid verifies
# for reproducible, developer-signed publishing). All credentials come from repo
# secrets (Settings > Actions > Secrets), never typed:
# Pushing a tag `v*` runs two independent jobs:
# play - build the signed AAB and ship it to Google Play (internal).
# fdroid_reference- build the per-ABI reference APKs the SAME way F-Droid will
# (fdroid build inside the fdroidserver buildserver image),
# sign them with the tane-upload key, and attach them to the
# Forgejo release. F-Droid re-runs `fdroid build` on the same
# commit+recipe+image, gets byte-identical output, verifies our
# signature (AllowedAPKSigningKeys) and publishes OUR APK — so
# F-Droid and Play share the same signing key.
#
# All credentials come from repo secrets (Settings > Actions > Secrets):
# TANE_KEYSTORE_BASE64 base64 of the dedicated tane-upload.jks
# TANE_KEYSTORE_PASSWORD store password
# TANE_KEY_ALIAS tane-upload
# TANE_KEY_PASSWORD key password
# SUPPLY_JSON_KEY_DATA Google Play service-account JSON (reused from Ğ1nkgo)
# SUPPLY_JSON_KEY_DATA Google Play service-account JSON
#
# Build + deploy live in ONE job so the AAB never has to cross a job boundary.
# NOTE: `runs-on` must match a label your Forgejo runner registered with; adjust
# if the Comunes runner uses something other than `docker`.
# NOTE: `runs-on` must match a label your Forgejo runner registered with.
# The fdroid_reference job pulls a large image (~2 GB) and builds tesseract from
# source, so it is slow (~20-40 min); the play job is independent and unaffected.
name: release
@ -23,7 +29,7 @@ on:
- 'v*'
jobs:
android:
play:
runs-on: docker
container:
image: ghcr.io/cirruslabs/flutter:3.41.9
@ -47,21 +53,6 @@ jobs:
dart run slang
dart run build_runner build --delete-conflicting-outputs
- name: Work around dead jcenter (route to mavenCentral)
run: |
# jcenter (bintray) is shut down; old transitive deps (e.g. flutter_tesseract_ocr
# -> AGP 7.1.2 -> apkzlib -> jsr305:1.3.9) still point there and fail to resolve.
# Inject mavenCentral/google globally so the SAME artifacts resolve from a live
# host — identical jars, so build bytes (and F-Droid reproducibility) are unchanged.
mkdir -p "$HOME/.gradle"
cat > "$HOME/.gradle/init.gradle" <<'EOF'
allprojects {
buildscript { repositories { google(); mavenCentral() } }
repositories { google(); mavenCentral() }
}
settingsEvaluated { s -> s.pluginManagement { repositories { google(); mavenCentral(); gradlePluginPortal() } } }
EOF
- name: Materialize signing material from secrets
working-directory: apps/app_seeds
env:
@ -78,54 +69,9 @@ jobs:
keyPassword=$TANE_KEY_PASSWORD
EOF
- name: Build tesseract4android from source (match F-Droid for reproducibility)
- name: Build signed AAB
working-directory: apps/app_seeds
run: |
set -eu
# F-Droid forbids the prebuilt AAR that ships in flutter_tesseract_ocr, so
# its recipe rebuilds tesseract4android@4.9.0 from source and swaps the AAR
# into the pub cache. We do the SAME here so our published reference APK is
# byte-for-byte reproducible against F-Droid's build (keeps AllowedAPKSigningKeys
# + binary: valid — see metadata/org.comunes.tane.yml in fdroiddata).
t4a="$GITHUB_WORKSPACE/tesseract4android-src"
git clone --depth 1 -b 4.9.0 https://github.com/adaptech-cz/Tesseract4Android.git "$t4a"
# Install the exact NDK the project pins, if the image lacks it.
ndkv=$(grep -RhoE 'ndkVersion[^0-9]*[0-9][0-9.]+' "$t4a" | grep -oE '[0-9]+\.[0-9.]+' | head -1 || true)
[ -n "${ndkv:-}" ] && (yes | sdkmanager "ndk;$ndkv" >/dev/null 2>&1 || true)
# gradlew lives at the repo root, task belongs to the :tesseract4android module.
( cd "$t4a" && ./gradlew --no-daemon :tesseract4android:assembleStandardRelease )
aar_dst=$(find ${PUB_CACHE:+$PUB_CACHE} "$HOME/.pub-cache" /root/.pub-cache /sdks 2>/dev/null \
-path '*flutter_tesseract_ocr-0.4.31/android/libs/tesseract4android-release.aar' | head -1)
cp "$t4a/tesseract4android/build/outputs/aar/tesseract4android-standard-release.aar" "$aar_dst"
echo "swapped source-built tesseract AAR -> $aar_dst"
- name: Build signed AAB + per-ABI APKs
working-directory: apps/app_seeds
run: |
flutter build appbundle --release
# Per-ABI splits: these are the reference binaries F-Droid rebuilds and
# verifies against (see docs/fdroid/org.comunes.tane.yml, binary:).
flutter build apk --release --split-per-abi
- name: Publish signed per-ABI APKs to the Forgejo release
working-directory: apps/app_seeds
env:
TOKEN: ${{ github.token }}
run: |
apt-get update -qq && apt-get install -y -qq curl jq
api="http://forgejo:3000/api/v1/repos/${GITHUB_REPOSITORY}"
tag="${GITHUB_REF_NAME}"
# Create the release for this tag if it does not exist yet, then get its id.
curl -sf -X POST "$api/releases" \
-H "Authorization: token ${TOKEN}" -H 'Content-Type: application/json' \
-d "{\"tag_name\":\"${tag}\",\"name\":\"${tag}\"}" >/dev/null || true
rid=$(curl -sf -H "Authorization: token ${TOKEN}" "$api/releases/tags/${tag}" | jq -r .id)
for abi in armeabi-v7a arm64-v8a x86_64; do
f="build/app/outputs/flutter-apk/app-${abi}-release.apk"
curl -sf -X POST "$api/releases/${rid}/assets?name=app-${abi}-release.apk" \
-H "Authorization: token ${TOKEN}" \
-F "attachment=@${f};type=application/vnd.android.package-archive"
done
run: flutter build appbundle --release
- name: Install fastlane
working-directory: apps/app_seeds
@ -145,3 +91,86 @@ jobs:
if: always()
working-directory: apps/app_seeds
run: rm -f android/key.properties "$GITHUB_WORKSPACE/tane-upload.jks"
fdroid_reference:
runs-on: docker
container:
image: registry.gitlab.com/fdroid/fdroidserver:buildserver-trixie
steps:
- name: Build reference APKs with fdroid, sign, and attach to the release
env:
TOKEN: ${{ github.token }}
TANE_KEYSTORE_BASE64: ${{ secrets.TANE_KEYSTORE_BASE64 }}
TANE_KEYSTORE_PASSWORD: ${{ secrets.TANE_KEYSTORE_PASSWORD }}
TANE_KEY_ALIAS: ${{ secrets.TANE_KEY_ALIAS }}
TANE_KEY_PASSWORD: ${{ secrets.TANE_KEY_PASSWORD }}
run: |
set -eu
# 1) Reproduce F-Droid's build environment (ANDROID_HOME, PATH, NDK...).
sh /opt/buildserver/setup-env-vars /opt/android-sdk
. /etc/profile.d/bsenv.sh
# 2) fdroidserver from master, exactly like the fdroiddata CI.
fds=/opt/fdroidserver-src
mkdir -p "$fds"
curl -sL https://gitlab.com/fdroid/fdroidserver/-/archive/master/fdroidserver-master.tar.gz \
| tar -xz -C "$fds" --strip-components=1
export PATH="$fds:$PATH"
export PYTHONPATH="$fds:$fds/examples"
git config --global --add safe.directory '*'
# 3) Fetch our in-repo recipe (the single source of truth) at this commit,
# plus the pubspec versionCode base for the per-ABI codes.
work=/tmp/tane-src
mkdir -p "$work" && cd "$work"
git init -q .
git remote add origin "http://x-access-token:${TOKEN}@forgejo:3000/${GITHUB_REPOSITORY}.git"
git fetch -q --depth 1 origin "${GITHUB_SHA}"
git checkout -q FETCH_HEAD
base=$(sed -n -E 's/^version:.*\+([0-9]+)$/\1/p' apps/app_seeds/pubspec.yaml)
[ -n "$base" ]
# 4) fdroiddata skeleton (config.yml + srclibs) from the fork; overlay our recipe.
fdd=/tmp/fdroiddata
git clone --depth 1 -b tane https://gitlab.com/vjrj/fdroiddata.git "$fdd"
cp "$work/docs/fdroid/org.comunes.tane.yml" "$fdd/metadata/org.comunes.tane.yml"
cd "$fdd"
# Build THIS commit; drop binary: (we PRODUCE the reference here, not verify it).
sed -i "s#^\( *commit: \).*#\1${GITHUB_SHA}#" metadata/org.comunes.tane.yml
sed -i '/^ *binary: /d' metadata/org.comunes.tane.yml
# 5) Build the three per-ABI splits with F-Droid's own toolchain.
fdroid build --verbose --no-tarball \
"org.comunes.tane:$((base * 10 + 1))" \
"org.comunes.tane:$((base * 10 + 2))" \
"org.comunes.tane:$((base * 10 + 3))"
# 6) Sign each unsigned APK with tane-upload and attach to the Forgejo release.
apksigner=$(ls -d "$ANDROID_HOME"/build-tools/*/apksigner | sort -V | tail -1)
echo "$TANE_KEYSTORE_BASE64" | base64 -d > /tmp/ks.jks
api="http://forgejo:3000/api/v1/repos/${GITHUB_REPOSITORY}"
tag="${GITHUB_REF_NAME}"
curl -sf -X POST "$api/releases" \
-H "Authorization: token ${TOKEN}" -H 'Content-Type: application/json' \
-d "{\"tag_name\":\"${tag}\",\"name\":\"${tag}\"}" >/dev/null || true
rid=$(curl -sf -H "Authorization: token ${TOKEN}" "$api/releases/tags/${tag}" \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
for f in unsigned/org.comunes.tane_*.apk; do
vc=$(echo "$f" | sed -E 's/.*_([0-9]+)\.apk/\1/')
case $((vc % 10)) in
1) abi=armeabi-v7a ;;
2) abi=arm64-v8a ;;
3) abi=x86_64 ;;
*) echo "unexpected versionCode $vc"; exit 1 ;;
esac
out="/tmp/app-${abi}-release.apk"
"$apksigner" sign --ks /tmp/ks.jks --ks-key-alias "$TANE_KEY_ALIAS" \
--ks-pass "pass:$TANE_KEYSTORE_PASSWORD" --key-pass "pass:$TANE_KEY_PASSWORD" \
--out "$out" "$f"
curl -sf -X POST "$api/releases/${rid}/assets?name=app-${abi}-release.apk" \
-H "Authorization: token ${TOKEN}" \
-F "attachment=@${out};type=application/vnd.android.package-archive"
echo "uploaded app-${abi}-release.apk (from ${f##*/})"
done
rm -f /tmp/ks.jks

View file

@ -15,87 +15,119 @@ AutoName: Tane
RepoType: git
Repo: https://git.comunes.org/comunes/tane.git
# Reproducible, developer-signed builds: F-Droid rebuilds from source and, if the
# output matches our own signed APK on git.comunes.org byte-for-byte, publishes
# OUR APK. Same signature as the Google Play build, so users move between stores
# without reinstalling. The fingerprint below is the SHA-256 of the tane-upload
# signing certificate (public value); the keystore/passwords never leave CI.
# Reproducible, developer-signed: F-Droid rebuilds each split and, if it matches
# our own signed APK on the git.comunes.org release byte-for-byte, publishes OUR
# APK (same signature as the Play build). Value is the SHA-256 of the tane-upload
# signing certificate (public).
AllowedAPKSigningKeys: ddfae432091b8248a8a4a1b353487fa626301f4357ef835e94ec312f69418e38
# One build entry per ABI: `flutter build --split-per-abi` yields smaller APKs and
# each split carries a distinct versionCode (gradle: versionCode*10 + {arm-v7a:1,
# arm64:2, x86_64:3}), so v0.1.2 (+4) -> 41/42/43. The `binary:` is the reference
# APK F-Droid verifies against, uploaded to the Forgejo release by
# .forgejo/workflows/release.yml on the `v*` tag.
Builds:
- versionName: 0.1.3
versionCode: 51
commit: v0.1.3
- versionName: 0.1.6
versionCode: 81
commit: 654ed1fe688d942f8bb9ec5505582532014d98a5
subdir: apps/app_seeds
sudo:
- apt-get update
- apt-get install -y git unzip xz-utils
- git clone --depth 1 -b 3.41.9 https://github.com/flutter/flutter.git /opt/flutter
- chown -R vagrant:vagrant /opt/flutter
output: build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
binary: https://git.comunes.org/comunes/tane/releases/download/v%v/app-armeabi-v7a-release.apk
srclibs:
- flutter@stable
- tesseract4android@4.9.0
prebuild:
- export PATH="/opt/flutter/bin:$PATH"
- git config --global --add safe.directory /opt/flutter
- flutter config --no-analytics
- flutter pub get
- dart run slang
- dart run build_runner build --delete-conflicting-outputs
- export flutterVersion=$(sed -n -E 's,.*cirruslabs/flutter:([0-9.]+).*,\1,p'
../../.forgejo/workflows/release.yml | head -1)
- '[[ $flutterVersion ]]'
- git -C $$flutter$$ checkout -f $flutterVersion
- cd ../..
- export PUB_CACHE=$(pwd)/.pub-cache
- $$flutter$$/bin/flutter config --no-analytics
- $$flutter$$/bin/flutter pub get --enforce-lockfile
- cd apps/app_seeds
- $$flutter$$/bin/dart run slang
- $$flutter$$/bin/dart run build_runner build --delete-conflicting-outputs
scandelete:
- .pub-cache
build:
- export PATH="/opt/flutter/bin:$PATH"
- flutter build apk --release --split-per-abi --target-platform=android-arm
- export PUB_CACHE=$(cd ../.. && pwd)/.pub-cache
- pushd $$tesseract4android$$/tesseract4android
- gradle assembleStandardRelease
- popd
- cp
$$tesseract4android$$/tesseract4android/build/outputs/aar/tesseract4android-standard-release.aar
$PUB_CACHE/hosted/pub.dev/flutter_tesseract_ocr-0.4.31/android/libs/tesseract4android-release.aar
- $$flutter$$/bin/flutter build apk --release --split-per-abi --target-platform=android-arm
- versionName: 0.1.3
versionCode: 52
commit: v0.1.3
- versionName: 0.1.6
versionCode: 82
commit: 654ed1fe688d942f8bb9ec5505582532014d98a5
subdir: apps/app_seeds
sudo:
- apt-get update
- apt-get install -y git unzip xz-utils
- git clone --depth 1 -b 3.41.9 https://github.com/flutter/flutter.git /opt/flutter
- chown -R vagrant:vagrant /opt/flutter
output: build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
binary: https://git.comunes.org/comunes/tane/releases/download/v%v/app-arm64-v8a-release.apk
srclibs:
- flutter@stable
- tesseract4android@4.9.0
prebuild:
- export PATH="/opt/flutter/bin:$PATH"
- git config --global --add safe.directory /opt/flutter
- flutter config --no-analytics
- flutter pub get
- dart run slang
- dart run build_runner build --delete-conflicting-outputs
- export flutterVersion=$(sed -n -E 's,.*cirruslabs/flutter:([0-9.]+).*,\1,p'
../../.forgejo/workflows/release.yml | head -1)
- '[[ $flutterVersion ]]'
- git -C $$flutter$$ checkout -f $flutterVersion
- cd ../..
- export PUB_CACHE=$(pwd)/.pub-cache
- $$flutter$$/bin/flutter config --no-analytics
- $$flutter$$/bin/flutter pub get --enforce-lockfile
- cd apps/app_seeds
- $$flutter$$/bin/dart run slang
- $$flutter$$/bin/dart run build_runner build --delete-conflicting-outputs
scandelete:
- .pub-cache
build:
- export PATH="/opt/flutter/bin:$PATH"
- flutter build apk --release --split-per-abi --target-platform=android-arm64
- export PUB_CACHE=$(cd ../.. && pwd)/.pub-cache
- pushd $$tesseract4android$$/tesseract4android
- gradle assembleStandardRelease
- popd
- cp
$$tesseract4android$$/tesseract4android/build/outputs/aar/tesseract4android-standard-release.aar
$PUB_CACHE/hosted/pub.dev/flutter_tesseract_ocr-0.4.31/android/libs/tesseract4android-release.aar
- $$flutter$$/bin/flutter build apk --release --split-per-abi --target-platform=android-arm64
- versionName: 0.1.3
versionCode: 53
commit: v0.1.3
- versionName: 0.1.6
versionCode: 83
commit: 654ed1fe688d942f8bb9ec5505582532014d98a5
subdir: apps/app_seeds
sudo:
- apt-get update
- apt-get install -y git unzip xz-utils
- git clone --depth 1 -b 3.41.9 https://github.com/flutter/flutter.git /opt/flutter
- chown -R vagrant:vagrant /opt/flutter
output: build/app/outputs/flutter-apk/app-x86_64-release.apk
binary: https://git.comunes.org/comunes/tane/releases/download/v%v/app-x86_64-release.apk
srclibs:
- flutter@stable
- tesseract4android@4.9.0
prebuild:
- export PATH="/opt/flutter/bin:$PATH"
- git config --global --add safe.directory /opt/flutter
- flutter config --no-analytics
- flutter pub get
- dart run slang
- dart run build_runner build --delete-conflicting-outputs
- export flutterVersion=$(sed -n -E 's,.*cirruslabs/flutter:([0-9.]+).*,\1,p'
../../.forgejo/workflows/release.yml | head -1)
- '[[ $flutterVersion ]]'
- git -C $$flutter$$ checkout -f $flutterVersion
- cd ../..
- export PUB_CACHE=$(pwd)/.pub-cache
- $$flutter$$/bin/flutter config --no-analytics
- $$flutter$$/bin/flutter pub get --enforce-lockfile
- cd apps/app_seeds
- $$flutter$$/bin/dart run slang
- $$flutter$$/bin/dart run build_runner build --delete-conflicting-outputs
scandelete:
- .pub-cache
build:
- export PATH="/opt/flutter/bin:$PATH"
- flutter build apk --release --split-per-abi --target-platform=android-x64
- export PUB_CACHE=$(cd ../.. && pwd)/.pub-cache
- pushd $$tesseract4android$$/tesseract4android
- gradle assembleStandardRelease
- popd
- cp
$$tesseract4android$$/tesseract4android/build/outputs/aar/tesseract4android-standard-release.aar
$PUB_CACHE/hosted/pub.dev/flutter_tesseract_ocr-0.4.31/android/libs/tesseract4android-release.aar
- $$flutter$$/bin/flutter build apk --release --split-per-abi --target-platform=android-x64
AutoUpdateMode: Version
UpdateCheckMode: Tags v[\d.]+
UpdateCheckData: apps/app_seeds/pubspec.yaml|version:\s*[\d.]+\+(\d+)|apps/app_seeds/pubspec.yaml|version:\s*([\d.]+)\+
CurrentVersion: 0.1.3
CurrentVersionCode: 53
VercodeOperation:
- '%c * 10 + 1'
- '%c * 10 + 2'
- '%c * 10 + 3'
UpdateCheckData:
apps/app_seeds/pubspec.yaml|version:\s*[\d.]+\+(\d+)|apps/app_seeds/pubspec.yaml|version:\s*([\d.]+)\+
CurrentVersion: 0.1.6
CurrentVersionCode: 83