Files

383 lines
14 KiB
YAML
Vendored

name: Build Single Popular App
env:
NODE_VERSION: "22"
PNPM_VERSION: "10.26.2"
on:
workflow_dispatch:
inputs:
name:
description: "App Name"
required: true
default: "twitter"
title:
description: "App Title"
required: true
default: "Twitter"
name_zh:
description: "App Name in Chinese"
required: true
default: "推特"
url:
description: "App URL"
required: true
default: "https://twitter.com/"
icon:
description: "App Icon"
required: false
new_window:
description: "Allow sites to open new windows"
required: false
default: false
workflow_call:
inputs:
name:
description: "App Name"
type: string
required: true
default: "twitter"
title:
description: "App Title"
required: true
type: string
default: "Twitter"
name_zh:
description: "App Name in Chinese"
required: true
type: string
default: "推特"
url:
description: "App URL"
required: true
type: string
default: "https://twitter.com/"
icon:
description: "App Icon"
type: string
required: false
new_window:
description: "Allow sites to open new windows"
type: boolean
required: false
default: false
secrets:
PAKE_SIGNING_IDENTITY:
required: false
PAKE_CERTIFICATE_P12:
required: false
PAKE_CERTIFICATE_PASSWORD:
required: false
PAKE_NOTARIZE_APPLE_ID:
required: false
PAKE_NOTARIZE_TEAM_ID:
required: false
PAKE_NOTARIZE_PASSWORD:
required: false
jobs:
build:
name: ${{ inputs.title }} (${{ matrix.build }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
build: [linux, macos, windows]
include:
- build: linux
os: ubuntu-latest
rust: stable
- build: windows
os: windows-latest
rust: stable-x86_64-msvc
- build: macos
os: macos-latest
rust: stable
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
- name: Setup Node.js Environment
uses: ./.github/actions/setup-env
with:
mode: build
- name: Download CLI Artifact
if: github.event_name != 'workflow_dispatch'
uses: actions/download-artifact@v7
with:
name: pake-cli-dist
path: dist
- name: Build CLI (workflow_dispatch)
if: github.event_name == 'workflow_dispatch'
run: pnpm run cli:build
- name: Setup mold linker
if: matrix.os == 'ubuntu-latest'
uses: rui314/setup-mold@v1
- name: Prepare macOS signing
if: matrix.os == 'macos-latest'
env:
PAKE_SIGNING_IDENTITY: ${{ secrets.PAKE_SIGNING_IDENTITY }}
PAKE_CERTIFICATE_P12: ${{ secrets.PAKE_CERTIFICATE_P12 }}
PAKE_CERTIFICATE_PASSWORD: ${{ secrets.PAKE_CERTIFICATE_PASSWORD }}
run: |
set -euo pipefail
SIGNING_IDENTITY="${PAKE_SIGNING_IDENTITY:-}"
if [ -n "${PAKE_CERTIFICATE_P12:-}" ] && [ -n "${PAKE_CERTIFICATE_PASSWORD:-}" ]; then
echo "Importing macOS signing certificate..."
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -base64 16)"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "$PAKE_CERTIFICATE_P12" | base64 --decode > "$RUNNER_TEMP/certificate.p12"
security import "$RUNNER_TEMP/certificate.p12" -P "$PAKE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH"
if [ -z "$SIGNING_IDENTITY" ]; then
SIGNING_IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | sed -n 's/.*"\(Developer ID Application:.*\)".*/\1/p' | head -n 1)"
fi
else
echo "No certificate secret configured, fallback to ad-hoc signing."
fi
if [ -z "$SIGNING_IDENTITY" ]; then
SIGNING_IDENTITY="-"
fi
SIGNING_IDENTITY="$SIGNING_IDENTITY" node <<'NODE'
const fs = require('fs');
const file = 'src-tauri/tauri.macos.conf.json';
const config = JSON.parse(fs.readFileSync(file, 'utf8'));
config.bundle ??= {};
config.bundle.macOS ??= {};
config.bundle.macOS.signingIdentity = process.env.SIGNING_IDENTITY || '-';
fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
NODE
echo "Using signing identity: $SIGNING_IDENTITY"
- name: Build for Linux
if: matrix.os == 'ubuntu-latest'
timeout-minutes: 20
run: |
ARGS=("${{ inputs.url }}" "--name" "${{ inputs.name }}")
# Auto-detect local icon or use provided icon
if [ -n "${{ inputs.icon }}" ]; then
ARGS+=("--icon" "${{ inputs.icon }}")
elif [ -f "src-tauri/png/${{ inputs.name }}_512.png" ]; then
ARGS+=("--icon" "src-tauri/png/${{ inputs.name }}_512.png")
fi
if [ "${{ inputs.new_window }}" = "true" ]; then
ARGS+=("--new-window")
fi
# Build once with multiple targets (faster than separate builds)
node dist/cli.js "${ARGS[@]}" --targets deb,appimage
mkdir -p output/linux
# The CLI copies files to project root and removes them from bundle directory
# Check project root first, then fallback to bundle directory
if [ -f "${{ inputs.name }}.deb" ]; then
mv "${{ inputs.name }}.deb" output/linux/${{inputs.title}}_`arch`.deb
elif [ -n "$(ls src-tauri/target/release/bundle/deb/*.deb 2>/dev/null)" ]; then
mv src-tauri/target/release/bundle/deb/*.deb output/linux/${{inputs.title}}_`arch`.deb
else
echo "Error: No DEB file found"
find . -name "*.deb" -type f
exit 1
fi
if [ -f "${{ inputs.name }}.AppImage" ]; then
mv "${{ inputs.name }}.AppImage" output/linux/"${{inputs.title}}"_`arch`.AppImage
elif [ -n "$(ls src-tauri/target/release/bundle/appimage/*.AppImage 2>/dev/null)" ]; then
mv src-tauri/target/release/bundle/appimage/*.AppImage output/linux/"${{inputs.title}}"_`arch`.AppImage
else
echo "Error: No AppImage file found"
find . -name "*.AppImage" -type f
exit 1
fi
- name: Build for macOS
if: matrix.os == 'macos-latest'
timeout-minutes: 25
env:
TAURI_BUNDLER_DMG_IGNORE_CI: "true"
run: |
# Use title as app product name on macOS to preserve display casing in .app
ARGS=("${{ inputs.url }}" "--name" "${{ inputs.title }}" "--hide-title-bar")
# Auto-detect local icon or use provided icon
if [ -n "${{ inputs.icon }}" ]; then
ARGS+=("--icon" "${{ inputs.icon }}")
elif [ -f "src-tauri/icons/${{ inputs.name }}.icns" ]; then
ARGS+=("--icon" "src-tauri/icons/${{ inputs.name }}.icns")
fi
if [ "${{ inputs.new_window }}" = "true" ]; then
ARGS+=("--new-window")
fi
node dist/cli.js "${ARGS[@]}" --targets universal --multi-arch
mkdir -p output/macos
# The CLI copies the DMG to project root and removes it from bundle directory
# Check project root first, then fallback to bundle directory
if [ -f "${{ inputs.title }}.dmg" ]; then
mv "${{ inputs.title }}.dmg" output/macos/"${{inputs.title}}".dmg
elif [ -f "${{ inputs.name }}.dmg" ]; then
mv "${{ inputs.name }}.dmg" output/macos/"${{inputs.title}}".dmg
elif [ -n "$(ls src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg 2>/dev/null)" ]; then
mv src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg output/macos/"${{inputs.title}}".dmg
else
echo "Error: No DMG file found"
echo "Searched locations:"
echo " - ${{ inputs.name }}.dmg (project root)"
echo " - src-tauri/target/universal-apple-darwin/release/bundle/dmg/"
find . -name "*.dmg" -type f
exit 1
fi
- name: Notarize macOS DMG
if: matrix.os == 'macos-latest'
env:
PAKE_NOTARIZE_APPLE_ID: ${{ secrets.PAKE_NOTARIZE_APPLE_ID }}
PAKE_NOTARIZE_TEAM_ID: ${{ secrets.PAKE_NOTARIZE_TEAM_ID }}
PAKE_NOTARIZE_PASSWORD: ${{ secrets.PAKE_NOTARIZE_PASSWORD }}
run: |
set -euo pipefail
if [ -z "${PAKE_NOTARIZE_APPLE_ID:-}" ] || [ -z "${PAKE_NOTARIZE_TEAM_ID:-}" ] || [ -z "${PAKE_NOTARIZE_PASSWORD:-}" ]; then
echo "Notarization secrets not configured, skipping notarization."
exit 0
fi
DMG_PATH="output/macos/${{inputs.title}}.dmg"
if [ ! -f "$DMG_PATH" ]; then
echo "Error: DMG file not found at $DMG_PATH"
exit 1
fi
echo "Submitting DMG to Apple notarization service..."
xcrun notarytool submit "$DMG_PATH" \
--apple-id "$PAKE_NOTARIZE_APPLE_ID" \
--team-id "$PAKE_NOTARIZE_TEAM_ID" \
--password "$PAKE_NOTARIZE_PASSWORD" \
--wait
echo "Stapling notarization ticket..."
xcrun stapler staple "$DMG_PATH"
- name: Build for Windows
if: matrix.os == 'windows-latest'
timeout-minutes: 20
run: |
# Use title as app product name on Windows to preserve display casing
$args = "${{ inputs.url }}", "--name", "${{ inputs.title }}"
# Auto-detect local icon or use provided icon
if ("${{ inputs.icon }}" -ne "") {
$args += "--icon", "${{ inputs.icon }}"
} elseif (Test-Path "src-tauri\png\${{ inputs.name }}_256.ico") {
$args += "--icon", "src-tauri\png\${{ inputs.name }}_256.ico"
}
if ("${{ inputs.new_window }}" -eq "true") {
$args += "--new-window"
}
$args += "--targets", "x64"
node dist/cli.js $args
New-Item -Path "output\windows" -ItemType Directory -Force
# The CLI copies the MSI to project root and removes it from bundle directory
# Check project root first, then fallback to bundle directories
$projectRootMsi = "${{ inputs.title }}.msi"
$legacyProjectRootMsi = "${{ inputs.name }}.msi"
if (Test-Path $projectRootMsi) {
Move-Item -Path $projectRootMsi -Destination "output\windows\${{inputs.title}}_x64.msi"
} elseif (Test-Path $legacyProjectRootMsi) {
Move-Item -Path $legacyProjectRootMsi -Destination "output\windows\${{inputs.title}}_x64.msi"
} else {
# Check architecture-specific path (x64 builds)
$msiFiles = Get-ChildItem -Path "src-tauri\target\x86_64-pc-windows-msvc\release\bundle\msi\*.msi" -ErrorAction SilentlyContinue
# Fallback to generic path
if (-not $msiFiles) {
$msiFiles = Get-ChildItem -Path "src-tauri\target\release\bundle\msi\*.msi" -ErrorAction SilentlyContinue
}
if ($msiFiles) {
Move-Item -Path $msiFiles[0].FullName -Destination "output\windows\${{inputs.title}}_x64.msi"
} else {
Write-Error "No MSI files found in expected locations"
Write-Host "Searched paths:"
Write-Host " - $projectRootMsi (project root)"
Write-Host " - $legacyProjectRootMsi (project root)"
Write-Host " - src-tauri\target\x86_64-pc-windows-msvc\release\bundle\msi\"
Write-Host " - src-tauri\target\release\bundle\msi\"
Write-Host "`nAll MSI files in target directory:"
Get-ChildItem -Path "src-tauri\target\" -Recurse -Name "*.msi" | Write-Host
exit 1
}
}
git checkout -- src-tauri/Cargo.lock
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: ${{ inputs.title }}-${{ matrix.build }}
path: output/*/*.*
retention-days: 3
- name: Upload to release (Linux)
uses: ncipollo/release-action@v1 # cspell:disable-line
if: matrix.os == 'ubuntu-latest' && startsWith(github.ref, 'refs/tags/')
with:
allowUpdates: true
omitBody: true
omitName: true
artifacts: "output/linux/*.deb,output/linux/*.AppImage"
token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload to release (macOS)
uses: ncipollo/release-action@v1 # cspell:disable-line
if: matrix.os == 'macos-latest' && startsWith(github.ref, 'refs/tags/')
with:
allowUpdates: true
omitBody: true
omitName: true
artifacts: "output/macos/*.dmg"
token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload to release (Windows)
uses: ncipollo/release-action@v1 # cspell:disable-line
if: matrix.os == 'windows-latest' && startsWith(github.ref, 'refs/tags/')
with:
allowUpdates: true
omitBody: true
omitName: true
artifacts: "output/windows/*.msi"
token: ${{ secrets.GITHUB_TOKEN }}