Jira-Scripte/Console - Maintenance/03. Unused Issue Type Scheme cleaner.groovy

194 lines
5.7 KiB
Groovy
Raw Permalink 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.

/**
* Issue Type Scheme Housekeeping (Cloud) Report + optional Delete
* ----------------------------------------------------------------
* Korrekte Strategie:
* - Hole alle Projekte (id + key)
* - Für jedes Projekt: hole IssueTypeScheme-Zuordnung
* - Delete nur, wenn Scheme an KEINEM Projekt hängt
* - Extra-Schutz über IDs + Name-Patterns
*/
def PROTECTED_PROJECT_KEYS = ["NIN","NICS","NINPDS","NINPDSARC","CS","CRON"]
// Zusätzliche Sicherung:
def PROTECTED_SCHEME_IDS = [
// "10000"
]
def PROTECTED_NAME_PATTERNS = [
"default",
"system"
]
def DRY_RUN = true // erst auf false setzen wenn Report passt
logger.info("=== Issue Type Scheme Housekeeping ===")
logger.info("Protected projects (keys): ${PROTECTED_PROJECT_KEYS}")
logger.info("Protected scheme IDs: ${PROTECTED_SCHEME_IDS}")
logger.info("Protected name patterns: ${PROTECTED_NAME_PATTERNS}")
logger.info("DRY_RUN: ${DRY_RUN}")
def isNameProtected = { String name ->
def n = (name ?: "").toLowerCase()
return PROTECTED_NAME_PATTERNS.any { p -> n.contains((p ?: "").toLowerCase()) }
}
def isIdProtected = { String id ->
return PROTECTED_SCHEME_IDS.any { it?.toString() == id?.toString() }
}
/**
* 1) Alle Projekte holen (key + id)
*/
def projects = []
def startAt = 0
def maxResults = 50
while (true) {
def resp = get("/rest/api/3/project/search?startAt=${startAt}&maxResults=${maxResults}")
.asObject(Map)
if (resp.status != 200) {
logger.error("ERROR|PROJECT_SEARCH_FAILED|status=${resp.status}|body=${resp.body}")
break
}
def values = resp.body?.values ?: []
projects.addAll(values)
def isLast = resp.body?.isLast
if (isLast == true || values.isEmpty()) break
startAt += maxResults
}
logger.info("INFO|TOTAL_PROJECTS|${projects.size()}")
def projectIdToKey = [:]
projects.each { p ->
def pid = p?.id?.toString()
def pkey = p?.key?.toString()
if (pid && pkey) projectIdToKey[pid] = pkey
}
/**
* 2) Mapping: schemeId -> [projectIds]
* (korrekt: Endpoint braucht projectId)
*/
def schemeToProjectIds = [:].withDefault { [] }
projectIdToKey.keySet().each { pid ->
def resp = get("/rest/api/3/issuetypescheme/project?projectId=${pid}")
.asObject(Map)
if (resp.status != 200) {
logger.warn("WARN|ISSUETYPE_SCHEME_LOOKUP_FAILED|projectId=${pid}|status=${resp.status}")
return
}
def values = resp.body?.values ?: []
values.each { row ->
def schemeId = row?.issueTypeScheme?.id?.toString()
def pids = (row?.projectIds ?: []).collect { it?.toString() }.findAll { it != null }
if (schemeId) {
// Achtung: pids enthält hier normalerweise genau das pid, wir mergen trotzdem sauber
schemeToProjectIds[schemeId] = (schemeToProjectIds[schemeId] + pids).unique()
}
}
}
/**
* 3) Geschützte Projekt-IDs berechnen (für Reporting)
*/
def protectedProjectIds = projectIdToKey.findAll { id, key -> PROTECTED_PROJECT_KEYS.contains(key) }*.key
logger.info("INFO|PROTECTED_PROJECT_IDS|${protectedProjectIds}")
/**
* 4) Alle Issue Type Schemes holen
*/
def schemes = []
startAt = 0
maxResults = 100
while (true) {
def resp = get("/rest/api/3/issuetypescheme?startAt=${startAt}&maxResults=${maxResults}")
.asObject(Map)
if (resp.status != 200) {
logger.error("ERROR|SCHEME_LIST_FAILED|status=${resp.status}|body=${resp.body}")
break
}
def values = resp.body?.values ?: []
schemes.addAll(values)
def isLast = resp.body?.isLast
if (isLast == true || values.isEmpty()) break
startAt += maxResults
}
logger.info("INFO|TOTAL_SCHEMES|${schemes.size()}")
/**
* 5) Auswertung
*/
def keptByUsage = 0
def keptByRule = 0
def candidates = [] // [id,name,reason]
schemes.each { s ->
def schemeId = s?.id?.toString()
def schemeName = s?.name?.toString()
def assocProjectIds = schemeToProjectIds[schemeId] ?: []
def assocProjectKeys = assocProjectIds.collect { projectIdToKey[it] }.findAll { it != null }.unique()
def usedByProtectedKeys = assocProjectKeys.intersect(PROTECTED_PROJECT_KEYS)
def nameProtected = isNameProtected(schemeName)
def idProtected = isIdProtected(schemeId)
if (idProtected || nameProtected) {
keptByRule++
def why = []
if (idProtected) why << "ID_PROTECTED"
if (nameProtected) why << "NAME_PROTECTED"
logger.info("KEEP|schemeId=${schemeId}|name=${schemeName}|assocProjects=${assocProjectKeys}|usedByProtected=${usedByProtectedKeys}|reason=${why}")
return
}
// Wenn es an irgendeinem Projekt hängt: nicht löschbar
if (!assocProjectIds.isEmpty()) {
keptByUsage++
logger.info("KEEP|schemeId=${schemeId}|name=${schemeName}|assocProjects=${assocProjectKeys}|usedByProtected=${usedByProtectedKeys}|reason=ASSOCIATED_TO_PROJECTS")
return
}
// wirklich unzugeordnet: Kandidat
logger.info("DEL?|schemeId=${schemeId}|name=${schemeName}|assocProjects=[]|reason=UNASSOCIATED")
candidates << [schemeId, schemeName, "UNASSOCIATED"]
if (!DRY_RUN) {
def delResp = delete("/rest/api/3/issuetypescheme/${schemeId}")
.asString()
if (delResp.status == 204) {
logger.info("DEL|OK|schemeId=${schemeId}|name=${schemeName}")
} else {
logger.error("DEL|FAIL|schemeId=${schemeId}|name=${schemeName}|status=${delResp.status}|body=${delResp.body}")
}
}
}
logger.info("=== SUMMARY ===")
logger.info("Total schemes: ${schemes.size()}")
logger.info("Kept (associated to any projects): ${keptByUsage}")
logger.info("Kept (protected by rules): ${keptByRule}")
logger.info("Delete candidates: ${candidates.size()}")
candidates.each { c ->
logger.info("CANDIDATE|schemeId=${c[0]}|name=${c[1]}|reason=${c[2]}")
}
logger.info("=== DONE ===")