Jira-Scripte/Console - Maintenance/09. Unused Screens cleaner.groovy

223 lines
6.8 KiB
Groovy
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String> 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<String> } // screenId -> schemeNames
def usedScreenIds = new HashSet<String>()
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<String>()
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 ===")