Merge pull request 'feat(ci): build Linux AppImage and update updater metadata' (#16) from build/arch_image into main
publish / publish-tauri (nsis, windows-latest, windows-x86_64) (release) Has been cancelled
publish / publish-tauri (appimage, ubuntu-22.04, linux-x86_64) (release) Failing after 2m18s

Reviewed-on: #16
This commit was merged in pull request #16.
This commit is contained in:
2026-07-05 21:12:29 +00:00
3 changed files with 131 additions and 37 deletions
+14 -14
View File
@@ -32,16 +32,19 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 1
matrix: matrix:
include: include:
# - platform: 'macos-latest' # - platform: 'macos-latest'
# args: '--target aarch64-apple-darwin' # args: '--target aarch64-apple-darwin'
# - platform: 'macos-latest' # - platform: 'macos-latest'
# args: '--target x86_64-apple-darwin' # args: '--target x86_64-apple-darwin'
# - platform: 'ubuntu-22.04'
# args: ''
- platform: "windows-latest" - platform: "windows-latest"
args: "" bundles: "nsis"
updater_platform: "windows-x86_64"
- platform: "ubuntu-22.04"
bundles: "appimage"
updater_platform: "linux-x86_64"
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
@@ -55,7 +58,7 @@ jobs:
if: matrix.platform == 'ubuntu-22.04' if: matrix.platform == 'ubuntu-22.04'
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf sudo apt-get install -y build-essential curl wget file libssl-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libxdo-dev libfuse2
- name: setup node - name: setup node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@@ -69,18 +72,13 @@ jobs:
run: npm version ${{ github.ref_name }} --no-git-tag-version --allow-same-version run: npm version ${{ github.ref_name }} --no-git-tag-version --allow-same-version
- name: Update tauri.conf.json Version - name: Update tauri.conf.json Version
shell: powershell env:
RELEASE_VERSION: ${{ github.ref_name }}
run: | run: |
$path = ".\src-tauri\tauri.conf.json" node -e "const fs=require('fs'); const path='src-tauri/tauri.conf.json'; const config=JSON.parse(fs.readFileSync(path,'utf8')); config.version=process.env.RELEASE_VERSION; fs.writeFileSync(path, JSON.stringify(config,null,2)+'\n');"
$version = '${{ github.ref_name }}'
$content = Get-Content -Raw -Encoding UTF8 $path
$regex = [regex]'"version"\s*:\s*"[^"]*"'
$replacement = '"version": "' + $version + '"'
$updated = $regex.Replace($content, $replacement, 1)
$encoding = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($path, $updated, $encoding)
- name: Commit and Push Changes - name: Commit and Push Changes
if: matrix.platform == 'windows-latest'
shell: powershell shell: powershell
run: | run: |
git config user.name '${{ vars.USERNAME_GIT }}' git config user.name '${{ vars.USERNAME_GIT }}'
@@ -131,12 +129,14 @@ jobs:
env: env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD}} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD}}
run: npm run tauri build run: npm run tauri -- build --bundles ${{ matrix.bundles }}
# ---------------------- # ----------------------
# Upload to MinIO (via mc) and create latest.json # Upload to MinIO (via mc) and create latest.json
# ---------------------- # ----------------------
- name: Upload artifacts to MinIO with cicd_tool - name: Upload artifacts to MinIO with cicd_tool
working-directory: GitLite/cicd_tool working-directory: GitLite/cicd_tool
env:
PUBLISH_PLATFORM: ${{ matrix.updater_platform }}
run: | run: |
uv sync uv sync
uv run main.py uv run main.py
+111 -23
View File
@@ -4,7 +4,7 @@ import json
import os import os
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Iterable, List, Optional from typing import Any, Iterable, List, Optional
from urllib.parse import urlparse from urllib.parse import urlparse
from minio import Minio from minio import Minio
@@ -118,6 +118,9 @@ def _collect_artifacts() -> List[Path]:
for directory in ( for directory in (
ARTIFACT_ROOT / "msi", ARTIFACT_ROOT / "msi",
ARTIFACT_ROOT / "nsis", ARTIFACT_ROOT / "nsis",
ARTIFACT_ROOT / "appimage",
ARTIFACT_ROOT / "deb",
ARTIFACT_ROOT / "rpm",
ARTIFACT_ROOT / "app", ARTIFACT_ROOT / "app",
): ):
if directory.exists(): if directory.exists():
@@ -125,13 +128,39 @@ def _collect_artifacts() -> List[Path]:
return artifacts return artifacts
def _select_primary_artifact(artifacts: Iterable[Path]) -> Optional[Path]: def _select_primary_artifact(
priority = (".exe", ".msi", ".zip", ".dmg") 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 ext in priority:
for artifact in artifacts: for artifact in artifact_list:
if artifact.suffix.lower() == ext: if artifact.suffix.lower() == ext:
return artifact 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: 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}") raise FileNotFoundError(f"Signature not found for {artifact.name}")
def _build_latest_json( def _build_artifact_url(
version: str, version: str,
primary_artifact: Path, primary_artifact: Path,
signature: str,
artifact_base_url: str, artifact_base_url: str,
app_dir: str, app_dir: str,
) -> str: ) -> str:
base_url = artifact_base_url.rstrip("/") base_url = artifact_base_url.rstrip("/")
path_parts = [app_dir.strip("/"), version, primary_artifact.name] 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 = ( pub_date = (
datetime.now(timezone.utc) datetime.now(timezone.utc)
@@ -163,18 +231,27 @@ def _build_latest_json(
.replace("+00:00", "Z") .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, "version": version,
"pub_date": pub_date, "pub_date": pub_date,
"notes": os.environ.get("RELEASE_NOTES", "Automated release"), "notes": os.environ.get("RELEASE_NOTES")
"platforms": { or existing_notes
"windows-x86_64": { or "Automated release",
"signature": signature, "platforms": platforms,
"url": artifact_url,
}
},
} }
return json.dumps(latest_payload, separators=(",", ":"))
def main(version: Optional[str] = None, app_dir: Optional[str] = None) -> None: 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." "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: if not primary:
raise FileNotFoundError("Unable to determine primary artifact to publish.") raise FileNotFoundError("Unable to determine primary artifact to publish.")
platform = publish_platform or _infer_platform(primary)
signature = _read_signature(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, version=resolved_version,
platform=platform,
primary_artifact=primary, primary_artifact=primary,
signature=signature, signature=signature,
artifact_base_url=artifact_base_url, artifact_base_url=artifact_base_url,
app_dir=bucket_prefix, 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 = PROJECT_DIR / "dist"
local_dist_dir.mkdir(parents=True, exist_ok=True) local_dist_dir.mkdir(parents=True, exist_ok=True)
latest_json_path = local_dist_dir / "latest.json" latest_json_path = local_dist_dir / "latest.json"
latest_json_path.write_text(latest_json + "\n", encoding="utf-8") latest_json_path.write_text(latest_json + "\n", encoding="utf-8")
client = _create_minio_client()
_ensure_bucket(client, bucket_name)
for artifact in artifacts: for artifact in artifacts:
object_segments = [part for part in (resolved_version, artifact.name) if part] object_segments = [part for part in (resolved_version, artifact.name) if part]
object_name = "/".join(object_segments) 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), length=len(latest_bytes),
content_type="application/json", 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__": if __name__ == "__main__":
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://schema.tauri.app/config/2",
"bundle": {
"targets": ["appimage"]
}
}