feat(jira): mehrere Issues schließen inkl. statusbasierter Transition

- Jira-Workflow um optionale Mehrfachverarbeitung von Issue-Keys erweitert
- neue ENV-Parameter eingeführt:
  - `JIRA_CLOSE_ISSUE_KEYS` (CSV)
  - `JIRA_CLOSE_ALLOWED_STATUSES` (CSV)
  - `JIRA_CLOSE_TRANSITION_NAME` (Default: `Geschlossen`)
- Jira-Client um Schließ-Logik ergänzt:
  - Issue-Status lesen
  - verfügbare Transitions lesen
  - Transition per ID ausführen
  - Ergebnis-Summary mit closed/skipped/errors
- Hauptworkflow (`src/index.js`) um Ticket-Schließung inkl. detailliertem Logging erweitert
- Ergebnis der Ticket-Schließung in Mail-Text und Abschlussausgabe integriert
- `.env.example` und `README.md` um neue Parameter ergänzt
- TeamCity-Skripte (`run-workflow.sh` / `run-workflow.ps1`) um neue ENV-Weitergabe erweitert
This commit is contained in:
2026-03-24 07:54:28 +00:00
parent 18f964e6aa
commit a809623908
7 changed files with 187 additions and 4 deletions
+7
View File
@@ -20,6 +20,13 @@ XRAY_REPORT_URL=https://xray.example.com/report/123
SONAR_REPORT_URL=https://sonar.example.com/dashboard?id=project
JIRA_DRY_RUN=false
# Optional: Mehrfachangabe von Tickets, die geschlossen werden sollen (CSV)
JIRA_CLOSE_ISSUE_KEYS=SRC-101,SRC-102
# 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
# Atlassian Wiki / Confluence (optional)
ATLASSIAN_WIKI_ENABLED=false
# Optional, faellt auf JIRA_BASE_URL zurueck:
+6
View File
@@ -51,6 +51,9 @@ Empfohlen:
- `SONAR_REPORT_URL` (optional)
- `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_ALLOWED_STATUSES` (CSV, optional; z. B. `Done,Resolved`)
- `JIRA_CLOSE_TRANSITION_NAME` (optional; Default: `Geschlossen`)
Atlassian Wiki / Confluence optional:
@@ -103,6 +106,9 @@ Lege in TeamCity die Variablen als Parameter vom Typ `env.*` an, z. B.:
- `env.SOLUTION_VERSION`
- `env.XRAY_REPORT_URL`
- `env.SONAR_REPORT_URL`
- `env.JIRA_CLOSE_ISSUE_KEYS`
- `env.JIRA_CLOSE_ALLOWED_STATUSES`
- `env.JIRA_CLOSE_TRANSITION_NAME`
- `env.ATLASSIAN_WIKI_ENABLED`
- `env.ATLASSIAN_WIKI_SPACE_KEY`
- `env.ATLASSIAN_WIKI_PARENT_PAGE_ID`
+10
View File
@@ -76,6 +76,13 @@ function getConfig() {
xrayReportUrl: (process.env.XRAY_REPORT_URL || "").trim(),
sonarReportUrl: (process.env.SONAR_REPORT_URL || "").trim(),
dryRun: parseBoolean(process.env.JIRA_DRY_RUN, false),
issueClose: {
issueKeys: parseList(process.env.JIRA_CLOSE_ISSUE_KEYS),
allowedStatuses: parseList(process.env.JIRA_CLOSE_ALLOWED_STATUSES),
transitionName:
(process.env.JIRA_CLOSE_TRANSITION_NAME || "Geschlossen").trim() ||
"Geschlossen",
},
},
smtp: {
enabled: parseBoolean(process.env.SMTP_ENABLED, false),
@@ -140,6 +147,9 @@ Wichtige ENV Variablen:
SOLUTION_VERSION
XRAY_REPORT_URL
SONAR_REPORT_URL
JIRA_CLOSE_ISSUE_KEYS
JIRA_CLOSE_ALLOWED_STATUSES
JIRA_CLOSE_TRANSITION_NAME
ATLASSIAN_WIKI_ENABLED=true|false
ATLASSIAN_WIKI_SPACE_KEY
SMTP_ENABLED=true|false
+68 -2
View File
@@ -39,7 +39,7 @@ async function run() {
}`
);
logStep(
`Optionen: Wiki=${config.wiki.enabled}, SMTP=${config.smtp.enabled}, Output=${config.outputHtmlPath}`
`Optionen: Wiki=${config.wiki.enabled}, SMTP=${config.smtp.enabled}, Output=${config.outputHtmlPath}, CloseKeys=${config.jira.issueClose.issueKeys.length}`
);
const jira = createJiraClient(config.jira);
@@ -129,6 +129,54 @@ async function run() {
logStep("Wiki-Seite deaktiviert (ATLASSIAN_WIKI_ENABLED=false).");
}
let issueCloseResult = null;
if (config.jira.issueClose.issueKeys.length > 0 && !dryRun) {
logStep(
`Starte Ticket-Schliessung fuer ${config.jira.issueClose.issueKeys.length} Tickets (Transition: ${config.jira.issueClose.transitionName})...`
);
if (config.jira.issueClose.allowedStatuses.length > 0) {
logStep(
`Ticket-Schliessung nur aus Status: ${config.jira.issueClose.allowedStatuses.join(
", "
)}`
);
}
issueCloseResult = await jira.closeIssues({
issueKeys: config.jira.issueClose.issueKeys,
allowedStatuses: config.jira.issueClose.allowedStatuses,
transitionName: config.jira.issueClose.transitionName,
});
logStep(
`Ticket-Schliessung abgeschlossen: geschlossen=${issueCloseResult.closed.length}, uebersprungenStatus=${issueCloseResult.skippedByStatus.length}, uebersprungenTransition=${issueCloseResult.skippedByMissingTransition.length}, fehler=${issueCloseResult.errors.length}`
);
for (const item of issueCloseResult.closed) {
logStep(
`Ticket geschlossen: ${item.issueKey} (${item.fromStatus} -> ${item.transition})`
);
}
for (const item of issueCloseResult.skippedByStatus) {
logStep(
`Ticket uebersprungen (Status nicht erlaubt): ${item.issueKey} (aktuell: ${item.currentStatus || "-"})`
);
}
for (const item of issueCloseResult.skippedByMissingTransition) {
logStep(
`Ticket uebersprungen (Transition nicht gefunden): ${item.issueKey} (gewuenscht: ${item.requestedTransition})`
);
}
for (const item of issueCloseResult.errors) {
logStep(`Ticket Fehler: ${item.issueKey}`);
console.error(item.message);
}
} else if (config.jira.issueClose.issueKeys.length > 0) {
logStep("Ticket-Schliessung uebersprungen (Dry-Run).");
} else {
logStep("Ticket-Schliessung nicht konfiguriert.");
}
logStep("Erzeuge HTML-Zusammenfassung...");
const html = buildHtmlSummary(report, createdIssueUrl);
const outputDir = path.dirname(config.outputHtmlPath);
@@ -151,12 +199,24 @@ async function run() {
createdIssueUrl || "nicht erstellt (Dry-Run)"
}\nWiki Seite: ${wikiStatusText}`;
let issueCloseText = "\nTicket-Schliessung: nicht konfiguriert.";
if (config.jira.issueClose.issueKeys.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}`;
}
}
const mailTextWithIssueClose = `${mailText}${issueCloseText}`;
logStep("Starte E-Mail-Versand...");
const mailResult = await sendReportMail({
smtpConfig: config.smtp,
subject: mailSubject,
html,
text: mailText,
text: mailTextWithIssueClose,
});
logStep("E-Mail-Versand abgeschlossen.");
@@ -187,6 +247,12 @@ async function run() {
} else {
console.log(`E-Mail Versand: erfolgreich (Message-ID ${mailResult.messageId})`);
}
if (issueCloseResult) {
console.log(
`Ticket-Schliessung: geschlossen=${issueCloseResult.closed.length}, uebersprungenStatus=${issueCloseResult.skippedByStatus.length}, uebersprungenTransition=${issueCloseResult.skippedByMissingTransition.length}, fehler=${issueCloseResult.errors.length}`
);
}
}
run().catch((error) => {
+88
View File
@@ -77,10 +77,98 @@ function createJiraClient(config) {
return response.data;
}
async function getIssueStatus(issueKey) {
const encodedIssueKey = encodeURIComponent(issueKey);
const response = await api.get(`/issue/${encodedIssueKey}`, {
params: { fields: "status" },
});
return response.data?.fields?.status?.name || "";
}
async function getTransitions(issueKey) {
const encodedIssueKey = encodeURIComponent(issueKey);
const response = await api.get(`/issue/${encodedIssueKey}/transitions`);
return response.data?.transitions || [];
}
async function transitionIssueById(issueKey, transitionId) {
const encodedIssueKey = encodeURIComponent(issueKey);
await api.post(`/issue/${encodedIssueKey}/transitions`, {
transition: { id: String(transitionId) },
});
}
async function closeIssues({
issueKeys,
allowedStatuses = [],
transitionName = "Geschlossen",
}) {
const uniqueKeys = Array.from(
new Set((issueKeys || []).map((key) => String(key).trim()).filter(Boolean))
);
const allowedSet = new Set(
(allowedStatuses || []).map((status) => status.toLowerCase())
);
const transitionNameNormalized = transitionName.trim().toLowerCase();
const result = {
closed: [],
skippedByStatus: [],
skippedByMissingTransition: [],
errors: [],
};
for (const issueKey of uniqueKeys) {
try {
const currentStatus = await getIssueStatus(issueKey);
if (
allowedSet.size > 0 &&
!allowedSet.has(String(currentStatus).toLowerCase())
) {
result.skippedByStatus.push({ issueKey, currentStatus });
continue;
}
const transitions = await getTransitions(issueKey);
const closeTransition = transitions.find(
(transition) =>
String(transition.name || "").trim().toLowerCase() ===
transitionNameNormalized
);
if (!closeTransition) {
result.skippedByMissingTransition.push({
issueKey,
currentStatus,
requestedTransition: transitionName,
});
continue;
}
await transitionIssueById(issueKey, closeTransition.id);
result.closed.push({
issueKey,
fromStatus: currentStatus,
transition: closeTransition.name,
});
} catch (error) {
result.errors.push({
issueKey,
message: error?.response?.data || error.message || String(error),
});
}
}
return result;
}
return {
getRecentIssues,
resolveProjectKeyFromBoardId,
createIssue,
closeIssues,
};
}
+4 -1
View File
@@ -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), 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), 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 `
@@ -25,6 +25,9 @@ docker run --rm `
-e SOLUTION_VERSION `
-e XRAY_REPORT_URL `
-e SONAR_REPORT_URL `
-e JIRA_CLOSE_ISSUE_KEYS `
-e JIRA_CLOSE_ALLOWED_STATUSES `
-e JIRA_CLOSE_TRANSITION_NAME `
-e ATLASSIAN_WIKI_ENABLED `
-e ATLASSIAN_WIKI_BASE_URL `
-e ATLASSIAN_WIKI_USER_EMAIL `
+4 -1
View File
@@ -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}, 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:-}, 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 \
@@ -26,6 +26,9 @@ docker run --rm \
-e SOLUTION_VERSION \
-e XRAY_REPORT_URL \
-e SONAR_REPORT_URL \
-e JIRA_CLOSE_ISSUE_KEYS \
-e JIRA_CLOSE_ALLOWED_STATUSES \
-e JIRA_CLOSE_TRANSITION_NAME \
-e ATLASSIAN_WIKI_ENABLED \
-e ATLASSIAN_WIKI_BASE_URL \
-e ATLASSIAN_WIKI_USER_EMAIL \