6 Commits

Author SHA1 Message Date
Hermes
9fea20b751 ci: add BuildTool and route Android builds through it
All checks were successful
Android build / build (push) Successful in 9m6s
Android build / build (release) Successful in 16m31s
2026-09-01 17:16:54 -05:00
Hermes
cc2a16bf66 ci: broaden release asset upload token fallback
All checks were successful
Android build / build (push) Successful in 10m54s
Android build / build (release) Successful in 10m14s
2026-08-31 16:12:26 -05:00
Hermes
56a7103503 ci: bump APK versionCode and timestamp AAB names
Some checks failed
Android build / build (push) Successful in 15m22s
Android build / build (release) Failing after 12m34s
2026-08-31 15:06:03 -05:00
Hermes
f7722216a0 build: upgrade AGP for Android 16 support
All checks were successful
Android build / build (push) Successful in 16m36s
Android build / build (release) Successful in 13m37s
2026-08-31 13:31:01 -05:00
Hermes
8af42dbe48 build: target Android 16 / API 36
All checks were successful
Android build / build (push) Successful in 19m59s
2026-08-31 12:18:31 -05:00
Hermes
ebd7910b32 fix: resolve MindMachine release keystore path from repo root
All checks were successful
Android build / build (push) Successful in 11m29s
Android build / build (release) Successful in 11m56s
2026-08-29 14:03:23 -05:00
7 changed files with 312 additions and 82 deletions

View File

@@ -1,7 +1,7 @@
name: Android build name: Android build
permissions: permissions:
contents: read contents: write
releases: write releases: write
on: on:
@@ -29,110 +29,56 @@ jobs:
distribution: temurin distribution: temurin
java-version: '17' 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 shell: bash
env:
CI_VERSION_CODE: ${{ github.run_number }}
run: | run: |
set -euxo pipefail set -euxo pipefail
export ANDROID_SDK_ROOT="$HOME/.android/sdk" chmod +x ./BuildTool.sh
export ANDROID_HOME="$ANDROID_SDK_ROOT" ./BuildTool.sh --no-commit --apk-only
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-35" \
"build-tools;35.0.0"
- name: Prepare optional release signing - name: Run project build tooling for release AAB
if: github.event_name == 'release' if: github.event_name == 'release'
continue-on-error: true shell: bash
env: env:
CI_VERSION_CODE: ${{ github.run_number }}
CI_ARTIFACT_TIMESTAMP: ${{ github.run_id }}
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} 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 <<EOF
storeFile=.ci-secrets/release-keystore.jks
storePassword=$ANDROID_KEYSTORE_PASSWORD
keyAlias=$ANDROID_KEY_ALIAS
keyPassword=$ANDROID_KEY_PASSWORD
EOF
- name: Make Gradle wrapper executable
run: chmod +x gradlew
- name: Write Android SDK location
shell: bash
run: | run: |
set -euxo pipefail set -euxo pipefail
printf 'sdk.dir=%s/.android/sdk\n' "$HOME" > local.properties chmod +x ./BuildTool.sh
./BuildTool.sh --no-commit --with-aab
- 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: Upload debug APK - name: Upload debug APK
if: github.event_name != 'release' if: github.event_name != 'release'
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: debug-apk name: debug-apk
path: app/build/outputs/apk/debug/*.apk path: dist/*.apk
if-no-files-found: error if-no-files-found: error
- name: Attach release AAB to Gitea release - name: Attach release AAB to Gitea release
if: github.event_name == 'release' if: github.event_name == 'release'
env: env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITHUB_TOKEN: ${{ github.token }}
shell: bash shell: bash
run: | run: |
set -euxo pipefail set -euxo pipefail
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"])')" release_id="$(python3 -c 'import json, os; print(json.load(open(os.environ["GITHUB_EVENT_PATH"], encoding="utf-8"))["release"]["id"])')"
aab="$(find app/build/outputs/bundle/release -name '*.aab' | head -n 1)" aab="$(find dist -name '*.aab' | head -n 1)"
curl --fail-with-body \ curl --fail-with-body \
--request POST \ --request POST \
--header "Authorization: token ${GITEA_TOKEN}" \ --header "Authorization: token ${token}" \
--form "attachment=@${aab}" \ --form "attachment=@${aab}" \
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets?name=$(basename "$aab")" "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets?name=$(basename "$aab")"

6
.gitignore vendored
View File

@@ -3,9 +3,13 @@
*.iml *.iml
build/ build/
app/build/ app/build/
dist/
commit-message.txt
.android-sdk/
local.properties local.properties
# Signing # Signing
keystore/*.jks keystore/*.jks
keystore/*.properties keystore/*.properties
keystore.properties
.ci-secrets/

263
BuildTool.sh Normal file
View File

@@ -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 <<EOF
Usage: ./BuildTool.sh [options]
Options:
--setup-only Check/install build environment and exit
--no-commit Build successfully but do not auto-commit
--apk-only Build debug APK only (default)
--with-aab Also build release AAB
--help Show this help
Behavior:
- Disables Gradle daemon
- Increments versionCode/versionName for local builds
- Honors CI_VERSION_CODE for CI builds
- Names CI release AABs with APP_NAME + CI_ARTIFACT_TIMESTAMP when provided
- Uses repo secrets from env for release signing when provided
- Copies artifacts to dist/
EOF
}
SETUP_ONLY=false
DO_COMMIT=true
WITH_AAB=false
for arg in "$@"; do
case "$arg" in
--setup-only) SETUP_ONLY=true ;;
--no-commit) DO_COMMIT=false ;;
--apk-only) WITH_AAB=false ;;
--with-aab) WITH_AAB=true ;;
--help|-h) usage; exit 0 ;;
*) echo "Unknown option: $arg" >&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 <<EOF
storeFile=.ci-secrets/release-keystore.jks
storePassword=$ANDROID_KEYSTORE_PASSWORD
keyAlias=$ANDROID_KEY_ALIAS
keyPassword=$ANDROID_KEY_PASSWORD
EOF
}
ensure_gitignore_entries() {
touch .gitignore
for entry in \
"$COMMIT_MSG_FILE" \
".android-sdk/" \
".gradle/" \
"$DIST_DIR/" \
"local.properties" \
"**/build/" \
"*.iml" \
".idea/" \
; do
grep -qxF "$entry" .gitignore || echo "$entry" >> .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 "$@"

View File

@@ -2,6 +2,23 @@
## Build prerequisites ## 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 - JDK 17
- Android Studio (recommended) - Android Studio (recommended)
- Android SDK installed - Android SDK installed

View File

@@ -21,12 +21,12 @@ val hasReleaseSigning = listOf(
android { android {
namespace = "solutions.tretter.mindmachine" namespace = "solutions.tretter.mindmachine"
compileSdk = 35 compileSdk = 36
defaultConfig { defaultConfig {
applicationId = "solutions.tretter.mindmachine" applicationId = "solutions.tretter.mindmachine"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 36
versionCode = 18 versionCode = 18
versionName = "1.0.13" versionName = "1.0.13"
@@ -36,7 +36,7 @@ android {
signingConfigs { signingConfigs {
if (hasReleaseSigning) { if (hasReleaseSigning) {
create("release") { create("release") {
storeFile = file(keystoreProperties.getProperty("storeFile")) storeFile = rootProject.file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword") storePassword = keystoreProperties.getProperty("storePassword")
keyAlias = keystoreProperties.getProperty("keyAlias") keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword") keyPassword = keystoreProperties.getProperty("keyPassword")

View File

@@ -1,5 +1,5 @@
plugins { plugins {
id("com.android.application") version "8.5.2" apply false id("com.android.application") version "8.10.0" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "2.1.0" apply false id("org.jetbrains.kotlin.plugin.serialization") version "2.1.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME