release: prepare August 1 Android RC

This commit is contained in:
key
2026-07-29 00:35:15 +08:00
parent 9ae5f750c8
commit 9ea5d79a62
41 changed files with 2322 additions and 71 deletions
+235
View File
@@ -0,0 +1,235 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ApkPath,
[ValidateSet('dcloud-test', 'release')]
[string]$BuildKind = 'dcloud-test',
[string]$ExpectedSignerSha256,
[string]$TermsUrl = 'https://peilian.njzhmj.top/legal/terms.html',
[string]$PrivacyUrl = 'https://peilian.njzhmj.top/legal/privacy.html',
[switch]$SkipBuild,
[switch]$AllowDirtyRehearsal
)
$ErrorActionPreference = 'Stop'
$projectRoot = Split-Path -Parent $PSScriptRoot
Set-Location -LiteralPath $projectRoot
function Invoke-Checked {
param(
[Parameter(Mandatory = $true)]
[string]$FilePath,
[string[]]$Arguments = @()
)
& $FilePath @Arguments
if ($LASTEXITCODE -ne 0) {
throw "Command failed with exit code $LASTEXITCODE`: $FilePath $($Arguments -join ' ')"
}
}
function Read-Git {
param([string[]]$Arguments)
$output = & git @Arguments
if ($LASTEXITCODE -ne 0) {
throw "git command failed: git $($Arguments -join ' ')"
}
return ($output | Out-String).Trim()
}
$worktreeStatus = Read-Git @('status', '--porcelain')
$isDirty = -not [string]::IsNullOrWhiteSpace($worktreeStatus)
if ($isDirty -and -not $AllowDirtyRehearsal) {
throw 'RC preparation requires a clean worktree. Commit the reviewed release batch first.'
}
if ($BuildKind -eq 'release' -and [string]::IsNullOrWhiteSpace($ExpectedSignerSha256)) {
throw 'Enterprise release verification requires -ExpectedSignerSha256.'
}
$resolvedApkPath = (Resolve-Path -LiteralPath $ApkPath).Path
$manifestPath = Join-Path $projectRoot 'mobile-uni\src\manifest.json'
$packagePath = Join-Path $projectRoot 'mobile-uni\package.json'
$manifest = Get-Content -Raw -Encoding UTF8 -LiteralPath $manifestPath | ConvertFrom-Json
$package = Get-Content -Raw -Encoding UTF8 -LiteralPath $packagePath | ConvertFrom-Json
if ([string]$manifest.versionName -ne [string]$package.version) {
throw "mobile-uni package version $($package.version) does not match manifest version $($manifest.versionName)."
}
$commit = Read-Git @('rev-parse', 'HEAD')
$shortCommit = Read-Git @('rev-parse', '--short=8', 'HEAD')
$upstreamDelta = Read-Git @('rev-list', '--left-right', '--count', 'HEAD...origin/main')
Invoke-Checked -FilePath 'git' -Arguments @('diff', '--check')
$env:VITE_APP_TERMS_URL = $TermsUrl
$env:VITE_APP_PRIVACY_URL = $PrivacyUrl
Remove-Item Env:VITE_DEV_SMS_HINT_ENABLED -ErrorAction SilentlyContinue
Remove-Item Env:VITE_DEV_SMS_HINT_VALUE -ErrorAction SilentlyContinue
if (-not $SkipBuild) {
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'test:unit')
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'typecheck')
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'configure:app-privacy')
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'build:h5')
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'build:app')
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'mobile-uni', 'run', 'verify:app-release')
Invoke-Checked -FilePath 'npm' -Arguments @('--prefix', 'frontend', 'run', 'build:prod')
Push-Location (Join-Path $projectRoot 'backend')
try {
Invoke-Checked -FilePath 'mvn' -Arguments @(
'-pl', 'ruoyi-modules/ruoyi-aihr',
'-am',
'-DskipTests=false',
'test'
)
Invoke-Checked -FilePath 'mvn' -Arguments @(
'-pl', 'ruoyi-admin',
'-am',
'-DskipTests',
'package'
)
}
finally {
Pop-Location
}
}
$postBuildWorktreeStatus = Read-Git @('status', '--porcelain')
$postBuildDirty = -not [string]::IsNullOrWhiteSpace($postBuildWorktreeStatus)
if (-not $isDirty -and $postBuildDirty) {
throw 'RC build changed the previously clean worktree. Review and commit or revert generated changes before release.'
}
$apkVerifier = Join-Path $projectRoot 'mobile-uni\scripts\verify-android-apk.ps1'
$verifyArguments = @(
'-NoProfile',
'-ExecutionPolicy', 'Bypass',
'-File', $apkVerifier,
'-ApkPath', $resolvedApkPath,
'-BuildKind', $BuildKind,
'-ExpectedTermsUrl', $TermsUrl,
'-ExpectedPrivacyUrl', $PrivacyUrl
)
if (-not [string]::IsNullOrWhiteSpace($ExpectedSignerSha256)) {
$verifyArguments += @('-ExpectedSignerSha256', $ExpectedSignerSha256)
}
$apkVerificationOutput = & powershell @verifyArguments 2>&1
$apkVerificationOutput | ForEach-Object { Write-Output $_ }
if ($LASTEXITCODE -ne 0) {
throw 'Android APK verification failed.'
}
$apkVerificationText = ($apkVerificationOutput | Out-String)
$signerMatch = [regex]::Match($apkVerificationText, 'signer SHA-256:\s*([0-9A-Fa-f]+)')
$cleartextMatch = [regex]::Match($apkVerificationText, 'usesCleartextTraffic:\s*(True|False)')
if (-not $signerMatch.Success -or -not $cleartextMatch.Success) {
throw 'APK verifier output is missing signer or cleartext metadata.'
}
$versionName = [string]$manifest.versionName
$versionCode = [string]$manifest.versionCode
$releaseEligible = -not $isDirty -and -not $postBuildDirty -and -not $SkipBuild
$modeSuffix = if ($releaseEligible) { 'frozen' } else { 'rehearsal' }
$evidenceDirectory = Join-Path $projectRoot "output\aug1-rc\$versionName-$versionCode-$shortCommit-$modeSuffix"
New-Item -ItemType Directory -Path $evidenceDirectory -Force | Out-Null
$artifactName = "bangdao-$versionName-$versionCode-$shortCommit-$BuildKind.apk"
$artifactPath = Join-Path $evidenceDirectory $artifactName
Copy-Item -LiteralPath $resolvedApkPath -Destination $artifactPath -Force
$frontendIndex = Join-Path $projectRoot 'frontend\dist\index.html'
$mobileIndex = Join-Path $projectRoot 'mobile-uni\dist\build\h5\index.html'
$backendJar = Join-Path $projectRoot 'backend\ruoyi-admin\target\ruoyi-admin.jar'
foreach ($requiredArtifact in @($frontendIndex, $mobileIndex, $backendJar)) {
if (-not (Test-Path -LiteralPath $requiredArtifact)) {
throw "Required release artifact is missing: $requiredArtifact"
}
}
$rehearsalReasons = @()
if ($isDirty) {
$rehearsalReasons += 'worktree contains uncommitted changes'
}
if ($SkipBuild) {
$rehearsalReasons += 'full build and tests were skipped'
}
$evidence = [ordered]@{
generatedAt = (Get-Date).ToUniversalTime().ToString('o')
releaseTarget = '2026-08-01 Android controlled internal test'
releaseEligible = $releaseEligible
rehearsalReasons = $rehearsalReasons
git = [ordered]@{
commit = $commit
upstreamDelta = $upstreamDelta
clean = -not $postBuildDirty
}
app = [ordered]@{
packageName = [string]$manifest.'app-plus'.distribute.android.packagename
versionName = $versionName
versionCode = $versionCode
targetSdkVersion = [int]$manifest.'app-plus'.distribute.android.targetSdkVersion
buildKind = $BuildKind
signerSha256 = $signerMatch.Groups[1].Value.ToUpperInvariant()
usesCleartextTraffic = [bool]::Parse($cleartextMatch.Groups[1].Value)
legalUrls = @($TermsUrl, $PrivacyUrl)
}
artifacts = [ordered]@{
apk = [ordered]@{
path = $artifactPath
bytes = (Get-Item -LiteralPath $artifactPath).Length
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $artifactPath).Hash
}
frontendIndexSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $frontendIndex).Hash
mobileH5IndexSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $mobileIndex).Hash
backendJarSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $backendJar).Hash
}
automatedGates = if ($SkipBuild) {
@('artifact-only rehearsal; review prior build evidence separately')
}
else {
@(
'mobile unit tests',
'mobile typecheck',
'mobile H5 build',
'mobile App build',
'App release asset and sensitive-value scan',
'frontend production build',
'ruoyi-aihr full test suite',
'backend package',
'APK metadata, legal links, signature, content match and sensitive-value scan'
)
}
manualGatesRemaining = @(
'privacy reject and consent choices',
'login error and re-login',
'microphone allow and deny',
'media cancellation',
'foreground/background and network recovery',
'authenticated search, practice, ask and five direct-feedback channels',
'business content and authorization sign-off',
'release-unit and rollback sign-off'
)
}
$jsonPath = Join-Path $evidenceDirectory 'release-evidence.json'
$markdownPath = Join-Path $evidenceDirectory 'release-evidence.md'
$evidence | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $jsonPath -Encoding UTF8
@(
'# Bangdao August 1 Android RC evidence',
'',
"- Generated at: $($evidence['generatedAt'])",
"- Release eligible: $releaseEligible",
"- Git commit: $commit",
"- Clean worktree: $(-not $postBuildDirty)",
"- App: $($evidence['app']['packageName']) $versionName ($versionCode), API $($evidence['app']['targetSdkVersion'])",
"- APK: $artifactName",
"- APK SHA-256: $($evidence['artifacts']['apk']['sha256'])",
"- Signer SHA-256: $($evidence['app']['signerSha256'])",
"- Cleartext traffic allowed: $($evidence['app']['usesCleartextTraffic'])",
'',
'## Manual gates remaining',
'',
($evidence.manualGatesRemaining | ForEach-Object { "- $_" })
) | Set-Content -LiteralPath $markdownPath -Encoding UTF8
Write-Output "RC evidence written to $evidenceDirectory"
if (-not $releaseEligible) {
Write-Warning 'This is a rehearsal and cannot be distributed as the August 1 RC.'
}