Stop Wasting Time: Automate Mobile Releases with Fastlane and GitHub Actions

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

The High Cost of Manual Mobile Deployment

Manual deployment is a productivity killer. Any mobile developer knows the drill: you finish a feature, then spend the next hour generating .ipa or .apk files. You manually juggle provisioning profiles while praying the build doesn’t fail 90% of the way through. I used to lose at least 4 to 6 hours every release week just babysitting Xcode and the Google Play Console.

Switching to an automated CI/CD pipeline changed my entire workflow. Instead of staring at progress bars, my team now focuses on shipping features while the machines handle the heavy lifting. We use Fastlane to execute build logic and GitHub Actions to provide the cloud infrastructure. This setup ensures that every build is clean, signed correctly, and delivered to testers without human intervention.

Setting Up Your Environment

Before we automate, we need Fastlane running locally. Fastlane is Ruby-based, so you will need a working Ruby environment. While macOS includes Ruby, I recommend using rbenv or asdf. These managers prevent the permission headaches often caused by the system-default Ruby version.

1. Install Fastlane via Bundler

Avoid installing Fastlane as a global gem. Instead, use a Gemfile in your project root to ensure your entire team uses the same version. Run these commands to get started:

gem install bundler
echo 'source "https://rubygems.org"
gem "fastlane"' > Gemfile
bundle install

Now, initialize Fastlane in your project folder (where your .xcodeproj or build.gradle lives):

bundle exec fastlane init

Fastlane prompts you with several setup options. For iOS, you will typically choose between automating screenshots, TestFlight, or full App Store distribution. For Android, you will need your package name and a JSON secret key from the Google Play Console. Once finished, Fastlane creates a fastlane folder containing your Fastfile.

2. Prepare the GitHub Actions Structure

GitHub Actions doesn’t require a separate installation. It lives directly in your repository. You just need to create the directory where your automation scripts will reside:

mkdir -p .github/workflows

Writing the Automation Logic

The Fastfile is where you translate manual clicks into code. You define “lanes,” which are essentially scripts for specific tasks like beta testing or production releases.

Defining Your Fastfile Lanes

Think of a lane as a repeatable recipe. Here is a practical example of an iOS beta lane that handles certificates, builds the app, and pushes to TestFlight:

platform :ios do
  desc "Push a new beta build to TestFlight"
  lane :beta do
    setup_ci
    match(type: "appstore") # Syncs certificates
    increment_build_number(build_number: ENV["GITHUB_RUN_NUMBER"])
    build_app(scheme: "YourAppName")
    upload_to_testflight
  end
end

For Android, the process is equally straightforward. This lane generates a production-ready App Bundle and sends it to the internal testing track:

platform :android do
  desc "Submit a new build to the Google Play Internal Track"
  lane :beta do
    gradle(task: "bundle", build_type: "Release")
    upload_to_play_store(track: "internal")
  end
end

Solving the Code Signing Nightmare

iOS code signing is notoriously difficult on CI servers because you cannot manually click “Allow” on keychain prompts. Fastlane Match solves this by storing certificates in a private, encrypted Git repository. This approach provides a single source of truth for your whole team. Run fastlane match init to set this up. It ensures that your CI runner always has the exact credentials needed to sign your app.

Configuring the GitHub Actions Workflow

Create a .github/workflows/deploy.yml file. This script tells GitHub to trigger your Fastlane lanes whenever code is pushed to the main branch.

name: Deploy to Beta
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.1'
          bundler-cache: true
      - name: Run Fastlane
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
        run: bundle exec fastlane ios beta

One critical tip: never hardcode passwords. Store sensitive values like MATCH_PASSWORD in GitHub Settings > Secrets. This keeps your credentials secure while making them accessible to the build runner.

Monitoring and Optimization

Once you push this configuration, the “Actions” tab in GitHub will show your build progress in real-time. If a build fails, Fastlane usually provides a clear error table. You might see “Error 403” if your API keys lack permissions or “Version Conflict” if you forgot to increment the build number.

Watch Your Build Times

Expect iOS builds on GitHub’s macos-latest runners to take between 15 and 30 minutes. Since macOS runners are significantly more expensive than Linux ones—often costing 10 times more per minute—optimize your triggers. Don’t run a full deployment on every small commit. Instead, trigger builds only when a Pull Request is merged or a specific tag is created.

Keep the Team Informed

Finally, I recommend adding a Slack notification to your Fastfile. Use the slack action to post a message to your team channel when a build succeeds or fails. This keeps everyone updated without forcing them to manually check GitHub logs. Once this system is running, you will realize just how much mental energy you used to waste on deployments.

after_all do |lane|
  slack(message: "Successfully deployed version #{get_build_number} to TestFlight!")
end
Share: