
In a previous look at GitLab and fastlane, we discussed how fastlane now automatically publishes the Gitter Android app to the Google Play Store, but at GitLab, we live on review apps, and review apps for Android applications didn't really exist… until Appetize.io came to our attention.
Just a simple extension of our existing .gitlab-ci.yml
, we can utilize
Appetize.io to spin up review apps of our Android application.
If you'd rather just skip to the end, you can see my MR to the Gitter Android project.
Setting up Fastlane
Fortunately for us, fastlane has integrated support for Appetize.io, so all
that's needed to hit Appetize is the addition of a new lane
:
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index eb47819..f013a86 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -32,6 +32,13 @@ platform :android do
gradle(task: "test")
end
+ desc 'Pushes the app to Appetize and updates a review app'
+ lane :review do
+ appetize(api_token: ENV['APPETIZE_TOKEN'],
+ path: 'app/build/outputs/apk/debug/app-debug.apk',
+ platform: 'android')
+ end
+
desc "Submit a new Internal Build to Play Store"
lane :internal do
upload_to_play_store(track: 'internal', apk: 'app/build/outputs/apk/release/app-release.apk')
APPETIZE_TOKEN
is an Appetize.io API token that can be generated on the
Appetize API docs after signing up for an account. Once we add a new job and
stage to our .gitlab-ci.yml
, we will be able to deploy our APK to Appetize and
run them in the browser!
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d9863d7..e4d0ce3 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -5,6 +5,7 @@ stages:
- environment
- build
- test
+ - review
- internal
- alpha
- beta
@@ -81,6 +82,16 @@ buildRelease:
environment:
name: production
+deployReview:
+ stage: review
+ image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
+ script:
+ - bundle exec fastlane review
+ only:
+ - branches
+ except:
+ - master
+
testDebug:
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
stage: test
Great! Review apps will be deployed when branches other than master
build.
Unfortunately, there is no environment
block, so there's nothing linking these
deployed review apps to GitLab. Let's fix that next.
Dynamic Environment URLs
Previously, GitLab only liked environment URLs that used pre-existing CI
variables (like $CI_COMMT_REF_NAME
) in their definition. Since 12.9, however,
a new way of defining environment urls with alternative variables exists.
By creating a dotenv
file and submitting it as an artifact
in our build, we
can define custom variables to use in our environment's URL. As all Appetize.io
app URLs take the pattern of https://appetize.io.app/$PUBLIC_KEY
, where
$PUBLIC_KEY
is randomly generated when the app is created, we need to get the
public key from the Appetize response in our Fastfile
, and put it in a
dotenv
file.
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 7b5f9d1..ae3867c 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -13,6 +13,13 @@
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
+
+def update_deployment_url(pub_key)
+ File.open('../deploy.env', 'w') do |f|
+ f.write("APPETIZE_PUBLIC_KEY=#{pub_key}")
+ end
+end
+
default_platform(:android)
platform :android do
@@ -37,6 +44,7 @@ platform :android do
appetize(api_token: ENV['APPETIZE_TOKEN'],
path: 'app/build/outputs/apk/debug/app-debug.apk',
platform: 'android')
+ update_deployment_url(lane_context[SharedValues::APPETIZE_PUBLIC_KEY])
end
desc "Submit a new Internal Build to Play Store"
We also need to add an environment
block to our .gitlab-ci.yml
to capture an
environment name and URL.
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f5a8648..c834077 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -85,12 +85,18 @@ buildCreateReleaseNotes:
deployReview:
stage: review
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ url: https://appetize.io/app/$APPETIZE_PUBLIC_KEY
script:
- bundle exec fastlane review
only:
- branches
except:
- master
+ artifacts:
+ reports:
+ dotenv: deploy.env
testDebug:
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
Once committed, pushed, and a pipeline runs, we should see our environment deployed!
Optimizing Updates
After running with this for a bit, we realized that we were accidentally creating a new app on Appetize.io with every new build! Their docs specify how to update existing apps, so we went about seeing if we could smartly update existing environments.
Spoiler alert: We could.
First, we need to save the public key granted to us by Appetize.io somewhere. We
decided to put it in a JSON file and save that as an artifact of the build.
Fortunately, the Fastfile
is just ruby, which allows us to quickly write it
out to a file with a few lines of code, as well as attempt to fetch the artifact
for the last build of the current branch.
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index ae3867c..61e9226 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -13,8 +13,32 @@
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
+require 'net/http'
+require 'json'
+
+GITLAB_TOKEN = ENV['PRIVATE_TOKEN']
+PROJECT_ID = ENV['CI_PROJECT_ID']
+REF = ENV['CI_COMMIT_REF_NAME']
+JOB = ENV['CI_JOB_NAME']
+API_ROOT = ENV['CI_API_V4_URL']
+
+def public_key
+ uri = URI("#{API_ROOT}/projects/#{PROJECT_ID}/jobs/artifacts/#{REF}/raw/appetize-information.json?job=#{JOB}")
+ http = Net::HTTP.new(uri.host, uri.port)
+ http.use_ssl = true
+ req = Net::HTTP::Get.new(uri)
+ req['PRIVATE-TOKEN'] = GITLAB_TOKEN
+ response = http.request(req)
+ return '' if response.code.equal?('404')
+
+ appetize_info = JSON.parse(response.body)
+ appetize_info['publicKey']
+end
def update_deployment_url(pub_key)
+ File.open('../appetize-information.json', 'w') do |f|
+ f.write(JSON.generate(publicKey: pub_key))
+ end
File.open('../deploy.env', 'w') do |f|
f.write("APPETIZE_PUBLIC_KEY=#{pub_key}")
end
@@ -42,6 +66,7 @@ platform :android do
desc 'Pushes the app to Appetize and updates a review app'
lane :review do
appetize(api_token: ENV['APPETIZE_TOKEN'],
+ public_key: public_key,
path: 'app/build/outputs/apk/debug/app-debug.apk',
platform: 'android')
update_deployment_url(lane_context[SharedValues::APPETIZE_PUBLIC_KEY])
When we go to deploy our app to Appetize, we hit the Jobs API to see if we
have a public key for this branch. If the API returns a 404
, we know we are
building a fresh branch and return an empty string, else we parse the JSON and
return our public key. The Fastlane docs state the appetize
action can take
a public_key
to update an existing app. Here, ''
is considered the same as
not providing a public key, so a new application is still deployed as we expect.
NOTE: If you've read the diff
closely, you'll notice the usage of an
environment variable called PRIVATE_TOKEN
. This is a GitLab private token
created with the read_api
scope and injected into our build as an environment
variable. This is required to authenticate with the GitLab API and fetch
artifacts.
Once we update .gitlab-ci.yml
to save the new appetize-information.json
file
as an artifact, later builds on the same branch will be smart and update the
existing Appetize app!
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c834077..54cf3f6 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -95,6 +95,8 @@ deployReview:
except:
- master
artifacts:
+ paths:
+ - appetize-information.json
reports:
dotenv: deploy.env
Cleaning up
All that's left is to delete old apps from Appetize once we don't need them
anymore. We can do that by leveraging on_stop
and creating a stop
job that
will delete our app from Appetize.io
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 54cf3f6..f6ecf7e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -10,6 +10,7 @@ stages:
- alpha
- beta
- production
+ - stop
.updateContainerJob:
@@ -88,6 +89,7 @@ deployReview:
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://appetize.io/app/$APPETIZE_PUBLIC_KEY
+ on_stop: stopReview
script:
- bundle exec fastlane review
only:
@@ -100,6 +102,22 @@ deployReview:
reports:
dotenv: deploy.env
+stopReview:
+ stage: stop
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ action: stop
+ variables:
+ GIT_STRATEGY: none
+ when: manual
+ only:
+ - branches
+ except:
+ - master
+ script:
+ - apt-get -y update && apt-get -y upgrade && apt-get -y install jq curl
+ - curl --request DELETE https://[email protected]/v1/apps/`jq -r '.publicKey' < appetize-information.json`
+
testDebug:
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
stage: test
Once your MR is merged and your branch is deleted, the stopReview
job runs,
calling the DELETE
endpoint of the Appetize.io API with the public key that
is contained in appetize-information.json
. We don't need to fetch
appetize-information.json
because the artifact is already present in our build
context. This is because the stop
stage happens after the review
stage.
Conclusion
Thanks to some integration with fastlane and the addition of a couple environment variables, having the ability to create review apps for an Android project was surpsingly simple. GitLab's review apps are not just for web-based projects, even though it may take a little tinkering to get working. Appetize.io also supports iOS applications, so all mobile native applications can be turned into review apps. I would love to see this strategy be applied to a React Native project as well!