Skip to content
This repository has been archived by the owner on Dec 5, 2024. It is now read-only.

Commit

Permalink
Add testflight publish (#55)
Browse files Browse the repository at this point in the history
Description
========

This patch adds a new task type `PublishTestFlight` to the plugin. This task uses `fastlane pilot upload`. This solution would bring the most options for the future but I'm still unsure if we should not simple use `altool`. The plugin creates a new task `publishTestFlight` and hooks are the base information like fastlane credentials and path to the `ipa` being published.

The publish task nows a few more parameters that are passed down to `fastlane pilot upload`

| parameter                     | fastlane flag                       | description |
| ----------------------------- | ----------------------------------- | ----------- |
| appIdentifier                 | --app_identifier                    | STRING The bundle identifier of the app to upload or manage testers
| teamId                        | --team_id                           | The ID of your App Store Connect team if you're in multiple teams
| teamName                      | --team_name                         | STRING The name of your App Store Connect team if you're in multiple teams
| itcProvider                   | --itc_provider                      | STRING The provider short name to be used with the iTMSTransporter to identify your team. This value will override the automatically detected provider short name. |
| devPortalTeamId               | --dev_portal_team_id                | The short ID of your team in the developer portal, if you're in multiple teams. Different from your iTC team ID! |
| username                      | --username                          | STRING Your Apple ID Username
| skipSubmission                | --skip_submission                   | [VALUE] Skip the distributing action of pilot and only upload the ipa file |
| skipWaitingForBuildProcessing | --skip_waiting_for_build_processing | [VALUE] Don't wait for the build to process. If set to true, the changelog won't be set, `distribute_external` option won't work and no build will be distributed to testers. (You might want to use this option if you are using this action on CI and have to pay for 'minutes used' on your CI plan) |
| ipa                           | --ipa                               | STRING     Path to the ipa file to upload |
| password                      |                                     | Your Apple ID Password |

Other options might follow in the future but these should be enough for a simple upload and testflight app setup.

The `publishTestFlight` task is wired to the global `publish` task when the extention parameter `publishToTestFlight` is set to true. (default `false`)

Changes
=======

* ![ADD] ![IOS] task `PublishTestflight`
* ![IMPROVE] test setup
* ![IMPROVE] test coverage
  • Loading branch information
Larusso authored May 7, 2020
1 parent 06e59b8 commit babe0da
Show file tree
Hide file tree
Showing 8 changed files with 703 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package wooga.gradle.build.unity.ios.tasks

import wooga.gradle.build.IntegrationSpec

class FastlaneSpec extends IntegrationSpec {
File fastlaneMock
File fastlaneMockPath

def setupFastlaneMock() {
fastlaneMockPath = File.createTempDir("fastlane", "mock")

def path = System.getenv("PATH")
environmentVariables.clear("PATH")
String newPath = "${fastlaneMockPath}${File.pathSeparator}${path}"
environmentVariables.set("PATH", newPath)
assert System.getenv("PATH") == newPath


fastlaneMock = createFile("fastlane", fastlaneMockPath)
fastlaneMock.executable = true
fastlaneMock << """
#!/usr/bin/env bash
echo \$@
env
""".stripIndent()
}

def setup() {
setupFastlaneMock()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,7 @@ import wooga.gradle.build.IntegrationSpec
* account with necessary credentials. We only test the invocation of fastlane and its parameters.
*/
@Requires({ os.macOs })
class ImportProvisioningProfileSpec extends IntegrationSpec {

File fastlaneMock
File fastlaneMockPath

def setupFastlaneMock() {
fastlaneMockPath = File.createTempDir("fastlane", "mock")

def path = System.getenv("PATH")
environmentVariables.clear("PATH")
String newPath = "${fastlaneMockPath}${File.pathSeparator}${path}"
environmentVariables.set("PATH", newPath)
assert System.getenv("PATH") == newPath


fastlaneMock = createFile("fastlane", fastlaneMockPath)
fastlaneMock.executable = true
fastlaneMock << """
#!/usr/bin/env bash
echo \$@
env
""".stripIndent()
}
class ImportProvisioningProfileSpec extends FastlaneSpec {

def setup() {
buildFile << """
Expand All @@ -63,8 +41,6 @@ class ImportProvisioningProfileSpec extends IntegrationSpec {
destinationDir = file("build")
}
""".stripIndent()

setupFastlaneMock()
}

@Issue("https://github.com/wooga/atlas-build-unity/issues/38")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package wooga.gradle.build.unity.ios.tasks

import spock.lang.Requires
import spock.lang.Unroll

@Requires({ os.macOs })
class PublishTestFlightSpec extends FastlaneSpec {

def setup() {
def ipaFile = File.createTempFile("mockIpa", ".ipa")

buildFile << """
task publishTestFlight(type: wooga.gradle.build.unity.ios.tasks.PublishTestFlight) {
appIdentifier = "com.test.testapp"
teamId = "fakeTeamId"
ipa = file("${ipaFile.path}")
}
""".stripIndent()
}

@Unroll
def "task :#taskToRun executes fastlane #fastlaneCommand #fastlaneSubCommand"() {
given: "ipa path"
def ipaFile = File.createTempFile("mockIpa", ".ipa")

and: "a configured task"
buildFile << """
${taskToRun} {
ipa = file("${ipaFile.path}")
}
""".stripIndent()

when:
def result = runTasksSuccessfully(taskToRun)

then:
result.standardOutput.readLines().any { it.matches("${fastlaneCommand} ${fastlaneSubCommand}.*? --ipa ${ipaFile.path}") }

where:
taskToRun = "publishTestFlight"
fastlaneCommand = "pilot"
fastlaneSubCommand = "upload"
}

@Unroll
def "task :#taskToRun accepts input #parameter with #method and type #type"() {
given: "task with configured properties"
buildFile << """
${taskToRun} {
${method}(${value})
}
""".stripIndent()

and:
if (parameter == "ipa") {
createFile(rawValue.toString(), projectDir)
}

when:
def result = runTasksSuccessfully(taskToRun)

then:
result.standardOutput.contains(expectedCommandlineSwitch.replace("#{value_path}", new File(projectDir, rawValue.toString()).path))

where:
parameter | rawValue | type | useSetter | expectedCommandlineSwitchRaw
"appIdentifier" | "com.test.app2" | 'String' | true | "--app_identifier #{value}"
"appIdentifier" | "com.test.app3" | 'String' | false | "--app_identifier #{value}"
"appIdentifier" | "com.test.app4" | 'Closure' | true | "--app_identifier #{value}"
"appIdentifier" | "com.test.app5" | 'Closure' | false | "--app_identifier #{value}"
"appIdentifier" | "com.test.app6" | 'Callable' | true | "--app_identifier #{value}"
"appIdentifier" | "com.test.app7" | 'Callable' | false | "--app_identifier #{value}"
"appIdentifier" | "com.test.app8" | 'Object' | true | "--app_identifier #{value}"
"appIdentifier" | "com.test.app9" | 'Object' | false | "--app_identifier #{value}"

"teamId" | "1234561" | 'String' | true | "--team_id #{value}"
"teamId" | "1234562" | 'String' | false | "--team_id #{value}"
"teamId" | "1234563" | 'Closure' | true | "--team_id #{value}"
"teamId" | "1234564" | 'Closure' | false | "--team_id #{value}"
"teamId" | "1234565" | 'Callable' | true | "--team_id #{value}"
"teamId" | "1234566" | 'Callable' | false | "--team_id #{value}"
"teamId" | "1234567" | 'Object' | true | "--team_id #{value}"
"teamId" | "1234568" | 'Object' | false | "--team_id #{value}"

"teamName" | "testName" | 'String' | true | "--team_name #{value}"
"teamName" | "testName" | 'String' | false | "--team_name #{value}"
"teamName" | "testName" | 'Closure' | true | "--team_name #{value}"
"teamName" | "testName" | 'Closure' | false | "--team_name #{value}"
"teamName" | "testName" | 'Callable' | true | "--team_name #{value}"
"teamName" | "testName" | 'Callable' | false | "--team_name #{value}"
"teamName" | "testName" | 'Object' | true | "--team_name #{value}"
"teamName" | "testName" | 'Object' | false | "--team_name #{value}"

"itcProvider" | "testItcProvider" | 'String' | true | "--itc_provider #{value}"
"itcProvider" | "testItcProvider" | 'String' | false | "--itc_provider #{value}"
"itcProvider" | "testItcProvider" | 'Closure' | true | "--itc_provider #{value}"
"itcProvider" | "testItcProvider" | 'Closure' | false | "--itc_provider #{value}"
"itcProvider" | "testItcProvider" | 'Callable' | true | "--itc_provider #{value}"
"itcProvider" | "testItcProvider" | 'Callable' | false | "--itc_provider #{value}"
"itcProvider" | "testItcProvider" | 'Object' | true | "--itc_provider #{value}"
"itcProvider" | "testItcProvider" | 'Object' | false | "--itc_provider #{value}"

"devPortalTeamId" | "1234561" | 'String' | true | "--dev_portal_team_id #{value}"
"devPortalTeamId" | "1234562" | 'String' | false | "--dev_portal_team_id #{value}"
"devPortalTeamId" | "1234563" | 'Closure' | true | "--dev_portal_team_id #{value}"
"devPortalTeamId" | "1234564" | 'Closure' | false | "--dev_portal_team_id #{value}"
"devPortalTeamId" | "1234565" | 'Callable' | true | "--dev_portal_team_id #{value}"
"devPortalTeamId" | "1234566" | 'Callable' | false | "--dev_portal_team_id #{value}"
"devPortalTeamId" | "1234567" | 'Object' | true | "--dev_portal_team_id #{value}"
"devPortalTeamId" | "1234568" | 'Object' | false | "--dev_portal_team_id #{value}"

"username" | "tester1" | 'String' | true | "--username #{value}"
"username" | "tester2" | 'String' | false | "--username #{value}"
"username" | "tester3" | 'Closure' | true | "--username #{value}"
"username" | "tester4" | 'Closure' | false | "--username #{value}"
"username" | "tester5" | 'Callable' | true | "--username #{value}"
"username" | "tester6" | 'Callable' | false | "--username #{value}"
"username" | "tester7" | 'Object' | true | "--username #{value}"
"username" | "tester8" | 'Object' | false | "--username #{value}"

"password" | "pass1" | 'String' | true | "FASTLANE_PASSWORD=#{value}"
"password" | "pass2" | 'String' | false | "FASTLANE_PASSWORD=#{value}"
"password" | "pass3" | 'Closure' | true | "FASTLANE_PASSWORD=#{value}"
"password" | "pass4" | 'Closure' | false | "FASTLANE_PASSWORD=#{value}"
"password" | "pass5" | 'Callable' | true | "FASTLANE_PASSWORD=#{value}"
"password" | "pass6" | 'Callable' | false | "FASTLANE_PASSWORD=#{value}"
"password" | "pass7" | 'Object' | true | "FASTLANE_PASSWORD=#{value}"
"password" | "pass8" | 'Object' | false | "FASTLANE_PASSWORD=#{value}"

"skipSubmission" | true | 'Boolean' | true | "--skip_submission true"
"skipSubmission" | true | 'Boolean' | false | "--skip_submission true"
"skipSubmission" | true | 'Closure' | true | "--skip_submission true"
"skipSubmission" | true | 'Closure' | false | "--skip_submission true"
"skipSubmission" | true | 'Callable' | true | "--skip_submission true"
"skipSubmission" | true | 'Callable' | false | "--skip_submission true"
"skipSubmission" | false | 'Boolean' | true | "--skip_submission false"
"skipSubmission" | false | 'Boolean' | false | "--skip_submission false"
"skipSubmission" | false | 'Closure' | true | "--skip_submission false"
"skipSubmission" | false | 'Closure' | false | "--skip_submission false"
"skipSubmission" | false | 'Callable' | true | "--skip_submission false"
"skipSubmission" | false | 'Callable' | false | "--skip_submission false"

"skipWaitingForBuildProcessing" | true | 'Boolean' | true | "--skip_waiting_for_build_processing true"
"skipWaitingForBuildProcessing" | true | 'Boolean' | false | "--skip_waiting_for_build_processing true"
"skipWaitingForBuildProcessing" | true | 'Closure' | true | "--skip_waiting_for_build_processing true"
"skipWaitingForBuildProcessing" | true | 'Closure' | false | "--skip_waiting_for_build_processing true"
"skipWaitingForBuildProcessing" | true | 'Callable' | true | "--skip_waiting_for_build_processing true"
"skipWaitingForBuildProcessing" | true | 'Callable' | false | "--skip_waiting_for_build_processing true"
"skipWaitingForBuildProcessing" | false | 'Boolean' | true | "--skip_waiting_for_build_processing false"
"skipWaitingForBuildProcessing" | false | 'Boolean' | false | "--skip_waiting_for_build_processing false"
"skipWaitingForBuildProcessing" | false | 'Closure' | true | "--skip_waiting_for_build_processing false"
"skipWaitingForBuildProcessing" | false | 'Closure' | false | "--skip_waiting_for_build_processing false"
"skipWaitingForBuildProcessing" | false | 'Callable' | true | "--skip_waiting_for_build_processing false"
"skipWaitingForBuildProcessing" | false | 'Callable' | false | "--skip_waiting_for_build_processing false"

"ipa" | "build/out1/test.ipa" | 'String' | true | "--ipa #{value_path}"
"ipa" | "build/out2/test.ipa" | 'String' | false | "--ipa #{value_path}"
"ipa" | "build/out3/test.ipa" | 'File' | true | "--ipa #{value_path}"
"ipa" | "build/out4/test.ipa" | 'File' | false | "--ipa #{value_path}"
"ipa" | "build/out5/test.ipa" | 'Closure' | true | "--ipa #{value_path}"
"ipa" | "build/out6/test.ipa" | 'Closure' | false | "--ipa #{value_path}"

taskToRun = "publishTestFlight"
value = wrapValueBasedOnType(rawValue, type)
method = (useSetter) ? "set${parameter.capitalize()}" : parameter
expectedCommandlineSwitch = expectedCommandlineSwitchRaw.replace("#{value}", rawValue.toString())
}
}
35 changes: 33 additions & 2 deletions src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import org.gradle.api.Project
import org.gradle.api.logging.Logger
import org.gradle.api.logging.Logging
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.publish.plugins.PublishingPlugin
import org.gradle.api.tasks.Sync
import org.gradle.util.GUtil
import wooga.gradle.build.unity.ios.internal.DefaultIOSBuildPluginExtension
Expand All @@ -32,6 +33,7 @@ import wooga.gradle.build.unity.ios.tasks.ImportProvisioningProfile
import wooga.gradle.build.unity.ios.tasks.KeychainTask
import wooga.gradle.build.unity.ios.tasks.ListKeychainTask
import wooga.gradle.build.unity.ios.tasks.LockKeychainTask
import wooga.gradle.build.unity.ios.tasks.PublishTestFlight
import wooga.gradle.build.unity.ios.tasks.XCodeArchiveTask
import wooga.gradle.build.unity.ios.tasks.XCodeExportTask

Expand All @@ -50,6 +52,7 @@ class IOSBuildPlugin implements Plugin<Project> {
}

project.pluginManager.apply(BasePlugin.class)
project.pluginManager.apply(PublishingPlugin.class)
def extension = project.getExtensions().create(IOSBuildPluginExtension, EXTENSION_NAME, DefaultIOSBuildPluginExtension.class)

//register some defaults
Expand Down Expand Up @@ -125,14 +128,26 @@ class IOSBuildPlugin implements Plugin<Project> {
}
})

project.tasks.withType(PublishTestFlight.class, new Action<PublishTestFlight>() {
@Override
void execute(PublishTestFlight task) {
def conventionMapping = task.getConventionMapping()
conventionMapping.map("username", { extension.fastlaneCredentials.username })
conventionMapping.map("password", { extension.fastlaneCredentials.password })
conventionMapping.map("devPortalTeamId", { extension.getTeamId() })
conventionMapping.map("appIdentifier", { extension.getAppIdentifier() })
conventionMapping.map("ipa", { extension.getAppIdentifier() })
}
})

def projects = project.fileTree(project.projectDir) { it.include("*.xcodeproj/project.pbxproj") }.files
projects.each { File xcodeProject ->
def base = xcodeProject.parentFile
def taskNameBase = base.name.replace('.xcodeproj', '').toLowerCase().replaceAll(/[-_.]/, '')
if (projects.size() == 1) {
taskNameBase = ""
}
generateBuildTasks(taskNameBase, project, base)
generateBuildTasks(taskNameBase, project, base, extension)
}
}

Expand All @@ -147,7 +162,7 @@ class IOSBuildPlugin implements Plugin<Project> {
return ""
}

void generateBuildTasks(final String baseName, final Project project, File xcodeProject) {
void generateBuildTasks(final String baseName, final Project project, File xcodeProject, IOSBuildPluginExtension extension) {
def tasks = project.tasks
def buildKeychain = tasks.create(maybeBaseName(baseName, "buildKeychain"), KeychainTask) {
it.baseName = maybeBaseName(baseName, "build")
Expand Down Expand Up @@ -196,6 +211,22 @@ class IOSBuildPlugin implements Plugin<Project> {
it.xcarchivePath xcodeArchive
}

def publishTestFlight = tasks.create(maybeBaseName(baseName, "publishTestFlight"), PublishTestFlight) {
it.ipa xcodeExport
it.group = PublishingPlugin.PUBLISH_TASK_GROUP
it.description = "Upload binary to TestFlightApp"
}

project.afterEvaluate(new Action<Project>() {
@Override
void execute(Project _) {
if(extension.publishToTestFlight) {
def lifecyclePublishTask = tasks.getByName(PublishingPlugin.PUBLISH_LIFECYCLE_TASK_NAME)
lifecyclePublishTask.dependsOn(publishTestFlight)
}
}
})

removeKeychain.mustRunAfter([xcodeArchive, xcodeExport])
lockKeychain.mustRunAfter([xcodeArchive, xcodeExport])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,8 @@ interface IOSBuildPluginExtension {
void setAdhoc(Boolean value)
IOSBuildPluginExtension adhoc(Boolean value)

}
Boolean getPublishToTestFlight()
void setPublishToTestFlight(Boolean value)
IOSBuildPluginExtension publishToTestFlight(Boolean value)

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class DefaultIOSBuildPluginExtension implements IOSBuildPluginExtension {
private String configuration
private String provisioningName
private Boolean adhoc = false
private Boolean publishToTestFlight = false

@Override
org.gradle.api.credentials.PasswordCredentials getFastlaneCredentials() {
Expand Down Expand Up @@ -192,6 +193,22 @@ class DefaultIOSBuildPluginExtension implements IOSBuildPluginExtension {
return this
}

@Override
Boolean getPublishToTestFlight() {
return publishToTestFlight
}

@Override
void setPublishToTestFlight(Boolean value) {
publishToTestFlight = value
}

@Override
IOSBuildPluginExtension publishToTestFlight(Boolean value) {
setPublishToTestFlight(value)
return this
}

DefaultIOSBuildPluginExtension() {
fastlaneCredentials = new DefaultPasswordCredentials()
}
Expand Down
Loading

0 comments on commit babe0da

Please sign in to comment.