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