Files
Termix/.github/workflows/electron-build.yml
2025-10-25 14:12:09 -05:00

572 lines
22 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Build Electron App
on:
workflow_dispatch:
inputs:
build_type:
description: "Platform to build for"
required: true
default: "all"
type: choice
options:
- all
- windows
- linux
- macos
artifact_destination:
description: "What to do with the built app"
required: true
default: "file"
type: choice
options:
- none
- file
- release
- submit
jobs:
build-windows:
runs-on: windows-latest
if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == ''
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Get version
id: package-version
run: |
$VERSION = (Get-Content package.json | ConvertFrom-Json).version
echo "version=$VERSION" >> $env:GITHUB_OUTPUT
echo "Building version: $VERSION"
- name: Build Windows (All Architectures)
run: npm run build && npx electron-builder --win --x64 --ia32
- name: List release files
run: |
echo "Contents of release directory:"
dir release
- name: Upload Windows x64 NSIS Installer
uses: actions/upload-artifact@v4
if: hashFiles('release/*_x64_*_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_windows_x64_nsis
path: release/*_x64_*_nsis.exe
retention-days: 30
- name: Upload Windows ia32 NSIS Installer
uses: actions/upload-artifact@v4
if: hashFiles('release/*_ia32_*_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_windows_ia32_nsis
path: release/*_ia32_*_nsis.exe
retention-days: 30
- name: Upload Windows x64 MSI Installer
uses: actions/upload-artifact@v4
if: hashFiles('release/*_x64_*_msi.msi') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_windows_x64_msi
path: release/*_x64_*_msi.msi
retention-days: 30
- name: Upload Windows ia32 MSI Installer
uses: actions/upload-artifact@v4
if: hashFiles('release/*_ia32_*_msi.msi') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_windows_ia32_msi
path: release/*_ia32_*_msi.msi
retention-days: 30
- name: Create Windows x64 Portable zip
if: hashFiles('release/win-unpacked/*') != ''
run: |
$VERSION = "${{ steps.package-version.outputs.version }}"
Compress-Archive -Path "release\win-unpacked\*" -DestinationPath "termix_windows_x64_${VERSION}_portable.zip"
- name: Create Windows ia32 Portable zip
if: hashFiles('release/win-ia32-unpacked/*') != ''
run: |
$VERSION = "${{ steps.package-version.outputs.version }}"
Compress-Archive -Path "release\win-ia32-unpacked\*" -DestinationPath "termix_windows_ia32_${VERSION}_portable.zip"
- name: Upload Windows x64 Portable
uses: actions/upload-artifact@v4
if: hashFiles('termix_windows_x64_*_portable.zip') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_windows_x64_portable
path: termix_windows_x64_*_portable.zip
retention-days: 30
- name: Upload Windows ia32 Portable
uses: actions/upload-artifact@v4
if: hashFiles('termix_windows_ia32_*_portable.zip') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_windows_ia32_portable
path: termix_windows_ia32_*_portable.zip
retention-days: 30
build-linux:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == ''
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: |
rm -f package-lock.json
npm install
npm install --force @rollup/rollup-linux-x64-gnu
npm install --force @rollup/rollup-linux-arm64-gnu
npm install --force @rollup/rollup-linux-arm-gnueabihf
- name: Build Linux (All Architectures)
run: npm run build && npx electron-builder --linux --x64 --arm64 --armv7l
- name: Rename tar.gz files to match convention
run: |
VERSION=$(node -p "require('./package.json').version")
cd release
# Rename x64 tar.gz if it exists
if [ -f "termix-${VERSION}-x64.tar.gz" ]; then
mv "termix-${VERSION}-x64.tar.gz" "termix_linux_x64_${VERSION}_portable.tar.gz"
echo "Renamed x64 tar.gz"
fi
# Rename arm64 tar.gz if it exists
if [ -f "termix-${VERSION}-arm64.tar.gz" ]; then
mv "termix-${VERSION}-arm64.tar.gz" "termix_linux_arm64_${VERSION}_portable.tar.gz"
echo "Renamed arm64 tar.gz"
fi
# Rename armv7l tar.gz if it exists
if [ -f "termix-${VERSION}-armv7l.tar.gz" ]; then
mv "termix-${VERSION}-armv7l.tar.gz" "termix_linux_armv7l_${VERSION}_portable.tar.gz"
echo "Renamed armv7l tar.gz"
fi
cd ..
- name: List release files
run: |
echo "Contents of release directory:"
ls -la release/
- name: Upload Linux x64 AppImage
uses: actions/upload-artifact@v4
if: hashFiles('release/*_x64_*_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_linux_x64_appimage
path: release/*_x64_*_appimage.AppImage
retention-days: 30
- name: Upload Linux arm64 AppImage
uses: actions/upload-artifact@v4
if: hashFiles('release/*_arm64_*_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_linux_arm64_appimage
path: release/*_arm64_*_appimage.AppImage
retention-days: 30
- name: Upload Linux x64 DEB
uses: actions/upload-artifact@v4
if: hashFiles('release/*_x64_*_deb.deb') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_linux_x64_deb
path: release/*_x64_*_deb.deb
retention-days: 30
- name: Upload Linux arm64 DEB
uses: actions/upload-artifact@v4
if: hashFiles('release/*_arm64_*_deb.deb') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_linux_arm64_deb
path: release/*_arm64_*_deb.deb
retention-days: 30
- name: Upload Linux armv7l DEB
uses: actions/upload-artifact@v4
if: hashFiles('release/*_armv7l_*_deb.deb') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_linux_armv7l_deb
path: release/*_armv7l_*_deb.deb
retention-days: 30
- name: Upload Linux x64 tar.gz
uses: actions/upload-artifact@v4
if: hashFiles('release/*_x64_*_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_linux_x64_portable
path: release/*_x64_*_portable.tar.gz
retention-days: 30
- name: Upload Linux arm64 tar.gz
uses: actions/upload-artifact@v4
if: hashFiles('release/*_arm64_*_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_linux_arm64_portable
path: release/*_arm64_*_portable.tar.gz
retention-days: 30
- name: Upload Linux armv7l tar.gz
uses: actions/upload-artifact@v4
if: hashFiles('release/*_armv7l_*_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_linux_armv7l_portable
path: release/*_armv7l_*_portable.tar.gz
retention-days: 30
build-macos:
runs-on: macos-latest
if: github.event.inputs.build_type == 'macos' || github.event.inputs.build_type == 'all'
needs: []
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: |
npm ci
npm install --force @rollup/rollup-darwin-arm64
npm install dmg-license
- name: Check for Code Signing Certificates
id: check_certs
run: |
if [ -n "${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.MAC_P12_PASSWORD }}" ]; then
echo "has_certs=true" >> $GITHUB_OUTPUT
else
echo "has_certs=false" >> $GITHUB_OUTPUT
echo "⚠️ Code signing certificates not configured. MAS build will be unsigned."
fi
- name: Import Code Signing Certificates
if: steps.check_certs.outputs.has_certs == 'true'
env:
MAC_BUILD_CERTIFICATE_BASE64: ${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }}
MAC_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.MAC_INSTALLER_CERTIFICATE_BASE64 }}
MAC_P12_PASSWORD: ${{ secrets.MAC_P12_PASSWORD }}
MAC_KEYCHAIN_PASSWORD: ${{ secrets.MAC_KEYCHAIN_PASSWORD }}
run: |
APP_CERT_PATH=$RUNNER_TEMP/app_certificate.p12
INSTALLER_CERT_PATH=$RUNNER_TEMP/installer_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# Decode certificates
echo -n "$MAC_BUILD_CERTIFICATE_BASE64" | base64 --decode -o $APP_CERT_PATH
if [ -n "$MAC_INSTALLER_CERTIFICATE_BASE64" ]; then
echo "Decoding installer certificate..."
echo -n "$MAC_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $INSTALLER_CERT_PATH
else
echo "⚠️ MAC_INSTALLER_CERTIFICATE_BASE64 is empty"
fi
# Create and configure keychain
security create-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Import application certificate
echo "Importing application certificate..."
security import $APP_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
# Import installer certificate if it exists
if [ -f "$INSTALLER_CERT_PATH" ]; then
echo "Importing installer certificate..."
security import $INSTALLER_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
else
echo "⚠️ Installer certificate file not found, skipping import"
fi
security list-keychain -d user -s $KEYCHAIN_PATH
echo "Imported certificates:"
security find-identity -v -p codesigning $KEYCHAIN_PATH
- name: Build macOS App Store Package
if: steps.check_certs.outputs.has_certs == 'true'
env:
ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true
run: |
# Get current version for display
CURRENT_VERSION=$(node -p "require('./package.json').version")
BUILD_VERSION="${{ github.run_number }}"
echo "✅ Package version: $CURRENT_VERSION (unchanged)"
echo "✅ Build number for Apple: $BUILD_VERSION"
# Build MAS with custom buildVersion
npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION"
- name: Clean up MAS keychain before DMG build
if: steps.check_certs.outputs.has_certs == 'true'
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
echo "Cleaned up MAS keychain"
- name: Check for Developer ID Certificates
id: check_dev_id_certs
run: |
if [ -n "${{ secrets.DEVELOPER_ID_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.DEVELOPER_ID_P12_PASSWORD }}" ]; then
echo "has_dev_id_certs=true" >> $GITHUB_OUTPUT
echo "✅ Developer ID certificates configured for DMG signing"
else
echo "has_dev_id_certs=false" >> $GITHUB_OUTPUT
echo "⚠️ Developer ID certificates not configured. DMG will be unsigned."
echo "Add DEVELOPER_ID_CERTIFICATE_BASE64 and DEVELOPER_ID_P12_PASSWORD secrets to enable DMG signing."
fi
- name: Import Developer ID Certificates
if: steps.check_dev_id_certs.outputs.has_dev_id_certs == 'true'
env:
DEVELOPER_ID_CERTIFICATE_BASE64: ${{ secrets.DEVELOPER_ID_CERTIFICATE_BASE64 }}
DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64: ${{ secrets.DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64 }}
DEVELOPER_ID_P12_PASSWORD: ${{ secrets.DEVELOPER_ID_P12_PASSWORD }}
MAC_KEYCHAIN_PASSWORD: ${{ secrets.MAC_KEYCHAIN_PASSWORD }}
run: |
DEV_CERT_PATH=$RUNNER_TEMP/dev_certificate.p12
DEV_INSTALLER_CERT_PATH=$RUNNER_TEMP/dev_installer_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/dev-signing.keychain-db
# Decode Developer ID certificate
echo -n "$DEVELOPER_ID_CERTIFICATE_BASE64" | base64 --decode -o $DEV_CERT_PATH
if [ -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64" ]; then
echo "Decoding Developer ID installer certificate..."
echo -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $DEV_INSTALLER_CERT_PATH
else
echo "⚠️ DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64 is empty (optional)"
fi
# Create and configure keychain
security create-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Import Developer ID Application certificate
echo "Importing Developer ID Application certificate..."
security import $DEV_CERT_PATH -P "$DEVELOPER_ID_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
# Import Developer ID Installer certificate if it exists
if [ -f "$DEV_INSTALLER_CERT_PATH" ]; then
echo "Importing Developer ID Installer certificate..."
security import $DEV_INSTALLER_CERT_PATH -P "$DEVELOPER_ID_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
fi
security list-keychain -d user -s $KEYCHAIN_PATH
echo "Imported Developer ID certificates:"
security find-identity -v -p codesigning $KEYCHAIN_PATH
- name: Build macOS DMG
env:
ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
# Build DMG without running npm run build again (already built above or skip if no certs)
if [ "${{ steps.check_certs.outputs.has_certs }}" == "true" ]; then
# Frontend already built, just package DMG
npx electron-builder --mac dmg --universal --x64 --arm64
else
# No certs, need to build frontend first
npm run build && npx electron-builder --mac dmg --universal --x64 --arm64
fi
- name: List release directory
if: steps.check_certs.outputs.has_certs == 'true'
run: |
echo "Contents of release directory:"
ls -R release/ || echo "Release directory not found"
- name: Upload macOS MAS PKG
if: steps.check_certs.outputs.has_certs == 'true' && hashFiles('release/*_*_*_mas.pkg') != '' && (github.event.inputs.artifact_destination == 'file' || github.event.inputs.artifact_destination == 'release' || github.event.inputs.artifact_destination == 'submit')
uses: actions/upload-artifact@v4
with:
name: termix_macos_mas
path: release/*_*_*_mas.pkg
retention-days: 30
if-no-files-found: warn
- name: Upload macOS Universal DMG
uses: actions/upload-artifact@v4
if: hashFiles('release/*_universal_*_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_macos_universal_dmg
path: release/*_universal_*_dmg.dmg
retention-days: 30
- name: Upload macOS x64 DMG
uses: actions/upload-artifact@v4
if: hashFiles('release/*_x64_*_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_macos_x64_dmg
path: release/*_x64_*_dmg.dmg
retention-days: 30
- name: Upload macOS arm64 DMG
uses: actions/upload-artifact@v4
if: hashFiles('release/*_arm64_*_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_macos_arm64_dmg
path: release/*_arm64_*_dmg.dmg
retention-days: 30
- name: Check for App Store Connect API credentials
if: steps.check_certs.outputs.has_certs == 'true'
id: check_asc_creds
run: |
if [ -n "${{ secrets.APPLE_KEY_ID }}" ] && [ -n "${{ secrets.APPLE_ISSUER_ID }}" ] && [ -n "${{ secrets.APPLE_KEY_CONTENT }}" ]; then
echo "has_credentials=true" >> $GITHUB_OUTPUT
if [ "${{ github.event.inputs.artifact_destination }}" == "submit" ]; then
echo "✅ App Store Connect API credentials found. Will deploy to TestFlight."
else
echo " App Store Connect API credentials found, but store submission is disabled."
fi
else
echo "has_credentials=false" >> $GITHUB_OUTPUT
echo "⚠️ App Store Connect API credentials not configured. Skipping deployment."
echo "Add APPLE_KEY_ID, APPLE_ISSUER_ID, and APPLE_KEY_CONTENT secrets to enable automatic deployment."
fi
- name: Setup Ruby for Fastlane
if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit'
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: false
- name: Install Fastlane
if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit'
run: |
gem install fastlane -N
fastlane --version
- name: Deploy to App Store Connect (TestFlight)
if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit'
run: |
PKG_FILE=$(find release -name "*.pkg" -type f | head -n 1)
if [ -z "$PKG_FILE" ]; then
echo "Error: No .pkg file found in release directory"
exit 1
fi
echo "Found package: $PKG_FILE"
# Create API key file
mkdir -p ~/private_keys
echo "${{ secrets.APPLE_KEY_CONTENT }}" | base64 --decode > ~/private_keys/AuthKey_${{ secrets.APPLE_KEY_ID }}.p8
# Upload to App Store Connect using xcrun altool
xcrun altool --upload-app -f "$PKG_FILE" \
--type macos \
--apiKey "${{ secrets.APPLE_KEY_ID }}" \
--apiIssuer "${{ secrets.APPLE_ISSUER_ID }}"
echo "✅ Upload complete! Build will appear in App Store Connect after processing (10-30 minutes)"
continue-on-error: true
- name: Clean up keychains
if: always()
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
security delete-keychain $RUNNER_TEMP/dev-signing.keychain-db || true
upload-to-release:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.event.inputs.artifact_destination == 'release'
needs: [build-windows, build-linux, build-macos]
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Get latest release
id: get_release
run: |
echo "Fetching latest release from ${{ github.repository }}..."
LATEST_RELEASE=$(gh release list --repo ${{ github.repository }} --limit 1 --json tagName,name,isLatest -q '.[0]')
if [ -z "$LATEST_RELEASE" ]; then
echo "ERROR: No releases found in ${{ github.repository }}"
exit 1
fi
RELEASE_TAG=$(echo "$LATEST_RELEASE" | jq -r '.tagName')
RELEASE_NAME=$(echo "$LATEST_RELEASE" | jq -r '.name')
echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
echo "name=$RELEASE_NAME" >> $GITHUB_OUTPUT
echo "Latest release: $RELEASE_NAME ($RELEASE_TAG)"
env:
GH_TOKEN: ${{ github.token }}
- name: Display artifact structure
run: |
echo "Artifact structure:"
ls -R artifacts/
- name: Upload artifacts to latest release
run: |
RELEASE_TAG="${{ steps.get_release.outputs.tag }}"
echo "Uploading artifacts to release: $RELEASE_TAG"
echo ""
cd artifacts
for dir in */; do
echo "Processing directory: $dir"
cd "$dir"
for file in *; do
if [ -f "$file" ]; then
echo "Uploading: $file"
gh release upload "$RELEASE_TAG" "$file" --repo ${{ github.repository }} --clobber
echo "✓ $file uploaded successfully"
fi
done
cd ..
done
echo ""
echo "All artifacts uploaded to: https://github.com/${{ github.repository }}/releases/tag/$RELEASE_TAG"
env:
GH_TOKEN: ${{ github.token }}