Compare commits

...
8 changed files with 545 additions and 25 deletions
+2
View File
@@ -9,3 +9,5 @@ keystore.properties
*.jks
release-artifacts/
app/src/main/res_bak/
scripts/*.json
scripts/*.json:Zone.Identifier
+45 -3
View File
@@ -31,9 +31,51 @@ Przed wydaniem zapisz wszystkie zmiany źródłowe w commicie. Następnie urucho
```
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>/`.
buduje podpisany APK oraz AAB, a po poprawnym buildzie pyta, czy wysłać aktualizację do
Google Play. Po odpowiedzi „tak” publikuje AAB w skonfigurowanym teście zamkniętym i
wysyła zmiany do weryfikacji. Następnie tworzy commit wersji, tag, push i Gitea Release.
Artefakty zostają również zapisane lokalnie w `release-artifacts/<tag>/`.
Do publikacji Google Play konto serwisowe musi mieć dla aplikacji
`pl.firmatpp.kierowca` uprawnienia „Wyświetlanie informacji o aplikacji (tylko do odczytu)”
oraz „Publikowanie aplikacji na ścieżkach testowych”. Pobrany klucz JSON umieść
bezpośrednio w folderze `scripts`, na przykład:
```text
scripts/google-play-service-account.json
```
Skrypt sam znajduje jedyny plik `scripts/*.json`, sprawdza, czy jest prawidłowym kluczem
konta serwisowego, i używa go do autoryzacji. Pliki `scripts/*.json` są ignorowane przez
Git i nie mogą zostać przypadkowo dodane do repozytorium. Jeśli w folderze znajduje się
więcej niż jeden prawidłowy klucz, skrypt przerwie działanie i poprosi o pozostawienie
jednego.
`GOOGLE_PLAY_TRACK` musi być identyfikatorem istniejącego testu zamkniętego. Domyślna
wartość to `alpha`. Opcjonalnie można ustawić `GOOGLE_PLAY_PACKAGE_NAME` i
`GOOGLE_PLAY_RELEASE_LANGUAGE` (domyślnie `pl-PL`). Zmienna
`GOOGLE_PLAY_SERVICE_ACCOUNT_CREDENTIALS` pozostaje opcjonalnym sposobem wskazania klucza
spoza folderu `scripts`. Skrypt nie zapisuje tokenu ani klucza w repozytorium; prywatny
klucz tymczasowy jest usuwany po zakończeniu.
Przed zatwierdzeniem skrypt waliduje edycję Google Play. Commit edycji jawnie wysyła
zmiany do weryfikacji. Jeżeli inne zmiany są już sprawdzane, skrypt nie anuluje ich —
kończy się błędem i można go wznowić po zakończeniu poprzedniej weryfikacji.
Jeżeli na koncie jest włączone publikowanie zarządzane, po akceptacji Google nadal trzeba
wybrać „Opublikuj zmiany” w Play Console; bez publikowania zarządzanego zaakceptowane
wydanie zostanie udostępnione testerom automatycznie.
Awaryjne wydanie wyłącznie do Gitea, bez Google Play:
```bash
./scripts/release-android.sh --skip-google-play
```
W środowisku bez interaktywnego terminala decyzję trzeba podać jawnie:
```bash
./scripts/release-android.sh --publish-google-play
```
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ć:
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId = "pl.firmatpp.kierowca"
minSdk = 26
targetSdk = 35
versionCode = 114
versionName = "1.0.61"
versionCode = 116
versionName = "1.0.63"
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -67,9 +67,11 @@ class RoutePointWorker(
"retryable" to error.retryable,
),
)
if (shouldStopRouteTrackingAfterPointError(error.kind, error.code)) {
ActiveRouteTrackingService.stop(applicationContext, routeId)
}
if (isInactiveRoutePointError(error.code)) {
dao.delete(pointIds)
ActiveRouteTrackingService.stop(applicationContext, routeId)
continue
}
@@ -105,3 +107,6 @@ class RoutePointWorker(
}
internal fun isInactiveRoutePointError(code: String?): Boolean = code == "ROUTE_POINTS_INVALID_STATUS"
internal fun shouldStopRouteTrackingAfterPointError(kind: ApiErrorKind, code: String?): Boolean =
kind == ApiErrorKind.Auth || isInactiveRoutePointError(code)
@@ -136,6 +136,13 @@ class ActiveRouteTrackingService : Service(), LocationListener {
override fun onDestroy() {
runCatching { locationManager.removeUpdates(this) }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
@Suppress("DEPRECATION")
stopForeground(true)
}
getSystemService(NotificationManager::class.java).cancel(NOTIFICATION_ID)
getSharedPreferences(TRACKING_PREFERENCES, Context.MODE_PRIVATE)
.edit()
.remove(ACTIVE_ROUTE_ID)
@@ -463,15 +463,14 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
}.onFailure { throwable ->
if (generation != routesRequestGeneration) return@onFailure
reportHandledException("load_routes", throwable, mapOf("date" to date))
_state.update {
val apiError = ApiErrorMapper.map(throwable)
it.withApiError(throwable).copy(
screen = when {
apiError.kind == ApiErrorKind.Auth -> DriverScreen.Phone
it.screen == DriverScreen.Initializing -> DriverScreen.Routes
else -> it.screen
},
)
if (ApiErrorMapper.map(throwable).kind == ApiErrorKind.Auth) {
handleExpiredSession()
} else {
_state.update {
it.withApiError(throwable).copy(
screen = if (it.screen == DriverScreen.Initializing) DriverScreen.Routes else it.screen,
)
}
}
}
@@ -548,7 +547,12 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
}
}
.onFailure { throwable ->
if (generation == routeDetailRequestGeneration) _state.update { it.withApiError(throwable) }
if (generation != routeDetailRequestGeneration) return@onFailure
if (ApiErrorMapper.map(throwable).kind == ApiErrorKind.Auth) {
handleExpiredSession()
} else {
_state.update { it.withApiError(throwable) }
}
}
if (generation == routeDetailRequestGeneration) _state.update { it.copy(refreshing = false) }
@@ -1455,6 +1459,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
dispatchSheetUploadsJob?.cancel()
realtimeBannerJob?.cancel()
liveSyncClient.stop()
ActiveRouteTrackingService.stop(getApplication())
offlineOutboxManager.clearAll()
repository.logout(notifyServer = _state.value.isOnline)
syncRepository.clearCache()
@@ -1665,6 +1670,7 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
private suspend fun handleExpiredSession() {
liveSyncClient.stop()
ActiveRouteTrackingService.stop(getApplication())
offlineOutboxManager.clearAll()
repository.logout(notifyServer = false)
syncRepository.clearCache()
@@ -3,6 +3,7 @@ package pl.firmatpp.kierowca.data.upload
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import pl.firmatpp.kierowca.data.ApiErrorKind
class RoutePointWorkerRulesTest {
@Test
@@ -15,4 +16,14 @@ class RoutePointWorkerRulesTest {
assertFalse(isInactiveRoutePointError("SERVER_UNAVAILABLE"))
assertFalse(isInactiveRoutePointError(null))
}
@Test
fun revokedSessionStopsRouteTracking() {
assertTrue(shouldStopRouteTrackingAfterPointError(ApiErrorKind.Auth, null))
}
@Test
fun temporaryFailureKeepsRouteTrackingActive() {
assertFalse(shouldStopRouteTrackingAfterPointError(ApiErrorKind.Server, "SERVER_UNAVAILABLE"))
}
}
+456 -9
View File
@@ -7,11 +7,17 @@ VERSION_FILE="$ROOT_DIR/app/build.gradle.kts"
REMOTE_NAME="${GIT_REMOTE:-origin}"
NOTES_FILE=""
SKIP_TESTS=0
GOOGLE_PLAY_MODE="ask"
REUSE_CURRENT=0
VERSION_FILE_CHANGED=0
VERSION_COMMITTED=0
VERSION_BACKUP=""
RELEASE_SUCCEEDED=0
GOOGLE_PLAY_ACCESS_TOKEN=""
GOOGLE_PLAY_EDIT_ID=""
GOOGLE_PLAY_TEMP_DIR=""
GOOGLE_PLAY_CURRENT_VERSION_STATE=""
GOOGLE_PLAY_SUMMARY="pominięto"
usage() {
cat <<'EOF'
@@ -21,13 +27,17 @@ 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,
- buduje podpisany AAB i publikuje go w teście zamkniętym Google Play,
- wysyła zmiany Google Play do weryfikacji,
- 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.
--skip-google-play Nie pytaj i pomiń wysyłanie AAB do Google Play.
--publish-google-play
Nie pytaj i wyślij AAB do Google Play.
--reuse-current Nie zwiększaj wersji. Wznów publikację wersji zapisanej
obecnie w app/build.gradle.kts.
-h, --help Pokaż pomoc.
@@ -37,9 +47,19 @@ Zmienne środowiskowe:
GITEA_BASE_URL Nadpisuje adres Gitea, np. https://gitea.example.com.
GITEA_REPOSITORY Nadpisuje owner/repository.
GIT_REMOTE Remote Git, domyślnie origin.
GOOGLE_PLAY_SERVICE_ACCOUNT_CREDENTIALS
Opcjonalne nadpisanie automatycznie znalezionego klucza JSON.
Domyślnie skrypt znajduje konto serwisowe w scripts/*.json.
Alternatywnie: GOOGLE_APPLICATION_CREDENTIALS.
GOOGLE_PLAY_PACKAGE_NAME
Identyfikator aplikacji, domyślnie pl.firmatpp.kierowca.
GOOGLE_PLAY_TRACK Nazwa testu zamkniętego, domyślnie alpha.
GOOGLE_PLAY_RELEASE_LANGUAGE
Język informacji o wersji, domyślnie pl-PL.
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.
Google Play otrzymuje AAB przez Android Publisher API. Konto serwisowe musi mieć
uprawnienie „Publikowanie aplikacji na ścieżkach testowych”.
EOF
}
@@ -54,6 +74,14 @@ while (($# > 0)); do
SKIP_TESTS=1
shift
;;
--skip-google-play)
GOOGLE_PLAY_MODE="skip"
shift
;;
--publish-google-play)
GOOGLE_PLAY_MODE="publish"
shift
;;
--reuse-current)
REUSE_CURRENT=1
shift
@@ -82,6 +110,10 @@ 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; }
if [[ -n "$NOTES_FILE" && ! -f "$NOTES_FILE" ]]; then
echo "Nie istnieje plik release notes: $NOTES_FILE" >&2
exit 1
fi
branch="$(git branch --show-current)"
[[ -n "$branch" ]] || { echo "Release nie może być wykonany z detached HEAD." >&2; exit 1; }
@@ -165,11 +197,24 @@ play_aab_name="TPP-Kierowca-${next_name}-${next_code}.aab"
cleanup_on_exit() {
local exit_code=$?
if ((RELEASE_SUCCEEDED == 0)) && [[ -n "$GOOGLE_PLAY_EDIT_ID" && -n "$GOOGLE_PLAY_ACCESS_TOKEN" ]]; then
package_encoded="$(urlencode "$google_play_package")"
edit_encoded="$(urlencode "$GOOGLE_PLAY_EDIT_ID")"
curl -sS -o /dev/null \
--connect-timeout 20 \
--max-time 60 \
-X DELETE \
-H "@$GOOGLE_PLAY_TEMP_DIR/auth-header.txt" \
"https://androidpublisher.googleapis.com/androidpublisher/v3/applications/$package_encoded/edits/$edit_encoded" || true
fi
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 [[ -n "$GOOGLE_PLAY_TEMP_DIR" && -d "$GOOGLE_PLAY_TEMP_DIR" ]]; then
rm -rf -- "$GOOGLE_PLAY_TEMP_DIR"
fi
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
@@ -180,6 +225,391 @@ trap cleanup_on_exit EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
urlencode() {
jq -rn --arg value "$1" '$value | @uri'
}
base64url() {
openssl base64 -A | tr '+/' '-_' | tr -d '='
}
show_google_play_error() {
local response_file="$1"
jq '.error | {code, message, status, details}' "$response_file" >&2 2>/dev/null || true
}
decide_google_play_upload() {
local answer
case "$GOOGLE_PLAY_MODE" in
publish)
return 0
;;
skip)
GOOGLE_PLAY_SUMMARY="pominięto (--skip-google-play)"
return 0
;;
ask)
if [[ ! -t 0 ]]; then
echo "Nie można zapytać o publikację Google Play bez interaktywnego terminala." >&2
echo "Użyj --publish-google-play albo --skip-google-play." >&2
return 1
fi
while true; do
printf "\nBuild %s (%s) zakończony poprawnie. Czy wysłać aktualizację do Google Play? [t/N] " "$next_name" "$next_code"
if ! IFS= read -r answer; then
echo "Nie udało się odczytać odpowiedzi." >&2
return 1
fi
case "${answer,,}" in
t|tak|y|yes)
GOOGLE_PLAY_MODE="publish"
break
;;
""|n|nie|no)
GOOGLE_PLAY_MODE="skip"
GOOGLE_PLAY_SUMMARY="pominięto przez użytkownika"
break
;;
*)
echo "Nie rozpoznano odpowiedzi. Wpisz tak albo nie." >&2
;;
esac
done
;;
*)
echo "Niepoprawny tryb publikacji Google Play: $GOOGLE_PLAY_MODE" >&2
return 1
;;
esac
}
prepare_google_play() {
local credentials_path client_email now expires header payload unsigned_jwt signature assertion previous_umask
local token_response token_status preflight_response preflight_status package_encoded track_encoded
local candidate
local -a credentials_candidates=()
google_play_package="${GOOGLE_PLAY_PACKAGE_NAME:-pl.firmatpp.kierowca}"
google_play_track="${GOOGLE_PLAY_TRACK:-alpha}"
google_play_release_language="${GOOGLE_PLAY_RELEASE_LANGUAGE:-pl-PL}"
credentials_path="${GOOGLE_PLAY_SERVICE_ACCOUNT_CREDENTIALS:-${GOOGLE_APPLICATION_CREDENTIALS:-}}"
if [[ -z "$credentials_path" ]]; then
while IFS= read -r -d '' candidate; do
if jq -e '
.type == "service_account"
and (.client_email | type == "string" and length > 0)
and (.private_key | type == "string" and length > 0)
' "$candidate" >/dev/null 2>&1; then
credentials_candidates+=("$candidate")
fi
done < <(find "$ROOT_DIR/scripts" -maxdepth 1 -type f -name '*.json' -print0)
case "${#credentials_candidates[@]}" in
0)
echo "Nie znaleziono klucza konta serwisowego w $ROOT_DIR/scripts/*.json." >&2
echo "Umieść dokładnie jeden poprawny plik JSON konta serwisowego w folderze scripts." >&2
return 1
;;
1)
credentials_path="${credentials_candidates[0]}"
;;
*)
echo "Znaleziono więcej niż jeden klucz konta serwisowego w folderze scripts:" >&2
for candidate in "${credentials_candidates[@]}"; do
printf ' - %s\n' "$(basename "$candidate")" >&2
done
echo "Pozostaw jeden plik albo ustaw GOOGLE_PLAY_SERVICE_ACCOUNT_CREDENTIALS." >&2
return 1
;;
esac
fi
command -v openssl >/dev/null 2>&1 || {
echo "Brak wymaganego polecenia: openssl" >&2
return 1
}
[[ "$google_play_package" =~ ^[A-Za-z0-9._]+$ ]] || {
echo "Niepoprawne GOOGLE_PLAY_PACKAGE_NAME: $google_play_package" >&2
return 1
}
[[ -n "$google_play_track" ]] || { echo "GOOGLE_PLAY_TRACK nie może być puste." >&2; return 1; }
[[ "$google_play_release_language" =~ ^[A-Za-z0-9-]+$ ]] || {
echo "Niepoprawne GOOGLE_PLAY_RELEASE_LANGUAGE: $google_play_release_language" >&2
return 1
}
[[ -n "$credentials_path" ]] || {
echo "Nie wskazano ani nie znaleziono klucza konta serwisowego Google Play." >&2
return 1
}
[[ -r "$credentials_path" ]] || {
echo "Nie można odczytać klucza konta serwisowego: $credentials_path" >&2
return 1
}
GOOGLE_PLAY_TEMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/tpp-google-play.XXXXXX")"
chmod 700 "$GOOGLE_PLAY_TEMP_DIR"
client_email="$(jq -er '.client_email | select(type == "string" and length > 0)' "$credentials_path")" || {
echo "Klucz Google Play nie zawiera client_email." >&2
return 1
}
previous_umask="$(umask)"
umask 077
if ! jq -er '.private_key | select(type == "string" and length > 0)' "$credentials_path" > "$GOOGLE_PLAY_TEMP_DIR/private-key.pem"; then
umask "$previous_umask"
echo "Klucz Google Play nie zawiera private_key." >&2
return 1
fi
umask "$previous_umask"
now="$(date +%s)"
expires=$((now + 3600))
header="$(printf '%s' '{"alg":"RS256","typ":"JWT"}' | base64url)"
payload="$(jq -cn \
--arg iss "$client_email" \
--arg scope 'https://www.googleapis.com/auth/androidpublisher' \
--arg aud 'https://oauth2.googleapis.com/token' \
--argjson iat "$now" \
--argjson exp "$expires" \
'{iss:$iss,scope:$scope,aud:$aud,iat:$iat,exp:$exp}' | base64url)"
unsigned_jwt="$header.$payload"
signature="$(printf '%s' "$unsigned_jwt" \
| openssl dgst -sha256 -sign "$GOOGLE_PLAY_TEMP_DIR/private-key.pem" \
| base64url)"
assertion="$unsigned_jwt.$signature"
printf '%s' "$assertion" > "$GOOGLE_PLAY_TEMP_DIR/assertion.txt"
chmod 600 "$GOOGLE_PLAY_TEMP_DIR/assertion.txt"
token_response="$GOOGLE_PLAY_TEMP_DIR/token.json"
token_status="$(curl -sS \
--connect-timeout 20 \
--max-time 60 \
-o "$token_response" \
-w '%{http_code}' \
-X POST \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' \
--data-urlencode "assertion@$GOOGLE_PLAY_TEMP_DIR/assertion.txt" \
'https://oauth2.googleapis.com/token')"
[[ "$token_status" == "200" ]] || {
echo "Nie udało się pobrać tokenu Google Play (HTTP $token_status)." >&2
jq '{error, error_description}' "$token_response" >&2 2>/dev/null || true
return 1
}
GOOGLE_PLAY_ACCESS_TOKEN="$(jq -er '.access_token | select(type == "string" and length > 0)' "$token_response")" || {
echo "Google OAuth nie zwrócił access_token." >&2
return 1
}
printf 'Authorization: Bearer %s\n' "$GOOGLE_PLAY_ACCESS_TOKEN" > "$GOOGLE_PLAY_TEMP_DIR/auth-header.txt"
chmod 600 "$GOOGLE_PLAY_TEMP_DIR/auth-header.txt"
rm -f "$GOOGLE_PLAY_TEMP_DIR/private-key.pem" "$GOOGLE_PLAY_TEMP_DIR/assertion.txt" "$token_response"
package_encoded="$(urlencode "$google_play_package")"
track_encoded="$(urlencode "$google_play_track")"
preflight_response="$GOOGLE_PLAY_TEMP_DIR/preflight.json"
preflight_status="$(curl -sS \
--connect-timeout 20 \
--max-time 60 \
-o "$preflight_response" \
-w '%{http_code}' \
-H "@$GOOGLE_PLAY_TEMP_DIR/auth-header.txt" \
"https://androidpublisher.googleapis.com/androidpublisher/v3/applications/$package_encoded/tracks/$track_encoded/releases")"
[[ "$preflight_status" == "200" ]] || {
echo "Brak dostępu do ścieżki Google Play $google_play_track dla $google_play_package (HTTP $preflight_status)." >&2
show_google_play_error "$preflight_response"
return 1
}
GOOGLE_PLAY_CURRENT_VERSION_STATE="$(jq -r --argjson code "$next_code" '
.releases[]?
| select(any(.activeArtifacts[]?; .versionCode == $code))
| .releaseLifecycleState // empty
' "$preflight_response" | sed -n '1p')"
if [[ -n "$GOOGLE_PLAY_CURRENT_VERSION_STATE" && "$REUSE_CURRENT" == "0" ]]; then
echo "Google Play ma już versionCode $next_code na ścieżce $google_play_track ($GOOGLE_PLAY_CURRENT_VERSION_STATE)." >&2
echo "Zwiększ wersję lokalną albo świadomie użyj --reuse-current." >&2
return 1
fi
if [[ "$GOOGLE_PLAY_CURRENT_VERSION_STATE" == "RELEASE_LIFECYCLE_STATE_NOT_APPROVED" ]]; then
echo "Wersja $next_code została odrzucona w Google Play. Zwiększ versionCode i utwórz nowe wydanie." >&2
return 1
fi
echo "Google Play: używam klucza $(basename "$credentials_path")."
echo "Google Play: zweryfikowano dostęp do $google_play_package, ścieżka $google_play_track."
}
publish_to_google_play() {
local release_notes="$1"
local package_encoded track_encoded edit_encoded api_base
local response status bundle_version_code existing_sha256 local_sha256
local play_notes track_payload release_name
case "$GOOGLE_PLAY_CURRENT_VERSION_STATE" in
RELEASE_LIFECYCLE_STATE_IN_REVIEW|RELEASE_LIFECYCLE_STATE_APPROVED_NOT_PUBLISHED|RELEASE_LIFECYCLE_STATE_PUBLISHED)
GOOGLE_PLAY_SUMMARY="już istnieje: $GOOGLE_PLAY_CURRENT_VERSION_STATE"
echo "Google Play: wersja $next_name ($next_code) ma już stan $GOOGLE_PLAY_CURRENT_VERSION_STATE; pomijam ponowną publikację."
return 0
;;
esac
package_encoded="$(urlencode "$google_play_package")"
track_encoded="$(urlencode "$google_play_track")"
api_base="https://androidpublisher.googleapis.com/androidpublisher/v3/applications/$package_encoded"
response="$GOOGLE_PLAY_TEMP_DIR/edit-create.json"
status="$(curl -sS \
--connect-timeout 20 \
--max-time 60 \
-o "$response" \
-w '%{http_code}' \
-X POST \
-H "@$GOOGLE_PLAY_TEMP_DIR/auth-header.txt" \
-H 'Content-Type: application/json' \
-d '{}' \
"$api_base/edits")"
[[ "$status" == "200" ]] || {
echo "Nie udało się utworzyć edycji Google Play (HTTP $status)." >&2
show_google_play_error "$response"
return 1
}
GOOGLE_PLAY_EDIT_ID="$(jq -er '.id | select(type == "string" and length > 0)' "$response")" || {
echo "Google Play nie zwrócił identyfikatora edycji." >&2
return 1
}
edit_encoded="$(urlencode "$GOOGLE_PLAY_EDIT_ID")"
response="$GOOGLE_PLAY_TEMP_DIR/track-get.json"
status="$(curl -sS \
--connect-timeout 20 \
--max-time 60 \
-o "$response" \
-w '%{http_code}' \
-H "@$GOOGLE_PLAY_TEMP_DIR/auth-header.txt" \
"$api_base/edits/$edit_encoded/tracks/$track_encoded")"
[[ "$status" == "200" ]] || {
echo "Nie znaleziono ścieżki Google Play $google_play_track (HTTP $status)." >&2
show_google_play_error "$response"
return 1
}
response="$GOOGLE_PLAY_TEMP_DIR/bundles.json"
status="$(curl -sS \
--connect-timeout 20 \
--max-time 60 \
-o "$response" \
-w '%{http_code}' \
-H "@$GOOGLE_PLAY_TEMP_DIR/auth-header.txt" \
"$api_base/edits/$edit_encoded/bundles")"
[[ "$status" == "200" ]] || {
echo "Nie udało się sprawdzić pakietów Google Play (HTTP $status)." >&2
show_google_play_error "$response"
return 1
}
existing_sha256="$(jq -r --argjson code "$next_code" '.bundles[]? | select(.versionCode == $code) | .sha256 // empty' "$response" | sed -n '1p')"
local_sha256="$(sha256sum "$artifact_dir/google-play/$play_aab_name" | awk '{print $1}')"
if [[ -n "$existing_sha256" ]]; then
if [[ "${existing_sha256,,}" != "${local_sha256,,}" && "$REUSE_CURRENT" == "0" ]]; then
echo "Google Play ma już versionCode $next_code z inną sumą SHA-256." >&2
return 1
fi
if [[ "${existing_sha256,,}" != "${local_sha256,,}" ]]; then
echo "Uwaga: lokalny AAB ma inną sumę niż istniejący versionCode $next_code; --reuse-current zachowuje pakiet już wysłany do Google Play." >&2
fi
bundle_version_code="$next_code"
echo "Google Play ma już AAB $next_name ($next_code); pomijam ponowny upload."
else
response="$GOOGLE_PLAY_TEMP_DIR/bundle-upload.json"
status="$(curl -sS \
--connect-timeout 20 \
--max-time 600 \
-o "$response" \
-w '%{http_code}' \
-X POST \
-H "@$GOOGLE_PLAY_TEMP_DIR/auth-header.txt" \
-H 'Content-Type: application/octet-stream' \
--data-binary "@$artifact_dir/google-play/$play_aab_name" \
"https://androidpublisher.googleapis.com/upload/androidpublisher/v3/applications/$package_encoded/edits/$edit_encoded/bundles?uploadType=media")"
[[ "$status" == "200" ]] || {
echo "Nie udało się wysłać AAB do Google Play (HTTP $status)." >&2
show_google_play_error "$response"
return 1
}
bundle_version_code="$(jq -er '.versionCode' "$response")" || {
echo "Google Play nie zwrócił versionCode przesłanego AAB." >&2
return 1
}
[[ "$bundle_version_code" == "$next_code" ]] || {
echo "Google Play odczytał versionCode $bundle_version_code zamiast $next_code." >&2
return 1
}
fi
play_notes="$(jq -rn --arg notes "$release_notes" '$notes | if length > 500 then .[0:497] + "..." else . end')"
release_name="TPP Kierowca $next_name ($next_code)"
track_payload="$(jq -n \
--arg track "$google_play_track" \
--arg release_name "$release_name" \
--arg version_code "$bundle_version_code" \
--arg language "$google_play_release_language" \
--arg notes "$play_notes" \
'{track:$track,releases:[{name:$release_name,versionCodes:[$version_code],status:"completed",releaseNotes:[{language:$language,text:$notes}]}]}')"
response="$GOOGLE_PLAY_TEMP_DIR/track-update.json"
status="$(curl -sS \
--connect-timeout 20 \
--max-time 60 \
-o "$response" \
-w '%{http_code}' \
-X PUT \
-H "@$GOOGLE_PLAY_TEMP_DIR/auth-header.txt" \
-H 'Content-Type: application/json' \
-d "$track_payload" \
"$api_base/edits/$edit_encoded/tracks/$track_encoded")"
[[ "$status" == "200" ]] || {
echo "Nie udało się przypisać AAB do ścieżki $google_play_track (HTTP $status)." >&2
show_google_play_error "$response"
return 1
}
response="$GOOGLE_PLAY_TEMP_DIR/edit-validate.json"
status="$(curl -sS \
--connect-timeout 20 \
--max-time 120 \
-o "$response" \
-w '%{http_code}' \
-X POST \
-H "@$GOOGLE_PLAY_TEMP_DIR/auth-header.txt" \
"$api_base/edits/$edit_encoded:validate")"
[[ "$status" == "200" ]] || {
echo "Walidacja edycji Google Play nie powiodła się (HTTP $status)." >&2
show_google_play_error "$response"
return 1
}
response="$GOOGLE_PLAY_TEMP_DIR/edit-commit.json"
status="$(curl -sS \
--connect-timeout 20 \
--max-time 120 \
-o "$response" \
-w '%{http_code}' \
-X POST \
-H "@$GOOGLE_PLAY_TEMP_DIR/auth-header.txt" \
"$api_base/edits/$edit_encoded:commit?changesNotSentForReview=false&changesInReviewBehavior=ERROR_IF_IN_REVIEW")"
[[ "$status" == "200" ]] || {
echo "Nie udało się zatwierdzić i wysłać zmian Google Play do weryfikacji (HTTP $status)." >&2
show_google_play_error "$response"
return 1
}
GOOGLE_PLAY_EDIT_ID=""
GOOGLE_PLAY_SUMMARY="wysłano do weryfikacji"
echo "Google Play: $release_name wysłano na ścieżkę $google_play_track i przekazano do weryfikacji."
}
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
@@ -235,14 +665,18 @@ cp "$aab_source" "$artifact_dir/google-play/$play_aab_name"
sha256sum "gitea/$gitea_apk_name" "google-play/$play_aab_name" > SHA256SUMS.txt
)
decide_google_play_upload
if [[ "$GOOGLE_PLAY_MODE" == "publish" ]]; then
prepare_google_play
fi
if ((REUSE_CURRENT == 0)); then
git add "$VERSION_FILE"
git diff --cached --check
git commit -m "chore: release android driver $next_name ($next_code)"
git commit -m "Wydaj aplikację kierowcy $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
@@ -252,10 +686,8 @@ head_commit="$(git rev-parse HEAD)"
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" | sed -n '1p' || true)"
@@ -264,7 +696,7 @@ else
else
changes="$(git log -20 --format='- %s (%h)')"
fi
release_notes="TPP Kierowca $next_name ($next_code)
release_notes="TPP Kierowca $next_name ($next_code)
Zmiany:
${changes:-'- Wydanie techniczne.'}
@@ -273,6 +705,15 @@ Artefakt Google Play Console (AAB) znajduje się lokalnie w:
release-artifacts/$tag_name/google-play/$play_aab_name"
fi
if [[ "$GOOGLE_PLAY_MODE" == "publish" ]]; then
publish_to_google_play "$release_notes"
GOOGLE_PLAY_ACCESS_TOKEN=""
rm -f "$GOOGLE_PLAY_TEMP_DIR/auth-header.txt"
fi
git push "$REMOTE_NAME" "$branch"
git push "$REMOTE_NAME" "$tag_name"
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
@@ -348,8 +789,12 @@ published_apk_hash="$(sha256sum "$downloaded_apk" | awk '{print $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"
if [[ -n "$GOOGLE_PLAY_TEMP_DIR" && -d "$GOOGLE_PLAY_TEMP_DIR" ]]; then
rm -rf -- "$GOOGLE_PLAY_TEMP_DIR"
GOOGLE_PLAY_TEMP_DIR=""
fi
trap - EXIT INT TERM
cat <<EOF
@@ -358,6 +803,8 @@ Release zakończony:
Tag: $tag_name
Gitea: $release_url
APK: $artifact_dir/gitea/$gitea_apk_name
Google Play: $artifact_dir/google-play/$play_aab_name
AAB: $artifact_dir/google-play/$play_aab_name
Google Play: $GOOGLE_PLAY_SUMMARY
Play track: ${google_play_track:-pominięto}
Checksumy: $artifact_dir/SHA256SUMS.txt
EOF