param( [Parameter(Mandatory = $true)] [ValidatePattern('^\d+\.\d+\.\d+$')] [string]$VersionName, [Parameter(Mandatory = $true)] [ValidateRange(1, 2147483647)] [int]$VersionCode, [string]$ReleaseNotes = 'Healer Man Android update.', [string]$GiteaBaseUrl = 'http://192.168.1.180:30008', [string]$GiteaPublicBaseUrl = 'https://git.whoagland.com', [string]$Owner = 'phenom', [string]$Repository = 'healer-man' ) $ErrorActionPreference = 'Stop' $requiredSecrets = @( 'ANDROID_KEYSTORE_FILE', 'ANDROID_KEYSTORE_PASSWORD', 'ANDROID_KEY_ALIAS', 'ANDROID_KEY_PASSWORD', 'GITEA_TOKEN' ) foreach ($secret in $requiredSecrets) { if (-not [Environment]::GetEnvironmentVariable($secret)) { throw "Required environment variable $secret is not set." } } $projectRoot = Split-Path -Parent $PSScriptRoot $packageVersion = (Get-Content -LiteralPath (Join-Path $projectRoot 'package.json') -Raw | ConvertFrom-Json).version $releaseVersionPath = Join-Path $projectRoot 'release-version.json' if (-not (Test-Path -LiteralPath $releaseVersionPath)) { throw "Canonical release metadata is missing: $releaseVersionPath" } $releaseVersion = Get-Content -LiteralPath $releaseVersionPath -Raw | ConvertFrom-Json if ($packageVersion -ne $VersionName -or $releaseVersion.versionName -ne $VersionName) { throw "VersionName $VersionName does not match package.json and release-version.json." } if ([int]$releaseVersion.versionCode -ne $VersionCode) { throw "VersionCode $VersionCode does not match release-version.json ($($releaseVersion.versionCode))." } $env:ANDROID_VERSION_NAME = $VersionName $env:ANDROID_VERSION_CODE = [string]$VersionCode Push-Location $projectRoot try { $initialGitStatus = & git status --porcelain if ($LASTEXITCODE -ne 0) { throw "Unable to inspect the Git working tree." } if ($initialGitStatus) { throw "APK publication requires a clean Git working tree." } & npm run android:sync if ($LASTEXITCODE -ne 0) { throw "Android web build and Capacitor sync failed with exit code $LASTEXITCODE" } $postSyncGitStatus = & git status --porcelain --untracked-files=no if ($LASTEXITCODE -ne 0) { throw "Unable to inspect Android sync changes." } if ($postSyncGitStatus) { throw "Android sync changed tracked files. Review and commit them before publishing the APK." } & powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/runAndroidGradle.ps1 assembleRelease if ($LASTEXITCODE -ne 0) { throw "Android release build failed with exit code $LASTEXITCODE" } $apkName = "healer-man-$VersionName-release.apk" $apkPath = Join-Path $projectRoot "android\app\build\outputs\apk\release\$apkName" if (-not (Test-Path -LiteralPath $apkPath)) { throw "Expected release APK was not created: $apkPath" } $checksumPath = "$apkPath.sha256" $checksum = (Get-FileHash -LiteralPath $apkPath -Algorithm SHA256).Hash.ToLowerInvariant() [IO.File]::WriteAllText($checksumPath, "$checksum $apkName`n", [Text.UTF8Encoding]::new($false)) $apiRoot = $GiteaBaseUrl.TrimEnd('/') + '/api/v1' $headers = @{ Authorization = "token $($env:GITEA_TOKEN.Trim())" } $tag = "v$VersionName" $releaseBody = @{ tag_name = $tag target_commitish = 'main' name = "Healer Man $VersionName" body = $ReleaseNotes draft = $false prerelease = $false } | ConvertTo-Json $encodedTag = [Uri]::EscapeDataString($tag) try { $release = Invoke-RestMethod ` -Method Get ` -Uri "$apiRoot/repos/$Owner/$Repository/releases/tags/$encodedTag" ` -Headers $headers Write-Host "Resuming existing Gitea release $tag." } catch { $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { 0 } if ($statusCode -ne 404) { throw } $release = Invoke-RestMethod ` -Method Post ` -Uri "$apiRoot/repos/$Owner/$Repository/releases" ` -Headers $headers ` -ContentType 'application/json' ` -Body $releaseBody } foreach ($attachment in @($apkPath, $checksumPath)) { $attachmentName = Split-Path -Leaf $attachment $attachmentSize = (Get-Item -LiteralPath $attachment).Length $encodedName = [Uri]::EscapeDataString($attachmentName) $release = Invoke-RestMethod ` -Method Get ` -Uri "$apiRoot/repos/$Owner/$Repository/releases/$($release.id)" ` -Headers $headers $existingAsset = @($release.assets | Where-Object { $_.name -eq $attachmentName }) | Select-Object -First 1 if ($existingAsset) { if ([long]$existingAsset.size -ne $attachmentSize) { throw "Release asset $attachmentName exists with the wrong size ($($existingAsset.size), expected $attachmentSize)." } Write-Host "Release asset $attachmentName already exists with the expected size; skipping upload." continue } $uploaded = $false for ($attempt = 1; $attempt -le 3 -and -not $uploaded; $attempt++) { try { Invoke-RestMethod ` -Method Post ` -Uri "$apiRoot/repos/$Owner/$Repository/releases/$($release.id)/assets?name=$encodedName" ` -Headers $headers ` -ContentType 'application/octet-stream' ` -InFile $attachment | Out-Null $uploaded = $true } catch { $refreshedRelease = Invoke-RestMethod ` -Method Get ` -Uri "$apiRoot/repos/$Owner/$Repository/releases/$($release.id)" ` -Headers $headers $uploadedAsset = @($refreshedRelease.assets | Where-Object { $_.name -eq $attachmentName -and [long]$_.size -eq $attachmentSize }) | Select-Object -First 1 if ($uploadedAsset) { $uploaded = $true continue } if ($attempt -eq 3) { throw } Write-Warning "Upload attempt $attempt for $attachmentName failed; retrying through the LAN API." Start-Sleep -Seconds ([math]::Pow(2, $attempt)) } } } Write-Host "Published $tag with $apkName and its SHA-256 checksum." Write-Host "$($GiteaPublicBaseUrl.TrimEnd('/'))/$Owner/$Repository/releases/tag/$tag" } finally { Pop-Location }