Jira-Scripte/Console - Maintenance/08. Unused Workflow cleaner.groovy

164 lines
4.9 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.

import java.net.URLEncoder
/**
* Workflow Housekeeping (Cloud) FIX: workflowSchemes.values korrekt auswerten
* ----------------------------------------------------------------------------
* Search: GET /rest/api/3/workflow/search -> values[].id.{name,entityId}
* Usage: GET /rest/api/3/workflow/{workflowId}/workflowSchemes
* Delete: DEL /rest/api/3/workflow/{workflowId}
*
* WICHTIG: Usage-Response hat workflowSchemes.values (nicht body.values).
*/
def DRY_RUN = true // <<< für "hart löschen" auf false
def PROTECTED_WORKFLOW_ENTITY_IDS = [
// z.B. "ec4480b2-623a-4b9b-78c0-2af0d15196ff" // classic default workflow
]
def PROTECTED_NAME_PATTERNS = [
"classic"
]
def PAGE_SIZE = 50
def USAGE_PAGE_SIZE = 50
logger.info("=== Workflow Housekeeping (FIX usage parsing) ===")
logger.info("Protected workflow entityIds: ${PROTECTED_WORKFLOW_ENTITY_IDS}")
logger.info("Protected name patterns: ${PROTECTED_NAME_PATTERNS}")
logger.info("DRY_RUN: ${DRY_RUN}")
def isNameProtected = { String name ->
def n = (name ?: "").toLowerCase()
PROTECTED_NAME_PATTERNS.any { p -> n.contains((p ?: "").toLowerCase()) }
}
def isEntityIdProtected = { String entityId ->
PROTECTED_WORKFLOW_ENTITY_IDS.any { it?.toString() == entityId?.toString() }
}
// Encode für Pfadsegmente, damit auch "Builds Workflow" kein URI-Problem macht
def encPath = { String s ->
URLEncoder.encode(s ?: "", "UTF-8").replace("+", "%20")
}
/**
* Usage korrekt lesen:
* body.workflowSchemes.values
* body.workflowSchemes.nextPageToken
*
* Doku: GET /rest/api/3/workflow/{workflowId}/workflowSchemes :contentReference[oaicite:1]{index=1}
*/
def getWorkflowSchemeUsageCount = { String workflowId ->
int count = 0
String token = null
while (true) {
def url = "/rest/api/3/workflow/${encPath(workflowId)}/workflowSchemes?maxResults=${USAGE_PAGE_SIZE}" +
(token ? "&nextPageToken=${URLEncoder.encode(token, 'UTF-8')}" : "")
def resp = get(url).asObject(Map)
if (resp.status != 200) {
return [failed: true, status: resp.status, body: resp.body, count: count]
}
def body = resp.body ?: [:]
def ws = body?.workflowSchemes ?: [:]
def values = ws?.values ?: []
count += values.size()
token = ws?.nextPageToken
if (!token) break
}
return [failed: false, count: count]
}
// 1) Workflows holen (dein JSON: values[].id.{name,entityId})
def raw = []
def startAt = 0
while (true) {
def resp = get("/rest/api/3/workflow/search?startAt=${startAt}&maxResults=${PAGE_SIZE}")
.asObject(Map)
if (resp.status != 200) {
logger.error("ERROR|WF_SEARCH_FAILED|status=${resp.status}|body=${resp.body}")
return
}
def values = resp.body?.values ?: []
raw.addAll(values)
def isLast = resp.body?.isLast
if (isLast == true || values.isEmpty()) break
startAt += PAGE_SIZE
}
logger.info("INFO|TOTAL_ITEMS_FROM_SEARCH|${raw.size()}")
def workflows = raw.collect { wf ->
def idObj = wf?.id
def name = (idObj instanceof Map) ? idObj?.name?.toString() : null
def entityId = (idObj instanceof Map) ? idObj?.entityId?.toString() : null
return [name: name, entityId: entityId]
}.findAll { it.entityId && it.name }
logger.info("INFO|REAL_WORKFLOWS|${workflows.size()}")
logger.info("INFO|SKIPPED_ITEMS_NO_NAME_OR_ENTITYID|${raw.size() - workflows.size()}")
// 2) Auswertung + optional Delete
def deleted = 0
def keptProtected = 0
def keptUsed = 0
def keptUsageLookupFailed = 0
def deleteFailures = 0
def candidates = 0
workflows.each { wf ->
def name = wf.name
def entityId = wf.entityId
if (isEntityIdProtected(entityId) || isNameProtected(name)) {
keptProtected++
logger.info("KEEP|entityId=${entityId}|name=${name}|reason=PROTECTED")
return
}
def usage = getWorkflowSchemeUsageCount(entityId)
if (usage.failed) {
keptUsageLookupFailed++
logger.info("KEEP|entityId=${entityId}|name=${name}|reason=USAGE_LOOKUP_FAILED|status=${usage.status}")
return
}
if (usage.count > 0) {
keptUsed++
logger.info("KEEP|entityId=${entityId}|name=${name}|reason=USED_BY_SCHEMES|usedBySchemes=${usage.count}")
return
}
candidates++
logger.info("DEL?|entityId=${entityId}|name=${name}|reason=UNUSED_NO_SCHEME_ASSOC")
if (!DRY_RUN) {
def delResp = delete("/rest/api/3/workflow/${encPath(entityId)}").asString()
if (delResp.status == 204) {
deleted++
logger.info("DEL|OK|entityId=${entityId}|name=${name}")
} else {
deleteFailures++
logger.error("DEL|FAIL|entityId=${entityId}|name=${name}|status=${delResp.status}|body=${delResp.body}")
}
}
}
logger.info("=== SUMMARY ===")
logger.info("Real workflows processed: ${workflows.size()}")
logger.info("Delete candidates: ${candidates}")
logger.info("Deleted: ${deleted}")
logger.info("Kept (protected): ${keptProtected}")
logger.info("Kept (used by schemes): ${keptUsed}")
logger.info("Kept (usage lookup failed): ${keptUsageLookupFailed}")
logger.info("Delete failures: ${deleteFailures}")
logger.info("=== DONE ===")