Skip to content

Tauri App Release

Tauri App Release #30

name: Tauri App Release
on:
workflow_dispatch:
inputs:
release_tag:
description: Existing or new GitHub Release tag, for example v0.2.5
required: true
release_draft:
description: Create or update a draft release
required: true
type: boolean
default: false
prerelease:
description: Mark the release as prerelease
required: true
type: boolean
default: false
permissions:
contents: write
actions: write
jobs:
create-release:
runs-on: ubuntu-22.04
steps:
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ inputs.release_tag }}
RELEASE_DRAFT: ${{ inputs.release_draft }}
PRERELEASE: ${{ inputs.prerelease }}
run: |
if gh release view "$RELEASE_TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "Release $RELEASE_TAG already exists."
exit 0
fi
flags=()
if [ "$RELEASE_DRAFT" = "true" ]; then
flags+=(--draft)
fi
if [ "$PRERELEASE" = "true" ]; then
flags+=(--prerelease)
fi
gh release create "$RELEASE_TAG" \
--repo "${{ github.repository }}" \
--title "GPT Image 2 $RELEASE_TAG" \
--notes "Desktop app installers for GPT Image 2." \
"${flags[@]}"
build-app:
needs: create-release
strategy:
fail-fast: false
matrix:
include:
- platform: macos-15
rust_target: aarch64-apple-darwin
args: --target aarch64-apple-darwin --bundles app
updater_arch: aarch64
updater_suffix: app.tar.gz
- platform: macos-15-intel
rust_target: x86_64-apple-darwin
args: --target x86_64-apple-darwin --bundles app
updater_arch: x64
updater_suffix: app.tar.gz
- platform: ubuntu-22.04
rust_target: ""
args: --bundles appimage,deb,rpm
updater_arch: amd64
updater_suffix: AppImage
- platform: windows-latest
rust_target: ""
args: --bundles nsis
updater_arch: x64
updater_suffix: setup.exe
runs-on: ${{ matrix.platform }}
env:
APPLE_TEAM_ID: 3VT538F8B6
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ inputs.release_tag }}
- name: Install Linux dependencies
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
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: npm
cache-dependency-path: apps/gpt-image-2-app/package-lock.json
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Add Rust target
if: matrix.rust_target != ''
run: rustup target add ${{ matrix.rust_target }}
- name: Cache Rust
uses: swatinem/rust-cache@v2
with:
workspaces: |
. -> target
- name: Install frontend dependencies
run: npm ci --prefix apps/gpt-image-2-app
- name: Import Apple Developer certificate
if: startsWith(matrix.platform, 'macos')
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
echo "$APPLE_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/certificate.p12"
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
security import "$RUNNER_TEMP/certificate.p12" -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
CERT_ID="$(security find-identity -v -p codesigning build.keychain | awk '/Developer ID Application/ { print $2; exit }')"
if [ -z "$CERT_ID" ]; then
echo "Developer ID Application certificate was not imported."
security find-identity -v -p codesigning build.keychain
exit 1
fi
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> "$GITHUB_ENV"
- name: Write Apple API key
if: startsWith(matrix.platform, 'macos')
env:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
run: |
KEY_PATH="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8"
printf '%s' "$APPLE_API_KEY_P8" > "$KEY_PATH"
chmod 600 "$KEY_PATH"
echo "APPLE_API_KEY_PATH=$KEY_PATH" >> "$GITHUB_ENV"
- name: Prepare bundled CLI sidecar
env:
GPT_IMAGE_2_APP_TARGET: ${{ matrix.rust_target }}
run: npm --prefix apps/gpt-image-2-app run prepare:sidecar
- name: Build Tauri app
env:
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY_PATH: ${{ env.APPLE_API_KEY_PATH }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: npm --prefix apps/gpt-image-2-app run tauri -- build ${{ matrix.args }} --ci
- name: Create macOS DMG
if: startsWith(matrix.platform, 'macos')
env:
RELEASE_TAG: ${{ inputs.release_tag }}
UPDATER_ARCH: ${{ matrix.updater_arch }}
shell: bash
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
app_bundle=""
while IFS= read -r candidate; do
app_bundle="$candidate"
break
done < <(find target -type d -name 'GPT Image 2.app' -prune | sort)
if [ -z "$app_bundle" ]; then
echo "No macOS app bundle found."
find target -type d -path '*/release/bundle/*' | sort
exit 1
fi
bundle_dir="$(dirname "$(dirname "$app_bundle")")"
dmg_dir="$bundle_dir/dmg"
staging_dir="$RUNNER_TEMP/gpt-image-2-dmg-${UPDATER_ARCH}"
dmg_path="$dmg_dir/GPT Image 2_${version}_${UPDATER_ARCH}.dmg"
rm -rf "$staging_dir"
mkdir -p "$staging_dir" "$dmg_dir"
ditto "$app_bundle" "$staging_dir/GPT Image 2.app"
ln -s /Applications "$staging_dir/Applications"
rm -f "$dmg_path"
hdiutil create \
-volname "GPT Image 2" \
-srcfolder "$staging_dir" \
-ov \
-format UDZO \
"$dmg_path"
if [ -n "${APPLE_SIGNING_IDENTITY:-}" ]; then
codesign --force --sign "$APPLE_SIGNING_IDENTITY" --timestamp "$dmg_path"
fi
hdiutil verify "$dmg_path"
- name: Normalize updater artifacts
env:
RELEASE_TAG: ${{ inputs.release_tag }}
UPDATER_ARCH: ${{ matrix.updater_arch }}
UPDATER_SUFFIX: ${{ matrix.updater_suffix }}
shell: bash
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
upload_dir="$RUNNER_TEMP/tauri-updater-upload"
mkdir -p "$upload_dir"
case "$UPDATER_SUFFIX" in
app.tar.gz)
payload_name='*.app.tar.gz'
;;
AppImage)
payload_name='*.AppImage'
;;
setup.exe)
payload_name='*-setup.exe'
;;
*)
payload_name="*.${UPDATER_SUFFIX}"
;;
esac
payload=""
while IFS= read -r artifact; do
payload="$artifact"
break
done < <(find target -type f -name "$payload_name" ! -name '*.sig' | sort)
if [ -z "$payload" ]; then
echo "No updater payload found."
echo "Available bundle artifacts:"
find target -type f -path '*/release/bundle/*' | sort
exit 1
fi
sig="${payload}.sig"
if [ ! -f "$sig" ]; then
echo "Missing updater signature: $sig"
exit 1
fi
if [ "$UPDATER_SUFFIX" = "setup.exe" ]; then
dest="$upload_dir/GPT.Image.2_${version}_${UPDATER_ARCH}-setup.exe"
else
dest="$upload_dir/GPT.Image.2_${version}_${UPDATER_ARCH}.${UPDATER_SUFFIX}"
fi
cp "$payload" "$dest"
cp "$sig" "$dest.sig"
- name: Notarize DMG files
if: startsWith(matrix.platform, 'macos')
env:
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
APPLE_API_KEY_PATH: ${{ env.APPLE_API_KEY_PATH }}
run: |
shopt -s nullglob
for dmg in target/release/bundle/dmg/*.dmg target/${{ matrix.rust_target }}/release/bundle/dmg/*.dmg; do
echo "Notarizing $dmg"
xcrun notarytool submit "$dmg" \
--key "$APPLE_API_KEY_PATH" \
--key-id "$APPLE_API_KEY" \
--issuer "$APPLE_API_ISSUER" \
--team-id "$APPLE_TEAM_ID" \
--wait
xcrun stapler staple "$dmg"
xcrun stapler validate "$dmg"
spctl -a -vvv -t open --context context:primary-signature "$dmg"
done
- name: Upload app installers
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ inputs.release_tag }}
UPDATER_ARCH: ${{ matrix.updater_arch }}
shell: bash
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
installer_dir="$RUNNER_TEMP/tauri-installer-upload"
mkdir -p "$installer_dir"
copy_first() {
local pattern="$1"
local dest="$2"
local artifact=""
while IFS= read -r artifact; do
cp "$artifact" "$dest"
return 0
done < <(find target -type f -path "$pattern" | sort)
}
rpm_arch="$UPDATER_ARCH"
if [ "$rpm_arch" = "amd64" ]; then
rpm_arch="x86_64"
fi
copy_first '*/release/bundle/dmg/*.dmg' "$installer_dir/GPT.Image.2_${version}_${UPDATER_ARCH}.dmg"
if [ ! -f "$RUNNER_TEMP/tauri-updater-upload/GPT.Image.2_${version}_${UPDATER_ARCH}-setup.exe" ]; then
copy_first '*/release/bundle/nsis/*.exe' "$installer_dir/GPT.Image.2_${version}_${UPDATER_ARCH}-setup.exe"
fi
copy_first '*/release/bundle/deb/*.deb' "$installer_dir/GPT.Image.2_${version}_${UPDATER_ARCH}.deb"
copy_first '*/release/bundle/rpm/*.rpm' "$installer_dir/GPT.Image.2-${version}-1.${rpm_arch}.rpm"
artifacts=()
while IFS= read -r artifact; do
artifacts+=("$artifact")
done < <(
find "$installer_dir" "$RUNNER_TEMP/tauri-updater-upload" -type f | sort
)
if [ "${#artifacts[@]}" -eq 0 ]; then
echo "No app installers found."
exit 1
fi
gh release upload "$RELEASE_TAG" "${artifacts[@]}" --repo "${{ github.repository }}" --clobber
publish-updater-manifest:
needs: build-app
runs-on: ubuntu-22.04
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ inputs.release_tag }}
SOURCE_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Generate latest.json for Tauri updater
shell: bash
run: |
set -euo pipefail
version="${RELEASE_TAG#v}"
sig_dir="$RUNNER_TEMP/tauri-updater-sigs"
mkdir -p "$sig_dir"
gh release download "$RELEASE_TAG" \
--repo "$SOURCE_REPO" \
--pattern "GPT.Image.2_${version}_*.sig" \
--dir "$sig_dir" \
--clobber
published_at="$(gh release view "$RELEASE_TAG" --repo "$SOURCE_REPO" --json publishedAt --jq '.publishedAt // empty')"
if [ -z "$published_at" ]; then
published_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
fi
node scripts/release/generate-tauri-updater-manifest.mjs \
--version "$version" \
--tag "$RELEASE_TAG" \
--repo "$SOURCE_REPO" \
--sig-dir "$sig_dir" \
--pub-date "$published_at" \
--out "$RUNNER_TEMP/latest.json"
gh release upload "$RELEASE_TAG" "$RUNNER_TEMP/latest.json" --repo "$SOURCE_REPO" --clobber
publish-homebrew-cask:
needs: [build-app, publish-updater-manifest]
runs-on: macos-15
if: ${{ !inputs.release_draft && !inputs.prerelease }}
env:
SOURCE_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ inputs.release_tag }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_USER: "github-actions[bot]"
GITHUB_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
steps:
- uses: actions/checkout@v6
with:
repository: "Wangnov/homebrew-tap"
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
persist-credentials: true
- name: Move checkout into Homebrew tap path
shell: bash
run: |
set -euo pipefail
tap_dir="$(brew --repo)/Library/Taps/wangnov/homebrew-tap"
mkdir -p "$(dirname "$tap_dir")"
rm -rf "$tap_dir"
rsync -a --delete "$PWD/" "$tap_dir/"
git -C "$tap_dir" remote set-url origin "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/Wangnov/homebrew-tap.git"
echo "HOMEBREW_TAP_DIR=$tap_dir" >> "$GITHUB_ENV"
- name: Generate Homebrew cask
run: |
set -euo pipefail
cd "$HOMEBREW_TAP_DIR"
version="${RELEASE_TAG#v}"
arm_asset="GPT.Image.2_${version}_aarch64.dmg"
intel_asset="GPT.Image.2_${version}_x64.dmg"
assets_json="$RUNNER_TEMP/release-assets.json"
gh release view "$RELEASE_TAG" --repo "$SOURCE_REPO" --json assets > "$assets_json"
arm_sha="$(jq -r --arg name "$arm_asset" '.assets[] | select(.name == $name) | .digest' "$assets_json" | sed 's/^sha256://')"
intel_sha="$(jq -r --arg name "$intel_asset" '.assets[] | select(.name == $name) | .digest' "$assets_json" | sed 's/^sha256://')"
if [ -z "$arm_sha" ] || [ "$arm_sha" = "null" ]; then
echo "Missing digest for $arm_asset"
exit 1
fi
if [ -z "$intel_sha" ] || [ "$intel_sha" = "null" ]; then
echo "Missing digest for $intel_asset"
exit 1
fi
mkdir -p Casks
cat > Casks/gpt-image-2.rb <<RUBY
cask "gpt-image-2" do
arch arm: "aarch64", intel: "x64"
version "$version"
sha256 arm: "$arm_sha",
intel: "$intel_sha"
url "https://github.com/Wangnov/gpt-image-2-skill/releases/download/v#{version}/GPT.Image.2_#{version}_#{arch}.dmg"
name "GPT Image 2"
desc "Desktop image generation and editing for GPT Image 2"
homepage "https://github.com/Wangnov/gpt-image-2-skill"
auto_updates true
depends_on macos: :big_sur
app "GPT Image 2.app"
zap trash: [
"~/Library/Application Support/com.wangnov.gpt-image-2",
"~/Library/Logs/com.wangnov.gpt-image-2",
"~/Library/Saved Application State/com.wangnov.gpt-image-2.savedState",
]
end
RUBY
- name: Style Homebrew cask
run: |
set -euo pipefail
cd "$HOMEBREW_TAP_DIR"
brew style --cask Casks/gpt-image-2.rb
- name: Commit and push cask
run: |
set -euo pipefail
cd "$HOMEBREW_TAP_DIR"
version="${RELEASE_TAG#v}"
git config user.name "$GITHUB_USER"
git config user.email "$GITHUB_EMAIL"
if ! git status --short -- Casks/gpt-image-2.rb | grep -q .; then
echo "No Homebrew cask changes."
exit 0
fi
git add Casks/gpt-image-2.rb
git commit -m "chore: update gpt-image-2 cask to ${version}"
git push
dispatch-post-release-verify:
needs: publish-homebrew-cask
runs-on: ubuntu-22.04
if: ${{ !inputs.release_draft && !inputs.prerelease && needs.publish-homebrew-cask.result == 'success' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ inputs.release_tag }}
steps:
- name: Dispatch post release verify with desktop cask
shell: bash
run: gh workflow run "Post Release Verify" --repo "${{ github.repository }}" -f version="${RELEASE_TAG#v}" -f verify_desktop_cask=true