// ScriptRunner for Jira Cloud - Scheduled Job // Auto-close issues in "Waiting for Customer" after X days inactivity. // 1) JQL search (paging via nextPageToken) // 2) If needed set customfield_11433 based on customfield_10039 // 3) Add comment (ADF) BEFORE closing (so customer gets notification) // 4) Transition to "Closed" via fixed transition ID (411) and set Resolution "Keine Lösung" (10010) final String projectNameOrKey = 'Customer Service Desk' // oder Projekt-Key "CSD" final String waitingStatusName = 'Waiting for Customer' final int inactivityDays = 2 final int maxPerPage = 50 final boolean dryRun = false // Transition "Schließen" final String transitionIdClose = "411" // Resolution: "Keine Lösung" final String resolutionIdNoSolution = "10010" // Custom fields final String cfChannel = "customfield_10039" // Single select: "Customer Service" | "Partner Support" final String cfFlag = "customfield_11433" // Set to "Ja" or "Nein" // Select values final String channelCustomerService = "Customer Service" final String channelPartnerSupport = "Partner Support" // 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 = \"${projectNameOrKey}\" AND status = \"${waitingStatusName}\" AND resolution IS EMPTY AND updated < startOfDay(-${inactivityDays})" // ## Test mit einzelticket ##### //final String jql = "project = \"${projectNameOrKey}\" AND key in (CSD-2124)" Map buildAdfComment(String text) { [ type: "doc", version: 1, content: [[ type: "paragraph", content: [[type: "text", text: text]] ]] ] } logger.info("=== Auto-Close Job gestartet ===") logger.info("JQL: ${jql}") logger.info("Transition ID (Close): ${transitionIdClose}") logger.info("Resolution ID (Keine Lösung): ${resolutionIdNoSolution}") 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()) // Wichtig: cfChannel mitsenden, sonst können wir es nicht auswerten .queryString("fields", "status,resolution,updated,${cfChannel}") 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 } if (!statusName?.equalsIgnoreCase(waitingStatusName)) { skipped++ logger.info("${issueKey}: Status '${statusName}' != '${waitingStatusName}' -> skip") return } /* if (resolution != null) { skipped++ logger.info("${issueKey}: hat bereits Resolution -> skip") return } */ logger.info("${issueKey}: Kandidat (updated=${updated})") if (dryRun) { logger.info("${issueKey}: DRY_RUN -> würde Feld setzen + kommentieren + schließen") return } // ========= Erweiterung: customfield_10039 auswerten und customfield_11433 setzen ========= def channelValueObj = i?.fields?."${cfChannel}" // i.fields.customfield_10039 String channelValue = channelValueObj?.value?.toString() String flagValue = null if (channelValue?.equalsIgnoreCase(channelCustomerService)) { flagValue = "Ja" } else if (channelValue?.equalsIgnoreCase(channelPartnerSupport)) { flagValue = "Nein" } else { // Wenn leer/unerwartet: nichts setzen, aber loggen logger.warn("${issueKey}: ${cfChannel} ist leer oder unerwartet ('${channelValue}'), setze ${cfFlag} nicht.") } if (flagValue != null) { def updateResp = put("/rest/api/3/issue/${issueKey}") .header("Content-Type", "application/json") .body([ fields: [ (cfFlag): [value: flagValue] // Single select sauber setzen ] ]) .asString() if (updateResp.status == 204) { logger.info("${issueKey}: ${cfFlag} gesetzt auf '${flagValue}' (basierend auf ${cfChannel}='${channelValue}')") } else { failed++ logger.error("${issueKey}: Setzen von ${cfFlag} fehlgeschlagen: ${updateResp.status} - ${updateResp.body}") return } } // ========= Kommentar (vor dem Schließen) ========= String commentText = String.format(commentTemplate, inactivityDays, issueKey) def commentResp = post("/rest/api/3/issue/${issueKey}/comment") .header("Content-Type", "application/json") .body([body: buildAdfComment(commentText)]) .asObject(Map) if (!(commentResp.status in [200, 201])) { failed++ logger.error("${issueKey}: Kommentar fehlgeschlagen: ${commentResp.status} - ${commentResp.body}") return } // ========= Schließen via Transition-ID (ohne Resolution im Payload) ========= def closeResp = post("/rest/api/3/issue/${issueKey}/transitions") .header("Content-Type", "application/json") .body([transition: [id: transitionIdClose]]) .asString() if (closeResp.status != 204) { failed++ logger.error("${issueKey}: Schließen fehlgeschlagen: ${closeResp.status} - ${closeResp.body}") return } logger.info("${issueKey}: erfolgreich geschlossen (transitionId=${transitionIdClose})") // ========= Resolution nachträglich setzen ========= def resResp = put("/rest/api/3/issue/${issueKey}") .header("Content-Type", "application/json") .body([fields: [resolution: [id: resolutionIdNoSolution]]]) .asString() if (resResp.status == 204) { logger.info("${issueKey}: Resolution gesetzt auf '${resolutionIdNoSolution}' (Keine Lösung)") } else { failed++ logger.error("${issueKey}: Resolution setzen fehlgeschlagen: ${resResp.status} - ${resResp.body}") // Ticket ist schon geschlossen, daher hier NICHT returnen zwingend, // aber wir markieren es als failed für die Statistik. } } if (!nextPageToken) break } logger.info("=== Auto-Close Job fertig ===") logger.info("Processed=${processed}, Closed=${closed}, Skipped=${skipped}, Failed=${failed}")