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