Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions src/main/groovy/com/deploygate/gradle/plugins/Config.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,4 @@ class Config {
VERSION_NAME = "unavailable"
}
}

static boolean shouldOpenAppDetailAfterUpload() {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced with openBrowserAfterUpload property in UploadArtifactTask

return System.getenv(DeployGatePlugin.ENV_NAME_OPEN_APP_DETAIL_AFTER_UPLOAD)
}
}
101 changes: 89 additions & 12 deletions src/main/groovy/com/deploygate/gradle/plugins/DeployGatePlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import static com.deploygate.gradle.plugins.internal.agp.AndroidGradlePlugin.and
import static com.deploygate.gradle.plugins.internal.agp.AndroidGradlePlugin.androidBundleTaskName
import static com.deploygate.gradle.plugins.internal.gradle.ProviderFactoryUtils.environmentVariable

import com.deploygate.gradle.plugins.artifacts.AabInfo
import com.deploygate.gradle.plugins.artifacts.ApkInfo
import com.deploygate.gradle.plugins.artifacts.DefaultPresetAabInfo
import com.deploygate.gradle.plugins.artifacts.DefaultPresetApkInfo
import com.deploygate.gradle.plugins.dsl.DeployGateExtension
Expand Down Expand Up @@ -55,26 +57,47 @@ class DeployGatePlugin implements Plugin<Project> {

setupExtension(project)

GradleCompat.init(project)

// the presence of the value is same to the existence of the directory.
// Defer file operations to execution time for Configuration Cache compatibility
Provider<String> credentialDirPathProvider = project.providers.systemProperty("user.home").map { home ->
File f = new File(home, '.dg')
(f.directory || f.mkdirs()) ? f.absolutePath : null
new File(home, '.dg').absolutePath
}

// Detect AGP version for HttpClient
def agpVersionProvider = project.providers.provider {
try {
def agpPlugin = project.plugins.findPlugin("com.android.application")
if (agpPlugin) {
return AndroidGradlePlugin.getVersionString(agpPlugin.class.classLoader)
}
} catch (Throwable ignored) {
}
return "unknown"
}
Comment thread
tnj marked this conversation as resolved.

def httpClientProvider = project.gradle.sharedServices.registerIfAbsent("httpclient", HttpClient) { spec ->
spec.parameters.endpoint.set(environmentVariable(project.providers, "TEST_SERVER_URL").orElse(Config.getDEPLOYGATE_ROOT()))
spec.parameters.agpVersion.set(agpVersionProvider)
spec.parameters.pluginVersion.set(project.providers.provider { Config.VERSION })
spec.parameters.pluginVersionCode.set(project.providers.provider { Config.VERSION_CODE.toString() })
spec.parameters.pluginVersionName.set(project.providers.provider { Config.VERSION_NAME })
}

def localServerProvider = project.gradle.sharedServices.registerIfAbsent("httpserver", LocalServer) { spec ->
spec.parameters.httpClient.set(httpClientProvider)
spec.parameters.credentialsDirPath.set(credentialDirPathProvider)
}

// Use Provider API for configuration cache compatibility
def extension = project.extensions.getByName(EXTENSION_NAME) as DeployGateExtension
def appOwnerNameProvider = project.providers.provider { extension.appOwnerName }
def apiTokenProvider = project.providers.provider { extension.apiToken }
def endpointProvider = project.providers.provider { extension.endpoint }
def openBrowserProvider = environmentVariable(project.providers, ENV_NAME_OPEN_APP_DETAIL_AFTER_UPLOAD).map { it?.toBoolean() ?: false }
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated

def loginTaskProvider = project.tasks.register(Constants.LOGIN_TASK_NAME, LoginTask) { task ->
task.explicitAppOwnerName.set(project.deploygate.appOwnerName)
task.explicitApiToken.set(project.deploygate.apiToken)
task.explicitAppOwnerName.set(appOwnerNameProvider)
task.explicitApiToken.set(apiTokenProvider)
task.credentialsDirPath.set(credentialDirPathProvider)
task.httpClient.set(httpClientProvider)
task.localServer.set(localServerProvider)
Expand All @@ -98,37 +121,47 @@ class DeployGatePlugin implements Plugin<Project> {
task.group = Constants.TASK_GROUP_NAME
}

project.deploygate.deployments.configureEach { NamedDeployment deployment ->
extension.deployments.configureEach { NamedDeployment deployment ->
project.tasks.named(Constants.SUFFIX_APK_TASK_NAME).configure { task ->
task.dependsOn(Constants.uploadApkTaskName(deployment.name))
}

project.tasks.named(Constants.SUFFIX_AAB_TASK_NAME).configure { task ->
task.dependsOn(Constants.uploadAabTaskName(deployment.name))
}

project.tasks.register(Constants.uploadApkTaskName(deployment.name), UploadApkTask) { task ->
task.description = "Deploy assembled ${deployment.name} APK to DeployGate"
task.group = Constants.TASK_GROUP_NAME

if (!deployment.skipAssemble) {
task.logger.debug("${deployment.name} required assmble but ignored")
task.logger.debug("${deployment.name} required assemble but ignored")
}

task.credentials.set(loginTaskProvider.map { it.credentials })
task.deployment.copyFrom(deployment)
task.apkInfo.set(new DefaultPresetApkInfo(deployment.name))
task.httpClient.set(httpClientProvider)
task.endpoint.set(endpointProvider)
task.openBrowserAfterUpload.set(openBrowserProvider)
task.usesService(httpClientProvider)
task.dependsOn(loginTaskProvider)
}

project.tasks.register(Constants.uploadAabTaskName(deployment.name), UploadAabTask) { task ->
task.description = "Deploy bundled ${deployment.name} AAB to DeployGate"
task.group = Constants.TASK_GROUP_NAME

if (!deployment.skipAssemble) {
task.logger.debug("${deployment.name} required assmble but ignored")
task.logger.debug("${deployment.name} required assemble but ignored")
}

task.credentials.set(loginTaskProvider.map { it.credentials })
task.deployment.copyFrom(deployment)
task.aabInfo.set(new DefaultPresetAabInfo(deployment.name))
task.httpClient.set(httpClientProvider)
task.endpoint.set(endpointProvider)
task.openBrowserAfterUpload.set(openBrowserProvider)
task.usesService(httpClientProvider)
task.dependsOn(loginTaskProvider)
}
Expand All @@ -139,10 +172,13 @@ class DeployGatePlugin implements Plugin<Project> {
def variantProxy = new IApplicationVariantImpl(variant)

namedOrRegister(project, Constants.uploadApkTaskName(variantProxy.name), UploadApkTask).configure { task ->
task.description = "Deploy assembled ${variantProxy.name} APK to DeployGate"
task.credentials.set(loginTaskProvider.map { it.credentials })

task.apkInfo.set(variantProxy.packageApplicationTaskProvider().map {getApkInfo(it, variantProxy.name) })
task.apkInfo.set(createApkInfoProvider(variantProxy, agpVersionProvider))
task.httpClient.set(httpClientProvider)
task.endpoint.set(endpointProvider)
task.openBrowserAfterUpload.set(openBrowserProvider)
task.usesService(httpClientProvider)

if (deployment.skipAssemble.get()) {
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
Expand All @@ -153,10 +189,13 @@ class DeployGatePlugin implements Plugin<Project> {
}

namedOrRegister(project, Constants.uploadAabTaskName(variantProxy.name), UploadAabTask).configure { task ->
task.description = "Deploy bundled ${variantProxy.name} AAB to DeployGate"
task.credentials.set(loginTaskProvider.map { it.credentials })

task.aabInfo.set(variantProxy.packageApplicationTaskProvider().map {getAabInfo(it, variantProxy.name, project.buildDir) })
task.aabInfo.set(createAabInfoProvider(variantProxy, agpVersionProvider, project))
task.httpClient.set(httpClientProvider)
task.endpoint.set(endpointProvider)
task.openBrowserAfterUpload.set(openBrowserProvider)
task.usesService(httpClientProvider)

if (deployment.skipAssemble.get()) {
Expand All @@ -182,4 +221,42 @@ class DeployGatePlugin implements Plugin<Project> {
// TODO we should use ExtensionSyntax as the 1st argument but we need to investigate the expected side effects first.
project.extensions.create(DeployGateExtension, EXTENSION_NAME, DeployGateExtension, deployments)
}

/**
* Creates a provider for APK info that properly chains the package task provider
* with the AGP version provider for configuration cache compatibility.
*
* @param variantProxy The application variant proxy
* @param agpVersionProvider Provider for the AGP version string
* @return Provider for ApkInfo
*/
private static Provider<ApkInfo> createApkInfoProvider(
@NotNull IApplicationVariantImpl variantProxy,
@NotNull Provider<String> agpVersionProvider) {
return variantProxy.packageApplicationTaskProvider().flatMap { packageTask ->
agpVersionProvider.map { agpVersion ->
getApkInfo(packageTask, variantProxy.name, agpVersion)
}
}
}

/**
* Creates a provider for AAB info that properly chains the package task provider
* with the AGP version provider for configuration cache compatibility.
*
* @param variantProxy The application variant proxy
* @param agpVersionProvider Provider for the AGP version string
* @param project The project instance for accessing build directory
* @return Provider for AabInfo
*/
private static Provider<AabInfo> createAabInfoProvider(
@NotNull IApplicationVariantImpl variantProxy,
@NotNull Provider<String> agpVersionProvider,
@NotNull Project project) {
return variantProxy.packageApplicationTaskProvider().flatMap { packageTask ->
agpVersionProvider.map { agpVersion ->
getAabInfo(packageTask, variantProxy.name, project.layout.buildDirectory.get().asFile, agpVersion)
}
}
Comment thread
tnj marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
package com.deploygate.gradle.plugins.artifacts

import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.jetbrains.annotations.NotNull
import org.jetbrains.annotations.Nullable

interface AabInfo {
@Input
@NotNull
String getVariantName()

@InputFile
@Optional
@PathSensitive(PathSensitivity.ABSOLUTE)
@Nullable
File getAabFile()
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
package com.deploygate.gradle.plugins.artifacts

import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.jetbrains.annotations.NotNull
import org.jetbrains.annotations.Nullable

interface ApkInfo {
@Input
@NotNull
String getVariantName()

@InputFile
@Optional
@PathSensitive(PathSensitivity.ABSOLUTE)
@Nullable
File getApkFile()

@Input
boolean isSigningReady()

@Input
boolean isUniversalApk()
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ class PackageAppTaskCompat {
}

@NotNull
static ApkInfo getApkInfo(@NotNull /* PackageApplication */ packageAppTask, @NotNull String variantName) {
static ApkInfo getApkInfo(@NotNull /* PackageApplication */ packageAppTask, @NotNull String variantName, @NotNull String agpVersion) {
// outputScope is retrieved by the reflection
Collection<String> apkNames = getApkNames(packageAppTask)
Collection<String> apkNames = getApkNames(packageAppTask, agpVersion)
File outputDir = getOutputDirectory(packageAppTask)
boolean isUniversal = apkNames.size() == 1
boolean isSigningReady = hasSigningConfig(packageAppTask)
boolean isSigningReady = hasSigningConfig(packageAppTask, agpVersion)

return new DirectApkInfo(
variantName,
Expand All @@ -25,12 +25,12 @@ class PackageAppTaskCompat {
}

@NotNull
static AabInfo getAabInfo(@NotNull /* PackageApplication */ packageAppTask, @NotNull String variantName, @NotNull File buildDir) {
static AabInfo getAabInfo(@NotNull /* PackageApplication */ packageAppTask, @NotNull String variantName, @NotNull File buildDir, @NotNull String agpVersion) {
final String aabName

// TODO Use Artifact API
// outputScope is retrieved by the reflection
Collection<String> apkNames = getApkNames(packageAppTask)
Collection<String> apkNames = getApkNames(packageAppTask, agpVersion)
aabName = ((String) apkNames[0]).replaceFirst("\\.apk\$", ".aab")

def outputDir = new File(buildDir, "outputs/bundle/${variantName}")
Expand All @@ -42,8 +42,8 @@ class PackageAppTaskCompat {
}

@PackageScope
static boolean hasSigningConfig(packageAppTask) {
if (AndroidGradlePlugin.isInternalSigningConfigData()) {
static boolean hasSigningConfig(packageAppTask, String agpVersion) {
if (AndroidGradlePlugin.isInternalSigningConfigData(agpVersion)) {
return packageAppTask.signingConfigVersions.any { it.exists() }
} else {
return packageAppTask.signingConfigData.resolve() != null
Expand All @@ -54,8 +54,8 @@ class PackageAppTaskCompat {
return packageAppTask.outputDirectory.getAsFile().get()
}

static Collection<String> getApkNames(packageAppTask) {
if (AndroidGradlePlugin.hasOutputsHandlerApiOnPackageApplication()) {
static Collection<String> getApkNames(packageAppTask, String agpVersion) {
if (AndroidGradlePlugin.hasOutputsHandlerApiOnPackageApplication(agpVersion)) {
return packageAppTask.outputsHandler.get().getOutputs { true }.collect { it.outputFileName }
} else {
return packageAppTask.variantOutputs.get().collect { it.outputFileName.get() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class DeployGateExtension implements ExtensionSyntax {

String appOwnerName

String endpoint = 'https://deploygate.com'

@NotNull
private final NamedDomainObjectContainer<NamedDeployment> deployments

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,17 @@ import org.jetbrains.annotations.NotNull
import org.slf4j.Logger

class AndroidGradlePlugin {
private static VersionString AGP_VERSION

static void ifPresent(@NotNull Project project, @NotNull Action<?> onFound) {
try {
def agpPlugin = project.plugins.findPlugin("com.android.application")

if (agpPlugin) {
AGP_VERSION = VersionString.tryParse(getVersionString(agpPlugin.class.classLoader))
checkModelLevel(agpPlugin.class.classLoader, project.logger)
onFound.execute("dummy")
} else {
project.plugins.matching { it.class.name == "com.android.build.gradle.AppPlugin" }.whenPluginAdded { Plugin plugin ->
project.logger.warn("com.android.application should be applied before DeployGate plugin")
AGP_VERSION = VersionString.tryParse(getVersionString(plugin.class.classLoader))
checkModelLevel(agpPlugin.class.classLoader, project.logger)
checkModelLevel(plugin.class.classLoader, project.logger)
onFound.execute("dummy")
}
}
Expand All @@ -31,27 +27,41 @@ class AndroidGradlePlugin {
}
}

static VersionString getVersion() {
if (!AGP_VERSION) {
AGP_VERSION = VersionString.tryParse(getVersionString())
}

return AGP_VERSION
/**
* Parses the AGP version string into a VersionString object.
* This method no longer caches the version in a static field to support configuration cache.
*
* @param versionString The AGP version string to parse (e.g., "8.1.0")
* @return Parsed VersionString object, or null if parsing fails
* @since 3.0.0
*/
static VersionString getVersion(@NotNull String versionString) {
return VersionString.tryParse(versionString)
}

/**
* Checks if AGP uses internal signing config data structure.
* This change was introduced in AGP 8.3.0.
*
* @param versionString The AGP version string
* @return true if AGP version is 8.3.0 or higher
* @since AGP 8.3.0 https://cs.android.com/android-studio/platform/tools/base/+/ff361912406f0eafc42b6ff2a293ee8a17ff77ee:build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/PackageAndroidArtifact.kt;dlc=c2e97e2ca61a5575ccfb48f9528a11c38d651841
*/
static boolean isInternalSigningConfigData() {
def version = getVersion()
static boolean isInternalSigningConfigData(@NotNull String versionString) {
def version = getVersion(versionString)
return version.major >= 8 && version.minor >= 3
}

/**
* Checks if AGP has the OutputsHandler API on PackageApplication task.
* This API was introduced in AGP 8.1.0.
*
* @param versionString The AGP version string
* @return true if AGP version is 8.1.0 or higher
* @since AGP 8.1.0 https://android.googlesource.com/platform/tools/base/+/da5cbdf59f91f7480a5d9615a20f766d19c6034a%5E%21/#F32
*/
static boolean hasOutputsHandlerApiOnPackageApplication() {
def version = getVersion()
static boolean hasOutputsHandlerApiOnPackageApplication(@NotNull String versionString) {
def version = getVersion(versionString)
return version.major >= 8 && version.minor >= 1
}

Expand All @@ -71,7 +81,7 @@ class AndroidGradlePlugin {
* @param classLoader might be based on AGP's class loader
* @return
*/
private static String getVersionString(ClassLoader classLoader) {
static String getVersionString(ClassLoader classLoader) {
try {
return classLoader.loadClass("com.android.Version").getField("ANDROID_GRADLE_PLUGIN_VERSION").get(null)
} catch (Throwable ignored) {
Expand Down
Loading
Loading