This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
param(
|
||||
[string]$ManifestPath,
|
||||
[string]$SshHost = "192.168.0.185",
|
||||
[string]$SshUser = "sevenhill",
|
||||
[string]$ArgusDataPath = "/srv/argus-data",
|
||||
[switch]$RestartArgus
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Resolve-ProjectRoot {
|
||||
return (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
}
|
||||
|
||||
function Resolve-LatestManifestPath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ProjectRoot
|
||||
)
|
||||
|
||||
$argusRoot = Join-Path (Join-Path $ProjectRoot "artifacts") "argus"
|
||||
if (-not (Test-Path -LiteralPath $argusRoot)) {
|
||||
throw "Argus artifacts directory not found: $argusRoot"
|
||||
}
|
||||
|
||||
$latest = Get-ChildItem -LiteralPath $argusRoot -Recurse -Filter "argus-release.json" -File |
|
||||
Sort-Object LastWriteTimeUtc -Descending |
|
||||
Select-Object -First 1
|
||||
if (-not $latest) {
|
||||
throw "No argus-release.json files found under $argusRoot"
|
||||
}
|
||||
|
||||
return $latest.FullName
|
||||
}
|
||||
|
||||
function Assert-RequiredString {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object]$Manifest,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PropertyName
|
||||
)
|
||||
|
||||
if (-not ($Manifest.PSObject.Properties.Name -contains $PropertyName)) {
|
||||
throw "Argus manifest is missing required property: $PropertyName"
|
||||
}
|
||||
|
||||
$value = [string]$Manifest.$PropertyName
|
||||
if ([string]::IsNullOrWhiteSpace($value)) {
|
||||
throw "Argus manifest property is blank: $PropertyName"
|
||||
}
|
||||
|
||||
return $value
|
||||
}
|
||||
|
||||
function ConvertTo-BashSingleQuoted {
|
||||
param([AllowNull()][string]$Value)
|
||||
|
||||
if ($null -eq $Value) {
|
||||
return "''"
|
||||
}
|
||||
|
||||
return "'" + ($Value -replace "'", "'\''") + "'"
|
||||
}
|
||||
|
||||
function Get-PublicCatalogFlag {
|
||||
param([object]$Manifest)
|
||||
|
||||
if ($Manifest.PSObject.Properties.Name -contains "publicCatalog") {
|
||||
return if ([bool]$Manifest.publicCatalog) { "1" } else { "0" }
|
||||
}
|
||||
|
||||
return "1"
|
||||
}
|
||||
|
||||
function Invoke-CheckedNativeCommand {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$Arguments
|
||||
)
|
||||
|
||||
& $FilePath @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$FilePath exited with code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
|
||||
$projectRoot = Resolve-ProjectRoot
|
||||
$resolvedManifestPath = if ($ManifestPath) {
|
||||
(Resolve-Path -LiteralPath $ManifestPath).Path
|
||||
}
|
||||
else {
|
||||
Resolve-LatestManifestPath -ProjectRoot $projectRoot
|
||||
}
|
||||
|
||||
$manifestDirectory = Split-Path -Parent $resolvedManifestPath
|
||||
$manifest = Get-Content -LiteralPath $resolvedManifestPath -Raw | ConvertFrom-Json
|
||||
|
||||
$slug = Assert-RequiredString -Manifest $manifest -PropertyName "slug"
|
||||
$name = Assert-RequiredString -Manifest $manifest -PropertyName "name"
|
||||
$summary = Assert-RequiredString -Manifest $manifest -PropertyName "summary"
|
||||
$description = Assert-RequiredString -Manifest $manifest -PropertyName "description"
|
||||
$version = Assert-RequiredString -Manifest $manifest -PropertyName "version"
|
||||
$channel = Assert-RequiredString -Manifest $manifest -PropertyName "channel"
|
||||
$platform = Assert-RequiredString -Manifest $manifest -PropertyName "platform"
|
||||
$packageKind = Assert-RequiredString -Manifest $manifest -PropertyName "packageKind"
|
||||
$packageFile = Assert-RequiredString -Manifest $manifest -PropertyName "packageFile"
|
||||
$packageSha256 = Assert-RequiredString -Manifest $manifest -PropertyName "packageSha256"
|
||||
$releaseNotes = if ($manifest.PSObject.Properties.Name -contains "releaseNotes") { [string]$manifest.releaseNotes } else { "" }
|
||||
$repositoryUrl = if ($manifest.PSObject.Properties.Name -contains "repositoryUrl") { [string]$manifest.repositoryUrl } else { "" }
|
||||
$homepageUrl = if ($manifest.PSObject.Properties.Name -contains "homepageUrl") { [string]$manifest.homepageUrl } else { "" }
|
||||
$isListed = Get-PublicCatalogFlag -Manifest $manifest
|
||||
|
||||
if ($slug -notmatch "^[a-z0-9][a-z0-9-]{0,99}$") {
|
||||
throw "Argus slug must contain only lowercase letters, digits, and hyphens, max 100 chars: $slug"
|
||||
}
|
||||
if ($packageSha256 -notmatch "^[0-9a-f]{64}$") {
|
||||
throw "packageSha256 must be a lowercase SHA-256 hex digest: $packageSha256"
|
||||
}
|
||||
|
||||
$packagePath = Join-Path $manifestDirectory $packageFile
|
||||
if (-not (Test-Path -LiteralPath $packagePath)) {
|
||||
throw "Package from Argus manifest not found: $packagePath"
|
||||
}
|
||||
if ([IO.Path]::GetFileName($packageFile) -ne $packageFile) {
|
||||
throw "packageFile must be a file name without directories: $packageFile"
|
||||
}
|
||||
|
||||
$actualSha256 = ((Get-FileHash -LiteralPath $packagePath -Algorithm SHA256).Hash).ToLowerInvariant()
|
||||
if ($actualSha256 -ne $packageSha256) {
|
||||
throw "Package SHA-256 mismatch. Manifest: $packageSha256 Actual: $actualSha256"
|
||||
}
|
||||
|
||||
$remoteTarget = "$SshUser@$SshHost"
|
||||
$remoteSourceFile = "/tmp/$packageFile"
|
||||
|
||||
Invoke-CheckedNativeCommand -FilePath "scp" -Arguments @(
|
||||
$packagePath,
|
||||
"$remoteTarget`:$remoteSourceFile"
|
||||
)
|
||||
|
||||
$pythonBlock = @'
|
||||
import hashlib
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sqlite3
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
source = Path(sys.argv[1]).resolve()
|
||||
data_root = Path(os.environ.get("ARGUS_DATA", "/srv/argus-data")).resolve()
|
||||
db_path = data_root / "argus.db"
|
||||
packages_root = data_root / "Packages"
|
||||
|
||||
slug = os.environ["ARGUS_SLUG"].strip()
|
||||
name = os.environ["ARGUS_NAME"].strip()
|
||||
summary = os.environ["ARGUS_SUMMARY"].strip()
|
||||
description = os.environ["ARGUS_DESCRIPTION"].strip()
|
||||
repository_url = os.environ.get("ARGUS_REPOSITORY_URL", "").strip() or None
|
||||
homepage_url = os.environ.get("ARGUS_HOMEPAGE_URL", "").strip() or None
|
||||
is_listed = 1 if os.environ.get("ARGUS_IS_LISTED", "1").strip() != "0" else 0
|
||||
|
||||
version = os.environ["ARGUS_VERSION"].strip()
|
||||
channel = os.environ.get("ARGUS_CHANNEL", "stable").strip().lower()
|
||||
platform = os.environ.get("ARGUS_PLATFORM", "generic").strip().lower()
|
||||
package_kind = os.environ.get("ARGUS_PACKAGE_KIND", "binary").strip().lower()
|
||||
notes = os.environ.get("ARGUS_NOTES", "").strip() or None
|
||||
|
||||
if not re.fullmatch(r"[a-z0-9][a-z0-9-]{0,99}", slug):
|
||||
raise SystemExit("ARGUS_SLUG must contain only lowercase letters, digits, and hyphens, max 100 chars.")
|
||||
if not source.is_file():
|
||||
raise SystemExit(f"SOURCE_FILE does not exist: {source}")
|
||||
if not db_path.is_file():
|
||||
raise SystemExit(f"Argus database does not exist: {db_path}")
|
||||
for key, value in {
|
||||
"ARGUS_NAME": name,
|
||||
"ARGUS_SUMMARY": summary,
|
||||
"ARGUS_DESCRIPTION": description,
|
||||
"ARGUS_VERSION": version,
|
||||
}.items():
|
||||
if not value:
|
||||
raise SystemExit(f"{key} is required.")
|
||||
|
||||
release_id = str(uuid.uuid4()).upper()
|
||||
now = datetime.now(timezone.utc).isoformat(timespec="microseconds")
|
||||
safe_version = re.sub(r"[^a-zA-Z0-9._-]+", "-", version).strip("-._") or "release"
|
||||
extension = source.suffix or ".bin"
|
||||
stored_name = f"{datetime.now(timezone.utc):%Y%m%d%H%M%S}-{safe_version}-{release_id.replace('-', '')}{extension}"
|
||||
stored_relative_path = f"{slug}/{stored_name}"
|
||||
target_dir = packages_root / slug
|
||||
target_path = target_dir / stored_name
|
||||
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source, target_path)
|
||||
|
||||
size_bytes = target_path.stat().st_size
|
||||
sha256 = hashlib.sha256(target_path.read_bytes()).hexdigest()
|
||||
content_type = mimetypes.guess_type(source.name)[0] or "application/octet-stream"
|
||||
if source.suffix.lower() == ".apk":
|
||||
content_type = "application/vnd.android.package-archive"
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
try:
|
||||
conn.execute("PRAGMA foreign_keys = ON")
|
||||
conn.execute("BEGIN")
|
||||
|
||||
row = conn.execute('SELECT "Id" FROM "Apps" WHERE "Slug" = ?', (slug,)).fetchone()
|
||||
if row is None:
|
||||
app_id = str(uuid.uuid4()).upper()
|
||||
conn.execute(
|
||||
'''
|
||||
INSERT INTO "Apps"
|
||||
("Id", "Slug", "Name", "Summary", "Description", "RepositoryUrl", "HomepageUrl", "IsListed", "CreatedAt", "UpdatedAt")
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''',
|
||||
(app_id, slug, name, summary, description, repository_url, homepage_url, is_listed, now, now),
|
||||
)
|
||||
else:
|
||||
app_id = row[0]
|
||||
conn.execute(
|
||||
'''
|
||||
UPDATE "Apps"
|
||||
SET "Name" = ?,
|
||||
"Summary" = ?,
|
||||
"Description" = ?,
|
||||
"RepositoryUrl" = ?,
|
||||
"HomepageUrl" = ?,
|
||||
"IsListed" = ?,
|
||||
"UpdatedAt" = ?
|
||||
WHERE "Id" = ?
|
||||
''',
|
||||
(name, summary, description, repository_url, homepage_url, is_listed, now, app_id),
|
||||
)
|
||||
|
||||
duplicate = conn.execute(
|
||||
'''
|
||||
SELECT "Id"
|
||||
FROM "Releases"
|
||||
WHERE "CatalogAppId" = ? AND "Version" = ? AND "Channel" = ? AND "Platform" = ?
|
||||
''',
|
||||
(app_id, version, channel, platform),
|
||||
).fetchone()
|
||||
if duplicate is not None:
|
||||
raise RuntimeError(f"Release already exists for {slug} {version} {channel} {platform}.")
|
||||
|
||||
conn.execute(
|
||||
'''
|
||||
INSERT INTO "Releases"
|
||||
("Id", "CatalogAppId", "Version", "Channel", "Platform", "PackageKind",
|
||||
"OriginalFileName", "StoredRelativePath", "ContentType", "PackageSizeBytes",
|
||||
"Sha256", "Notes", "PublishedAt")
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''',
|
||||
(
|
||||
release_id,
|
||||
app_id,
|
||||
version,
|
||||
channel,
|
||||
platform,
|
||||
package_kind,
|
||||
source.name,
|
||||
stored_relative_path,
|
||||
content_type,
|
||||
size_bytes,
|
||||
sha256,
|
||||
notes,
|
||||
now,
|
||||
),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
try:
|
||||
target_path.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
print(f"Published app={slug} version={version} channel={channel} platform={platform}")
|
||||
print(f"StoredRelativePath={stored_relative_path}")
|
||||
print(f"Size={size_bytes}")
|
||||
print(f"Sha256={sha256}")
|
||||
print(f"Manifest=https://argus.kusoft.xyz/api/apps/{slug}/manifest?platform={platform}&channel={channel}")
|
||||
'@
|
||||
|
||||
$remoteScriptLines = @(
|
||||
"set -euo pipefail",
|
||||
"export ARGUS_DATA=$(ConvertTo-BashSingleQuoted $ArgusDataPath)",
|
||||
"export SOURCE_FILE=$(ConvertTo-BashSingleQuoted $remoteSourceFile)",
|
||||
"export ARGUS_SLUG=$(ConvertTo-BashSingleQuoted $slug)",
|
||||
"export ARGUS_NAME=$(ConvertTo-BashSingleQuoted $name)",
|
||||
"export ARGUS_SUMMARY=$(ConvertTo-BashSingleQuoted $summary)",
|
||||
"export ARGUS_DESCRIPTION=$(ConvertTo-BashSingleQuoted $description)",
|
||||
"export ARGUS_REPOSITORY_URL=$(ConvertTo-BashSingleQuoted $repositoryUrl)",
|
||||
"export ARGUS_HOMEPAGE_URL=$(ConvertTo-BashSingleQuoted $homepageUrl)",
|
||||
"export ARGUS_IS_LISTED=$(ConvertTo-BashSingleQuoted $isListed)",
|
||||
"export ARGUS_VERSION=$(ConvertTo-BashSingleQuoted $version)",
|
||||
"export ARGUS_CHANNEL=$(ConvertTo-BashSingleQuoted $channel)",
|
||||
"export ARGUS_PLATFORM=$(ConvertTo-BashSingleQuoted $platform)",
|
||||
"export ARGUS_PACKAGE_KIND=$(ConvertTo-BashSingleQuoted $packageKind)",
|
||||
"export ARGUS_NOTES=$(ConvertTo-BashSingleQuoted $releaseNotes)",
|
||||
'python3 - "$SOURCE_FILE" <<''PY''',
|
||||
$pythonBlock,
|
||||
"PY",
|
||||
'curl -sS "http://127.0.0.1:5105/api/apps/$ARGUS_SLUG/manifest?platform=$ARGUS_PLATFORM&channel=$ARGUS_CHANNEL"',
|
||||
""
|
||||
)
|
||||
|
||||
if ($RestartArgus) {
|
||||
$remoteScriptLines += "docker restart argus"
|
||||
}
|
||||
|
||||
$remoteScript = $remoteScriptLines -join [Environment]::NewLine
|
||||
$tempRemoteScript = New-TemporaryFile
|
||||
try {
|
||||
Set-Content -LiteralPath $tempRemoteScript -Value $remoteScript -Encoding utf8
|
||||
Get-Content -LiteralPath $tempRemoteScript -Raw | & ssh $remoteTarget "bash" "-s"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "ssh exited with code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item -LiteralPath $tempRemoteScript -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
$publicManifestUrl = "https://argus.kusoft.xyz/api/apps/$slug/manifest?platform=$platform&channel=$channel"
|
||||
$publicManifest = Invoke-RestMethod -Uri $publicManifestUrl -TimeoutSec 30
|
||||
if ([string]$publicManifest.release.version -ne $version) {
|
||||
throw "Public Argus manifest version mismatch. Expected $version, got $($publicManifest.release.version)."
|
||||
}
|
||||
if ([string]$publicManifest.release.sha256 -ne $packageSha256) {
|
||||
throw "Public Argus manifest SHA-256 mismatch. Expected $packageSha256, got $($publicManifest.release.sha256)."
|
||||
}
|
||||
|
||||
Write-Host "Argus publication verified:"
|
||||
Write-Host "Manifest: $publicManifestUrl"
|
||||
Write-Host "Version: $($publicManifest.release.version)"
|
||||
Write-Host "SHA-256: $($publicManifest.release.sha256)"
|
||||
@@ -0,0 +1,111 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
$layoutRoot = Join-Path $repoRoot "app/src/main/res/layout"
|
||||
$valuesRoot = Join-Path $repoRoot "app/src/main/res/values"
|
||||
$themesPath = Join-Path $valuesRoot "themes.xml"
|
||||
$failures = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
function Add-Failure([string] $message) {
|
||||
$failures.Add($message) | Out-Null
|
||||
}
|
||||
|
||||
function Get-XmlLine([string] $content, [int] $index) {
|
||||
if ($index -lt 0) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return (($content.Substring(0, $index) -split "`n").Count)
|
||||
}
|
||||
|
||||
function Get-AttributeValue([string] $block, [string] $attributeName) {
|
||||
$escapedName = [regex]::Escape($attributeName)
|
||||
$match = [regex]::Match($block, "$escapedName=""([^""]+)""")
|
||||
if ($match.Success) {
|
||||
return $match.Groups[1].Value
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Assert-MinDp([string] $value, [int] $minimum, [string] $where, [string] $attributeName) {
|
||||
if ([string]::IsNullOrWhiteSpace($value)) {
|
||||
Add-Failure "$where is missing $attributeName"
|
||||
return
|
||||
}
|
||||
|
||||
if ($value -notmatch "^([0-9]+)dp$") {
|
||||
Add-Failure "$where has non-fixed $attributeName='$value'"
|
||||
return
|
||||
}
|
||||
|
||||
$actual = [int] $Matches[1]
|
||||
if ($actual -lt $minimum) {
|
||||
Add-Failure "$where has $attributeName=${actual}dp, expected at least ${minimum}dp"
|
||||
}
|
||||
}
|
||||
|
||||
$xmlFiles = Get-ChildItem -Path @($layoutRoot, $valuesRoot) -Filter "*.xml" -Recurse
|
||||
foreach ($file in $xmlFiles) {
|
||||
$content = Get-Content $file.FullName -Raw -Encoding UTF8
|
||||
$relativePath = Resolve-Path -Relative $file.FullName
|
||||
|
||||
$textSizeDpMatches = [regex]::Matches($content, 'android:textSize="[0-9.]+dp"')
|
||||
foreach ($match in $textSizeDpMatches) {
|
||||
$line = Get-XmlLine $content $match.Index
|
||||
Add-Failure "${relativePath}:$line uses dp for textSize; use sp"
|
||||
}
|
||||
|
||||
$materialButtons = [regex]::Matches($content, '(?s)<com\.google\.android\.material\.button\.MaterialButton\b.*?/>')
|
||||
foreach ($match in $materialButtons) {
|
||||
$line = Get-XmlLine $content $match.Index
|
||||
$where = "${relativePath}:$line MaterialButton"
|
||||
$block = $match.Value
|
||||
|
||||
if ($block -match 'android:minWidth="0dp"') {
|
||||
Add-Failure "$where resets minWidth to 0dp"
|
||||
}
|
||||
|
||||
$minHeight = Get-AttributeValue $block "android:minHeight"
|
||||
if (-not [string]::IsNullOrWhiteSpace($minHeight)) {
|
||||
Assert-MinDp $minHeight 48 $where "android:minHeight"
|
||||
}
|
||||
}
|
||||
|
||||
$imageButtons = [regex]::Matches($content, '(?s)<ImageButton\b.*?/>')
|
||||
foreach ($match in $imageButtons) {
|
||||
$line = Get-XmlLine $content $match.Index
|
||||
$where = "${relativePath}:$line ImageButton"
|
||||
$block = $match.Value
|
||||
|
||||
Assert-MinDp (Get-AttributeValue $block "android:layout_width") 48 $where "android:layout_width"
|
||||
Assert-MinDp (Get-AttributeValue $block "android:layout_height") 48 $where "android:layout_height"
|
||||
}
|
||||
}
|
||||
|
||||
$themesContent = Get-Content $themesPath -Raw -Encoding UTF8
|
||||
foreach ($styleName in @("Primary", "Secondary", "Tonal")) {
|
||||
$stylePattern = "(?s)<style\s+name=""Widget\.Aletheia\.Button\.$styleName"".*?</style>"
|
||||
$styleMatch = [regex]::Match($themesContent, $stylePattern)
|
||||
if (-not $styleMatch.Success) {
|
||||
Add-Failure "themes.xml is missing Widget.Aletheia.Button.$styleName"
|
||||
continue
|
||||
}
|
||||
|
||||
$heightMatch = [regex]::Match($styleMatch.Value, '<item\s+name="android:minHeight">([0-9]+)dp</item>')
|
||||
if (-not $heightMatch.Success) {
|
||||
Add-Failure "Widget.Aletheia.Button.$styleName is missing android:minHeight"
|
||||
continue
|
||||
}
|
||||
|
||||
$height = [int] $heightMatch.Groups[1].Value
|
||||
if ($height -lt 48) {
|
||||
Add-Failure "Widget.Aletheia.Button.$styleName has android:minHeight=${height}dp, expected at least 48dp"
|
||||
}
|
||||
}
|
||||
|
||||
if ($failures.Count -gt 0) {
|
||||
Write-Error ("Accessibility layout smoke test failed:`n" + ($failures -join "`n"))
|
||||
}
|
||||
|
||||
Write-Host "Accessibility layout smoke test passed."
|
||||
@@ -0,0 +1,129 @@
|
||||
param(
|
||||
[string]$ManifestPath
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Resolve-ProjectRoot {
|
||||
return (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
}
|
||||
|
||||
function Resolve-LatestManifestPath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ProjectRoot
|
||||
)
|
||||
|
||||
$argusRoot = Join-Path (Join-Path $ProjectRoot "artifacts") "argus"
|
||||
if (-not (Test-Path -LiteralPath $argusRoot)) {
|
||||
throw "Argus artifacts directory not found: $argusRoot"
|
||||
}
|
||||
|
||||
$manifests = Get-ChildItem -LiteralPath $argusRoot -Recurse -Filter "argus-release.json" -File |
|
||||
ForEach-Object {
|
||||
$versionText = Split-Path -Leaf (Split-Path -Parent $_.FullName)
|
||||
$version = $null
|
||||
if (-not [version]::TryParse($versionText, [ref]$version)) {
|
||||
throw "Argus manifest directory is not a version: $($_.FullName)"
|
||||
}
|
||||
[pscustomobject]@{
|
||||
Version = $version
|
||||
Path = $_.FullName
|
||||
}
|
||||
} |
|
||||
Sort-Object Version -Descending
|
||||
|
||||
$latest = $manifests | Select-Object -First 1
|
||||
if (-not $latest) {
|
||||
throw "No argus-release.json files found under $argusRoot"
|
||||
}
|
||||
|
||||
return $latest.Path
|
||||
}
|
||||
|
||||
function Assert-RequiredString {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[object]$Manifest,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PropertyName
|
||||
)
|
||||
|
||||
if (-not ($Manifest.PSObject.Properties.Name -contains $PropertyName)) {
|
||||
throw "Argus manifest is missing required property: $PropertyName"
|
||||
}
|
||||
|
||||
$value = [string]$Manifest.$PropertyName
|
||||
if ([string]::IsNullOrWhiteSpace($value)) {
|
||||
throw "Argus manifest property is blank: $PropertyName"
|
||||
}
|
||||
|
||||
return $value
|
||||
}
|
||||
|
||||
$projectRoot = Resolve-ProjectRoot
|
||||
$resolvedManifestPath = if ($ManifestPath) {
|
||||
(Resolve-Path -LiteralPath $ManifestPath).Path
|
||||
}
|
||||
else {
|
||||
Resolve-LatestManifestPath -ProjectRoot $projectRoot
|
||||
}
|
||||
|
||||
$manifestDirectory = Split-Path -Parent $resolvedManifestPath
|
||||
$manifest = Get-Content -LiteralPath $resolvedManifestPath -Raw | ConvertFrom-Json
|
||||
|
||||
$slug = Assert-RequiredString -Manifest $manifest -PropertyName "slug"
|
||||
$name = Assert-RequiredString -Manifest $manifest -PropertyName "name"
|
||||
$version = Assert-RequiredString -Manifest $manifest -PropertyName "version"
|
||||
$channel = Assert-RequiredString -Manifest $manifest -PropertyName "channel"
|
||||
$platform = Assert-RequiredString -Manifest $manifest -PropertyName "platform"
|
||||
$packageKind = Assert-RequiredString -Manifest $manifest -PropertyName "packageKind"
|
||||
$packageFile = Assert-RequiredString -Manifest $manifest -PropertyName "packageFile"
|
||||
$packageSha256 = Assert-RequiredString -Manifest $manifest -PropertyName "packageSha256"
|
||||
$signerInfo = Assert-RequiredString -Manifest $manifest -PropertyName "signerInfo"
|
||||
|
||||
if ($platform -ne "android") {
|
||||
throw "Expected platform android, got: $platform"
|
||||
}
|
||||
if ($packageKind -ne "apk") {
|
||||
throw "Expected packageKind apk, got: $packageKind"
|
||||
}
|
||||
if ($packageFile -notmatch '\.apk$') {
|
||||
throw "packageFile must point to an APK: $packageFile"
|
||||
}
|
||||
if ($packageSha256 -notmatch '^[0-9a-f]{64}$') {
|
||||
throw "packageSha256 must be a lowercase SHA-256 hex digest: $packageSha256"
|
||||
}
|
||||
if ($signerInfo -notmatch "Signer #1 certificate SHA-256 digest:") {
|
||||
throw "signerInfo does not contain signer SHA-256 digest."
|
||||
}
|
||||
|
||||
$pathVersion = Split-Path -Leaf $manifestDirectory
|
||||
if ($version -ne $pathVersion) {
|
||||
throw "Manifest version '$version' does not match artifact directory '$pathVersion'."
|
||||
}
|
||||
|
||||
$apkPath = Join-Path $manifestDirectory $packageFile
|
||||
if (-not (Test-Path -LiteralPath $apkPath)) {
|
||||
throw "APK from Argus manifest not found: $apkPath"
|
||||
}
|
||||
|
||||
$idsigPath = $apkPath + ".idsig"
|
||||
if (-not (Test-Path -LiteralPath $idsigPath)) {
|
||||
throw "APK idsig file not found: $idsigPath"
|
||||
}
|
||||
|
||||
$actualSha256 = ((Get-FileHash -LiteralPath $apkPath -Algorithm SHA256).Hash).ToLowerInvariant()
|
||||
if ($actualSha256 -ne $packageSha256) {
|
||||
throw "APK SHA-256 mismatch. Manifest: $packageSha256 Actual: $actualSha256"
|
||||
}
|
||||
|
||||
Write-Host "Argus manifest smoke test passed:"
|
||||
Write-Host "Manifest: $resolvedManifestPath"
|
||||
Write-Host "Slug: $slug"
|
||||
Write-Host "Name: $name"
|
||||
Write-Host "Version: $version"
|
||||
Write-Host "Channel: $channel"
|
||||
Write-Host "Package: $apkPath"
|
||||
Write-Host "SHA-256: $actualSha256"
|
||||
Reference in New Issue
Block a user