Skip to content

Build, Stage, and Deploy Release #33

Build, Stage, and Deploy Release

Build, Stage, and Deploy Release #33

Workflow file for this run

name: Build, Stage, and Deploy Release
# Workflow supports three deployment modes (selectable via dropdown when manually triggered):
# 1. 'production' - Full deployment to all stores and Docker Hub (auto-selected for PR merges)
# 2. 'stage-only' - Creates draft/candidate releases in stores, skips Docker push
# 3. 'dry-run' - Builds everything but doesn't deploy anywhere (default for manual triggers)
on:
pull_request:
types: [ closed ]
branches: [ release ]
workflow_dispatch:
inputs:
deployment_mode:
description: 'Deployment Mode'
required: true
type: choice
default: 'dry-run'
options:
- 'dry-run' # Build only, no deployments
- 'stage-only' # Deploy to stores as drafts/candidates, skip Docker
- 'production' # Full production deployment
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 }}
deployment_mode: ${{ steps.deployment_mode.outputs.mode }}
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 deployment mode
id: deployment_mode
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
MODE="${{ inputs.deployment_mode }}"
else
# PR merges to release branch are always production
MODE="production"
fi
# Set individual flags based on mode
if [ "$MODE" = "dry-run" ]; then
DRY_RUN="true"
STAGE_ONLY="false"
elif [ "$MODE" = "stage-only" ]; then
DRY_RUN="false"
STAGE_ONLY="true"
else # production
DRY_RUN="false"
STAGE_ONLY="false"
fi
echo "mode=$MODE" >> $GITHUB_OUTPUT
echo "Deployment mode: $MODE"
- name: Set dry run mode
id: dry_run
run: |
# Extract from deployment mode
MODE="${{ steps.deployment_mode.outputs.mode }}"
if [ "$MODE" = "dry-run" ]; then
DRY_RUN="true"
else
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: |
# Extract from deployment mode
MODE="${{ steps.deployment_mode.outputs.mode }}"
if [ "$MODE" = "stage-only" ]; then
STAGE_ONLY="true"
else
STAGE_ONLY="false"
fi
echo "enabled=$STAGE_ONLY" >> $GITHUB_OUTPUT
echo "Stage only mode: $STAGE_ONLY"
- name: Check all secrets availability
run: |
echo "🎯 DEPLOYMENT MODE: ${{ steps.deployment_mode.outputs.mode }}"
echo "============================"
echo ""
if [ "${{ steps.deployment_mode.outputs.mode }}" = "dry-run" ]; then
echo "🏃 DRY RUN MODE - Building only, no deployments will be made"
elif [ "${{ steps.deployment_mode.outputs.mode }}" = "stage-only" ]; then
echo "📦 STAGE ONLY MODE - Will create drafts/candidates, no production releases"
else
echo "🚀 PRODUCTION MODE - Full deployment to all platforms"
fi
echo ""
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 with mode info
MODE="${{ steps.deployment_mode.outputs.mode }}"
PR_BODY="Manual release deployment (${MODE} mode) 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=docker,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"
elif [ "${{ needs.calculate-version.outputs.stage_only }}" = "true" ]; then
echo "📦 STAGE ONLY MODE - No certificates available"
else
echo "⚠️ PRODUCTION MODE - No certificates available"
fi
echo "Creating dummy pkg for validation..."
mkdir -p desktop-ce
# Create a non-empty dummy file (1 byte minimum for GitHub release)
echo "dummy-package-placeholder" > "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" ] && [ "${{ needs.calculate-version.outputs.stage_only }}" = "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: Check Snapcraft credentials availability
id: check_snap_creds
run: |
if [ -n "${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}" ]; then
echo "has_creds=true" >> $GITHUB_OUTPUT
echo "✅ Snapcraft credentials are configured"
else
echo "has_creds=false" >> $GITHUB_OUTPUT
echo "⚠️ Snapcraft credentials not configured"
if [ "${{ needs.calculate-version.outputs.stage_only }}" = "true" ]; then
echo "Will skip Snap Store upload in stage-only mode"
elif [ "${{ needs.calculate-version.outputs.dry_run }}" = "false" ]; then
echo "::error::Production deployment requires SNAPCRAFT_STORE_CREDENTIALS"
exit 1
fi
fi
- name: Find and list snap files
if: needs.calculate-version.outputs.dry_run == 'false'
id: find_snaps
run: |
echo "Looking for snap files..."
ls -la *.snap || echo "No snap files found"
# Find all snap files and create a list
SNAP_FILES=$(ls *.snap 2>/dev/null | head -1 || echo "")
if [ -z "$SNAP_FILES" ]; then
echo "❌ No snap files found in current directory"
if [ "${{ needs.calculate-version.outputs.stage_only }}" = "true" ]; then
echo "Continuing in stage-only mode..."
echo "snap_found=false" >> $GITHUB_OUTPUT
else
exit 1
fi
else
echo "✅ Found snap file: $SNAP_FILES"
echo "snap_file=$SNAP_FILES" >> $GITHUB_OUTPUT
echo "snap_found=true" >> $GITHUB_OUTPUT
fi
- name: Publish Snaps to Store (Direct Method)
if: needs.calculate-version.outputs.dry_run == 'false' && steps.check_snap_creds.outputs.has_creds == 'true' && steps.find_snaps.outputs.snap_found == 'true'
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
run: |
# Install snapcraft
echo "Installing snapcraft..."
sudo snap install snapcraft --classic
# Determine release channel
if [ "${{ needs.calculate-version.outputs.stage_only }}" = "true" ]; then
CHANNEL="candidate"
echo "📦 Publishing to candidate channel (stage only)"
else
CHANNEL="stable"
echo "📦 Publishing to stable channel"
fi
# Upload AMD64 snap
AMD64_SNAP=$(ls *amd64*.snap 2>/dev/null | head -1 || echo "")
if [ -n "$AMD64_SNAP" ]; then
echo "Found AMD64 snap: $AMD64_SNAP"
echo "Uploading $AMD64_SNAP to $CHANNEL channel..."
snapcraft upload "$AMD64_SNAP" --release "$CHANNEL" || {
echo "❌ Failed to upload AMD64 snap"
if [ "${{ needs.calculate-version.outputs.stage_only }}" = "true" ]; then
echo "Continuing in stage-only mode despite failure..."
else
exit 1
fi
}
else
echo "❌ No AMD64 snap found"
exit 1
fi
- name: Publish ARM64 Snap if exists
if: needs.calculate-version.outputs.dry_run == 'false' && steps.check_snap_creds.outputs.has_creds == 'true'
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
run: |
# Find and upload ARM64 snap if it exists
ARM64_SNAP=$(ls *arm64*.snap 2>/dev/null | head -1 || echo "")
if [ -n "$ARM64_SNAP" ]; then
echo "Found ARM64 snap: $ARM64_SNAP"
# Snapcraft should already be installed from previous step
if ! command -v snapcraft &> /dev/null; then
echo "Installing snapcraft..."
sudo snap install snapcraft --classic
fi
# Determine release channel
if [ "${{ needs.calculate-version.outputs.stage_only }}" = "true" ]; then
CHANNEL="candidate"
echo "📦 Uploading ARM64 to candidate channel"
else
CHANNEL="stable"
echo "📦 Uploading ARM64 to stable channel"
fi
# Upload ARM64 snap
echo "Uploading $ARM64_SNAP to $CHANNEL channel..."
snapcraft upload "$ARM64_SNAP" --release "$CHANNEL" || {
echo "❌ Failed to upload ARM64 snap"
if [ "${{ needs.calculate-version.outputs.stage_only }}" = "true" ]; then
echo "Continuing in stage-only mode despite failure..."
else
exit 1
fi
}
else
echo "No ARM64 snap found, skipping"
fi
- 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 and Submit 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 }}
MS_APP_ID: ${{ secrets.MS_APP_ID }}
run: |
# Install Windows Store CLI tools
Write-Host "Installing StoreBroker module..."
Install-Module -Name StoreBroker -Force -Scope CurrentUser -AllowClobber
Import-Module StoreBroker
# Validate credentials are available
if ([string]::IsNullOrWhiteSpace($env:MS_TENANT_ID) -or
[string]::IsNullOrWhiteSpace($env:MS_CLIENT_ID) -or
[string]::IsNullOrWhiteSpace($env:MS_CLIENT_SECRET)) {
Write-Error "Missing Microsoft Store credentials. Please check MS_TENANT_ID, MS_CLIENT_ID, and MS_CLIENT_SECRET secrets."
exit 1
}
# Authenticate to Partner Center
Write-Host "Authenticating to Microsoft Partner Center..."
Write-Host "Tenant ID: $env:MS_TENANT_ID"
Write-Host "Client ID: $env:MS_CLIENT_ID"
try {
$securePassword = ConvertTo-SecureString $env:MS_CLIENT_SECRET -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($env:MS_CLIENT_ID, $securePassword)
Set-StoreBrokerAuthentication -TenantId $env:MS_TENANT_ID -Credential $credential
Write-Host "✅ Successfully authenticated to Microsoft Partner Center"
}
catch {
Write-Error "Failed to authenticate to Microsoft Partner Center: $_"
exit 1
}
# Now create and submit the app submission
Write-Host "Creating Microsoft Store submission..."
$appId = $env:MS_APP_ID
if ([string]::IsNullOrWhiteSpace($appId)) {
Write-Error "MS_APP_ID is not set or is empty"
exit 1
}
$msixPath = Get-ChildItem -Path "." -Filter "*.msixbundle" | Select-Object -First 1
if ($null -eq $msixPath) {
Write-Error "No MSIX bundle found in current directory"
exit 1
}
Write-Host "Found MSIX bundle: $($msixPath.Name)"
Write-Host "App ID: $appId"
try {
# Test API access first
Write-Host "Testing Partner Center API access..."
try {
# This should work if permissions are correct
$testApps = Get-Applications
Write-Host "✅ Successfully accessed Partner Center API"
Write-Host "Found $($testApps.Count) apps in account"
}
catch {
Write-Host "❌ Cannot access Partner Center API"
Write-Host "Error details: $_"
Write-Host "This usually means:"
Write-Host " 1. The Azure AD app doesn't have Partner Center API permissions"
Write-Host " 2. Admin consent hasn't been granted for the permissions"
Write-Host " 3. The app isn't properly linked in Partner Center"
throw
}
# Create new submission
Write-Host "Creating new submission..."
$submission = New-ApplicationSubmission -AppId $appId -Force
# Update submission with new package
Write-Host "Updating submission with 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
Write-Host "Submitting to store with publish mode: $publishMode"
$submissionId = Submit-ApplicationSubmission -AppId $appId -SubmissionDataPath $submission -TargetPublishMode $publishMode
Write-Host "✅ Submission created successfully 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"
}
}
catch {
Write-Error "Failed to create Microsoft Store submission: $_"
exit 1
}
- 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: |
# Check if credentials are available
if [ -z "$APP_STORE_CONNECT_API_KEY" ] || [ -z "$APP_STORE_CONNECT_API_KEY_ID" ] || [ -z "$APP_STORE_CONNECT_ISSUER_ID" ]; then
echo "⚠️ Apple Store Connect API credentials are not configured"
echo "Skipping API key setup"
exit 0
fi
# 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
echo "✅ Apple Store Connect API key configured"
- 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: |
# Check if credentials are available
if [ -z "$APP_STORE_CONNECT_API_KEY_ID" ] || [ -z "$APP_STORE_CONNECT_ISSUER_ID" ]; then
echo "⚠️ Apple Store Connect API credentials are not configured"
echo "Missing one or more of: APP_STORE_CONNECT_API_KEY_ID, APP_STORE_CONNECT_ISSUER_ID"
if [ "${{ needs.calculate-version.outputs.stage_only }}" = "true" ]; then
echo "Skipping Apple Store upload in stage-only mode due to missing credentials"
echo "To test Apple Store uploads, please configure the following secrets:"
echo " - APP_STORE_CONNECT_API_KEY"
echo " - APP_STORE_CONNECT_API_KEY_ID"
echo " - APP_STORE_CONNECT_ISSUER_ID"
exit 0 # Don't fail in stage-only mode
else
echo "::error::Production deployment requires Apple Store Connect credentials"
exit 1
fi
fi
# Find the .pkg file
PKG_FILE=$(find . -name "*.pkg" -type f | head -1)
if [ -z "$PKG_FILE" ]; then
echo "❌ No .pkg file found in the current directory"
exit 1
fi
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
echo "📦 Preparing release assets..."
# Function to copy only non-empty files
copy_if_not_empty() {
local src="$1"
local dest="$2"
if [ -f "$src" ]; then
size=$(stat -c%s "$src" 2>/dev/null || stat -f%z "$src" 2>/dev/null || echo "0")
if [ "$size" -gt 0 ]; then
echo " ✅ Including $(basename "$src") (${size} bytes)"
cp "$src" "$dest"
else
echo " ⚠️ Skipping $(basename "$src") (empty file)"
fi
fi
}
# Copy Windows MSIX bundle (for users who prefer direct download)
for file in release-assets/windows-msix/*.msixbundle release-assets/windows-msix/*.sig; do
[ -f "$file" ] && copy_if_not_empty "$file" final-assets/
done || true
# Copy Mac App Store package (for users who prefer direct download)
for file in release-assets/mac-app-store/*.pkg release-assets/mac-app-store/*.sig; do
[ -f "$file" ] && copy_if_not_empty "$file" final-assets/
done || true
- name: Generate checksums
working-directory: final-assets
run: |
if [ "$(ls -A .)" ]; then
sha256sum * > SHA256SUMS.txt || true
else
echo "No files to generate checksums for"
echo "No release assets available" > SHA256SUMS.txt
fi
- name: Validate release assets
working-directory: final-assets
run: |
echo "📋 Final release assets:"
echo ""
for file in *; do
if [ -f "$file" ]; then
size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo "0")
if [ "$size" -gt 0 ]; then
echo " ✅ $file (${size} bytes)"
else
echo " ❌ $file is empty - removing"
rm "$file"
fi
fi
done
echo ""
echo "Total files: $(ls -1 | wc -l)"
- 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" ] && [ "${{ needs.calculate-version.outputs.stage_only }}" = "false" ]; then
echo "::error::Production deployment requires Docker Hub credentials"
exit 1
elif [ "${{ needs.calculate-version.outputs.stage_only }}" = "true" ]; then
echo "ℹ️ Docker Hub credentials not required for stage-only mode (Docker push is skipped)"
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 "Deployment Mode: ${{ needs.calculate-version.outputs.deployment_mode }}"
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."