feat: optionale Confluence-Seite im Workflow + Docker auf UBI9 umstellen

- optionales Atlassian-Wiki/Confluence-Feature über `ATLASSIAN_WIKI_*` ergänzt
- neue Confluence-Integration (`src/confluence.js`) zum Anlegen von Seiten via REST API
- Workflow erweitert: Wiki-Seite mit Ticket-Liste und Xray/Sonar-Links erstellen
- Dry-Run-/Status-Handling und Ausgabe der Wiki-URL im Log/Mailtext ergänzt
- Konfiguration, `.env.example` und README für Wiki-Optionen erweitert
- TeamCity-Skripte (`run-workflow.sh/.ps1`) um Wiki-ENVs ergänzt
- Docker-Base-Image auf `ubi9/nodejs-20` umgestellt
- Container-Volume-Mounts auf `/opt/app-root/src/report-output` angepasst
This commit is contained in:
2026-03-20 14:21:05 +00:00
parent fae1771c61
commit eed6681329
9 changed files with 255 additions and 9 deletions
+12
View File
@@ -20,6 +20,18 @@ 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
# Atlassian Wiki / Confluence (optional)
ATLASSIAN_WIKI_ENABLED=false
# Optional, faellt auf JIRA_BASE_URL zurueck:
ATLASSIAN_WIKI_BASE_URL=https://your-domain.atlassian.net
# Optional, fallen auf JIRA_USER_EMAIL / JIRA_API_TOKEN zurueck:
ATLASSIAN_WIKI_USER_EMAIL=automation@company.com
ATLASSIAN_WIKI_API_TOKEN=your-api-token
ATLASSIAN_WIKI_SPACE_KEY=TEAM
# Optional: Seite unterhalb einer Parent-Page anlegen
ATLASSIAN_WIKI_PARENT_PAGE_ID=
ATLASSIAN_WIKI_TITLE_PREFIX=Weekly Jira Wiki Report
# HTML-Ausgabe # HTML-Ausgabe
OUTPUT_HTML_PATH=report-output/report.html OUTPUT_HTML_PATH=report-output/report.html
+5 -2
View File
@@ -1,10 +1,13 @@
FROM node:20-alpine FROM ubi9/nodejs-20
WORKDIR /app WORKDIR /opt/app-root/src
USER 0
COPY package*.json ./ COPY package*.json ./
RUN npm ci --omit=dev RUN npm ci --omit=dev
COPY . . COPY . .
RUN chown -R 1001:0 /opt/app-root/src
USER 1001
CMD ["npm", "start"] CMD ["npm", "start"]
+16 -2
View File
@@ -6,7 +6,8 @@ Dieses Projekt automatisiert folgenden Ablauf:
2. Zusammenfassung erzeugen (Status-Verteilung + Ticketliste). 2. Zusammenfassung erzeugen (Status-Verteilung + Ticketliste).
3. Neues Jira Ticket in einem anderen Projekt/Board erstellen. 3. Neues Jira Ticket in einem anderen Projekt/Board erstellen.
4. Optional Xray/Sonar Links und Loesungsversion in den Report aufnehmen. 4. Optional Xray/Sonar Links und Loesungsversion in den Report aufnehmen.
5. HTML-Report erzeugen und optional per E-Mail versenden. 5. Optional eine Atlassian Wiki/Confluence Seite erzeugen.
6. HTML-Report erzeugen und optional per E-Mail versenden.
## Voraussetzungen ## Voraussetzungen
@@ -51,6 +52,16 @@ Empfohlen:
- `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`)
Atlassian Wiki / Confluence optional:
- `ATLASSIAN_WIKI_ENABLED` (`true`/`false`, Default: `false`)
- `ATLASSIAN_WIKI_SPACE_KEY` (Confluence Space Key, Pflicht wenn aktiviert)
- `ATLASSIAN_WIKI_PARENT_PAGE_ID` (optional)
- `ATLASSIAN_WIKI_TITLE_PREFIX` (Default: `Automatischer Jira Wiki Report`)
- `ATLASSIAN_WIKI_BASE_URL` (optional, Default: `JIRA_BASE_URL`)
- `ATLASSIAN_WIKI_USER_EMAIL` (optional, Default: `JIRA_USER_EMAIL`)
- `ATLASSIAN_WIKI_API_TOKEN` (optional, Default: `JIRA_API_TOKEN`)
SMTP optional: SMTP optional:
- `SMTP_ENABLED=true` - `SMTP_ENABLED=true`
@@ -70,7 +81,7 @@ docker build -t its-workflow-npm:local .
Container starten: Container starten:
```bash ```bash
docker run --rm --env-file .env -v "$(pwd)/report-output:/app/report-output" its-workflow-npm:local docker run --rm --env-file .env -v "$(pwd)/report-output:/opt/app-root/src/report-output" its-workflow-npm:local
``` ```
## TeamCity Integration ## TeamCity Integration
@@ -92,6 +103,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.ATLASSIAN_WIKI_ENABLED`
- `env.ATLASSIAN_WIKI_SPACE_KEY`
- `env.ATLASSIAN_WIKI_PARENT_PAGE_ID`
- `env.SMTP_ENABLED` - `env.SMTP_ENABLED`
- `env.SMTP_HOST` ... - `env.SMTP_HOST` ...
+39 -1
View File
@@ -53,7 +53,9 @@ function getConfig() {
); );
} }
return { const wikiEnabled = parseBoolean(process.env.ATLASSIAN_WIKI_ENABLED, false);
const config = {
outputHtmlPath: outputHtmlPath:
process.env.OUTPUT_HTML_PATH || path.join("report-output", "report.html"), process.env.OUTPUT_HTML_PATH || path.join("report-output", "report.html"),
jira: { jira: {
@@ -86,7 +88,41 @@ function getConfig() {
to: parseList(process.env.SMTP_TO), to: parseList(process.env.SMTP_TO),
subjectPrefix: process.env.SMTP_SUBJECT_PREFIX || "Jira Zusammenfassung", subjectPrefix: process.env.SMTP_SUBJECT_PREFIX || "Jira Zusammenfassung",
}, },
wiki: {
enabled: wikiEnabled,
baseUrl: (
process.env.ATLASSIAN_WIKI_BASE_URL || process.env.JIRA_BASE_URL || ""
)
.trim()
.replace(/\/$/, ""),
userEmail: (
process.env.ATLASSIAN_WIKI_USER_EMAIL || process.env.JIRA_USER_EMAIL || ""
).trim(),
apiToken: (
process.env.ATLASSIAN_WIKI_API_TOKEN || process.env.JIRA_API_TOKEN || ""
).trim(),
spaceKey: (process.env.ATLASSIAN_WIKI_SPACE_KEY || "").trim(),
parentPageId: (process.env.ATLASSIAN_WIKI_PARENT_PAGE_ID || "").trim(),
titlePrefix:
process.env.ATLASSIAN_WIKI_TITLE_PREFIX ||
"Automatischer Jira Wiki Report",
},
}; };
if (config.wiki.enabled) {
if (
!config.wiki.baseUrl ||
!config.wiki.userEmail ||
!config.wiki.apiToken ||
!config.wiki.spaceKey
) {
throw new Error(
"ATLASSIAN_WIKI_ENABLED=true, aber ATLASSIAN_WIKI_BASE_URL/USER_EMAIL/API_TOKEN/SPACE_KEY sind nicht vollstaendig gesetzt."
);
}
}
return config;
} }
function printHelp() { function printHelp() {
@@ -104,6 +140,8 @@ Wichtige ENV Variablen:
SOLUTION_VERSION SOLUTION_VERSION
XRAY_REPORT_URL XRAY_REPORT_URL
SONAR_REPORT_URL SONAR_REPORT_URL
ATLASSIAN_WIKI_ENABLED=true|false
ATLASSIAN_WIKI_SPACE_KEY
SMTP_ENABLED=true|false SMTP_ENABLED=true|false
`); `);
} }
+56
View File
@@ -0,0 +1,56 @@
const axios = require("axios");
function createConfluenceClient(config) {
const auth = Buffer.from(
`${config.userEmail}:${config.apiToken}`,
"utf8"
).toString("base64");
const api = axios.create({
baseURL: `${config.baseUrl}/wiki/rest/api`,
headers: {
Authorization: `Basic ${auth}`,
Accept: "application/json",
"Content-Type": "application/json",
},
timeout: 30000,
});
async function createPage({ title, storageBody, spaceKey, parentPageId }) {
const payload = {
type: "page",
title,
space: { key: spaceKey },
body: {
storage: {
value: storageBody,
representation: "storage",
},
},
};
if (parentPageId) {
payload.ancestors = [{ id: String(parentPageId) }];
}
const response = await api.post("/content", payload);
const page = response.data || {};
const base = page?._links?.base || `${config.baseUrl}/wiki`;
const webui = page?._links?.webui || "";
const pageUrl = webui ? new URL(webui, base).toString() : null;
return {
id: page.id || null,
title: page.title || title,
url: pageUrl,
};
}
return {
createPage,
};
}
module.exports = {
createConfluenceClient,
};
+42 -2
View File
@@ -3,7 +3,12 @@ const path = require("node:path");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const { getConfig, printHelp } = require("./config"); const { getConfig, printHelp } = require("./config");
const { createJiraClient } = require("./jira"); const { createJiraClient } = require("./jira");
const { buildReport, buildHtmlSummary } = require("./report"); const {
buildReport,
buildHtmlSummary,
buildConfluenceStorage,
} = require("./report");
const { createConfluenceClient } = require("./confluence");
const { sendReportMail } = require("./email"); const { sendReportMail } = require("./email");
function hasFlag(flag) { function hasFlag(flag) {
@@ -64,6 +69,22 @@ async function run() {
? `${config.jira.baseUrl}/browse/${createdIssueKey}` ? `${config.jira.baseUrl}/browse/${createdIssueKey}`
: null; : null;
let createdWikiPage = null;
if (config.wiki.enabled && !dryRun) {
const confluence = createConfluenceClient(config.wiki);
const wikiTitle = `${config.wiki.titlePrefix} ${dayjs().format(
"YYYY-MM-DD"
)} (${report.totalIssues} Tickets)`;
const wikiBody = buildConfluenceStorage(report, createdIssueUrl);
createdWikiPage = await confluence.createPage({
title: wikiTitle,
storageBody: wikiBody,
spaceKey: config.wiki.spaceKey,
parentPageId: config.wiki.parentPageId,
});
}
const html = buildHtmlSummary(report, createdIssueUrl); const html = buildHtmlSummary(report, createdIssueUrl);
const outputDir = path.dirname(config.outputHtmlPath); const outputDir = path.dirname(config.outputHtmlPath);
fs.mkdirSync(outputDir, { recursive: true }); fs.mkdirSync(outputDir, { recursive: true });
@@ -73,9 +94,16 @@ async function run() {
"YYYY-MM-DD" "YYYY-MM-DD"
)}`; )}`;
const wikiStatusText = config.wiki.enabled
? createdWikiPage?.url ||
(dryRun
? "nicht erstellt (Dry-Run)"
: "erstellt, aber ohne URL in der API-Antwort")
: "uebersprungen (ATLASSIAN_WIKI_ENABLED=false)";
const mailText = `${report.textSummary}\n\nNeues Jira Ticket: ${ const mailText = `${report.textSummary}\n\nNeues Jira Ticket: ${
createdIssueUrl || "nicht erstellt (Dry-Run)" createdIssueUrl || "nicht erstellt (Dry-Run)"
}`; }\nWiki Seite: ${wikiStatusText}`;
const mailResult = await sendReportMail({ const mailResult = await sendReportMail({
smtpConfig: config.smtp, smtpConfig: config.smtp,
@@ -94,6 +122,18 @@ async function run() {
console.log("Neues Jira Ticket: nicht erstellt (Dry-Run)"); console.log("Neues Jira Ticket: nicht erstellt (Dry-Run)");
} }
if (config.wiki.enabled) {
if (createdWikiPage?.url) {
console.log(`Wiki Seite: ${createdWikiPage.url}`);
} else if (dryRun) {
console.log("Wiki Seite: nicht erstellt (Dry-Run)");
} else {
console.log("Wiki Seite: erstellt, aber ohne URL in der API-Antwort.");
}
} else {
console.log("Wiki Seite: uebersprungen (ATLASSIAN_WIKI_ENABLED=false)");
}
if (mailResult.skipped) { if (mailResult.skipped) {
console.log(`E-Mail Versand: uebersprungen (${mailResult.reason})`); console.log(`E-Mail Versand: uebersprungen (${mailResult.reason})`);
} else { } else {
+69
View File
@@ -148,6 +148,74 @@ function buildHtmlSummary(report, createdIssueUrl) {
</html>`; </html>`;
} }
function buildConfluenceStorage(report, createdIssueUrl) {
const lines = [];
lines.push("<h1>Jira Zusammenfassung</h1>");
lines.push(
`<p><strong>Erstellt am:</strong> ${escapeHtml(report.generatedAt)}</p>`
);
lines.push(`<p><strong>JQL:</strong> ${escapeHtml(report.sourceJql)}</p>`);
lines.push(`<p><strong>Gefundene Tickets:</strong> ${report.totalIssues}</p>`);
if (report.solutionVersion) {
lines.push(
`<p><strong>Loesungsversion:</strong> ${escapeHtml(
report.solutionVersion
)}</p>`
);
}
if (createdIssueUrl) {
lines.push(
`<p><strong>Neues Jira Ticket:</strong> <a href="${escapeHtml(
createdIssueUrl
)}">${escapeHtml(createdIssueUrl)}</a></p>`
);
}
if (report.xrayReportUrl || report.sonarReportUrl) {
lines.push("<h2>Report Links</h2>");
lines.push("<ul>");
if (report.xrayReportUrl) {
lines.push(
`<li><a href="${escapeHtml(report.xrayReportUrl)}">Xray Report</a></li>`
);
}
if (report.sonarReportUrl) {
lines.push(
`<li><a href="${escapeHtml(report.sonarReportUrl)}">Sonar Report</a></li>`
);
}
lines.push("</ul>");
}
lines.push("<h2>Status-Verteilung</h2>");
lines.push("<ul>");
for (const status of report.statusCounts) {
lines.push(
`<li>${escapeHtml(status.status)}: ${escapeHtml(status.count)}</li>`
);
}
lines.push("</ul>");
lines.push("<h2>Ticket-Inhalte</h2>");
lines.push("<ul>");
for (const issue of report.issues) {
lines.push(
`<li><a href="${escapeHtml(issue.url)}">${escapeHtml(
issue.key
)}</a>: ${escapeHtml(issue.summary)} | ${escapeHtml(
issue.status
)} | ${escapeHtml(issue.assignee)} | ${escapeHtml(issue.updated)}</li>`
);
}
lines.push("</ul>");
return lines.join("\n");
}
function makeParagraph(text) { function makeParagraph(text) {
return { return {
type: "paragraph", type: "paragraph",
@@ -223,4 +291,5 @@ function buildReport({
module.exports = { module.exports = {
buildReport, buildReport,
buildHtmlSummary, buildHtmlSummary,
buildConfluenceStorage,
}; };
+8 -1
View File
@@ -21,6 +21,13 @@ 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 ATLASSIAN_WIKI_ENABLED `
-e ATLASSIAN_WIKI_BASE_URL `
-e ATLASSIAN_WIKI_USER_EMAIL `
-e ATLASSIAN_WIKI_API_TOKEN `
-e ATLASSIAN_WIKI_SPACE_KEY `
-e ATLASSIAN_WIKI_PARENT_PAGE_ID `
-e ATLASSIAN_WIKI_TITLE_PREFIX `
-e JIRA_DRY_RUN ` -e JIRA_DRY_RUN `
-e OUTPUT_HTML_PATH ` -e OUTPUT_HTML_PATH `
-e SMTP_ENABLED ` -e SMTP_ENABLED `
@@ -32,5 +39,5 @@ docker run --rm `
-e SMTP_FROM ` -e SMTP_FROM `
-e SMTP_TO ` -e SMTP_TO `
-e SMTP_SUBJECT_PREFIX ` -e SMTP_SUBJECT_PREFIX `
-v "${PWD}\report-output:/app/report-output" ` -v "${PWD}\report-output:/opt/app-root/src/report-output" `
$imageName $imageName
+8 -1
View File
@@ -22,6 +22,13 @@ 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 ATLASSIAN_WIKI_ENABLED \
-e ATLASSIAN_WIKI_BASE_URL \
-e ATLASSIAN_WIKI_USER_EMAIL \
-e ATLASSIAN_WIKI_API_TOKEN \
-e ATLASSIAN_WIKI_SPACE_KEY \
-e ATLASSIAN_WIKI_PARENT_PAGE_ID \
-e ATLASSIAN_WIKI_TITLE_PREFIX \
-e JIRA_DRY_RUN \ -e JIRA_DRY_RUN \
-e OUTPUT_HTML_PATH \ -e OUTPUT_HTML_PATH \
-e SMTP_ENABLED \ -e SMTP_ENABLED \
@@ -33,5 +40,5 @@ docker run --rm \
-e SMTP_FROM \ -e SMTP_FROM \
-e SMTP_TO \ -e SMTP_TO \
-e SMTP_SUBJECT_PREFIX \ -e SMTP_SUBJECT_PREFIX \
-v "$(pwd)/report-output:/app/report-output" \ -v "$(pwd)/report-output:/opt/app-root/src/report-output" \
"${IMAGE_NAME}" "${IMAGE_NAME}"