feat(workflow): automate Jira summary pipeline with optional Xray/Sonar links and solution version
- add npm workflow to fetch recent Jira issues, summarize them, and create a target Jira ticket - generate HTML report and optional SMTP email output - add Docker runtime and TeamCity runner scripts (sh/ps1) - support optional SOLUTION_VERSION, XRAY_REPORT_URL, SONAR_REPORT_URL - map SOLUTION_VERSION to Jira fixVersions on ticket creation - update README and .env.example with new configuration
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
# Jira Zugang
|
||||||
|
JIRA_BASE_URL=https://your-domain.atlassian.net
|
||||||
|
JIRA_USER_EMAIL=automation@company.com
|
||||||
|
JIRA_API_TOKEN=your-api-token
|
||||||
|
|
||||||
|
# Quell-Tickets (letzte Tickets aus beliebigem Board/Projekt)
|
||||||
|
JIRA_SOURCE_JQL=project = SRC ORDER BY updated DESC
|
||||||
|
JIRA_SOURCE_MAX_RESULTS=10
|
||||||
|
|
||||||
|
# Ziel-Ticket (anderes Board/Projekt)
|
||||||
|
# Entweder direkt Project Key setzen:
|
||||||
|
JIRA_TARGET_PROJECT_KEY=DEST
|
||||||
|
# Oder alternativ ueber Board-ID aufloesen:
|
||||||
|
# JIRA_TARGET_BOARD_ID=123
|
||||||
|
JIRA_TARGET_ISSUE_TYPE=Task
|
||||||
|
JIRA_TARGET_SUMMARY_PREFIX=Weekly Jira Report
|
||||||
|
JIRA_TARGET_LABELS=automation,teamcity
|
||||||
|
SOLUTION_VERSION=1.2.3
|
||||||
|
XRAY_REPORT_URL=https://xray.example.com/report/123
|
||||||
|
SONAR_REPORT_URL=https://sonar.example.com/dashboard?id=project
|
||||||
|
JIRA_DRY_RUN=false
|
||||||
|
|
||||||
|
# HTML-Ausgabe
|
||||||
|
OUTPUT_HTML_PATH=report-output/report.html
|
||||||
|
|
||||||
|
# SMTP (optional)
|
||||||
|
SMTP_ENABLED=false
|
||||||
|
SMTP_HOST=smtp.example.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_SECURE=false
|
||||||
|
SMTP_USER=smtp-user
|
||||||
|
SMTP_PASS=smtp-pass
|
||||||
|
SMTP_FROM=bot@example.com
|
||||||
|
SMTP_TO=pm@example.com,teamlead@example.com
|
||||||
|
SMTP_SUBJECT_PREFIX=Jira Ticket Zusammenfassung
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
report-output/
|
||||||
|
npm-debug.log*
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["npm", "start"]
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Jira Workflow Automation (NPM + Docker + TeamCity)
|
||||||
|
|
||||||
|
Dieses Projekt automatisiert folgenden Ablauf:
|
||||||
|
|
||||||
|
1. Letzte Jira Tickets anhand einer JQL abfragen.
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- Node.js 20+
|
||||||
|
- Docker
|
||||||
|
- Jira Cloud API Token
|
||||||
|
- Optional: SMTP Zugangsdaten fuer Mailversand
|
||||||
|
|
||||||
|
## Lokaler Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
cp .env.example .env
|
||||||
|
npm run dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn alles passt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wichtige Umgebungsvariablen
|
||||||
|
|
||||||
|
Pflicht:
|
||||||
|
|
||||||
|
- `JIRA_BASE_URL`
|
||||||
|
- `JIRA_USER_EMAIL`
|
||||||
|
- `JIRA_API_TOKEN`
|
||||||
|
- `JIRA_TARGET_PROJECT_KEY` oder `JIRA_TARGET_BOARD_ID`
|
||||||
|
|
||||||
|
Empfohlen:
|
||||||
|
|
||||||
|
- `JIRA_SOURCE_JQL` (z. B. `project = SRC ORDER BY updated DESC`)
|
||||||
|
- `JIRA_SOURCE_MAX_RESULTS` (Default: `10`)
|
||||||
|
- `JIRA_TARGET_ISSUE_TYPE` (Default: `Task`)
|
||||||
|
- `JIRA_TARGET_SUMMARY_PREFIX` (Default: `Automatischer Jira Report`)
|
||||||
|
- `JIRA_TARGET_LABELS` (CSV)
|
||||||
|
- `SOLUTION_VERSION` (setzt auch `fixVersions` im Jira Ticket)
|
||||||
|
- `XRAY_REPORT_URL` (optional)
|
||||||
|
- `SONAR_REPORT_URL` (optional)
|
||||||
|
- `JIRA_DRY_RUN` (`true`/`false`)
|
||||||
|
- `OUTPUT_HTML_PATH` (Default: `report-output/report.html`)
|
||||||
|
|
||||||
|
SMTP optional:
|
||||||
|
|
||||||
|
- `SMTP_ENABLED=true`
|
||||||
|
- `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE`
|
||||||
|
- `SMTP_USER`, `SMTP_PASS`
|
||||||
|
- `SMTP_FROM`, `SMTP_TO`
|
||||||
|
- `SMTP_SUBJECT_PREFIX`
|
||||||
|
|
||||||
|
## Docker Nutzung
|
||||||
|
|
||||||
|
Image bauen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## TeamCity Integration
|
||||||
|
|
||||||
|
Empfohlene Build Steps:
|
||||||
|
|
||||||
|
1. `Command Line` (Linux Agent):
|
||||||
|
- Script: `sh teamcity/run-workflow.sh`
|
||||||
|
2. Alternativ `PowerShell` (Windows Agent):
|
||||||
|
- Script file: `teamcity/run-workflow.ps1`
|
||||||
|
|
||||||
|
Lege in TeamCity die Variablen als Parameter vom Typ `env.*` an, z. B.:
|
||||||
|
|
||||||
|
- `env.JIRA_BASE_URL`
|
||||||
|
- `env.JIRA_USER_EMAIL`
|
||||||
|
- `env.JIRA_API_TOKEN` (als Password Parameter)
|
||||||
|
- `env.JIRA_SOURCE_JQL`
|
||||||
|
- `env.JIRA_TARGET_PROJECT_KEY`
|
||||||
|
- `env.SOLUTION_VERSION`
|
||||||
|
- `env.XRAY_REPORT_URL`
|
||||||
|
- `env.SONAR_REPORT_URL`
|
||||||
|
- `env.SMTP_ENABLED`
|
||||||
|
- `env.SMTP_HOST` ...
|
||||||
|
|
||||||
|
Der Report wird im Build-Workspace unter `report-output/report.html` abgelegt und kann als Build Artifact publiziert werden.
|
||||||
|
|
||||||
|
## Ablauf im Fehlerfall
|
||||||
|
|
||||||
|
- Jira API Fehler werden mit Antwortdetails ausgegeben.
|
||||||
|
- Bei `SMTP_ENABLED=true` ohne vollstaendige SMTP Daten wird der Lauf abgebrochen.
|
||||||
|
- Bei `JIRA_DRY_RUN=true` wird kein Ticket erstellt, aber Report + optional Mail erzeugt.
|
||||||
Generated
+325
@@ -0,0 +1,325 @@
|
|||||||
|
{
|
||||||
|
"name": "its-workflow-npm",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "its-workflow-npm",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.13.6",
|
||||||
|
"dayjs": "^1.11.20",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
|
"nodemailer": "^8.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.13.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
||||||
|
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.11",
|
||||||
|
"form-data": "^4.0.5",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
|
||||||
|
"integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "17.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
|
||||||
|
"integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nodemailer": {
|
||||||
|
"version": "8.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.3.tgz",
|
||||||
|
"integrity": "sha512-JQNBqvK+bj3NMhUFR3wmCl3SYcOeMotDiwDBvIoCuQdF0PvlIY0BH+FJ2CG7u4cXKPChplE78oowlH/Otsc4ZQ==",
|
||||||
|
"license": "MIT-0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "its-workflow-npm",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Automatisierung fuer Jira-Zusammenfassungen inkl. Ticket-Erstellung und HTML-Mail Versand",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/index.js",
|
||||||
|
"dry-run": "node src/index.js --dry-run",
|
||||||
|
"test": "node src/index.js --help"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.13.6",
|
||||||
|
"dayjs": "^1.11.20",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
|
"nodemailer": "^8.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
const path = require("node:path");
|
||||||
|
const dotenv = require("dotenv");
|
||||||
|
|
||||||
|
dotenv.config({ quiet: true });
|
||||||
|
|
||||||
|
function parseBoolean(value, defaultValue = false) {
|
||||||
|
if (value === undefined || value === null || value === "") {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = String(value).trim().toLowerCase();
|
||||||
|
return ["1", "true", "yes", "y", "on"].includes(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNumber(value, defaultValue) {
|
||||||
|
if (value === undefined || value === null || value === "") {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (Number.isNaN(parsed)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseList(value) {
|
||||||
|
if (!value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value)
|
||||||
|
.split(/[,;]+/)
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequiredEnv(name) {
|
||||||
|
const value = process.env[name];
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`Fehlende Pflichtvariable: ${name}`);
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfig() {
|
||||||
|
const targetProjectKey = (process.env.JIRA_TARGET_PROJECT_KEY || "").trim();
|
||||||
|
const targetBoardId = (process.env.JIRA_TARGET_BOARD_ID || "").trim();
|
||||||
|
|
||||||
|
if (!targetProjectKey && !targetBoardId) {
|
||||||
|
throw new Error(
|
||||||
|
"Bitte JIRA_TARGET_PROJECT_KEY oder JIRA_TARGET_BOARD_ID setzen."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
outputHtmlPath:
|
||||||
|
process.env.OUTPUT_HTML_PATH || path.join("report-output", "report.html"),
|
||||||
|
jira: {
|
||||||
|
baseUrl: getRequiredEnv("JIRA_BASE_URL").replace(/\/$/, ""),
|
||||||
|
userEmail: getRequiredEnv("JIRA_USER_EMAIL"),
|
||||||
|
apiToken: getRequiredEnv("JIRA_API_TOKEN"),
|
||||||
|
sourceJql:
|
||||||
|
process.env.JIRA_SOURCE_JQL ||
|
||||||
|
"updated >= -14d ORDER BY updated DESC",
|
||||||
|
sourceMaxResults: parseNumber(process.env.JIRA_SOURCE_MAX_RESULTS, 10),
|
||||||
|
targetProjectKey,
|
||||||
|
targetBoardId,
|
||||||
|
targetIssueType: process.env.JIRA_TARGET_ISSUE_TYPE || "Task",
|
||||||
|
targetSummaryPrefix:
|
||||||
|
process.env.JIRA_TARGET_SUMMARY_PREFIX || "Automatischer Jira Report",
|
||||||
|
targetLabels: parseList(process.env.JIRA_TARGET_LABELS),
|
||||||
|
solutionVersion: (process.env.SOLUTION_VERSION || "").trim(),
|
||||||
|
xrayReportUrl: (process.env.XRAY_REPORT_URL || "").trim(),
|
||||||
|
sonarReportUrl: (process.env.SONAR_REPORT_URL || "").trim(),
|
||||||
|
dryRun: parseBoolean(process.env.JIRA_DRY_RUN, false),
|
||||||
|
},
|
||||||
|
smtp: {
|
||||||
|
enabled: parseBoolean(process.env.SMTP_ENABLED, false),
|
||||||
|
host: process.env.SMTP_HOST || "",
|
||||||
|
port: parseNumber(process.env.SMTP_PORT, 587),
|
||||||
|
secure: parseBoolean(process.env.SMTP_SECURE, false),
|
||||||
|
user: process.env.SMTP_USER || "",
|
||||||
|
pass: process.env.SMTP_PASS || "",
|
||||||
|
from: process.env.SMTP_FROM || "",
|
||||||
|
to: parseList(process.env.SMTP_TO),
|
||||||
|
subjectPrefix: process.env.SMTP_SUBJECT_PREFIX || "Jira Zusammenfassung",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.log(`
|
||||||
|
Nutzung:
|
||||||
|
npm start
|
||||||
|
npm run dry-run
|
||||||
|
|
||||||
|
Wichtige ENV Variablen:
|
||||||
|
JIRA_BASE_URL
|
||||||
|
JIRA_USER_EMAIL
|
||||||
|
JIRA_API_TOKEN
|
||||||
|
JIRA_SOURCE_JQL
|
||||||
|
JIRA_TARGET_PROJECT_KEY oder JIRA_TARGET_BOARD_ID
|
||||||
|
SOLUTION_VERSION
|
||||||
|
XRAY_REPORT_URL
|
||||||
|
SONAR_REPORT_URL
|
||||||
|
SMTP_ENABLED=true|false
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getConfig,
|
||||||
|
parseBoolean,
|
||||||
|
printHelp,
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
const nodemailer = require("nodemailer");
|
||||||
|
|
||||||
|
async function sendReportMail({ smtpConfig, subject, html, text }) {
|
||||||
|
if (!smtpConfig.enabled) {
|
||||||
|
return { skipped: true, reason: "SMTP_ENABLED=false" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!smtpConfig.host ||
|
||||||
|
!smtpConfig.port ||
|
||||||
|
!smtpConfig.user ||
|
||||||
|
!smtpConfig.pass ||
|
||||||
|
!smtpConfig.from ||
|
||||||
|
smtpConfig.to.length === 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"SMTP ist aktiviert, aber SMTP_HOST/PORT/USER/PASS/FROM/TO sind nicht vollstaendig gesetzt."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transport = nodemailer.createTransport({
|
||||||
|
host: smtpConfig.host,
|
||||||
|
port: smtpConfig.port,
|
||||||
|
secure: smtpConfig.secure,
|
||||||
|
auth: {
|
||||||
|
user: smtpConfig.user,
|
||||||
|
pass: smtpConfig.pass,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const info = await transport.sendMail({
|
||||||
|
from: smtpConfig.from,
|
||||||
|
to: smtpConfig.to.join(", "),
|
||||||
|
subject,
|
||||||
|
text,
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { skipped: false, messageId: info.messageId };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sendReportMail,
|
||||||
|
};
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
const { getConfig, printHelp } = require("./config");
|
||||||
|
const { createJiraClient } = require("./jira");
|
||||||
|
const { buildReport, buildHtmlSummary } = require("./report");
|
||||||
|
const { sendReportMail } = require("./email");
|
||||||
|
|
||||||
|
function hasFlag(flag) {
|
||||||
|
return process.argv.includes(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
if (hasFlag("--help")) {
|
||||||
|
printHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
const dryRun = config.jira.dryRun || hasFlag("--dry-run");
|
||||||
|
|
||||||
|
const jira = createJiraClient(config.jira);
|
||||||
|
const issues = await jira.getRecentIssues(
|
||||||
|
config.jira.sourceJql,
|
||||||
|
config.jira.sourceMaxResults
|
||||||
|
);
|
||||||
|
|
||||||
|
if (issues.length === 0) {
|
||||||
|
throw new Error("Keine Jira Tickets fuer die konfigurierte JQL gefunden.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const report = buildReport({
|
||||||
|
issues,
|
||||||
|
jiraBaseUrl: config.jira.baseUrl,
|
||||||
|
sourceJql: config.jira.sourceJql,
|
||||||
|
solutionVersion: config.jira.solutionVersion,
|
||||||
|
xrayReportUrl: config.jira.xrayReportUrl,
|
||||||
|
sonarReportUrl: config.jira.sonarReportUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
let targetProjectKey = config.jira.targetProjectKey;
|
||||||
|
if (!targetProjectKey && config.jira.targetBoardId) {
|
||||||
|
targetProjectKey = await jira.resolveProjectKeyFromBoardId(
|
||||||
|
config.jira.targetBoardId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticketSummary = `${config.jira.targetSummaryPrefix} ${dayjs().format("YYYY-MM-DD")} (${report.totalIssues} Tickets)`;
|
||||||
|
|
||||||
|
let createdIssue = null;
|
||||||
|
if (!dryRun) {
|
||||||
|
createdIssue = await jira.createIssue({
|
||||||
|
projectKey: targetProjectKey,
|
||||||
|
issueType: config.jira.targetIssueType,
|
||||||
|
summary: ticketSummary,
|
||||||
|
descriptionAdf: report.adfDescription,
|
||||||
|
labels: config.jira.targetLabels,
|
||||||
|
solutionVersion: config.jira.solutionVersion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdIssueKey = createdIssue?.key || null;
|
||||||
|
const createdIssueUrl = createdIssueKey
|
||||||
|
? `${config.jira.baseUrl}/browse/${createdIssueKey}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const html = buildHtmlSummary(report, createdIssueUrl);
|
||||||
|
const outputDir = path.dirname(config.outputHtmlPath);
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
fs.writeFileSync(config.outputHtmlPath, html, "utf8");
|
||||||
|
|
||||||
|
const mailSubject = `${config.smtp.subjectPrefix} - ${dayjs().format(
|
||||||
|
"YYYY-MM-DD"
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
const mailText = `${report.textSummary}\n\nNeues Jira Ticket: ${
|
||||||
|
createdIssueUrl || "nicht erstellt (Dry-Run)"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const mailResult = await sendReportMail({
|
||||||
|
smtpConfig: config.smtp,
|
||||||
|
subject: mailSubject,
|
||||||
|
html,
|
||||||
|
text: mailText,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Workflow abgeschlossen.");
|
||||||
|
console.log(`Issues verarbeitet: ${report.totalIssues}`);
|
||||||
|
console.log(`HTML Report: ${config.outputHtmlPath}`);
|
||||||
|
|
||||||
|
if (createdIssueUrl) {
|
||||||
|
console.log(`Neues Jira Ticket: ${createdIssueUrl}`);
|
||||||
|
} else {
|
||||||
|
console.log("Neues Jira Ticket: nicht erstellt (Dry-Run)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mailResult.skipped) {
|
||||||
|
console.log(`E-Mail Versand: uebersprungen (${mailResult.reason})`);
|
||||||
|
} else {
|
||||||
|
console.log(`E-Mail Versand: erfolgreich (Message-ID ${mailResult.messageId})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((error) => {
|
||||||
|
console.error("Workflow fehlgeschlagen.");
|
||||||
|
console.error(error?.response?.data || error.message || error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+89
@@ -0,0 +1,89 @@
|
|||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
function createJiraClient(config) {
|
||||||
|
const auth = Buffer.from(
|
||||||
|
`${config.userEmail}:${config.apiToken}`,
|
||||||
|
"utf8"
|
||||||
|
).toString("base64");
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Basic ${auth}`,
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: `${config.baseUrl}/rest/api/3`,
|
||||||
|
headers,
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const agileApi = axios.create({
|
||||||
|
baseURL: `${config.baseUrl}/rest/agile/1.0`,
|
||||||
|
headers,
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getRecentIssues(jql, maxResults) {
|
||||||
|
const payload = {
|
||||||
|
jql,
|
||||||
|
maxResults,
|
||||||
|
fields: ["summary", "status", "assignee", "updated", "labels"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await api.post("/search", payload);
|
||||||
|
return response.data.issues || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveProjectKeyFromBoardId(boardId) {
|
||||||
|
const response = await agileApi.get(`/board/${boardId}/configuration`);
|
||||||
|
const board = response.data || {};
|
||||||
|
const projectKey = board?.location?.projectKey;
|
||||||
|
|
||||||
|
if (!projectKey) {
|
||||||
|
throw new Error(
|
||||||
|
`Konnte aus Board ${boardId} keinen Project Key ableiten.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createIssue({
|
||||||
|
projectKey,
|
||||||
|
issueType,
|
||||||
|
summary,
|
||||||
|
descriptionAdf,
|
||||||
|
labels,
|
||||||
|
solutionVersion,
|
||||||
|
}) {
|
||||||
|
const fields = {
|
||||||
|
project: { key: projectKey },
|
||||||
|
issuetype: { name: issueType },
|
||||||
|
summary,
|
||||||
|
description: descriptionAdf,
|
||||||
|
labels: labels || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (solutionVersion) {
|
||||||
|
fields.fixVersions = [{ name: solutionVersion }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
fields,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await api.post("/issue", payload);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getRecentIssues,
|
||||||
|
resolveProjectKeyFromBoardId,
|
||||||
|
createIssue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createJiraClient,
|
||||||
|
};
|
||||||
+226
@@ -0,0 +1,226 @@
|
|||||||
|
const dayjs = require("dayjs");
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatIssue(issue, jiraBaseUrl) {
|
||||||
|
const assignee =
|
||||||
|
issue.fields?.assignee?.displayName || issue.fields?.assignee?.emailAddress;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: issue.key,
|
||||||
|
summary: issue.fields?.summary || "(ohne Summary)",
|
||||||
|
status: issue.fields?.status?.name || "(ohne Status)",
|
||||||
|
assignee: assignee || "nicht zugewiesen",
|
||||||
|
updated: issue.fields?.updated
|
||||||
|
? dayjs(issue.fields.updated).format("YYYY-MM-DD HH:mm")
|
||||||
|
: "-",
|
||||||
|
labels: issue.fields?.labels || [],
|
||||||
|
url: `${jiraBaseUrl}/browse/${issue.key}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusStats(issues) {
|
||||||
|
const countMap = new Map();
|
||||||
|
|
||||||
|
for (const issue of issues) {
|
||||||
|
countMap.set(issue.status, (countMap.get(issue.status) || 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(countMap.entries())
|
||||||
|
.map(([status, count]) => ({ status, count }))
|
||||||
|
.sort((a, b) => b.count - a.count);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTextSummary(report) {
|
||||||
|
const lines = [];
|
||||||
|
lines.push(`Jira Zusammenfassung (${report.generatedAt})`);
|
||||||
|
lines.push("");
|
||||||
|
lines.push(`JQL: ${report.sourceJql}`);
|
||||||
|
lines.push(`Gefundene Tickets: ${report.totalIssues}`);
|
||||||
|
if (report.solutionVersion) {
|
||||||
|
lines.push(`Loesungsversion: ${report.solutionVersion}`);
|
||||||
|
}
|
||||||
|
if (report.xrayReportUrl) {
|
||||||
|
lines.push(`Xray Report: ${report.xrayReportUrl}`);
|
||||||
|
}
|
||||||
|
if (report.sonarReportUrl) {
|
||||||
|
lines.push(`Sonar Report: ${report.sonarReportUrl}`);
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
lines.push("Status-Verteilung:");
|
||||||
|
|
||||||
|
for (const item of report.statusCounts) {
|
||||||
|
lines.push(`- ${item.status}: ${item.count}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("");
|
||||||
|
lines.push("Tickets:");
|
||||||
|
for (const issue of report.issues) {
|
||||||
|
lines.push(
|
||||||
|
`- ${issue.key}: ${issue.summary} | ${issue.status} | ${issue.assignee} | ${issue.updated}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHtmlSummary(report, createdIssueUrl) {
|
||||||
|
const statsRows = report.statusCounts
|
||||||
|
.map(
|
||||||
|
(item) =>
|
||||||
|
`<tr><td>${escapeHtml(item.status)}</td><td>${item.count}</td></tr>`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
const issueRows = report.issues
|
||||||
|
.map((issue) => {
|
||||||
|
return `<tr>
|
||||||
|
<td><a href="${escapeHtml(issue.url)}">${escapeHtml(issue.key)}</a></td>
|
||||||
|
<td>${escapeHtml(issue.summary)}</td>
|
||||||
|
<td>${escapeHtml(issue.status)}</td>
|
||||||
|
<td>${escapeHtml(issue.assignee)}</td>
|
||||||
|
<td>${escapeHtml(issue.updated)}</td>
|
||||||
|
</tr>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
const createdIssueBlock = createdIssueUrl
|
||||||
|
? `<p><strong>Neues Jira Ticket:</strong> <a href="${escapeHtml(createdIssueUrl)}">${escapeHtml(createdIssueUrl)}</a></p>`
|
||||||
|
: "<p><strong>Neues Jira Ticket:</strong> Nicht erstellt (Dry-Run).</p>";
|
||||||
|
|
||||||
|
const xrayBlock = report.xrayReportUrl
|
||||||
|
? `<p><strong>Xray Report:</strong> <a href="${escapeHtml(report.xrayReportUrl)}">${escapeHtml(report.xrayReportUrl)}</a></p>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const sonarBlock = report.sonarReportUrl
|
||||||
|
? `<p><strong>Sonar Report:</strong> <a href="${escapeHtml(report.sonarReportUrl)}">${escapeHtml(report.sonarReportUrl)}</a></p>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const solutionVersionBlock = report.solutionVersion
|
||||||
|
? `<p><strong>Loesungsversion:</strong> ${escapeHtml(report.solutionVersion)}</p>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Jira Zusammenfassung</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; color: #1f2937; margin: 24px; }
|
||||||
|
h1 { margin: 0 0 12px; font-size: 22px; }
|
||||||
|
p { margin: 8px 0; }
|
||||||
|
table { border-collapse: collapse; width: 100%; margin-top: 8px; margin-bottom: 20px; }
|
||||||
|
th, td { border: 1px solid #e5e7eb; padding: 8px; text-align: left; vertical-align: top; }
|
||||||
|
th { background: #f3f4f6; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Jira Zusammenfassung</h1>
|
||||||
|
<p><strong>Erstellt am:</strong> ${escapeHtml(report.generatedAt)}</p>
|
||||||
|
<p><strong>JQL:</strong> ${escapeHtml(report.sourceJql)}</p>
|
||||||
|
<p><strong>Gefundene Tickets:</strong> ${report.totalIssues}</p>
|
||||||
|
${solutionVersionBlock}
|
||||||
|
${xrayBlock}
|
||||||
|
${sonarBlock}
|
||||||
|
${createdIssueBlock}
|
||||||
|
|
||||||
|
<h2>Status-Verteilung</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Status</th><th>Anzahl</th></tr></thead>
|
||||||
|
<tbody>${statsRows}</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Tickets</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Key</th><th>Summary</th><th>Status</th><th>Assignee</th><th>Updated</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${issueRows}</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeParagraph(text) {
|
||||||
|
return {
|
||||||
|
type: "paragraph",
|
||||||
|
content: [{ type: "text", text }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAdfDescription(report) {
|
||||||
|
const content = [
|
||||||
|
makeParagraph(`Automatischer Jira Report vom ${report.generatedAt}`),
|
||||||
|
makeParagraph(`Quelle (JQL): ${report.sourceJql}`),
|
||||||
|
makeParagraph(`Gefundene Tickets: ${report.totalIssues}`),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (report.solutionVersion) {
|
||||||
|
content.push(makeParagraph(`Loesungsversion: ${report.solutionVersion}`));
|
||||||
|
}
|
||||||
|
if (report.xrayReportUrl) {
|
||||||
|
content.push(makeParagraph(`Xray Report: ${report.xrayReportUrl}`));
|
||||||
|
}
|
||||||
|
if (report.sonarReportUrl) {
|
||||||
|
content.push(makeParagraph(`Sonar Report: ${report.sonarReportUrl}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
content.push(
|
||||||
|
makeParagraph("Status-Verteilung:"),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const status of report.statusCounts) {
|
||||||
|
content.push(makeParagraph(`- ${status.status}: ${status.count}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
content.push(makeParagraph("Tickets:"));
|
||||||
|
|
||||||
|
for (const issue of report.issues) {
|
||||||
|
const line = `${issue.key}: ${issue.summary} | ${issue.status} | ${issue.assignee} | ${issue.updated}`;
|
||||||
|
content.push(makeParagraph(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "doc",
|
||||||
|
version: 1,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReport({
|
||||||
|
issues,
|
||||||
|
jiraBaseUrl,
|
||||||
|
sourceJql,
|
||||||
|
solutionVersion = "",
|
||||||
|
xrayReportUrl = "",
|
||||||
|
sonarReportUrl = "",
|
||||||
|
}) {
|
||||||
|
const normalized = issues.map((issue) => formatIssue(issue, jiraBaseUrl));
|
||||||
|
|
||||||
|
const report = {
|
||||||
|
generatedAt: dayjs().format("YYYY-MM-DD HH:mm"),
|
||||||
|
sourceJql,
|
||||||
|
solutionVersion,
|
||||||
|
xrayReportUrl,
|
||||||
|
sonarReportUrl,
|
||||||
|
totalIssues: normalized.length,
|
||||||
|
statusCounts: statusStats(normalized),
|
||||||
|
issues: normalized,
|
||||||
|
};
|
||||||
|
|
||||||
|
report.textSummary = buildTextSummary(report);
|
||||||
|
report.adfDescription = buildAdfDescription(report);
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
buildReport,
|
||||||
|
buildHtmlSummary,
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$imageTag = if ($env:TEAMCITY_BUILD_NUMBER) { $env:TEAMCITY_BUILD_NUMBER } else { "local" }
|
||||||
|
$imageName = "its-workflow-npm:$imageTag"
|
||||||
|
|
||||||
|
Write-Host "Builde Docker Image $imageName"
|
||||||
|
docker build -t $imageName .
|
||||||
|
|
||||||
|
Write-Host "Starte Workflow Container"
|
||||||
|
docker run --rm `
|
||||||
|
-e JIRA_BASE_URL `
|
||||||
|
-e JIRA_USER_EMAIL `
|
||||||
|
-e JIRA_API_TOKEN `
|
||||||
|
-e JIRA_SOURCE_JQL `
|
||||||
|
-e JIRA_SOURCE_MAX_RESULTS `
|
||||||
|
-e JIRA_TARGET_PROJECT_KEY `
|
||||||
|
-e JIRA_TARGET_BOARD_ID `
|
||||||
|
-e JIRA_TARGET_ISSUE_TYPE `
|
||||||
|
-e JIRA_TARGET_SUMMARY_PREFIX `
|
||||||
|
-e JIRA_TARGET_LABELS `
|
||||||
|
-e SOLUTION_VERSION `
|
||||||
|
-e XRAY_REPORT_URL `
|
||||||
|
-e SONAR_REPORT_URL `
|
||||||
|
-e JIRA_DRY_RUN `
|
||||||
|
-e OUTPUT_HTML_PATH `
|
||||||
|
-e SMTP_ENABLED `
|
||||||
|
-e SMTP_HOST `
|
||||||
|
-e SMTP_PORT `
|
||||||
|
-e SMTP_SECURE `
|
||||||
|
-e SMTP_USER `
|
||||||
|
-e SMTP_PASS `
|
||||||
|
-e SMTP_FROM `
|
||||||
|
-e SMTP_TO `
|
||||||
|
-e SMTP_SUBJECT_PREFIX `
|
||||||
|
-v "${PWD}\report-output:/app/report-output" `
|
||||||
|
$imageName
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
IMAGE_TAG="${TEAMCITY_BUILD_NUMBER:-local}"
|
||||||
|
IMAGE_NAME="its-workflow-npm:${IMAGE_TAG}"
|
||||||
|
|
||||||
|
echo "Builde Docker Image ${IMAGE_NAME}"
|
||||||
|
docker build -t "${IMAGE_NAME}" .
|
||||||
|
|
||||||
|
echo "Starte Workflow Container"
|
||||||
|
docker run --rm \
|
||||||
|
-e JIRA_BASE_URL \
|
||||||
|
-e JIRA_USER_EMAIL \
|
||||||
|
-e JIRA_API_TOKEN \
|
||||||
|
-e JIRA_SOURCE_JQL \
|
||||||
|
-e JIRA_SOURCE_MAX_RESULTS \
|
||||||
|
-e JIRA_TARGET_PROJECT_KEY \
|
||||||
|
-e JIRA_TARGET_BOARD_ID \
|
||||||
|
-e JIRA_TARGET_ISSUE_TYPE \
|
||||||
|
-e JIRA_TARGET_SUMMARY_PREFIX \
|
||||||
|
-e JIRA_TARGET_LABELS \
|
||||||
|
-e SOLUTION_VERSION \
|
||||||
|
-e XRAY_REPORT_URL \
|
||||||
|
-e SONAR_REPORT_URL \
|
||||||
|
-e JIRA_DRY_RUN \
|
||||||
|
-e OUTPUT_HTML_PATH \
|
||||||
|
-e SMTP_ENABLED \
|
||||||
|
-e SMTP_HOST \
|
||||||
|
-e SMTP_PORT \
|
||||||
|
-e SMTP_SECURE \
|
||||||
|
-e SMTP_USER \
|
||||||
|
-e SMTP_PASS \
|
||||||
|
-e SMTP_FROM \
|
||||||
|
-e SMTP_TO \
|
||||||
|
-e SMTP_SUBJECT_PREFIX \
|
||||||
|
-v "$(pwd)/report-output:/app/report-output" \
|
||||||
|
"${IMAGE_NAME}"
|
||||||
Reference in New Issue
Block a user