Skip to content

Merge pull request #636 from appdevforall/feat/K2GO-452-v1.1.0-release #35

Merge pull request #636 from appdevforall/feat/K2GO-452-v1.1.0-release

Merge pull request #636 from appdevforall/feat/K2GO-452-v1.1.0-release #35

name: Build and Release K2Go APKs
on:
push:
# Only triggers automatically when a release tag is pushed (e.g., v1.0.0)
tags:
- 'v*'
workflow_dispatch:
jobs:
build-and-release:
name: Build Release APKs & Upload to R2
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./controller
steps:
- name: Checkout Code
uses: actions/checkout@v5
with:
submodules: recursive
# TODO (Future): Add a Jira step here to verify if the version exists in Jira
# or to extract release notes directly from a Jira board.
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: '17'
cache: 'gradle'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# ADFA-4466: the google-services plugin needs google-services.json at build time;
# inject it from the dedicated K2Go analytics secret so the build compiles.
- name: Create google-services.json
env:
GOOGLE_SERVICES_JSON_K2GO_ANALYTICS: ${{ secrets.GOOGLE_SERVICES_JSON_K2GO_ANALYTICS }}
run: echo "$GOOGLE_SERVICES_JSON_K2GO_ANALYTICS" > app/google-services.json
- name: Decode Keystore
env:
ENCODED_STRING: ${{ secrets.KEYSTORE_BASE64 }}
run: |
echo "$ENCODED_STRING" | base64 -d > keystore.jks
# --- VERSION AUDIT ---
- name: Log Pinned Binary Version
run: |
echo "=========================================="
echo "Compiling with native binaries pinned to:"
cat binary_version.txt
echo ""
echo "=========================================="
# --- BUILD AND SIGNING ---
- name: Build and Sign Release APKs
env:
# :app:syncNativeArtifacts queries the GitHub API for the pinned binaries
# release; unauthenticated it gets HTTP 403 (shared-runner rate limit), so
# provide the auto token for the M15 authenticated-retry fallback (build.gradle).
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
# ADFA-4533: GlitchTip DSN for release builds
SENTRY_DSN_RELEASE: ${{ secrets.SENTRY_DSN_RELEASE }}
run: |
# Release tags ship a clean version (v1.0.0), no commit hash. build.gradle only
# falls back to the git SHA suffix when versionSuffix is absent; pass it empty here.
# In-APK traceability is unaffected: BuildConfig.GIT_SHA still records the commit.
# K2GO-431: build both flavors. standard -> GitHub + R2 + OTA update.json (as before);
# fdroid -> R2 only (F-Droid distributes it; it carries no in-app OTA).
# K2GO-433: also build the Play AAB (play flavor); archived to R2 below.
./gradlew assembleStandardRelease assembleFdroidRelease bundlePlayRelease \
-PversionSuffix="" \
-Pandroid.injected.signing.store.file=$(pwd)/keystore.jks \
-Pandroid.injected.signing.store.password=$KEYSTORE_PASSWORD \
-Pandroid.injected.signing.key.alias=$KEY_ALIAS \
-Pandroid.injected.signing.key.password=$KEY_PASSWORD
# --- GITHUB RELEASES ---
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v3
with:
# Automatically marks as Pre-release if the tag ends in "-beta" or "-rc"
prerelease: ${{ contains(github.ref, '-beta') || contains(github.ref, '-rc') }}
files: controller/**/build/outputs/apk/standard/release/*.apk
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# --- CLOUDFLARE R2 UPLOAD ---
- name: Upload to Cloudflare R2
if: startsWith(github.ref, 'refs/tags/v')
env:
AWS_ACCESS_KEY_ID: ${{ vars.CLOUDFLARE_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
R2_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
BUCKET_NAME: "k2go-apk-repo"
run: |
# K2GO-431: upload BOTH flavors (standard + fdroid) to R2. The flavor dir sits between
# apk/ and release/ (apk/standard/release, apk/fdroid/release); the old apk/release path
# never matched a flavored build.
for apk in $(find . -path "*/build/outputs/apk/*/release/*.apk"); do
filename=$(basename "$apk")
echo "Uploading $filename to Cloudflare R2 ($BUCKET_NAME)..."
# Use aws-cli configured to point to Cloudflare's S3-compatible API
aws s3 cp "$apk" "s3://$BUCKET_NAME/$filename" \
--endpoint-url "https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com" \
--content-type "application/vnd.android.package-archive"
done
# K2GO-433: archive the Play AAB to R2 (backup copy, one object per release by versioned filename).
for aab in $(find . -path "*/build/outputs/bundle/playRelease/*.aab"); do
filename=$(basename "$aab")
echo "Uploading $filename to Cloudflare R2 ($BUCKET_NAME)..."
aws s3 cp "$aab" "s3://$BUCKET_NAME/$filename" \
--endpoint-url "https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com" \
--content-type "application/octet-stream"
done
# --- OTA MANIFEST (update.json) ---
# ADFA-4984: publish the OTA manifest LAST, after the APKs are already in R2, so it never
# points at a missing binary. update.json is a fixed key -> it overwrites the previous manifest
# and always resolves to the latest release (no .1/.2 copies). The changelog is copied verbatim
# from the top entry of controller/ci/ota-release-notes.md, which must match the release tag.
- name: Generate and upload update.json
if: startsWith(github.ref, 'refs/tags/v')
working-directory: ${{ github.workspace }}
env:
AWS_ACCESS_KEY_ID: ${{ vars.CLOUDFLARE_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
R2_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
BUCKET_NAME: "k2go-apk-repo"
run: |
set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}"
echo "Release tag: $TAG"
# versionCodeBase = the app module's raw versionCode (the app divides the installed,
# ABI-multiplied code by 10 to compare). First versionCode in controller/app/build.gradle.
VCODE=$(grep -oE 'versionCode[[:space:]]+[0-9]+' controller/app/build.gradle | head -1 | grep -oE '[0-9]+')
echo "versionCodeBase: $VCODE"
# Top entry of controller/ci/ota-release-notes.md; its header must equal the tag (guard against stale notes).
HEADER=$(grep -m1 '^## ' controller/ci/ota-release-notes.md | sed 's/^##[[:space:]]*//')
if [ "$HEADER" != "$TAG" ]; then
echo "::error::controller/ci/ota-release-notes.md top entry '$HEADER' does not match tag '$TAG'. Update the notes before tagging."
exit 1
fi
# Body = the lines between the first '## ' header and the next one (blank lines trimmed).
CHANGELOG=$(awk '/^## /{n++; next} n==1{print}' controller/ci/ota-release-notes.md | sed '/^[[:space:]]*$/d')
echo "Changelog:"; printf '%s\n' "$CHANGELOG"
# Built APK basenames per ABI (the binaries just uploaded to R2).
# OTA update.json covers the standard flavor only (fdroid ships with OTA disabled).
find_apk() { find controller -path "*/build/outputs/apk/standard/release/*$1*.apk" -printf '%f\n' | head -1; }
# The APK filenames abbreviate the split's ABI (arm64-v8a -> v8a); see build.gradle.
APK_ARM64=$(find_apk "-v8a-")
APK_ARM32=$(find_apk "-v7a-")
APK_UNIVERSAL=$(find_apk "-universal-")
echo "arm64=$APK_ARM64 | arm32=$APK_ARM32 | universal=$APK_UNIVERSAL"
if [ -z "$APK_ARM64" ] || [ -z "$APK_ARM32" ] || [ -z "$APK_UNIVERSAL" ]; then
echo "::error::Missing one or more built APKs (arm64='$APK_ARM64' arm32='$APK_ARM32' universal='$APK_UNIVERSAL'); aborting instead of publishing an incomplete manifest."
exit 1
fi
# jq escapes the multiline changelog safely into a JSON string.
jq -n \
--argjson vcode "$VCODE" \
--arg vname "$TAG" \
--arg changelog "$CHANGELOG" \
--arg arm64 "$APK_ARM64" \
--arg arm32 "$APK_ARM32" \
--arg universal "$APK_UNIVERSAL" \
'{versionCodeBase: $vcode, versionName: $vname, changelog: $changelog,
apk_arm64_v8a: $arm64, apk_armeabi_v7a: $arm32, apk_universal: $universal}' \
> update.json
echo "----- update.json -----"; cat update.json
# Upload LAST (after the APKs). Fixed key -> overwrites the previous manifest.
aws s3 cp update.json "s3://$BUCKET_NAME/update.json" \
--endpoint-url "https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com" \
--content-type "application/json"
# TODO (Future): Add Jira finalization step here to automatically mark
# the Jira version as "Released" and close the corresponding tickets.
# TODO (Future): Add a Slack notification step to announce the official release
# with links to the GitHub Release page and direct Cloudflare downloads.
# --- SECURITY CLEANUP ---
- name: Cleanup Keystore
if: always()
run: rm -f keystore.jks