← Back to Blog

Flutter CI/CD: Git Push to Firebase Distribution

A practical guide to building an automated Flutter delivery pipeline with GitHub Actions.

Every Flutter change needs analysis, tests, an APK build, code-quality checks, API validation, and distribution to QA. CI/CD connects these steps into one automated, repeatable process.

This guide uses a Proof of Concept (PoC)β€”a small project built to demonstrate the approach. The PoC includes a simple Flutter app with a complete pipeline around it, using GitHub Actions, Karate and Cucumber for API testing, SonarQube Cloud, and Firebase App Distribution.

1. The Problem CI/CD Solves

Imagine a developer changes a login screen and pushes the code. Without automation, someone must manually run tests, build an APK, verify code quality, and upload the build for QA. As the project grows, these steps become repetitive and error-prone.

CI/CD moves those manual steps into an automated pipeline. A developer pushes code, and the pipeline automatically verifies the change, builds the application, and distributes a validated artifact to testers.

The Real Goal

The point is not simply automating commands. It is creating a predictable, repeatable path from source code to a testable software build.

2. What CI and CD Mean

Continuous Integration (CI)

Continuous Integration means integrating code changes frequently and automatically validating them. In this PoC, a Git push or pull request triggers GitHub Actions, which runs Flutter analysis, automated tests, builds, code-quality analysis, and API tests.

The benefit is early feedback: instead of discovering a broken change during QA, the team sees the failure close to the commit that introduced it.

Continuous Delivery vs Continuous Deployment

Continuous Delivery means software is kept in a releasable state through automation. Continuous Deployment automatically releases every successful change to its target environment.

This PoC automatically distributes the APK to a Firebase App Distribution tester group after required checks pass. This is pre-release delivery to QA, not production release. A production release to an app store would be a separate, deliberately controlled step.

3. The Complete Flutter CI/CD Flow

Developer GitHub GitHub Actions Test Build Quality / API Checks Artifact Firebase App Distribution QA
  • Developer β€” modifies Flutter/Dart code and pushes to GitHub
  • GitHub β€” stores source code and triggers the workflow
  • GitHub Actions β€” starts the automated workflow
  • Testing β€” validates application behavior with Flutter tests
  • Build β€” compiles the application into an APK
  • Quality and API checks β€” analyze code quality and verify backend behavior
  • Artifact β€” preserves the validated APK for deployment
  • Firebase App Distribution β€” delivers the APK to QA testers

4. Tools Used in the PoC

Tool Role in the pipeline
Flutter / DartMobile application and automated tests
GitHubSource repository and workflow trigger
GitHub ActionsAutomation engine for CI/CD
Flutter TestUnit and widget test execution
MavenBuild tool for Java-based test projects
KarateAPI testing with Gherkin syntax
CucumberBDD framework with Gherkin and step definitions
SonarQube CloudStatic code analysis and Quality Gates
Firebase App DistributionPre-release APK distribution to QA

5. Testing: Why One Test Type Is Not Enough

A single test layer cannot validate every part of a system. The PoC combines different checks: Flutter tests validate application behavior, while API tests validate backend behavior independently of the Flutter UI.

Flutter Unit and Widget Tests

Unit tests focus on individual functions and classes. Widget tests render Flutter widgets and interact with them without requiring a physical device. In the PoC, the test job runs Flutter analysis and tests with coverage reporting.

API Testing

API tests verify backend behavior separately from the mobile UI. A Flutter screen can appear to work correctly while the underlying API returns incorrect data or error responses. API tests catch these issues independently.

Karate

Karate allows API scenarios to be written in Gherkin-style syntax without requiring a separate Java step definition for every test step. The PoC uses Maven to execute the Karate test project.

Karate Project Structure

karate_tests/
β”œβ”€β”€ pom.xml
└── src/test/java/
    β”œβ”€β”€ karate-config.js
    └── examples/login/login.feature

karate_tests/pom.xml (essentials)

<dependency>
  <groupId>com.intuit.karate</groupId>
  <artifactId>karate-junit5</artifactId>
  <version>1.4.1</version>
  <scope>test</scope>
</dependency>

login.feature

Feature: Login API

  Background:
    * url baseUrl

  Scenario: Successful login
    Given path '/auth/login'
    And request { username: 'qa_user', password: 'Test@123' }
    When method post
    Then status 200
    And match response.token != null

Java Runner

class LoginRunner {
    @Karate.Test
    Karate testLogin() {
        return Karate.run("login").relativeTo(getClass());
    }
}

Cucumber

Cucumber is a BDD framework where readable Gherkin scenarios are connected to implementation code through step definitions. This approach provides more control, but requires more setup code compared to Karate.

Cucumber Project Structure

cucumber_tests/
β”œβ”€β”€ pom.xml
└── src/test/
    β”œβ”€β”€ java/com/byteping/
    β”‚   β”œβ”€β”€ RunCucumberTest.java
    β”‚   └── steps/LoginSteps.java
    └── resources/features/login.feature

login.feature

Feature: Login API validation

  Scenario: Successful login
    Given the API base URL is "https://staging-api.byteping.com"
    When I POST to "/auth/login" with username "qa_user" and password "Test@123"
    Then the response status should be 200

LoginSteps.java

public class LoginSteps {
    private String baseUrl;
    private Response response;

    @Given("the API base URL is {string}")
    public void setBaseUrl(String url) { this.baseUrl = url; }

    @When("I POST to {string} with username {string} and password {string}")
    public void postLogin(String path, String user, String pass) {
        String body = String.format(
            "{\"username\":\"%s\",\"password\":\"%s\"}", user, pass);
        response = RestAssured.given()
            .header("Content-Type", "application/json")
            .body(body)
            .post(baseUrl + path);
    }

    @Then("the response status should be {int}")
    public void verifyStatus(int expected) {
        assertEquals(expected, response.getStatusCode());
    }
}

6. Karate vs Cucumber: Which Approach?

Aspect Karate Cucumber
Use caseDirect API testingBDD-style API testing
Scenario syntaxGherkin, written directlyGherkin with step definitions
Setup overheadMinimalModerate (Java step classes)
FlexibilityGood for straightforward API flowsGood for complex business logic

The PoC demonstrates both frameworks to show their architectural differences. In a real project, teams would typically select one framework based on their testing requirements rather than maintaining both.

7. Maven: Building Java Test Projects

Maven is used to compile and run the Java-based Karate and Cucumber test projects. Its pom.xml file defines dependencies, plugins, versions, and build configuration. Maven does not build the Flutter applicationβ€”it manages only the separate Java test projects.

cd karate_tests && mvn test
cd cucumber_tests && mvn test

8. SonarQube Cloud and Quality Gates

Automated tests answer "does this work?" Code-quality analysis answers different questions: are there maintainability issues, duplicated code, or security vulnerabilities?

SonarQube Cloud analyzes source code against configured quality rules. A Quality Gate is an automated checkpoint that passes or fails based on quality metrics. A passing test suite and a passing Quality Gate are related but separate: tests verify behavior, quality analysis verifies code characteristics.

SonarQube Cloud does not generate test coverage. Flutter produces an LCOV coverage report, which is provided to SonarQube:

flutter test --coverage
sonar.dart.lcov.reportPaths=coverage/lcov.info

sonar-project.properties

sonar.projectKey=your_org_your_project_key
sonar.organization=your-org-key
sonar.sources=lib
sonar.tests=test
sonar.dart.lcov.reportPaths=coverage/lcov.info
sonar.dart.analyzer.mode=flutter
sonar.exclusions=**/*.g.dart,**/*.freezed.dart,build/**,android/**,ios/**
sonar.coverage.exclusions=test/**,**/*_test.dart
sonar.qualitygate.wait=true

Quality checks should happen before a build reaches QA or production environments.

9. GitHub Actions: The Automation Engine

GitHub Actions reads workflow definitions stored in .github/workflows. In the PoC, the workflow triggers on pushes to configured branches and pull requests targeting main.

Pipeline Jobs

Job Purpose
TestFlutter analysis and unit/widget tests with coverage
BuildCompile APK and store as GitHub artifact
Sonar ScanAnalyze code quality and coverage in SonarQube Cloud
Karate TestsExecute Karate API test suite
Cucumber TestsExecute Cucumber API test suite
DeployDownload APK artifact and upload to Firebase

Workflow YAML (condensed)

name: Flutter CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: 'temurin', java-version: '17' }
      - uses: subosito/flutter-action@v2
        with: { flutter-version: '3.24.0', channel: 'stable', cache: true }
      - run: flutter pub get
      - run: flutter analyze
      - run: flutter test --coverage
      - uses: actions/upload-artifact@v4
        with: { name: coverage-report, path: coverage/lcov.info }

  build:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with: { flutter-version: '3.24.0', channel: 'stable' }
      - run: flutter pub get
      - run: flutter build apk --release
      - uses: actions/upload-artifact@v4
        with:
          name: app-release-apk
          path: build/app/outputs/flutter-apk/app-release.apk

  sonar:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: actions/download-artifact@v4
        with: { name: coverage-report, path: coverage/ }
      - uses: SonarSource/sonarqube-scan-action@v2
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

  karate-tests:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: 'temurin', java-version: '17' }
      - run: cd karate_tests && mvn test

  cucumber-tests:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: 'temurin', java-version: '17' }
      - run: cd cucumber_tests && mvn test

  deploy:
    runs-on: ubuntu-latest
    needs: [sonar, karate-tests, cucumber-tests]
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with: { name: app-release-apk, path: build/app/outputs/flutter-apk/ }
      - uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{ secrets.FIREBASE_APP_ID }}
          serviceCredentialsFileContent: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }}
          groups: qa-testers
          file: build/app/outputs/flutter-apk/app-release.apk

10. Build Once, Reuse the Artifact

A key design principle in the PoC is that deployment does not rebuild the APK. The build job creates the APK and stores it as a GitHub artifact. The deployment job later downloads that exact same artifact.

Build APK Artifact Test & Validation Download Artifact Deploy

This ensures the artifact distributed to testers is the exact same build that passed all earlier validation stages. Rebuilding during deployment would create a different artifact that may not have been tested.

11. GitHub Secrets and Credentials

CI/CD systems require credentials for external services, but these must never be committed to source code. GitHub Actions exposes repository secrets to workflow steps securely.

SecretPurposeWhere
SONAR_TOKENSonarQube authSonarCloud β†’ My Account
FIREBASE_APP_IDFirebase app IDFirebase Console β†’ Settings
FIREBASE_SERVICE_ACCOUNT_JSONService account JSONFirebase β†’ Service Accounts

Credential Security Rules

  • Never commit service-account JSON, API keys, or access tokens to Git
  • Store sensitive credentials in a secure secret manager
  • If credentials are exposed, revoke and rotate them immediately
  • Grant service accounts only the minimum required roles
  • Rotate tokens regularly (e.g., every 90 days)

.gitignore Additions

google-services.json
GoogleService-Info.plist
*.jks
*.keystore
.env
.env.*
coverage/

12. Firebase App Distribution

Firebase App Distribution delivers pre-release APKs to QA testers. It is not the same as publishing to the Play Store or App Storeβ€”it is for fast internal distribution of test builds.

Firebase Setup (Quick)

# 1. Install CLI
npm install -g firebase-tools
firebase login

# 2. Create service account:
#    Firebase Console β†’ Project Settings β†’ Service Accounts
#    β†’ Generate New Private Key β†’ Download JSON

# 3. Add to GitHub Secrets:
#    FIREBASE_APP_ID
#    FIREBASE_SERVICE_ACCOUNT_JSON

# 4. Create tester group "qa-testers" in Firebase Console

Local Test (Before Automating)

firebase appdistribution:distribute \
  build/app/outputs/flutter-apk/app-release.apk \
  --app 1:1234567890:android:abcdef1234567890 \
  --groups "qa-testers" \
  --release-notes "Local test build"

13. End-to-End Example: What Happens After a Git Push?

Consider a developer who changes login validation logic and pushes the commit.

  1. GitHub receives the push and triggers the workflow.
  2. Flutter analysis and automated tests run.
  3. If tests pass, the application is compiled into an APK.
  4. The APK is stored as a GitHub artifact.
  5. SonarQube Cloud analyzes the source code and coverage data.
  6. Karate and Cucumber API tests run in parallel.
  7. The deployment job waits for all required checks to pass.
  8. The deployment job downloads the existing APK artifact (no rebuild).
  9. Firebase App Distribution receives the APK and notifies the tester group.
  10. QA testers receive a notification and can install the build.

14. Real Problems Encountered During the PoC

These are actual obstacles encountered while building the PoC. Each problem illustrates why certain configurations matter.

GitHub Push Permission Problem

Problem: Push was rejected despite correct repository URL.

Cause: Git for Windows had cached credentials for a different GitHub account. Running git config user.name changes commit identity but not authentication credentials.

Fix: Inspect the credential cache and verify the authenticated account matches the repository owner.

Lesson

Commit identity and authentication identity are separate. When push permissions fail, verify cached credentials, not just the git config.

SonarQube Cloud Organization Key

Problem: SonarQube Cloud integration failed with incorrect organization key.

Cause: The display name and the actual organization key are different. Configuration required the exact key, including its case sensitivity.

Fix: Use the organization key shown in SonarQube Cloud settings, not the display name.

Lesson

When a service provides both a display name and an internal key, use the actual key shown by the service, not a guessed variant.

Firebase Service Account Key Restrictions

Problem: Service-account key creation was blocked by policy.

Cause: Organization policy on the Google Workspace account prevented creating service-account keys.

Fix: Contact the organization administrator or use a different account with appropriate permissions.

Lesson

Cloud permissions can be controlled at the organization level. Investigate account ownership and organization policies before assuming an application issue.

Checking an Old GitHub Actions Run

Problem: After fixing configuration, an old workflow run still showed the previous error.

Cause: A workflow run represents the state of the repository at that execution. Changing configuration does not update historical runs.

Fix: After configuration changes, inspect a new workflow run or explicitly trigger a rerun.

Lesson

After modifying pipeline configuration, verify the fix with a fresh workflow run, not by re-inspecting an old one.

15. Troubleshooting Quick Reference

ErrorFix
remote: Permission deniedClear cached Git credentials and re-auth
SonarQube: Project not foundUse exact org key from Sonar dashboard
Firebase: 403 ForbiddenGrant App Distribution Admin role to service account
Flutter: command not foundAdd subosito/flutter-action step before use
Coverage shows 0% in SonarVerify coverage/lcov.info exists
Tester group not foundGroup name is case-sensitive β€” match exactly

16. Common Mistakes to Avoid

  • Using an incorrect SonarQube Cloud organization or project key
  • Committing tokens, API keys, or service-account credentials to version control
  • Using a Firebase tester group name that does not match the workflow configuration
  • Assuming git config user.name controls GitHub authentication
  • Debugging pipeline failures by examining old workflow runs instead of new ones
  • Rebuilding the APK during deployment instead of reusing the validated artifact
  • Treating passing application tests as equivalent to passing a Quality Gate
  • Relying on only one type of automated test

17. Project Structure

cicd_poc/
β”œβ”€β”€ lib/                         # Flutter application
β”œβ”€β”€ test/                        # Flutter unit and widget tests
β”œβ”€β”€ karate_tests/                # Karate API test project
β”œβ”€β”€ cucumber_tests/              # Cucumber BDD test project
β”œβ”€β”€ sonar-project.properties     # SonarQube Cloud configuration
β”œβ”€β”€ .github/workflows/           # GitHub Actions workflow files
└── pubspec.yaml                 # Flutter dependencies

pubspec.yaml

name: cicd_poc
version: 1.0.0+1

environment:
  sdk: '>=3.5.0 <4.0.0'
  flutter: '>=3.24.0'

dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^4.0.0

flutter:
  uses-material-design: true

18. Manual Process vs Automated Pipeline

Manual approach Automated approach
Developer runs checks manuallyPipeline runs checks automatically on every push
APK built manually by a developerAPK generated automatically by CI
Developer uploads APK to testers manuallyBuild distributed automatically to tester group
QA waits for someone to send a buildQA receives notification automatically
Process depends on individual memoryProcess is defined as code and reproducible

19. What This PoC Actually Proves

The Flutter application in the PoC is intentionally small. Its purpose is to provide enough code for the pipeline to build and test. The real achievement is the automated workflow:

  • Automated Flutter verification on every push
  • Multiple testing layers (unit, widget, and API)
  • Automated code-quality analysis with Quality Gates
  • Automated APK compilation
  • Artifact preservation and reuse
  • Automated Firebase distribution to QA testers

This PoC demonstrates a repeatable CI/CD workflow, not a production mobile application.

20. Cost & Free Tier Reference

ServiceFree TierNotes
GitHub Actions2,000 min/month (private)Unlimited for public repos
SonarQube CloudFree for public reposPrivate needs paid plan
Firebase App DistributionFreeUnlimited testers

Small team estimate: $0 – $75/month depending on repo visibility.

21. Possible Extensions for a Real Project

The documented PoC can be extended according to project requirements. These are future possibilities, not features claimed as implemented in the current PoC.

  • Separate development, staging, and production environments
  • Release builds with controlled production deployment
  • Automated versioning and changelog generation
  • Pull-request-specific quality gates and reporting
  • Android Play Store and iOS App Store release workflows
  • Manual approval gates for production deployments
  • Database migration automation for backend services

22. Final Takeaway

CI/CD is an engineering discipline that automates the path from code to production. It is not simply running commands in GitHub Actions. It is about removing human error from repeatable processes and creating visibility into how software changes move through validation stages.

Code Verify Analyze Build Preserve Distribute

This PoC shows what is possible with basic automation and standard tools. A Flutter team can apply these patterns immediately, and extend them as product requirements grow.

Official References

← Back to All Articles