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:
@@ -20,6 +20,13 @@ XRAY_REPORT_URL=https://xray.example.com/report/123
|
|||||||
SONAR_REPORT_URL=https://sonar.example.com/dashboard?id=project
|
SONAR_REPORT_URL=https://sonar.example.com/dashboard?id=project
|
||||||
JIRA_DRY_RUN=false
|
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 / Confluence (optional)
|
||||||
ATLASSIAN_WIKI_ENABLED=false
|
ATLASSIAN_WIKI_ENABLED=false
|
||||||
# Optional, faellt auf JIRA_BASE_URL zurueck:
|
# Optional, faellt auf JIRA_BASE_URL zurueck:
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ Empfohlen:
|
|||||||
- `SONAR_REPORT_URL` (optional)
|
- `SONAR_REPORT_URL` (optional)
|
||||||
- `JIRA_DRY_RUN` (`true`/`false`)
|
- `JIRA_DRY_RUN` (`true`/`false`)
|
||||||
- `OUTPUT_HTML_PATH` (Default: `report-output/report.html`)
|
- `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:
|
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.SOLUTION_VERSION`
|
||||||
- `env.XRAY_REPORT_URL`
|
- `env.XRAY_REPORT_URL`
|
||||||
- `env.SONAR_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_ENABLED`
|
||||||
- `env.ATLASSIAN_WIKI_SPACE_KEY`
|
- `env.ATLASSIAN_WIKI_SPACE_KEY`
|
||||||
- `env.ATLASSIAN_WIKI_PARENT_PAGE_ID`
|
- `env.ATLASSIAN_WIKI_PARENT_PAGE_ID`
|
||||||
|
|||||||
@@ -76,6 +76,13 @@ function getConfig() {
|
|||||||
xrayReportUrl: (process.env.XRAY_REPORT_URL || "").trim(),
|
xrayReportUrl: (process.env.XRAY_REPORT_URL || "").trim(),
|
||||||
sonarReportUrl: (process.env.SONAR_REPORT_URL || "").trim(),
|
sonarReportUrl: (process.env.SONAR_REPORT_URL || "").trim(),
|
||||||
dryRun: parseBoolean(process.env.JIRA_DRY_RUN, false),
|
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: {
|
smtp: {
|
||||||
enabled: parseBoolean(process.env.SMTP_ENABLED, false),
|
enabled: parseBoolean(process.env.SMTP_ENABLED, false),
|
||||||
@@ -140,6 +147,9 @@ Wichtige ENV Variablen:
|
|||||||
SOLUTION_VERSION
|
SOLUTION_VERSION
|
||||||
XRAY_REPORT_URL
|
XRAY_REPORT_URL
|
||||||
SONAR_REPORT_URL
|
SONAR_REPORT_URL
|
||||||
|
JIRA_CLOSE_ISSUE_KEYS
|
||||||
|
JIRA_CLOSE_ALLOWED_STATUSES
|
||||||
|
JIRA_CLOSE_TRANSITION_NAME
|
||||||
ATLASSIAN_WIKI_ENABLED=true|false
|
ATLASSIAN_WIKI_ENABLED=true|false
|
||||||
ATLASSIAN_WIKI_SPACE_KEY
|
ATLASSIAN_WIKI_SPACE_KEY
|
||||||
SMTP_ENABLED=true|false
|
SMTP_ENABLED=true|false
|
||||||
|
|||||||
+68
-2
@@ -39,7 +39,7 @@ async function run() {
|
|||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
logStep(
|
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);
|
const jira = createJiraClient(config.jira);
|
||||||
@@ -129,6 +129,54 @@ async function run() {
|
|||||||
logStep("Wiki-Seite deaktiviert (ATLASSIAN_WIKI_ENABLED=false).");
|
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...");
|
logStep("Erzeuge HTML-Zusammenfassung...");
|
||||||
const html = buildHtmlSummary(report, createdIssueUrl);
|
const html = buildHtmlSummary(report, createdIssueUrl);
|
||||||
const outputDir = path.dirname(config.outputHtmlPath);
|
const outputDir = path.dirname(config.outputHtmlPath);
|
||||||
@@ -151,12 +199,24 @@ async function run() {
|
|||||||
createdIssueUrl || "nicht erstellt (Dry-Run)"
|
createdIssueUrl || "nicht erstellt (Dry-Run)"
|
||||||
}\nWiki Seite: ${wikiStatusText}`;
|
}\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...");
|
logStep("Starte E-Mail-Versand...");
|
||||||
const mailResult = await sendReportMail({
|
const mailResult = await sendReportMail({
|
||||||
smtpConfig: config.smtp,
|
smtpConfig: config.smtp,
|
||||||
subject: mailSubject,
|
subject: mailSubject,
|
||||||
html,
|
html,
|
||||||
text: mailText,
|
text: mailTextWithIssueClose,
|
||||||
});
|
});
|
||||||
logStep("E-Mail-Versand abgeschlossen.");
|
logStep("E-Mail-Versand abgeschlossen.");
|
||||||
|
|
||||||
@@ -187,6 +247,12 @@ async function run() {
|
|||||||
} else {
|
} else {
|
||||||
console.log(`E-Mail Versand: erfolgreich (Message-ID ${mailResult.messageId})`);
|
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) => {
|
run().catch((error) => {
|
||||||
|
|||||||
+88
@@ -77,10 +77,98 @@ function createJiraClient(config) {
|
|||||||
return response.data;
|
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 {
|
return {
|
||||||
getRecentIssues,
|
getRecentIssues,
|
||||||
resolveProjectKeyFromBoardId,
|
resolveProjectKeyFromBoardId,
|
||||||
createIssue,
|
createIssue,
|
||||||
|
closeIssues,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Write-Host "Builde Docker Image $imageName"
|
|||||||
docker build -t $imageName .
|
docker build -t $imageName .
|
||||||
|
|
||||||
Write-Host "Starte Workflow Container"
|
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"
|
Write-Host "Volume Mapping: $hostReportDir -> $containerReportDir"
|
||||||
docker run --rm `
|
docker run --rm `
|
||||||
-e JIRA_BASE_URL `
|
-e JIRA_BASE_URL `
|
||||||
@@ -25,6 +25,9 @@ docker run --rm `
|
|||||||
-e SOLUTION_VERSION `
|
-e SOLUTION_VERSION `
|
||||||
-e XRAY_REPORT_URL `
|
-e XRAY_REPORT_URL `
|
||||||
-e SONAR_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_ENABLED `
|
||||||
-e ATLASSIAN_WIKI_BASE_URL `
|
-e ATLASSIAN_WIKI_BASE_URL `
|
||||||
-e ATLASSIAN_WIKI_USER_EMAIL `
|
-e ATLASSIAN_WIKI_USER_EMAIL `
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ echo "Builde Docker Image ${IMAGE_NAME}"
|
|||||||
docker build -t "${IMAGE_NAME}" .
|
docker build -t "${IMAGE_NAME}" .
|
||||||
|
|
||||||
echo "Starte Workflow Container"
|
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}"
|
echo "Volume Mapping: ${HOST_REPORT_DIR} -> ${CONTAINER_REPORT_DIR}"
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-e JIRA_BASE_URL \
|
-e JIRA_BASE_URL \
|
||||||
@@ -26,6 +26,9 @@ docker run --rm \
|
|||||||
-e SOLUTION_VERSION \
|
-e SOLUTION_VERSION \
|
||||||
-e XRAY_REPORT_URL \
|
-e XRAY_REPORT_URL \
|
||||||
-e SONAR_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_ENABLED \
|
||||||
-e ATLASSIAN_WIKI_BASE_URL \
|
-e ATLASSIAN_WIKI_BASE_URL \
|
||||||
-e ATLASSIAN_WIKI_USER_EMAIL \
|
-e ATLASSIAN_WIKI_USER_EMAIL \
|
||||||
|
|||||||
Reference in New Issue
Block a user