name: Android APK Release on: push: branches: - main - master workflow_dispatch: permissions: actions: read contents: write releases: write env: PUB_HOSTED_URL: https://pub.flutter-io.cn FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn jobs: android: name: Build Android APK runs-on: ubuntu-latest steps: - name: Preflight disk cleanup shell: bash run: | set -euo pipefail run_sudo() { if command -v sudo >/dev/null 2>&1; then sudo "$@" else "$@" fi } free_mb() { df -Pm "${1:-/}" | awk 'NR==2 {print $4}' } echo "Disk usage before cleanup:" df -h if command -v docker >/dev/null 2>&1; then docker system prune -af --volumes || true docker builder prune -af || true fi run_sudo rm -rf /tmp/* /var/tmp/* || true if [ "$(free_mb /)" -lt 8192 ]; then echo "Free disk space is below 8GB. Cleaning user build caches..." rm -rf "$HOME/.gradle/caches" "$HOME/.gradle/wrapper/dists" "$HOME/.pub-cache/_temp" || true fi echo "Disk usage after cleanup:" df -h if [ "$(free_mb /)" -lt 4096 ]; then echo "Free disk space is still below 4GB after cleanup. Runner disk is effectively full." exit 1 fi - name: Checkout uses: actions/checkout@v4 with: repository: gongyun/app ref: ${{ github.ref }} token: ${{ secrets.GITEA_TOKEN }} github-server-url: https://git.saont.net - name: Set up Java 17 uses: actions/setup-java@v4 with: distribution: temurin java-version: "17" - name: Set up Flutter uses: subosito/flutter-action@v2 with: channel: stable flutter-version: "3.41.9" cache: true - name: Set up Android SDK uses: android-actions/setup-android@v3 - name: Install Android SDK packages shell: bash run: | set -euo pipefail run_sdkmanager() { set +e yes | sdkmanager "$@" local status="${PIPESTATUS[1]}" set -e return "$status" } run_sdkmanager --install \ "platform-tools" \ "platforms;android-36" \ "build-tools;36.0.0" \ "cmdline-tools;latest" run_sdkmanager --licenses >/dev/null flutter config --android-sdk "$ANDROID_HOME" - name: Check build environment shell: bash run: | set -euo pipefail df -h echo "ANDROID_HOME=$ANDROID_HOME" java -version flutter --version flutter doctor -v - name: Install dependencies run: flutter pub get --enforce-lockfile - name: Restore Android signing files shell: bash env: ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: | set -euo pipefail if [ -z "${ANDROID_KEYSTORE_BASE64:-}" ] || [ -z "${ANDROID_STORE_PASSWORD:-}" ] || [ -z "${ANDROID_KEY_ALIAS:-}" ] || [ -z "${ANDROID_KEY_PASSWORD:-}" ]; then echo "Android signing secrets are required: ANDROID_KEYSTORE_BASE64, ANDROID_STORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD" exit 1 fi printf "%s" "$ANDROID_KEYSTORE_BASE64" | base64 -d > android/app-release.jks { printf "storeFile=../app-release.jks\n" printf "storePassword=%s\n" "$ANDROID_STORE_PASSWORD" printf "keyAlias=%s\n" "$ANDROID_KEY_ALIAS" printf "keyPassword=%s\n" "$ANDROID_KEY_PASSWORD" } > android/key.properties - name: Build Android release APK shell: bash env: IP_NODE_API_BASE_URL: ${{ secrets.IP_NODE_API_BASE_URL }} run: | set -euo pipefail rm -rf build dist android/.gradle pkg_dir="$PWD/dist/android" if [ -d "$pkg_dir" ]; then echo "Cleaning old Android build artifacts: $pkg_dir" rm -rf "$pkg_dir" fi build_args=(--release --no-pub) if [ -n "${IP_NODE_API_BASE_URL:-}" ]; then build_args+=("--dart-define=IP_NODE_API_BASE_URL=${IP_NODE_API_BASE_URL}") fi echo "Building Flutter Android release APK..." flutter build apk "${build_args[@]}" mkdir -p "$pkg_dir" apk_path="$(find "$PWD/build/app/outputs" -type f -name '*release*.apk' | head -n 1)" if [ -z "$apk_path" ]; then echo "Release APK not found under build/app/outputs" exit 1 fi cp "$apk_path" "$pkg_dir/gongyun-android-app-release.apk" ls -alh "$pkg_dir/gongyun-android-app-release.apk" - name: Prepare Android APK artifact shell: bash run: | set -euo pipefail apk="dist/android/gongyun-android-app-release.apk" if [ ! -f "$apk" ]; then echo "Release APK was not found at $apk." exit 1 fi cp "$apk" gongyun-android-app-release.apk ls -alh gongyun-android-app-release.apk - name: Resolve Android release tag id: android_tag shell: bash env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} run: | set -euo pipefail if [ -z "${GITEA_TOKEN:-}" ]; then echo "GITEA_TOKEN is required to create Android release tags." exit 1 fi if [[ "$GITHUB_REF" == refs/tags/v* ]]; then tag_name="${GITHUB_REF#refs/tags/}" else app_version="$(awk -F '[:+]' '/^version:/ {gsub(/[[:space:]]/, "", $2); print $2; exit}' pubspec.yaml)" if [ -z "$app_version" ]; then echo "Unable to resolve app version from pubspec.yaml." exit 1 fi tag_name="v${app_version}" fi echo "tag_name=$tag_name" >> "$GITHUB_OUTPUT" echo "Android release tag: $tag_name" current_commit="$(git rev-parse HEAD)" if git ls-remote --exit-code --tags origin "refs/tags/$tag_name" >/dev/null 2>&1; then git fetch --force --depth=1 origin "refs/tags/$tag_name:refs/tags/$tag_name" existing_tag_commit="$(git rev-list -n 1 "$tag_name")" if [ "$existing_tag_commit" != "$current_commit" ]; then echo "Tag $tag_name already exists at $existing_tag_commit, but this build is $current_commit." echo "Bump pubspec.yaml version before creating a new Android release." exit 1 fi echo "Tag $tag_name already exists for this commit." exit 0 fi git config user.name "gitea-actions" git config user.email "gitea-actions@git.saont.net" git tag "$tag_name" "$current_commit" git push origin "refs/tags/$tag_name" - name: Upload Android APK artifact uses: actions/upload-artifact@v3 with: name: gongyun-android-app-release.apk path: gongyun-android-app-release.apk - name: Publish Gitea release shell: bash env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_API_URL: https://git.saont.net/api/v1 GITEA_OWNER: gongyun GITEA_REPO: app TAG_NAME: ${{ steps.android_tag.outputs.tag_name }} TARGET_COMMITISH: ${{ github.sha }} ANDROID_APK_FILE: gongyun-android-app-release.apk run: | set -euo pipefail if [ -z "${GITEA_TOKEN:-}" ]; then echo "GITEA_TOKEN is required to publish release assets." exit 1 fi releases_url="$GITEA_API_URL/repos/$GITEA_OWNER/$GITEA_REPO/releases" release_by_tag_url="$releases_url/tags/$TAG_NAME" export RELEASE_TITLE="${TAG_NAME}" export RELEASE_BODY="Android app installation package (APK) for ${TAG_NAME}." status="$(curl -sS -o release.json -w "%{http_code}" \ -H "Authorization: token $GITEA_TOKEN" \ "$release_by_tag_url")" if [ "$status" = "404" ]; then release_payload="$(python3 - <<'PY' import json import os tag = os.environ["TAG_NAME"] print(json.dumps({ "tag_name": tag, "target_commitish": os.environ.get("TARGET_COMMITISH", ""), "name": os.environ["RELEASE_TITLE"], "body": f"Android app installation package (APK) for {tag}.", "draft": False, "prerelease": False, })) PY )" status="$(curl -sS -o release.json -w "%{http_code}" \ -X POST \ -H "Authorization: token $GITEA_TOKEN" \ -H "Content-Type: application/json" \ -d "$release_payload" \ "$releases_url")" fi if [ "$status" != "200" ] && [ "$status" != "201" ]; then echo "Failed to create or load release. HTTP status: $status" cat release.json exit 1 fi release_id="$(python3 - <<'PY' import json with open("release.json", encoding="utf-8") as f: print(json.load(f)["id"]) PY )" release_update_payload="$(python3 - <<'PY' import json import os print(json.dumps({ "name": os.environ["RELEASE_TITLE"], "body": os.environ["RELEASE_BODY"], "draft": False, "prerelease": False, })) PY )" update_status="$(curl -sS -o release.json -w "%{http_code}" \ -X PATCH \ -H "Authorization: token $GITEA_TOKEN" \ -H "Content-Type: application/json" \ -d "$release_update_payload" \ "$releases_url/$release_id")" if [ "$update_status" != "200" ]; then echo "Failed to update Android release metadata. HTTP status: $update_status" cat release.json exit 1 fi assets_url="$releases_url/$release_id/assets" curl -sS -f \ -H "Authorization: token $GITEA_TOKEN" \ "$assets_url" \ -o assets.json python3 - <<'PY' > asset-ids-to-delete.txt import json import os tag = os.environ["TAG_NAME"] names = { os.environ["ANDROID_APK_FILE"], f"gongyun-{tag}.apk", "gongyun-app-release.apk", } with open("assets.json", encoding="utf-8") as f: for asset in json.load(f): if asset.get("name") in names: print(asset["id"]) PY while IFS= read -r asset_id; do if [ -n "$asset_id" ]; then curl -sS -f \ -X DELETE \ -H "Authorization: token $GITEA_TOKEN" \ "$assets_url/$asset_id" fi done < asset-ids-to-delete.txt release_asset_name="app-release-${TAG_NAME}.apk" curl -sS -f \ -X POST \ -H "Authorization: token $GITEA_TOKEN" \ -F "attachment=@$ANDROID_APK_FILE" \ "$assets_url?name=$release_asset_name" - name: Post-build disk cleanup if: always() shell: bash run: | set -euo pipefail rm -rf build dist .dart_tool android/.gradle android/app-release.jks android/key.properties gongyun-android-app-release.apk gongyun-app-release.apk || true if command -v docker >/dev/null 2>&1; then docker system prune -af --volumes || true docker builder prune -af || true fi df -h