Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/ApiToken.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Classic scopes:
- `read:jira-work`
- `write:jira-work`
- `read:jira-user`
- `manage:jira-project`

## Cloud ID

Expand Down Expand Up @@ -103,4 +104,9 @@ Scopes (classic): `read:jira-work`

### [POST /issue/{issueIdOrKey}/transitions](https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issues/#api-rest-api-2-issue-issueidorkey-transitions-post)

Scopes (classic): `write:jira-work`
Scopes (classic): `write:jira-work`


### [PUT /version/{id}](https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-project-versions/#api-rest-api-2-version-id-put)

Scopes (classic): `manage:jira-project`
12 changes: 12 additions & 0 deletions src/main/kotlin/io/github/mojira/arisa/apiclient/JiraClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import io.github.mojira.arisa.apiclient.models.Project
import io.github.mojira.arisa.apiclient.models.SearchResults
import io.github.mojira.arisa.apiclient.models.Transitions
import io.github.mojira.arisa.apiclient.models.User
import io.github.mojira.arisa.apiclient.models.Version
import io.github.mojira.arisa.apiclient.models.Visibility
import io.github.mojira.arisa.apiclient.requestModels.AddCommentBody
import io.github.mojira.arisa.apiclient.requestModels.CreateIssueLinkBody
Expand All @@ -24,6 +25,7 @@ import io.github.mojira.arisa.apiclient.requestModels.JiraSearchRequest
import io.github.mojira.arisa.apiclient.requestModels.TransitionIssueBody
import io.github.mojira.arisa.apiclient.requestModels.UpdateCommentBody
import io.github.mojira.arisa.apiclient.requestModels.UpdateCommentQueryParams
import io.github.mojira.arisa.apiclient.requestModels.UpdateVersionBody
import io.github.mojira.arisa.log
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
Expand Down Expand Up @@ -149,6 +151,12 @@ interface JiraApi {
@Path("issueIdOrKey") issueIdOrKey: String,
@Body body: TransitionIssueBody
): Call<Unit>

@PUT("version/{id}")
fun updateVersion(
@Path("id") versionId: String,
@Body body: UpdateVersionBody
): Call<Version>
}

/**
Expand Down Expand Up @@ -335,4 +343,8 @@ class JiraClient(
fun performTransition(issueIdOrKey: String, body: TransitionIssueBody) {
jiraApi.performTransition(issueIdOrKey, body).executeOrThrow()
}

fun updateVersion(versionId: String, body: UpdateVersionBody): Version {
return jiraApi.updateVersion(versionId, body).executeOrThrow()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ data class IssueFields(
val statuscategorychangedate: String? = null,
val subtasks: List<IssueBean>? = null,
val summary: String? = null,
val versions: List<JsonElement> = emptyList(),
val versions: List<Version> = emptyList(),
val timeoriginalestimate: JsonElement? = null,
val timespent: Long? = null,
@Serializable(with = OffsetDateTimeSerializer::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,9 @@ data class IssueUpdateContext(
var transitionName: String? = null,
val otherOperations: MutableList<() -> Either<Throwable, Unit>> =
emptyList<() -> Either<Throwable, Unit>>().toMutableList(),
val preOperations: MutableList<() -> Either<Throwable, Unit>> =
emptyList<() -> Either<Throwable, Unit>>().toMutableList(),
val postOperations: MutableList<() -> Either<Throwable, Unit>> =
emptyList<() -> Either<Throwable, Unit>>().toMutableList(),
var triggeredBy: String? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.github.mojira.arisa.domain.Comment
import io.github.mojira.arisa.domain.CommentOptions
import io.github.mojira.arisa.domain.Project
import io.github.mojira.arisa.domain.User
import io.github.mojira.arisa.domain.Version
import java.io.File
import java.time.Instant

Expand All @@ -30,7 +31,7 @@ data class CloudIssue(
// val platform: String?,
// val dungeonsPlatform: String?,
// val legendsPlatform: String?,
// val affectedVersions: List<Version>,
val affectedVersions: List<Version>,
// val fixVersions: List<Version>,
val attachments: List<Attachment>,
val comments: List<Comment>,
Expand All @@ -50,7 +51,9 @@ data class CloudIssue(
// val updateLegendsPlatform: (String) -> Unit,
// val updateLinked: (Double) -> Unit,
val setPrivate: () -> Unit,
// val addAffectedVersionById: (id: String) -> Unit,
val addAffectedVersionById: (id: String) -> Unit,
val unarchiveVersion: (id: String) -> Unit,
val archiveVersion: (id: String) -> Unit,
// val addAffectedVersion: (version: Version) -> Unit,
// val removeAffectedVersion: (version: Version) -> Unit,
// val createLink: (type: String, key: String, outwards: Boolean) -> Unit,
Expand Down
161 changes: 125 additions & 36 deletions src/main/kotlin/io/github/mojira/arisa/infrastructure/jira/Functions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,42 @@ package io.github.mojira.arisa.infrastructure.jira

import arrow.core.Either
import arrow.syntax.function.partially1
import io.github.mojira.arisa.apiclient.JiraClient
import io.github.mojira.arisa.apiclient.builders.FluentObjectBuilder
import io.github.mojira.arisa.apiclient.builders.string
import io.github.mojira.arisa.apiclient.exceptions.ClientErrorException
import io.github.mojira.arisa.apiclient.exceptions.JiraClientException
import io.github.mojira.arisa.apiclient.models.IssueTransition
import io.github.mojira.arisa.apiclient.models.Visibility
import io.github.mojira.arisa.apiclient.requestModels.EditIssueBody
import io.github.mojira.arisa.apiclient.requestModels.TransitionIssueBody
import io.github.mojira.arisa.apiclient.requestModels.UpdateCommentBody
import io.github.mojira.arisa.apiclient.requestModels.UpdateVersionBody
import io.github.mojira.arisa.domain.IssueUpdateContext
import io.github.mojira.arisa.infrastructure.CommentCache
import io.github.mojira.arisa.jiraClient
import io.github.mojira.arisa.log
import io.github.mojira.arisa.modules.FailedModuleResponse
import io.github.mojira.arisa.modules.ModuleResponse
import io.github.mojira.arisa.modules.tryRunAll
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import io.github.mojira.arisa.apiclient.JiraClient as MojiraClient
import io.github.mojira.arisa.apiclient.models.AttachmentBean as MojiraAttachment
import io.github.mojira.arisa.apiclient.models.Comment as MojiraComment
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import net.rcarz.jiraclient.Field
import io.github.mojira.arisa.apiclient.models.IssueLink as MojiraIssueLink
import net.rcarz.jiraclient.JiraException
import net.rcarz.jiraclient.RestException
import io.github.mojira.arisa.apiclient.exceptions.ClientErrorException
import io.github.mojira.arisa.apiclient.models.Version as MojiraVersion
import org.apache.http.HttpStatus
import java.io.File
import java.io.InputStream
import java.time.Instant
import java.time.temporal.ChronoField
import io.github.mojira.arisa.apiclient.JiraClient
import io.github.mojira.arisa.apiclient.builders.FluentObjectBuilder
import io.github.mojira.arisa.apiclient.builders.string
import io.github.mojira.arisa.apiclient.exceptions.JiraClientException
import io.github.mojira.arisa.apiclient.models.IssueTransition
import io.github.mojira.arisa.apiclient.models.Visibility
import io.github.mojira.arisa.apiclient.requestModels.EditIssueBody
import io.github.mojira.arisa.apiclient.requestModels.TransitionIssueBody
import io.github.mojira.arisa.apiclient.requestModels.UpdateCommentBody
import io.github.mojira.arisa.jiraClient
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import io.github.mojira.arisa.apiclient.JiraClient as MojiraClient
import io.github.mojira.arisa.apiclient.models.AttachmentBean as MojiraAttachment
import io.github.mojira.arisa.apiclient.models.Comment as MojiraComment
import io.github.mojira.arisa.apiclient.models.IssueLink as MojiraIssueLink
import io.github.mojira.arisa.apiclient.models.Version as MojiraVersion

/**
* Get a list of tickets matching a JQL query.
Expand Down Expand Up @@ -87,7 +88,11 @@ fun updateCHK(context: Lazy<IssueUpdateContext>, chkField: String) {
)
}

fun updateConfirmation(context: Lazy<IssueUpdateContext>, confirmationField: String, value: String) {
fun updateConfirmation(
context: Lazy<IssueUpdateContext>,
confirmationField: String,
value: String
) {
context.value.hasUpdates = true
context.value.update.field(confirmationField) {
subField("value", value)
Expand All @@ -101,14 +106,22 @@ fun updatePriority(context: Lazy<IssueUpdateContext>, priorityField: String, val
}
}

fun updateDungeonsPlatform(context: Lazy<IssueUpdateContext>, dungeonsPlatformField: String, value: String) {
fun updateDungeonsPlatform(
context: Lazy<IssueUpdateContext>,
dungeonsPlatformField: String,
value: String
) {
context.value.hasEdits = true
context.value.edit.field(dungeonsPlatformField) {
subField("value", value)
}
}

fun updateLegendsPlatform(context: Lazy<IssueUpdateContext>, legendsPlatformField: String, value: String) {
fun updateLegendsPlatform(
context: Lazy<IssueUpdateContext>,
legendsPlatformField: String,
value: String
) {
context.value.hasEdits = true
context.value.edit.field(legendsPlatformField) {
subField("value", value)
Expand Down Expand Up @@ -154,9 +167,7 @@ fun removeAffectedVersion(context: Lazy<IssueUpdateContext>, version: MojiraVers

fun addAffectedVersionById(context: Lazy<IssueUpdateContext>, id: String) {
context.value.hasEdits = true
context.value.edit.field("versions") {
subField("id", id)
}
context.value.edit.add("versions", buildJsonObject { put("id", id) })
}

fun removeAffectedVersionById(context: Lazy<IssueUpdateContext>, id: String) {
Expand All @@ -175,7 +186,18 @@ fun updateDescription(context: Lazy<IssueUpdateContext>, description: String) {
}

fun applyIssueChanges(context: IssueUpdateContext): Either<FailedModuleResponse, ModuleResponse> {
val functions = context.otherOperations.toMutableList()
val functions = mutableListOf<() -> Either<Throwable, Unit>>()

// Build execution list in reverse order for transitions/updates/edits
// since they're added at index 0

// Step 6: Add postOperations last (will run after everything)
functions.addAll(context.postOperations)

// Step 5: Add otherOperations (will run after edits)
functions.addAll(0, context.otherOperations)

// Step 4: Add edits (will run after updates)
if (context.hasEdits) {
functions.add(
0,
Expand All @@ -184,6 +206,8 @@ fun applyIssueChanges(context: IssueUpdateContext): Either<FailedModuleResponse,
.partially1(context.edit)
)
}

// Step 3: Add updates (will run after transitions)
if (context.hasUpdates) {
functions.add(
0,
Expand All @@ -193,6 +217,8 @@ fun applyIssueChanges(context: IssueUpdateContext): Either<FailedModuleResponse,
.partially1("Update issue")
)
}

// Step 2: Add transitions (will run after preOperations)
if (context.transitionName != null) {
functions.add(
0,
Expand All @@ -202,6 +228,10 @@ fun applyIssueChanges(context: IssueUpdateContext): Either<FailedModuleResponse,
.partially1(context.transitionName!!)
)
}

// Step 1: Add preOperations first (will run FIRST)
functions.addAll(0, context.preOperations)

return tryRunAll(functions, context)
}

Expand Down Expand Up @@ -230,19 +260,25 @@ private fun applyFluentUpdate(issueKey: String, edit: FluentObjectBuilder) = run
}
}

private fun getIssueTransition(issueKey: String, transitionName: String): IssueTransition = runBlocking {
val allTransitions = jiraClient.getTransitions(issueKey)
val transition = allTransitions.transitions?.firstOrNull { it.name == transitionName }
private fun getIssueTransition(issueKey: String, transitionName: String): IssueTransition =
runBlocking {
val allTransitions = jiraClient.getTransitions(issueKey)
val transition = allTransitions.transitions?.firstOrNull { it.name == transitionName }

if (transition == null) {
val availableTransitions = allTransitions.transitions?.joinToString(",\n") { "${it.name} (${it.id})" }
throw JiraClientException("Transition $transitionName not found. Available transitions:\n$availableTransitions")
}
if (transition == null) {
val availableTransitions =
allTransitions.transitions?.joinToString(",\n") { "${it.name} (${it.id})" }
throw JiraClientException("Transition $transitionName not found. Available transitions:\n$availableTransitions")
}

return@runBlocking transition
}
return@runBlocking transition
}

private fun applyFluentTransition(issueKey: String, update: FluentObjectBuilder, transitionName: String) = runBlocking {
private fun applyFluentTransition(
issueKey: String,
update: FluentObjectBuilder,
transitionName: String
) = runBlocking {
Either.catch {
val transition = getIssueTransition(issueKey, transitionName)

Expand Down Expand Up @@ -349,7 +385,8 @@ fun addRestrictedComment(
is Either.Left -> log.error(checkResult.a.message)
is Either.Right -> {
val issueId = context.value.jiraIssue.id
val visibility = Visibility(value = restrictionLevel, type = Visibility.Type.Group.value)
val visibility =
Visibility(value = restrictionLevel, type = Visibility.Type.Group.value)
context.value.jiraClient.addRestrictedComment(issueId, comment, visibility)
}
}
Expand Down Expand Up @@ -421,6 +458,58 @@ fun deleteLink(context: Lazy<IssueUpdateContext>, link: MojiraIssueLink) {
}
}

fun unarchiveVersion(context: Lazy<IssueUpdateContext>, versionId: String) {
context.value.preOperations.add {
runBlocking {
Either.catch {
withContext(Dispatchers.IO) {
try {
context.value.jiraClient.updateVersion(
versionId,
UpdateVersionBody(archived = false)
)
} catch (e: ClientErrorException) {
if (e.code == HttpStatus.SC_NOT_FOUND ||
e.code >= HttpStatus.SC_INTERNAL_SERVER_ERROR
) {
log.warn("Failed to unarchive version $versionId")
} else {
throw e
}
}
}
Unit
}
}
}
}

fun archiveVersion(context: Lazy<IssueUpdateContext>, versionId: String) {
context.value.postOperations.add {
runBlocking {
Either.catch {
withContext(Dispatchers.IO) {
try {
context.value.jiraClient.updateVersion(
versionId,
UpdateVersionBody(archived = true)
)
} catch (e: ClientErrorException) {
if (e.code == HttpStatus.SC_NOT_FOUND ||
e.code >= HttpStatus.SC_INTERNAL_SERVER_ERROR
) {
log.warn("Failed to archive version $versionId")
} else {
throw e
}
}
}
Unit
}
}
}
}

fun updateCommentBody(context: Lazy<IssueUpdateContext>, comment: MojiraComment, body: String) {
context.value.otherOperations.add {
runBlocking {
Expand Down
Loading
Loading