Build, Stage, and Deploy Release #20
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 | |
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 | |
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 }} | |
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: 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" | |
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: make ${{ matrix.make-target }} VERSION=${{ needs.calculate-version.outputs.version }} | |
- 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: 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 ] | |
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: Skip Mac App Store packaging in dry run | |
if: needs.calculate-version.outputs.dry_run == 'true' | |
run: | | |
echo "🏃 DRY RUN MODE - Skipping Mac App Store packaging (requires certificates)" | |
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: needs.calculate-version.outputs.dry_run == 'false' | |
working-directory: ./desktop-ce | |
env: | |
MAS_CODESIGN_ID: ${{ secrets.MAS_CODESIGN_ID }} | |
MAS_INSTALLER_ID: ${{ secrets.MAS_INSTALLER_ID }} | |
run: | | |
# 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 ] | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- 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: Skip MSIX packaging in dry run | |
if: needs.calculate-version.outputs.dry_run == 'true' | |
shell: pwsh | |
run: | | |
echo "🏃 DRY RUN MODE - Skipping MSIX packaging (requires certificate)" | |
echo "Creating dummy MSIX bundle for validation..." | |
# Create dummy file in the same location where the actual build would place it | |
New-Item -ItemType Directory -Force -Path "." | Out-Null | |
New-Item -ItemType File -Path "./WhoDB-${{ needs.calculate-version.outputs.version }}.msixbundle" -Force | |
- name: Decode certificate | |
if: needs.calculate-version.outputs.dry_run == 'false' | |
shell: pwsh | |
run: | | |
$bytes = [Convert]::FromBase64String("${{ secrets.WINDOWS_PFX_BASE64 }}") | |
[IO.File]::WriteAllBytes("cert.pfx", $bytes) | |
- name: Build MSIX amd64 | |
if: needs.calculate-version.outputs.dry_run == 'false' | |
shell: pwsh | |
env: | |
WINDOWS_PFX_PASSWORD: ${{ secrets.WINDOWS_PFX_PASSWORD }} | |
run: | | |
.\scripts\build-msix.ps1 -Architecture amd64 -Version ${{ needs.calculate-version.outputs.version }} -PublisherCN "${{ secrets.WINDOWS_PUBLISHER_CN }}" -CertPath cert.pfx | |
# 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 package (AMD64 only) | |
if: needs.calculate-version.outputs.dry_run == 'false' | |
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 ] | |
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 ] | |
# 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 ] | |
steps: | |
- 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 ] | |
steps: | |
- name: Checkout for scripts | |
uses: actions/checkout@v4 | |
- 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: | | |
# Snap Store automatically handles multi-arch packages when uploaded separately | |
for snap in *.snap; do | |
echo "Publishing $snap to Snap Store..." | |
snapcraft upload "$snap" --release=stable | |
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:" | |
echo " - Snap packages (amd64 and arm64) to stable channel" | |
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' | |
steps: | |
- name: Checkout for scripts | |
uses: actions/checkout@v4 | |
- 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 | |
# Submit to Store (as draft initially) | |
$submissionId = Submit-ApplicationSubmission -AppId $appId -SubmissionDataPath $submission -TargetPublishMode Immediate | |
Write-Host "Submission created with ID: $submissionId" | |
Write-Host "Check Partner Center to review and publish the submission" | |
- 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' | |
steps: | |
- name: Checkout for scripts | |
uses: actions/checkout@v4 | |
- 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) | |
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" | |
echo "📝 Review and submit for review in App Store Connect" | |
- 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: | | |
echo "Package is now available in App Store Connect for TestFlight distribution" | |
echo "Manual review and release required via App Store Connect dashboard" | |
- 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 ] | |
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: Release v${{ needs.calculate-version.outputs.version }} | |
body: | | |
# WhoDB v${{ needs.calculate-version.outputs.version }} | |
## 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 | |
## Release Notes | |
[TODO: Add release notes here] | |
## 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: false | |
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 }}" | |
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 ] | |
steps: | |
- name: Checkout for scripts | |
uses: actions/checkout@v4 | |
- 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' | |
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' | |
run: | | |
cosign sign --yes clidey/whodb:${{ needs.calculate-version.outputs.version }} | |
cosign sign --yes 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, 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 }} | |
WINDOWS_TENANT_ID: ${{ secrets.WINDOWS_TENANT_ID }} | |
WINDOWS_CLIENT_ID: ${{ secrets.WINDOWS_CLIENT_ID }} | |
WINDOWS_CLIENT_SECRET: ${{ secrets.WINDOWS_CLIENT_SECRET }} | |
WINDOWS_APP_ID: ${{ secrets.WINDOWS_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' | |
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" |