Ctrl K
ComunidadGuías…

scripts/release-app.ps1

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# =====================================================
# TapLedger APP 一键发布脚本(默认完整发布流程)
# 用法(任意目录):
#   powershell -ExecutionPolicy Bypass -File scripts\release-app.ps1                 # 自动 patch +1
#   powershell -ExecutionPolicy Bypass -File scripts\release-app.ps1 -Version 1.0.0  # 指定版本
#   可选 -CommitMsg "feat: xxx" 自定义提交信息(默认 chore: 发布 vX.Y.Z)
# 前置:
#   - Node / Android SDK(gradlew + aapt)/ GitHub CLI(gh) 已登录
#   - android/ 已存在(首次需 npx expo prebuild --platform android)
#   - CHANGELOG.md 已包含目标版本条目(## [X.Y.Z])
# 流程:
#   版本号同步(app.json/package.json/package-lock.json/build.gradle/README)
#   → 单测 + 类型检查 → 本地构建 APK → aapt 校验 → 复制 APK 到根目录
#   → git 提交推送 → 创建 GitHub Release 并上传 APK(说明取自 CHANGELOG)
# =====================================================

param(
  [string]$Version = '',
  [string]$CommitMsg = ''
)

$ErrorActionPreference = 'Stop'
$utf8 = New-Object System.Text.UTF8Encoding($false)

function Fail([string]$msg) { Write-Host "[错误] $msg" -ForegroundColor Red; exit 1 }

$root = Split-Path -Parent $PSScriptRoot
Push-Location $root
try {
  # ---------- 0. 解析目标版本 ----------
  $appJson = Get-Content 'app.json' -Raw -Encoding UTF8 | ConvertFrom-Json
  $current = $appJson.expo.version
  if (-not $Version) {
    # 默认:补丁位 +1
    $parts = $current.Split('.')
    $parts[2] = [string]([int]$parts[2] + 1)
    $Version = $parts -join '.'
  }
  if ($Version -notmatch '^\d+\.\d+\.\d+$') { Fail "版本号格式错误:$Version(应为 X.Y.Z)" }
  $vp = $Version.Split('.')
  $versionCode = [int]$vp[0] * 10000 + [int]$vp[1] * 100 + [int]$vp[2]
  if ([int]$vp[1] -ge 100 -or [int]$vp[2] -ge 100) { Fail "minor/patch 必须 < 100" }
  Write-Host "==> 发布 v$Version(当前 $current,versionCode $versionCode)" -ForegroundColor Cyan

  # ---------- 1. CHANGELOG 校验(说明来源,缺失则中止) ----------
  $changelogPath = Join-Path $root 'CHANGELOG.md'
  $changelogRaw = [System.IO.File]::ReadAllText($changelogPath, $utf8)
  if ($changelogRaw -notmatch ('## \[' + [regex]::Escape($Version) + '\]')) {
    Fail "CHANGELOG.md 缺少 [$Version] 条目,请先补充(## [版本] - 日期)再发布。"
  }

  # ---------- 2. 版本号同步 ----------
  Write-Host "==> 同步版本号:app.json / package.json / package-lock.json" -ForegroundColor Cyan
  & node (Join-Path $PSScriptRoot 'sync-version.js') $Version
  if ($LASTEXITCODE -ne 0) { Fail "版本号同步失败。" }

  Write-Host "==> 同步 android/app/build.gradle(versionCode $versionCode / versionName $Version)" -ForegroundColor Cyan
  $gradlePath = Join-Path $root 'android\app\build.gradle'
  if (-not (Test-Path $gradlePath)) { Fail "未找到 android\app\build.gradle,请先执行 npx expo prebuild --platform android" }
  $g = [System.IO.File]::ReadAllText($gradlePath, $utf8)
  $g = $g -replace 'versionCode\s+\d+', "versionCode $versionCode"
  $g = $g -replace 'versionName\s+"[\d.]+"', "versionName `"$Version`""
  [System.IO.File]::WriteAllText($gradlePath, $g, $utf8)

  Write-Host "==> 同步 README 版本行" -ForegroundColor Cyan
  $readmePath = Join-Path $root 'README.md'
  $readmeRaw = [System.IO.File]::ReadAllText($readmePath, $utf8)
  $readmeRaw = $readmeRaw -replace '当前版本:[\d.]+', "当前版本:$Version"
  [System.IO.File]::WriteAllText($readmePath, $readmeRaw, $utf8)

  # ---------- 3. 单测 + 类型检查 + Lint ----------
  Write-Host "==> vitest + tsc + eslint" -ForegroundColor Cyan
  & npx vitest run
  if ($LASTEXITCODE -ne 0) { Fail "单测未通过。" }
  & npx tsc --noEmit
  if ($LASTEXITCODE -ne 0) { Fail "类型检查未通过。" }
  & npx eslint .
  if ($LASTEXITCODE -ne 0) { Fail "ESLint 未通过。" }
  & npx prettier --check "src/**/*.{ts,tsx}" "server/src/**/*.ts" | Out-Null
  if ($LASTEXITCODE -ne 0) { Fail "Prettier 格式检查未通过(先跑 npm run format)。" }

  # ---------- 3.5 release 签名注入自检(v0.11.8)----------
  # 只看构建产物会被 apksigner 拦住,但那时已经白跑几分钟 Gradle;
  # 这里先确认 build.gradle 里插件的三段注入都在,缺任何一段就直接失败。
  $gCheck = [System.IO.File]::ReadAllText($gradlePath, $utf8)
  foreach ($needle in @(
      'tapledgerKeystorePropsFile = rootProject',
      'storeFile file(tapledgerKeystoreProps',
      'signingConfig tapledgerKeystorePropsFile.exists() ? signingConfigs.release : signingConfigs.debug')) {
    if ($gCheck -notlike "*$needle*") {
      Fail "android/app/build.gradle 缺少 release 签名注入片段:$needle`n请确认 keystore.properties 存在后重新 npx expo prebuild --platform android"
    }
  }
  if (-not (Test-Path (Join-Path $root 'keystore.properties'))) { Fail "缺少 keystore.properties,禁止发布。" }

  # ---------- 4. 本地构建 release APK ----------
  Write-Host "==> gradlew assembleRelease" -ForegroundColor Cyan
  Push-Location (Join-Path $root 'android')
  try { & .\gradlew.bat assembleRelease --console=plain }
  finally { Pop-Location }
  if ($LASTEXITCODE -ne 0) { Fail "APK 构建失败。" }

  $apkPath = Join-Path $root 'android\app\build\outputs\apk\release\app-release.apk'
  if (-not (Test-Path $apkPath)) { Fail "未找到构建产物:$apkPath" }

  # ---------- 5. aapt 校验 versionName / versionCode ----------
  $aapt = Get-ChildItem "$env:LOCALAPPDATA\Android\Sdk\build-tools" -Recurse -Filter aapt.exe -ErrorAction SilentlyContinue |
          Sort-Object DirectoryName -Descending | Select-Object -First 1 -ExpandProperty FullName
  if (-not $aapt) { Fail "未找到 aapt.exe(Android SDK build-tools),无法校验版本号。" }
  $badgingOut = & $aapt dump badging $apkPath
  $pkgLine = ($badgingOut | Where-Object { $_ -match '^package:' }) -join ' '
  Write-Host "aapt: $pkgLine" -ForegroundColor Gray
  if ($pkgLine -notmatch [regex]::Escape("versionName='$Version'")) { Fail "versionName 校验失败(期望 $Version)" }
  if ($pkgLine -notmatch [regex]::Escape("versionCode='$versionCode'")) { Fail "versionCode 校验失败(期望 $versionCode)" }

  # ---------- 5.5 apksigner 校验签名者(v0.11:禁止 debug 签名的 release 包发布) ----------
  $apksigner = Get-ChildItem "$env:LOCALAPPDATA\Android\Sdk\build-tools" -Recurse -Filter apksigner.bat -ErrorAction SilentlyContinue |
               Sort-Object DirectoryName -Descending | Select-Object -First 1 -ExpandProperty FullName
  if (-not $apksigner) { Fail "未找到 apksigner.bat(Android SDK build-tools),无法校验签名。" }
  $certOut = & $apksigner verify --print-certs $apkPath 2>&1 | Out-String
  Write-Host "签名: $(($certOut -split "`n" | Where-Object { $_ -match 'certificate DN' }) -join ' ')" -ForegroundColor Gray
  if ($certOut -match 'CN=Android Debug') {
    Fail "APK 使用 debug keystore 签名,禁止发布!请确认 keystore.properties 与 keystore/ 目录存在后重新 prebuild。"
  }

  # ---------- 6. 复制 APK 到根目录 ----------
  $apkDest = Join-Path $root "TapLedger-v$Version.apk"
  Copy-Item $apkPath $apkDest -Force
  $mb = [math]::Round((Get-Item $apkDest).Length / 1MB, 1)
  Write-Host "==> APK 就绪:TapLedger-v$Version.apk($mb MB)" -ForegroundColor Green

  # ---------- 7. git 提交推送 ----------
  # 只暂存发布链路真正改到的文件:git add -A 会把工作区里无关的半成品/意外改动
  # 一起卷进 release 提交(本次审查就撞到一个:一次编码事故被 -A 顺手提交的风险)
  Write-Host "==> git 提交推送" -ForegroundColor Cyan
  # 注意:android/ 整个目录在 .gitignore 里(expo prebuild 产物,版本号只在本地的
  # build.gradle 里生效),把它列进 pathspec 会让 git add 直接以非零码退出「ignored
  # paths」——v0.11.12 就是被这一步卡住的。上面第 2 步仍然要写 build.gradle,
  # 但那是本地构建输入,不是提交内容。
  $stagePaths = @(
    'app.json', 'package.json', 'package-lock.json', 'server/package-lock.json',
    'README.md', 'CHANGELOG.md',
    'src', 'server/src', 'App.tsx', 'plugins', 'scripts'
  )
  & git add -- $stagePaths
  if ($LASTEXITCODE -ne 0) { Fail "git add 失败。" }
  & git status --short
  if (-not $CommitMsg) { $CommitMsg = "chore: 发布 v$Version" }
  & git commit -m $CommitMsg
  if ($LASTEXITCODE -ne 0) { Fail "git commit 失败(可能无可提交改动)。" }
  & git push
  if ($LASTEXITCODE -ne 0) { Fail "git push 失败。" }

  # ---------- 8. GitHub Release + 上传 APK ----------
  Write-Host "==> gh release create v$Version" -ForegroundColor Cyan
  # Release 说明 = CHANGELOG 中本版本条目正文
  # v0.11 修复:改用 --notes-file(经验教训:--notes 传含 [x] 的内容会被 PowerShell 通配符干扰)
  $pattern = '(?s)## \[' + [regex]::Escape($Version) + '\] - [^\r\n]*\r?\n(.*?)(?=\r?\n## \[|$)'
  $m = [regex]::Match($changelogRaw, $pattern)
  $notes = if ($m.Success) { $m.Groups[1].Value.Trim() } else { "详见 CHANGELOG.md" }
  $notesFile = Join-Path ([System.IO.Path]::GetTempPath()) "tapledger-release-notes-$Version.md"
  [System.IO.File]::WriteAllText($notesFile, $notes, $utf8)
  try {
    & gh release create "v$Version" --repo sctale/TapLedger --title "v$Version" --target main --notes-file $notesFile $apkDest
    if ($LASTEXITCODE -ne 0) { Fail "GitHub Release 创建失败。" }
  } finally {
    Remove-Item $notesFile -ErrorAction SilentlyContinue
  }

  Write-Host ""
  Write-Host "发布完成:v$Version" -ForegroundColor Green
  Write-Host "  Release: https://github.com/sctale/TapLedger/releases/tag/v$Version" -ForegroundColor Cyan
}
finally {
  Pop-Location
}