feat(jira): auf REST API v2 umgestellt und Ticket-Schließung erweitert

- 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
This commit is contained in:
2026-03-24 08:20:25 +00:00
parent a809623908
commit 821486e2a0
7 changed files with 96 additions and 14 deletions
+6
View File
@@ -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
+6
View File
@@ -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`
+6
View File
@@ -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
+37 -7
View File
@@ -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}`;
+33 -5
View File
@@ -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,
+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), 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 `
+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}, 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 \