Jira-Scripte/Console - Maintenance/Unused Notification Schemes cleaner.groovy
2025-12-14 19:17:45 +01:00

228 lines
7.6 KiB
Groovy

// -----------------------------------------------------------------------------
// Housekeeping: Unbenutzte Notification Schemes finden (projektbasiert)
// Jira Cloud - ScriptRunner Console
// -----------------------------------------------------------------------------
import groovy.json.JsonOutput
// --- Konfiguration -----------------------------------------------------------
// true -> nur Testlauf, es wird NICHT gelöscht
// false -> unbenutzte Notification Schemes (ohne EXCLUDED_IDS) würden gelöscht
// (Löschlogik ist unten schon vorbereitet, aber per Default aus)
final boolean DRY_RUN = true
// Notification-Scheme-IDs, die auf keinen Fall gelöscht werden sollen
// (z.B. Standardschemata / System-Schemata)
final Set<Long> EXCLUDED_IDS = [10000L] as Set
// --- Schritt 1: Alle Notification Schemes laden (paginierte API) ------------
List<Map> allSchemes = []
int startAt = 0
int maxResults = 50
boolean finished = false
while (!finished) {
def resp = get("/rest/api/3/notificationscheme?startAt=${startAt}&maxResults=${maxResults}")
.asObject(Map)
if (resp.status != 200) {
logger.error("Konnte Notification Schemes nicht laden (startAt=${startAt}): ${resp.status} - ${resp.body}")
return "Fehler beim Laden der Notification Schemes. Siehe Log."
}
def body = resp.body ?: [:]
def values = (body.values ?: []) as List<Map>
allSchemes.addAll(values)
boolean isLast = (body.isLast == true)
int total = (body.total ?: (startAt + values.size())) as int
logger.info "Notification Schemes geladen: ${allSchemes.size()} (total ~ ${total}), isLast=${isLast}"
if (isLast || values.isEmpty()) {
finished = true
} else {
startAt += maxResults
if (startAt >= total) {
finished = true
}
}
}
logger.info "Anzahl Notification Schemes insgesamt: ${allSchemes.size()}"
// Map: schemeId -> [scheme: <Objekt>, used: boolean, projects: Set<projectKey>]
def schemeUsage = allSchemes.collectEntries { scheme ->
Long id = (scheme.id as Long)
[
(id): [
scheme : scheme,
used : false,
projects: [] as Set<String>
]
]
}
// --- Schritt 2: Alle Projekte laden und pro Projekt das Notification Scheme holen ---
int totalProjects = 0
startAt = 0
finished = false
while (!finished) {
def projResp = get("/rest/api/3/project/search?startAt=${startAt}&maxResults=${maxResults}")
.asObject(Map)
if (projResp.status != 200) {
logger.error("Konnte Projekte nicht laden (startAt=${startAt}): ${projResp.status} - ${projResp.body}")
break
}
def body = projResp.body ?: [:]
def projects = (body.values ?: []) as List<Map>
totalProjects += projects.size()
logger.info "Verarbeite Projekte ${startAt} bis ${startAt + projects.size() - 1} ..."
projects.each { proj ->
String projectKey = proj.key
String projectId = proj.id?.toString()
// Für jedes Projekt das zugeordnete Notification Scheme holen
def notifResp = get("/rest/api/3/project/${projectId}/notificationscheme")
.asObject(Map)
if (notifResp.status == 200) {
def schemeId = notifResp.body?.id
if (schemeId) {
Long idLong = (schemeId as Long)
def entry = schemeUsage[idLong]
if (entry) {
entry.used = true
entry.projects << projectKey
} else {
// Projekt nutzt ein Scheme, das nicht in der Liste war (sollte selten sein)
logger.warn "Projekt ${projectKey} nutzt Notification Scheme ID=${schemeId}, das nicht in der globalen Liste war."
}
}
} else if (notifResp.status == 404) {
// z.B. Team-managed-Projekte, die kein klassisches Notification Scheme haben
logger.debug "Projekt ${projectKey} hat kein klassisches Notification Scheme (404)."
} else {
logger.warn "Konnte Notification Scheme für Projekt ${projectKey} nicht laden: ${notifResp.status} - ${notifResp.body}"
}
}
int total = (body.total ?: totalProjects) as int
startAt += maxResults
if (startAt >= total) {
finished = true
}
}
// --- Schritt 3: Unbenutzte (und nicht ausgeschlossene) Schemes bestimmen -----
def unused = schemeUsage.values()
.findAll { entry ->
Long id = (entry.scheme.id as Long)
!entry.used && !EXCLUDED_IDS.contains(id)
}
.sort { it.scheme.name?.toString()?.toLowerCase() }
logger.info "Projekte insgesamt : ${totalProjects}"
logger.info "Ausgeschlossene Notification-IDs : ${EXCLUDED_IDS.join(', ')}"
logger.info "Unbenutzte Notification Schemes : ${unused.size()}"
// --- Schritt 4: Optional löschen (aktuell noch durch DRY_RUN geschützt) -----
List<Map> deleted = []
List<Map> failed = []
if (!DRY_RUN) {
unused.each { entry ->
def s = entry.scheme
Long id = (s.id as Long)
logger.info "Lösche Notification Scheme ID=${id}, Name=\"${s.name}\" ..."
def delResp = delete("/rest/api/3/notificationscheme/${id}")
.asString()
if (delResp.status in [200, 204]) {
logger.info "Erfolgreich gelöscht: ID=${id}, Name=\"${s.name}\""
deleted << [
id : id,
name : s.name,
description: s.description
]
} else {
logger.warn "Löschen fehlgeschlagen für ID=${id}, Name=\"${s.name}\": Status=${delResp.status}, Body=${delResp.body}"
failed << [
id : id,
name : s.name,
status : delResp.status,
body : delResp.body
]
}
}
} else {
logger.info "DRY_RUN = true -> Es wird NICHT gelöscht, nur Kandidaten ermittelt."
}
// --- Schritt 5: Zusammenfassung ---------------------------------------------
def lines = []
lines << "=== Notification Schemes Housekeeping (projektbasiert) ==="
lines << "DRY_RUN : ${DRY_RUN}"
lines << "Gesamt Notification Schemes : ${allSchemes.size()}"
lines << "Gesamt Projekte : ${totalProjects}"
lines << "Ausgeschlossene IDs : ${EXCLUDED_IDS.join(', ')}"
lines << "Kandidaten (unused) : ${unused.size()}"
if (!DRY_RUN) {
lines << "Gelöscht : ${deleted.size()}"
lines << "Fehlgeschlagen : ${failed.size()}"
}
lines << ""
lines << "Kandidaten (unbenutzte Schemes, exkl. EXCLUDED_IDS):"
unused.each { entry ->
def s = entry.scheme
lines << String.format(
"- ID=%s | Name=\"%s\" | Beschreibung=\"%s\" | Projekte=%s",
s.id,
s.name ?: "",
(s.description ?: "").replaceAll('\\s+', ' ').trim(),
entry.projects ?: []
)
}
def result = [
summary : [
dryRun : DRY_RUN,
totalNotificationSchemes: allSchemes.size(),
totalProjects : totalProjects,
excludedIDs : EXCLUDED_IDS,
candidateUnused : unused.size(),
deleted : deleted.size(),
failed : failed.size()
],
candidateUnusedSchemes: unused.collect { e ->
def s = e.scheme
[
id : s.id,
name : s.name,
description : s.description,
projectsUsing: e.projects
]
},
deletedNotificationSchemes: deleted,
failedDeletions : failed
]
logger.info lines.join("\n")
return lines.join("\n") + "\n\nJSON:\n" + JsonOutput.prettyPrint(JsonOutput.toJson(result))