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

178 lines
5.1 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 Screen Scheme Housekeeping (Cloud) Report + optional Delete
* -----------------------------------------------------------------------
* Delete rule:
* - Scheme darf nur gelöscht werden, wenn es mit KEINEM Projekt verknüpft ist.
* - Extra-Schutz über PROTECTED_SCHEME_IDS und PROTECTED_NAME_PATTERNS.
*/
def PROTECTED_SCHEME_IDS = [
// "10000"
]
def PROTECTED_NAME_PATTERNS = [
"default",
"system"
]
def DRY_RUN = true // erst auf false setzen, wenn Report passt
def PROJECT_PAGE_SIZE = 50 // paging für project/search
def SCHEME_PAGE_SIZE = 100 // paging für issuetypescreenscheme
logger.info("=== Issue Type Screen Scheme Housekeeping ===")
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 (id + key)
*/
def projects = []
def startAt = 0
while (true) {
def resp = get("/rest/api/3/project/search?startAt=${startAt}&maxResults=${PROJECT_PAGE_SIZE}")
.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 += PROJECT_PAGE_SIZE
}
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: issueTypeScreenSchemeId -> [projectIds]
* (Endpoint braucht projectId, daher über alle Projekte iterieren)
*/
def schemeToProjectIds = [:].withDefault { [] }
projectIdToKey.keySet().each { pid ->
def resp = get("/rest/api/3/issuetypescreenscheme/project?projectId=${pid}")
.asObject(Map)
if (resp.status != 200) {
logger.warn("WARN|SCHEME_LOOKUP_FAILED|projectId=${pid}|status=${resp.status}")
return
}
def values = resp.body?.values ?: []
values.each { row ->
def schemeId = row?.issueTypeScreenScheme?.id?.toString()
def pids = (row?.projectIds ?: []).collect { it?.toString() }.findAll { it != null }
if (schemeId) {
schemeToProjectIds[schemeId] = (schemeToProjectIds[schemeId] + pids).unique()
}
}
}
/**
* 3) Alle Issue Type Screen Schemes holen
*/
def schemes = []
startAt = 0
while (true) {
def resp = get("/rest/api/3/issuetypescreenscheme?startAt=${startAt}&maxResults=${SCHEME_PAGE_SIZE}")
.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 += SCHEME_PAGE_SIZE
}
logger.info("INFO|TOTAL_SCHEMES|${schemes.size()}")
/**
* 4) Auswertung + optional Delete
*/
def keptAssociated = 0
def keptProtected = 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 nameProtected = isNameProtected(schemeName)
def idProtected = isIdProtected(schemeId)
if (idProtected || nameProtected) {
keptProtected++
def why = []
if (idProtected) why << "ID_PROTECTED"
if (nameProtected) why << "NAME_PROTECTED"
logger.info("KEEP|schemeId=${schemeId}|name=${schemeName}|assocProjects=${assocProjectKeys}|reason=${why}")
return
}
// Wenn irgendwo zugeordnet: nicht löschen
if (!assocProjectIds.isEmpty()) {
keptAssociated++
logger.info("KEEP|schemeId=${schemeId}|name=${schemeName}|assocProjects=${assocProjectKeys}|reason=ASSOCIATED_TO_PROJECTS")
return
}
// Wirklich unassoziiert: Kandidat
logger.info("DEL?|schemeId=${schemeId}|name=${schemeName}|assocProjects=[]|reason=UNASSOCIATED")
candidates << [schemeId, schemeName, "UNASSOCIATED"]
if (!DRY_RUN) {
def delResp = delete("/rest/api/3/issuetypescreenscheme/${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): ${keptAssociated}")
logger.info("Kept (protected by rules): ${keptProtected}")
logger.info("Delete candidates: ${candidates.size()}")
candidates.each { c ->
logger.info("CANDIDATE|schemeId=${c[0]}|name=${c[1]}|reason=${c[2]}")
}
logger.info("=== DONE ===")