From 59bc58afb715430e123ba12f6ed8fafc6bb7ddd6 Mon Sep 17 00:00:00 2001 From: Martin Date: Sun, 14 Dec 2025 19:17:45 +0100 Subject: [PATCH] initialer upload --- Automations/[CS] Initial Ticket Setup.json | 1 + ...CS] Setze pds oder Partner Bearbeiter.json | 1 + ...zugefügt wurde → Status aktualisieren.json | 1 + ... (kein Screen & kein Projekt-Kontext).groovy | 256 ++++++++++++++++++ ...ype Screen Schemes – Unused Cleaner.groovy | 115 ++++++++ ... (inactiv) Workflow Schemes cleaner.groovy | 224 +++++++++++++++ ...Unused Notification Schemes cleaner.groovy | 227 ++++++++++++++++ .../Unused Screen Schemes cleaner.groovy | 135 +++++++++ .../Unused permission Schemes cleaner.groovy | 194 +++++++++++++ .../Herstellersupport ablehnen.groovy | 32 +++ .../Herstellersupport anfragen.groovy | 32 +++ .../Herstellersupport annehmen.groovy | 72 +++++ ... Schließen (benötigt für Auto-Close).groovy | 24 ++ ...eße Tickets aus dem Status Resolved.groovy | 119 ++++++++ Scheduled Jobs/close_resolved_issues.drawio | 46 ++++ 15 files changed, 1479 insertions(+) create mode 100644 Automations/[CS] Initial Ticket Setup.json create mode 100644 Automations/[CS] Setze pds oder Partner Bearbeiter.json create mode 100644 Automations/[CS] Wenn ein Kommentar hinzugefügt wurde → Status aktualisieren.json create mode 100644 Console - Maintenance/Custom Fields – Unused Cleaner (kein Screen & kein Projekt-Kontext).groovy create mode 100644 Console - Maintenance/Issue Type Screen Schemes – Unused Cleaner.groovy create mode 100644 Console - Maintenance/Unused (inactiv) Workflow Schemes cleaner.groovy create mode 100644 Console - Maintenance/Unused Notification Schemes cleaner.groovy create mode 100644 Console - Maintenance/Unused Screen Schemes cleaner.groovy create mode 100644 Console - Maintenance/Unused permission Schemes cleaner.groovy create mode 100644 Postfunctions/Herstellersupport ablehnen.groovy create mode 100644 Postfunctions/Herstellersupport anfragen.groovy create mode 100644 Postfunctions/Herstellersupport annehmen.groovy create mode 100644 Postfunctions/Setze Fälligkeit beim Schließen (benötigt für Auto-Close).groovy create mode 100644 Scheduled Jobs/Schließe Tickets aus dem Status Resolved.groovy create mode 100644 Scheduled Jobs/close_resolved_issues.drawio diff --git a/Automations/[CS] Initial Ticket Setup.json b/Automations/[CS] Initial Ticket Setup.json new file mode 100644 index 0000000..b1b0e7a --- /dev/null +++ b/Automations/[CS] Initial Ticket Setup.json @@ -0,0 +1 @@ +{"cloud":true,"rules":[{"id":2793380,"clientKey":"d938d60f-6f1a-38fa-b0cc-68cfc2a01a24","name":"[CS] Initial Ticket Setup","state":"ENABLED","description":"Wenn ein Ticket erstellt wird, werden Assetfelder und Security-Level gesetzt.","authorAccountId":"5c094a465e2eee35d79f8e39","actor":{"type":"ACCOUNT_ID","value":"557058:f58131cb-b67d-43c7-b30d-6b58d40bd077"},"created":1760435373695,"updated":1760523588979,"trigger":{"id":"58253c67-e61c-4061-b6dc-27e922e2eb3f","component":"TRIGGER","parentId":null,"conditionParentId":null,"schemaVersion":1,"type":"jira.issue.event.trigger:created","value":{"eventKey":"jira:issue_created","issueEvent":"issue_created","eventFilters":["ari:cloud:jira:4c80f24d-3b7c-4757-95bb-1040fe2a9fe0:project/10065"]},"children":[],"conditions":[],"connectionId":null},"components":[{"id":"cc43b18f-2a64-44ee-8fa8-f6d2b83f2b0a","component":"CONDITION","parentId":null,"conditionParentId":null,"schemaVersion":1,"type":"jira.jql.condition","value":"project = CSD","children":[],"conditions":[],"connectionId":null},{"id":"ad474dd0-343c-42ae-a058-a1a5b4f54d6a","component":"CONDITION","parentId":null,"conditionParentId":null,"schemaVersion":1,"type":"jira.condition.container.block","value":null,"children":[{"id":"2933cbaf-2e38-4511-8340-875682106bf1","component":"CONDITION_BLOCK","parentId":"ad474dd0-343c-42ae-a058-a1a5b4f54d6a","conditionParentId":null,"schemaVersion":1,"type":"jira.condition.if.block","value":{"conditionMatchType":"ALL"},"children":[{"id":"d7400fca-c5e7-4964-ab6c-28cce74e3c97","component":"ACTION","parentId":"2933cbaf-2e38-4511-8340-875682106bf1","conditionParentId":null,"schemaVersion":1,"type":"codebarrel.action.log","value":"Start Customer issue creation ({{creator.displayName}})","children":[],"conditions":[],"connectionId":null},{"id":"5fd5e241-ecff-4ff1-8347-5b244c4efe72","component":"ACTION","parentId":"2933cbaf-2e38-4511-8340-875682106bf1","conditionParentId":null,"schemaVersion":1,"type":"cmdb.lookup.objects","value":{"workspaceId":"db32e8e0-b481-4e24-9c40-ac7423593e9d","schemaId":"5","customSmartValue":{"id":"_customsmartvalue_id_1750156183968","name":{"type":"FREE","value":"lookupObjects"},"type":"IQL","query":{"type":"SMART","value":"objectType = \"SupportUser\" AND \"E-Mail\" = \"{{creator.emailAddress}}\""},"lazy":false}},"children":[],"conditions":[],"connectionId":null},{"id":"e178bdad-708d-451d-80d9-8718d45db8a9","component":"ACTION","parentId":"2933cbaf-2e38-4511-8340-875682106bf1","conditionParentId":null,"schemaVersion":12,"type":"jira.issue.edit","value":{"operations":[{"field":{"type":"NAME","value":"SupportUser"},"fieldType":"com.atlassian.jira.plugins.cmdb:cmdb-object-cftype","type":"SET","value":"objectType = SupportUser AND Key = {{lookupObjects.get(0).Key}}"}],"advancedFields":null,"sendNotifications":true},"children":[],"conditions":[],"connectionId":null},{"id":"90c50426-48cc-4452-a4b1-ca50f1788081","component":"ACTION","parentId":"2933cbaf-2e38-4511-8340-875682106bf1","conditionParentId":null,"schemaVersion":12,"type":"jira.issue.edit","value":{"operations":[{"field":{"type":"NAME","value":"Request participants"},"fieldType":"com.atlassian.servicedesk:sd-request-participants","type":"SET","value":[{"type":"SMART","value":"{{creator.emailAddress}}"}]}],"advancedFields":null,"sendNotifications":true},"children":[],"conditions":[],"connectionId":null},{"id":"e815eb82-78fa-4370-ab61-b6a8cf530798","component":"ACTION","parentId":"2933cbaf-2e38-4511-8340-875682106bf1","conditionParentId":null,"schemaVersion":1,"type":"cmdb.lookup.objects","value":{"workspaceId":"db32e8e0-b481-4e24-9c40-ac7423593e9d","schemaId":"5","customSmartValue":{"id":"_customsmartvalue_id_1750166808133","name":{"type":"FREE","value":"lookupObjects"},"type":"IQL","query":{"type":"SMART","value":"object HAVING inboundReferences(Key = {{lookupObjects.get(0).Key}})"},"lazy":false}},"children":[],"conditions":[],"connectionId":null},{"id":"81268914-bfb8-4ea4-bd5d-6864a6ec5aa9","component":"ACTION","parentId":"2933cbaf-2e38-4511-8340-875682106bf1","conditionParentId":null,"schemaVersion":12,"type":"jira.issue.edit","value":{"operations":[{"field":{"type":"NAME","value":"Kunde"},"fieldType":"com.atlassian.jira.plugins.cmdb:cmdb-object-cftype","type":"SET","value":"objectType = Kunde AND Key = {{lookupObjects.get(0).Key}}"}],"advancedFields":null,"sendNotifications":true},"children":[],"conditions":[],"connectionId":null},{"id":"afc9f794-fe22-474e-aeb0-5f2bca5840d8","component":"ACTION","parentId":"2933cbaf-2e38-4511-8340-875682106bf1","conditionParentId":null,"schemaVersion":1,"type":"cmdb.lookup.objects","value":{"workspaceId":"db32e8e0-b481-4e24-9c40-ac7423593e9d","schemaId":"5","customSmartValue":{"id":"_customsmartvalue_id_1750167219164","name":{"type":"FREE","value":"lookupObjects"},"type":"IQL","query":{"type":"SMART","value":"object HAVING inboundReferences(Key = {{lookupObjects.get(0).Key}})"},"lazy":false}},"children":[],"conditions":[],"connectionId":null},{"id":"897cb9de-3ed3-4d1a-a3c2-b28dd09c1abe","component":"ACTION","parentId":"2933cbaf-2e38-4511-8340-875682106bf1","conditionParentId":null,"schemaVersion":12,"type":"jira.issue.edit","value":{"operations":[{"field":{"type":"NAME","value":"Partner"},"fieldType":"com.atlassian.jira.plugins.cmdb:cmdb-object-cftype","type":"SET","value":"objectType = Partner AND Key = {{lookupObjects.get(0).Key}}"}],"advancedFields":null,"sendNotifications":true},"children":[],"conditions":[],"connectionId":null},{"id":"8557bbb4-784b-493c-b54e-87424e943e8c","component":"ACTION","parentId":"2933cbaf-2e38-4511-8340-875682106bf1","conditionParentId":null,"schemaVersion":1,"type":"codebarrel.action.log","value":"Ende Ticketerstellung durch Customer","children":[],"conditions":[],"connectionId":null}],"conditions":[{"id":"e93a0d96-256d-4939-90f0-2975391b87a2","component":"CONDITION","parentId":null,"conditionParentId":"2933cbaf-2e38-4511-8340-875682106bf1","schemaVersion":6,"type":"jira.user.condition","value":{"conditions":[{"field":"reporter","check":"IS_CUSTOMER","criteria":[]},{"field":"reporter","check":"NOT_IN_ROLE","criteria":[{"type":"NAME","value":"Partner"}]},{"field":"reporter","check":"NOT_IN_ROLE","criteria":[{"type":"NAME","value":"Service Desk Team"}]}],"operator":"AND"},"children":[],"conditions":[],"connectionId":null}],"connectionId":null},{"id":"26f65274-32ae-49da-99e9-2719a9fd828b","component":"CONDITION_BLOCK","parentId":"ad474dd0-343c-42ae-a058-a1a5b4f54d6a","conditionParentId":null,"schemaVersion":1,"type":"jira.condition.if.block","value":{"conditionMatchType":"ANY"},"children":[{"id":"27fead8d-2dc1-41d2-92be-826de08bebfa","component":"ACTION","parentId":"26f65274-32ae-49da-99e9-2719a9fd828b","conditionParentId":null,"schemaVersion":1,"type":"codebarrel.action.log","value":"Setze Partner","children":[],"conditions":[],"connectionId":null},{"id":"2afa431e-51e9-4757-8479-7ac4107f08d3","component":"ACTION","parentId":"26f65274-32ae-49da-99e9-2719a9fd828b","conditionParentId":null,"schemaVersion":1,"type":"jira.issue.refresh.issue","value":{"delayMs":null},"children":[],"conditions":[],"connectionId":null},{"id":"7f25114d-de00-4459-b8d5-3eb1e00c840e","component":"ACTION","parentId":"26f65274-32ae-49da-99e9-2719a9fd828b","conditionParentId":null,"schemaVersion":1,"type":"cmdb.lookup.objects","value":{"workspaceId":"db32e8e0-b481-4e24-9c40-ac7423593e9d","schemaId":"5","customSmartValue":{"id":"_customsmartvalue_id_1750170768955","name":{"type":"FREE","value":"lookupObjects"},"type":"IQL","query":{"type":"SMART","value":"object HAVING inboundReferences(objectType = Kunde AND Name = \"{{issue.customfield_10210.label}}\")"},"lazy":false}},"children":[],"conditions":[],"connectionId":null},{"id":"99bd2278-af7a-4906-bb63-ff43bdead8e0","component":"ACTION","parentId":"26f65274-32ae-49da-99e9-2719a9fd828b","conditionParentId":null,"schemaVersion":12,"type":"jira.issue.edit","value":{"operations":[{"field":{"type":"NAME","value":"Partner"},"fieldType":"com.atlassian.jira.plugins.cmdb:cmdb-object-cftype","type":"SET","value":"objectType = Partner AND Key = {{lookupObjects.get(0).Key}}"}],"advancedFields":null,"sendNotifications":true},"children":[],"conditions":[],"connectionId":null},{"id":"f55d837e-d78e-4b07-9498-ac3d005a73f1","component":"ACTION","parentId":"26f65274-32ae-49da-99e9-2719a9fd828b","conditionParentId":null,"schemaVersion":1,"type":"cmdb.lookup.objects","value":{"workspaceId":"db32e8e0-b481-4e24-9c40-ac7423593e9d","schemaId":"5","customSmartValue":{"id":"_customsmartvalue_id_1760449850745","name":{"type":"FREE","value":"lookupObjects"},"type":"IQL","query":{"type":"SMART","value":"objecttype = SupportUser AND Nachname = \"{{issue.customfield_10209.label}}\""},"lazy":false}},"children":[],"conditions":[],"connectionId":null},{"id":"0dfaf99c-2022-47fa-af92-7e9cdbccb5bb","component":"ACTION","parentId":"26f65274-32ae-49da-99e9-2719a9fd828b","conditionParentId":null,"schemaVersion":12,"type":"jira.issue.edit","value":{"operations":[{"field":{"type":"NAME","value":"Request participants"},"fieldType":"com.atlassian.servicedesk:sd-request-participants","type":"SET","value":[{"type":"SMART","value":"{{lookupObjects.get(0).E-Mail}}"}]}],"advancedFields":null,"sendNotifications":true},"children":[],"conditions":[],"connectionId":null},{"id":"0e943ede-8f7a-4fce-b068-a99245245355","component":"ACTION","parentId":"26f65274-32ae-49da-99e9-2719a9fd828b","conditionParentId":null,"schemaVersion":1,"type":"codebarrel.action.log","value":"Ende Partner setzen --- mail: {{lookupObjects.get(0).E-Mail}}","children":[],"conditions":[],"connectionId":null}],"conditions":[{"id":"dd95ce3e-61fa-472c-b3c6-891835f0111b","component":"CONDITION","parentId":null,"conditionParentId":"26f65274-32ae-49da-99e9-2719a9fd828b","schemaVersion":6,"type":"jira.user.condition","value":{"conditions":[{"field":"reporter","check":"IN_ROLE","criteria":[{"type":"NAME","value":"Service Desk Team"}]},{"field":"reporter","check":"IN_ROLE","criteria":[{"type":"NAME","value":"Partner"}]}],"operator":"OR"},"children":[],"conditions":[],"connectionId":null}],"connectionId":null}],"conditions":[],"connectionId":null},{"id":"db5cacaf-d492-4dfd-8f61-dc47f0e80524","component":"ACTION","parentId":null,"conditionParentId":null,"schemaVersion":1,"type":"jira.issue.refresh.issue","value":{"delayMs":null},"children":[],"conditions":[],"connectionId":null},{"id":"549b3ba3-2a06-4aaf-8ed1-1daf7ce796d3","component":"ACTION","parentId":null,"conditionParentId":null,"schemaVersion":1,"type":"codebarrel.action.log","value":"Setze Security - Level für Partner {{customfield_10211.label}}","children":[],"conditions":[],"connectionId":null},{"id":"f695c390-8608-489b-8095-af23b56adf3e","component":"ACTION","parentId":null,"conditionParentId":null,"schemaVersion":1,"type":"jira.create.variable","value":{"id":"_customsmartvalue_id_1760523087377","name":{"type":"FREE","value":"pName"},"type":"SMART","query":{"type":"SMART","value":"{{customfield_10211.label}}"},"lazy":false},"children":[],"conditions":[],"connectionId":null},{"id":"17daa670-e8b9-42a4-8ca1-49369aa6e4e4","component":"ACTION","parentId":null,"conditionParentId":null,"schemaVersion":1,"type":"jira.create.mapping-variable","value":{"name":{"type":"FREE","value":"seclevel"},"mappings":[{"key":"Bleckmann","value":"PRTNBLECK"},{"key":"CP Artis","value":"PRTNCP"},{"key":"G & N","value":"PRTNGN"},{"key":"Hahn","value":"PRTNHAHN"},{"key":"Hamann","value":"PRTNHAMN"},{"key":"Hold & Spada","value":"PRTNHS"},{"key":"hup-si","value":"PRTNHUP"},{"key":"ITSM","value":"PRTNITSM"},{"key":"Minerva","value":"PRTNPDS"},{"key":"Novis","value":"PRTNPDS"}]},"children":[],"conditions":[],"connectionId":null},{"id":"a87237fd-c94e-4950-a8e3-0afcc4f2fb28","component":"ACTION","parentId":null,"conditionParentId":null,"schemaVersion":1,"type":"codebarrel.action.log","value":"Var = {{pName}} -- Sec = {{seclevel.get(pName)}}","children":[],"conditions":[],"connectionId":null},{"id":"bca78314-b7b4-4fb8-8de7-848b6545aa98","component":"ACTION","parentId":null,"conditionParentId":null,"schemaVersion":12,"type":"jira.issue.edit","value":{"operations":[],"advancedFields":"{\n \"fields\": {\n \"security\": {\n \"name\": \"{{seclevel.get(pName)}}\"\n }\n }\n}","sendNotifications":false},"children":[],"conditions":[],"connectionId":null}],"canOtherRuleTrigger":false,"notifyOnError":"FIRSTERROR","projects":[],"labels":[],"tags":[{"id":4228798,"ruleIdUuid":"0199e220-0680-7938-a23f-4151fcc9b87e","tagType":"IS_RULE_UPDATED","tagValue":"true"}],"ruleScope":{"resources":["ari:cloud:jira:4c80f24d-3b7c-4757-95bb-1040fe2a9fe0:project/10065"]},"ruleHome":{"ruleLifecycleHome":{"locationARI":"ari:cloud:jira:4c80f24d-3b7c-4757-95bb-1040fe2a9fe0:project/10065"},"ruleBillingHome":{"locationARI":"ari:cloud:jira-servicedesk::site/4c80f24d-3b7c-4757-95bb-1040fe2a9fe0"}},"writeAccessType":"UNRESTRICTED","collaborators":[],"billingType":"NORMAL","idUuid":"0199e220-0680-7938-a23f-4151fcc9b87e","partitionId":"87112ee8-86e1-4253-bebc-1d4f37c0fb22","currentVersionId":null}]} \ No newline at end of file diff --git a/Automations/[CS] Setze pds oder Partner Bearbeiter.json b/Automations/[CS] Setze pds oder Partner Bearbeiter.json new file mode 100644 index 0000000..fa14076 --- /dev/null +++ b/Automations/[CS] Setze pds oder Partner Bearbeiter.json @@ -0,0 +1 @@ +{"cloud":true,"rules":[{"id":2792663,"clientKey":"d938d60f-6f1a-38fa-b0cc-68cfc2a01a24","name":"[CS] Setze pds oder Partner Bearbeiter","state":"ENABLED","description":"Je nach Ausführendem, wird der pds, bzw. Partner Bearbeiter gesetzt","authorAccountId":"5c094a465e2eee35d79f8e39","actor":{"type":"ACCOUNT_ID","value":"557058:f58131cb-b67d-43c7-b30d-6b58d40bd077"},"created":1760426586057,"updated":1760625770284,"trigger":{"id":"126ec75d-679d-4d20-8827-c0ee6c61cc28","component":"TRIGGER","parentId":null,"conditionParentId":null,"schemaVersion":1,"type":"jira.issue.event.trigger:transitioned","value":{"fromStatus":[{"type":"NAME","value":"Waiting for support"}],"toStatus":[{"type":"NAME","value":"In Progress"}],"eventFilters":["ari:cloud:jira:4c80f24d-3b7c-4757-95bb-1040fe2a9fe0:project/10065"],"eventKey":"jira:issue_updated","issueEvent":"issue_generic"},"children":[],"conditions":[],"connectionId":null},"components":[{"id":"a5097ea5-5f94-44e4-84ab-4af3b7cede6d","component":"ACTION","parentId":null,"conditionParentId":null,"schemaVersion":1,"type":"codebarrel.action.log","value":"Initiator: {{initiator.accountId}} / Name: {{initiator.displayName}} ","children":[],"conditions":[],"connectionId":null},{"id":"35f3e4fb-516c-4a20-b900-4cf1038316af","component":"CONDITION","parentId":null,"conditionParentId":null,"schemaVersion":1,"type":"jira.condition.container.block","value":null,"children":[{"id":"2ac3f923-4072-473d-b652-1559822734ba","component":"CONDITION_BLOCK","parentId":"35f3e4fb-516c-4a20-b900-4cf1038316af","conditionParentId":null,"schemaVersion":1,"type":"jira.condition.if.block","value":{"conditionMatchType":"ALL"},"children":[{"id":"e2e44177-e740-4d23-a8c5-4b85f1c3cba6","component":"ACTION","parentId":"2ac3f923-4072-473d-b652-1559822734ba","conditionParentId":null,"schemaVersion":1,"type":"codebarrel.action.log","value":"Setze pds Bearbeiter: {{initiator.accountId}}","children":[],"conditions":[],"connectionId":null},{"id":"86823bc3-57e2-475c-be5d-d4bab2ca1808","component":"ACTION","parentId":"2ac3f923-4072-473d-b652-1559822734ba","conditionParentId":null,"schemaVersion":12,"type":"jira.issue.edit","value":{"operations":[],"advancedFields":"{\n \"fields\": {\n \"customfield_10036\": [\n { \"accountId\": \"{{initiator.accountId}}\" }\n ],\n \"customfield_10054\": []\n }\n}\n","sendNotifications":false},"children":[],"conditions":[],"connectionId":null}],"conditions":[{"id":"8dde9d8c-b6c2-44c7-9bc5-d114c65a5f0f","component":"CONDITION","parentId":null,"conditionParentId":"2ac3f923-4072-473d-b652-1559822734ba","schemaVersion":6,"type":"jira.user.condition","value":{"conditions":[{"field":"initiator","check":"IN_ROLE","criteria":[{"type":"NAME","value":"Service Desk Team"}]}],"operator":"OR"},"children":[],"conditions":[],"connectionId":null}],"connectionId":null},{"id":"54169b17-57fe-4049-8a0a-436b6465980f","component":"CONDITION_BLOCK","parentId":"35f3e4fb-516c-4a20-b900-4cf1038316af","conditionParentId":null,"schemaVersion":1,"type":"jira.condition.if.block","value":{"conditionMatchType":"ALL"},"children":[{"id":"365a4322-0137-4576-9fc7-0532934a90a6","component":"ACTION","parentId":"54169b17-57fe-4049-8a0a-436b6465980f","conditionParentId":null,"schemaVersion":1,"type":"codebarrel.action.log","value":"Setze Partner Bearbeiter: {{initiator.accountId}}","children":[],"conditions":[],"connectionId":null},{"id":"8cd0c1a1-2a6a-4836-8513-be25f452a5cd","component":"ACTION","parentId":"54169b17-57fe-4049-8a0a-436b6465980f","conditionParentId":null,"schemaVersion":12,"type":"jira.issue.edit","value":{"operations":[],"advancedFields":"{\n \"fields\": {\n \"customfield_10054\": [\n { \"accountId\": \"{{initiator.accountId}}\" }\n ],\n \"customfield_10036\": []\n }\n}\n","sendNotifications":false},"children":[],"conditions":[],"connectionId":null}],"conditions":[{"id":"5300e3f5-6ecf-47f6-8071-43ae2f48b0e8","component":"CONDITION","parentId":null,"conditionParentId":"54169b17-57fe-4049-8a0a-436b6465980f","schemaVersion":6,"type":"jira.user.condition","value":{"conditions":[{"field":"initiator","check":"IN_ROLE","criteria":[{"type":"NAME","value":"Partner"}]}],"operator":"OR"},"children":[],"conditions":[],"connectionId":null}],"connectionId":null}],"conditions":[],"connectionId":null}],"canOtherRuleTrigger":false,"notifyOnError":"FIRSTERROR","projects":[],"labels":[],"tags":[{"id":4262584,"ruleIdUuid":"0199e199-efc8-723c-a27b-a82ed88ed041","tagType":"IS_RULE_UPDATED","tagValue":"true"}],"ruleScope":{"resources":["ari:cloud:jira:4c80f24d-3b7c-4757-95bb-1040fe2a9fe0:project/10065"]},"ruleHome":{"ruleLifecycleHome":{"locationARI":"ari:cloud:jira:4c80f24d-3b7c-4757-95bb-1040fe2a9fe0:project/10065"},"ruleBillingHome":{"locationARI":"ari:cloud:jira-servicedesk::site/4c80f24d-3b7c-4757-95bb-1040fe2a9fe0"}},"writeAccessType":"UNRESTRICTED","collaborators":[],"billingType":"NORMAL","idUuid":"0199e199-efc8-723c-a27b-a82ed88ed041","partitionId":"87112ee8-86e1-4253-bebc-1d4f37c0fb22","currentVersionId":null}]} \ No newline at end of file diff --git a/Automations/[CS] Wenn ein Kommentar hinzugefügt wurde → Status aktualisieren.json b/Automations/[CS] Wenn ein Kommentar hinzugefügt wurde → Status aktualisieren.json new file mode 100644 index 0000000..98abda9 --- /dev/null +++ b/Automations/[CS] Wenn ein Kommentar hinzugefügt wurde → Status aktualisieren.json @@ -0,0 +1 @@ +{"cloud":true,"rules":[{"id":2787567,"clientKey":"d938d60f-6f1a-38fa-b0cc-68cfc2a01a24","name":"[CS] Wenn ein Kommentar hinzugefügt wurde → Status aktualisieren","state":"ENABLED","description":"","authorAccountId":"5c094a465e2eee35d79f8e39","actor":{"type":"ACCOUNT_ID","value":"557058:f58131cb-b67d-43c7-b30d-6b58d40bd077"},"created":1760369398962,"updated":1760429266947,"trigger":{"id":"51a44bd1-7457-41b6-b2d2-b3aed0bb6df3","component":"TRIGGER","parentId":null,"conditionParentId":null,"schemaVersion":1,"type":"jira.issue.event.trigger:commented","value":{"eventTypes":["PRIMARY_ACTION"],"eventFilters":["ari:cloud:jira:4c80f24d-3b7c-4757-95bb-1040fe2a9fe0:project/10065"],"eventKey":"jira:issue_updated","issueEvent":"issue_commented"},"children":[],"conditions":[],"connectionId":null},"components":[{"id":"5a85d21f-d58e-421b-8482-28923005c5c2","component":"CONDITION","parentId":null,"conditionParentId":null,"schemaVersion":1,"type":"jira.comparator.condition","value":{"first":"{{comment.internal}}","second":"false","operator":"EQUALS"},"children":[],"conditions":[],"connectionId":null},{"id":"3768eefa-8dfd-4357-a5f3-71597db0f3f2","component":"CONDITION","parentId":null,"conditionParentId":null,"schemaVersion":1,"type":"jira.condition.container.block","value":null,"children":[{"id":"5a826123-01af-4ea9-8286-770a961057f6","component":"CONDITION_BLOCK","parentId":"3768eefa-8dfd-4357-a5f3-71597db0f3f2","conditionParentId":null,"schemaVersion":1,"type":"jira.condition.if.block","value":{"conditionMatchType":"ALL"},"children":[{"id":"131607dc-6b0d-4e45-979c-11e5f5e27015","component":"ACTION","parentId":"5a826123-01af-4ea9-8286-770a961057f6","conditionParentId":null,"schemaVersion":11,"type":"jira.issue.transition","value":{"operations":[],"advancedFields":null,"sendNotifications":true,"destinationStatus":{"type":"NAME","value":"Waiting for customer"},"transitionMatch":""},"children":[],"conditions":[],"connectionId":null}],"conditions":[{"id":"ff80018b-81f5-457f-9f9d-07d4c7b6b40b","component":"CONDITION","parentId":null,"conditionParentId":"5a826123-01af-4ea9-8286-770a961057f6","schemaVersion":6,"type":"jira.user.condition","value":{"conditions":[{"field":"initiator","check":"IN_ROLE","criteria":[{"type":"NAME","value":"Service Desk Team"}]}],"operator":"OR"},"children":[],"conditions":[],"connectionId":null},{"id":"e9c6cedc-7228-4924-813b-98c4c5e93906","component":"CONDITION","parentId":null,"conditionParentId":"5a826123-01af-4ea9-8286-770a961057f6","schemaVersion":3,"type":"jira.issue.condition","value":{"selectedField":{"type":"ID","value":"status"},"selectedFieldType":"status","comparison":"EQUALS","compareValue":{"type":"NAME","modifier":null,"value":"Waiting for support","multiValue":false,"source":null}},"children":[],"conditions":[],"connectionId":null}],"connectionId":null},{"id":"365ef8aa-318e-4fe1-87f0-0db3f1f559ae","component":"CONDITION_BLOCK","parentId":"3768eefa-8dfd-4357-a5f3-71597db0f3f2","conditionParentId":null,"schemaVersion":1,"type":"jira.condition.if.block","value":{"conditionMatchType":"ALL"},"children":[{"id":"a52c0ded-cd93-442a-a22d-93e3d3b770fc","component":"ACTION","parentId":"365ef8aa-318e-4fe1-87f0-0db3f1f559ae","conditionParentId":null,"schemaVersion":11,"type":"jira.issue.transition","value":{"operations":[],"advancedFields":null,"sendNotifications":true,"destinationStatus":{"type":"NAME","value":"Waiting for support"},"transitionMatch":""},"children":[],"conditions":[],"connectionId":null}],"conditions":[{"id":"8984a77f-9a89-4fa1-9345-8bf71f08f5d9","component":"CONDITION","parentId":null,"conditionParentId":"365ef8aa-318e-4fe1-87f0-0db3f1f559ae","schemaVersion":6,"type":"jira.user.condition","value":{"conditions":[{"field":"initiator","check":"NOT_IN_ROLE","criteria":[{"type":"NAME","value":"Service Desk Team"}]}],"operator":"OR"},"children":[],"conditions":[],"connectionId":null},{"id":"82af5292-714c-41b2-828e-c33696bcb5dd","component":"CONDITION","parentId":null,"conditionParentId":"365ef8aa-318e-4fe1-87f0-0db3f1f559ae","schemaVersion":3,"type":"jira.issue.condition","value":{"selectedField":{"type":"ID","value":"status"},"selectedFieldType":"status","comparison":"EQUALS","compareValue":{"type":"NAME","modifier":null,"value":"Waiting for customer","multiValue":false,"source":null}},"children":[],"conditions":[],"connectionId":null}],"connectionId":null}],"conditions":[],"connectionId":null}],"canOtherRuleTrigger":false,"notifyOnError":"FIRSTERROR","projects":[],"labels":[],"tags":[{"id":4199773,"ruleIdUuid":"0199de31-54b2-75e9-a4c5-202caea43676","tagType":"IS_RULE_UPDATED","tagValue":"true"}],"ruleScope":{"resources":["ari:cloud:jira:4c80f24d-3b7c-4757-95bb-1040fe2a9fe0:project/10065"]},"ruleHome":{"ruleLifecycleHome":{"locationARI":"ari:cloud:jira:4c80f24d-3b7c-4757-95bb-1040fe2a9fe0:project/10065"},"ruleBillingHome":{"locationARI":"ari:cloud:jira-servicedesk::site/4c80f24d-3b7c-4757-95bb-1040fe2a9fe0"}},"writeAccessType":"UNRESTRICTED","collaborators":[],"billingType":"NORMAL","idUuid":"0199de31-54b2-75e9-a4c5-202caea43676","partitionId":"87112ee8-86e1-4253-bebc-1d4f37c0fb22","currentVersionId":null}]} \ No newline at end of file diff --git a/Console - Maintenance/Custom Fields – Unused Cleaner (kein Screen & kein Projekt-Kontext).groovy b/Console - Maintenance/Custom Fields – Unused Cleaner (kein Screen & kein Projekt-Kontext).groovy new file mode 100644 index 0000000..2e7a7ad --- /dev/null +++ b/Console - Maintenance/Custom Fields – Unused Cleaner (kein Screen & kein Projekt-Kontext).groovy @@ -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 EXCLUDE_BY_NAME = [] as Set +Set 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 pagedGetValues(String path, int pageSize=50) { + int startAt = 0; List all = [] + while (true) { + Map body = getAsMap(path, [startAt:startAt, maxResults:pageSize]) + List vals = (body.values ?: []) as List + all.addAll(vals as List) + 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 fetchCustomFields(int pageSize){ + logInfo("Lade Custom Fields…") + List 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 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 fetchScreens(int pageSize){ + logInfo("Lade Screens…") + List list = pagedGetValues("/rest/api/3/screens", pageSize) + logInfo("Screens gefunden: " + list.size()) + return list +} +List fetchScreenTabs(Long screenId){ + Map body = getAsMap("/rest/api/3/screens/${screenId}/tabs", [startAt:0, maxResults:50]) + return (body.values ?: []) as List +} +List 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 scanScreensFor(Set fieldIds, int pageSize){ + Map 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 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 candidates, int limit){ + int n = Math.min(limit, candidates.size()) + if (n == 0) return + logWarn("Beispiele (${n}/${candidates.size()}):") + for (int i=0;i 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 excludeByName, Set excludeById){ + + // 1) Felder (limitierbar) + List allFields = fetchCustomFields(pageSize) + List 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 hasProjAssoc = [:].withDefault{false} + List 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 forScreenScan = noScope.collect{ (it.id ?: "").toString() } as Set + Map useOnScreens = scanScreensFor(forScreenScan, pageSize) + + // 4) Kandidaten bestimmen (MIGRATED_POLICY) + List 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) + + diff --git a/Console - Maintenance/Issue Type Screen Schemes – Unused Cleaner.groovy b/Console - Maintenance/Issue Type Screen Schemes – Unused Cleaner.groovy new file mode 100644 index 0000000..2dddfeb --- /dev/null +++ b/Console - Maintenance/Issue Type Screen Schemes – Unused Cleaner.groovy @@ -0,0 +1,115 @@ +import groovy.json.JsonOutput + +// ------------------ Konfig ------------------ +boolean DRY_RUN = true +int PAGE_SIZE = 50 +Set 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 pagedGetValues(String path, int pageSize=50) { + int startAt = 0; List all = [] + while (true) { + Map body = getAsMap(path, [startAt:startAt, maxResults:pageSize]) + List vals = (body.values ?: []) as List + all.addAll(vals as List) + 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 fetchITSS(int pageSize){ + logInfo("Lade Issue Type Screen Schemes…") + def list = pagedGetValues("/rest/api/3/issuetypescreenscheme", pageSize) + logInfo("ITSS gefunden: " + list.size()) + list +} +List 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 excludeByName){ + def itss = fetchITSS(pageSize) + if (itss.isEmpty()){ logInfo("Keine ITSS vorhanden – nichts zu tun."); return } + Map itssById = [:] + itss.each{ Map x -> if (x.id!=null) itssById[Long.valueOf(x.id.toString())] = x } + + def projects = fetchProjects(pageSize) + Set 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 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) diff --git a/Console - Maintenance/Unused (inactiv) Workflow Schemes cleaner.groovy b/Console - Maintenance/Unused (inactiv) Workflow Schemes cleaner.groovy new file mode 100644 index 0000000..bef817b --- /dev/null +++ b/Console - Maintenance/Unused (inactiv) Workflow Schemes cleaner.groovy @@ -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 EXCLUDED_IDS = [10000L] as Set + +// --- Schritt 1: Alle Workflow Schemes laden (paginiert) --------------------- + +List 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 + + 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: , used: boolean, projects: Set] +def schemeUsage = allSchemes.collectEntries { scheme -> + Long id = (scheme.id as Long) + [ + (id): [ + scheme : scheme, + used : false, + projects: [] as Set + ] + ] +} + +// --- Schritt 2: Mappings Workflow Scheme <-> Projekte laden ----------------- +// +// GET /rest/api/3/workflowscheme/project +// liefert PageBean mit values[ { workflowSchemeId, projectId, projectKey, ... } ] + +List 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 + + 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 deleted = [] +List 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)) diff --git a/Console - Maintenance/Unused Notification Schemes cleaner.groovy b/Console - Maintenance/Unused Notification Schemes cleaner.groovy new file mode 100644 index 0000000..a2f5490 --- /dev/null +++ b/Console - Maintenance/Unused Notification Schemes cleaner.groovy @@ -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 EXCLUDED_IDS = [10000L] as Set + +// --- Schritt 1: Alle Notification Schemes laden (paginierte API) ------------ + +List 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 + + 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: , used: boolean, projects: Set] +def schemeUsage = allSchemes.collectEntries { scheme -> + Long id = (scheme.id as Long) + [ + (id): [ + scheme : scheme, + used : false, + projects: [] as Set + ] + ] +} + +// --- 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 + + 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 deleted = [] +List 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)) diff --git a/Console - Maintenance/Unused Screen Schemes cleaner.groovy b/Console - Maintenance/Unused Screen Schemes cleaner.groovy new file mode 100644 index 0000000..3e38692 --- /dev/null +++ b/Console - Maintenance/Unused Screen Schemes cleaner.groovy @@ -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 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 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 pagedGetValues(String path, int pageSize) { + int startAt = 0 + List all = [] + while (true) { + Map body = getAsMap(path, [startAt: startAt, maxResults: pageSize]) + List vals = (body.values ?: []) as List + all.addAll(vals as List) + 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 fetchScreenSchemes(int pageSize) { + logInfo("Lade Screen Schemes…") + List list = pagedGetValues("/rest/api/3/screenscheme", pageSize) + logInfo("Screen Schemes gefunden: " + list.size()) + return list +} + +List fetchIssueTypeScreenSchemes(int pageSize) { + logInfo("Lade Issue Type Screen Schemes…") + List 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 fetchReferencedScreenSchemeIds(int pageSize, List itssList) { + Set 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 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 excludeByName) { + List screenSchemes = fetchScreenSchemes(pageSize) + if (screenSchemes.isEmpty()) { logInfo("Keine Screen Schemes vorhanden – nichts zu tun."); return } + + List itssList = fetchIssueTypeScreenSchemes(pageSize) + Set referenced = fetchReferencedScreenSchemeIds(pageSize, itssList) + + List 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) diff --git a/Console - Maintenance/Unused permission Schemes cleaner.groovy b/Console - Maintenance/Unused permission Schemes cleaner.groovy new file mode 100644 index 0000000..d754925 --- /dev/null +++ b/Console - Maintenance/Unused permission Schemes cleaner.groovy @@ -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 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: , 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)) diff --git a/Postfunctions/Herstellersupport ablehnen.groovy b/Postfunctions/Herstellersupport ablehnen.groovy new file mode 100644 index 0000000..8e3bc87 --- /dev/null +++ b/Postfunctions/Herstellersupport ablehnen.groovy @@ -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}" +} \ No newline at end of file diff --git a/Postfunctions/Herstellersupport anfragen.groovy b/Postfunctions/Herstellersupport anfragen.groovy new file mode 100644 index 0000000..9bab532 --- /dev/null +++ b/Postfunctions/Herstellersupport anfragen.groovy @@ -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}" +} \ No newline at end of file diff --git a/Postfunctions/Herstellersupport annehmen.groovy b/Postfunctions/Herstellersupport annehmen.groovy new file mode 100644 index 0000000..87bc1e4 --- /dev/null +++ b/Postfunctions/Herstellersupport annehmen.groovy @@ -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}" +} diff --git a/Postfunctions/Setze Fälligkeit beim Schließen (benötigt für Auto-Close).groovy b/Postfunctions/Setze Fälligkeit beim Schließen (benötigt für Auto-Close).groovy new file mode 100644 index 0000000..4406b79 --- /dev/null +++ b/Postfunctions/Setze Fälligkeit beim Schließen (benötigt für Auto-Close).groovy @@ -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}" +} diff --git a/Scheduled Jobs/Schließe Tickets aus dem Status Resolved.groovy b/Scheduled Jobs/Schließe Tickets aus dem Status Resolved.groovy new file mode 100644 index 0000000..b187f5c --- /dev/null +++ b/Scheduled Jobs/Schließe Tickets aus dem Status Resolved.groovy @@ -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 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})") diff --git a/Scheduled Jobs/close_resolved_issues.drawio b/Scheduled Jobs/close_resolved_issues.drawio new file mode 100644 index 0000000..3e0de04 --- /dev/null +++ b/Scheduled Jobs/close_resolved_issues.drawio @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +