From ebf7521fa1a2970b8b56d91fa38d68d2b982d2e7 Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Sat, 27 Jul 2024 04:15:57 -0400 Subject: [PATCH] cd: improve release workflow w/nightly --- .github/workflows/pre-release.yml | 124 ----------------------- .github/workflows/release.yml | 129 ++++++++++++++++++------ app/build.gradle.kts | 61 ++++------- buildSrc/src/main/kotlin/BuildHelper.kt | 47 --------- buildSrc/src/main/kotlin/Constants.kt | 3 + buildSrc/src/main/kotlin/Extensions.kt | 92 +++++++++++++++++ gradle/libs.versions.toml | 4 +- logcatter/build.gradle.kts | 10 +- 8 files changed, 221 insertions(+), 249 deletions(-) delete mode 100644 .github/workflows/pre-release.yml delete mode 100644 buildSrc/src/main/kotlin/BuildHelper.kt create mode 100644 buildSrc/src/main/kotlin/Extensions.kt diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml deleted file mode 100644 index 1e16358..0000000 --- a/.github/workflows/pre-release.yml +++ /dev/null @@ -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) - diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1828628..dcf8144 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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<> $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 }}) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e3f28ca..01bc679 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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}" +} diff --git a/buildSrc/src/main/kotlin/BuildHelper.kt b/buildSrc/src/main/kotlin/BuildHelper.kt deleted file mode 100644 index 546a645..0000000 --- a/buildSrc/src/main/kotlin/BuildHelper.kt +++ /dev/null @@ -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", - )) - } -} diff --git a/buildSrc/src/main/kotlin/Constants.kt b/buildSrc/src/main/kotlin/Constants.kt index 9614602..f89a1bd 100644 --- a/buildSrc/src/main/kotlin/Constants.kt +++ b/buildSrc/src/main/kotlin/Constants.kt @@ -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 } diff --git a/buildSrc/src/main/kotlin/Extensions.kt b/buildSrc/src/main/kotlin/Extensions.kt new file mode 100644 index 0000000..fcf270a --- /dev/null +++ b/buildSrc/src/main/kotlin/Extensions.kt @@ -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 +} + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1501659..7fd32b4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } \ No newline at end of file +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +grgit = { id = "org.ajoberstar.grgit.service", version.ref = "gradlePlugins-grgit" } \ No newline at end of file diff --git a/logcatter/build.gradle.kts b/logcatter/build.gradle.kts index 50e9b17..5767c3e 100644 --- a/logcatter/build.gradle.kts +++ b/logcatter/build.gradle.kts @@ -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 } }