# Argus Publication And Update Integration Эта инструкция является единой точкой входа для любого приложения, которое нужно публиковать через Argus или подключить к системе обновлений Argus. Argus в текущей архитектуре является read-only web catalog. У него нет HTTP admin API, upload endpoint, токена публикации или браузерной формы публикации. Публикация выполняется только на сервере через SSH: файл релиза кладётся в `Data/Packages`, а метаданные записываются в SQLite `Data/argus.db`. ## Production Argus - Public base URL: `https://argus.kusoft.xyz` - SSH host: `192.168.0.185` - SSH user: `sevenhill` - Server project path: `/home/sevenhill/argus` - Data path: `/srv/argus-data` - SQLite database: `/srv/argus-data/argus.db` - Package root: `/srv/argus-data/Packages` Do not put SSH passwords, private keys, or local `.env` files into application repositories. ## App Values Every app must choose stable values: - `slug`: public Argus id. Use lowercase letters, digits, and hyphens only, for example `keeper-android` or `my-desktop-app`. - `name`: display name. - `summary`: short catalog text. - `description`: full catalog text. - `version`: release version. Semantic versions are recommended, for example `1.2.3`. - `channel`: release channel, normally `stable`. - `platform`: target platform, for example `windows-x64`, `linux-arm64`, `android`, `web`. - `packageKind`: package type, for example `zip`, `msi`, `deb`, `apk`, `binary`. Argus selects the latest release by semantic version first, then by publish time. The running service currently prunes to one retained release per app on service startup. ## Publish A Release Over SSH From the development machine, copy the built artifact to the Pi: ```bash scp ./path/to/my-app-1.2.3.zip sevenhill@192.168.0.185:/tmp/my-app-1.2.3.zip ``` Then connect to the Pi: ```bash ssh sevenhill@192.168.0.185 ``` Run this publication block on the Pi after editing the variables at the top: ```bash set -euo pipefail export ARGUS_DATA="/srv/argus-data" export SOURCE_FILE="/tmp/my-app-1.2.3.zip" export ARGUS_SLUG="my-app" export ARGUS_NAME="My App" export ARGUS_SUMMARY="Short summary for the Argus catalog." export ARGUS_DESCRIPTION="Full description shown in the Argus catalog." export ARGUS_REPOSITORY_URL="" export ARGUS_HOMEPAGE_URL="" export ARGUS_IS_LISTED="1" export ARGUS_VERSION="1.2.3" export ARGUS_CHANNEL="stable" export ARGUS_PLATFORM="windows-x64" export ARGUS_PACKAGE_KIND="zip" export ARGUS_NOTES="What changed in this release." 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=https://argus.kusoft.xyz/api/apps/{slug}/manifest?platform={platform}&channel={channel}") PY ``` Verify publication from the Pi: ```bash curl -sS "http://127.0.0.1:5105/api/apps/$ARGUS_SLUG/manifest?platform=$ARGUS_PLATFORM&channel=$ARGUS_CHANNEL" ``` Verify publication from outside the Pi: ```bash curl -sS "https://argus.kusoft.xyz/api/apps/$ARGUS_SLUG/manifest?platform=$ARGUS_PLATFORM&channel=$ARGUS_CHANNEL" ``` If you want Argus to apply its one-release retention rule immediately after manual publication, restart the container: ```bash docker restart argus ``` The restart is optional for making a newer semantic version visible. It is only needed to force startup pruning immediately. ## Public API Contract For Update Clients Update clients must use the public read-only API. They must not call `/api/admin/*`; those routes are not part of this system. Check for an update: ```text GET https://argus.kusoft.xyz/api/apps/{slug}/manifest?platform={platform}&channel={channel} ``` A `404` response means one of these states: - the app slug is not listed; - there is no release for the requested `platform`; - there is no release for the requested `channel`. A successful manifest response contains: ```json { "slug": "my-app", "name": "My App", "summary": "Short summary.", "description": "Full description.", "repositoryUrl": null, "homepageUrl": null, "release": { "id": "release-guid", "version": "1.2.3", "channel": "stable", "platform": "windows-x64", "packageKind": "zip", "downloadPath": "/api/apps/my-app/releases/release-guid/download", "originalFileName": "my-app-1.2.3.zip", "contentType": "application/zip", "packageSizeBytes": 123456, "sha256": "hex-encoded-sha256", "publishedAt": "2026-06-13T18:00:00+00:00", "notes": "What changed." } } ``` Download URL: ```text https://argus.kusoft.xyz{release.downloadPath} ``` There is also a latest-download endpoint: ```text GET https://argus.kusoft.xyz/api/apps/{slug}/download/latest?platform={platform}&channel={channel} ``` The manifest path is preferred for update clients because it provides `sha256`, `packageSizeBytes`, `version`, and release metadata before downloading. ## Required Update Client Behavior Every app integrating with Argus must implement this flow: 1. Store its current installed version locally. 2. Request the manifest for its `slug`, `platform`, and `channel`. 3. Treat HTTP `404` as "no update available for this app/platform/channel". 4. Compare local version to `release.version`. 5. If update is needed, download `baseUrl + release.downloadPath` to a temporary file. 6. Calculate SHA-256 of the downloaded file. 7. Compare the calculated SHA-256 to `release.sha256`. 8. Reject and delete the temporary file if SHA-256 does not match. 9. Apply the update with the app platform's own installer or replacement mechanism. 10. Store the new installed version only after a successful install or successful staged update. Argus does not install files on client devices. It only publishes metadata and serves package bytes. Installation logic belongs to each app. ## Versioning Rule Use semantic versions when possible: ```text 1.2.3 1.2.4 1.3.0 2.0.0 ``` Argus latest selection is semantic-version aware. If an app uses a non-semantic version string, Argus falls back to case-insensitive string comparison for ordering. For predictable updates, keep release versions semantic. ## Recommended Prompt For App Repositories When asking another app project to integrate with Argus, use this instruction: ```text Read ARGUS_PUBLICATION.md from the Argus repository. Implement the "Required Update Client Behavior" section for this app. Use: - baseUrl: https://argus.kusoft.xyz - slug: - platform: - channel: stable Do not implement publishing over HTTP. Publishing is SSH-only and happens on the Argus server. Always verify release.sha256 before installing or applying an update. ```