Forgejo workflows are independent — ci.yml never triggers on tag pushes (its branches filter skips refs/tags/*), so releases shipped with zero test gating. Duplicate analyze + test-commons-core + test-app-seeds into release.yml and make play needs them; gate the fdroid chain on the same tests via explicit result checks while keeping !cancelled() so it stays decoupled from play's result. Red suite now blocks every deploy.
607 lines
32 KiB
YAML
607 lines
32 KiB
YAML
# Release automation for git.comunes.org (Forgejo Actions).
|
|
#
|
|
# Pushing a tag `v*` runs two independent jobs:
|
|
# play - build the signed AAB and ship it to Google Play (production, 100%).
|
|
# 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
|
|
#
|
|
# NOTE: `runs-on` must match a label your Forgejo runner registered with.
|
|
# fdroid_reference is 3 separate jobs chained with `needs:` (armeabi-v7a ->
|
|
# arm64-v8a -> x86_64), NOT a matrix: this runner's act_runner does not honor
|
|
# `strategy.max-parallel`, so a matrix always ran all 3 concurrently — 2-3
|
|
# simultaneous cold NDK/CMake/Gradle builds contend for RAM and get OOM-killed
|
|
# (silent "Gradle build daemon disappeared", no error message). `needs:` is
|
|
# core Actions syntax with no such gap, so it actually serializes them. Each
|
|
# needs its own full cold NDK+tesseract-native+flutter compile (~30-35min
|
|
# alone), which is why they're chained rather than run in one job (that alone
|
|
# was too slow even for a 90m timeout).
|
|
|
|
name: release
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*'
|
|
# Manual trigger to test the fdroid_reference job on a branch without cutting a
|
|
# tag or deploying to Play (the play job below is guarded to tags only).
|
|
# Pass ref_tag (e.g. v0.1.10) to rebuild the reference APKs for an EXISTING
|
|
# release and re-upload them (idempotently), without cutting a new tag — used
|
|
# to iterate on build-reproducibility fixes without re-deploying to Play.
|
|
workflow_dispatch:
|
|
inputs:
|
|
ref_tag:
|
|
description: 'Existing tag to rebuild+re-upload reference APKs for (e.g. v0.1.10). Empty = build-only, no upload.'
|
|
required: false
|
|
default: ''
|
|
|
|
jobs:
|
|
# Test gate. Forgejo/Actions workflows are independent — there is no cross-workflow
|
|
# `needs:`, and ci.yml does NOT trigger on tag pushes (its `branches` filter never
|
|
# matches refs/tags/*). So the CI test jobs are duplicated here (verbatim from
|
|
# ci.yml) to gate the deploy: nothing ships unless analyze + both test suites pass.
|
|
# Keep these in sync with ci.yml by hand.
|
|
analyze:
|
|
runs-on: docker
|
|
container:
|
|
image: ghcr.io/cirruslabs/flutter:3.41.9
|
|
steps:
|
|
- name: Checkout
|
|
env:
|
|
TOKEN: ${{ github.token }}
|
|
run: |
|
|
git config --global --add safe.directory '*'
|
|
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
|
|
- run: flutter pub get
|
|
- run: flutter analyze
|
|
|
|
test-commons-core:
|
|
runs-on: docker
|
|
container:
|
|
image: ghcr.io/cirruslabs/flutter:3.41.9
|
|
steps:
|
|
- name: Checkout
|
|
env:
|
|
TOKEN: ${{ github.token }}
|
|
run: |
|
|
git config --global --add safe.directory '*'
|
|
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
|
|
- run: flutter pub get
|
|
- name: dart test
|
|
working-directory: packages/commons_core
|
|
run: dart test
|
|
|
|
test-app-seeds:
|
|
runs-on: docker
|
|
container:
|
|
image: ghcr.io/cirruslabs/flutter:3.41.9
|
|
steps:
|
|
- name: Checkout
|
|
env:
|
|
TOKEN: ${{ github.token }}
|
|
run: |
|
|
git config --global --add safe.directory '*'
|
|
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
|
|
# SQLCipher so the "no plaintext at rest" security test actually runs.
|
|
- run: apt-get update -qq && apt-get install -y -qq libsqlcipher-dev
|
|
- run: flutter pub get
|
|
- name: Generate code + test
|
|
working-directory: apps/app_seeds
|
|
run: |
|
|
dart run slang
|
|
dart run build_runner build --delete-conflicting-outputs
|
|
flutter test --coverage
|
|
|
|
play:
|
|
# Gate on the tests: with no status-check function in this `if`, the implicit
|
|
# success() on `needs` still applies — play runs only on a tag AND green tests.
|
|
needs: [analyze, test-commons-core, test-app-seeds]
|
|
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
|
runs-on: docker
|
|
container:
|
|
image: ghcr.io/cirruslabs/flutter:3.41.9
|
|
steps:
|
|
# Manual git checkout (no Node): the flutter image has no node, so JS
|
|
# actions like actions/checkout@v4 fail with "exec: node: not found".
|
|
- name: Checkout
|
|
env:
|
|
TOKEN: ${{ github.token }}
|
|
run: |
|
|
git config --global --add safe.directory '*'
|
|
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
|
|
|
|
- name: Generate code (i18n + Drift)
|
|
working-directory: apps/app_seeds
|
|
run: |
|
|
flutter pub get
|
|
# Same two hardenings the fdroid recipe applies (this job builds the AAB
|
|
# directly, not via the recipe, so a cold cache is still exposed):
|
|
# 1) strip flutter_tesseract_ocr's dead-jcenter buildscript (AGP 7.1.2) so
|
|
# it inherits the app's AGP instead of hitting jcenter.bintray.com;
|
|
# 2) cap the Gradle heap so the build fits the shared CI host's RAM.
|
|
find "${PUB_CACHE:-$HOME/.pub-cache}" -path '*flutter_tesseract_ocr-*/android/build.gradle' -exec sed -i '/^buildscript {/,/^}/d' {} +
|
|
printf 'org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g\norg.gradle.daemon=false\n' >> android/gradle.properties
|
|
dart run slang
|
|
dart run build_runner build --delete-conflicting-outputs
|
|
|
|
- name: Materialize signing material from secrets
|
|
working-directory: apps/app_seeds
|
|
env:
|
|
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: |
|
|
echo "$TANE_KEYSTORE_BASE64" | base64 -d > "$GITHUB_WORKSPACE/tane-upload.jks"
|
|
cat > android/key.properties <<EOF
|
|
storeFile=$GITHUB_WORKSPACE/tane-upload.jks
|
|
storePassword=$TANE_KEYSTORE_PASSWORD
|
|
keyAlias=$TANE_KEY_ALIAS
|
|
keyPassword=$TANE_KEY_PASSWORD
|
|
EOF
|
|
|
|
- name: Build signed AAB
|
|
working-directory: apps/app_seeds
|
|
run: flutter build appbundle --release
|
|
|
|
- name: Install fastlane
|
|
working-directory: apps/app_seeds
|
|
run: |
|
|
apt-get update -qq
|
|
apt-get install -y -qq ruby ruby-dev build-essential
|
|
gem install bundler --no-document
|
|
bundle install
|
|
|
|
- name: Publish to Google Play (production track, 100%)
|
|
working-directory: apps/app_seeds
|
|
env:
|
|
SUPPLY_JSON_KEY_DATA: ${{ secrets.SUPPLY_JSON_KEY_DATA }}
|
|
run: bundle exec fastlane deploy_play
|
|
|
|
- name: Scrub signing material
|
|
if: always()
|
|
working-directory: apps/app_seeds
|
|
run: rm -f android/key.properties "$GITHUB_WORKSPACE/tane-upload.jks"
|
|
|
|
fdroid_reference_armeabi_v7a:
|
|
# Serialize after play so two heavy Android builds never run concurrently on
|
|
# the shared host (capacity:2) — that contention OOM-killed play's Gradle
|
|
# daemon. Gated on the test jobs, but kept INDEPENDENT of play's result:
|
|
# `!cancelled()` is a status function, which drops the implicit success() on
|
|
# `needs`, so play failing/skipping (e.g. on workflow_dispatch, where play is
|
|
# tag-only) does not block the fdroid chain — while the explicit result checks
|
|
# still hard-gate on green tests. Nothing gets built/uploaded on a red suite.
|
|
needs: [analyze, test-commons-core, test-app-seeds, play]
|
|
if: ${{ !cancelled()
|
|
&& needs.analyze.result == 'success'
|
|
&& needs.test-commons-core.result == 'success'
|
|
&& needs.test-app-seeds.result == 'success' }}
|
|
runs-on: docker
|
|
container:
|
|
image: registry.gitlab.com/fdroid/fdroidserver:buildserver-trixie
|
|
steps:
|
|
- name: Build the armeabi-v7a reference APK with fdroid, sign, and attach it
|
|
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 }}
|
|
REF_TAG: ${{ github.event.inputs.ref_tag }}
|
|
ABI: armeabi-v7a
|
|
OFFSET: 1
|
|
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"
|
|
# Download to a file with retries: a mid-transfer vSwitch blip used to
|
|
# pipe an empty stream straight into tar ("gzip: unexpected end of
|
|
# file"). -f fails on HTTP errors; --retry-all-errors covers resets.
|
|
for a in 1 2 3 4 5; do
|
|
curl -fsSL --retry 5 --retry-delay 3 --retry-all-errors \
|
|
https://gitlab.com/fdroid/fdroidserver/-/archive/master/fdroidserver-master.tar.gz \
|
|
-o /tmp/fds.tgz && tar -tzf /tmp/fds.tgz >/dev/null 2>&1 && break
|
|
echo "fdroidserver download attempt $a failed; retrying in 5s" >&2; sleep 5
|
|
done
|
|
tar -xz -C "$fds" --strip-components=1 -f /tmp/fds.tgz
|
|
export PATH="$fds:$PATH"
|
|
export PYTHONPATH="$fds:$fds/examples"
|
|
git config --global --add safe.directory '*'
|
|
# The job container reaches github.com but not the host's public git.comunes.org
|
|
# (NAT hairpin); redirect the app clone to the internal Forgejo host so
|
|
# `fdroid build` can fetch the source (and SOURCE_DATE_EPOCH from its commit).
|
|
git config --global url."http://x-access-token:${TOKEN}@forgejo:3000/".insteadOf "https://git.comunes.org/"
|
|
|
|
# 3) Fetch our in-repo recipe (the single source of truth) at this commit,
|
|
# plus the pubspec versionCode base for this ABI's versionCode.
|
|
# Which app commit/tag to build: on a tag push, the pushed commit; on a
|
|
# manual dispatch with ref_tag set, that tag (to rebuild+re-upload an
|
|
# existing release's references without cutting a new tag / Play deploy).
|
|
BUILD_REF="${REF_TAG:-$GITHUB_SHA}"
|
|
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 "$BUILD_REF"
|
|
git checkout -q FETCH_HEAD
|
|
base=$(sed -n -E 's/^version:.*\+([0-9]+)$/\1/p' apps/app_seeds/pubspec.yaml)
|
|
[ -n "$base" ]
|
|
vc=$((base * 10 + OFFSET))
|
|
|
|
# 4) Assemble a minimal fdroiddata (config.yml + srclibs + our recipe) from the
|
|
# in-repo files. No external host needed — the job container reaches
|
|
# git.comunes.org (via forgejo:3000) but not gitlab.com.
|
|
# Match F-Droid's buildserver path (~/build/<appid>) so the native .so
|
|
# embed identical build paths — required for the byte-for-byte repro check.
|
|
fdd=/home/vagrant
|
|
mkdir -p "$fdd/metadata" "$fdd/srclibs"
|
|
cp "$work/fdroid-ci/config.yml" "$fdd/config.yml"
|
|
cp "$work"/fdroid-ci/srclibs/*.yml "$fdd/srclibs/"
|
|
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${BUILD_REF}#" metadata/org.comunes.tane.yml
|
|
sed -i '/^ *binary: /d' metadata/org.comunes.tane.yml
|
|
|
|
# Pre-clone the app into fdroid's build dir: fdroidserver computes
|
|
# SOURCE_DATE_EPOCH from this checkout BEFORE it clones anything — on a
|
|
# fresh dir it falls back to `git log` of the fdroiddata tree, which our
|
|
# assembled skeleton doesn't have, and crashes on None. With the clone in
|
|
# place the primary path pins SOURCE_DATE_EPOCH to the app commit
|
|
# timestamp (deterministic; the public URL is redirected to forgejo:3000
|
|
# by the insteadOf rule above).
|
|
mkdir -p build
|
|
git clone -q https://git.comunes.org/comunes/tane.git build/org.comunes.tane
|
|
git -C build/org.comunes.tane checkout -q "${BUILD_REF}"
|
|
|
|
# 5) Build ONLY this ABI's split with F-Droid's own toolchain. Splitting
|
|
# per-ABI into its own job/matrix-entry (rather than all 3 sequentially in
|
|
# one job) is what keeps each build inside the runner's max-job-runtime —
|
|
# each needs a full cold NDK+tesseract-native+flutter compile.
|
|
# Retry the build: a transient network blip mid-Gradle (maven/pub) used
|
|
# to kill it. `fdroid build` exits 0 even on failure, so gate the retry
|
|
# on the APK actually appearing. Up to 3 attempts fit the 90m timeout.
|
|
for a in 1 2 3; do
|
|
fdroid build --verbose --no-tarball "org.comunes.tane:${vc}" || true
|
|
if [ -f "unsigned/org.comunes.tane_${vc}.apk" ]; then break; fi
|
|
echo "fdroid build attempt $a produced no APK; retrying" >&2
|
|
done
|
|
|
|
# `fdroid build` exits 0 even when builds fail — demand the APK.
|
|
f="unsigned/org.comunes.tane_${vc}.apk"
|
|
if [ ! -f "$f" ]; then
|
|
echo "expected $f, not found" >&2
|
|
tail -n 80 logs/*.log 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
|
|
# 6) Sign the unsigned APK with tane-upload and attach it to the Forgejo release.
|
|
apksigner=$(find "$ANDROID_HOME" -name apksigner -type f 2>/dev/null | sort -V | tail -1)
|
|
if [ -z "$apksigner" ]; then
|
|
yes | sdkmanager "build-tools;34.0.0" >/dev/null 2>&1 || true
|
|
apksigner=$(find "$ANDROID_HOME" -name apksigner -type f 2>/dev/null | sort -V | tail -1)
|
|
fi
|
|
echo "$TANE_KEYSTORE_BASE64" | base64 -d > /tmp/ks.jks
|
|
out="/tmp/app-${ABI}-release.apk"
|
|
# v2/v3 only — NO v1 (JAR) signature. minSdk is 24, so v1 is never
|
|
# needed, and v1 adds META-INF/*.SF/*.RSA/MANIFEST.MF as zip ENTRIES
|
|
# that F-Droid's rebuild doesn't have. F-Droid's apksigcopier transplants
|
|
# only the v2/v3 signing block, so those extra v1 entries make the
|
|
# reference's signed content differ from F-Droid's rebuild (CHUNKED_SHA512
|
|
# mismatch) even when every file is identical. Dropping v1 makes the
|
|
# reference byte-match F-Droid's build so the signature copy verifies.
|
|
"$apksigner" sign --ks /tmp/ks.jks --ks-key-alias "$TANE_KEY_ALIAS" \
|
|
--ks-pass "pass:$TANE_KEYSTORE_PASSWORD" --key-pass "pass:$TANE_KEY_PASSWORD" \
|
|
--v1-signing-enabled false --v2-signing-enabled true --v3-signing-enabled true \
|
|
--alignment-preserved true \
|
|
--out "$out" "$f"
|
|
# Prove the signer cert matches AllowedAPKSigningKeys.
|
|
"$apksigner" verify --print-certs "$out" | grep -i 'SHA-256' || true
|
|
rm -f /tmp/ks.jks
|
|
|
|
# Upload target: the pushed tag, or (on a manual dispatch) the ref_tag
|
|
# input. A plain dispatch with no ref_tag just builds+signs, no upload.
|
|
api="http://forgejo:3000/api/v1/repos/${GITHUB_REPOSITORY}"
|
|
UPLOAD_TAG=""
|
|
if [ "${GITHUB_REF_TYPE:-}" = tag ]; then
|
|
UPLOAD_TAG="${GITHUB_REF_NAME}"
|
|
elif [ -n "${REF_TAG:-}" ]; then
|
|
UPLOAD_TAG="${REF_TAG}"
|
|
fi
|
|
if [ -n "$UPLOAD_TAG" ]; then
|
|
curl -sf -X POST "$api/releases" \
|
|
-H "Authorization: token ${TOKEN}" -H 'Content-Type: application/json' \
|
|
-d "{\"tag_name\":\"${UPLOAD_TAG}\",\"name\":\"${UPLOAD_TAG}\"}" >/dev/null || true
|
|
rid=$(curl -sf -H "Authorization: token ${TOKEN}" "$api/releases/tags/${UPLOAD_TAG}" \
|
|
| python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
|
|
# Idempotent re-upload: delete any existing asset of this name first
|
|
# (the assets POST rejects a duplicate name).
|
|
curl -sf -H "Authorization: token ${TOKEN}" "$api/releases/${rid}/assets" \
|
|
| python3 -c "import sys,json;[print(a['id']) for a in json.load(sys.stdin) if a['name']=='app-${ABI}-release.apk']" 2>/dev/null \
|
|
| while read -r aid; do [ -n "$aid" ] && curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "$api/releases/${rid}/assets/${aid}" >/dev/null || true; done
|
|
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 to ${UPLOAD_TAG} (from ${f##*/})"
|
|
else
|
|
echo "built+signed app-${ABI}-release.apk (dispatch: no ref_tag, upload skipped)"
|
|
fi
|
|
|
|
fdroid_reference_arm64_v8a:
|
|
needs: fdroid_reference_armeabi_v7a
|
|
runs-on: docker
|
|
container:
|
|
image: registry.gitlab.com/fdroid/fdroidserver:buildserver-trixie
|
|
steps:
|
|
- name: Build the arm64-v8a reference APK with fdroid, sign, and attach it
|
|
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 }}
|
|
REF_TAG: ${{ github.event.inputs.ref_tag }}
|
|
ABI: arm64-v8a
|
|
OFFSET: 2
|
|
run: |
|
|
set -eu
|
|
sh /opt/buildserver/setup-env-vars /opt/android-sdk
|
|
. /etc/profile.d/bsenv.sh
|
|
fds=/opt/fdroidserver-src
|
|
mkdir -p "$fds"
|
|
# Download to a file with retries: a mid-transfer vSwitch blip used to
|
|
# pipe an empty stream straight into tar ("gzip: unexpected end of
|
|
# file"). -f fails on HTTP errors; --retry-all-errors covers resets.
|
|
for a in 1 2 3 4 5; do
|
|
curl -fsSL --retry 5 --retry-delay 3 --retry-all-errors \
|
|
https://gitlab.com/fdroid/fdroidserver/-/archive/master/fdroidserver-master.tar.gz \
|
|
-o /tmp/fds.tgz && tar -tzf /tmp/fds.tgz >/dev/null 2>&1 && break
|
|
echo "fdroidserver download attempt $a failed; retrying in 5s" >&2; sleep 5
|
|
done
|
|
tar -xz -C "$fds" --strip-components=1 -f /tmp/fds.tgz
|
|
export PATH="$fds:$PATH"
|
|
export PYTHONPATH="$fds:$fds/examples"
|
|
git config --global --add safe.directory '*'
|
|
git config --global url."http://x-access-token:${TOKEN}@forgejo:3000/".insteadOf "https://git.comunes.org/"
|
|
# Which app commit/tag to build: on a tag push, the pushed commit; on a
|
|
# manual dispatch with ref_tag set, that tag (to rebuild+re-upload an
|
|
# existing release's references without cutting a new tag / Play deploy).
|
|
BUILD_REF="${REF_TAG:-$GITHUB_SHA}"
|
|
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 "$BUILD_REF"
|
|
git checkout -q FETCH_HEAD
|
|
base=$(sed -n -E 's/^version:.*\+([0-9]+)$/\1/p' apps/app_seeds/pubspec.yaml)
|
|
[ -n "$base" ]
|
|
vc=$((base * 10 + OFFSET))
|
|
# Match F-Droid's buildserver path (~/build/<appid>) so the native .so
|
|
# embed identical build paths — required for the byte-for-byte repro check.
|
|
fdd=/home/vagrant
|
|
mkdir -p "$fdd/metadata" "$fdd/srclibs"
|
|
cp "$work/fdroid-ci/config.yml" "$fdd/config.yml"
|
|
cp "$work"/fdroid-ci/srclibs/*.yml "$fdd/srclibs/"
|
|
cp "$work/docs/fdroid/org.comunes.tane.yml" "$fdd/metadata/org.comunes.tane.yml"
|
|
cd "$fdd"
|
|
sed -i "s#^\( *commit: \).*#\1${BUILD_REF}#" metadata/org.comunes.tane.yml
|
|
sed -i '/^ *binary: /d' metadata/org.comunes.tane.yml
|
|
mkdir -p build
|
|
git clone -q https://git.comunes.org/comunes/tane.git build/org.comunes.tane
|
|
git -C build/org.comunes.tane checkout -q "${BUILD_REF}"
|
|
# Retry the build: a transient network blip mid-Gradle (maven/pub) used
|
|
# to kill it. `fdroid build` exits 0 even on failure, so gate the retry
|
|
# on the APK actually appearing. Up to 3 attempts fit the 90m timeout.
|
|
for a in 1 2 3; do
|
|
fdroid build --verbose --no-tarball "org.comunes.tane:${vc}" || true
|
|
if [ -f "unsigned/org.comunes.tane_${vc}.apk" ]; then break; fi
|
|
echo "fdroid build attempt $a produced no APK; retrying" >&2
|
|
done
|
|
f="unsigned/org.comunes.tane_${vc}.apk"
|
|
if [ ! -f "$f" ]; then
|
|
echo "expected $f, not found" >&2
|
|
tail -n 80 logs/*.log 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
apksigner=$(find "$ANDROID_HOME" -name apksigner -type f 2>/dev/null | sort -V | tail -1)
|
|
if [ -z "$apksigner" ]; then
|
|
yes | sdkmanager "build-tools;34.0.0" >/dev/null 2>&1 || true
|
|
apksigner=$(find "$ANDROID_HOME" -name apksigner -type f 2>/dev/null | sort -V | tail -1)
|
|
fi
|
|
echo "$TANE_KEYSTORE_BASE64" | base64 -d > /tmp/ks.jks
|
|
out="/tmp/app-${ABI}-release.apk"
|
|
# v2/v3 only — NO v1 (JAR) signature. minSdk is 24, so v1 is never
|
|
# needed, and v1 adds META-INF/*.SF/*.RSA/MANIFEST.MF as zip ENTRIES
|
|
# that F-Droid's rebuild doesn't have. F-Droid's apksigcopier transplants
|
|
# only the v2/v3 signing block, so those extra v1 entries make the
|
|
# reference's signed content differ from F-Droid's rebuild (CHUNKED_SHA512
|
|
# mismatch) even when every file is identical. Dropping v1 makes the
|
|
# reference byte-match F-Droid's build so the signature copy verifies.
|
|
"$apksigner" sign --ks /tmp/ks.jks --ks-key-alias "$TANE_KEY_ALIAS" \
|
|
--ks-pass "pass:$TANE_KEYSTORE_PASSWORD" --key-pass "pass:$TANE_KEY_PASSWORD" \
|
|
--v1-signing-enabled false --v2-signing-enabled true --v3-signing-enabled true \
|
|
--alignment-preserved true \
|
|
--out "$out" "$f"
|
|
"$apksigner" verify --print-certs "$out" | grep -i 'SHA-256' || true
|
|
rm -f /tmp/ks.jks
|
|
# Upload target: the pushed tag, or (on a manual dispatch) the ref_tag
|
|
# input. A plain dispatch with no ref_tag just builds+signs, no upload.
|
|
api="http://forgejo:3000/api/v1/repos/${GITHUB_REPOSITORY}"
|
|
UPLOAD_TAG=""
|
|
if [ "${GITHUB_REF_TYPE:-}" = tag ]; then
|
|
UPLOAD_TAG="${GITHUB_REF_NAME}"
|
|
elif [ -n "${REF_TAG:-}" ]; then
|
|
UPLOAD_TAG="${REF_TAG}"
|
|
fi
|
|
if [ -n "$UPLOAD_TAG" ]; then
|
|
curl -sf -X POST "$api/releases" \
|
|
-H "Authorization: token ${TOKEN}" -H 'Content-Type: application/json' \
|
|
-d "{\"tag_name\":\"${UPLOAD_TAG}\",\"name\":\"${UPLOAD_TAG}\"}" >/dev/null || true
|
|
rid=$(curl -sf -H "Authorization: token ${TOKEN}" "$api/releases/tags/${UPLOAD_TAG}" \
|
|
| python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
|
|
# Idempotent re-upload: delete any existing asset of this name first
|
|
# (the assets POST rejects a duplicate name).
|
|
curl -sf -H "Authorization: token ${TOKEN}" "$api/releases/${rid}/assets" \
|
|
| python3 -c "import sys,json;[print(a['id']) for a in json.load(sys.stdin) if a['name']=='app-${ABI}-release.apk']" 2>/dev/null \
|
|
| while read -r aid; do [ -n "$aid" ] && curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "$api/releases/${rid}/assets/${aid}" >/dev/null || true; done
|
|
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 to ${UPLOAD_TAG} (from ${f##*/})"
|
|
else
|
|
echo "built+signed app-${ABI}-release.apk (dispatch: no ref_tag, upload skipped)"
|
|
fi
|
|
|
|
fdroid_reference_x86_64:
|
|
needs: fdroid_reference_arm64_v8a
|
|
runs-on: docker
|
|
container:
|
|
image: registry.gitlab.com/fdroid/fdroidserver:buildserver-trixie
|
|
steps:
|
|
- name: Build the x86_64 reference APK with fdroid, sign, and attach it
|
|
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 }}
|
|
REF_TAG: ${{ github.event.inputs.ref_tag }}
|
|
ABI: x86_64
|
|
OFFSET: 3
|
|
run: |
|
|
set -eu
|
|
sh /opt/buildserver/setup-env-vars /opt/android-sdk
|
|
. /etc/profile.d/bsenv.sh
|
|
fds=/opt/fdroidserver-src
|
|
mkdir -p "$fds"
|
|
# Download to a file with retries: a mid-transfer vSwitch blip used to
|
|
# pipe an empty stream straight into tar ("gzip: unexpected end of
|
|
# file"). -f fails on HTTP errors; --retry-all-errors covers resets.
|
|
for a in 1 2 3 4 5; do
|
|
curl -fsSL --retry 5 --retry-delay 3 --retry-all-errors \
|
|
https://gitlab.com/fdroid/fdroidserver/-/archive/master/fdroidserver-master.tar.gz \
|
|
-o /tmp/fds.tgz && tar -tzf /tmp/fds.tgz >/dev/null 2>&1 && break
|
|
echo "fdroidserver download attempt $a failed; retrying in 5s" >&2; sleep 5
|
|
done
|
|
tar -xz -C "$fds" --strip-components=1 -f /tmp/fds.tgz
|
|
export PATH="$fds:$PATH"
|
|
export PYTHONPATH="$fds:$fds/examples"
|
|
git config --global --add safe.directory '*'
|
|
git config --global url."http://x-access-token:${TOKEN}@forgejo:3000/".insteadOf "https://git.comunes.org/"
|
|
# Which app commit/tag to build: on a tag push, the pushed commit; on a
|
|
# manual dispatch with ref_tag set, that tag (to rebuild+re-upload an
|
|
# existing release's references without cutting a new tag / Play deploy).
|
|
BUILD_REF="${REF_TAG:-$GITHUB_SHA}"
|
|
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 "$BUILD_REF"
|
|
git checkout -q FETCH_HEAD
|
|
base=$(sed -n -E 's/^version:.*\+([0-9]+)$/\1/p' apps/app_seeds/pubspec.yaml)
|
|
[ -n "$base" ]
|
|
vc=$((base * 10 + OFFSET))
|
|
# Match F-Droid's buildserver path (~/build/<appid>) so the native .so
|
|
# embed identical build paths — required for the byte-for-byte repro check.
|
|
fdd=/home/vagrant
|
|
mkdir -p "$fdd/metadata" "$fdd/srclibs"
|
|
cp "$work/fdroid-ci/config.yml" "$fdd/config.yml"
|
|
cp "$work"/fdroid-ci/srclibs/*.yml "$fdd/srclibs/"
|
|
cp "$work/docs/fdroid/org.comunes.tane.yml" "$fdd/metadata/org.comunes.tane.yml"
|
|
cd "$fdd"
|
|
sed -i "s#^\( *commit: \).*#\1${BUILD_REF}#" metadata/org.comunes.tane.yml
|
|
sed -i '/^ *binary: /d' metadata/org.comunes.tane.yml
|
|
mkdir -p build
|
|
git clone -q https://git.comunes.org/comunes/tane.git build/org.comunes.tane
|
|
git -C build/org.comunes.tane checkout -q "${BUILD_REF}"
|
|
# Retry the build: a transient network blip mid-Gradle (maven/pub) used
|
|
# to kill it. `fdroid build` exits 0 even on failure, so gate the retry
|
|
# on the APK actually appearing. Up to 3 attempts fit the 90m timeout.
|
|
for a in 1 2 3; do
|
|
fdroid build --verbose --no-tarball "org.comunes.tane:${vc}" || true
|
|
if [ -f "unsigned/org.comunes.tane_${vc}.apk" ]; then break; fi
|
|
echo "fdroid build attempt $a produced no APK; retrying" >&2
|
|
done
|
|
f="unsigned/org.comunes.tane_${vc}.apk"
|
|
if [ ! -f "$f" ]; then
|
|
echo "expected $f, not found" >&2
|
|
tail -n 80 logs/*.log 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
apksigner=$(find "$ANDROID_HOME" -name apksigner -type f 2>/dev/null | sort -V | tail -1)
|
|
if [ -z "$apksigner" ]; then
|
|
yes | sdkmanager "build-tools;34.0.0" >/dev/null 2>&1 || true
|
|
apksigner=$(find "$ANDROID_HOME" -name apksigner -type f 2>/dev/null | sort -V | tail -1)
|
|
fi
|
|
echo "$TANE_KEYSTORE_BASE64" | base64 -d > /tmp/ks.jks
|
|
out="/tmp/app-${ABI}-release.apk"
|
|
# v2/v3 only — NO v1 (JAR) signature. minSdk is 24, so v1 is never
|
|
# needed, and v1 adds META-INF/*.SF/*.RSA/MANIFEST.MF as zip ENTRIES
|
|
# that F-Droid's rebuild doesn't have. F-Droid's apksigcopier transplants
|
|
# only the v2/v3 signing block, so those extra v1 entries make the
|
|
# reference's signed content differ from F-Droid's rebuild (CHUNKED_SHA512
|
|
# mismatch) even when every file is identical. Dropping v1 makes the
|
|
# reference byte-match F-Droid's build so the signature copy verifies.
|
|
"$apksigner" sign --ks /tmp/ks.jks --ks-key-alias "$TANE_KEY_ALIAS" \
|
|
--ks-pass "pass:$TANE_KEYSTORE_PASSWORD" --key-pass "pass:$TANE_KEY_PASSWORD" \
|
|
--v1-signing-enabled false --v2-signing-enabled true --v3-signing-enabled true \
|
|
--alignment-preserved true \
|
|
--out "$out" "$f"
|
|
"$apksigner" verify --print-certs "$out" | grep -i 'SHA-256' || true
|
|
rm -f /tmp/ks.jks
|
|
# Upload target: the pushed tag, or (on a manual dispatch) the ref_tag
|
|
# input. A plain dispatch with no ref_tag just builds+signs, no upload.
|
|
api="http://forgejo:3000/api/v1/repos/${GITHUB_REPOSITORY}"
|
|
UPLOAD_TAG=""
|
|
if [ "${GITHUB_REF_TYPE:-}" = tag ]; then
|
|
UPLOAD_TAG="${GITHUB_REF_NAME}"
|
|
elif [ -n "${REF_TAG:-}" ]; then
|
|
UPLOAD_TAG="${REF_TAG}"
|
|
fi
|
|
if [ -n "$UPLOAD_TAG" ]; then
|
|
curl -sf -X POST "$api/releases" \
|
|
-H "Authorization: token ${TOKEN}" -H 'Content-Type: application/json' \
|
|
-d "{\"tag_name\":\"${UPLOAD_TAG}\",\"name\":\"${UPLOAD_TAG}\"}" >/dev/null || true
|
|
rid=$(curl -sf -H "Authorization: token ${TOKEN}" "$api/releases/tags/${UPLOAD_TAG}" \
|
|
| python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
|
|
# Idempotent re-upload: delete any existing asset of this name first
|
|
# (the assets POST rejects a duplicate name).
|
|
curl -sf -H "Authorization: token ${TOKEN}" "$api/releases/${rid}/assets" \
|
|
| python3 -c "import sys,json;[print(a['id']) for a in json.load(sys.stdin) if a['name']=='app-${ABI}-release.apk']" 2>/dev/null \
|
|
| while read -r aid; do [ -n "$aid" ] && curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "$api/releases/${rid}/assets/${aid}" >/dev/null || true; done
|
|
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 to ${UPLOAD_TAG} (from ${f##*/})"
|
|
else
|
|
echo "built+signed app-${ABI}-release.apk (dispatch: no ref_tag, upload skipped)"
|
|
fi
|