223 lines
6.8 KiB
Groovy
223 lines
6.8 KiB
Groovy
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 ===")
|