Skip to content

Commit

Permalink
Add expirimental support for app center publish (#6)
Browse files Browse the repository at this point in the history
Description
===========

> On November 16, 2019, HockeyApp will transition fully to App Center.

This patch adds a new task type `AppCenterUploadTask` which works
similar to the old `HockeyUploadTask`. The new task allows to upload a
binary (`ipa`,`apk`) to a appcenter app.

The configuration looks as
follows:

```groovy
publishAppCenter {
  owner = ""
  apiToken = ""
  applicationIdentifier = ""

  binary = File
  destination "", ""
  buildInfo {
     branchName = ""
     commitHash = ""
     commitMessage = ""
  }
}
```

> the API is not yet stable!

Properties
----------

| name                  | type           | description | required |
| --------------------- | -------------- | ----------- | -------- |
| owner                 | String         | app center user/owner | yes |
| applicationIdentifier | String         | app center application name | yes |
| apiToken              | String         | app center api token with write access to specified `owner/applicationIdentifier` | yes |
| binary                | String or File | file path to binary for upload
| destination           | -              | this is a method which either accepts a list of strings or variadic argument list of strings | no
| buildInfo             | -              | this is a configuration method which can be called with a closure to configure optional build informations | no

Changes
=======

![ADD] ![NEW] task type `AppCenterUploadTask`
  • Loading branch information
Larusso authored Aug 28, 2019
1 parent a055a74 commit 307f522
Show file tree
Hide file tree
Showing 5 changed files with 639 additions and 7 deletions.
11 changes: 9 additions & 2 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@

withCredentials([string(credentialsId: 'atlas_hockey_integration_api_token', variable: 'hockeyToken'),
string(credentialsId: 'atlas_hockey_integration_application_identifier', variable: 'hockeyAppId'),
string(credentialsId: 'atlas_hockey_coveralls_token', variable: 'coveralls_token')]) {
string(credentialsId: 'atlas_hockey_integration_appcenter_token', variable: 'appcenterToken'),
string(credentialsId: 'atlas_hockey_integration_appcenter_application_identifier', variable: 'appcenterAppId'),
string(credentialsId: 'atlas_hockey_integration_appcenter_application_owner', variable: 'appcenterOwner'),
string(credentialsId: 'atlas_hockey_coveralls_token', variable: 'coveralls_token')
]) {

def testEnvironment = [
"ATLAS_HOCKEY_INTEGRATION_API_TOKEN=${hockeyToken}",
"ATLAS_HOCKEY_INTEGRATION_APPLICATION_IDENTIFIER=${hockeyAppId}"
"ATLAS_HOCKEY_INTEGRATION_APPLICATION_IDENTIFIER=${hockeyAppId}",
"ATLAS_APP_CENTER_INTEGRATION_API_TOKEN=${appcenterToken}",
"ATLAS_APP_CENTER_OWNER=${appcenterOwner}",
"ATLAS_APP_CENTER_INTEGRATION_APPLICATION_IDENTIFIER=${appcenterAppId}"
]

buildGradlePlugin plaforms: ['osx', 'windows', 'linux'], coverallsToken: coveralls_token, testEnvironment: testEnvironment
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
/*
* Copyright 2019 Wooga GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package wooga.gradle.hockey.tasks

import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import org.apache.http.HttpResponse
import org.apache.http.client.HttpClient
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpPost
import org.apache.http.client.methods.HttpDelete
import org.apache.http.entity.ContentType
import org.apache.http.entity.StringEntity
import org.apache.http.impl.client.HttpClientBuilder
import wooga.gradle.hockey.HockeyPlugin
import wooga.gradle.hockey.IntegrationSpec

class AppCenterUploadTaskIntegrationSpec extends IntegrationSpec {
static String apiToken = System.env["ATLAS_APP_CENTER_INTEGRATION_API_TOKEN"]
static String owner = System.env["ATLAS_APP_CENTER_OWNER"]
static String applicationIdentifier = System.env["ATLAS_APP_CENTER_INTEGRATION_APPLICATION_IDENTIFIER"]

def setup() {
buildFile << """
version = "0.1.0"
${applyPlugin(HockeyPlugin)}
publishAppCenter {
owner = "$owner"
apiToken = "$apiToken"
applicationIdentifier = "$applicationIdentifier"
}
""".stripIndent()
}

def "uploads dummy ipa to AppCenter successfully"() {
given: "a dummy ipa binary to upload"

def testFile = getClass().getClassLoader().getResource("test.ipa").path
buildFile << """
publishAppCenter.binary = "$testFile"
""".stripIndent()

expect:
runTasksSuccessfully("publishAppCenter")
}

def "writes json file with uploaded version meta data"() {
given: "a dummy ipa binary to upload"
def testFile = getClass().getClassLoader().getResource("test.ipa").path
buildFile << """
publishAppCenter.binary = "$testFile"
""".stripIndent()

and: "a future version meta file"
def versionMeta = new File(projectDir,"build/tmp/publishAppCenter/${owner}_${applicationIdentifier}.json")
assert !versionMeta.exists()

when:
runTasksSuccessfully("publishAppCenter")

then:
versionMeta.exists()
}

def "publishes to Collaborators group when no groups are configured"() {
given: "a dummy ipa binary to upload"
def testFile = getClass().getClassLoader().getResource("test.ipa").path
buildFile << """
publishAppCenter.binary = "$testFile"
""".stripIndent()

and: "a future version meta file"
def versionMeta = new File(projectDir,"build/tmp/publishAppCenter/${owner}_${applicationIdentifier}.json")
assert !versionMeta.exists()

and: "no configured distribution groups"

when:
runTasksSuccessfully("publishAppCenter")

then:
def release = getRelease(versionMeta)
def destinations = release["destinations"]
destinations.any {it["name"] == "Collaborators"}
}

def "can publish to custom distribution groups"() {
given: "a new distribution group"
ensureDistributionGroup("Test")
ensureDistributionGroup("Test2")

buildFile << """
publishAppCenter.destination "Test", "Test2"
""".stripIndent()

and: "a dummy ipa binary to upload"
def testFile = getClass().getClassLoader().getResource("test.ipa").path
buildFile << """
publishAppCenter.binary = "$testFile"
""".stripIndent()

and: "a future version meta file"
def versionMeta = new File(projectDir,"build/tmp/publishAppCenter/${owner}_${applicationIdentifier}.json")
assert !versionMeta.exists()

when:
runTasksSuccessfully("publishAppCenter")

then:
def release = getRelease(versionMeta)
def destinations = release["destinations"]
destinations.any {it["name"] == "Test" || it["name"] == "Test2"}
!destinations.any {it["name"] == "Collaborators"}
}

def "fails when distribution group is invalid"() {
given: "a publish task with invalid distribution groups"
buildFile << """
publishAppCenter.destination "some value", "some other group"
""".stripIndent()

and: "a dummy ipa binary to upload"
def testFile = getClass().getClassLoader().getResource("test.ipa").path
buildFile << """
publishAppCenter.binary = "$testFile"
""".stripIndent()

expect:
runTasksWithFailure("publishAppCenter")
}

def "can publish custom build infos"() {
given: "publish task with build infos"
buildFile << """
publishAppCenter.buildInfo {
branchName = "master"
commitHash = "000000000000"
commitMessage = "Fix tests"
}
""".stripIndent()

and: "a dummy ipa binary to upload"
def testFile = getClass().getClassLoader().getResource("test.ipa").path
buildFile << """
publishAppCenter.binary = "$testFile"
""".stripIndent()

and: "a future version meta file"
def versionMeta = new File(projectDir,"build/tmp/publishAppCenter/${owner}_${applicationIdentifier}.json")
assert !versionMeta.exists()

when:
runTasksSuccessfully("publishAppCenter")

then:
versionMeta.exists()
def jsonSlurper = new JsonSlurper()
def releaseMeta = jsonSlurper.parse(versionMeta)

def releaseId = releaseMeta["release_id"]
def release = getRelease(releaseId)

def buildInfo = release["build"]
buildInfo["branch_name"] == "master"
buildInfo["commit_hash"] == "000000000000"
buildInfo["commit_message"] == "Fix tests"
}

void deleteDistributionGroup(String name) {
HttpClient client = HttpClientBuilder.create().build()
HttpDelete request = new HttpDelete("https://api.appcenter.ms/v0.1/apps/${owner}/${applicationIdentifier}/distribution_groups/${URLEncoder.encode(name,"UTF-8")}")

request.setHeader("Accept", 'application/json')
request.setHeader("X-API-Token", apiToken)

HttpResponse response = client.execute(request)

if (response.statusLine.statusCode != 204) {
throw new Exception("Failed to delete distribution group")
}
}

void ensureDistributionGroup(String name) {
HttpClient client = HttpClientBuilder.create().build()
HttpPost request = new HttpPost("https://api.appcenter.ms/v0.1/apps/${owner}/${applicationIdentifier}/distribution_groups")

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))

HttpResponse response = client.execute(request)

if (response.statusLine.statusCode != 201 && response.statusLine.statusCode != 409) {
throw new Exception("Failed to create distribution group")
}
}

Map getRelease(File versionMeta) {
def jsonSlurper = new JsonSlurper()
def releaseMeta = jsonSlurper.parse(versionMeta)

String releaseId = releaseMeta["release_id"]
getRelease(releaseId)
}

Map getRelease(String releaseId) {
HttpClient client = HttpClientBuilder.create().build()
HttpGet request = new HttpGet("https://api.appcenter.ms/v0.1/apps/${owner}/${applicationIdentifier}/releases/${releaseId}")

request.setHeader("Accept", 'application/json')
request.setHeader("X-API-Token", apiToken)

HttpResponse response = client.execute(request)
def jsonSlurper = new JsonSlurper()
jsonSlurper.parseText(response.entity.content.text) as Map
}
}
27 changes: 22 additions & 5 deletions src/main/groovy/wooga/gradle/hockey/HockeyPlugin.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,20 @@

package wooga.gradle.hockey

import org.gradle.api.Action
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.publish.plugins.PublishingPlugin
import wooga.gradle.hockey.tasks.AppCenterUploadTask
import wooga.gradle.hockey.tasks.HockeyUploadTask

class HockeyPlugin implements Plugin<Project> {

static final String TASK_NAME = "publishHockey"
static final String TASK_DESCRIPTION = "Upload binary to HockeyApp."
static final String PUBLISH_HOCKEY_TASK_NAME = "publishHockey"
static final String PUBLISH_HOCKEY_TASK_DESCRIPTION = "Upload binary to HockeyApp."

static final String PUBLISH_APP_CENTER_TASK_NAME = "publishAppCenter"
static final String PUBLISH_APP_CENTER_TASK_DESCRIPTION = "Upload binary to AppCenter."

@Override
void apply(Project project) {
Expand All @@ -33,10 +38,22 @@ class HockeyPlugin implements Plugin<Project> {

def tasks = project.tasks

def publishHockey = tasks.create(name: TASK_NAME, type: HockeyUploadTask, group: PublishingPlugin.PUBLISH_TASK_GROUP)
publishHockey.description = TASK_DESCRIPTION
def publishHockey = tasks.create(name: PUBLISH_HOCKEY_TASK_NAME, type: HockeyUploadTask, group: PublishingPlugin.PUBLISH_TASK_GROUP)
publishHockey.description = PUBLISH_HOCKEY_TASK_DESCRIPTION

def publishAppCenter = tasks.create(name: PUBLISH_APP_CENTER_TASK_NAME, type: AppCenterUploadTask, group: PublishingPlugin.PUBLISH_TASK_GROUP)
publishAppCenter.description = PUBLISH_APP_CENTER_TASK_DESCRIPTION

tasks.withType(AppCenterUploadTask, new Action<AppCenterUploadTask>() {
@Override
void execute(AppCenterUploadTask t) {
def conventionMapping = t.getConventionMapping()
conventionMapping.map("buildVersion", {project.version})
conventionMapping.map("destinations", {[["name": "Collaborators"]]})
}
})

def lifecyclePublishTask = tasks.getByName(PublishingPlugin.PUBLISH_LIFECYCLE_TASK_NAME)
lifecyclePublishTask.dependsOn(publishHockey)
lifecyclePublishTask.dependsOn(publishHockey, publishHockey)
}
}
33 changes: 33 additions & 0 deletions src/main/groovy/wooga/gradle/hockey/api/AppCenterBuildInfo.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2019 Wooga GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package wooga.gradle.hockey.api

import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional

class AppCenterBuildInfo {
@Input
@Optional
String branchName
@Input
@Optional
String commitHash
@Input
@Optional
String commitMessage
}
Loading

0 comments on commit 307f522

Please sign in to comment.