Build, Stage, and Deploy Release #26
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: Build, Stage, and Deploy Release | |
# Workflow supports three modes: | |
# 1. Production mode (PR merge to release): Full deployment to all stores and Docker Hub | |
# 2. Stage-only mode (manual trigger): Creates draft/candidate releases in stores, skips Docker push | |
# 3. Dry-run mode (manual trigger): Builds everything but doesn't deploy anywhere | |
on: | |
pull_request: | |
types: [ closed ] | |
branches: [ release ] | |
workflow_dispatch: | |
inputs: | |
dry_run: | |
description: 'Dry run mode (no actual deployments)' | |
required: false | |
type: boolean | |
default: true | |
stage_only: | |
description: 'Stage to stores without publishing (draft/candidate releases)' | |
required: false | |
type: boolean | |
default: false | |
jobs: | |
calculate-version: | |
name: Calculate Next Version | |
runs-on: ubuntu-latest | |
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'release') | |
outputs: | |
version: ${{ steps.version.outputs.version }} | |
previous_version: ${{ steps.version.outputs.previous_version }} | |
dry_run: ${{ steps.dry_run.outputs.enabled }} | |
stage_only: ${{ steps.stage_only.outputs.enabled }} | |
pr_body: ${{ steps.pr_details.outputs.body }} | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
with: | |
fetch-depth: 0 | |
- name: Set dry run mode | |
id: dry_run | |
run: | | |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
DRY_RUN="${{ inputs.dry_run }}" | |
else | |
# PR merges to release branch are always production | |
DRY_RUN="false" | |
fi | |
echo "enabled=$DRY_RUN" >> $GITHUB_OUTPUT | |
echo "Dry run mode: $DRY_RUN" | |
- name: Set stage only mode | |
id: stage_only | |
run: | | |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
STAGE_ONLY="${{ inputs.stage_only }}" | |
else | |
# PR merges to release branch are always production (not stage-only) | |
STAGE_ONLY="false" | |
fi | |
echo "enabled=$STAGE_ONLY" >> $GITHUB_OUTPUT | |
echo "Stage only mode: $STAGE_ONLY" | |
- name: Check all secrets availability | |
run: | | |
echo "🔐 SECRET AVAILABILITY CHECK" | |
echo "============================" | |
echo "" | |
echo "📦 Docker Hub:" | |
[ -n "${{ secrets.DOCKERHUB_USERNAME }}" ] && echo " ✅ DOCKERHUB_USERNAME" || echo " ❌ DOCKERHUB_USERNAME" | |
[ -n "${{ secrets.DOCKERHUB_TOKEN }}" ] && echo " ✅ DOCKERHUB_TOKEN" || echo " ❌ DOCKERHUB_TOKEN" | |
echo "" | |
echo "📦 Snap Store:" | |
[ -n "${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}" ] && echo " ✅ SNAPCRAFT_STORE_CREDENTIALS" || echo " ❌ SNAPCRAFT_STORE_CREDENTIALS" | |
echo "" | |
echo "🪟 Microsoft Store:" | |
[ -n "${{ secrets.MS_TENANT_ID }}" ] && echo " ✅ MS_TENANT_ID" || echo " ❌ MS_TENANT_ID" | |
[ -n "${{ secrets.MS_CLIENT_ID }}" ] && echo " ✅ MS_CLIENT_ID" || echo " ❌ MS_CLIENT_ID" | |
[ -n "${{ secrets.MS_CLIENT_SECRET }}" ] && echo " ✅ MS_CLIENT_SECRET" || echo " ❌ MS_CLIENT_SECRET" | |
[ -n "${{ secrets.MS_APP_ID }}" ] && echo " ✅ MS_APP_ID" || echo " ❌ MS_APP_ID" | |
echo "" | |
echo "🍎 Mac App Store:" | |
[ -n "${{ secrets.MAS_CODESIGN_ID }}" ] && echo " ✅ MAS_CODESIGN_ID" || echo " ❌ MAS_CODESIGN_ID" | |
[ -n "${{ secrets.MAS_INSTALLER_ID }}" ] && echo " ✅ MAS_INSTALLER_ID" || echo " ❌ MAS_INSTALLER_ID" | |
echo "" | |
echo "🍎 Apple Store Connect API:" | |
[ -n "${{ secrets.APP_STORE_CONNECT_API_KEY }}" ] && echo " ✅ APP_STORE_CONNECT_API_KEY" || echo " ❌ APP_STORE_CONNECT_API_KEY" | |
[ -n "${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}" ] && echo " ✅ APP_STORE_CONNECT_API_KEY_ID" || echo " ❌ APP_STORE_CONNECT_API_KEY_ID" | |
[ -n "${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}" ] && echo " ✅ APP_STORE_CONNECT_ISSUER_ID" || echo " ❌ APP_STORE_CONNECT_ISSUER_ID" | |
[ -n "${{ secrets.APPLE_ID }}" ] && echo " ✅ APPLE_ID" || echo " ❌ APPLE_ID" | |
echo "" | |
echo "============================" | |
echo "" | |
- name: Calculate version | |
id: version | |
run: | | |
NEXT_VERSION=$(bash scripts/version-bump.sh) | |
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") | |
PREVIOUS_VERSION="${LATEST_TAG#v}" | |
echo "version=$NEXT_VERSION" >> $GITHUB_OUTPUT | |
echo "previous_version=$PREVIOUS_VERSION" >> $GITHUB_OUTPUT | |
echo "Next version: $NEXT_VERSION" | |
echo "Previous version: $PREVIOUS_VERSION" | |
- name: Capture PR details | |
id: pr_details | |
run: | | |
if [ "${{ github.event_name }}" = "pull_request" ]; then | |
# PR event - capture the PR body | |
PR_BODY="${{ github.event.pull_request.body }}" | |
else | |
# Manual trigger - provide default release notes | |
PR_BODY="Manual release deployment for v${{ steps.version.outputs.version }}" | |
fi | |
# Escape special characters and prepare for multi-line output | |
PR_BODY="${PR_BODY//'%'/'%25'}" | |
PR_BODY="${PR_BODY//$'\n'/'%0A'}" | |
PR_BODY="${PR_BODY//$'\r'/'%0D'}" | |
echo "body=$PR_BODY" >> $GITHUB_OUTPUT | |
build-docker-ce: | |
name: Build Docker Images (CE) | |
strategy: | |
matrix: | |
include: | |
- runner: ubuntu-latest | |
platform: linux/amd64 | |
arch: amd64 | |
- runner: ubuntu-24.04-arm # Native ARM64 build | |
platform: linux/arm64 | |
arch: arm64 | |
runs-on: ${{ matrix.runner }} | |
needs: calculate-version | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- name: Set up Docker Buildx | |
uses: docker/setup-buildx-action@v3 | |
- name: Build Docker image for ${{ matrix.platform }} | |
uses: docker/build-push-action@v6 | |
with: | |
context: . | |
file: ./core/Dockerfile | |
platforms: ${{ matrix.platform }} | |
push: false | |
tags: clidey/whodb:${{ needs.calculate-version.outputs.version }}-${{ matrix.arch }} | |
outputs: type=oci,dest=/tmp/whodb-docker-${{ matrix.arch }}.tar | |
- name: Upload Docker artifact | |
uses: actions/upload-artifact@v4 | |
with: | |
name: docker-image-${{ matrix.arch }} | |
path: /tmp/whodb-docker-${{ matrix.arch }}.tar | |
retention-days: 1 | |
# Build desktop executables for store packaging | |
build-desktop-ce: | |
name: Build Desktop Executables (CE) | |
strategy: | |
matrix: | |
include: | |
# Windows builds for Microsoft Store | |
- os: windows-latest | |
platform: windows | |
arch: amd64 | |
make-target: build-prod-windows-amd64 | |
- os: windows-11-arm # Use native ARM64 runner for Windows ARM64 | |
platform: windows | |
arch: arm64 | |
make-target: build-prod-windows-arm64 | |
# macOS build for Mac App Store | |
- os: macos-latest | |
platform: darwin | |
arch: universal | |
make-target: build-prod-mac | |
runs-on: ${{ matrix.os }} | |
needs: calculate-version | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- name: Setup Go | |
uses: actions/setup-go@v5 | |
with: | |
go-version-file: 'desktop-ce/go.mod' | |
- name: Setup Node.js and pnpm | |
uses: actions/setup-node@v4 | |
with: | |
node-version: 'lts/*' | |
- uses: pnpm/action-setup@v4 | |
with: | |
version: 10 | |
- name: Install frontend dependencies | |
working-directory: ./frontend | |
run: pnpm i | |
- name: Install Wails CLI | |
run: go install github.com/wailsapp/wails/v2/cmd/wails@latest | |
- name: Setup ARM64 compiler on Windows ARM | |
if: matrix.platform == 'windows' && matrix.arch == 'arm64' | |
shell: pwsh | |
run: | | |
Write-Host "Checking available GCC compilers..." | |
Get-Command gcc -ErrorAction SilentlyContinue | Format-List | |
Write-Host "Go environment:" | |
go env GOARCH | |
go env CC | |
Write-Host "Setting GOARCH=arm64 explicitly..." | |
$env:GOARCH = "arm64" | |
Write-Host "Updated GOARCH:" | |
go env GOARCH | |
- name: Build Windows | |
if: matrix.platform == 'windows' && matrix.arch == 'amd64' | |
working-directory: ./desktop-ce | |
shell: pwsh | |
env: | |
GOARCH: ${{ matrix.arch }} | |
run: | | |
# Build raw exe for MSIX packaging (not NSIS installer) | |
Write-Host "Building raw Windows executable for MSIX packaging..." | |
$env:GOWORK = "$PWD/../go.work.desktop-ce" | |
# Clean previous build | |
Remove-Item -Path build -Recurse -Force -ErrorAction SilentlyContinue | |
# Prepare frontend | |
Write-Host "Preparing frontend..." | |
New-Item -ItemType Directory -Path frontend/dist -Force | Out-Null | |
Set-Location ../frontend | |
pnpm run build:ce | |
Copy-Item -Path build/* -Destination ../desktop-ce/frontend/dist/ -Recurse | |
Set-Location ../desktop-ce | |
# Build without NSIS flag to get raw exe | |
Write-Host "Building with wails..." | |
wails build -clean -platform windows/amd64 ` | |
-windowsconsole=false -ldflags="-s -w -H windowsgui" ` | |
-o whodb.exe | |
if ($LASTEXITCODE -ne 0) { | |
Write-Error "Wails build failed with exit code: $LASTEXITCODE" | |
exit 1 | |
} | |
# Find where wails actually put the exe | |
Write-Host "Looking for built executable..." | |
Write-Host "Contents of build directory:" | |
Get-ChildItem -Path build -Recurse -ErrorAction SilentlyContinue | Format-Table Name, FullName | |
# Check common locations | |
$possibleLocations = @( | |
"build\bin\whodb.exe", | |
"build\bin\whodb-amd64.exe", | |
"build\whodb.exe" | |
) | |
$foundExe = $null | |
foreach ($location in $possibleLocations) { | |
if (Test-Path $location) { | |
$foundExe = $location | |
Write-Host "Found exe at: $foundExe" | |
break | |
} | |
} | |
if (-not $foundExe) { | |
Write-Error "Could not find whodb.exe after build" | |
exit 1 | |
} | |
# Move to expected location | |
New-Item -ItemType Directory -Path build/windows/amd64 -Force | Out-Null | |
Move-Item $foundExe build/windows/amd64/whodb.exe -Force | |
Write-Host "Raw executable moved to: build/windows/amd64/whodb.exe" | |
Write-Host "Verifying final location..." | |
if (Test-Path "build/windows/amd64/whodb.exe") { | |
$fileInfo = Get-Item "build/windows/amd64/whodb.exe" | |
Write-Host "✅ Executable ready: $($fileInfo.FullName) (Size: $($fileInfo.Length) bytes)" | |
} else { | |
Write-Error "Failed to move executable to expected location" | |
exit 1 | |
} | |
- name: Skip Windows ARM64 Build (Temporarily Disabled) | |
if: matrix.platform == 'windows' && matrix.arch == 'arm64' | |
shell: pwsh | |
run: | | |
Write-Host "⚠️ Windows ARM64 build is temporarily disabled due to toolchain issues" | |
Write-Host "Creating placeholder for build artifacts..." | |
New-Item -ItemType Directory -Force -Path "./desktop-ce/build/windows/arm64" | |
Write-Host "Skipping actual build..." | |
- name: Build macOS | |
if: matrix.platform == 'darwin' | |
working-directory: ./desktop-ce | |
run: make ${{ matrix.make-target }} VERSION=${{ needs.calculate-version.outputs.version }} | |
- name: List build outputs before upload | |
if: matrix.platform == 'windows' && matrix.arch == 'amd64' | |
shell: pwsh | |
working-directory: ./desktop-ce | |
run: | | |
Write-Host "📦 Listing build outputs before upload:" | |
if (Test-Path "build") { | |
Get-ChildItem -Path build -Recurse | Format-Table Name, FullName, Length | |
} else { | |
Write-Host "❌ Build directory does not exist!" | |
} | |
- name: Upload artifacts (AMD64 and macOS) | |
if: matrix.arch != 'arm64' || matrix.platform != 'windows' | |
uses: actions/upload-artifact@v4 | |
with: | |
name: desktop-${{ matrix.platform }}-${{ matrix.arch || 'all' }} | |
path: | | |
desktop-ce/build/ | |
retention-days: 1 | |
- name: Skip ARM64 Artifacts Upload | |
if: matrix.platform == 'windows' && matrix.arch == 'arm64' | |
shell: pwsh | |
run: | | |
Write-Host "⚠️ Skipping ARM64 artifact upload (build was disabled)" | |
# Package for Mac App Store | |
package-mac-app-store: | |
name: Package for Mac App Store | |
runs-on: macos-latest | |
needs: [ calculate-version, build-desktop-ce ] | |
continue-on-error: ${{ needs.calculate-version.outputs.stage_only == 'true' }} | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- name: Download macOS build | |
uses: actions/download-artifact@v4 | |
with: | |
name: desktop-darwin-universal | |
path: desktop-ce/ | |
- name: Check certificate availability | |
id: check_mac_cert | |
run: | | |
echo "🔍 Checking Mac App Store certificates..." | |
if [ -n "${{ secrets.MAS_CODESIGN_ID }}" ]; then | |
echo " ✅ MAS_CODESIGN_ID is configured" | |
else | |
echo " ❌ MAS_CODESIGN_ID is NOT configured" | |
fi | |
if [ -n "${{ secrets.MAS_INSTALLER_ID }}" ]; then | |
echo " ✅ MAS_INSTALLER_ID is configured" | |
else | |
echo " ❌ MAS_INSTALLER_ID is NOT configured" | |
fi | |
if [ -n "${{ secrets.MAS_CODESIGN_ID }}" ] && [ -n "${{ secrets.MAS_INSTALLER_ID }}" ]; then | |
echo "has_cert=true" >> $GITHUB_OUTPUT | |
echo "✅ Mac certificates available - will build pkg package" | |
else | |
echo "has_cert=false" >> $GITHUB_OUTPUT | |
echo "⚠️ Mac certificates not configured - will create dummy package" | |
fi | |
- name: Create dummy pkg for validation (no certs) | |
if: steps.check_mac_cert.outputs.has_cert != 'true' | |
run: | | |
if [ "${{ needs.calculate-version.outputs.dry_run }}" = "true" ]; then | |
echo "🏃 DRY RUN MODE - No certificates available" | |
else | |
echo "⚠️ PRODUCTION MODE - No certificates available" | |
fi | |
echo "Creating dummy pkg for validation..." | |
mkdir -p desktop-ce | |
touch "desktop-ce/WhoDB-${{ needs.calculate-version.outputs.version }}.pkg" | |
- name: Package for Mac App Store | |
if: steps.check_mac_cert.outputs.has_cert == 'true' | |
working-directory: ./desktop-ce | |
env: | |
MAS_CODESIGN_ID: ${{ secrets.MAS_CODESIGN_ID }} | |
MAS_INSTALLER_ID: ${{ secrets.MAS_INSTALLER_ID }} | |
run: | | |
if [ "${{ needs.calculate-version.outputs.dry_run }}" = "true" ]; then | |
echo "🏃 DRY RUN MODE - Building Mac App Store package (will not publish)" | |
fi | |
# Sign the app for Mac App Store | |
codesign --deep --force --options runtime --sign "$MAS_CODESIGN_ID" \ | |
"build/darwin/universal/WhoDB.app" | |
# Create Mac App Store package | |
productbuild --component "build/darwin/universal/WhoDB.app" /Applications \ | |
--sign "$MAS_INSTALLER_ID" \ | |
"WhoDB-${{ needs.calculate-version.outputs.version }}.pkg" | |
- name: Upload Mac App Store package | |
uses: actions/upload-artifact@v4 | |
with: | |
name: mac-app-store | |
path: desktop-ce/WhoDB-*.pkg | |
retention-days: 1 | |
package-windows-msix: | |
name: Package Windows MSIX | |
runs-on: windows-latest | |
needs: [ calculate-version, build-desktop-ce ] | |
continue-on-error: ${{ needs.calculate-version.outputs.stage_only == 'true' }} | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- name: Check Windows SDK availability | |
shell: pwsh | |
run: | | |
Write-Host "🔍 Checking for Windows SDK..." | |
# Check if makeappx is directly available | |
$makeappxCmd = Get-Command makeappx -ErrorAction SilentlyContinue | |
if ($makeappxCmd) { | |
Write-Host "✅ makeappx is available in PATH: $($makeappxCmd.Source)" | |
} else { | |
# Check if makeappx exists in any SDK version | |
$sdkRoot = "C:\Program Files (x86)\Windows Kits\10\bin" | |
if (Test-Path $sdkRoot) { | |
$versions = Get-ChildItem -Path $sdkRoot -Directory | | |
Where-Object { $_.Name -match "^10\.\d+\.\d+\.\d+$" } | | |
Sort-Object { [version]($_.Name -replace "^10\.", "") } -Descending | |
Write-Host "Found Windows SDK versions:" | |
$versions | ForEach-Object { Write-Host " - $($_.Name)" } | |
$makeappxFound = $false | |
foreach ($version in $versions) { | |
if (Test-Path (Join-Path $version.FullName "x64\makeappx.exe")) { | |
Write-Host "✅ makeappx.exe available in SDK $($version.Name)" | |
$makeappxFound = $true | |
break | |
} | |
} | |
if (-not $makeappxFound) { | |
Write-Host "⚠️ makeappx.exe not found in any SDK version" | |
Write-Host "Note: Will attempt to use makeappx directly or create dummy for dry run" | |
} | |
} else { | |
Write-Host "⚠️ Windows SDK not found at expected location" | |
} | |
} | |
- name: Download Windows amd64 builds | |
uses: actions/download-artifact@v4 | |
with: | |
name: desktop-windows-amd64 | |
path: desktop-ce/ | |
# ARM64 builds temporarily disabled | |
# - name: Download Windows arm64 builds | |
# uses: actions/download-artifact@v4 | |
# with: | |
# name: desktop-windows-arm64 | |
# path: desktop-ce/ | |
- name: List downloaded Windows build artifacts | |
shell: pwsh | |
run: | | |
Write-Host "📁 Current directory: $PWD" | |
Write-Host "" | |
Write-Host "📁 Contents of desktop-ce directory:" | |
if (Test-Path "desktop-ce") { | |
Get-ChildItem -Path "desktop-ce" -Recurse | Format-Table Name, FullName, Length | |
} else { | |
Write-Host "❌ desktop-ce directory does not exist!" | |
Write-Host "Looking for any build directories..." | |
Get-ChildItem -Path . | Format-Table Name, FullName | |
} | |
Write-Host "" | |
Write-Host "🔍 Looking for .exe files anywhere:" | |
Get-ChildItem -Path . -Filter "*.exe" -Recurse -ErrorAction SilentlyContinue | Format-Table Name, FullName, Length | |
- name: Build unsigned MSIX for Microsoft Store | |
shell: pwsh | |
run: | | |
if ("${{ needs.calculate-version.outputs.dry_run }}" -eq "true") { | |
echo "🏃 DRY RUN MODE - Building unsigned MSIX package (will not publish)" | |
} else { | |
echo "📦 Building unsigned MSIX package for Microsoft Store" | |
} | |
echo "ℹ️ Microsoft will sign this package when uploaded to Partner Center" | |
# Add Windows SDK to PATH if available | |
$sdkVersions = Get-ChildItem -Path "C:\Program Files (x86)\Windows Kits\10\bin" -Directory -ErrorAction SilentlyContinue | | |
Where-Object { $_.Name -match "^10\.\d+\.\d+\.\d+$" } | | |
Sort-Object { [version]($_.Name -replace "^10\.", "") } -Descending | |
foreach ($version in $sdkVersions) { | |
$binPath = Join-Path $version.FullName "x64" | |
if (Test-Path (Join-Path $binPath "makeappx.exe")) { | |
$env:PATH = "$binPath;$env:PATH" | |
Write-Host "Added Windows SDK to PATH: $binPath" | |
break | |
} | |
} | |
# Try to build MSIX package | |
$ErrorActionPreference = "Stop" | |
try { | |
.\scripts\build-msix.ps1 -Architecture amd64 -Version ${{ needs.calculate-version.outputs.version }} -SkipSigning | |
Write-Host "✅ MSIX build script completed" | |
} catch { | |
Write-Host "Failed to build MSIX: $_" | |
if ("${{ needs.calculate-version.outputs.dry_run }}" -eq "true") { | |
Write-Host "Creating dummy MSIX bundle for dry run validation..." | |
New-Item -ItemType File -Path "WhoDB-${{ needs.calculate-version.outputs.version }}.msixbundle" -Force | Out-Null | |
Write-Host "✅ Created dummy MSIX bundle for dry run" | |
} else { | |
Write-Error "Failed to create MSIX package: $_" | |
exit 1 | |
} | |
} | |
# ARM64 build temporarily disabled | |
# - name: Build MSIX arm64 | |
# if: needs.calculate-version.outputs.dry_run == 'false' | |
# shell: pwsh | |
# env: | |
# WINDOWS_PFX_PASSWORD: ${{ secrets.WINDOWS_PFX_PASSWORD }} | |
# run: | | |
# .\scripts\build-msix.ps1 -Architecture arm64 -Version ${{ needs.calculate-version.outputs.version }} -PublisherCN "${{ secrets.WINDOWS_PUBLISHER_CN }}" -CertPath cert.pfx | |
- name: Create MSIX bundle (AMD64 only for now) | |
shell: pwsh | |
run: | | |
# Since we only have AMD64 for now, rename it as the bundle | |
Move-Item "WhoDB-${{ needs.calculate-version.outputs.version }}-amd64.msix" "WhoDB-${{ needs.calculate-version.outputs.version }}.msixbundle" -Force | |
- name: Upload MSIX artifacts | |
uses: actions/upload-artifact@v4 | |
with: | |
name: windows-msix | |
path: WhoDB-*.msixbundle | |
retention-days: 1 | |
build-snap: | |
name: Build Snap Package | |
strategy: | |
matrix: | |
include: | |
- runner: ubuntu-latest | |
arch: amd64 | |
- runner: ubuntu-24.04-arm | |
arch: arm64 | |
runs-on: ${{ matrix.runner }} | |
needs: [ calculate-version ] | |
continue-on-error: ${{ needs.calculate-version.outputs.stage_only == 'true' }} | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- name: Prepare snap build | |
run: | | |
# Copy the CE snapcraft file to the expected location | |
cp snapcraft-ce.yaml snapcraft.yaml | |
- name: Build snap from source | |
uses: snapcore/action-build@v1 | |
id: build | |
- name: Rename snap with architecture | |
run: | | |
SNAP_FILE="${{ steps.build.outputs.snap }}" | |
NEW_NAME="${SNAP_FILE%.snap}_${{ matrix.arch }}.snap" | |
mv "$SNAP_FILE" "$NEW_NAME" | |
echo "SNAP_FILE=$NEW_NAME" >> $GITHUB_ENV | |
- name: Upload snap artifact | |
uses: actions/upload-artifact@v4 | |
with: | |
name: snap-package-${{ matrix.arch }} | |
path: ${{ env.SNAP_FILE }} | |
retention-days: 1 | |
sign-with-sigstore: | |
name: Sign Artifacts with Sigstore | |
runs-on: ubuntu-latest | |
needs: [ calculate-version, package-windows-msix, package-mac-app-store, build-snap, build-desktop-ce ] | |
continue-on-error: ${{ needs.calculate-version.outputs.stage_only == 'true' }} | |
# Need write permissions for OIDC token | |
permissions: | |
contents: write | |
id-token: write | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- name: Install Cosign | |
uses: sigstore/cosign-installer@v3 | |
- name: Download all artifacts | |
uses: actions/download-artifact@v4 | |
with: | |
path: artifacts/ | |
# Continue even if dockerbuild metadata artifacts fail to download | |
continue-on-error: true | |
- name: Sign Windows MSIX bundle | |
env: | |
COSIGN_EXPERIMENTAL: 1 | |
run: | | |
for file in artifacts/windows-msix/*.msixbundle; do | |
if [ -f "$file" ]; then | |
echo "Signing $file with Sigstore..." | |
cosign sign-blob --yes "$file" \ | |
--output-signature="${file}.sig" \ | |
--output-certificate="${file}.pem" \ | |
--oidc-issuer=https://token.actions.githubusercontent.com \ | |
|| echo "Warning: Failed to sign $file, continuing..." | |
fi | |
done | |
- name: Sign Mac App Store package | |
env: | |
COSIGN_EXPERIMENTAL: 1 | |
run: | | |
for file in artifacts/mac-app-store/*.pkg; do | |
if [ -f "$file" ]; then | |
echo "Signing $file with Sigstore..." | |
cosign sign-blob --yes "$file" \ | |
--output-signature="${file}.sig" \ | |
--output-certificate="${file}.pem" \ | |
--oidc-issuer=https://token.actions.githubusercontent.com \ | |
|| echo "Warning: Failed to sign $file, continuing..." | |
fi | |
done | |
- name: Upload signed artifacts | |
uses: actions/upload-artifact@v4 | |
with: | |
name: signed-artifacts | |
path: artifacts/ | |
retention-days: 1 | |
validate-all-builds: | |
name: Validate All Builds | |
runs-on: ubuntu-latest | |
needs: [ sign-with-sigstore, build-docker-ce, calculate-version ] | |
steps: | |
- name: Checkout for scripts | |
uses: actions/checkout@v4 | |
- name: Download required artifacts for validation | |
uses: actions/download-artifact@v4 | |
with: | |
path: artifacts/ | |
# Continue even if dockerbuild metadata artifacts fail | |
continue-on-error: true | |
- name: Clean up Docker metadata artifacts | |
run: | | |
# Remove any dockerbuild artifacts that may have partially downloaded | |
rm -rf artifacts/*dockerbuild* || true | |
echo "Cleaned up Docker metadata artifacts (not required for validation)" | |
- name: Validate artifacts | |
run: | | |
echo "Checking for required artifacts..." | |
[ -f "artifacts/docker-image-amd64/whodb-docker-amd64.tar" ] || (echo "Docker amd64 image missing" && exit 1) | |
[ -f "artifacts/docker-image-arm64/whodb-docker-arm64.tar" ] || (echo "Docker arm64 image missing" && exit 1) | |
[ -f "artifacts/windows-msix/"*.msixbundle ] || (echo "Windows MSIX bundle missing" && exit 1) | |
[ -f "artifacts/mac-app-store/"*.pkg ] || (echo "Mac App Store package missing" && exit 1) | |
[ -f "artifacts/snap-package-amd64/"*.snap ] || (echo "Snap amd64 package missing" && exit 1) | |
[ -f "artifacts/snap-package-arm64/"*.snap ] || (echo "Snap arm64 package missing" && exit 1) | |
echo "✓ All required artifacts present" | |
echo "Note: Windows ARM64 build is temporarily disabled" | |
- name: Initialize deployment manifest | |
run: | | |
bash scripts/track-deployment.sh docker pending ${{ needs.calculate-version.outputs.version }} | |
- name: Upload deployment manifest | |
uses: actions/upload-artifact@v4 | |
with: | |
name: deployment-manifest | |
path: deployment_manifest.json | |
retention-days: 1 | |
# Deploy to stores first (these can be reviewed/rolled back before going live) | |
deploy-snap: | |
name: Deploy to Snap Store | |
runs-on: ubuntu-latest | |
needs: [ calculate-version, validate-all-builds ] | |
continue-on-error: ${{ needs.calculate-version.outputs.stage_only == 'true' }} | |
steps: | |
- name: Checkout for scripts | |
uses: actions/checkout@v4 | |
- name: Check Snapcraft credentials | |
run: | | |
echo "🔍 Checking Snapcraft credentials..." | |
if [ -n "${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}" ]; then | |
echo " ✅ SNAPCRAFT_STORE_CREDENTIALS is configured" | |
else | |
echo " ❌ SNAPCRAFT_STORE_CREDENTIALS is NOT configured" | |
if [ "${{ needs.calculate-version.outputs.dry_run }}" = "false" ]; then | |
echo "::error::Production deployment requires SNAPCRAFT_STORE_CREDENTIALS" | |
exit 1 | |
fi | |
fi | |
- name: Download snap amd64 artifact | |
uses: actions/download-artifact@v4 | |
with: | |
name: snap-package-amd64 | |
- name: Download snap arm64 artifact | |
uses: actions/download-artifact@v4 | |
with: | |
name: snap-package-arm64 | |
- name: Download deployment manifest | |
uses: actions/download-artifact@v4 | |
with: | |
name: deployment-manifest | |
path: . | |
- name: Capture current Snap Store state | |
if: needs.calculate-version.outputs.dry_run == 'false' | |
env: | |
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} | |
run: | | |
bash scripts/capture-store-state.sh whodb | |
- name: Publish to Snap Store | |
if: needs.calculate-version.outputs.dry_run == 'false' | |
env: | |
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} | |
run: | | |
# Determine release channel based on stage_only flag | |
if [ "${{ needs.calculate-version.outputs.stage_only }}" = "true" ]; then | |
CHANNEL="candidate" | |
echo "📦 STAGE ONLY MODE - Uploading to candidate channel (not releasing to stable)" | |
else | |
CHANNEL="stable" | |
echo "📦 PRODUCTION MODE - Releasing to stable channel" | |
fi | |
# Snap Store automatically handles multi-arch packages when uploaded separately | |
for snap in *.snap; do | |
echo "Publishing $snap to Snap Store ($CHANNEL channel)..." | |
snapcraft upload "$snap" --release="$CHANNEL" | |
done | |
- name: Track Snap deployment | |
if: needs.calculate-version.outputs.dry_run == 'false' | |
run: | | |
bash scripts/track-deployment.sh snap deployed ${{ needs.calculate-version.outputs.version }} | |
- name: Dry run summary | |
if: needs.calculate-version.outputs.dry_run == 'true' | |
run: | | |
echo "🏃 DRY RUN MODE - Would have published:" | |
if [ "${{ needs.calculate-version.outputs.stage_only }}" = "true" ]; then | |
echo " - Snap packages (amd64 and arm64) to candidate channel (stage only)" | |
else | |
echo " - Snap packages (amd64 and arm64) to stable channel" | |
fi | |
ls -lh *.snap | |
- name: Upload updated manifest | |
if: always() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: deployment-manifest-snap | |
path: deployment_manifest.json | |
retention-days: 1 | |
- name: Upload store state | |
if: always() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: store-state | |
path: store_state.json | |
retention-days: 1 | |
deploy-microsoft-store: | |
name: Deploy to Microsoft Store | |
runs-on: windows-latest | |
needs: [ calculate-version, package-windows-msix, deploy-snap ] | |
if: | | |
needs.calculate-version.outputs.dry_run == 'false' && | |
(needs.calculate-version.outputs.stage_only == 'true' || needs.deploy-snap.result == 'success') | |
continue-on-error: ${{ needs.calculate-version.outputs.stage_only == 'true' }} | |
steps: | |
- name: Checkout for scripts | |
uses: actions/checkout@v4 | |
- name: Check Microsoft Store credentials | |
shell: pwsh | |
run: | | |
Write-Host "🔍 Checking Microsoft Store credentials..." | |
if ("${{ secrets.MS_TENANT_ID }}" -ne "") { | |
Write-Host " ✅ MS_TENANT_ID is configured" | |
} else { | |
Write-Host " ❌ MS_TENANT_ID is NOT configured" | |
} | |
if ("${{ secrets.MS_CLIENT_ID }}" -ne "") { | |
Write-Host " ✅ MS_CLIENT_ID is configured" | |
} else { | |
Write-Host " ❌ MS_CLIENT_ID is NOT configured" | |
} | |
if ("${{ secrets.MS_CLIENT_SECRET }}" -ne "") { | |
Write-Host " ✅ MS_CLIENT_SECRET is configured" | |
} else { | |
Write-Host " ❌ MS_CLIENT_SECRET is NOT configured" | |
} | |
if ("${{ secrets.MS_APP_ID }}" -ne "") { | |
Write-Host " ✅ MS_APP_ID is configured" | |
} else { | |
Write-Host " ❌ MS_APP_ID is NOT configured" | |
} | |
$allConfigured = ("${{ secrets.MS_TENANT_ID }}" -ne "") -and | |
("${{ secrets.MS_CLIENT_ID }}" -ne "") -and | |
("${{ secrets.MS_CLIENT_SECRET }}" -ne "") -and | |
("${{ secrets.MS_APP_ID }}" -ne "") | |
if ($allConfigured) { | |
Write-Host "✅ All Microsoft Store credentials are configured" | |
} else { | |
Write-Host "⚠️ Some Microsoft Store credentials are missing" | |
Write-Host "::warning::Microsoft Store deployment may fail due to missing credentials" | |
} | |
- name: Download MSIX package | |
uses: actions/download-artifact@v4 | |
with: | |
name: windows-msix | |
path: . | |
- name: Download deployment manifest | |
uses: actions/download-artifact@v4 | |
with: | |
name: deployment-manifest-snap | |
path: . | |
- name: Setup Microsoft Store CLI | |
shell: pwsh | |
run: | | |
# Install Windows Store CLI tools | |
Install-Module -Name StoreBroker -Force -Scope CurrentUser | |
Import-Module StoreBroker | |
- name: Authenticate to Microsoft Store | |
shell: pwsh | |
env: | |
MS_TENANT_ID: ${{ secrets.MS_TENANT_ID }} | |
MS_CLIENT_ID: ${{ secrets.MS_CLIENT_ID }} | |
MS_CLIENT_SECRET: ${{ secrets.MS_CLIENT_SECRET }} | |
run: | | |
$securePassword = ConvertTo-SecureString $env:MS_CLIENT_SECRET -AsPlainText -Force | |
$credential = New-Object System.Management.Automation.PSCredential($env:MS_CLIENT_ID, $securePassword) | |
# Authenticate to Partner Center | |
Set-StoreBrokerAuthentication -TenantId $env:MS_TENANT_ID -Credential $credential | |
- name: Create and submit Microsoft Store submission | |
shell: pwsh | |
env: | |
MS_APP_ID: ${{ secrets.MS_APP_ID }} | |
run: | | |
$appId = $env:MS_APP_ID | |
$msixPath = Get-ChildItem -Path "." -Filter "*.msixbundle" | Select-Object -First 1 | |
# Create new submission | |
$submission = New-ApplicationSubmission -AppId $appId -Force | |
# Update submission with new package | |
Update-ApplicationSubmission -AppId $appId -SubmissionDataPath $submission -PackagePath $msixPath.FullName | |
# Determine publish mode based on stage_only flag | |
if ("${{ needs.calculate-version.outputs.stage_only }}" -eq "true") { | |
Write-Host "📦 STAGE ONLY MODE - Creating draft submission (not publishing)" | |
$publishMode = "Manual" | |
} else { | |
Write-Host "📦 PRODUCTION MODE - Submitting for immediate publication" | |
$publishMode = "Immediate" | |
} | |
# Submit to Store | |
$submissionId = Submit-ApplicationSubmission -AppId $appId -SubmissionDataPath $submission -TargetPublishMode $publishMode | |
Write-Host "Submission created with ID: $submissionId" | |
if ("${{ needs.calculate-version.outputs.stage_only }}" -eq "true") { | |
Write-Host "Draft submission created. Manual review and publish required in Partner Center" | |
} else { | |
Write-Host "Submission will be published after Partner Center review" | |
} | |
- name: Upload updated manifest | |
if: always() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: deployment-manifest-msstore | |
path: deployment_manifest.json | |
retention-days: 1 | |
deploy-apple-store: | |
name: Deploy to Apple App Store | |
runs-on: macos-latest | |
needs: [ calculate-version, package-mac-app-store, deploy-snap ] | |
if: | | |
needs.calculate-version.outputs.dry_run == 'false' && | |
(needs.calculate-version.outputs.stage_only == 'true' || needs.deploy-snap.result == 'success') | |
continue-on-error: ${{ needs.calculate-version.outputs.stage_only == 'true' }} | |
steps: | |
- name: Checkout for scripts | |
uses: actions/checkout@v4 | |
- name: Check Apple Store credentials | |
run: | | |
echo "🔍 Checking Apple Store credentials..." | |
if [ -n "${{ secrets.APP_STORE_CONNECT_API_KEY }}" ]; then | |
echo " ✅ APP_STORE_CONNECT_API_KEY is configured" | |
else | |
echo " ❌ APP_STORE_CONNECT_API_KEY is NOT configured" | |
fi | |
if [ -n "${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}" ]; then | |
echo " ✅ APP_STORE_CONNECT_API_KEY_ID is configured" | |
else | |
echo " ❌ APP_STORE_CONNECT_API_KEY_ID is NOT configured" | |
fi | |
if [ -n "${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}" ]; then | |
echo " ✅ APP_STORE_CONNECT_ISSUER_ID is configured" | |
else | |
echo " ❌ APP_STORE_CONNECT_ISSUER_ID is NOT configured" | |
fi | |
if [ -n "${{ secrets.APPLE_ID }}" ]; then | |
echo " ✅ APPLE_ID is configured" | |
else | |
echo " ❌ APPLE_ID is NOT configured" | |
fi | |
if [ -n "${{ secrets.APP_STORE_CONNECT_API_KEY }}" ] && [ -n "${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}" ] && [ -n "${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}" ]; then | |
echo "✅ All Apple Store credentials are configured" | |
else | |
echo "⚠️ Some Apple Store credentials are missing" | |
echo "::warning::Apple Store deployment may fail due to missing credentials" | |
fi | |
- name: Download Mac App Store package | |
uses: actions/download-artifact@v4 | |
with: | |
name: mac-app-store | |
path: . | |
- name: Download deployment manifest | |
uses: actions/download-artifact@v4 | |
with: | |
name: deployment-manifest-snap | |
path: . | |
continue-on-error: true | |
- name: Setup App Store Connect API Key | |
env: | |
APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }} | |
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} | |
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} | |
run: | | |
# Create private keys directory | |
mkdir -p ~/.appstoreconnect/private_keys | |
# Save API key | |
echo "$APP_STORE_CONNECT_API_KEY" > ~/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8 | |
# Set proper permissions | |
chmod 600 ~/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8 | |
- name: Upload to App Store Connect | |
env: | |
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} | |
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} | |
APPLE_ID: ${{ secrets.APPLE_ID }} | |
run: | | |
# Find the .pkg file | |
PKG_FILE=$(find . -name "*.pkg" -type f | head -1) | |
if [ "${{ needs.calculate-version.outputs.stage_only }}" = "true" ]; then | |
echo "📦 STAGE ONLY MODE - Uploading for TestFlight only (not for App Store review)" | |
else | |
echo "📦 PRODUCTION MODE - Uploading for App Store release" | |
fi | |
echo "Uploading $PKG_FILE to App Store Connect..." | |
# Upload using Transporter (altool is deprecated) | |
xcrun iTMSTransporter -m upload \ | |
-assetFile "$PKG_FILE" \ | |
-apiKey "$APP_STORE_CONNECT_API_KEY_ID" \ | |
-apiIssuer "$APP_STORE_CONNECT_ISSUER_ID" \ | |
-v detailed \ | |
-verboseLogging | |
echo "✅ Package uploaded to App Store Connect" | |
if [ "${{ needs.calculate-version.outputs.stage_only }}" = "true" ]; then | |
echo "📝 Package available for TestFlight distribution only" | |
echo "📝 Manual submission required for App Store release" | |
else | |
echo "📝 Review and submit for App Store review in App Store Connect" | |
fi | |
- name: Create TestFlight release (optional) | |
env: | |
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} | |
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} | |
run: | | |
if [ "${{ needs.calculate-version.outputs.stage_only }}" = "true" ]; then | |
echo "📦 Stage only mode: Package uploaded for TestFlight testing only" | |
echo "TestFlight distribution can be configured in App Store Connect" | |
else | |
echo "Package is now available in App Store Connect for TestFlight distribution" | |
echo "Manual review and release required via App Store Connect dashboard" | |
fi | |
- name: Upload updated manifest | |
if: always() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: deployment-manifest-appstore | |
path: deployment_manifest.json | |
retention-days: 1 | |
# Create GitHub release after stores are deployed | |
create-github-release: | |
name: Create GitHub Release | |
runs-on: ubuntu-latest | |
needs: [ calculate-version, deploy-snap, deploy-microsoft-store, deploy-apple-store ] | |
if: | | |
always() && | |
needs.calculate-version.result == 'success' && | |
(needs.calculate-version.outputs.stage_only == 'true' || | |
(needs.deploy-snap.result == 'success' && | |
needs.deploy-microsoft-store.result == 'success' && | |
needs.deploy-apple-store.result == 'success')) | |
continue-on-error: ${{ needs.calculate-version.outputs.stage_only == 'true' }} | |
permissions: | |
contents: write | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- name: Download deployment manifest | |
uses: actions/download-artifact@v4 | |
with: | |
name: deployment-manifest-snap | |
path: . | |
continue-on-error: true | |
- name: Download all signed artifacts | |
uses: actions/download-artifact@v4 | |
with: | |
name: signed-artifacts | |
path: release-assets/ | |
- name: Prepare release assets | |
run: | | |
mkdir -p final-assets | |
# Copy Windows MSIX bundle (for users who prefer direct download) | |
cp release-assets/windows-msix/*.msixbundle final-assets/ || true | |
cp release-assets/windows-msix/*.sig final-assets/ || true | |
# Copy Mac App Store package (for users who prefer direct download) | |
cp release-assets/mac-app-store/*.pkg final-assets/ || true | |
cp release-assets/mac-app-store/*.sig final-assets/ || true | |
- name: Generate checksums | |
working-directory: final-assets | |
run: | | |
sha256sum * > SHA256SUMS.txt | |
- name: Create Release | |
id: create_release | |
if: needs.calculate-version.outputs.dry_run == 'false' | |
uses: softprops/action-gh-release@v2 | |
with: | |
tag_name: v${{ needs.calculate-version.outputs.version }} | |
name: v${{ needs.calculate-version.outputs.version }} | |
body: | | |
# WhoDB v${{ needs.calculate-version.outputs.version }} | |
## Release Notes | |
${{ needs.calculate-version.outputs.pr_body }} | |
## Docker Images | |
- `docker pull clidey/whodb:${{ needs.calculate-version.outputs.version }}` | |
- `docker pull clidey/whodb:latest` | |
## Installation | |
**Snap:** | |
```bash | |
sudo snap install whodb | |
``` | |
**Flatpak:** | |
Available on [Flathub](https://flathub.org/apps/com.clidey.whodb) | |
**Windows:** | |
Available on Microsoft Store | |
**macOS:** | |
Available on Mac App Store | |
## Verification | |
All binaries are signed with Sigstore. Verify signatures using cosign: | |
```bash | |
cosign verify-blob --signature <file>.sig --certificate <file>.pem <file> | |
``` | |
files: final-assets/* | |
draft: ${{ needs.calculate-version.outputs.stage_only == 'true' }} | |
prerelease: false | |
- name: Track GitHub release | |
if: needs.calculate-version.outputs.dry_run == 'false' | |
run: | | |
bash scripts/track-deployment.sh github_release deployed ${{ needs.calculate-version.outputs.version }} "${{ steps.create_release.outputs.id }}" | |
- name: Dry run summary | |
if: needs.calculate-version.outputs.dry_run == 'true' | |
run: | | |
echo "🏃 DRY RUN MODE - Would have created GitHub release:" | |
echo " - Tag: v${{ needs.calculate-version.outputs.version }}" | |
if [ "${{ needs.calculate-version.outputs.stage_only }}" = "true" ]; then | |
echo " - Status: Draft (stage only)" | |
else | |
echo " - Status: Published" | |
fi | |
echo " - Files:" | |
ls -lh final-assets/ | |
- name: Upload updated manifest | |
if: always() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: deployment-manifest-github | |
path: deployment_manifest.json | |
retention-days: 1 | |
# Deploy Docker images last (hardest to rollback) | |
deploy-docker: | |
name: Deploy Docker Images | |
runs-on: ubuntu-latest | |
needs: [ calculate-version, create-github-release ] | |
if: | | |
always() && | |
needs.calculate-version.result == 'success' && | |
(needs.calculate-version.outputs.stage_only == 'true' || | |
needs.create-github-release.result == 'success') | |
continue-on-error: ${{ needs.calculate-version.outputs.stage_only == 'true' }} | |
steps: | |
- name: Checkout for scripts | |
uses: actions/checkout@v4 | |
- name: Check Docker Hub credentials | |
run: | | |
echo "🔍 Checking Docker Hub credentials..." | |
if [ -n "${{ secrets.DOCKERHUB_USERNAME }}" ]; then | |
echo " ✅ DOCKERHUB_USERNAME is configured" | |
else | |
echo " ❌ DOCKERHUB_USERNAME is NOT configured" | |
fi | |
if [ -n "${{ secrets.DOCKERHUB_TOKEN }}" ]; then | |
echo " ✅ DOCKERHUB_TOKEN is configured" | |
else | |
echo " ❌ DOCKERHUB_TOKEN is NOT configured" | |
fi | |
if [ -n "${{ secrets.DOCKERHUB_USERNAME }}" ] && [ -n "${{ secrets.DOCKERHUB_TOKEN }}" ]; then | |
echo "✅ All Docker Hub credentials are configured" | |
else | |
echo "❌ Docker Hub credentials are missing" | |
if [ "${{ needs.calculate-version.outputs.dry_run }}" = "false" ]; then | |
echo "::error::Production deployment requires Docker Hub credentials" | |
exit 1 | |
fi | |
fi | |
- name: Download Docker amd64 artifact | |
uses: actions/download-artifact@v4 | |
with: | |
name: docker-image-amd64 | |
path: /tmp | |
- name: Download Docker arm64 artifact | |
uses: actions/download-artifact@v4 | |
with: | |
name: docker-image-arm64 | |
path: /tmp | |
- name: Download deployment manifest | |
uses: actions/download-artifact@v4 | |
with: | |
name: deployment-manifest-github | |
path: . | |
- name: Load Docker images | |
run: | | |
docker load --input /tmp/whodb-docker-amd64.tar | |
docker load --input /tmp/whodb-docker-arm64.tar | |
- name: Login to Docker Hub | |
uses: docker/login-action@v3 | |
with: | |
username: ${{ secrets.DOCKERHUB_USERNAME }} | |
password: ${{ secrets.DOCKERHUB_TOKEN }} | |
- name: Install Cosign | |
if: needs.calculate-version.outputs.dry_run == 'false' | |
uses: sigstore/cosign-installer@v3 | |
- name: Set up Docker Buildx for manifest | |
uses: docker/setup-buildx-action@v3 | |
- name: Push Docker images and create manifest | |
if: needs.calculate-version.outputs.dry_run == 'false' && needs.calculate-version.outputs.stage_only != 'true' | |
run: | | |
# Push individual platform images | |
docker push clidey/whodb:${{ needs.calculate-version.outputs.version }}-amd64 || exit 1 | |
docker push clidey/whodb:${{ needs.calculate-version.outputs.version }}-arm64 || exit 1 | |
# Create and push multi-platform manifests | |
docker buildx imagetools create -t clidey/whodb:${{ needs.calculate-version.outputs.version }} \ | |
clidey/whodb:${{ needs.calculate-version.outputs.version }}-amd64 \ | |
clidey/whodb:${{ needs.calculate-version.outputs.version }}-arm64 | |
docker buildx imagetools create -t clidey/whodb:latest \ | |
clidey/whodb:${{ needs.calculate-version.outputs.version }}-amd64 \ | |
clidey/whodb:${{ needs.calculate-version.outputs.version }}-arm64 | |
bash scripts/track-deployment.sh docker deployed ${{ needs.calculate-version.outputs.version }} | |
- name: Sign Docker images | |
if: needs.calculate-version.outputs.dry_run == 'false' && needs.calculate-version.outputs.stage_only != 'true' | |
run: | | |
cosign sign --yes clidey/whodb:${{ needs.calculate-version.outputs.version }} | |
cosign sign --yes clidey/whodb:latest | |
- name: Stage only summary | |
if: needs.calculate-version.outputs.stage_only == 'true' | |
run: | | |
echo "📦 STAGE ONLY MODE" | |
echo "✅ Successfully logged in to Docker Hub" | |
echo "✅ Docker images built and loaded locally" | |
echo "⏸️ Docker push skipped (stage-only mode behaves like dry run for Docker)" | |
echo "📝 Images ready but not published:" | |
echo " - clidey/whodb:${{ needs.calculate-version.outputs.version }}" | |
echo " - clidey/whodb:latest" | |
- name: Dry run summary | |
if: needs.calculate-version.outputs.dry_run == 'true' | |
run: | | |
echo "🏃 DRY RUN MODE" | |
echo "✅ Successfully logged in to Docker Hub" | |
echo "📦 Would have pushed:" | |
echo " - clidey/whodb:${{ needs.calculate-version.outputs.version }}" | |
echo " - clidey/whodb:latest" | |
echo "🔏 Would have signed both images with Sigstore" | |
- name: Upload updated manifest | |
if: always() | |
uses: actions/upload-artifact@v4 | |
with: | |
name: deployment-manifest-docker | |
path: deployment_manifest.json | |
retention-days: 1 | |
rollback: | |
name: Rollback on Failure | |
runs-on: ubuntu-latest | |
if: failure() && needs.calculate-version.outputs.dry_run == 'false' && needs.calculate-version.outputs.stage_only != 'true' | |
needs: [ calculate-version, deploy-docker, create-github-release, deploy-snap, deploy-microsoft-store, deploy-apple-store ] | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- name: Download deployment manifest | |
uses: actions/download-artifact@v4 | |
with: | |
name: deployment-manifest-docker | |
path: . | |
continue-on-error: true | |
- name: Download store state | |
uses: actions/download-artifact@v4 | |
with: | |
name: store-state | |
path: . | |
continue-on-error: true | |
- name: Enhanced comprehensive rollback | |
env: | |
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | |
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
GITHUB_REPOSITORY: ${{ github.repository }} | |
MS_TENANT_ID: ${{ secrets.MS_TENANT_ID }} | |
MS_CLIENT_ID: ${{ secrets.MS_CLIENT_ID }} | |
MS_CLIENT_SECRET: ${{ secrets.MS_CLIENT_SECRET }} | |
MS_APP_ID: ${{ secrets.MS_APP_ID }} | |
run: | | |
bash scripts/rollback-all.sh \ | |
${{ needs.calculate-version.outputs.version }} \ | |
${{ needs.calculate-version.outputs.previous_version }} \ | |
deployment_manifest.json \ | |
store_state.json | |
- name: Notify rollback | |
run: | | |
echo "::error::Deployment failed and has been rolled back" | |
echo "Failed version: ${{ needs.calculate-version.outputs.version }}" | |
echo "Reverted to: ${{ needs.calculate-version.outputs.previous_version }}" | |
echo "" | |
echo "Rollback actions taken:" | |
echo " - Docker: Reverted latest tag to ${{ needs.calculate-version.outputs.previous_version }}" | |
echo " - Docker: Deleted tag ${{ needs.calculate-version.outputs.version }}" | |
echo " - Snap: Manual verification required in Snapcraft dashboard" | |
echo " - GitHub: Release and tag deleted if they existed" | |
exit 1 | |
verify-deployment: | |
name: Verify Deployment Success | |
runs-on: ubuntu-latest | |
needs: [ calculate-version, deploy-docker ] | |
if: needs.calculate-version.outputs.dry_run == 'false' && needs.calculate-version.outputs.stage_only != 'true' | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- name: Download final deployment manifest | |
uses: actions/download-artifact@v4 | |
with: | |
name: deployment-manifest-docker | |
path: . | |
- name: Verify all deployments successful | |
run: | | |
echo "Verifying deployment status..." | |
DOCKER_STATUS=$(jq -r '.docker.status' deployment_manifest.json) | |
SNAP_STATUS=$(jq -r '.snap.status' deployment_manifest.json) | |
GITHUB_STATUS=$(jq -r '.github_release.status' deployment_manifest.json) | |
echo "Docker deployment: $DOCKER_STATUS" | |
echo "Snap deployment: $SNAP_STATUS" | |
echo "GitHub release: $GITHUB_STATUS" | |
if [ "$DOCKER_STATUS" != "deployed" ] || [ "$SNAP_STATUS" != "deployed" ] || [ "$GITHUB_STATUS" != "deployed" ]; then | |
echo "::error::Not all deployments completed successfully" | |
exit 1 | |
fi | |
echo "✓ All deployments verified successful" | |
- name: Test Docker image availability | |
run: | | |
echo "Testing Docker image pull..." | |
docker pull clidey/whodb:${{ needs.calculate-version.outputs.version }} || exit 1 | |
echo "✓ Docker image is accessible" | |
- name: Verify GitHub release | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
run: | | |
echo "Verifying GitHub release..." | |
RELEASE_URL="https://api.github.com/repos/${{ github.repository }}/releases/tags/v${{ needs.calculate-version.outputs.version }}" | |
RELEASE_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token $GITHUB_TOKEN" "$RELEASE_URL") | |
if [ "$RELEASE_STATUS" != "200" ]; then | |
echo "::error::GitHub release not found (HTTP $RELEASE_STATUS)" | |
exit 1 | |
fi | |
echo "✓ GitHub release verified" | |
# Summary job for stage-only mode to report what worked and what didn't | |
stage-only-summary: | |
name: Stage-Only Mode Summary | |
runs-on: ubuntu-latest | |
if: | | |
always() && | |
needs.calculate-version.outputs.stage_only == 'true' && | |
needs.calculate-version.result == 'success' | |
needs: [ calculate-version, deploy-snap, deploy-microsoft-store, deploy-apple-store, create-github-release, deploy-docker, package-mac-app-store, package-windows-msix ] | |
steps: | |
- name: Generate deployment summary | |
run: | | |
echo "📊 STAGE-ONLY MODE DEPLOYMENT SUMMARY" | |
echo "=====================================" | |
echo "" | |
echo "Version: ${{ needs.calculate-version.outputs.version }}" | |
echo "" | |
echo "📦 Store Deployments (Best Effort):" | |
echo "" | |
# Check each deployment status | |
if [ "${{ needs.deploy-snap.result }}" = "success" ]; then | |
echo "✅ Snap Store: Successfully staged to candidate channel" | |
elif [ "${{ needs.deploy-snap.result }}" = "skipped" ]; then | |
echo "⏭️ Snap Store: Skipped" | |
else | |
echo "❌ Snap Store: Failed (check logs for details)" | |
fi | |
if [ "${{ needs.deploy-microsoft-store.result }}" = "success" ]; then | |
echo "✅ Microsoft Store: Successfully created draft submission" | |
elif [ "${{ needs.deploy-microsoft-store.result }}" = "skipped" ]; then | |
echo "⏭️ Microsoft Store: Skipped" | |
else | |
echo "❌ Microsoft Store: Failed (check logs for details)" | |
fi | |
if [ "${{ needs.deploy-apple-store.result }}" = "success" ]; then | |
echo "✅ Apple App Store: Successfully uploaded for TestFlight" | |
elif [ "${{ needs.deploy-apple-store.result }}" = "skipped" ]; then | |
echo "⏭️ Apple App Store: Skipped" | |
else | |
echo "❌ Apple App Store: Failed (check logs for details)" | |
fi | |
echo "" | |
echo "📄 GitHub Release:" | |
if [ "${{ needs.create-github-release.result }}" = "success" ]; then | |
echo "✅ Draft release created successfully" | |
elif [ "${{ needs.create-github-release.result }}" = "skipped" ]; then | |
echo "⏭️ GitHub Release: Skipped" | |
else | |
echo "❌ Failed to create draft release" | |
fi | |
echo "" | |
echo "🐳 Docker:" | |
if [ "${{ needs.deploy-docker.result }}" = "success" ]; then | |
echo "✅ Images built and loaded (not pushed in stage-only mode)" | |
elif [ "${{ needs.deploy-docker.result }}" = "skipped" ]; then | |
echo "⏭️ Docker: Skipped" | |
else | |
echo "❌ Failed to build Docker images" | |
fi | |
echo "" | |
echo "📦 Packaging Status:" | |
if [ "${{ needs.package-mac-app-store.result }}" = "success" ]; then | |
echo "✅ Mac App Store package created" | |
elif [ "${{ needs.package-mac-app-store.result }}" = "skipped" ]; then | |
echo "⏭️ Mac App Store package: Skipped" | |
else | |
echo "❌ Mac App Store package failed" | |
fi | |
if [ "${{ needs.package-windows-msix.result }}" = "success" ]; then | |
echo "✅ Windows MSIX package created" | |
elif [ "${{ needs.package-windows-msix.result }}" = "skipped" ]; then | |
echo "⏭️ Windows MSIX package: Skipped" | |
else | |
echo "❌ Windows MSIX package failed" | |
fi | |
echo "" | |
echo "=====================================" | |
echo "" | |
echo "ℹ️ Stage-only mode uses a best-effort approach." | |
echo "ℹ️ Failed deployments did not block other deployments." | |
echo "ℹ️ Check individual job logs for failure details." |