194 lines
5.7 KiB
Groovy
194 lines
5.7 KiB
Groovy
/**
|
||
* 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 ===")
|