Bouwen-playstore.ps1: AAB naar de Play Console met versiecontrole (niet getest)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaFmzMN5i7qyZihKBsksxz
This commit is contained in:
jf
2026-10-20 07:58:09 +00:00
co-authored by Claude Sonnet 5.5
parent 3b7f0412b6
commit a3a0bcfc05
2 changed files with 213 additions and 0 deletions
+8
View File
@@ -330,3 +330,11 @@ meldingstoestemming wordt op het moment zelf gevraagd.
In SideKick (project MijnBesluit) heet het onderdeel `mijnbesluit-app`, met Sleutel `android`. SideKick zoekt voor zo'n onderdeel eerst `tools\Bouwen-<Sleutel>.ps1`; dat is hier `tools\Bouwen-android.ps1`, dat `tools\Apk-maken.ps1` aanroept en de ondertekende APK in `D:\MijnBesluit VS\MijnBesluitpublish\android` zet. Zonder dat script bouwt SideKick een Windows-programma (win-x64), en dat faalt op een Android-project met `NU1102 ... Mono.win-x64`. In SideKick (project MijnBesluit) heet het onderdeel `mijnbesluit-app`, met Sleutel `android`. SideKick zoekt voor zo'n onderdeel eerst `tools\Bouwen-<Sleutel>.ps1`; dat is hier `tools\Bouwen-android.ps1`, dat `tools\Apk-maken.ps1` aanroept en de ondertekende APK in `D:\MijnBesluit VS\MijnBesluitpublish\android` zet. Zonder dat script bouwt SideKick een Windows-programma (win-x64), en dat faalt op een Android-project met `NU1102 ... Mono.win-x64`.
Het wachtwoord van het sleutelbestand komt uit de gebruikersvariabele `MIJNBESLUIT_KEYSTORE_WACHTWOORD`; start SideKick opnieuw als je die net hebt gezet. Voor een AAB (Play Store) draai je `Apk-maken.ps1 -Aab` zelf. Het wachtwoord van het sleutelbestand komt uit de gebruikersvariabele `MIJNBESLUIT_KEYSTORE_WACHTWOORD`; start SideKick opnieuw als je die net hebt gezet. Voor een AAB (Play Store) draai je `Apk-maken.ps1 -Aab` zelf.
## Uploaden naar de Play Store vanuit SideKick (niet getest tegen Google)
`tools\Bouwen-playstore.ps1` zet de nieuwste AAB uit `MijnBesluitpublish\android-aab` in de Play Console via de Android Publisher API. Het controleert eerst de versiecode: `ApplicationVersion` in de csproj moet hoger zijn dan de hoogste versiecode die Google al kent (tracks en bundels), anders stopt het. Het zet de release standaard als **concept** op de track **intern testen**; publiceren doe je zelf in de Play Console. Alleen controleren zonder uploaden: `tools\Bouwen-playstore.ps1 -Controle` in PowerShell. Andere track of meteen uitrollen: `-Track alpha|beta|production`, `-Status completed`, release-notes met `-Notities`.
Eenmalig: de app moet in de Play Console staan en de eerste AAB is met de hand geupload; maak een serviceaccount met sleutelbestand (JSON) in Google Cloud (Google Play Android Developer API aan), nodig dat account uit in de Play Console onder Gebruikers en machtigingen, en zet de gebruikersvariabele `MIJNBESLUIT_PLAY_SLEUTEL` op het pad van het JSON-bestand (buiten de repo, nooit in git). Start SideKick daarna opnieuw.
In SideKick: onderdeel `mijnbesluit-playstore` (Sleutel `playstore`, Exe `playstore-upload.txt`). Het staat uit; zet bij Instellingen, Algemeen, het vinkje Actief aan als je het wilt gebruiken. Bouw eerst de AAB (`mijnbesluit-aab`), dan pas dit onderdeel.
+205
View File
@@ -0,0 +1,205 @@
<#
Zet de nieuwste AAB in de Google Play Console (Android Publisher API), met versiecontrole.
Voor SideKick: onderdeel mijnbesluit-playstore, Sleutel playstore. NIET GETEST tegen Google.
Wat het doet
1. Leest ApplicationVersion uit MijnBesluit.App.csproj en vraagt aan Google welke versiecodes
al op de tracks en in de bundels staan. Is de versie niet hoger dan de hoogste: stoppen.
2. Uploadt de nieuwste *.aab uit -AabMap.
3. Zet hem op de track (standaard intern testen) als CONCEPT: er gaat niets live. Publiceren
doe je zelf in de Play Console.
4. Schrijft playstore-upload.txt in -Uitvoer (zo ziet SideKick dat het gelukt is).
Eenmalig regelen (zie docs\Android-app.md)
* De app staat al in de Play Console en de eerste AAB is met de hand geupload.
* Een serviceaccount met sleutelbestand (JSON) en recht om releases te beheren.
* Gebruikersvariabele MIJNBESLUIT_PLAY_SLEUTEL = pad naar dat JSON-bestand (buiten de repo).
SideKick opnieuw starten na het zetten van de variabele.
Het sleutelbestand wordt alleen gelezen en komt nooit in een log of de repo.
#>
[CmdletBinding()]
param(
[string] $Configuratie = 'Release',
# Waar playstore-upload.txt komt (SideKick: <publicatiemap>\playstore).
[string] $Uitvoer,
# De map met de AAB van Bouwen-android-aab.ps1.
[string] $AabMap = 'D:\MijnBesluit VS\MijnBesluitpublish\android-aab',
[string] $Pakket = 'nl.mijnbesluit.app',
# internal = intern testen, alpha = gesloten testen, production = productie.
[ValidateSet('internal', 'alpha', 'beta', 'production')]
[string] $Track = 'internal',
# draft = concept, nog te publiceren in de Play Console. completed = meteen uitrollen
# (bij een app die nog nooit is gepubliceerd weigert Google dat).
[ValidateSet('draft', 'completed')]
[string] $Status = 'draft',
[string] $Notities = 'Nieuwe versie.',
[string] $Taal = 'nl-NL',
# Alleen controleren (versies ophalen en vergelijken); niets uploaden.
[switch] $Controle
)
$ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$csproj = Join-Path $PSScriptRoot '..\src\MijnBesluit.App\MijnBesluit.App.csproj' | Resolve-Path
# ------------------------------------------------------------------ versie uit de csproj
$tekst = Get-Content $csproj -Raw
if ($tekst -notmatch '<ApplicationVersion>\s*(\d+)\s*</ApplicationVersion>') {
throw "Geen ApplicationVersion (geheel getal) gevonden in $csproj."
}
$nieuweVersie = [int]$Matches[1]
$weergave = if ($tekst -match '<ApplicationDisplayVersion>\s*([^<]+?)\s*</ApplicationDisplayVersion>') { $Matches[1] } else { '?' }
Write-Host "Versie in de csproj: $weergave (versiecode $nieuweVersie)"
# ------------------------------------------------------------------ welke AAB
if (-not (Test-Path $AabMap)) { throw "De AAB-map bestaat niet: $AabMap. Bouw eerst de AAB (mijnbesluit-aab)." }
$aab = Get-ChildItem $AabMap -Filter '*.aab' | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $aab) { throw "Geen .aab gevonden in $AabMap. Bouw eerst de AAB (mijnbesluit-aab)." }
Write-Host "AAB: $($aab.FullName) ($([math]::Round($aab.Length / 1MB, 1)) MB, $($aab.LastWriteTime))"
if ($aab.LastWriteTime -lt (Get-Item $csproj).LastWriteTime) {
throw "De AAB is ouder dan de csproj. Bouw de AAB opnieuw (mijnbesluit-aab), anders zit er een andere versiecode in dan hierboven."
}
# ------------------------------------------------------------------ sleutelbestand en token
$sleutelPad = $env:MIJNBESLUIT_PLAY_SLEUTEL
if (-not $sleutelPad) {
throw @"
De gebruikersvariabele MIJNBESLUIT_PLAY_SLEUTEL ontbreekt.
Zet hem op het pad van het JSON-sleutelbestand van het serviceaccount (buiten de repo):
[Environment]::SetEnvironmentVariable('MIJNBESLUIT_PLAY_SLEUTEL', 'D:\pad\naar\sleutel.json', 'User')
Start SideKick daarna opnieuw.
"@
}
if (-not (Test-Path $sleutelPad)) { throw "Het sleutelbestand bestaat niet: $sleutelPad" }
$sa = Get-Content $sleutelPad -Raw | ConvertFrom-Json
if (-not $sa.client_email -or -not $sa.private_key) { throw "Dit is geen sleutelbestand van een serviceaccount (client_email/private_key ontbreken)." }
function ConvertTo-Base64Url([byte[]] $bytes) {
[Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
}
function Get-Token {
$nu = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$kop = ConvertTo-Base64Url ([Text.Encoding]::UTF8.GetBytes('{"alg":"RS256","typ":"JWT"}'))
$aud = if ($sa.token_uri) { $sa.token_uri } else { 'https://oauth2.googleapis.com/token' }
$claims = @{ iss = $sa.client_email; scope = 'https://www.googleapis.com/auth/androidpublisher'; aud = $aud; iat = $nu; exp = $nu + 3000 } | ConvertTo-Json -Compress
$inhoud = ConvertTo-Base64Url ([Text.Encoding]::UTF8.GetBytes($claims))
$teTekenen = [Text.Encoding]::ASCII.GetBytes("$kop.$inhoud")
$pem = ($sa.private_key -replace '-----[A-Z ]+-----', '' -replace '\s', '')
$sleutel = [Security.Cryptography.CngKey]::Import([Convert]::FromBase64String($pem), [Security.Cryptography.CngKeyBlobFormat]::Pkcs8PrivateBlob)
$rsa = New-Object Security.Cryptography.RSACng($sleutel)
$handtekening = $rsa.SignData($teTekenen, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1)
$jwt = "$kop.$inhoud." + (ConvertTo-Base64Url $handtekening)
$antwoord = Invoke-RestMethod -Method Post -Uri $aud -ContentType 'application/x-www-form-urlencoded' `
-Body "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=$jwt"
return $antwoord.access_token
}
function Invoke-Play([string] $methode, [string] $url, $body = $null, [string] $infile = $null) {
$kopjes = @{ Authorization = "Bearer $script:token" }
try {
if ($infile) {
return Invoke-RestMethod -Method $methode -Uri $url -Headers $kopjes -ContentType 'application/octet-stream' -InFile $infile -TimeoutSec 900
}
if ($null -ne $body) {
return Invoke-RestMethod -Method $methode -Uri $url -Headers $kopjes -ContentType 'application/json' -Body ($body | ConvertTo-Json -Depth 10)
}
return Invoke-RestMethod -Method $methode -Uri $url -Headers $kopjes
}
catch {
$detail = $_.ErrorDetails.Message
if (-not $detail -and $_.Exception.Response) {
try { $detail = (New-Object IO.StreamReader($_.Exception.Response.GetResponseStream())).ReadToEnd() } catch { }
}
throw "Google Play weigerde $methode $url : $($_.Exception.Message) $detail"
}
}
Write-Host "Inloggen bij Google Play als $($sa.client_email)..."
$token = Get-Token
$basis = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/$Pakket"
$edit = Invoke-Play 'POST' "$basis/edits" @{}
$editId = $edit.id
Write-Host "Bewerking $editId geopend."
try {
# -------------------------------------------------------------- versiecontrole
$hoogste = 0
$gevonden = @()
$tracks = Invoke-Play 'GET' "$basis/edits/$editId/tracks"
foreach ($t in @($tracks.tracks)) {
foreach ($r in @($t.releases)) {
foreach ($v in @($r.versionCodes)) {
$gevonden += "$($t.track): $v ($($r.status))"
if ([int]$v -gt $hoogste) { $hoogste = [int]$v }
}
}
}
$bundels = Invoke-Play 'GET' "$basis/edits/$editId/bundles"
foreach ($b in @($bundels.bundles)) {
if ([int]$b.versionCode -gt $hoogste) { $hoogste = [int]$b.versionCode }
}
if ($gevonden.Count) { Write-Host "Al bij Google: $($gevonden -join '; ')" } else { Write-Host "Nog geen versies bij Google gevonden." }
Write-Host "Hoogste versiecode bij Google: $hoogste; nieuwe versiecode: $nieuweVersie"
if ($nieuweVersie -le $hoogste) {
throw "De versiecode in de csproj ($nieuweVersie) is niet hoger dan wat Google al heeft ($hoogste). Verhoog ApplicationVersion in MijnBesluit.App.csproj en bouw de AAB opnieuw."
}
if ($Controle) {
Write-Host "Controle gelukt: versiecode $nieuweVersie mag geupload worden. Er is niets geupload." -ForegroundColor Green
Invoke-Play 'DELETE' "$basis/edits/$editId" | Out-Null
return
}
# -------------------------------------------------------------- uploaden
Write-Host "AAB uploaden..."
$bundel = Invoke-Play 'POST' "https://androidpublisher.googleapis.com/upload/androidpublisher/v3/applications/$Pakket/edits/$editId/bundles?uploadType=media" -infile $aab.FullName
$code = [int]$bundel.versionCode
Write-Host "Geupload als versiecode $code."
if ($code -ne $nieuweVersie) {
Write-Host "Let op: de AAB heeft versiecode $code, de csproj zegt $nieuweVersie. Bouw de AAB opnieuw als dat niet de bedoeling is." -ForegroundColor Yellow
}
# -------------------------------------------------------------- op de track zetten
$release = @{
name = "$weergave ($code)"
versionCodes = @("$code")
status = $Status
releaseNotes = @(@{ language = $Taal; text = $Notities })
}
Invoke-Play 'PUT' "$basis/edits/$editId/tracks/$Track" @{ track = $Track; releases = @($release) } | Out-Null
Write-Host "Op track '$Track' gezet (status: $Status)."
Invoke-Play 'POST' "$basis/edits/$editId`:commit" @{} | Out-Null
Write-Host "Vastgelegd bij Google Play." -ForegroundColor Green
}
catch {
try { Invoke-Play 'DELETE' "$basis/edits/$editId" | Out-Null } catch { }
throw
}
$samenvatting = "Geupload naar Google Play ($Pakket): versie $weergave, versiecode $code, track $Track, status $Status, op $(Get-Date -Format 'yyyy-MM-dd HH:mm')."
if ($Uitvoer) {
New-Item -ItemType Directory -Path $Uitvoer -Force | Out-Null
Set-Content -Path (Join-Path $Uitvoer 'playstore-upload.txt') -Value $samenvatting -Encoding UTF8
}
Write-Host $samenvatting
if ($Status -eq 'draft') { Write-Host "Het is een concept: publiceer de release zelf in de Play Console." }