From 61438be5f15545f63d6a0e284e2bc702ab7ef726 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 03:10:19 +0000 Subject: [PATCH 1/3] Initial plan From 12c239e8b5ea18c3bde8862f0304e191d2eb704e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 03:22:54 +0000 Subject: [PATCH 2/3] Rename package to org.joefang.webdav and add product flavors Co-authored-by: MinecraftFuns <25814618+MinecraftFuns@users.noreply.github.com> --- app/build.gradle | 50 +++++++++++++++++-- .../1.json | 0 .../2.json | 0 .../3.json | 0 .../4.json | 0 .../5.json | 0 .../6.json | 0 .../joefang}/webdav/DocumentsProviderTest.kt | 14 +++--- .../joefang}/webdav/ScreenshotTest.kt | 12 ++--- .../joefang}/webdav/WebDavTestRunner.kt | 2 +- app/src/main/AndroidManifest.xml | 2 +- .../joefang}/webdav/WebDavApplication.kt | 2 +- .../webdav/activities/MainActivity.kt | 10 ++-- .../webdav/adapters/AccountAdapter.kt | 8 +-- .../webdav/adapters/CustomHeaderAdapter.kt | 6 +-- .../joefang}/webdav/data/Account.kt | 6 +-- .../joefang}/webdav/data/AccountDao.kt | 2 +- .../joefang}/webdav/data/AppDatabase.kt | 2 +- .../joefang}/webdav/data/CacheDao.kt | 2 +- .../joefang}/webdav/data/CacheEntry.kt | 2 +- .../joefang}/webdav/data/CustomHeader.kt | 2 +- .../webdav/data/CustomHeaderListConverter.kt | 2 +- .../joefang}/webdav/data/HeaderProfile.kt | 2 +- .../joefang}/webdav/data/SecretString.kt | 2 +- .../webdav/data/SecretStringConverter.kt | 2 +- .../webdav/dialogs/ChangelogDialog.kt | 4 +- .../joefang}/webdav/dialogs/Dialogs.kt | 6 +-- .../joefang}/webdav/dialogs/LicenseDialog.kt | 4 +- .../webdav/dialogs/SimpleWebViewDialog.kt | 4 +- .../webdav/extensions/WebDavExtensions.kt | 2 +- .../webdav/fragments/AboutFragment.kt | 12 ++--- .../webdav/fragments/AccountFragment.kt | 26 +++++----- .../webdav/fragments/CustomHeadersFragment.kt | 10 ++-- .../webdav/fragments/LicensesFragment.kt | 2 +- .../joefang}/webdav/fragments/MainFragment.kt | 18 +++---- .../joefang}/webdav/helpers/MetricsHelper.kt | 2 +- .../webdav/modules/ApplicationModule.kt | 8 +-- .../joefang}/webdav/modules/DatabaseModule.kt | 8 +-- .../provider/CustomHeadersInterceptor.kt | 4 +- .../SimpleXmlConverterFactoryCompat.kt | 2 +- .../joefang}/webdav/provider/WebDavCache.kt | 8 +-- .../joefang}/webdav/provider/WebDavClient.kt | 6 +-- .../webdav/provider/WebDavClientManager.kt | 4 +- .../webdav/provider/WebDavCredentials.kt | 2 +- .../joefang}/webdav/provider/WebDavFile.kt | 2 +- .../joefang}/webdav/provider/WebDavOptions.kt | 2 +- .../joefang}/webdav/provider/WebDavPath.kt | 4 +- .../webdav/provider/WebDavProvider.kt | 12 ++--- .../provider/WebDavReadProxyCallback.kt | 2 +- .../joefang}/webdav/provider/WebDavService.kt | 2 +- .../provider/WebDavWriteProxyCallback.kt | 2 +- app/src/main/res/layout/card_account.xml | 2 +- app/src/main/res/layout/fragment_account.xml | 2 +- .../main/res/layout/item_custom_header.xml | 2 +- app/src/main/res/menu/menu_main.xml | 2 +- app/src/main/res/navigation/nav_graph.xml | 12 ++--- app/src/main/res/values/strings.xml | 2 +- .../joefang}/webdav/data/CustomHeaderTest.kt | 2 +- .../provider/CustomHeadersInterceptorTest.kt | 4 +- 59 files changed, 180 insertions(+), 136 deletions(-) rename app/schemas/{dev.rocli.android.webdav.data.AppDatabase => org.joefang.webdav.data.AppDatabase}/1.json (100%) rename app/schemas/{dev.rocli.android.webdav.data.AppDatabase => org.joefang.webdav.data.AppDatabase}/2.json (100%) rename app/schemas/{dev.rocli.android.webdav.data.AppDatabase => org.joefang.webdav.data.AppDatabase}/3.json (100%) rename app/schemas/{dev.rocli.android.webdav.data.AppDatabase => org.joefang.webdav.data.AppDatabase}/4.json (100%) rename app/schemas/{dev.rocli.android.webdav.data.AppDatabase => org.joefang.webdav.data.AppDatabase}/5.json (100%) rename app/schemas/{dev.rocli.android.webdav.data.AppDatabase => org.joefang.webdav.data.AppDatabase}/6.json (100%) rename app/src/androidTest/java/{dev/rocli/android => org/joefang}/webdav/DocumentsProviderTest.kt (96%) rename app/src/androidTest/java/{dev/rocli/android => org/joefang}/webdav/ScreenshotTest.kt (93%) rename app/src/androidTest/java/{dev/rocli/android => org/joefang}/webdav/WebDavTestRunner.kt (93%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/WebDavApplication.kt (79%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/activities/MainActivity.kt (95%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/adapters/AccountAdapter.kt (94%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/adapters/CustomHeaderAdapter.kt (95%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/data/Account.kt (96%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/data/AccountDao.kt (91%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/data/AppDatabase.kt (96%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/data/CacheDao.kt (94%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/data/CacheEntry.kt (95%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/data/CustomHeader.kt (97%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/data/CustomHeaderListConverter.kt (98%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/data/HeaderProfile.kt (92%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/data/SecretString.kt (70%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/data/SecretStringConverter.kt (98%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/dialogs/ChangelogDialog.kt (84%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/dialogs/Dialogs.kt (96%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/dialogs/LicenseDialog.kt (86%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/dialogs/SimpleWebViewDialog.kt (97%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/extensions/WebDavExtensions.kt (91%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/fragments/AboutFragment.kt (93%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/fragments/AccountFragment.kt (96%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/fragments/CustomHeadersFragment.kt (97%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/fragments/LicensesFragment.kt (95%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/fragments/MainFragment.kt (92%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/helpers/MetricsHelper.kt (86%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/modules/ApplicationModule.kt (77%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/modules/DatabaseModule.kt (82%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/provider/CustomHeadersInterceptor.kt (91%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/provider/SimpleXmlConverterFactoryCompat.kt (98%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/provider/WebDavCache.kt (96%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/provider/WebDavClient.kt (98%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/provider/WebDavClientManager.kt (95%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/provider/WebDavCredentials.kt (80%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/provider/WebDavFile.kt (98%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/provider/WebDavOptions.kt (58%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/provider/WebDavPath.kt (72%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/provider/WebDavProvider.kt (98%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/provider/WebDavReadProxyCallback.kt (98%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/provider/WebDavService.kt (95%) rename app/src/main/java/{dev/rocli/android => org/joefang}/webdav/provider/WebDavWriteProxyCallback.kt (98%) rename app/src/test/java/{dev/rocli/android => org/joefang}/webdav/data/CustomHeaderTest.kt (98%) rename app/src/test/java/{dev/rocli/android => org/joefang}/webdav/provider/CustomHeadersInterceptorTest.kt (98%) diff --git a/app/build.gradle b/app/build.gradle index 2405e41..d2edc8f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,9 +10,11 @@ plugins { def appName = "WebDAV Provider" def appNameDebug = "${appName} (Debug)" -def packageName = "dev.rocli.android.webdav" +def appNameTest = "${appName} (Test)" +def packageName = "org.joefang.webdav" def providerAuthority = "${packageName}.provider" def providerAuthorityDebug = "${packageName}.debug.provider" +def providerAuthorityTest = "${packageName}.test.provider" def getCmdOutput = { cmd -> def stdout = new ByteArrayOutputStream() @@ -130,7 +132,30 @@ if (!isStable && buildCount > 0) { } def computedVersionCode = calculateVersionCode(vMajor, vMinor, vPatch, isStable, buildCount) -def computedVersionName = isStable ? "v${vMajor}.${vMinor}.${vPatch}" : "v${vMajor}.${vMinor}.${vPatch}-beta.${buildCount}" + +// CI_BUILD_TYPE controls version naming: +// - "release": Stable release (v1.2.3) +// - "prerelease": Pre-release from main branch (v1.2.3-dev.X+) +// - "test" or other: Untrusted test build (ci-test-untrusted-1.2.3-dev.X+) +def ciBuildType = System.getenv("CI_BUILD_TYPE") ?: "local" +def gitSha = getGitHash() + +def computedVersionName +switch (ciBuildType) { + case "release": + computedVersionName = "v${vMajor}.${vMinor}.${vPatch}" + break + case "prerelease": + computedVersionName = "v${vMajor}.${vMinor}.${vPatch}-dev.${buildCount}+${gitSha}" + break + case "test": + // Explicitly non-SemVer for untrusted test builds + computedVersionName = "ci-test-untrusted-${vMajor}.${vMinor}.${vPatch}-dev.${buildCount}+${gitSha}" + break + default: + // Local development builds + computedVersionName = "v${vMajor}.${vMinor}.${vPatch}-dev.${buildCount}+${gitSha}" +} android { namespace packageName @@ -155,7 +180,7 @@ android { } } - testInstrumentationRunner "dev.rocli.android.webdav.WebDavTestRunner" + testInstrumentationRunner "org.joefang.webdav.WebDavTestRunner" testInstrumentationRunnerArguments clearPackageData: 'true' } @@ -185,6 +210,25 @@ android { buildConfig true } + // Product flavors for co-installation support + // 'prod' flavor installs as org.joefang.webdav (production releases) + // 'untrusted' flavor installs as org.joefang.webdav.test (untrusted test builds) + flavorDimensions += "env" + productFlavors { + prod { + dimension "env" + // Uses base applicationId: org.joefang.webdav + } + untrusted { + dimension "env" + // Allows untrusted builds to be installed alongside production + applicationIdSuffix ".test" + resValue("string", "app_name", appNameTest) + manifestPlaceholders = [providerAuthority: providerAuthorityTest] + buildConfigField("String", "PROVIDER_AUTHORITY", "\"${providerAuthorityTest}\"") + } + } + signingConfigs { if (System.getenv("ANDROID_KEYSTORE") != null) { create("multiset") { diff --git a/app/schemas/dev.rocli.android.webdav.data.AppDatabase/1.json b/app/schemas/org.joefang.webdav.data.AppDatabase/1.json similarity index 100% rename from app/schemas/dev.rocli.android.webdav.data.AppDatabase/1.json rename to app/schemas/org.joefang.webdav.data.AppDatabase/1.json diff --git a/app/schemas/dev.rocli.android.webdav.data.AppDatabase/2.json b/app/schemas/org.joefang.webdav.data.AppDatabase/2.json similarity index 100% rename from app/schemas/dev.rocli.android.webdav.data.AppDatabase/2.json rename to app/schemas/org.joefang.webdav.data.AppDatabase/2.json diff --git a/app/schemas/dev.rocli.android.webdav.data.AppDatabase/3.json b/app/schemas/org.joefang.webdav.data.AppDatabase/3.json similarity index 100% rename from app/schemas/dev.rocli.android.webdav.data.AppDatabase/3.json rename to app/schemas/org.joefang.webdav.data.AppDatabase/3.json diff --git a/app/schemas/dev.rocli.android.webdav.data.AppDatabase/4.json b/app/schemas/org.joefang.webdav.data.AppDatabase/4.json similarity index 100% rename from app/schemas/dev.rocli.android.webdav.data.AppDatabase/4.json rename to app/schemas/org.joefang.webdav.data.AppDatabase/4.json diff --git a/app/schemas/dev.rocli.android.webdav.data.AppDatabase/5.json b/app/schemas/org.joefang.webdav.data.AppDatabase/5.json similarity index 100% rename from app/schemas/dev.rocli.android.webdav.data.AppDatabase/5.json rename to app/schemas/org.joefang.webdav.data.AppDatabase/5.json diff --git a/app/schemas/dev.rocli.android.webdav.data.AppDatabase/6.json b/app/schemas/org.joefang.webdav.data.AppDatabase/6.json similarity index 100% rename from app/schemas/dev.rocli.android.webdav.data.AppDatabase/6.json rename to app/schemas/org.joefang.webdav.data.AppDatabase/6.json diff --git a/app/src/androidTest/java/dev/rocli/android/webdav/DocumentsProviderTest.kt b/app/src/androidTest/java/org/joefang/webdav/DocumentsProviderTest.kt similarity index 96% rename from app/src/androidTest/java/dev/rocli/android/webdav/DocumentsProviderTest.kt rename to app/src/androidTest/java/org/joefang/webdav/DocumentsProviderTest.kt index 610a8bc..523cbb2 100644 --- a/app/src/androidTest/java/dev/rocli/android/webdav/DocumentsProviderTest.kt +++ b/app/src/androidTest/java/org/joefang/webdav/DocumentsProviderTest.kt @@ -1,16 +1,16 @@ -package dev.rocli.android.webdav +package org.joefang.webdav import android.content.Context import androidx.documentfile.provider.DocumentFile import androidx.test.platform.app.InstrumentationRegistry import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import dev.rocli.android.webdav.data.Account -import dev.rocli.android.webdav.data.AccountDao -import dev.rocli.android.webdav.data.CacheDao -import dev.rocli.android.webdav.data.CacheEntry -import dev.rocli.android.webdav.data.SecretString -import dev.rocli.android.webdav.provider.WebDavProvider +import org.joefang.webdav.data.Account +import org.joefang.webdav.data.AccountDao +import org.joefang.webdav.data.CacheDao +import org.joefang.webdav.data.CacheEntry +import org.joefang.webdav.data.SecretString +import org.joefang.webdav.provider.WebDavProvider import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.internal.EMPTY_REQUEST diff --git a/app/src/androidTest/java/dev/rocli/android/webdav/ScreenshotTest.kt b/app/src/androidTest/java/org/joefang/webdav/ScreenshotTest.kt similarity index 93% rename from app/src/androidTest/java/dev/rocli/android/webdav/ScreenshotTest.kt rename to app/src/androidTest/java/org/joefang/webdav/ScreenshotTest.kt index 131c0cf..5952bf5 100644 --- a/app/src/androidTest/java/dev/rocli/android/webdav/ScreenshotTest.kt +++ b/app/src/androidTest/java/org/joefang/webdav/ScreenshotTest.kt @@ -1,4 +1,4 @@ -package dev.rocli.android.webdav +package org.joefang.webdav import androidx.test.core.app.ActivityScenario import androidx.test.core.app.takeScreenshot @@ -20,11 +20,11 @@ import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import dev.rocli.android.webdav.activities.MainActivity -import dev.rocli.android.webdav.adapters.AccountAdapter -import dev.rocli.android.webdav.data.Account -import dev.rocli.android.webdav.data.AccountDao -import dev.rocli.android.webdav.data.SecretString +import org.joefang.webdav.activities.MainActivity +import org.joefang.webdav.adapters.AccountAdapter +import org.joefang.webdav.data.Account +import org.joefang.webdav.data.AccountDao +import org.joefang.webdav.data.SecretString import org.junit.After import org.junit.Before import org.junit.Rule diff --git a/app/src/androidTest/java/dev/rocli/android/webdav/WebDavTestRunner.kt b/app/src/androidTest/java/org/joefang/webdav/WebDavTestRunner.kt similarity index 93% rename from app/src/androidTest/java/dev/rocli/android/webdav/WebDavTestRunner.kt rename to app/src/androidTest/java/org/joefang/webdav/WebDavTestRunner.kt index f0205c2..1b8f895 100644 --- a/app/src/androidTest/java/dev/rocli/android/webdav/WebDavTestRunner.kt +++ b/app/src/androidTest/java/org/joefang/webdav/WebDavTestRunner.kt @@ -1,4 +1,4 @@ -package dev.rocli.android.webdav +package org.joefang.webdav import android.app.Application import android.content.Context diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7af71ad..9ec5444 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,7 +25,7 @@ diff --git a/app/src/main/java/dev/rocli/android/webdav/provider/WebDavPath.kt b/app/src/main/java/org/joefang/webdav/provider/WebDavPath.kt similarity index 72% rename from app/src/main/java/dev/rocli/android/webdav/provider/WebDavPath.kt rename to app/src/main/java/org/joefang/webdav/provider/WebDavPath.kt index 8ddc8a1..c453782 100644 --- a/app/src/main/java/dev/rocli/android/webdav/provider/WebDavPath.kt +++ b/app/src/main/java/org/joefang/webdav/provider/WebDavPath.kt @@ -1,6 +1,6 @@ -package dev.rocli.android.webdav.provider +package org.joefang.webdav.provider -import dev.rocli.android.webdav.extensions.ensureTrailingSlash +import org.joefang.webdav.extensions.ensureTrailingSlash import java.nio.file.Path class WebDavPath(val path: Path, val isDirectory: Boolean) { diff --git a/app/src/main/java/dev/rocli/android/webdav/provider/WebDavProvider.kt b/app/src/main/java/org/joefang/webdav/provider/WebDavProvider.kt similarity index 98% rename from app/src/main/java/dev/rocli/android/webdav/provider/WebDavProvider.kt rename to app/src/main/java/org/joefang/webdav/provider/WebDavProvider.kt index 63de96c..e60fd59 100644 --- a/app/src/main/java/dev/rocli/android/webdav/provider/WebDavProvider.kt +++ b/app/src/main/java/org/joefang/webdav/provider/WebDavProvider.kt @@ -1,4 +1,4 @@ -package dev.rocli.android.webdav.provider +package org.joefang.webdav.provider import android.content.Context import android.database.Cursor @@ -16,11 +16,11 @@ import dagger.hilt.EntryPoints import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.* -import dev.rocli.android.webdav.BuildConfig -import dev.rocli.android.webdav.R -import dev.rocli.android.webdav.data.Account -import dev.rocli.android.webdav.data.AccountDao -import dev.rocli.android.webdav.extensions.urlEncode +import org.joefang.webdav.BuildConfig +import org.joefang.webdav.R +import org.joefang.webdav.data.Account +import org.joefang.webdav.data.AccountDao +import org.joefang.webdav.extensions.urlEncode import okio.IOException import java.io.FileNotFoundException import java.nio.file.Path diff --git a/app/src/main/java/dev/rocli/android/webdav/provider/WebDavReadProxyCallback.kt b/app/src/main/java/org/joefang/webdav/provider/WebDavReadProxyCallback.kt similarity index 98% rename from app/src/main/java/dev/rocli/android/webdav/provider/WebDavReadProxyCallback.kt rename to app/src/main/java/org/joefang/webdav/provider/WebDavReadProxyCallback.kt index d8afeea..709c5c4 100644 --- a/app/src/main/java/dev/rocli/android/webdav/provider/WebDavReadProxyCallback.kt +++ b/app/src/main/java/org/joefang/webdav/provider/WebDavReadProxyCallback.kt @@ -1,4 +1,4 @@ -package dev.rocli.android.webdav.provider +package org.joefang.webdav.provider import android.os.ProxyFileDescriptorCallback import android.system.ErrnoException diff --git a/app/src/main/java/dev/rocli/android/webdav/provider/WebDavService.kt b/app/src/main/java/org/joefang/webdav/provider/WebDavService.kt similarity index 95% rename from app/src/main/java/dev/rocli/android/webdav/provider/WebDavService.kt rename to app/src/main/java/org/joefang/webdav/provider/WebDavService.kt index daffb6f..0517442 100644 --- a/app/src/main/java/dev/rocli/android/webdav/provider/WebDavService.kt +++ b/app/src/main/java/org/joefang/webdav/provider/WebDavService.kt @@ -1,4 +1,4 @@ -package dev.rocli.android.webdav.provider +package org.joefang.webdav.provider import com.thegrizzlylabs.sardineandroid.model.Multistatus import okhttp3.RequestBody diff --git a/app/src/main/java/dev/rocli/android/webdav/provider/WebDavWriteProxyCallback.kt b/app/src/main/java/org/joefang/webdav/provider/WebDavWriteProxyCallback.kt similarity index 98% rename from app/src/main/java/dev/rocli/android/webdav/provider/WebDavWriteProxyCallback.kt rename to app/src/main/java/org/joefang/webdav/provider/WebDavWriteProxyCallback.kt index 73b7b16..fb4fdcf 100644 --- a/app/src/main/java/dev/rocli/android/webdav/provider/WebDavWriteProxyCallback.kt +++ b/app/src/main/java/org/joefang/webdav/provider/WebDavWriteProxyCallback.kt @@ -1,4 +1,4 @@ -package dev.rocli.android.webdav.provider +package org.joefang.webdav.provider import android.os.ProxyFileDescriptorCallback import android.system.ErrnoException diff --git a/app/src/main/res/layout/card_account.xml b/app/src/main/res/layout/card_account.xml index ca14aba..8c5219b 100644 --- a/app/src/main/res/layout/card_account.xml +++ b/app/src/main/res/layout/card_account.xml @@ -4,7 +4,7 @@ xmlns:tool="http://schemas.android.com/tools"> - + - + - + + tools:context="org.joefang.webdav.activities.MainActivity"> + android:name="org.joefang.webdav.fragments.LicensesFragment"> support+webdav@rocli.dev https://github.com/alexbakker/webdav-provider https://rocli.dev - https://play.google.com/store/apps/details?id=dev.rocli.android.webdav + https://play.google.com/store/apps/details?id=org.joefang.webdav Version copied to the clipboard diff --git a/app/src/test/java/dev/rocli/android/webdav/data/CustomHeaderTest.kt b/app/src/test/java/org/joefang/webdav/data/CustomHeaderTest.kt similarity index 98% rename from app/src/test/java/dev/rocli/android/webdav/data/CustomHeaderTest.kt rename to app/src/test/java/org/joefang/webdav/data/CustomHeaderTest.kt index 69f08ab..e41d839 100644 --- a/app/src/test/java/dev/rocli/android/webdav/data/CustomHeaderTest.kt +++ b/app/src/test/java/org/joefang/webdav/data/CustomHeaderTest.kt @@ -1,4 +1,4 @@ -package dev.rocli.android.webdav.data +package org.joefang.webdav.data import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse diff --git a/app/src/test/java/dev/rocli/android/webdav/provider/CustomHeadersInterceptorTest.kt b/app/src/test/java/org/joefang/webdav/provider/CustomHeadersInterceptorTest.kt similarity index 98% rename from app/src/test/java/dev/rocli/android/webdav/provider/CustomHeadersInterceptorTest.kt rename to app/src/test/java/org/joefang/webdav/provider/CustomHeadersInterceptorTest.kt index 5f69eda..18ac5a0 100644 --- a/app/src/test/java/dev/rocli/android/webdav/provider/CustomHeadersInterceptorTest.kt +++ b/app/src/test/java/org/joefang/webdav/provider/CustomHeadersInterceptorTest.kt @@ -1,6 +1,6 @@ -package dev.rocli.android.webdav.provider +package org.joefang.webdav.provider -import dev.rocli.android.webdav.data.CustomHeader +import org.joefang.webdav.data.CustomHeader import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.mockwebserver.MockResponse From 592d3b0544e4dcc9cf4bff4661ba1dfc36a0c2bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 03:29:33 +0000 Subject: [PATCH 3/3] Add CI/CD signing architecture with separate environments and update docs Co-authored-by: MinecraftFuns <25814618+MinecraftFuns@users.noreply.github.com> --- .github/workflows/build-release.yaml | 112 +++----------- .github/workflows/build.yml | 127 ++++++++++++++++ .github/workflows/ci-build.yml | 86 +++-------- .github/workflows/ci-tests.yml | 4 +- .github/workflows/release.yml | 126 +++------------- .github/workflows/sign-release.yml | 112 ++++++++++++++ .github/workflows/sign-test.yml | 85 +++++++++++ README.md | 23 ++- docs/VERSIONING.md | 216 ++++++++++++++++++--------- 9 files changed, 547 insertions(+), 344 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/sign-release.yml create mode 100644 .github/workflows/sign-test.yml diff --git a/.github/workflows/build-release.yaml b/.github/workflows/build-release.yaml index 97b7a5a..5780952 100644 --- a/.github/workflows/build-release.yaml +++ b/.github/workflows/build-release.yaml @@ -1,105 +1,31 @@ -# DEPRECATED: This workflow is being replaced by: -# - ci-tests.yml (for PR/push testing) -# - ci-build.yml (for unsigned build artifacts) -# - release.yml (for tag-triggered signed releases) +# DEPRECATED: This workflow has been replaced by the new CI/CD architecture: +# - build.yml (unsigned builds for all environments) +# - sign-test.yml (signs untrusted PR builds with test key) +# - sign-release.yml (signs production builds with release key) # -# This workflow is kept temporarily for backward compatibility. -# For new releases, use 'git tag v*.*.*' and push the tag. +# This workflow is kept temporarily for backward compatibility and will be removed. +# See docs/VERSIONING.md for the new signing architecture documentation. -name: Build and Publish Android App (Legacy) +name: Build and Publish Android App (Legacy) [DEPRECATED] on: - push: - branches: [master] + # Disabled - replaced by build.yml + sign-release.yml + workflow_dispatch: {} permissions: contents: read jobs: - build_sign_release: - name: Build, Sign, and Release Android App - permissions: - contents: write + deprecated: + name: Deprecated Workflow runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Fetch all history for version calculation - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - cache: 'gradle' - - - name: Calculate version - id: version + - name: Notice run: | - # Get the latest v*.*.* tag - LATEST_TAG=$(git describe --tags --abbrev=0 --match "v*.*.*" 2>/dev/null || echo "v0.0.0") - echo "Latest tag: $LATEST_TAG" - - # Parse version components - VERSION_REGEX="^v([0-9]+)\.([0-9]+)\.([0-9]+)$" - if [[ $LATEST_TAG =~ $VERSION_REGEX ]]; then - MAJOR="${BASH_REMATCH[1]}" - MINOR="${BASH_REMATCH[2]}" - PATCH="${BASH_REMATCH[3]}" - else - MAJOR=0 - MINOR=0 - PATCH=0 - fi - - # Count commits since the last tag - BUILD_COUNT=$(git rev-list --count HEAD "^${LATEST_TAG}" 2>/dev/null || echo "0") - - # For beta builds, increment patch by 1 so betas are HIGHER than previous stable - # Example: after v5.4.3, betas are v5.4.4-beta.X (not v5.4.3-beta.X) - if [[ "$BUILD_COUNT" -gt 0 ]]; then - PATCH=$((PATCH + 1)) - fi - - echo "VERSION_MAJOR=$MAJOR" >> $GITHUB_OUTPUT - echo "VERSION_MINOR=$MINOR" >> $GITHUB_OUTPUT - echo "VERSION_PATCH=$PATCH" >> $GITHUB_OUTPUT - echo "VERSION_BUILD_COUNT=$BUILD_COUNT" >> $GITHUB_OUTPUT - echo "VERSION_NAME=v${MAJOR}.${MINOR}.${PATCH}-beta.${BUILD_COUNT}" >> $GITHUB_OUTPUT - echo "Version: v${MAJOR}.${MINOR}.${PATCH}-beta.${BUILD_COUNT}" - - - name: Run tests before release - run: ./gradlew --no-daemon testDebugUnitTest - env: - VERSION_MAJOR: ${{ steps.version.outputs.VERSION_MAJOR }} - VERSION_MINOR: ${{ steps.version.outputs.VERSION_MINOR }} - VERSION_PATCH: ${{ steps.version.outputs.VERSION_PATCH }} - VERSION_BUILD_COUNT: ${{ steps.version.outputs.VERSION_BUILD_COUNT }} - VERSION_STABLE: 'false' - - - name: Decode and Import Keystore - run: | - echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > $GITHUB_WORKSPACE/keystore.jks - env: - ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} - - - name: Build and Sign the App - run: ./gradlew --no-daemon app:assembleRelease - env: - ANDROID_KEYSTORE: ${{ github.workspace }}/keystore.jks - ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASS }} - ANDROID_KEY_ALIAS: android - ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASS }} - VERSION_MAJOR: ${{ steps.version.outputs.VERSION_MAJOR }} - VERSION_MINOR: ${{ steps.version.outputs.VERSION_MINOR }} - VERSION_PATCH: ${{ steps.version.outputs.VERSION_PATCH }} - VERSION_BUILD_COUNT: ${{ steps.version.outputs.VERSION_BUILD_COUNT }} - VERSION_STABLE: 'false' - - - name: Create a GitHub Release - uses: softprops/action-gh-release@v2 - with: - prerelease: true - files: app/build/outputs/apk/release/app-release.apk - tag_name: ${{ steps.version.outputs.VERSION_NAME }} + echo "This workflow is deprecated and has been replaced by:" + echo " - build.yml (unsigned builds)" + echo " - sign-test.yml (test signing)" + echo " - sign-release.yml (production signing)" + echo "" + echo "For new releases, push a git tag matching v*.*.* pattern." + exit 1 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..54ac73d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,127 @@ +# Build Workflow - Builds unsigned APKs for all environments +# +# This workflow builds APKs but NEVER signs them. It has no access to signing secrets. +# The appropriate version naming is baked into the binary based on CI_BUILD_TYPE: +# - release: v1.2.3 +# - prerelease: v1.2.3-dev.X+ +# - test: ci-test-untrusted-1.2.3-dev.X+ +# +# Artifacts are uploaded for downstream signing workflows. + +name: Build + +on: + push: + branches: ["master"] + tags: ["v*"] + pull_request: + branches: ["master"] + +permissions: + contents: read + +jobs: + build: + name: Build Unsigned APKs + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for version calculation + + - name: Determine Build Type + id: build-type + run: | + if [[ $GITHUB_REF == refs/tags/v* ]]; then + echo "CI_BUILD_TYPE=release" >> $GITHUB_ENV + echo "build_type=release" >> $GITHUB_OUTPUT + elif [[ $GITHUB_REF == refs/heads/master ]]; then + echo "CI_BUILD_TYPE=prerelease" >> $GITHUB_ENV + echo "build_type=prerelease" >> $GITHUB_OUTPUT + else + echo "CI_BUILD_TYPE=test" >> $GITHUB_ENV + echo "build_type=test" >> $GITHUB_OUTPUT + fi + + - name: Calculate Version + id: version + run: | + # Get the latest v*.*.* tag + LATEST_TAG=$(git describe --tags --abbrev=0 --match "v*.*.*" 2>/dev/null || echo "v0.0.0") + echo "Latest tag: $LATEST_TAG" + + # Parse version components + VERSION_REGEX="^v([0-9]+)\.([0-9]+)\.([0-9]+)$" + if [[ $LATEST_TAG =~ $VERSION_REGEX ]]; then + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + PATCH="${BASH_REMATCH[3]}" + else + MAJOR=0 + MINOR=0 + PATCH=0 + fi + + # Count commits since the last tag + BUILD_COUNT=$(git rev-list --count HEAD "^${LATEST_TAG}" 2>/dev/null || echo "0") + + echo "VERSION_MAJOR=$MAJOR" >> $GITHUB_OUTPUT + echo "VERSION_MINOR=$MINOR" >> $GITHUB_OUTPUT + echo "VERSION_PATCH=$PATCH" >> $GITHUB_OUTPUT + echo "VERSION_BUILD_COUNT=$BUILD_COUNT" >> $GITHUB_OUTPUT + echo "Version: ${MAJOR}.${MINOR}.${PATCH} (build count: ${BUILD_COUNT})" + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + cache: gradle + + # Build untrusted flavor for test builds (PRs) + - name: Build Untrusted APK (for test signing) + if: steps.build-type.outputs.build_type == 'test' + run: ./gradlew --no-daemon :app:assembleUntrustedRelease + env: + VERSION_MAJOR: ${{ steps.version.outputs.VERSION_MAJOR }} + VERSION_MINOR: ${{ steps.version.outputs.VERSION_MINOR }} + VERSION_PATCH: ${{ steps.version.outputs.VERSION_PATCH }} + VERSION_BUILD_COUNT: ${{ steps.version.outputs.VERSION_BUILD_COUNT }} + VERSION_STABLE: 'false' + CI_BUILD_TYPE: test + + # Build prod flavor for pre-release and release builds + - name: Build Prod APK (for release signing) + if: steps.build-type.outputs.build_type != 'test' + run: ./gradlew --no-daemon :app:assembleProdRelease + env: + VERSION_MAJOR: ${{ steps.version.outputs.VERSION_MAJOR }} + VERSION_MINOR: ${{ steps.version.outputs.VERSION_MINOR }} + VERSION_PATCH: ${{ steps.version.outputs.VERSION_PATCH }} + VERSION_BUILD_COUNT: ${{ steps.version.outputs.VERSION_BUILD_COUNT }} + VERSION_STABLE: ${{ steps.build-type.outputs.build_type == 'release' && 'true' || 'false' }} + CI_BUILD_TYPE: ${{ steps.build-type.outputs.build_type }} + + - name: Upload Untrusted APK Artifact + if: steps.build-type.outputs.build_type == 'test' + uses: actions/upload-artifact@v4 + with: + name: unsigned-untrusted-apk + path: app/build/outputs/apk/untrusted/release/*.apk + retention-days: 30 + + - name: Upload Prod APK Artifact + if: steps.build-type.outputs.build_type != 'test' + uses: actions/upload-artifact@v4 + with: + name: unsigned-prod-apk + path: app/build/outputs/apk/prod/release/*.apk + retention-days: 30 + + outputs: + build_type: ${{ steps.build-type.outputs.build_type }} + version_major: ${{ steps.version.outputs.VERSION_MAJOR }} + version_minor: ${{ steps.version.outputs.VERSION_MINOR }} + version_patch: ${{ steps.version.outputs.VERSION_PATCH }} + version_build_count: ${{ steps.version.outputs.VERSION_BUILD_COUNT }} diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index cc56d68..8c3a659 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -1,77 +1,31 @@ -name: CI - Build (Unsigned) +# DEPRECATED: This workflow has been replaced by the new CI/CD architecture: +# - build.yml (unsigned builds for all environments) +# - sign-test.yml (signs untrusted PR builds with test key) +# - sign-release.yml (signs production builds with release key) +# +# This workflow is kept temporarily for backward compatibility and will be removed. +# See docs/VERSIONING.md for the new signing architecture documentation. + +name: CI - Build (Unsigned) [DEPRECATED] on: - pull_request: - branches: [master] - push: - branches: [master] + # Disabled - replaced by build.yml + workflow_dispatch: {} permissions: contents: read jobs: build: - name: Build Unsigned APK + name: Build Unsigned APK (DEPRECATED) runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Fetch all history for version calculation - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: '17' - cache: gradle - - - name: Calculate version - id: version + - name: Notice run: | - # Get the latest v*.*.* tag - LATEST_TAG=$(git describe --tags --abbrev=0 --match "v*.*.*" 2>/dev/null || echo "v0.0.0") - echo "Latest tag: $LATEST_TAG" - - # Parse version components - VERSION_REGEX="^v([0-9]+)\.([0-9]+)\.([0-9]+)$" - if [[ $LATEST_TAG =~ $VERSION_REGEX ]]; then - MAJOR="${BASH_REMATCH[1]}" - MINOR="${BASH_REMATCH[2]}" - PATCH="${BASH_REMATCH[3]}" - else - MAJOR=0 - MINOR=0 - PATCH=0 - fi - - # Count commits since the last tag - BUILD_COUNT=$(git rev-list --count HEAD "^${LATEST_TAG}" 2>/dev/null || echo "0") - - # For beta builds, increment patch by 1 so betas are HIGHER than previous stable - # Example: after v5.4.3, betas are v5.4.4-beta.X (not v5.4.3-beta.X) - if [[ "$BUILD_COUNT" -gt 0 ]]; then - PATCH=$((PATCH + 1)) - fi - - echo "VERSION_MAJOR=$MAJOR" >> $GITHUB_OUTPUT - echo "VERSION_MINOR=$MINOR" >> $GITHUB_OUTPUT - echo "VERSION_PATCH=$PATCH" >> $GITHUB_OUTPUT - echo "VERSION_BUILD_COUNT=$BUILD_COUNT" >> $GITHUB_OUTPUT - echo "Version: v${MAJOR}.${MINOR}.${PATCH}-beta.${BUILD_COUNT}" - - - name: Build debug APK - run: ./gradlew --no-daemon :app:assembleDebug - env: - VERSION_MAJOR: ${{ steps.version.outputs.VERSION_MAJOR }} - VERSION_MINOR: ${{ steps.version.outputs.VERSION_MINOR }} - VERSION_PATCH: ${{ steps.version.outputs.VERSION_PATCH }} - VERSION_BUILD_COUNT: ${{ steps.version.outputs.VERSION_BUILD_COUNT }} - VERSION_STABLE: 'false' - - - name: Upload debug APK - uses: actions/upload-artifact@v4 - with: - name: app-debug - path: app/build/outputs/apk/debug/app-debug.apk - retention-days: 30 + echo "This workflow is deprecated and has been replaced by:" + echo " - build.yml (unsigned builds)" + echo " - sign-test.yml (test signing)" + echo " - sign-release.yml (production signing)" + echo "" + echo "Please update your references to use the new workflows." + exit 1 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 309f7c5..286e7e7 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -27,7 +27,7 @@ jobs: cache: gradle - name: Run unit tests - run: ./gradlew --no-daemon testDebugUnitTest + run: ./gradlew --no-daemon testProdDebugUnitTest - name: Upload test reports if: failure() @@ -54,7 +54,7 @@ jobs: cache: gradle - name: Run lint - run: ./gradlew --no-daemon lint + run: ./gradlew --no-daemon lintProdDebug - name: Upload lint reports if: failure() diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d39330f..9e0e3e6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,118 +1,30 @@ -name: Release - Sign and Publish +# DEPRECATED: This workflow has been replaced by the new CI/CD architecture: +# - build.yml (unsigned builds for all environments) +# - sign-release.yml (signs production builds with release key) +# +# This workflow is kept temporarily for backward compatibility. +# See docs/VERSIONING.md for the new signing architecture documentation. + +name: Release - Sign and Publish [DEPRECATED] on: - push: - tags: ['v*.*.*'] + # Disabled - replaced by build.yml + sign-release.yml workflow_dispatch: {} permissions: contents: read jobs: - verify: - name: Verify Tests Pass + deprecated: + name: Deprecated Workflow runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Fetch all history for version calculation - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: '17' - cache: gradle - - - name: Calculate version - id: version + - name: Notice run: | - if [[ "${{ github.ref_type }}" == "tag" ]]; then - TAG="${{ github.ref_name }}" - else - # For workflow_dispatch, get the latest tag - TAG=$(git describe --tags --abbrev=0 --match "v*.*.*" 2>/dev/null || echo "v0.0.0") - fi - echo "Tag: $TAG" - - # Parse version components - VERSION_REGEX="^v([0-9]+)\.([0-9]+)\.([0-9]+)$" - if [[ $TAG =~ $VERSION_REGEX ]]; then - MAJOR="${BASH_REMATCH[1]}" - MINOR="${BASH_REMATCH[2]}" - PATCH="${BASH_REMATCH[3]}" - else - echo "Error: Tag $TAG does not match v*.*.* pattern" - exit 1 - fi - - echo "VERSION_MAJOR=$MAJOR" >> $GITHUB_OUTPUT - echo "VERSION_MINOR=$MINOR" >> $GITHUB_OUTPUT - echo "VERSION_PATCH=$PATCH" >> $GITHUB_OUTPUT - echo "Stable Version: v${MAJOR}.${MINOR}.${PATCH}" - - - name: Run tests (release gate) - run: ./gradlew --no-daemon testDebugUnitTest lint - env: - VERSION_MAJOR: ${{ steps.version.outputs.VERSION_MAJOR }} - VERSION_MINOR: ${{ steps.version.outputs.VERSION_MINOR }} - VERSION_PATCH: ${{ steps.version.outputs.VERSION_PATCH }} - VERSION_STABLE: 'true' - outputs: - version_major: ${{ steps.version.outputs.VERSION_MAJOR }} - version_minor: ${{ steps.version.outputs.VERSION_MINOR }} - version_patch: ${{ steps.version.outputs.VERSION_PATCH }} - - sign_and_release: - name: Sign and Release - needs: verify - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Fetch all history for version calculation - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: '17' - cache: gradle - - - name: Decode keystore - run: echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > $GITHUB_WORKSPACE/keystore.jks - env: - ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} - - - name: Build and sign - run: ./gradlew --no-daemon :app:assembleRelease - env: - ANDROID_KEYSTORE: ${{ github.workspace }}/keystore.jks - ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASS }} - ANDROID_KEY_ALIAS: android - # Note: Using same password for keystore and key (common pattern for CI keystores) - ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASS }} - VERSION_MAJOR: ${{ needs.verify.outputs.version_major }} - VERSION_MINOR: ${{ needs.verify.outputs.version_minor }} - VERSION_PATCH: ${{ needs.verify.outputs.version_patch }} - VERSION_STABLE: 'true' - - - name: Get version from tag - id: get-version - run: | - if [[ "${{ github.ref_type }}" == "tag" ]]; then - echo "version=${{ github.ref_name }}" >> $GITHUB_OUTPUT - else - echo "version=v${{ needs.verify.outputs.version_major }}.${{ needs.verify.outputs.version_minor }}.${{ needs.verify.outputs.version_patch }}" >> $GITHUB_OUTPUT - fi - - - name: Publish GitHub Release - uses: softprops/action-gh-release@v2 - with: - prerelease: ${{ github.ref_type != 'tag' }} - tag_name: ${{ steps.get-version.outputs.version }} - files: app/build/outputs/apk/release/app-release.apk + echo "This workflow is deprecated and has been replaced by:" + echo " - build.yml (unsigned builds)" + echo " - sign-release.yml (production signing)" + echo "" + echo "For new releases, push a git tag matching v*.*.* pattern." + echo "The build.yml and sign-release.yml workflows will handle signing and publishing." + exit 1 diff --git a/.github/workflows/sign-release.yml b/.github/workflows/sign-release.yml new file mode 100644 index 0000000..73e22ba --- /dev/null +++ b/.github/workflows/sign-release.yml @@ -0,0 +1,112 @@ +# Sign Release Workflow - Signs production builds (pre-releases and releases) +# +# This workflow signs builds from the main branch and tags using the ci:release environment. +# It is triggered by completion of the Build workflow for trusted branches. +# +# - Pre-releases (main branch): Creates a GitHub pre-release +# - Releases (v*.*.* tags): Creates a GitHub release +# +# Environment: ci:release (production key, with protection rules) + +name: Sign and Publish Release + +on: + workflow_run: + workflows: ["Build"] + types: [completed] + +jobs: + sign-release: + name: Sign and Publish + # Only run for main branch or tags that completed successfully + if: > + github.event.workflow_run.conclusion == 'success' && + (github.event.workflow_run.head_branch == 'master' || + startsWith(github.event.workflow_run.head_branch, 'v')) + runs-on: ubuntu-latest + environment: ci:release + permissions: + contents: write + actions: read + + steps: + - name: Download Unsigned Artifact + uses: actions/download-artifact@v4 + with: + name: unsigned-prod-apk + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + path: ./unsigned + + - name: Set up JDK 17 (for apksigner) + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - name: Determine Release Type + id: release-type + run: | + BRANCH="${{ github.event.workflow_run.head_branch }}" + if [[ "$BRANCH" == v* ]]; then + echo "is_stable=true" >> $GITHUB_OUTPUT + echo "tag_name=$BRANCH" >> $GITHUB_OUTPUT + echo "release_name=Release $BRANCH" >> $GITHUB_OUTPUT + else + # Generate a pre-release tag based on date and run ID + TAG="pre-$(date +%Y%m%d)-${{ github.event.workflow_run.run_number }}" + echo "is_stable=false" >> $GITHUB_OUTPUT + echo "tag_name=$TAG" >> $GITHUB_OUTPUT + echo "release_name=Pre-release $TAG" >> $GITHUB_OUTPUT + fi + + - name: Sign Prod APK + run: | + # 1. Recover keystore from base64 secret + echo "${{ secrets.ANDROID_KEYSTORE_B64 }}" | base64 -d > release.jks + + # 2. Find the unsigned APK + APK_PATH=$(find ./unsigned -name "*.apk" | head -n 1) + if [[ -z "$APK_PATH" ]]; then + echo "Error: No APK found in artifacts" + exit 1 + fi + echo "Signing: $APK_PATH" + + # 3. Find apksigner + APKSIGNER=$(find $ANDROID_SDK_ROOT/build-tools -name apksigner | sort -V | tail -n1) + echo "Using apksigner: $APKSIGNER" + + # 4. Determine output filename + if [[ "${{ steps.release-type.outputs.is_stable }}" == "true" ]]; then + OUTPUT_NAME="webdav-${{ steps.release-type.outputs.tag_name }}.apk" + else + OUTPUT_NAME="webdav-prerelease-${{ steps.release-type.outputs.tag_name }}.apk" + fi + + # 5. Sign the APK + $APKSIGNER sign \ + --ks release.jks \ + --ks-pass "pass:${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" \ + --key-pass "pass:${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" \ + --ks-key-alias "${{ vars.ANDROID_KEY_ALIAS }}" \ + --out "$OUTPUT_NAME" \ + "$APK_PATH" + + # 6. Verify signature + $APKSIGNER verify --verbose "$OUTPUT_NAME" + + # 7. Set output for next step + echo "SIGNED_APK=$OUTPUT_NAME" >> $GITHUB_ENV + + # 8. Cleanup keystore + rm release.jks + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: ${{ env.SIGNED_APK }} + tag_name: ${{ steps.release-type.outputs.tag_name }} + name: ${{ steps.release-type.outputs.release_name }} + prerelease: ${{ steps.release-type.outputs.is_stable != 'true' }} + generate_release_notes: true diff --git a/.github/workflows/sign-test.yml b/.github/workflows/sign-test.yml new file mode 100644 index 0000000..dca53b1 --- /dev/null +++ b/.github/workflows/sign-test.yml @@ -0,0 +1,85 @@ +# Sign Test Workflow - Signs untrusted PR builds +# +# This workflow signs PR builds using the ci:test environment. +# It is triggered by completion of the Build workflow and runs the version +# of the workflow defined in 'master', not the PR branch, ensuring security. +# +# Security: Does NOT checkout code from the PR. Only downloads and signs artifacts. +# +# The signed APK is uploaded as an artifact for testers to download and install. +# WARNING: These binaries are signed with a TEST key, NOT the production key. +# They are explicitly UNTRUSTED and should only be used for testing purposes. + +name: Sign Test APK + +on: + workflow_run: + workflows: ["Build"] + types: [completed] + +jobs: + sign-test: + name: Sign Untrusted Test APK + # Only run for PRs that completed successfully + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + environment: ci:test + permissions: + actions: read + contents: read + + steps: + - name: Download Unsigned Artifact + uses: actions/download-artifact@v4 + with: + name: unsigned-untrusted-apk + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + path: ./unsigned + + - name: Set up JDK 17 (for apksigner) + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - name: Sign Test APK + run: | + # 1. Recover keystore from base64 secret + echo "${{ secrets.ANDROID_KEYSTORE_B64 }}" | base64 -d > test.jks + + # 2. Find the unsigned APK + APK_PATH=$(find ./unsigned -name "*.apk" | head -n 1) + if [[ -z "$APK_PATH" ]]; then + echo "Error: No APK found in artifacts" + exit 1 + fi + echo "Signing: $APK_PATH" + + # 3. Find apksigner + APKSIGNER=$(find $ANDROID_SDK_ROOT/build-tools -name apksigner | sort -V | tail -n1) + echo "Using apksigner: $APKSIGNER" + + # 4. Sign the APK + $APKSIGNER sign \ + --ks test.jks \ + --ks-pass "pass:${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" \ + --key-pass "pass:${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" \ + --ks-key-alias "${{ vars.ANDROID_KEY_ALIAS }}" \ + --out "webdav-test-signed.apk" \ + "$APK_PATH" + + # 5. Verify signature + $APKSIGNER verify --verbose "webdav-test-signed.apk" + + # 6. Cleanup keystore + rm test.jks + + - name: Upload Signed Test APK + uses: actions/upload-artifact@v4 + with: + name: webdav-test-signed-apk + path: webdav-test-signed.apk + retention-days: 30 diff --git a/README.md b/README.md index 9eebd3a..c55b6f4 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,26 @@ App icon -# WebDAV Provider [![build](https://github.com/alexbakker/webdav-provider/actions/workflows/build.yaml/badge.svg)](https://github.com/alexbakker/webdav-provider/actions/workflows/build.yaml) +# WebDAV Provider [![Build](https://github.com/BTreeMap/WebDAV/actions/workflows/build.yml/badge.svg)](https://github.com/BTreeMap/WebDAV/actions/workflows/build.yml) __WebDAV Provider__ is an Android app that can expose WebDAV through Android's Storage Access Framework (SAF). This allows you to access your WebDAV storage through Android's built-in file explorer, as well as other apps on your device. -[Get it on Google Play](https://play.google.com/store/apps/details?id=dev.rocli.android.webdav) +## Fork Information + +> **⚠️ This is a fork of [alexbakker/webdav-provider](https://github.com/alexbakker/webdav-provider)** +> +> This project has been forked and significantly modified. We use a **different package name** (`org.joefang.webdav` instead of `dev.rocli.android.webdav`) and **do not maintain compatibility** with the original project. Updates and features may diverge from the upstream project. +> +> If you're looking for the original project, please visit [alexbakker/webdav-provider](https://github.com/alexbakker/webdav-provider). + +### Key Differences from Upstream + +- **Package Name**: `org.joefang.webdav` (original: `dev.rocli.android.webdav`) +- **Separate Signing Keys**: Test builds use a different signing key than production releases +- **No Backward Compatibility**: This fork may introduce breaking changes +- **Independent Development**: Features and bug fixes may not be merged back upstream ## Screenshots @@ -49,5 +60,5 @@ docker compose --project-directory tests down -v This project uses an automatic versioning system based on git tags. See [docs/VERSIONING.md](docs/VERSIONING.md) for details on: - The 30-bit version code schema -- How stable vs beta versions are calculated -- CI/CD workflow configuration +- How stable vs development versions are calculated +- CI/CD workflow configuration and signing architecture diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index f1f5437..0458a99 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -1,12 +1,12 @@ -# Version Code Generation +# Version Code Generation & CI/CD Signing Architecture -This document describes the automatic version code generation system used in WebDAV Provider. +This document describes the automatic version code generation system and the secure CI/CD signing architecture used in WebDAV Provider. ## Overview The app uses a **30-bit version code schema** that automatically derives version codes from git tags. This ensures: -1. **Proper ordering**: Beta builds are always higher than previous stable releases +1. **Proper ordering**: Development builds are always higher than previous stable releases 2. **Google Play compliance**: Maximum value (~1.07 billion) is under the 2.1 billion limit 3. **Automation**: No manual version code management needed @@ -17,7 +17,7 @@ The app uses a **30-bit version code schema** that automatically derives version | Major | 7 | 0-127 | `<< 23` | Major version number | | Minor | 7 | 0-127 | `<< 16` | Minor version number | | Patch | 7 | 0-127 | `<< 9` | Patch version number | -| Qualifier | 9 | 0-511 | `<< 0` | 511 for stable, 0-510 for beta builds | +| Qualifier | 9 | 0-511 | `<< 0` | 511 for stable, 0-510 for dev builds | ### Version Code Formula @@ -31,108 +31,186 @@ versionCode = (Major << 23) | (Minor << 16) | (Patch << 9) | Qualifier - **Maximum version code**: 1,073,741,823 (2³⁰ - 1) - **Google Play limit**: 2,100,000,000 -## Release Types +## Build Types & Version Naming -### Stable Releases (Tagged) +The version name format differs based on the build type: -Stable releases are created by pushing a git tag matching `v*.*.*` pattern: +| Build Type | Trigger | Version Name Format | Example | +|--------------|----------------------|-----------------------------------------------|----------------------------------| +| **Release** | Git tag `v*.*.*` | `v{major}.{minor}.{patch}` | `v5.4.3` | +| **Pre-release** | Push to `master` | `v{major}.{minor}.{patch}-dev.{count}+{sha}` | `v5.4.4-dev.42+a1b2c3d` | +| **Test** | Pull Request | `ci-test-untrusted-{major}.{minor}.{patch}-dev.{count}+{sha}` | `ci-test-untrusted-5.4.4-dev.42+a1b2c3d` | -```bash -git tag v5.4.3 -git push origin v5.4.3 -``` +### Important Notes -- **Qualifier**: 511 (maximum value) -- **Version Name**: `v5.4.3` -- **Version Code**: `(5 << 23) | (4 << 16) | (3 << 9) | 511 = 42,207,231` +- **Pre-release** and **Test** builds increment the patch version by 1 to ensure they are always HIGHER than the previous stable release +- **Test** builds use an explicitly **non-SemVer** format to make it clear they are untrusted -### Beta/Development Builds +## Product Flavors -Beta builds are created automatically for any commit after a stable release: +The app has two product flavors for co-installation support: -- **Qualifier**: Commits since last tag (1-510) -- **Patch**: Incremented by 1 from the tag version -- **Version Name**: `v5.4.4-beta.X` (where X = commits since tag) +| Flavor | Application ID | Purpose | +|--------------|---------------------------|-----------------------------------| +| `prod` | `org.joefang.webdav` | Production releases | +| `untrusted` | `org.joefang.webdav.test` | Untrusted test builds (PRs) | -This increment ensures beta builds are **always higher** than the previous stable release. +This allows testers to install both the production app and test builds side-by-side. -## Version Ordering Example +## Secure Signing Architecture -Assuming the latest release tag is `v5.4.3`: +### GitHub Environments + +The CI/CD pipeline uses two separate GitHub Environments to isolate signing keys: + +| Environment | Purpose | Key Usage | Protection Rules | +|---------------|------------------------------|------------------|-------------------------------| +| `ci:test` | Signing untrusted PR builds | **Test Key** | No restrictions (automated) | +| `ci:release` | Signing main/tag builds | **Release Key** | Branch: `master`, `v*` | + +### Key Isolation + +> ⚠️ **SECURITY**: The test key and release key are **completely separate**. Untrusted PR code never has access to the production release key. + +- **Test builds** (from PRs) are signed with a dedicated test key +- **Production builds** (from main branch and tags) are signed with the production release key +- The Build workflow (`build.yml`) has **no access to any signing secrets** + +### CI/CD Workflows + +#### 1. Build Workflow (`build.yml`) -| Version | Type | Major | Minor | Patch | Qualifier | Version Code | -|------------------|--------|-------|-------|-------|-----------|--------------| -| v5.4.3 | Stable | 5 | 4 | 3 | 511 | 42,207,231 | -| v5.4.4-beta.1 | Beta | 5 | 4 | 4 | 1 | 42,207,233 | -| v5.4.4-beta.2 | Beta | 5 | 4 | 4 | 2 | 42,207,234 | -| v5.4.4-beta.55 | Beta | 5 | 4 | 4 | 55 | 42,207,287 | -| v5.4.4 | Stable | 5 | 4 | 4 | 511 | 42,207,743 | -| v5.5.0 | Stable | 5 | 5 | 0 | 511 | 42,271,231 | +The "untrusted builder" that builds APKs but never signs them: -**Key insight**: `v5.4.3 < v5.4.4-beta.1 < v5.4.4-beta.2 < v5.4.4 < v5.5.0` +- Triggered by: PRs, pushes to `master`, and tags +- Determines `CI_BUILD_TYPE` based on event +- Builds the appropriate flavor (untrusted for PRs, prod for releases) +- Uploads unsigned APKs as artifacts +- **Has no access to signing secrets** -## Why Increment Patch for Betas? +#### 2. Sign Test Workflow (`sign-test.yml`) -If betas used the same Major.Minor.Patch as the previous stable: -- `v5.4.3` (stable) = `(5 << 23) | (4 << 16) | (3 << 9) | 511` = 42,207,231 -- `v5.4.3-beta.1` = `(5 << 23) | (4 << 16) | (3 << 9) | 1` = 42,206,721 ❌ **LOWER!** +Signs untrusted PR builds with the test key: -By incrementing patch: -- `v5.4.3` (stable) = `(5 << 23) | (4 << 16) | (3 << 9) | 511` = 42,207,231 -- `v5.4.4-beta.1` = `(5 << 23) | (4 << 16) | (4 << 9) | 1` = 42,207,233 ✅ **HIGHER!** +- Triggered by: Completion of Build workflow for PRs +- Uses environment: `ci:test` +- Downloads unsigned artifacts from Build workflow +- Signs with test key +- Uploads signed test APK for testers +- **Does NOT checkout code from the PR** (security measure) -## Build Count Limits +#### 3. Sign Release Workflow (`sign-release.yml`) -The qualifier field has 9 bits, allowing values 0-511: -- **0**: Used when on the exact tag commit itself (no beta, no stable flag set) -- **1-510**: Available for beta builds (commits after last tag) -- **511**: Reserved for stable releases +Signs production builds with the release key: -If you exceed 510 commits between releases, the build will fail. Solution: create a new release tag. +- Triggered by: Completion of Build workflow for main/tags +- Uses environment: `ci:release` (with protection rules) +- Downloads unsigned artifacts from Build workflow +- Signs with production release key +- Creates GitHub Release + +## ⚠️ Test Builds Warning + +> **IMPORTANT**: Test builds (`.test` variant) are signed with a **separate test key** and are **UNTRUSTED**. +> +> These builds are provided for convenience of testing and should **NEVER** be used in production environments. They are clearly marked with the `ci-test-untrusted-` prefix in their version name. +> +> The test key is not the same as our production signing key. Installing test builds does not verify the authenticity of the source. + +## Version Ordering Example + +Assuming the latest release tag is `v5.4.3`: + +| Version | Type | Qualifier | Version Code | +|--------------------------------------|-------------|-----------|--------------| +| v5.4.3 | Release | 511 | 42,207,231 | +| v5.4.4-dev.1+a1b2c3d | Pre-release | 1 | 42,207,233 | +| ci-test-untrusted-5.4.4-dev.1+a1b2c3d| Test | 1 | 42,207,233 | +| v5.4.4-dev.42+b2c3d4e | Pre-release | 42 | 42,207,274 | +| v5.4.4 | Release | 511 | 42,207,743 | ## Environment Variables The version system can be controlled via environment variables (used in CI): -| Variable | Description | Default | -|----------------------|--------------------------------------------------|----------------| -| `VERSION_MAJOR` | Override major version | From git tag | -| `VERSION_MINOR` | Override minor version | From git tag | -| `VERSION_PATCH` | Override patch version | From git tag | -| `VERSION_BUILD_COUNT`| Override build count (commits since tag) | From git | -| `VERSION_STABLE` | Set to `"true"` for stable releases | `"false"` | +| Variable | Description | Default | +|-----------------------|--------------------------------------------------|----------------| +| `VERSION_MAJOR` | Override major version | From git tag | +| `VERSION_MINOR` | Override minor version | From git tag | +| `VERSION_PATCH` | Override patch version | From git tag | +| `VERSION_BUILD_COUNT` | Override build count (commits since tag) | From git | +| `VERSION_STABLE` | Set to `"true"` for stable releases | `"false"` | +| `CI_BUILD_TYPE` | Controls version name format | `"local"` | -## CI/CD Workflows +### CI_BUILD_TYPE Values -### Pull Request / Push Builds (`ci-build.yml`) +| Value | Effect | +|--------------|-----------------------------------------------------------| +| `release` | Version name: `v{M}.{m}.{p}` | +| `prerelease` | Version name: `v{M}.{m}.{p}-dev.{count}+{sha}` | +| `test` | Version name: `ci-test-untrusted-{M}.{m}.{p}-dev.{count}+{sha}` | +| `local` | Same as `prerelease` (default for local builds) | -- Builds debug APK with beta versioning -- Patch is auto-incremented for beta builds -- Artifacts uploaded for testing +## Key Generation -### Tag-Triggered Releases (`release.yml`) +To generate signing keys for the GitHub Environments: -- Triggered by pushing `v*.*.*` tags -- Builds signed release APK -- Creates GitHub release with APK attachment +```bash +# Generate Test Key (for ci:test environment) +export ANDROID_KEY_ALIAS="test" +export ANDROID_KEYSTORE_PASSWORD="" +keytool -genkeypair -v \ + -keystore project-test.jks \ + -storetype JKS \ + -alias "$ANDROID_KEY_ALIAS" \ + -keyalg RSA \ + -keysize 4096 \ + -validity 1048576 \ + -storepass "$ANDROID_KEYSTORE_PASSWORD" \ + -keypass "$ANDROID_KEYSTORE_PASSWORD" \ + -dname "CN=test,O=YourOrg,OU=Mobile,L=Toronto,ST=Ontario,C=CA" + +# Generate Release Key (for ci:release environment) +export ANDROID_KEY_ALIAS="release" +export ANDROID_KEYSTORE_PASSWORD="" +keytool -genkeypair -v \ + -keystore project-release.jks \ + -storetype JKS \ + -alias "$ANDROID_KEY_ALIAS" \ + -keyalg RSA \ + -keysize 4096 \ + -validity 1048576 \ + -storepass "$ANDROID_KEYSTORE_PASSWORD" \ + -keypass "$ANDROID_KEYSTORE_PASSWORD" \ + -dname "CN=release,O=YourOrg,OU=Mobile,L=Toronto,ST=Ontario,C=CA" + +# Base64 encode for GitHub Secret +base64 -w 0 project-test.jks > project-test.jks.b64 +base64 -w 0 project-release.jks > project-release.jks.b64 +``` -### Legacy Workflow (`build-release.yaml`) +### Required Secrets/Variables per Environment -- Deprecated, kept for backward compatibility -- Builds beta releases on master push -- Use tag-triggered releases for new stable releases +| Environment | Secrets | Variables | +|--------------|------------------------------------------------------|------------------------| +| `ci:test` | `ANDROID_KEYSTORE_B64`, `ANDROID_KEYSTORE_PASSWORD` | `ANDROID_KEY_ALIAS` | +| `ci:release` | `ANDROID_KEYSTORE_B64`, `ANDROID_KEYSTORE_PASSWORD` | `ANDROID_KEY_ALIAS` | ## Local Development When building locally: 1. **With git tags**: Version derived automatically from latest `v*.*.*` tag -2. **Without git tags**: Falls back to `v0.0.1-beta.0` +2. **Without git tags**: Falls back to `v0.0.1-dev.0` 3. **Override**: Use environment variables to set specific versions ```bash -# Build with custom version -VERSION_MAJOR=1 VERSION_MINOR=2 VERSION_PATCH=3 VERSION_STABLE=true ./gradlew assembleRelease +# Build with custom version (stable release) +VERSION_MAJOR=1 VERSION_MINOR=2 VERSION_PATCH=3 VERSION_STABLE=true CI_BUILD_TYPE=release \ + ./gradlew assembleProdRelease + +# Build test variant +CI_BUILD_TYPE=test ./gradlew assembleUntrustedRelease ``` ## Troubleshooting @@ -143,9 +221,7 @@ You have more than 510 commits since the last release. Create a new release tag. ### "Version components exceed allocated bit width" -Major, minor, or patch version exceeds 127. This is extremely rare and indicates one of: -- **Patch overflow**: The build system automatically rolls patch → minor → major when patch exceeds 127 for beta builds. If this happens, the next stable release should use the rolled-over version. -- **Very high version numbers**: If you've legitimately reached v127.x.x, consider resetting to v0.0.0 with a new package ID. +Major, minor, or patch version exceeds 127. The build system automatically rolls patch → minor → major when patch exceeds 127 for dev builds. ### Version code is lower than expected