feat(ci): build Linux AppImage and update updater metadata
The CI workflow now builds a single bundle per run, adds Linux AppImage output, and updates the Tauri version using a Node script. The publishing tool is extended to recognize Linux artifacts, select a primary artifact based on the target platform, and merge platform entries into an existing latest.json when present. - Limit matrix parallelism and pass bundle targets to Tauri - Update updater platform handling and latest.json merging - Add Linux Tauri config for AppImage bundling
This commit is contained in:
+111
-23
@@ -4,7 +4,7 @@ import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Iterable, List, Optional
|
||||
from typing import Any, Iterable, List, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from minio import Minio
|
||||
@@ -118,6 +118,9 @@ def _collect_artifacts() -> List[Path]:
|
||||
for directory in (
|
||||
ARTIFACT_ROOT / "msi",
|
||||
ARTIFACT_ROOT / "nsis",
|
||||
ARTIFACT_ROOT / "appimage",
|
||||
ARTIFACT_ROOT / "deb",
|
||||
ARTIFACT_ROOT / "rpm",
|
||||
ARTIFACT_ROOT / "app",
|
||||
):
|
||||
if directory.exists():
|
||||
@@ -125,13 +128,39 @@ def _collect_artifacts() -> List[Path]:
|
||||
return artifacts
|
||||
|
||||
|
||||
def _select_primary_artifact(artifacts: Iterable[Path]) -> Optional[Path]:
|
||||
priority = (".exe", ".msi", ".zip", ".dmg")
|
||||
def _select_primary_artifact(
|
||||
artifacts: Iterable[Path], platform: Optional[str] = None
|
||||
) -> Optional[Path]:
|
||||
artifact_list = list(artifacts)
|
||||
priorities = {
|
||||
"windows-x86_64": (".exe", ".msi"),
|
||||
"linux-x86_64": (".appimage", ".deb", ".rpm"),
|
||||
"darwin-x86_64": (".dmg", ".zip"),
|
||||
"darwin-aarch64": (".dmg", ".zip"),
|
||||
}
|
||||
priority = priorities.get(platform or "", ())
|
||||
priority += (".exe", ".msi", ".appimage", ".deb", ".rpm", ".zip", ".dmg")
|
||||
for ext in priority:
|
||||
for artifact in artifacts:
|
||||
for artifact in artifact_list:
|
||||
if artifact.suffix.lower() == ext:
|
||||
return artifact
|
||||
return next(iter(artifacts), None)
|
||||
return next(
|
||||
(artifact for artifact in artifact_list if artifact.suffix != ".sig"), None
|
||||
)
|
||||
|
||||
|
||||
def _infer_platform(artifact: Path) -> str:
|
||||
suffix = artifact.suffix.lower()
|
||||
if suffix in {".exe", ".msi"}:
|
||||
return "windows-x86_64"
|
||||
if suffix in {".appimage", ".deb", ".rpm"}:
|
||||
return "linux-x86_64"
|
||||
if suffix in {".dmg", ".zip"}:
|
||||
return "darwin-x86_64"
|
||||
raise ConfigurationError(
|
||||
f"Cannot infer updater platform from artifact: {artifact.name}. "
|
||||
"Set PUBLISH_PLATFORM explicitly."
|
||||
)
|
||||
|
||||
|
||||
def _read_signature(artifact: Path) -> str:
|
||||
@@ -145,16 +174,55 @@ def _read_signature(artifact: Path) -> str:
|
||||
raise FileNotFoundError(f"Signature not found for {artifact.name}")
|
||||
|
||||
|
||||
def _build_latest_json(
|
||||
def _build_artifact_url(
|
||||
version: str,
|
||||
primary_artifact: Path,
|
||||
signature: str,
|
||||
artifact_base_url: str,
|
||||
app_dir: str,
|
||||
) -> str:
|
||||
base_url = artifact_base_url.rstrip("/")
|
||||
path_parts = [app_dir.strip("/"), version, primary_artifact.name]
|
||||
artifact_url = "/".join([base_url, *filter(None, path_parts)])
|
||||
return "/".join([base_url, *filter(None, path_parts)])
|
||||
|
||||
|
||||
def _read_existing_latest_json(
|
||||
client: Minio, bucket_name: str
|
||||
) -> Optional[dict[str, Any]]:
|
||||
try:
|
||||
response = client.get_object(bucket_name, "latest.json")
|
||||
except S3Error as exc:
|
||||
if exc.code in {"NoSuchKey", "NoSuchObject"}:
|
||||
return None
|
||||
raise
|
||||
|
||||
try:
|
||||
payload = json.loads(response.read().decode("utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ConfigurationError("Existing latest.json is invalid JSON") from exc
|
||||
finally:
|
||||
response.close()
|
||||
response.release_conn()
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
raise ConfigurationError("Existing latest.json must be a JSON object")
|
||||
return payload
|
||||
|
||||
|
||||
def _build_latest_payload(
|
||||
version: str,
|
||||
platform: str,
|
||||
primary_artifact: Path,
|
||||
signature: str,
|
||||
artifact_base_url: str,
|
||||
app_dir: str,
|
||||
existing_payload: Optional[dict[str, Any]] = None,
|
||||
) -> dict[str, Any]:
|
||||
artifact_url = _build_artifact_url(
|
||||
version=version,
|
||||
primary_artifact=primary_artifact,
|
||||
artifact_base_url=artifact_base_url,
|
||||
app_dir=app_dir,
|
||||
)
|
||||
|
||||
pub_date = (
|
||||
datetime.now(timezone.utc)
|
||||
@@ -163,18 +231,27 @@ def _build_latest_json(
|
||||
.replace("+00:00", "Z")
|
||||
)
|
||||
|
||||
latest_payload = {
|
||||
platforms: dict[str, Any] = {}
|
||||
existing_notes = None
|
||||
if existing_payload and existing_payload.get("version") == version:
|
||||
existing_platforms = existing_payload.get("platforms")
|
||||
if isinstance(existing_platforms, dict):
|
||||
platforms.update(existing_platforms)
|
||||
existing_notes = existing_payload.get("notes")
|
||||
|
||||
platforms[platform] = {
|
||||
"signature": signature,
|
||||
"url": artifact_url,
|
||||
}
|
||||
|
||||
return {
|
||||
"version": version,
|
||||
"pub_date": pub_date,
|
||||
"notes": os.environ.get("RELEASE_NOTES", "Automated release"),
|
||||
"platforms": {
|
||||
"windows-x86_64": {
|
||||
"signature": signature,
|
||||
"url": artifact_url,
|
||||
}
|
||||
},
|
||||
"notes": os.environ.get("RELEASE_NOTES")
|
||||
or existing_notes
|
||||
or "Automated release",
|
||||
"platforms": platforms,
|
||||
}
|
||||
return json.dumps(latest_payload, separators=(",", ":"))
|
||||
|
||||
|
||||
def main(version: Optional[str] = None, app_dir: Optional[str] = None) -> None:
|
||||
@@ -194,28 +271,36 @@ def main(version: Optional[str] = None, app_dir: Optional[str] = None) -> None:
|
||||
"No release artifacts found in src-tauri/target/release/bundle."
|
||||
)
|
||||
|
||||
primary = _select_primary_artifact(artifacts)
|
||||
publish_platform = os.environ.get("PUBLISH_PLATFORM") or os.environ.get(
|
||||
"UPDATER_PLATFORM"
|
||||
)
|
||||
primary = _select_primary_artifact(artifacts, publish_platform)
|
||||
if not primary:
|
||||
raise FileNotFoundError("Unable to determine primary artifact to publish.")
|
||||
|
||||
platform = publish_platform or _infer_platform(primary)
|
||||
signature = _read_signature(primary)
|
||||
|
||||
latest_json = _build_latest_json(
|
||||
client = _create_minio_client()
|
||||
_ensure_bucket(client, bucket_name)
|
||||
existing_latest_json = _read_existing_latest_json(client, bucket_name)
|
||||
|
||||
latest_payload = _build_latest_payload(
|
||||
version=resolved_version,
|
||||
platform=platform,
|
||||
primary_artifact=primary,
|
||||
signature=signature,
|
||||
artifact_base_url=artifact_base_url,
|
||||
app_dir=bucket_prefix,
|
||||
existing_payload=existing_latest_json,
|
||||
)
|
||||
latest_json = json.dumps(latest_payload, separators=(",", ":"))
|
||||
|
||||
local_dist_dir = PROJECT_DIR / "dist"
|
||||
local_dist_dir.mkdir(parents=True, exist_ok=True)
|
||||
latest_json_path = local_dist_dir / "latest.json"
|
||||
latest_json_path.write_text(latest_json + "\n", encoding="utf-8")
|
||||
|
||||
client = _create_minio_client()
|
||||
_ensure_bucket(client, bucket_name)
|
||||
|
||||
for artifact in artifacts:
|
||||
object_segments = [part for part in (resolved_version, artifact.name) if part]
|
||||
object_name = "/".join(object_segments)
|
||||
@@ -231,7 +316,10 @@ def main(version: Optional[str] = None, app_dir: Optional[str] = None) -> None:
|
||||
length=len(latest_bytes),
|
||||
content_type="application/json",
|
||||
)
|
||||
print(f"Uploaded latest.json -> s3://{bucket_name}/{latest_object_name}")
|
||||
print(
|
||||
f"Uploaded latest.json for {platform} -> "
|
||||
f"s3://{bucket_name}/{latest_object_name}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user