Jira-Scripte/Console - Maintenance/07. Unused Workflow Scheme Housekeeping cleaner.groovy

184 lines
5.1 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.

/**
* Workflow 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.
*
* Mapping:
* - Wir holen alle Projekte (id + key)
* - Dann: GET /rest/api/3/workflowscheme/project?projectId=...
* -> liefert für das Projekt die Workflow-Scheme-Zuordnung
*/
def PROTECTED_SCHEME_IDS = [
// "10000"
]
def PROTECTED_NAME_PATTERNS = [
"default",
"classic"
]
def DRY_RUN = true
def PROJECT_PAGE_SIZE = 50
def SCHEME_PAGE_SIZE = 100
logger.info("=== Workflow 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: workflowSchemeId -> [projectIds]
* REST: GET /rest/api/3/workflowscheme/project?projectId={projectId}
*/
def schemeToProjectIds = [:].withDefault { [] }
projectIdToKey.keySet().each { pid ->
def resp = get("/rest/api/3/workflowscheme/project?projectId=${pid}")
.asObject(Map)
if (resp.status != 200) {
logger.warn("WARN|WFSCHEME_LOOKUP_FAILED|projectId=${pid}|status=${resp.status}")
return
}
// Response: values: [ { workflowScheme: {id,name,...}, projectIds:[...] }, ... ]
def values = resp.body?.values ?: []
values.each { row ->
def schemeId = row?.workflowScheme?.id?.toString()
def pids = (row?.projectIds ?: []).collect { it?.toString() }.findAll { it != null }
if (schemeId) {
schemeToProjectIds[schemeId] = (schemeToProjectIds[schemeId] + pids).unique()
}
}
}
/**
* 3) Alle Workflow Schemes holen
* REST: GET /rest/api/3/workflowscheme
*/
def schemes = []
startAt = 0
while (true) {
def resp = get("/rest/api/3/workflowscheme?startAt=${startAt}&maxResults=${SCHEME_PAGE_SIZE}")
.asObject(Map)
if (resp.status != 200) {
logger.error("ERROR|WFSCHEME_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_WORKFLOW_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
}
if (!assocProjectIds.isEmpty()) {
keptAssociated++
logger.info("KEEP|schemeId=${schemeId}|name=${schemeName}|assocProjects=${assocProjectKeys}|reason=ASSOCIATED_TO_PROJECTS")
return
}
logger.info("DEL?|schemeId=${schemeId}|name=${schemeName}|assocProjects=[]|reason=UNASSOCIATED")
candidates << [schemeId, schemeName, "UNASSOCIATED"]
if (!DRY_RUN) {
def delResp = delete("/rest/api/3/workflowscheme/${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 workflow 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 ===")