Skip to content

Merge pull request #46 from DavidAmunga/feat/add-topcontacts-sheet #140

Merge pull request #46 from DavidAmunga/feat/add-topcontacts-sheet

Merge pull request #46 from DavidAmunga/feat/add-topcontacts-sheet #140

Workflow file for this run

name: "Main Release"
on:
push:
branches: [main]
paths-ignore:
- "**.md"
- "docs/**"
- ".github/**.md"
- "README.md"
- "CHANGELOG.md"
workflow_dispatch:
inputs:
force_release:
description: "Force a release even if no changesets"
required: false
default: false
type: boolean
release_type:
description: "Type of release"
required: false
default: "auto"
type: choice
options:
- auto
- patch
- minor
- major
concurrency:
group: main-release
cancel-in-progress: false
jobs:
# Step 1: Determine if we should release
check-release:
name: "Check Release Status"
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.check.outputs.should_release }}
version: ${{ steps.check.outputs.version }}
has_changesets: ${{ steps.check.outputs.has_changesets }}
is_changeset_release: ${{ steps.check.outputs.is_changeset_release }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check release conditions
id: check
run: |
COMMIT_MSG=$(git log -1 --pretty=format:"%s")
RECENT_COMMITS=$(git log -10 --pretty=format:"%s")
IS_CHANGESET_RELEASE="false"
if [[ "$COMMIT_MSG" == *"chore: release version packages"* ]] || \
[[ "$COMMIT_MSG" == *"Version Packages"* ]] || \
[[ "$COMMIT_MSG" == *"changeset-release/main"* ]] || \
[[ "$COMMIT_MSG" == *"chore: sync version files"* ]] || \
[[ "$RECENT_COMMITS" == *"chore: release version packages"* ]] || \
[[ "$RECENT_COMMITS" == *"Version Packages"* ]] || \
[[ "$RECENT_COMMITS" == *"chore: sync version files"* ]]; then
IS_CHANGESET_RELEASE="true"
fi
HAS_CHANGESETS="false"
CHANGESET_COUNT=0
if [ -d ".changeset" ]; then
CHANGESET_COUNT=$(find .changeset -name "*.md" -not -name "README.md" -not -name "config.json" 2>/dev/null | wc -l)
if [ "$CHANGESET_COUNT" -gt 0 ]; then
HAS_CHANGESETS="true"
fi
fi
VERSION_CHANGED="false"
if git diff HEAD~1 HEAD --name-only | grep -q "package.json\|src-tauri/Cargo.toml\|src-tauri/tauri.conf.json"; then
VERSION_CHANGED="true"
fi
MANUAL_VERSION_BUMP="false"
if [[ "$COMMIT_MSG" == *"bump version"* ]] || [[ "$COMMIT_MSG" == *"version"* ]] && [[ "$COMMIT_MSG" == *"v"* ]]; then
MANUAL_VERSION_BUMP="true"
fi
SHOULD_RELEASE="false"
RELEASE_REASON=""
if [[ "$IS_CHANGESET_RELEASE" == "true" ]]; then
SHOULD_RELEASE="true"
RELEASE_REASON="changeset release commit"
elif [[ "$HAS_CHANGESETS" == "true" && "$VERSION_CHANGED" == "true" ]]; then
SHOULD_RELEASE="true"
RELEASE_REASON="changesets present and version files updated"
elif [[ "${{ github.event.inputs.force_release }}" == "true" ]]; then
SHOULD_RELEASE="true"
RELEASE_REASON="manual force release"
elif [[ "$HAS_CHANGESETS" == "true" && "${{ github.event_name }}" == "workflow_dispatch" ]]; then
SHOULD_RELEASE="true"
RELEASE_REASON="manual dispatch with pending changesets"
elif [[ "$MANUAL_VERSION_BUMP" == "true" ]]; then
SHOULD_RELEASE="true"
RELEASE_REASON="manual version bump detected"
fi
if [[ "$SHOULD_RELEASE" == "true" ]]; then
echo "✅ Release conditions met: $RELEASE_REASON"
fi
VERSION=$(node -p "require('./package.json').version")
echo "should_release=$SHOULD_RELEASE" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "has_changesets=$HAS_CHANGESETS" >> $GITHUB_OUTPUT
echo "is_changeset_release=$IS_CHANGESET_RELEASE" >> $GITHUB_OUTPUT
echo "release_reason=$RELEASE_REASON" >> $GITHUB_OUTPUT
# Step 2: Pre-release validation
pre-release-validation:
name: "Pre-Release Validation"
runs-on: ubuntu-latest
needs: check-release
if: needs.check-release.outputs.should_release == 'true'
outputs:
validation_passed: ${{ steps.validate.outputs.passed }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install system dependencies for validation
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libglib2.0-dev curl wget unzip
- name: Run comprehensive validation
id: validate
run: |
VALIDATION_PASSED="true"
VALIDATION_ERRORS=""
if ! pnpm audit --audit-level moderate; then
VALIDATION_ERRORS="${VALIDATION_ERRORS}Security vulnerabilities found. "
VALIDATION_PASSED="false"
fi
VERSION=$(node -p "require('./package.json').version")
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
VALIDATION_ERRORS="${VALIDATION_ERRORS}Invalid version format: $VERSION. "
VALIDATION_PASSED="false"
fi
if ! git diff --quiet; then
VALIDATION_ERRORS="${VALIDATION_ERRORS}Uncommitted changes detected. "
VALIDATION_PASSED="false"
fi
if ! node -e "JSON.parse(require('fs').readFileSync('src-tauri/tauri.conf.json', 'utf8'))"; then
VALIDATION_ERRORS="${VALIDATION_ERRORS}Invalid tauri.conf.json. "
VALIDATION_PASSED="false"
fi
if ! pnpm run build; then
VALIDATION_ERRORS="${VALIDATION_ERRORS}Frontend build failed. "
VALIDATION_PASSED="false"
fi
if ! bash src-tauri/scripts/setup-build-jre.sh; then
VALIDATION_ERRORS="${VALIDATION_ERRORS}JRE setup failed. "
VALIDATION_PASSED="false"
fi
cd src-tauri
if ! cargo check --quiet; then
VALIDATION_ERRORS="${VALIDATION_ERRORS}Cargo check failed. "
VALIDATION_PASSED="false"
fi
cd ..
if [[ "$VALIDATION_PASSED" == "true" ]]; then
echo "✅ Validation passed"
else
echo "❌ Validation failed: $VALIDATION_ERRORS"
fi
echo "passed=$VALIDATION_PASSED" >> $GITHUB_OUTPUT
# Step 3: Handle changesets (version bumping)
handle-changesets:
name: "Handle Changesets"
runs-on: ubuntu-latest
needs: [check-release]
if: needs.check-release.outputs.has_changesets == 'true' && needs.check-release.outputs.is_changeset_release == 'false'
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Create Release Pull Request
uses: changesets/action@v1
with:
version: pnpm run version
title: "chore: release version packages"
commit: "chore: release version packages"
createGithubReleases: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Step 4: Sync versions and create tag
prepare-release:
name: "Prepare Release"
runs-on: ubuntu-latest
needs: [check-release, pre-release-validation]
if: needs.check-release.outputs.should_release == 'true' && needs.pre-release-validation.outputs.validation_passed == 'true'
permissions:
contents: write
outputs:
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Sync versions
run: |
pnpm run sync-versions
if ! git diff --quiet; then
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .
git commit -m "chore: sync version files"
git push
fi
- name: Get version and create tag
id: version
run: |
VERSION=$(node -p "require('./package.json').version")
TAG="v$VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=$TAG" >> $GITHUB_OUTPUT
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Clean up any existing draft releases
RELEASE_ID=$(gh release view "$TAG" --json id,isDraft --jq 'select(.isDraft == true) | .id' 2>/dev/null || echo "")
if [ -n "$RELEASE_ID" ]; then
gh release delete "$TAG" --yes 2>/dev/null || true
fi
# Create tag if needed
if ! git tag -l "$TAG" | grep -q "$TAG"; then
if git ls-remote --tags origin | grep -q "refs/tags/$TAG$"; then
git fetch origin "refs/tags/$TAG:refs/tags/$TAG"
else
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"
fi
fi
# Step 5: Build and release desktop
build-and-release:
name: "Build & Release"
needs: [check-release, pre-release-validation, prepare-release]
if: needs.check-release.outputs.should_release == 'true' && needs.pre-release-validation.outputs.validation_passed == 'true'
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: "macos-latest"
args: "--target aarch64-apple-darwin"
target: "aarch64-apple-darwin"
build_name: "macos-aarch64"
- platform: "macos-latest"
args: "--target x86_64-apple-darwin"
target: "x86_64-apple-darwin"
build_name: "macos-x64"
- platform: "ubuntu-22.04"
args: "--target x86_64-unknown-linux-gnu"
target: "x86_64-unknown-linux-gnu"
build_name: "linux-x64"
- platform: "windows-latest"
args: "--target x86_64-pc-windows-msvc"
target: "x86_64-pc-windows-msvc"
build_name: "windows-x64"
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.prepare-release.outputs.tag }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install system dependencies (Ubuntu)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libappindicator3-dev \
librsvg2-dev \
patchelf \
curl \
wget \
unzip \
libfuse2 \
file \
libglib2.0-dev
# Configure AppImage environment for CI
# AppImages need FUSE but GitHub Actions doesn't have it by default
# We'll extract the AppImage instead of running it with FUSE
echo "APPIMAGE_EXTRACT_AND_RUN=1" >> $GITHUB_ENV
- name: Install create-dmg (macOS)
if: matrix.platform == 'macos-latest'
run: |
brew install create-dmg
- name: Import Apple Code Signing Certificate
if: matrix.platform == 'macos-latest'
run: |
# Create keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
# Import certificate
echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
# Clean up
rm certificate.p12
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Cache pnpm dependencies
if: matrix.platform != 'windows-latest'
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Cache Rust dependencies
if: matrix.platform != 'windows-latest'
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-${{ matrix.target }}-cargo-
${{ runner.os }}-cargo-
- name: Cache JRE for Tabula
uses: actions/cache@v4
with:
path: |
src-tauri/resources/build-jre
key: ${{ runner.os }}-${{ matrix.platform == 'macos-latest' && 'macos-universal' || matrix.target }}-jre-v4-${{ hashFiles('src-tauri/scripts/setup-build-jre.sh') }}
restore-keys: |
${{ runner.os }}-${{ matrix.platform == 'macos-latest' && 'macos-universal' || matrix.target }}-jre-v4-
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Setup JRE for Tabula
shell: bash
run: |
# For macOS, download both architectures since we build universal
if [[ "${{ matrix.platform }}" == "macos-latest" ]]; then
echo "📦 Setting up JREs for macOS (both architectures)..."
# Download ARM64 JRE
echo "⬇️ Downloading ARM64 JRE..."
TARGET_PLATFORM=jre-macos-arm64 bash src-tauri/scripts/setup-build-jre.sh
# Download x86_64 JRE
echo "⬇️ Downloading x86_64 JRE..."
TARGET_PLATFORM=jre-macos-x64 bash src-tauri/scripts/setup-build-jre.sh
echo "✅ Both JREs downloaded successfully"
else
# For other platforms, use default detection
# The script will automatically configure Linux JRE for AppImage bundling
bash src-tauri/scripts/setup-build-jre.sh
fi
- name: Verify JRE Setup
shell: bash
run: |
echo "🔍 Verifying JRE setup..."
if [[ "${{ matrix.platform }}" == "windows-latest" ]]; then
JRE_DIR="src-tauri/resources/build-jre/jre-windows-x64"
JAVA_EXE="$JRE_DIR/bin/java.exe"
elif [[ "${{ matrix.platform }}" == "ubuntu-22.04" ]]; then
JRE_DIR="src-tauri/resources/build-jre/jre-linux-x64"
JAVA_EXE="$JRE_DIR/bin/java"
elif [[ "${{ matrix.platform }}" == "macos-latest" ]]; then
# Check both macOS JREs
for arch in arm64 x64; do
JRE_DIR="src-tauri/resources/build-jre/jre-macos-$arch"
if [ -d "$JRE_DIR" ]; then
echo "✅ Found macOS $arch JRE at: $JRE_DIR"
echo " Size: $(du -sh "$JRE_DIR" | cut -f1)"
if [ -f "$JRE_DIR/bin/java" ]; then
echo "✅ java binary exists"
"$JRE_DIR/bin/java" -version 2>&1 || echo "⚠️ Could not run java"
else
echo "❌ java binary NOT found at: $JRE_DIR/bin/java"
fi
echo ""
else
echo "❌ macOS $arch JRE NOT found at: $JRE_DIR"
fi
done
exit 0
fi
echo "Checking JRE directory: $JRE_DIR"
if [ -d "$JRE_DIR" ]; then
echo "✅ JRE directory exists"
echo " Size: $(du -sh "$JRE_DIR" | cut -f1)"
echo ""
if [ -d "$JRE_DIR/bin" ]; then
echo "✅ bin directory exists"
echo " Contents of bin directory (first 20 files):"
ls -lh "$JRE_DIR/bin" | head -20
else
echo "❌ bin directory NOT found"
echo " Contents of JRE directory:"
ls -lh "$JRE_DIR"
fi
echo ""
if [ -f "$JAVA_EXE" ]; then
echo "✅ Java executable exists at: $JAVA_EXE"
echo " Testing Java execution:"
"$JAVA_EXE" -version 2>&1 || echo "⚠️ Java execution failed"
else
echo "❌ Java executable NOT found at: $JAVA_EXE"
fi
else
echo "❌ JRE directory NOT found: $JRE_DIR"
echo ""
echo "Contents of build-jre:"
ls -lh src-tauri/resources/build-jre/ || echo "build-jre directory doesn't exist"
fi
- name: Setup AppImage build tools (Ubuntu)
if: matrix.platform == 'ubuntu-22.04'
shell: bash
run: |
echo "🔧 Setting up AppImage build tools for CI environment..."
# Create tools directory
mkdir -p ~/.local/bin
export PATH="$HOME/.local/bin:$PATH"
# Pre-download and extract linuxdeploy (so it doesn't need FUSE)
echo "⬇️ Downloading linuxdeploy..."
cd ~/.local/bin
# Download linuxdeploy
wget -q https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy/linuxdeploy-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage
# Extract it so it doesn't need FUSE
./linuxdeploy-x86_64.AppImage --appimage-extract >/dev/null 2>&1 || true
if [ -d "squashfs-root" ]; then
mv squashfs-root linuxdeploy-extracted
# Create a wrapper script
cat > linuxdeploy-x86_64.AppImage << 'EOF'
#!/bin/bash
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "$DIR/linuxdeploy-extracted/AppRun" "$@"
EOF
chmod +x linuxdeploy-x86_64.AppImage
echo "✅ linuxdeploy extracted and ready"
fi
# Download linuxdeploy-plugin-appimage
echo "⬇️ Downloading linuxdeploy-plugin-appimage..."
wget -q https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage
chmod +x linuxdeploy-plugin-appimage-x86_64.AppImage
# Extract it
./linuxdeploy-plugin-appimage-x86_64.AppImage --appimage-extract >/dev/null 2>&1 || true
if [ -d "squashfs-root" ]; then
mv squashfs-root linuxdeploy-plugin-appimage-extracted
# Create a wrapper script
cat > linuxdeploy-plugin-appimage-x86_64.AppImage << 'EOF'
#!/bin/bash
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export APPIMAGE_EXTRACT_AND_RUN=1
exec "$DIR/linuxdeploy-plugin-appimage-extracted/AppRun" "$@"
EOF
chmod +x linuxdeploy-plugin-appimage-x86_64.AppImage
echo "✅ linuxdeploy-plugin-appimage extracted and ready"
fi
# Download GTK and GStreamer plugins (these are shell scripts, not AppImages)
echo "⬇️ Downloading GTK plugin..."
wget -q https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh
chmod +x linuxdeploy-plugin-gtk.sh
echo "⬇️ Downloading GStreamer plugin..."
wget -q https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gstreamer/master/linuxdeploy-plugin-gstreamer.sh
chmod +x linuxdeploy-plugin-gstreamer.sh
echo "✅ All plugins downloaded"
# Add to PATH for the rest of the workflow
echo "$HOME/.local/bin" >> $GITHUB_PATH
# Return to workspace
cd "$GITHUB_WORKSPACE"
# Verify JRE is set up
echo ""
echo "🔍 Verifying build environment..."
if [ -d "src-tauri/resources/build-jre" ]; then
echo "✅ JRE directory exists"
echo "JRE size: $(du -sh src-tauri/resources/build-jre | cut -f1)"
else
echo "⚠️ JRE directory not found"
fi
# List what's in PATH
echo ""
echo "Tools in ~/.local/bin:"
ls -lh ~/.local/bin/ | grep -E "linuxdeploy|AppImage" || echo "No tools found"
echo "✅ AppImage build tools setup complete"
- name: Extract changelog for release
id: changelog
shell: bash
continue-on-error: false
run: |
set +e # Disable exit on error for this script
VERSION="${{ needs.prepare-release.outputs.version }}"
if [ -f "CHANGELOG.md" ]; then
# Extract the changelog section for the current version
CHANGELOG_CONTENT=$(awk -v version="$VERSION" '
/^## / {
if (found) exit
if ($0 ~ "## .*" version) found=1
next
}
found && /^## / { exit }
found { print }
' CHANGELOG.md | sed '/^$/d' | head -30)
# Get previous version for comparison link
PREVIOUS_VERSION=$(awk '/^## / && !/'"$VERSION"'/ { print $2; exit }' CHANGELOG.md)
if [ -n "$CHANGELOG_CONTENT" ]; then
# Analyze change types - handle both "feat:" and "hash: feat:" formats
FEATURES=$(echo "$CHANGELOG_CONTENT" | grep -E "(^- [a-f0-9]+: )?feat:" | wc -l | tr -d ' ')
FIXES=$(echo "$CHANGELOG_CONTENT" | grep -E "(^- [a-f0-9]+: )?fix:" | wc -l | tr -d ' ')
BREAKING=$(echo "$CHANGELOG_CONTENT" | grep -E "BREAKING CHANGE|!" | wc -l | tr -d ' ')
DOCS=$(echo "$CHANGELOG_CONTENT" | grep -E "(^- [a-f0-9]+: )?docs:" | wc -l | tr -d ' ')
PERF=$(echo "$CHANGELOG_CONTENT" | grep -E "(^- [a-f0-9]+: )?perf:" | wc -l | tr -d ' ')
REFACTOR=$(echo "$CHANGELOG_CONTENT" | grep -E "(^- [a-f0-9]+: )?refactor:" | wc -l | tr -d ' ')
CHORE=$(echo "$CHANGELOG_CONTENT" | grep -E "(^- [a-f0-9]+: )?chore:" | wc -l | tr -d ' ')
SECURITY=$(echo "$CHANGELOG_CONTENT" | grep -i "security\|vulnerability\|cve\|exploit" | wc -l | tr -d ' ')
# Count total changes
TOTAL_CHANGES=$(echo "$CHANGELOG_CONTENT" | grep "^-" | wc -l | tr -d ' ')
FORMATTED_CHANGELOG="$CHANGELOG_CONTENT"
# Sanitize: remove all control characters except newline, escape backslashes and quotes
FORMATTED_CHANGELOG=$(printf '%s' "$FORMATTED_CHANGELOG" | LC_ALL=C tr -d '\000-\011\013-\037\177' | sed 's/\\/\\\\/g' | sed 's/"/\\"/g')
echo "changelog<<EOF" >> $GITHUB_OUTPUT
printf '%s\n' "$FORMATTED_CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Output change statistics
echo "features=$FEATURES" >> $GITHUB_OUTPUT
echo "fixes=$FIXES" >> $GITHUB_OUTPUT
echo "breaking=$BREAKING" >> $GITHUB_OUTPUT
echo "docs=$DOCS" >> $GITHUB_OUTPUT
echo "perf=$PERF" >> $GITHUB_OUTPUT
echo "refactor=$REFACTOR" >> $GITHUB_OUTPUT
echo "security=$SECURITY" >> $GITHUB_OUTPUT
echo "total_changes=$TOTAL_CHANGES" >> $GITHUB_OUTPUT
else
echo "changelog=See CHANGELOG.md for details." >> $GITHUB_OUTPUT
echo "features=0" >> $GITHUB_OUTPUT
echo "fixes=0" >> $GITHUB_OUTPUT
echo "breaking=0" >> $GITHUB_OUTPUT
echo "security=0" >> $GITHUB_OUTPUT
echo "total_changes=0" >> $GITHUB_OUTPUT
fi
echo "previous_version=$PREVIOUS_VERSION" >> $GITHUB_OUTPUT
else
echo "changelog=No changelog available." >> $GITHUB_OUTPUT
echo "previous_version=0.0.2" >> $GITHUB_OUTPUT
echo "features=0" >> $GITHUB_OUTPUT
echo "fixes=0" >> $GITHUB_OUTPUT
echo "breaking=0" >> $GITHUB_OUTPUT
echo "security=0" >> $GITHUB_OUTPUT
echo "total_changes=0" >> $GITHUB_OUTPUT
fi
- name: Generate build timestamp
id: timestamp
run: echo "build_time=$(date -u +"%Y-%m-%d %H:%M UTC")" >> $GITHUB_OUTPUT
- name: Build Tauri app
id: tauri-build
continue-on-error: true
shell: bash
run: |
echo "🔨 Building Tauri app for ${{ matrix.build_name }}..."
# Clean previous build artifacts to avoid version conflicts
echo "🧹 Cleaning previous build artifacts..."
rm -rf dist src-tauri/target/release/bundle
echo "✅ Build directory cleaned"
# Configure LD_LIBRARY_PATH for Linux to help linuxdeploy find JRE libraries
if [[ "${{ matrix.platform }}" == "ubuntu-22.04" ]]; then
JRE_DIRS=$(find src-tauri/resources/build-jre -type d -name "lib" 2>/dev/null || echo "")
if [ -n "$JRE_DIRS" ]; then
for JRE_LIB in $JRE_DIRS; do
if [ -d "$JRE_LIB/server" ]; then
export LD_LIBRARY_PATH="$PWD/$JRE_LIB/server:${LD_LIBRARY_PATH}"
fi
export LD_LIBRARY_PATH="$PWD/$JRE_LIB:${LD_LIBRARY_PATH}"
done
fi
fi
# Build with retry logic
set +e
pnpm tauri build ${{ matrix.args }} --verbose
BUILD_EXIT_CODE=$?
set -e
if [[ $BUILD_EXIT_CODE -ne 0 ]]; then
echo "❌ First build attempt failed (exit code: $BUILD_EXIT_CODE)"
echo "🔄 Attempting clean build..."
# Clean and retry
rm -rf dist src-tauri/target/release
pnpm tauri build ${{ matrix.args }} --verbose
BUILD_EXIT_CODE=$?
if [[ $BUILD_EXIT_CODE -ne 0 ]]; then
echo "❌ Build failed after retry (exit code: $BUILD_EXIT_CODE)"
echo "build_success=false" >> $GITHUB_OUTPUT
exit 1
fi
fi
echo "✅ Tauri build completed successfully"
echo "build_success=true" >> $GITHUB_OUTPUT
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TAURI_BUNDLE_MACOS_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CI: true
- name: Create GitHub Release
if: steps.tauri-build.outputs.build_success == 'true'
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.prepare-release.outputs.tag }}
name: "mpesa2csv ${{ needs.prepare-release.outputs.tag }}"
body: |
## What's New in v${{ needs.prepare-release.outputs.version }}
${{ steps.changelog.outputs.total_changes > 0 && format('📊 **Release Summary**: {0} changes', steps.changelog.outputs.total_changes) || '' }}
${{ steps.changelog.outputs.breaking > 0 && '> ⚠️ **BREAKING CHANGES**: This release contains breaking changes. Please review the changelog carefully before updating.' || '' }}
${{ steps.changelog.outputs.security > 0 && '> 🔒 **SECURITY UPDATE**: This release includes important security fixes. We recommend updating as soon as possible.' || '' }}
${{ steps.changelog.outputs.changelog }}
${{ steps.changelog.outputs.features > 0 && steps.changelog.outputs.fixes > 0 && format('🎯 **Highlights**: This release brings {0} new features and fixes {1} bugs for a better user experience.', steps.changelog.outputs.features, steps.changelog.outputs.fixes) || '' }}
${{ steps.changelog.outputs.features > 0 && steps.changelog.outputs.fixes == 0 && format('✨ **Feature Release**: Introducing {0} new features to enhance your workflow.', steps.changelog.outputs.features) || '' }}
${{ steps.changelog.outputs.features == 0 && steps.changelog.outputs.fixes > 0 && format('🔧 **Maintenance Release**: This update focuses on stability with {0} bug fixes.', steps.changelog.outputs.fixes) || '' }}
---
## 📥 Download & Installation
Choose the appropriate download for your platform:
| Platform | Architecture | Download | Installation |
|----------|-------------|----------|--------------|
| **Windows** | x64 (Intel/AMD) | [📥 Download .exe](https://github.com/DavidAmunga/mpesa2csv/releases/download/${{ needs.prepare-release.outputs.tag }}/mpesa2csv_${{ needs.prepare-release.outputs.version }}_x64-setup.exe) | Run the installer and follow the setup wizard |
| **macOS** | Apple Silicon (M1/M2/M3) | [📥 Download .dmg](https://github.com/DavidAmunga/mpesa2csv/releases/download/${{ needs.prepare-release.outputs.tag }}/mpesa2csv_${{ needs.prepare-release.outputs.version }}_aarch64.dmg) | Open DMG and drag to Applications |
| **macOS** | Intel x64 | [📥 Download .dmg](https://github.com/DavidAmunga/mpesa2csv/releases/download/${{ needs.prepare-release.outputs.tag }}/mpesa2csv_${{ needs.prepare-release.outputs.version }}_x64.dmg) | Open DMG and drag to Applications |
| **Linux** | x64 (Ubuntu/Debian) | [📥 Download .deb](https://github.com/DavidAmunga/mpesa2csv/releases/download/${{ needs.prepare-release.outputs.tag }}/mpesa2csv_${{ needs.prepare-release.outputs.version }}_amd64.deb) | `sudo dpkg -i mpesa2csv_*.deb` |
| **Linux** | x64 (Portable) | [📥 Download .AppImage](https://github.com/DavidAmunga/mpesa2csv/releases/download/${{ needs.prepare-release.outputs.tag }}/mpesa2csv_${{ needs.prepare-release.outputs.version }}_amd64.AppImage) | `chmod +x mpesa2csv_*.AppImage && ./mpesa2csv_*.AppImage` |
### 📊 Release Metadata
- **Version**: `${{ needs.prepare-release.outputs.version }}`
- **Build Date**: `${{ steps.timestamp.outputs.build_time }}`
- **Total Changes**: ${{ steps.changelog.outputs.total_changes }}
- **Platforms**: Windows, macOS (Intel + Apple Silicon), Linux
${{ steps.changelog.outputs.breaking > 0 && format('- **Breaking Changes**: {0}', steps.changelog.outputs.breaking) || '' }}
### 🔍 System Requirements
- **Windows**: Windows 10 version 1903 or later
- **macOS**: macOS 10.15 Catalina or later
- **Linux**: Modern distribution with GTK 3.24+ and WebKit2GTK 4.1+
### 🔄 Auto-Update
> **Note**: If you have a previous version installed, the app will automatically notify you when this update is available and guide you through the update process.
---
**Full Changelog**: https://github.com/${{ github.repository }}/compare/v${{ steps.changelog.outputs.previous_version || '0.0.2' }}...v${{ needs.prepare-release.outputs.version }}
draft: false
prerelease: false
files: |
src-tauri/target/*/release/bundle/dmg/*.dmg
src-tauri/target/*/release/bundle/deb/*.deb
src-tauri/target/*/release/bundle/appimage/*.AppImage
src-tauri/target/*/release/bundle/nsis/*.exe
src-tauri/target/*/release/bundle/msi/*.msi
src-tauri/target/*/release/bundle/macos/*.app.tar.gz
src-tauri/target/*/release/bundle/appimage/*.AppImage.tar.gz
src-tauri/target/*/release/bundle/nsis/*.nsis.zip
src-tauri/target/*/release/bundle/**/*.sig
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Step 6: Generate and upload updater JSON
generate-updater-json:
name: "Generate Updater JSON"
needs:
[
check-release,
pre-release-validation,
prepare-release,
build-and-release,
]
if: needs.check-release.outputs.should_release == 'true' && needs.pre-release-validation.outputs.validation_passed == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Generate and upload latest.json
run: |
echo "🔄 Generating latest.json for Tauri updater..."
VERSION="${{ needs.prepare-release.outputs.version }}"
TAG="${{ needs.prepare-release.outputs.tag }}"
# Create the latest.json file with proper Tauri updater format
# Try to get release notes from the GitHub API
RELEASE_NOTES="Update to version $VERSION. See full release notes at https://github.com/${{ github.repository }}/releases/tag/$TAG"
# Try to fetch release notes from GitHub API
if command -v gh &> /dev/null; then
GITHUB_NOTES=$(gh api repos/${{ github.repository }}/releases/latest --jq '.body' 2>/dev/null || echo "")
if [ -n "$GITHUB_NOTES" ] && [ "$GITHUB_NOTES" != "null" ]; then
RELEASE_NOTES=$(printf '%s' "$GITHUB_NOTES" | head -20 | sed 's/^## /## /g' | sed 's/^### /### /g' | LC_ALL=C tr -d '\000-\011\013-\037\177')
fi
fi
# Wait for GitHub to process release assets
sleep 10
# Download signature files from the release
mkdir -p signatures
for sig_file in "mpesa2csv.app.tar.gz.sig" "mpesa2csv_${VERSION}_amd64.AppImage.sig" "mpesa2csv_${VERSION}_x64-setup.exe.sig"; do
gh release download "$TAG" -p "$sig_file" -D signatures 2>/dev/null || echo "" > "signatures/$sig_file"
done
# Read signature files
# Tauri v2 uses updater bundle signatures, not installer signatures
DARWIN_SIG=$(cat signatures/mpesa2csv.app.tar.gz.sig 2>/dev/null || echo "")
LINUX_X86_64_SIG=$(cat signatures/mpesa2csv_${VERSION}_amd64.AppImage.sig 2>/dev/null || echo "")
WINDOWS_X86_64_SIG=$(cat signatures/mpesa2csv_${VERSION}_x64-setup.exe.sig 2>/dev/null || echo "")
# Use jq to properly create JSON with escaped strings
# URLs point to updater bundles (.tar.gz, .zip), not installers
jq -n \
--arg version "$VERSION" \
--arg notes "$RELEASE_NOTES" \
--arg pub_date "$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")" \
--arg darwin_aarch64_url "https://github.com/${{ github.repository }}/releases/download/$TAG/mpesa2csv.app.tar.gz" \
--arg darwin_aarch64_sig "$DARWIN_SIG" \
--arg darwin_x86_64_url "https://github.com/${{ github.repository }}/releases/download/$TAG/mpesa2csv.app.tar.gz" \
--arg darwin_x86_64_sig "$DARWIN_SIG" \
--arg linux_x86_64_url "https://github.com/${{ github.repository }}/releases/download/$TAG/mpesa2csv_${VERSION}_amd64.AppImage" \
--arg linux_x86_64_sig "$LINUX_X86_64_SIG" \
--arg windows_x86_64_url "https://github.com/${{ github.repository }}/releases/download/$TAG/mpesa2csv_${VERSION}_x64-setup.exe" \
--arg windows_x86_64_sig "$WINDOWS_X86_64_SIG" \
'{
version: $version,
notes: $notes,
pub_date: $pub_date,
platforms: {
"darwin-aarch64": {
signature: $darwin_aarch64_sig,
url: $darwin_aarch64_url
},
"darwin-x86_64": {
signature: $darwin_x86_64_sig,
url: $darwin_x86_64_url
},
"linux-x86_64": {
signature: $linux_x86_64_sig,
url: $linux_x86_64_url
},
"windows-x86_64": {
signature: $windows_x86_64_sig,
url: $windows_x86_64_url
}
}
}' > latest.json
# Upload to the release
gh release upload "$TAG" latest.json --clobber
echo "✅ latest.json uploaded"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Step 7: Create release branch for hotfixes
create-release-branch:
name: "Create Release Branch"
needs:
[
check-release,
pre-release-validation,
prepare-release,
build-and-release,
generate-updater-json,
]
if: needs.check-release.outputs.should_release == 'true' && needs.pre-release-validation.outputs.validation_passed == 'true'
uses: ./.github/workflows/reusable-create-release-branch.yml
with:
version: ${{ needs.prepare-release.outputs.tag }}
max_branches: 5
secrets: inherit
# Step 8: Notify about release completion
notify-completion:
name: "Release Complete"
runs-on: ubuntu-latest
needs:
[
pre-release-validation,
prepare-release,
build-and-release,
generate-updater-json,
create-release-branch,
]
if: always() && needs.prepare-release.result == 'success'
steps:
- name: Release summary
run: |
echo "🎉 Release ${{ needs.prepare-release.outputs.tag }} completed!"
echo "✅ Desktop binaries built and published"
echo "✅ Release branch created for hotfixes"
echo "🔗 View release: https://github.com/${{ github.repository }}/releases/tag/${{ needs.prepare-release.outputs.tag }}"
# Create a comprehensive release summary
cat > release-summary.md << EOF
# 🚀 Release ${{ needs.prepare-release.outputs.tag }} Summary
## ✅ Completed Tasks
- [x] Pre-release validation passed
- [x] Version files synced
- [x] Git tag created: ${{ needs.prepare-release.outputs.tag }}
- [x] Desktop binaries built for all platforms
- [x] GitHub release created
- [x] Updater JSON generated
- [x] Release branch created for hotfixes
## 📊 Build Status
- **Release Reason**: ${{ needs.check-release.outputs.release_reason }}
- **Validation**: ✅ Passed
- **Desktop Builds**: ✅ Success
- **Release Creation**: ✅ Success
## 🔗 Links
- **Release**: https://github.com/${{ github.repository }}/releases/tag/${{ needs.prepare-release.outputs.tag }}
- **Release Branch**: release/${{ needs.prepare-release.outputs.version }}
- **Updater JSON**: https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare-release.outputs.tag }}/latest.json
## 📱 Available Downloads
- Windows x64: [Download .exe](https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare-release.outputs.tag }}/mpesa2csv_${{ needs.prepare-release.outputs.version }}_x64-setup.exe)
- macOS Apple Silicon: [Download .dmg](https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare-release.outputs.tag }}/mpesa2csv_${{ needs.prepare-release.outputs.version }}_aarch64.dmg)
- macOS Intel: [Download .dmg](https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare-release.outputs.tag }}/mpesa2csv_${{ needs.prepare-release.outputs.version }}_x64.dmg)
- Linux x64: [Download .deb](https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare-release.outputs.tag }}/mpesa2csv_${{ needs.prepare-release.outputs.version }}_amd64.deb)
- Linux Portable: [Download .AppImage](https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare-release.outputs.tag }}/mpesa2csv_${{ needs.prepare-release.outputs.version }}_amd64.AppImage)
---
**Release completed at**: $(date -u '+%Y-%m-%d %H:%M:%S UTC')
EOF
echo "📄 Release summary created"
cat release-summary.md