{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreieddzergu4kedyoi6uz6grihudpgx62l5ye3htdobani6nom62cde",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpyvaev35o72"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreiei5myt7xm63b5rgiv6jtnivtdbwmxekotyy6cyzbkvizkvdaujly"
    },
    "mimeType": "image/webp",
    "size": 345614
  },
  "path": "/madhan_gannarapu/automating-flutter-android-releases-to-google-play-using-github-actions-9lj",
  "publishedAt": "2026-07-06T19:27:02.000Z",
  "site": "https://dev.to",
  "tags": [
    "android",
    "cicd",
    "flutter",
    "github"
  ],
  "textContent": "Publishing a Flutter app manually is fine once.\n\nDoing it every time by downloading an `.aab`, opening Play Console, uploading it, checking versions, and creating releases manually is trash. It is slow, error-prone, and sooner or later you will upload the wrong build.\n\nIn this article, I’ll document the complete end-to-end flow to publish a Flutter Android app to Google Play using GitHub Actions.\n\n##  Final Release Flow\n\n\n    feature/*\n          │\n          ▼\n    develop\n          │\n          ▼\n    Flutter CI\n\n    ────────────────────────────\n\n    main\n          │\n          ▼\n    Flutter CI\n          │\n          ▼\n    Internal Release\n          │\n          ▼\n    Google Play Internal Testing\n\n    ────────────────────────────\n\n    git tag v1.0.0\n          │\n          ▼\n    Production Release\n          │\n          ▼\n    Google Play Production\n          │\n          ▼\n    GitHub Release\n\n\n#  Part 1: Google Play Console Setup\n\nBefore GitHub Actions can upload builds to Google Play, Google Play must trust a service account.\n\nThe flow is:\n\n\n\n    Google Play Console\n        ↓\n    Google Cloud Project\n        ↓\n    Enable Google Play Android Developer API\n        ↓\n    Create Service Account\n        ↓\n    Create JSON Key\n        ↓\n    Invite Service Account in Play Console\n        ↓\n    Grant Release Permissions\n        ↓\n    Store JSON in GitHub Environment Secret\n\n\n##  Step 1: Create App in Google Play Console\n\nGo to Google Play Console and create your app.\n\nYou need to configure:\n\n  * App name\n  * Default language\n  * App type\n  * Free or paid\n  * Declarations\n  * Privacy Policy\n  * App access\n  * Ads declaration\n  * Data safety\n  * Content rating\n  * Target audience\n  * Store listing\n\n\n\nDo not automate this part first. Get your Play Console app created manually before setting up CI/CD.\n\n##  Step 2: Link Google Play Console with Google Cloud Project\n\nGo to:\n\n\n\n    Google Play Console\n    → Setup\n    → API access\n\n\nCreate or link a Google Cloud Project.\n\nThis project will be used to create the service account that GitHub Actions will use.\n\n##  Step 3: Enable Google Play Android Developer API\n\nIn Google Cloud Console:\n\n\n\n    APIs & Services\n    → Library\n    → Google Play Android Developer API\n    → Enable\n\n\nWithout this API, your GitHub workflow cannot upload Android App Bundles to Play Console.\n\n##  Step 4: Create Service Account\n\nIn Google Cloud Console:\n\n\n\n    IAM & Admin\n    → Service Accounts\n    → Create Service Account\n\n\nExample name:\n\n\n\n    github-actions-playstore-release\n\n\nAfter creating it, copy the service account email.\n\nIt will look like this:\n\n\n\n    github-actions-playstore-release@your-project-id.iam.gserviceaccount.com\n\n\n##  Step 5: Create JSON Key\n\nOpen the created service account.\n\nGo to:\n\n\n\n    Keys\n    → Add Key\n    → Create New Key\n    → JSON\n\n\nDownload the JSON file.\n\nImportant:\n\nNever commit this JSON file.\n\nIf you commit this file, your release pipeline is compromised. Delete the key immediately and create a new one.\n\n##  Step 6: Invite Service Account in Play Console\n\nGo back to Play Console:\n\n\n\n    Users and permissions\n    → Invite new users\n\n\nPaste the service account email.\n\nGrant app-level access to your app.\n\nRecommended permissions:\n\n\n\n    View app information\n    Create and edit draft apps\n    Release to testing tracks\n    Release to production\n    Manage testing tracks\n\n\nFor internal release only, production permission is not required.\n\nFor production workflow, production release permission is required.\n\n#  Part 2: GitHub Secrets Setup\n\nUse GitHub Environment Secrets, not plain repository secrets.\n\nCreate environments:\n\n\n\n    internal\n    production\n\n\nGo to:\n\n\n\n    GitHub Repository\n    → Settings\n    → Environments\n    → internal\n    → Environment secrets\n\n\nAdd:\n\n\n\n    PLAY_STORE_SERVICE_ACCOUNT_JSON\n\n\nPaste the full JSON content from the service account key.\n\nFor production, repeat the same under:\n\n\n\n    production\n\n\nAlso add signing secrets:\n\n\n\n    ANDROID_KEYSTORE_BASE64\n    ANDROID_KEYSTORE_PASSWORD\n    ANDROID_KEY_ALIAS\n    ANDROID_KEY_PASSWORD\n\n\n#  Part 3: Flutter Android Signing Setup\n\nYour Flutter app must generate a signed Android App Bundle.\n\nCreate:\n\n\n\n    android/key.properties\n\n\nDo not commit the real file.\n\nExample:\n\n\n\n    storePassword=your_store_password\n    keyPassword=your_key_password\n    keyAlias=upload\n    storeFile=upload-keystore.jks\n\n\nIn CI, GitHub Actions will recreate this file from secrets.\n\nAlso make sure your `android/app/build.gradle` is configured for release signing.\n\n#  Part 4: Flutter CI Workflow\n\nThis workflow runs for feature branches, develop, and main.\n\nFile:\n\n\n\n    .github/workflows/flutter-ci.yml\n\n\n\n    name: Flutter CI\n\n    on:\n      pull_request:\n        branches:\n          - develop\n          - main\n\n      push:\n        branches:\n          - develop\n          - main\n          - \"feature/**\"\n\n    permissions:\n      contents: read\n\n    jobs:\n      flutter-ci:\n        name: Flutter Analyze & Test\n        runs-on: ubuntu-latest\n        timeout-minutes: 20\n\n        steps:\n          - name: Checkout Repository\n            uses: actions/checkout@v4\n\n          - name: Setup Flutter\n            uses: ./.github/actions/setup-flutter\n\n          - name: Install Dependencies\n            run: flutter pub get\n\n          - name: Analyze\n            run: flutter analyze\n\n          - name: Run Tests\n            run: flutter test\n\n\nThis is your quality gate.\n\nIf this fails, no release should happen.\n\n#  Part 5: Internal Release Workflow\n\nThis workflow runs after Flutter CI succeeds on `main`.\n\nFile:\n\n\n\n    .github/workflows/internal-release.yml\n\n\n\n    name: Internal Release\n\n    on:\n      workflow_run:\n        workflows:\n          - Flutter CI\n        types:\n          - completed\n\n    permissions:\n      contents: read\n\n    concurrency:\n      group: internal-release-${{ github.event.workflow_run.head_branch }}\n      cancel-in-progress: true\n\n    jobs:\n      build-and-deploy:\n        name: Build & Deploy Internal Release\n        runs-on: ubuntu-latest\n        timeout-minutes: 35\n        environment: internal\n\n        if: >\n          github.event.workflow_run.conclusion == 'success' &&\n          github.event.workflow_run.head_branch == 'main'\n\n        steps:\n          - name: Checkout Repository\n            uses: actions/checkout@v4\n            with:\n              ref: ${{ github.event.workflow_run.head_branch }}\n              fetch-depth: 0\n\n          - name: Setup Flutter\n            uses: ./.github/actions/setup-flutter\n\n          - name: Restore Android Keystore\n            run: |\n              echo \"${{ secrets.ANDROID_KEYSTORE_BASE64 }}\" | base64 --decode > android/app/upload-keystore.jks\n\n          - name: Create key.properties\n            run: |\n              cat > android/key.properties <<EOF\n              storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}\n              keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}\n              keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}\n              storeFile=upload-keystore.jks\n              EOF\n\n          - name: Create Play Store Service Account JSON\n            run: |\n              echo '${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }}' > play-store-service-account.json\n\n          - name: Install Dependencies\n            run: flutter pub get\n\n          - name: Build Android App Bundle\n            run: flutter build appbundle --release\n\n          - name: Upload to Play Store Internal Testing\n            uses: r0adkll/upload-google-play@v1\n            with:\n              serviceAccountJson: play-store-service-account.json\n              packageName: com.yourcompany.yourapp\n              releaseFiles: build/app/outputs/bundle/release/app-release.aab\n              track: internal\n              status: completed\n\n\nReplace:\n\n\n\n    com.yourcompany.yourapp\n\n\nwith your actual Android package name.\n\n#  Part 6: Production Release Workflow\n\nProduction releases should not happen on every merge.\n\nThat is reckless.\n\nUse Git tags.\n\nExample:\n\n\n\n    git tag v1.0.0\n    git push origin v1.0.0\n\n\nFile:\n\n\n\n    .github/workflows/production-release.yml\n\n\n\n    name: Production Release\n\n    on:\n      push:\n        tags:\n          - \"v*.*.*\"\n\n    permissions:\n      contents: write\n\n    concurrency:\n      group: production-release\n      cancel-in-progress: false\n\n    jobs:\n      production-release:\n        name: Build & Deploy Production Release\n        runs-on: ubuntu-latest\n        timeout-minutes: 45\n        environment: production\n\n        steps:\n          - name: Checkout Repository\n            uses: actions/checkout@v4\n            with:\n              fetch-depth: 0\n\n          - name: Setup Flutter\n            uses: ./.github/actions/setup-flutter\n\n          - name: Restore Android Keystore\n            run: |\n              echo \"${{ secrets.ANDROID_KEYSTORE_BASE64 }}\" | base64 --decode > android/app/upload-keystore.jks\n\n          - name: Create key.properties\n            run: |\n              cat > android/key.properties <<EOF\n              storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}\n              keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}\n              keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}\n              storeFile=upload-keystore.jks\n              EOF\n\n          - name: Create Play Store Service Account JSON\n            run: |\n              echo '${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }}' > play-store-service-account.json\n\n          - name: Install Dependencies\n            run: flutter pub get\n\n          - name: Build Android App Bundle\n            run: flutter build appbundle --release\n\n          - name: Upload to Play Store Production\n            uses: r0adkll/upload-google-play@v1\n            with:\n              serviceAccountJson: play-store-service-account.json\n              packageName: com.yourcompany.yourapp\n              releaseFiles: build/app/outputs/bundle/release/app-release.aab\n              track: production\n              status: completed\n\n          - name: Create GitHub Release\n            uses: softprops/action-gh-release@v2\n            with:\n              files: build/app/outputs/bundle/release/app-release.aab\n\n\n#  Part 7: Versioning\n\nFlutter uses version from `pubspec.yaml`.\n\nExample:\n\n\n\n    version: 1.0.0+1\n\n\nFormat:\n\n\n\n    versionName+versionCode\n\n\nExample:\n\n\n\n    1.0.0+1\n\n\nMeans:\n\n\n\n    versionName = 1.0.0\n    versionCode = 1\n\n\nEvery Play Store upload must have a higher `versionCode`.\n\nIf you upload the same version code again, Play Console will reject it.\n\nFor next release:\n\n\n\n    version: 1.0.1+2\n\n\n#  Part 8: Recommended Branch Strategy\n\nUse this:\n\n\n\n    feature/*  → development work\n    develop    → integration branch\n    main       → release-ready branch\n    tag        → production release\n\n\nFlow:\n\n\n\n    feature/login-screen\n          ↓\n    develop\n          ↓\n    main\n          ↓\n    internal testing\n          ↓\n    tag v1.0.0\n          ↓\n    production\n\n\nDo not release production directly from a random feature branch.\n\nThat is not CI/CD. That is gambling.\n\n#  Part 9: Required GitHub Environments\n\nCreate these environments:\n\n\n\n    internal\n    production\n\n\nRecommended protection:\n\nFor `internal`:\n\n\n\n    No manual approval required\n\n\nFor `production`:\n\n\n\n    Required reviewers enabled\n\n\nThis means production release waits for approval before uploading to Google Play.\n\n#  Part 10: Common Mistakes\n\n##  Mistake 1: Service account created but not invited to Play Console\n\nCreating a service account in Google Cloud is not enough.\n\nYou must invite the service account email in Play Console.\n\n##  Mistake 2: Missing release permissions\n\nIf the service account does not have release permissions, upload will fail.\n\nGrant only the permissions required for that environment.\n\nInternal environment should not need production release access.\n\n##  Mistake 3: Committing the JSON key\n\nNever commit:\n\n\n\n    play-store-service-account.json\n    upload-keystore.jks\n    key.properties\n\n\nAdd them to `.gitignore`.\n\n\n\n    android/key.properties\n    android/app/upload-keystore.jks\n    play-store-service-account.json\n\n\n##  Mistake 4: Reusing same versionCode\n\nPlay Console requires every uploaded app bundle to have a higher version code.\n\nBad:\n\n\n\n    version: 1.0.0+1\n\n\nagain and again.\n\nGood:\n\n\n\n    version: 1.0.0+1\n    version: 1.0.1+2\n    version: 1.0.2+3\n\n\n##  Mistake 5: Production release on every main merge\n\nMain branch should trigger internal testing.\n\nProduction should trigger only from tags.\n\nThat gives you control.\n\n#  Final Flow\n\n\n    Developer pushes feature branch\n          ↓\n    Pull request to develop\n          ↓\n    Flutter CI runs\n          ↓\n    Merge develop to main\n          ↓\n    Flutter CI runs\n          ↓\n    Internal release workflow uploads AAB to Play Store Internal Testing\n          ↓\n    Testers verify app\n          ↓\n    Create git tag v1.0.0\n          ↓\n    Production release workflow uploads AAB to Play Store Production\n          ↓\n    GitHub Release is created\n\n\n#  Conclusion\n\nThis setup gives a clean Flutter Android release pipeline:\n\n  * Feature branches only run CI\n  * `develop` validates integration\n  * `main` publishes to Internal Testing\n  * Git tags publish to Production\n  * Service account handles Play Store upload\n  * GitHub Environment Secrets protect credentials\n  * Production can be protected with manual approval\n\n\n\nManual Play Store uploads are fine for the first release.\n\nAfter that, automate it.\n\nOtherwise, your release process will eventually break at the worst possible time.",
  "title": "Automating Flutter Android Releases to Google Play using GitHub Actions"
}