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 β 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 / Dart | Mobile application and automated tests |
| GitHub | Source repository and workflow trigger |
| GitHub Actions | Automation engine for CI/CD |
| Flutter Test | Unit and widget test execution |
| Maven | Build tool for Java-based test projects |
| Karate | API testing with Gherkin syntax |
| Cucumber | BDD framework with Gherkin and step definitions |
| SonarQube Cloud | Static code analysis and Quality Gates |
| Firebase App Distribution | Pre-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 case | Direct API testing | BDD-style API testing |
| Scenario syntax | Gherkin, written directly | Gherkin with step definitions |
| Setup overhead | Minimal | Moderate (Java step classes) |
| Flexibility | Good for straightforward API flows | Good 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 |
|---|---|
| Test | Flutter analysis and unit/widget tests with coverage |
| Build | Compile APK and store as GitHub artifact |
| Sonar Scan | Analyze code quality and coverage in SonarQube Cloud |
| Karate Tests | Execute Karate API test suite |
| Cucumber Tests | Execute Cucumber API test suite |
| Deploy | Download 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.
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.
| Secret | Purpose | Where |
|---|---|---|
SONAR_TOKEN | SonarQube auth | SonarCloud β My Account |
FIREBASE_APP_ID | Firebase app ID | Firebase Console β Settings |
FIREBASE_SERVICE_ACCOUNT_JSON | Service account JSON | Firebase β 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.
- GitHub receives the push and triggers the workflow.
- Flutter analysis and automated tests run.
- If tests pass, the application is compiled into an APK.
- The APK is stored as a GitHub artifact.
- SonarQube Cloud analyzes the source code and coverage data.
- Karate and Cucumber API tests run in parallel.
- The deployment job waits for all required checks to pass.
- The deployment job downloads the existing APK artifact (no rebuild).
- Firebase App Distribution receives the APK and notifies the tester group.
- 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
| Error | Fix |
|---|---|
| remote: Permission denied | Clear cached Git credentials and re-auth |
| SonarQube: Project not found | Use exact org key from Sonar dashboard |
| Firebase: 403 Forbidden | Grant App Distribution Admin role to service account |
| Flutter: command not found | Add subosito/flutter-action step before use |
| Coverage shows 0% in Sonar | Verify coverage/lcov.info exists |
| Tester group not found | Group 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.namecontrols 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 manually | Pipeline runs checks automatically on every push |
| APK built manually by a developer | APK generated automatically by CI |
| Developer uploads APK to testers manually | Build distributed automatically to tester group |
| QA waits for someone to send a build | QA receives notification automatically |
| Process depends on individual memory | Process 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
| Service | Free Tier | Notes |
|---|---|---|
| GitHub Actions | 2,000 min/month (private) | Unlimited for public repos |
| SonarQube Cloud | Free for public repos | Private needs paid plan |
| Firebase App Distribution | Free | Unlimited 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.
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.