From b0e49ffb4f07abf526acb33fb00da4dda044dcd7 Mon Sep 17 00:00:00 2001 From: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Date: Wed, 29 Oct 2025 19:19:02 -0500 Subject: [PATCH] Update print statement from 'Hello' to 'Goodbye' --- .github/workflows/docker-image.yml | 1199 ++++++++++++++++++++++++++-- 1 file changed, 1135 insertions(+), 64 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 80ab5da6..b50a8905 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,94 +1,1165 @@ -name: Build and Push Docker Image +name: Build and Push Electron App on: workflow_dispatch: inputs: - version: - description: "Version to build (e.g., 1.8.0)" + build_type: + description: "Platform to build for" required: true - production: - description: "Is this a production build?" + default: "all" + type: choice + options: + - all + - windows + - linux + - macos + artifact_destination: + description: "What to do with the built app" required: true - default: false - type: boolean + default: "file" + type: choice + options: + - none + - file + - release + - submit jobs: - build: - runs-on: ubuntu-latest + 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: Set up QEMU - uses: docker/setup-qemu-action@v3 + - name: Setup Node.js + uses: actions/setup-node@v4 with: - platforms: linux/amd64,linux/arm64,linux/arm/v7 + node-version: "20" + cache: "npm" - - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Determine tags - id: tags + - name: Install dependencies run: | - VERSION=${{ github.event.inputs.version }} - PROD=${{ github.event.inputs.production }} + # Retry npm ci up to 3 times on failure + $maxAttempts = 3 + $attempt = 1 + while ($attempt -le $maxAttempts) { + try { + npm ci + break + } catch { + if ($attempt -eq $maxAttempts) { + Write-Error "npm ci failed after $maxAttempts attempts" + exit 1 + } + Write-Host "npm ci attempt $attempt failed, retrying in 10 seconds..." + Start-Sleep -Seconds 10 + $attempt++ + } + } - TAGS=() - ALL_TAGS=() + - 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" - if [ "$PROD" = "true" ]; then - # Production build → push release + latest to both GHCR and Docker Hub - TAGS+=("release-$VERSION" "latest") - for tag in "${TAGS[@]}"; do - ALL_TAGS+=("ghcr.io/lukegus/termix:$tag") - ALL_TAGS+=("docker.io/bugattiguy527/termix:$tag") - done - else - # Dev build → push only dev-x.x.x to GHCR - TAGS+=("dev-$VERSION") - for tag in "${TAGS[@]}"; do - ALL_TAGS+=("ghcr.io/lukegus/termix:$tag") - done + - 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 + # Retry npm install up to 3 times on failure + for i in 1 2 3; do + if npm install; then + break + else + if [ $i -eq 3 ]; then + echo "npm install failed after 3 attempts" + exit 1 + fi + echo "npm install attempt $i failed, retrying in 10 seconds..." + sleep 10 + fi + done + 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 - echo "ALL_TAGS=${ALL_TAGS[*]}" >> $GITHUB_ENV - echo "All tags to build:" - printf '%s\n' "${ALL_TAGS[@]}" + # 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 - - name: Login to GHCR - uses: docker/login-action@v3 + # 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: - registry: ghcr.io - username: lukegus - password: ${{ secrets.GHCR_TOKEN }} + name: termix_linux_x64_appimage + path: release/*_x64_*_appimage.AppImage + retention-days: 30 - - name: Login to Docker Hub (prod only) - if: ${{ github.event.inputs.production == 'true' }} - uses: docker/login-action@v3 + - name: Upload Linux arm64 AppImage + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_arm64_*_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none' with: - username: bugattiguy527 - password: ${{ secrets.DOCKERHUB_TOKEN }} + name: termix_linux_arm64_appimage + path: release/*_arm64_*_appimage.AppImage + retention-days: 30 - - name: Build and push multi-arch image - uses: docker/build-push-action@v5 + - name: Upload Linux x64 DEB + uses: actions/upload-artifact@v4 + if: hashFiles('release/*_x64_*_deb.deb') != '' && github.event.inputs.artifact_destination != 'none' with: - context: . - file: ./docker/Dockerfile - push: true - platforms: linux/amd64,linux/arm64,linux/arm/v7 - tags: ${{ env.ALL_TAGS }} - build-args: | - BUILDKIT_INLINE_CACHE=1 - BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 - labels: | - org.opencontainers.image.source=https://github.com/${{ github.repository }} - org.opencontainers.image.revision=${{ github.sha }} - outputs: type=registry,compression=zstd,compression-level=19 + name: termix_linux_x64_deb + path: release/*_x64_*_deb.deb + retention-days: 30 - - name: Cleanup Docker + - 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: | + # Retry npm ci up to 3 times on failure + for i in 1 2 3; do + if npm ci; then + break + else + if [ $i -eq 3 ]; then + echo "npm ci failed after 3 attempts" + exit 1 + fi + echo "npm ci attempt $i failed, retrying in 10 seconds..." + sleep 10 + fi + done + 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: | - docker image prune -af - docker system prune -af --volumes + security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true + security delete-keychain $RUNNER_TEMP/dev-signing.keychain-db || true + + submit-to-chocolatey: + runs-on: windows-latest + if: github.event.inputs.artifact_destination == 'submit' + needs: [build-windows] + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Get version from package.json + id: package-version + run: | + $VERSION = (Get-Content package.json | ConvertFrom-Json).version + echo "version=$VERSION" >> $env:GITHUB_OUTPUT + echo "Building Chocolatey package for version: $VERSION" + + - name: Download Windows x64 MSI artifact + uses: actions/download-artifact@v4 + with: + name: termix_windows_x64_msi + path: artifact + + - name: Get MSI file info + id: msi-info + run: | + $VERSION = "${{ steps.package-version.outputs.version }}" + $MSI_FILE = Get-ChildItem -Path artifact -Filter "*.msi" | Select-Object -First 1 + $MSI_NAME = $MSI_FILE.Name + $CHECKSUM = (Get-FileHash -Path $MSI_FILE.FullName -Algorithm SHA256).Hash + + echo "msi_name=$MSI_NAME" >> $env:GITHUB_OUTPUT + echo "checksum=$CHECKSUM" >> $env:GITHUB_OUTPUT + echo "MSI File: $MSI_NAME" + echo "SHA256: $CHECKSUM" + + - name: Prepare Chocolatey package + run: | + $VERSION = "${{ steps.package-version.outputs.version }}" + $CHECKSUM = "${{ steps.msi-info.outputs.checksum }}" + $MSI_NAME = "${{ steps.msi-info.outputs.msi_name }}" + + # Construct the download URL with the actual release tag format + $DOWNLOAD_URL = "https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$MSI_NAME" + + # Copy chocolatey files to build directory + New-Item -ItemType Directory -Force -Path "choco-build" + Copy-Item -Path "chocolatey\*" -Destination "choco-build" -Recurse -Force + + # Update chocolateyinstall.ps1 with actual values + $installScript = Get-Content "choco-build\tools\chocolateyinstall.ps1" -Raw -Encoding UTF8 + $installScript = $installScript -replace 'DOWNLOAD_URL_PLACEHOLDER', $DOWNLOAD_URL + $installScript = $installScript -replace 'CHECKSUM_PLACEHOLDER', $CHECKSUM + [System.IO.File]::WriteAllText("$PWD\choco-build\tools\chocolateyinstall.ps1", $installScript, [System.Text.UTF8Encoding]::new($false)) + + # Update nuspec with version (preserve UTF-8 encoding without BOM) + $nuspec = Get-Content "choco-build\termix-ssh.nuspec" -Raw -Encoding UTF8 + $nuspec = $nuspec -replace 'VERSION_PLACEHOLDER', $VERSION + [System.IO.File]::WriteAllText("$PWD\choco-build\termix-ssh.nuspec", $nuspec, [System.Text.UTF8Encoding]::new($false)) + + echo "Chocolatey package prepared for version $VERSION" + echo "Download URL: $DOWNLOAD_URL" + + # Verify the nuspec is valid + echo "" + echo "Verifying nuspec content:" + Get-Content "choco-build\termix-ssh.nuspec" -Head 10 + echo "" + + - name: Install Chocolatey + run: | + Set-ExecutionPolicy Bypass -Scope Process -Force + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 + iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) + + - name: Pack Chocolatey package + run: | + cd choco-build + echo "Packing Chocolatey package..." + choco pack termix-ssh.nuspec + + if ($LASTEXITCODE -ne 0) { + echo "❌ Failed to pack Chocolatey package" + exit 1 + } + + echo "" + echo "✅ Package created successfully" + echo "Package contents:" + Get-ChildItem *.nupkg | ForEach-Object { echo $_.Name } + + - name: Check for Chocolatey API Key + id: check_choco_key + run: | + if ("${{ secrets.CHOCOLATEY_API_KEY }}" -ne "") { + echo "has_key=true" >> $env:GITHUB_OUTPUT + echo "✅ Chocolatey API key found. Will push to Chocolatey." + } else { + echo "has_key=false" >> $env:GITHUB_OUTPUT + echo "⚠️ Chocolatey API key not configured. Package will be created but not pushed." + echo "Add CHOCOLATEY_API_KEY secret to enable automatic submission." + } + + - name: Push to Chocolatey + if: steps.check_choco_key.outputs.has_key == 'true' + run: | + $VERSION = "${{ steps.package-version.outputs.version }}" + cd choco-build + choco apikey --key "${{ secrets.CHOCOLATEY_API_KEY }}" --source https://push.chocolatey.org/ + + try { + choco push "termix-ssh.$VERSION.nupkg" --source https://push.chocolatey.org/ + if ($LASTEXITCODE -eq 0) { + echo "" + echo "✅ Package pushed to Chocolatey successfully!" + echo "View at: https://community.chocolatey.org/packages/termix-ssh/$VERSION" + } else { + throw "Chocolatey push failed with exit code $LASTEXITCODE" + } + } catch { + echo "" + echo "❌ Failed to push to Chocolatey" + echo "" + echo "Common reasons:" + echo "1. Package ID 'termix-ssh' is already owned by another user" + echo "2. You need to register/claim the package ID first" + echo "3. API key doesn't have push permissions" + echo "" + echo "Solutions:" + echo "1. Check if package exists: https://community.chocolatey.org/packages/termix-ssh" + echo "2. If it exists and is yours, contact Chocolatey support to claim it" + echo "3. Register a new package ID at: https://community.chocolatey.org/" + echo "" + echo "The package artifact has been saved for manual submission." + echo "" + exit 1 + } + + - name: Upload Chocolatey package as artifact + uses: actions/upload-artifact@v4 + with: + name: chocolatey-package + path: choco-build/*.nupkg + retention-days: 30 + + submit-to-flatpak: + runs-on: ubuntu-latest + if: github.event.inputs.artifact_destination == 'submit' + needs: [build-linux] + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Get version from package.json + id: package-version + run: | + VERSION=$(node -p "require('./package.json').version") + RELEASE_DATE=$(date +%Y-%m-%d) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT + echo "Building Flatpak submission for version: $VERSION" + + - name: Download Linux x64 AppImage artifact + uses: actions/download-artifact@v4 + with: + name: termix_linux_x64_appimage + path: artifact-x64 + + - name: Download Linux arm64 AppImage artifact + uses: actions/download-artifact@v4 + with: + name: termix_linux_arm64_appimage + path: artifact-arm64 + + - name: Get AppImage file info + id: appimage-info + run: | + VERSION="${{ steps.package-version.outputs.version }}" + + # x64 AppImage + APPIMAGE_X64_FILE=$(find artifact-x64 -name "*.AppImage" -type f | head -n 1) + APPIMAGE_X64_NAME=$(basename "$APPIMAGE_X64_FILE") + CHECKSUM_X64=$(sha256sum "$APPIMAGE_X64_FILE" | awk '{print $1}') + + # arm64 AppImage + APPIMAGE_ARM64_FILE=$(find artifact-arm64 -name "*.AppImage" -type f | head -n 1) + APPIMAGE_ARM64_NAME=$(basename "$APPIMAGE_ARM64_FILE") + CHECKSUM_ARM64=$(sha256sum "$APPIMAGE_ARM64_FILE" | awk '{print $1}') + + echo "appimage_x64_name=$APPIMAGE_X64_NAME" >> $GITHUB_OUTPUT + echo "checksum_x64=$CHECKSUM_X64" >> $GITHUB_OUTPUT + echo "appimage_arm64_name=$APPIMAGE_ARM64_NAME" >> $GITHUB_OUTPUT + echo "checksum_arm64=$CHECKSUM_ARM64" >> $GITHUB_OUTPUT + + echo "x64 AppImage: $APPIMAGE_X64_NAME" + echo "x64 SHA256: $CHECKSUM_X64" + echo "arm64 AppImage: $APPIMAGE_ARM64_NAME" + echo "arm64 SHA256: $CHECKSUM_ARM64" + + - name: Install ImageMagick for icon generation + run: | + sudo apt-get update + sudo apt-get install -y imagemagick + + - name: Prepare Flatpak submission files + run: | + VERSION="${{ steps.package-version.outputs.version }}" + CHECKSUM_X64="${{ steps.appimage-info.outputs.checksum_x64 }}" + CHECKSUM_ARM64="${{ steps.appimage-info.outputs.checksum_arm64 }}" + RELEASE_DATE="${{ steps.package-version.outputs.release_date }}" + APPIMAGE_X64_NAME="${{ steps.appimage-info.outputs.appimage_x64_name }}" + APPIMAGE_ARM64_NAME="${{ steps.appimage-info.outputs.appimage_arm64_name }}" + + # Create submission directory + mkdir -p flatpak-submission + + # Copy Flatpak files to submission directory + cp flatpak/com.karmaa.termix.yml flatpak-submission/ + cp flatpak/com.karmaa.termix.desktop flatpak-submission/ + cp flatpak/com.karmaa.termix.metainfo.xml flatpak-submission/ + cp flatpak/flathub.json flatpak-submission/ + + # Copy and prepare icons + cp public/icon.svg flatpak-submission/com.karmaa.termix.svg + convert public/icon.png -resize 256x256 flatpak-submission/icon-256.png + convert public/icon.png -resize 128x128 flatpak-submission/icon-128.png + + # Update manifest with version and checksums + sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.yml + sed -i "s/CHECKSUM_X64_PLACEHOLDER/$CHECKSUM_X64/g" flatpak-submission/com.karmaa.termix.yml + sed -i "s/CHECKSUM_ARM64_PLACEHOLDER/$CHECKSUM_ARM64/g" flatpak-submission/com.karmaa.termix.yml + + # Update metainfo with version and date + sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.metainfo.xml + sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" flatpak-submission/com.karmaa.termix.metainfo.xml + + echo "✅ Flatpak submission files prepared for version $VERSION" + echo "x64 Download URL: https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_X64_NAME" + echo "arm64 Download URL: https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_ARM64_NAME" + + - name: Create submission instructions + run: | + cat > flatpak-submission/SUBMISSION_INSTRUCTIONS.md << 'EOF' + # Flathub Submission Instructions for Termix + + ## Automatic Submission (Recommended) + + All files needed for Flathub submission are in this artifact. Follow these steps: + + 1. **Fork the Flathub repository**: + - Go to https://github.com/flathub/flathub + - Click "Fork" button + + 2. **Clone your fork**: + ```bash + git clone https://github.com/YOUR-USERNAME/flathub.git + cd flathub + git checkout -b com.karmaa.termix + ``` + + 3. **Copy all files from this artifact** to the root of your flathub fork + + 4. **Commit and push**: + ```bash + git add . + git commit -m "Add Termix ${{ steps.package-version.outputs.version }}" + git push origin com.karmaa.termix + ``` + + 5. **Create Pull Request**: + - Go to https://github.com/YOUR-USERNAME/flathub + - Click "Compare & pull request" + - Submit PR to flathub/flathub + + ## Files in this submission: + + - `com.karmaa.termix.yml` - Flatpak manifest + - `com.karmaa.termix.desktop` - Desktop entry + - `com.karmaa.termix.metainfo.xml` - AppStream metadata + - `flathub.json` - Flathub configuration + - `com.karmaa.termix.svg` - SVG icon + - `icon-256.png` - 256x256 icon + - `icon-128.png` - 128x128 icon + + ## Version Information: + + - Version: ${{ steps.package-version.outputs.version }} + - Release Date: ${{ steps.package-version.outputs.release_date }} + - x64 AppImage SHA256: ${{ steps.appimage-info.outputs.checksum_x64 }} + - arm64 AppImage SHA256: ${{ steps.appimage-info.outputs.checksum_arm64 }} + + ## After Submission: + + 1. Flathub maintainers will review your submission (usually 1-5 days) + 2. They may request changes - be responsive to feedback + 3. Once approved, Termix will be available via: `flatpak install flathub com.karmaa.termix` + + ## Resources: + + - [Flathub Submission Guidelines](https://docs.flathub.org/docs/for-app-authors/submission) + - [Flatpak Documentation](https://docs.flatpak.org/) + EOF + + echo "✅ Created submission instructions" + + - name: List submission files + run: | + echo "Flatpak submission files:" + ls -la flatpak-submission/ + + - name: Upload Flatpak submission as artifact + uses: actions/upload-artifact@v4 + with: + name: flatpak-submission + path: flatpak-submission/* + retention-days: 30 + + - name: Display next steps + run: | + echo "" + echo "🎉 Flatpak submission files ready!" + echo "" + echo "📦 Download the 'flatpak-submission' artifact and follow SUBMISSION_INSTRUCTIONS.md" + echo "" + echo "Quick summary:" + echo "1. Fork https://github.com/flathub/flathub" + echo "2. Copy artifact files to your fork" + echo "3. Create PR to flathub/flathub" + echo "" + + submit-to-homebrew: + runs-on: macos-latest + if: github.event.inputs.artifact_destination == 'submit' + needs: [build-macos] + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Get version from package.json + id: package-version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Building Homebrew Cask for version: $VERSION" + + - name: Download macOS Universal DMG artifact + uses: actions/download-artifact@v4 + with: + name: termix_macos_universal_dmg + path: artifact + + - name: Get DMG file info + id: dmg-info + run: | + VERSION="${{ steps.package-version.outputs.version }}" + DMG_FILE=$(find artifact -name "*.dmg" -type f | head -n 1) + DMG_NAME=$(basename "$DMG_FILE") + CHECKSUM=$(shasum -a 256 "$DMG_FILE" | awk '{print $1}') + + echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT + echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT + echo "DMG File: $DMG_NAME" + echo "SHA256: $CHECKSUM" + + - name: Prepare Homebrew submission files + run: | + VERSION="${{ steps.package-version.outputs.version }}" + CHECKSUM="${{ steps.dmg-info.outputs.checksum }}" + DMG_NAME="${{ steps.dmg-info.outputs.dmg_name }}" + + # Create submission directory + mkdir -p homebrew-submission/Casks/t + + # Copy Homebrew cask file + cp homebrew/termix.rb homebrew-submission/Casks/t/termix.rb + cp homebrew/README.md homebrew-submission/ + + # Update cask with version and checksum + sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" homebrew-submission/Casks/t/termix.rb + sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-submission/Casks/t/termix.rb + + echo "✅ Homebrew Cask prepared for version $VERSION" + echo "Download URL: https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$DMG_NAME" + + - name: Verify Cask syntax + run: | + # Install Homebrew if not present (should be on macos-latest) + if ! command -v brew &> /dev/null; then + echo "Installing Homebrew..." + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + fi + + # Basic syntax check + ruby -c homebrew-submission/Casks/t/termix.rb + echo "✅ Cask syntax is valid" + + - name: Create submission instructions + run: | + cat > homebrew-submission/SUBMISSION_INSTRUCTIONS.md << 'EOF' + # Homebrew Cask Submission Instructions for Termix + + ## Option 1: Submit to Official Homebrew Cask (Recommended) + + ### Prerequisites + - macOS with Homebrew installed + - GitHub account + + ### Steps + + 1. **Fork the Homebrew Cask repository**: + - Go to https://github.com/Homebrew/homebrew-cask + - Click "Fork" button + + 2. **Clone your fork**: + ```bash + git clone https://github.com/YOUR-USERNAME/homebrew-cask.git + cd homebrew-cask + git checkout -b termix + ``` + + 3. **Copy the cask file**: + - Copy `Casks/t/termix.rb` from this artifact to your fork at `Casks/t/termix.rb` + - Note: Casks are organized by first letter in subdirectories + + 4. **Test the cask locally**: + ```bash + brew install --cask ./Casks/t/termix.rb + brew uninstall --cask termix + ``` + + 5. **Run audit checks**: + ```bash + brew audit --cask --online ./Casks/t/termix.rb + brew style ./Casks/t/termix.rb + ``` + + 6. **Commit and push**: + ```bash + git add Casks/t/termix.rb + git commit -m "Add Termix ${{ steps.package-version.outputs.version }}" + git push origin termix + ``` + + 7. **Create Pull Request**: + - Go to https://github.com/YOUR-USERNAME/homebrew-cask + - Click "Compare & pull request" + - Fill in the PR template + - Submit to Homebrew/homebrew-cask + + ### PR Requirements + + Your PR should include: + - Clear commit message: "Add Termix X.Y.Z" or "Update Termix to X.Y.Z" + - All audit checks passing + - Working download URL + - Valid SHA256 checksum + + ## Option 2: Create Your Own Tap (Alternative) + + If you want more control and faster updates: + + 1. **Create a tap repository**: + - Create repo: `Termix-SSH/homebrew-termix` + - Add `Casks/termix.rb` to the repo + + 2. **Users install with**: + ```bash + brew tap termix-ssh/termix + brew install --cask termix + ``` + + ### Advantages of Custom Tap + - No approval process + - Instant updates + - Full control + - Can include beta versions + + ### Disadvantages + - Less discoverable + - Users must add tap first + - You maintain it yourself + + ## Files in this submission: + + - `Casks/t/termix.rb` - Homebrew Cask formula + - `README.md` - Detailed documentation + - `SUBMISSION_INSTRUCTIONS.md` - This file + + ## Version Information: + + - Version: ${{ steps.package-version.outputs.version }} + - DMG SHA256: ${{ steps.dmg-info.outputs.checksum }} + - DMG URL: https://github.com/Termix-SSH/Termix/releases/download/release-${{ steps.package-version.outputs.version }}-tag/${{ steps.dmg-info.outputs.dmg_name }} + + ## After Submission: + + ### Official Homebrew Cask: + 1. Maintainers will review (usually 24-48 hours) + 2. May request changes or fixes + 3. Once merged, users can install with: `brew install --cask termix` + 4. Homebrew bot will auto-update for future releases + + ### Custom Tap: + 1. Push to your tap repository + 2. Immediately available to users + 3. Update the cask file for each new release + + ## Resources: + + - [Homebrew Cask Documentation](https://docs.brew.sh/Cask-Cookbook) + - [Acceptable Casks](https://docs.brew.sh/Acceptable-Casks) + - [How to Open a PR](https://docs.brew.sh/How-To-Open-a-Homebrew-Pull-Request) + EOF + + echo "✅ Created submission instructions" + + - name: List submission files + run: | + echo "Homebrew submission files:" + find homebrew-submission -type f + + - name: Upload Homebrew submission as artifact + uses: actions/upload-artifact@v4 + with: + name: homebrew-submission + path: homebrew-submission/* + retention-days: 30 + + - name: Display next steps + run: | + echo "" + echo "🍺 Homebrew Cask ready!" + echo "" + echo "📦 Download the 'homebrew-submission' artifact and follow SUBMISSION_INSTRUCTIONS.md" + echo "" + echo "Quick summary:" + echo "Option 1 (Recommended): Fork https://github.com/Homebrew/homebrew-cask and submit PR" + echo "Option 2 (Alternative): Create your own tap at Termix-SSH/homebrew-termix" + echo "" + + 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 }}