cd: improve release workflow w/nightly

This commit is contained in:
Zane Schepke 2024-07-27 04:15:57 -04:00
parent 7a2d96fcd7
commit ebf7521fa1
8 changed files with 221 additions and 249 deletions

View File

@ -1,124 +0,0 @@
name: Android CI Tag Deployment (Pre-release)
on:
workflow_dispatch:
push:
tags:
- '*.*.*-**'
jobs:
build:
name: Build Signed APK
runs-on: ubuntu-latest
env:
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
GH_USER: ${{ secrets.GH_USER }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# Here we need to decode keystore.jks from base64 string and place it
# in the folder specified in the release signing configuration
- name: Decode Keystore
id: decode_keystore
uses: timheuer/base64-to-file@v1.2
with:
fileName: ${{ env.KEY_STORE_FILE }}
fileDir: ${{ env.KEY_STORE_LOCATION }}
encodedString: ${{ secrets.KEYSTORE }}
# create keystore path for gradle to read
- name: Create keystore path env var
run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- name: Create service_account.json
id: createServiceAccount
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
# Build and sign APK ("-x test" argument is used to skip tests)
# add fdroid flavor for apk upload
- name: Build Fdroid Release APK
run: ./gradlew :app:assembleFdroidRelease -x test
# get fdroid flavor release apk path
- name: Get apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
- name: Get version code
run: |
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK
uses: actions/upload-artifact@v4.3.4
with:
name: wgtunnel
path: ${{ steps.apk-path.outputs.path }}
- name: Download APK from build
uses: actions/download-artifact@v4
with:
name: wgtunnel
- name: Create Release with Fastlane changelog notes
id: create_release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# fix hardcode changelog file name
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
draft: false
prerelease: true
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
- name: Install apksigner
run: |
sudo apt-get update
sudo apt-get install -y apksigner
- name: Get checksum
id: checksum
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
- name: Append checksum
id: append_checksum
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
body: |
SHA256 fingerprint:
```${{ steps.checksum.outputs.checksum }}```
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
draft: false
prerelease: true
append_body: true
- name: Deploy with fastlane
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2' # Not needed with a .ruby-version file
bundler-cache: true
- name: Distribute app to Beta track 🚀
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane beta)

View File

@ -1,17 +1,40 @@
# name of the workflow
name: Android CI Tag Deployment (Release)
name: release-android
on:
schedule:
- cron: "4 3 * * *"
workflow_dispatch:
push:
tags:
- '*.*.*'
- '!*.*.*-**'
inputs:
track:
type: choice
description: "Google play release track"
options:
- none
- internal
- alpha
- beta
- production
default: alpha
required: true
release_type:
type: choice
description: "GitHub release type"
options:
- none
- prerelease
- nightly
- release
default: release
required: true
tag_name:
description: "Tag name for release"
required: false
default: nightly
jobs:
build:
name: Build Signed APK
if: ${{ inputs.release_type != 'none' }}
runs-on: ubuntu-latest
env:
@ -57,44 +80,66 @@ jobs:
# Build and sign APK ("-x test" argument is used to skip tests)
# add fdroid flavor for apk upload
- name: Build Fdroid Release APK
run: ./gradlew :app:assembleFdroidRelease -x test
if: ${{ inputs.release_type != '' && inputs.release_type != 'nightly' }}
run: |
./gradlew :app:assembleFdroidRelease -x test
echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
- name: Build Fdroid Nightly APK
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' }}
run: |
./gradlew :app:assembleFdroidNightly -x test
echo "APK_PATH=$(find . -regex '^.*/build/outputs/apk/fdroid/nightly/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
# get fdroid flavor release apk path
- name: Get apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/release/.*\.apk$' -type f | head -1)" >> $GITHUB_OUTPUT
- name: Get version code
run: |
version_code=$(grep "VERSION_CODE" buildSrc/src/main/kotlin/Constants.kt | awk '{print $5}' | tr -d '\n')
echo "VERSION_CODE=$version_code" >> $GITHUB_ENV
# Save the APK after the Build job is complete to publish it as a Github release in the next job
- name: Upload APK
uses: actions/upload-artifact@v4.3.4
with:
name: wgtunnel
path: ${{ steps.apk-path.outputs.path }}
path: ${{ env.APK_PATH }}
- name: Download APK from build
uses: actions/download-artifact@v4
with:
name: wgtunnel
- name: Repository Dispatch for my F-Droid repo
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.PAT }}
repository: zaneschepke/fdroid
event-type: fdroid-update
- name: Create Release with Fastlane changelog notes
id: create_release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
body_path: ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
draft: false
prerelease: false
files: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
- name: Set version release notes
if: ${{ inputs.release_type == 'release' || inputs.release_type == 'prerelease' }}
run: |
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt)"
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "$RELEASE_NOTES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: On nightly release
if: ${{ contains(env.TAG_NAME, 'nightly') }}
run: |
echo "RELEASE_NOTES=Nightly build of the latest development version of the android client." >> $GITHUB_ENV
gh release delete nightly --yes || true
# Setup TAG_NAME, which is used as a general "name"
- if: github.event_name == 'workflow_dispatch'
run: echo "TAG_NAME=${{ github.event.inputs.tag_name }}" >> $GITHUB_ENV
- if: github.event_name == 'schedule'
run: echo "TAG_NAME=nightly-android" >> $GITHUB_ENV
- name: On nightly release
if: ${{ contains(env.TAG_NAME, 'nightly') }}
run: |
echo "RELEASE_NOTES=Nightly build of the latest development version of the android client." >> $GITHUB_ENV
gh release delete nightly-android --yes || true
- name: Install apksigner
run: |
@ -103,29 +148,47 @@ jobs:
- name: Get checksum
id: checksum
run: echo "checksum=$(apksigner verify -print-certs ${{ steps.apk-path.outputs.path }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
run: echo "checksum=$(apksigner verify -print-certs ${{ env.APK_PATH }} | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
- name: Append checksum
id: append_checksum
- name: Create Release with Fastlane changelog notes
id: create_release
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
body: |
${{ env.RELEASE_NOTES }}
SHA256 fingerprint:
```${{ steps.checksum.outputs.checksum }}```
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
tag_name: ${{ env.TAG_NAME }}
name: ${{ env.TAG_NAME }}
draft: false
prerelease: false
append_body: true
prerelease: ${{ inputs.release_type == 'prerelease' || inputs.release_type == '' || input.release_type == 'nightly' }}
make_latest: ${{ inputs.release_type == 'release' }}
files: ${{ github.workspace }}/${{ env.APK_PATH }}
publish-play:
if: ${{ inputs.track != 'none' && inputs.track != '' }}
name: Publish to Google Play
runs-on: ubuntu-latest
env:
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
GH_USER: ${{ secrets.GH_USER }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
steps:
- name: Deploy with fastlane
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2' # Not needed with a .ruby-version file
bundler-cache: true
- name: Distribute app to Prod track 🚀
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane production)
run: (cd ${{ github.workspace }} && bundle install && bundle exec fastlane ${{ inputs.track }})

View File

@ -7,6 +7,7 @@ plugins {
alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.ksp)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.grgit)
}
android {
@ -22,8 +23,8 @@ android {
applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK
targetSdk = Constants.TARGET_SDK
versionCode = Constants.VERSION_CODE
versionName = Constants.VERSION_NAME
versionCode = versionCode()
versionName = versionName()
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
@ -37,44 +38,10 @@ android {
signingConfigs {
create(Constants.RELEASE) {
val properties =
Properties().apply {
// created local file for signing details
try {
load(file("signing.properties").reader())
} catch (_: Exception) {
load(file("signing_template.properties").reader())
}
}
// try to get secrets from env first for pipeline build, then properties file for local
// build
storeFile =
file(
System.getenv()
.getOrDefault(
Constants.KEY_STORE_PATH_VAR,
properties.getProperty(Constants.KEY_STORE_PATH_VAR),
),
)
storePassword =
System.getenv()
.getOrDefault(
Constants.STORE_PASS_VAR,
properties.getProperty(Constants.STORE_PASS_VAR),
)
keyAlias =
System.getenv()
.getOrDefault(
Constants.KEY_ALIAS_VAR,
properties.getProperty(Constants.KEY_ALIAS_VAR),
)
keyPassword =
System.getenv()
.getOrDefault(
Constants.KEY_PASS_VAR,
properties.getProperty(Constants.KEY_PASS_VAR),
)
storeFile = getStoreFile()
storePassword = getSigningProperty(Constants.STORE_PASS_VAR)
keyAlias = getSigningProperty(Constants.KEY_ALIAS_VAR)
keyPassword = getSigningProperty(Constants.KEY_PASS_VAR)
}
}
@ -102,9 +69,13 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
signingConfig = signingConfigs.getByName(Constants.RELEASE)
signingConfig = signingConfigs.getByName(signingConfigName())
}
debug { isDebuggable = true }
create("nightly") {
initWith(getByName("release"))
}
}
flavorDimensions.add(Constants.TYPE)
productFlavors {
@ -213,3 +184,11 @@ dependencies {
// splash
implementation(libs.androidx.core.splashscreen)
}
fun versionCode() : Int {
return if(!isNightlyBuild()) Constants.VERSION_CODE else Constants.VERSION_CODE + Constants.NIGHTLY_VERSION_CODE
}
fun versionName() : String {
return if(!isNightlyBuild()) Constants.VERSION_NAME else Constants.VERSION_NAME + "-${grgitService.service.get().grgit.head().abbreviatedId}"
}

View File

@ -1,47 +0,0 @@
import org.gradle.api.invocation.Gradle
import java.io.File
object BuildHelper {
private fun getCurrentFlavor(gradle: Gradle): String {
val taskRequestsStr = gradle.startParameter.taskRequests.toString()
val pattern: java.util.regex.Pattern =
if (taskRequestsStr.contains("assemble")) {
java.util.regex.Pattern.compile("assemble(\\w+)(Release|Debug)")
} else {
java.util.regex.Pattern.compile("bundle(\\w+)(Release|Debug)")
}
val matcher = pattern.matcher(taskRequestsStr)
val flavor =
if (matcher.find()) {
matcher.group(1).lowercase()
} else {
print("NO FLAVOR FOUND")
""
}
return flavor
}
fun getLocalProperty(key: String, file: String = "local.properties"): String? {
val properties = java.util.Properties()
val localProperties = File(file)
if (localProperties.isFile) {
java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8)
.use { reader ->
properties.load(reader)
}
} else return null
return properties.getProperty(key)
}
fun isGeneralFlavor(gradle: Gradle): Boolean {
return getCurrentFlavor(gradle) == "general"
}
fun isReleaseBuild(gradle: Gradle): Boolean {
return (gradle.startParameter.taskNames.size > 0 &&
gradle.startParameter.taskNames[0].contains(
"Release",
))
}
}

View File

@ -13,5 +13,8 @@ object Constants {
const val KEY_STORE_PATH_VAR = "KEY_STORE_PATH"
const val RELEASE = "release"
const val DEBUG = "debug"
const val TYPE = "type"
const val NIGHTLY_VERSION_CODE = 42
}

View File

@ -0,0 +1,92 @@
import org.gradle.api.Project
import org.gradle.api.invocation.Gradle
import java.io.File
import java.util.Properties
private fun getCurrentFlavor(gradle: Gradle): String {
val taskRequestsStr = gradle.startParameter.taskRequests.toString()
val pattern: java.util.regex.Pattern =
if (taskRequestsStr.contains("assemble")) {
java.util.regex.Pattern.compile("assemble(\\w+)(Release|Debug)")
} else {
java.util.regex.Pattern.compile("bundle(\\w+)(Release|Debug)")
}
val matcher = pattern.matcher(taskRequestsStr)
val flavor =
if (matcher.find()) {
matcher.group(1).lowercase()
} else {
print("NO FLAVOR FOUND")
""
}
return flavor
}
fun getLocalProperty(key: String, file: String = "local.properties"): String? {
val properties = java.util.Properties()
val localProperties = File(file)
if (localProperties.isFile) {
java.io.InputStreamReader(java.io.FileInputStream(localProperties), Charsets.UTF_8)
.use { reader ->
properties.load(reader)
}
} else return null
return properties.getProperty(key)
}
fun Project.isGeneralFlavor(gradle: Gradle): Boolean {
return getCurrentFlavor(gradle) == "general"
}
fun Project.isReleaseBuild(): Boolean {
return (gradle.startParameter.taskNames.size > 0 &&
gradle.startParameter.taskNames[0].contains(
"Release",
))
}
fun Project.isNightlyBuild(): Boolean {
return (gradle.startParameter.taskNames.size > 0 &&
gradle.startParameter.taskNames[0].contains(
"Nightly",
))
}
fun Project.getSigningProperties() : Properties {
return Properties().apply {
// created local file for signing details
try {
load(file("signing.properties").reader())
} catch (_: Exception) {
load(file("signing_template.properties").reader())
}
}
}
fun Project.getStoreFile() : File {
return file(
System.getenv()
.getOrDefault(
Constants.KEY_STORE_PATH_VAR,
getSigningProperties().getProperty(Constants.KEY_STORE_PATH_VAR),
),
)
}
fun Project.getSigningProperty(property: String) : String {
// try to get secrets from env first for pipeline build, then properties file for local
return System.getenv()
.getOrDefault(
property,
getSigningProperties().getProperty(property),
)
}
fun Project.signingConfigName() : String {
return if(getSigningProperty(Constants.KEY_PASS_VAR) == "") Constants.DEBUG else Constants.RELEASE
}

View File

@ -29,6 +29,7 @@ composeBom = "2024.06.00"
compose = "1.6.8"
zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1"
gradlePlugins-grgit="5.2.2"
#plugins
material = "1.12.0"
@ -99,4 +100,5 @@ hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndro
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
androidLibrary = { id = "com.android.library", version.ref = "androidGradlePlugin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
grgit = { id = "org.ajoberstar.grgit.service", version.ref = "gradlePlugins-grgit" }

View File

@ -22,13 +22,17 @@ android {
"proguard-rules.pro",
)
}
create("nightly") {
initWith(getByName("release"))
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = Constants.JVM_TARGET
}
}