/** * 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 ===")