From 251391b1a993808d2c4e85e2525cda808c2d32b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=83=D1=80=D0=BD=D0=B0=D1=82=20=D0=90=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=B9?= Date: Fri, 12 Jun 2026 22:30:58 +0300 Subject: [PATCH] Require git provenance for Android Argus staging --- Android/.gitignore | 3 + Android/docs/argus-rollout-policy.md | 7 +- Android/scripts/prepare-argus-publication.ps1 | 80 ++++++++++++++++++- PRODUCTION_READINESS.md | 1 + 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/Android/.gitignore b/Android/.gitignore index 8266ae2b..e3939691 100644 --- a/Android/.gitignore +++ b/Android/.gitignore @@ -16,6 +16,9 @@ local.properties argus-signing.local.properties signing/ +# Local temporary release scratch files +.codex-temp/ + # Mac .DS_Store files .DS_Store diff --git a/Android/docs/argus-rollout-policy.md b/Android/docs/argus-rollout-policy.md index 33f76c9b..bff6d8e5 100644 --- a/Android/docs/argus-rollout-policy.md +++ b/Android/docs/argus-rollout-policy.md @@ -18,6 +18,7 @@ ## Release Checklist 1. Build the stable APK with the same release key material through `QSFERA_RELEASE_KEYSTORE`, `QSFERA_RELEASE_KEYSTORE_PASSWORD`, `QSFERA_RELEASE_KEY_ALIAS`, and `QSFERA_RELEASE_KEY_PASSWORD`. -2. Verify the APK certificate before upload with `apksigner verify --print-certs`. -3. Publish to Argus only after certificate, package name, version code, and SHA-256 checks pass. -4. Verify the published manifest and download with `scripts/verify-production-state.ps1 -VerifyAndroidDownload` from the repository root. +2. Prepare Argus publication only from a clean release branch whose `HEAD` matches its upstream; `Android/scripts/prepare-argus-publication.ps1` enforces this by default and records repository root, branch, upstream, commit, and verification time in `argus-publication.json`. When the Android checkout is used from the pushed `QSfera.git` monorepo, run it from `Android/` with `-GitProvenanceRoot ..`. +3. Verify the APK certificate before upload with `apksigner verify --print-certs`. +4. Publish to Argus only after certificate, package name, version code, SHA-256, and git provenance checks pass. +5. Verify the published manifest and download with `scripts/verify-production-state.ps1 -VerifyAndroidDownload` from the repository root. diff --git a/Android/scripts/prepare-argus-publication.ps1 b/Android/scripts/prepare-argus-publication.ps1 index 2766ba9f..08568c8d 100644 --- a/Android/scripts/prepare-argus-publication.ps1 +++ b/Android/scripts/prepare-argus-publication.ps1 @@ -9,8 +9,10 @@ param( [string]$Notes, [string]$RepositoryUrl = 'https://git.kusoft.xyz/sevenhill/QSfera.git', [string]$HomepageUrl = 'https://qsfera.kusoft.xyz', + [string]$GitProvenanceRoot, [switch]$Unlisted, - [switch]$SkipSigningInitialization + [switch]$SkipSigningInitialization, + [switch]$SkipGitProvenanceCheck ) $ErrorActionPreference = 'Stop' @@ -112,8 +114,83 @@ function Find-FirstTool([string]$AndroidHome, [string]$ToolName) { return $tool.FullName } +function Invoke-Git([string[]]$Arguments) { + $output = & git @Arguments 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "git $($Arguments -join ' ') failed with exit code $LASTEXITCODE. Output: $($output | Out-String)" + } + return @($output) +} + +function Assert-GitReleaseProvenance([string]$RepositoryRoot) { + if (-not (Get-Command git -ErrorAction SilentlyContinue)) { + throw 'git was not found. Argus publication requires git provenance verification.' + } + + $previousLocation = Get-Location + try { + Set-Location $RepositoryRoot + + $insideWorkTree = (Invoke-Git @('rev-parse', '--is-inside-work-tree') | Select-Object -First 1).Trim() + if ($insideWorkTree -ne 'true') { + throw 'Argus publication must be prepared from inside a git work tree.' + } + + $status = Invoke-Git @('status', '--porcelain') + if ($status.Count -gt 0) { + throw "Git work tree has uncommitted changes. Commit or stash all changes before preparing Argus publication." + } + + $branch = (Invoke-Git @('rev-parse', '--abbrev-ref', 'HEAD') | Select-Object -First 1).Trim() + if ($branch -eq 'HEAD') { + throw 'Git HEAD is detached. Check out the release branch before preparing Argus publication.' + } + + try { + $upstream = (Invoke-Git @('rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}') | Select-Object -First 1).Trim() + } catch { + throw "Git branch has no upstream. Push $branch and set its upstream before Argus publication." + } + + Invoke-Git @('fetch', '--quiet') + + $aheadBehind = (Invoke-Git @('rev-list', '--left-right', '--count', 'HEAD...@{u}') | Select-Object -First 1).Trim() -split '\s+' + $ahead = [int]$aheadBehind[0] + $behind = [int]$aheadBehind[1] + if ($ahead -ne 0 -or $behind -ne 0) { + throw "Git HEAD must match upstream before Argus publication. Branch $branch tracks $upstream, ahead=$ahead, behind=$behind." + } + + return [ordered]@{ + repositoryRoot = (Invoke-Git @('rev-parse', '--show-toplevel') | Select-Object -First 1).Trim() + branch = $branch + upstream = $upstream + commit = (Invoke-Git @('rev-parse', 'HEAD') | Select-Object -First 1).Trim() + verifiedAt = [DateTimeOffset]::UtcNow.ToString('O') + } + } finally { + Set-Location $previousLocation + } +} + $repoRoot = Split-Path -Parent $PSScriptRoot Set-Location $repoRoot +$gitProvenanceRootPath = if ($GitProvenanceRoot) { + (Resolve-Path -LiteralPath $GitProvenanceRoot).Path +} else { + $repoRoot +} + +$gitProvenance = if ($SkipGitProvenanceCheck) { + [ordered]@{ + skipped = $true + repositoryRoot = $gitProvenanceRootPath + reason = 'SkipGitProvenanceCheck was set' + verifiedAt = [DateTimeOffset]::UtcNow.ToString('O') + } +} else { + Assert-GitReleaseProvenance $gitProvenanceRootPath +} $javaHome = Resolve-JavaHome $androidHome = Resolve-AndroidHome @@ -251,6 +328,7 @@ $metadata = [ordered]@{ notes = if ($Notes) { $Notes } else { $null } repositoryUrl = if ($RepositoryUrl) { $RepositoryUrl } else { $null } homepageUrl = if ($HomepageUrl) { $HomepageUrl } else { $null } + git = $gitProvenance androidPackageName = $packageName androidVersionCode = [int]$versionCode targetSdkVersion = [int]$targetSdk diff --git a/PRODUCTION_READINESS.md b/PRODUCTION_READINESS.md index fff8489e..6778a1d3 100644 --- a/PRODUCTION_READINESS.md +++ b/PRODUCTION_READINESS.md @@ -24,6 +24,7 @@ Date: 2026-06-08. - Android release builds now fail fast when release signing variables are missing, so unsigned release artifacts cannot be published by accident. - Android version was raised to `1.3.12` with `versionCode` `40`. - Android now has QSfera-specific Argus publishing scripts: local upload-keystore generation, signed APK staging, APK metadata verification, debug-certificate rejection, and Argus upload. +- Android Argus staging now enforces release git provenance by default: the work tree must be clean, the branch must have an upstream, `git fetch` must succeed, and `HEAD` must match `@{u}` before a stable APK is prepared for upload. The generated `argus-publication.json` records the verified repository root, branch, upstream, commit, and timestamp; current monorepo publication can verify the pushed root `QSfera.git` branch by running from `Android/` with `-GitProvenanceRoot ..`. - Android `1.3.9`/`37` was published to Argus as `qsfera-mobile` on 2026-06-10. Argus upload returned release id `53ecc24e-106b-43a5-a39e-ffacd72a43f8`, package size `12226572`, and SHA-256 `cbd2c25328e395ea4da4e0f582feac39dec18d2d47b3e105d344904d8a24f017`. - Android `1.3.10`/`38` was published to Argus as `qsfera-mobile` on 2026-06-10. Argus upload returned release id `8465ba6d-a008-45f5-9c70-e6cd2c94317a`, package size `12228464`, and SHA-256 `ce33a73ce122de6c9c797e7cb552e5493b628c60ddc98fda6a9e230e3c1ed7fe`. - Android `1.3.11`/`39` was published to Argus as `qsfera-mobile` on 2026-06-10. Argus upload returned release id `3dbac5ad-b19c-46e6-b24f-8f22e1c466dd`, package size `12234884`, and SHA-256 `c9eae41bbaae421e1a69721e891d8431ce599684e0f655b71693a422aa83720a`.