Skip to content

Update build.yml

Update build.yml #5

Workflow file for this run

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