initialer upload
This commit is contained in:
parent
c776ee7b40
commit
59bc58afb7
1
Automations/[CS] Initial Ticket Setup.json
Normal file
1
Automations/[CS] Initial Ticket Setup.json
Normal file
File diff suppressed because one or more lines are too long
1
Automations/[CS] Setze pds oder Partner Bearbeiter.json
Normal file
1
Automations/[CS] Setze pds oder Partner Bearbeiter.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,256 @@
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
logger.info("geht los ..")
|
||||
|
||||
// ============= KONFIG =============
|
||||
// Lauf
|
||||
boolean DRY_RUN = true
|
||||
int PAGE_SIZE = 50
|
||||
int CF_LIMIT = -1 // -1=alle, 10/100/... = erste n
|
||||
int SAMPLE_LIMIT = 10 // wie viele Kandidaten exemplarisch loggen
|
||||
|
||||
// Behandlung von "(migrated)"-Feldern im Namen (case-insensitive):
|
||||
// "off" = keine Sonderbehandlung
|
||||
// "strict" = löschen nur wenn (migrated) UND ungenutzt
|
||||
// "plus" = löschen wenn ungenutzt ODER Name enthält (migrated)
|
||||
// "only" = NUR (migrated)-Felder löschen (Nutzung egal)
|
||||
String MIGRATED_FILTER = "plus"
|
||||
|
||||
// Output-Drosselung // wie viele Kandidaten exemplarisch loggen
|
||||
boolean PRINT_JSON = false // JSON-Dump AUS (Payload sparen)
|
||||
boolean PRINT_CSV = false // CSV-Dump AUS
|
||||
int CSV_MAX_CHARS = 20000 // harte Kappung
|
||||
|
||||
// Whitelist
|
||||
Set<String> EXCLUDE_BY_NAME = [] as Set
|
||||
Set<String> EXCLUDE_BY_ID = [] as Set
|
||||
|
||||
// ============= Logging =============
|
||||
void logInfo(String m){ try{ logger.info(m) }catch(e){ println m } }
|
||||
void logWarn(String m){ try{ logger.warn(m) }catch(e){ println "WARN: " + m } }
|
||||
void logErr (String m){ try{ logger.error(m)}catch(e){ println "ERR: " + m } }
|
||||
|
||||
// ============= Utils =============
|
||||
boolean isMigratedName(String name){ name?.toLowerCase()?.contains("migrated") ?: false }
|
||||
|
||||
// Ruhiger HTTP-Getter: asString() + Slurper (unterdrückt 'Serializing object...' Logs)
|
||||
Map getAsMap(String path, Map q=[:]) {
|
||||
def req = get(path)
|
||||
q.each{ k,v -> req = req.queryString(k, v) }
|
||||
def resp = req.asString()
|
||||
if (!(resp.status in [200])) {
|
||||
throw new RuntimeException("GET ${path} failed: HTTP ${resp.status} :: ${resp.body}")
|
||||
}
|
||||
def text = (resp.body ?: "")
|
||||
if (!text) return [:]
|
||||
return (new JsonSlurper().parseText(text) ?: [:]) as Map
|
||||
}
|
||||
|
||||
List<Map> pagedGetValues(String path, int pageSize=50) {
|
||||
int startAt = 0; List<Map> all = []
|
||||
while (true) {
|
||||
Map body = getAsMap(path, [startAt:startAt, maxResults:pageSize])
|
||||
List vals = (body.values ?: []) as List
|
||||
all.addAll(vals as List<Map>)
|
||||
int total = (body.total ?: (startAt + vals.size())) as int
|
||||
int nextStart = startAt + ((body.maxResults ?: vals.size()) as int)
|
||||
if (vals.isEmpty() || nextStart >= total) break
|
||||
startAt = nextStart
|
||||
}
|
||||
all
|
||||
}
|
||||
|
||||
// ============= Fetch: Fields & Contexts =============
|
||||
List<Map> fetchCustomFields(int pageSize){
|
||||
logInfo("Lade Custom Fields…")
|
||||
List<Map> list = pagedGetValues("/rest/api/3/field/search?type=custom", pageSize)
|
||||
logInfo("Custom Fields gefunden: " + list.size())
|
||||
return list
|
||||
}
|
||||
Map fetchFieldContexts(String fieldId, int pageSize){
|
||||
Map ctx = getAsMap("/rest/api/3/field/${fieldId}/contexts", [startAt:0, maxResults:pageSize])
|
||||
return [contexts: ctx]
|
||||
}
|
||||
boolean hasAnyProjectAssociation(Map ctxBundle){
|
||||
Map ctx = (ctxBundle.contexts ?: [:]) as Map
|
||||
List<Map> values = (ctx.values ?: []) as List
|
||||
if (values.isEmpty()) return false
|
||||
for (Map c : values){
|
||||
Map scope = (c.scope ?: [:]) as Map
|
||||
String t = (scope.type ?: "").toString().toUpperCase()
|
||||
if (t == "GLOBAL") return true
|
||||
if (t == "PROJECT") {
|
||||
List pids = (scope.projectIds ?: []) as List
|
||||
if (pids != null && !pids.isEmpty()) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ============= Fetch: Screens on demand =============
|
||||
List<Map> fetchScreens(int pageSize){
|
||||
logInfo("Lade Screens…")
|
||||
List<Map> list = pagedGetValues("/rest/api/3/screens", pageSize)
|
||||
logInfo("Screens gefunden: " + list.size())
|
||||
return list
|
||||
}
|
||||
List<Map> fetchScreenTabs(Long screenId){
|
||||
Map body = getAsMap("/rest/api/3/screens/${screenId}/tabs", [startAt:0, maxResults:50])
|
||||
return (body.values ?: []) as List<Map>
|
||||
}
|
||||
List<Map> fetchScreenTabFields(Long screenId, Long tabId){
|
||||
Map body = getAsMap("/rest/api/3/screens/${screenId}/tabs/${tabId}/fields", [startAt:0, maxResults:200])
|
||||
return (body.values ?: []) as List<Map>
|
||||
}
|
||||
Map<String,Integer> scanScreensFor(Set<String> fieldIds, int pageSize){
|
||||
Map<String,Integer> useCount = [:].withDefault{0}
|
||||
if (fieldIds.isEmpty()) return useCount
|
||||
def screens = fetchScreens(pageSize)
|
||||
for (Map s : screens){
|
||||
Long sid = (s.id == null ? null : Long.valueOf(s.id.toString()))
|
||||
if (sid == null) continue
|
||||
try{
|
||||
for (Map t : fetchScreenTabs(sid)){
|
||||
Long tid = (t.id == null ? null : Long.valueOf(t.id.toString()))
|
||||
if (tid == null) continue
|
||||
for (Map sf : fetchScreenTabFields(sid, tid)){
|
||||
String fid = (sf.id ?: "").toString()
|
||||
if (fieldIds.contains(fid)) useCount[fid] = useCount[fid] + 1
|
||||
}
|
||||
}
|
||||
} catch (Exception ex){
|
||||
logWarn("Screenscan Fehler bei Screen ${sid}: " + ex.message)
|
||||
}
|
||||
}
|
||||
return useCount
|
||||
}
|
||||
|
||||
// ============= Delete =============
|
||||
boolean deleteCustomField(String fieldId, String name){
|
||||
def resp = delete("/rest/api/3/field/${fieldId}").asString()
|
||||
if (resp.status in [200,204]) { logInfo("Gelöscht: [${fieldId}] ${name}"); return true }
|
||||
logWarn("Nicht gelöscht [${fieldId}] ${name} :: HTTP ${resp.status} :: ${resp.body}")
|
||||
return false
|
||||
}
|
||||
|
||||
// ============= Output helpers =============
|
||||
void printSummary(List<Map> candidates, int totalChecked, String policy){
|
||||
int migrated = candidates.count{ it.migrated == true }
|
||||
int unused = candidates.count{ (it.onScreens == 0) && (it.hasProjectAssoc == false) }
|
||||
logWarn("Kandidaten: ${candidates.size()} (Policy=${policy}, geprüft=${totalChecked}) | migrated=${migrated}, unused=${unused}")
|
||||
}
|
||||
void printSample(List<Map> candidates, int limit){
|
||||
int n = Math.min(limit, candidates.size())
|
||||
if (n == 0) return
|
||||
logWarn("Beispiele (${n}/${candidates.size()}):")
|
||||
for (int i=0;i<n;i++){
|
||||
def c = candidates[i]
|
||||
String flags = (c.migrated ? "migrated" : "") + (((c.onScreens==0)&&!c.hasProjectAssoc) ? (c.migrated ? ", unused" : "unused") : "")
|
||||
logWarn(" - [${c.id}] ${c.name}" + (flags ? " ["+flags+"]" : ""))
|
||||
}
|
||||
}
|
||||
String toCSV(List<Map> items){
|
||||
String header = "id;name;type;migrated;onScreens;hasProjectAssoc\n"
|
||||
StringBuilder sb = new StringBuilder(header)
|
||||
items.each{ c ->
|
||||
sb.append("${c.id};\"${(c.name as String).replaceAll('\"','\"\"')}\";${c.type};${c.migrated};${c.onScreens};${c.hasProjectAssoc}\n")
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
// ============= MAIN =============
|
||||
void runCustomFieldCleanup(boolean dryRun, int pageSize, int cfLimit, String migratedPolicy,
|
||||
Set<String> excludeByName, Set<String> excludeById){
|
||||
|
||||
// 1) Felder (limitierbar)
|
||||
List<Map> allFields = fetchCustomFields(pageSize)
|
||||
List<Map> customFields = allFields.findAll{ (it.id ?: "").toString().startsWith("customfield_") }
|
||||
if (cfLimit > -1 && customFields.size() > cfLimit) {
|
||||
customFields = customFields.subList(0, cfLimit)
|
||||
logInfo("Begrenze Prüfung auf " + cfLimit + " Custom Fields.")
|
||||
}
|
||||
int totalChecked = customFields.size()
|
||||
|
||||
// 2) Kontexte → nur Kandidaten ohne Projekt-Scope weiter prüfen
|
||||
Map<String,Boolean> hasProjAssoc = [:].withDefault{false}
|
||||
List<Map> noScope = []
|
||||
for (Map f : customFields){
|
||||
String fid = (f.id ?: "").toString()
|
||||
if (!fid) continue
|
||||
try {
|
||||
Map bundle = fetchFieldContexts(fid, pageSize)
|
||||
boolean assoc = hasAnyProjectAssociation(bundle)
|
||||
hasProjAssoc[fid] = assoc
|
||||
if (!assoc) noScope << f
|
||||
} catch (Exception ex){
|
||||
hasProjAssoc[fid] = true
|
||||
logWarn("Kontexte für Feld ${fid} nicht vollständig lesbar: " + ex.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Nur für noScope die Screen-Verwendung prüfen
|
||||
Set<String> forScreenScan = noScope.collect{ (it.id ?: "").toString() } as Set<String>
|
||||
Map<String,Integer> useOnScreens = scanScreensFor(forScreenScan, pageSize)
|
||||
|
||||
// 4) Kandidaten bestimmen (MIGRATED_POLICY)
|
||||
List<Map> candidates = []
|
||||
for (Map f : customFields){
|
||||
String fid = (f.id ?: "").toString()
|
||||
String fname = (f.name ?: "").toString()
|
||||
if (excludeById.contains(fid) || excludeByName.contains(fname)) continue
|
||||
|
||||
boolean migrated = isMigratedName(fname)
|
||||
int onScreens = useOnScreens.containsKey(fid) ? useOnScreens[fid] : 0
|
||||
boolean assoc = hasProjAssoc[fid]
|
||||
boolean unused = (onScreens == 0 && !assoc)
|
||||
|
||||
boolean pick
|
||||
switch (migratedPolicy?.toLowerCase()){
|
||||
case "off": pick = unused; break
|
||||
case "strict": pick = migrated && unused; break
|
||||
case "plus": pick = unused || migrated; break
|
||||
case "only": pick = migrated; break
|
||||
default: pick = unused
|
||||
}
|
||||
if (pick){
|
||||
candidates << [id: fid, name: fname, type: (f.schema?.type ?: f.type ?: "custom"),
|
||||
migrated: migrated, onScreens: onScreens, hasProjectAssoc: assoc]
|
||||
}
|
||||
}
|
||||
|
||||
candidates.sort{ a,b -> a.name <=> b.name }
|
||||
printSummary(candidates, totalChecked, migratedPolicy)
|
||||
//printSample(candidates, SAMPLE_LIMIT)
|
||||
|
||||
if (PRINT_JSON) {
|
||||
def json = JsonOutput.prettyPrint(JsonOutput.toJson(candidates))
|
||||
logInfo("JSON (gekürzt) Länge=" + json.length())
|
||||
logInfo(json.substring(0, Math.min(json.length(), 20000)))
|
||||
}
|
||||
if (PRINT_CSV) {
|
||||
String csv = toCSV(candidates)
|
||||
if (csv.length() > CSV_MAX_CHARS) {
|
||||
logInfo("CSV gekappt von ${csv.length()} auf ${CSV_MAX_CHARS} Zeichen.")
|
||||
csv = csv.substring(0, CSV_MAX_CHARS)
|
||||
}
|
||||
logInfo("CSV (gekürzt):\n" + csv)
|
||||
}
|
||||
|
||||
if (dryRun) { logInfo("Dry-Run aktiv → nichts gelöscht."); return }
|
||||
|
||||
int deleted=0, skipped=0
|
||||
for (Map c : candidates){
|
||||
try {
|
||||
if (deleteCustomField(c.id.toString(), c.name.toString())) deleted++ else skipped++
|
||||
} catch (Exception ex){
|
||||
skipped++; logErr("Fehler beim Löschen [${c.id}] ${c.name} :: " + ex.message)
|
||||
}
|
||||
}
|
||||
logWarn("Fertig. Ergebnis: deleted=${deleted}, skipped=${skipped}")
|
||||
}
|
||||
|
||||
// ---- Start ----
|
||||
runCustomFieldCleanup(DRY_RUN, PAGE_SIZE, CF_LIMIT, MIGRATED_FILTER, EXCLUDE_BY_NAME, EXCLUDE_BY_ID)
|
||||
|
||||
|
||||
@ -0,0 +1,115 @@
|
||||
import groovy.json.JsonOutput
|
||||
|
||||
// ------------------ Konfig ------------------
|
||||
boolean DRY_RUN = true
|
||||
int PAGE_SIZE = 50
|
||||
Set<String> EXCLUDE_BY_NAME = [] as Set // optional Namen schützen
|
||||
|
||||
// ------------------ Logging -----------------
|
||||
void logInfo(String m){ try{ logger.info(m) }catch(e){ println m } }
|
||||
void logWarn(String m){ try{ logger.warn(m) }catch(e){ println "WARN: " + m } }
|
||||
void logErr (String m){ try{ logger.error(m)}catch(e){ println "ERR: " + m } }
|
||||
|
||||
// ------------------ HTTP Helpers -----------
|
||||
Map getAsMap(String path, Map q=[:]) {
|
||||
def req = get(path)
|
||||
q.each{ k,v -> req = req.queryString(k, v) }
|
||||
def r = req.asObject(Map)
|
||||
if (r.status != 200) throw new RuntimeException("GET " + path + " failed: HTTP " + r.status + " :: " + r.body)
|
||||
(r.body ?: [:]) as Map
|
||||
}
|
||||
List<Map> pagedGetValues(String path, int pageSize=50) {
|
||||
int startAt = 0; List<Map> all = []
|
||||
while (true) {
|
||||
Map body = getAsMap(path, [startAt:startAt, maxResults:pageSize])
|
||||
List vals = (body.values ?: []) as List
|
||||
all.addAll(vals as List<Map>)
|
||||
int total = (body.total ?: (startAt + vals.size())) as int
|
||||
int nextStart = startAt + ((body.maxResults ?: vals.size()) as int)
|
||||
if (vals.isEmpty() || nextStart >= total) break
|
||||
startAt = nextStart
|
||||
}
|
||||
all
|
||||
}
|
||||
|
||||
// ------------------ Fetchers ----------------
|
||||
List<Map> fetchITSS(int pageSize){
|
||||
logInfo("Lade Issue Type Screen Schemes…")
|
||||
def list = pagedGetValues("/rest/api/3/issuetypescreenscheme", pageSize)
|
||||
logInfo("ITSS gefunden: " + list.size())
|
||||
list
|
||||
}
|
||||
List<Map> fetchProjects(int pageSize){
|
||||
logInfo("Lade Projekte…")
|
||||
def list = pagedGetValues("/rest/api/3/project/search", pageSize)
|
||||
logInfo("Projekte gefunden: " + list.size())
|
||||
list
|
||||
}
|
||||
Long fetchITSSForProject(Long projectId){
|
||||
// Liefert die ITSS-ID, die einem Projekt zugewiesen ist
|
||||
def m = getAsMap("/rest/api/3/project/${projectId}/issuetypescreenscheme")
|
||||
def id = m.get("issueTypeScreenSchemeId")
|
||||
return (id == null ? null : Long.valueOf(id.toString()))
|
||||
}
|
||||
|
||||
// ------------------ Delete ------------------
|
||||
boolean deleteITSS(long id, String name){
|
||||
def resp = delete("/rest/api/3/issuetypescreenscheme/${id}").asString()
|
||||
if (resp.status in [200,204]) { logInfo("Gelöscht: [${id}] ${name}"); return true }
|
||||
logWarn("Nicht gelöscht [${id}] ${name} :: HTTP ${resp.status} :: ${resp.body}")
|
||||
false
|
||||
}
|
||||
|
||||
// ------------------ Main --------------------
|
||||
void runITSSCleanup(boolean dryRun, int pageSize, Set<String> excludeByName){
|
||||
def itss = fetchITSS(pageSize)
|
||||
if (itss.isEmpty()){ logInfo("Keine ITSS vorhanden – nichts zu tun."); return }
|
||||
Map<Long,Map> itssById = [:]
|
||||
itss.each{ Map x -> if (x.id!=null) itssById[Long.valueOf(x.id.toString())] = x }
|
||||
|
||||
def projects = fetchProjects(pageSize)
|
||||
Set<Long> referenced = new LinkedHashSet<>()
|
||||
projects.each{ Map p ->
|
||||
def pid = p.get("id"); if (pid==null) return
|
||||
try {
|
||||
Long ref = fetchITSSForProject(Long.valueOf(pid.toString()))
|
||||
if (ref!=null) referenced << ref
|
||||
} catch (Exception ex) {
|
||||
logWarn("ITSS-Mapping für Projekt ${p.key ?: pid} nicht lesbar: " + ex.message)
|
||||
}
|
||||
}
|
||||
logInfo("Referenzierte ITSS gesamt: " + referenced.size())
|
||||
|
||||
List<Map> candidates = []
|
||||
itssById.each{ Long id, Map row ->
|
||||
String name = (row.name ?: "") as String
|
||||
if (!referenced.contains(id) && !excludeByName.contains(name)){
|
||||
candidates << [id:id, name:name, description:(row.description ?: "")]
|
||||
}
|
||||
}
|
||||
candidates.sort{ a,b -> a.name <=> b.name }
|
||||
|
||||
if (candidates.isEmpty()){ logInfo("Keine ungenutzten ITSS gefunden. ✅"); return }
|
||||
|
||||
logWarn("Ungenutzte ITSS (${candidates.size()}):")
|
||||
candidates.each{ c -> logWarn(" - [${c.id}] ${c.name}") }
|
||||
|
||||
if (dryRun){
|
||||
logInfo("Dry-Run aktiv → nichts gelöscht.")
|
||||
logInfo("JSON:\n" + JsonOutput.prettyPrint(JsonOutput.toJson(candidates)))
|
||||
return
|
||||
}
|
||||
|
||||
int deleted=0, skipped=0
|
||||
candidates.each{ c ->
|
||||
try {
|
||||
if (deleteITSS((c.id as Long), c.name.toString())) deleted++ else skipped++
|
||||
} catch (Exception ex){
|
||||
skipped++; logErr("Fehler beim Löschen [${c.id}] ${c.name} :: " + ex.message)
|
||||
}
|
||||
}
|
||||
logWarn("Fertig. Ergebnis: deleted=${deleted}, skipped=${skipped}")
|
||||
}
|
||||
|
||||
// ---- Start ----
|
||||
runITSSCleanup(DRY_RUN, PAGE_SIZE, EXCLUDE_BY_NAME)
|
||||
@ -0,0 +1,224 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// Housekeeping: Inaktive Workflow Schemes (ohne Projektzuordnung) löschen
|
||||
// Jira Cloud - ScriptRunner Console
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
|
||||
// --- Konfiguration -----------------------------------------------------------
|
||||
|
||||
// false -> inaktive Workflow Schemes (ohne EXCLUDED_IDS) werden GELÖSCHT
|
||||
// true -> nur Testlauf, es wird NICHT gelöscht
|
||||
final boolean DRY_RUN = true
|
||||
|
||||
// Workflow-Scheme-IDs, die NIEMALS gelöscht werden sollen
|
||||
// (z.B. Default-System-Schema; ID bitte ggf. anpassen/ergänzen)
|
||||
final Set<Long> EXCLUDED_IDS = [10000L] as Set
|
||||
|
||||
// --- Schritt 1: Alle Workflow Schemes laden (paginiert) ---------------------
|
||||
|
||||
List<Map> allSchemes = []
|
||||
int startAt = 0
|
||||
int maxResults = 50
|
||||
boolean finished = false
|
||||
|
||||
while (!finished) {
|
||||
def resp = get("/rest/api/3/workflowscheme?startAt=${startAt}&maxResults=${maxResults}")
|
||||
.asObject(Map)
|
||||
|
||||
if (resp.status != 200) {
|
||||
logger.error("Konnte Workflow Schemes nicht laden (startAt=${startAt}): ${resp.status} - ${resp.body}")
|
||||
return "Fehler beim Laden der Workflow 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 "Workflow 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 Workflow 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: Mappings Workflow Scheme <-> Projekte laden -----------------
|
||||
//
|
||||
// GET /rest/api/3/workflowscheme/project
|
||||
// liefert PageBean mit values[ { workflowSchemeId, projectId, projectKey, ... } ]
|
||||
|
||||
List<Map> allMappings = []
|
||||
startAt = 0
|
||||
finished = false
|
||||
|
||||
while (!finished) {
|
||||
def resp = get("/rest/api/3/workflowscheme/project?startAt=${startAt}&maxResults=${maxResults}")
|
||||
.asObject(Map)
|
||||
|
||||
if (resp.status != 200) {
|
||||
logger.error("Konnte Workflow-Scheme-Mappings nicht laden (startAt=${startAt}): ${resp.status} - ${resp.body}")
|
||||
break
|
||||
}
|
||||
|
||||
def body = resp.body ?: [:]
|
||||
def values = (body.values ?: []) as List<Map>
|
||||
|
||||
allMappings.addAll(values)
|
||||
|
||||
boolean isLast = (body.isLast == true)
|
||||
int total = (body.total ?: (startAt + values.size())) as int
|
||||
|
||||
logger.info "Workflow-Scheme-Mappings geladen: ${allMappings.size()} (total ~ ${total}), isLast=${isLast}"
|
||||
|
||||
if (isLast || values.isEmpty()) {
|
||||
finished = true
|
||||
} else {
|
||||
startAt += maxResults
|
||||
if (startAt >= total) {
|
||||
finished = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mappings in schemeUsage eintragen
|
||||
allMappings.each { m ->
|
||||
Long schemeId = (m.workflowSchemeId as Long)
|
||||
String projKey = m.projectKey?.toString()
|
||||
|
||||
def entry = schemeUsage[schemeId]
|
||||
if (entry) {
|
||||
entry.used = true
|
||||
if (projKey) {
|
||||
entry.projects << projKey
|
||||
}
|
||||
} else {
|
||||
logger.warn "Mapping gefunden für Workflow Scheme ID=${schemeId}, das nicht in allSchemes war. Projekt=${projKey}"
|
||||
}
|
||||
}
|
||||
|
||||
def projectsWithWorkflowScheme = allMappings.collect { it.projectKey }.findAll { it }.toSet()
|
||||
logger.info "Anzahl Projekte mit Workflow Scheme: ${projectsWithWorkflowScheme.size()}"
|
||||
|
||||
// --- Schritt 3: Inaktive (unbenutzte & nicht ausgeschlossene) Schemes -------
|
||||
|
||||
def inactive = 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 "Ausgeschlossene Workflow-Scheme-IDs : ${EXCLUDED_IDS.join(', ')}"
|
||||
logger.info "Inaktive Workflow Schemes (Kandidaten): ${inactive.size()}"
|
||||
|
||||
// --- Schritt 4: Optional löschen --------------------------------------------
|
||||
|
||||
List<Map> deleted = []
|
||||
List<Map> failed = []
|
||||
|
||||
if (!DRY_RUN) {
|
||||
inactive.each { entry ->
|
||||
def s = entry.scheme
|
||||
Long id = (s.id as Long)
|
||||
|
||||
logger.info "Lösche Workflow Scheme ID=${id}, Name=\"${s.name}\" ..."
|
||||
|
||||
def delResp = delete("/rest/api/3/workflowscheme/${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 << "=== Workflow Schemes Housekeeping ==="
|
||||
lines << "DRY_RUN : ${DRY_RUN}"
|
||||
lines << "Gesamt Workflow Schemes : ${allSchemes.size()}"
|
||||
lines << "Projekte mit Scheme-Mapping : ${projectsWithWorkflowScheme.size()}"
|
||||
lines << "Ausgeschlossene IDs : ${EXCLUDED_IDS.join(', ')}"
|
||||
lines << "Inaktive Kandidaten : ${inactive.size()}"
|
||||
if (!DRY_RUN) {
|
||||
lines << "Gelöscht : ${deleted.size()}"
|
||||
lines << "Fehlgeschlagen : ${failed.size()}"
|
||||
}
|
||||
lines << ""
|
||||
lines << "Inaktive (unbenutzte) Schemes, exkl. EXCLUDED_IDS:"
|
||||
inactive.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,
|
||||
totalWorkflowSchemes : allSchemes.size(),
|
||||
projectsWithMapping : projectsWithWorkflowScheme.size(),
|
||||
excludedIDs : EXCLUDED_IDS,
|
||||
inactiveCandidates : inactive.size(),
|
||||
deleted : deleted.size(),
|
||||
failed : failed.size()
|
||||
],
|
||||
inactiveWorkflowSchemes: inactive.collect { e ->
|
||||
def s = e.scheme
|
||||
[
|
||||
id : s.id,
|
||||
name : s.name,
|
||||
description : s.description,
|
||||
projectsUsing: e.projects
|
||||
]
|
||||
},
|
||||
deletedWorkflowSchemes: deleted,
|
||||
failedDeletions : failed
|
||||
]
|
||||
|
||||
logger.info lines.join("\n")
|
||||
|
||||
return lines.join("\n") + "\n\nJSON:\n" + JsonOutput.prettyPrint(JsonOutput.toJson(result))
|
||||
227
Console - Maintenance/Unused Notification Schemes cleaner.groovy
Normal file
227
Console - Maintenance/Unused Notification Schemes cleaner.groovy
Normal file
@ -0,0 +1,227 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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))
|
||||
135
Console - Maintenance/Unused Screen Schemes cleaner.groovy
Normal file
135
Console - Maintenance/Unused Screen Schemes cleaner.groovy
Normal file
@ -0,0 +1,135 @@
|
||||
import groovy.json.JsonOutput
|
||||
|
||||
// -------------------------- Konfiguration --------------------------
|
||||
boolean DRY_RUN = true // erst prüfen; auf false stellen, wenn die Kandidatenliste stimmt
|
||||
int PAGE_SIZE = 50
|
||||
|
||||
Set<String> EXCLUDE_BY_NAME = [
|
||||
'Default Screen Scheme'
|
||||
] as Set
|
||||
|
||||
// -------------------------- Logging -------------------------------
|
||||
void logInfo(String msg){ try { logger.info(msg) } catch(e){ println msg } }
|
||||
void logWarn(String msg){ try { logger.warn(msg) } catch(e){ println "WARN: " + msg } }
|
||||
void logErr (String msg){ try { logger.error(msg)} catch(e){ println "ERR: " + msg } }
|
||||
|
||||
// -------------------------- HTTP Helpers --------------------------
|
||||
Map getAsMap(String path, Map<String,Object> q=[:]) {
|
||||
def req = get(path)
|
||||
q.each { k,v -> req = req.queryString(k, v) }
|
||||
def resp = req.asObject(Map)
|
||||
if (resp.status != 200) {
|
||||
throw new RuntimeException("GET " + path + " failed: HTTP " + resp.status + " :: " + resp.body)
|
||||
}
|
||||
return (resp.body ?: [:]) as Map
|
||||
}
|
||||
|
||||
List<Map> pagedGetValues(String path, int pageSize) {
|
||||
int startAt = 0
|
||||
List<Map> all = []
|
||||
while (true) {
|
||||
Map body = getAsMap(path, [startAt: startAt, maxResults: pageSize])
|
||||
List vals = (body.values ?: []) as List
|
||||
all.addAll(vals as List<Map>)
|
||||
int total = (body.total ?: (startAt + vals.size())) as int
|
||||
int nextStart = startAt + ((body.maxResults ?: vals.size()) as int)
|
||||
if (vals.isEmpty() || nextStart >= total) break
|
||||
startAt = nextStart
|
||||
}
|
||||
return all
|
||||
}
|
||||
|
||||
// -------------------------- Fetchers ------------------------------
|
||||
List<Map> fetchScreenSchemes(int pageSize) {
|
||||
logInfo("Lade Screen Schemes…")
|
||||
List<Map> list = pagedGetValues("/rest/api/3/screenscheme", pageSize)
|
||||
logInfo("Screen Schemes gefunden: " + list.size())
|
||||
return list
|
||||
}
|
||||
|
||||
List<Map> fetchIssueTypeScreenSchemes(int pageSize) {
|
||||
logInfo("Lade Issue Type Screen Schemes…")
|
||||
List<Map> list = pagedGetValues("/rest/api/3/issuetypescreenscheme", pageSize)
|
||||
logInfo("Issue Type Screen Schemes gefunden: " + list.size())
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt alle Referenzen auf Screen Schemes:
|
||||
* - defaultScreenSchemeId je ITSS
|
||||
* - globale ITSS→IssueType→ScreenScheme Mappings aus /issuetypescreenscheme/mapping
|
||||
*/
|
||||
Set<Long> fetchReferencedScreenSchemeIds(int pageSize, List<Map> itssList) {
|
||||
Set<Long> refs = new LinkedHashSet<>()
|
||||
|
||||
// 1) Default-Zuordnungen je ITSS
|
||||
for (Map itss : itssList) {
|
||||
def defId = itss.get("defaultScreenSchemeId")
|
||||
if (defId != null) refs.add(Long.valueOf(defId.toString()))
|
||||
}
|
||||
|
||||
// 2) Globale Mappings
|
||||
logInfo("Lade globale IssueType→ScreenScheme Mappings…")
|
||||
List<Map> globalMaps = pagedGetValues("/rest/api/3/issuetypescreenscheme/mapping", pageSize)
|
||||
for (Map m : globalMaps) {
|
||||
def ssId = m.get("screenSchemeId")
|
||||
if (ssId != null) refs.add(Long.valueOf(ssId.toString()))
|
||||
}
|
||||
logInfo("Referenzierte Screen Schemes gesamt: " + refs.size())
|
||||
return refs
|
||||
}
|
||||
|
||||
// -------------------------- Delete -------------------------------
|
||||
boolean deleteScreenScheme(long id, String name) {
|
||||
def resp = delete("/rest/api/3/screenscheme/" + id).asString()
|
||||
if (resp.status in [200,204]) {
|
||||
logInfo("Gelöscht: [" + id + "] " + name)
|
||||
return true
|
||||
}
|
||||
logWarn("Nicht gelöscht [" + id + "] " + name + " :: HTTP " + resp.status + " :: " + resp.body)
|
||||
return false
|
||||
}
|
||||
|
||||
// -------------------------- Main -------------------------------
|
||||
void runCleanup(boolean dryRun, int pageSize, Set<String> excludeByName) {
|
||||
List<Map> screenSchemes = fetchScreenSchemes(pageSize)
|
||||
if (screenSchemes.isEmpty()) { logInfo("Keine Screen Schemes vorhanden – nichts zu tun."); return }
|
||||
|
||||
List<Map> itssList = fetchIssueTypeScreenSchemes(pageSize)
|
||||
Set<Long> referenced = fetchReferencedScreenSchemeIds(pageSize, itssList)
|
||||
|
||||
List<Map> candidates = []
|
||||
for (Map s : screenSchemes) {
|
||||
long id = Long.valueOf(s.get("id").toString())
|
||||
String name = (s.get("name") ?: "") as String
|
||||
if (!referenced.contains(id) && !excludeByName.contains(name)) {
|
||||
candidates.add([id: id, name: name, description: (s.get("description") ?: "")])
|
||||
}
|
||||
}
|
||||
candidates.sort { a, b -> a.name <=> b.name }
|
||||
|
||||
if (candidates.isEmpty()) { logInfo("Keine ungenutzten Screen Schemes gefunden. ✅"); return }
|
||||
|
||||
logWarn("Ungenutzte Kandidaten (" + candidates.size() + "):")
|
||||
for (Map c : candidates) logWarn(" - [" + c.id + "] " + c.name)
|
||||
|
||||
if (dryRun) {
|
||||
logInfo("Dry-Run aktiv → nichts gelöscht.")
|
||||
logInfo("JSON Dump:\n" + JsonOutput.prettyPrint(JsonOutput.toJson(candidates)))
|
||||
return
|
||||
}
|
||||
|
||||
int deleted = 0, skipped = 0
|
||||
for (Map c : candidates) {
|
||||
try {
|
||||
if (deleteScreenScheme((c.id as Long), c.name.toString())) deleted++ else skipped++
|
||||
} catch (Exception ex) {
|
||||
skipped++
|
||||
logErr("Fehler beim Löschen [" + c.id + "] " + c.name + " :: " + ex.message)
|
||||
}
|
||||
}
|
||||
logWarn("Fertig. Ergebnis: deleted=" + deleted + ", skipped=" + skipped)
|
||||
}
|
||||
|
||||
// ---- Start ----
|
||||
runCleanup(DRY_RUN, PAGE_SIZE, EXCLUDE_BY_NAME)
|
||||
194
Console - Maintenance/Unused permission Schemes cleaner.groovy
Normal file
194
Console - Maintenance/Unused permission Schemes cleaner.groovy
Normal file
@ -0,0 +1,194 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// Housekeeping: Unbenutzte Berechtigungsschemata finden UND löschen
|
||||
// Jira Cloud - ScriptRunner Console
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
|
||||
// --- Konfiguration -----------------------------------------------------------
|
||||
|
||||
// 🔥 Wenn true -> nur Testlauf, nichts wird gelöscht.
|
||||
// 🔥 Wenn false -> unbenutzte Schemas (ohne EXCLUDED_IDS) werden gelöscht.
|
||||
final boolean DRY_RUN = true
|
||||
|
||||
// IDs, die niemals gelöscht werden sollen (z. B. Default/System-Schemata)
|
||||
final Set<Long> EXCLUDED_IDS = [0] as Set // bei Bedarf ergänzen, z.B. 10000L etc.
|
||||
|
||||
// --- Schritt 1: Alle Berechtigungsschemata holen ----------------------------
|
||||
|
||||
def schemesResp = get("/rest/api/3/permissionscheme").asObject(Map)
|
||||
|
||||
if (schemesResp.status != 200) {
|
||||
logger.error("Konnte Berechtigungsschemata nicht laden: ${schemesResp.status} - ${schemesResp.body}")
|
||||
return "Fehler beim Laden der Berechtigungsschemata. Siehe Log."
|
||||
}
|
||||
|
||||
def schemes = schemesResp.body?.permissionSchemes ?: []
|
||||
logger.info "Anzahl Berechtigungsschemata insgesamt: ${schemes.size()}"
|
||||
|
||||
// Map: schemeId -> [scheme: <Objekt>, used: boolean, projects: [keys]]
|
||||
def schemeUsage = schemes.collectEntries { scheme ->
|
||||
def id = (scheme.id ?: scheme["id"]) as Long
|
||||
[
|
||||
(id): [
|
||||
scheme : scheme,
|
||||
used : false,
|
||||
projects: []
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
// --- Schritt 2: Alle Projekte holen & Permission-Scheme je Projekt ermitteln --
|
||||
|
||||
int startAt = 0
|
||||
int maxResults = 50
|
||||
int totalProjects = 0
|
||||
boolean 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 ?: []
|
||||
totalProjects += projects.size()
|
||||
|
||||
projects.each { proj ->
|
||||
def projectKey = proj.key
|
||||
def projectId = proj.id
|
||||
|
||||
def permResp = get("/rest/api/3/project/${projectId}/permissionscheme")
|
||||
.asObject(Map)
|
||||
|
||||
if (permResp.status == 200) {
|
||||
def schemeId = permResp.body?.id
|
||||
if (schemeId) {
|
||||
def idLong = (schemeId as Long)
|
||||
def entry = schemeUsage[idLong]
|
||||
if (entry) {
|
||||
entry.used = true
|
||||
entry.projects << projectKey
|
||||
} else {
|
||||
logger.warn "Projekt ${projectKey} nutzt Berechtigungsschema ${schemeId}, das nicht in der globalen Liste war."
|
||||
}
|
||||
}
|
||||
} else if (permResp.status == 404) {
|
||||
// Team-managed-Projekte -> haben kein klassisches Permission Scheme
|
||||
} else {
|
||||
logger.warn "Konnte Permission Scheme für Projekt ${projectKey} nicht laden: ${permResp.status}"
|
||||
}
|
||||
}
|
||||
|
||||
int total = (body.total ?: totalProjects) as int
|
||||
startAt += maxResults
|
||||
if (startAt >= total) {
|
||||
finished = true
|
||||
}
|
||||
}
|
||||
|
||||
// --- Schritt 3: Unbenutzte (und nicht ausgeschlossene) Schemata bestimmen ---
|
||||
|
||||
def unused = schemeUsage.values()
|
||||
.findAll { entry ->
|
||||
def id = (entry.scheme.id ?: 0L) as Long
|
||||
!entry.used && !EXCLUDED_IDS.contains(id)
|
||||
}
|
||||
.sort { it.scheme.name?.toString()?.toLowerCase() }
|
||||
|
||||
logger.info "Projekte insgesamt : ${totalProjects}"
|
||||
logger.info "Ausgeschlossene Schema-IDs : ${EXCLUDED_IDS.join(', ')}"
|
||||
logger.info "Unbenutzte Schemata (Kandidaten): ${unused.size()}"
|
||||
|
||||
// --- Schritt 4: Optional löschen --------------------------------------------
|
||||
|
||||
def deleted = []
|
||||
def failed = []
|
||||
|
||||
if (!DRY_RUN) {
|
||||
unused.each { entry ->
|
||||
def s = entry.scheme
|
||||
def id = (s.id as Long)
|
||||
|
||||
logger.info "Lösche Berechtigungsschema ID=${id}, Name=\"${s.name}\" ..."
|
||||
|
||||
def delResp = delete("/rest/api/3/permissionscheme/${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 nichts gelöscht, nur Kandidaten ermittelt."
|
||||
}
|
||||
|
||||
// --- Schritt 5: Zusammenfassung zurückgeben ---------------------------------
|
||||
|
||||
def lines = []
|
||||
lines << "=== Berechtigungsschemata Housekeeping ==="
|
||||
lines << "DRY_RUN : ${DRY_RUN}"
|
||||
lines << "Gesamt-Schemata : ${schemes.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 Schemas, 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,
|
||||
totalSchemes : schemes.size(),
|
||||
totalProjects : totalProjects,
|
||||
excludedIDs : EXCLUDED_IDS,
|
||||
candidateUnused : unused.size(),
|
||||
deleted : deleted.size(),
|
||||
failed : failed.size()
|
||||
],
|
||||
deletedPermissionSchemes: deleted,
|
||||
failedDeletions : failed,
|
||||
candidateUnusedSchemes : unused.collect { e ->
|
||||
def s = e.scheme
|
||||
[
|
||||
id : s.id,
|
||||
name : s.name,
|
||||
description : s.description,
|
||||
projectsUsing: e.projects
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
logger.info lines.join("\n")
|
||||
|
||||
return lines.join("\n") + "\n\nJSON:\n" + JsonOutput.prettyPrint(JsonOutput.toJson(result))
|
||||
32
Postfunctions/Herstellersupport ablehnen.groovy
Normal file
32
Postfunctions/Herstellersupport ablehnen.groovy
Normal file
@ -0,0 +1,32 @@
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Postfunction: Setzt LabelManager-Feld Partneranfrage auf Partneranfrage-Abgelehnt
|
||||
// ---------------------------------------------------------------------------------
|
||||
|
||||
// ID deines Label-Manager-Feldes
|
||||
final LABEL_MANAGER_CF_ID = 'customfield_11037'
|
||||
|
||||
// Welchen Eintrag setzen?
|
||||
final SELECTED_LABEL = 'Partneranfrage-Abgelehnt'
|
||||
final LABEL_COLOR = 'red' // oder "red", "green", etc.
|
||||
final LABEL_SOURCE = 'project' // weil eine projekt-spezifische Konfig verwendet wird
|
||||
|
||||
// Issue-Key aus dem Workflow-Kontext
|
||||
def issueKey = issue.key
|
||||
|
||||
def response = put("/rest/api/3/issue/${issueKey}")
|
||||
.header('Content-Type', 'application/json')
|
||||
.body([
|
||||
fields: [
|
||||
// wichtig: Feld-ID als Schlüssel, Value ist ein Objekt mit labels/colors/labelSource
|
||||
(LABEL_MANAGER_CF_ID): [
|
||||
labels : [SELECTED_LABEL],
|
||||
colors : [LABEL_COLOR],
|
||||
labelSource: LABEL_SOURCE
|
||||
]
|
||||
]
|
||||
])
|
||||
.asString()
|
||||
|
||||
if (response.status != 204) {
|
||||
logger.warn "Label-Manager-Feld konnte nicht aktualisiert werden: ${response.status} - ${response.body}"
|
||||
}
|
||||
32
Postfunctions/Herstellersupport anfragen.groovy
Normal file
32
Postfunctions/Herstellersupport anfragen.groovy
Normal file
@ -0,0 +1,32 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// Postfunction: Setzt LabelManager-Feld Partneranfrage auf Partneranfrage-Check
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// ID deines Label-Manager-Feldes
|
||||
final LABEL_MANAGER_CF_ID = 'customfield_11037'
|
||||
|
||||
// Welchen Eintrag setzen?
|
||||
final SELECTED_LABEL = 'Partneranfrage-Check'
|
||||
final LABEL_COLOR = 'yellow' // oder "red", "green", etc.
|
||||
final LABEL_SOURCE = 'project' // weil eine projekt-spezifische Konfig verwendet wird
|
||||
|
||||
// Issue-Key aus dem Workflow-Kontext
|
||||
def issueKey = issue.key
|
||||
|
||||
def response = put("/rest/api/3/issue/${issueKey}")
|
||||
.header('Content-Type', 'application/json')
|
||||
.body([
|
||||
fields: [
|
||||
// wichtig: Feld-ID als Schlüssel, Value ist ein Objekt mit labels/colors/labelSource
|
||||
(LABEL_MANAGER_CF_ID): [
|
||||
labels : [SELECTED_LABEL],
|
||||
colors : [LABEL_COLOR],
|
||||
labelSource: LABEL_SOURCE
|
||||
]
|
||||
]
|
||||
])
|
||||
.asString()
|
||||
|
||||
if (response.status != 204) {
|
||||
logger.warn "Label-Manager-Feld konnte nicht aktualisiert werden: ${response.status} - ${response.body}"
|
||||
}
|
||||
72
Postfunctions/Herstellersupport annehmen.groovy
Normal file
72
Postfunctions/Herstellersupport annehmen.groovy
Normal file
@ -0,0 +1,72 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// Postfunction: Setzt LabelManager-Feld, Security-Level, Dropdown-Wert,
|
||||
// leert ein Multi-User-Feld und setzt ein anderes Multi-User-Feld auf den Assignee
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// LabelManager-Konfiguration
|
||||
final LABEL_MANAGER_CF_ID = 'customfield_11037'
|
||||
final SELECTED_LABEL = 'Partneranfrage-Angenommen'
|
||||
final LABEL_COLOR = 'green'
|
||||
final LABEL_SOURCE = 'project'
|
||||
|
||||
// Security-Level "CS"
|
||||
final SECURITY_LEVEL_ID = '10125'
|
||||
|
||||
// Dropdown-Feld (Single Select)
|
||||
final DROPDOWN_CF_ID = 'customfield_10039'
|
||||
final DROPDOWN_VALUE = 'Customer Service'
|
||||
|
||||
// Multi-User-Felder
|
||||
final USERFIELD_CLEAR_ID = 'customfield_10054' // Partner Bearbeiter wird geleert
|
||||
final USERFIELD_SET_ID = 'customfield_10036' // pds Bearbeiter wird auf Assignee gesetzt
|
||||
|
||||
// Issue-Key aus dem Workflow-Kontext
|
||||
def issueKey = issue.key
|
||||
def assignee = issue.fields.assignee
|
||||
|
||||
// Wenn kein Assignee vorhanden ist, Log-Eintrag
|
||||
if (!assignee) {
|
||||
logger.warn "Kein Assignee für ${issueKey} vorhanden – ${USERFIELD_SET_ID} bleibt leer."
|
||||
}
|
||||
|
||||
// Request-Body zusammensetzen
|
||||
def payload = [
|
||||
fields: [
|
||||
// LabelManager-Feld
|
||||
(LABEL_MANAGER_CF_ID): [
|
||||
labels : [SELECTED_LABEL],
|
||||
colors : [LABEL_COLOR],
|
||||
labelSource: LABEL_SOURCE
|
||||
],
|
||||
|
||||
// Security-Level
|
||||
security: [
|
||||
id: SECURITY_LEVEL_ID
|
||||
],
|
||||
|
||||
// Dropdown-Feld (Single Select)
|
||||
(DROPDOWN_CF_ID): [
|
||||
value: DROPDOWN_VALUE
|
||||
],
|
||||
|
||||
// Multi-User-Feld leeren
|
||||
(USERFIELD_CLEAR_ID): [],
|
||||
|
||||
// Multi-User-Feld auf aktuellen Assignee setzen (falls vorhanden)
|
||||
(USERFIELD_SET_ID): assignee ? [[ accountId: assignee.accountId ]] : []
|
||||
]
|
||||
]
|
||||
|
||||
// PUT-Request an Jira REST API
|
||||
def response = put("/rest/api/3/issue/${issueKey}")
|
||||
.header('Content-Type', 'application/json')
|
||||
.body(payload)
|
||||
.asString()
|
||||
|
||||
// Erfolg / Fehler protokollieren
|
||||
if (response.status == 204) {
|
||||
logger.info "Felder erfolgreich aktualisiert für ${issueKey}: Label='${SELECTED_LABEL}', Security=CS, Dropdown='${DROPDOWN_VALUE}', " +
|
||||
"${USERFIELD_CLEAR_ID} geleert, ${USERFIELD_SET_ID} auf Assignee gesetzt."
|
||||
} else {
|
||||
logger.warn "Update fehlgeschlagen (${response.status}) für ${issueKey}: ${response.body}"
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
/*
|
||||
Workflow: CS: Workflow 4 SD
|
||||
Transition: Ticket lösen
|
||||
*/
|
||||
|
||||
|
||||
import java.time.LocalDate
|
||||
|
||||
// aktuelles Datum + 2 Tage
|
||||
def newDueDate = LocalDate.now().plusDays(2).toString() // yyyy-MM-dd
|
||||
|
||||
// nur setzen, wenn noch nicht vorhanden
|
||||
if (!issue["fields"]["duedate"]) {
|
||||
def resp = put("/rest/api/3/issue/${issue["key"]}")
|
||||
.header("Content-Type", "application/json")
|
||||
.body([
|
||||
fields: [
|
||||
duedate: newDueDate
|
||||
]
|
||||
])
|
||||
.asObject(Map)
|
||||
|
||||
assert resp.status == 204 : "Update fehlgeschlagen: ${resp.status} - ${resp.body}"
|
||||
}
|
||||
119
Scheduled Jobs/Schließe Tickets aus dem Status Resolved.groovy
Normal file
119
Scheduled Jobs/Schließe Tickets aus dem Status Resolved.groovy
Normal file
@ -0,0 +1,119 @@
|
||||
// ScriptRunner Cloud - Scheduled Job
|
||||
// 1) Issues per JQL suchen (/rest/api/3/search/jql + nextPageToken)
|
||||
// 2) Transition nach "Closed" (per ID ODER Ziel-Status ODER Aktionsname)
|
||||
// 3) DueDate leeren
|
||||
// 4) Kommentar (ADF) hinzufügen
|
||||
|
||||
// ===================== Konfiguration =====================
|
||||
def JQL = 'project = "Customer Service Desk" AND status = Resolved AND duedate IS NOT EMPTY AND due <= endOfDay()'
|
||||
def MAX_PER_PAGE = 50
|
||||
|
||||
// Wenn du die Transition-ID kennst, HIER setzen (als String). Beispiel: "331" für "Schließen".
|
||||
// Wenn null/leer, wird via Name gesucht (erst Ziel-Status, dann Aktionsname).
|
||||
final String TRANSITION_ID = "331" // <- bei dir vorhanden; sonst auf null setzen
|
||||
final String TARGET_STATUS_NAME = "Closed" // Ziel-Statusname
|
||||
final List<String> ACTION_NAME_FALLBACKS = ["Schließen", "Close", "Closed"] // Aktionsnamen-Fallbacks
|
||||
|
||||
// Kommentartext (als ADF gesendet)
|
||||
def commentText = """
|
||||
Dieses Ticket wurde automatisch geschlossen, nachdem der Support die Lösung präsentiert hat und wir davon ausgehen, dass die Lösung korrekt ist.
|
||||
Sollten weiterhin Fragen bestehen oder erneut Unterstützung benötigt werden, können Sie eine neue Anfrage stellen.
|
||||
Ihr pds Support
|
||||
""".trim()
|
||||
// ========================================================
|
||||
|
||||
def adfBody = [
|
||||
type: "doc",
|
||||
version: 1,
|
||||
content: [[ type: "paragraph", content: [[ type: "text", text: commentText ]] ]]
|
||||
]
|
||||
|
||||
// --- Helper: Transition-ID bestimmen ---
|
||||
def resolveTransitionId = { String issueKey ->
|
||||
if (TRANSITION_ID?.trim()) {
|
||||
return TRANSITION_ID.trim()
|
||||
}
|
||||
def resp = get("/rest/api/3/issue/${issueKey}/transitions").asObject(Map)
|
||||
assert resp.status == 200 : "Transitions nicht lesbar für ${issueKey}: ${resp.status} - ${resp.body}"
|
||||
|
||||
def transitions = (resp.body?.transitions as List) ?: []
|
||||
|
||||
// 1) nach Ziel-Status (to.name)
|
||||
def byTargetStatus = transitions.find { it?.to?.name?.equalsIgnoreCase(TARGET_STATUS_NAME) }
|
||||
if (byTargetStatus?.id) return byTargetStatus.id as String
|
||||
|
||||
// 2) nach Aktionsname (name)
|
||||
def byActionName = transitions.find { t ->
|
||||
ACTION_NAME_FALLBACKS.any { fn -> t?.name?.equalsIgnoreCase(fn) }
|
||||
}
|
||||
return byActionName?.id as String
|
||||
}
|
||||
|
||||
// --- Helper: Transition ausführen ---
|
||||
def doTransition = { String issueKey, String transitionId ->
|
||||
def resp = post("/rest/api/3/issue/${issueKey}/transitions")
|
||||
.header("Content-Type", "application/json")
|
||||
.body([ transition: [ id: transitionId ] ])
|
||||
.asString()
|
||||
assert resp.status == 204 : "Transition fehlgeschlagen für ${issueKey}: ${resp.status} - ${resp.body}"
|
||||
}
|
||||
|
||||
// --- Helper: DueDate leeren ---
|
||||
def clearDueDate = { String issueKey ->
|
||||
def resp = put("/rest/api/3/issue/${issueKey}")
|
||||
.header("Content-Type", "application/json")
|
||||
.body([ fields: [ duedate: null ] ])
|
||||
.asString()
|
||||
assert resp.status == 204 : "DueDate leeren fehlgeschlagen für ${issueKey}: ${resp.status} - ${resp.body}"
|
||||
}
|
||||
|
||||
// --- Helper: Kommentar hinzufügen (ADF) ---
|
||||
def addComment = { String issueKey, Map adf ->
|
||||
def resp = post("/rest/api/3/issue/${issueKey}/comment")
|
||||
.header("Content-Type", "application/json")
|
||||
.body([ body: adf ])
|
||||
.asObject(Map)
|
||||
assert resp.status == 201 : "Kommentar fehlgeschlagen für ${issueKey}: ${resp.status} - ${resp.body}"
|
||||
}
|
||||
|
||||
// --- Suche mit neuer API und Token-Pagination ---
|
||||
def processed = 0
|
||||
String nextPageToken = null
|
||||
boolean isLast = false
|
||||
|
||||
while (!isLast) {
|
||||
def req = get("/rest/api/3/search/jql")
|
||||
.queryString("jql", JQL)
|
||||
.queryString("fields", "status,duedate") // key ist immer dabei
|
||||
.queryString("maxResults", MAX_PER_PAGE as String)
|
||||
if (nextPageToken) req = req.queryString("nextPageToken", nextPageToken)
|
||||
|
||||
def searchResp = req.asObject(Map)
|
||||
assert searchResp.status == 200 : "Search-Fehler: ${searchResp.status} - ${searchResp.body}"
|
||||
|
||||
def body = searchResp.body as Map
|
||||
def issues = (body?.issues as List) ?: []
|
||||
isLast = (body?.isLast == true)
|
||||
nextPageToken = body?.nextPageToken as String
|
||||
|
||||
issues.each { Map iss ->
|
||||
def key = iss["key"] as String
|
||||
try {
|
||||
def transitionId = resolveTransitionId(key)
|
||||
if (!transitionId) {
|
||||
logger.warn("Keine passende Transition für ${key} gefunden (Status='${TARGET_STATUS_NAME}', Fallbacks=${ACTION_NAME_FALLBACKS}) – übersprungen.")
|
||||
return
|
||||
}
|
||||
doTransition(key, transitionId)
|
||||
clearDueDate(key)
|
||||
addComment(key, adfBody)
|
||||
|
||||
processed++
|
||||
logger.info("OK: ${key} -> Transition=${transitionId}, DueDate geleert, Kommentar gesetzt.")
|
||||
} catch (Throwable t) {
|
||||
logger.error("Fehler bei ${key}: ${t.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Fertig. Verarbeitete Tickets: ${processed} (JQL: ${JQL})")
|
||||
46
Scheduled Jobs/close_resolved_issues.drawio
Normal file
46
Scheduled Jobs/close_resolved_issues.drawio
Normal file
@ -0,0 +1,46 @@
|
||||
<mxfile host="app.diagrams.net" modified="2025-11-06T00:00:00.000Z" agent="GPT-5" version="22.0.8">
|
||||
<diagram name="JSM Auto Close 48h" id="autoCloseFlow">
|
||||
<mxGraphModel dx="1250" dy="800" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="1200">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="2" value="Scheduled Job startet (z. B. täglich)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;" vertex="1" parent="1">
|
||||
<mxGeometry x="200" y="50" width="300" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="API-Call: /rest/api/3/search/jql - Status = Resolved - resolutiondate 48–72 h" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;fontSize=13;" vertex="1" parent="1">
|
||||
<mxGeometry x="200" y="150" width="300" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="Für jedes Ticket" style="shape=rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
|
||||
<mxGeometry x="220" y="270" width="260" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="Transition-ID ermitteln (über ID, Statusname oder Aktionsname)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=13;" vertex="1" parent="1">
|
||||
<mxGeometry x="200" y="380" width="300" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6" value="Transition zu Closed (POST /issue/{key}/transitions)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;" vertex="1" parent="1">
|
||||
<mxGeometry x="200" y="490" width="300" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="DueDate löschen (PUT /issue/{key})" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=13;" vertex="1" parent="1">
|
||||
<mxGeometry x="200" y="600" width="300" height="70" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="Kommentar hinzufügen (POST /issue/{key}/comment)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;" vertex="1" parent="1">
|
||||
<mxGeometry x="200" y="700" width="300" height="70" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="9" value="Log schreiben (OK / Fehler)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;" vertex="1" parent="1">
|
||||
<mxGeometry x="200" y="800" width="300" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="Nächste Seite (Pagination) oder Ende" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#666666;fontSize=13;" vertex="1" parent="1">
|
||||
<mxGeometry x="200" y="900" width="300" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<!-- Verbindungen -->
|
||||
<mxCell id="11" edge="1" source="2" target="3" style="endArrow=block;rounded=1;strokeWidth=1.5;" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="12" edge="1" source="3" target="4" style="endArrow=block;rounded=1;strokeWidth=1.5;" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="13" edge="1" source="4" target="5" style="endArrow=block;rounded=1;strokeWidth=1.5;" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="14" edge="1" source="5" target="6" style="endArrow=block;rounded=1;strokeWidth=1.5;" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="15" edge="1" source="6" target="7" style="endArrow=block;rounded=1;strokeWidth=1.5;" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="16" edge="1" source="7" target="8" style="endArrow=block;rounded=1;strokeWidth=1.5;" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="17" edge="1" source="8" target="9" style="endArrow=block;rounded=1;strokeWidth=1.5;" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
<mxCell id="18" edge="1" source="9" target="10" style="endArrow=block;rounded=1;strokeWidth=1.5;" parent="1"><mxGeometry relative="1" as="geometry"/></mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
Loading…
Reference in New Issue
Block a user