diff --git a/.gitea/workflows/app_builder.yaml b/.gitea/workflows/app_builder.yaml index b541076..0c3356d 100644 --- a/.gitea/workflows/app_builder.yaml +++ b/.gitea/workflows/app_builder.yaml @@ -32,16 +32,19 @@ jobs: strategy: fail-fast: false + max-parallel: 1 matrix: include: # - platform: 'macos-latest' # args: '--target aarch64-apple-darwin' # - platform: 'macos-latest' # args: '--target x86_64-apple-darwin' - # - platform: 'ubuntu-22.04' - # args: '' - 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 }} @@ -55,7 +58,7 @@ jobs: if: matrix.platform == 'ubuntu-22.04' run: | 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 uses: actions/setup-node@v4 @@ -69,18 +72,13 @@ jobs: run: npm version ${{ github.ref_name }} --no-git-tag-version --allow-same-version - name: Update tauri.conf.json Version - shell: powershell + env: + RELEASE_VERSION: ${{ github.ref_name }} run: | - $path = ".\src-tauri\tauri.conf.json" - $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) + 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');" - name: Commit and Push Changes + if: matrix.platform == 'windows-latest' shell: powershell run: | git config user.name '${{ vars.USERNAME_GIT }}' @@ -131,12 +129,14 @@ jobs: env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} 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 # ---------------------- - name: Upload artifacts to MinIO with cicd_tool working-directory: GitLite/cicd_tool + env: + PUBLISH_PLATFORM: ${{ matrix.updater_platform }} run: | uv sync uv run main.py diff --git a/cicd_tool/main.py b/cicd_tool/main.py index 4aa442e..9e03d81 100644 --- a/cicd_tool/main.py +++ b/cicd_tool/main.py @@ -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__": diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json new file mode 100644 index 0000000..f79a8b8 --- /dev/null +++ b/src-tauri/tauri.linux.conf.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "bundle": { + "targets": ["appimage"] + } +}