From b191963257a82b7b3431ab71980c5cf189cd8006 Mon Sep 17 00:00:00 2001 From: Vladislav Kortikov Date: Wed, 22 May 2024 15:48:56 +0200 Subject: [PATCH 1/2] Add task to automate ClickUp workflows --- example/app/build.gradle.kts | 8 ++ .../publish/plugin/BuildPublishPlugin.kt | 52 ++++++++ .../plugin/extension/BuildPublishExtension.kt | 3 + .../plugin/extension/config/ClickUpConfig.kt | 38 ++++++ .../task/clickup/ClickUpAutomationTask.kt | 125 ++++++++++++++++++ .../plugin/task/clickup/api/ClickUpApi.kt | 22 +++ .../clickup/entity/AddFieldToTaskRequest.kt | 9 ++ .../task/clickup/service/ClickUpService.kt | 76 +++++++++++ .../task/clickup/work/AddFixVersionWork.kt | 33 +++++ .../task/clickup/work/AddTagToTaskWork.kt | 29 ++++ 10 files changed, 395 insertions(+) create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/config/ClickUpConfig.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/ClickUpAutomationTask.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/api/ClickUpApi.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/entity/AddFieldToTaskRequest.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/service/ClickUpService.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/work/AddFixVersionWork.kt create mode 100644 plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/work/AddTagToTaskWork.kt diff --git a/example/app/build.gradle.kts b/example/app/build.gradle.kts index 1738f8f..a296290 100644 --- a/example/app/build.gradle.kts +++ b/example/app/build.gradle.kts @@ -74,6 +74,14 @@ buildPublish { fixVersionPattern.set("fix_%2\$s_%1\$s") } } + clickUp { + register("default") { + apiTokenFile = File("clickup-token.txt") + fixVersionPattern = "fix_%2\$s_%1\$s" + fixVersionFieldId = "01234567qwerty" + tagName = "test_tag_name" + } + } telegram { register("default") { botId.set("0000") diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/BuildPublishPlugin.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/BuildPublishPlugin.kt index 6fd4e6b..532d39d 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/BuildPublishPlugin.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/BuildPublishPlugin.kt @@ -31,6 +31,7 @@ import ru.kode.android.build.publish.plugin.extension.BuildPublishExtension import ru.kode.android.build.publish.plugin.extension.EXTENSION_NAME import ru.kode.android.build.publish.plugin.extension.config.AppCenterDistributionConfig import ru.kode.android.build.publish.plugin.extension.config.ChangelogConfig +import ru.kode.android.build.publish.plugin.extension.config.ClickUpConfig import ru.kode.android.build.publish.plugin.extension.config.FirebaseAppDistributionConfig import ru.kode.android.build.publish.plugin.extension.config.JiraConfig import ru.kode.android.build.publish.plugin.extension.config.OutputConfig @@ -39,6 +40,7 @@ import ru.kode.android.build.publish.plugin.extension.config.SlackConfig import ru.kode.android.build.publish.plugin.extension.config.TelegramConfig import ru.kode.android.build.publish.plugin.task.appcenter.AppCenterDistributionTask import ru.kode.android.build.publish.plugin.task.changelog.GenerateChangelogTask +import ru.kode.android.build.publish.plugin.task.clickup.ClickUpAutomationTask import ru.kode.android.build.publish.plugin.task.jira.JiraAutomationTask import ru.kode.android.build.publish.plugin.task.play.PlayDistributionTask import ru.kode.android.build.publish.plugin.task.slack.changelog.SendSlackChangelogTask @@ -65,6 +67,7 @@ internal const val APP_CENTER_DISTRIBUTION_UPLOAD_TASK_PREFIX = "appCenterDistri internal const val PLAY_DISTRIBUTION_UPLOAD_TASK_PREFIX = "playUpload" internal const val SLACK_DISTRIBUTION_UPLOAD_TASK_PREFIX = "slackDistributionUpload" internal const val JIRA_AUTOMATION_TASK = "jiraAutomation" +internal const val CLICK_UP_AUTOMATION_TASK = "clickUpAutomation" internal const val DEFAULT_CONTAINER_NAME = "default" internal object AgpVersions { @@ -275,6 +278,20 @@ abstract class BuildPublishPlugin : Plugin { tagBuildProvider, ) } + + val clickUpConfig = + with(buildPublishExtension.clickUp) { + findByName(buildVariant.name) ?: findByName(DEFAULT_CONTAINER_NAME) + } + if (clickUpConfig != null) { + tasks.registerClickUpTasks( + clickUpConfig, + buildVariant, + changelogConfig.issueNumberPattern, + changelogFile, + tagBuildProvider, + ) + } } return OutputProviders( versionName = versionNameProvider, @@ -313,6 +330,41 @@ abstract class BuildPublishPlugin : Plugin { } } + private fun TaskContainer.registerClickUpTasks( + config: ClickUpConfig, + buildVariant: BuildVariant, + issueNumberPattern: Provider, + changelogFileProvider: Provider, + tagBuildProvider: Provider, + ) { + val fixVersionIsPresent = + config.fixVersionPattern.isPresent && config.fixVersionFieldId.isPresent + val hasMissingFixVersionProperties = + config.fixVersionPattern.isPresent || config.fixVersionFieldId.isPresent + + if (!fixVersionIsPresent && hasMissingFixVersionProperties) { + throw GradleException( + "To use the fixVersion logic, the fixVersionPattern or fixVersionFieldId " + + "properties must be specified", + ) + } + + if (fixVersionIsPresent || config.tagName.isPresent) { + register( + "$CLICK_UP_AUTOMATION_TASK${buildVariant.capitalizedName()}", + ClickUpAutomationTask::class.java, + ) { + it.tagBuildFile.set(tagBuildProvider) + it.changelogFile.set(changelogFileProvider) + it.issueNumberPattern.set(issueNumberPattern) + it.apiTokenFile.set(config.apiTokenFile) + it.fixVersionPattern.set(config.fixVersionPattern) + it.fixVersionFieldId.set(config.fixVersionFieldId) + it.taskTag.set(config.tagName) + } + } + } + @Suppress("LongParameterList") // TODO Get parameters inside private fun TaskContainer.registerSlackTasks( outputConfig: OutputConfig, diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/BuildPublishExtension.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/BuildPublishExtension.kt index 4c74087..cb423eb 100644 --- a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/BuildPublishExtension.kt +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/BuildPublishExtension.kt @@ -4,6 +4,7 @@ import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.model.ObjectFactory import ru.kode.android.build.publish.plugin.extension.config.AppCenterDistributionConfig import ru.kode.android.build.publish.plugin.extension.config.ChangelogConfig +import ru.kode.android.build.publish.plugin.extension.config.ClickUpConfig import ru.kode.android.build.publish.plugin.extension.config.FirebaseAppDistributionConfig import ru.kode.android.build.publish.plugin.extension.config.JiraConfig import ru.kode.android.build.publish.plugin.extension.config.OutputConfig @@ -24,6 +25,8 @@ abstract class BuildPublishExtension objectFactory.domainObjectContainer(ChangelogConfig::class.java) val jira: NamedDomainObjectContainer = objectFactory.domainObjectContainer(JiraConfig::class.java) + val clickUp: NamedDomainObjectContainer = + objectFactory.domainObjectContainer(ClickUpConfig::class.java) val telegram: NamedDomainObjectContainer = objectFactory.domainObjectContainer(TelegramConfig::class.java) val slack: NamedDomainObjectContainer = diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/config/ClickUpConfig.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/config/ClickUpConfig.kt new file mode 100644 index 0000000..d6fa323 --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/extension/config/ClickUpConfig.kt @@ -0,0 +1,38 @@ +package ru.kode.android.build.publish.plugin.extension.config + +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Optional + +interface ClickUpConfig { + val name: String + + /** + * The path to the file containing the API token for the ClickUp + */ + @get:InputFile + val apiTokenFile: RegularFileProperty + + /** + * Pattern to be used to format version to the ClickUp tasks + */ + @get:Input + @get:Optional + val fixVersionPattern: Property + + /** + * The id of the custom field to be used for the fix version in the ClickUp tasks + */ + @get:Input + @get:Optional + val fixVersionFieldId: Property + + /** + * The tag name to be used for the ClickUp tasks + */ + @get:Input + @get:Optional + val tagName: Property +} diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/ClickUpAutomationTask.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/ClickUpAutomationTask.kt new file mode 100644 index 0000000..1226266 --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/ClickUpAutomationTask.kt @@ -0,0 +1,125 @@ +package ru.kode.android.build.publish.plugin.task.clickup + +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.plugins.BasePlugin +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.options.Option +import org.gradle.workers.WorkQueue +import org.gradle.workers.WorkerExecutor +import ru.kode.android.build.publish.plugin.enity.Tag +import ru.kode.android.build.publish.plugin.enity.mapper.fromJson +import ru.kode.android.build.publish.plugin.task.clickup.work.AddFixVersionWork +import ru.kode.android.build.publish.plugin.task.clickup.work.AddTagToTaskWork +import javax.inject.Inject + +abstract class ClickUpAutomationTask + @Inject + constructor( + private val workerExecutor: WorkerExecutor, + ) : DefaultTask() { + init { + description = "Task to automate ClickUp statuses" + group = BasePlugin.BUILD_GROUP + } + + @get:InputFile + @get:Option(option = "tagBuildFile", description = "Json contains info about tag build") + abstract val tagBuildFile: RegularFileProperty + + @get:InputFile + @get:Option(option = "changelogFile", description = "File with saved changelog") + abstract val changelogFile: RegularFileProperty + + @get:Input + @get:Option( + option = "issueNumberPattern", + description = "How task number formatted", + ) + abstract val issueNumberPattern: Property + + @get:InputFile + @get:Option( + option = "apiTokenFile", + description = "API token for ClickUp", + ) + abstract val apiTokenFile: RegularFileProperty + + @get:Input + @get:Option( + option = "fixVersionPattern", + description = "How fix version should be formatted", + ) + @get:Optional + abstract val fixVersionPattern: Property + + @get:Input + @get:Option( + option = "fixVersionFieldId", + description = "Field id for fix version in ClickUp", + ) + @get:Optional + abstract val fixVersionFieldId: Property + + @get:Input + @get:Option( + option = "taskTag", + description = "What tag should be used for tasks", + ) + @get:Optional + abstract val taskTag: Property + + @TaskAction + fun upload() { + val currentBuildTag = fromJson(tagBuildFile.asFile.get()) + val changelog = changelogFile.asFile.get().readText() + val issues = + Regex(issueNumberPattern.get()) + .findAll(changelog) + .mapTo(mutableSetOf()) { it.groupValues[0] } + if (issues.isEmpty()) { + logger.info("issues not found in the changelog, nothing will change") + } else { + val workQueue: WorkQueue = workerExecutor.noIsolation() + workQueue.submitUpdateVersionIfPresent(currentBuildTag, issues) + workQueue.submitSetTagIfPresent(issues) + } + } + + private fun WorkQueue.submitUpdateVersionIfPresent( + currentBuildTag: Tag.Build, + issues: Set, + ) { + if (fixVersionPattern.isPresent && fixVersionFieldId.isPresent) { + val version = + fixVersionPattern.get() + .format( + currentBuildTag.buildVersion, + currentBuildTag.buildNumber, + currentBuildTag.buildVariant, + ) + val fieldId = fixVersionFieldId.get() + submit(AddFixVersionWork::class.java) { parameters -> + parameters.apiToken.set(apiTokenFile.asFile.get().readText()) + parameters.issues.set(issues) + parameters.version.set(version) + parameters.fieldId.set(fieldId) + } + } + } + + private fun WorkQueue.submitSetTagIfPresent(issues: Set) { + if (taskTag.isPresent) { + val tagName = taskTag.get() + submit(AddTagToTaskWork::class.java) { parameters -> + parameters.apiToken.set(apiTokenFile.asFile.get().readText()) + parameters.issues.set(issues) + parameters.tagName.set(tagName) + } + } + } + } diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/api/ClickUpApi.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/api/ClickUpApi.kt new file mode 100644 index 0000000..fe53661 --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/api/ClickUpApi.kt @@ -0,0 +1,22 @@ +package ru.kode.android.build.publish.plugin.task.clickup.api + +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.POST +import retrofit2.http.Path +import ru.kode.android.build.publish.plugin.task.clickup.entity.AddFieldToTaskRequest + +interface ClickUpApi { + @POST("v2/task/{task_id}/tag/{tag_name}") + fun addTagToTask( + @Path("task_id") taskId: String, + @Path("tag_name") tagName: String, + ): Call + + @POST("v2/task/{task_id}/field/{field_id}") + fun addFieldToTask( + @Path("task_id") taskId: String, + @Path("field_id") fieldId: String, + @Body request: AddFieldToTaskRequest, + ): Call +} diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/entity/AddFieldToTaskRequest.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/entity/AddFieldToTaskRequest.kt new file mode 100644 index 0000000..9b19604 --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/entity/AddFieldToTaskRequest.kt @@ -0,0 +1,9 @@ +package ru.kode.android.build.publish.plugin.task.clickup.entity + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +@Suppress("ConstructorParameterNaming") // network model +data class AddFieldToTaskRequest( + val value: String, +) diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/service/ClickUpService.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/service/ClickUpService.kt new file mode 100644 index 0000000..f4dd89a --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/service/ClickUpService.kt @@ -0,0 +1,76 @@ +package ru.kode.android.build.publish.plugin.task.clickup.service + +import com.squareup.moshi.Moshi +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.gradle.api.logging.Logger +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import ru.kode.android.build.publish.plugin.task.clickup.api.ClickUpApi +import ru.kode.android.build.publish.plugin.task.clickup.entity.AddFieldToTaskRequest +import ru.kode.android.build.publish.plugin.util.executeOptionalOrThrow +import java.util.concurrent.TimeUnit + +internal class ClickUpService( + logger: Logger, + apiToken: String, +) { + private val client = + OkHttpClient.Builder() + .connectTimeout(HTTP_CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .readTimeout(HTTP_CONNECT_TIMEOUT_SECONDS, TimeUnit.MINUTES) + .writeTimeout(HTTP_CONNECT_TIMEOUT_SECONDS, TimeUnit.MINUTES) + .addInterceptor(AttachTokenInterceptor(apiToken)) + .apply { + val loggingInterceptor = HttpLoggingInterceptor { message -> logger.info(message) } + loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY + addNetworkInterceptor(loggingInterceptor) + } + .build() + + private val moshi = Moshi.Builder().build() + + private inline fun createApi(baseUrl: String): T { + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(client) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + .create(T::class.java) + } + + private val api = createApi("https://api.clickup.com/api/") + + fun addTagToTask( + taskId: String, + tagName: String, + ) { + api.addTagToTask(taskId, tagName).executeOptionalOrThrow() + } + + fun addFieldToTask( + taskId: String, + fieldId: String, + fieldValue: String, + ) { + val request = AddFieldToTaskRequest(value = fieldValue) + api.addFieldToTask(taskId, fieldId, request).executeOptionalOrThrow() + } +} + +private class AttachTokenInterceptor( + private val apiToken: String, +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): okhttp3.Response { + val originalRequest = chain.request() + val newRequest = + originalRequest.newBuilder() + .addHeader(name = "Content-Type", "application/json") + .addHeader(name = "Authorization", apiToken) + .build() + return chain.proceed(newRequest) + } +} + +private const val HTTP_CONNECT_TIMEOUT_SECONDS = 60L diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/work/AddFixVersionWork.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/work/AddFixVersionWork.kt new file mode 100644 index 0000000..ffd7512 --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/work/AddFixVersionWork.kt @@ -0,0 +1,33 @@ +package ru.kode.android.build.publish.plugin.task.clickup.work + +import org.gradle.api.logging.Logging +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import ru.kode.android.build.publish.plugin.task.clickup.service.ClickUpService + +interface AddFixVersionParameters : WorkParameters { + val apiToken: Property + val issues: SetProperty + val version: Property + val fieldId: Property +} + +abstract class AddFixVersionWork : WorkAction { + private val logger = Logging.getLogger(this::class.java) + + override fun execute() { + val service = + ClickUpService( + logger, + parameters.apiToken.get(), + ) + val issues = parameters.issues.get() + val version = parameters.version.get() + val fieldId = parameters.fieldId.get() + issues.forEach { issue -> + service.addFieldToTask(issue, fieldId, version) + } + } +} diff --git a/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/work/AddTagToTaskWork.kt b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/work/AddTagToTaskWork.kt new file mode 100644 index 0000000..939ee24 --- /dev/null +++ b/plugin-build/plugin/src/main/java/ru/kode/android/build/publish/plugin/task/clickup/work/AddTagToTaskWork.kt @@ -0,0 +1,29 @@ +package ru.kode.android.build.publish.plugin.task.clickup.work + +import org.gradle.api.logging.Logging +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import ru.kode.android.build.publish.plugin.task.clickup.service.ClickUpService + +interface AddTagToTaskParameters : WorkParameters { + val apiToken: Property + val tagName: Property + val issues: SetProperty +} + +abstract class AddTagToTaskWork : WorkAction { + private val logger = Logging.getLogger(this::class.java) + + override fun execute() { + val service = + ClickUpService( + logger, + parameters.apiToken.get(), + ) + val issues = parameters.issues.get() + val tagName = parameters.tagName.get() + issues.forEach { issue -> service.addTagToTask(issue, tagName) } + } +} From 48d96cf807dd4d229a504ec3d9143bb0a88c7241 Mon Sep 17 00:00:00 2001 From: Vladislav Kortikov Date: Thu, 23 May 2024 15:52:09 +0200 Subject: [PATCH 2/2] Increase version to 1.3.0-alpha05 --- plugin-build/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin-build/build.gradle.kts b/plugin-build/build.gradle.kts index 6902f9a..3f4a755 100644 --- a/plugin-build/build.gradle.kts +++ b/plugin-build/build.gradle.kts @@ -7,7 +7,7 @@ plugins { allprojects { group = "ru.kode.android" - version = "1.3.0-alpha04" + version = "1.3.0-alpha05" } val dependsOnRecursivelyByName = { task: Task, name: String ->