Skip to content

CI/CD: Unified release automation for all platforms #74

@sfloess

Description

@sfloess

Overview

Create a single automated release workflow that builds and publishes artifacts for all platforms (Desktop JAR, Android APK, iOS IPA, macOS DMG) from one tag push.

Current State

  • ✅ Desktop: Automated builds on every push
  • ✅ Desktop: Automated packagecloud.io deployment
  • ✅ Android: Builds debug/release APKs
  • ❌ Android: Manual release process
  • ❌ iOS: No CI/CD automation
  • ❌ No unified release workflow

CI/CD Score Impact

Current: CI/CD A- (90/100)
With this + #74: CI/CD A+ (98/100)

Proposed Unified Workflow

Trigger

git tag v2.1.0
git push origin v2.1.0
# Single command triggers builds for all platforms

Create .github/workflows/release.yml

name: Unified Release

on:
  push:
    tags:
      - 'v*'

jobs:
  prepare:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.get_version.outputs.version }}
      changelog: ${{ steps.get_changelog.outputs.changelog }}
    steps:
      - uses: actions/checkout@v4
      
      - name: Get version from tag
        id: get_version
        run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
      
      - name: Extract changelog for this version
        id: get_changelog
        run: |
          # Extract section from CHANGELOG.md for this version
          sed -n "/## \[${{ steps.get_version.outputs.version }}\]/,/## \[/p" CHANGELOG.md | head -n -1 > release-notes.md
          echo "changelog<<EOF" >> $GITHUB_OUTPUT
          cat release-notes.md >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT
      
      - name: Verify version consistency
        run: |
          # Check pom.xml, build.gradle versions match tag
          grep "<version>${{ steps.get_version.outputs.version }}</version>" pom.xml || exit 1
          grep "version = '${{ steps.get_version.outputs.version }}'" jnexus-core/build.gradle || exit 1

  build-desktop:
    needs: prepare
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '21'
      
      - name: Build JAR
        run: mvn clean package
      
      - name: Upload JAR
        uses: actions/upload-artifact@v4
        with:
          name: jnexus-desktop-${{ needs.prepare.outputs.version }}
          path: target/jnexus-*-jar-with-dependencies.jar

  build-android:
    needs: prepare
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '21'
      
      - name: Build Core
        run: gradle :jnexus-core:build
      
      - name: Build Android Release APK
        run: gradle :jnexus-android:assembleRelease
      
      - name: Sign APK
        uses: r0adkll/sign-android-release@v1
        with:
          releaseDirectory: jnexus-android/build/outputs/apk/release
          signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }}
          alias: ${{ secrets.ANDROID_KEY_ALIAS }}
          keyStorePassword: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
          keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
      
      - name: Upload APK
        uses: actions/upload-artifact@v4
        with:
          name: jnexus-android-${{ needs.prepare.outputs.version }}
          path: jnexus-android/build/outputs/apk/release/*-signed.apk

  build-ios:
    needs: prepare
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Xcode
        uses: maxim-lobanov/setup-xcode@v1
        with:
          xcode-version: latest-stable
      
      - name: Install Apple Certificate
        env:
          BUILD_CERTIFICATE_BASE64: ${{ secrets.IOS_BUILD_CERTIFICATE }}
          P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
          KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
        run: |
          # Decode certificate
          echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o certificate.p12
          
          # Create keychain
          security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
          
          # Import certificate
          security import certificate.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign
          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
      
      - name: Install Provisioning Profile
        env:
          PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE }}
        run: |
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          echo -n "$PROVISIONING_PROFILE_BASE64" | base64 --decode -o ~/Library/MobileDevice/Provisioning\ Profiles/profile.mobileprovision
      
      - name: Build iOS IPA
        run: |
          cd jnexus-ios
          xcodebuild -scheme JNexus-iOS \
            -configuration Release \
            -archivePath build/JNexus.xcarchive \
            archive
          
          xcodebuild -exportArchive \
            -archivePath build/JNexus.xcarchive \
            -exportPath build \
            -exportOptionsPlist ExportOptions.plist
      
      - name: Upload IPA
        uses: actions/upload-artifact@v4
        with:
          name: jnexus-ios-${{ needs.prepare.outputs.version }}
          path: jnexus-ios/build/JNexus.ipa

  build-macos:
    needs: prepare
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Xcode
        uses: maxim-lobanov/setup-xcode@v1
        with:
          xcode-version: latest-stable
      
      - name: Build macOS App
        run: |
          cd jnexus-ios
          xcodebuild -scheme JNexus-macOS \
            -configuration Release \
            -archivePath build/JNexus-macOS.xcarchive \
            archive
      
      - name: Create DMG
        run: |
          # Create DMG from .app bundle
          create-dmg \
            --volname "JNexus" \
            --window-pos 200 120 \
            --window-size 600 400 \
            --icon-size 100 \
            --app-drop-link 425 120 \
            jnexus-macos-${{ needs.prepare.outputs.version }}.dmg \
            jnexus-ios/build/JNexus-macOS.xcarchive/Products/Applications/JNexus.app
      
      - name: Upload DMG
        uses: actions/upload-artifact@v4
        with:
          name: jnexus-macos-${{ needs.prepare.outputs.version }}
          path: jnexus-macos-${{ needs.prepare.outputs.version }}.dmg

  create-release:
    needs: [prepare, build-desktop, build-android, build-ios, build-macos]
    runs-on: ubuntu-latest
    steps:
      - name: Download all artifacts
        uses: actions/download-artifact@v4
      
      - name: Create GitHub Release
        uses: softprops/action-gh-release@v1
        with:
          name: Release v${{ needs.prepare.outputs.version }}
          body: ${{ needs.prepare.outputs.changelog }}
          draft: false
          prerelease: false
          files: |
            jnexus-desktop-${{ needs.prepare.outputs.version }}/*.jar
            jnexus-android-${{ needs.prepare.outputs.version }}/*.apk
            jnexus-ios-${{ needs.prepare.outputs.version }}/*.ipa
            jnexus-macos-${{ needs.prepare.outputs.version }}/*.dmg
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Deploy to packagecloud.io
        run: |
          # Existing packagecloud deployment
          curl -F "package[distro_version_id]=190" \
               -F "package[package_file]=@jnexus-desktop-${{ needs.prepare.outputs.version }}/*.jar" \
               https://${{ secrets.PACKAGECLOUD_TOKEN }}:@packagecloud.io/api/v1/repos/flossware/jnexus/packages.json
      
      - name: Notify release
        run: |
          # Send notification (Slack, Discord, email, etc.)
          echo "Released v${{ needs.prepare.outputs.version }} successfully"

iOS Signing Setup

Required Secrets (GitHub Settings → Secrets):

  • : Base64-encoded .p12 file
  • : Password for .p12 file
  • : Temporary keychain password
  • : Base64-encoded .mobileprovision file

Generate:

# Export certificate from Xcode
base64 -i certificate.p12 | pbcopy

# Export provisioning profile
base64 -i profile.mobileprovision | pbcopy

Android Signing Setup

Required Secrets:

  • : Base64-encoded keystore file
  • : Key alias
  • : Keystore password
  • : Key password

Generate:

# Create keystore (one-time)
keytool -genkey -v -keystore jnexus.keystore -alias jnexus -keyalg RSA -keysize 2048 -validity 10000

# Encode for GitHub Secrets
base64 -i jnexus.keystore | pbcopy

Release Checklist (Automated)

The workflow enforces:

  • Version in pom.xml matches tag
  • Version in build.gradle matches tag
  • CHANGELOG.md has entry for this version
  • All tests pass (via existing CI)
  • Code quality checks pass

Manual Pre-Release Steps

Before pushing tag:

  1. Update version in pom.xml, build.gradle, Info.plist
  2. Update CHANGELOG.md with release notes
  3. Commit: On branch main
    Your branch is up to date with 'github/main'.

nothing to commit, working tree clean
4. Tag:
5. Push:

Post-Release Steps (Automated)

Workflow will:

  • Build all artifacts
  • Create GitHub Release
  • Attach all artifacts
  • Deploy JAR to packagecloud.io
  • Send notifications

Rollback Process

If release fails:

# Delete tag locally and remotely
git tag -d v2.1.0
git push origin :refs/tags/v2.1.0

# Delete GitHub Release (manual via UI or gh CLI)
gh release delete v2.1.0

# Fix issue, increment version, retry

Acceptance Criteria

  • Unified workflow created
  • iOS signing configured
  • Android signing configured
  • All 4 platforms build successfully
  • GitHub Release created with all artifacts
  • CHANGELOG extraction works
  • Version validation works
  • Notifications sent
  • Documentation updated (CONTRIBUTING.md)

Priority

Medium - Streamlines release process significantly

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions