diff --git a/.gitea/workflows/android-build.yml b/.gitea/workflows/android-build.yml index a650852..58546a6 100644 --- a/.gitea/workflows/android-build.yml +++ b/.gitea/workflows/android-build.yml @@ -29,132 +29,37 @@ jobs: distribution: temurin java-version: '17' - - name: Install Android SDK command-line tools and packages + - name: Run project build tooling for debug APK + if: github.event_name != 'release' shell: bash + env: + CI_VERSION_CODE: ${{ github.run_number }} run: | set -euxo pipefail - export ANDROID_SDK_ROOT="$HOME/.android/sdk" - export ANDROID_HOME="$ANDROID_SDK_ROOT" - mkdir -p "$ANDROID_SDK_ROOT/cmdline-tools" - if [ ! -x "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" ]; then - tools_url="$(curl -fsSL https://developer.android.com/studio | grep -o 'https://dl.google.com/android/repository/commandlinetools-linux-[0-9][0-9]*_latest.zip' | head -n 1)" - test -n "$tools_url" - curl -fsSL -o /tmp/commandlinetools.zip "$tools_url" - rm -rf "$ANDROID_SDK_ROOT/cmdline-tools/latest" "$ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools" - unzip -q /tmp/commandlinetools.zip -d "$ANDROID_SDK_ROOT/cmdline-tools" - mv "$ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools" "$ANDROID_SDK_ROOT/cmdline-tools/latest" - fi - export PATH="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools:$PATH" - set +o pipefail - yes | sdkmanager --licenses >/dev/null - set -o pipefail - sdkmanager \ - "platform-tools" \ - "platforms;android-36" \ - "build-tools;36.0.0" + chmod +x ./BuildTool.sh + ./BuildTool.sh --no-commit --apk-only - - name: Prepare optional release signing + - name: Run project build tooling for release AAB if: github.event_name == 'release' - continue-on-error: true + shell: bash env: + CI_VERSION_CODE: ${{ github.run_number }} + CI_ARTIFACT_TIMESTAMP: ${{ github.run_id }} ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} - shell: bash - run: | - set -euo pipefail - missing=() - for key in ANDROID_KEYSTORE_BASE64 ANDROID_KEYSTORE_PASSWORD ANDROID_KEY_ALIAS ANDROID_KEY_PASSWORD; do - if [ -z "${!key:-}" ]; then - missing+=("$key") - fi - done - - if [ ${#missing[@]} -gt 0 ]; then - printf -v missing_csv '%s, ' "${missing[@]}" - missing_csv="${missing_csv%, }" - echo "::error::Release signing secrets missing: ${missing_csv}. Continuing with unsigned/default-signed release AAB." - exit 1 - fi - - mkdir -p .ci-secrets - printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 -d > .ci-secrets/release-keystore.jks - cat > keystore.properties < local.properties - - - name: Prepare CI build metadata - id: build_meta - shell: bash - run: | - set -euxo pipefail - timestamp_utc="$(date -u +%Y%m%d-%H%M%S)" - version_code="$(date -u +%s)" - project_name="$(basename "$GITHUB_REPOSITORY")" - python3 - "$version_code" <<'PY' - from pathlib import Path - import re, sys - path = Path('app/build.gradle.kts') - text = path.read_text(encoding='utf-8') - new_text, count = re.subn(r'(\bversionCode\s*=\s*)\d+', rf'\g<1>{sys.argv[1]}', text, count=1) - if count != 1: - raise SystemExit('versionCode not found in app/build.gradle.kts') - path.write_text(new_text, encoding='utf-8') - PY - echo "Using CI versionCode: $version_code" - echo "project_name=$project_name" >> "$GITHUB_OUTPUT" - echo "timestamp_utc=$timestamp_utc" >> "$GITHUB_OUTPUT" - echo "version_code=$version_code" >> "$GITHUB_OUTPUT" - - - name: Build debug APK - if: github.event_name != 'release' - shell: bash - run: | - set -euxo pipefail - export ANDROID_SDK_ROOT="$HOME/.android/sdk" - export ANDROID_HOME="$ANDROID_SDK_ROOT" - ./gradlew --no-daemon clean assembleDebug - - - name: Build release AAB - if: github.event_name == 'release' - shell: bash - run: | - set -euxo pipefail - export ANDROID_SDK_ROOT="$HOME/.android/sdk" - export ANDROID_HOME="$ANDROID_SDK_ROOT" - ./gradlew --no-daemon bundleRelease - - - name: Rename release AAB for upload - if: github.event_name == 'release' - id: release_aab - shell: bash - run: | - set -euxo pipefail - aab="$(find app/build/outputs/bundle/release -name '*.aab' | head -n 1)" - renamed="app/build/outputs/bundle/release/${{ steps.build_meta.outputs.project_name }}-${{ steps.build_meta.outputs.timestamp_utc }}-release.aab" - mv -f "$aab" "$renamed" - echo "Renamed release AAB to $(basename "$renamed")" - echo "path=$renamed" >> "$GITHUB_OUTPUT" + chmod +x ./BuildTool.sh + ./BuildTool.sh --no-commit --with-aab - name: Upload debug APK if: github.event_name != 'release' uses: actions/upload-artifact@v3 with: name: debug-apk - path: app/build/outputs/apk/debug/*.apk + path: dist/*.apk if-no-files-found: error - name: Attach release AAB to Gitea release @@ -165,13 +70,13 @@ jobs: shell: bash run: | set -euxo pipefail - token="${GITEA_TOKEN:-${GITHUB_TOKEN:-}}" + token="${GITHUB_TOKEN:-${GITEA_TOKEN:-}}" if [ -z "$token" ]; then echo "::error::No release upload token is available from secrets.GITEA_TOKEN or github.token" exit 1 fi release_id="$(python3 -c 'import json, os; print(json.load(open(os.environ["GITHUB_EVENT_PATH"], encoding="utf-8"))["release"]["id"])')" - aab="${{ steps.release_aab.outputs.path }}" + aab="$(find dist -name '*.aab' | head -n 1)" curl --fail-with-body \ --request POST \ --header "Authorization: token ${token}" \ diff --git a/.gitignore b/.gitignore index c881ca6..1f2569c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,13 @@ *.iml build/ app/build/ +dist/ +commit-message.txt +.android-sdk/ local.properties # Signing keystore/*.jks keystore/*.properties - +keystore.properties +.ci-secrets/ diff --git a/BuildTool.sh b/BuildTool.sh new file mode 100644 index 0000000..fea9d1f --- /dev/null +++ b/BuildTool.sh @@ -0,0 +1,263 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +APP_NAME="MindMachine" +MODULE="app" +API_LEVEL="36" +BUILD_TOOLS_VERSION="36.0.0" +DIST_DIR="dist" +COMMIT_MSG_FILE="commit-message.txt" + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$ROOT_DIR" + +export GRADLE_OPTS="${GRADLE_OPTS:-} -Dorg.gradle.daemon=false" + +usage() { + cat <&2; usage; exit 1 ;; + esac +done + +CI_VERSION_CODE="${CI_VERSION_CODE:-}" +CI_ARTIFACT_TIMESTAMP="${CI_ARTIFACT_TIMESTAMP:-}" + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "Missing required command: $1" >&2 + exit 1 + } +} + +ensure_java() { + need_cmd java + local major + major="$(java -version 2>&1 | awk -F[\".] '/version/ {print $2}')" + if [[ "${major:-0}" -lt 17 ]]; then + echo "Java 17+ is required. Found Java ${major:-unknown}." >&2 + exit 1 + fi +} + +ensure_android_sdk() { + if [[ -z "${ANDROID_HOME:-}" ]]; then + export ANDROID_HOME="$ROOT_DIR/.android-sdk" + fi + export ANDROID_SDK_ROOT="$ANDROID_HOME" + export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH" + + if [[ ! -x "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" ]]; then + need_cmd curl + need_cmd unzip + mkdir -p "$ANDROID_HOME/cmdline-tools" + local zip_path="$ANDROID_HOME/cmdline-tools/commandlinetools.zip" + curl -L -o "$zip_path" "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" + rm -rf "$ANDROID_HOME/cmdline-tools/latest" "$ANDROID_HOME/cmdline-tools/cmdline-tools" + unzip -q "$zip_path" -d "$ANDROID_HOME/cmdline-tools" + mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest" + rm "$zip_path" + fi + + yes | sdkmanager --licenses >/dev/null || true + sdkmanager \ + "platform-tools" \ + "platforms;android-$API_LEVEL" \ + "build-tools;$BUILD_TOOLS_VERSION" >/dev/null +} + +ensure_gradle_wrapper() { + if [[ ! -x "./gradlew" ]]; then + if [[ -f "./gradlew" ]]; then + chmod +x ./gradlew + else + echo "Missing Gradle wrapper ./gradlew" >&2 + exit 1 + fi + fi +} + +gradle_no_daemon() { + ./gradlew --no-daemon -Dorg.gradle.daemon=false "$@" +} + +prepare_optional_release_signing() { + local required=(ANDROID_KEYSTORE_BASE64 ANDROID_KEYSTORE_PASSWORD ANDROID_KEY_ALIAS ANDROID_KEY_PASSWORD) + local missing=() + local key + for key in "${required[@]}"; do + if [[ -z "${!key:-}" ]]; then + missing+=("$key") + fi + done + + if [[ ${#missing[@]} -eq ${#required[@]} ]]; then + return + fi + if [[ ${#missing[@]} -gt 0 ]]; then + printf 'Release signing secrets missing: %s\n' "${missing[*]}" >&2 + exit 1 + fi + + mkdir -p .ci-secrets + printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 -d > .ci-secrets/release-keystore.jks + cat > keystore.properties <> .gitignore + done +} + +increment_versions() { + local gradle_file="$MODULE/build.gradle.kts" + python3 - "$gradle_file" "$CI_VERSION_CODE" <<'PY' +import re +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +text = path.read_text() +ci_version_code = sys.argv[2].strip() + +m_code = re.search(r'\bversionCode\s*=\s*(\d+)', text) +m_name = re.search(r'\bversionName\s*=\s*"(\d+(?:\.\d+)*)"', text) +if not m_code or not m_name: + raise SystemExit('Could not find versionCode/versionName in app/build.gradle.kts') + +old_code = int(m_code.group(1)) +new_code = int(ci_version_code) if ci_version_code else old_code + 1 +parts = m_name.group(1).split('.') +parts[-1] = str(int(parts[-1]) + 1) +new_name = '.'.join(parts) + +text = re.sub(r'(\bversionCode\s*=\s*)\d+', rf'\g<1>{new_code}', text, count=1) +if not ci_version_code: + text = re.sub(r'(\bversionName\s*=\s*)"\d+(?:\.\d+)*"', rf'\g<1>"{new_name}"', text, count=1) +path.write_text(text) +print(new_code) +PY +} + +build_artifacts() { + gradle_no_daemon clean assembleDebug + if [[ "$WITH_AAB" == true ]]; then + gradle_no_daemon bundleRelease + fi +} + +copy_artifacts() { + local version_code="$1" + mkdir -p "$DIST_DIR" + + local debug_apk + debug_apk="$(find "$MODULE/build/outputs/apk/debug" -name "*.apk" | head -n 1 || true)" + [[ -f "$debug_apk" ]] || { echo "Debug APK not found." >&2; exit 1; } + cp "$debug_apk" "$DIST_DIR/${APP_NAME}-v${version_code}-debug.apk" + + if [[ "$WITH_AAB" == true ]]; then + local release_aab + release_aab="$(find "$MODULE/build/outputs/bundle/release" -name "*.aab" | head -n 1 || true)" + [[ -f "$release_aab" ]] || { echo "Release AAB not found." >&2; exit 1; } + local release_name + if [[ -n "$CI_ARTIFACT_TIMESTAMP" ]]; then + release_name="${APP_NAME}-${CI_ARTIFACT_TIMESTAMP}-release.aab" + else + release_name="${APP_NAME}-v${version_code}-release.aab" + fi + cp "$release_aab" "$DIST_DIR/$release_name" + fi +} + +commit_successful_build() { + git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return + local message + if [[ -s "$COMMIT_MSG_FILE" ]]; then + message="$(cat "$COMMIT_MSG_FILE")" + else + message="No Details" + fi + git add -A + git reset -q HEAD -- keystore.properties '.ci-secrets' '*.jks' 2>/dev/null || true + if ! git diff --cached --quiet; then + git commit -m "$message" + fi + : > "$COMMIT_MSG_FILE" +} + +main() { + need_cmd python3 + ensure_java + ensure_android_sdk + ensure_gradle_wrapper + ensure_gitignore_entries + + printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties + + if [[ "$SETUP_ONLY" == true ]]; then + echo "Build environment setup complete." + exit 0 + fi + + if [[ "$WITH_AAB" == true ]]; then + prepare_optional_release_signing + fi + + local version_code + version_code="$(increment_versions)" + build_artifacts + copy_artifacts "$version_code" + + if [[ "$DO_COMMIT" == true ]]; then + commit_successful_build + fi + + echo "Build completed successfully for versionCode $version_code." +} + +main "$@" diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index abaf99b..5fa97e8 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -2,6 +2,23 @@ ## Build prerequisites +Use the root build tooling so local builds and Gitea Actions run through the same script: + +```bash +./BuildTool.sh --setup-only +./BuildTool.sh --no-commit --apk-only +./BuildTool.sh --no-commit --with-aab +``` + +Behavior summary: +- `--setup-only` prepares the local Android SDK/tooling and exits. +- `--apk-only` builds the debug APK. +- `--with-aab` also builds the release AAB. +- `--no-commit` skips the post-build git commit step. +- Local builds increment `versionCode` and `versionName`. +- CI can provide `CI_VERSION_CODE` to force the build version code and `CI_ARTIFACT_TIMESTAMP` to control the release AAB filename. +- Artifacts are copied into `dist/`. + - JDK 17 - Android Studio (recommended) - Android SDK installed