diff --git a/.gitignore b/.gitignore index 30189ea..6703893 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ target .idea readyci.iml .scannerwork -.DS_Store +.DS_Store \ No newline at end of file diff --git a/pom.xml b/pom.xml index f7711e6..aa568a5 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.squarepolka readyci - 0.6.1 + 0.6.3 org.springframework.boot @@ -21,6 +21,11 @@ + + net.dongliu + apk-parser + 2.6.5 + org.jetbrains.kotlin @@ -136,7 +141,7 @@ org.jacoco jacoco-maven-plugin - 0.8.1 + 0.8.3 ${basedir}/target/coverage-reports/jacoco-unit.exec ${basedir}/target/coverage-reports/jacoco-unit.exec diff --git a/src/main/java/com/squarepolka/readyci/configuration/AndroidPropConstants.java b/src/main/java/com/squarepolka/readyci/configuration/AndroidPropConstants.java index 8462428..8093abe 100644 --- a/src/main/java/com/squarepolka/readyci/configuration/AndroidPropConstants.java +++ b/src/main/java/com/squarepolka/readyci/configuration/AndroidPropConstants.java @@ -8,7 +8,6 @@ private AndroidPropConstants() { public static final String BUILD_PROP_SCHEME = "scheme"; public static final String BUILD_PROP_DEPLOY_TRACK = "deployTrack"; - public static final String BUILD_PROP_PACKAGE_NAME = "packageName"; public static final String BUILD_PROP_SERVICE_ACCOUNT_FILE = "playStoreAuthCert"; public static final String BUILD_PROP_SERVICE_ACCOUNT_EMAIL = "playStoreEmail"; public static final String BUILD_PROP_JAVA_KEYSTORE_PATH = "javaKeystorePath"; @@ -19,5 +18,6 @@ private AndroidPropConstants() { public static final String BUILD_PROP_HOCKEYAPP_TOKEN = "hockappToken"; public static final String BUILD_PROP_HOCKEYAPP_RELEASE_TAGS = "hockeyappReleaseTags"; public static final String BUILD_PROP_HOCKEYAPP_RELEASE_NOTES = "hockeyappReleaseNotes"; + public static final String BUILD_PROP_FILENAME_FILTERS = "fileNameFilters"; } diff --git a/src/main/java/com/squarepolka/readyci/taskrunner/BuildEnvironment.java b/src/main/java/com/squarepolka/readyci/taskrunner/BuildEnvironment.java index b126db4..148f731 100644 --- a/src/main/java/com/squarepolka/readyci/taskrunner/BuildEnvironment.java +++ b/src/main/java/com/squarepolka/readyci/taskrunner/BuildEnvironment.java @@ -75,6 +75,20 @@ public List getProperties(String propertyName) { return values; } + /** + * Fetch a list of environment properties + * @param propertyName + * @return list of String property values + * @throws PropertyMissingException if the property does not exist + */ + public List getProperties(String propertyName, List defaultValues) { + List values = (List) buildParameters.get(propertyName); + if (null == values || values.isEmpty()) { + return defaultValues; + } + return values; + } + /** * Fetch a single environment property * @param propertyName diff --git a/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidSignApp.java b/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidSignApp.java index 7acd0a6..6b92939 100644 --- a/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidSignApp.java +++ b/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidSignApp.java @@ -29,6 +29,8 @@ public void performTask(BuildEnvironment buildEnvironment) throws TaskFailedExce String scheme = buildEnvironment.getProperty(BUILD_PROP_SCHEME); String keystorePath = buildEnvironment.getProperty(BUILD_PROP_JAVA_KEYSTORE_PATH); String storePass = buildEnvironment.getProperty(BUILD_PROP_STOREPASS); + + // TODO: these are likely to be incorrect String unsignedApkPath = String.format("%s/app/build/outputs/apk/%s/app-%s-unsigned.apk", buildEnvironment.getProjectPath(), scheme.toLowerCase(), scheme.toLowerCase()); String signedApkPath = String.format("%s/app/build/outputs/apk/%s/app-%s.apk", buildEnvironment.getProjectPath(), scheme.toLowerCase(), scheme.toLowerCase()); diff --git a/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidUploadHockeyapp.java b/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidUploadHockeyapp.java deleted file mode 100644 index 75d3e49..0000000 --- a/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidUploadHockeyapp.java +++ /dev/null @@ -1,72 +0,0 @@ - -package com.squarepolka.readyci.tasks.app.android; - - -import com.squarepolka.readyci.configuration.ReadyCIConfiguration; -import com.squarepolka.readyci.taskrunner.BuildEnvironment; -import com.squarepolka.readyci.tasks.Task; -import com.squarepolka.readyci.util.Util; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -import java.io.*; -import java.util.Collection; - -@Component -public class AndroidUploadHockeyapp extends Task { - - - public static final String TASK_UPLOAD_HOCKEYAPP = "android_upload_hockeyapp"; - public static final String BUILD_PROP_HOCKEYAPP_TOKEN = "hockappToken"; - public static final String BUILD_PROP_HOCKEYAPP_RELEASE_TAGS = "hockeyappReleaseTags"; - public static final String BUILD_PROP_HOCKEYAPP_RELEASE_NOTES = "hockeyappReleaseNotes"; - - private static final String COMMAND_GIT = "/usr/bin/git"; - - private static final Logger LOGGER = LoggerFactory.getLogger(ReadyCIConfiguration.class); - - @Override - public String taskIdentifier() { - return TASK_UPLOAD_HOCKEYAPP; - } - - @Override - public void performTask(BuildEnvironment buildEnvironment) { - - String hockappToken = buildEnvironment.getProperty(BUILD_PROP_HOCKEYAPP_TOKEN); - String releaseTags = buildEnvironment.getProperty(BUILD_PROP_HOCKEYAPP_RELEASE_TAGS, ""); - String releaseNotes = buildEnvironment.getProperty(BUILD_PROP_HOCKEYAPP_RELEASE_NOTES, ""); - - if(releaseNotes.isEmpty()) { - InputStream inputStream = executeCommand(new String[]{COMMAND_GIT, "log", "-1", "--pretty=%B"}, buildEnvironment.getProjectPath()); - try { - releaseNotes = Util.readInputStream(inputStream); - } catch (IOException e) { - e.printStackTrace(); - } - } - - // upload all the apk builds that it finds - Collection files = Util.findAllByExtension(new File(buildEnvironment.getProjectPath()), ".apk"); - for (File apk : files) { - if(apk.getAbsolutePath().contains("build")) { - LOGGER.warn("uploading "+ apk.getAbsolutePath()); - // Upload to HockeyApp - executeCommand(new String[]{"/usr/bin/curl", - "https://rink.hockeyapp.net/api/2/apps/upload", - "-H", "X-HockeyAppToken: " + hockappToken, - "-F", "ipa=@" + apk.getAbsolutePath(), - "-F", "notes=" + releaseNotes, - "-F", "tags=" + releaseTags, - "-F", "notes_type=0", // Textual release notes - "-F", "status=2", // Make this version available for download - "-F", "notify=1", // Notify users who can install the app - "-F", "strategy=add", // Add the build if one with the same build number exists - "-F", "mandatory=1" // Download is mandatory - }, buildEnvironment.getProjectPath()); - } - } - } - -} \ No newline at end of file diff --git a/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidUploadHockeyapp.kt b/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidUploadHockeyapp.kt new file mode 100644 index 0000000..e8a9af3 --- /dev/null +++ b/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidUploadHockeyapp.kt @@ -0,0 +1,64 @@ +package com.squarepolka.readyci.tasks.app.android + +import com.squarepolka.readyci.configuration.AndroidPropConstants +import com.squarepolka.readyci.configuration.ReadyCIConfiguration +import com.squarepolka.readyci.taskrunner.BuildEnvironment +import com.squarepolka.readyci.tasks.Task +import com.squarepolka.readyci.tasks.app.android.extensions.isDebuggable +import com.squarepolka.readyci.tasks.app.android.extensions.isSigned +import com.squarepolka.readyci.util.Util +import net.dongliu.apk.parser.ApkFile +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component + +import java.io.File + +@Component +class AndroidUploadHockeyapp : Task() { + + companion object { + private const val TASK_UPLOAD_HOCKEYAPP = "android_upload_hockeyapp" + } + + override fun taskIdentifier(): String = TASK_UPLOAD_HOCKEYAPP + + override fun performTask(buildEnvironment: BuildEnvironment) { + val hockappToken = buildEnvironment.getProperty(AndroidPropConstants.BUILD_PROP_HOCKEYAPP_TOKEN) + val releaseTags = buildEnvironment.getProperty(AndroidPropConstants.BUILD_PROP_HOCKEYAPP_RELEASE_TAGS, "") + val releaseNotes = buildEnvironment.getProperty(AndroidPropConstants.BUILD_PROP_HOCKEYAPP_RELEASE_NOTES, "") + val filenameFilters = buildEnvironment.getProperties(AndroidPropConstants.BUILD_PROP_FILENAME_FILTERS, + listOf("zipaligned", "unsigned")) + + if(hockappToken == null) { + throw RuntimeException("AndroidUploadStore: Missing vital details for play store deployment:\n- hockappToken is required") + } + + val filteredApks = AndroidUtil.findAllApkOutputs(buildEnvironment.projectPath) + .filter { file -> + filenameFilters.none { file.nameWithoutExtension.contains(it) } + } + .map { Pair(it, ApkFile(it)) } + .filter { it.second.isSigned } + + when(filteredApks.size) { + 0 -> throw RuntimeException("Could not find the signed APK") + 1 -> {} // do nothing + else -> throw RuntimeException("There are too many valid APKs that we can upload, please provide a more specific scheme for this pipeline ") + } + + val rawFile = filteredApks.first().first + + executeCommand(arrayOf("/usr/bin/curl", "https://rink.hockeyapp.net/api/2/apps/upload", + "-H", "X-HockeyAppToken: $hockappToken", + "-F", "ipa=@${rawFile.absolutePath}", + "-F", "notes=$releaseNotes", + "-F", "tags=$releaseTags", + "-F", "notes_type=0", // Textual release notes + "-F", "status=2", // Make this version available for download + "-F", "notify=1", // Notify users who can install the app + "-F", "strategy=add", // Add the build if one with the same build number exists + "-F", "mandatory=1" // Download is mandatory + ), buildEnvironment.projectPath) + } +} \ No newline at end of file diff --git a/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidUploadStore.java b/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidUploadStore.java deleted file mode 100644 index c524dbd..0000000 --- a/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidUploadStore.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.squarepolka.readyci.tasks.app.android; - -import com.google.api.client.http.AbstractInputStreamContent; -import com.google.api.client.http.FileContent; -import com.google.api.services.androidpublisher.AndroidPublisher; -import com.google.api.services.androidpublisher.model.*; -import com.squarepolka.readyci.configuration.ReadyCIConfiguration; -import com.squarepolka.readyci.taskrunner.BuildEnvironment; -import com.squarepolka.readyci.taskrunner.TaskFailedException; -import com.squarepolka.readyci.tasks.Task; -import com.squarepolka.readyci.util.android.AndroidPublisherHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import static com.squarepolka.readyci.configuration.AndroidPropConstants.*; - -@Component -public class AndroidUploadStore extends Task { - - public static final String TASK_UPLOAD_STORE = "android_upload_play_store"; - - private static final Logger LOGGER = LoggerFactory.getLogger(ReadyCIConfiguration.class); - - @Override - public String taskIdentifier() { - return TASK_UPLOAD_STORE; - } - - @Override - public void performTask(BuildEnvironment buildEnvironment) throws TaskFailedException { - - try { - - String deployTrack = buildEnvironment.getProperty(BUILD_PROP_DEPLOY_TRACK, ""); - String packageName = buildEnvironment.getProperty(BUILD_PROP_PACKAGE_NAME, ""); - String playStoreEmail = buildEnvironment.getProperty(BUILD_PROP_SERVICE_ACCOUNT_EMAIL, ""); - String playStoreCert = buildEnvironment.getProperty(BUILD_PROP_SERVICE_ACCOUNT_FILE, ""); - - if (deployTrack.isEmpty() || - packageName.isEmpty() || - playStoreEmail.isEmpty() || - playStoreCert.isEmpty()) { - - StringBuilder sb = new StringBuilder(); - - sb.append("AndroidUploadStore: Missing vital details for play store deployment:"); - if(deployTrack.isEmpty()) - sb.append("\n- deployTrack is required"); - if(packageName.isEmpty()) - sb.append("\n- packageName is required"); - if(playStoreEmail.isEmpty()) - sb.append("\n- playStoreEmail is required"); - if(playStoreCert.isEmpty()) - sb.append("\n- playStoreCert is required"); - - throw new Exception(sb.toString()); - } - - String playStoreCertLocation = String.format("%s/%s", buildEnvironment.getCredentialsPath(), playStoreCert); - - String scheme = buildEnvironment.getProperty(BUILD_PROP_SCHEME); - String appBinaryPath = String.format("%s/app/build/outputs/apk/%s/app-%s.apk", - buildEnvironment.getProjectPath(), scheme.toLowerCase(), scheme.toLowerCase()); - - LOGGER.warn("AndroidUploadStore: uploading "+appBinaryPath); - - - // Create the API service. - AndroidPublisher service = AndroidPublisherHelper.init(packageName, playStoreEmail, playStoreCertLocation); - final AndroidPublisher.Edits edits = service.edits(); - - // Create a new edit to make changes to your listing. - AndroidPublisher.Edits.Insert editRequest = edits.insert(packageName, null); - AppEdit edit = editRequest.execute(); - final String editId = edit.getId(); - LOGGER.info("AndroidUploadStore: Created edit with id: {}", editId); - - final AbstractInputStreamContent apkFile = new FileContent(AndroidPublisherHelper.MIME_TYPE_APK, new File(appBinaryPath)); - AndroidPublisher.Edits.Apks.Upload uploadRequest = edits - .apks() - .upload(packageName, editId, apkFile); - - Apk apk = uploadRequest.execute(); - LOGGER.info("AndroidUploadStore: Version code {} has been uploaded", apk.getVersionCode()); - - - // Assign apk to alpha track. - List apkVersionCodes = new ArrayList(); - apkVersionCodes.add(Long.valueOf(apk.getVersionCode())); - AndroidPublisher.Edits.Tracks.Update updateTrackRequest = edits - .tracks() - .update(packageName, - editId, - deployTrack, - new Track().setReleases( - Collections.singletonList( - new TrackRelease() - .setName("Alpha Release") - .setVersionCodes(apkVersionCodes) - .setStatus("completed") - .setReleaseNotes(Collections.singletonList( - new LocalizedText() - .setLanguage("en-AU") - .setText("This is an alpha release")))))); - Track updatedTrack = updateTrackRequest.execute(); - LOGGER.info("AndroidUploadStore: Track {} has been updated.", updatedTrack.getTrack()); - - - // Commit changes for edit. - AndroidPublisher.Edits.Commit commitRequest = edits.commit(packageName, editId); - AppEdit appEdit = commitRequest.execute(); - LOGGER.info("AndroidUploadStore: App edit with id {} has been committed", appEdit.getId()); - - } catch (Exception ex) { - LOGGER.error("AndroidUploadStore: Exception was thrown while uploading apk to alpha track", ex); - } - } - -} diff --git a/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidUploadStore.kt b/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidUploadStore.kt new file mode 100644 index 0000000..ca60238 --- /dev/null +++ b/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidUploadStore.kt @@ -0,0 +1,108 @@ +package com.squarepolka.readyci.tasks.app.android + +import com.google.api.client.http.FileContent +import com.google.api.services.androidpublisher.model.* +import com.squarepolka.readyci.configuration.AndroidPropConstants +import com.squarepolka.readyci.configuration.ReadyCIConfiguration +import com.squarepolka.readyci.taskrunner.BuildEnvironment +import com.squarepolka.readyci.taskrunner.TaskFailedException +import com.squarepolka.readyci.tasks.Task +import com.squarepolka.readyci.util.android.AndroidPublisherHelper +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import net.dongliu.apk.parser.ApkFile +import java.util.ArrayList +import com.squarepolka.readyci.configuration.AndroidPropConstants.* +import com.squarepolka.readyci.tasks.app.android.extensions.isDebuggable +import com.squarepolka.readyci.tasks.app.android.extensions.isSigned + +@Component +class AndroidUploadStore : Task() { + companion object { + private const val TASK_UPLOAD_STORE = "android_upload_play_store" + private val LOGGER = LoggerFactory.getLogger(ReadyCIConfiguration::class.java) + } + + override fun taskIdentifier(): String = TASK_UPLOAD_STORE + + @Throws(TaskFailedException::class) + override fun performTask(buildEnvironment: BuildEnvironment) { + try { + val deployTrack = buildEnvironment.getProperty(BUILD_PROP_DEPLOY_TRACK, "") + val playStoreEmail = buildEnvironment.getProperty(BUILD_PROP_SERVICE_ACCOUNT_EMAIL, "") + val playStoreCert = buildEnvironment.getProperty(BUILD_PROP_SERVICE_ACCOUNT_FILE, "") + val filenameFilters = buildEnvironment.getProperties(AndroidPropConstants.BUILD_PROP_FILENAME_FILTERS, + listOf("zipaligned", "unsigned")) + + if ((deployTrack.isEmpty() || playStoreEmail.isEmpty() || playStoreCert.isEmpty())) { + val sb = StringBuilder() + sb.append("AndroidUploadStore: Missing vital details for play store deployment:") + if (deployTrack.isEmpty()) + sb.append("\n- deployTrack is required") + if (playStoreEmail.isEmpty()) + sb.append("\n- playStoreEmail is required") + if (playStoreCert.isEmpty()) + sb.append("\n- playStoreCert is required") + throw Exception(sb.toString()) + } + + val playStoreCertLocation = String.format("%s/%s", buildEnvironment.credentialsPath, playStoreCert) + + val filteredApks = AndroidUtil.findAllApkOutputs(buildEnvironment.projectPath) + .filter { file -> + filenameFilters.none { file.nameWithoutExtension.contains(it) } + } + .map { Pair(it, ApkFile(it)) } + .filter { it.second.isSigned } + .filter { !it.second.isDebuggable } + + when(filteredApks.size) { + 0 -> throw RuntimeException("Could not find the signed APK") + 1 -> {} // do nothing + else -> throw RuntimeException("There are too many valid APKs that we can upload, please provide a more specific scheme for this pipeline ") + } + + val rawFile = filteredApks.first().first + val apkMetadata = filteredApks.first().second.apkMeta + + // Create the API service. + val service = AndroidPublisherHelper.init(apkMetadata.packageName, playStoreEmail, playStoreCertLocation) + val edits = service.edits() + + // Create a new edit to make changes to your listing. + val editRequest = edits.insert(apkMetadata.packageName, null) + val edit = editRequest.execute() + LOGGER.info("AndroidUploadStore: Created edit with id: {}", edit.id) + + val apkFile = FileContent(AndroidPublisherHelper.MIME_TYPE_APK, rawFile) + val uploadRequest = edits + .apks() + .upload(apkMetadata.packageName, edit.id, apkFile) + val apk = uploadRequest.execute() + LOGGER.info("AndroidUploadStore: Version code {} has been uploaded", apk.versionCode) + + // Assign apk to alpha track. + val apkVersionCodes = ArrayList() + apkVersionCodes.add(apk.versionCode.toLong()) + val updateTrackRequest = edits + .tracks() + .update(apkMetadata.packageName, + edit.id, + deployTrack, + Track().setReleases( + listOf(TrackRelease() + .setName(apkMetadata.versionName) + .setVersionCodes(apkVersionCodes) + .setStatus("completed")))) + val updatedTrack = updateTrackRequest.execute() + LOGGER.info("AndroidUploadStore: Track {} has been updated.", updatedTrack.track) + + // Commit changes for edit. + val commitRequest = edits.commit(apkMetadata.packageName, edit.id) + val appEdit = commitRequest.execute() + LOGGER.info("AndroidUploadStore: App edit with id {} has been committed", appEdit.id) + } catch (ex: Exception) { + LOGGER.error("AndroidUploadStore: Exception was thrown while uploading apk to alpha track", ex) + } + } +} \ No newline at end of file diff --git a/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidUtil.kt b/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidUtil.kt new file mode 100644 index 0000000..463573c --- /dev/null +++ b/src/main/java/com/squarepolka/readyci/tasks/app/android/AndroidUtil.kt @@ -0,0 +1,11 @@ +package com.squarepolka.readyci.tasks.app.android + +import com.squarepolka.readyci.util.Util +import java.io.File + +object AndroidUtil { + fun findAllApkOutputs(dir: String) = Util.findAllByExtension(File(dir), ".apk") + .filter { + it.absolutePath.contains("build/outputs") + } +} \ No newline at end of file diff --git a/src/main/java/com/squarepolka/readyci/tasks/app/android/extensions/ApkFileExtensions.kt b/src/main/java/com/squarepolka/readyci/tasks/app/android/extensions/ApkFileExtensions.kt new file mode 100644 index 0000000..c479c9a --- /dev/null +++ b/src/main/java/com/squarepolka/readyci/tasks/app/android/extensions/ApkFileExtensions.kt @@ -0,0 +1,20 @@ +package com.squarepolka.readyci.tasks.app.android.extensions + +import net.dongliu.apk.parser.ApkFile +import net.dongliu.apk.parser.bean.ApkSignStatus +import org.w3c.dom.Element +import org.xml.sax.InputSource +import java.io.StringReader +import javax.xml.parsers.DocumentBuilderFactory + +val ApkFile.isSigned: Boolean + get() = verifyApk() == ApkSignStatus.signed + +val ApkFile.isDebuggable: Boolean + get() { + val inputSource = InputSource(StringReader(manifestXml)) + val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputSource) + val debuggable = (doc.getElementsByTagName("application").item(0) as Element).getAttribute("android:debuggable") + + return debuggable == null || !debuggable.toBoolean() + } \ No newline at end of file diff --git a/src/main/main.iml b/src/main/main.iml new file mode 100644 index 0000000..284dd72 --- /dev/null +++ b/src/main/main.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/test.iml b/src/test/test.iml new file mode 100644 index 0000000..4b5a533 --- /dev/null +++ b/src/test/test.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file