# 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 </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/) 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/) 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/) 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