164 lines
4.9 KiB
Groovy
164 lines
4.9 KiB
Groovy
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 ===")
|