All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.bugsnag.android.gradle.BugsnagReleasesTask.kt Maven / Gradle / Ivy

There is a newer version: 8.1.0
Show newest version
package com.bugsnag.android.gradle

import com.bugsnag.android.gradle.internal.BugsnagHttpClientHelper
import com.bugsnag.android.gradle.internal.UploadRequestClient
import com.bugsnag.android.gradle.internal.mapProperty
import com.bugsnag.android.gradle.internal.property
import com.bugsnag.android.gradle.internal.register
import com.bugsnag.android.gradle.internal.runRequestWithRetries
import com.bugsnag.android.gradle.internal.systemPropertyCompat
import com.squareup.moshi.JsonClass
import okhttp3.OkHttpClient
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.logging.LogLevel
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
import org.gradle.process.ExecOperations
import org.gradle.process.ExecResult
import org.gradle.process.ExecSpec
import org.gradle.process.internal.ExecException
import org.gradle.util.VersionNumber
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
import retrofit2.create
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Url
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.nio.charset.Charset
import javax.inject.Inject

open class BugsnagReleasesTask @Inject constructor(
    objects: ObjectFactory,
    private val providerFactory: ProviderFactory,
    private val execOperations: ExecOperations
) : DefaultTask(), AndroidManifestInfoReceiver {

    init {
        group = BugsnagPlugin.GROUP_NAME
        description = "Assembles information about the build that will be sent to the releases API"
    }

    @get:InputFile
    override val manifestInfo: RegularFileProperty = objects.fileProperty()

    @get:Internal
    internal val uploadRequestClient: Property = objects.property()

    @get:Internal
    internal val httpClientHelper: Property = objects.property()

    @get:OutputFile
    val requestOutputFile: RegularFileProperty = objects.fileProperty()

    // should take the JVM + NDK mapping files as inputs because the manifestInfo will
    // not necessarily vary between different builds. it is not guaranteed that
    // either of these properties will be set so they are marked as optional.

    @get:InputFiles
    @get:Optional
    val jvmMappingFileProperty: ConfigurableFileCollection = objects.fileCollection()

    @get:InputFiles
    @get:Optional
    val ndkMappingFileProperty: ConfigurableFileCollection = objects.fileCollection()

    @get:Input
    val retryCount: Property = objects.property()

    @get:Input
    val timeoutMillis: Property = objects.property()

    @get:Input
    val failOnUploadError: Property = objects.property()

    @get:Input
    val releasesEndpoint: Property = objects.property()

    @get:Optional
    @get:Input
    val builderName: Property = objects.property()

    @get:Optional
    @get:Input
    val metadata: MapProperty = objects.mapProperty()

    @get:Optional
    @get:Input
    val sourceControlProvider: Property = objects.property()

    @get:Optional
    @get:Input
    val sourceControlRepository: Property = objects.property()

    @get:Optional
    @get:Input
    val sourceControlRevision: Property = objects.property()

    @get:Input
    @get:Optional
    val osArch: Property = objects.property()

    @get:Input
    @get:Optional
    val osName: Property = objects.property()

    @get:Input
    @get:Optional
    val osVersion: Property = objects.property()

    @get:Input
    @get:Optional
    val javaVersion: Property = objects.property()

    @get:Input
    @get:Optional
    val gradleVersion: Property = objects.property()

    @get:Input
    @get:Optional
    val gitVersion: Property = objects.property()

    fun exec(action: (ExecSpec) -> Unit): ExecResult {
        return execOperations.exec(action)
    }

    @TaskAction
    fun fetchReleaseInfo() {
        val manifestInfo = parseManifestInfo()
        val payload = generateJsonPayload(manifestInfo)

        val response =
            uploadRequestClient.get().makeRequestIfNeeded(manifestInfo, payload.hashCode()) {
                logger.lifecycle("Bugsnag: Uploading to Releases API")
                val response = try {
                    deliverPayload(payload, manifestInfo)
                } catch (exc: Throwable) {
                    when {
                        failOnUploadError.get() -> throw exc
                        else -> "Failure"
                    }
                }
                response
            }
        requestOutputFile.asFile.get().writeText(response)
    }

    private fun deliverPayload(
        payload: ReleasePayload,
        manifestInfo: AndroidManifestInfo
    ): String {
        val okHttpClient = httpClientHelper.get().okHttpClient
        val bugsnagService = createService(okHttpClient)

        return try {
            runRequestWithRetries(retryCount.get()) {
                val response = bugsnagService.upload(
                    releasesEndpoint.get(),
                    apiKey = manifestInfo.apiKey,
                    payload = payload
                )
                readRequestResponse(response.execute())
            }
        } catch (e: IOException) {
            throw IllegalStateException(
                "Request to Bugsnag Releases API failed, aborting build.",
                e
            )
        }
    }

    private fun readRequestResponse(response: Response): String {
        val statusCode = response.code()
        val success = statusCode == 200
        val responseData = when {
            success -> response.body().orEmpty()
            else -> response.errorBody()?.string().orEmpty()
        }
        return when {
            success -> responseData
            else -> {
                logger.error(responseData)
                throw IllegalStateException("Request to Bugsnag Releases API failed, aborting build.")
            }
        }
    }

    private fun generateJsonPayload(manifestInfo: AndroidManifestInfo): ReleasePayload {
        return ReleasePayload(
            buildTool = "gradle-android",
            apiKey = manifestInfo.apiKey,
            appVersion = manifestInfo.versionName,
            appVersionCode = manifestInfo.versionCode,
            metadata = generateMetadataJson(),
            sourceControl = generateVcsJson(),
            builderName = if (builderName.isPresent) {
                builderName.get()
            } else {
                runCmd("whoami")
            }
        )
    }

    private fun generateVcsJson(): Map {
        var vcsUrl = sourceControlRepository.orNull
        var commitHash = sourceControlRevision.orNull
        var vcsProvider = sourceControlProvider.orNull
        if (vcsUrl == null) {
            vcsUrl = runCmd(VCS_COMMAND, "config", "--get", "remote.origin.url")
        }
        if (commitHash == null) {
            commitHash = runCmd(VCS_COMMAND, "rev-parse", "HEAD")
        }
        if (vcsProvider == null) {
            vcsProvider = parseProviderUrl(vcsUrl)
        }
        val sourceControlObj = mutableMapOf()
        sourceControlObj["repository"] = vcsUrl
        sourceControlObj["revision"] = commitHash
        if (isValidVcsProvider(vcsProvider)) {
            sourceControlObj["provider"] = vcsProvider
        }
        return sourceControlObj
    }

    private fun generateMetadataJson(): Map {
        val metadataMap = mutableMapOf()
        collectDefaultMetaData(metadataMap)
        metadataMap.putAll(metadata.orNull.orEmpty())
        return metadataMap.toMap()
    }

    private fun collectDefaultMetaData(map: MutableMap) {
        map["os_arch"] = osArch.orNull
        map["os_name"] = osName.orNull
        map["os_version"] = osVersion.orNull
        map["java_version"] = javaVersion.orNull
        map["gradle_version"] = gradleVersion.orNull
        map["git_version"] = gitVersion.orNull
    }

    /**
     * Runs a command on the shell
     * @param cmd the command (arguments must be separate strings)
     * @return the cmd output
     */
    private fun runCmd(vararg cmd: String): String? {
        return try {
            val baos = ByteArrayOutputStream()
            exec { execSpec ->
                execSpec.commandLine(*cmd)
                execSpec.standardOutput = baos
                logging.captureStandardError(LogLevel.INFO)
            }
            String(baos.toByteArray(), Charset.forName(CHARSET_UTF8)).trim { it <= ' ' }
        } catch (ignored: ExecException) {
            null
        }
    }

    internal fun configureMetadata() {
        val gradleVersionNumber = gradleVersion.orNull?.let {
            gradleVersion.set(it)
            VersionNumber.parse(it)
        }
        gitVersion.set(providerFactory.provider { runCmd(VCS_COMMAND, "--version") })
        osArch.set(providerFactory.systemPropertyCompat(MK_OS_ARCH, gradleVersionNumber))
        osName.set(providerFactory.systemPropertyCompat(MK_OS_NAME, gradleVersionNumber))
        osVersion.set(providerFactory.systemPropertyCompat(MK_OS_VERSION, gradleVersionNumber))
        javaVersion.set(providerFactory.systemPropertyCompat(MK_JAVA_VERSION, gradleVersionNumber))
    }

    companion object {
        private val VALID_VCS_PROVIDERS: Collection = listOf(
            "github-enterprise",
            "bitbucket-server",
            "gitlab-onpremise",
            "bitbucket",
            "github",
            "gitlab"
        )
        private const val MK_OS_ARCH = "os.arch"
        private const val MK_OS_NAME = "os.name"
        private const val MK_OS_VERSION = "os.version"
        private const val MK_JAVA_VERSION = "java.version"
        private const val VCS_COMMAND = "git"
        private const val CHARSET_UTF8 = "UTF-8"

        @JvmStatic
        fun isValidVcsProvider(provider: String?): Boolean {
            return provider == null || VALID_VCS_PROVIDERS.contains(provider)
        }

        @JvmStatic
        fun parseProviderUrl(url: String?): String? {
            if (url != null) {
                for (provider: String in VALID_VCS_PROVIDERS) {
                    if (url.contains((provider))) {
                        return provider
                    }
                }
            }
            return null
        }

        internal fun createService(
            okHttpClient: OkHttpClient
        ): BugsnagReleasesService {
            return Retrofit.Builder()
                .baseUrl("https://upload.bugsnag.com") // Not actually used
                .validateEagerly(true)
                .callFactory(okHttpClient)
                .addConverterFactory(ScalarsConverterFactory.create())
                .addConverterFactory(MoshiConverterFactory.create())
                .build()
                .create()
        }

        /**
         * Registers the appropriate subtype to this [project] with the given [name] and
         * [configurationAction]
         */
        internal fun register(
            project: Project,
            name: String,
            configurationAction: BugsnagReleasesTask.() -> Unit
        ): TaskProvider {
            return project.tasks.register(name, configurationAction)
        }
    }
}

@JsonClass(generateAdapter = true)
internal data class ReleasePayload(
    val buildTool: String,
    val apiKey: String,
    val appVersion: String,
    val appVersionCode: String,
    val metadata: Map,
    val sourceControl: Map,
    val builderName: String?
)

internal interface BugsnagReleasesService {
    @POST
    fun upload(
        @Url endpoint: String,
        @Header("Bugsnag-Api-Key") apiKey: String,
        @Body payload: ReleasePayload
    ): retrofit2.Call
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy