chore: automate Android release builds

This commit is contained in:
admin
2026-07-11 00:27:54 +02:00
parent 7bba4dc878
commit 956f33140e
3 changed files with 384 additions and 0 deletions
+1
View File
@@ -7,3 +7,4 @@ app/google-services.json:Zone.Identifier
*.iml *.iml
keystore.properties keystore.properties
*.jks *.jks
release-artifacts/
+20
View File
@@ -21,3 +21,23 @@ Natywna aplikacja Android Jetpack Compose dla kierowcow TPP.
```bash ```bash
./gradlew :app:testDebugUnitTest ./gradlew :app:testDebugUnitTest
``` ```
## Release Gitea i Google Play
Przed wydaniem zapisz wszystkie zmiany źródłowe w commicie. Następnie uruchom:
```bash
./scripts/release-android.sh
```
Skrypt automatycznie zwiększa `versionCode` i ostatni człon `versionName`, uruchamia testy,
buduje podpisany APK dla Gitea oraz AAB dla Google Play Console, tworzy commit wersji,
tag, push i Gitea Release. Artefakty zostają również zapisane lokalnie w
`release-artifacts/<tag>/`.
Do autoryzacji Gitea używany jest `GITEA_TOKEN` albo dane zapisane w Git credential helper.
Jeśli publikacja została przerwana już po utworzeniu commita wersji, można ją wznowić:
```bash
./scripts/release-android.sh --reuse-current
```
+363
View File
@@ -0,0 +1,363 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VERSION_FILE="$ROOT_DIR/app/build.gradle.kts"
REMOTE_NAME="${GIT_REMOTE:-origin}"
NOTES_FILE=""
SKIP_TESTS=0
REUSE_CURRENT=0
VERSION_FILE_CHANGED=0
VERSION_COMMITTED=0
VERSION_BACKUP=""
RELEASE_SUCCEEDED=0
usage() {
cat <<'EOF'
Użycie: scripts/release-android.sh [opcje]
Buduje i publikuje kolejną wersję aplikacji kierowcy:
- automatycznie zwiększa versionCode o 1 i patch versionName o 1,
- uruchamia testy jednostkowe,
- buduje podpisany APK dla Gitea Release,
- buduje podpisany AAB dla Google Play Console,
- zapisuje zmianę wersji w commicie, tworzy tag i wykonuje push,
- tworzy lub aktualizuje Gitea Release i wysyła APK oraz SHA256SUMS.
Opcje:
--notes-file PLIK Treść release notes z podanego pliku.
--skip-tests Pomiń testDebugUnitTest.
--reuse-current Nie zwiększaj wersji. Wznów publikację wersji zapisanej
obecnie w app/build.gradle.kts.
-h, --help Pokaż pomoc.
Zmienne środowiskowe:
GITEA_TOKEN Token API Gitea. Jeśli go brak, używany jest git credential helper.
GITEA_BASE_URL Nadpisuje adres Gitea, np. https://gitea.example.com.
GITEA_REPOSITORY Nadpisuje owner/repository.
GIT_REMOTE Remote Git, domyślnie origin.
Wersja początkowa jest zawsze odczytywana z app/build.gradle.kts.
Google Play Console otrzymuje plik AAB, ponieważ jest to właściwy format publikacyjny.
EOF
}
while (($# > 0)); do
case "$1" in
--notes-file)
NOTES_FILE="${2:-}"
[[ -n "$NOTES_FILE" ]] || { echo "Brak wartości dla --notes-file." >&2; exit 2; }
shift 2
;;
--skip-tests)
SKIP_TESTS=1
shift
;;
--reuse-current)
REUSE_CURRENT=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Nieznana opcja: $1" >&2
usage >&2
exit 2
;;
esac
done
cd "$ROOT_DIR"
for command in git curl jq perl sha256sum jarsigner; do
command -v "$command" >/dev/null 2>&1 || {
echo "Brak wymaganego polecenia: $command" >&2
exit 1
}
done
[[ -x ./gradlew ]] || { echo "Brak wykonywalnego ./gradlew." >&2; exit 1; }
[[ -f keystore.properties ]] || { echo "Brak keystore.properties wymaganego do podpisania release." >&2; exit 1; }
[[ -f "$VERSION_FILE" ]] || { echo "Brak $VERSION_FILE." >&2; exit 1; }
branch="$(git branch --show-current)"
[[ -n "$branch" ]] || { echo "Release nie może być wykonany z detached HEAD." >&2; exit 1; }
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "Repozytorium ma niezapisane zmiany śledzonych plików. Najpierw wykonaj commit." >&2
exit 1
fi
relevant_untracked="$(git status --porcelain --untracked-files=all -- app gradle scripts build.gradle.kts settings.gradle.kts gradle.properties | awk '$1 == "??" { print }')"
if [[ -n "$relevant_untracked" ]]; then
echo "Repozytorium ma nieśledzone pliki źródłowe. Najpierw zdecyduj, czy mają wejść do release:" >&2
printf '%s\n' "$relevant_untracked" >&2
exit 1
fi
git fetch --tags "$REMOTE_NAME"
remote_url="$(git remote get-url "$REMOTE_NAME")"
sanitized_remote="$(printf '%s' "$remote_url" | sed -E 's#^(https?://)[^/@]+@#\1#')"
if [[ "$sanitized_remote" =~ ^(https?)://([^/]+)/(.+)$ ]]; then
detected_protocol="${BASH_REMATCH[1]}"
detected_host="${BASH_REMATCH[2]}"
detected_repository="${BASH_REMATCH[3]%.git}"
else
detected_protocol="https"
detected_host=""
detected_repository=""
fi
gitea_base_url="${GITEA_BASE_URL:-${detected_protocol}://${detected_host}}"
gitea_repository="${GITEA_REPOSITORY:-$detected_repository}"
gitea_base_url="${gitea_base_url%/}"
[[ "$gitea_base_url" =~ ^https?://[^/]+$ ]] || { echo "Ustaw poprawne GITEA_BASE_URL." >&2; exit 1; }
[[ "$gitea_repository" =~ ^[^/]+/[^/]+$ ]] || { echo "Ustaw poprawne GITEA_REPOSITORY=owner/repository." >&2; exit 1; }
auth_args=()
if [[ -n "${GITEA_TOKEN:-}" ]]; then
auth_args=(-H "Authorization: token $GITEA_TOKEN")
else
credential="$(printf 'protocol=%s\nhost=%s\n\n' "$detected_protocol" "$detected_host" | git credential fill)"
gitea_username="$(printf '%s\n' "$credential" | sed -n 's/^username=//p')"
gitea_password="$(printf '%s\n' "$credential" | sed -n 's/^password=//p')"
[[ -n "$gitea_username" && -n "$gitea_password" ]] || {
echo "Brak danych Gitea. Ustaw GITEA_TOKEN albo skonfiguruj git credential helper." >&2
exit 1
}
auth_args=(-u "$gitea_username:$gitea_password")
fi
api_base="$gitea_base_url/api/v1/repos/$gitea_repository"
preflight_status="$(curl -sS "${auth_args[@]}" -o /dev/null -w '%{http_code}' "$api_base")"
[[ "$preflight_status" == "200" ]] || {
echo "Brak dostępu do repozytorium Gitea $gitea_repository (HTTP $preflight_status)." >&2
exit 1
}
read_version() {
local code name
code="$(sed -nE 's/^[[:space:]]*versionCode = ([0-9]+).*$/\1/p' "$VERSION_FILE" | head -n 1)"
name="$(sed -nE 's/^[[:space:]]*versionName = "([^"]+)".*$/\1/p' "$VERSION_FILE" | head -n 1)"
[[ "$code" =~ ^[0-9]+$ ]] || { echo "Nie można odczytać versionCode." >&2; return 1; }
[[ "$name" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "versionName musi mieć format major.minor.patch." >&2; return 1; }
printf '%s %s\n' "$code" "$name"
}
read -r current_code current_name < <(read_version)
if ((REUSE_CURRENT == 1)); then
next_code="$current_code"
next_name="$current_name"
else
IFS=. read -r version_major version_minor version_patch <<<"$current_name"
next_code=$((current_code + 1))
next_name="${version_major}.${version_minor}.$((version_patch + 1))"
fi
tag_name="v${next_name}-${next_code}"
artifact_dir="$ROOT_DIR/release-artifacts/$tag_name"
gitea_apk_name="TPP-Kierowca-${next_name}-${next_code}.apk"
play_aab_name="TPP-Kierowca-${next_name}-${next_code}.aab"
cleanup_on_exit() {
local exit_code=$?
if ((RELEASE_SUCCEEDED == 0 && VERSION_FILE_CHANGED == 1 && VERSION_COMMITTED == 0)) && [[ -n "$VERSION_BACKUP" && -f "$VERSION_BACKUP" ]]; then
cp "$VERSION_BACKUP" "$VERSION_FILE"
echo "Przywrócono poprzednią wersję w app/build.gradle.kts." >&2
fi
[[ -n "$VERSION_BACKUP" && -f "$VERSION_BACKUP" ]] && rm -f "$VERSION_BACKUP"
if ((RELEASE_SUCCEEDED == 0)); then
echo "Release nie został ukończony. Po usunięciu przyczyny użyj --reuse-current, jeśli commit wersji już powstał." >&2
fi
trap - EXIT INT TERM
exit "$exit_code"
}
trap cleanup_on_exit EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
if ((REUSE_CURRENT == 0)); then
if git show-ref --verify --quiet "refs/tags/$tag_name" || git ls-remote --exit-code --tags "$REMOTE_NAME" "refs/tags/$tag_name" >/dev/null 2>&1; then
echo "Tag $tag_name już istnieje. Użyj --reuse-current albo zwiększ wersję ręcznie." >&2
exit 1
fi
VERSION_BACKUP="$(mktemp)"
cp "$VERSION_FILE" "$VERSION_BACKUP"
perl -0pi -e "s/versionCode = \\d+/versionCode = $next_code/" "$VERSION_FILE"
perl -0pi -e "s/versionName = \"[^\"]+\"/versionName = \"$next_name\"/" "$VERSION_FILE"
VERSION_FILE_CHANGED=1
read -r written_code written_name < <(read_version)
[[ "$written_code" == "$next_code" && "$written_name" == "$next_name" ]] || {
echo "Nie udało się jednoznacznie zapisać nowej wersji." >&2
exit 1
}
fi
echo "Buduję TPP Kierowca $next_name ($next_code)..."
gradle_tasks=(assembleRelease bundleRelease)
if ((SKIP_TESTS == 0)); then
gradle_tasks=(testDebugUnitTest "${gradle_tasks[@]}")
fi
./gradlew "${gradle_tasks[@]}"
apk_source="$(find app/build/outputs/apk/release -maxdepth 1 -type f -name '*.apk' -print -quit)"
aab_source="$(find app/build/outputs/bundle/release -maxdepth 1 -type f -name '*.aab' -print -quit)"
[[ -n "$apk_source" && -f "$apk_source" ]] || { echo "Nie znaleziono release APK." >&2; exit 1; }
[[ -n "$aab_source" && -f "$aab_source" ]] || { echo "Nie znaleziono release AAB." >&2; exit 1; }
sdk_dir="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
if [[ -z "$sdk_dir" && -f local.properties ]]; then
sdk_dir="$(sed -n 's/^sdk.dir=//p' local.properties | head -n 1)"
fi
apksigner="$(find "$sdk_dir/build-tools" -type f -name apksigner 2>/dev/null | sort -V | tail -n 1)"
aapt="$(find "$sdk_dir/build-tools" -type f -name aapt 2>/dev/null | sort -V | tail -n 1)"
[[ -x "$apksigner" && -x "$aapt" ]] || { echo "Nie znaleziono apksigner/aapt w Android SDK." >&2; exit 1; }
"$apksigner" verify --verbose "$apk_source"
apk_badging="$("$aapt" dump badging "$apk_source" | head -n 1)"
[[ "$apk_badging" == *"versionCode='$next_code'"* && "$apk_badging" == *"versionName='$next_name'"* ]] || {
echo "APK ma inny numer wersji niż oczekiwany $next_name ($next_code)." >&2
exit 1
}
jarsigner -verify "$aab_source" >/dev/null
rm -rf "$artifact_dir"
mkdir -p "$artifact_dir/gitea" "$artifact_dir/google-play"
cp "$apk_source" "$artifact_dir/gitea/$gitea_apk_name"
cp "$aab_source" "$artifact_dir/google-play/$play_aab_name"
(
cd "$artifact_dir"
sha256sum "gitea/$gitea_apk_name" "google-play/$play_aab_name" > SHA256SUMS.txt
)
if ((REUSE_CURRENT == 0)); then
git add "$VERSION_FILE"
git diff --cached --check
git commit -m "chore: release android driver $next_name ($next_code)"
VERSION_COMMITTED=1
fi
git push "$REMOTE_NAME" "$branch"
if ! git show-ref --verify --quiet "refs/tags/$tag_name"; then
git tag -a "$tag_name" -m "TPP Kierowca $next_name ($next_code)"
fi
tag_commit="$(git rev-list -n 1 "$tag_name")"
head_commit="$(git rev-parse HEAD)"
[[ "$tag_commit" == "$head_commit" ]] || {
echo "Tag $tag_name nie wskazuje aktualnego commita ($head_commit)." >&2
exit 1
}
git push "$REMOTE_NAME" "$tag_name"
if [[ -n "$NOTES_FILE" ]]; then
[[ -f "$NOTES_FILE" ]] || { echo "Nie istnieje plik release notes: $NOTES_FILE" >&2; exit 1; }
release_notes="$(<"$NOTES_FILE")"
else
previous_tag="$(git tag --sort=-version:refname | grep -Fxv "$tag_name" | head -n 1 || true)"
if [[ -n "$previous_tag" ]]; then
changes="$(git log --format='- %s (%h)' "$previous_tag..HEAD")"
else
changes="$(git log -20 --format='- %s (%h)')"
fi
release_notes="TPP Kierowca $next_name ($next_code)
Zmiany:
${changes:-'- Wydanie techniczne.'}
Artefakt Google Play Console (AAB) znajduje się lokalnie w:
release-artifacts/$tag_name/google-play/$play_aab_name"
fi
release_response="$(mktemp)"
release_status="$(curl -sS "${auth_args[@]}" -o "$release_response" -w '%{http_code}' "$api_base/releases/tags/$tag_name")"
if [[ "$release_status" == "200" ]]; then
release_id="$(jq -r '.id' "$release_response")"
elif [[ "$release_status" == "404" ]]; then
release_payload="$(jq -n \
--arg tag "$tag_name" \
--arg target "$branch" \
--arg name "TPP Kierowca $next_name ($next_code)" \
--arg body "$release_notes" \
'{tag_name:$tag,target_commitish:$target,name:$name,body:$body,draft:false,prerelease:false}')"
release_status="$(curl -sS "${auth_args[@]}" \
-H 'Content-Type: application/json' \
-o "$release_response" \
-w '%{http_code}' \
-X POST \
-d "$release_payload" \
"$api_base/releases")"
[[ "$release_status" == "201" ]] || {
echo "Nie udało się utworzyć Gitea Release (HTTP $release_status)." >&2
jq '{message,errors}' "$release_response" >&2 || true
exit 1
}
release_id="$(jq -r '.id' "$release_response")"
else
echo "Nie udało się sprawdzić Gitea Release (HTTP $release_status)." >&2
exit 1
fi
upload_asset() {
local file_path="$1"
local asset_name="$2"
local mime_type="$3"
local assets_response existing_asset_id upload_response upload_status
assets_response="$(mktemp)"
curl -fsS "${auth_args[@]}" -o "$assets_response" "$api_base/releases/$release_id/assets"
existing_asset_id="$(jq -r --arg name "$asset_name" '.[] | select(.name == $name) | .id' "$assets_response" | head -n 1)"
if [[ -n "$existing_asset_id" ]]; then
curl -fsS "${auth_args[@]}" -X DELETE "$api_base/releases/$release_id/assets/$existing_asset_id" >/dev/null
fi
upload_response="$(mktemp)"
upload_status="$(curl -sS "${auth_args[@]}" \
-o "$upload_response" \
-w '%{http_code}' \
-X POST \
-F "attachment=@$file_path;type=$mime_type" \
"$api_base/releases/$release_id/assets?name=$asset_name")"
[[ "$upload_status" == "201" ]] || {
echo "Nie udało się wysłać $asset_name (HTTP $upload_status)." >&2
jq '{message,errors}' "$upload_response" >&2 || true
return 1
}
}
upload_asset "$artifact_dir/gitea/$gitea_apk_name" "$gitea_apk_name" "application/vnd.android.package-archive"
upload_asset "$artifact_dir/SHA256SUMS.txt" "SHA256SUMS.txt" "text/plain"
assets_response="$(mktemp)"
curl -fsS "${auth_args[@]}" -o "$assets_response" "$api_base/releases/$release_id/assets"
published_apk_url="$(jq -r --arg name "$gitea_apk_name" '.[] | select(.name == $name) | .browser_download_url' "$assets_response" | head -n 1)"
[[ -n "$published_apk_url" && "$published_apk_url" != "null" ]] || {
echo "Nie znaleziono opublikowanego APK w Gitea Release." >&2
exit 1
}
downloaded_apk="$(mktemp)"
curl -fsSL "${auth_args[@]}" -o "$downloaded_apk" "$published_apk_url"
local_apk_hash="$(sha256sum "$artifact_dir/gitea/$gitea_apk_name" | awk '{print $1}')"
published_apk_hash="$(sha256sum "$downloaded_apk" | awk '{print $1}')"
[[ "$local_apk_hash" == "$published_apk_hash" ]] || {
echo "Checksum opublikowanego APK nie zgadza się z lokalnym plikiem." >&2
exit 1
}
release_url="$gitea_base_url/$gitea_repository/releases/tag/$tag_name"
RELEASE_SUCCEEDED=1
trap - EXIT INT TERM
[[ -n "$VERSION_BACKUP" ]] && rm -f "$VERSION_BACKUP"
cat <<EOF
Release zakończony:
Wersja: $next_name ($next_code)
Tag: $tag_name
Gitea: $release_url
APK: $artifact_dir/gitea/$gitea_apk_name
Google Play: $artifact_dir/google-play/$play_aab_name
Checksumy: $artifact_dir/SHA256SUMS.txt
EOF