Skip to content

Instantly share code, notes, and snippets.

@limkhashing
Last active November 22, 2023 19:36
Show Gist options
  • Select an option

  • Save limkhashing/bca46ed2165c7be7d44c32d2d16bfa2b to your computer and use it in GitHub Desktop.

Select an option

Save limkhashing/bca46ed2165c7be7d44c32d2d16bfa2b to your computer and use it in GitHub Desktop.
GitHub Actions Android workflow with creating GitHub tagging and release
name: Android Production CI/CD
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
pull_request:
types: [synchronize, opened, reopened, ready_for_review]
branches: [master, development]
paths-ignore:
- '.idea/**'
- '.gitattributes'
- '.github/**.json'
- '.gitignore'
- '.gitmodules'
- '**.md'
- 'LICENSE'
- 'NOTICE'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
inputs:
new_server_endpoint:
description: 'Override the default server endpoint'
required: false
default: 'https://api.myfave.com'
env:
# Keystore Password
SIGNING_KEYSTORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
# Keystore Alias
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
# Keystore Alias Password
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
# Token and Key needed for Core BuildConfig
APPSFLYER_DEV_KEY: ${{ secrets.APPSFLYER_DEV_KEY }}
COUNTRY_STATE_API_KEY: ${{ secrets.COUNTRY_STATE_API_KEY }}
ADYEN_ENV_TOKEN: ${{ secrets.ADYEN_ENV_TOKEN }}
HUAWEI_APP_ID: ${{ secrets.HUAWEI_APP_ID }}
GOOGLE_BACKEND_CID: ${{ secrets.GOOGLE_BACKEND_CID }}
ZENDESK_MESSAGING_CHANNEL_KEY: ${{ secrets.ZENDESK_MESSAGING_CHANNEL_KEY }}
permissions:
contents: write # Required in order to submit the graph via the GitHub API
pull-requests: write # Requires write permission to comment on PRs
jobs:
build-and-distribute-android:
if: ${{ !github.event.pull_request.draft }}
runs-on: macos-latest-xl
environment: production
steps:
- name: Checkout the branch
uses: actions/checkout@v4
- name: Setup JDK 18
uses: actions/setup-java@v3
with:
distribution: zulu
java-version: 18
- name: Setup Gradle
id: setup-gradle
uses: gradle/gradle-build-action@v2
with:
dependency-graph: generate-and-submit
cache-disabled: false
# Only write to the cache for builds of the PRs that target 'master' branch
# Other PRs and workflow dispatch will read the caches from the 'master' branch instead
cache-read-only: ${{ github.base_ref != 'master' }}
# cache-write-only: true
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- name: Create Services Account and configuration files
run: |
mkdir app/src/release
jq -n ${{ secrets.SERVICE_CREDENTIALS_FIREBASE_APP_DISTRIBUTION }} > fave-production-service-account-firebase-app-distribution.json
jq -n ${{ secrets.AGCONNECT_SERVICES }} > app/src/release/agconnect-services.json
jq -n ${{ secrets.GOOGLE_SERVICES }} > app/src/release/google-services.json
- name: Decode Keystore from Base64 and save it to a file
env:
ENCODED_KEYSTORE: ${{ secrets.SIGNING_KEYSTORE }}
run: |
tmp_keystore_directory="${RUNNER_TEMP}"/keystore
mkdir "${tmp_keystore_directory}"
echo $ENCODED_KEYSTORE | base64 -D > "${tmp_keystore_directory}"/keystore.jks
echo "SIGNING_KEYSTORE_FILE_PATH="${tmp_keystore_directory}"/keystore.jks" >> $GITHUB_ENV
# We only run this job if we trigger workflow manually and the new server endpoint is not empty
- name: Replace the default server endpoint with the new server endpoint
if: ${{ ( github.event_name == 'workflow_dispatch') && (inputs.new_server_endpoint != '') }}
run: |
sed -i '' -e "s#https://api.myfave.com#${{ inputs.new_server_endpoint }}#g" core/src/main/java/com/kfit/fave/core/utils/NetworkUtils.kt
cat core/src/main/java/com/kfit/fave/core/utils/NetworkUtils.kt # Print the file contents after changing
- name: Run unit tests
env:
SIGNING_KEYSTORE_FILE_PATH: ${{ env.SIGNING_KEYSTORE_FILE_PATH }}
SIGNING_KEYSTORE_PASSWORD: ${{ env.SIGNING_KEYSTORE_PASSWORD }}
SIGNING_KEY_ALIAS: ${{ env.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ env.SIGNING_KEY_PASSWORD }}
APPSFLYER_DEV_KEY: ${{ env.APPSFLYER_DEV_KEY }}
COUNTRY_STATE_API_KEY: ${{ env.COUNTRY_STATE_API_KEY }}
ADYEN_ENV_TOKEN: ${{ env.ADYEN_ENV_TOKEN }}
HUAWEI_APP_ID: ${{ env.HUAWEI_APP_ID }}
GOOGLE_BACKEND_CID: ${{ env.GOOGLE_BACKEND_CID }}
ZENDESK_MESSAGING_CHANNEL_KEY: ${{ env.ZENDESK_MESSAGING_CHANNEL_KEY }}
VERSION_CODE: ${{ github.run_number }}
run: ./gradlew testRelease --stacktrace
- name: Build release app bundle
env:
SIGNING_KEYSTORE_FILE_PATH: ${{ env.SIGNING_KEYSTORE_FILE_PATH }}
SIGNING_KEYSTORE_PASSWORD: ${{ env.SIGNING_KEYSTORE_PASSWORD }}
SIGNING_KEY_ALIAS: ${{ env.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ env.SIGNING_KEY_PASSWORD }}
APPSFLYER_DEV_KEY: ${{ env.APPSFLYER_DEV_KEY }}
COUNTRY_STATE_API_KEY: ${{ env.COUNTRY_STATE_API_KEY }}
ADYEN_ENV_TOKEN: ${{ env.ADYEN_ENV_TOKEN }}
HUAWEI_APP_ID: ${{ env.HUAWEI_APP_ID }}
GOOGLE_BACKEND_CID: ${{ env.GOOGLE_BACKEND_CID }}
ZENDESK_MESSAGING_CHANNEL_KEY: ${{ env.ZENDESK_MESSAGING_CHANNEL_KEY }}
VERSION_CODE: ${{ github.run_number }}
run: ./gradlew bundleRelease --stacktrace
- name: Export and unzip APK from app bundle
id: export-apk
uses: mukeshsolanki/bundletool-action@v1.0.2
with:
aabFile: app/build/outputs/bundle/release/app-release.aab
base64Keystore: ${{ secrets.SIGNING_KEYSTORE }}
keystorePassword: ${{ env.SIGNING_KEYSTORE_PASSWORD }}
keystoreAlias: ${{ env.SIGNING_KEY_ALIAS }}
keyPassword: ${{ env.SIGNING_KEY_PASSWORD }}
- name: Upload aab to Firebase App Distribution
run: |
echo "${{ github.event.pull_request.title }}" > releaseNotes.txt
echo "" >> releaseNotes.txt
echo "${{ github.event.pull_request.body }}" >> releaseNotes.txt
echo "Author: ${{ github.event.pull_request.user.login }}" >> releaseNotes.txt
./gradlew appDistributionUploadRelease --artifactType="AAB" --serviceCredentialsFile="fave-production-service-account-firebase-app-distribution.json" --artifactPath="app/build/outputs/bundle/release/app-release.aab" --appId="${{ secrets.FIREBASE_APP_ID }}" --groups="Fave-Engineer, Fave-QA, Fave-Product" --releaseNotesFile=releaseNotes.txt
- name: Upload to BrowserStack for QA automation if is Regression testing
if: ${{ github.base_ref == 'development' }}
uses: Gildofj/upload-app-to-app-live-browserstack@master
with:
app-path: ${{ steps.export-apk.outputs.apkPath }}
browserstack-username: ${{ secrets.BROWSERSTACK_USERNAME }}
browserstack-accesskey: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
- name: Upload release app bundle and apk to artifacts
uses: actions/upload-artifact@v3
with:
name: release-android-app-bundle-and-apk
path: |
app/build/outputs/bundle/release/app-release.aab
${{ steps.export-apk.outputs.apkPath }}
app/build/outputs/mapping/release/mapping.txt
app/build/intermediates/merged_native_libs/release/out/lib
retention-days: 5
- name: Set a custom version number that is offset by 30000 (run_number + 30000)
id: set-version-number
uses: actions/github-script@v7
with:
result-encoding: string
script: |
const runNumber = ${{ github.run_number }};
const versionNumber = runNumber + 30000;
core.setOutput('versionNumber', versionNumber);
core.setOutput('versionCode', runNumber);
- name: Print build number summary
run: |
{
echo "## Build and distribute Android success! 🚀 "
echo "The build number is ${{steps.set-version-number.outputs.versionNumber}}"
} >> $GITHUB_STEP_SUMMARY
deployment:
if: ${{ github.base_ref == 'master' }}
runs-on: ubuntu-latest
environment: production
needs: build-and-distribute-android
steps:
- name: Checkout the branch
uses: actions/checkout@v4
- name: Download artifacts from build job
uses: actions/download-artifact@v3
with:
name: release-android-app-bundle-and-apk
- name: Create Services Account and configuration file
run: |
jq -n ${{ secrets.SERVICE_CREDENTIALS_GOOGLE_PLAY }} > fave-production-service-account-upload-google-play.json
- name: Upload release app bundle to Google Play Store and submit review
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJson: fave-production-service-account-upload-google-play.json
packageName: com.kfit.fave
releaseFiles: app/build/outputs/bundle/release/app-release.aab
track: production
status: completed
mappingFile: app/build/outputs/mapping/release/mapping.txt
debugSymbols: app/build/intermediates/merged_native_libs/release/out/lib
- name: Upload release app bundle to Huawei AppGallery and submit review
uses: kienlv58/appgallery-deply-action@main
with:
client-id: ${{ secrets.HUAWEI_CLIENT_ID }}
client-key: ${{ secrets.HUAWEI_CLIENT_KEY }}
app-id: ${{secrets.HUAWEI_APP_ID}}
file-extension: "aab"
file-path: "app/build/outputs/bundle/release/app-release.aab"
file-name: "app-release.aab"
submit: true
- name: Upload release app bundle and apk to artifacts
uses: actions/upload-artifact@v3
with:
name: release-android-app-bundle-and-apk
path: |
app/build/outputs/bundle/release/app-release.aab
app-release.apk
app/build/outputs/mapping/release/mapping.txt
app/build/intermediates/merged_native_libs/release/out/lib
retention-days: 5
github-release:
if: ${{ github.base_ref == 'master' }}
runs-on: ubuntu-latest
environment: production
needs: deployment
steps:
- name: Checkout the branch
uses: actions/checkout@v4
- name: Download artifacts from build job
uses: actions/download-artifact@v3
with:
name: release-android-app-bundle-and-apk
- name: Create a new tag
id: tag-version
uses: actions/github-script@v7
with:
result-encoding: string
script: |
const fs = require('fs');
const filePath = 'app/build.gradle.kts';
const fileContent = fs.readFileSync(filePath, 'utf-8');
const regexPattern = /versionName = "([0-9\.]{6}([-\+][\w\.0-9]+)?)"/;
const match = fileContent.match(regexPattern);
const versionName = match ? match[1] : null;
const prefixedVersionName = versionName ? `v${versionName}` : null;
if (prefixedVersionName == null) {
throw new Error('Version name not found in file: ' + filePath);
}
console.log('Version name: ' + prefixedVersionName);
return prefixedVersionName;
- name: Install archiver
run: npm install archiver
- name: Zip files for release artifacts
uses: actions/github-script@v7
with:
script: |
const archiver = require('archiver');
const fs = require('fs');
const path = require('path');
const outputFilePath = 'artifacts.zip';
const currentWorkingDirectory = process.cwd();
// Create a writable stream for the zip file
const output = fs.createWriteStream(outputFilePath);
const archive = archiver('zip');
output.on('close', function () {
console.log(archive.pointer() + ' total bytes');
console.log('archiver has been finalized and the output file descriptor has closed.');
});
output.on('end', function () {
console.log('Data has been drained');
});
// Pipe the output file stream to the archive
archive.pipe(output);
// Add files to the zip archive
const filesToAdd = [
'app/build/outputs/bundle/release/app-release.aab',
'app-release.apk',
'app/build/outputs/mapping/release/mapping.txt',
'app/build/intermediates/merged_native_libs/release/out/lib',
];
filesToAdd.forEach((file) => {
const fullPath = path.resolve(currentWorkingDirectory, file);
const stat = fs.statSync(fullPath);
if (stat.isFile()) {
archive.file(fullPath, { name: path.basename(file) });
} else if (stat.isDirectory()) {
archive.directory(fullPath, path.basename(file));
}
});
// Finalize the archive
archive.finalize();
- name: Push the new tag and new release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create ${{steps.tag-version.outputs.result}} --target development --generate-notes ./artifacts.zip
review-dependency-graph:
if: ${{ github.event_name == 'pull_request' }}
needs: build-and-distribute-android
runs-on: ubuntu-latest
steps:
- name: Checkout the branch
uses: actions/checkout@v4
- name: Perform dependency review
uses: actions/dependency-review-action@v3
with:
fail-on-severity: critical
comment-summary-in-pr: on-failure
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment