194 lines
6.2 KiB
Groovy
194 lines
6.2 KiB
Groovy
/*
|
|
* ScriptRunner Cloud - Scheduled Job
|
|
*
|
|
* Zweck:
|
|
* - Findet Issues, die seit 48-72h "Resolved" sind und noch ein DueDate haben
|
|
* - Setzt Kommentar (ADF) VOR dem Schließen (da Closed nicht kommentierbar ist)
|
|
* - Leert danach DueDate
|
|
* - Transition nach Zielstatus "Closed"
|
|
*
|
|
* Ablauf pro Issue:
|
|
* 1) Kommentar hinzufügen
|
|
* 2) DueDate leeren
|
|
* 3) Transition -> Closed
|
|
*
|
|
* Hinweise:
|
|
* - Transition-ID ist optional; Standard ist Auflösung über Zielstatus (to.name).
|
|
* - Bei gemischten Workflows ist die Zielstatus-Auflösung meist stabiler als feste IDs.
|
|
*/
|
|
|
|
// ===================== Konfiguration =====================
|
|
final String JQL = 'project = "Customer Service Desk" AND status = Resolved AND duedate IS NOT EMPTY AND resolutiondate >= -72h AND resolutiondate <= -48h'
|
|
|
|
|
|
final int MAX_PER_PAGE = 50
|
|
|
|
// Optional: Wenn du eine Transition-ID erzwingen willst (z.B. "331"), hier setzen. Sonst null lassen.
|
|
final String TRANSITION_ID = null
|
|
|
|
// Zielstatus-Name (wird bevorzugt genutzt)
|
|
final String TARGET_STATUS_NAME = "Closed"
|
|
|
|
// Fallback über Aktionsnamen (falls "to.name" nicht hilft)
|
|
final List<String> ACTION_NAME_FALLBACKS = ["Schließen", "Close", "Closed"]
|
|
|
|
// Kommentartext (als ADF gesendet)
|
|
final String COMMENT_TEXT = """
|
|
Dieses Ticket wurde automatisch geschlossen, nachdem der Support die Lösung präsentiert hat und wir davon ausgehen, dass die Lösung korrekt ist.
|
|
Sollten weiterhin Fragen bestehen oder erneut Unterstützung benötigt werden, können Sie eine neue Anfrage stellen.
|
|
Ihr pds Support
|
|
""".trim()
|
|
|
|
final Map ADF_BODY = [
|
|
type : "doc",
|
|
version: 1,
|
|
content: [[
|
|
type : "paragraph",
|
|
content: [[ type: "text", text: COMMENT_TEXT ]]
|
|
]]
|
|
]
|
|
// ========================================================
|
|
|
|
|
|
// --------------------- Helper ---------------------------
|
|
|
|
/**
|
|
* Liefert die passende Transition-ID für das Issue.
|
|
* Priorität:
|
|
* 1) harte TRANSITION_ID (wenn gesetzt)
|
|
* 2) Transition, deren Zielstatus to.name == TARGET_STATUS_NAME
|
|
* 3) Transition, deren Aktionsname in ACTION_NAME_FALLBACKS ist
|
|
*/
|
|
def resolveTransitionId = { String issueKey ->
|
|
if (TRANSITION_ID?.trim()) {
|
|
return TRANSITION_ID.trim()
|
|
}
|
|
|
|
def resp = get("/rest/api/3/issue/${issueKey}/transitions").asObject(Map)
|
|
if (resp.status != 200) {
|
|
throw new IllegalStateException("Transitions nicht lesbar: HTTP ${resp.status} - ${resp.body}")
|
|
}
|
|
|
|
List transitions = (resp.body?.transitions as List) ?: []
|
|
|
|
// 1) nach Ziel-Status (to.name)
|
|
def byTargetStatus = transitions.find { t ->
|
|
(t?.to?.name as String)?.equalsIgnoreCase(TARGET_STATUS_NAME)
|
|
}
|
|
if (byTargetStatus?.id) return byTargetStatus.id as String
|
|
|
|
// 2) nach Aktionsname (name)
|
|
def byActionName = transitions.find { t ->
|
|
ACTION_NAME_FALLBACKS.any { fn -> (t?.name as String)?.equalsIgnoreCase(fn) }
|
|
}
|
|
return byActionName?.id as String
|
|
}
|
|
|
|
/** Kommentar hinzufügen (ADF) */
|
|
def addComment = { String issueKey, Map adf ->
|
|
def resp = post("/rest/api/3/issue/${issueKey}/comment")
|
|
.header("Content-Type", "application/json")
|
|
.body([ body: adf ])
|
|
.asObject(Map)
|
|
|
|
if (resp.status != 201) {
|
|
throw new IllegalStateException("Kommentar fehlgeschlagen: HTTP ${resp.status} - ${resp.body}")
|
|
}
|
|
}
|
|
|
|
/** DueDate leeren */
|
|
def clearDueDate = { String issueKey ->
|
|
def resp = put("/rest/api/3/issue/${issueKey}")
|
|
.header("Content-Type", "application/json")
|
|
.body([ fields: [ duedate: null ] ])
|
|
.asString()
|
|
|
|
if (resp.status != 204) {
|
|
throw new IllegalStateException("DueDate leeren fehlgeschlagen: HTTP ${resp.status} - ${resp.body}")
|
|
}
|
|
}
|
|
|
|
/** Transition ausführen */
|
|
def doTransition = { String issueKey, String transitionId ->
|
|
def resp = post("/rest/api/3/issue/${issueKey}/transitions")
|
|
.header("Content-Type", "application/json")
|
|
.body([ transition: [ id: transitionId ] ])
|
|
.asString()
|
|
|
|
if (resp.status != 204) {
|
|
throw new IllegalStateException("Transition fehlgeschlagen (id=${transitionId}): HTTP ${resp.status} - ${resp.body}")
|
|
}
|
|
}
|
|
|
|
/** Kleiner Helper fürs Logging, damit Logs konsistent sind */
|
|
def logOk = { String issueKey, String msg -> logger.info("[AUTO-CLOSE] ${issueKey}: ${msg}") }
|
|
def logWarn = { String issueKey, String msg -> logger.warn("[AUTO-CLOSE] ${issueKey}: ${msg}") }
|
|
def logErr = { String issueKey, String msg -> logger.error("[AUTO-CLOSE] ${issueKey}: ${msg}") }
|
|
|
|
// --------------------- Verarbeitung ----------------------
|
|
|
|
int processed = 0
|
|
int skipped = 0
|
|
int failed = 0
|
|
|
|
String nextPageToken = null
|
|
boolean isLast = false
|
|
|
|
logger.info("[AUTO-CLOSE] Job Start. JQL='${JQL}'")
|
|
|
|
while (!isLast) {
|
|
def req = get("/rest/api/3/search/jql")
|
|
.queryString("jql", JQL)
|
|
.queryString("fields", "status,duedate") // key ist immer dabei
|
|
.queryString("maxResults", MAX_PER_PAGE as String)
|
|
|
|
if (nextPageToken) {
|
|
req = req.queryString("nextPageToken", nextPageToken)
|
|
}
|
|
|
|
def searchResp = req.asObject(Map)
|
|
if (searchResp.status != 200) {
|
|
throw new IllegalStateException("Search-Fehler: HTTP ${searchResp.status} - ${searchResp.body}")
|
|
}
|
|
|
|
Map body = searchResp.body as Map
|
|
List issues = (body?.issues as List) ?: []
|
|
|
|
isLast = (body?.isLast == true)
|
|
nextPageToken = body?.nextPageToken as String
|
|
|
|
logger.info("[AUTO-CLOSE] Seite geladen: issues=${issues.size()}, isLast=${isLast}, nextPageToken=${nextPageToken}")
|
|
|
|
issues.each { Map iss ->
|
|
String key = iss["key"] as String
|
|
|
|
try {
|
|
// 1) Kommentar VOR dem Schließen
|
|
addComment(key, ADF_BODY)
|
|
logOk(key, "Kommentar gesetzt.")
|
|
|
|
// 2) DueDate leeren
|
|
clearDueDate(key)
|
|
logOk(key, "DueDate geleert.")
|
|
|
|
// 3) Transition -> Closed
|
|
String transitionId = resolveTransitionId(key)
|
|
if (!transitionId) {
|
|
skipped++
|
|
logWarn(key, "Keine passende Transition nach '${TARGET_STATUS_NAME}' gefunden (Fallbacks=${ACTION_NAME_FALLBACKS}). Ticket bleibt Resolved.")
|
|
return
|
|
}
|
|
|
|
doTransition(key, transitionId)
|
|
logOk(key, "Transition ausgeführt (id=${transitionId}) -> '${TARGET_STATUS_NAME}'.")
|
|
|
|
processed++
|
|
} catch (Throwable t) {
|
|
failed++
|
|
logErr(key, "Fehler: ${t.class.simpleName}: ${t.message}")
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.info("[AUTO-CLOSE] Job Ende. processed=${processed}, skipped=${skipped}, failed=${failed} (JQL='${JQL}')")
|