# How I release my Side Projects! Continuous deployment is always difficult. These are the strategies/automations I use to help ensure the apps I release are up to code! (get it?!?) # What tools do I use? ## Github workflows/actions Since my codebase lives on github I decided to use github actions as my trigger. This allows me to keep most configuration as code within github, and means that my workflow does not involve any other dashboards outside of Github and the Play Store ## Fastlane Fastlane is used as a platform agnostic (ie. host agnostic) system I use for my build steps. By deferring *what* is happening to fastlane, this means that regardless of which hosting platform I use (ex: Bitbucket, Gitlab, Github...) my fastlane scripts will work (assuming the machine is setup to run ruby!) Alternatively I could use services like CircleCI or Bitrise to abstract my build pipelines from my git host. Which is a very valid choice! I have worked with many systems where it worked very well! I have chosen against this for two reasons: 1. Fastlane is a local tool, not a service, that runs on a machine. - This ensures I can run the lanes in a similar fashion if required (ex: running tests for a failed pipeline) 2. These other tools require secondary dashboards that I would need to visit/learn to identify issues. ```Note Tools like CircleCI and Bitrise provide many other tools and services that I just dont require for my side projects. Github is "good enough" for my needs ``` ## Gradle While its possible to build android apps with other tools, Gradle is by far the easiest! I have then split the responsibilities of these tools like so: #### Github - Storage (code and secrets) - Deciding *when* things run - *when* a pull request created - *when* code is committed to `main` - the first wednesday of each month #### Fastlane - Deciding *what* is happening - *what* tests are being run - *what* artifacts are being created - *what* linting rules are required #### Gradle - Deciding *how* it happens - *how* the artifacts are being signed - linter configuration - test configuration [example github yaml](https://github.com/eholtrop/lift-bro/blob/main/.github/workflows/play_store_internal_testing.yaml) ## Pipeline Steps ### 1. Setup ```yaml steps: - name: Checkout Repo uses: actions/checkout@v2 - name: Set up Java uses: actions/setup-java@v2 with: distribution: "temurin" java-version: 17 - name: Set up Ruby 2.6 uses: ruby/setup-ruby@v1 with: ruby-version: 2.7.2 bundler-cache: true ``` Within my setup we are performing a handful of specific setup tasks. These are to prepare the current machine we are running on for our application 1. actions/checkout@v2 - this is used to pull the latest code to the current machine 2. actions/setup-java@v2 - this is used to install the required version of java for our application (in this case java 17) 3. ruby/setup-ruby@v1 - required for *fastlane* You may need to tweak the java version and ruby version to meet your needs! ### 2. Extract Secrets The two pieces of information we require ```yaml steps: - name: decode keystore id: decode_keystore uses: timheuer/[email protected] with: fileName: 'release.jks' fileDir: 'androidApp' encodedString: ${{ secrets.RELEASE_KEYSTORE }} - name: create-json id: create-json uses: jsdaniell/[email protected] with: name: "key.json" json: ${{ secrets.FASTLANE_JSON }} ``` These two steps will take the encoded keystore we have stored in our Github secrets and create a valid file for us to bundle and sign our application with. As well as extract the required FASTLANE_JSON configuration, which defines the correct play store as well as other google specific configurations ### 3. Bundle Artifact(s) Now that our machine has the correct dependencies installed as well as our extract secrets we can build our artifact to send to the Google Play Store This is also the first time we start seeing Github, Fastlane, and Gradle working together #### Github ```YAML - name: Run fastlane deploy env: STORE_PASSWORD: ${{ secrets.KEYSTORE_PW }} KEY_ALIAS: ${{ secrets.KEYSTORE_USR }} KEY_PASSWORD: ${{ secrets.KEY_PW }} run: fastlane deploy build_number:$GITHUB_RUN_NUMBER ``` Notice that we also need to create some environment variables that we set for our fastlane job. Fastlane will need to use these secrets for signing the artifact when we build! You will also notice that we are passing in the GITHUB_RUN_NUMBER. More on this later #### Fastlane ```ruby desc "Deploy a new version to the Google Play" lane :deploy do |options| gradle(task: "clean bundleRelease", flags: "-PbuildNumber=#{options[:build_number]}") supply(track: "beta") end ``` Again we see this build number passed through! What could it mean? 🤔 #### Gradle This is one of the only times where we need to touch our actual gradle files to setup the build configurations ##### Signing Configs ```ruby android { signingConfigs { register("release") { storeFile = file("release.jks") storePassword = System.getenv("STORE_PASSWORD") keyAlias = System.getenv("KEY_ALIAS") keyPassword = System.getenv("KEY_PASSWORD") } } } ``` First and foremost we need to set the Keystore alias and passwords. Since we set them as environment variables using GitHub we can reference them quite easily within gradle. Ensuring our apk is build according to plan ##### Version Code / Version Name For the veterans among you, you may be aware that when releasing to the play store you require two different "versions" to be set. 1. `versionName`: String - The name of the current version. This can be anything you like! In most cases I see companies implement some sort of `semver`. But I find that taking the current date is much more usable, especially when showing these types of values directly to users! 2. `versionCode`: Integer - An ever-incrementing unique number. If you try to release the same `versionCode` twice the play store will reject the release and you will need to rebuild your artifact starting from Step 3! ```groovy android { defaultConfig { versionCode = if (project.hasProperty("buildNumber")) { property("buildNumber").toString().toInt() } else { 1 } } } ``` And here we finally see what that pesky `buildNumber` being put to good use. Since the `GITHUB_RUN_NUMBER` increments each time we run this workflow. It means that our releases will be ever incrementing *and* unique! ###### Cons - If we push some code and a build fails, we will effectively be skipping that `versionCode`. Meaning that in the limit we could hit the MAX_VALUE of `versionCode` earlier than expected ### 4. Deploy! These steps take our freshly minted .abb and pushes it to both the Google Play Store, as well as github for safe keeping (you never know when you might need it!) ```YAML steps: - name: Upload Apk uses: actions/upload-artifact@v4 with: name: assets path: app/build/outputs/bundle/release/app-release.aab ``` ### 5. Notify (Optional) Send some notifications! Notify the stakeholders that a build has happened and that a new release is out. I currently do not do this, but have plans to set up something like this to notify a slack and/or discord channel Now.... ![[elephant_in_the_room_shitts_creek.gif]] What about tests? 😅 While in a corporate setting I will ALWAYS advocate for testing best practices. But this changes when its a personal project While I do have a few tests here and there for sanity purposes, I dont feel like the scope of my projects require a ton of testing. Testing usually helps solve a few problems: 1. Release Confidence 2. Accidental Bugs 3. Team Synergy By releasing to internal testing I get to dogfood my own apps before launching to full production. And due to the low feature set of both apps this feels "fine" for now. If either app were ever to "explode" in user base I would definitively add testing (when a PR is made to main or pre publish on main). But for now my internal, manual testing works for me and my needs. Being the sole developer on these projects also helps me keep full context of what is happening in the app. So unexpected bugs are reduced (although not fully gone! 🤣). If more developers were to ever join the project Testing would become a high priority! Both apps are built in a way where testing would be easy to add, But for now we are test free! Testing in general is a strategy used to increase confidence in releases. I am currently confident in my releases, so testing would be a increase in scope as well as increase the amount of time it takes for me to release!