import java.net.URLEncoder /** * Screens Housekeeping (Cloud) – Kandidatenliste * ----------------------------------------------------------- * Ziel: Screens finden, die in keinem Screen Scheme referenziert sind. * * Hinweis: * - Jira verhindert Delete, wenn Screen in Screen Scheme, Workflow oder Workflow Draft verwendet wird. :contentReference[oaicite:2]{index=2} * - Workflow/Draft-Usage kann man per REST nicht zuverlässig „auflisten“. :contentReference[oaicite:3]{index=3} * => Wir prüfen hier: "nicht in Screen Schemes". Beim echten Delete fängt Jira dann ggf. "used in workflow/draft" ab. ************************************************************************************************************************ * !!!! ES BESTEHT WEITERHIN DIE GEFAHR, DASS SCREENS GELÖSCHT WERDEN, DIE MAN WEITERHIN FÜR TRANSITIONEN BENÖTIGT!!! * !!!! DAHER IST DIESES SCRIPT MIT ÄUßERSTER SORGFALT ZU VERWENDEN!!!! ************************************************************************************************************************ */ def DRY_RUN = true def PROTECTED_SCREEN_IDS = [ // "1" ] def PROTECTED_NAME_PATTERNS = [ "default", "NICS: ", "NIN: ", "NINPDS", "CS: ", "PDS:" ] def PAGE_SIZE = 100 logger.info("=== Screens Housekeeping ===") logger.info("DRY_RUN: ${DRY_RUN}") logger.info("Protected screen IDs: ${PROTECTED_SCREEN_IDS}") logger.info("Protected name patterns: ${PROTECTED_NAME_PATTERNS}") def isNameProtected = { String name -> def n = (name ?: "").toLowerCase() PROTECTED_NAME_PATTERNS.any { p -> n.contains((p ?: "").toLowerCase()) } } def isIdProtected = { String id -> PROTECTED_SCREEN_IDS.any { it?.toString() == id?.toString() } } // für Pfadsegmente (IDs sind numerisch, aber sicher ist sicher) def encPath = { String s -> URLEncoder.encode(s ?: "", "UTF-8").replace("+", "%20") } /** * Recursively collect "screen id" values from a map: * - Viele Jira Responses haben z.B. defaultScreenId / screenId / createScreenId etc. * - Wir sammeln konservativ: keys die "screen" und "id" enthalten. */ def collectScreenIdsRecursive collectScreenIdsRecursive = { Object node, Set out -> if (node == null) return if (node instanceof Map) { (node as Map).each { k, v -> def key = k?.toString()?.toLowerCase() if (key && key.contains("screen") && key.contains("id")) { if (v != null) out << v.toString() } collectScreenIdsRecursive(v, out) } } else if (node instanceof List) { (node as List).each { item -> collectScreenIdsRecursive(item, out) } } } /** * 1) Alle Screens holen * API: GET /rest/api/3/screens (plural). :contentReference[oaicite:4]{index=4} */ def screens = [] def startAt = 0 while (true) { def resp = get("/rest/api/3/screens?startAt=${startAt}&maxResults=${PAGE_SIZE}").asObject(Map) if (resp.status != 200) { logger.error("ERROR|SCREENS_LIST_FAILED|status=${resp.status}|body=${resp.body}") return } def values = resp.body?.values ?: [] screens.addAll(values) def isLast = resp.body?.isLast if (isLast == true || values.isEmpty()) break startAt += PAGE_SIZE } logger.info("INFO|TOTAL_SCREENS|${screens.size()}") /** * 2) Alle Screen Schemes holen * API: GET /rest/api/3/screenscheme :contentReference[oaicite:5]{index=5} */ def screenSchemes = [] startAt = 0 while (true) { def resp = get("/rest/api/3/screenscheme?startAt=${startAt}&maxResults=${PAGE_SIZE}").asObject(Map) if (resp.status != 200) { logger.error("ERROR|SCREENSCHEME_LIST_FAILED|status=${resp.status}|body=${resp.body}") return } def values = resp.body?.values ?: [] screenSchemes.addAll(values) def isLast = resp.body?.isLast if (isLast == true || values.isEmpty()) break startAt += PAGE_SIZE } logger.info("INFO|TOTAL_SCREEN_SCHEMES|${screenSchemes.size()}") /** * 3) Referenz-Mapping: screenId -> [schemeNames...] * Wir holen pro Scheme die Details und sammeln alle enthaltenen screenIds. */ def usedBySchemeNames = [:].withDefault { [] as List } // screenId -> schemeNames def usedScreenIds = new HashSet() screenSchemes.each { ss -> def ssId = ss?.id?.toString() def ssName = ss?.name?.toString() ?: "?" if (!ssId) return def resp = get("/rest/api/3/screenscheme/${encPath(ssId)}").asObject(Map) if (resp.status != 200) { logger.warn("WARN|SCREENSCHEME_DETAILS_FAILED|schemeId=${ssId}|name=${ssName}|status=${resp.status}") return } def found = new HashSet() collectScreenIdsRecursive(resp.body, found) found.each { sid -> usedScreenIds << sid usedBySchemeNames[sid] = (usedBySchemeNames[sid] + ssName).unique() } } logger.info("INFO|USED_SCREEN_IDS|${usedScreenIds.size()}") /** * 4) Auswertung * Kandidat = Screen wird in keinem Screen Scheme referenziert + nicht protected */ def candidates = [] def keptProtected = 0 def keptUsed = 0 screens.each { s -> def screenId = s?.id?.toString() def screenName = s?.name?.toString() if (!screenId) return if (isIdProtected(screenId) || isNameProtected(screenName)) { keptProtected++ logger.info("KEEP|screenId=${screenId}|name=${screenName}|reason=PROTECTED") return } def schemes = usedBySchemeNames[screenId] ?: [] if (!schemes.isEmpty()) { keptUsed++ // optional kurz halten: nur bis zu 5 scheme names def preview = schemes.take(5) logger.info("KEEP|screenId=${screenId}|name=${screenName}|reason=USED_BY_SCREEN_SCHEMES|count=${schemes.size()}|examples=${preview}") return } // sauberer Kandidat (bezogen auf Screen Schemes) candidates << [id: screenId, name: screenName] } /** * 5) Saubere Kandidatenliste (Dry run) * 1 Zeile pro Screen, gut copy/paste */ candidates = candidates.sort { (it.name ?: "") as String } logger.info("=== CANDIDATES (NOT IN ANY SCREEN SCHEME) ===") candidates.each { c -> logger.info("CANDIDATE|screenId=${c.id}|name=${c.name}") } logger.info("=== SUMMARY ===") logger.info("Total screens: ${screens.size()}") logger.info("Kept (protected): ${keptProtected}") logger.info("Kept (used by screen schemes): ${keptUsed}") logger.info("Candidates (no screen scheme refs): ${candidates.size()}") /** * 6) Optional delete * API: DELETE /rest/api/3/screens/{screenId} :contentReference[oaicite:6]{index=6} * Jira wird blocken, wenn Workflow/Draft etc. doch referenziert. */ if (!DRY_RUN) { logger.info("=== DELETE PHASE ===") def deleted = 0 def failed = 0 candidates.each { c -> def delResp = delete("/rest/api/3/screens/${encPath(c.id)}").asString() if (delResp.status == 204) { deleted++ logger.info("DEL|OK|screenId=${c.id}|name=${c.name}") } else { failed++ logger.error("DEL|FAIL|screenId=${c.id}|name=${c.name}|status=${delResp.status}|body=${delResp.body}") } } logger.info("=== DELETE SUMMARY ===") logger.info("Deleted: ${deleted}") logger.info("Delete failures: ${failed}") } logger.info("=== DONE ===")