Skip to content

Merge pull request #35 from DavidAmunga/changeset-release/main #35

Merge pull request #35 from DavidAmunga/changeset-release/main

Merge pull request #35 from DavidAmunga/changeset-release/main #35

Workflow file for this run

name: "Main Release"
on:
push:
branches: [main]
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: |
# Check if this is a changeset release commit
COMMIT_MSG=$(git log -1 --pretty=format:"%s")
echo "Latest commit: $COMMIT_MSG"
# Check recent commits for changeset patterns (look deeper for merge commits)
RECENT_COMMITS=$(git log -5 --pretty=format:"%s")
echo "Recent commits:"
echo "$RECENT_COMMITS"
IS_CHANGESET_RELEASE="false"
if [[ "$COMMIT_MSG" == *"chore: release version packages"* ]] || \
[[ "$COMMIT_MSG" == *"Version Packages"* ]] || \
[[ "$COMMIT_MSG" == *"changeset-release/main"* ]] || \
[[ "$RECENT_COMMITS" == *"chore: release version packages"* ]] || \
[[ "$RECENT_COMMITS" == *"Version Packages"* ]]; then
IS_CHANGESET_RELEASE="true"
echo "✅ This is a changeset release commit"
fi
# Check for pending changesets
HAS_CHANGESETS="false"
if [ -d ".changeset" ] && [ "$(ls -1 .changeset/*.md 2>/dev/null | wc -l)" -gt 0 ]; then
# Exclude README.md and config.json
CHANGESET_COUNT=$(ls -1 .changeset/*.md 2>/dev/null | grep -v README.md | wc -l)
if [ "$CHANGESET_COUNT" -gt 0 ]; then
HAS_CHANGESETS="true"
echo "✅ Found $CHANGESET_COUNT pending changesets"
fi
fi
# Additional check: Look for version changes in the last commit
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
echo "📦 Version files changed in recent commit"
VERSION_CHANGED="true"
fi
# Determine if we should release
SHOULD_RELEASE="false"
if [[ "$IS_CHANGESET_RELEASE" == "true" ]] || \
[[ "$VERSION_CHANGED" == "true" && "$IS_CHANGESET_RELEASE" == "true" ]] || \
[[ "${{ github.event.inputs.force_release }}" == "true" ]] || \
[[ "$HAS_CHANGESETS" == "true" && "${{ github.event_name }}" == "workflow_dispatch" ]]; then
SHOULD_RELEASE="true"
echo "✅ Release conditions met"
else
echo "ℹ️ No release needed"
fi
# Get current version
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
# Step 2: 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 3: Sync versions and create tag
prepare-release:
name: "Prepare Release"
runs-on: ubuntu-latest
needs: check-release
if: needs.check-release.outputs.should_release == '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: |
echo "📦 Syncing all version files..."
pnpm run sync-versions
# Check if there are any changes to commit
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
echo "✅ Version files synced and committed"
else
echo "ℹ️ No version sync changes needed"
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
# Configure git
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Check if tag exists locally
if ! git tag -l "$TAG" | grep -q "$TAG"; then
# Check if tag exists remotely
if git ls-remote --tags origin | grep -q "refs/tags/$TAG$"; then
echo "ℹ️ Tag $TAG already exists remotely, fetching it"
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"
echo "✅ Created and pushed tag: $TAG"
fi
else
echo "ℹ️ Tag $TAG already exists locally"
fi
# Step 4: Build and release
build-and-release:
name: "Build & Release"
needs: [check-release, prepare-release]
if: needs.check-release.outputs.should_release == 'true'
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: "macos-latest"
args: "--target aarch64-apple-darwin"
target: "aarch64-apple-darwin"
- platform: "macos-latest"
args: "--target x86_64-apple-darwin"
target: "x86_64-apple-darwin"
- platform: "ubuntu-22.04"
args: ""
target: "x86_64-unknown-linux-gnu"
- platform: "windows-latest"
args: ""
target: "x86_64-pc-windows-msvc"
runs-on: ${{ matrix.platform }}
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 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
- 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: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Build frontend
run: pnpm build
- 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)
# Debug output
echo "Debug: VERSION=$VERSION"
echo "Debug: CHANGELOG_CONTENT length: $(echo "$CHANGELOG_CONTENT" | wc -c | tr -d ' ')"
if [ -n "$CHANGELOG_CONTENT" ]; then
echo "Debug: Found changelog content for version $VERSION"
else
echo "Debug: No changelog content found for version $VERSION"
fi
# 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 ' ')
# Create formatted changelog with emojis
FORMATTED_CHANGELOG=""
if [ "$FEATURES" -gt 0 ]; then
FORMATTED_CHANGELOG="${FORMATTED_CHANGELOG}### ✨ New Features ($FEATURES)"$'\n'"$(echo "$CHANGELOG_CONTENT" | grep -E "(^- [a-f0-9]+: )?feat:" | sed 's/^- /- /')"$'\n\n'
fi
if [ "$FIXES" -gt 0 ]; then
FORMATTED_CHANGELOG="${FORMATTED_CHANGELOG}### 🐛 Bug Fixes ($FIXES)"$'\n'"$(echo "$CHANGELOG_CONTENT" | grep -E "(^- [a-f0-9]+: )?fix:" | sed 's/^- /- /')"$'\n\n'
fi
if [ "$BREAKING" -gt 0 ]; then
FORMATTED_CHANGELOG="${FORMATTED_CHANGELOG}### ⚠️ Breaking Changes ($BREAKING)"$'\n'"$(echo "$CHANGELOG_CONTENT" | grep -E "BREAKING CHANGE|!" | sed 's/^- /- /')"$'\n\n'
fi
if [ "$PERF" -gt 0 ]; then
FORMATTED_CHANGELOG="${FORMATTED_CHANGELOG}### ⚡ Performance Improvements ($PERF)"$'\n'"$(echo "$CHANGELOG_CONTENT" | grep -E "(^- [a-f0-9]+: )?perf:" | sed 's/^- /- /')"$'\n\n'
fi
if [ "$REFACTOR" -gt 0 ]; then
FORMATTED_CHANGELOG="${FORMATTED_CHANGELOG}### 🔧 Code Refactoring ($REFACTOR)"$'\n'"$(echo "$CHANGELOG_CONTENT" | grep -E "(^- [a-f0-9]+: )?refactor:" | sed 's/^- /- /')"$'\n\n'
fi
if [ "$DOCS" -gt 0 ]; then
FORMATTED_CHANGELOG="${FORMATTED_CHANGELOG}### 📚 Documentation ($DOCS)"$'\n'"$(echo "$CHANGELOG_CONTENT" | grep -E "(^- [a-f0-9]+: )?docs:" | sed 's/^- /- /')"$'\n\n'
fi
if [ "$SECURITY" -gt 0 ]; then
FORMATTED_CHANGELOG="${FORMATTED_CHANGELOG}### 🔒 Security Updates ($SECURITY)"$'\n'"$(echo "$CHANGELOG_CONTENT" | grep -i "security\|vulnerability\|cve\|exploit" | sed 's/^- /- /')"$'\n\n'
fi
# Add other changes if any
OTHER_CHANGES=$(echo "$CHANGELOG_CONTENT" | grep -v -E "feat:|fix:|BREAKING CHANGE|!|perf:|refactor:|docs:|chore:|security|vulnerability|cve|exploit" | grep "^-" || true)
if [ -n "$OTHER_CHANGES" ]; then
FORMATTED_CHANGELOG="${FORMATTED_CHANGELOG}### 🔄 Other Changes"$'\n'"$OTHER_CHANGES"$'\n\n'
fi
# If no categorized changes found, use original content
if [ -z "$FORMATTED_CHANGELOG" ]; then
FORMATTED_CHANGELOG="$CHANGELOG_CONTENT"
fi
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo "$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 and release Tauri app
uses: tauri-apps/tauri-action@v0
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
with:
tagName: ${{ needs.prepare-release.outputs.tag }}
releaseName: "mpesa2csv ${{ needs.prepare-release.outputs.tag }}"
releaseBody: |
## 🚀 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 }}
releaseDraft: false
prerelease: false
args: ${{ matrix.args }}
# Step 5: Create release branch for hotfixes
create-release-branch:
name: "Create Release Branch"
needs: [check-release, prepare-release, build-and-release]
if: needs.check-release.outputs.should_release == 'true'
uses: ./.github/workflows/reusable-create-release-branch.yml
with:
version: ${{ needs.prepare-release.outputs.tag }}
max_branches: 5
secrets: inherit
# Step 6: Notify about release completion
notify-completion:
name: "Release Complete"
runs-on: ubuntu-latest
needs: [prepare-release, build-and-release, create-release-branch]
if: always() && needs.prepare-release.result == 'success'
steps:
- name: Release summary
run: |
echo "🎉 Release ${{ needs.prepare-release.outputs.tag }} completed!"
echo "✅ 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 }}"