232 lines
8.3 KiB
PowerShell
232 lines
8.3 KiB
PowerShell
param(
|
|
[string]$SshHost = $(if ($env:ARGUS_SSH_HOST) { $env:ARGUS_SSH_HOST } else { "192.168.0.185" }),
|
|
[string]$SshUser = $(if ($env:ARGUS_SSH_USER) { $env:ARGUS_SSH_USER } else { "sevenhill" }),
|
|
[string]$ArgusBaseUrl = $(if ($env:ARGUS_BASE_URL) { $env:ARGUS_BASE_URL } else { "https://argus.kusoft.xyz" }),
|
|
[string]$ArgusData = $(if ($env:ARGUS_DATA) { $env:ARGUS_DATA } else { "/srv/argus-data" }),
|
|
[string]$ApkPath = "keeperApp\build\outputs\apk\debug\keeperApp-debug.apk",
|
|
[string]$Slug = "keeper-android",
|
|
[string]$Name = "QKeep",
|
|
[string]$Summary = "Cloud notes app for QSfera with local storage and sync.",
|
|
[string]$Description = "QKeep stores notes locally on Android, uses QSfera browser authorization, syncs notes with the Keeper REST service, and installs SHA-256 verified Argus updates.",
|
|
[string]$RepositoryUrl = "",
|
|
[string]$HomepageUrl = "",
|
|
[string]$Version = "0.1.22",
|
|
[string]$Channel = "stable",
|
|
[string]$Platform = "android",
|
|
[string]$PackageKind = "apk",
|
|
[string]$Notes = "Aligns the Android updater and publication flow with ARGUS_PUBLICATION.md."
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
function ConvertTo-Base64Utf8([string]$Value) {
|
|
return [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Value))
|
|
}
|
|
|
|
function Assert-LastExitCode([string]$Action) {
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "$Action failed with exit code $LASTEXITCODE."
|
|
}
|
|
}
|
|
|
|
$resolvedApk = (Resolve-Path -LiteralPath $ApkPath).Path
|
|
$sshTarget = "$SshUser@$SshHost"
|
|
$remoteFile = "/tmp/$Slug-$Version.apk"
|
|
$manifestUrl = "$($ArgusBaseUrl.TrimEnd("/"))/api/apps/$Slug/manifest?platform=$Platform&channel=$Channel"
|
|
|
|
& scp "$resolvedApk" "${sshTarget}:$remoteFile"
|
|
Assert-LastExitCode "Copy APK to Argus host"
|
|
|
|
$nameB64 = ConvertTo-Base64Utf8 $Name
|
|
$summaryB64 = ConvertTo-Base64Utf8 $Summary
|
|
$descriptionB64 = ConvertTo-Base64Utf8 $Description
|
|
$repositoryUrlB64 = ConvertTo-Base64Utf8 $RepositoryUrl
|
|
$homepageUrlB64 = ConvertTo-Base64Utf8 $HomepageUrl
|
|
$notesB64 = ConvertTo-Base64Utf8 $Notes
|
|
|
|
$remoteScript = @"
|
|
set -euo pipefail
|
|
|
|
export ARGUS_DATA="$ArgusData"
|
|
export SOURCE_FILE="$remoteFile"
|
|
|
|
export ARGUS_SLUG="$Slug"
|
|
export ARGUS_NAME="`$(printf '%s' '$nameB64' | base64 -d)"
|
|
export ARGUS_SUMMARY="`$(printf '%s' '$summaryB64' | base64 -d)"
|
|
export ARGUS_DESCRIPTION="`$(printf '%s' '$descriptionB64' | base64 -d)"
|
|
export ARGUS_REPOSITORY_URL="`$(printf '%s' '$repositoryUrlB64' | base64 -d)"
|
|
export ARGUS_HOMEPAGE_URL="`$(printf '%s' '$homepageUrlB64' | base64 -d)"
|
|
export ARGUS_IS_LISTED="1"
|
|
|
|
export ARGUS_VERSION="$Version"
|
|
export ARGUS_CHANNEL="$Channel"
|
|
export ARGUS_PLATFORM="$Platform"
|
|
export ARGUS_PACKAGE_KIND="$PackageKind"
|
|
export ARGUS_NOTES="`$(printf '%s' '$notesB64' | base64 -d)"
|
|
|
|
trap 'rm -f "$SOURCE_FILE"' EXIT
|
|
|
|
python3 - "`$SOURCE_FILE" <<'PY'
|
|
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=$ArgusBaseUrl/api/apps/{slug}/manifest?platform={platform}&channel={channel}")
|
|
PY
|
|
|
|
curl -sS "http://127.0.0.1:5105/api/apps/`$ARGUS_SLUG/manifest?platform=`$ARGUS_PLATFORM&channel=`$ARGUS_CHANNEL" >/dev/null
|
|
"@
|
|
|
|
$remoteScript | & ssh "$sshTarget" "bash -s"
|
|
Assert-LastExitCode "Publish release on Argus host"
|
|
|
|
$manifest = Invoke-RestMethod -Uri $manifestUrl -Headers @{ Accept = "application/json" }
|
|
if ($manifest.release.version -ne $Version) {
|
|
throw "Argus manifest verification failed: expected version $Version, got $($manifest.release.version)."
|
|
}
|
|
|
|
Write-Output "Published $Slug $Version to $manifestUrl"
|