Jira-Scripte/Scheduled Jobs/[TS] Schließe Tickets ohne Rückmeldung des Kunden.groovy

150 lines
4.3 KiB
Groovy

// ScriptRunner for Jira Cloud - Scheduled Job
// Auto-close issues in "Waiting for Customer" after X days inactivity.
// Steps:
// 1) JQL search (paging via nextPageToken)
// 2) Add comment (ADF) BEFORE closing (so customer gets notification)
// 3) Transition to "Closed" via transition ID
// -------------------- Konfiguration --------------------
final String projectKey = "TS"
final String waitingStatusName = "Waiting for Customer"
//final String waitingStatusName = "Wartet auf Kunden"
final int inactivityDays = 14
final int maxPerPage = 50
final boolean dryRun = false
// Transition "Schließen"
final String transitionIdClose = "2"
// Comment template (issue key gets injected per issue)
final String commentTemplate =
"""Wir haben Ihre Supportanfrage geschlossen, da wir in den letzten %d Tagen keine Rückmeldung von Ihnen erhalten haben.
Sollten Sie weiterhin Unterstützung benötigen, erstellen Sie bitte eine neue Supportanfrage und verweisen Sie dabei gern auf die Anfragenummer %s."""
final String jql = "project = ${projectKey} AND status = '${waitingStatusName}' AND resolution IS EMPTY AND updated < startOfDay(-${inactivityDays})"
// ## Test mit Einzelticket #####
// final String jql = "project = ${projectKey} AND key = TS-35"
// -------------------- Helper --------------------
Map buildAdfComment(String text) {
[
type : "doc",
version: 1,
content: [[
type : "paragraph",
content: [[type: "text", text: text]]
]]
]
}
boolean addComment(String issueKey, String commentText) {
def resp = post("/rest/api/3/issue/${issueKey}/comment")
.header("Content-Type", "application/json")
.body([body: buildAdfComment(commentText)])
.asObject(Map)
if (!(resp.status in [200, 201])) {
logger.error("${issueKey}: Kommentar fehlgeschlagen: ${resp.status} - ${resp.body}")
return false
}
return true
}
boolean closeIssue(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) {
logger.error("${issueKey}: Schließen fehlgeschlagen (transitionId=${transitionId}): ${resp.status} - ${resp.body}")
return false
}
return true
}
// -------------------- Ablauf --------------------
logger.info("=== Auto-Close Job gestartet ===")
logger.info("JQL: ${jql}")
logger.info("Transition ID (Close): ${transitionIdClose}")
logger.info("DRY_RUN: ${dryRun}")
String nextPageToken = null
int processed = 0
int closed = 0
int skipped = 0
int failed = 0
while (true) {
def req = get("/rest/api/3/search/jql")
.queryString("jql", jql)
.queryString("maxResults", maxPerPage.toString())
// Nur Felder, die wir wirklich brauchen
.queryString("fields", "status,resolution,updated")
if (nextPageToken) {
req = req.queryString("nextPageToken", nextPageToken)
}
def searchResp = req.asObject(Map)
if (searchResp.status != 200) {
logger.error("JQL-Suche fehlgeschlagen: ${searchResp.status} - ${searchResp.body}")
break
}
def issues = (searchResp.body?.issues ?: []) as List
nextPageToken = searchResp.body?.nextPageToken as String
logger.info("Seite: ${issues.size()} Issues, nextPageToken=${nextPageToken ?: 'none'}")
if (!issues) break
issues.each { i ->
processed++
String issueKey = i?.key
String statusName = i?.fields?.status?.name
def resolution = i?.fields?.resolution
String updated = i?.fields?.updated
if (!issueKey) {
skipped++
logger.warn("Issue ohne Key übersprungen: ${i}")
return
}
logger.info("${issueKey}: Kandidat (updated=${updated})")
if (dryRun) {
logger.info("${issueKey}: DRY_RUN -> würde kommentieren + schließen")
return
}
// 1) Kommentar vor dem Schließen
String commentText = String.format(commentTemplate, inactivityDays, issueKey)
if (!addComment(issueKey, commentText)) {
failed++
return
}
// 2) Schließen
if (!closeIssue(issueKey, transitionIdClose)) {
failed++
return
}
closed++
logger.info("${issueKey}: erfolgreich geschlossen (transitionId=${transitionIdClose})")
}
if (!nextPageToken) break
}
logger.info("=== Auto-Close Job fertig ===")
logger.info("Processed=${processed}, Closed=${closed}, Skipped=${skipped}, Failed=${failed}")