Main Release #152
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: "Main Release" | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| skip_validation: | |
| description: "Skip pre-release validation (not recommended)" | |
| required: false | |
| default: false | |
| type: boolean | |
| concurrency: | |
| group: main-release | |
| cancel-in-progress: false | |
| jobs: | |
| # Step 1: Get version information | |
| check-release: | |
| name: "Get Release Info" | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| tag: ${{ steps.version.outputs.tag }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: lts/* | |
| - name: Get version | |
| id: version | |
| run: | | |
| VERSION=$(node -p "require('./package.json').version") | |
| TAG="v$VERSION" | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "tag=$TAG" >> $GITHUB_OUTPUT | |
| echo "📦 Preparing release for version: $VERSION" | |
| echo "🏷️ Tag: $TAG" | |
| # Check if tag already exists | |
| if git ls-remote --tags origin | grep -q "refs/tags/$TAG$"; then | |
| echo "⚠️ Warning: Tag $TAG already exists" | |
| echo " This release will update the existing release" | |
| fi | |
| # Step 2: Pre-release validation | |
| pre-release-validation: | |
| name: "Pre-Release Validation" | |
| runs-on: ubuntu-latest | |
| needs: check-release | |
| if: github.event.inputs.skip_validation != '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: Sync versions and create tag | |
| prepare-release: | |
| name: "Prepare Release" | |
| runs-on: ubuntu-latest | |
| needs: [check-release, pre-release-validation] | |
| if: always() && needs.check-release.result == 'success' && (needs.pre-release-validation.result == 'success' || needs.pre-release-validation.result == 'skipped') | |
| permissions: | |
| contents: write | |
| outputs: | |
| version: ${{ needs.check-release.outputs.version }} | |
| tag: ${{ needs.check-release.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: Create or update tag | |
| run: | | |
| VERSION="${{ needs.check-release.outputs.version }}" | |
| TAG="${{ needs.check-release.outputs.tag }}" | |
| 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 | |
| echo "🗑️ Cleaning up draft release for $TAG" | |
| 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 | |
| echo "⬇️ Fetching existing tag $TAG" | |
| git fetch origin "refs/tags/$TAG:refs/tags/$TAG" | |
| else | |
| echo "🏷️ Creating new tag $TAG" | |
| git tag -a "$TAG" -m "Release $TAG" | |
| git push origin "$TAG" | |
| fi | |
| else | |
| echo "✅ Tag $TAG already exists locally" | |
| fi | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Step 4: Build and release desktop | |
| build-and-release: | |
| name: "Build & Release" | |
| needs: [check-release, pre-release-validation, prepare-release] | |
| if: always() && needs.check-release.result == 'success' && needs.prepare-release.result == 'success' && (needs.pre-release-validation.result == 'success' || needs.pre-release-validation.result == 'skipped') | |
| 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.target }}-jre-v5-${{ hashFiles('src-tauri/scripts/setup-build-jre.sh') }} | |
| restore-keys: | | |
| ${{ runner.os }}-${{ matrix.target }}-jre-v5- | |
| - name: Install frontend dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Setup JRE for Tabula | |
| shell: bash | |
| run: | | |
| # For macOS, download only the architecture we're building for | |
| if [[ "${{ matrix.platform }}" == "macos-latest" ]]; then | |
| if [[ "${{ matrix.target }}" == "aarch64-apple-darwin" ]]; then | |
| echo "📦 Setting up ARM64 JRE for Apple Silicon build..." | |
| TARGET_PLATFORM=jre-macos-arm64 bash src-tauri/scripts/setup-build-jre.sh | |
| elif [[ "${{ matrix.target }}" == "x86_64-apple-darwin" ]]; then | |
| echo "📦 Setting up x64 JRE for Intel build..." | |
| TARGET_PLATFORM=jre-macos-x64 bash src-tauri/scripts/setup-build-jre.sh | |
| fi | |
| 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 only the JRE for the architecture we're building | |
| if [[ "${{ matrix.target }}" == "aarch64-apple-darwin" ]]; then | |
| JRE_DIR="src-tauri/resources/build-jre/jre-macos-arm64" | |
| else | |
| JRE_DIR="src-tauri/resources/build-jre/jre-macos-x64" | |
| fi | |
| JAVA_EXE="$JRE_DIR/bin/java" | |
| 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: Verify JRE bundling (Windows) | |
| if: matrix.platform == 'windows-latest' && steps.tauri-build.outputs.build_success == 'true' | |
| shell: bash | |
| run: | | |
| echo "🔍 Verifying JRE is bundled in Windows installers..." | |
| # Check NSIS installer | |
| NSIS_INSTALLER=$(find src-tauri/target/*/release/bundle/nsis -name "*.exe" 2>/dev/null | head -1) | |
| if [ -n "$NSIS_INSTALLER" ]; then | |
| echo "Found NSIS installer: $NSIS_INSTALLER" | |
| # Extract and check contents (basic size check) | |
| INSTALLER_SIZE=$(stat -c%s "$NSIS_INSTALLER" 2>/dev/null || stat -f%z "$NSIS_INSTALLER" 2>/dev/null || echo "0") | |
| INSTALLER_SIZE_MB=$((INSTALLER_SIZE / 1024 / 1024)) | |
| echo "NSIS installer size: ${INSTALLER_SIZE_MB}MB" | |
| # JRE is ~44MB, so installer should be at least 50MB to include it | |
| if [ $INSTALLER_SIZE_MB -lt 50 ]; then | |
| echo "WARNING: NSIS installer is only ${INSTALLER_SIZE_MB}MB" | |
| echo "This may indicate JRE was not bundled (expected >50MB)" | |
| echo "JRE size before bundling:" | |
| du -sh src-tauri/resources/build-jre || echo "JRE directory not found" | |
| exit 1 | |
| else | |
| echo "NSIS installer size looks good (${INSTALLER_SIZE_MB}MB includes JRE)" | |
| fi | |
| else | |
| echo "NSIS installer not found" | |
| fi | |
| # Check MSI installer | |
| MSI_INSTALLER=$(find src-tauri/target/*/release/bundle/msi -name "*.msi" 2>/dev/null | head -1) | |
| if [ -n "$MSI_INSTALLER" ]; then | |
| echo "Found MSI installer: $MSI_INSTALLER" | |
| MSI_SIZE=$(stat -c%s "$MSI_INSTALLER" 2>/dev/null || stat -f%z "$MSI_INSTALLER" 2>/dev/null || echo "0") | |
| MSI_SIZE_MB=$((MSI_SIZE / 1024 / 1024)) | |
| echo "MSI installer size: ${MSI_SIZE_MB}MB" | |
| if [ $MSI_SIZE_MB -lt 50 ]; then | |
| echo "⚠️ WARNING: MSI installer is only ${MSI_SIZE_MB}MB" | |
| echo "⚠️ This may indicate JRE was not bundled (expected >50MB)" | |
| echo "JRE size before bundling:" | |
| du -sh src-tauri/resources/build-jre || echo "JRE directory not found" | |
| exit 1 | |
| else | |
| echo "✅ MSI installer size looks good (${MSI_SIZE_MB}MB includes JRE)" | |
| fi | |
| else | |
| echo "⚠️ MSI installer not found" | |
| fi | |
| echo "✅ JRE bundling verification passed" | |
| - name: Verify JRE bundling (macOS) | |
| if: matrix.platform == 'macos-latest' && steps.tauri-build.outputs.build_success == 'true' | |
| shell: bash | |
| run: | | |
| echo "🔍 Verifying JRE is bundled in macOS .app..." | |
| # Find the .app bundle | |
| APP_BUNDLE=$(find src-tauri/target/*/release/bundle/macos -name "*.app" -type d 2>/dev/null | head -1) | |
| if [ -z "$APP_BUNDLE" ]; then | |
| echo "❌ .app bundle not found" | |
| exit 1 | |
| fi | |
| echo "Found .app bundle: $APP_BUNDLE" | |
| # Determine which JRE should be present based on target | |
| if [[ "${{ matrix.target }}" == "aarch64-apple-darwin" ]]; then | |
| JRE_SUBDIR="jre-macos-arm64" | |
| else | |
| JRE_SUBDIR="jre-macos-x64" | |
| fi | |
| # Check if JRE is in the .app bundle | |
| JRE_PATH="$APP_BUNDLE/Contents/Resources/build-jre/$JRE_SUBDIR" | |
| if [ ! -d "$JRE_PATH" ]; then | |
| echo "❌ JRE directory not found in .app bundle at: $JRE_PATH" | |
| echo "Contents of Resources/build-jre:" | |
| ls -la "$APP_BUNDLE/Contents/Resources/build-jre" || echo "build-jre directory doesn't exist" | |
| exit 1 | |
| fi | |
| echo "✅ JRE directory found: $JRE_PATH" | |
| # Check if java executable exists | |
| JAVA_EXEC="$JRE_PATH/bin/java" | |
| if [ ! -f "$JAVA_EXEC" ]; then | |
| # Try alternate path for macOS JRE structure | |
| JAVA_EXEC="$JRE_PATH/Contents/Home/bin/java" | |
| fi | |
| if [ ! -f "$JAVA_EXEC" ]; then | |
| echo "❌ Java executable not found in JRE" | |
| echo "Checked paths:" | |
| echo " - $JRE_PATH/bin/java" | |
| echo " - $JRE_PATH/Contents/Home/bin/java" | |
| echo "JRE contents:" | |
| ls -la "$JRE_PATH" || true | |
| exit 1 | |
| fi | |
| echo "✅ Java executable found: $JAVA_EXEC" | |
| # Check JRE size | |
| JRE_SIZE=$(du -sh "$JRE_PATH" | cut -f1) | |
| echo "JRE size in bundle: $JRE_SIZE" | |
| # Check overall .app size | |
| APP_SIZE=$(du -sh "$APP_BUNDLE" | cut -f1) | |
| echo ".app bundle total size: $APP_SIZE" | |
| echo "✅ macOS JRE bundling verification passed" | |
| 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 5: Generate and upload updater JSON | |
| generate-updater-json: | |
| name: "Generate Updater JSON" | |
| needs: | |
| [ | |
| check-release, | |
| pre-release-validation, | |
| prepare-release, | |
| build-and-release, | |
| ] | |
| if: always() && needs.check-release.result == 'success' && needs.build-and-release.result == 'success' && (needs.pre-release-validation.result == 'success' || needs.pre-release-validation.result == 'skipped') | |
| 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 6: 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: always() && needs.check-release.result == 'success' && needs.generate-updater-json.result == 'success' && (needs.pre-release-validation.result == 'success' || needs.pre-release-validation.result == 'skipped') | |
| uses: ./.github/workflows/reusable-create-release-branch.yml | |
| with: | |
| version: ${{ needs.prepare-release.outputs.tag }} | |
| max_branches: 5 | |
| secrets: inherit | |
| # Step 7: 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 | |
| - **Trigger**: Manual workflow dispatch | |
| - **Validation**: ${{ needs.pre-release-validation.result == 'success' && '✅ Passed' || '⏭️ Skipped' }} | |
| - **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 |