150 lines
4.3 KiB
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}")
|