Last Week
This past week, I focused on streamlining the process of getting my app from code to users by setting up the Continuous Delivery (CD) part of a CI/CD pipeline. While I haven’t implemented Continuous Integration (CI) yet – meaning automated tests to catch regressions – the CD pipeline is a huge step forward.
Previously, releasing a new version was a tedious, multi-step manual process: write code, commit it, manually build the app bundle using Android Studio or command line, sign the bundle (a crucial security step for Google Play), log into the Google Play Console, navigate through various menus, upload the signed bundle, manually configure release settings, painstakingly compile release notes by reviewing commit history, and finally submit for review and later authorize the actual publication. This process was not only time-consuming but also prone to errors, like forgetting to increment build numbers or versions correctly.
Now, with the CD pipeline implemented using GitHub Actions and a tool called Release Please, the workflow is dramatically simplified. I write code and push it to a development branch. When ready for release, I merge this into the main branch (often via a Pull Request on GitHub). Release Please automatically analyzes the commits since the last release, determines the correct version bump (major, minor, patch), generates release notes based on commit messages, and creates a new Pull Request for the release itself. Once I review and approve this release PR, GitHub Actions takes over: it automatically builds the app bundle with the correct version and build number, signs it using credentials stored securely in GitHub Secrets, and uploads it directly to the specified track (e.g., internal testing) on the Google Play Store, including the generated release notes.
The entire process is reduced to a few clicks on GitHub, saving significant time and reducing the potential for manual errors. It’s a worthwhile investment, even for a solo developer, because it makes the release process repeatable, reliable, and much faster.
Additionally, I successfully applied for and got approved for both my Apple Developer and Google Play Developer accounts, which are necessary steps for distributing the app on both platforms.
What Does It Mean in English: Making App Updates Automatic
Imagine baking a cake. Before, every time I wanted to share a new cake (a new app version), I had to:
- Mix the ingredients (write code).
- Remember the recipe changes (check code history).
- Bake the cake (build the app).
- Put it in a special, tamper-proof box (sign the app).
- Drive to the store (log into Google Play Console).
- Fill out delivery forms (configure the release).
- Write a note explaining what kind of cake it is (write release notes).
- Hand it over for inspection (submit for review).
- Get the final okay to put it on the shelf (publish).
This took a lot of time and sometimes I’d forget a step or mess up the form.
Now, I’ve built a little robot helper (the CD pipeline). All I do is mix the ingredients (write code) and tell the robot I’m ready. The robot automatically:
- Checks my recipe notes (reads code history).
- Bakes the cake perfectly (builds the app).
- Puts it in the special box (signs the app).
- Creates the delivery label with all the right info, including what’s new (generates release notes and version numbers).
- Asks me “Does this look right?” (creates a release Pull Request).
- Once I say “Yes,” it automatically sends the cake to the store (uploads to Google Play).
It’s much faster, less work for me, and the robot doesn’t make mistakes like forgetting to label the box correctly. This means I can share new cake versions more often and reliably.
Nerdy Details: Basic Android CD with GitHub Actions & Release Please
Setting up an automated Continuous Delivery pipeline for an Android app using GitHub Actions and Release Please involves a few key components. This example assumes you have an Android project hosted on GitHub and a Google Play Developer account.
Prerequisites:
- GitHub Repository: Your Android project code hosted on GitHub.
- Google Play Developer Account: Access to publish apps.
- Google Play Service Account: Create a service account in the Google Cloud Console linked to your Play Developer account, grant it appropriate permissions (e.g., “Service Account User” and API access in Play Console for releasing apps), and download its JSON key file.
- App Signing Keystore: Your
.jks
or.keystore
file used to sign release builds.
Steps:
1. Configure Release Please:
Add a
release-please-config.json
file to the root of your repository to configure how Release Please determines version bumps:{ "packages": { ".": { "release-type": "android", // Use android-specific versioning if needed, or 'simple' "changelog-path": "CHANGELOG.md", "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": true, "draft": false, "prerelease": false } } }
Add a GitHub Actions workflow file (e.g.,
.github/workflows/release-please.yml
) to run Release Please when changes are pushed to your main branch:# .github/workflows/release-please.yml name: Release Please on: push: branches: - main # Or your primary branch jobs: release-please: runs-on: ubuntu-latest steps: - uses: google-github-actions/release-please-action@v4 with: token: ${{ secrets.GITHUB_TOKEN }} # Default token is usually sufficient release-type: android # Match your config config-file: release-please-config.json # Optional: If using a personal access token for triggering downstream workflows # personal-access-token: ${{ secrets.YOUR_PAT }}
2. Set Up GitHub Secrets:
- Navigate to your repository’s
Settings
>Secrets and variables
>Actions
. - Add the following repository secrets:
PLAY_STORE_SERVICE_ACCOUNT_JSON
: Paste the entire content of the JSON key file downloaded from Google Cloud Console.SIGNING_KEYSTORE_BASE64
: The base64 encoded content of your keystore file. You can generate this locally usingbase64 <your_keystore_file.jks> | pbcopy
(macOS) orcertutil -encode <your_keystore_file.jks> tmp.b64 && findstr /v /c:"-----BEGIN CERTIFICATE-----" /c:"-----END CERTIFICATE-----" tmp.b64 && del tmp.b64
(Windows).SIGNING_KEY_ALIAS
: The alias of your signing key.SIGNING_KEY_PASSWORD
: The password for your key alias.SIGNING_KEYSTORE_PASSWORD
: The password for the keystore file itself.
3. Create the Build and Deploy Workflow:
Add another workflow file (e.g.,
.github/workflows/build-deploy-android.yml
) triggered when a release is published by Release Please:# .github/workflows/build-deploy-android.yml name: Build and Deploy Android Release on: release: types: [published] # Triggered when Release Please creates and publishes a GitHub Release jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' # Use the JDK version required by your project distribution: 'temurin' cache: 'gradle' - name: Grant execute permission for gradlew run: chmod +x gradlew # Decode Keystore from Base64 Secret - name: Decode Keystore env: SIGNING_KEYSTORE_BASE64: ${{ secrets.SIGNING_KEYSTORE_BASE64 }} run: | echo $SIGNING_KEYSTORE_BASE64 | base64 --decode > ${{ github.workspace }}/app/release.jks # Build Release App Bundle (.aab) # Note: Assumes version name/code are handled by Release Please or set elsewhere. # If not, you might need steps here to update build.gradle/build.gradle.kts - name: Build Release AAB run: ./gradlew bundleRelease env: # Pass secrets securely as environment variables for Gradle SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} SIGNING_KEYSTORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} # Point Gradle to the decoded keystore file SIGNING_KEYSTORE_PATH: ${{ github.workspace }}/app/release.jks # Upload to Google Play Store - name: Upload AAB to Play Store (Internal Track) uses: r0adkll/[email protected] # Check for latest version with: serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }} packageName: com.yourcompany.yourapp # Replace with your actual package name releaseFiles: app/build/outputs/bundle/release/app-release.aab # Path to your generated AAB track: internal # Or alpha, beta, production # Use the release notes generated by Release Please whatsNewDirectory: '.' # Point to where the release notes might be (often extracted from release body) # Alternatively, directly use the release body content if available # userFraction: 0.5 # Optional: For staged rollouts (e.g., 50%) status: completed # Or draft, inProgress # Optional: Clean up decoded keystore - name: Clean up Keystore if: always() # Ensure cleanup runs even if previous steps fail run: rm -f ${{ github.workspace }}/app/release.jks
Explanation:
release-please.yml
: Runs on pushes tomain
. It usesgoogle-github-actions/release-please-action
to analyze commits, determine the next version, generate aCHANGELOG.md
, and create a release PR. When you merge this PR, Release Please creates a formal GitHub Release, tagging the commit.build-deploy-android.yml
: Triggered by thepublished
event of a GitHub Release (which Release Please creates).- Checks out the code corresponding to the release tag.
- Sets up the correct Java environment.
- Decodes the base64 keystore stored in secrets back into a file.
- Runs the
./gradlew bundleRelease
command. It’s crucial that yourapp/build.gradle
orapp/build.gradle.kts
is configured to read the signing configuration from environment variables (passed in theenv
block) or Gradle properties. - Uses the
r0adkll/upload-google-play
action to upload the generated.aab
file to the specified track in the Play Store, using the service account JSON secret for authentication. It can also use the release notes generated by Release Please. - Cleans up the decoded keystore file.
This setup provides a robust starting point for automating your Android release process.
Next Week
Last week, I also realized that integrating monetization features, specifically handling in-app purchases and subscriptions, is more complex than initially anticipated. The handshake process between the app, the user, and the Google Play Store requires careful implementation to ensure security and reliability.
To manage this complexity, I’ve decided to use a third-party service called RevenueCat. It seems to offer robust support for cross-platform development, including Kotlin Multiplatform, which aligns well with my tech stack. It acts as an intermediary layer, simplifying the logic needed within the app to handle purchases, validate receipts, and manage subscription statuses across different platforms (Android and potentially iOS later).
So, the main goal for next week is to dive deep into RevenueCat’s documentation and integrate it into my app. I’ll focus on setting up the necessary configurations in both the RevenueCat dashboard and the Google Play Console, implementing the RevenueCat SDK in my Kotlin code, and creating a test setup to verify that the subscription purchase flow works correctly on the Google Play Store. This will likely involve setting up test products in the Play Console and using test accounts to simulate purchases. It feels like a significant task, but crucial for the app’s business model.