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:
@@ -20,6 +20,18 @@ XRAY_REPORT_URL=https://xray.example.com/report/123
|
||||
SONAR_REPORT_URL=https://sonar.example.com/dashboard?id=project
|
||||
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
|
||||
OUTPUT_HTML_PATH=report-output/report.html
|
||||
|
||||
|
||||
+5
-2
@@ -1,10 +1,13 @@
|
||||
FROM node:20-alpine
|
||||
FROM ubi9/nodejs-20
|
||||
|
||||
WORKDIR /app
|
||||
WORKDIR /opt/app-root/src
|
||||
|
||||
USER 0
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
COPY . .
|
||||
RUN chown -R 1001:0 /opt/app-root/src
|
||||
USER 1001
|
||||
|
||||
CMD ["npm", "start"]
|
||||
|
||||
@@ -6,7 +6,8 @@ Dieses Projekt automatisiert folgenden Ablauf:
|
||||
2. Zusammenfassung erzeugen (Status-Verteilung + Ticketliste).
|
||||
3. Neues Jira Ticket in einem anderen Projekt/Board erstellen.
|
||||
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
|
||||
|
||||
@@ -51,6 +52,16 @@ Empfohlen:
|
||||
- `JIRA_DRY_RUN` (`true`/`false`)
|
||||
- `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_ENABLED=true`
|
||||
@@ -70,7 +81,7 @@ docker build -t its-workflow-npm:local .
|
||||
Container starten:
|
||||
|
||||
```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
|
||||
@@ -92,6 +103,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.ATLASSIAN_WIKI_ENABLED`
|
||||
- `env.ATLASSIAN_WIKI_SPACE_KEY`
|
||||
- `env.ATLASSIAN_WIKI_PARENT_PAGE_ID`
|
||||
- `env.SMTP_ENABLED`
|
||||
- `env.SMTP_HOST` ...
|
||||
|
||||
|
||||
+39
-1
@@ -53,7 +53,9 @@ function getConfig() {
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
const wikiEnabled = parseBoolean(process.env.ATLASSIAN_WIKI_ENABLED, false);
|
||||
|
||||
const config = {
|
||||
outputHtmlPath:
|
||||
process.env.OUTPUT_HTML_PATH || path.join("report-output", "report.html"),
|
||||
jira: {
|
||||
@@ -86,7 +88,41 @@ function getConfig() {
|
||||
to: parseList(process.env.SMTP_TO),
|
||||
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() {
|
||||
@@ -104,6 +140,8 @@ Wichtige ENV Variablen:
|
||||
SOLUTION_VERSION
|
||||
XRAY_REPORT_URL
|
||||
SONAR_REPORT_URL
|
||||
ATLASSIAN_WIKI_ENABLED=true|false
|
||||
ATLASSIAN_WIKI_SPACE_KEY
|
||||
SMTP_ENABLED=true|false
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -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
@@ -3,7 +3,12 @@ const path = require("node:path");
|
||||
const dayjs = require("dayjs");
|
||||
const { getConfig, printHelp } = require("./config");
|
||||
const { createJiraClient } = require("./jira");
|
||||
const { buildReport, buildHtmlSummary } = require("./report");
|
||||
const {
|
||||
buildReport,
|
||||
buildHtmlSummary,
|
||||
buildConfluenceStorage,
|
||||
} = require("./report");
|
||||
const { createConfluenceClient } = require("./confluence");
|
||||
const { sendReportMail } = require("./email");
|
||||
|
||||
function hasFlag(flag) {
|
||||
@@ -64,6 +69,22 @@ async function run() {
|
||||
? `${config.jira.baseUrl}/browse/${createdIssueKey}`
|
||||
: 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 outputDir = path.dirname(config.outputHtmlPath);
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
@@ -73,9 +94,16 @@ async function run() {
|
||||
"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: ${
|
||||
createdIssueUrl || "nicht erstellt (Dry-Run)"
|
||||
}`;
|
||||
}\nWiki Seite: ${wikiStatusText}`;
|
||||
|
||||
const mailResult = await sendReportMail({
|
||||
smtpConfig: config.smtp,
|
||||
@@ -94,6 +122,18 @@ async function 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) {
|
||||
console.log(`E-Mail Versand: uebersprungen (${mailResult.reason})`);
|
||||
} else {
|
||||
|
||||
@@ -148,6 +148,74 @@ function buildHtmlSummary(report, createdIssueUrl) {
|
||||
</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) {
|
||||
return {
|
||||
type: "paragraph",
|
||||
@@ -223,4 +291,5 @@ function buildReport({
|
||||
module.exports = {
|
||||
buildReport,
|
||||
buildHtmlSummary,
|
||||
buildConfluenceStorage,
|
||||
};
|
||||
|
||||
@@ -21,6 +21,13 @@ docker run --rm `
|
||||
-e SOLUTION_VERSION `
|
||||
-e XRAY_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 OUTPUT_HTML_PATH `
|
||||
-e SMTP_ENABLED `
|
||||
@@ -32,5 +39,5 @@ docker run --rm `
|
||||
-e SMTP_FROM `
|
||||
-e SMTP_TO `
|
||||
-e SMTP_SUBJECT_PREFIX `
|
||||
-v "${PWD}\report-output:/app/report-output" `
|
||||
-v "${PWD}\report-output:/opt/app-root/src/report-output" `
|
||||
$imageName
|
||||
|
||||
@@ -22,6 +22,13 @@ docker run --rm \
|
||||
-e SOLUTION_VERSION \
|
||||
-e XRAY_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 OUTPUT_HTML_PATH \
|
||||
-e SMTP_ENABLED \
|
||||
@@ -33,5 +40,5 @@ docker run --rm \
|
||||
-e SMTP_FROM \
|
||||
-e SMTP_TO \
|
||||
-e SMTP_SUBJECT_PREFIX \
|
||||
-v "$(pwd)/report-output:/app/report-output" \
|
||||
-v "$(pwd)/report-output:/opt/app-root/src/report-output" \
|
||||
"${IMAGE_NAME}"
|
||||
|
||||
Reference in New Issue
Block a user