Update build.yml #5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build & Package Retouchly | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| branches: | |
| - main | |
| # Allow only one concurrent build per ref to avoid race conditions on tags | |
| concurrency: | |
| group: build-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: write # needed to push the version tag | |
| jobs: | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # JOB 1 — Compute and record the YYYY.MM.xx version | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| version: | |
| name: Compute version | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.calc.outputs.version }} # YYYY.MM.xx — display / tags | |
| semver: ${{ steps.calc.outputs.semver }} # YYYY.M.xx — semver for Tauri/Cargo | |
| steps: | |
| - name: Checkout (full history for tags) | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - name: Calculate next YYYY.MM.xx version | |
| id: calc | |
| shell: python3 {0} | |
| run: | | |
| import subprocess, datetime, os, re | |
| now = datetime.date.today() | |
| # Padded prefix used for tag lookup and display (e.g. "2026.08") | |
| prefix_padded = now.strftime("%Y.%m") | |
| # Unpadded prefix for semver (e.g. "2026.8") — semver forbids leading zeros | |
| prefix_semver = f"{now.year}.{now.month}" | |
| # Collect all tags matching our scheme for the current month | |
| raw = subprocess.run( | |
| ["git", "tag", "--list", f"{prefix_padded}.*"], | |
| capture_output=True, text=True | |
| ).stdout.strip() | |
| existing = [] | |
| for tag in raw.splitlines(): | |
| m = re.fullmatch(r"\d{4}\.\d{2}\.(\d+)", tag.strip()) | |
| if m: | |
| existing.append(int(m.group(1))) | |
| patch = (max(existing) + 1) if existing else 1 | |
| version = f"{prefix_padded}.{patch}" # e.g. "2026.08.1" | |
| semver = f"{prefix_semver}.{patch}" # e.g. "2026.8.1" (valid semver) | |
| print(f"Computed version : {version}") | |
| print(f"Semver version : {semver}") | |
| with open(os.environ["GITHUB_OUTPUT"], "a") as fh: | |
| fh.write(f"version={version}\n") | |
| fh.write(f"semver={semver}\n") | |
| # Only tag on a real push to main (not on PRs) to avoid duplicate tags | |
| - name: Create and push version tag | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git tag "${{ steps.calc.outputs.version }}" | |
| git push origin "${{ steps.calc.outputs.version }}" | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # JOB 2 — Windows → MSI, EXE (NSIS), MSIX (Store-compliant via MakeAppx) | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| build-windows: | |
| name: Build — Windows | |
| needs: version | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| - name: Setup Rust (stable) | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Rust build artifacts | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| src-tauri/target | |
| key: windows-cargo-${{ hashFiles('src-tauri/Cargo.lock') }} | |
| restore-keys: windows-cargo- | |
| - name: Install npm dependencies | |
| run: npm install | |
| # Patch version in every file that embeds it | |
| # tauri.conf.json and Cargo.toml require strict semver (no leading zeros) | |
| - name: Patch version — tauri.conf.json | |
| shell: python3 {0} | |
| run: | | |
| import json, pathlib | |
| p = pathlib.Path("src-tauri/tauri.conf.json") | |
| cfg = json.loads(p.read_text(encoding="utf-8")) | |
| cfg["version"] = "${{ needs.version.outputs.semver }}" | |
| p.write_text(json.dumps(cfg, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") | |
| - name: Patch version — Cargo.toml | |
| shell: python3 {0} | |
| run: | | |
| import re, pathlib | |
| p = pathlib.Path("src-tauri/Cargo.toml") | |
| txt = p.read_text(encoding="utf-8") | |
| txt = re.sub(r'^(version\s*=\s*)"[^"]+"', r'\g<1>"${{ needs.version.outputs.semver }}"', txt, count=1, flags=re.MULTILINE) | |
| p.write_text(txt, encoding="utf-8") | |
| - name: Patch version — i18n/en.json | |
| shell: python3 {0} | |
| run: | | |
| import json, pathlib | |
| p = pathlib.Path("src/assets/i18n/en.json") | |
| data = json.loads(p.read_text(encoding="utf-8")) | |
| data["about"]["version"] = "Version ${{ needs.version.outputs.version }}" | |
| p.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") | |
| - name: Patch version — i18n/fr.json | |
| shell: python3 {0} | |
| run: | | |
| import json, pathlib | |
| p = pathlib.Path("src/assets/i18n/fr.json") | |
| data = json.loads(p.read_text(encoding="utf-8")) | |
| data["about"]["version"] = "Version ${{ needs.version.outputs.version }}" | |
| p.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") | |
| # Tauri 2 supports msi and nsis only — MSIX is built separately below | |
| - name: Build Tauri (MSI + NSIS) | |
| run: npx tauri build --bundles msi,nsis | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| # ── Package MSIX for Microsoft Store from the MSI ────────────────────── | |
| # MakeAppx.exe is pre-installed on GitHub Windows runners (Windows SDK). | |
| # We wrap the MSI in a minimal MSIX layout and sign it with the PFX cert. | |
| # The WINDOWS_MSIX_PUBLISHER secret must match your PFX certificate CN | |
| # exactly (e.g. "CN=Martzcode, O=Martzcode, C=FR"). | |
| - name: Create & sign MSIX for Microsoft Store | |
| shell: pwsh | |
| run: | | |
| $version = "${{ needs.version.outputs.semver }}" | |
| $publisher = if ($env:MSIX_PUBLISHER) { $env:MSIX_PUBLISHER } else { "CN=Retouchly" } | |
| $msi = (Get-ChildItem -Path "src-tauri/target/release/bundle/msi" -Filter "*.msi" | Select-Object -First 1).FullName | |
| $msixDir = "$env:RUNNER_TEMP\msix_layout" | |
| $msixOut = "src-tauri/target/release/bundle/msix" | |
| New-Item -ItemType Directory -Force -Path $msixDir, $msixOut | Out-Null | |
| # Copy MSI as the payload | |
| Copy-Item $msi "$msixDir\Retouchly.msi" | |
| # AppxManifest — Publisher must match your signing certificate CN exactly | |
| $manifest = @" | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" | |
| xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" | |
| xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" | |
| IgnorableNamespaces="uap rescap"> | |
| <Identity Name="com.retouchly.app" | |
| Publisher="$publisher" | |
| Version="${version}.0" | |
| ProcessorArchitecture="x64" /> | |
| <Properties> | |
| <DisplayName>Retouchly</DisplayName> | |
| <PublisherDisplayName>Martzcode</PublisherDisplayName> | |
| <Description>Open source image editor</Description> | |
| <Logo>logo.png</Logo> | |
| </Properties> | |
| <Dependencies> | |
| <TargetDeviceFamily Name="Windows.Desktop" | |
| MinVersion="10.0.17763.0" | |
| MaxVersionTested="10.0.22621.0" /> | |
| </Dependencies> | |
| <Resources><Resource Language="en-US" /></Resources> | |
| <Applications> | |
| <Application Id="Retouchly" | |
| Executable="Retouchly.msi" | |
| EntryPoint="Windows.FullTrustApplication"> | |
| <uap:VisualElements DisplayName="Retouchly" | |
| Description="Open source image editor" | |
| BackgroundColor="transparent" | |
| Square150x150Logo="logo.png" | |
| Square44x44Logo="logo.png" /> | |
| </Application> | |
| </Applications> | |
| <Capabilities> | |
| <rescap:Capability Name="runFullTrust" /> | |
| </Capabilities> | |
| </Package> | |
| "@ | |
| $manifest | Out-File -Encoding UTF8 "$msixDir\AppxManifest.xml" | |
| # Use the 128x128 icon as MSIX logo | |
| $icon = Get-ChildItem -Path "src-tauri/icons" -Filter "128x128.png" | Select-Object -First 1 | |
| if ($icon) { Copy-Item $icon.FullName "$msixDir\logo.png" } | |
| # Build the MSIX package | |
| $makeAppx = "C:\Program Files (x86)\Windows Kits\10\App Certification Kit\makeappx.exe" | |
| & $makeAppx pack /d $msixDir /p "$msixOut\Retouchly_${version}_x64.msix" /o | |
| # Sign the MSIX (only if certificate is provided) | |
| if ($env:PFX_BASE64 -ne "") { | |
| $pfxBytes = [Convert]::FromBase64String("$env:PFX_BASE64") | |
| $pfxPath = "$env:RUNNER_TEMP\retouchly.pfx" | |
| [IO.File]::WriteAllBytes($pfxPath, $pfxBytes) | |
| $signTool = "C:\Program Files (x86)\Windows Kits\10\App Certification Kit\signtool.exe" | |
| & $signTool sign /fd SHA256 /td SHA256 /as /tr http://timestamp.digicert.com ` | |
| /f $pfxPath /p "$env:PFX_PASSWORD" "$msixOut\Retouchly_${version}_x64.msix" | |
| Remove-Item $pfxPath -Force | |
| } | |
| env: | |
| MSIX_PUBLISHER: ${{ secrets.WINDOWS_MSIX_PUBLISHER }} | |
| PFX_BASE64: ${{ secrets.WINDOWS_PFX_BASE64 }} | |
| PFX_PASSWORD: ${{ secrets.WINDOWS_PFX_PASSWORD }} | |
| - name: Upload Windows artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: windows-installers-${{ needs.version.outputs.version }} | |
| if-no-files-found: error | |
| retention-days: 90 | |
| path: | | |
| src-tauri/target/release/bundle/msix/*.msix | |
| src-tauri/target/release/bundle/msi/*.msi | |
| src-tauri/target/release/bundle/nsis/*.exe | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # JOB 3 — Linux → DEB + RPM | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| build-linux: | |
| name: Build — Linux | |
| needs: version | |
| runs-on: ubuntu-22.04 # older LTS for wider glibc compatibility | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| - name: Setup Rust (stable) | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache Rust build artifacts | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| src-tauri/target | |
| key: linux-cargo-${{ hashFiles('src-tauri/Cargo.lock') }} | |
| restore-keys: linux-cargo- | |
| # Tauri 2 on Linux requires WebKitGTK + other system libs | |
| - name: Install system dependencies | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y --no-install-recommends \ | |
| libwebkit2gtk-4.1-dev \ | |
| libgtk-3-dev \ | |
| libayatana-appindicator3-dev \ | |
| librsvg2-dev \ | |
| patchelf \ | |
| libssl-dev \ | |
| pkg-config \ | |
| rpm | |
| - name: Install npm dependencies | |
| run: npm install | |
| - name: Patch version — tauri.conf.json | |
| run: | | |
| python3 - <<'EOF' | |
| import json, pathlib | |
| p = pathlib.Path("src-tauri/tauri.conf.json") | |
| cfg = json.loads(p.read_text(encoding="utf-8")) | |
| cfg["version"] = "${{ needs.version.outputs.semver }}" | |
| p.write_text(json.dumps(cfg, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") | |
| EOF | |
| - name: Patch version — Cargo.toml | |
| run: | | |
| python3 - <<'EOF' | |
| import re, pathlib | |
| p = pathlib.Path("src-tauri/Cargo.toml") | |
| txt = p.read_text(encoding="utf-8") | |
| txt = re.sub(r'^(version\s*=\s*)"[^"]+"', r'\g<1>"${{ needs.version.outputs.semver }}"', txt, count=1, flags=re.MULTILINE) | |
| p.write_text(txt, encoding="utf-8") | |
| EOF | |
| - name: Patch version — i18n/en.json | |
| run: | | |
| python3 - <<'EOF' | |
| import json, pathlib | |
| p = pathlib.Path("src/assets/i18n/en.json") | |
| data = json.loads(p.read_text(encoding="utf-8")) | |
| data["about"]["version"] = "Version ${{ needs.version.outputs.version }}" | |
| p.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") | |
| EOF | |
| - name: Patch version — i18n/fr.json | |
| run: | | |
| python3 - <<'EOF' | |
| import json, pathlib | |
| p = pathlib.Path("src/assets/i18n/fr.json") | |
| data = json.loads(p.read_text(encoding="utf-8")) | |
| data["about"]["version"] = "Version ${{ needs.version.outputs.version }}" | |
| p.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") | |
| EOF | |
| - name: Build Tauri (DEB + RPM) | |
| run: npx tauri build --bundles deb,rpm | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| - name: Upload Linux artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: linux-packages-${{ needs.version.outputs.version }} | |
| if-no-files-found: error | |
| retention-days: 90 | |
| path: | | |
| src-tauri/target/release/bundle/deb/*.deb | |
| src-tauri/target/release/bundle/rpm/*.rpm | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # JOB 4 — macOS → DMG (universal: x86_64 + arm64) | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| build-macos: | |
| name: Build — macOS | |
| needs: version | |
| runs-on: macos-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| - name: Setup Rust (stable) with universal targets | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: aarch64-apple-darwin,x86_64-apple-darwin | |
| - name: Cache Rust build artifacts | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| src-tauri/target | |
| key: macos-cargo-${{ hashFiles('src-tauri/Cargo.lock') }} | |
| restore-keys: macos-cargo- | |
| - name: Install npm dependencies | |
| run: npm install | |
| - name: Patch version — tauri.conf.json | |
| run: | | |
| python3 - <<'EOF' | |
| import json, pathlib | |
| p = pathlib.Path("src-tauri/tauri.conf.json") | |
| cfg = json.loads(p.read_text(encoding="utf-8")) | |
| cfg["version"] = "${{ needs.version.outputs.semver }}" | |
| p.write_text(json.dumps(cfg, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") | |
| EOF | |
| - name: Patch version — Cargo.toml | |
| run: | | |
| python3 - <<'EOF' | |
| import re, pathlib | |
| p = pathlib.Path("src-tauri/Cargo.toml") | |
| txt = p.read_text(encoding="utf-8") | |
| txt = re.sub(r'^(version\s*=\s*)"[^"]+"', r'\g<1>"${{ needs.version.outputs.semver }}"', txt, count=1, flags=re.MULTILINE) | |
| p.write_text(txt, encoding="utf-8") | |
| EOF | |
| - name: Patch version — i18n/en.json | |
| run: | | |
| python3 - <<'EOF' | |
| import json, pathlib | |
| p = pathlib.Path("src/assets/i18n/en.json") | |
| data = json.loads(p.read_text(encoding="utf-8")) | |
| data["about"]["version"] = "Version ${{ needs.version.outputs.version }}" | |
| p.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") | |
| EOF | |
| - name: Patch version — i18n/fr.json | |
| run: | | |
| python3 - <<'EOF' | |
| import json, pathlib | |
| p = pathlib.Path("src/assets/i18n/fr.json") | |
| data = json.loads(p.read_text(encoding="utf-8")) | |
| data["about"]["version"] = "Version ${{ needs.version.outputs.version }}" | |
| p.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") | |
| EOF | |
| # ── macOS code-signing (optional but recommended) ────────────────────── | |
| # Set the secrets below to enable signing + notarization. | |
| # If the secrets are absent the build still succeeds (unsigned DMG). | |
| - name: Import Developer ID certificate | |
| if: env.MACOS_CERT_BASE64 != '' | |
| shell: bash | |
| run: | | |
| KEYCHAIN="build.keychain" | |
| KEYCHAIN_PASS=$(openssl rand -hex 16) | |
| security create-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN" | |
| security default-keychain -s "$KEYCHAIN" | |
| security unlock-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN" | |
| echo "$MACOS_CERT_BASE64" | base64 --decode > /tmp/retouchly.p12 | |
| security import /tmp/retouchly.p12 -k "$KEYCHAIN" -P "$MACOS_CERT_PASSWORD" \ | |
| -T /usr/bin/codesign -T /usr/bin/productbuild | |
| security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASS" "$KEYCHAIN" | |
| rm -f /tmp/retouchly.p12 | |
| env: | |
| MACOS_CERT_BASE64: ${{ secrets.MACOS_CERT_BASE64 }} | |
| MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} | |
| # Build a universal (fat) binary DMG | |
| - name: Build Tauri (DMG — universal binary) | |
| run: npx tauri build --target universal-apple-darwin --bundles dmg | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| # Code-signing identity (e.g. "Developer ID Application: Your Name (TEAMID)") | |
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | |
| # ── Notarization (requires an App Store Connect API key) ─────────────── | |
| - name: Notarize DMG | |
| if: env.APPLE_API_KEY_BASE64 != '' | |
| shell: bash | |
| run: | | |
| DMG=$(find src-tauri/target/universal-apple-darwin/release/bundle/dmg -name "*.dmg" | head -1) | |
| echo "$APPLE_API_KEY_BASE64" | base64 --decode > /tmp/AuthKey.p8 | |
| xcrun notarytool submit "$DMG" \ | |
| --key /tmp/AuthKey.p8 \ | |
| --key-id "$APPLE_API_KEY_ID" \ | |
| --issuer "$APPLE_API_ISSUER" \ | |
| --wait | |
| xcrun stapler staple "$DMG" | |
| rm -f /tmp/AuthKey.p8 | |
| env: | |
| APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} | |
| APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} | |
| APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} | |
| - name: Upload macOS artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: macos-dmg-${{ needs.version.outputs.version }} | |
| if-no-files-found: error | |
| retention-days: 90 | |
| path: | | |
| src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg |