diff --git a/subprojects/gradle/lint-report/src/integTest/kotlin/com/avito/android/lint/LintSlackAlertIntegrationTest.kt b/subprojects/gradle/lint-report/src/integTest/kotlin/com/avito/android/lint/LintSlackAlertIntegrationTest.kt index 8546d41898..c530ff367e 100644 --- a/subprojects/gradle/lint-report/src/integTest/kotlin/com/avito/android/lint/LintSlackAlertIntegrationTest.kt +++ b/subprojects/gradle/lint-report/src/integTest/kotlin/com/avito/android/lint/LintSlackAlertIntegrationTest.kt @@ -31,9 +31,17 @@ internal class LintSlackAlertIntegrationTest { lintSlackReporter.report( lintReport = reportModels, channel = testChannel, + channelForLintBugs = testChannel, buildUrl = "https://stubbuildurl".toHttpUrl() ) } } -inline fun fileFromJarResources(name: String) = File(C::class.java.classLoader.getResource(name).file) +inline fun fileFromJarResources(name: String): File { + val file = C::class.java.classLoader + ?.getResource(name) + ?.file + ?.let { File(it) } + + return requireNotNull(file) { "$name not found in resources" } +} diff --git a/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/LintReportExtension.kt b/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/LintReportExtension.kt index b51f0d2c69..7354ab20e0 100644 --- a/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/LintReportExtension.kt +++ b/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/LintReportExtension.kt @@ -9,4 +9,5 @@ open class LintReportExtension @Inject constructor(objects: ObjectFactory) { //todo some global slack settings? val slackToken = objects.property() val slackWorkspace = objects.property() + val slackChannelToReportLintBugs = objects.property() } diff --git a/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/model/LintIssue.kt b/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/model/LintIssue.kt index deec0d79dd..038bdb06dd 100644 --- a/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/model/LintIssue.kt +++ b/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/model/LintIssue.kt @@ -9,4 +9,9 @@ class LintIssue( val severity: Severity ) { enum class Severity { UNKNOWN, WARNING, ERROR } + + /** + * "lint failed to parse file" type of errors + */ + val isFatal: Boolean = id == "LintError" } diff --git a/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/slack/LintReportToSlackTaskFactory.kt b/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/slack/LintReportToSlackTaskFactory.kt index 464138de76..47f91c1775 100644 --- a/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/slack/LintReportToSlackTaskFactory.kt +++ b/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/slack/LintReportToSlackTaskFactory.kt @@ -19,10 +19,12 @@ class LintReportToSlackTaskFactory( private val androidLintAccessor: AndroidLintAccessor = AndroidLintAccessor(project) ) { + private val extension: LintReportExtension by lazy { + project.extensions.getByType() + } + @Suppress("UnstableApiUsage") private val slackClientProvider: Provider by lazy { - val extension = project.extensions.getByType() - extension.slackToken.zip(extension.slackWorkspace) { token, workspace -> SlackClient.Impl( token = token, @@ -53,6 +55,7 @@ class LintReportToSlackTaskFactory( } slackReportChannel.set(slackChannel) + slackChannelForLintBugs.set(extension.slackChannelToReportLintBugs) slackClient.set(slackClientProvider) } diff --git a/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/slack/LintSlackReportTask.kt b/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/slack/LintSlackReportTask.kt index 2702b3619c..710d09d958 100644 --- a/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/slack/LintSlackReportTask.kt +++ b/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/slack/LintSlackReportTask.kt @@ -18,6 +18,9 @@ abstract class LintSlackReportTask : DefaultTask() { @get:Input abstract val slackReportChannel: Property + @get:Input + abstract val slackChannelForLintBugs: Property + @get:InputFile abstract val lintXml: RegularFileProperty @@ -40,6 +43,7 @@ abstract class LintSlackReportTask : DefaultTask() { createLintSlackAlert().report( lintReport = models, channel = SlackChannel(slackReportChannel.get()), + channelForLintBugs = SlackChannel(slackChannelForLintBugs.get()), buildUrl = teamcityBuildLinkAccessor.getBuildUrl() ) } diff --git a/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/slack/LintSlackReporter.kt b/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/slack/LintSlackReporter.kt index 3778aa9e9e..1af703eeda 100644 --- a/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/slack/LintSlackReporter.kt +++ b/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/slack/LintSlackReporter.kt @@ -6,12 +6,14 @@ import com.avito.slack.SlackClient import com.avito.slack.model.SlackChannel import com.avito.utils.logging.CILogger import okhttp3.HttpUrl +import java.io.File interface LintSlackReporter { fun report( lintReport: LintReportModel, channel: SlackChannel, + channelForLintBugs: SlackChannel, buildUrl: HttpUrl ) @@ -25,43 +27,112 @@ interface LintSlackReporter { override fun report( lintReport: LintReportModel, channel: SlackChannel, + channelForLintBugs: SlackChannel, buildUrl: HttpUrl ) { - if (shouldSendAlert(lintReport)) { - logger.debug("$tag: Sending lint alert...") - - slackClient.uploadHtml( - channel = channel, - message = buildSlackMessage(lintReport, buildUrl), - file = lintReport.htmlFile - ).fold( - { logger.debug("$tag: Report sent successfully") }, - { error -> logger.critical("$tag: Can't send report ${error.message}") } - ) - } else { - logger.debug("$tag Skip sending lint alert") + when (lintReport) { + is LintReportModel.Valid -> { + val (errors, everythingElse) = lintReport.issues.partition { it.severity == LintIssue.Severity.ERROR } + val (warnings, unknowns) = everythingElse.partition { it.severity == LintIssue.Severity.WARNING } + val (fatalErrors, regularErrors) = errors.partition { it.isFatal } + + var isMessageSent = false + + if (regularErrors.isNotEmpty()) { + sendReport( + channel = channel, + message = buildSlackMessage( + projectPath = lintReport.projectRelativePath, + errors = regularErrors, + warnings = warnings, + buildUrl = buildUrl + ), + htmlReport = lintReport.htmlFile + ) + + isMessageSent = true + } + + if (fatalErrors.isNotEmpty() || unknowns.isNotEmpty()) { + sendReport( + channel = channelForLintBugs, + message = buildSlackMessageAboutLintBugs( + projectPath = lintReport.projectRelativePath, + fatalErrors = fatalErrors, + unknownErrors = unknowns, + buildUrl = buildUrl + ), + htmlReport = lintReport.htmlFile + ) + + isMessageSent = true + } + + if (!isMessageSent) { + logger.debug("$tag Not sending any reports") + } + } + is LintReportModel.Invalid -> { + logger.critical("$tag Not sending report: can't parse", lintReport.error) + } } } - private fun shouldSendAlert(model: LintReportModel): Boolean { - return (model is LintReportModel.Valid) && model.issues.any { it.severity == LintIssue.Severity.ERROR } + private fun sendReport( + channel: SlackChannel, + message: String, + htmlReport: File + ) { + slackClient.uploadHtml( + channel = channel, + message = message, + file = htmlReport + ).fold( + { logger.debug("$tag: Report sent successfully to $channel") }, + { error -> logger.critical("$tag: Can't send report to $channel", error) } + ) } - private fun buildSlackMessage(model: LintReportModel, buildUrl: HttpUrl): String { + private fun buildSlackMessage( + projectPath: String, + errors: List, + warnings: List, + buildUrl: HttpUrl + ): String { return buildString { - appendln("*Critical lint problems detected for project ${model.projectRelativePath}*") + appendln("*Critical lint problems detected for project ${projectPath}*") appendln("Build: <$buildUrl|link>") appendln() - if (model is LintReportModel.Valid) { - val errors = model.issues.filter { it.severity == LintIssue.Severity.ERROR } - val groupedErrors = errors.groupBy { it.summary } - groupedErrors.forEach { (summary, issue) -> - appendln(":red_circle: [${issue.size}x] $summary") - } - appendln(":warning: also ${model.issues.count { it.severity == LintIssue.Severity.WARNING }} warnings") + val groupedErrors = errors.groupBy { it.summary } + groupedErrors.forEach { (summary, issue) -> + appendln(":red_circle: [${issue.size}x] $summary") + } + appendln() + if (warnings.isEmpty()) { + appendln(":green_flag: No warnings!") } else { - logger.critical("LintSlackAlerter: There is a problem with report: ${model.htmlFile.path}") + appendln(":warning: also ${warnings.count()} warnings") + } + } + } + + private fun buildSlackMessageAboutLintBugs( + projectPath: String, + fatalErrors: List, + unknownErrors: List, + buildUrl: HttpUrl + ): String { + return buildString { + appendln("*Lint encountered a problem on project $projectPath*") + appendln("Build: <$buildUrl|link>") + appendln() + + if (fatalErrors.isNotEmpty()) { + appendln(":skull: [${fatalErrors.size}] Lint fatal errors") + } + if (unknownErrors.isNotEmpty()) { + appendln(":alien: [${unknownErrors.size}] Unknown type issues") } } } diff --git a/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/teamcity/TeamcityBuildLinkAccessor.kt b/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/teamcity/TeamcityBuildLinkAccessor.kt index 8d3f1d34fb..0481b89799 100644 --- a/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/teamcity/TeamcityBuildLinkAccessor.kt +++ b/subprojects/gradle/lint-report/src/main/kotlin/com/avito/android/lint/teamcity/TeamcityBuildLinkAccessor.kt @@ -6,7 +6,7 @@ import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import org.gradle.api.Project -interface TeamcityBuildLinkAccessor { +internal interface TeamcityBuildLinkAccessor { fun getBuildUrl(): HttpUrl