Tauri App Release #30
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: 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 |