diff --git a/build.gradle b/build.gradle index 8314a6e..0bf2fbe 100644 --- a/build.gradle +++ b/build.gradle @@ -52,5 +52,6 @@ cveHandler { dependencies { implementation 'org.apache.httpcomponents:httpclient:4.5.13' implementation 'org.apache.httpcomponents:httpmime:4.5.13' - implementation 'com.wooga.gradle:gradle-commons:[1,2)' + implementation 'com.wooga.gradle:gradle-commons:[2,3)' + testImplementation 'com.wooga.gradle:gradle-commons-test:[2,3)' } diff --git a/src/integrationTest/groovy/wooga/gradle/appcenter/AppCenterPluginIntegrationSpec.groovy b/src/integrationTest/groovy/wooga/gradle/appcenter/AppCenterPluginIntegrationSpec.groovy index 7422c89..5522b45 100644 --- a/src/integrationTest/groovy/wooga/gradle/appcenter/AppCenterPluginIntegrationSpec.groovy +++ b/src/integrationTest/groovy/wooga/gradle/appcenter/AppCenterPluginIntegrationSpec.groovy @@ -145,7 +145,13 @@ class AppCenterPluginIntegrationSpec extends IntegrationSpec { @Unroll def "can set property :#property with :#method and type '#type'"() { - given: "a task to print appCenter properties" + given: "the test value with replace placeholders" + def value = rawValue + if (value instanceof String) { + value = value.replaceAll("#projectDir#", escapedPath(projectDir.path)) + } + + and: "a task to print appCenter properties" buildFile << """ task(custom) { doLast { @@ -156,18 +162,20 @@ class AppCenterPluginIntegrationSpec extends IntegrationSpec { """ and: "some configured property" - buildFile << "appCenter.${method}(${value})" + buildFile << "appCenter.${method}(${wrapValueBasedOnType(value, type)})" - and: "the test value with replace placeholders" - if (value instanceof String) { - value = value.replaceAll("#projectDir#", escapedPath(projectDir.path)) - } - when: "" + + when: def result = runTasksSuccessfully("custom") + and: + + if(type.contains("File")) { + value = new File(value).path + } then: - result.standardOutput.contains("appCenter.${property}: ${rawValue}") + result.standardOutput.contains("appCenter.${property}: ${value}") where: property | method | rawValue | type @@ -176,6 +184,18 @@ class AppCenterPluginIntegrationSpec extends IntegrationSpec { "apiToken" | "apiToken.set" | "testToken3" | "Provider" "apiToken" | "setApiToken" | "testToken4" | "String" "apiToken" | "setApiToken" | "testToken5" | "Provider" + + "binary" | "binary.set" | "#projectDir#/bin2" | "RegularFile" + "binary" | "binary.set" | "#projectDir#/bin3" | "Provider" + "binary" | "setBinary" | "#projectDir#/bin4" | "File" + "binary" | "setBinary" | "#projectDir#/bin4" | "RegularFile" + "binary" | "setBinary" | "#projectDir#/bin5" | "Provider" + + "releaseNotes" | "releaseNotes.set" | "notes2" | "String" + "releaseNotes" | "releaseNotes.set" | "notes3" | "Provider" + "releaseNotes" | "setReleaseNotes" | "notes4" | "String" + "releaseNotes" | "setReleaseNotes" | "notes5" | "Provider" + "owner" | "owner" | "owner1" | "String" "owner" | "owner.set" | "owner2" | "String" "owner" | "owner.set" | "owner3" | "Provider" @@ -186,7 +206,6 @@ class AppCenterPluginIntegrationSpec extends IntegrationSpec { "applicationIdentifier" | "applicationIdentifier.set" | "applicationIdentifier3" | "Provider" "applicationIdentifier" | "setApplicationIdentifier" | "applicationIdentifier4" | "String" "applicationIdentifier" | "setApplicationIdentifier" | "applicationIdentifier5" | "Provider" - value = wrapValueBasedOnType(rawValue, type) } @@ -257,4 +276,43 @@ class AppCenterPluginIntegrationSpec extends IntegrationSpec { message = (dependsOnTask) ? "depends" : "depends not" } + def "uploads artifact set with appCenter.artifact() method"() { + given: "a project with property set" + def artifactFile = new File(projectDir, "artifacts/artifact.ipa") + buildFile << """ + def artifactTask = tasks.register("$artifactTaskName", Copy) { + from ${wrapValueBasedOnType(getClass().classLoader.getResource("test.ipa").path, "File")} + into ${wrapValueBasedOnType(artifactFile.parentFile.absolutePath, "File")} + rename ".*", ${wrapValueBasedOnType(artifactFile.name, "String")} + } + configurations.create("test") + artifacts.add("test", artifactTask.map { + it.outputs.files.find { + it.absolutePath == ${wrapValueBasedOnType(artifactFile.absolutePath, "File")} + } + }) { it.type = "myartifact" } + def artifactObj = configurations.test.artifacts.matching { it.type == "myartifact" }.first() + appCenter { + owner = "$owner" + apiToken = "$apiToken" + applicationIdentifier = "$applicationIdentifier" + publishEnabled = true + artifact(artifactObj) + } + """ + + and: "a dummy ipa binary to upload" + def testFile = getClass().getClassLoader().getResource("test.ipa").path + buildFile << """ + publishAppCenter.binary = file("$testFile") + """.stripIndent() + + expect: + def result = runTasksSuccessfully("publishAppCenter") + result.wasExecuted(artifactTaskName) + + where: + artifactTaskName = "artifactTask" + } + } diff --git a/src/integrationTest/groovy/wooga/gradle/appcenter/IntegrationSpec.groovy b/src/integrationTest/groovy/wooga/gradle/appcenter/IntegrationSpec.groovy index 5ff673c..73a9dfd 100644 --- a/src/integrationTest/groovy/wooga/gradle/appcenter/IntegrationSpec.groovy +++ b/src/integrationTest/groovy/wooga/gradle/appcenter/IntegrationSpec.groovy @@ -16,6 +16,7 @@ package wooga.gradle.appcenter +import com.wooga.gradle.test.PropertyUtils import groovy.json.StringEscapeUtils import nebula.test.functional.ExecutionResult @@ -101,7 +102,7 @@ class IntegrationSpec extends nebula.test.IntegrationSpec { value = "${rawValue}L" break default: - value = rawValue + value = PropertyUtils.wrapValue(rawValue, type) } value } diff --git a/src/integrationTest/groovy/wooga/gradle/appcenter/tasks/AppCenterUploadTaskIntegrationSpec.groovy b/src/integrationTest/groovy/wooga/gradle/appcenter/tasks/AppCenterUploadTaskIntegrationSpec.groovy index 1ab98b5..54e7e58 100644 --- a/src/integrationTest/groovy/wooga/gradle/appcenter/tasks/AppCenterUploadTaskIntegrationSpec.groovy +++ b/src/integrationTest/groovy/wooga/gradle/appcenter/tasks/AppCenterUploadTaskIntegrationSpec.groovy @@ -30,6 +30,9 @@ import org.apache.http.impl.client.HttpClientBuilder import spock.lang.Unroll import wooga.gradle.appcenter.AppCenterPlugin import wooga.gradle.appcenter.IntegrationSpec +import wooga.gradle.appcenter.error.RetryableException + +import java.util.function.Supplier class AppCenterUploadTaskIntegrationSpec extends IntegrationSpec { static String apiToken = System.env["ATLAS_APP_CENTER_INTEGRATION_API_TOKEN"] @@ -478,25 +481,27 @@ class AppCenterUploadTaskIntegrationSpec extends IntegrationSpec { } Map ensureDistributionGroup(String name) { - HttpClient client = HttpClientBuilder.create().build() - HttpPost request = new HttpPost("https://api.appcenter.ms/v0.1/apps/${owner}/${applicationIdentifierIos}/distribution_groups") + retry(5, 500) { + HttpClient client = HttpClientBuilder.create().build() + HttpPost request = new HttpPost("https://api.appcenter.ms/v0.1/apps/${owner}/${applicationIdentifierIos}/distribution_groups") - request.setHeader("Accept", 'application/json') - request.setHeader("X-API-Token", apiToken) + request.setHeader("Accept", 'application/json') + request.setHeader("X-API-Token", apiToken) - def body = ["name": name] - request.setEntity(new StringEntity(JsonOutput.toJson(body), ContentType.APPLICATION_JSON)) + def body = ["name": name] + request.setEntity(new StringEntity(JsonOutput.toJson(body), ContentType.APPLICATION_JSON)) - HttpResponse response = client.execute(request) + HttpResponse response = client.execute(request) - if (response.statusLine.statusCode != 201 && response.statusLine.statusCode != 409) { - throw new Exception("Failed to create distribution group") - } else if (response.statusLine.statusCode == 409) { - return loadDistributionGroup(name) - } + if (response.statusLine.statusCode != 201 && response.statusLine.statusCode != 409) { + throw new RetryableException("Failed to create distribution group") + } else if (response.statusLine.statusCode == 409) { + return loadDistributionGroup(name) + } - def jsonSlurper = new JsonSlurper() - jsonSlurper.parseText(response.entity.content.text) as Map + def jsonSlurper = new JsonSlurper() + return jsonSlurper.parseText(response.entity.content.text) as Map + } } Map loadDistributionGroup(String name) { @@ -535,4 +540,18 @@ class AppCenterUploadTaskIntegrationSpec extends IntegrationSpec { def jsonSlurper = new JsonSlurper() jsonSlurper.parseText(response.entity.content.text) as Map } + + T retry(int maxRetries, long waitMs, Supplier operation) { + try { + return operation.get() + } catch(RetryableException e) { + maxRetries-- + if(maxRetries > 0) { + Thread.sleep(waitMs) + return (T) retry(maxRetries, waitMs, operation) + } else { + throw new Exception("Operation exceed maximum amount of retries", e.cause) + } + } + } } diff --git a/src/main/groovy/wooga/gradle/appcenter/AppCenterConsts.groovy b/src/main/groovy/wooga/gradle/appcenter/AppCenterConsts.groovy index 501f1d9..c04342a 100644 --- a/src/main/groovy/wooga/gradle/appcenter/AppCenterConsts.groovy +++ b/src/main/groovy/wooga/gradle/appcenter/AppCenterConsts.groovy @@ -25,9 +25,11 @@ class AppCenterConsts { static PropertyLookup owner = new PropertyLookup("APP_CENTER_OWNER", "appCenter.owner", null) static PropertyLookup applicationIdentifier = new PropertyLookup("APP_CENTER_APPLICATION_IDENTIFIER", "appCenter.applicationIdentifier", null) - static PropertyLookup defaultDestinations = new PropertyLookup("APP_CENTER_DEFAULT_DESTINATIONS", "appCenter.defaultDestinations", "Collaborators") + static PropertyLookup defaultDestinations = new PropertyLookup("APP_CENTER_DEFAULT_DESTINATIONS", "appCenter.defaultDestinations", "Collaborators") static PropertyLookup publishEnabled = new PropertyLookup("APP_CENTER_PUBLISH_ENABLED", "appCenter.publishEnabled", true) static PropertyLookup retryTimeout = new PropertyLookup("APP_CENTER_RETRY_TIMEOUT", "appCenter.retryTimeout", 1000 * 60) static PropertyLookup retryCount = new PropertyLookup("APP_CENTER_RETRY_COUNT", "appCenter.retryCount", 30) + static PropertyLookup binary = new PropertyLookup("APP_CENTER_BINARY", "appCenter.binary", null) + static PropertyLookup releaseNotes = new PropertyLookup("APP_CENTER_RELEASE_NOTES", "appCenter.releaseNotes", null) } diff --git a/src/main/groovy/wooga/gradle/appcenter/AppCenterPlugin.groovy b/src/main/groovy/wooga/gradle/appcenter/AppCenterPlugin.groovy index d80d6aa..d6f1662 100644 --- a/src/main/groovy/wooga/gradle/appcenter/AppCenterPlugin.groovy +++ b/src/main/groovy/wooga/gradle/appcenter/AppCenterPlugin.groovy @@ -19,6 +19,8 @@ package wooga.gradle.appcenter import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.artifacts.PublishArtifact +import org.gradle.api.internal.provider.MissingValueException import org.gradle.api.publish.plugins.PublishingPlugin import wooga.gradle.appcenter.internal.DefaultAppCenterPluginExtension @@ -38,8 +40,8 @@ class AppCenterPlugin implements Plugin { createAndConfigureTasks(project, extension) } - protected static AppCenterPluginExtension createAndConfigureExtension(Project project) { - def extension = project.extensions.create(AppCenterPluginExtension, EXTENSION_NAME, DefaultAppCenterPluginExtension) + protected static DefaultAppCenterPluginExtension createAndConfigureExtension(Project project) { + def extension = project.extensions.create(AppCenterPluginExtension, EXTENSION_NAME, DefaultAppCenterPluginExtension) as DefaultAppCenterPluginExtension extension.defaultDestinations.set(AppCenterConsts.defaultDestinations.getStringValueProvider(project) .map({ @@ -54,10 +56,22 @@ class AppCenterPlugin implements Plugin { extension.retryTimeout.convention(AppCenterConsts.retryTimeout.getValueProvider(project, {Long.parseLong(it)})) extension.retryCount.convention(AppCenterConsts.retryCount.getIntegerValueProvider(project)) - extension + def artifactFile = extension.artifact.map { PublishArtifact it -> + try { + return project.layout.projectDirectory.file(it.file.absolutePath) + //if the backing of artifact.file is a provider it just tries to resolve it, and + // if there is nothing yet in the provider, it throws + } catch (MissingValueException _) { + return null + } + } + extension.binary.convention(artifactFile) + extension.releaseNotes.convention(AppCenterConsts.releaseNotes.getStringValueProvider(project)) + + return extension } - private static void createAndConfigureTasks(Project project, AppCenterPluginExtension extension) { + private static void createAndConfigureTasks(Project project, DefaultAppCenterPluginExtension extension) { def tasks = project.tasks def publishAppCenter = tasks.register(PUBLISH_APP_CENTER_TASK_NAME, AppCenterUploadTask, { t -> @@ -73,6 +87,10 @@ class AppCenterPlugin implements Plugin { t.owner.convention(extension.owner) t.retryCount.convention(extension.retryCount) t.retryTimeout.convention(extension.retryTimeout) + t.binary.convention(extension.binary) + if(extension.artifact.present && !t.binary.present) { + dependsOn(extension.artifact.get().buildDependencies) + } } project.afterEvaluate { if (extension.isPublishEnabled().get()) { diff --git a/src/main/groovy/wooga/gradle/appcenter/AppCenterPluginExtension.groovy b/src/main/groovy/wooga/gradle/appcenter/AppCenterPluginExtension.groovy index 6d1bfb9..cc57498 100644 --- a/src/main/groovy/wooga/gradle/appcenter/AppCenterPluginExtension.groovy +++ b/src/main/groovy/wooga/gradle/appcenter/AppCenterPluginExtension.groovy @@ -17,12 +17,42 @@ package wooga.gradle.appcenter - +import org.gradle.api.artifacts.PublishArtifact +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider trait AppCenterPluginExtension implements AppCenterSpec { + abstract void artifact(Provider artifact); + abstract void artifact(PublishArtifact artifact); + + private final Property releaseNotes = objects.property(String) + + Property getReleaseNotes() { + releaseNotes + } + + void setReleaseNotes(Provider value) { + releaseNotes.set(value) + } + + private final RegularFileProperty binary = objects.fileProperty() + + RegularFileProperty getBinary() { + binary + } + + void setBinary(Provider value) { + binary.set(value) + } + + void setBinary(File value) { + binary.set(value) + } + // TODO: Refactor, deprecate to use `destinations` instead? private final ListProperty> defaultDestinations = objects.listProperty(Map) diff --git a/src/main/groovy/wooga/gradle/appcenter/internal/DefaultAppCenterPluginExtension.groovy b/src/main/groovy/wooga/gradle/appcenter/internal/DefaultAppCenterPluginExtension.groovy index 1c87164..ce8c921 100644 --- a/src/main/groovy/wooga/gradle/appcenter/internal/DefaultAppCenterPluginExtension.groovy +++ b/src/main/groovy/wooga/gradle/appcenter/internal/DefaultAppCenterPluginExtension.groovy @@ -17,10 +17,29 @@ package wooga.gradle.appcenter.internal import org.gradle.api.Project +import org.gradle.api.artifacts.PublishArtifact +import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider import wooga.gradle.appcenter.AppCenterPluginExtension class DefaultAppCenterPluginExtension implements AppCenterPluginExtension { - DefaultAppCenterPluginExtension() { + private final Project project + + final Property artifact = objects.property(PublishArtifact) + + DefaultAppCenterPluginExtension(Project project) { + this.project = project + } + + + @Override + void artifact(Provider artifact) { + this.artifact.set(artifact) + } + + @Override + void artifact(PublishArtifact artifact) { + this.artifact.set(project.provider{ artifact }) } } diff --git a/src/test/groovy/wooga/gradle/appcenter/AppCenterPluginSpec.groovy b/src/test/groovy/wooga/gradle/appcenter/AppCenterPluginSpec.groovy index 9cc4be1..54ea99a 100644 --- a/src/test/groovy/wooga/gradle/appcenter/AppCenterPluginSpec.groovy +++ b/src/test/groovy/wooga/gradle/appcenter/AppCenterPluginSpec.groovy @@ -18,7 +18,9 @@ package wooga.gradle.appcenter import nebula.test.ProjectSpec import org.gradle.api.DefaultTask +import org.gradle.api.Task import org.gradle.api.publish.plugins.PublishingPlugin +import org.gradle.api.tasks.Copy import org.gradle.api.tasks.TaskProvider import spock.lang.Unroll import wooga.gradle.appcenter.tasks.AppCenterUploadTask @@ -82,4 +84,69 @@ class AppCenterPluginSpec extends ProjectSpec { taskName | _ "publishAppCenter" | _ } + + def "publishAppCenter task depends on artifact task if artifact is present and no binary was set"() { + given: "no plugin has been applied yet" + assert !project.plugins.hasPlugin(PLUGIN_NAME) + + and: "creates artifact generator task" + def artifactFile = new File(projectDir, "artifactDir/artifact") + def artifactTask = project.tasks.register("testArtifactTask", Copy) { + from new File(projectDir, "artifact") + into artifactFile.parentFile + rename ".*", artifactFile.name + } + + and: "stores artifact" + def artifact = project.with { + configurations.create("archives") + artifacts.add("archives", artifactTask.map { + it.outputs.files.find {it == artifactFile} + }) { + it.type = "myartifact" + } + return configurations.archives.artifacts.matching { it.type == "myartifact" }.first() + } + + when: "apply plugin" + assert project.plugins.apply(PLUGIN_NAME) + and: "add artifact to extension" + def appCenter = project.extensions.findByType(AppCenterPluginExtension) + appCenter.artifact(artifact) + and: "evaluated project" + project.evaluate() + then: "appCenterPublish depends on task used to generate artifact" + def appCenterPublish = project.tasks.named(AppCenterPlugin.PUBLISH_APP_CENTER_TASK_NAME, AppCenterUploadTask).get() + def dependencies = appCenterPublish.taskDependencies.getDependencies(appCenterPublish) + dependencies.contains(artifactTask.get()) + } + + def "publishAppCenter task depends on task used to generate appCenter binary"() { + given: "no plugin has been applied yet" + assert !project.plugins.hasPlugin(PLUGIN_NAME) + and: "a binary generator task" + def binary = new File(projectDir, "artifactDir/artifact") + def binaryTask = project.tasks.register("testBinaryTask", Copy) { + from new File(projectDir, "artifact") + into binary.parentFile + rename ".*", binary.name + } + def binaryProvider = project.layout.file(binaryTask.map { + it.outputs.files.find {it == binary} as File + }) + + when: "plugin is applied" + assert project.plugins.apply(PLUGIN_NAME) + and: "extension binary property is set" + def appCenter = project.extensions.findByType(AppCenterPluginExtension) + appCenter.binary = binaryProvider + and: "evaluated project" + project.evaluate() + + then: "appCenterPublish depends on task used to generate artifact" + def appCenterPublish = project.tasks.named(AppCenterPlugin.PUBLISH_APP_CENTER_TASK_NAME, AppCenterUploadTask).get() + def dependencies = appCenterPublish.taskDependencies.getDependencies(appCenterPublish) + dependencies.contains(binaryTask.get()) + + } }