refactor: app versioning and flavors

This commit is contained in:
Zane Schepke
2025-04-23 01:23:01 -04:00
parent 287732dfb8
commit 312062aa36
23 changed files with 373 additions and 372 deletions
+43 -41
View File
@@ -1,4 +1,5 @@
name: build
name: Build
on:
workflow_dispatch:
inputs:
@@ -12,6 +13,14 @@ on:
- prerelease
- nightly
- release
flavor:
type: choice
description: "Product flavor"
required: true
default: fdroid
options:
- fdroid
- full
secrets:
SIGNING_KEY_ALIAS:
required: false
@@ -30,6 +39,11 @@ on:
description: "Build type"
required: true
default: debug
flavor:
type: string
description: "Product flavor"
required: false
default: fdroid
secrets:
SIGNING_KEY_ALIAS:
required: false
@@ -41,6 +55,7 @@ on:
required: false
KEYSTORE:
required: false
env:
UPLOAD_DIR_ANDROID: android_artifacts
@@ -48,15 +63,17 @@ jobs:
build:
runs-on: ubuntu-latest
env:
SIGNING_KEY_ALIAS: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.ANDROID_SIGNING_STORE_PASSWORD }}
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/
outputs:
UPLOAD_DIR_ANDROID: ${{ env.UPLOAD_DIR_ANDROID }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
@@ -65,61 +82,46 @@ jobs:
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.ANDROID_KEYSTORE }}
# create keystore path for gradle to read
encodedString: ${{ secrets.KEYSTORE }}
- name: Create keystore path env var
if: ${{ inputs.build_type != 'debug' }}
run: |
store_path=${{ env.KEY_STORE_LOCATION }}${{ env.KEY_STORE_FILE }}
echo "KEY_STORE_PATH=$store_path" >> $GITHUB_ENV
- name: Create service_account.json
if: ${{ inputs.build_type != 'debug' }}
id: createServiceAccount
run: echo '${{ secrets.ANDROID_SERVICE_ACCOUNT_JSON }}' > service_account.json
- name: Build Fdroid Release APK
if: ${{ inputs.build_type == 'release' }}
run: ./gradlew :app:assembleFdroidRelease --info
- name: Build Fdroid Prerelease APK
if: ${{ inputs.build_type == 'prerelease' }}
run: ./gradlew :app:assembleFdroidPrerelease --info
- name: Build Fdroid Nightly APK
if: ${{ inputs.build_type == 'nightly' }}
run: ./gradlew :app:assembleFdroidNightly --info
- name: Build Debug APK
if: ${{ inputs.build_type == 'debug' }}
run: ./gradlew :app:assembleFdroidDebug --stacktrace
# bump versionCode for nightly and prerelease builds
- name: Commit and push versionCode changes
if: ${{ inputs.build_type == 'nightly' || inputs.build_type == 'prerelease' }}
run: echo '${{ secrets.SERVICE_ACCOUNT_JSON }}' > service_account.json
- name: Build APK
run: |
git config --global user.name 'GitHub Actions'
git config --global user.email 'actions@github.com'
git add versionCode.txt
git commit -m "Automated build update"
flavor=${{ inputs.flavor }}
build_type=${{ inputs.build_type }}
case $build_type in
"release")
./gradlew :app:assemble${flavor^}Release --info
;;
"prerelease")
./gradlew :app:assemble${flavor^}Prerelease --info
;;
"nightly")
./gradlew :app:assemble${flavor^}Nightly --info
;;
"debug")
./gradlew :app:assemble${flavor^}Debug --stacktrace
;;
esac
- name: Get release apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/fdroid/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
- name: Upload release apk
uses: actions/upload-artifact@v4
with:
name: ${{ env.UPLOAD_DIR_ANDROID }}
path: ${{github.workspace}}/${{ steps.apk-path.outputs.path }}
retention-days: 1
path: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
retention-days: 1
+40 -113
View File
@@ -2,12 +2,12 @@ name: publish
on:
schedule:
- cron: "4 3 * * *"
- cron: "4 3 * * *"
workflow_dispatch:
inputs:
track:
type: choice
description: "Google play release track"
description: "Google Play release track"
options:
- none
- internal
@@ -30,7 +30,22 @@ on:
description: "Tag name for release"
required: false
default: nightly
flavor:
type: choice
description: "Product flavor"
required: true
default: full
options:
- fdroid
- full
workflow_call:
inputs:
flavor:
type: string
description: "Product flavor"
required: false
default: full
env:
UPLOAD_DIR_ANDROID: android_artifacts
@@ -43,66 +58,69 @@ jobs:
runs-on: ubuntu-latest
outputs:
has_new_commits: ${{ steps.check.outputs.new_commits }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # This fetches all history so we can check commits
fetch-depth: 0
- name: Check for new commits
id: check
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
# This script checks for commits newer than 23 hours ago
NEW_COMMITS=$(git rev-list --count --after="$(date -Iseconds -d '23 hours ago')" ${{ github.sha }})
echo "new_commits=$NEW_COMMITS" >> $GITHUB_OUTPUT
build:
if: ${{ inputs.release_type != 'none' }}
build-fdroid:
if: ${{ inputs.release_type == 'release' || inputs.flavor == 'fdroid' }}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
build_type: ${{ inputs.release_type == '' && 'nightly' || inputs.release_type }}
flavor: fdroid
build-full:
if: ${{ inputs.release_type == 'release' || inputs.release_type == 'nightly' || inputs.release_type == 'prerelease' || inputs.flavor == 'full' }}
uses: ./.github/workflows/build.yml
secrets: inherit
with:
build_type: ${{ inputs.release_type == '' && 'nightly' || inputs.release_type }}
flavor: full
publish:
needs:
- check_commits
- build
- build-fdroid
- build-full
if: ${{ needs.check_commits.outputs.has_new_commits > 0 && inputs.release_type != 'none' }}
name: publish-github
runs-on: ubuntu-latest
env:
GH_USER: ${{ secrets.PAT_USERNAME }}
# GH needed for gh cli
GH_TOKEN: ${{ secrets.PAT }}
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install system dependencies
run: |
sudo apt update && sudo apt install -y gh apksigner
# update latest tag
- name: Set latest tag
uses: rickstaa/action-create-tag@v1
id: tag_creation
with:
tag: "latest" # or any tag name you wish to use
tag: "latest"
message: "Automated tag for HEAD commit"
force_push_tag: true
github_token: ${{ secrets.GITHUB_TOKEN }}
tag_exists_error: false
- name: Get latest release
id: latest_release
uses: kaliber5/action-get-release@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
latest: true
- name: Generate Changelog
id: changelog
uses: requarks/changelog-action@v1
@@ -110,65 +128,43 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
toTag: ${{ github.event_name == 'schedule' && 'nightly' || steps.latest_release.outputs.tag_name }}
fromTag: "latest"
writeToFile: false # we won't write to file, just output
- name: Get version code
if: ${{ inputs.release_type == 'release' }}
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
- name: Push changes
if: ${{ inputs.release_type == '' || inputs.release_type == 'nightly' || inputs.release_type == 'prerelease' }}
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.PAT }}
branch: ${{ github.ref }}
writeToFile: false
- name: Make download dir
run: mkdir ${{ github.workspace }}/temp
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: ${{ env.UPLOAD_DIR_ANDROID }}
path: ${{ github.workspace }}/temp
# 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" >> $GITHUB_ENV
- name: Set version release notes
if: ${{ inputs.release_type == 'release' }}
run: |
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${{ env.VERSION_CODE }}.txt)"
VERSION_NAME=$(grep "const val VERSION_NAME" buildSrc/src/main/kotlin/Constants.kt | awk -F'"' '{print $2}')
RELEASE_NOTES="$(cat ${{ github.workspace }}/fastlane/metadata/android/en-US/changelogs/${VERSION_NAME}.txt || echo "No changelog found for ${VERSION_NAME}")"
echo "RELEASE_NOTES<<EOF" >> $GITHUB_ENV
echo "$RELEASE_NOTES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: On nightly release notes
if: ${{ contains(env.TAG_NAME, 'nightly') }}
run: |
echo "RELEASE_NOTES=Nightly build for the latest development version of the app." >> $GITHUB_ENV
gh release delete nightly --yes || true
git push origin :nightly || true
- name: On prerelease release notes
if: ${{ inputs.release_type == 'prerelease' }}
run: |
echo "RELEASE_NOTES=Testing version of app for specific feature." >> $GITHUB_ENV
gh release delete ${{ github.event.inputs.tag_name }} --yes || true
- name: Get checksum
id: checksum
run: |
file_path=$(find ${{ github.workspace }}/temp -type f -iname "*.apk" | tail -n1)
file_path=$(find ${{ github.workspace }}/temp -type f -iname "*.apk" | tail -1)
echo "checksum=$(apksigner verify -print-certs $file_path | grep -Po "(?<=SHA-256 digest:) .*" | tr -d "[:blank:]")" >> $GITHUB_OUTPUT
- name: Create Release with Fastlane changelog notes
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2
env:
@@ -195,73 +191,4 @@ jobs:
prerelease: ${{ inputs.release_type == 'prerelease' || inputs.release_type == '' || inputs.release_type == 'nightly' }}
make_latest: ${{ inputs.release_type == 'release' }}
files: |
${{ github.workspace }}/temp/*
publish-fdroid:
runs-on: ubuntu-latest
needs:
- build
if: inputs.release_type == 'release'
steps:
- name: Dispatch update for fdroid repo
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.PAT }}
repository: zaneschepke/fdroid
event-type: fdroid-update
publish-play:
if: ${{ inputs.track != 'none' && inputs.track != '' }}
name: Publish to Google Play
runs-on: ubuntu-latest
env:
SIGNING_KEY_ALIAS: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }}
SIGNING_STORE_PASSWORD: ${{ secrets.ANDROID_SIGNING_STORE_PASSWORD }}
KEY_STORE_FILE: 'android_keystore.jks'
KEY_STORE_LOCATION: ${{ github.workspace }}/app/keystore/
GH_USER: ${{ secrets.PAT_USERNAME }}
GH_TOKEN: ${{ secrets.PAT }}
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.ANDROID_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
- 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 ${{ inputs.track }})
${{ github.workspace }}/temp/*
+1 -1
View File
@@ -70,5 +70,5 @@ lint/tmp/
app/release/output.json
.idea/codeStyles/
# where we keep our signing secrets locally
app/signing.properties
/.kotlin/
/app/keystore/
+38 -95
View File
@@ -9,33 +9,14 @@ plugins {
alias(libs.plugins.licensee)
}
val versionFile = file("$rootDir/versionCode.txt")
val versionCodeIncrement =
with(getBuildTaskName().lowercase()) {
when {
this.contains(Constants.NIGHTLY) || this.contains(Constants.PRERELEASE) -> {
if (versionFile.exists()) {
versionFile.readText().trim().toInt() + 1
} else {
1
}
}
else -> 0
}
}
android {
namespace = Constants.APP_ID
compileSdk = Constants.TARGET_SDK
androidResources { generateLocaleConfig = true }
// reproducibility
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
@@ -43,14 +24,12 @@ android {
applicationId = Constants.APP_ID
minSdk = Constants.MIN_SDK
targetSdk = Constants.TARGET_SDK
versionCode = Constants.VERSION_CODE + versionCodeIncrement
versionName = determineVersionName()
versionCode = computeVersionCode()
versionName = computeVersionName()
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
sourceSets {
getByName("debug").assets.srcDirs(files("$projectDir/schemas")) // Room
}
sourceSets { getByName("debug").assets.srcDirs(files("$projectDir/schemas")) }
buildConfigField(
"String[]",
@@ -64,15 +43,18 @@ android {
signingConfigs {
create(Constants.RELEASE) {
storeFile = getStoreFile()
storePassword = getSigningProperty(Constants.STORE_PASS_VAR)
keyAlias = getSigningProperty(Constants.KEY_ALIAS_VAR)
keyPassword = getSigningProperty(Constants.KEY_PASS_VAR)
storeFile = file(System.getenv("KEY_STORE_PATH") ?: "keystore/android_keystore.jks")
storePassword =
LocalProperties.get("SIGNING_STORE_PASSWORD")
?: System.getenv("SIGNING_STORE_PASSWORD")
keyAlias =
LocalProperties.get("SIGNING_KEY_ALIAS") ?: System.getenv("SIGNING_KEY_ALIAS")
keyPassword =
LocalProperties.get("SIGNING_KEY_PASSWORD") ?: System.getenv("SIGNING_KEY_PASSWORD")
}
}
buildTypes {
// don't strip
packaging.jniLibs.keepDebugSymbols.addAll(
listOf("libwg-go.so", "libwg-quick.so", "libwg.so")
)
@@ -88,6 +70,7 @@ android {
signingConfig = signingConfigs.getByName(Constants.RELEASE)
resValue("string", "provider", "\"${Constants.APP_NAME}.provider\"")
}
debug {
applicationIdSuffix = ".debug"
resValue("string", "app_name", "WG Tunnel - Debug")
@@ -108,27 +91,21 @@ android {
resValue("string", "app_name", "WG Tunnel - Nightly")
resValue("string", "provider", "\"${Constants.APP_NAME}.provider.nightly\"")
}
applicationVariants.all {
val variant = this
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
val outputFileName =
"${Constants.APP_NAME}-${variant.flavorName}-" +
"${variant.buildType.name}-${variant.versionName}.apk"
output.outputFileName = outputFileName
}
}
}
flavorDimensions.add(Constants.TYPE)
flavorDimensions.add("type")
productFlavors {
create("fdroid") {
dimension = Constants.TYPE
proguardFile("fdroid-rules.pro")
dimension = "type"
buildConfigField("String", "FLAVOR", "\"fdroid\"")
}
create("general") { dimension = Constants.TYPE }
create("google") {
dimension = "type"
buildConfigField("String", "FLAVOR", "\"google\"")
}
create("full") { dimension = "type" }
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
@@ -144,10 +121,23 @@ android {
licensee {
Constants.allowedLicenses.forEach { allow(it) }
allowUrl(Constants.XZING_LICENSE_URL)
// Fix for qrcode-kotlin (MIT, custom URL)
allowUrl("https://rafaellins.mit-license.org/2021/")
}
applicationVariants.all {
val variant = this
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
val outputFileName =
if (variant.flavorName == "fdroid" && variant.buildType.name == "release") {
"${Constants.APP_NAME}-fdroid-release-${variant.versionName}.apk"
} else {
"${Constants.APP_NAME}-${variant.flavorName}-v${variant.versionName}.apk"
}
output.outputFileName = outputFileName
}
}
}
dependencies {
@@ -156,8 +146,6 @@ dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
// helpers for implementing LifecycleOwner in a Service
implementation(libs.androidx.lifecycle.service)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
@@ -169,7 +157,6 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.storage)
// test
testImplementation(libs.junit)
testImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.junit)
@@ -180,107 +167,63 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.manifest)
// tunnel
implementation(libs.tunnel)
implementation(libs.amneziawg.android)
coreLibraryDesugaring(libs.desugar.jdk.libs)
// logging
implementation(libs.timber)
// compose navigation
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.hilt.navigation.compose)
// hilt
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
// accompanist
implementation(libs.accompanist.permissions)
implementation(libs.accompanist.drawablepainter)
// storage
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.datastore.preferences)
// lifecycle
implementation(libs.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.process)
// serialization
implementation(libs.kotlinx.serialization.json)
// ui
implementation(libs.zxing.android.embedded)
implementation(libs.material.icons.extended)
// bio
implementation(libs.androidx.biometric.ktx)
implementation(libs.pin.lock.compose)
// shortcuts
implementation(libs.androidx.core)
// splash
implementation(libs.androidx.core.splashscreen)
// worker
implementation(libs.androidx.work.runtime)
implementation(libs.androidx.hilt.work)
// util
implementation(libs.qrcode.kotlin)
implementation(libs.semver4j)
// Ktor
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
}
fun determineVersionName(): String {
return with(getBuildTaskName().lowercase()) {
when {
contains(Constants.NIGHTLY) || contains(Constants.PRERELEASE) ->
Constants.VERSION_NAME + "-${grgitService.service.get().grgit.head().abbreviatedId}"
else -> Constants.VERSION_NAME
}
}
}
val incrementVersionCode by
tasks.registering {
doLast {
val versionFile = file("$rootDir/versionCode.txt")
if (versionFile.exists()) {
versionFile.writeText(versionCodeIncrement.toString())
println("Incremented versionCode to $versionCodeIncrement")
}
}
}
tasks.whenTaskAdded {
if (name.startsWith("assemble") && !name.lowercase().contains("debug")) {
dependsOn(incrementVersionCode)
}
implementation(libs.slf4j.android)
}
tasks.register<Copy>("copyLicenseeJsonToAssets") {
dependsOn("licensee")
val outputAssets = layout.projectDirectory.dir("src/main/assets")
from(layout.buildDirectory.file("reports/licensee/androidFdroidRelease/artifacts.json")) {
rename("artifacts.json", "licenses.json")
}
into(outputAssets)
}
View File
+5
View File
@@ -0,0 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions specific to full -->
<!--updater-->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
</manifest>
-2
View File
@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!--updater-->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@@ -4,4 +4,6 @@ import com.zaneschepke.wireguardautotunnel.data.model.GitHubRelease
interface GitHubApi {
suspend fun getLatestRelease(owner: String, repo: String): Result<GitHubRelease>
suspend fun getNightlyRelease(owner: String, repo: String): Result<GitHubRelease>
}
@@ -24,4 +24,33 @@ class KtorGitHubApi(private val client: HttpClient) : GitHubApi {
Result.failure(e)
}
}
override suspend fun getNightlyRelease(owner: String, repo: String): Result<GitHubRelease> {
return try {
// Fetch all releases
val releases: List<GitHubRelease> =
client.get("https://api.github.com/repos/$owner/$repo/releases").body()
// Find the first release with "nightly" in the tag_name (case-insensitive)
val nightlyRelease =
releases.firstOrNull { release ->
release.tagName.contains("nightly", ignoreCase = true)
}
if (nightlyRelease != null) {
Result.success(nightlyRelease)
} else {
Result.failure(Exception("No release with 'nightly' tag found"))
}
} catch (e: ClientRequestException) {
when (e.response.status) {
HttpStatusCode.Forbidden -> Result.failure(Exception("Rate limit exceeded"))
HttpStatusCode.NotFound ->
Result.failure(Exception("Repository or release not found"))
else -> Result.failure(e)
}
} catch (e: Exception) {
Result.failure(e)
}
}
}
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.data.repository
import android.content.Context
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.data.network.GitHubApi
import com.zaneschepke.wireguardautotunnel.di.IoDispatcher
import com.zaneschepke.wireguardautotunnel.domain.entity.AppUpdate
@@ -16,6 +17,7 @@ import io.ktor.utils.io.readAvailable
import java.io.File
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber
class GitHubUpdateRepository(
private val gitHubApi: GitHubApi,
@@ -27,11 +29,24 @@ class GitHubUpdateRepository(
) : UpdateRepository {
override suspend fun checkForUpdate(currentVersion: String): Result<AppUpdate?> =
withContext(ioDispatcher) {
gitHubApi.getLatestRelease(githubOwner, githubRepo).map { release ->
if (
NumberUtils.compareVersions(release.tagName.removePrefix("v"), currentVersion) >
0
) {
Timber.i("Checking for update")
val release =
if (BuildConfig.VERSION_NAME.contains("nightly")) {
gitHubApi.getNightlyRelease(githubOwner, githubRepo)
} else {
gitHubApi.getLatestRelease(githubOwner, githubRepo)
}
release.map { release ->
val apkAsset =
release.assets.find { asset ->
asset.name.startsWith("wgtunnel-full-v") && asset.name.endsWith(".apk")
}
val newVersion =
apkAsset?.name?.removePrefix("wgtunnel-full-v")?.removeSuffix(".apk")
?: return@map null
Timber.i("Latest version: $newVersion, current version: $currentVersion")
if (NumberUtils.compareVersions(newVersion, currentVersion) > 0) {
release.toAppUpdate()
} else {
null
@@ -15,7 +15,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.zxing.client.android.BuildConfig
import com.zaneschepke.wireguardautotunnel.BuildConfig
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.SectionDivider
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
@@ -23,8 +23,8 @@ import com.zaneschepke.wireguardautotunnel.ui.common.label.GroupLabel
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.ContactSupportOptions
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.GeneralSupportOptions
import com.zaneschepke.wireguardautotunnel.ui.screens.support.components.UpdateSection
import com.zaneschepke.wireguardautotunnel.util.Constants
import com.zaneschepke.wireguardautotunnel.util.extensions.canInstallPackages
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
import com.zaneschepke.wireguardautotunnel.util.extensions.requestInstallPackagesPermission
import com.zaneschepke.wireguardautotunnel.util.extensions.showToast
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
@@ -54,6 +54,10 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: A
InfoDialog(
onDismiss = { viewModel.handleUpdateShown() },
onAttest = {
if (BuildConfig.FLAVOR != "full") {
uiState.appUpdate?.apkUrl?.let { context.openWebUrl(it) }
return@InfoDialog
}
if (context.canInstallPackages()) {
viewModel.handleDownloadAndInstallApk()
} else {
@@ -80,7 +84,12 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: A
}
}
},
confirmText = { Text(stringResource(R.string.download_and_install)) },
confirmText = {
Text(
if (BuildConfig.FLAVOR != "full") stringResource(R.string.download)
else stringResource(R.string.download_and_install)
)
},
)
}
@@ -110,15 +119,15 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: A
stringResource(R.string.thank_you),
modifier = Modifier.padding(horizontal = 12.dp),
)
if (BuildConfig.BUILD_TYPE == Constants.RELEASE) {
UpdateSection(
onUpdateCheck = {
context.showToast(R.string.checking_for_update)
viewModel.handleUpdateCheck()
}
)
SectionDivider()
}
UpdateSection(
onUpdateCheck = {
if (BuildConfig.DEBUG || BuildConfig.VERSION_NAME.contains("beta"))
return@UpdateSection context.showToast(R.string.update_check_unsupported)
context.showToast(R.string.checking_for_update)
viewModel.handleUpdateCheck()
}
)
SectionDivider()
GeneralSupportOptions(context)
SectionDivider()
ContactSupportOptions(context)
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.support.components
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CloudDownload
import androidx.compose.material.icons.rounded.CloudDownload
@@ -11,7 +12,6 @@ import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionIte
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItemLabel
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionLabelType
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SurfaceSelectionGroupButton
import com.zaneschepke.wireguardautotunnel.util.Constants
@Composable
fun UpdateSection(onUpdateCheck: () -> Unit = {}) {
@@ -26,16 +26,20 @@ fun UpdateSection(onUpdateCheck: () -> Unit = {}) {
)
},
description = {
val versionName =
if (BuildConfig.BUILD_TYPE == Constants.RELEASE) {
"v${BuildConfig.VERSION_NAME}"
} else {
"v${BuildConfig.VERSION_NAME}-${BuildConfig.BUILD_TYPE}"
}
SelectionItemLabel(
stringResource(R.string.version_template, versionName),
SelectionLabelType.DESCRIPTION,
)
Column {
SelectionItemLabel(
stringResource(
R.string.version_template,
"v${BuildConfig.VERSION_NAME +
if(BuildConfig.DEBUG) "-debug" else "" }",
),
SelectionLabelType.DESCRIPTION,
)
SelectionItemLabel(
stringResource(R.string.flavor_template, BuildConfig.FLAVOR),
SelectionLabelType.DESCRIPTION,
)
}
},
onClick = onUpdateCheck,
)
@@ -5,6 +5,7 @@ import java.math.BigDecimal
import java.time.Duration
import java.time.Instant
import kotlin.math.pow
import timber.log.Timber
object NumberUtils {
private const val BYTES_IN_KB = 1024.0
@@ -41,8 +42,13 @@ object NumberUtils {
}
fun compareVersions(newVersion: String, currentVersion: String): Int {
val newSemver = Semver(newVersion, Semver.SemverType.LOOSE)
val currentSemver = Semver(currentVersion, Semver.SemverType.LOOSE)
return newSemver.compareTo(currentSemver)
try {
val newSemver = Semver(newVersion, Semver.SemverType.LOOSE)
val currentSemver = Semver(currentVersion, Semver.SemverType.LOOSE)
return newSemver.compareTo(currentSemver)
} catch (e: Exception) {
Timber.e(e, "Failed to compare versions $newVersion and $currentVersion")
return 0
}
}
}
+3 -1
View File
@@ -220,7 +220,8 @@
<string name="tunnel_error_template">Tunnel failed with: %1$s</string>
<string name="wifi_name_template">Active: %1$s</string>
<string name="remote_key_template">Key: %1$s</string>
<string name="version_template">Current version: %1$s</string>
<string name="version_template">Version: %1$s</string>
<string name="flavor_template">Flavor: %1$s</string>
<string name="config_error">config error</string>
<string name="dns_resolve_error">dns resolution error</string>
<string name="invalid_config_error">invalid_config_error</string>
@@ -253,4 +254,5 @@
<string name="install_updated_permission">This app needs permission to install updates.</string>
<string name="allow">Allow</string>
<string name="licenses">Licenses</string>
<string name="update_check_unsupported">Update check not supported this build type.</string>
</resources>
+5
View File
@@ -6,3 +6,8 @@ repositories {
google()
mavenCentral()
}
dependencies {
implementation("org.semver4j:semver4j:5.6.0")
implementation("org.ajoberstar.grgit:grgit-core:5.3.0")
}
+1 -6
View File
@@ -7,15 +7,10 @@ object Constants {
const val APP_ID = "com.zaneschepke.wireguardautotunnel"
const val APP_NAME = "wgtunnel"
const val STORE_PASS_VAR = "SIGNING_STORE_PASSWORD"
const val KEY_ALIAS_VAR = "SIGNING_KEY_ALIAS"
const val KEY_PASS_VAR = "SIGNING_KEY_PASSWORD"
const val KEY_STORE_PATH_VAR = "KEY_STORE_PATH"
// build types
const val RELEASE = "release"
const val NIGHTLY = "nightly"
const val PRERELEASE = "prerelease"
const val TYPE = "type"
val allowedLicenses = listOf("MIT", "Apache-2.0", "BSD-3-Clause")
const val XZING_LICENSE_URL: String = "https://github.com/journeyapps/zxing-android-embedded/blob/master/COPYING"
+113 -72
View File
@@ -1,76 +1,7 @@
import org.ajoberstar.grgit.Grgit
import org.gradle.api.Project
import java.io.File
import java.util.*
fun Project.getCurrentFlavor(): String {
val taskRequestsStr = gradle.startParameter.taskRequests.toString()
val pattern: java.util.regex.Pattern =
if (taskRequestsStr.contains(":app:assemble")) {
java.util.regex.Pattern.compile(":app:assemble(\\w+)(Release|Debug)")
} else {
java.util.regex.Pattern.compile(":app: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 Project.getBuildTaskName(): String {
val taskRequestsStr = gradle.startParameter.taskRequests[0].toString()
return taskRequestsStr.also {
project.logger.lifecycle("Build task: $it")
}
}
fun getLocalProperty(key: String, file: String = "local.properties"): String? {
val properties = 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.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),
)
}
import org.semver4j.Semver
fun Project.languageList(): List<String> {
return fileTree("../app/src/main/res") { include("**/strings.xml") }
@@ -84,6 +15,116 @@ fun Project.languageList(): List<String> {
.toList() + "en"
}
// Get the Git commit hash
fun Project.getGitCommitHash(): String {
var grgit: Grgit? = null
try {
grgit = Grgit.open(mapOf("currentDir" to projectDir))
return grgit.head().abbreviatedId
} catch (e: Exception) {
logger.warn("Failed to get Git commit hash: ${e.message}. Using fallback.")
return "unknown"
} finally {
grgit?.close()
}
}
// Get commit count since last commit for versionCode increment
fun Project.getCommitCountSinceLastCommit(): Int {
var grgit: Grgit? = null
try {
grgit = Grgit.open(mapOf("currentDir" to projectDir))
val headCommit = grgit.head()
val log = grgit.log(mapOf(
"includes" to listOf(headCommit.id)
))
return log.size
} catch (e: Exception) {
logger.warn("Failed to get commit count: ${e.message}. Using fallback.")
return 0
} finally {
grgit?.close()
}
}
// Get versionCode increment for nightly/pre-release
fun Project.getVersionCodeIncrement(): Int {
val isNightlyBuild = gradle.startParameter.taskNames.any { it.lowercase().contains("nightly") }
val isPreReleaseBuild = gradle.startParameter.taskNames.any { it.lowercase().contains("prerelease") }
if (!isNightlyBuild && !isPreReleaseBuild) return 0
return System.getenv("GITHUB_RUN_NUMBER")?.toIntOrNull()
?: System.getenv("CI_BUILD_NUMBER")?.toIntOrNull()
?: getCommitCountSinceLastCommit()
}
// Compute versionName dynamic bumping for nightly/pre-release
fun Project.computeVersionName(): String {
val isNightlyBuild = isNightlyBuild()
val isPreReleaseBuild = isPrereleaseBuild()
// Static version from Constants.kt
val baseVersion = Semver.parse(Constants.VERSION_NAME) ?: Semver.of(0, 0, 0)
return when {
isNightlyBuild -> {
// Bump patch for nightly
val nightlyVersion = Semver.of(
baseVersion.major,
baseVersion.minor,
baseVersion.patch + 1
)
"${nightlyVersion}-nightly+git.${getGitCommitHash()}"
}
isPreReleaseBuild -> {
// Bump minor for pre-release
val preReleaseVersion = Semver.of(
baseVersion.major,
baseVersion.minor + 1,
0
)
"${preReleaseVersion}-beta+git.${getGitCommitHash()}"
}
else -> Constants.VERSION_NAME
}
}
fun Project.isNightlyBuild(): Boolean {
return gradle.startParameter.taskNames.any { it.lowercase().contains(Constants.NIGHTLY) }
}
fun Project.isPrereleaseBuild(): Boolean {
return gradle.startParameter.taskNames.any { it.lowercase().contains(Constants.PRERELEASE) }
}
// Compute versionCode (static baseline, dynamic bumping for nightly/pre-release)
fun Project.computeVersionCode(): Int {
val isNightlyBuild = isNightlyBuild()
val isPreReleaseBuild = isPrereleaseBuild()
// Static version from Constants.kt
val baseVersion = Semver.parse(Constants.VERSION_NAME) ?: Semver.of(0, 0, 0)
val version = when {
isNightlyBuild -> {
// Bump patch for nightly
Semver.of(
baseVersion.major,
baseVersion.minor,
baseVersion.patch + 1
)
}
isPreReleaseBuild -> {
// Bump minor for pre-release
Semver.of(
baseVersion.major,
baseVersion.minor + 1,
0
)
}
else -> baseVersion
}
val baseVersionCode = version.major * 10000 + version.minor * 100 + version.patch
return baseVersionCode + getVersionCodeIncrement()
}
@@ -0,0 +1,19 @@
import java.io.File
import java.io.FileInputStream
import java.util.Properties
object LocalProperties {
private val properties by lazy {
val props = Properties()
val file = File("local.properties")
if (file.exists()) {
FileInputStream(file).use { props.load(it) }
}
props
}
fun get(key: String): String? = properties.getProperty(key)
fun getOrDefault(key: String, default: String): String = properties.getProperty(key, default)
}
+4 -4
View File
@@ -4,25 +4,25 @@ platform :android do
desc 'Deploy a new internal version to the Google Play Store'
lane :internal do
gradle(task: "clean bundleGeneralRelease")
gradle(task: "clean bundleGoogleRelease")
upload_to_play_store(track: 'internal', skip_upload_apk: true)
end
desc "Deploy an alpha version to the Google Play"
lane :alpha do
gradle(task: "clean bundleGeneralRelease")
gradle(task: "clean bundleGoogleRelease")
upload_to_play_store(track: 'alpha', skip_upload_apk: true)
end
desc "Deploy a beta version to the Google Play"
lane :beta do
gradle(task: "clean bundleGeneralRelease")
gradle(task: "clean bundleGoogleRelease")
upload_to_play_store(track: 'beta', skip_upload_apk: true)
end
desc "Deploy a new version to the Google Play"
lane :production do
gradle(task: "clean bundleGeneralRelease")
gradle(task: "clean bundleGoogleRelease")
upload_to_play_store(skip_upload_apk: true)
end
+1 -1
View File
@@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+2 -2
View File
@@ -5,7 +5,6 @@ amneziawgAndroid = "1.3.8"
androidx-junit = "1.2.1"
appcompat = "1.7.0"
biometricKtx = "1.2.0-alpha05"
markdownCompose = "0.5.7"
coreKtx = "1.16.0"
datastorePreferences = "1.1.4"
desugar_jdk_libs = "2.1.5"
@@ -22,6 +21,7 @@ pinLockCompose = "1.0.4"
qrcodeKotlin = "4.4.1"
roomVersion = "2.7.0"
semver4j = "3.1.0"
slf4jAndroid = "1.7.36"
timber = "5.0.1"
tunnel = "1.2.14"
androidGradlePlugin = "8.9.2"
@@ -95,12 +95,12 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorCli
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktorClientCore" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktorClientCore" }
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle-runtime-compose" }
markdown-compose = { module = "com.github.jeziellago:compose-markdown", version.ref = "markdownCompose" }
material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" }
pin-lock-compose = { module = "com.zaneschepke:pin_lock_compose", version.ref = "pinLockCompose" }
qrcode-kotlin = { module = "io.github.g0dkar:qrcode-kotlin", version.ref = "qrcodeKotlin" }
semver4j = { module = "com.vdurmont:semver4j", version.ref = "semver4j" }
slf4j-android = { module = "org.slf4j:slf4j-android", version.ref = "slf4jAndroid" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
tunnel = { module = "com.zaneschepke:wireguard-android", version.ref = "tunnel" }
+2 -2
View File
@@ -5,10 +5,10 @@ plugins {
android {
namespace = "com.zaneschepke.networkmonitor"
compileSdk = 34
compileSdk = Constants.TARGET_SDK
defaultConfig {
minSdk = 26
minSdk = Constants.MIN_SDK
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
-1
View File
@@ -1 +0,0 @@
0