diff --git a/.env.example b/.env.example index b006145..142c3a2 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/Dockerfile b/Dockerfile index fea9e9b..bb1a1d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index b5c68b6..297811b 100644 --- a/README.md +++ b/README.md @@ -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` ... diff --git a/src/config.js b/src/config.js index a239c9b..6bd9329 100644 --- a/src/config.js +++ b/src/config.js @@ -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 `); } diff --git a/src/confluence.js b/src/confluence.js new file mode 100644 index 0000000..aac1bae --- /dev/null +++ b/src/confluence.js @@ -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, +}; diff --git a/src/index.js b/src/index.js index 1cfde63..406fd00 100644 --- a/src/index.js +++ b/src/index.js @@ -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 { diff --git a/src/report.js b/src/report.js index 64ca33a..aa4a674 100644 --- a/src/report.js +++ b/src/report.js @@ -148,6 +148,74 @@ function buildHtmlSummary(report, createdIssueUrl) { `; } +function buildConfluenceStorage(report, createdIssueUrl) { + const lines = []; + + lines.push("

Jira Zusammenfassung

"); + lines.push( + `

Erstellt am: ${escapeHtml(report.generatedAt)}

` + ); + lines.push(`

JQL: ${escapeHtml(report.sourceJql)}

`); + lines.push(`

Gefundene Tickets: ${report.totalIssues}

`); + + if (report.solutionVersion) { + lines.push( + `

Loesungsversion: ${escapeHtml( + report.solutionVersion + )}

` + ); + } + + if (createdIssueUrl) { + lines.push( + `

Neues Jira Ticket: ${escapeHtml(createdIssueUrl)}

` + ); + } + + if (report.xrayReportUrl || report.sonarReportUrl) { + lines.push("

Report Links

"); + lines.push(""); + } + + lines.push("

Status-Verteilung

"); + lines.push(""); + + lines.push("

Ticket-Inhalte

"); + lines.push(""); + return lines.join("\n"); +} + function makeParagraph(text) { return { type: "paragraph", @@ -223,4 +291,5 @@ function buildReport({ module.exports = { buildReport, buildHtmlSummary, + buildConfluenceStorage, }; diff --git a/teamcity/run-workflow.ps1 b/teamcity/run-workflow.ps1 index 38668d3..033fedf 100644 --- a/teamcity/run-workflow.ps1 +++ b/teamcity/run-workflow.ps1 @@ -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 diff --git a/teamcity/run-workflow.sh b/teamcity/run-workflow.sh index b0f4295..7767c3d 100644 --- a/teamcity/run-workflow.sh +++ b/teamcity/run-workflow.sh @@ -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}"