feat(ci): build Linux AppImage and update updater metadata #16
@@ -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
|
||||||
|
|||||||
+110
-22
@@ -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] = {}
|
||||||
"version": version,
|
existing_notes = None
|
||||||
"pub_date": pub_date,
|
if existing_payload and existing_payload.get("version") == version:
|
||||||
"notes": os.environ.get("RELEASE_NOTES", "Automated release"),
|
existing_platforms = existing_payload.get("platforms")
|
||||||
"platforms": {
|
if isinstance(existing_platforms, dict):
|
||||||
"windows-x86_64": {
|
platforms.update(existing_platforms)
|
||||||
|
existing_notes = existing_payload.get("notes")
|
||||||
|
|
||||||
|
platforms[platform] = {
|
||||||
"signature": signature,
|
"signature": signature,
|
||||||
"url": artifact_url,
|
"url": artifact_url,
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
return {
|
||||||
|
"version": version,
|
||||||
|
"pub_date": pub_date,
|
||||||
|
"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:
|
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__":
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"bundle": {
|
||||||
|
"targets": ["appimage"]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user