// 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}")