mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-07 02:55:24 +00:00
The Linux job runs on x64, so npm ci only installs @img/sharp-linux-x64 and @img/sharp-libvips-linux-x64. The arm64 and armv7l AppImages then carry x64 binaries and fail to load sharp on start. Install the arm packages at the versions sharp declares, as the Windows and macOS jobs already do. Fixes #1769 Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
1252 lines
50 KiB
YAML
1252 lines
50 KiB
YAML
name: Build and Push 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
|
|
source_ref:
|
|
description: "Git ref/SHA to build (defaults to the workflow ref)"
|
|
required: false
|
|
default: ""
|
|
workflow_call:
|
|
inputs:
|
|
build_type:
|
|
description: "Platform to build for (all, windows, linux, macos)"
|
|
required: true
|
|
type: string
|
|
artifact_destination:
|
|
description: "What to do with the built app (none, file, release, submit)"
|
|
required: true
|
|
type: string
|
|
release_tag:
|
|
description: "Explicit release tag to upload assets to (defaults to latest release when empty)"
|
|
required: false
|
|
type: string
|
|
default: ""
|
|
version_override:
|
|
description: "Version string to stamp into built artifacts instead of package.json's version"
|
|
required: false
|
|
type: string
|
|
default: ""
|
|
source_ref:
|
|
description: "Git ref/SHA to build"
|
|
required: false
|
|
type: string
|
|
default: ""
|
|
outputs:
|
|
macos_universal_dmg_sha256:
|
|
description: "SHA256 of the universal macOS DMG (for Homebrew cask)"
|
|
value: ${{ jobs.build-macos.outputs.dmg_sha256 }}
|
|
|
|
jobs:
|
|
build-windows:
|
|
runs-on: blacksmith-4vcpu-windows-2025
|
|
if: (inputs.build_type == 'all' || inputs.build_type == 'windows' || inputs.build_type == '') && inputs.artifact_destination != 'submit'
|
|
permissions:
|
|
contents: write
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ inputs.source_ref || github.ref }}
|
|
fetch-depth: 1
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version-file: ".nvmrc"
|
|
cache: "npm"
|
|
|
|
- name: Install Spectre-mitigated MSVC libraries
|
|
shell: pwsh
|
|
run: |
|
|
# node-pty's binding.gyp sets SpectreMitigation, so MSBuild refuses to
|
|
# build without these. They are not on the runner image by default.
|
|
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
|
$installPath = & $vswhere -latest -products * -property installationPath
|
|
if (-not $installPath) { throw "Visual Studio installation not found" }
|
|
|
|
# Derive the toolset version from the installed MSVC so the component
|
|
# id keeps matching when the runner image bumps the compiler.
|
|
$toolsetDir = Get-ChildItem -Path "$installPath\VC\Tools\MSVC" -Directory |
|
|
Sort-Object Name -Descending | Select-Object -First 1
|
|
if (-not $toolsetDir) { throw "No MSVC toolset found under $installPath" }
|
|
$parts = $toolsetDir.Name.Split(".")
|
|
$shortVer = "$($parts[0]).$($parts[1].Substring(0,2))"
|
|
Write-Host "MSVC toolset $($toolsetDir.Name) -> component version $shortVer"
|
|
|
|
$installer = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vs_installer.exe"
|
|
$components = @(
|
|
"Microsoft.VisualStudio.Component.VC.$shortVer.17.14.x86.x64.Spectre",
|
|
"Microsoft.VisualStudio.Component.VC.Runtimes.x86.x64.Spectre"
|
|
)
|
|
$args = @("modify", "--installPath", "`"$installPath`"", "--quiet", "--norestart", "--nocache")
|
|
foreach ($c in $components) { $args += @("--add", $c) }
|
|
|
|
Write-Host "Installing: $($components -join ', ')"
|
|
$proc = Start-Process -FilePath $installer -ArgumentList $args -Wait -PassThru -NoNewWindow
|
|
if ($proc.ExitCode -ne 0 -and $proc.ExitCode -ne 3010) {
|
|
Write-Host "vs_installer exited with $($proc.ExitCode); verifying libraries anyway"
|
|
}
|
|
|
|
$found = Get-ChildItem -Path "$installPath\VC\Tools\MSVC" -Recurse -Filter "*.lib" -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.FullName -match "\\spectre\\" } | Select-Object -First 1
|
|
if (-not $found) { throw "Spectre-mitigated libraries still missing after install" }
|
|
Write-Host "Spectre libs present: $($found.FullName)"
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
npm ci
|
|
node scripts/install-windows-sharp.cjs
|
|
|
|
# Bundled plugins are counted as downloads in the plugin store, so a
|
|
# build only fetches them when the pins change.
|
|
- name: Cache bundled plugins
|
|
uses: actions/cache@v6
|
|
with:
|
|
path: node_modules/.cache/termix-bundled-plugins
|
|
key: bundled-plugins-${{ hashFiles('docker/bundled-plugins.json') }}
|
|
enableCrossOsArchive: true
|
|
|
|
# A beta build has to know it is a beta, or it reports the stable version.
|
|
- name: Stamp beta version
|
|
if: inputs.version_override != ''
|
|
run: node scripts/sync-version.cjs --version "${{ inputs.version_override }}"
|
|
|
|
- name: Get version
|
|
id: package-version
|
|
run: |
|
|
$VERSION = "${{ inputs.version_override }}"
|
|
if ([string]::IsNullOrEmpty($VERSION)) {
|
|
$VERSION = (Get-Content package.json | ConvertFrom-Json).version
|
|
}
|
|
echo "version=$VERSION" >> $env:GITHUB_OUTPUT
|
|
|
|
- name: Build Windows (All Architectures)
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: npm run build && npx electron-builder --win --x64 --ia32 --arm64 --publish never
|
|
|
|
# The updater only installs the x64 NSIS build; 32-bit and ARM installs get a link.
|
|
# The combined multi-arch installer is never uploaded.
|
|
- name: Fix Windows update files
|
|
run: node scripts/fix-update-info.cjs --drop ia32 --drop arm64 --drop termix_windows_nsis.exe release/latest.yml release/beta.yml
|
|
|
|
- name: Upload Windows x64 NSIS Installer
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_windows_x64_nsis.exe') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_windows_x64_nsis
|
|
path: release/termix_windows_x64_nsis.exe
|
|
retention-days: 30
|
|
|
|
- name: Upload Windows ia32 NSIS Installer
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_windows_ia32_nsis.exe') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_windows_ia32_nsis
|
|
path: release/termix_windows_ia32_nsis.exe
|
|
retention-days: 30
|
|
|
|
- name: Upload Windows arm64 NSIS Installer
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_windows_arm64_nsis.exe') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_windows_arm64_nsis
|
|
path: release/termix_windows_arm64_nsis.exe
|
|
retention-days: 30
|
|
|
|
- name: Upload Windows x64 MSI Installer
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_windows_x64_msi.msi') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_windows_x64_msi
|
|
path: release/termix_windows_x64_msi.msi
|
|
retention-days: 30
|
|
|
|
- name: Upload Windows ia32 MSI Installer
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_windows_ia32_msi.msi') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_windows_ia32_msi
|
|
path: release/termix_windows_ia32_msi.msi
|
|
retention-days: 30
|
|
|
|
- name: Upload Windows arm64 MSI Installer
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_windows_arm64_msi.msi') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_windows_arm64_msi
|
|
path: release/termix_windows_arm64_msi.msi
|
|
retention-days: 30
|
|
|
|
- name: Create Windows x64 Portable zip
|
|
if: hashFiles('release/win-unpacked/*') != ''
|
|
run: |
|
|
Compress-Archive -Path "release\win-unpacked\*" -DestinationPath "termix_windows_x64_portable.zip"
|
|
|
|
- name: Create Windows ia32 Portable zip
|
|
if: hashFiles('release/win-ia32-unpacked/*') != ''
|
|
run: |
|
|
Compress-Archive -Path "release\win-ia32-unpacked\*" -DestinationPath "termix_windows_ia32_portable.zip"
|
|
|
|
- name: Create Windows arm64 Portable zip
|
|
if: hashFiles('release/win-arm64-unpacked/*') != ''
|
|
run: |
|
|
Compress-Archive -Path "release\win-arm64-unpacked\*" -DestinationPath "termix_windows_arm64_portable.zip"
|
|
|
|
- name: Upload Windows update files
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/*.yml') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_windows_update_meta
|
|
path: |
|
|
release/*.yml
|
|
release/termix_windows_x64_nsis.exe.blockmap
|
|
!release/builder-debug.yml
|
|
retention-days: 30
|
|
|
|
- name: Upload Windows x64 Portable
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('termix_windows_x64_portable.zip') != '' && 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@v7
|
|
if: hashFiles('termix_windows_ia32_portable.zip') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_windows_ia32_portable
|
|
path: termix_windows_ia32_portable.zip
|
|
retention-days: 30
|
|
|
|
- name: Upload Windows arm64 Portable
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('termix_windows_arm64_portable.zip') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_windows_arm64_portable
|
|
path: termix_windows_arm64_portable.zip
|
|
retention-days: 30
|
|
|
|
# The same app with every official plugin, for installs without
|
|
# internet. Built last so nothing above packs the full set, and with
|
|
# no update files: a full install updates through the normal build and
|
|
# downloads the plugins it had on.
|
|
- name: Build Windows x64 full installer
|
|
if: inputs.artifact_destination != 'none'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TERMIX_PLUGIN_PROFILE: full
|
|
run: |
|
|
npm run build:plugins
|
|
npx electron-builder --win nsis --x64 --publish never '-c.nsis.artifactName=termix_windows_x64_full_nsis.${ext}'
|
|
|
|
- name: Upload Windows x64 full installer
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_windows_x64_full_nsis.exe') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_windows_x64_full_nsis
|
|
path: release/termix_windows_x64_full_nsis.exe
|
|
retention-days: 30
|
|
|
|
build-linux:
|
|
runs-on: blacksmith-8vcpu-ubuntu-2404
|
|
if: (inputs.build_type == 'all' || inputs.build_type == 'linux' || inputs.build_type == '') && inputs.artifact_destination != 'submit'
|
|
permissions:
|
|
contents: write
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ inputs.source_ref || github.ref }}
|
|
fetch-depth: 1
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version-file: ".nvmrc"
|
|
cache: "npm"
|
|
|
|
- name: Install system dependencies
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y libfuse2
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
npm ci
|
|
npm install --force @rollup/rollup-linux-x64-gnu
|
|
npm install --force @rollup/rollup-linux-arm64-gnu
|
|
npm install --force @rollup/rollup-linux-arm-gnueabihf
|
|
node scripts/install-linux-sharp.cjs
|
|
|
|
# Bundled plugins are counted as downloads in the plugin store, so a
|
|
# build only fetches them when the pins change.
|
|
- name: Cache bundled plugins
|
|
uses: actions/cache@v6
|
|
with:
|
|
path: node_modules/.cache/termix-bundled-plugins
|
|
key: bundled-plugins-${{ hashFiles('docker/bundled-plugins.json') }}
|
|
enableCrossOsArchive: true
|
|
|
|
# A beta build has to know it is a beta, or it reports the stable version.
|
|
- name: Stamp beta version
|
|
if: inputs.version_override != ''
|
|
run: node scripts/sync-version.cjs --version "${{ inputs.version_override }}"
|
|
|
|
- name: Build Linux x64
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
DEBUG: electron-builder
|
|
run: npm run build && npx electron-builder --linux --x64 --publish never
|
|
|
|
- name: Build Linux arm64 and armv7l
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: npx electron-builder --linux --arm64 --armv7l --publish never
|
|
|
|
- name: Rename Linux artifacts for consistency
|
|
run: |
|
|
cd release
|
|
|
|
if [ -f "termix_linux_amd64_deb.deb" ]; then
|
|
mv "termix_linux_amd64_deb.deb" "termix_linux_x64_deb.deb"
|
|
fi
|
|
|
|
if [ -f "termix_linux_x86_64_appimage.AppImage" ]; then
|
|
mv "termix_linux_x86_64_appimage.AppImage" "termix_linux_x64_appimage.AppImage"
|
|
fi
|
|
|
|
cd ..
|
|
|
|
node scripts/fix-update-info.cjs \
|
|
--rename termix_linux_amd64_deb.deb=termix_linux_x64_deb.deb \
|
|
--rename termix_linux_x86_64_appimage.AppImage=termix_linux_x64_appimage.AppImage \
|
|
release/*-linux*.yml
|
|
|
|
- name: Upload Linux update files
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/*-linux*.yml') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_linux_update_meta
|
|
path: release/*-linux*.yml
|
|
retention-days: 30
|
|
|
|
- name: Upload Linux x64 AppImage
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_linux_x64_appimage.AppImage') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_linux_x64_appimage
|
|
path: release/termix_linux_x64_appimage.AppImage
|
|
retention-days: 30
|
|
|
|
- name: Upload Linux arm64 AppImage
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_linux_arm64_appimage.AppImage') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_linux_arm64_appimage
|
|
path: release/termix_linux_arm64_appimage.AppImage
|
|
retention-days: 30
|
|
|
|
- name: Upload Linux armv7l AppImage
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_linux_armv7l_appimage.AppImage') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_linux_armv7l_appimage
|
|
path: release/termix_linux_armv7l_appimage.AppImage
|
|
retention-days: 30
|
|
|
|
- name: Upload Linux x64 DEB
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_linux_x64_deb.deb') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_linux_x64_deb
|
|
path: release/termix_linux_x64_deb.deb
|
|
retention-days: 30
|
|
|
|
- name: Upload Linux arm64 DEB
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_linux_arm64_deb.deb') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_linux_arm64_deb
|
|
path: release/termix_linux_arm64_deb.deb
|
|
retention-days: 30
|
|
|
|
- name: Upload Linux armv7l DEB
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_linux_armv7l_deb.deb') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_linux_armv7l_deb
|
|
path: release/termix_linux_armv7l_deb.deb
|
|
retention-days: 30
|
|
|
|
- name: Upload Linux x64 tar.gz
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_linux_x64_portable.tar.gz') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_linux_x64_portable
|
|
path: release/termix_linux_x64_portable.tar.gz
|
|
retention-days: 30
|
|
|
|
- name: Upload Linux arm64 tar.gz
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_linux_arm64_portable.tar.gz') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_linux_arm64_portable
|
|
path: release/termix_linux_arm64_portable.tar.gz
|
|
retention-days: 30
|
|
|
|
- name: Upload Linux armv7l tar.gz
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_linux_armv7l_portable.tar.gz') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_linux_armv7l_portable
|
|
path: release/termix_linux_armv7l_portable.tar.gz
|
|
retention-days: 30
|
|
|
|
# Every official plugin, for installs without internet. No update files.
|
|
- name: Build Linux x64 full packages
|
|
if: inputs.artifact_destination != 'none'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TERMIX_PLUGIN_PROFILE: full
|
|
run: |
|
|
npm run build:plugins
|
|
npx electron-builder --linux AppImage deb --x64 --publish never \
|
|
'-c.appImage.artifactName=termix_linux_x64_full_appimage.${ext}' \
|
|
'-c.deb.artifactName=termix_linux_x64_full_deb.${ext}'
|
|
|
|
- name: Upload Linux x64 full packages
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_linux_x64_full_appimage.AppImage') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_linux_x64_full
|
|
path: |
|
|
release/termix_linux_x64_full_appimage.AppImage
|
|
release/termix_linux_x64_full_deb.deb
|
|
retention-days: 30
|
|
|
|
build-macos:
|
|
runs-on: blacksmith-6vcpu-macos-latest
|
|
if: (inputs.build_type == 'macos' || inputs.build_type == 'all') && inputs.artifact_destination != 'submit'
|
|
needs: []
|
|
permissions:
|
|
contents: write
|
|
outputs:
|
|
dmg_sha256: ${{ steps.dmg-checksum.outputs.sha256 }}
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ inputs.source_ref || github.ref }}
|
|
fetch-depth: 1
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version-file: ".nvmrc"
|
|
cache: "npm"
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
npm ci
|
|
npm install --force @rollup/rollup-darwin-arm64
|
|
npm install dmg-license
|
|
node scripts/install-macos-sharp.cjs
|
|
|
|
# Bundled plugins are counted as downloads in the plugin store, so a
|
|
# build only fetches them when the pins change.
|
|
- name: Cache bundled plugins
|
|
uses: actions/cache@v6
|
|
with:
|
|
path: node_modules/.cache/termix-bundled-plugins
|
|
key: bundled-plugins-${{ hashFiles('docker/bundled-plugins.json') }}
|
|
enableCrossOsArchive: true
|
|
|
|
# A beta build has to know it is a beta, or it reports the stable version.
|
|
- name: Stamp beta version
|
|
if: inputs.version_override != ''
|
|
run: node scripts/sync-version.cjs --version "${{ inputs.version_override }}"
|
|
|
|
- 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
|
|
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
|
|
|
|
echo -n "$MAC_BUILD_CERTIFICATE_BASE64" | base64 --decode -o $APP_CERT_PATH
|
|
|
|
if [ -n "$MAC_INSTALLER_CERTIFICATE_BASE64" ]; then
|
|
echo -n "$MAC_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $INSTALLER_CERT_PATH
|
|
fi
|
|
|
|
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
|
|
|
|
security import $APP_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
|
|
|
if [ -f "$INSTALLER_CERT_PATH" ]; then
|
|
security import $INSTALLER_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
|
fi
|
|
|
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
|
|
|
security find-identity -v -p codesigning $KEYCHAIN_PATH
|
|
|
|
# Betas never go to the App Store, so they skip the MAS package.
|
|
- name: Build macOS App Store Package
|
|
id: mas_build
|
|
if: steps.check_certs.outputs.has_certs == 'true' && inputs.version_override == ''
|
|
env:
|
|
ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
NODE_OPTIONS: --max-old-space-size=4096
|
|
run: |
|
|
BUILD_VERSION="${{ github.run_number }}"
|
|
|
|
npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION" --publish never
|
|
|
|
- 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
|
|
|
|
- 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
|
|
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
|
|
|
|
echo -n "$DEVELOPER_ID_CERTIFICATE_BASE64" | base64 --decode -o $DEV_CERT_PATH
|
|
|
|
if [ -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64" ]; then
|
|
echo -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $DEV_INSTALLER_CERT_PATH
|
|
fi
|
|
|
|
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
|
|
|
|
security import $DEV_CERT_PATH -P "$DEVELOPER_ID_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
|
|
|
if [ -f "$DEV_INSTALLER_CERT_PATH" ]; then
|
|
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
|
|
|
|
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 }}
|
|
NODE_OPTIONS: --max-old-space-size=4096
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
if [ "${{ steps.mas_build.outcome }}" != "success" ]; then
|
|
npm run build
|
|
fi
|
|
npx electron-builder --mac dmg zip --universal --x64 --arm64 --publish never
|
|
|
|
# The updater picks the x64 or arm64 zip, so the universal one is left out.
|
|
- name: Fix macOS update files
|
|
run: node scripts/fix-update-info.cjs --drop universal release/*-mac.yml
|
|
|
|
- name: Upload macOS update files
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/*-mac.yml') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_macos_update_meta
|
|
path: |
|
|
release/*-mac.yml
|
|
release/termix_macos_x64_update.zip
|
|
release/termix_macos_x64_update.zip.blockmap
|
|
release/termix_macos_arm64_update.zip
|
|
release/termix_macos_arm64_update.zip.blockmap
|
|
retention-days: 30
|
|
|
|
- name: Verify macOS sharp packaging
|
|
run: |
|
|
artifacts=(release/termix_macos_*_dmg.dmg)
|
|
MAS_PKG=$(find release -name "termix_macos_universal_mas.pkg" -type f | head -n 1)
|
|
if [ -n "$MAS_PKG" ]; then
|
|
artifacts+=("$MAS_PKG")
|
|
fi
|
|
node scripts/verify-macos-sharp.cjs "${artifacts[@]}"
|
|
|
|
- name: Locate macOS MAS PKG
|
|
id: locate_mas_pkg
|
|
run: |
|
|
MAS_PKG=$(find release -name "termix_macos_universal_mas.pkg" -type f | head -n 1)
|
|
echo "path=$MAS_PKG" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Upload macOS MAS PKG
|
|
if: steps.mas_build.outcome == 'success' && steps.locate_mas_pkg.outputs.path != '' && (inputs.artifact_destination == 'file' || inputs.artifact_destination == 'release' || inputs.artifact_destination == 'submit')
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: termix_macos_universal_mas
|
|
path: ${{ steps.locate_mas_pkg.outputs.path }}
|
|
retention-days: 30
|
|
if-no-files-found: warn
|
|
|
|
- name: Upload macOS Universal DMG
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_macos_universal_dmg.dmg') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_macos_universal_dmg
|
|
path: release/termix_macos_universal_dmg.dmg
|
|
retention-days: 30
|
|
|
|
- name: Upload macOS x64 DMG
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_macos_x64_dmg.dmg') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_macos_x64_dmg
|
|
path: release/termix_macos_x64_dmg.dmg
|
|
retention-days: 30
|
|
|
|
- name: Upload macOS arm64 DMG
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_macos_arm64_dmg.dmg') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_macos_arm64_dmg
|
|
path: release/termix_macos_arm64_dmg.dmg
|
|
retention-days: 30
|
|
|
|
- name: Get version for Homebrew
|
|
id: homebrew-version
|
|
run: |
|
|
VERSION="${{ inputs.version_override }}"
|
|
if [ -z "$VERSION" ]; then
|
|
VERSION=$(node -p "require('./package.json').version")
|
|
fi
|
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
|
|
- name: Compute universal DMG checksum
|
|
id: dmg-checksum
|
|
if: hashFiles('release/termix_macos_universal_dmg.dmg') != ''
|
|
run: |
|
|
CHECKSUM=$(shasum -a 256 "release/termix_macos_universal_dmg.dmg" | awk '{print $1}')
|
|
echo "sha256=$CHECKSUM" >> $GITHUB_OUTPUT
|
|
|
|
- name: Generate Homebrew Cask
|
|
if: hashFiles('release/termix_macos_universal_dmg.dmg') != '' && inputs.version_override == '' && (inputs.artifact_destination == 'file' || inputs.artifact_destination == 'release')
|
|
run: |
|
|
VERSION="${{ steps.homebrew-version.outputs.version }}"
|
|
DMG_PATH="release/termix_macos_universal_dmg.dmg"
|
|
|
|
CHECKSUM=$(shasum -a 256 "$DMG_PATH" | awk '{print $1}')
|
|
|
|
mkdir -p homebrew-generated
|
|
cp Casks/termix.rb homebrew-generated/termix.rb
|
|
|
|
sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" homebrew-generated/termix.rb
|
|
sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-generated/termix.rb
|
|
sed -i '' "s|version \".*\"|version \"$VERSION\"|g" homebrew-generated/termix.rb
|
|
sed -i '' "s|sha256 \".*\"|sha256 \"$CHECKSUM\"|g" homebrew-generated/termix.rb
|
|
sed -i '' "s|download/[^/]*/termix_macos|download/v$VERSION/termix_macos|g" homebrew-generated/termix.rb
|
|
|
|
- name: Upload Homebrew Cask as artifact
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('homebrew-generated/termix.rb') != '' && inputs.artifact_destination == 'file'
|
|
with:
|
|
name: termix_macos_homebrew_cask
|
|
path: homebrew-generated/termix.rb
|
|
retention-days: 30
|
|
|
|
- name: Upload Homebrew Cask to release
|
|
if: hashFiles('homebrew-generated/termix.rb') != '' && inputs.version_override == '' && inputs.artifact_destination == 'release'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
VERSION="${{ steps.homebrew-version.outputs.version }}"
|
|
RELEASE_TAG="v$VERSION"
|
|
|
|
gh release list --repo ${{ github.repository }} --limit 100 | grep -q "$RELEASE_TAG" || {
|
|
echo "Release $RELEASE_TAG not found"
|
|
exit 1
|
|
}
|
|
|
|
gh release upload "$RELEASE_TAG" homebrew-generated/termix.rb --repo ${{ github.repository }} --clobber
|
|
|
|
# Every official plugin, for installs without internet. Signed like the
|
|
# normal DMG, with no update files.
|
|
- name: Build macOS universal full DMG
|
|
if: inputs.artifact_destination != 'none'
|
|
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 }}
|
|
NODE_OPTIONS: --max-old-space-size=4096
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TERMIX_PLUGIN_PROFILE: full
|
|
run: |
|
|
npm run build:plugins
|
|
npx electron-builder --mac dmg --universal --publish never \
|
|
'-c.dmg.artifactName=termix_macos_${arch}_full_dmg.${ext}'
|
|
|
|
- name: Upload macOS universal full DMG
|
|
uses: actions/upload-artifact@v7
|
|
if: hashFiles('release/termix_macos_universal_full_dmg.dmg') != '' && inputs.artifact_destination != 'none'
|
|
with:
|
|
name: termix_macos_universal_full_dmg
|
|
path: release/termix_macos_universal_full_dmg.dmg
|
|
retention-days: 30
|
|
|
|
- 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
|
|
|
|
submit-to-chocolatey:
|
|
runs-on: blacksmith-4vcpu-windows-2025
|
|
if: inputs.artifact_destination == 'submit' && (inputs.build_type == 'all' || inputs.build_type == 'windows' || inputs.build_type == '')
|
|
permissions:
|
|
contents: read
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ inputs.source_ref || github.ref }}
|
|
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
|
|
|
|
- name: Download and prepare MSI info from public release
|
|
id: msi-info
|
|
run: |
|
|
$VERSION = "${{ steps.package-version.outputs.version }}"
|
|
$MSI_NAME = "termix_windows_x64_msi.msi"
|
|
$DOWNLOAD_URL = "https://github.com/Termix-SSH/Termix/releases/download/v$($VERSION)/$($MSI_NAME)"
|
|
|
|
Write-Host "Downloading from $DOWNLOAD_URL"
|
|
New-Item -ItemType Directory -Force -Path "release_asset"
|
|
$DOWNLOAD_PATH = "release_asset\$MSI_NAME"
|
|
|
|
try {
|
|
Invoke-WebRequest -Uri $DOWNLOAD_URL -OutFile $DOWNLOAD_PATH -UseBasicParsing
|
|
} catch {
|
|
Write-Error "Failed to download MSI from $DOWNLOAD_URL. Please ensure the release and asset exist."
|
|
exit 1
|
|
}
|
|
|
|
$CHECKSUM = (Get-FileHash -Path $DOWNLOAD_PATH -Algorithm SHA256).Hash
|
|
echo "msi_name=$MSI_NAME" >> $env:GITHUB_OUTPUT
|
|
echo "checksum=$CHECKSUM" >> $env:GITHUB_OUTPUT
|
|
|
|
- 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 }}"
|
|
|
|
$DOWNLOAD_URL = "https://github.com/Termix-SSH/Termix/releases/download/v$VERSION/$MSI_NAME"
|
|
|
|
New-Item -ItemType Directory -Force -Path "choco-build"
|
|
Copy-Item -Path "packaging\chocolatey\*" -Destination "choco-build" -Recurse -Force
|
|
|
|
$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))
|
|
|
|
$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))
|
|
|
|
- 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
|
|
choco pack termix-ssh.nuspec
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Chocolatey pack failed with exit code $LASTEXITCODE"
|
|
}
|
|
|
|
- name: Check for Chocolatey API Key
|
|
id: check_choco_key
|
|
run: |
|
|
if ("${{ secrets.CHOCOLATEY_API_KEY }}" -ne "") {
|
|
echo "has_key=true" >> $env:GITHUB_OUTPUT
|
|
}
|
|
|
|
- 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) {
|
|
} else {
|
|
throw "Chocolatey push failed with exit code $LASTEXITCODE"
|
|
}
|
|
} catch {
|
|
}
|
|
|
|
- name: Upload Chocolatey package as artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: chocolatey-package
|
|
path: choco-build/*.nupkg
|
|
retention-days: 30
|
|
|
|
submit-to-homebrew:
|
|
runs-on: blacksmith-6vcpu-macos-latest
|
|
if: inputs.artifact_destination == 'submit' && (inputs.build_type == 'all' || inputs.build_type == 'macos')
|
|
needs: []
|
|
permissions:
|
|
contents: read
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ inputs.source_ref || github.ref }}
|
|
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
|
|
|
|
- name: Download and prepare DMG info from public release
|
|
id: dmg-info
|
|
run: |
|
|
VERSION="${{ steps.package-version.outputs.version }}"
|
|
DMG_NAME="termix_macos_universal_dmg.dmg"
|
|
URL="https://github.com/Termix-SSH/Termix/releases/download/v$VERSION/$DMG_NAME"
|
|
|
|
mkdir -p release_asset
|
|
DOWNLOAD_PATH="release_asset/$DMG_NAME"
|
|
echo "Downloading DMG from $URL"
|
|
|
|
if command -v curl &> /dev/null; then
|
|
curl -L -o "$DOWNLOAD_PATH" "$URL"
|
|
elif command -v wget &> /dev/null; then
|
|
wget -O "$DOWNLOAD_PATH" "$URL"
|
|
else
|
|
echo "Neither curl nor wget is available, installing curl"
|
|
brew install curl
|
|
curl -L -o "$DOWNLOAD_PATH" "$URL"
|
|
fi
|
|
|
|
CHECKSUM=$(shasum -a 256 "$DOWNLOAD_PATH" | awk '{print $1}')
|
|
|
|
echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT
|
|
echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT
|
|
|
|
- name: Verify macOS sharp packaging
|
|
run: node scripts/verify-macos-sharp.cjs release_asset/termix_macos_universal_dmg.dmg
|
|
|
|
- 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 }}"
|
|
|
|
mkdir -p homebrew-submission/Casks/t
|
|
|
|
cp Casks/termix.rb homebrew-submission/Casks/t/termix.rb
|
|
|
|
sed -i '' "s|version \".*\"|version \"$VERSION\"|g" homebrew-submission/Casks/t/termix.rb
|
|
sed -i '' "s|sha256 \".*\"|sha256 \"$CHECKSUM\"|g" homebrew-submission/Casks/t/termix.rb
|
|
|
|
- name: Verify Cask syntax
|
|
run: |
|
|
ruby -c homebrew-submission/Casks/t/termix.rb
|
|
|
|
- name: Upload Homebrew submission as artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: homebrew-submission
|
|
path: homebrew-submission/*
|
|
retention-days: 30
|
|
|
|
upload-to-release:
|
|
runs-on: blacksmith-8vcpu-ubuntu-2404
|
|
if: inputs.artifact_destination == 'release'
|
|
needs: [build-windows, build-linux, build-macos]
|
|
permissions:
|
|
contents: write
|
|
|
|
steps:
|
|
- name: Download all artifacts
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
path: artifacts
|
|
|
|
- name: Resolve release tag
|
|
id: get_release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
INPUT_TAG: ${{ inputs.release_tag }}
|
|
run: |
|
|
if [ -n "$INPUT_TAG" ]; then
|
|
echo "RELEASE_TAG=$INPUT_TAG" >> $GITHUB_ENV
|
|
else
|
|
echo "RELEASE_TAG=$(gh release list --repo ${{ github.repository }} --limit 1 --json tagName -q '.[0].tagName')" >> $GITHUB_ENV
|
|
fi
|
|
|
|
# Update files go up last: the desktop updater treats a release as ready
|
|
# once its update file is there, so every installer must already be up.
|
|
- name: Upload artifacts to latest release
|
|
run: |
|
|
find artifacts -type f ! -name '*.yml' -print0 | while IFS= read -r -d '' file; do
|
|
gh release upload "$RELEASE_TAG" "$file" --repo ${{ github.repository }} --clobber
|
|
done
|
|
find artifacts -type f -name '*.yml' -print0 | while IFS= read -r -d '' file; do
|
|
gh release upload "$RELEASE_TAG" "$file" --repo ${{ github.repository }} --clobber
|
|
done
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
|
|
submit-to-app-store:
|
|
runs-on: blacksmith-6vcpu-macos-latest
|
|
if: inputs.artifact_destination == 'submit' && (inputs.build_type == 'all' || inputs.build_type == 'macos')
|
|
needs: []
|
|
permissions:
|
|
contents: write
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ inputs.source_ref || github.ref }}
|
|
fetch-depth: 1
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version-file: ".nvmrc"
|
|
cache: "npm"
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
npm ci
|
|
npm install --force @rollup/rollup-darwin-arm64
|
|
npm install dmg-license
|
|
node scripts/install-macos-sharp.cjs
|
|
|
|
# Bundled plugins are counted as downloads in the plugin store, so a
|
|
# build only fetches them when the pins change.
|
|
- name: Cache bundled plugins
|
|
uses: actions/cache@v6
|
|
with:
|
|
path: node_modules/.cache/termix-bundled-plugins
|
|
key: bundled-plugins-${{ hashFiles('docker/bundled-plugins.json') }}
|
|
enableCrossOsArchive: true
|
|
|
|
- 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
|
|
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
|
|
|
|
echo -n "$MAC_BUILD_CERTIFICATE_BASE64" | base64 --decode -o $APP_CERT_PATH
|
|
|
|
if [ -n "$MAC_INSTALLER_CERTIFICATE_BASE64" ]; then
|
|
echo -n "$MAC_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $INSTALLER_CERT_PATH
|
|
fi
|
|
|
|
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
|
|
|
|
security import $APP_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
|
|
|
if [ -f "$INSTALLER_CERT_PATH" ]; then
|
|
security import $INSTALLER_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
|
fi
|
|
|
|
security list-keychain -d user -s $KEYCHAIN_PATH
|
|
|
|
security find-identity -v -p codesigning $KEYCHAIN_PATH
|
|
|
|
- name: Check for App Store Connect API credentials
|
|
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
|
|
fi
|
|
|
|
- name: Setup Ruby for Fastlane
|
|
if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true'
|
|
uses: ruby/setup-ruby@v1
|
|
with:
|
|
ruby-version: "3.3"
|
|
bundler-cache: false
|
|
|
|
- name: Install Fastlane
|
|
if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true'
|
|
run: gem install fastlane -N
|
|
|
|
- name: Write App Store Connect API key
|
|
if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true'
|
|
env:
|
|
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
|
|
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
|
|
APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
|
|
run: |
|
|
# Write API key JSON that Fastlane expects; the PEM's newlines
|
|
# must be preserved as literal \n escapes, not stripped, or
|
|
# spaceship fails to parse the key (invalid curve name).
|
|
mkdir -p /tmp/asc_keys
|
|
KEY_P8_PATH="/tmp/asc_keys/AuthKey_${APPLE_KEY_ID}.p8"
|
|
API_KEY_JSON="/tmp/asc_keys/api_key.json"
|
|
|
|
echo "$APPLE_KEY_CONTENT" | base64 --decode > "$KEY_P8_PATH"
|
|
|
|
KEY_ID="$APPLE_KEY_ID" ISSUER_ID="$APPLE_ISSUER_ID" KEY_P8_PATH="$KEY_P8_PATH" \
|
|
node -e '
|
|
const fs = require("fs");
|
|
const key = fs.readFileSync(process.env.KEY_P8_PATH, "utf8");
|
|
process.stdout.write(JSON.stringify({
|
|
key_id: process.env.KEY_ID,
|
|
issuer_id: process.env.ISSUER_ID,
|
|
key,
|
|
in_house: false,
|
|
}, null, 2) + "\n");
|
|
' > "$API_KEY_JSON"
|
|
|
|
- name: Resolve next build number from App Store Connect
|
|
id: build_number
|
|
if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true'
|
|
run: |
|
|
APP_VERSION=$(node -p "require('./package.json').version")
|
|
OUT_FILE="$RUNNER_TEMP/latest_build_number.txt"
|
|
LANE_DIR="$RUNNER_TEMP/asc_lane/fastlane"
|
|
mkdir -p "$LANE_DIR"
|
|
|
|
cat > "$LANE_DIR/Fastfile" <<EOF
|
|
default_platform(:mac)
|
|
|
|
lane :fetch_build_number do
|
|
live_number = app_store_build_number(
|
|
live: true,
|
|
platform: "osx",
|
|
api_key_path: "/tmp/asc_keys/api_key.json",
|
|
app_identifier: "com.karmaa.termix",
|
|
initial_build_number: 0,
|
|
)
|
|
pending_number = app_store_build_number(
|
|
live: false,
|
|
platform: "osx",
|
|
api_key_path: "/tmp/asc_keys/api_key.json",
|
|
app_identifier: "com.karmaa.termix",
|
|
initial_build_number: 0,
|
|
)
|
|
number = [live_number, pending_number].max
|
|
File.write("$OUT_FILE", number.to_s)
|
|
end
|
|
EOF
|
|
|
|
(cd "$RUNNER_TEMP/asc_lane" && fastlane fetch_build_number) 2>&1 || true
|
|
|
|
LATEST=""
|
|
if [ -f "$OUT_FILE" ]; then
|
|
LATEST=$(cat "$OUT_FILE" | tr -d '[:space:]')
|
|
fi
|
|
|
|
if ! [[ "$LATEST" =~ ^[0-9]+$ ]]; then
|
|
echo "Could not resolve latest build number from App Store Connect; falling back to run number."
|
|
LATEST="${{ github.run_number }}"
|
|
fi
|
|
echo "build_version=$((LATEST + 1))" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Build macOS App Store Package
|
|
if: steps.check_certs.outputs.has_certs == 'true'
|
|
env:
|
|
ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
NODE_OPTIONS: --max-old-space-size=4096
|
|
run: |
|
|
BUILD_VERSION="${{ steps.build_number.outputs.build_version || github.run_number }}"
|
|
npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION" --publish never
|
|
|
|
- name: Verify macOS sharp packaging
|
|
if: steps.check_certs.outputs.has_certs == 'true'
|
|
run: |
|
|
MAS_PKG=$(find release -name "termix_macos_universal_mas.pkg" -type f | head -n 1)
|
|
if [ -z "$MAS_PKG" ]; then
|
|
echo "MAS pkg not found under release/; aborting."
|
|
exit 1
|
|
fi
|
|
node scripts/verify-macos-sharp.cjs "$MAS_PKG"
|
|
|
|
- name: Generate App Store release notes
|
|
id: asc_notes
|
|
if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true'
|
|
run: |
|
|
META_DIR="$RUNNER_TEMP/asc_metadata"
|
|
rm -rf "$META_DIR"
|
|
node scripts/generate-appstore-notes.cjs \
|
|
--changelog CHANGELOG.md \
|
|
--out-dir "$META_DIR" \
|
|
--locales "en-US"
|
|
echo "metadata_path=$META_DIR" >> "$GITHUB_OUTPUT"
|
|
|
|
# Apple won't create a new version while the last one is still in
|
|
# review. Pull it out so deliver can rename it to this version.
|
|
- name: Cancel pending App Store review
|
|
if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true'
|
|
run: |
|
|
LANE_DIR="$RUNNER_TEMP/asc_cancel/fastlane"
|
|
mkdir -p "$LANE_DIR"
|
|
|
|
cat > "$LANE_DIR/Fastfile" <<'EOF'
|
|
default_platform(:mac)
|
|
|
|
lane :cancel_pending_review do
|
|
Spaceship::ConnectAPI.token = Spaceship::ConnectAPI::Token.from_json_file("/tmp/asc_keys/api_key.json")
|
|
app = Spaceship::ConnectAPI::App.find("com.karmaa.termix")
|
|
UI.user_error!("App not found") unless app
|
|
|
|
app.get_app_store_versions(filter: { platform: "MAC_OS" }, limit: 5).each do |v|
|
|
UI.message("Version #{v.version_string}: #{v.app_version_state || v.app_store_state}")
|
|
end
|
|
|
|
active = %w[WAITING_FOR_REVIEW IN_REVIEW UNRESOLVED_ISSUES]
|
|
canceled = false
|
|
app.get_review_submissions(filter: { platform: "MAC_OS" }).each do |sub|
|
|
UI.message("Review submission #{sub.id}: #{sub.state}")
|
|
next unless active.include?(sub.state)
|
|
sub.cancel_submission
|
|
canceled = true
|
|
UI.important("Canceled review submission #{sub.id}")
|
|
end
|
|
next unless canceled
|
|
|
|
30.times do
|
|
busy = app.get_review_submissions(filter: { platform: "MAC_OS" }).any? do |sub|
|
|
(active + %w[CANCELING]).include?(sub.state)
|
|
end
|
|
break unless busy
|
|
sleep(10)
|
|
end
|
|
end
|
|
EOF
|
|
|
|
cd "$RUNNER_TEMP/asc_cancel" && fastlane cancel_pending_review
|
|
|
|
- name: Upload and submit to Mac App Store
|
|
if: steps.check_certs.outputs.has_certs == 'true' && steps.check_asc_creds.outputs.has_credentials == 'true'
|
|
run: |
|
|
PKG_FILE=$(find release -name "termix_macos_universal_mas.pkg" -type f | head -n 1)
|
|
if [ -z "$PKG_FILE" ]; then
|
|
echo "PKG file not found in release/; aborting."
|
|
exit 1
|
|
fi
|
|
|
|
VERSION=$(node -p "require('./package.json').version")
|
|
API_KEY_JSON="/tmp/asc_keys/api_key.json"
|
|
|
|
fastlane deliver \
|
|
--pkg "$PKG_FILE" \
|
|
--api_key_path "$API_KEY_JSON" \
|
|
--app_version "$VERSION" \
|
|
--platform osx \
|
|
--app_identifier "com.karmaa.termix" \
|
|
--skip_metadata false \
|
|
--metadata_path "${{ steps.asc_notes.outputs.metadata_path }}" \
|
|
--skip_screenshots false \
|
|
--screenshots_path packaging/appstore/screenshots \
|
|
--overwrite_screenshots true \
|
|
--skip_app_version_update false \
|
|
--submit_for_review true \
|
|
--automatic_release true \
|
|
--precheck_include_in_app_purchases false \
|
|
--submission_information "{\"export_compliance_uses_encryption\": false}" \
|
|
--force true
|
|
|
|
- name: Clean up keychains
|
|
if: always()
|
|
run: |
|
|
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
|
|
rm -rf /tmp/asc_keys
|