Insider Pre-Release #352
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Insider Pre-Release | |
| on: | |
| schedule: | |
| - cron: "0 0 * * *" # daily midnight UTC | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| jobs: | |
| check-commits: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| should_build: ${{ steps.check.outputs.should_build }} | |
| commit_count: ${{ steps.check.outputs.commit_count }} | |
| steps: | |
| - name: Checkout dev branch | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: dev | |
| fetch-depth: 0 | |
| - name: Check for commits in the last 24 hours | |
| id: check | |
| run: | | |
| COMMIT_COUNT=$(git log --since="24 hours ago" --oneline | wc -l) | |
| echo "commit_count=$COMMIT_COUNT" >> $GITHUB_OUTPUT | |
| if [ "$COMMIT_COUNT" -gt 0 ]; then | |
| echo "should_build=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "should_build=false" >> $GITHUB_OUTPUT | |
| fi | |
| build: | |
| needs: check-commits | |
| if: needs.check-commits.outputs.should_build == 'true' | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| config: buildScripts/electron-builder-linux-insider.json | |
| artifact_name: linux-build | |
| - os: windows-latest | |
| config: buildScripts/electron-builder-win-insider.json | |
| artifact_name: windows-x64-build | |
| win_arch: x64 | |
| win_unpacked_dir: win-unpacked | |
| - os: windows-latest | |
| config: buildScripts/electron-builder-win-arm64-insider.json | |
| artifact_name: windows-arm64-build | |
| win_arch: arm64 | |
| win_unpacked_dir: win-arm64-unpacked | |
| - os: macos-latest | |
| config: buildScripts/electron-builder-mac-insider.json | |
| artifact_name: macos-build | |
| steps: | |
| - name: Checkout dev branch | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: dev | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| - name: Install pnpm | |
| run: npm install -g pnpm@10.18.3 | |
| - name: Install dependencies | |
| run: pnpm install | |
| - name: Update version for insider build | |
| id: version | |
| run: | | |
| CURRENT_VERSION=$(node -p "require('./package.json').version") | |
| DATE_TAG=$(date +%Y%m%d) | |
| NEW_VERSION="$CURRENT_VERSION-dev.$DATE_TAG" | |
| echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT | |
| echo "📦 Insider version: $NEW_VERSION" | |
| # Update package.json with new version | |
| node -e "const fs=require('fs'); const pkg=require('./package.json'); pkg.version='$NEW_VERSION'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));" | |
| shell: bash | |
| - name: Build application | |
| run: pnpm run build | |
| env: | |
| PPTB_CHANNEL: insider | |
| SUPABASE_URL: ${{ secrets.SUPABASE_URL }} | |
| SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} | |
| APPINSIGHTS_CONNECTION_STRING: ${{ secrets.APPINSIGHTS_CONNECTION_STRING }} | |
| SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| SENTRY_ORG: ${{ secrets.SENTRY_ORG }} | |
| SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} | |
| - name: Package application (Ubuntu) | |
| if: matrix.os == 'ubuntu-latest' | |
| run: node ./buildScripts/package.js --config=${{ matrix.config }} | |
| env: | |
| PPTB_CHANNEL: insider | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| SUPABASE_URL: ${{ secrets.SUPABASE_URL }} | |
| SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} | |
| APPINSIGHTS_CONNECTION_STRING: ${{ secrets.APPINSIGHTS_CONNECTION_STRING }} | |
| SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| SENTRY_ORG: ${{ secrets.SENTRY_ORG }} | |
| SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} | |
| - name: Build application directory (Windows) | |
| if: matrix.os == 'windows-latest' | |
| run: pnpm exec electron-builder --config ${{ matrix.config }} --dir --${{ matrix.win_arch }} | |
| env: | |
| PPTB_CHANNEL: insider | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| SUPABASE_URL: ${{ secrets.SUPABASE_URL }} | |
| SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} | |
| APPINSIGHTS_CONNECTION_STRING: ${{ secrets.APPINSIGHTS_CONNECTION_STRING }} | |
| SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| SENTRY_ORG: ${{ secrets.SENTRY_ORG }} | |
| SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} | |
| - name: Sign application binaries with Azure Trusted Signing (Windows) | |
| if: matrix.os == 'windows-latest' | |
| uses: azure/artifact-signing-action@v1 | |
| with: | |
| azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} | |
| azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} | |
| azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} | |
| endpoint: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }} | |
| signing-account-name: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }} | |
| certificate-profile-name: ${{ secrets.TRUSTED_SIGNING_CERTIFICATE_PROFILE }} | |
| files-folder: ${{ github.workspace }}/build/${{ matrix.win_unpacked_dir }} | |
| files-folder-filter: exe,dll | |
| files-folder-recurse: true | |
| timestamp-rfc3161: http://timestamp.acs.microsoft.com | |
| timestamp-digest: SHA256 | |
| description: Power Platform ToolBox (Insider) | |
| description-url: https://github.com/PowerPlatformToolBox/desktop-app | |
| - name: Generate app-update.yml metadata (Windows) | |
| if: matrix.os == 'windows-latest' | |
| shell: powershell | |
| run: | | |
| $projectRoot = "${{ github.workspace }}" | |
| $unpackedDir = Join-Path $projectRoot "build/${{ matrix.win_unpacked_dir }}" | |
| $resourcesDir = Join-Path $unpackedDir "resources" | |
| $appUpdateYml = Join-Path $resourcesDir "app-update.yml" | |
| if (Test-Path $appUpdateYml) { | |
| Write-Host "app-update.yml already exists at $appUpdateYml" | |
| exit 0 | |
| } | |
| $configPath = Join-Path $projectRoot "${{ matrix.config }}" | |
| $config = Get-Content $configPath -Raw | ConvertFrom-Json | |
| $parentConfig = $null | |
| if ($config.extends) { | |
| $parentPath = Join-Path $projectRoot $config.extends | |
| if (Test-Path $parentPath) { | |
| $parentConfig = Get-Content $parentPath -Raw | ConvertFrom-Json | |
| } | |
| } | |
| $publish = $null | |
| if ($config.publish) { | |
| $publish = $config.publish | |
| } elseif ($parentConfig -and $parentConfig.publish) { | |
| $publish = $parentConfig.publish | |
| } else { | |
| $publish = [pscustomobject]@{ | |
| provider = "github" | |
| owner = "PowerPlatformToolBox" | |
| repo = "desktop-app" | |
| } | |
| } | |
| $packageJsonPath = Join-Path $projectRoot "package.json" | |
| $packageJson = Get-Content $packageJsonPath -Raw | ConvertFrom-Json | |
| $sanitizedName = ($packageJson.name -replace "[^a-zA-Z0-9._-]", "") | |
| $updaterCacheDirName = "{0}-updater" -f $sanitizedName.ToLowerInvariant() | |
| if (-not (Test-Path $resourcesDir)) { | |
| New-Item -ItemType Directory -Path $resourcesDir -Force | Out-Null | |
| } | |
| $lines = @() | |
| $lines += "provider: $($publish.provider)" | |
| if ($publish.owner) { $lines += "owner: $($publish.owner)" } | |
| if ($publish.repo) { $lines += "repo: $($publish.repo)" } | |
| if ($publish.releaseType) { $lines += "releaseType: $($publish.releaseType)" } | |
| if ($publish.channel) { $lines += "channel: $($publish.channel)" } | |
| $lines += "updaterCacheDirName: $updaterCacheDirName" | |
| Set-Content -Path $appUpdateYml -Value ($lines -join "`n") -NoNewline | |
| Write-Host "Generated app-update.yml at $appUpdateYml" | |
| - name: Validate app-update.yml exists (Windows) | |
| if: matrix.os == 'windows-latest' | |
| shell: powershell | |
| run: | | |
| $unpackedDir = Join-Path "${{ github.workspace }}/build" "${{ matrix.win_unpacked_dir }}" | |
| $resourcesDir = Join-Path $unpackedDir "resources" | |
| $appUpdateYml = Join-Path $resourcesDir "app-update.yml" | |
| if (-not (Test-Path $appUpdateYml)) { | |
| Write-Host "❌ Missing updater config: $appUpdateYml" | |
| Write-Host "Windows in-app auto-update requires NSIS metadata in resources/app-update.yml." | |
| if (Test-Path $resourcesDir) { | |
| Write-Host "Contents of resources directory:" | |
| Get-ChildItem $resourcesDir | Select-Object Name, Length | |
| } | |
| exit 1 | |
| } | |
| Write-Host "✅ Found updater config: $appUpdateYml" | |
| - name: Package installers from signed directory (Windows) | |
| if: matrix.os == 'windows-latest' | |
| run: pnpm exec electron-builder --config ${{ matrix.config }} --prepackaged build/${{ matrix.win_unpacked_dir }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| SUPABASE_URL: ${{ secrets.SUPABASE_URL }} | |
| SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} | |
| APPINSIGHTS_CONNECTION_STRING: ${{ secrets.APPINSIGHTS_CONNECTION_STRING }} | |
| SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| SENTRY_ORG: ${{ secrets.SENTRY_ORG }} | |
| SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} | |
| - name: Sign Windows installers with Azure Trusted Signing | |
| if: matrix.os == 'windows-latest' | |
| uses: azure/artifact-signing-action@v1 | |
| with: | |
| azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} | |
| azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} | |
| azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} | |
| endpoint: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }} | |
| signing-account-name: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }} | |
| certificate-profile-name: ${{ secrets.TRUSTED_SIGNING_CERTIFICATE_PROFILE }} | |
| files-folder: ${{ github.workspace }}/build | |
| files-folder-filter: exe,msi | |
| files-folder-recurse: false | |
| timestamp-rfc3161: http://timestamp.acs.microsoft.com | |
| timestamp-digest: SHA256 | |
| description: Power Platform ToolBox (Insider) | |
| description-url: https://github.com/PowerPlatformToolBox/desktop-app | |
| - name: Regenerate latest.yml with correct SHA256 hashes (Windows) | |
| if: matrix.os == 'windows-latest' | |
| shell: powershell | |
| run: | | |
| $buildDir = "${{ github.workspace }}/build" | |
| $latestYml = Join-Path $buildDir "latest.yml" | |
| if (Test-Path $latestYml) { | |
| Write-Host "Regenerating latest.yml with correct hashes..." | |
| # Read the existing YAML to preserve version and other metadata | |
| $ymlContent = Get-Content $latestYml -Raw | |
| # Extract version from existing YAML | |
| $versionMatch = [regex]::Match($ymlContent, 'version:\s+([^\s]+)') | |
| $version = if ($versionMatch.Success) { $versionMatch.Groups[1].Value } else { "unknown" } | |
| # Find the main EXE file (look for the installer) | |
| $exeFiles = @(Get-ChildItem "$buildDir/*.exe" -ErrorAction SilentlyContinue) | |
| if ($exeFiles.Count -gt 0) { | |
| # Sort to get the latest/main installer (NSIS or main app exe) | |
| $mainExe = $exeFiles | Where-Object { $_.Name -match "(NSIS|Setup|Installer)" } | Select-Object -First 1 | |
| if (-not $mainExe) { | |
| $mainExe = $exeFiles[0] # Fallback to first exe | |
| } | |
| Write-Host "Using EXE: $($mainExe.Name)" | |
| # Calculate SHA256 and SHA512 hashes | |
| $sha256 = (Get-FileHash -Path $mainExe.FullName -Algorithm SHA256).Hash.ToLower() | |
| $sha512 = (Get-FileHash -Path $mainExe.FullName -Algorithm SHA512).Hash.ToLower() | |
| $size = $mainExe.Length | |
| Write-Host "SHA256: $sha256" | |
| Write-Host "SHA512: $sha512" | |
| Write-Host "Size: $size" | |
| # Create new YAML content with correct hashes | |
| $releaseDate = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.000Z') | |
| $yml = @{ | |
| version = $version | |
| files = @( | |
| @{ | |
| url = $mainExe.Name | |
| sha512 = $sha512 | |
| sha256 = $sha256 | |
| size = $size | |
| blockMapSize = $null | |
| } | |
| ) | |
| releaseDate = $releaseDate | |
| } | |
| $newYmlContent = "version: $version`n" | |
| $newYmlContent += "files:`n" | |
| $newYmlContent += " - url: $($mainExe.Name)`n" | |
| $newYmlContent += " sha512: $sha512`n" | |
| $newYmlContent += " sha256: $sha256`n" | |
| $newYmlContent += " size: $size`n" | |
| $newYmlContent += " blockMapSize: null`n" | |
| $newYmlContent += "releaseDate: $releaseDate" | |
| # Write updated YAML | |
| Set-Content -Path $latestYml -Value $newYmlContent | |
| Write-Host "✅ latest.yml regenerated with correct hashes" | |
| } else { | |
| Write-Host "⚠️ No EXE files found, skipping YAML regeneration" | |
| } | |
| } else { | |
| Write-Host "⚠️ latest.yml not found at $latestYml" | |
| } | |
| - name: Prepare macOS signing certificate | |
| if: matrix.os == 'macos-latest' | |
| shell: bash | |
| env: | |
| MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }} | |
| MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| if [[ -z "$MACOS_CERT_P12" || -z "$MACOS_CERT_PASSWORD" || -z "$APPLE_ID" || -z "$APPLE_APP_SPECIFIC_PASSWORD" || -z "$APPLE_TEAM_ID" ]]; then | |
| echo "Mac signing secrets are not configured." | |
| exit 1 | |
| fi | |
| CERT_PATH="$RUNNER_TEMP/macos-signing-cert.p12" | |
| printf '%s' "$MACOS_CERT_P12" | base64 --decode > "$CERT_PATH" | |
| chmod 600 "$CERT_PATH" | |
| { | |
| echo "CSC_LINK=$CERT_PATH" | |
| echo "CSC_KEY_PASSWORD=$MACOS_CERT_PASSWORD" | |
| echo "APPLE_ID=$APPLE_ID" | |
| echo "APPLE_APP_SPECIFIC_PASSWORD=$APPLE_APP_SPECIFIC_PASSWORD" | |
| echo "APPLE_TEAM_ID=$APPLE_TEAM_ID" | |
| echo "MACOS_CERT_PATH=$CERT_PATH" | |
| } >> "$GITHUB_ENV" | |
| - name: Verify certificate details | |
| if: matrix.os == 'macos-latest' | |
| shell: bash | |
| run: | | |
| echo "=== Checking available signing identities ===" | |
| security find-identity -v -p codesigning | |
| echo "" | |
| echo "Note: Should show 'Developer ID Application' not 'Mac App Distribution'" | |
| - name: Package application (macOS) | |
| if: matrix.os == 'macos-latest' | |
| shell: bash | |
| env: | |
| PPTB_CHANNEL: insider | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| SUPABASE_URL: ${{ secrets.SUPABASE_URL }} | |
| SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} | |
| APPINSIGHTS_CONNECTION_STRING: ${{ secrets.APPINSIGHTS_CONNECTION_STRING }} | |
| SENTRY_DSN: ${{ secrets.SENTRY_DSN }} | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| SENTRY_ORG: ${{ secrets.SENTRY_ORG }} | |
| SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} | |
| DEBUG: electron-builder | |
| CSC_FOR_PULL_REQUEST: true | |
| CSC_IDENTITY_AUTO_DISCOVERY: true | |
| run: | | |
| node ./buildScripts/package.js --config=${{ matrix.config }} | |
| - name: Verify macOS code signing (pre-notarization) | |
| if: matrix.os == 'macos-latest' | |
| shell: bash | |
| run: | | |
| APP_PATH="build/mac/Power Platform ToolBox Insider.app" | |
| echo "=== Verifying main app signature ===" | |
| codesign --verify --deep --strict --verbose=4 "$APP_PATH" 2>&1 | |
| echo "" | |
| echo "=== Checking for timestamps on main app ===" | |
| codesign -dvvv "$APP_PATH" 2>&1 | grep -i timestamp || echo "❌ WARNING: NO TIMESTAMP FOUND ON MAIN APP!" | |
| echo "" | |
| echo "=== Verifying critical nested binaries ===" | |
| # Check helper apps | |
| for helper in "$APP_PATH/Contents/Frameworks/"*.app; do | |
| if [ -d "$helper" ]; then | |
| echo "Checking helper: $(basename "$helper")" | |
| codesign --verify --strict "$helper" 2>&1 || echo "❌ Failed: $helper" | |
| codesign -dvvv "$helper" 2>&1 | grep -i timestamp || echo "❌ No timestamp: $helper" | |
| fi | |
| done | |
| # Check frameworks | |
| echo "" | |
| echo "=== Checking frameworks ===" | |
| find "$APP_PATH/Contents/Frameworks" -name "*.framework" -type d -maxdepth 1 | while read framework; do | |
| echo "Checking: $(basename "$framework")" | |
| codesign --verify --strict "$framework" 2>&1 || echo "❌ Failed: $framework" | |
| done | |
| # Check dylibs | |
| echo "" | |
| echo "=== Checking dynamic libraries ===" | |
| find "$APP_PATH" -name "*.dylib" -type f | head -5 | while read dylib; do | |
| echo "Checking: $(basename "$dylib")" | |
| codesign --verify --strict "$dylib" 2>&1 || echo "❌ Failed: $dylib" | |
| done | |
| echo "" | |
| echo "=== Note ===" | |
| echo "Skipping 'spctl --assess' here because it typically reports 'source=Unnotarized Developer ID' until the notarization+stapling job completes." | |
| echo "" | |
| echo "✅ macOS code signing verification complete" | |
| - name: Submit macOS notarization request | |
| if: matrix.os == 'macos-latest' | |
| shell: bash | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| node ./buildScripts/notarize.js submit --assets="build/*.dmg,build/*.zip,build/*.pkg" --app="build/mac/Power Platform ToolBox Insider.app" --output="build/notarization-info.json" | |
| - name: Cleanup macOS signing certificate | |
| if: ${{ always() && matrix.os == 'macos-latest' }} | |
| shell: bash | |
| run: | | |
| if [[ -n "${MACOS_CERT_PATH:-}" && -f "$MACOS_CERT_PATH" ]]; then | |
| rm -f "$MACOS_CERT_PATH" | |
| echo "Removed temporary macOS signing certificate." | |
| fi | |
| - name: Remove quarantine attributes (macOS) | |
| if: matrix.os == 'macos-latest' | |
| run: | | |
| echo "Removing quarantine attributes from DMG files..." | |
| find build -name "*.dmg" -exec xattr -cr {} \; || true | |
| echo "✅ Quarantine attributes removed" | |
| shell: bash | |
| - name: Regenerate latest-linux.yml with correct SHA256 hashes (Linux) | |
| if: matrix.os == 'ubuntu-latest' | |
| shell: bash | |
| run: | | |
| echo "🔄 Regenerating latest-linux.yml with correct artifact hashes..." | |
| BUILD_DIR="${{ github.workspace }}/build" | |
| # Find all YAML files | |
| YML_FILES=$(find "$BUILD_DIR" -name "latest*.yml" -o -name "*-linux.yml") | |
| if [[ -z "$YML_FILES" ]]; then | |
| echo "⚠️ No YAML files found, skipping regeneration" | |
| exit 0 | |
| fi | |
| for YML_FILE in $YML_FILES; do | |
| echo "Processing: $YML_FILE" | |
| # Extract version from existing YAML | |
| VERSION=$(grep -oP 'version:\s+\K[^\s]+' "$YML_FILE" || echo "unknown") | |
| # Find the main AppImage file | |
| APP_IMAGE=$(find "$BUILD_DIR" -maxdepth 1 -type f -name "*.AppImage" | head -n 1) | |
| if [[ -n "$APP_IMAGE" && -f "$APP_IMAGE" ]]; then | |
| echo " Found AppImage: $(basename "$APP_IMAGE")" | |
| # Calculate SHA256 and SHA512 hashes | |
| HASH256=$(sha256sum "$APP_IMAGE" | awk '{print $1}') | |
| HASH512=$(sha512sum "$APP_IMAGE" | awk '{print $1}') | |
| SIZE=$(stat -c %s "$APP_IMAGE" 2>/dev/null || stat -f %z "$APP_IMAGE" 2>/dev/null) | |
| echo " SHA256: $HASH256" | |
| echo " SHA512: $HASH512" | |
| echo " Size: $SIZE" | |
| # Create new YAML with correct hashes using printf to avoid YAML parsing issues | |
| printf "version: %s\nfiles:\n - url: %s\n sha512: %s\n sha256: %s\n size: %s\n blockMapSize: null\nreleaseDate: %s\n" \ | |
| "$VERSION" \ | |
| "$(basename "$APP_IMAGE")" \ | |
| "$HASH512" \ | |
| "$HASH256" \ | |
| "$SIZE" \ | |
| "$(date -u +'%Y-%m-%dT%H:%M:%S.000Z')" > "$YML_FILE" | |
| echo " ✅ Updated $YML_FILE" | |
| else | |
| echo " ⚠️ No AppImage found in $BUILD_DIR" | |
| fi | |
| done | |
| echo "✅ Regeneration complete" | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact_name }} | |
| path: | | |
| build/*.dmg | |
| build/*.pkg | |
| build/*.exe | |
| build/*.msi | |
| build/*.zip | |
| build/*.AppImage | |
| build/*.yml | |
| build/notarization-info.json | |
| retention-days: 30 | |
| prepare-release-metadata: | |
| needs: [check-commits, build] | |
| if: needs.check-commits.outputs.should_build == 'true' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| new_version: ${{ steps.version.outputs.new_version }} | |
| tag_name: ${{ steps.version.outputs.tag_name }} | |
| release_name: ${{ steps.version.outputs.release_name }} | |
| whatsnew: ${{ steps.whatsnew.outputs.commits }} | |
| steps: | |
| - name: Checkout dev branch | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: dev | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| - name: Calculate version | |
| id: version | |
| run: | | |
| CURRENT_VERSION=$(node -p "require('./package.json').version") | |
| DATE_TAG=$(date +%Y%m%d) | |
| NEW_VERSION="$CURRENT_VERSION-dev.$DATE_TAG" | |
| echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT | |
| echo "tag_name=v$NEW_VERSION" >> $GITHUB_OUTPUT | |
| echo "release_name=Insider Dev Build - $NEW_VERSION" >> $GITHUB_OUTPUT | |
| shell: bash | |
| - name: Generate what's new | |
| id: whatsnew | |
| run: | | |
| LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") | |
| if [ -n "$LAST_TAG" ]; then | |
| COMMITS=$(git log --oneline --no-merges -10 "$LAST_TAG"..HEAD) | |
| else | |
| COMMITS=$(git log --oneline --no-merges -10) | |
| fi | |
| WHATSNEW=$(echo "$COMMITS" | sed 's/^/- /') | |
| echo "commits<<EOF" >> $GITHUB_OUTPUT | |
| echo "$WHATSNEW" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| shell: bash | |
| mac-notarization: | |
| needs: [check-commits, build] | |
| if: needs.check-commits.outputs.should_build == 'true' | |
| runs-on: macos-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: dev | |
| fetch-depth: 0 | |
| - name: Download macOS artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: macos-build | |
| path: notarize | |
| merge-multiple: true | |
| - name: Locate notarization info | |
| id: notarize-info | |
| shell: bash | |
| run: | | |
| INFO_FILE=$(find notarize -name "notarization-info.json" | head -n 1) | |
| if [[ -z "$INFO_FILE" ]]; then | |
| echo "Notarization info file not found within downloaded artifacts." >&2 | |
| find notarize -maxdepth 3 -type f || true | |
| exit 1 | |
| fi | |
| echo "info_path=$INFO_FILE" >> $GITHUB_OUTPUT | |
| - name: Wait for notarization acceptance | |
| shell: bash | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| node ./buildScripts/notarize.js wait --info="${{ steps.notarize-info.outputs.info_path }}" --timeout-hours=12 --interval-minutes=5 | |
| - name: Staple macOS artifacts | |
| shell: bash | |
| run: | | |
| found=0 | |
| while IFS= read -r -d '' file; do | |
| found=1 | |
| echo "Stapling $(basename "$file")" | |
| xcrun stapler staple "$file" | |
| done < <(find notarize -type f \( -name "*.dmg" -o -name "*.pkg" \) -print0) | |
| if [[ "$found" -eq 0 ]]; then | |
| echo "No macOS artifacts found to staple under 'notarize'." >&2 | |
| find notarize -maxdepth 4 -type f || true | |
| exit 1 | |
| fi | |
| - name: Regenerate latest-mac.yml with correct SHA256 hashes | |
| shell: bash | |
| run: | | |
| echo "🔄 Merging macOS x64 and ARM64 latest-mac.yml files..." | |
| # Find all ZIP files (required for mac auto-update) | |
| X64_ZIP=$(find notarize -name "*-x64-mac.zip" -type f | head -n 1) | |
| ARM64_ZIP=$(find notarize -name "*-arm64-mac.zip" -type f | head -n 1) | |
| if [[ -z "$X64_ZIP" || -z "$ARM64_ZIP" ]]; then | |
| echo "⚠️ One or both macOS ZIP files not found. Skipping merge." | |
| echo "X64_ZIP: $X64_ZIP" | |
| echo "ARM64_ZIP: $ARM64_ZIP" | |
| # Fallback to single architecture (original behavior) | |
| YML_FILES=$(find notarize -name "latest*.yml" -o -name "*-mac.yml") | |
| if [[ -n "$YML_FILES" ]]; then | |
| for YML_FILE in $YML_FILES; do | |
| VERSION=$(grep -oP 'version:\s+\K[^\s]+' "$YML_FILE" || echo "unknown") | |
| ZIP_FILE=$(find "$(dirname "$YML_FILE")" -maxdepth 1 -name "*.zip" | head -n 1) | |
| if [[ -n "$ZIP_FILE" && -f "$ZIP_FILE" ]]; then | |
| HASH256=$(shasum -a 256 "$ZIP_FILE" | awk '{print $1}') | |
| HASH512=$(shasum -a 512 "$ZIP_FILE" | awk '{print $1}') | |
| SIZE=$(stat -f '%z' "$ZIP_FILE" 2>/dev/null || stat -c '%s' "$ZIP_FILE" 2>/dev/null) | |
| printf "version: %s\nfiles:\n - url: %s\n sha512: %s\n sha256: %s\n size: %s\n blockMapSize: null\nreleaseDate: %s\n" \ | |
| "$VERSION" "$(basename "$ZIP_FILE")" "$HASH512" "$HASH256" "$SIZE" "$(date -u +'%Y-%m-%dT%H:%M:%S.000Z')" > "$YML_FILE" | |
| fi | |
| done | |
| fi | |
| exit 0 | |
| fi | |
| echo "Found X64 ZIP: $(basename "$X64_ZIP")" | |
| echo "Found ARM64 ZIP: $(basename "$ARM64_ZIP")" | |
| # Find any existing YAML to extract version | |
| EXISTING_YML=$(find notarize -name "latest*.yml" -o -name "*-mac.yml" | head -n 1) | |
| VERSION=$(awk '/^version:/{print $2; exit}' "$EXISTING_YML" 2>/dev/null || echo "unknown") | |
| echo "Version: $VERSION" | |
| # Calculate hashes for x64 ZIP | |
| echo "🔐 Calculating hashes for x64 ZIP..." | |
| X64_SHA256=$(shasum -a 256 "$X64_ZIP" | awk '{print $1}') | |
| X64_SHA512=$(shasum -a 512 "$X64_ZIP" | awk '{print $1}') | |
| X64_SIZE=$(stat -f '%z' "$X64_ZIP" 2>/dev/null || stat -c '%s' "$X64_ZIP" 2>/dev/null) | |
| echo " SHA256: $X64_SHA256" | |
| echo " SHA512: $X64_SHA512" | |
| echo " Size: $X64_SIZE" | |
| # Calculate hashes for ARM64 ZIP | |
| echo "🔐 Calculating hashes for ARM64 ZIP..." | |
| ARM64_SHA256=$(shasum -a 256 "$ARM64_ZIP" | awk '{print $1}') | |
| ARM64_SHA512=$(shasum -a 512 "$ARM64_ZIP" | awk '{print $1}') | |
| ARM64_SIZE=$(stat -f '%z' "$ARM64_ZIP" 2>/dev/null || stat -c '%s' "$ARM64_ZIP" 2>/dev/null) | |
| echo " SHA256: $ARM64_SHA256" | |
| echo " SHA512: $ARM64_SHA512" | |
| echo " Size: $ARM64_SIZE" | |
| # Create merged YAML with both architectures | |
| MERGED_YML="notarize/latest-mac.yml" | |
| cat > "$MERGED_YML" << EOF | |
| version: $VERSION | |
| files: | |
| - url: $(basename "$X64_ZIP") | |
| sha512: $X64_SHA512 | |
| sha256: $X64_SHA256 | |
| size: $X64_SIZE | |
| blockMapSize: null | |
| - url: $(basename "$ARM64_ZIP") | |
| sha512: $ARM64_SHA512 | |
| sha256: $ARM64_SHA256 | |
| size: $ARM64_SIZE | |
| blockMapSize: null | |
| releaseDate: $(date -u +'%Y-%m-%dT%H:%M:%S.000Z') | |
| EOF | |
| echo "" | |
| echo "✅ Merged latest-mac.yml created:" | |
| cat "$MERGED_YML" | |
| # Remove any other YAML files in subdirectories to prevent conflicts | |
| find notarize -name "latest*.yml" -o -name "*-mac.yml" | while read yml; do | |
| if [[ "$yml" != "$MERGED_YML" ]]; then | |
| rm -f "$yml" | |
| echo "Removed: $yml" | |
| fi | |
| done | |
| echo "✅ Regeneration complete" | |
| - name: Upload stapled macOS artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: macos-build | |
| path: | | |
| notarize/**/*.dmg | |
| notarize/**/*.pkg | |
| notarize/**/*.zip | |
| notarize/**/*.yml | |
| notarize/**/notarization-info.json | |
| retention-days: 30 | |
| overwrite: true | |
| publish-release: | |
| needs: [check-commits, build, prepare-release-metadata, mac-notarization] | |
| if: needs.check-commits.outputs.should_build == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout dev branch | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: dev | |
| fetch-depth: 0 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: Display structure of downloaded files | |
| run: ls -R artifacts | |
| - name: Delete old pre-releases | |
| run: | | |
| echo "🗑️ Deleting old pre-releases..." | |
| gh release list --limit 1000 --json tagName,isPrerelease --jq '.[] | select(.isPrerelease == true) | .tagName' | awk 'NR>2' | while IFS= read -r TAG; do | |
| if [ -n "$TAG" ]; then | |
| echo "Deleting pre-release: $TAG" | |
| gh release delete "$TAG" --yes --cleanup-tag || echo "⚠️ Failed to delete $TAG (may not exist or already deleted)" | |
| fi | |
| done | |
| echo "✅ Pre-release cleanup completed" | |
| echo "⏳ Waiting for deletion to propagate..." | |
| sleep 5 | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Merge Windows latest.yml files | |
| run: | | |
| echo "🔄 Merging Windows x64 and ARM64 latest.yml files..." | |
| # Find the Windows YAML files | |
| X64_YML=$(find artifacts/windows-x64-build -name "latest.yml" 2>/dev/null || echo "") | |
| ARM64_YML=$(find artifacts/windows-arm64-build -name "latest.yml" 2>/dev/null || echo "") | |
| if [[ -z "$X64_YML" || -z "$ARM64_YML" ]]; then | |
| echo "⚠️ One or both Windows YAML files not found. Skipping merge." | |
| echo "X64_YML: $X64_YML" | |
| echo "ARM64_YML: $ARM64_YML" | |
| exit 0 | |
| fi | |
| echo "Found X64 YAML: $X64_YML" | |
| echo "Found ARM64 YAML: $ARM64_YML" | |
| # Extract version and release date from x64 YAML (should be the same for both) | |
| VERSION=$(grep -oP 'version:\s+\K[^\s]+' "$X64_YML" || echo "unknown") | |
| RELEASE_DATE=$(grep -oP 'releaseDate:\s+\K.+' "$X64_YML" || date -u +'%Y-%m-%dT%H:%M:%S.000Z') | |
| echo "Version: $VERSION" | |
| echo "Release Date: $RELEASE_DATE" | |
| # Find the EXE files in each artifact directory | |
| X64_EXE=$(find artifacts/windows-x64-build -name "*-x64-win.exe" -type f | head -n 1) | |
| ARM64_EXE=$(find artifacts/windows-arm64-build -name "*-arm64-win.exe" -type f | head -n 1) | |
| if [[ -z "$X64_EXE" || -z "$ARM64_EXE" ]]; then | |
| echo "❌ Could not find both x64 and ARM64 EXE files" | |
| echo "X64_EXE: $X64_EXE" | |
| echo "ARM64_EXE: $ARM64_EXE" | |
| exit 1 | |
| fi | |
| echo "Found X64 EXE: $(basename "$X64_EXE")" | |
| echo "Found ARM64 EXE: $(basename "$ARM64_EXE")" | |
| # Calculate hashes for x64 | |
| X64_SHA256=$(sha256sum "$X64_EXE" | awk '{print $1}') | |
| X64_SHA512=$(sha512sum "$X64_EXE" | awk '{print $1}') | |
| X64_SIZE=$(stat -c %s "$X64_EXE" 2>/dev/null || stat -f %z "$X64_EXE" 2>/dev/null) | |
| echo "X64 SHA256: $X64_SHA256" | |
| echo "X64 SHA512: $X64_SHA512" | |
| echo "X64 Size: $X64_SIZE" | |
| # Calculate hashes for ARM64 | |
| ARM64_SHA256=$(sha256sum "$ARM64_EXE" | awk '{print $1}') | |
| ARM64_SHA512=$(sha512sum "$ARM64_EXE" | awk '{print $1}') | |
| ARM64_SIZE=$(stat -c %s "$ARM64_EXE" 2>/dev/null || stat -f %z "$ARM64_EXE" 2>/dev/null) | |
| echo "ARM64 SHA256: $ARM64_SHA256" | |
| echo "ARM64 SHA512: $ARM64_SHA512" | |
| echo "ARM64 Size: $ARM64_SIZE" | |
| # Create merged YAML with both architectures | |
| MERGED_YML="artifacts/latest.yml" | |
| cat > "$MERGED_YML" << EOF | |
| version: $VERSION | |
| files: | |
| - url: $(basename "$X64_EXE") | |
| sha512: $X64_SHA512 | |
| sha256: $X64_SHA256 | |
| size: $X64_SIZE | |
| blockMapSize: null | |
| - url: $(basename "$ARM64_EXE") | |
| sha512: $ARM64_SHA512 | |
| sha256: $ARM64_SHA256 | |
| size: $ARM64_SIZE | |
| blockMapSize: null | |
| releaseDate: $RELEASE_DATE | |
| EOF | |
| echo "" | |
| echo "✅ Merged latest.yml created:" | |
| cat "$MERGED_YML" | |
| # Remove the individual YAML files so they don't get uploaded | |
| rm -f "$X64_YML" "$ARM64_YML" | |
| echo "" | |
| echo "✅ Individual YAML files removed" | |
| shell: bash | |
| - name: Prepare release files | |
| run: | | |
| echo "📦 Preparing release files..." | |
| mkdir -p release-files | |
| # Copy all release artifacts (excluding individual Windows YAML files which were removed) | |
| find artifacts -type f \( -name "*.AppImage" -o -name "*.dmg" -o -name "*.zip" -o -name "*.exe" -o -name "*.msi" -o -name "*.pkg" -o -name "*.snap" -o -name "*.deb" -o -name "*.rpm" -o -name "latest*.yml" \) -exec cp {} release-files/ \; | |
| echo "✅ Release files prepared" | |
| ls -lh release-files/ | |
| shell: bash | |
| - name: Publish release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ needs.prepare-release-metadata.outputs.tag_name }} | |
| name: ${{ needs.prepare-release-metadata.outputs.release_name }} | |
| body: | | |
| # 👀 Insider Pre-Release | |
| ## 📋 What's New | |
| ${{ needs.prepare-release-metadata.outputs.whatsnew }} | |
| ## TESTING PURPOSES ONLY - NOT FOR PRODUCTION USE | |
| **Version:** ${{ needs.prepare-release-metadata.outputs.new_version }} | |
| **Branch:** dev | |
| **Commits:** ${{ needs.check-commits.outputs.commit_count }} in the last 24 hours | |
| **Build Date:** ${{ github.run_id }} | |
| ## ⚠️ Pre-release Warning | |
| This is an automated insider build from the development branch. It may contain: | |
| - 🐛 Bugs and instability | |
| - 🚧 Incomplete features | |
| - 💥 Breaking changes | |
| **Not recommended for production use!** | |
| ## 📦 Installation | |
| Choose the appropriate installer for your platform: | |
| - **Windows**: Download the `.exe` installer or the portable `.zip` | |
| - **macOS**: Download the `.dmg` (signed + notarized) or `.zip` | |
| - **Linux**: Download the `.AppImage` file | |
| ## 🔗 Links | |
| - [View commits](${{ github.server_url }}/${{ github.repository }}/commits/dev) | |
| - [Report an issue](${{ github.server_url }}/${{ github.repository }}/issues/new) | |
| --- | |
| *This release was automatically generated by the nightly insider packaging workflow.* | |
| files: | | |
| release-files/* | |
| prerelease: true | |
| draft: false | |
| fail_on_unmatched_files: false | |
| make_latest: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # publish-types-dev: | |
| # needs: [check-commits, publish-release, prepare-release-metadata] | |
| # if: needs.check-commits.outputs.should_build == 'true' | |
| # permissions: | |
| # contents: read | |
| # id-token: write | |
| # uses: ./.github/workflows/publish-npm-types.yml | |
| # with: | |
| # branch: dev | |
| # tag: dev | |
| # version: ${{ needs.prepare-release-metadata.outputs.new_version }} | |
| # secrets: inherit |