From 821486e2a0ca3e4c51c6b0ef1ee7633e1e9aee81 Mon Sep 17 00:00:00 2001 From: admrene Date: Tue, 24 Mar 2026 08:20:25 +0000 Subject: [PATCH] =?UTF-8?q?feat(jira):=20auf=20REST=20API=20v2=20umgestell?= =?UTF-8?q?t=20und=20Ticket-Schlie=C3=9Fung=20erweitert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Jira Core API-Aufrufe von `rest/api/3` auf `rest/api/2` umgestellt - Ticket-Schließung um JQL-basierte Selektion erweitert: - `JIRA_CLOSE_JQL` - `JIRA_CLOSE_JQL_MAX_RESULTS` (Default: 100) - Kombination aus expliziten Keys und per JQL gefundenen Keys implementiert (dedupliziert) - Optionales Setzen der Resolution beim Transition-Call ergänzt: - `JIRA_CLOSE_RESOLUTION_NAME` - Workflow-Logging für Close-JQL, gefundene Tickets und Resolution erweitert - Config/Help um neue Close-Parameter ergänzt - `.env.example` und README mit neuer Konfiguration aktualisiert - TeamCity-Skripte (`run-workflow.sh`, `run-workflow.ps1`) um neue ENV-Variablen ergänzt --- .env.example | 6 ++++++ README.md | 6 ++++++ src/config.js | 6 ++++++ src/index.js | 44 ++++++++++++++++++++++++++++++++------- src/jira.js | 38 ++++++++++++++++++++++++++++----- teamcity/run-workflow.ps1 | 5 ++++- teamcity/run-workflow.sh | 5 ++++- 7 files changed, 96 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index 7a89d74..9917695 100644 --- a/.env.example +++ b/.env.example @@ -22,10 +22,16 @@ JIRA_DRY_RUN=false # Optional: Mehrfachangabe von Tickets, die geschlossen werden sollen (CSV) JIRA_CLOSE_ISSUE_KEYS=SRC-101,SRC-102 +# Optional: Tickets per JQL fuer Schliessung ermitteln +JIRA_CLOSE_JQL=project = SRC AND fixVersion = 12345 AND status = Resolved +# Optional: maxResults fuer JQL-Selektion +JIRA_CLOSE_JQL_MAX_RESULTS=100 # Optional: Nur Tickets mit diesen aktuellen Status werden geschlossen (CSV) JIRA_CLOSE_ALLOWED_STATUSES=Done,Resolved # Optional: Name der Jira Transition in Richtung "geschlossen" JIRA_CLOSE_TRANSITION_NAME=Geschlossen +# Optional: Resolution beim Schliessen setzen (z. B. Erledigt) +JIRA_CLOSE_RESOLUTION_NAME=Erledigt # Atlassian Wiki / Confluence (optional) ATLASSIAN_WIKI_ENABLED=false diff --git a/README.md b/README.md index e90acb8..e4a3b3a 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,11 @@ Empfohlen: - `JIRA_DRY_RUN` (`true`/`false`) - `OUTPUT_HTML_PATH` (Default: `report-output/report.html`) - `JIRA_CLOSE_ISSUE_KEYS` (CSV, optional; z. B. `ABC-1,ABC-2`) +- `JIRA_CLOSE_JQL` (optional; Tickets fuer Schliessung per JQL ermitteln) +- `JIRA_CLOSE_JQL_MAX_RESULTS` (optional; Default: `100`) - `JIRA_CLOSE_ALLOWED_STATUSES` (CSV, optional; z. B. `Done,Resolved`) - `JIRA_CLOSE_TRANSITION_NAME` (optional; Default: `Geschlossen`) +- `JIRA_CLOSE_RESOLUTION_NAME` (optional; z. B. `Erledigt`) Atlassian Wiki / Confluence optional: @@ -107,8 +110,11 @@ Lege in TeamCity die Variablen als Parameter vom Typ `env.*` an, z. B.: - `env.XRAY_REPORT_URL` - `env.SONAR_REPORT_URL` - `env.JIRA_CLOSE_ISSUE_KEYS` +- `env.JIRA_CLOSE_JQL` +- `env.JIRA_CLOSE_JQL_MAX_RESULTS` - `env.JIRA_CLOSE_ALLOWED_STATUSES` - `env.JIRA_CLOSE_TRANSITION_NAME` +- `env.JIRA_CLOSE_RESOLUTION_NAME` - `env.ATLASSIAN_WIKI_ENABLED` - `env.ATLASSIAN_WIKI_SPACE_KEY` - `env.ATLASSIAN_WIKI_PARENT_PAGE_ID` diff --git a/src/config.js b/src/config.js index 1170d20..5a717d8 100644 --- a/src/config.js +++ b/src/config.js @@ -78,10 +78,13 @@ function getConfig() { dryRun: parseBoolean(process.env.JIRA_DRY_RUN, false), issueClose: { issueKeys: parseList(process.env.JIRA_CLOSE_ISSUE_KEYS), + jql: (process.env.JIRA_CLOSE_JQL || "").trim(), + jqlMaxResults: parseNumber(process.env.JIRA_CLOSE_JQL_MAX_RESULTS, 100), allowedStatuses: parseList(process.env.JIRA_CLOSE_ALLOWED_STATUSES), transitionName: (process.env.JIRA_CLOSE_TRANSITION_NAME || "Geschlossen").trim() || "Geschlossen", + resolutionName: (process.env.JIRA_CLOSE_RESOLUTION_NAME || "").trim(), }, }, smtp: { @@ -148,8 +151,11 @@ Wichtige ENV Variablen: XRAY_REPORT_URL SONAR_REPORT_URL JIRA_CLOSE_ISSUE_KEYS + JIRA_CLOSE_JQL + JIRA_CLOSE_JQL_MAX_RESULTS JIRA_CLOSE_ALLOWED_STATUSES JIRA_CLOSE_TRANSITION_NAME + JIRA_CLOSE_RESOLUTION_NAME ATLASSIAN_WIKI_ENABLED=true|false ATLASSIAN_WIKI_SPACE_KEY SMTP_ENABLED=true|false diff --git a/src/index.js b/src/index.js index 848a79c..a829e40 100644 --- a/src/index.js +++ b/src/index.js @@ -39,7 +39,7 @@ async function run() { }` ); logStep( - `Optionen: Wiki=${config.wiki.enabled}, SMTP=${config.smtp.enabled}, Output=${config.outputHtmlPath}, CloseKeys=${config.jira.issueClose.issueKeys.length}` + `Optionen: Wiki=${config.wiki.enabled}, SMTP=${config.smtp.enabled}, Output=${config.outputHtmlPath}, CloseKeys=${config.jira.issueClose.issueKeys.length}, CloseJql=${config.jira.issueClose.jql ? "ja" : "nein"}` ); const jira = createJiraClient(config.jira); @@ -129,10 +129,29 @@ async function run() { logStep("Wiki-Seite deaktiviert (ATLASSIAN_WIKI_ENABLED=false)."); } - let issueCloseResult = null; - if (config.jira.issueClose.issueKeys.length > 0 && !dryRun) { + const issueCloseConfigured = + config.jira.issueClose.issueKeys.length > 0 || + Boolean(config.jira.issueClose.jql); + + let closeIssueKeys = [...config.jira.issueClose.issueKeys]; + if (config.jira.issueClose.jql) { logStep( - `Starte Ticket-Schliessung fuer ${config.jira.issueClose.issueKeys.length} Tickets (Transition: ${config.jira.issueClose.transitionName})...` + `Lade zu schliessende Tickets per JQL (maxResults=${config.jira.issueClose.jqlMaxResults})...` + ); + const issueKeysFromJql = await jira.getIssueKeysByJql( + config.jira.issueClose.jql, + config.jira.issueClose.jqlMaxResults + ); + logStep( + `Ticket-Schliessung JQL gefunden: ${issueKeysFromJql.length} Tickets` + ); + closeIssueKeys = Array.from(new Set([...closeIssueKeys, ...issueKeysFromJql])); + } + + let issueCloseResult = null; + if (closeIssueKeys.length > 0 && !dryRun) { + logStep( + `Starte Ticket-Schliessung fuer ${closeIssueKeys.length} Tickets (Transition: ${config.jira.issueClose.transitionName})...` ); if (config.jira.issueClose.allowedStatuses.length > 0) { logStep( @@ -141,11 +160,17 @@ async function run() { )}` ); } + if (config.jira.issueClose.resolutionName) { + logStep( + `Setze Resolution beim Schliessen: ${config.jira.issueClose.resolutionName}` + ); + } issueCloseResult = await jira.closeIssues({ - issueKeys: config.jira.issueClose.issueKeys, + issueKeys: closeIssueKeys, allowedStatuses: config.jira.issueClose.allowedStatuses, transitionName: config.jira.issueClose.transitionName, + resolutionName: config.jira.issueClose.resolutionName, }); logStep( @@ -171,8 +196,10 @@ async function run() { logStep(`Ticket Fehler: ${item.issueKey}`); console.error(item.message); } - } else if (config.jira.issueClose.issueKeys.length > 0) { + } else if (closeIssueKeys.length > 0) { logStep("Ticket-Schliessung uebersprungen (Dry-Run)."); + } else if (issueCloseConfigured) { + logStep("Ticket-Schliessung konfiguriert, aber keine Tickets gefunden."); } else { logStep("Ticket-Schliessung nicht konfiguriert."); } @@ -200,13 +227,16 @@ async function run() { }\nWiki Seite: ${wikiStatusText}`; let issueCloseText = "\nTicket-Schliessung: nicht konfiguriert."; - if (config.jira.issueClose.issueKeys.length > 0) { + if (closeIssueKeys.length > 0) { if (dryRun) { issueCloseText = "\nTicket-Schliessung: nicht ausgefuehrt (Dry-Run)."; } else if (issueCloseResult) { issueCloseText = `\nTicket-Schliessung: geschlossen=${issueCloseResult.closed.length}, uebersprungenStatus=${issueCloseResult.skippedByStatus.length}, uebersprungenTransition=${issueCloseResult.skippedByMissingTransition.length}, fehler=${issueCloseResult.errors.length}`; } + } else if (issueCloseConfigured) { + issueCloseText = + "\nTicket-Schliessung: konfiguriert, aber keine Tickets gefunden."; } const mailTextWithIssueClose = `${mailText}${issueCloseText}`; diff --git a/src/jira.js b/src/jira.js index 63b03a0..0e05535 100644 --- a/src/jira.js +++ b/src/jira.js @@ -13,7 +13,7 @@ function createJiraClient(config) { }; const api = axios.create({ - baseURL: `${config.baseUrl}/rest/api/3`, + baseURL: `${config.baseUrl}/rest/api/2`, headers, timeout: 30000, }); @@ -35,6 +35,18 @@ function createJiraClient(config) { return response.data.issues || []; } + async function getIssueKeysByJql(jql, maxResults = 100) { + const payload = { + jql, + maxResults, + fields: ["status"], + }; + + const response = await api.post("/search", payload); + const issues = response.data?.issues || []; + return issues.map((issue) => issue.key).filter(Boolean); + } + async function resolveProjectKeyFromBoardId(boardId) { const response = await agileApi.get(`/board/${boardId}/configuration`); const board = response.data || {}; @@ -91,17 +103,28 @@ function createJiraClient(config) { return response.data?.transitions || []; } - async function transitionIssueById(issueKey, transitionId) { + async function transitionIssueById(issueKey, transitionId, resolutionName) { const encodedIssueKey = encodeURIComponent(issueKey); - await api.post(`/issue/${encodedIssueKey}/transitions`, { + const payload = { transition: { id: String(transitionId) }, - }); + }; + + if (resolutionName) { + payload.fields = { + resolution: { + name: resolutionName, + }, + }; + } + + await api.post(`/issue/${encodedIssueKey}/transitions`, payload); } async function closeIssues({ issueKeys, allowedStatuses = [], transitionName = "Geschlossen", + resolutionName = "", }) { const uniqueKeys = Array.from( new Set((issueKeys || []).map((key) => String(key).trim()).filter(Boolean)) @@ -147,7 +170,11 @@ function createJiraClient(config) { continue; } - await transitionIssueById(issueKey, closeTransition.id); + await transitionIssueById( + issueKey, + closeTransition.id, + resolutionName + ); result.closed.push({ issueKey, fromStatus: currentStatus, @@ -166,6 +193,7 @@ function createJiraClient(config) { return { getRecentIssues, + getIssueKeysByJql, resolveProjectKeyFromBoardId, createIssue, closeIssues, diff --git a/teamcity/run-workflow.ps1 b/teamcity/run-workflow.ps1 index 39b266b..43d37e5 100644 --- a/teamcity/run-workflow.ps1 +++ b/teamcity/run-workflow.ps1 @@ -9,7 +9,7 @@ Write-Host "Builde Docker Image $imageName" docker build -t $imageName . Write-Host "Starte Workflow Container" -Write-Host "Kontext: IMAGE_TAG=$imageTag, JIRA_DRY_RUN=$($env:JIRA_DRY_RUN), JIRA_CLOSE_ISSUE_KEYS=$($env:JIRA_CLOSE_ISSUE_KEYS), ATLASSIAN_WIKI_ENABLED=$($env:ATLASSIAN_WIKI_ENABLED), SMTP_ENABLED=$($env:SMTP_ENABLED)" +Write-Host "Kontext: IMAGE_TAG=$imageTag, JIRA_DRY_RUN=$($env:JIRA_DRY_RUN), JIRA_CLOSE_ISSUE_KEYS=$($env:JIRA_CLOSE_ISSUE_KEYS), JIRA_CLOSE_JQL=$($env:JIRA_CLOSE_JQL), ATLASSIAN_WIKI_ENABLED=$($env:ATLASSIAN_WIKI_ENABLED), SMTP_ENABLED=$($env:SMTP_ENABLED)" Write-Host "Volume Mapping: $hostReportDir -> $containerReportDir" docker run --rm ` -e JIRA_BASE_URL ` @@ -26,8 +26,11 @@ docker run --rm ` -e XRAY_REPORT_URL ` -e SONAR_REPORT_URL ` -e JIRA_CLOSE_ISSUE_KEYS ` + -e JIRA_CLOSE_JQL ` + -e JIRA_CLOSE_JQL_MAX_RESULTS ` -e JIRA_CLOSE_ALLOWED_STATUSES ` -e JIRA_CLOSE_TRANSITION_NAME ` + -e JIRA_CLOSE_RESOLUTION_NAME ` -e ATLASSIAN_WIKI_ENABLED ` -e ATLASSIAN_WIKI_BASE_URL ` -e ATLASSIAN_WIKI_USER_EMAIL ` diff --git a/teamcity/run-workflow.sh b/teamcity/run-workflow.sh index bd6968e..03577cc 100644 --- a/teamcity/run-workflow.sh +++ b/teamcity/run-workflow.sh @@ -10,7 +10,7 @@ echo "Builde Docker Image ${IMAGE_NAME}" docker build -t "${IMAGE_NAME}" . echo "Starte Workflow Container" -echo "Kontext: IMAGE_TAG=${IMAGE_TAG}, JIRA_DRY_RUN=${JIRA_DRY_RUN:-false}, JIRA_CLOSE_ISSUE_KEYS=${JIRA_CLOSE_ISSUE_KEYS:-}, ATLASSIAN_WIKI_ENABLED=${ATLASSIAN_WIKI_ENABLED:-false}, SMTP_ENABLED=${SMTP_ENABLED:-false}" +echo "Kontext: IMAGE_TAG=${IMAGE_TAG}, JIRA_DRY_RUN=${JIRA_DRY_RUN:-false}, JIRA_CLOSE_ISSUE_KEYS=${JIRA_CLOSE_ISSUE_KEYS:-}, JIRA_CLOSE_JQL=${JIRA_CLOSE_JQL:-}, ATLASSIAN_WIKI_ENABLED=${ATLASSIAN_WIKI_ENABLED:-false}, SMTP_ENABLED=${SMTP_ENABLED:-false}" echo "Volume Mapping: ${HOST_REPORT_DIR} -> ${CONTAINER_REPORT_DIR}" docker run --rm \ -e JIRA_BASE_URL \ @@ -27,8 +27,11 @@ docker run --rm \ -e XRAY_REPORT_URL \ -e SONAR_REPORT_URL \ -e JIRA_CLOSE_ISSUE_KEYS \ + -e JIRA_CLOSE_JQL \ + -e JIRA_CLOSE_JQL_MAX_RESULTS \ -e JIRA_CLOSE_ALLOWED_STATUSES \ -e JIRA_CLOSE_TRANSITION_NAME \ + -e JIRA_CLOSE_RESOLUTION_NAME \ -e ATLASSIAN_WIKI_ENABLED \ -e ATLASSIAN_WIKI_BASE_URL \ -e ATLASSIAN_WIKI_USER_EMAIL \