Migrate all repos into monorepo context folders
Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
"""Prueft ob das Remote-Repo Commits hat."""
|
||||
import urllib.request, ssl, json
|
||||
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
TOKEN = secrets.get("GITLAB_TOKEN_AUDIT", "")
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
# Pruefe Projekt
|
||||
url = "https://git.tech.rz.db.de/api/v4/projects/AndreKnie%2Fproject-audit"
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header("PRIVATE-TOKEN", TOKEN)
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=15)
|
||||
proj = json.loads(resp.read().decode("utf-8"))
|
||||
print(f"Projekt: {proj['path_with_namespace']}")
|
||||
print(f"Default Branch: {proj.get('default_branch', '?')}")
|
||||
print(f"Letzter Push: {proj.get('last_activity_at', '?')}")
|
||||
print(f"Leer: {proj.get('empty_repo', '?')}")
|
||||
except Exception as e:
|
||||
print(f"Projekt-Fehler: {e}")
|
||||
exit(1)
|
||||
|
||||
# Pruefe Commits
|
||||
print()
|
||||
url2 = "https://git.tech.rz.db.de/api/v4/projects/AndreKnie%2Fproject-audit/repository/commits?per_page=5"
|
||||
req2 = urllib.request.Request(url2)
|
||||
req2.add_header("PRIVATE-TOKEN", TOKEN)
|
||||
try:
|
||||
resp2 = urllib.request.urlopen(req2, context=ctx, timeout=15)
|
||||
commits = json.loads(resp2.read().decode("utf-8"))
|
||||
print(f"Remote Commits: {len(commits)}")
|
||||
for c in commits:
|
||||
print(f" {c['short_id']} | {c['created_at'][:16]} | {c['title'][:50]}")
|
||||
except Exception as e:
|
||||
print(f"Commits-Fehler: {e}")
|
||||
|
||||
# Pruefe Branches
|
||||
print()
|
||||
url3 = "https://git.tech.rz.db.de/api/v4/projects/AndreKnie%2Fproject-audit/repository/branches"
|
||||
req3 = urllib.request.Request(url3)
|
||||
req3.add_header("PRIVATE-TOKEN", TOKEN)
|
||||
try:
|
||||
resp3 = urllib.request.urlopen(req3, context=ctx, timeout=15)
|
||||
branches = json.loads(resp3.read().decode("utf-8"))
|
||||
print(f"Branches: {len(branches)}")
|
||||
for b in branches:
|
||||
print(f" {b['name']}: {b['commit']['short_id']} ({b['commit']['created_at'][:16]})")
|
||||
except Exception as e:
|
||||
print(f"Branches-Fehler: {e}")
|
||||
@@ -0,0 +1,77 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Prueft welche Unterseiten unter der Analyse-Stammseite existieren
|
||||
#>
|
||||
|
||||
$ConfluenceUrl = "https://arija-confluence.jaas.service.deutschebahn.com"
|
||||
$SpaceKey = "BES"
|
||||
$ParentPageId = "581013135"
|
||||
$ApiBase = "$ConfluenceUrl/rest/api"
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
# Token aus .secrets laden
|
||||
$SecretsPath = Join-Path $ScriptDir "..\.secrets"
|
||||
$PAT = $null
|
||||
if (Test-Path $SecretsPath) {
|
||||
Get-Content $SecretsPath | ForEach-Object {
|
||||
if ($_ -match "^CONFLUENCE_TOKEN=(.+)$") {
|
||||
$val = $Matches[1].Trim()
|
||||
if ($val -ne "HIER_TOKEN_EINTRAGEN") { $PAT = $val }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $PAT) { $PAT = Read-Host "Confluence PAT" }
|
||||
$Headers = @{ "Authorization" = "Bearer $PAT"; "Content-Type" = "application/json" }
|
||||
|
||||
Write-Host "=== Confluence Seiten-Check ===" -ForegroundColor Cyan
|
||||
Write-Host "Parent: $ParentPageId"
|
||||
Write-Host ""
|
||||
|
||||
# Alle Unterseiten laden
|
||||
$Uri = "$ApiBase/content/$ParentPageId/child/page?limit=50&expand=version"
|
||||
try {
|
||||
$Resp = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$Data = $Resp.Content | ConvertFrom-Json
|
||||
|
||||
Write-Host "Gefundene Unterseiten: $($Data.results.Count)" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host ("{0,-5} {1,-55} {2,-10} {3}" -f "Ver", "Titel", "ID", "Status")
|
||||
Write-Host ("-" * 90)
|
||||
|
||||
$Data.results | Sort-Object { $_.title } | ForEach-Object {
|
||||
$ver = $_.version.number
|
||||
$title = $_.title
|
||||
$id = $_.id
|
||||
Write-Host ("{0,-5} {1,-55} {2,-10}" -f "v$ver", $title, $id)
|
||||
}
|
||||
} catch {
|
||||
Write-Host "FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Titel-Suche fuer Seiten 1-5 ===" -ForegroundColor Cyan
|
||||
|
||||
$Titles = @(
|
||||
"1. Uebersicht und Gesamtbild",
|
||||
"2. Identifizierte Problemfelder",
|
||||
"3. Technische Roadmap 2026 - Zusammenfassung",
|
||||
"4. GitLab Projektlandschaft",
|
||||
"5. Abkuerzungsverzeichnis"
|
||||
)
|
||||
|
||||
foreach ($t in $Titles) {
|
||||
$SearchUri = "$ApiBase/content?spaceKey=$SpaceKey&title=$([Uri]::EscapeDataString($t))&type=page"
|
||||
try {
|
||||
$SearchResp = Invoke-WebRequest -Uri $SearchUri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$SearchData = $SearchResp.Content | ConvertFrom-Json
|
||||
if ($SearchData.results -and $SearchData.results.Count -gt 0) {
|
||||
Write-Host " GEFUNDEN: $t (ID: $($SearchData.results[0].id))" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " NICHT GEFUNDEN: $t" -ForegroundColor Red
|
||||
}
|
||||
} catch {
|
||||
Write-Host " FEHLER bei: $t - $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Loescht die Test-Seite "TEST Umlaut-Pruefung" (ID: 583338908)
|
||||
#>
|
||||
|
||||
$ConfluenceUrl = "https://arija-confluence.jaas.service.deutschebahn.com"
|
||||
$ApiBase = "$ConfluenceUrl/rest/api"
|
||||
$PageId = "583338908"
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
# Token aus .secrets laden
|
||||
$SecretsPath = Join-Path $ScriptDir "..\.secrets"
|
||||
$PAT = $null
|
||||
if (Test-Path $SecretsPath) {
|
||||
Get-Content $SecretsPath | ForEach-Object {
|
||||
if ($_ -match "^CONFLUENCE_TOKEN=(.+)$") {
|
||||
$val = $Matches[1].Trim()
|
||||
if ($val -ne "HIER_TOKEN_EINTRAGEN") { $PAT = $val }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $PAT) { $PAT = Read-Host "Confluence PAT" }
|
||||
$Headers = @{ "Authorization" = "Bearer $PAT"; "Content-Type" = "application/json" }
|
||||
|
||||
Write-Host "=== Loesche Test-Seite ===" -ForegroundColor Yellow
|
||||
Write-Host " Seite: TEST Umlaut-Pruefung (ID: $PageId)"
|
||||
|
||||
# Pruefen ob Seite existiert
|
||||
try {
|
||||
$Resp = Invoke-WebRequest -Uri "$ApiBase/content/$PageId" -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$Data = $Resp.Content | ConvertFrom-Json
|
||||
Write-Host " Gefunden: $($Data.title)" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host " Seite nicht gefunden oder bereits geloescht." -ForegroundColor DarkGray
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Loeschen
|
||||
try {
|
||||
Invoke-WebRequest -Uri "$ApiBase/content/$PageId" -Method Delete -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
Write-Host " GELOESCHT!" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Confluence Space Export fuer pathOS / Bestellsystem
|
||||
.DESCRIPTION
|
||||
Exportiert die Seitenstruktur und Inhalte aus dem Confluence Space BES.
|
||||
Kompatibel mit PowerShell 5.1 - keine Sonderzeichen.
|
||||
#>
|
||||
|
||||
$ConfluenceUrl = "https://arija-confluence.jaas.service.deutschebahn.com"
|
||||
$SpaceKey = "BES"
|
||||
$RootPageId = "196749641"
|
||||
$ApiBase = "$ConfluenceUrl/rest/api"
|
||||
$PerPage = 50
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$OutputDir = Join-Path (Split-Path -Parent $ScriptDir) "data\confluence-export"
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
Write-Host "============================================================"
|
||||
Write-Host " pathOS / Bestellsystem - Confluence Export"
|
||||
Write-Host "============================================================"
|
||||
Write-Host " Confluence: $ConfluenceUrl"
|
||||
Write-Host " Space: $SpaceKey"
|
||||
Write-Host ""
|
||||
Write-Host "Authentifizierung:"
|
||||
Write-Host " Option 1: Personal Access Token (PAT) - empfohlen"
|
||||
Write-Host " Option 2: Benutzername + Passwort"
|
||||
Write-Host ""
|
||||
|
||||
$AuthMethod = Read-Host "PAT oder Passwort? (pat/pw)"
|
||||
|
||||
if ($AuthMethod -eq "pw") {
|
||||
$Username = Read-Host "Benutzername"
|
||||
$Password = Read-Host "Passwort"
|
||||
$Pair = "${Username}:${Password}"
|
||||
$Bytes = [System.Text.Encoding]::ASCII.GetBytes($Pair)
|
||||
$Base64 = [System.Convert]::ToBase64String($Bytes)
|
||||
$Headers = @{ "Authorization" = "Basic $Base64" }
|
||||
} else {
|
||||
$PAT = Read-Host "Personal Access Token"
|
||||
$Headers = @{ "Authorization" = "Bearer $PAT" }
|
||||
}
|
||||
|
||||
function Safe-String($val) {
|
||||
if ($null -eq $val) { return "" }
|
||||
return [string]$val
|
||||
}
|
||||
|
||||
function Invoke-ConfluenceApi {
|
||||
param([string]$Endpoint, [hashtable]$Params = @{})
|
||||
$QS = ($Params.GetEnumerator() | ForEach-Object {
|
||||
"$($_.Key)=$([Uri]::EscapeDataString($_.Value))"
|
||||
}) -join "&"
|
||||
$Uri = "$ApiBase$Endpoint"
|
||||
if ($QS) { $Uri = "$Uri`?$QS" }
|
||||
try {
|
||||
$Resp = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
return ($Resp.Content | ConvertFrom-Json)
|
||||
} catch {
|
||||
Write-Host " FEHLER bei $Uri : $($_.Exception.Message)" -ForegroundColor Red
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ChildPages {
|
||||
param([string]$PageId)
|
||||
$AllChildren = @()
|
||||
$Start = 0
|
||||
do {
|
||||
$Result = Invoke-ConfluenceApi -Endpoint "/content/$PageId/child/page" -Params @{
|
||||
start = "$Start"
|
||||
limit = "$PerPage"
|
||||
expand = "version"
|
||||
}
|
||||
if ($null -eq $Result) { break }
|
||||
$Pages = $Result.results
|
||||
if ($null -eq $Pages -or $Pages.Count -eq 0) { break }
|
||||
$AllChildren += $Pages
|
||||
$Start += $PerPage
|
||||
$Size = 0
|
||||
if ($Result.size) { $Size = $Result.size }
|
||||
} while ($Size -ge $PerPage)
|
||||
return $AllChildren
|
||||
}
|
||||
|
||||
function Get-PageContent {
|
||||
param([string]$PageId)
|
||||
return Invoke-ConfluenceApi -Endpoint "/content/$PageId" -Params @{
|
||||
expand = "body.storage,version,ancestors,metadata.labels,space"
|
||||
}
|
||||
}
|
||||
|
||||
function Convert-HtmlToText($html) {
|
||||
if ($null -eq $html) { return "" }
|
||||
$text = $html -replace '<br\s*/?>', "`n"
|
||||
$text = $text -replace '<p[^>]*>', "`n"
|
||||
$text = $text -replace '</p>', ""
|
||||
$text = $text -replace '<li[^>]*>', "`n- "
|
||||
$text = $text -replace '<h[1-6][^>]*>', "`n## "
|
||||
$text = $text -replace '</h[1-6]>', "`n"
|
||||
$text = $text -replace '<tr[^>]*>', "`n| "
|
||||
$text = $text -replace '<td[^>]*>', " | "
|
||||
$text = $text -replace '<th[^>]*>', " | "
|
||||
$text = $text -replace '<[^>]+>', ''
|
||||
Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue
|
||||
try { $text = [System.Web.HttpUtility]::HtmlDecode($text) } catch {}
|
||||
return $text.Trim()
|
||||
}
|
||||
|
||||
function Export-PageTree {
|
||||
param(
|
||||
[string]$PageId,
|
||||
[string]$ParentPath,
|
||||
[int]$Depth = 0,
|
||||
[ref]$Counter
|
||||
)
|
||||
$Indent = " " * $Depth
|
||||
$Counter.Value++
|
||||
$Num = $Counter.Value
|
||||
|
||||
$Page = Get-PageContent -PageId $PageId
|
||||
if ($null -eq $Page) { return $null }
|
||||
|
||||
$Title = Safe-String $Page.title
|
||||
Write-Host "$Indent[$Num] $Title"
|
||||
|
||||
$Labels = @()
|
||||
if ($Page.metadata -and $Page.metadata.labels -and $Page.metadata.labels.results) {
|
||||
$Labels = @($Page.metadata.labels.results | ForEach-Object { $_.name })
|
||||
}
|
||||
|
||||
$VersionNum = 0
|
||||
if ($Page.version) { $VersionNum = $Page.version.number }
|
||||
|
||||
$PageInfo = [PSCustomObject]@{
|
||||
id = $Page.id
|
||||
title = $Title
|
||||
version = $VersionNum
|
||||
labels = $Labels
|
||||
depth = $Depth
|
||||
path = "$ParentPath/$Title"
|
||||
children = @()
|
||||
}
|
||||
|
||||
# Inhalt speichern
|
||||
$HtmlContent = ""
|
||||
$TextContent = ""
|
||||
if ($Page.body -and $Page.body.storage) {
|
||||
$HtmlContent = Safe-String $Page.body.storage.value
|
||||
$TextContent = Convert-HtmlToText $HtmlContent
|
||||
}
|
||||
|
||||
$SafeTitle = $Title -replace '[\\/:*?"<>|]', '_'
|
||||
if ($SafeTitle.Length -gt 80) { $SafeTitle = $SafeTitle.Substring(0, 80) }
|
||||
$PageDir = Join-Path $OutputDir "pages"
|
||||
|
||||
# Markdown
|
||||
$MdLines = @()
|
||||
$MdLines += "# $Title"
|
||||
$MdLines += ""
|
||||
$MdLines += "> Confluence Page ID: $($Page.id)"
|
||||
$MdLines += "> Version: $VersionNum"
|
||||
$MdLines += "> Pfad: $($PageInfo.path)"
|
||||
$MdLines += "> Labels: $(($Labels) -join ', ')"
|
||||
$MdLines += ""
|
||||
$MdLines += "---"
|
||||
$MdLines += ""
|
||||
$MdLines += $TextContent
|
||||
$MdPath = Join-Path $PageDir "$($Page.id)_$SafeTitle.md"
|
||||
$MdLines -join "`n" | Out-File -FilePath $MdPath -Encoding UTF8
|
||||
|
||||
# HTML
|
||||
$HtmlPath = Join-Path $PageDir "$($Page.id)_$SafeTitle.html"
|
||||
$HtmlContent | Out-File -FilePath $HtmlPath -Encoding UTF8
|
||||
|
||||
# Kindseiten
|
||||
$Children = @(Get-ChildPages -PageId $PageId)
|
||||
$ChildInfos = @()
|
||||
foreach ($Child in $Children) {
|
||||
$ChildInfo = Export-PageTree -PageId $Child.id -ParentPath $PageInfo.path -Depth ($Depth + 1) -Counter $Counter
|
||||
if ($null -ne $ChildInfo) {
|
||||
$ChildInfos += $ChildInfo
|
||||
}
|
||||
}
|
||||
$PageInfo.children = $ChildInfos
|
||||
|
||||
return $PageInfo
|
||||
}
|
||||
|
||||
function Write-TreeLines {
|
||||
param($Node, [int]$IndentLevel = 0, [System.Collections.ArrayList]$Lines)
|
||||
$Spaces = " " * $IndentLevel
|
||||
$null = $Lines.Add("${Spaces}+-- $($Node.title) (ID: $($Node.id), v$($Node.version))")
|
||||
foreach ($Child in $Node.children) {
|
||||
Write-TreeLines -Node $Child -IndentLevel ($IndentLevel + 1) -Lines $Lines
|
||||
}
|
||||
}
|
||||
|
||||
# === MAIN ===
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path "$OutputDir\pages" -Force | Out-Null
|
||||
Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue
|
||||
|
||||
try {
|
||||
Write-Host ""
|
||||
Write-Host ">> Teste Verbindung..." -ForegroundColor Yellow
|
||||
$TestResult = Invoke-ConfluenceApi -Endpoint "/space/$SpaceKey"
|
||||
if ($null -eq $TestResult) {
|
||||
Write-Host " Verbindung fehlgeschlagen. Token/Credentials pruefen." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host " Space gefunden: $($TestResult.name) ($SpaceKey)" -ForegroundColor Green
|
||||
|
||||
Write-Host ""
|
||||
Write-Host ">> Exportiere Seitenbaum ab Seite $RootPageId..." -ForegroundColor Yellow
|
||||
$Counter = [ref]0
|
||||
$Tree = Export-PageTree -PageId $RootPageId -ParentPath "" -Depth 0 -Counter $Counter
|
||||
|
||||
Write-Host ""
|
||||
Write-Host ">> $($Counter.Value) Seiten exportiert" -ForegroundColor Green
|
||||
|
||||
# Baumstruktur
|
||||
$TreeLines = [System.Collections.ArrayList]::new()
|
||||
Write-TreeLines -Node $Tree -Lines $TreeLines
|
||||
$TreePath = Join-Path $OutputDir "page-tree.md"
|
||||
$TreeOut = @()
|
||||
$TreeOut += "# Confluence Seitenbaum - pathOS ($SpaceKey)"
|
||||
$TreeOut += ""
|
||||
$TreeOut += "> Exportiert: $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
|
||||
$TreeOut += "> Seiten: $($Counter.Value)"
|
||||
$TreeOut += ""
|
||||
$TreeOut += $TreeLines
|
||||
$TreeOut -join "`n" | Out-File -FilePath $TreePath -Encoding UTF8
|
||||
Write-Host " Baumstruktur: $TreePath"
|
||||
|
||||
# JSON-Index
|
||||
$JsonPath = Join-Path $OutputDir "page-index.json"
|
||||
$Tree | ConvertTo-Json -Depth 20 | Out-File -FilePath $JsonPath -Encoding UTF8
|
||||
Write-Host " JSON-Index: $JsonPath"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
Write-Host " FERTIG! $($Counter.Value) Seiten exportiert." -ForegroundColor Green
|
||||
Write-Host " Ergebnisse: $OutputDir" -ForegroundColor Green
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
|
||||
} catch {
|
||||
Write-Host ""
|
||||
Write-Host "FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
if ($_.Exception.Message -match "401|403") {
|
||||
Write-Host " Authentifizierung fehlgeschlagen." -ForegroundColor Yellow
|
||||
}
|
||||
if ($_.Exception.Message -match "resolve|Remotename") {
|
||||
Write-Host " Verbindung fehlgeschlagen. VPN aktiv?" -ForegroundColor Yellow
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
"""Pusht Meilensteinplan 12 Monate als Confluence-Seite 27."""
|
||||
import urllib.request, json, ssl, urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
TOKEN = secrets.get("CONFLUENCE_TOKEN", "")
|
||||
BASE = "https://arija-confluence.jaas.service.deutschebahn.com/rest/api"
|
||||
SPACE = "BES"
|
||||
PARENT = "581013135"
|
||||
TITLE = "27. Meilensteinplan 12 Monate (Jun 2026 - Mai 2027)"
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
html = """<h1>Meilensteinplan pathOS — 12 Monate</h1>
|
||||
<p><strong>Stand:</strong> 2026-05-12 | <strong>Status:</strong> ENTWURF<br/>
|
||||
<strong>Vision:</strong> pathOS als Kernplattform eines integrierten InfraGO-Bestellportals<br/>
|
||||
<strong>Prioritäten:</strong> Stabilität → Regulatorik → Kunden-Features<br/>
|
||||
<strong>Kapazität:</strong> ~400 Issues/Monat (alle Teams), ~35 Personen</p>
|
||||
|
||||
<ac:structured-macro ac:name="info"><ac:rich-text-body>
|
||||
<p><strong>4 Phasen, 13 Meilensteine:</strong> Stabilisieren → Normalbetrieb + Regulatorik → Skalieren + Reorg → Integrieren + Vision</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
|
||||
<h2>Grafische Übersicht: Timeline</h2>
|
||||
|
||||
<table style="width:100%; border-collapse:collapse; font-size:12px;">
|
||||
<tr style="background:#f0f0f0;">
|
||||
<th style="width:18%; padding:4px; border:1px solid #ccc;">Meilenstein</th>
|
||||
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Jun</th>
|
||||
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Jul</th>
|
||||
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Aug</th>
|
||||
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Sep</th>
|
||||
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Okt</th>
|
||||
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Nov</th>
|
||||
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Dez</th>
|
||||
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Jan</th>
|
||||
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Feb</th>
|
||||
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Mär</th>
|
||||
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Apr</th>
|
||||
<th style="width:6.8%; padding:4px; border:1px solid #ccc; text-align:center;">Mai</th>
|
||||
</tr>
|
||||
"""
|
||||
html += """<tr style="background:#fff3e0; font-weight:bold;">
|
||||
<td style="padding:2px 4px; border:1px solid #ccc;" colspan="13">Phase 1: STABILISIEREN</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:4px; border:1px solid #ccc;">M1: Spring Boot 4 ⚠</td>
|
||||
<td style="background:#e53935; border:1px solid #ccc; text-align:center; color:white;">★</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:4px; border:1px solid #ccc;">M2: Bug-Rate <25%</td>
|
||||
<td style="background:#ff9800; border:1px solid #ccc;"></td>
|
||||
<td style="background:#ff9800; border:1px solid #ccc; text-align:center; color:white;">★</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:4px; border:1px solid #ccc;">M3: Führungswechsel</td>
|
||||
<td style="border:1px solid #ccc;"></td>
|
||||
<td style="background:#1565c0; border:1px solid #ccc; text-align:center; color:white;">★</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
</tr>
|
||||
"""
|
||||
html += """<tr style="background:#e3f2fd; font-weight:bold;">
|
||||
<td style="padding:2px 4px; border:1px solid #ccc;" colspan="13">Phase 2: NORMALBETRIEB + REGULATORIK</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:4px; border:1px solid #ccc;">M4: SL Silber Plus ⚠</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
<td style="background:#e53935; border:1px solid #ccc; text-align:center; color:white;">★</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:4px; border:1px solid #ccc;">M5: Abrechnung TTT</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
<td style="background:#ff9800; border:1px solid #ccc;"></td>
|
||||
<td style="background:#ff9800; border:1px solid #ccc;"></td>
|
||||
<td style="background:#ff9800; border:1px solid #ccc; text-align:center; color:white;">★</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:4px; border:1px solid #ccc;">M6: NAÄ produktiv</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
<td style="background:#4caf50; border:1px solid #ccc;"></td>
|
||||
<td style="background:#4caf50; border:1px solid #ccc; text-align:center; color:white;">★</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:4px; border:1px solid #ccc;">M7: OpsDev formiert</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
<td style="background:#7b1fa2; border:1px solid #ccc;"></td>
|
||||
<td style="background:#7b1fa2; border:1px solid #ccc; text-align:center; color:white;">★</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
</tr>
|
||||
"""
|
||||
html += """<tr style="background:#e8f5e9; font-weight:bold;">
|
||||
<td style="padding:2px 4px; border:1px solid #ccc;" colspan="13">Phase 3: SKALIEREN + REORG</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:4px; border:1px solid #ccc;">M8: FplJ 28 ⚠</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
<td style="background:#e53935; border:1px solid #ccc;"></td>
|
||||
<td style="background:#e53935; border:1px solid #ccc;"></td>
|
||||
<td style="background:#e53935; border:1px solid #ccc; text-align:center; color:white;">★</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:4px; border:1px solid #ccc;">M9: Rechnungsbeginn TTT ⚠</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
<td style="background:#e53935; border:1px solid #ccc; text-align:center; color:white;">★</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:4px; border:1px solid #ccc;">M10: Reorg abgeschlossen</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
<td style="background:#7b1fa2; border:1px solid #ccc;"></td>
|
||||
<td style="background:#7b1fa2; border:1px solid #ccc;"></td>
|
||||
<td style="background:#7b1fa2; border:1px solid #ccc; text-align:center; color:white;">★</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
</tr>
|
||||
"""
|
||||
html += """<tr style="background:#fce4ec; font-weight:bold;">
|
||||
<td style="padding:2px 4px; border:1px solid #ccc;" colspan="13">Phase 4: INTEGRIEREN + VISION</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:4px; border:1px solid #ccc;">M11: TrassenOrder-Integr.</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
<td style="background:#1565c0; border:1px solid #ccc; text-align:center; color:white;">★</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:4px; border:1px solid #ccc;">M12: Plattform-Vision</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
<td style="background:#1565c0; border:1px solid #ccc;"></td>
|
||||
<td style="background:#1565c0; border:1px solid #ccc; text-align:center; color:white;">★</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:4px; border:1px solid #ccc;">M13: Kundenzufriedenheit</td>
|
||||
<td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td><td style="border:1px solid #ccc;"></td>
|
||||
<td style="background:#4caf50; border:1px solid #ccc;"></td>
|
||||
<td style="background:#4caf50; border:1px solid #ccc;"></td>
|
||||
<td style="background:#4caf50; border:1px solid #ccc; text-align:center; color:white;">★</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p><strong>Legende:</strong> ★ = Deadline | ⚠ = Harte externe Deadline |
|
||||
<span style="background:#e53935; color:white; padding:2px 6px;">MUSS</span>
|
||||
<span style="background:#ff9800; color:white; padding:2px 6px;">HOCH</span>
|
||||
<span style="background:#4caf50; color:white; padding:2px 6px;">Feature</span>
|
||||
<span style="background:#1565c0; color:white; padding:2px 6px;">Strategie</span>
|
||||
<span style="background:#7b1fa2; color:white; padding:2px 6px;">Reorg</span></p>
|
||||
"""
|
||||
html += """
|
||||
<h2>Phase 1: STABILISIEREN (Jun – Jul 2026)</h2>
|
||||
<ac:structured-macro ac:name="panel"><ac:parameter ac:name="title">Ziel</ac:parameter><ac:rich-text-body>
|
||||
<p>Technische Schulden abbauen, Hypercare beenden, Führungswechsel vorbereiten</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
|
||||
<h3>M1: Spring Boot 4 abgeschlossen (30. Jun 2026) ⚠</h3>
|
||||
<table>
|
||||
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
|
||||
<tr><td>SV, AV, IFP auf Spring Boot 4</td><td>CIB (Saurav, Hans-Henning)</td><td>🔄 In Arbeit</td></tr>
|
||||
<tr><td>CI, AV-Trasse auf Spring Boot 4</td><td>Zero (O2CZERO-7043)</td><td>🔄 In Arbeit</td></tr>
|
||||
<tr><td>Alle Services auf Java 21</td><td>Alle</td><td>Teilweise</td></tr>
|
||||
<tr><td>Regressionstests nach Upgrade</td><td>QA + Teams</td><td>Offen</td></tr>
|
||||
</table>
|
||||
<ac:structured-macro ac:name="warning"><ac:rich-text-body>
|
||||
<p><strong>Risiko:</strong> EOL 3.5 ist Juni 2026 — kein Security-Support mehr danach. MUSS-Termin.</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
|
||||
<h3>M2: Bug-Rate Portal unter 25% (31. Jul 2026)</h3>
|
||||
<table>
|
||||
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
|
||||
<tr><td>Entkopplung Zuglaufpunkte/Zugcharakteristik (Hauptursache)</td><td>404</td><td>Offen</td></tr>
|
||||
<tr><td>Fehlermeldungen-Konzept umsetzen</td><td>404</td><td>Offen</td></tr>
|
||||
<tr><td>E2E-Tests (Playwright) für Top-10-Flows</td><td>404</td><td>Offen</td></tr>
|
||||
<tr><td>Testumgebung mit TPN dauerhaft verfügbar</td><td>404 + OPs</td><td>Offen</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>M3: Führungswechsel + Hypercare-Ende (Jul 2026)</h3>
|
||||
<table>
|
||||
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
|
||||
<tr><td>Neue Führung eingesetzt</td><td>Management</td><td>Offen</td></tr>
|
||||
<tr><td>Hypercare-Kriterien definiert und erfüllt</td><td>PO + BO</td><td>Offen</td></tr>
|
||||
<tr><td>Übergang zu Normalbetrieb dokumentiert</td><td>Alle</td><td>Offen</td></tr>
|
||||
<tr><td>Support-Prozesse definiert (Eskalation, SLAs)</td><td>BSSUPPORT + OPs</td><td>Offen</td></tr>
|
||||
</table>
|
||||
"""
|
||||
html += """
|
||||
<h2>Phase 2: NORMALBETRIEB + REGULATORIK (Aug – Nov 2026)</h2>
|
||||
<ac:structured-macro ac:name="panel"><ac:parameter ac:name="title">Ziel</ac:parameter><ac:rich-text-body>
|
||||
<p>Vertragliche Pflichten erfüllen, Abrechnung stabilisieren, Team-Reorg starten</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
|
||||
<h3>M4: Service Level Silber Plus (31. Aug 2026) ⚠</h3>
|
||||
<table>
|
||||
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
|
||||
<tr><td>Netcool-Anbindung produktiv</td><td>OPs</td><td>🔄 In Arbeit</td></tr>
|
||||
<tr><td>Monitoring-Konzept abgeschlossen</td><td>OPs + Zero</td><td>🔄 In Arbeit</td></tr>
|
||||
<tr><td>Disaster-Recovery-Test 3 bestanden</td><td>OPs</td><td>🔄 Validierung</td></tr>
|
||||
<tr><td>Deployment-Automatisierung (Smoketests)</td><td>OPs/DevOps</td><td>🔄 In Arbeit</td></tr>
|
||||
<tr><td>Secret-Rotation implementiert</td><td>OPs</td><td>Offen</td></tr>
|
||||
<tr><td>Kubernetes-Namespaces umgestellt</td><td>OPs</td><td>🔄 In Arbeit</td></tr>
|
||||
</table>
|
||||
<ac:structured-macro ac:name="warning"><ac:rich-text-body>
|
||||
<p><strong>Risiko:</strong> Vertragsverletzung bei Versäumnis. Keine Verhandlungsmasse.</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
|
||||
<h3>M5: Abrechnung TTT produktionsreif (31. Okt 2026)</h3>
|
||||
<table>
|
||||
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
|
||||
<tr><td>20h-Zug Abrechnung (TTTSOL-2149) entblockt</td><td>CIB</td><td>❌ Blocked</td></tr>
|
||||
<tr><td>Zugtrasse mit Umleitung ohne VT-Wechsel (TTTSOL-2147)</td><td>CIB</td><td>Offen</td></tr>
|
||||
<tr><td>Abrechnung fremde Infrastruktur (TTTSOL-2148)</td><td>CIB</td><td>Offen</td></tr>
|
||||
<tr><td>Neue Zugnummer Umleitungsfall (TTTSOL-2035)</td><td>CIB</td><td>🔄 In Arbeit</td></tr>
|
||||
<tr><td>VDV-Erweiterung für AC Trasse (strategisch)</td><td>CIB + Zero</td><td>Offen</td></tr>
|
||||
<tr><td>Alle 41 Abrechnungs-Tickets priorisiert</td><td>CIB PO</td><td>Offen</td></tr>
|
||||
</table>
|
||||
<ac:structured-macro ac:name="note"><ac:rich-text-body>
|
||||
<p>Rechnungsbeginn TTT ist Januar 2027. Bis Oktober müssen kritische Fälle gelöst sein. Nov/Dez = Buffer für Regressionstests.</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
|
||||
<h3>M6: NAÄ implizite Annahme produktiv (30. Sep 2026)</h3>
|
||||
<table>
|
||||
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
|
||||
<tr><td>O2CCIB-6931 assigned und implementiert</td><td>CIB</td><td>⚠ Geschoben</td></tr>
|
||||
<tr><td>Prozess-Design mit Fachbereich abgestimmt</td><td>CIB BA</td><td>Offen</td></tr>
|
||||
<tr><td>E2E-Test NAÄ-Szenario</td><td>CIB + 404</td><td>Offen</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>M7: Team-Reorg Phase 1 — OpsDev formiert (30. Sep 2026)</h3>
|
||||
<table>
|
||||
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
|
||||
<tr><td>OpsDev-Team definiert (~6 Personen)</td><td>Management + Leads</td><td>Offen</td></tr>
|
||||
<tr><td>OpsDev-Lead benannt</td><td>Management</td><td>Offen</td></tr>
|
||||
<tr><td>Jan Lubenow Wissenstransfer (50% abgeschlossen)</td><td>DevOps</td><td>⚠ Kritisch</td></tr>
|
||||
<tr><td>Bug-Triage-Prozess definiert</td><td>OpsDev-Lead</td><td>Offen</td></tr>
|
||||
<tr><td>OPs-Rotation beendet (feste Zuordnung)</td><td>Management</td><td>Offen</td></tr>
|
||||
</table>
|
||||
"""
|
||||
html += """
|
||||
<h2>Phase 3: SKALIEREN + REORG (Dez 2026 – Feb 2027)</h2>
|
||||
<ac:structured-macro ac:name="panel"><ac:parameter ac:name="title">Ziel</ac:parameter><ac:rich-text-body>
|
||||
<p>Fahrplanwechsel absichern, Team-Umstellung abschließen, Plattform-Denken etablieren</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
|
||||
<h3>M8: Fahrplanwechsel FplJ 28 (Dez 2026) ⚠</h3>
|
||||
<table>
|
||||
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
|
||||
<tr><td>GelV-Erweiterungen (TTT-Meilenstein)</td><td>CIB + Zero</td><td>Offen</td></tr>
|
||||
<tr><td>OTN-Vergabe Fpl 2027 (TTTSOL-1683)</td><td>CIB</td><td>🔄 In Arbeit</td></tr>
|
||||
<tr><td>ujBau Funktionalität</td><td>Alle</td><td>🔄 In Arbeit</td></tr>
|
||||
<tr><td>FplJ-28-spezifische Validierungen getestet</td><td>QA</td><td>Offen</td></tr>
|
||||
<tr><td>Daten-Partitionierung (Performance)</td><td>CIB/Zero</td><td>Offen</td></tr>
|
||||
</table>
|
||||
<ac:structured-macro ac:name="warning"><ac:rich-text-body>
|
||||
<p><strong>Risiko:</strong> Fahrplanwechsel ist harter externer Termin. Kein Verschieben möglich.</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
|
||||
<h3>M9: Rechnungsbeginn TTT (Jan 2027) ⚠</h3>
|
||||
<table>
|
||||
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
|
||||
<tr><td>Alle kritischen Abrechnungsfälle gelöst (aus M5)</td><td>CIB</td><td>Abhängig von M5</td></tr>
|
||||
<tr><td>Rechnungslauf erfolgreich getestet (Staging)</td><td>CIB + QA</td><td>Offen</td></tr>
|
||||
<tr><td>Monitoring für Abrechnungsfehler aktiv</td><td>OPs/OpsDev</td><td>Offen</td></tr>
|
||||
<tr><td>Fallback-Prozess bei Abrechnungsfehlern definiert</td><td>CIB + BO</td><td>Offen</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>M10: Team-Reorg abgeschlossen (31. Jan 2027)</h3>
|
||||
<table>
|
||||
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
|
||||
<tr><td>2 fachliche Teams formiert ("Bestellen" + "Verarbeiten")</td><td>Management</td><td>Offen</td></tr>
|
||||
<tr><td>Service-Ownership klar zugeordnet</td><td>Team-Leads</td><td>Offen</td></tr>
|
||||
<tr><td>PO-Struktur definiert</td><td>Management</td><td>Offen</td></tr>
|
||||
<tr><td>Wissenstransfer abgeschlossen</td><td>Alle</td><td>Offen</td></tr>
|
||||
<tr><td>Erste Sprints in neuer Struktur erfolgreich</td><td>Teams</td><td>Offen</td></tr>
|
||||
<tr><td>Metriken definiert (Bug-Rate, Durchlaufzeit, Deploy-Freq.)</td><td>BO + Leads</td><td>Offen</td></tr>
|
||||
</table>
|
||||
<ac:structured-macro ac:name="note"><ac:rich-text-body>
|
||||
<p>Reorg NACH Fahrplanwechsel abschließen — nicht während des kritischen Zeitraums umbauen.</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
"""
|
||||
html += """
|
||||
<h2>Phase 4: INTEGRIEREN + VISION (Feb – Mai 2027)</h2>
|
||||
<ac:structured-macro ac:name="panel"><ac:parameter ac:name="title">Ziel</ac:parameter><ac:rich-text-body>
|
||||
<p>TrassenOrder-Learnings integrieren, Plattform-Architektur vorbereiten, Kundenzufriedenheit steigern</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
|
||||
<h3>M11: TrassenOrder-Integration definiert (28. Feb 2027)</h3>
|
||||
<table>
|
||||
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
|
||||
<tr><td>TrassenOrder Livegang evaluiert (Q4 2026)</td><td>TraPo + BO</td><td>Offen</td></tr>
|
||||
<tr><td>Abgrenzung pathOS/TrassenOrder dokumentiert</td><td>POs + Architektur</td><td>Offen</td></tr>
|
||||
<tr><td>API-Vertrag zwischen pathOS und TrassenOrder</td><td>CIB/Zero + TraPo</td><td>Offen</td></tr>
|
||||
<tr><td>Entscheidung: GelV bei pathOS oder TraPo?</td><td>BO + Management</td><td>Offen</td></tr>
|
||||
<tr><td>Übernahme-Szenario TraPo → pathOS-Team bewertet</td><td>Management</td><td>Offen</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>M12: Plattform-Architektur-Vision (31. März 2027)</h3>
|
||||
<table>
|
||||
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
|
||||
<tr><td>Architektur-Vision "Integriertes Bestellportal" dokumentiert</td><td>Architektur + BO</td><td>Offen</td></tr>
|
||||
<tr><td>Kernkomponenten als integrierbare Module identifiziert</td><td>Architektur</td><td>Offen</td></tr>
|
||||
<tr><td>Schnittstellen-Strategie (API-First) definiert</td><td>Architektur + Teams</td><td>Offen</td></tr>
|
||||
<tr><td>Roadmap für Anlage + Nebenleistungen (nächste 12M)</td><td>BO + POs</td><td>Offen</td></tr>
|
||||
<tr><td>TPN-Ablösung Zeitplan konkretisiert</td><td>POs + Kunden</td><td>Offen</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>M13: Kundenzufriedenheit messbar verbessert (Mai 2027)</h3>
|
||||
<table>
|
||||
<tr><th>Was</th><th>Wer</th><th>Status</th></tr>
|
||||
<tr><td>Bug-Rate Portal unter 15% (von 33%)</td><td>Team "Bestellen"</td><td>Offen</td></tr>
|
||||
<tr><td>Vorprüfung Portal produktiv</td><td>CIB + 404</td><td>Offen</td></tr>
|
||||
<tr><td>Expertenmodus verfügbar</td><td>404</td><td>Offen</td></tr>
|
||||
<tr><td>Übersichtsseite Vorgänge live</td><td>404</td><td>Offen</td></tr>
|
||||
<tr><td>Kundenfeedback-Loop etabliert</td><td>PO + UX</td><td>Offen</td></tr>
|
||||
<tr><td>NPS oder CSAT Baseline gemessen</td><td>PO + BO</td><td>Offen</td></tr>
|
||||
</table>
|
||||
"""
|
||||
html += """
|
||||
<h2>Kritischer Pfad & Abhängigkeiten</h2>
|
||||
<table>
|
||||
<tr><th>Pfad</th><th>Abhängigkeit</th><th>Konsequenz bei Verzögerung</th></tr>
|
||||
<tr><td>M1 → M4</td><td>Spring Boot 4 muss fertig sein für SL Silber+ (Security-Compliance)</td><td>Vertragsverletzung</td></tr>
|
||||
<tr><td>M5 → M9</td><td>Abrechnung muss bis Okt stehen für Rechnungsbeginn Jan</td><td>Falsche Rechnungen an Kunden</td></tr>
|
||||
<tr><td>M3 → M7 → M10</td><td>Führungswechsel ermöglicht Reorg, Reorg nach FplJ abschließen</td><td>Produktivitätsverlust</td></tr>
|
||||
<tr><td>M8 (extern)</td><td>Fahrplanwechsel ist Fixpunkt — alles andere drumherum planen</td><td>Fahrplan gefährdet</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Risiko-Matrix</h2>
|
||||
<table>
|
||||
<tr><th>Risiko</th><th>Wahrscheinlichkeit</th><th>Impact</th><th>Mitigation</th></tr>
|
||||
<tr style="background:#ffcdd2;"><td>Abrechnung bleibt blocked (20h-Zug)</td><td>HOCH</td><td>KRITISCH</td><td>Eskalation an TTT-Programm, Workaround</td></tr>
|
||||
<tr style="background:#ffcdd2;"><td>Bug-Rate 404 steigt weiter</td><td>HOCH</td><td>HOCH</td><td>Entkopplung Zuglaufpunkte priorisieren</td></tr>
|
||||
<tr><td>SL Silber+ Deadline gerissen</td><td>Mittel</td><td>HOCH</td><td>Früh mit UKA abstimmen, Teilabnahme?</td></tr>
|
||||
<tr><td>Reorg destabilisiert Teams</td><td>Mittel</td><td>HOCH</td><td>Sukzessiv, nicht Big Bang. Nach FplJ.</td></tr>
|
||||
<tr><td>Jan Lubenow fällt aus (SPOF)</td><td>Niedrig</td><td>KRITISCH</td><td>Wissenstransfer ab sofort priorisieren</td></tr>
|
||||
<tr><td>TrassenOrder scheitert/pivotiert</td><td>Mittel</td><td>MITTEL</td><td>pathOS bleibt autark, GelV intern weiter</td></tr>
|
||||
<tr><td>Spring Boot 4 nicht rechtzeitig</td><td>Niedrig</td><td>KRITISCH</td><td>Fokus, keine Ablenkung</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Reorg-Zeitplan (sukzessiv)</h2>
|
||||
<table>
|
||||
<tr><th>Zeitraum</th><th>Schritt</th><th>Risiko</th></tr>
|
||||
<tr><td><strong>Jul 2026</strong></td><td>Führungswechsel, Vision kommunizieren</td><td>Niedrig</td></tr>
|
||||
<tr><td><strong>Aug-Sep 2026</strong></td><td>OpsDev-Team formieren (aus OPs + DevOps-Teilen)</td><td>Mittel</td></tr>
|
||||
<tr><td><strong>Okt 2026</strong></td><td>OPs-Rotation beenden, feste Zuordnung</td><td>Niedrig</td></tr>
|
||||
<tr><td><strong>Nov 2026</strong></td><td>Fachliche Teams vorbereiten (Service-Mapping, Wissenstransfer)</td><td>Mittel</td></tr>
|
||||
<tr><td><strong>Jan 2027</strong></td><td>Umstellung auf 2 fachliche Teams ("Bestellen" + "Verarbeiten")</td><td>Hoch</td></tr>
|
||||
<tr><td><strong>Feb 2027</strong></td><td>Erste Sprints in neuer Struktur, Nachjustierung</td><td>Mittel</td></tr>
|
||||
<tr><td><strong>März 2027</strong></td><td>TrassenOrder-Team-Integration bewerten</td><td>Mittel</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Metriken zur Erfolgsmessung</h2>
|
||||
<table>
|
||||
<tr><th>Metrik</th><th>Baseline (Mai 2026)</th><th>Ziel Sep 2026</th><th>Ziel Jan 2027</th><th>Ziel Mai 2027</th></tr>
|
||||
<tr><td>Bug-Rate Portal</td><td>33%</td><td>25%</td><td>20%</td><td><15%</td></tr>
|
||||
<tr><td>Bug-Rate Gesamt</td><td>20%</td><td>18%</td><td>15%</td><td><12%</td></tr>
|
||||
<tr><td>Deployment-Frequenz</td><td>? (messen!)</td><td>1x/Woche</td><td>2x/Woche</td><td>Täglich möglich</td></tr>
|
||||
<tr><td>Mean Time to Recovery</td><td>? (messen!)</td><td><4h</td><td><2h</td><td><1h</td></tr>
|
||||
<tr><td>Offene Highest-Tickets</td><td>8+</td><td><5</td><td><3</td><td>0</td></tr>
|
||||
<tr><td>SPOF-Index (>30%)</td><td>2 (Jan, Steven)</td><td>1</td><td>0</td><td>0</td></tr>
|
||||
<tr><td>Kundenzufriedenheit</td><td>Nicht gemessen</td><td>Baseline</td><td>+10 Pkt</td><td>+20 Pkt</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Offene Entscheidungen</h2>
|
||||
<table>
|
||||
<tr><th>#</th><th>Entscheidung</th><th>Deadline</th><th>Wer</th></tr>
|
||||
<tr><td>1</td><td>Wer wird neue Führung?</td><td>Jun 2026</td><td>Management</td></tr>
|
||||
<tr><td>2</td><td>OpsDev-Lead: Intern oder extern?</td><td>Jul 2026</td><td>Management</td></tr>
|
||||
<tr><td>3</td><td>SV-Monolith: Aufteilen oder einem Team zuordnen?</td><td>Okt 2026</td><td>Architektur + BO</td></tr>
|
||||
<tr><td>4</td><td>PO-Struktur: 1 pro Team oder übergreifend?</td><td>Nov 2026</td><td>BO</td></tr>
|
||||
<tr><td>5</td><td>TrassenOrder: Eigenes Team oder Integration?</td><td>Feb 2027</td><td>BO + Management</td></tr>
|
||||
<tr><td>6</td><td>GelV-Ownership: pathOS oder TraPo?</td><td>Feb 2027</td><td>BO</td></tr>
|
||||
<tr><td>7</td><td>Nächste Domäne nach Trasse (Anlage? Nebenleistungen?)</td><td>März 2027</td><td>BO + Strategie</td></tr>
|
||||
</table>
|
||||
|
||||
<hr/>
|
||||
<p><em>Erstellt im Rahmen des pathOS Portfolio-Audits. Kontakt: Andre Knie</em></p>
|
||||
"""
|
||||
|
||||
# Push to Confluence
|
||||
def api_request(url, method="GET", data=None):
|
||||
req = urllib.request.Request(url, method=method)
|
||||
req.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
req.add_header("Content-Type", "application/json")
|
||||
if data:
|
||||
req.data = json.dumps(data).encode("utf-8")
|
||||
return json.loads(urllib.request.urlopen(req, context=ctx, timeout=30).read().decode("utf-8"))
|
||||
|
||||
# Check if page exists
|
||||
search_url = f"{BASE}/content?spaceKey={SPACE}&title={urllib.parse.quote(TITLE)}&type=page"
|
||||
req = urllib.request.Request(search_url)
|
||||
req.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
|
||||
search_data = json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
if search_data.get("results"):
|
||||
# Update existing page
|
||||
page_id = search_data["results"][0]["id"]
|
||||
ver_url = f"{BASE}/content/{page_id}?expand=version"
|
||||
req2 = urllib.request.Request(ver_url)
|
||||
req2.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
ver_data = json.loads(urllib.request.urlopen(req2, context=ctx, timeout=30).read().decode("utf-8"))
|
||||
version = ver_data["version"]["number"] + 1
|
||||
|
||||
body = json.dumps({
|
||||
"version": {"number": version},
|
||||
"title": TITLE,
|
||||
"type": "page",
|
||||
"body": {"storage": {"value": html, "representation": "storage"}}
|
||||
}).encode("utf-8")
|
||||
req3 = urllib.request.Request(f"{BASE}/content/{page_id}", method="PUT", data=body)
|
||||
req3.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
req3.add_header("Content-Type", "application/json")
|
||||
result = json.loads(urllib.request.urlopen(req3, context=ctx, timeout=30).read().decode("utf-8"))
|
||||
print(f"Aktualisiert: https://arija-confluence.jaas.service.deutschebahn.com/pages/viewpage.action?pageId={result['id']}")
|
||||
else:
|
||||
# Create new page
|
||||
body = json.dumps({
|
||||
"type": "page",
|
||||
"title": TITLE,
|
||||
"space": {"key": SPACE},
|
||||
"ancestors": [{"id": PARENT}],
|
||||
"body": {"storage": {"value": html, "representation": "storage"}}
|
||||
}).encode("utf-8")
|
||||
req3 = urllib.request.Request(f"{BASE}/content", method="POST", data=body)
|
||||
req3.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
req3.add_header("Content-Type", "application/json")
|
||||
result = json.loads(urllib.request.urlopen(req3, context=ctx, timeout=30).read().decode("utf-8"))
|
||||
print(f"Erstellt: https://arija-confluence.jaas.service.deutschebahn.com/pages/viewpage.action?pageId={result['id']}")
|
||||
@@ -0,0 +1,295 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Pusht die Personal- und Bug-Analyse als Confluence-Seite
|
||||
.USAGE
|
||||
powershell -ExecutionPolicy Bypass -File ".\project-audit\scripts\confluence-push-personal.ps1"
|
||||
#>
|
||||
|
||||
$ConfluenceUrl = "https://arija-confluence.jaas.service.deutschebahn.com"
|
||||
$SpaceKey = "BES"
|
||||
$ParentPageId = "581013135"
|
||||
$ApiBase = "$ConfluenceUrl/rest/api"
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
Write-Host "============================================================"
|
||||
Write-Host " Personal- und Bug-Analyse -> Confluence"
|
||||
Write-Host "============================================================"
|
||||
|
||||
$PAT = Read-Host "Confluence PAT"
|
||||
$Headers = @{ "Authorization" = "Bearer $PAT"; "Content-Type" = "application/json" }
|
||||
|
||||
function Create-Or-Update-Page {
|
||||
param([string]$Title, [string]$HtmlBody, [string]$ParentId)
|
||||
|
||||
# Umlaute automatisch konvertieren
|
||||
$HtmlBody = Fix-Umlauts $HtmlBody
|
||||
|
||||
$SearchUri = "$ApiBase/content?spaceKey=$SpaceKey&title=$([Uri]::EscapeDataString($Title))&type=page"
|
||||
try {
|
||||
$SearchResp = Invoke-WebRequest -Uri $SearchUri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$SearchData = $SearchResp.Content | ConvertFrom-Json
|
||||
} catch {
|
||||
Write-Host " FEHLER bei Suche: $($_.Exception.Message)" -ForegroundColor Red
|
||||
return $null
|
||||
}
|
||||
if ($SearchData.results -and $SearchData.results.Count -gt 0) {
|
||||
$PageId = $SearchData.results[0].id
|
||||
try {
|
||||
$VerResp = Invoke-WebRequest -Uri "$ApiBase/content/$PageId`?expand=version,body.storage" -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$VerData = $VerResp.Content | ConvertFrom-Json
|
||||
$Version = $VerData.version.number + 1
|
||||
} catch { $Version = $SearchData.results[0].version.number + 1 }
|
||||
Write-Host " Aktualisiere (ID: $PageId, Version: $Version)..." -NoNewline
|
||||
$Body = @{
|
||||
version = @{ number = $Version }
|
||||
title = $Title
|
||||
type = "page"
|
||||
body = @{ storage = @{ value = $HtmlBody; representation = "storage" } }
|
||||
} | ConvertTo-Json -Depth 10
|
||||
try {
|
||||
$Resp = Invoke-WebRequest -Uri "$ApiBase/content/$PageId" -Method Put -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($Body)) -UseBasicParsing -ErrorAction Stop
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
return ($Resp.Content | ConvertFrom-Json).id
|
||||
} catch {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
return $null
|
||||
}
|
||||
} else {
|
||||
Write-Host " Erstelle neue Seite..." -NoNewline
|
||||
$Body = @{
|
||||
type = "page"
|
||||
title = $Title
|
||||
space = @{ key = $SpaceKey }
|
||||
ancestors = @(@{ id = $ParentId })
|
||||
body = @{ storage = @{ value = $HtmlBody; representation = "storage" } }
|
||||
} | ConvertTo-Json -Depth 10
|
||||
try {
|
||||
$Resp = Invoke-WebRequest -Uri "$ApiBase/content" -Method Post -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($Body)) -UseBasicParsing -ErrorAction Stop
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
return ($Resp.Content | ConvertFrom-Json).id
|
||||
} catch {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
return $null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# SEITE: Personal- und Bug-Analyse
|
||||
# ============================================================
|
||||
|
||||
Write-Host ""
|
||||
Write-Host ">> Seite 22: Personal- und Bug-Analyse" -ForegroundColor Yellow
|
||||
|
||||
$PersonalHtml = @"
|
||||
<h2>Personal- und Bug-Analyse pathOS</h2>
|
||||
<p><strong>Stand:</strong> 2026-04-27 | <strong>Quellen:</strong> Jira (90d + 12M), Rollen-Mapping (BO-Input), Tenure-Daten</p>
|
||||
|
||||
<ac:structured-macro ac:name="info"><ac:rich-text-body>
|
||||
<p>Diese Analyse korreliert Personaldaten (Rollen, Tenure, Aktivitaet) mit Bug-Raten und Qualitaetskennzahlen pro Team und Entwickler.</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
|
||||
<h3>Team-Gesundheit im Ueberblick</h3>
|
||||
<table>
|
||||
<tr><th>Team</th><th>Personen</th><th>Kern-Devs</th><th>Bug-Rate (90d)</th><th>Bug-Rate (12M)</th><th>Trend</th><th>Bewertung</th></tr>
|
||||
<tr style="background:#f8cecc;"><td><strong>Team 404</strong></td><td>10</td><td>7</td><td>33%</td><td>25%</td><td>↑ Steigend</td><td>KRITISCH</td></tr>
|
||||
<tr style="background:#d5e8d4;"><td><strong>Team CIB</strong></td><td>13</td><td>6</td><td>20%</td><td>23%</td><td>↓ Sinkend</td><td>GUT</td></tr>
|
||||
<tr style="background:#d5e8d4;"><td><strong>Team Zero</strong></td><td>9</td><td>6</td><td>15%</td><td>15%</td><td>→ Stabil</td><td>GUT</td></tr>
|
||||
<tr style="background:#fff2cc;"><td><strong>OPs Squad</strong></td><td>7+ rot.</td><td>2 fest</td><td>31%</td><td>27%</td><td>↓ Sinkend</td><td>BEOBACHTEN</td></tr>
|
||||
<tr style="background:#fff2cc;"><td><strong>DevOps</strong></td><td>7</td><td>3</td><td>9%</td><td>7%</td><td>→ Stabil</td><td>OK (SPOF)</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Team 404 (Portal) — KRITISCH</h3>
|
||||
<ac:structured-macro ac:name="warning"><ac:rich-text-body>
|
||||
<p>Hoechste Bug-Rate (33%), steigend seit Go-Live. Portal ist kundenseitig — Bugs werden direkt von EVUs gemeldet.</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
<table>
|
||||
<tr><th>Person</th><th>Rolle</th><th>Seit</th><th>Issues (90d)</th><th>Bug-Anteil</th><th>Done%</th><th>Bewertung</th></tr>
|
||||
<tr style="background:#d5e8d4;"><td>Ana Cvitkovic</td><td>Business Engineer</td><td>2021-08</td><td>78</td><td>niedrig</td><td>hoch</td><td>TOP — Anker des Teams</td></tr>
|
||||
<tr><td>Jasmin Keskin</td><td>Frontend Dev</td><td>2021-08</td><td>45</td><td>42%</td><td>mittel</td><td>Bug-anfaellig</td></tr>
|
||||
<tr style="background:#f8cecc;"><td>Emmanuel Kontcheu Tagne</td><td>Frontend Dev</td><td>2021-08</td><td>42</td><td>69%</td><td>mittel</td><td>KRITISCH</td></tr>
|
||||
<tr><td>Diego Da Costa Souza</td><td>Frontend Dev</td><td>2021-09</td><td>41</td><td>mittel</td><td>mittel</td><td>Spring Boot 4 Portal</td></tr>
|
||||
<tr style="background:#f8cecc;"><td>Leon Hoerpel</td><td>Dev</td><td>?</td><td>35</td><td>71%</td><td>mittel</td><td>KRITISCH</td></tr>
|
||||
<tr><td>Dominik Ruecker</td><td>Backend Dev</td><td>?</td><td>31</td><td>mittel</td><td>mittel</td><td></td></tr>
|
||||
<tr style="background:#d5e8d4;"><td>Annette Halbhuber</td><td>Lead Dev</td><td>2021-09</td><td>26</td><td>niedrig</td><td>hoch</td><td>Stabil, Lead</td></tr>
|
||||
<tr style="background:#d5e8d4;"><td>Simon Reitinger</td><td>Dev</td><td>2025-11</td><td>15</td><td>0%</td><td>100%</td><td>Exzellenter Start</td></tr>
|
||||
<tr><td>Marcel Hufgard</td><td>PO</td><td>2021-12</td><td>8</td><td>—</td><td>—</td><td>Product Owner</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>Luca Caracciolo</td><td>QA</td><td>2021-09</td><td>5</td><td>—</td><td>—</td><td>Wenig aktiv</td></tr>
|
||||
</table>
|
||||
<p><strong>Kernproblem:</strong> Emmanuel (69% Bugs) und Leon (71% Bugs) arbeiten beide im Frontend. Das Portal-Frontend hat ein systematisches Qualitaetsproblem. QA (Luca) ist wenig aktiv — Testabdeckung unzureichend.</p>
|
||||
|
||||
<h3>Team CIB (Prozesse/Backend) — Erfolgsgeschichte</h3>
|
||||
<ac:structured-macro ac:name="tip"><ac:rich-text-body>
|
||||
<p>Bug-Rate von 36% (Jan) auf 9% (Apr) gesunken. Backend stabilisiert sich nach Go-Live.</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
<table>
|
||||
<tr><th>Person</th><th>Rolle</th><th>Seit</th><th>Issues (90d)</th><th>Bug-Anteil</th><th>Done%</th><th>Bewertung</th></tr>
|
||||
<tr style="background:#d5e8d4;"><td>Bishara Jaser</td><td>Dev</td><td>2022-08</td><td>73</td><td>niedrig</td><td>93%</td><td>TOP — Leistungstraeger</td></tr>
|
||||
<tr style="background:#d5e8d4;"><td>Saurav Kumar</td><td>Dev</td><td>2022-01</td><td>48</td><td>8%</td><td>94%</td><td>TOP — niedrigste Bug-Rate</td></tr>
|
||||
<tr style="background:#d5e8d4;"><td>Jonas Koehler</td><td>Dev</td><td>?</td><td>34</td><td>niedrig</td><td>100%</td><td>Exzellent</td></tr>
|
||||
<tr><td>Dong-Won Han</td><td>Dev</td><td>2021-09</td><td>20</td><td>mittel</td><td>mittel</td><td>Veteran</td></tr>
|
||||
<tr><td>Hans-Henning Ramberger</td><td>Dev</td><td>2024-05</td><td>16</td><td>mittel</td><td>mittel</td><td>Auch OPs</td></tr>
|
||||
<tr><td>Vasileios Dimitriadis</td><td>Dev</td><td>2023-11</td><td>11</td><td>mittel</td><td>mittel</td><td>Wenig aktiv</td></tr>
|
||||
<tr><td>Christian Meins</td><td>PO</td><td>2021-10</td><td>9</td><td>—</td><td>—</td><td>Product Owner</td></tr>
|
||||
<tr><td>Harry Braun</td><td>Dev</td><td>2023-01</td><td>5</td><td>mittel</td><td>mittel</td><td>SB4 SV</td></tr>
|
||||
<tr><td>Olaf Becken</td><td>Business Engineer</td><td>2024-05</td><td>5</td><td>—</td><td>—</td><td></td></tr>
|
||||
<tr style="background:#fff2cc;"><td>Alexander Petioky</td><td>Dev</td><td>2021-10</td><td>1</td><td>—</td><td>—</td><td>Kaum aktiv</td></tr>
|
||||
</table>
|
||||
<p><strong>Kern-Trio:</strong> Bishara + Saurav + Jonas (155 Issues, >93% Done). Deren Arbeitsweise sollte als Vorbild fuer andere Teams dienen.</p>
|
||||
|
||||
<h3>Team Zero (TAF/TAP) — Stabil</h3>
|
||||
<ac:structured-macro ac:name="tip"><ac:rich-text-body>
|
||||
<p>Niedrigste Bug-Rate (15%), stabiler Output. Beste SonarQube-Qualitaet (4.2 Smells/1K, 91.8% Coverage).</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
<table>
|
||||
<tr><th>Person</th><th>Rolle</th><th>Seit</th><th>Issues (90d)</th><th>Bug-Anteil</th><th>Bewertung</th></tr>
|
||||
<tr style="background:#d5e8d4;"><td>Steven Meixner</td><td>Dev</td><td>2022-07</td><td>64 + 27 OPs</td><td>niedrig</td><td>TOP — Allrounder (SPOF!)</td></tr>
|
||||
<tr style="background:#d5e8d4;"><td>Bing Shi</td><td>Dev</td><td>2022-04</td><td>63</td><td>niedrig</td><td>Stark</td></tr>
|
||||
<tr style="background:#d5e8d4;"><td>Frank Lemke</td><td>Dev</td><td>2022-12</td><td>49</td><td>niedrig</td><td>Auch OPs</td></tr>
|
||||
<tr><td>Kathrin Schleich</td><td>Dev</td><td>2021-09</td><td>27</td><td>niedrig</td><td>Auch OPs</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>Michael Weisberg</td><td>Test</td><td>2024-11</td><td>23</td><td>—</td><td>52% offen</td></tr>
|
||||
<tr><td>Bernd Klebl</td><td>PO</td><td>2021-09</td><td>16</td><td>—</td><td>Auch OPs</td></tr>
|
||||
<tr style="background:#f8cecc;"><td>Norbert Maurer</td><td>Dev</td><td>2021-11</td><td>12</td><td>—</td><td>83% offen — ausgeschieden?</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>DevOps — SPOF-Risiko</h3>
|
||||
<ac:structured-macro ac:name="warning"><ac:rich-text-body>
|
||||
<p><strong>Jan Lubenow = 39% aller DevOps-Issues</strong> — kritischster Single Point of Failure im gesamten Programm.</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
<table>
|
||||
<tr><th>Person</th><th>Rolle</th><th>Seit</th><th>Issues (90d)</th><th>Anteil</th></tr>
|
||||
<tr style="background:#f8cecc;"><td><strong>Jan Lubenow</strong></td><td>Lead Dev</td><td>2022-09</td><td>114</td><td>39%</td></tr>
|
||||
<tr><td>David Steinkopff</td><td>Dev</td><td>2023-03</td><td>18</td><td>6%</td></tr>
|
||||
<tr><td>Henrik Scholl</td><td>Dev</td><td>2024-10</td><td>10</td><td>3%</td></tr>
|
||||
<tr><td>Christian Prause</td><td>Dev</td><td>?</td><td>7</td><td>2%</td></tr>
|
||||
<tr><td>Patrick Lewandowski</td><td>Dev</td><td>?</td><td>6</td><td>2%</td></tr>
|
||||
<tr><td>Michael Mh Jahn</td><td>Dev</td><td>?</td><td>6</td><td>2%</td></tr>
|
||||
<tr><td>Sebastian Goendoer</td><td>Dev</td><td>?</td><td>3</td><td>1%</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Tenure vs. Bug-Rate — Korrelation</h3>
|
||||
<table>
|
||||
<tr><th>Team</th><th>Hypothese: Laenger dabei = weniger Bugs?</th><th>Ergebnis</th></tr>
|
||||
<tr><td>Team 404</td><td>Emmanuel (56 Monate, 69% Bugs), Simon (5 Monate, 0% Bugs)</td><td><strong>WIDERLEGT</strong> — Systematisches Problem</td></tr>
|
||||
<tr><td>Team CIB</td><td>Saurav (51 Monate, 8% Bugs), Bishara (44 Monate, niedrig)</td><td><strong>BESTAETIGT</strong> — Erfahrung + gute Prozesse</td></tr>
|
||||
<tr><td>Team Zero</td><td>Alle Veteranen niedrige Bug-Rate, beste SonarQube-Werte</td><td><strong>BESTAETIGT</strong> — Stabiles Team + Praktiken</td></tr>
|
||||
</table>
|
||||
<p><strong>Fazit:</strong> Tenure allein erklaert die Bug-Rate nicht. Team 404 hat ein systematisches Frontend-Qualitaetsproblem, das nicht durch Erfahrung geloest wird. CIB und Zero zeigen, dass gute Prozesse + Erfahrung zusammen wirken.</p>
|
||||
|
||||
<h3>Risiko-Matrix Personal</h3>
|
||||
<h4>KRITISCH (sofort handeln)</h4>
|
||||
<table>
|
||||
<tr><th>Risiko</th><th>Person(en)</th><th>Impact</th><th>Massnahme</th></tr>
|
||||
<tr style="background:#f8cecc;"><td>SPOF DevOps</td><td>Jan Lubenow (39%)</td><td>Deployment, CI/CD, Infra</td><td>Wissenstransfer, Dokumentation</td></tr>
|
||||
<tr style="background:#f8cecc;"><td>SPOF Zero/OPs</td><td>Steven Meixner (91 Issues)</td><td>TAF/TAP + Infrastruktur</td><td>Entlastung, Backup aufbauen</td></tr>
|
||||
<tr style="background:#f8cecc;"><td>Bug-Verursacher 404</td><td>Emmanuel (69%), Leon (71%)</td><td>Portal-Qualitaet</td><td>Code-Reviews, Pair Programming</td></tr>
|
||||
</table>
|
||||
<h4>HOCH (kurzfristig)</h4>
|
||||
<table>
|
||||
<tr><th>Risiko</th><th>Person(en)</th><th>Impact</th><th>Massnahme</th></tr>
|
||||
<tr style="background:#fff2cc;"><td>QA-Engpass 404</td><td>Luca Caracciolo (5 Issues)</td><td>Testabdeckung Portal</td><td>Aktivieren oder ersetzen</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>Test-Backlog Zero</td><td>Michael Weisberg (52% offen)</td><td>Testabdeckung TAF/TAP</td><td>Priorisierung</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>Inaktive Mitglieder</td><td>Norbert Maurer, Alexander Petioky</td><td>Offene Issues, Wissen</td><td>Status klaeren</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Top 5 Massnahmen (priorisiert)</h3>
|
||||
<ol>
|
||||
<li><strong>Jan Lubenow entlasten</strong> — Wissenstransfer auf David Steinkopff + Henrik Scholl. DevOps-Runbook beschleunigen. Ziel: Kein Einzelner >25% der Issues.</li>
|
||||
<li><strong>Portal-Qualitaet steigern (404)</strong> — Code-Reviews fuer Emmanuel/Leon verstaerken. Frontend-Testautomatisierung ausbauen. QA aktivieren. Fehlermeldungen-Konzept umsetzen.</li>
|
||||
<li><strong>Steven Meixner entlasten</strong> — OPs-Rotation auf CIB/404 ausweiten. Steven soll sich auf Zero-Kernarbeit konzentrieren.</li>
|
||||
<li><strong>Inaktive klaeren</strong> — Norbert Maurer (Zero), Alexander Petioky (CIB), Sebastian Goendoer (OPs): Status klaeren, offene Issues umverteilen.</li>
|
||||
<li><strong>CIB Best Practices teilen</strong> — Bishara/Saurav/Jonas als Mentoren. Deren Arbeitsweise (>93% Done, <10% Bugs) als Vorbild fuer 404.</li>
|
||||
</ol>
|
||||
"@
|
||||
|
||||
# Umlaute: Nutze gleiche Funktion wie confluence-push.ps1
|
||||
# Schritt 1: Englische Woerter schuetzen, Schritt 2: Ersetzen, Schritt 3: Schutz aufheben
|
||||
function Fix-Umlauts($html) {
|
||||
if ($null -eq $html -or $html -eq "") { return $html }
|
||||
$Protect = @(
|
||||
'Israel','Michael','Raphael','Emmanuel','Manuel','Daniel','Samuel',
|
||||
'Maurer','Enabler','enabler','Container','container','Cluster','cluster',
|
||||
'User','user','Tracing','Staging','Logging','Alerting','Monitoring',
|
||||
'Deployment','deployment','Security','security','Recovery','Discovery',
|
||||
'Delivery','Pipeline','pipeline','Release','release',
|
||||
'Performance','performance','Interface','interface',
|
||||
'Service','service','Feature','feature','Issue','issue',
|
||||
'value','true','blue','Due','due','Queue','queue',
|
||||
'Unique','unique','Continue','continue',
|
||||
'Funnel','Tunnel','tunnel','Buildpacks','Dockerfile',
|
||||
'OpenSearch','Gherkin','Kubernetes','Helmfile','Renovate',
|
||||
'Operate','Template','template','Tasklist',
|
||||
'Smoke','smoke','Analyse','analyse','Analysis',
|
||||
'Baseline','baseline','Namespace','namespace',
|
||||
'Codebase','Database','database','Rescue','rescue'
|
||||
)
|
||||
foreach ($w in $Protect) { $html = $html.Replace($w, "PROTECT_$w") }
|
||||
# Namen
|
||||
$html = $html.Replace('Hoerpel', 'Hörpel')
|
||||
$html = $html.Replace('Ruecker', 'Rücker')
|
||||
$html = $html.Replace('Koehler', 'Köhler')
|
||||
$html = $html.Replace('Goendoer', 'Göndör')
|
||||
# ue
|
||||
$html = $html.Replace('Ueber', 'Über')
|
||||
$html = $html.Replace('ueber', 'über')
|
||||
$html = $html.Replace('Pruef', 'Prüf'); $html = $html.Replace('pruef', 'prüf')
|
||||
$html = $html.Replace('fuehrung', 'führung'); $html = $html.Replace('Fuehrung', 'Führung')
|
||||
$html = $html.Replace('Verfueg', 'Verfüg'); $html = $html.Replace('verfueg', 'verfüg')
|
||||
$html = $html.Replace('Verschluess', 'Verschlüss'); $html = $html.Replace('verschluess', 'verschlüss')
|
||||
$html = $html.Replace('Schluess', 'Schlüss'); $html = $html.Replace('schluess', 'schlüss')
|
||||
$html = $html.Replace('Verknuepf', 'Verknüpf'); $html = $html.Replace('verknuepf', 'verknüpf')
|
||||
$html = $html.Replace('Unterstuetz', 'Unterstütz'); $html = $html.Replace('unterstuetz', 'unterstütz')
|
||||
$html = $html.Replace('Luecke', 'Lücke'); $html = $html.Replace('luecke', 'lücke')
|
||||
$html = $html.Replace('Rueck', 'Rück'); $html = $html.Replace('rueck', 'rück')
|
||||
$html = $html.Replace('Frueh', 'Früh'); $html = $html.Replace('frueh', 'früh')
|
||||
$html = $html.Replace('Nuetzlich', 'Nützlich'); $html = $html.Replace('nuetzlich', 'nützlich')
|
||||
$html = $html.Replace('Abkuerz', 'Abkürz'); $html = $html.Replace('abkuerz', 'abkürz')
|
||||
$html = $html.Replace('wuerde', 'würde'); $html = $html.Replace('Wuerde', 'Würde')
|
||||
$html = $html.Replace('muessen', 'müssen'); $html = $html.Replace('Muessen', 'Müssen')
|
||||
$html = $html.Replace('fuer ', 'für '); $html = $html.Replace('Fuer ', 'Für ')
|
||||
# oe
|
||||
$html = $html.Replace('Loesung', 'Lösung'); $html = $html.Replace('loesung', 'lösung')
|
||||
$html = $html.Replace('Erhoeh', 'Erhöh'); $html = $html.Replace('erhoeh', 'erhöh')
|
||||
$html = $html.Replace('noetig', 'nötig'); $html = $html.Replace('Noetig', 'Nötig')
|
||||
$html = $html.Replace('moeglich', 'möglich'); $html = $html.Replace('Moeglich', 'Möglich')
|
||||
$html = $html.Replace('geloest', 'gelöst')
|
||||
$html = $html.Replace('hoechst', 'höchst'); $html = $html.Replace('Hoechst', 'Höchst')
|
||||
$html = $html.Replace('koennen', 'können'); $html = $html.Replace('Koennen', 'Können')
|
||||
$html = $html.Replace('Zugehoer', 'Zugehör'); $html = $html.Replace('zugehoer', 'zugehör')
|
||||
$html = $html.Replace('gehoer', 'gehör')
|
||||
# ae
|
||||
$html = $html.Replace('Aenderung', 'Änderung'); $html = $html.Replace('aenderung', 'änderung')
|
||||
$html = $html.Replace('anfaellig', 'anfällig'); $html = $html.Replace('faellig', 'fällig')
|
||||
$html = $html.Replace('faehig', 'fähig')
|
||||
$html = $html.Replace('traeger', 'träger'); $html = $html.Replace('Traeger', 'Träger')
|
||||
$html = $html.Replace('laenger', 'länger'); $html = $html.Replace('Laenger', 'Länger')
|
||||
$html = $html.Replace('erklaer', 'erklär')
|
||||
$html = $html.Replace('klaer', 'klär'); $html = $html.Replace('Klaer', 'Klär')
|
||||
$html = $html.Replace('verstaerk', 'verstärk')
|
||||
$html = $html.Replace('Verstaend', 'Verständ'); $html = $html.Replace('verstaend', 'verständ')
|
||||
$html = $html.Replace('Abhaengig', 'Abhängig'); $html = $html.Replace('abhaengig', 'abhängig')
|
||||
$html = $html.Replace('haeufig', 'häufig'); $html = $html.Replace('Haeufig', 'Häufig')
|
||||
$html = $html.Replace('laeuft', 'läuft'); $html = $html.Replace('Laeuft', 'Läuft')
|
||||
$html = $html.Replace('waere', 'wäre'); $html = $html.Replace('Waere', 'Wäre')
|
||||
$html = $html.Replace('schaerfen', 'schärfen'); $html = $html.Replace('Schaerfen', 'Schärfen')
|
||||
$html = $html.Replace('aufraeumen', 'aufräumen'); $html = $html.Replace('Aufraeumen', 'Aufräumen')
|
||||
$html = $html.Replace('Verlaenger', 'Verlänger'); $html = $html.Replace('verlaenger', 'verlänger')
|
||||
$html = $html.Replace('Naechst', 'Nächst'); $html = $html.Replace('naechst', 'nächst')
|
||||
$html = $html.Replace('Massnahme', 'Maßnahme'); $html = $html.Replace('massnahme', 'maßnahme')
|
||||
$html = $html.Replace('Groesse', 'Größe'); $html = $html.Replace('groesse', 'größe')
|
||||
$html = $html.Replace('gleichmaessig', 'gleichmäßig')
|
||||
$html = $html -replace '([a-z])taet\b', '$1tät'
|
||||
# Schutz aufheben
|
||||
$html = $html.Replace('PROTECT_', '')
|
||||
return $html
|
||||
}
|
||||
|
||||
$PageId = Create-Or-Update-Page -Title "22. Personal- und Bug-Analyse" -HtmlBody $PersonalHtml -ParentId $ParentPageId
|
||||
|
||||
Write-Host ""
|
||||
if ($PageId) {
|
||||
Write-Host "FERTIG! $ConfluenceUrl/pages/viewpage.action?pageId=$PageId" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "FEHLER beim Erstellen der Seite" -ForegroundColor Red
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Pusht die Roadmap-Tabelle als Confluence-Seite
|
||||
#>
|
||||
$ConfluenceUrl = "https://arija-confluence.jaas.service.deutschebahn.com"
|
||||
$SpaceKey = "BES"
|
||||
$ParentPageId = "581013135"
|
||||
$ApiBase = "$ConfluenceUrl/rest/api"
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
$SecretsPath = Join-Path $ScriptDir "..\.secrets"
|
||||
$PAT = $null
|
||||
if (Test-Path $SecretsPath) {
|
||||
Get-Content $SecretsPath | ForEach-Object {
|
||||
if ($_ -match "^CONFLUENCE_TOKEN=(.+)$") {
|
||||
$val = $Matches[1].Trim()
|
||||
if ($val -ne "HIER_TOKEN_EINTRAGEN") { $PAT = $val }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $PAT) { $PAT = Read-Host "Confluence PAT" }
|
||||
$Headers = @{ "Authorization" = "Bearer $PAT"; "Content-Type" = "application/json" }
|
||||
|
||||
$Title = "24. Roadmap-Tabelle (alle Themen)"
|
||||
Write-Host ">> Push: $Title" -ForegroundColor Yellow
|
||||
|
||||
$Html = @"
|
||||
<h2>Roadmap-Tabelle — Alle Themen nach Team</h2>
|
||||
<p><strong>Stand:</strong> $(Get-Date -Format 'yyyy-MM-dd') | 77 Themen | Quellen: Technische Roadmap, PO-Input, BO-Input, Jira TTTSol, Risiko-Radar</p>
|
||||
<p><strong>Legende Prio:</strong> MUSS = regulatorisch/vertraglich | HOCH = geschaeftskritisch | MITTEL = wichtig | NIEDRIG = nice-to-have</p>
|
||||
<p><strong>Legende Dauer:</strong> S = Sprint (2W) | M = Monat | Q = Quartal | L = Langfristig (>Q)</p>
|
||||
|
||||
<h3>Kritischer Pfad (harte Deadlines)</h3>
|
||||
<table>
|
||||
<tr style="background:#f8cecc;"><th>Deadline</th><th>Thema</th><th>Team</th><th>Konsequenz bei Versaeumnis</th></tr>
|
||||
<tr style="background:#f8cecc;"><td><strong>Jun 2026</strong></td><td>Spring Boot 4</td><td>Alle</td><td>Kein Security-Support mehr</td></tr>
|
||||
<tr style="background:#fff2cc;"><td><strong>Aug 2026</strong></td><td>SL Silber Plus</td><td>OPs</td><td>Vertragsverletzung</td></tr>
|
||||
<tr style="background:#fff2cc;"><td><strong>Dez 2026</strong></td><td>FplJ 28</td><td>Alle</td><td>Fahrplanwechsel gefaehrdet</td></tr>
|
||||
<tr><td>Laufend</td><td>Abrechnung TTT</td><td>CIB</td><td>Falsche Rechnungen an Kunden</td></tr>
|
||||
<tr><td>Laufend</td><td>NAÄ implizite Annahme</td><td>CIB</td><td>Kundenversprechen gebrochen</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Team CIB (17 Themen)</h3>
|
||||
<table>
|
||||
<tr><th>#</th><th>Thema</th><th>Kurzfassung</th><th>Prio</th><th>Dauer</th><th>Deadline</th><th>Status</th></tr>
|
||||
<tr style="background:#f8cecc;"><td>C1</td><td><strong>Implizite Annahme NAÄ</strong></td><td>Kundenversprechen: Standard-Prozess fuer NAÄ. O2CCIB-6931 unassigned.</td><td>MUSS</td><td>M</td><td>PI 41</td><td>⚠ Geschoben</td></tr>
|
||||
<tr style="background:#f8cecc;"><td>C2</td><td><strong>Spring Boot 4 (SV, AV, IFP)</strong></td><td>EOL 3.5 Juni 2026. SV (Saurav), Archivierung (Hans-Henning), IFP aktiv.</td><td>MUSS</td><td>M</td><td><strong>Jun 2026</strong></td><td>🔄 In Arbeit</td></tr>
|
||||
<tr style="background:#f8cecc;"><td>C3</td><td><strong>Abrechnung TTT/AC</strong></td><td>41 offene Tickets, 8 Highest. Grundrisiko: Nicht korrekt abrechnen.</td><td>MUSS</td><td>Q</td><td>Laufend</td><td>🔄 Taskforce</td></tr>
|
||||
<tr style="background:#f8cecc;"><td>C4</td><td><strong>20h Zug Abrechnung</strong></td><td>TTTSOL-2149 BLOCKED. Loesung funktioniert nicht.</td><td>MUSS</td><td>M</td><td>PI 41</td><td>❌ Blocked</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>C5</td><td>Vertragskorrektur rueckwirkend</td><td>TTTSOL-1956 Highest. Konzernreporting.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>🔄</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>C6</td><td>NEP2 Funktionalitaet</td><td>Aktueller TTT-Meilenstein. Laeuft stabil.</td><td>HOCH</td><td>Q</td><td>Laufend</td><td>🔄</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>C7</td><td>Aenderungswesen finalisieren</td><td>Parallele Aenderungsbestellungen validieren.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>C8</td><td>Vorpruefung Portal</td><td>Validierung VOR Absenden. Verhindert Haengenbleiben.</td><td>HOCH</td><td>M</td><td>PI 41/42</td><td>Offen</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>C9</td><td>Click&Ride Anbindung</td><td>ADR-72. SUBP arbeitet an Teilen.</td><td>HOCH</td><td>Q</td><td>PI 41</td><td>Teilweise</td></tr>
|
||||
<tr><td>C10</td><td>Camunda 8.9</td><td>Support-Zeitraeume beachten.</td><td>MITTEL</td><td>S</td><td>PI 41</td><td>Offen</td></tr>
|
||||
<tr><td>C11</td><td>Full-Table-Scans AV</td><td>Performance-Problem.</td><td>MITTEL</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
|
||||
<tr><td>C12</td><td>RouteUpdate-Prozess</td><td>Noch nicht implementiert. Nachlieferung.</td><td>MITTEL</td><td>Q</td><td>PI 42</td><td>Offen</td></tr>
|
||||
<tr><td>C13</td><td>§22 ERegG</td><td>Eintritt Drittunternehmen in Vertrag.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
|
||||
<tr><td>C14</td><td>Leistungsverweigerung</td><td>Durchsetzung Stufe 2.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
|
||||
<tr><td>C15</td><td>Rahmenvertraege aufraeumen</td><td>Reduziert Pflegeaufwand.</td><td>NIEDRIG</td><td>S</td><td>—</td><td>Offen</td></tr>
|
||||
<tr><td>C16</td><td>Mehrfach-Beanstandung Fix</td><td>Nur erste wird weitergeleitet.</td><td>NIEDRIG</td><td>S</td><td>—</td><td>Offen</td></tr>
|
||||
<tr><td>C17</td><td>SV-Smells abbauen</td><td>2214 Smells. Sprint-weise.</td><td>NIEDRIG</td><td>L</td><td>Laufend</td><td>Offen</td></tr>
|
||||
</table>
|
||||
"@
|
||||
|
||||
$Html += @"
|
||||
|
||||
<h3>Team 404 (17 Themen)</h3>
|
||||
<table>
|
||||
<tr><th>#</th><th>Thema</th><th>Kurzfassung</th><th>Prio</th><th>Dauer</th><th>Deadline</th><th>Status</th></tr>
|
||||
<tr style="background:#fff2cc;"><td>P1</td><td><strong>Portal-Bug-Rate senken</strong></td><td>33% Bug-Rate. Emmanuel 69%, Leon 71%. Systematisch.</td><td>HOCH</td><td>L</td><td>Laufend</td><td>⚠ Kritisch</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>P2</td><td><strong>Entkopplung Zuglaufpunkte</strong></td><td>Vererbungslogik entfernen — Hauptquelle vieler Bugs.</td><td>HOCH</td><td>Q</td><td>PI 41/42</td><td>Offen</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>P3</td><td>Fehlermeldungen-Konzept</td><td>Verstaendliche Meldungen. BO-Prioritaet.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>P4</td><td>Uebersichtsseite Vorgaenge</td><td>Gruppiert nach RouteID.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>P5</td><td>E2E-Tests ausbauen</td><td>Playwright/Cypress. Coverage gut, Testqualitaet nicht.</td><td>HOCH</td><td>L</td><td>Laufend</td><td>Offen</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>P6</td><td>Testumgebung mit TPN</td><td>Dauerhaft verfuegbar.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>P7</td><td>Vorab-Validierung (BEP)</td><td>Zusammen mit CIB C8.</td><td>HOCH</td><td>M</td><td>PI 41/42</td><td>Offen</td></tr>
|
||||
<tr><td>P8</td><td>Benachrichtigungen E-Mail</td><td>Bei NAÄ oder Angebotseingang.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
|
||||
<tr><td>P9</td><td>Expertenmodus</td><td>Kompakte Maske fuer Erfahrene.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
|
||||
<tr><td>P10</td><td>Massenkopie Taktbestellungen</td><td>Mehrere Takte kopieren.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
|
||||
<tr><td>P11</td><td>Suchfunktion</td><td>Leistungsfaehige Suche.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
|
||||
<tr><td>P12</td><td>Kalenderfunktion</td><td>Kunden tun sich schwer.</td><td>MITTEL</td><td>S</td><td>PI 42</td><td>Offen</td></tr>
|
||||
<tr><td>P13</td><td>Verknuepfungslogik</td><td>Planned/RelatedPlanned IDs.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
|
||||
<tr><td>P14</td><td>Vorlagen ueberarbeiten</td><td>Fehleranfaellig. Konzept noetig.</td><td>MITTEL</td><td>M</td><td>PI 43</td><td>Offen</td></tr>
|
||||
<tr><td>P15</td><td>Auslands-/NE-Strecken</td><td>Hilfe beim Anlegen.</td><td>NIEDRIG</td><td>M</td><td>—</td><td>Offen</td></tr>
|
||||
<tr><td>P16</td><td>TPN-Import optimieren</td><td>Zu komplex.</td><td>NIEDRIG</td><td>Q</td><td>—</td><td>Offen</td></tr>
|
||||
<tr><td>P17</td><td>Nutzerfuehrung</td><td>Verkehrsart zu Beginn.</td><td>NIEDRIG</td><td>S</td><td>—</td><td>Offen</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Team Zero (16 Themen)</h3>
|
||||
<table>
|
||||
<tr><th>#</th><th>Thema</th><th>Kurzfassung</th><th>Prio</th><th>Dauer</th><th>Deadline</th><th>Status</th></tr>
|
||||
<tr style="background:#f8cecc;"><td>Z1</td><td><strong>Spring Boot 4 (CI, AV)</strong></td><td>AV-Trasse Test-Branch aktiv.</td><td>MUSS</td><td>M</td><td><strong>Jun 2026</strong></td><td>🔄</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>Z2</td><td>NEP2 Schnittstellen</td><td>Aktueller TTT-Meilenstein.</td><td>HOCH</td><td>Q</td><td>Laufend</td><td>🔄</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>Z3</td><td>Workarounds TTK aufraeumen</td><td>Technische Schuld.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>Z4</td><td>Performance AV</td><td>Auftrags-Verwaltung.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>Z5</td><td>Verschluesselung</td><td>Security-Anforderung.</td><td>HOCH</td><td>S</td><td>PI 41</td><td>Offen</td></tr>
|
||||
<tr><td>Z6</td><td>Monitoring Grafana</td><td>Boards nutzbar machen.</td><td>MITTEL</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
|
||||
<tr><td>Z7</td><td>Stationsportal</td><td>Fertigstellen.</td><td>MITTEL</td><td>M</td><td>PI 41/42</td><td>Offen</td></tr>
|
||||
<tr><td>Z8</td><td>LuP VNP Versand</td><td>Last- und Performance.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
|
||||
<tr><td>Z9</td><td>PCS aufraeumen</td><td>Gestaltung Rueckweg. Gross.</td><td>MITTEL</td><td>Q</td><td>PI 42/43</td><td>Offen</td></tr>
|
||||
<tr><td>Z10</td><td>Runbook</td><td>Aktualisieren.</td><td>NIEDRIG</td><td>M</td><td>Laufend</td><td>Offen</td></tr>
|
||||
<tr><td>Z11</td><td>Click&Ride</td><td>Mit CIB.</td><td>NIEDRIG</td><td>Q</td><td>PI 43+</td><td>Offen</td></tr>
|
||||
<tr><td>Z12</td><td>Infrastruktur Revisionen</td><td>Verarbeiten.</td><td>NIEDRIG</td><td>M</td><td>—</td><td>Offen</td></tr>
|
||||
<tr><td>Z13</td><td>Ersatz Message Routing ID</td><td>SenderReference. Gross.</td><td>NIEDRIG</td><td>Q</td><td>—</td><td>Offen</td></tr>
|
||||
<tr><td>Z14</td><td>XSD 3.5.2</td><td>Kann gross werden.</td><td>NIEDRIG</td><td>Q</td><td>—</td><td>Offen</td></tr>
|
||||
<tr><td>Z15</td><td>LuP neu</td><td>Komplett neu.</td><td>NIEDRIG</td><td>L</td><td>—</td><td>Offen</td></tr>
|
||||
<tr><td>Z16</td><td>TDM aufraeumen</td><td>Datenmodell.</td><td>NIEDRIG</td><td>M</td><td>—</td><td>Offen</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>OPs / DevOps (14 Themen)</h3>
|
||||
<table>
|
||||
<tr><th>#</th><th>Thema</th><th>Kurzfassung</th><th>Prio</th><th>Dauer</th><th>Deadline</th><th>Status</th></tr>
|
||||
<tr style="background:#f8cecc;"><td>O1</td><td><strong>Netcool-Anbindung</strong></td><td>Betriebsueberwachung. UKA.</td><td>MUSS</td><td>M</td><td>PI 40/41</td><td>🔄</td></tr>
|
||||
<tr style="background:#f8cecc;"><td>O2</td><td><strong>Monitoring-Konzept</strong></td><td>Umsetzung seit PI 40.</td><td>MUSS</td><td>M</td><td>PI 41</td><td>🔄</td></tr>
|
||||
<tr style="background:#f8cecc;"><td>O3</td><td><strong>SL Silber Plus</strong></td><td>Vertragliche Verpflichtung.</td><td>MUSS</td><td>Q</td><td><strong>Aug 2026</strong></td><td>Offen</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>O4</td><td>DR-Test 3</td><td>SL-Voraussetzung.</td><td>HOCH</td><td>M</td><td>PI 40/41</td><td>🔄</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>O5</td><td>Secret-Rotation</td><td>Security-Konzept.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>O6</td><td>Deployment-Automatisierung</td><td>Smoketests. Seit 03/2026.</td><td>HOCH</td><td>Q</td><td>PI 41/42</td><td>🔄</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>O7</td><td>K8s-Cluster/Namespaces</td><td>Umstellung. UKA.</td><td>HOCH</td><td>M</td><td>PI 40</td><td>🔄</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>O8</td><td>Jan Lubenow entlasten</td><td>SPOF 39%. Wissenstransfer.</td><td>HOCH</td><td>L</td><td>Laufend</td><td>⚠</td></tr>
|
||||
<tr><td>O9</td><td>AppMesh-Alternative</td><td>AWS EOL 2026.</td><td>MITTEL</td><td>Q</td><td>PI 42</td><td>Offen</td></tr>
|
||||
<tr><td>O10</td><td>Umgebungen konsolidieren</td><td>17+ reduzieren.</td><td>MITTEL</td><td>Q</td><td>PI 42/43</td><td>Offen</td></tr>
|
||||
<tr><td>O11</td><td>Keycloak konsolidieren</td><td>5+ Repos.</td><td>NIEDRIG</td><td>M</td><td>—</td><td>Offen</td></tr>
|
||||
<tr><td>O12</td><td>ECR statt Artifactory</td><td>Laufzeit-Artefakte. UKA.</td><td>MITTEL</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
|
||||
<tr><td>O13</td><td>Security-Groups egress</td><td>Netzwerk-Security.</td><td>MITTEL</td><td>M</td><td>PI 41</td><td>Offen</td></tr>
|
||||
<tr><td>O14</td><td>Runbook beschleunigen</td><td>DevOps-Doku.</td><td>MITTEL</td><td>L</td><td>Laufend</td><td>🔄</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Uebergreifend / Programm (13 Themen)</h3>
|
||||
<table>
|
||||
<tr><th>#</th><th>Thema</th><th>Kurzfassung</th><th>Prio</th><th>Dauer</th><th>Deadline</th><th>Status</th></tr>
|
||||
<tr style="background:#fff2cc;"><td>U1</td><td><strong>Hypercare beenden</strong></td><td>Normalbetrieb.</td><td>HOCH</td><td>—</td><td>Jul 2026</td><td>Offen</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>U2</td><td><strong>GelV Erweiterungen</strong></td><td>TTT-Meilenstein.</td><td>HOCH</td><td>Q</td><td>Sep 2026</td><td>Offen</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>U3</td><td>ujBau Funktionalitaet</td><td>TTT-Meilenstein.</td><td>HOCH</td><td>Q</td><td>Laufend</td><td>🔄</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>U4</td><td>FplJ 28 Vorbereitung</td><td>Fahrplanwechsel Dez 2026.</td><td>HOCH</td><td>Q</td><td><strong>Dez 2026</strong></td><td>Offen</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>U5</td><td>DB Cargo Eskalation CIO</td><td>TTTSOL-2186. PCS + VNP.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>🔄</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>U6</td><td>OTN-Vergabe Fpl 2027</td><td>Muss vor FplW geloest sein.</td><td>HOCH</td><td>M</td><td>Dez 2026</td><td>🔄</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>U7</td><td>Ad-hoc Verkehre <1h</td><td>BLOCKED. TopThema. FV.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>❌</td></tr>
|
||||
<tr style="background:#fff2cc;"><td>U8</td><td>Mittiger Teilausfall SEV</td><td>Fernverkehr. Taskforce.</td><td>HOCH</td><td>M</td><td>PI 41</td><td>🔄</td></tr>
|
||||
<tr><td>U9</td><td>Team-Reorganisation</td><td>Subdomaenen-Schnitt.</td><td>MITTEL</td><td>Q</td><td>Q1 2027</td><td>Offen</td></tr>
|
||||
<tr><td>U10</td><td>Support-Enablement</td><td>Tools, Zugang, Prozesse.</td><td>MITTEL</td><td>M</td><td>PI 42</td><td>Offen</td></tr>
|
||||
<tr><td>U11</td><td>Contract Testing</td><td>Cross-Team Qualitaet.</td><td>NIEDRIG</td><td>Q</td><td>PI 43</td><td>Offen</td></tr>
|
||||
<tr><td>U12</td><td>TPN abloesen</td><td>Parallelbetrieb beenden.</td><td>NIEDRIG</td><td>L</td><td>Q2 2027</td><td>Offen</td></tr>
|
||||
<tr><td>U13</td><td>Daten-Partitionierung</td><td>Performance wachsende Daten.</td><td>MITTEL</td><td>Q</td><td>Dez 2026</td><td>Offen</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Zusammenfassung</h3>
|
||||
<table>
|
||||
<tr><th>Team</th><th>MUSS</th><th>HOCH</th><th>MITTEL</th><th>NIEDRIG</th><th>Gesamt</th></tr>
|
||||
<tr><td><strong>CIB</strong></td><td>4</td><td>5</td><td>5</td><td>3</td><td>17</td></tr>
|
||||
<tr><td><strong>404</strong></td><td>0</td><td>7</td><td>7</td><td>3</td><td>17</td></tr>
|
||||
<tr><td><strong>Zero</strong></td><td>1</td><td>4</td><td>4</td><td>7</td><td>16</td></tr>
|
||||
<tr><td><strong>OPs/DevOps</strong></td><td>3</td><td>5</td><td>4</td><td>2</td><td>14</td></tr>
|
||||
<tr><td><strong>Uebergreifend</strong></td><td>0</td><td>8</td><td>3</td><td>2</td><td>13</td></tr>
|
||||
<tr style="background:#f5f5f5;"><td><strong>GESAMT</strong></td><td><strong>8</strong></td><td><strong>29</strong></td><td><strong>23</strong></td><td><strong>17</strong></td><td><strong>77</strong></td></tr>
|
||||
</table>
|
||||
"@
|
||||
|
||||
# Create or Update
|
||||
$SearchUri = "$ApiBase/content?spaceKey=$SpaceKey&title=$([Uri]::EscapeDataString($Title))&type=page"
|
||||
try {
|
||||
$SearchResp = Invoke-WebRequest -Uri $SearchUri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$SearchData = $SearchResp.Content | ConvertFrom-Json
|
||||
} catch { Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red; exit 1 }
|
||||
|
||||
if ($SearchData.results -and $SearchData.results.Count -gt 0) {
|
||||
$PageId = $SearchData.results[0].id
|
||||
$VerResp = Invoke-WebRequest -Uri "$ApiBase/content/$PageId`?expand=version" -Headers $Headers -UseBasicParsing
|
||||
$VerData = $VerResp.Content | ConvertFrom-Json
|
||||
$Version = $VerData.version.number + 1
|
||||
Write-Host " Aktualisiere (ID: $PageId, v$Version)..." -NoNewline
|
||||
$Body = @{ version = @{ number = $Version }; title = $Title; type = "page"; body = @{ storage = @{ value = $Html; representation = "storage" } } } | ConvertTo-Json -Depth 10
|
||||
$Resp = Invoke-WebRequest -Uri "$ApiBase/content/$PageId" -Method Put -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($Body)) -UseBasicParsing
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
$ResultId = ($Resp.Content | ConvertFrom-Json).id
|
||||
} else {
|
||||
Write-Host " Erstelle neue Seite..." -NoNewline
|
||||
$Body = @{ type = "page"; title = $Title; space = @{ key = $SpaceKey }; ancestors = @(@{ id = $ParentPageId }); body = @{ storage = @{ value = $Html; representation = "storage" } } } | ConvertTo-Json -Depth 10
|
||||
$Resp = Invoke-WebRequest -Uri "$ApiBase/content" -Method Post -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($Body)) -UseBasicParsing
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
$ResultId = ($Resp.Content | ConvertFrom-Json).id
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
Write-Host " Seite: $ConfluenceUrl/pages/viewpage.action?pageId=$ResultId" -ForegroundColor Green
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
@@ -0,0 +1,161 @@
|
||||
"""Pusht Team-Reorganisation als Confluence-Seite."""
|
||||
import urllib.request, json, ssl, urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
TOKEN = secrets.get("CONFLUENCE_TOKEN", "")
|
||||
BASE = "https://arija-confluence.jaas.service.deutschebahn.com/rest/api"
|
||||
SPACE = "BES"
|
||||
PARENT = "581013135"
|
||||
TITLE = "25. Team-Reorganisation (Optionen)"
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
# Read the markdown and convert key sections to HTML
|
||||
html = """<h2>Team-Reorganisation pathOS — Optionen und Bewertung</h2>
|
||||
<p><strong>Stand:</strong> 2026-04-30 | Status: Entwurf zur Diskussion</p>
|
||||
|
||||
<ac:structured-macro ac:name="info"><ac:rich-text-body>
|
||||
<p>Dieses Dokument analysiert Optionen fuer die Neuaufstellung der pathOS-Teams. Ziele: Schnellere Umsetzung, weniger Komplexitaet, bessere Qualitaet und Verantwortlichkeit.</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
|
||||
<h3>Ausgangslage</h3>
|
||||
<table>
|
||||
<tr><th>Team</th><th>Personen</th><th>Verantwortung</th><th>Services</th><th>Problem</th></tr>
|
||||
<tr><td>Team 404</td><td>~10</td><td>Portal UI + Middleware</td><td>2 (68K LoC)</td><td>33% Bug-Rate, steigend</td></tr>
|
||||
<tr><td>Team CIB</td><td>~13</td><td>Prozesse, Backend, Camunda</td><td>~15 (122K LoC)</td><td>SV-Monolith (93K, 2214 Smells)</td></tr>
|
||||
<tr><td>Team Zero</td><td>~9</td><td>TAF/TAP Schnittstellen</td><td>~9 (38K LoC)</td><td>OPs-Rotation belastet</td></tr>
|
||||
<tr><td>OPs Squad</td><td>2 fest + rot.</td><td>Infrastruktur, Betrieb</td><td>Infra-Repos</td><td>Nur 2 feste Mitglieder</td></tr>
|
||||
<tr><td>DevOps</td><td>~7</td><td>CI/CD, Tooling</td><td>Pipelines</td><td>Jan Lubenow = 39% SPOF</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Ziele</h3>
|
||||
<table>
|
||||
<tr><th>Primaer</th><th>Nachrangig</th></tr>
|
||||
<tr><td>Schnelle Umsetzungsgeschwindigkeit</td><td>Staerkenorientierter Einsatz</td></tr>
|
||||
<tr><td>Weniger Komplexitaet</td><td>Wissen breiter verteilen</td></tr>
|
||||
<tr><td>Weniger Abhaengigkeiten</td><td>Flexibilitaet bei Prioritaetswechsel</td></tr>
|
||||
<tr><td>Bessere Qualitaet</td><td></td></tr>
|
||||
<tr><td>Bessere Verantwortlichkeit</td><td></td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Option A: OpsDev + Feature-Pool</h3>
|
||||
<ac:structured-macro ac:name="panel"><ac:parameter ac:name="title">Modell</ac:parameter><ac:rich-text-body>
|
||||
<p><strong>OpsDev Team</strong> (permanent, ~8-10): Betrieb, Bugs, kleine Features. Lead OpsDev ohne PO.</p>
|
||||
<p><strong>Feature-Pool</strong> (~20-25): Entwickler + BAs + Feature-POs. Bilden temporaere Feature-Teams (3-5 Pers.) fuer grosse Features. Nach Abschluss: 1 Dev → OpsDev (Hypercare).</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
<table>
|
||||
<tr><th>Vorteile</th><th>Nachteile</th></tr>
|
||||
<tr><td>Feature-Team ownt end-to-end</td><td>Kontextwechsel, Onboarding-Aufwand</td></tr>
|
||||
<tr><td>Keine Cross-Team-Deps fuer Features</td><td>Kein stabiles Team, Teambildung leidet</td></tr>
|
||||
<tr><td>Flexibel nach Prioritaet</td><td>OpsDev wird Muellhalde fuer Bugs</td></tr>
|
||||
<tr><td>Wissenstransfer durch Hypercare</td><td>PO-Overhead (3-4 POs parallel)</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Option B: Fachlicher Schnitt (4 Varianten)</h3>
|
||||
|
||||
<h4>B1: Bestellen vs. Abwickeln</h4>
|
||||
<table>
|
||||
<tr><th>Team "Bestellen"</th><th>Team "Abwickeln"</th></tr>
|
||||
<tr><td>Portal UI + MW, Common Interface, Stammdaten, Kundendaten, TAF/TAP-Konverter</td><td>Steuerung Vertrieb (Camunda), Auftrags-Verwaltung, IFP-Connector, Vertragsdaten, Archivierung, Abrechnung</td></tr>
|
||||
<tr><td><em>Fokus: Was der Kunde sieht</em></td><td><em>Fokus: Was nach der Bestellung passiert</em></td></tr>
|
||||
</table>
|
||||
<p>✔ Klarer Kundenfokus | ✘ NAÄ betrifft beide, SV ist Monolith</p>
|
||||
|
||||
<h4>B4: Trasse vs. Vertrag (bester fachlicher Schnitt)</h4>
|
||||
<table>
|
||||
<tr><th>Team "Trasse"</th><th>Team "Vertrag"</th></tr>
|
||||
<tr><td>Trassenanmeldung (Portal+CI), Trassenkonstruktion (IFP), Stammdaten, TAF/TAP</td><td>Angebot + Vertrag (SV), Abrechnung, Stornierung, Rahmenvertraege, Vertragsdaten</td></tr>
|
||||
<tr><td><em>Vom Kundenwunsch bis Konstruktionsauftrag</em></td><td><em>Vom Angebot bis zur Rechnung</em></td></tr>
|
||||
</table>
|
||||
<p>✔ Sauberster Schnitt entlang Geschaeftsprozess, Abrechnung hat eigenes Team | ✘ SV muesste aufgeteilt werden, Portal zeigt beides</p>
|
||||
|
||||
<h3>Option C: Hybrid — EMPFOHLEN</h3>
|
||||
<ac:structured-macro ac:name="tip"><ac:rich-text-body>
|
||||
<p><strong>Empfohlenes Modell:</strong> OpsDev (permanent) + 2 fachliche Teams (permanent) + temporaere Feature-Squads fuer grosse Themen.</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
<table>
|
||||
<tr><th>OpsDev (~6 Pers.)</th><th>Team "Bestellen" (~12)</th><th>Team "Verarbeiten" (~12)</th></tr>
|
||||
<tr><td>Betrieb, Deployment<br/>Monitoring, Infrastruktur<br/>Bug-Triage<br/>Lead: OpsDev-Lead</td><td>Portal UI + MW<br/>Common Interface<br/>Stammdaten, Kundendaten<br/>TAF/TAP Konverter<br/>Click&Ride<br/>PO + BA + Devs</td><td>Steuerung Vertrieb<br/>Auftrags-Verwaltung<br/>IFP-Connector<br/>Archivierung<br/>Vertragsdaten, Abrechnung<br/>PO + BA + Devs</td></tr>
|
||||
<tr><td>Bugs: Infrastruktur</td><td>Bugs: Portal, CI, STB</td><td>Bugs: SV, AV, IFP</td></tr>
|
||||
</table>
|
||||
<p><strong>Fuer grosse Features</strong> (GelV, ujBau, NAÄ): Temporaer 2-3 Personen aus beiden Teams zusammenziehen. Nach Abschluss: zurueck + 1 Person Hypercare in OpsDev.</p>
|
||||
|
||||
<h3>Gesamtbewertung</h3>
|
||||
<table>
|
||||
<tr><th>Option</th><th>Speed</th><th>Komplexitaet</th><th>Deps</th><th>Qualitaet</th><th>Ownership</th></tr>
|
||||
<tr><td>A: OpsDev + Pool</td><td>⭐⭐⭐⭐</td><td>⭐⭐</td><td>⭐⭐⭐⭐⭐</td><td>⭐⭐⭐</td><td>⭐⭐⭐</td></tr>
|
||||
<tr><td>B1: Bestellen/Abwickeln</td><td>⭐⭐⭐</td><td>⭐⭐⭐</td><td>⭐⭐⭐</td><td>⭐⭐⭐</td><td>⭐⭐⭐⭐</td></tr>
|
||||
<tr><td>B4: Trasse/Vertrag</td><td>⭐⭐⭐</td><td>⭐⭐⭐⭐</td><td>⭐⭐⭐⭐</td><td>⭐⭐⭐⭐</td><td>⭐⭐⭐⭐</td></tr>
|
||||
<tr style="background:#d5e8d4;"><td><strong>C: Hybrid (empfohlen)</strong></td><td>⭐⭐⭐⭐</td><td>⭐⭐⭐⭐</td><td>⭐⭐⭐</td><td>⭐⭐⭐⭐</td><td>⭐⭐⭐⭐⭐</td></tr>
|
||||
<tr><td>D: Spotify</td><td>⭐⭐⭐</td><td>⭐</td><td>⭐⭐⭐</td><td>⭐⭐⭐</td><td>⭐⭐⭐</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Noch zu klaeren</h3>
|
||||
<ol>
|
||||
<li><strong>SV aufteilen?</strong> — Ist der Monolith (93K LoC) technisch teilbar?</li>
|
||||
<li><strong>NAÄ als Querschnitt</strong> — Dediziertes Feature-Team oder feste Zuordnung?</li>
|
||||
<li><strong>Portal-Ownership</strong> — Portal zeigt Daten aus allen Services. Eigener Querschnitt?</li>
|
||||
<li><strong>Abrechnung</strong> — Eigenes Team wert? Oder bei "Verarbeiten"?</li>
|
||||
<li><strong>Personelle Passung</strong> — Staerken-Mapping der 35 Personen</li>
|
||||
<li><strong>Uebergangsphase</strong> — Dauer, Produktivitaetsverlust?</li>
|
||||
<li><strong>PO-Struktur</strong> — 1 PO pro Team oder uebergreifend + Feature-POs?</li>
|
||||
<li><strong>Metriken</strong> — Bug-Rate, Durchlaufzeit, Deployment-Frequenz als Erfolgsmessung</li>
|
||||
</ol>
|
||||
|
||||
<h3>Fehlende Daten fuer Entscheidung</h3>
|
||||
<ul>
|
||||
<li>Git-Contributions pro Person/Service (wer kennt welchen Code?)</li>
|
||||
<li>Abhaengigkeits-Graph zwischen Services (Kafka-Topics, REST-Calls)</li>
|
||||
<li>Kundenfeedback: Welche Features werden am meisten nachgefragt?</li>
|
||||
<li>Camunda-Prozess-Instanzen: Wie oft laeuft welcher Prozess?</li>
|
||||
</ul>
|
||||
"""
|
||||
|
||||
# Push to Confluence
|
||||
def api_request(url, method="GET", data=None):
|
||||
req = urllib.request.Request(url, method=method)
|
||||
req.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
req.add_header("Content-Type", "application/json")
|
||||
if data:
|
||||
req.data = json.dumps(data).encode("utf-8")
|
||||
return json.loads(urllib.request.urlopen(req, context=ctx, timeout=30).read().decode("utf-8"))
|
||||
|
||||
# Check if page exists
|
||||
search_url = f"{BASE}/content?spaceKey={SPACE}&title={urllib.parse.quote(TITLE)}&type=page"
|
||||
req = urllib.request.Request(search_url)
|
||||
req.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
|
||||
search_data = json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
if search_data.get("results"):
|
||||
# Update
|
||||
page_id = search_data["results"][0]["id"]
|
||||
ver_url = f"{BASE}/content/{page_id}?expand=version"
|
||||
req2 = urllib.request.Request(ver_url)
|
||||
req2.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
ver_data = json.loads(urllib.request.urlopen(req2, context=ctx, timeout=30).read().decode("utf-8"))
|
||||
version = ver_data["version"]["number"] + 1
|
||||
|
||||
body = json.dumps({"version": {"number": version}, "title": TITLE, "type": "page", "body": {"storage": {"value": html, "representation": "storage"}}}).encode("utf-8")
|
||||
req3 = urllib.request.Request(f"{BASE}/content/{page_id}", method="PUT", data=body)
|
||||
req3.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
req3.add_header("Content-Type", "application/json")
|
||||
result = json.loads(urllib.request.urlopen(req3, context=ctx, timeout=30).read().decode("utf-8"))
|
||||
print(f"Aktualisiert: https://arija-confluence.jaas.service.deutschebahn.com/pages/viewpage.action?pageId={result['id']}")
|
||||
else:
|
||||
# Create
|
||||
body = json.dumps({"type": "page", "title": TITLE, "space": {"key": SPACE}, "ancestors": [{"id": PARENT}], "body": {"storage": {"value": html, "representation": "storage"}}}).encode("utf-8")
|
||||
req3 = urllib.request.Request(f"{BASE}/content", method="POST", data=body)
|
||||
req3.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
req3.add_header("Content-Type", "application/json")
|
||||
result = json.loads(urllib.request.urlopen(req3, context=ctx, timeout=30).read().decode("utf-8"))
|
||||
print(f"Erstellt: https://arija-confluence.jaas.service.deutschebahn.com/pages/viewpage.action?pageId={result['id']}")
|
||||
@@ -0,0 +1,128 @@
|
||||
"""Pusht Team-Uebersicht inkl. TrassenOrder als Confluence-Seite."""
|
||||
import urllib.request, json, ssl, urllib.parse
|
||||
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
TOKEN = secrets.get("CONFLUENCE_TOKEN", "")
|
||||
BASE = "https://arija-confluence.jaas.service.deutschebahn.com/rest/api"
|
||||
SPACE = "BES"
|
||||
PARENT = "581013135"
|
||||
TITLE = "26. Teams inkl. TrassenOrder"
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
html = """<h2>Team-Landschaft pathOS + TrassenOrder</h2>
|
||||
<p><strong>Stand:</strong> 2026-04-30 | Kontext: Reorganisations-Planung</p>
|
||||
|
||||
<ac:structured-macro ac:name="info"><ac:rich-text-body>
|
||||
<p><strong>TrassenOrder (TraPo)</strong> ist ein Testballon, um zu pruefen ob eine andere Herangehensweise (entkoppelt, UX-first, klein) schneller, besser und sicherer funktioniert als der pathOS-Ansatz (Microservice-Monolith, 167 Repos, 35+ Personen).</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
|
||||
<h3>Aktuelle Team-Landschaft</h3>
|
||||
<table>
|
||||
<tr><th>Team</th><th>Personen</th><th>Fokus</th><th>Services</th><th>Deployment</th><th>Besonderheit</th></tr>
|
||||
<tr><td><strong>Team 404</strong></td><td>~10</td><td>Portal UI + Middleware</td><td>2 (68K LoC)</td><td>pathOS Release-Zug</td><td>33% Bug-Rate, kundenseitig</td></tr>
|
||||
<tr><td><strong>Team CIB</strong></td><td>~13</td><td>Prozesse, Camunda, Backend</td><td>~15 (122K LoC)</td><td>pathOS Release-Zug</td><td>SV-Monolith, Abrechnung</td></tr>
|
||||
<tr><td><strong>Team Zero</strong></td><td>~9</td><td>TAF/TAP Schnittstellen</td><td>~9 (38K LoC)</td><td>pathOS Release-Zug</td><td>Beste Qualitaet, OPs-Rotation</td></tr>
|
||||
<tr><td><strong>OPs Squad</strong></td><td>2 fest + rot.</td><td>Betrieb, Infrastruktur</td><td>Infra-Repos</td><td>pathOS Release-Zug</td><td>Seit PI 39 (ex-STeam)</td></tr>
|
||||
<tr><td><strong>DevOps</strong></td><td>~7</td><td>CI/CD, Tooling, Pipelines</td><td>Pipelines, Helm</td><td>pathOS Release-Zug</td><td>Jan Lubenow = 39% SPOF</td></tr>
|
||||
<tr style="background:#dae8fc;"><td><strong>TrassenOrder (TraPo)</strong></td><td>~5 (klein)</td><td>GelV-Portal (UX-first)</td><td>1 (neu)</td><td><strong>Unabhaengig!</strong></td><td>Testballon, Discovery-Phase</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>TrassenOrder vs. pathOS — Vergleich der Ansaetze</h3>
|
||||
<table>
|
||||
<tr><th>Dimension</th><th>pathOS (aktuell)</th><th>TrassenOrder (Testballon)</th></tr>
|
||||
<tr><td><strong>Architektur</strong></td><td>Microservice-Monolith (synchrone Releases, 167 Repos)</td><td>Externes System, eigene Schnittstellen, unabhaengig deploybar</td></tr>
|
||||
<tr><td><strong>Teamgroesse</strong></td><td>~35+ Personen, 5 Teams</td><td>~5 Personen, 1 Team</td></tr>
|
||||
<tr><td><strong>Zielgruppe</strong></td><td>Alle EVUs (Experten + Anfaenger)</td><td>Kleine EVUs, Ein-Mann-Betriebe ("WhatsApp-Nutzer")</td></tr>
|
||||
<tr><td><strong>UX-Ansatz</strong></td><td>240+ Formularfelder, Experten-Tool</td><td>3 Angaben fuer eine Bestellung, radikal vereinfacht</td></tr>
|
||||
<tr><td><strong>Deployment</strong></td><td>Release-Zug (alle Services zusammen)</td><td>Unabhaengig, eigener Rhythmus</td></tr>
|
||||
<tr><td><strong>Abhaengigkeiten</strong></td><td>Hoch (Kafka, Camunda, 15+ Services)</td><td>Minimal (nur API-Schnittstellen zu Primaerquellen)</td></tr>
|
||||
<tr><td><strong>Geschwindigkeit</strong></td><td>PI-getaktet (10 Wochen)</td><td>Kontinuierlich, Feature-basiert</td></tr>
|
||||
<tr><td><strong>Scope</strong></td><td>Netzfahrplan + GelV + ujBau + Abrechnung</td><td>Nur GelV (Gelegenheitsverkehr)</td></tr>
|
||||
<tr><td><strong>Phase</strong></td><td>Produktiv seit Dez 2025</td><td>Discovery seit Jan 2026, Livegang Q4 2026</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Was testet der Testballon?</h3>
|
||||
<ac:structured-macro ac:name="panel"><ac:parameter ac:name="title">Hypothesen</ac:parameter><ac:rich-text-body>
|
||||
<ol>
|
||||
<li><strong>Schneller:</strong> Kann ein kleines, entkoppeltes Team schneller liefern als ein grosses im Release-Zug?</li>
|
||||
<li><strong>Besser:</strong> Fuehrt UX-first + radikale Vereinfachung zu besserer Nutzerzufriedenheit?</li>
|
||||
<li><strong>Sicherer:</strong> Reduziert Entkopplung das Risiko (kein Dominoeffekt bei Fehlern)?</li>
|
||||
</ol>
|
||||
<p><strong>Wenn der Testballon erfolgreich ist:</strong> Das Modell koennte auf weitere Bereiche uebertragen werden (Netzfahrplan, ujBau). Langfristig koennte TrassenOrder das pathOS-Portal <strong>abloesen</strong>.</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
|
||||
<h3>Schnittstellen zwischen pathOS und TrassenOrder</h3>
|
||||
<table>
|
||||
<tr><th>Schnittstelle</th><th>Richtung</th><th>Zweck</th></tr>
|
||||
<tr><td>Trassenanmeldung API</td><td>TraPo → pathOS</td><td>Bestellung absetzen</td></tr>
|
||||
<tr><td>Stammdaten API</td><td>TraPo ← Primaerquelle</td><td>Direkt, NICHT ueber pathOS</td></tr>
|
||||
<tr><td>Angebots-Rueckmeldung</td><td>pathOS → TraPo</td><td>Ergebnis der Konstruktion</td></tr>
|
||||
<tr><td>Trassenfinder</td><td>TraPo ← BVU</td><td>Routing, Validierung (Fernziel)</td></tr>
|
||||
</table>
|
||||
<p><strong>Architekturentscheidung:</strong> TrassenOrder greift auf Primaerquellen direkt zu — nicht ueber eine pathOS-Zwischenschicht. Das vermeidet Abhaengigkeiten vom pathOS Release-Zug.</p>
|
||||
|
||||
<h3>Implikationen fuer Team-Reorganisation</h3>
|
||||
<table>
|
||||
<tr><th>Szenario</th><th>Auswirkung auf pathOS-Teams</th></tr>
|
||||
<tr><td><strong>TraPo erfolgreich → GelV wandert zu TraPo</strong></td><td>pathOS kann sich auf Netzfahrplan + ujBau + Abrechnung konzentrieren. Vereinfacht den fachlichen Schnitt erheblich. Click&Ride (ADR-72) wird obsolet.</td></tr>
|
||||
<tr><td><strong>TraPo erfolgreich → Modell wird uebertragen</strong></td><td>Weitere kleine Teams fuer spezifische Domaenen. pathOS wird zum API-Backend-Layer. Portal-Team (404) wird langfristig obsolet.</td></tr>
|
||||
<tr><td><strong>TraPo scheitert → GelV bleibt bei pathOS</strong></td><td>Keine Aenderung. Click&Ride Anbindung (ADR-72) wird weiter von CIB gebaut.</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Offene Fragen</h3>
|
||||
<ul>
|
||||
<li>Wann ist der Testballon "erfolgreich"? Welche Metriken? (Time-to-Market, Nutzerzufriedenheit, Bug-Rate?)</li>
|
||||
<li>Wie wird die Schnittstelle zwischen TraPo und pathOS-Backend definiert und versioniert?</li>
|
||||
<li>Wer pflegt die Schnittstelle langfristig? (API-Vertrag)</li>
|
||||
<li>Kann das TraPo-Modell auf Netzfahrplan skaliert werden? (Komplexitaet ist dort 10x hoeher)</li>
|
||||
<li>Was passiert mit Team 404 wenn TrassenOrder das Portal langfristig abloest?</li>
|
||||
</ul>
|
||||
|
||||
<h3>Team-Kennzahlen (Vergleich)</h3>
|
||||
<table>
|
||||
<tr><th>Metrik</th><th>pathOS gesamt</th><th>TrassenOrder</th><th>Faktor</th></tr>
|
||||
<tr><td>Personen</td><td>~35</td><td>~5</td><td>7x</td></tr>
|
||||
<tr><td>Services</td><td>~40 aktiv</td><td>1</td><td>40x</td></tr>
|
||||
<tr><td>Lines of Code</td><td>~260K</td><td>~0 (Discovery)</td><td>—</td></tr>
|
||||
<tr><td>Jira-Projekte</td><td>6 (O2C*, TTTI)</td><td>1 (TRAPO)</td><td>6x</td></tr>
|
||||
<tr><td>Release-Frequenz</td><td>~2x/Monat (KTU)</td><td>Kontinuierlich (Ziel)</td><td>—</td></tr>
|
||||
<tr><td>Bug-Rate</td><td>20-33%</td><td>0% (noch kein Code)</td><td>—</td></tr>
|
||||
<tr><td>Deployment-Abhaengigkeiten</td><td>Hoch (17+ Umgebungen)</td><td>Keine</td><td>—</td></tr>
|
||||
</table>
|
||||
"""
|
||||
|
||||
# Push
|
||||
search_url = f"{BASE}/content?spaceKey={SPACE}&title={urllib.parse.quote(TITLE)}&type=page"
|
||||
req = urllib.request.Request(search_url)
|
||||
req.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
|
||||
search_data = json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
if search_data.get("results"):
|
||||
page_id = search_data["results"][0]["id"]
|
||||
ver_url = f"{BASE}/content/{page_id}?expand=version"
|
||||
req2 = urllib.request.Request(ver_url)
|
||||
req2.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
ver_data = json.loads(urllib.request.urlopen(req2, context=ctx, timeout=30).read().decode("utf-8"))
|
||||
version = ver_data["version"]["number"] + 1
|
||||
body = json.dumps({"version": {"number": version}, "title": TITLE, "type": "page", "body": {"storage": {"value": html, "representation": "storage"}}}).encode("utf-8")
|
||||
req3 = urllib.request.Request(f"{BASE}/content/{page_id}", method="PUT", data=body)
|
||||
req3.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
req3.add_header("Content-Type", "application/json")
|
||||
result = json.loads(urllib.request.urlopen(req3, context=ctx, timeout=30).read().decode("utf-8"))
|
||||
print(f"Aktualisiert: https://arija-confluence.jaas.service.deutschebahn.com/pages/viewpage.action?pageId={result['id']}")
|
||||
else:
|
||||
body = json.dumps({"type": "page", "title": TITLE, "space": {"key": SPACE}, "ancestors": [{"id": PARENT}], "body": {"storage": {"value": html, "representation": "storage"}}}).encode("utf-8")
|
||||
req3 = urllib.request.Request(f"{BASE}/content", method="POST", data=body)
|
||||
req3.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
req3.add_header("Content-Type", "application/json")
|
||||
result = json.loads(urllib.request.urlopen(req3, context=ctx, timeout=30).read().decode("utf-8"))
|
||||
print(f"Erstellt: https://arija-confluence.jaas.service.deutschebahn.com/pages/viewpage.action?pageId={result['id']}")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Liest spezifische Confluence-Seiten per API
|
||||
#>
|
||||
|
||||
$ConfluenceUrl = "https://arija-confluence.jaas.service.deutschebahn.com"
|
||||
$ApiBase = "$ConfluenceUrl/rest/api"
|
||||
$OutputDir = "project-audit\data\confluence-ttsi"
|
||||
|
||||
$PageIds = @(
|
||||
@{ id = "424295009"; name = "TAF-TAP-TSI-Programmakte" }
|
||||
@{ id = "428930895"; name = "TAF-TAP-TSI-Programmsteuerung" }
|
||||
@{ id = "490596078"; name = "TAF-TAP-TSI-Fachliche-Dokumentation" }
|
||||
)
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
Write-Host "============================================================"
|
||||
Write-Host " Confluence TTSI-Space Seiten lesen"
|
||||
Write-Host "============================================================"
|
||||
|
||||
$PAT = Read-Host "Confluence PAT"
|
||||
$Headers = @{ "Authorization" = "Bearer $PAT" }
|
||||
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
|
||||
Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue
|
||||
|
||||
function Convert-HtmlToText($html) {
|
||||
if ($null -eq $html) { return "" }
|
||||
$text = $html -replace '<br\s*/?>', "`n"
|
||||
$text = $text -replace '<p[^>]*>', "`n"
|
||||
$text = $text -replace '</p>', ""
|
||||
$text = $text -replace '<li[^>]*>', "`n- "
|
||||
$text = $text -replace '<h[1-6][^>]*>', "`n## "
|
||||
$text = $text -replace '</h[1-6]>', "`n"
|
||||
$text = $text -replace '<tr[^>]*>', "`n| "
|
||||
$text = $text -replace '<td[^>]*>', " | "
|
||||
$text = $text -replace '<th[^>]*>', " | "
|
||||
$text = $text -replace '<[^>]+>', ''
|
||||
try { $text = [System.Web.HttpUtility]::HtmlDecode($text) } catch {}
|
||||
return $text.Trim()
|
||||
}
|
||||
|
||||
foreach ($Page in $PageIds) {
|
||||
$PageId = $Page.id
|
||||
$PName = $Page.name
|
||||
Write-Host ""
|
||||
Write-Host ">> $PName (ID: $PageId)" -ForegroundColor Yellow
|
||||
|
||||
$Uri = "$ApiBase/content/$PageId`?expand=body.storage,version,space,children.page"
|
||||
try {
|
||||
$Resp = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$Data = $Resp.Content | ConvertFrom-Json
|
||||
Write-Host " Titel: $($Data.title) (v$($Data.version.number))" -ForegroundColor Green
|
||||
|
||||
# HTML speichern
|
||||
$HtmlContent = ""
|
||||
if ($Data.body -and $Data.body.storage) { $HtmlContent = $Data.body.storage.value }
|
||||
$HtmlContent | Out-File "$OutputDir\$PName.html" -Encoding UTF8
|
||||
|
||||
# Markdown speichern
|
||||
$TextContent = Convert-HtmlToText $HtmlContent
|
||||
$MdContent = "# $($Data.title)`n`n> Page ID: $PageId | Version: $($Data.version.number) | Space: $($Data.space.key)`n`n---`n`n$TextContent"
|
||||
$MdContent | Out-File "$OutputDir\$PName.md" -Encoding UTF8
|
||||
|
||||
Write-Host " Gespeichert ($($HtmlContent.Length) Zeichen)"
|
||||
|
||||
# Kindseiten auflisten
|
||||
if ($Data.children -and $Data.children.page -and $Data.children.page.results) {
|
||||
Write-Host " Kindseiten:" -ForegroundColor DarkGray
|
||||
foreach ($Child in $Data.children.page.results) {
|
||||
Write-Host " - $($Child.title) (ID: $($Child.id))"
|
||||
}
|
||||
|
||||
# Kindseiten auch lesen
|
||||
foreach ($Child in $Data.children.page.results) {
|
||||
Write-Host " >> Kindseite: $($Child.title)..." -NoNewline
|
||||
$ChildUri = "$ApiBase/content/$($Child.id)`?expand=body.storage,version"
|
||||
try {
|
||||
$CResp = Invoke-WebRequest -Uri $ChildUri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$CData = $CResp.Content | ConvertFrom-Json
|
||||
$CHtml = ""
|
||||
if ($CData.body -and $CData.body.storage) { $CHtml = $CData.body.storage.value }
|
||||
$CText = Convert-HtmlToText $CHtml
|
||||
$SafeTitle = $Child.title -replace '[\\/:*?"<>|]', '_'
|
||||
if ($SafeTitle.Length -gt 60) { $SafeTitle = $SafeTitle.Substring(0, 60) }
|
||||
$CMd = "# $($Child.title)`n`n> Page ID: $($Child.id) | Parent: $PName`n`n---`n`n$CText"
|
||||
$CMd | Out-File "$OutputDir\$PName`_$SafeTitle.md" -Encoding UTF8
|
||||
$CHtml | Out-File "$OutputDir\$PName`_$SafeTitle.html" -Encoding UTF8
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host " FEHLER" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
Write-Host " FERTIG! Ergebnisse in $OutputDir" -ForegroundColor Green
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
@@ -0,0 +1,283 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Reorganisiert die Analyse-pathOS Confluence-Seiten in Kategorien mit Unterseiten
|
||||
#>
|
||||
|
||||
$ConfluenceUrl = "https://arija-confluence.jaas.service.deutschebahn.com"
|
||||
$SpaceKey = "BES"
|
||||
$ParentPageId = "581013135" # Analyse pathOS Stammseite
|
||||
$ApiBase = "$ConfluenceUrl/rest/api"
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
Write-Host "============================================================"
|
||||
Write-Host " Confluence Reorganisation - Analyse pathOS"
|
||||
Write-Host "============================================================"
|
||||
|
||||
$PAT = $null
|
||||
$SecretsPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\.secrets"
|
||||
if (Test-Path $SecretsPath) {
|
||||
Get-Content $SecretsPath | ForEach-Object {
|
||||
if ($_ -match "^CONFLUENCE_TOKEN=(.+)$") {
|
||||
$val = $Matches[1].Trim()
|
||||
if ($val -ne "HIER_TOKEN_EINTRAGEN") { $PAT = $val }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $PAT) { $PAT = Read-Host "Confluence PAT" }
|
||||
$Headers = @{ "Authorization" = "Bearer $PAT"; "Content-Type" = "application/json" }
|
||||
|
||||
function Invoke-ConfApi($Method, $Uri, $JsonBody) {
|
||||
try {
|
||||
if ($JsonBody) {
|
||||
$Resp = Invoke-WebRequest -Uri $Uri -Method $Method -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($JsonBody)) -UseBasicParsing -ErrorAction Stop
|
||||
} else {
|
||||
$Resp = Invoke-WebRequest -Uri $Uri -Method $Method -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
}
|
||||
return ($Resp.Content | ConvertFrom-Json)
|
||||
} catch {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
function Create-CategoryPage($Title, $ParentId, $Body) {
|
||||
# Pruefen ob existiert
|
||||
$SearchUri = "$ApiBase/content?spaceKey=$SpaceKey&title=$([Uri]::EscapeDataString($Title))&type=page"
|
||||
$Search = Invoke-ConfApi "Get" $SearchUri
|
||||
if ($Search.results -and $Search.results.Count -gt 0) {
|
||||
Write-Host " Existiert bereits (ID: $($Search.results[0].id))" -ForegroundColor DarkGray
|
||||
return $Search.results[0].id
|
||||
}
|
||||
$Json = @{
|
||||
type = "page"
|
||||
title = $Title
|
||||
space = @{ key = $SpaceKey }
|
||||
ancestors = @(@{ id = $ParentId })
|
||||
body = @{ storage = @{ value = $Body; representation = "storage" } }
|
||||
} | ConvertTo-Json -Depth 10
|
||||
$Result = Invoke-ConfApi "Post" "$ApiBase/content" $Json
|
||||
if ($Result) {
|
||||
Write-Host " Erstellt (ID: $($Result.id))" -ForegroundColor Green
|
||||
return $Result.id
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Move-Page($PageId, $NewParentId) {
|
||||
# Aktuelle Version und Parent holen
|
||||
$Page = Invoke-ConfApi "Get" "$ApiBase/content/$PageId`?expand=version,ancestors"
|
||||
if (-not $Page) { return $false }
|
||||
|
||||
# Pruefen ob schon unter richtigem Parent
|
||||
if ($Page.ancestors -and $Page.ancestors.Count -gt 0) {
|
||||
$CurrentParent = $Page.ancestors[-1].id
|
||||
if ($CurrentParent -eq $NewParentId) {
|
||||
Write-Host " Bereits korrekt" -ForegroundColor DarkGray
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
$Version = $Page.version.number + 1
|
||||
$Title = $Page.title
|
||||
$Json = @{
|
||||
type = "page"
|
||||
title = $Title
|
||||
version = @{ number = $Version }
|
||||
ancestors = @(@{ id = $NewParentId })
|
||||
} | ConvertTo-Json -Depth 10
|
||||
$Result = Invoke-ConfApi "Put" "$ApiBase/content/$PageId" $Json
|
||||
if ($Result) { return $true }
|
||||
return $false
|
||||
}
|
||||
|
||||
# === Bekannte Seiten-IDs (aus letztem Push) ===
|
||||
$Pages = @{
|
||||
"summary" = "581019325" # 0. Management Summary
|
||||
"uebersicht" = "581013185" # 1. Uebersicht
|
||||
"probleme" = "581013186" # 2. Problemfelder
|
||||
"roadmap2026" = "581013187" # 3. Technische Roadmap 2026
|
||||
"gitlab" = "581013188" # 4. GitLab
|
||||
"glossar" = "581013189" # 5. Abkuerzungen
|
||||
"architektur" = "581013355" # 6. Architekturskizze
|
||||
"fortschritt" = "581013356" # 7. Projektfortschritt
|
||||
"links" = "581013548" # 8. Links
|
||||
"journey" = "581013549" # 9. Customer Journey
|
||||
"deepdive" = "581015678" # 10. Tech Deep Dive
|
||||
"teamworkflow" = "581015844" # 11. Team Analyse
|
||||
"aktionsplan" = "581016065" # 12. Roadmap Aktionsplan
|
||||
"jira" = "581016316" # 13. Jira
|
||||
"next" = "581019221" # 14. Naechste Schritte
|
||||
"ttt" = "581019444" # 15. TTT/NEP2
|
||||
"roadmapfull" = "581019652" # 16. Roadmap uebergreifend
|
||||
"ttti" = "581023872" # 17. TTTI Deep Dive
|
||||
"velocity" = "581024118" # 18. Velocity
|
||||
}
|
||||
|
||||
# Dynamisch: SonarQube-Seite per Titel suchen (ID noch unbekannt)
|
||||
$SonarSearch = Invoke-ConfApi "Get" "$ApiBase/content?spaceKey=$SpaceKey&title=$([Uri]::EscapeDataString('19. SonarQube Code-Qualitaet'))&type=page"
|
||||
if ($SonarSearch -and $SonarSearch.results -and $SonarSearch.results.Count -gt 0) {
|
||||
$Pages["sonarqube"] = $SonarSearch.results[0].id
|
||||
Write-Host " SonarQube-Seite gefunden: $($Pages['sonarqube'])" -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
# === Test Verbindung ===
|
||||
Write-Host ""
|
||||
Write-Host ">> Teste Verbindung..." -ForegroundColor Yellow
|
||||
$Test = Invoke-ConfApi "Get" "$ApiBase/content/$ParentPageId"
|
||||
if (-not $Test) { Write-Host "Verbindung fehlgeschlagen!" -ForegroundColor Red; exit 1 }
|
||||
Write-Host " OK: $($Test.title)" -ForegroundColor Green
|
||||
|
||||
# === Schritt 1: Kategorie-Seiten erstellen ===
|
||||
Write-Host ""
|
||||
Write-Host ">> Erstelle Kategorie-Seiten..." -ForegroundColor Yellow
|
||||
|
||||
Write-Host " Systemueberblick..." -NoNewline
|
||||
$CatSystem = Create-CategoryPage "Systemueberblick" $ParentPageId '<h2>Systemueberblick</h2><p>Architektur, Komponenten und Schnittstellen von pathOS.</p><h3>Unterseiten</h3><table><tr><th>Seite</th><th>Inhalt</th></tr><tr><td><strong>Uebersicht und Gesamtbild</strong></td><td>Zahlen auf einen Blick: 167 Projekte, 15 Kern-Services, Tech-Stack, Teams</td></tr><tr><td><strong>Architekturskizze</strong></td><td>Alle Komponenten und 15+ externe Schnittstellen als Tabelle mit Farbkodierung</td></tr><tr><td><strong>Customer Journey</strong></td><td>Happy Path Trassenbestellung: 7 Schritte aus Kundensicht mit Datenfluss durch alle IT-Systeme</td></tr><tr><td><strong>GitLab Projektlandschaft</strong></td><td>167 Projekte in 9 Gruppen, Sprachen-Verteilung, Aktivitaets-Status</td></tr><tr><td><strong>Abkuerzungsverzeichnis</strong></td><td>35+ Fachbegriffe und Abkuerzungen (TPN, BEP, IFP, PMW, etc.)</td></tr></table><ac:structured-macro ac:name="children"><ac:parameter ac:name="all">true</ac:parameter></ac:structured-macro>'
|
||||
|
||||
Write-Host " Technische Analyse..." -NoNewline
|
||||
$CatTech = Create-CategoryPage "Technische Analyse" $ParentPageId '<h2>Technische Analyse</h2><p>Versionen, Dependencies, Code-Qualitaet und technische Roadmap.</p><h3>Unterseiten</h3><table><tr><th>Seite</th><th>Inhalt</th></tr><tr><td><strong>Technischer Deep Dive</strong></td><td>Gesundheits-Scorecard: Spring Boot 3.5 (Upgrade laeuft), Java 21, Camunda 8.8, CVE-Patches</td></tr><tr><td><strong>SonarQube Code-Qualitaet</strong></td><td>74 Projekte: Coverage, Bugs, Smells nach Team. Zero=Vorbild, CIB=SV-Schuld, tadef=0% Coverage</td></tr><tr><td><strong>Technische Roadmap 2026</strong></td><td>53 Items aus der internen Roadmap: 17 MUSS UKA, 24 MUSS 2026, 12 SOLL</td></tr><tr><td><strong>Nuetzliche Links</strong></td><td>GitLab, Confluence, Runbook, Portal-URLs, Kafka-UI, DefectDojo, oeffentliche Quellen</td></tr></table><ac:structured-macro ac:name="children"><ac:parameter ac:name="all">true</ac:parameter></ac:structured-macro>'
|
||||
|
||||
Write-Host " Teams und Velocity..." -NoNewline
|
||||
$CatTeams = Create-CategoryPage "Teams und Velocity" $ParentPageId '<h2>Teams und Velocity</h2><p>Team-Struktur, Jira-Analyse, Durchsatz und Personenanalyse.</p><h3>Unterseiten</h3><table><tr><th>Seite</th><th>Inhalt</th></tr><tr><td><strong>Team und Workflow Analyse</strong></td><td>6 Teams (Zero, 404, CIB, OPs, BSSUPPORT, FbF), 5 Engpaesse, 9 Reorganisations-Empfehlungen</td></tr><tr><td><strong>Velocity und Team-Analyse</strong></td><td>12-Monats-Velocity, Go-Live-Effekt, Bug-Rate 10%→29%→20%, MVPs und Schwachstellen pro Person</td></tr><tr><td><strong>Jira-Analyse</strong></td><td>3845 Issues ueber 10 Boards, Backlog-Verteilung, Team-Auslastung</td></tr><tr><td><strong>Personal- und Bug-Analyse</strong></td><td>Rollen, Tenure, Bug-Korrelation pro Entwickler. SPOFs, Risiko-Matrix, Top 5 Massnahmen</td></tr></table><ac:structured-macro ac:name="children"><ac:parameter ac:name="all">true</ac:parameter></ac:structured-macro>'
|
||||
|
||||
Write-Host " TTT-Programm..." -NoNewline
|
||||
$CatTTT = Create-CategoryPage "TTT-Programm" $ParentPageId '<h2>TTT-Programm</h2><p>TAF/TAP TSI Programmkontext, NEP2, TTTI-Integration und Problemfelder.</p><h3>Unterseiten</h3><table><tr><th>Seite</th><th>Inhalt</th></tr><tr><td><strong>TTT-Programm und NEP2</strong></td><td>Go-Live Dez 2025, Go/No-Go Sep 2025 positiv, NEP2 stabil, Programmrisiken TTTSOL-42 bis -52</td></tr><tr><td><strong>TTTI Deep Dive</strong></td><td>1000 Issues, aber nur 42 echte Bugs. Kritischste nach Bereich (pathOS, TPN, ujBau, Abrechnung)</td></tr><tr><td><strong>Identifizierte Problemfelder</strong></td><td>5 Hauptprobleme: Deployment-Komplexitaet, Dokumentation, Tech Debt, Altsystem, Teams</td></tr></table><ac:structured-macro ac:name="children"><ac:parameter ac:name="all">true</ac:parameter></ac:structured-macro>'
|
||||
|
||||
Write-Host " Roadmap und Planung..." -NoNewline
|
||||
$CatRoadmap = Create-CategoryPage "Roadmap und Planung" $ParentPageId '<h2>Roadmap und Planung</h2><p>Priorisierte Aktionen, Zeitplaene und naechste Schritte.</p><h3>Unterseiten</h3><table><tr><th>Seite</th><th>Inhalt</th></tr><tr><td><strong>Roadmap uebergreifend und pathOS</strong></td><td>4 Phasen bis Mitte 2027 mit grafischem Team-Zeitstrahl: Stabilisieren, Haerten, Skalieren, Weiterentwickeln</td></tr><tr><td><strong>Roadmap und Aktionsplan</strong></td><td>24 priorisierte Aktionen (SOFORT/KURZ/MITTEL/LANG) mit Risiko-Matrix</td></tr><tr><td><strong>Produkt-Roadmap (konsolidiert)</strong></td><td>PO-Input aller 3 Teams + BO-Sicht. 18 neue Themen, 6 strategische Fragen, 4 Phasen</td></tr><tr><td><strong>Naechste Schritte</strong></td><td>Offene Punkte, Datenquellen-Status, priorisierte naechste Schritte</td></tr><tr><td><strong>Projektfortschritt</strong></td><td>Alle Iterationen mit Tasks, Zeiten und Ergebnissen</td></tr></table><ac:structured-macro ac:name="children"><ac:parameter ac:name="all">true</ac:parameter></ac:structured-macro>'
|
||||
|
||||
# === Schritt 2: Seiten verschieben ===
|
||||
Write-Host ""
|
||||
Write-Host ">> Verschiebe Seiten in Kategorien..." -ForegroundColor Yellow
|
||||
|
||||
# Systemueberblick
|
||||
$Moves = @(
|
||||
@{ id = $Pages["uebersicht"]; parent = $CatSystem; name = "Uebersicht" }
|
||||
@{ id = $Pages["architektur"]; parent = $CatSystem; name = "Architektur" }
|
||||
@{ id = $Pages["journey"]; parent = $CatSystem; name = "Customer Journey" }
|
||||
@{ id = $Pages["gitlab"]; parent = $CatSystem; name = "GitLab" }
|
||||
@{ id = $Pages["glossar"]; parent = $CatSystem; name = "Glossar" }
|
||||
# Technische Analyse
|
||||
@{ id = $Pages["deepdive"]; parent = $CatTech; name = "Tech Deep Dive" }
|
||||
@{ id = $Pages["roadmap2026"]; parent = $CatTech; name = "Roadmap 2026" }
|
||||
@{ id = $Pages["links"]; parent = $CatTech; name = "Links" }
|
||||
# Teams und Velocity
|
||||
@{ id = $Pages["teamworkflow"]; parent = $CatTeams; name = "Team Analyse" }
|
||||
@{ id = $Pages["velocity"]; parent = $CatTeams; name = "Velocity" }
|
||||
@{ id = $Pages["jira"]; parent = $CatTeams; name = "Jira" }
|
||||
# TTT-Programm
|
||||
@{ id = $Pages["ttt"]; parent = $CatTTT; name = "TTT/NEP2" }
|
||||
@{ id = $Pages["ttti"]; parent = $CatTTT; name = "TTTI" }
|
||||
@{ id = $Pages["probleme"]; parent = $CatTTT; name = "Problemfelder" }
|
||||
# Roadmap und Planung
|
||||
@{ id = $Pages["roadmapfull"]; parent = $CatRoadmap; name = "Roadmap gesamt" }
|
||||
@{ id = $Pages["aktionsplan"]; parent = $CatRoadmap; name = "Aktionsplan" }
|
||||
@{ id = $Pages["next"]; parent = $CatRoadmap; name = "Naechste Schritte" }
|
||||
@{ id = $Pages["fortschritt"]; parent = $CatRoadmap; name = "Fortschritt" }
|
||||
)
|
||||
|
||||
if ($Pages["sonarqube"]) {
|
||||
$Moves += @{ id = $Pages["sonarqube"]; parent = $CatTech; name = "SonarQube" }
|
||||
}
|
||||
|
||||
# Dynamisch: Neuere Seiten per Titel suchen und einsortieren
|
||||
$DynPages = @(
|
||||
@{ title = "19a. SonarQube Smells Detail"; parent = "CatTech"; name = "Smells Detail" }
|
||||
@{ title = "20. Geschaeftsprozesse"; parent = "CatSystem"; name = "Geschaeftsprozesse" }
|
||||
@{ title = "21. Produkt-Roadmap (konsolidiert)"; parent = "CatRoadmap"; name = "Produkt-Roadmap" }
|
||||
@{ title = "22. Personal- und Bug-Analyse"; parent = "CatTeams"; name = "Personal-Analyse" }
|
||||
)
|
||||
|
||||
foreach ($DP in $DynPages) {
|
||||
$DSearch = Invoke-ConfApi "Get" "$ApiBase/content?spaceKey=$SpaceKey&title=$([Uri]::EscapeDataString($DP.title))&type=page"
|
||||
if ($DSearch -and $DSearch.results -and $DSearch.results.Count -gt 0) {
|
||||
$TargetParent = switch ($DP.parent) {
|
||||
"CatTech" { $CatTech }
|
||||
"CatSystem" { $CatSystem }
|
||||
"CatRoadmap" { $CatRoadmap }
|
||||
"CatTeams" { $CatTeams }
|
||||
"CatTTT" { $CatTTT }
|
||||
}
|
||||
$Moves += @{ id = $DSearch.results[0].id; parent = $TargetParent; name = $DP.name }
|
||||
Write-Host " $($DP.name) gefunden (ID: $($DSearch.results[0].id))" -ForegroundColor DarkGray
|
||||
} else {
|
||||
Write-Host " $($DP.name) nicht gefunden (noch nicht gepusht?)" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($M in $Moves) {
|
||||
Write-Host " $($M.name) -> Kategorie..." -NoNewline
|
||||
$Ok = Move-Page $M.id $M.parent
|
||||
if ($Ok) { Write-Host " OK" -ForegroundColor Green }
|
||||
else { Write-Host " FEHLER" -ForegroundColor Red }
|
||||
}
|
||||
|
||||
# Management Summary umbenennen - ENTFERNT (alte Seite bereits geloescht)
|
||||
Write-Host " Management Summary: Bereits korrekt" -ForegroundColor DarkGray
|
||||
|
||||
# === Schritt 3: Stammseite aktualisieren ===
|
||||
Write-Host ""
|
||||
Write-Host ">> Stammseite aktualisieren..." -ForegroundColor Yellow
|
||||
|
||||
$StammPage = Invoke-ConfApi "Get" "$ApiBase/content/$ParentPageId`?expand=version"
|
||||
$StammVersion = $StammPage.version.number + 1
|
||||
|
||||
$StammHtml = @"
|
||||
<h2>Analyse pathOS — Portfolio-Audit</h2>
|
||||
<p><strong>Business Owner:</strong> Andre Knie | <strong>Start:</strong> 2026-04-22 | <strong>Methodik:</strong> Iterativ</p>
|
||||
|
||||
<ac:structured-macro ac:name="info"><ac:rich-text-body>
|
||||
<p>Systematische Analyse der pathOS-Plattform (~140 GitLab-Projekte, Confluence, Jira, Runbook). pathOS ist seit Dezember 2025 produktiv (FplJ 27). Aktuell: Verlaengerte Hypercare + NEP2.</p>
|
||||
</ac:rich-text-body></ac:structured-macro>
|
||||
|
||||
<h3>Schnelleinstieg</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td style="width:50%;vertical-align:top;padding:10px;">
|
||||
<h4><ac:link><ri:page ri:content-title="0. Management Summary"/></ac:link></h4>
|
||||
<p>Kompakte Zusammenfassung mit Zeitstrahl, Top-5 Risiken und Staerken.</p>
|
||||
</td>
|
||||
<td style="width:50%;vertical-align:top;padding:10px;">
|
||||
<h4><ac:link><ri:page ri:content-title="Roadmap und Planung"/></ac:link></h4>
|
||||
<p>Priorisierte Aktionen und Zeitplaene bis Mitte 2027.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3>Bereiche</h3>
|
||||
<table>
|
||||
<tr><th>Bereich</th><th>Inhalt</th></tr>
|
||||
<tr><td><ac:link><ri:page ri:content-title="Systemueberblick"/></ac:link></td><td>Architektur, Komponenten, Customer Journey, GitLab-Landschaft, Glossar</td></tr>
|
||||
<tr><td><ac:link><ri:page ri:content-title="Technische Analyse"/></ac:link></td><td>Versionen, Dependencies, Gesundheits-Scorecard, Technische Roadmap 2026</td></tr>
|
||||
<tr><td><ac:link><ri:page ri:content-title="Teams und Velocity"/></ac:link></td><td>Team-Struktur, 12-Monats-Velocity, Jira-Analyse, MVPs und Schwachstellen</td></tr>
|
||||
<tr><td><ac:link><ri:page ri:content-title="TTT-Programm"/></ac:link></td><td>TAF/TAP TSI Kontext, NEP2, TTTI-Integration, Problemfelder</td></tr>
|
||||
<tr><td><ac:link><ri:page ri:content-title="Roadmap und Planung"/></ac:link></td><td>Aktionsplan (24 Massnahmen), Roadmap bis Mitte 2027, Naechste Schritte</td></tr>
|
||||
</table>
|
||||
|
||||
<h3>Aktueller Status</h3>
|
||||
<table>
|
||||
<tr><th>Phase</th><th>Status</th></tr>
|
||||
<tr style="background:#d5e8d4;"><td>Phase 1: Domain Discovery</td><td>✅ Abgeschlossen (80 Min)</td></tr>
|
||||
<tr style="background:#d5e8d4;"><td>Phase 2: Technischer Deep Dive</td><td>✅ Abgeschlossen (55 Min)</td></tr>
|
||||
<tr style="background:#d5e8d4;"><td>Phase 3: Team & Workflow</td><td>✅ Abgeschlossen (30 Min)</td></tr>
|
||||
<tr style="background:#d5e8d4;"><td>Phase 4: Roadmap & Aktionsplan</td><td>✅ Abgeschlossen</td></tr>
|
||||
<tr style="background:#d5e8d4;"><td>Velocity-Analyse (12 Monate)</td><td>✅ Abgeschlossen</td></tr>
|
||||
<tr style="background:#d5e8d4;"><td>TTTI + TTTSol Deep Dive</td><td>✅ Abgeschlossen</td></tr>
|
||||
</table>
|
||||
"@
|
||||
|
||||
$StammJson = @{
|
||||
version = @{ number = $StammVersion }
|
||||
title = "Analyse pathOS"
|
||||
type = "page"
|
||||
body = @{ storage = @{ value = $StammHtml; representation = "storage" } }
|
||||
} | ConvertTo-Json -Depth 10
|
||||
|
||||
$StammResult = Invoke-ConfApi "Put" "$ApiBase/content/$ParentPageId" $StammJson
|
||||
if ($StammResult) { Write-Host " Stammseite aktualisiert" -ForegroundColor Green }
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
Write-Host " FERTIG! Seiten reorganisiert." -ForegroundColor Green
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
@@ -0,0 +1,183 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Test: Pusht eine einzelne Seite um Umlaut-Konvertierung zu pruefen
|
||||
#>
|
||||
|
||||
$ConfluenceUrl = "https://arija-confluence.jaas.service.deutschebahn.com"
|
||||
$SpaceKey = "BES"
|
||||
$ParentPageId = "581013135"
|
||||
$ApiBase = "$ConfluenceUrl/rest/api"
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
# Token aus .secrets
|
||||
$SecretsPath = Join-Path $ScriptDir "..\.secrets"
|
||||
$PAT = $null
|
||||
if (Test-Path $SecretsPath) {
|
||||
Get-Content $SecretsPath | ForEach-Object {
|
||||
if ($_ -match "^CONFLUENCE_TOKEN=(.+)$") {
|
||||
$val = $Matches[1].Trim()
|
||||
if ($val -ne "HIER_TOKEN_EINTRAGEN") { $PAT = $val }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $PAT) { $PAT = Read-Host "Confluence PAT" }
|
||||
$Headers = @{ "Authorization" = "Bearer $PAT"; "Content-Type" = "application/json" }
|
||||
|
||||
# Fix-Umlauts Funktion (identisch zum Haupt-Script)
|
||||
function Fix-Umlauts($html) {
|
||||
if ($null -eq $html -or $html -eq "") { return $html }
|
||||
$Protect = @(
|
||||
'Israel','Michael','Raphael','Emmanuel','Manuel','Daniel','Samuel',
|
||||
'Maurer','Enabler','enabler','Container','container','Cluster','cluster',
|
||||
'User','user','Tracing','Staging','Logging','Alerting','Monitoring',
|
||||
'Deployment','deployment','Security','security','Recovery','Discovery',
|
||||
'Delivery','Pipeline','pipeline','Release','release',
|
||||
'Performance','performance','Interface','interface',
|
||||
'Service','service','Feature','feature','Issue','issue',
|
||||
'value','true','blue','Due','due','Queue','queue',
|
||||
'Unique','unique','Continue','continue',
|
||||
'Funnel','Tunnel','tunnel','Buildpacks','Dockerfile',
|
||||
'OpenSearch','Gherkin','Kubernetes','Helmfile','Renovate',
|
||||
'Operate','Template','template','Tasklist',
|
||||
'Smoke','smoke','Analyse','analyse','Analysis',
|
||||
'Baseline','baseline','Namespace','namespace',
|
||||
'Codebase','Database','database','Rescue','rescue'
|
||||
)
|
||||
foreach ($w in $Protect) { $html = $html.Replace($w, "PROTECT_$w") }
|
||||
# Namen
|
||||
$html = $html.Replace('Hoerpel', 'Hörpel')
|
||||
$html = $html.Replace('Ruecker', 'Rücker')
|
||||
$html = $html.Replace('Koehler', 'Köhler')
|
||||
$html = $html.Replace('Goendoer', 'Göndör')
|
||||
# ue
|
||||
$html = $html.Replace('Ueber', 'Über')
|
||||
$html = $html.Replace('ueber', 'über')
|
||||
$html = $html.Replace('Pruef', 'Prüf'); $html = $html.Replace('pruef', 'prüf')
|
||||
$html = $html.Replace('fuehrung', 'führung'); $html = $html.Replace('Fuehrung', 'Führung')
|
||||
$html = $html.Replace('Verfueg', 'Verfüg'); $html = $html.Replace('verfueg', 'verfüg')
|
||||
$html = $html.Replace('Verschluess', 'Verschlüss'); $html = $html.Replace('verschluess', 'verschlüss')
|
||||
$html = $html.Replace('Schluess', 'Schlüss'); $html = $html.Replace('schluess', 'schlüss')
|
||||
$html = $html.Replace('Verknuepf', 'Verknüpf'); $html = $html.Replace('verknuepf', 'verknüpf')
|
||||
$html = $html.Replace('Unterstuetz', 'Unterstütz'); $html = $html.Replace('unterstuetz', 'unterstütz')
|
||||
$html = $html.Replace('Luecke', 'Lücke'); $html = $html.Replace('luecke', 'lücke')
|
||||
$html = $html.Replace('Rueck', 'Rück'); $html = $html.Replace('rueck', 'rück')
|
||||
$html = $html.Replace('Frueh', 'Früh'); $html = $html.Replace('frueh', 'früh')
|
||||
$html = $html.Replace('Nuetzlich', 'Nützlich'); $html = $html.Replace('nuetzlich', 'nützlich')
|
||||
$html = $html.Replace('Abkuerz', 'Abkürz'); $html = $html.Replace('abkuerz', 'abkürz')
|
||||
$html = $html.Replace('wuerde', 'würde'); $html = $html.Replace('Wuerde', 'Würde')
|
||||
$html = $html.Replace('muessen', 'müssen'); $html = $html.Replace('Muessen', 'Müssen')
|
||||
$html = $html.Replace('muesste', 'müsste'); $html = $html.Replace('Muesste', 'Müsste')
|
||||
$html = $html.Replace('koennte', 'könnte'); $html = $html.Replace('Koennte', 'Könnte')
|
||||
$html = $html.Replace('fuer ', 'für '); $html = $html.Replace('Fuer ', 'Für ')
|
||||
# oe
|
||||
$html = $html.Replace('Loesung', 'Lösung'); $html = $html.Replace('loesung', 'lösung')
|
||||
$html = $html.Replace('Erhoeh', 'Erhöh'); $html = $html.Replace('erhoeh', 'erhöh')
|
||||
$html = $html.Replace('noetig', 'nötig'); $html = $html.Replace('Noetig', 'Nötig')
|
||||
$html = $html.Replace('moeglich', 'möglich'); $html = $html.Replace('Moeglich', 'Möglich')
|
||||
$html = $html.Replace('geloest', 'gelöst')
|
||||
$html = $html.Replace('hoechst', 'höchst'); $html = $html.Replace('Hoechst', 'Höchst')
|
||||
$html = $html.Replace('koennen', 'können'); $html = $html.Replace('Koennen', 'Können')
|
||||
$html = $html.Replace('Zugehoer', 'Zugehör'); $html = $html.Replace('zugehoer', 'zugehör')
|
||||
$html = $html.Replace('gehoer', 'gehör')
|
||||
# ae
|
||||
$html = $html.Replace('Aenderung', 'Änderung'); $html = $html.Replace('aenderung', 'änderung')
|
||||
$html = $html.Replace('anfaellig', 'anfällig'); $html = $html.Replace('faellig', 'fällig')
|
||||
$html = $html.Replace('faehig', 'fähig')
|
||||
$html = $html.Replace('traeger', 'träger'); $html = $html.Replace('Traeger', 'Träger')
|
||||
$html = $html.Replace('laenger', 'länger'); $html = $html.Replace('Laenger', 'Länger')
|
||||
$html = $html.Replace('erklaer', 'erklär')
|
||||
$html = $html.Replace('klaer', 'klär'); $html = $html.Replace('Klaer', 'Klär')
|
||||
$html = $html.Replace('verstaerk', 'verstärk')
|
||||
$html = $html.Replace('Verstaend', 'Verständ'); $html = $html.Replace('verstaend', 'verständ')
|
||||
$html = $html.Replace('Abhaengig', 'Abhängig'); $html = $html.Replace('abhaengig', 'abhängig')
|
||||
$html = $html.Replace('haeufig', 'häufig'); $html = $html.Replace('Haeufig', 'Häufig')
|
||||
$html = $html.Replace('laeuft', 'läuft'); $html = $html.Replace('Laeuft', 'Läuft')
|
||||
$html = $html.Replace('waere', 'wäre'); $html = $html.Replace('Waere', 'Wäre')
|
||||
$html = $html.Replace('schaerfen', 'schärfen'); $html = $html.Replace('Schaerfen', 'Schärfen')
|
||||
$html = $html.Replace('aufraeumen', 'aufräumen'); $html = $html.Replace('Aufraeumen', 'Aufräumen')
|
||||
$html = $html.Replace('Verlaenger', 'Verlänger'); $html = $html.Replace('verlaenger', 'verlänger')
|
||||
$html = $html.Replace('Naechst', 'Nächst'); $html = $html.Replace('naechst', 'nächst')
|
||||
$html = $html.Replace('Massnahme', 'Maßnahme'); $html = $html.Replace('massnahme', 'maßnahme')
|
||||
$html = $html.Replace('Groesse', 'Größe'); $html = $html.Replace('groesse', 'größe')
|
||||
$html = $html.Replace('gleichmaessig', 'gleichmäßig')
|
||||
$html = $html -replace '([a-z])taet\b', '$1tät'
|
||||
# Schutz aufheben
|
||||
$html = $html.Replace('PROTECT_', '')
|
||||
return $html
|
||||
}
|
||||
|
||||
# Fuer Seitentitel: Echte Unicode-Zeichen (PS5.1 kompatibel via Char-Codes)
|
||||
function Fix-Title($title) {
|
||||
$title = $title.Replace('ue', "$([char]0xFC)")
|
||||
$title = $title.Replace('Ue', "$([char]0xDC)")
|
||||
$title = $title.Replace('oe', "$([char]0xF6)")
|
||||
$title = $title.Replace('Oe', "$([char]0xD6)")
|
||||
$title = $title.Replace('ae', "$([char]0xE4)")
|
||||
$title = $title.Replace('Ae', "$([char]0xC4)")
|
||||
return $title
|
||||
}
|
||||
|
||||
# Test-HTML mit vielen Umlauten
|
||||
$TestHtml = @"
|
||||
<h2>Umlaut-Test</h2>
|
||||
<p>Ueberblick ueber die Qualitaet: Hoechste Prioritaet fuer Massnahmen.</p>
|
||||
<p>Das Team muesste die Komplexitaet verstaendlich erklaeren koennen.</p>
|
||||
<table>
|
||||
<tr><th>Person</th><th>Rolle</th><th>Bewertung</th></tr>
|
||||
<tr><td>Leon Hoerpel</td><td>Dev</td><td>Bug-anfaellig</td></tr>
|
||||
<tr><td>Dominik Ruecker</td><td>Backend Dev</td><td>Verfuegbar</td></tr>
|
||||
<tr><td>Jonas Koehler</td><td>Dev</td><td>Leistungstraeger</td></tr>
|
||||
<tr><td>Sebastian Goendoer</td><td>Dev (ehem. STeam)</td><td>Wenig aktiv</td></tr>
|
||||
<tr><td>Emmanuel Kontcheu Tagne</td><td>Frontend Dev</td><td>KRITISCH</td></tr>
|
||||
</table>
|
||||
<p>Naechste Schritte: Abhaengigkeiten klaeren, Loesungen erhoehen, Verschluesselung pruefen.</p>
|
||||
"@
|
||||
|
||||
# Konvertieren und anzeigen
|
||||
$Converted = Fix-Umlauts $TestHtml
|
||||
|
||||
Write-Host "=== VORHER (Auszug) ===" -ForegroundColor Yellow
|
||||
Write-Host " Ueberblick, Qualitaet, Hoerpel, Goendoer, Emmanuel"
|
||||
Write-Host ""
|
||||
Write-Host "=== NACHHER (Auszug) ===" -ForegroundColor Green
|
||||
# Zeige ein paar konvertierte Stellen
|
||||
if ($Converted -match 'Überblick') { Write-Host " Ueberblick -> OK" -ForegroundColor Green }
|
||||
if ($Converted -match 'Qualität') { Write-Host " Qualitaet -> OK" -ForegroundColor Green }
|
||||
if ($Converted -match 'Hörpel') { Write-Host " Hoerpel -> OK" -ForegroundColor Green }
|
||||
if ($Converted -match 'Göndör') { Write-Host " Goendoer -> OK" -ForegroundColor Green }
|
||||
if ($Converted -match 'Emmanuel') { Write-Host " Emmanuel -> OK (nicht konvertiert)" -ForegroundColor Green }
|
||||
if ($Converted -match 'Maßnahme') { Write-Host " Massnahme -> OK" -ForegroundColor Green }
|
||||
if ($Converted -match 'für ') { Write-Host " fuer -> OK" -ForegroundColor Green }
|
||||
if ($Converted -match 'Abhängig') { Write-Host " Abhaengig -> OK" -ForegroundColor Green }
|
||||
if ($Converted -match 'klär') { Write-Host " klaeren -> OK" -ForegroundColor Green }
|
||||
if ($Converted -match 'Komplexität') { Write-Host " Komplexitaet -> OK" -ForegroundColor Green }
|
||||
|
||||
# Pruefen ob Emmanuel NICHT kaputt ist
|
||||
if ($Converted -match 'E&') { Write-Host " FEHLER: Emmanuel wurde konvertiert!" -ForegroundColor Red }
|
||||
|
||||
Write-Host ""
|
||||
Write-Host ">> Pushe Test-Seite nach Confluence..." -ForegroundColor Yellow
|
||||
|
||||
# Push
|
||||
$SearchUri = "$ApiBase/content?spaceKey=$SpaceKey&title=$([Uri]::EscapeDataString('TEST Umlaut-Pruefung'))&type=page"
|
||||
$SearchResp = Invoke-WebRequest -Uri $SearchUri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$SearchData = $SearchResp.Content | ConvertFrom-Json
|
||||
|
||||
if ($SearchData.results -and $SearchData.results.Count -gt 0) {
|
||||
$PageId = $SearchData.results[0].id
|
||||
$VerResp = Invoke-WebRequest -Uri "$ApiBase/content/$PageId`?expand=version" -Headers $Headers -UseBasicParsing
|
||||
$Version = ($VerResp.Content | ConvertFrom-Json).version.number + 1
|
||||
$Body = @{ version = @{ number = $Version }; title = "TEST Umlaut-Pruefung"; type = "page"; body = @{ storage = @{ value = $Converted; representation = "storage" } } } | ConvertTo-Json -Depth 10
|
||||
$Resp = Invoke-WebRequest -Uri "$ApiBase/content/$PageId" -Method Put -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($Body)) -UseBasicParsing
|
||||
Write-Host " Aktualisiert: $ConfluenceUrl/pages/viewpage.action?pageId=$PageId" -ForegroundColor Green
|
||||
} else {
|
||||
$Body = @{ type = "page"; title = "TEST Umlaut-Pruefung"; space = @{ key = $SpaceKey }; ancestors = @(@{ id = $ParentPageId }); body = @{ storage = @{ value = $Converted; representation = "storage" } } } | ConvertTo-Json -Depth 10
|
||||
$Resp = Invoke-WebRequest -Uri "$ApiBase/content" -Method Post -Headers $Headers -Body ([System.Text.Encoding]::UTF8.GetBytes($Body)) -UseBasicParsing
|
||||
$NewId = ($Resp.Content | ConvertFrom-Json).id
|
||||
Write-Host " Erstellt: $ConfluenceUrl/pages/viewpage.action?pageId=$NewId" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Pruefe die Seite in Confluence und sag mir ob die Umlaute stimmen!" -ForegroundColor Cyan
|
||||
@@ -0,0 +1,146 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Zieht pom.xml und package.json der Kern-Services fuer Versionsanalyse
|
||||
#>
|
||||
|
||||
$GitLabUrl = "https://git.tech.rz.db.de"
|
||||
$ApiBase = "$GitLabUrl/api/v4"
|
||||
$OutputDir = "project-audit\data\pom-analysis"
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
$Token = Read-Host "GitLab Token (read_api)"
|
||||
$Headers = @{ "PRIVATE-TOKEN" = $Token }
|
||||
|
||||
# Kern-Services (apps/ + libraries/ + ausgewaehlte apis/)
|
||||
$Projects = @(
|
||||
@{ path = "bestellsystem1/apps/steuerung-vertrieb"; name = "steuerung-vertrieb" }
|
||||
@{ path = "bestellsystem1/apps/auftrags-verwaltung-trasse"; name = "auftrags-verwaltung-trasse" }
|
||||
@{ path = "bestellsystem1/apps/portal-middleware"; name = "portal-middleware" }
|
||||
@{ path = "bestellsystem1/apps/portal-ui"; name = "portal-ui" }
|
||||
@{ path = "bestellsystem1/apps/common-interface"; name = "common-interface" }
|
||||
@{ path = "bestellsystem1/apps/stammdaten-bereitstellung"; name = "stammdaten-bereitstellung" }
|
||||
@{ path = "bestellsystem1/apps/kundendaten-bereitstellung"; name = "kundendaten-bereitstellung" }
|
||||
@{ path = "bestellsystem1/apps/ifp-connector"; name = "ifp-connector" }
|
||||
@{ path = "bestellsystem1/apps/taftap-tdm-konverter"; name = "taftap-tdm-konverter" }
|
||||
@{ path = "bestellsystem1/apps/vertragsdaten-verteiler"; name = "vertragsdaten-verteiler" }
|
||||
@{ path = "bestellsystem1/apps/archivierungsservice"; name = "archivierungsservice" }
|
||||
@{ path = "bestellsystem1/apps/rabattnummern-bereitstellung"; name = "rabattnummern-bereitstellung" }
|
||||
@{ path = "bestellsystem1/apps/tbv-absicherung-konverter"; name = "tbv-absicherung-konverter" }
|
||||
@{ path = "bestellsystem1/apps/tadef-connector"; name = "tadef-connector" }
|
||||
@{ path = "bestellsystem1/apps/auftrag-service"; name = "auftrag-service" }
|
||||
@{ path = "bestellsystem1/apps/bestellsystem-parent-pom"; name = "bestellsystem-parent-pom" }
|
||||
@{ path = "bestellsystem1/libraries/core-components-common"; name = "core-components-common" }
|
||||
@{ path = "bestellsystem1/libraries/core-components-kafka"; name = "core-components-kafka" }
|
||||
@{ path = "bestellsystem1/libraries/core-components-test"; name = "core-components-test" }
|
||||
@{ path = "bestellsystem1/libraries/logging-utils"; name = "logging-utils" }
|
||||
@{ path = "bestellsystem1/libraries/signature-database"; name = "signature-database" }
|
||||
@{ path = "bestellsystem1/libraries/signature-message"; name = "signature-message" }
|
||||
@{ path = "bestellsystem1/apis/data-model"; name = "data-model-api" }
|
||||
)
|
||||
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
|
||||
Write-Host "Lade pom.xml / package.json fuer $($Projects.Count) Projekte..."
|
||||
|
||||
foreach ($Proj in $Projects) {
|
||||
$Encoded = [Uri]::EscapeDataString($Proj.path)
|
||||
$Name = $Proj.name
|
||||
Write-Host " $Name..." -NoNewline
|
||||
|
||||
# pom.xml versuchen (main, dann master)
|
||||
$Found = $false
|
||||
foreach ($Branch in @("main", "master")) {
|
||||
foreach ($File in @("pom.xml", "package.json")) {
|
||||
$Uri = "$ApiBase/projects/$Encoded/repository/files/$([Uri]::EscapeDataString($File))/raw?ref=$Branch"
|
||||
try {
|
||||
$Resp = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$OutPath = Join-Path $OutputDir "$Name`_$File"
|
||||
$Resp.Content | Out-File -FilePath $OutPath -Encoding UTF8
|
||||
Write-Host " $File ($Branch)" -ForegroundColor Green
|
||||
$Found = $true
|
||||
break
|
||||
} catch { continue }
|
||||
}
|
||||
if ($Found) { break }
|
||||
}
|
||||
if (-not $Found) { Write-Host " nicht gefunden" -ForegroundColor Yellow }
|
||||
}
|
||||
|
||||
# Analyse: Spring Boot + Java Versionen extrahieren
|
||||
Write-Host ""
|
||||
Write-Host ">> Analysiere Versionen..." -ForegroundColor Yellow
|
||||
|
||||
$Results = @()
|
||||
$PomFiles = Get-ChildItem $OutputDir -Filter "*_pom.xml"
|
||||
foreach ($Pom in $PomFiles) {
|
||||
$Name = $Pom.Name -replace "_pom.xml", ""
|
||||
$Content = Get-Content $Pom.FullName -Raw
|
||||
|
||||
$SpringBoot = ""
|
||||
$Java = ""
|
||||
$Parent = ""
|
||||
$Camunda = ""
|
||||
|
||||
# Spring Boot Version
|
||||
if ($Content -match "spring-boot-starter-parent.*?<version>(.*?)</version>") { $SpringBoot = $Matches[1] }
|
||||
if ($Content -match "spring\.boot\.version.*?>(.*?)<") { $SpringBoot = $Matches[1] }
|
||||
if ($Content -match "spring-boot\.version.*?>(.*?)<") { $SpringBoot = $Matches[1] }
|
||||
|
||||
# Java Version
|
||||
if ($Content -match "<java\.version>(.*?)</java\.version>") { $Java = $Matches[1] }
|
||||
if ($Content -match "<maven\.compiler\.source>(.*?)</maven\.compiler\.source>") { $Java = $Matches[1] }
|
||||
|
||||
# Parent POM
|
||||
if ($Content -match "<parent>.*?<artifactId>(.*?)</artifactId>.*?<version>(.*?)</version>.*?</parent>") {
|
||||
$Parent = "$($Matches[1]):$($Matches[2])"
|
||||
}
|
||||
|
||||
# Camunda
|
||||
if ($Content -match "camunda.*?<version>(.*?)</version>") { $Camunda = $Matches[1] }
|
||||
|
||||
$Results += [PSCustomObject]@{
|
||||
Service = $Name
|
||||
SpringBoot = $SpringBoot
|
||||
Java = $Java
|
||||
Parent = $Parent
|
||||
Camunda = $Camunda
|
||||
}
|
||||
}
|
||||
|
||||
# Package.json analysieren
|
||||
$PkgFiles = Get-ChildItem $OutputDir -Filter "*_package.json"
|
||||
foreach ($Pkg in $PkgFiles) {
|
||||
$Name = $Pkg.Name -replace "_package.json", ""
|
||||
$Content = Get-Content $Pkg.FullName -Raw | ConvertFrom-Json
|
||||
|
||||
$Angular = ""
|
||||
if ($Content.dependencies -and $Content.dependencies.'@angular/core') {
|
||||
$Angular = $Content.dependencies.'@angular/core'
|
||||
}
|
||||
|
||||
$Results += [PSCustomObject]@{
|
||||
Service = $Name
|
||||
SpringBoot = ""
|
||||
Java = ""
|
||||
Parent = ""
|
||||
Camunda = ""
|
||||
Angular = $Angular
|
||||
NodeVersion = if ($Content.engines -and $Content.engines.node) { $Content.engines.node } else { "" }
|
||||
}
|
||||
}
|
||||
|
||||
# Ergebnis speichern
|
||||
$ResultPath = Join-Path $OutputDir "version-analysis.md"
|
||||
$Md = @("# Versionsanalyse pathOS Kern-Services", "", "| Service | Spring Boot | Java | Parent POM | Camunda |")
|
||||
$Md += "|---------|------------|------|------------|---------|"
|
||||
foreach ($R in $Results) {
|
||||
$Md += "| $($R.Service) | $($R.SpringBoot) | $($R.Java) | $($R.Parent) | $($R.Camunda) |"
|
||||
}
|
||||
$Md -join "`n" | Out-File -FilePath $ResultPath -Encoding UTF8
|
||||
|
||||
$Results | Export-Csv -Path (Join-Path $OutputDir "version-analysis.csv") -Delimiter ";" -Encoding UTF8 -NoTypeInformation
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "FERTIG! Ergebnisse in $OutputDir" -ForegroundColor Green
|
||||
$Results | Format-Table -AutoSize
|
||||
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Git-Init fuer project-audit (nutzt dulwich statt git binary)
|
||||
Initialisiert Repo, staged alle Dateien, erstellt Initial-Commit.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# dulwich imports
|
||||
from dulwich.repo import Repo
|
||||
from dulwich.objects import Blob, Tree, Commit
|
||||
from dulwich import porcelain
|
||||
|
||||
REPO_PATH = Path(__file__).parent.parent # project-audit/
|
||||
REMOTE_URL = "https://git.tech.rz.db.de/AndreKnie/project-audit.git"
|
||||
|
||||
|
||||
def main():
|
||||
print("=== Git Init: project-audit ===")
|
||||
print(f" Pfad: {REPO_PATH}")
|
||||
|
||||
# Repo initialisieren
|
||||
if (REPO_PATH / ".git").exists():
|
||||
print(" Repo existiert bereits.")
|
||||
repo = Repo(str(REPO_PATH))
|
||||
else:
|
||||
print(" Initialisiere neues Repo...")
|
||||
repo = Repo.init(str(REPO_PATH))
|
||||
print(" OK")
|
||||
|
||||
# .gitignore lesen fuer Ausschluesse
|
||||
ignore_patterns = []
|
||||
gitignore_path = REPO_PATH / ".gitignore"
|
||||
if gitignore_path.exists():
|
||||
with open(gitignore_path, "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#"):
|
||||
ignore_patterns.append(line)
|
||||
|
||||
def should_ignore(rel_path):
|
||||
"""Einfacher .gitignore Check."""
|
||||
rel_str = str(rel_path).replace("\\", "/")
|
||||
for pattern in ignore_patterns:
|
||||
pattern_clean = pattern.strip("/")
|
||||
if pattern_clean in rel_str:
|
||||
return True
|
||||
if rel_str.endswith(pattern_clean):
|
||||
return True
|
||||
return False
|
||||
|
||||
# Alle Dateien stagen
|
||||
print(" Stage Dateien...")
|
||||
staged = 0
|
||||
for root, dirs, files in os.walk(REPO_PATH):
|
||||
# .git Verzeichnis ueberspringen
|
||||
if ".git" in root:
|
||||
continue
|
||||
for fname in files:
|
||||
full_path = Path(root) / fname
|
||||
rel_path = full_path.relative_to(REPO_PATH)
|
||||
if should_ignore(rel_path):
|
||||
continue
|
||||
try:
|
||||
porcelain.add(str(REPO_PATH), paths=[str(rel_path)])
|
||||
staged += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print(f" {staged} Dateien gestaged")
|
||||
|
||||
# Commit
|
||||
print(" Erstelle Initial-Commit...")
|
||||
try:
|
||||
porcelain.commit(
|
||||
str(REPO_PATH),
|
||||
message=b"Initial: pathOS Portfolio-Audit (4 Tage Analyse)",
|
||||
author=b"Andre Knie <andre.knie@deutschebahn.com>",
|
||||
committer=b"Andre Knie <andre.knie@deutschebahn.com>"
|
||||
)
|
||||
print(" Commit erstellt!")
|
||||
except Exception as e:
|
||||
print(f" Commit-Fehler: {e}")
|
||||
|
||||
# Remote hinzufuegen
|
||||
print(f" Remote: {REMOTE_URL}")
|
||||
try:
|
||||
config = repo.get_config()
|
||||
config.set((b"remote", b"origin"), b"url", REMOTE_URL.encode())
|
||||
config.set((b"remote", b"origin"), b"fetch", b"+refs/heads/*:refs/remotes/origin/*")
|
||||
config.write_to_path()
|
||||
print(" Remote 'origin' konfiguriert")
|
||||
except Exception as e:
|
||||
print(f" Remote-Fehler: {e}")
|
||||
|
||||
print("\n FERTIG!")
|
||||
print(f" Push spaeter mit: python -m dulwich push {REMOTE_URL}")
|
||||
print(f" Oder wenn git installiert: git push -u origin main")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
Git Push via GitLab Commits API (REST).
|
||||
Umgeht das Git-HTTP-Protokoll komplett.
|
||||
Nutzt POST /api/v4/projects/:id/repository/commits
|
||||
|
||||
Strategie: Vergleiche lokale Dateien mit Remote, pushe nur Aenderungen.
|
||||
"""
|
||||
import urllib.request, json, ssl, base64, os
|
||||
from pathlib import Path
|
||||
|
||||
REPO_PATH = Path(__file__).parent.parent # project-audit/
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
TOKEN = secrets.get("GITLAB_TOKEN_AUDIT", "")
|
||||
PROJECT = "AndreKnie%2Fproject-audit"
|
||||
BASE = f"https://git.tech.rz.db.de/api/v4/projects/{PROJECT}"
|
||||
BRANCH = "master"
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
|
||||
def api_get(path):
|
||||
url = BASE + path
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header("PRIVATE-TOKEN", TOKEN)
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def api_post(path, data):
|
||||
url = BASE + path
|
||||
body = json.dumps(data).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=body, method="POST")
|
||||
req.add_header("PRIVATE-TOKEN", TOKEN)
|
||||
req.add_header("Content-Type", "application/json")
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=60)
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def get_remote_tree(path="", ref=BRANCH):
|
||||
"""Holt den Dateibaum vom Remote rekursiv."""
|
||||
files = {}
|
||||
page = 1
|
||||
while True:
|
||||
try:
|
||||
items = api_get(f"/repository/tree?ref={ref}&path={path}&per_page=100&page={page}&recursive=true")
|
||||
if not items:
|
||||
break
|
||||
for item in items:
|
||||
if item["type"] == "blob":
|
||||
files[item["path"]] = item["id"] # SHA des Blobs
|
||||
page += 1
|
||||
except Exception as e:
|
||||
print(f" Tree-Fehler (Seite {page}): {e}")
|
||||
break
|
||||
return files
|
||||
|
||||
|
||||
def get_local_files():
|
||||
"""Sammelt alle lokalen Dateien (respektiert .gitignore)."""
|
||||
patterns = []
|
||||
gitignore = REPO_PATH / ".gitignore"
|
||||
if gitignore.exists():
|
||||
with open(gitignore) as f:
|
||||
patterns = [l.strip().strip("/") for l in f if l.strip() and not l.startswith("#")]
|
||||
|
||||
files = {}
|
||||
for root, dirs, fnames in os.walk(REPO_PATH):
|
||||
if ".git" in root:
|
||||
continue
|
||||
for fname in fnames:
|
||||
full = Path(root) / fname
|
||||
rel = str(full.relative_to(REPO_PATH)).replace("\\", "/")
|
||||
if any(p in rel for p in patterns):
|
||||
continue
|
||||
files[rel] = full
|
||||
return files
|
||||
|
||||
|
||||
def main():
|
||||
print(f"=== Git Push via API ===")
|
||||
print(f" Token: ...{TOKEN[-4:]}")
|
||||
print(f" Branch: {BRANCH}")
|
||||
print()
|
||||
|
||||
# 1. Remote-Tree holen
|
||||
print(" Remote-Tree laden...")
|
||||
remote_files = get_remote_tree()
|
||||
print(f" Remote: {len(remote_files)} Dateien")
|
||||
|
||||
# 2. Lokale Dateien sammeln
|
||||
print(" Lokale Dateien sammeln...")
|
||||
local_files = get_local_files()
|
||||
print(f" Lokal: {len(local_files)} Dateien")
|
||||
|
||||
# 3. Diff berechnen
|
||||
actions = []
|
||||
new_files = set(local_files.keys()) - set(remote_files.keys())
|
||||
deleted_files = set(remote_files.keys()) - set(local_files.keys())
|
||||
|
||||
# Neue Dateien
|
||||
for f in sorted(new_files):
|
||||
path = local_files[f]
|
||||
try:
|
||||
content = path.read_bytes()
|
||||
actions.append({
|
||||
"action": "create",
|
||||
"file_path": f,
|
||||
"content": base64.b64encode(content).decode(),
|
||||
"encoding": "base64"
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Geaenderte Dateien (vereinfacht: alle existierenden neu hochladen)
|
||||
# Nur wenn es neue/geloeschte gibt, sonst nichts zu tun
|
||||
changed = []
|
||||
for f in sorted(set(local_files.keys()) & set(remote_files.keys())):
|
||||
path = local_files[f]
|
||||
try:
|
||||
content = path.read_bytes()
|
||||
# Einfacher Check: Dateigroesse oder Inhalt hat sich geaendert
|
||||
# Wir koennen den SHA nicht lokal berechnen ohne git, also updaten wir alles
|
||||
# was in den letzten Tagen geaendert wurde
|
||||
mtime = os.path.getmtime(path)
|
||||
import time
|
||||
if time.time() - mtime < 7 * 86400: # Letzte 7 Tage
|
||||
changed.append(f)
|
||||
actions.append({
|
||||
"action": "update",
|
||||
"file_path": f,
|
||||
"content": base64.b64encode(content).decode(),
|
||||
"encoding": "base64"
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Geloeschte Dateien
|
||||
for f in sorted(deleted_files):
|
||||
actions.append({
|
||||
"action": "delete",
|
||||
"file_path": f
|
||||
})
|
||||
|
||||
print(f" Neu: {len(new_files)}, Geaendert: {len(changed)}, Geloescht: {len(deleted_files)}")
|
||||
|
||||
if not actions:
|
||||
print(" Keine Aenderungen zu pushen.")
|
||||
return
|
||||
|
||||
# GitLab API hat ein Limit von ~100 Actions pro Commit
|
||||
# Aufteilen in Batches
|
||||
BATCH_SIZE = 50
|
||||
total_batches = (len(actions) + BATCH_SIZE - 1) // BATCH_SIZE
|
||||
print(f" Push in {total_batches} Batch(es) a {BATCH_SIZE} Dateien...")
|
||||
|
||||
for i in range(0, len(actions), BATCH_SIZE):
|
||||
batch = actions[i:i+BATCH_SIZE]
|
||||
batch_num = i // BATCH_SIZE + 1
|
||||
commit_msg = f"Audit Update (Batch {batch_num}/{total_batches})" if total_batches > 1 else "Audit Update"
|
||||
|
||||
print(f" Batch {batch_num}: {len(batch)} Aktionen...")
|
||||
try:
|
||||
result = api_post("/repository/commits", {
|
||||
"branch": BRANCH,
|
||||
"commit_message": commit_msg,
|
||||
"actions": batch
|
||||
})
|
||||
print(f" OK: {result.get('short_id', '?')} - {result.get('title', '?')}")
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode("utf-8", errors="replace")
|
||||
print(f" FEHLER {e.code}: {body[:300]}")
|
||||
if e.code == 400 and "already exists" in body:
|
||||
print(" Tipp: Datei existiert bereits, versuche 'update' statt 'create'")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f" FEHLER: {e}")
|
||||
break
|
||||
|
||||
print()
|
||||
print(" Fertig!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Nur Push (kein Stage/Commit) - testet ob oauth2-Auth den Push ermoeglicht."""
|
||||
from pathlib import Path
|
||||
from dulwich import porcelain
|
||||
|
||||
REPO_PATH = str(Path(__file__).parent.parent)
|
||||
REMOTE_URL = "https://git.tech.rz.db.de/AndreKnie/project-audit.git"
|
||||
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
token = secrets.get("GITLAB_TOKEN_AUDIT", "")
|
||||
print(f"Push mit oauth2-Auth (Token: ...{token[-4:]})")
|
||||
print(f"Repo: {REPO_PATH}")
|
||||
print(f"Remote: {REMOTE_URL}")
|
||||
print(f"Refspec: master -> master")
|
||||
print()
|
||||
|
||||
try:
|
||||
porcelain.push(
|
||||
REPO_PATH,
|
||||
REMOTE_URL,
|
||||
refspecs=b"refs/heads/master:refs/heads/master",
|
||||
username="oauth2",
|
||||
password=token
|
||||
)
|
||||
print("PUSH ERFOLGREICH!")
|
||||
except Exception as e:
|
||||
print(f"FEHLER: {e}")
|
||||
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
Git Push v2 - mit korrekter Auth (oauth2:token als Basic Auth).
|
||||
Behebt den 502-Fehler der durch falsche Auth-Methode verursacht wurde.
|
||||
|
||||
Usage:
|
||||
python project-audit/scripts/git-push-v2.py "Commit-Nachricht"
|
||||
"""
|
||||
import os, sys
|
||||
from pathlib import Path
|
||||
from dulwich import porcelain
|
||||
from dulwich.repo import Repo
|
||||
|
||||
REPO_PATH = Path(__file__).parent.parent # project-audit/
|
||||
SECRETS_PATH = REPO_PATH / ".secrets"
|
||||
REMOTE_URL = "https://git.tech.rz.db.de/AndreKnie/project-audit.git"
|
||||
|
||||
|
||||
def load_token():
|
||||
with open(SECRETS_PATH, "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith("GITLAB_TOKEN_AUDIT="):
|
||||
return line.split("=", 1)[1].strip()
|
||||
print("FEHLER: GITLAB_TOKEN_AUDIT nicht in .secrets!")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
message = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "Update pathOS Audit"
|
||||
token = load_token()
|
||||
|
||||
print(f"=== Git Push v2: {message} ===")
|
||||
print(f" Token: ...{token[-4:]}")
|
||||
print(f" Auth: oauth2 (Basic Auth)")
|
||||
|
||||
# Stage + Commit falls noetig
|
||||
from dulwich import porcelain as p
|
||||
st = p.status(str(REPO_PATH))
|
||||
needs_commit = st.unstaged or st.untracked
|
||||
|
||||
if needs_commit:
|
||||
print(f" Unstaged: {len(st.unstaged)}, Untracked: {len(st.untracked)}")
|
||||
# Stage all
|
||||
patterns = []
|
||||
gitignore = REPO_PATH / ".gitignore"
|
||||
if gitignore.exists():
|
||||
with open(gitignore) as f:
|
||||
patterns = [l.strip().strip("/") for l in f if l.strip() and not l.startswith("#")]
|
||||
|
||||
staged = 0
|
||||
for root, dirs, files in os.walk(REPO_PATH):
|
||||
if ".git" in root:
|
||||
continue
|
||||
for fname in files:
|
||||
full = Path(root) / fname
|
||||
rel = full.relative_to(REPO_PATH)
|
||||
rel_str = str(rel).replace("\\", "/")
|
||||
if any(p in rel_str for p in patterns):
|
||||
continue
|
||||
try:
|
||||
porcelain.add(str(REPO_PATH), paths=[str(rel)])
|
||||
staged += 1
|
||||
except Exception:
|
||||
pass
|
||||
print(f" {staged} Dateien gestaged")
|
||||
|
||||
try:
|
||||
porcelain.commit(
|
||||
str(REPO_PATH),
|
||||
message=message.encode("utf-8"),
|
||||
author=b"Andre Knie <andre.knie@deutschebahn.com>",
|
||||
committer=b"Andre Knie <andre.knie@deutschebahn.com>"
|
||||
)
|
||||
print(f" Commit erstellt: {message}")
|
||||
except Exception as e:
|
||||
if "nothing to commit" not in str(e).lower():
|
||||
print(f" Commit-Fehler: {e}")
|
||||
else:
|
||||
print(" Keine neuen Aenderungen, pushe bestehenden Commit.")
|
||||
|
||||
# Push mit oauth2 als Username (korrektes GitLab-Auth-Schema)
|
||||
print(" Push zu origin (master)...")
|
||||
try:
|
||||
porcelain.push(
|
||||
str(REPO_PATH),
|
||||
REMOTE_URL,
|
||||
refspecs=b"refs/heads/master:refs/heads/master",
|
||||
username="oauth2",
|
||||
password=token
|
||||
)
|
||||
print(" PUSH ERFOLGREICH!")
|
||||
except Exception as e:
|
||||
err = str(e)
|
||||
print(f" Push-Fehler: {err}")
|
||||
if "502" in err:
|
||||
print(" 502 = Server-Problem oder Auth-Reject durch Proxy")
|
||||
print(" Versuche Alternative: Push via GitLab API...")
|
||||
push_via_api(token)
|
||||
|
||||
|
||||
def push_via_api(token):
|
||||
"""Fallback: Datei-Upload via GitLab Repository Files API."""
|
||||
import urllib.request, json, ssl
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
base = "https://git.tech.rz.db.de/api/v4/projects/AndreKnie%2Fproject-audit"
|
||||
headers = {"PRIVATE-TOKEN": token, "Content-Type": "application/json"}
|
||||
|
||||
# Pruefe welche Dateien sich geaendert haben (vergleiche mit Remote)
|
||||
print(" API-Fallback: Pruefe Remote-Status...")
|
||||
url = f"{base}/repository/tree?per_page=100&ref=master"
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header("PRIVATE-TOKEN", token)
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=15)
|
||||
print(f" Remote-Tree erreichbar. Aber API-Push ist komplex.")
|
||||
print(" Empfehlung: git binary installieren oder spaeter erneut versuchen.")
|
||||
except Exception as e2:
|
||||
print(f" API-Fallback auch fehlgeschlagen: {e2}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
Git Push via dulwich (kein git binary noetig).
|
||||
Liest Token aus .secrets, committed Aenderungen und pusht.
|
||||
|
||||
Usage:
|
||||
python project-audit/scripts/git-push.py "Commit-Nachricht"
|
||||
python project-audit/scripts/git-push.py (nutzt Standard-Nachricht)
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from dulwich import porcelain
|
||||
from dulwich.repo import Repo
|
||||
|
||||
REPO_PATH = Path(__file__).parent.parent # project-audit/
|
||||
SECRETS_PATH = REPO_PATH / ".secrets"
|
||||
REMOTE_URL = "https://git.tech.rz.db.de/AndreKnie/project-audit.git"
|
||||
|
||||
|
||||
def load_token():
|
||||
"""Liest GITLAB_TOKEN_AUDIT aus .secrets Datei."""
|
||||
if not SECRETS_PATH.exists():
|
||||
print(f"FEHLER: {SECRETS_PATH} nicht gefunden!")
|
||||
print("Erstelle die Datei mit: GITLAB_TOKEN_AUDIT=dein_token")
|
||||
sys.exit(1)
|
||||
with open(SECRETS_PATH, "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith("GITLAB_TOKEN_AUDIT="):
|
||||
val = line.split("=", 1)[1].strip()
|
||||
if val and val != "HIER_TOKEN_EINTRAGEN":
|
||||
return val
|
||||
print("FEHLER: GITLAB_TOKEN_AUDIT nicht in .secrets gesetzt!")
|
||||
print("Trage deinen Token ein: GITLAB_TOKEN_AUDIT=glpat-xxxxx")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_ignore_patterns():
|
||||
"""Liest .gitignore Patterns."""
|
||||
patterns = []
|
||||
gitignore = REPO_PATH / ".gitignore"
|
||||
if gitignore.exists():
|
||||
with open(gitignore, "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#"):
|
||||
patterns.append(line.strip("/"))
|
||||
return patterns
|
||||
|
||||
|
||||
def should_ignore(rel_path, patterns):
|
||||
"""Prueft ob Datei ignoriert werden soll."""
|
||||
rel_str = str(rel_path).replace("\\", "/")
|
||||
for p in patterns:
|
||||
if p in rel_str:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
message = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "Update pathOS Audit"
|
||||
|
||||
print(f"=== Git Push: {message} ===")
|
||||
print(f" Repo: {REPO_PATH}")
|
||||
print(f" Remote: {REMOTE_URL}")
|
||||
|
||||
# Token laden
|
||||
token = load_token()
|
||||
print(f" Token: ...{token[-4:]}")
|
||||
|
||||
# Repo oeffnen
|
||||
if not (REPO_PATH / ".git").exists():
|
||||
print(" Kein Repo gefunden, initialisiere...")
|
||||
Repo.init(str(REPO_PATH))
|
||||
|
||||
# Alle Dateien stagen (neue + geaenderte)
|
||||
print(" Stage Aenderungen...")
|
||||
patterns = get_ignore_patterns()
|
||||
staged = 0
|
||||
for root, dirs, files in os.walk(REPO_PATH):
|
||||
if ".git" in root:
|
||||
continue
|
||||
for fname in files:
|
||||
full_path = Path(root) / fname
|
||||
rel_path = full_path.relative_to(REPO_PATH)
|
||||
if should_ignore(rel_path, patterns):
|
||||
continue
|
||||
try:
|
||||
porcelain.add(str(REPO_PATH), paths=[str(rel_path)])
|
||||
staged += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print(f" {staged} Dateien gestaged")
|
||||
|
||||
# Commit
|
||||
print(f" Commit: {message}")
|
||||
try:
|
||||
porcelain.commit(
|
||||
str(REPO_PATH),
|
||||
message=message.encode("utf-8"),
|
||||
author=b"Andre Knie <andre.knie@deutschebahn.com>",
|
||||
committer=b"Andre Knie <andre.knie@deutschebahn.com>"
|
||||
)
|
||||
except Exception as e:
|
||||
if "nothing to commit" in str(e).lower():
|
||||
print(" Keine Aenderungen zum Committen.")
|
||||
return
|
||||
print(f" Commit-Fehler: {e}")
|
||||
|
||||
# Push
|
||||
print(" Push zu origin...")
|
||||
try:
|
||||
porcelain.push(
|
||||
str(REPO_PATH),
|
||||
REMOTE_URL,
|
||||
refspecs=b"refs/heads/master:refs/heads/master",
|
||||
username="AndreKnie",
|
||||
password=token
|
||||
)
|
||||
print(" PUSH ERFOLGREICH!" )
|
||||
except Exception as e:
|
||||
print(f" Push-Fehler: {e}")
|
||||
print(" Tipp: Pruefen ob das Remote-Repo existiert und der Token Schreibrechte hat.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Test: Git Push Auth-Strategien."""
|
||||
import urllib.request, ssl, base64
|
||||
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
token = secrets.get("GITLAB_TOKEN_AUDIT", "")
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
base_url = "https://git.tech.rz.db.de/AndreKnie/project-audit.git"
|
||||
|
||||
# Test 1: Basic Auth mit oauth2:token (GitLab-Standard fuer PAT ueber HTTP)
|
||||
print("=== Test 1: Basic Auth (oauth2:token) ===")
|
||||
url1 = base_url + "/info/refs?service=git-receive-pack"
|
||||
creds = base64.b64encode(f"oauth2:{token}".encode()).decode()
|
||||
req1 = urllib.request.Request(url1)
|
||||
req1.add_header("Authorization", "Basic " + creds)
|
||||
try:
|
||||
resp1 = urllib.request.urlopen(req1, context=ctx, timeout=15)
|
||||
print("Status:", resp1.status)
|
||||
body = resp1.read(300)
|
||||
print("OK! Endpunkt erreichbar.")
|
||||
print("Body:", body[:200])
|
||||
except Exception as e:
|
||||
print("FEHLER:", e)
|
||||
|
||||
print()
|
||||
|
||||
# Test 2: Basic Auth mit username:token
|
||||
print("=== Test 2: Basic Auth (AndreKnie:token) ===")
|
||||
creds2 = base64.b64encode(f"AndreKnie:{token}".encode()).decode()
|
||||
req2 = urllib.request.Request(url1)
|
||||
req2.add_header("Authorization", "Basic " + creds2)
|
||||
try:
|
||||
resp2 = urllib.request.urlopen(req2, context=ctx, timeout=15)
|
||||
print("Status:", resp2.status)
|
||||
print("OK! Endpunkt erreichbar.")
|
||||
except Exception as e:
|
||||
print("FEHLER:", e)
|
||||
|
||||
print()
|
||||
|
||||
# Test 3: Token in URL (https://oauth2:token@host/...)
|
||||
print("=== Test 3: Token in URL ===")
|
||||
url3 = f"https://oauth2:{token}@git.tech.rz.db.de/AndreKnie/project-audit.git/info/refs?service=git-receive-pack"
|
||||
req3 = urllib.request.Request(url3)
|
||||
try:
|
||||
resp3 = urllib.request.urlopen(req3, context=ctx, timeout=15)
|
||||
print("Status:", resp3.status)
|
||||
print("OK! Endpunkt erreichbar.")
|
||||
except Exception as e:
|
||||
print("FEHLER:", e)
|
||||
|
||||
print()
|
||||
|
||||
# Test 4: upload-pack mit Basic Auth (Lese-Endpunkt zum Vergleich)
|
||||
print("=== Test 4: upload-pack mit Basic Auth (Vergleich) ===")
|
||||
url4 = base_url + "/info/refs?service=git-upload-pack"
|
||||
req4 = urllib.request.Request(url4)
|
||||
req4.add_header("Authorization", "Basic " + creds)
|
||||
try:
|
||||
resp4 = urllib.request.urlopen(req4, context=ctx, timeout=15)
|
||||
print("Status:", resp4.status)
|
||||
print("OK! Lese-Endpunkt erreichbar.")
|
||||
except Exception as e:
|
||||
print("FEHLER:", e)
|
||||
@@ -0,0 +1,182 @@
|
||||
"""
|
||||
GitLab Branch-Analyse: Aktive Branches der Kern-Services.
|
||||
Nutzt Projekt-IDs direkt (zuverlaessiger als Pfad-Encoding).
|
||||
"""
|
||||
import urllib.request, json, ssl
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
|
||||
GITLAB_URL = "https://git.tech.rz.db.de"
|
||||
API = f"{GITLAB_URL}/api/v4"
|
||||
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
TOKEN = secrets.get("GITLAB_TOKEN_BESTELLSYSTEM", "")
|
||||
if not TOKEN:
|
||||
print("FEHLER: GITLAB_TOKEN_BESTELLSYSTEM nicht in .secrets!")
|
||||
exit(1)
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
def api_get(path):
|
||||
url = f"{API}{path}"
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header("PRIVATE-TOKEN", TOKEN)
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
# Kern-Services mit IDs (aus gitlab-find-projects.py)
|
||||
CORE_PROJECTS = {
|
||||
"portal-middleware": 56664,
|
||||
"portal-ui": 51502,
|
||||
"steuerung-vertrieb": 54688,
|
||||
"auftrags-verwaltung-trasse": 63436,
|
||||
"common-interface": 52825,
|
||||
"stammdaten-bereitstellung": 150168,
|
||||
"tadef-connector": 352680,
|
||||
"archivierungsservice": 294401,
|
||||
"ifp-connector": 256070,
|
||||
"kundendaten-bereitstellung": 109956,
|
||||
"vertragsdaten-verteiler": 352409,
|
||||
"rabattnummern-bereitstellung": 216978,
|
||||
"taftap-tdm-konverter": 256243,
|
||||
}
|
||||
|
||||
print("=== GitLab Branch-Analyse ===")
|
||||
print(f"Datum: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
||||
print(f"Projekte: {len(CORE_PROJECTS)}")
|
||||
print()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
all_branches = []
|
||||
api_calls = 0
|
||||
|
||||
for proj_name, proj_id in CORE_PROJECTS.items():
|
||||
# Branches laden (paginiert)
|
||||
branches = []
|
||||
page = 1
|
||||
while True:
|
||||
data = api_get(f"/projects/{proj_id}/repository/branches?per_page=100&page={page}")
|
||||
api_calls += 1
|
||||
if not data:
|
||||
break
|
||||
branches.extend(data)
|
||||
if len(data) < 100:
|
||||
break
|
||||
page += 1
|
||||
|
||||
feature_branches = []
|
||||
for b in branches:
|
||||
name = b["name"]
|
||||
if name in ("master", "main", "develop"):
|
||||
continue
|
||||
|
||||
commit = b.get("commit", {})
|
||||
committed_date = commit.get("committed_date", "")
|
||||
author = commit.get("author_name", "?")
|
||||
|
||||
if committed_date:
|
||||
try:
|
||||
dt = datetime.fromisoformat(committed_date.replace("Z", "+00:00"))
|
||||
age_days = (now - dt).days
|
||||
except:
|
||||
age_days = -1
|
||||
else:
|
||||
age_days = -1
|
||||
|
||||
feature_branches.append({
|
||||
"project": proj_name,
|
||||
"branch": name,
|
||||
"author": author,
|
||||
"age_days": age_days,
|
||||
"date": committed_date[:10] if committed_date else "?",
|
||||
})
|
||||
|
||||
all_branches.extend(feature_branches)
|
||||
active = [b for b in feature_branches if 0 <= b["age_days"] <= 30]
|
||||
stale = [b for b in feature_branches if b["age_days"] > 30]
|
||||
print(f" {proj_name:<35} {len(feature_branches):>3} Branches ({len(active)} aktiv, {len(stale)} stale)")
|
||||
|
||||
print(f"\nAPI-Calls: {api_calls}")
|
||||
print(f"Branches gesamt: {len(all_branches)}")
|
||||
|
||||
# Aktive Branches (letzte 30 Tage)
|
||||
active_branches = [b for b in all_branches if 0 <= b["age_days"] <= 30]
|
||||
active_branches.sort(key=lambda x: x["age_days"])
|
||||
|
||||
print(f"\n--- Aktive Branches (letzte 30 Tage): {len(active_branches)} ---")
|
||||
print(f"{'Projekt':<30} {'Branch':<60} {'Autor':<25} {'Tage'}")
|
||||
print("-" * 125)
|
||||
for b in active_branches[:50]:
|
||||
branch_short = b["branch"][:59]
|
||||
print(f"{b['project']:<30} {branch_short:<60} {b['author']:<25} {b['age_days']}d")
|
||||
|
||||
# Stale Branches (>60 Tage)
|
||||
stale_branches = [b for b in all_branches if b["age_days"] > 60]
|
||||
stale_branches.sort(key=lambda x: x["age_days"], reverse=True)
|
||||
|
||||
print(f"\n--- Stale Branches (>60 Tage): {len(stale_branches)} ---")
|
||||
if stale_branches:
|
||||
print(f"{'Projekt':<30} {'Branch':<60} {'Tage'}")
|
||||
print("-" * 95)
|
||||
for b in stale_branches[:20]:
|
||||
branch_short = b["branch"][:59]
|
||||
print(f"{b['project']:<30} {branch_short:<60} {b['age_days']}d")
|
||||
|
||||
# Analyse: Wer arbeitet an was?
|
||||
print(f"\n--- Aktivitaet pro Autor (letzte 30 Tage) ---")
|
||||
authors = defaultdict(list)
|
||||
for b in active_branches:
|
||||
authors[b["author"]].append(b)
|
||||
|
||||
for author, branches in sorted(authors.items(), key=lambda x: -len(x[1]))[:20]:
|
||||
projects = set(b["project"] for b in branches)
|
||||
print(f" {author:<30} {len(branches):>2} Branches in: {', '.join(sorted(projects))}")
|
||||
|
||||
# Spring Boot 4 Branches
|
||||
sb4_branches = [b for b in all_branches if "spring" in b["branch"].lower() or "boot-4" in b["branch"].lower() or "springboot" in b["branch"].lower()]
|
||||
print(f"\n--- Spring Boot 4 Branches: {len(sb4_branches)} ---")
|
||||
for b in sorted(sb4_branches, key=lambda x: x["age_days"]):
|
||||
status = "AKTIV" if b["age_days"] <= 30 else f"STALE ({b['age_days']}d)"
|
||||
print(f" {b['project']:<30} {b['branch']:<55} {status}")
|
||||
|
||||
# Renovate Branches
|
||||
renovate_branches = [b for b in all_branches if "renovate" in b["branch"].lower()]
|
||||
renovate_active = [b for b in renovate_branches if b["age_days"] <= 7]
|
||||
renovate_stale = [b for b in renovate_branches if b["age_days"] > 30]
|
||||
print(f"\n--- Renovate Branches: {len(renovate_branches)} gesamt ---")
|
||||
print(f" Frisch (<=7d): {len(renovate_active)}")
|
||||
print(f" Stale (>30d): {len(renovate_stale)}")
|
||||
|
||||
# Ticket-Extraktion aus Branch-Namen
|
||||
print(f"\n--- Jira-Tickets in aktiven Branches ---")
|
||||
import re
|
||||
tickets = defaultdict(list)
|
||||
for b in active_branches:
|
||||
matches = re.findall(r'(O2C[A-Z]+-\d+|TTTI-\d+)', b["branch"].upper())
|
||||
for ticket in matches:
|
||||
tickets[ticket].append(b)
|
||||
|
||||
if tickets:
|
||||
for ticket, branches in sorted(tickets.items()):
|
||||
projs = set(b["project"] for b in branches)
|
||||
print(f" {ticket:<15} in {', '.join(sorted(projs))}")
|
||||
|
||||
# CSV Export
|
||||
csv_path = Path("project-audit/data/gitlab-branches.csv")
|
||||
with open(csv_path, "w", encoding="utf-8") as f:
|
||||
f.write('"Project";"Branch";"Author";"LastCommit";"AgeDays"\n')
|
||||
for b in sorted(all_branches, key=lambda x: (x["project"], x["age_days"])):
|
||||
f.write(f'"{b["project"]}";"{b["branch"]}";"{b["author"]}";"{b["date"]}";"{b["age_days"]}"\n')
|
||||
print(f"\nCSV exportiert: {csv_path}")
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Prueft ob Shared Libraries eigene Repos mit Tests haben."""
|
||||
import urllib.request, json, ssl
|
||||
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
TOKEN = secrets.get("GITLAB_TOKEN_BESTELLSYSTEM", "")
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
def api_get(url):
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header("PRIVATE-TOKEN", TOKEN)
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
# Alle Projekte in libraries/ Subgruppe
|
||||
print("=== Libraries-Subgruppe ===")
|
||||
url = "https://git.tech.rz.db.de/api/v4/groups/bestellsystem1/projects?per_page=100&include_subgroups=true"
|
||||
all_projects = api_get(url)
|
||||
libs = [p for p in all_projects if "/libraries/" in p["path_with_namespace"]]
|
||||
print(f"Projekte in libraries/: {len(libs)}")
|
||||
for p in libs:
|
||||
print(f" {p['path_with_namespace']:<60} ID={p['id']} Aktiv={p.get('last_activity_at','')[:10]}")
|
||||
|
||||
# Fuer jede Library: CI-Pipeline pruefen (hat sie .gitlab-ci.yml?)
|
||||
print()
|
||||
print("=== CI-Pipeline und Tests pruefen ===")
|
||||
for p in libs:
|
||||
pid = p["id"]
|
||||
name = p["path_with_namespace"].split("/")[-1]
|
||||
# Pruefen ob .gitlab-ci.yml existiert
|
||||
try:
|
||||
ci_url = f"https://git.tech.rz.db.de/api/v4/projects/{pid}/repository/files/.gitlab-ci.yml?ref=master"
|
||||
ci_data = api_get(ci_url)
|
||||
has_ci = True
|
||||
except:
|
||||
has_ci = False
|
||||
|
||||
# Pruefen ob src/test existiert
|
||||
try:
|
||||
test_url = f"https://git.tech.rz.db.de/api/v4/projects/{pid}/repository/tree?path=src/test&ref=master"
|
||||
test_data = api_get(test_url)
|
||||
has_tests = len(test_data) > 0 if test_data else False
|
||||
except:
|
||||
has_tests = False
|
||||
|
||||
ci_str = "CI" if has_ci else "kein CI"
|
||||
test_str = "Tests vorhanden" if has_tests else "KEINE Tests"
|
||||
print(f" {name:<40} {ci_str:<8} {test_str}")
|
||||
@@ -0,0 +1,268 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
GitLab Projekt-Inventar fuer pathOS / Bestellsystem
|
||||
.DESCRIPTION
|
||||
Zieht alle Projekte aus der GitLab-Gruppe und erstellt eine strukturierte Uebersicht.
|
||||
Kompatibel mit PowerShell 5.1 — keine zusaetzlichen Installationen noetig.
|
||||
#>
|
||||
|
||||
$GitLabUrl = "https://git.tech.rz.db.de"
|
||||
$GroupPath = "bestellsystem1"
|
||||
$ApiBase = "$GitLabUrl/api/v4"
|
||||
$PerPage = 100
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$OutputDir = Join-Path (Split-Path -Parent $ScriptDir) "data\gitlab-inventory"
|
||||
|
||||
# SSL-Handling (auskommentieren falls selbstsigniertes Zertifikat)
|
||||
# [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
|
||||
|
||||
# TLS 1.2 erzwingen
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
# Token
|
||||
$Token = Read-Host -Prompt "GitLab Personal Access Token (read_api)"
|
||||
|
||||
$Headers = @{ "PRIVATE-TOKEN" = $Token }
|
||||
|
||||
function Safe-String($val) {
|
||||
if ($null -eq $val) { return "" }
|
||||
return [string]$val
|
||||
}
|
||||
|
||||
function Invoke-GitLabApi {
|
||||
param([string]$Endpoint, [hashtable]$Params = @{})
|
||||
$Params["per_page"] = $PerPage
|
||||
$AllResults = @()
|
||||
$Page = 1
|
||||
do {
|
||||
$Params["page"] = $Page
|
||||
$QS = ($Params.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join "&"
|
||||
$Uri = "$ApiBase$Endpoint`?$QS"
|
||||
try {
|
||||
$Resp = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$Data = $Resp.Content | ConvertFrom-Json
|
||||
if ($null -eq $Data) { break }
|
||||
if ($Data -is [array]) {
|
||||
if ($Data.Count -eq 0) { break }
|
||||
$AllResults += $Data
|
||||
} else {
|
||||
return $Data
|
||||
}
|
||||
$TP = 1
|
||||
if ($Resp.Headers["X-Total-Pages"]) { $TP = [int]$Resp.Headers["X-Total-Pages"] }
|
||||
Write-Host " Seite $Page/$TP ($($Data.Count) Eintraege)" -ForegroundColor DarkGray
|
||||
$Page++
|
||||
} catch {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
throw
|
||||
}
|
||||
} while ($Page -le $TP)
|
||||
return $AllResults
|
||||
}
|
||||
|
||||
function Get-ProjectLanguages($ProjectId) {
|
||||
try {
|
||||
$Uri = "$ApiBase/projects/$ProjectId/languages"
|
||||
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
return ($R.Content | ConvertFrom-Json)
|
||||
} catch { return $null }
|
||||
}
|
||||
|
||||
function Get-ProjectReadme($ProjectId) {
|
||||
foreach ($Branch in @("main", "master", "develop")) {
|
||||
try {
|
||||
$Uri = "$ApiBase/projects/$ProjectId/repository/files/README.md/raw?ref=$Branch"
|
||||
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$C = $R.Content
|
||||
if ($C.Length -gt 3000) { $C = $C.Substring(0, 3000) + "`n`n... (gekuerzt)" }
|
||||
return $C
|
||||
} catch { continue }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-RepoTree($ProjectId) {
|
||||
foreach ($Branch in @("main", "master", "develop")) {
|
||||
try {
|
||||
$Uri = "$ApiBase/projects/$ProjectId/repository/tree?ref=$Branch&per_page=100"
|
||||
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
return ($R.Content | ConvertFrom-Json)
|
||||
} catch { continue }
|
||||
}
|
||||
return @()
|
||||
}
|
||||
|
||||
function Get-ProjectTags($Project, $Languages, $Tree) {
|
||||
$Tags = @()
|
||||
$Name = (Safe-String $Project.name).ToLower()
|
||||
$Desc = (Safe-String $Project.description).ToLower()
|
||||
$Files = @($Tree | ForEach-Object { $_.name.ToLower() })
|
||||
|
||||
if ($Languages) {
|
||||
$LangProps = $Languages | Get-Member -MemberType NoteProperty
|
||||
foreach ($lp in $LangProps) {
|
||||
$lang = $lp.Name.ToLower()
|
||||
if ($lang -eq "java") { $Tags += "java" }
|
||||
if ($lang -eq "kotlin") { $Tags += "kotlin" }
|
||||
if ($lang -match "typescript|javascript") { $Tags += "frontend" }
|
||||
if ($lang -eq "python") { $Tags += "python" }
|
||||
}
|
||||
}
|
||||
|
||||
if ("pom.xml" -in $Files) { $Tags += "maven" }
|
||||
if ("build.gradle" -in $Files -or "build.gradle.kts" -in $Files) { $Tags += "gradle" }
|
||||
if ("package.json" -in $Files) { $Tags += "npm" }
|
||||
if ("Dockerfile" -in $Files -or "dockerfile" -in $Files) { $Tags += "docker" }
|
||||
|
||||
if ($Name -match "lib|library|common|shared|util" -or $Desc -match "lib|library|common|shared|util") { $Tags += "library" }
|
||||
if ($Name -match "service|api|backend" -or $Desc -match "service|api|backend") { $Tags += "service" }
|
||||
if ($Name -match "ui|frontend|web|app" -or $Desc -match "ui|frontend|web|app") { $Tags += "frontend-app" }
|
||||
if ($Name -match "test|e2e|integration" -or $Desc -match "test|e2e|integration") { $Tags += "testing" }
|
||||
if ($Name -match "config|infra|deploy|pipeline|ci" -or $Desc -match "config|infra|deploy|pipeline|ci") { $Tags += "infrastructure" }
|
||||
|
||||
if ($Project.last_activity_at) {
|
||||
try {
|
||||
$Last = [DateTime]::Parse($Project.last_activity_at)
|
||||
$Days = ((Get-Date) - $Last).Days
|
||||
if ($Days -gt 365) { $Tags += "inactive-1y+" }
|
||||
elseif ($Days -gt 180) { $Tags += "inactive-6m+" }
|
||||
} catch {}
|
||||
}
|
||||
if ($Project.archived) { $Tags += "archived" }
|
||||
|
||||
return ($Tags | Select-Object -Unique)
|
||||
}
|
||||
|
||||
# === MAIN ===
|
||||
Write-Host "============================================================" -ForegroundColor Cyan
|
||||
Write-Host " pathOS / Bestellsystem - GitLab Projekt-Inventar" -ForegroundColor Cyan
|
||||
Write-Host "============================================================" -ForegroundColor Cyan
|
||||
Write-Host " GitLab: $GitLabUrl"
|
||||
Write-Host " Gruppe: $GroupPath"
|
||||
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path "$OutputDir\readmes" -Force | Out-Null
|
||||
|
||||
try {
|
||||
Write-Host "`n>> Lade Gruppeninfo..." -ForegroundColor Yellow
|
||||
$EncodedGroup = [Uri]::EscapeDataString($GroupPath)
|
||||
$GroupInfo = Invoke-GitLabApi -Endpoint "/groups/$EncodedGroup"
|
||||
Write-Host " Gruppe: $($GroupInfo.name) (ID: $($GroupInfo.id))" -ForegroundColor Green
|
||||
|
||||
Write-Host "`n>> Lade Untergruppen..." -ForegroundColor Yellow
|
||||
$Subgroups = @(Invoke-GitLabApi -Endpoint "/groups/$EncodedGroup/subgroups" -Params @{ all_available = "true" })
|
||||
Write-Host " $($Subgroups.Count) Untergruppen" -ForegroundColor Green
|
||||
|
||||
Write-Host "`n>> Lade alle Projekte..." -ForegroundColor Yellow
|
||||
$Projects = @(Invoke-GitLabApi -Endpoint "/groups/$EncodedGroup/projects" -Params @{
|
||||
include_subgroups = "true"; with_shared = "false"; order_by = "name"; sort = "asc"
|
||||
})
|
||||
Write-Host " $($Projects.Count) Projekte" -ForegroundColor Green
|
||||
|
||||
Write-Host "`n>> Reichere Projekte an..." -ForegroundColor Yellow
|
||||
$Enriched = @()
|
||||
$Total = $Projects.Count
|
||||
$i = 0
|
||||
foreach ($P in $Projects) {
|
||||
$i++
|
||||
Write-Host " [$i/$Total] $($P.name)..." -NoNewline
|
||||
$Langs = Get-ProjectLanguages $P.id
|
||||
$Readme = Get-ProjectReadme $P.id
|
||||
$Tree = @(Get-RepoTree $P.id)
|
||||
$Tags = @(Get-ProjectTags $P $Langs $Tree)
|
||||
|
||||
$Obj = [PSCustomObject]@{
|
||||
id = $P.id
|
||||
name = $P.name
|
||||
path = $P.path_with_namespace
|
||||
description = Safe-String $P.description
|
||||
branch = $P.default_branch
|
||||
archived = $P.archived
|
||||
created = $P.created_at
|
||||
last_active = $P.last_activity_at
|
||||
namespace = $P.namespace.full_path
|
||||
web_url = $P.web_url
|
||||
languages = $Langs
|
||||
tags = $Tags
|
||||
files = @($Tree | ForEach-Object { $_.name })
|
||||
readme = $Readme
|
||||
}
|
||||
$Enriched += $Obj
|
||||
|
||||
if ($Readme) {
|
||||
$SafeName = $P.path_with_namespace -replace "/", "__"
|
||||
"# $($P.name)`n`n> Quelle: $($P.web_url)`n`n$Readme" | Out-File "$OutputDir\readmes\$SafeName.md" -Encoding UTF8
|
||||
}
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# JSON
|
||||
$Enriched | ConvertTo-Json -Depth 10 | Out-File "$OutputDir\projects-full.json" -Encoding UTF8
|
||||
Write-Host "`n JSON gespeichert" -ForegroundColor DarkGray
|
||||
|
||||
# CSV
|
||||
$CsvRows = $Enriched | ForEach-Object {
|
||||
$LS = ""
|
||||
if ($_.languages) {
|
||||
$LP = $_.languages | Get-Member -MemberType NoteProperty
|
||||
$LS = ($LP | ForEach-Object { $_.Name }) -join ", "
|
||||
}
|
||||
[PSCustomObject]@{
|
||||
Name=$_.name; Pfad=$_.path; Beschreibung=$_.description;
|
||||
Sprachen=$LS; Tags=($_.tags -join ", "); LetzteAktivitaet=$_.last_active;
|
||||
Archiviert=$_.archived; Branch=$_.branch; Namespace=$_.namespace
|
||||
}
|
||||
}
|
||||
$CsvRows | Export-Csv "$OutputDir\projects-overview.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
|
||||
Write-Host " CSV gespeichert" -ForegroundColor DarkGray
|
||||
|
||||
# Markdown
|
||||
$Md = @("# GitLab Projektuebersicht - $(Safe-String $GroupInfo.name)", "",
|
||||
"> Generiert: $(Get-Date -Format 'yyyy-MM-dd HH:mm')",
|
||||
"> Projekte: $($Enriched.Count)", "")
|
||||
if ($Subgroups.Count -gt 0) {
|
||||
$Md += "## Untergruppen"; $Md += ""
|
||||
foreach ($sg in $Subgroups) { $Md += "- **$($sg.name)** (``$($sg.full_path)``)" }
|
||||
$Md += ""
|
||||
}
|
||||
$ByNs = $Enriched | Group-Object -Property namespace
|
||||
foreach ($G in ($ByNs | Sort-Object Name)) {
|
||||
$Md += "## $($G.Name)"; $Md += ""
|
||||
$Md += "| Projekt | Beschreibung | Sprachen | Tags | Aktiv |"
|
||||
$Md += "|---------|-------------|----------|------|-------|"
|
||||
foreach ($p in $G.Group) {
|
||||
$LS = ""; if ($p.languages) { $LP = $p.languages | Get-Member -MemberType NoteProperty; $LS = ($LP | ForEach-Object { $_.Name }) -join ", " }
|
||||
$TS = ($p.tags -join ", ")
|
||||
$DS = if ($p.description.Length -gt 60) { $p.description.Substring(0,60) + "..." } else { $p.description }
|
||||
if (-not $DS) { $DS = "-" }
|
||||
$LA = if ($p.last_active) { $p.last_active.Substring(0,10) } else { "-" }
|
||||
$Md += "| $($p.name) | $DS | $LS | $TS | $LA |"
|
||||
}
|
||||
$Md += ""
|
||||
}
|
||||
$Md -join "`n" | Out-File "$OutputDir\projects-overview.md" -Encoding UTF8
|
||||
Write-Host " Markdown gespeichert" -ForegroundColor DarkGray
|
||||
|
||||
# Statistiken
|
||||
$St = @("# Projektstatistiken", "", "- Projekte: $($Enriched.Count)", "- Untergruppen: $($Subgroups.Count)",
|
||||
"- Archiviert: $(($Enriched | Where-Object { $_.archived }).Count)", "", "## Sprachen")
|
||||
$LC = @{}; foreach ($p in $Enriched) { if ($p.languages) { $p.languages | Get-Member -MemberType NoteProperty | ForEach-Object { $LC[$_.Name] = ($LC[$_.Name]) + 1 } } }
|
||||
foreach ($e in ($LC.GetEnumerator() | Sort-Object Value -Descending)) { $St += "- $($e.Key): $($e.Value)" }
|
||||
$St += ""; $St += "## Tags"
|
||||
$TC = @{}; foreach ($p in $Enriched) { foreach ($t in $p.tags) { $TC[$t] = ($TC[$t]) + 1 } }
|
||||
foreach ($e in ($TC.GetEnumerator() | Sort-Object Value -Descending)) { $St += "- $($e.Key): $($e.Value)" }
|
||||
$St -join "`n" | Out-File "$OutputDir\statistics.md" -Encoding UTF8
|
||||
Write-Host " Statistiken gespeichert" -ForegroundColor DarkGray
|
||||
|
||||
Write-Host "`n============================================================" -ForegroundColor Green
|
||||
Write-Host " FERTIG! $($Enriched.Count) Projekte inventarisiert." -ForegroundColor Green
|
||||
Write-Host " Ergebnisse: $OutputDir" -ForegroundColor Green
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
|
||||
} catch {
|
||||
Write-Host "`nFEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
if ($_.Exception.Message -match "401") { Write-Host " Token pruefen (read_api Scope)." -ForegroundColor Yellow }
|
||||
if ($_.Exception.Message -match "404") { Write-Host " Gruppe '$GroupPath' nicht gefunden." -ForegroundColor Yellow }
|
||||
if ($_.Exception.Message -match "resolve|Remotename") { Write-Host " VPN aktiv?" -ForegroundColor Yellow }
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GitLab Project Inventory Script für pathOS / Bestellsystem
|
||||
===========================================================
|
||||
Zieht alle Projekte aus der GitLab-Gruppe und erstellt eine strukturierte Übersicht.
|
||||
|
||||
Voraussetzungen:
|
||||
- Python 3.8+
|
||||
- pip install requests
|
||||
|
||||
Verwendung:
|
||||
python gitlab-inventory.py
|
||||
|
||||
Du wirst nach deinem Personal Access Token gefragt (read_api Scope reicht).
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import csv
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from getpass import getpass
|
||||
from pathlib import Path
|
||||
|
||||
# === Konfiguration ===
|
||||
GITLAB_URL = "https://git.tech.rz.db.de"
|
||||
GROUP_PATH = "bestellsystem1"
|
||||
API_BASE = f"{GITLAB_URL}/api/v4"
|
||||
OUTPUT_DIR = Path(__file__).parent.parent / "data" / "gitlab-inventory"
|
||||
PER_PAGE = 100
|
||||
|
||||
|
||||
def get_token():
|
||||
"""Token aus Umgebungsvariable oder interaktiver Eingabe."""
|
||||
token = os.environ.get("GITLAB_TOKEN")
|
||||
if not token:
|
||||
token = getpass("GitLab Personal Access Token (read_api): ")
|
||||
return token
|
||||
|
||||
|
||||
def api_get(session, endpoint, params=None):
|
||||
"""Paginierte API-Abfrage mit automatischem Durchblättern."""
|
||||
if params is None:
|
||||
params = {}
|
||||
params["per_page"] = PER_PAGE
|
||||
|
||||
all_results = []
|
||||
page = 1
|
||||
|
||||
while True:
|
||||
params["page"] = page
|
||||
resp = session.get(f"{API_BASE}{endpoint}", params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
if not data:
|
||||
break
|
||||
|
||||
all_results.extend(data)
|
||||
total_pages = int(resp.headers.get("X-Total-Pages", 1))
|
||||
|
||||
print(f" Seite {page}/{total_pages} geladen ({len(data)} Einträge)")
|
||||
|
||||
if page >= total_pages:
|
||||
break
|
||||
page += 1
|
||||
|
||||
return all_results
|
||||
|
||||
|
||||
def fetch_group_info(session):
|
||||
"""Gruppeninformationen abrufen."""
|
||||
print(f"\n📁 Lade Gruppeninfo für '{GROUP_PATH}'...")
|
||||
resp = session.get(f"{API_BASE}/groups/{requests.utils.quote(GROUP_PATH, safe='')}")
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def fetch_all_projects(session):
|
||||
"""Alle Projekte der Gruppe inkl. Untergruppen."""
|
||||
print("\n📦 Lade alle Projekte (inkl. Untergruppen)...")
|
||||
projects = api_get(
|
||||
session,
|
||||
f"/groups/{requests.utils.quote(GROUP_PATH, safe='')}/projects",
|
||||
params={
|
||||
"include_subgroups": "true",
|
||||
"with_shared": "false",
|
||||
"order_by": "name",
|
||||
"sort": "asc",
|
||||
},
|
||||
)
|
||||
print(f" → {len(projects)} Projekte gefunden")
|
||||
return projects
|
||||
|
||||
|
||||
def fetch_subgroups(session):
|
||||
"""Alle Untergruppen der Hauptgruppe."""
|
||||
print("\n📂 Lade Untergruppen...")
|
||||
subgroups = api_get(
|
||||
session,
|
||||
f"/groups/{requests.utils.quote(GROUP_PATH, safe='')}/subgroups",
|
||||
params={"all_available": "true"},
|
||||
)
|
||||
print(f" → {len(subgroups)} Untergruppen gefunden")
|
||||
return subgroups
|
||||
|
||||
|
||||
def fetch_project_languages(session, project_id):
|
||||
"""Programmiersprachen eines Projekts."""
|
||||
try:
|
||||
resp = session.get(f"{API_BASE}/projects/{project_id}/languages")
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def fetch_project_readme(session, project_id):
|
||||
"""README.md eines Projekts (falls vorhanden)."""
|
||||
try:
|
||||
resp = session.get(
|
||||
f"{API_BASE}/projects/{project_id}/repository/files/README.md/raw",
|
||||
params={"ref": "main"},
|
||||
)
|
||||
if resp.status_code == 404:
|
||||
resp = session.get(
|
||||
f"{API_BASE}/projects/{project_id}/repository/files/README.md/raw",
|
||||
params={"ref": "master"},
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
return resp.text[:3000] # Erste 3000 Zeichen
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def fetch_repo_tree(session, project_id, path="", ref="main"):
|
||||
"""Top-Level Verzeichnisstruktur eines Repos."""
|
||||
try:
|
||||
resp = session.get(
|
||||
f"{API_BASE}/projects/{project_id}/repository/tree",
|
||||
params={"ref": ref, "path": path, "per_page": 100},
|
||||
)
|
||||
if resp.status_code == 404 and ref == "main":
|
||||
return fetch_repo_tree(session, project_id, path, "master")
|
||||
if resp.status_code == 200:
|
||||
return resp.json()
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
|
||||
def enrich_project(session, project, index, total):
|
||||
"""Projekt mit zusätzlichen Details anreichern."""
|
||||
pid = project["id"]
|
||||
name = project["name"]
|
||||
print(f" [{index}/{total}] {name}...", end=" ", flush=True)
|
||||
|
||||
# Sprachen
|
||||
languages = fetch_project_languages(session, pid)
|
||||
project["_languages"] = languages
|
||||
|
||||
# README (erste 3000 Zeichen)
|
||||
readme = fetch_project_readme(session, pid)
|
||||
project["_readme_excerpt"] = readme
|
||||
|
||||
# Top-Level Dateistruktur
|
||||
tree = fetch_repo_tree(session, pid)
|
||||
project["_top_level_files"] = [
|
||||
{"name": f["name"], "type": f["type"]} for f in tree
|
||||
]
|
||||
|
||||
print("✓")
|
||||
return project
|
||||
|
||||
|
||||
def classify_project(project):
|
||||
"""Projekt grob klassifizieren basierend auf verfügbaren Daten."""
|
||||
name = project.get("name", "").lower()
|
||||
desc = (project.get("description") or "").lower()
|
||||
topics = [t.lower() for t in project.get("topics", [])]
|
||||
files = [f["name"].lower() for f in project.get("_top_level_files", [])]
|
||||
languages = project.get("_languages", {})
|
||||
|
||||
tags = []
|
||||
|
||||
# Sprach-Tags
|
||||
if "Java" in languages:
|
||||
tags.append("java")
|
||||
if "Kotlin" in languages:
|
||||
tags.append("kotlin")
|
||||
if "TypeScript" in languages or "JavaScript" in languages:
|
||||
tags.append("frontend")
|
||||
if "Python" in languages:
|
||||
tags.append("python")
|
||||
|
||||
# Build-System
|
||||
if "pom.xml" in files:
|
||||
tags.append("maven")
|
||||
if "build.gradle" in files or "build.gradle.kts" in files:
|
||||
tags.append("gradle")
|
||||
if "package.json" in files:
|
||||
tags.append("npm")
|
||||
if "Dockerfile" in files or "dockerfile" in files:
|
||||
tags.append("docker")
|
||||
if any("helm" in f for f in files) or "Chart.yaml" in files:
|
||||
tags.append("helm")
|
||||
|
||||
# Typ-Erkennung
|
||||
if any(kw in name or kw in desc for kw in ["lib", "library", "common", "shared", "util"]):
|
||||
tags.append("library")
|
||||
if any(kw in name or kw in desc for kw in ["service", "api", "backend"]):
|
||||
tags.append("service")
|
||||
if any(kw in name or kw in desc for kw in ["ui", "frontend", "web", "app"]):
|
||||
tags.append("frontend-app")
|
||||
if any(kw in name or kw in desc for kw in ["test", "e2e", "integration"]):
|
||||
tags.append("testing")
|
||||
if any(kw in name or kw in desc for kw in ["config", "infra", "deploy", "pipeline", "ci"]):
|
||||
tags.append("infrastructure")
|
||||
if any(kw in name or kw in desc for kw in ["doc", "wiki", "readme"]):
|
||||
tags.append("documentation")
|
||||
|
||||
# Aktivitätsstatus
|
||||
last_activity = project.get("last_activity_at", "")
|
||||
if last_activity:
|
||||
try:
|
||||
last = datetime.fromisoformat(last_activity.replace("Z", "+00:00"))
|
||||
days_ago = (datetime.now(last.tzinfo) - last).days
|
||||
if days_ago > 365:
|
||||
tags.append("inactive-1y+")
|
||||
elif days_ago > 180:
|
||||
tags.append("inactive-6m+")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if project.get("archived"):
|
||||
tags.append("archived")
|
||||
|
||||
return tags
|
||||
|
||||
|
||||
def save_outputs(projects, subgroups, group_info):
|
||||
"""Alle Ergebnisse speichern."""
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 1. Vollständige JSON-Daten
|
||||
json_path = OUTPUT_DIR / "projects-full.json"
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
json.dump(projects, f, indent=2, ensure_ascii=False, default=str)
|
||||
print(f"\n💾 Vollständige Daten: {json_path}")
|
||||
|
||||
# 2. Übersichts-CSV
|
||||
csv_path = OUTPUT_DIR / "projects-overview.csv"
|
||||
with open(csv_path, "w", encoding="utf-8", newline="") as f:
|
||||
writer = csv.writer(f, delimiter=";")
|
||||
writer.writerow([
|
||||
"Name", "Pfad", "Beschreibung", "Sprachen", "Tags",
|
||||
"Letzte Aktivität", "Erstellt", "Archiviert",
|
||||
"Default Branch", "Namespace"
|
||||
])
|
||||
for p in projects:
|
||||
langs = ", ".join(f"{k} ({v:.0f}%)" for k, v in p.get("_languages", {}).items())
|
||||
tags = ", ".join(p.get("_tags", []))
|
||||
writer.writerow([
|
||||
p.get("name", ""),
|
||||
p.get("path_with_namespace", ""),
|
||||
(p.get("description") or "")[:200],
|
||||
langs,
|
||||
tags,
|
||||
p.get("last_activity_at", ""),
|
||||
p.get("created_at", ""),
|
||||
p.get("archived", False),
|
||||
p.get("default_branch", ""),
|
||||
p.get("namespace", {}).get("full_path", ""),
|
||||
])
|
||||
print(f"📊 Übersichts-CSV: {csv_path}")
|
||||
|
||||
# 3. Markdown-Übersicht
|
||||
md_path = OUTPUT_DIR / "projects-overview.md"
|
||||
with open(md_path, "w", encoding="utf-8") as f:
|
||||
f.write(f"# GitLab Projektübersicht — {group_info.get('name', GROUP_PATH)}\n\n")
|
||||
f.write(f"> Generiert am {datetime.now().strftime('%Y-%m-%d %H:%M')}\n")
|
||||
f.write(f"> Quelle: {GITLAB_URL}/{GROUP_PATH}\n")
|
||||
f.write(f"> Anzahl Projekte: {len(projects)}\n\n")
|
||||
|
||||
# Untergruppen
|
||||
if subgroups:
|
||||
f.write("## Untergruppen\n\n")
|
||||
for sg in subgroups:
|
||||
f.write(f"- **{sg['name']}** (`{sg['full_path']}`) — {sg.get('description', 'Keine Beschreibung')}\n")
|
||||
f.write("\n")
|
||||
|
||||
# Projekte nach Namespace gruppiert
|
||||
by_namespace = {}
|
||||
for p in projects:
|
||||
ns = p.get("namespace", {}).get("full_path", "root")
|
||||
by_namespace.setdefault(ns, []).append(p)
|
||||
|
||||
for ns in sorted(by_namespace.keys()):
|
||||
f.write(f"## {ns}\n\n")
|
||||
f.write("| Projekt | Beschreibung | Sprachen | Tags | Letzte Aktivität |\n")
|
||||
f.write("|---------|-------------|----------|------|------------------|\n")
|
||||
for p in by_namespace[ns]:
|
||||
langs = ", ".join(p.get("_languages", {}).keys())
|
||||
tags = ", ".join(p.get("_tags", []))
|
||||
desc = (p.get("description") or "—")[:80]
|
||||
last = (p.get("last_activity_at") or "—")[:10]
|
||||
f.write(f"| {p['name']} | {desc} | {langs} | {tags} | {last} |\n")
|
||||
f.write("\n")
|
||||
|
||||
print(f"📝 Markdown-Übersicht: {md_path}")
|
||||
|
||||
# 4. README-Sammlung
|
||||
readme_dir = OUTPUT_DIR / "readmes"
|
||||
readme_dir.mkdir(exist_ok=True)
|
||||
count = 0
|
||||
for p in projects:
|
||||
if p.get("_readme_excerpt"):
|
||||
safe_name = p["path_with_namespace"].replace("/", "__")
|
||||
with open(readme_dir / f"{safe_name}.md", "w", encoding="utf-8") as f:
|
||||
f.write(f"# {p['name']}\n\n")
|
||||
f.write(f"> Quelle: {GITLAB_URL}/{p['path_with_namespace']}\n\n")
|
||||
f.write(p["_readme_excerpt"])
|
||||
count += 1
|
||||
print(f"📖 READMEs gespeichert: {count} Dateien in {readme_dir}")
|
||||
|
||||
# 5. Statistik-Zusammenfassung
|
||||
stats_path = OUTPUT_DIR / "statistics.md"
|
||||
with open(stats_path, "w", encoding="utf-8") as f:
|
||||
f.write("# Projektstatistiken\n\n")
|
||||
f.write(f"- **Gesamtanzahl Projekte**: {len(projects)}\n")
|
||||
f.write(f"- **Untergruppen**: {len(subgroups)}\n")
|
||||
f.write(f"- **Archiviert**: {sum(1 for p in projects if p.get('archived'))}\n")
|
||||
|
||||
# Sprachen-Verteilung
|
||||
lang_count = {}
|
||||
for p in projects:
|
||||
for lang in p.get("_languages", {}):
|
||||
lang_count[lang] = lang_count.get(lang, 0) + 1
|
||||
f.write("\n## Sprachen-Verteilung\n\n")
|
||||
for lang, count in sorted(lang_count.items(), key=lambda x: -x[1]):
|
||||
f.write(f"- {lang}: {count} Projekte\n")
|
||||
|
||||
# Tag-Verteilung
|
||||
tag_count = {}
|
||||
for p in projects:
|
||||
for tag in p.get("_tags", []):
|
||||
tag_count[tag] = tag_count.get(tag, 0) + 1
|
||||
f.write("\n## Tag-Verteilung\n\n")
|
||||
for tag, count in sorted(tag_count.items(), key=lambda x: -x[1]):
|
||||
f.write(f"- {tag}: {count} Projekte\n")
|
||||
|
||||
print(f"📈 Statistiken: {stats_path}")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print(" pathOS / Bestellsystem — GitLab Projekt-Inventar")
|
||||
print("=" * 60)
|
||||
print(f"\n GitLab: {GITLAB_URL}")
|
||||
print(f" Gruppe: {GROUP_PATH}")
|
||||
|
||||
token = get_token()
|
||||
|
||||
session = requests.Session()
|
||||
session.headers["PRIVATE-TOKEN"] = token
|
||||
session.verify = True # Auf False setzen falls selbstsigniertes Zertifikat
|
||||
|
||||
try:
|
||||
# Gruppeninfo
|
||||
group_info = fetch_group_info(session)
|
||||
print(f" ✓ Gruppe gefunden: {group_info.get('name')} (ID: {group_info.get('id')})")
|
||||
|
||||
# Untergruppen
|
||||
subgroups = fetch_subgroups(session)
|
||||
|
||||
# Alle Projekte
|
||||
projects = fetch_all_projects(session)
|
||||
|
||||
# Projekte anreichern
|
||||
print(f"\n🔍 Reichere {len(projects)} Projekte mit Details an...")
|
||||
for i, project in enumerate(projects, 1):
|
||||
enrich_project(session, project, i, len(projects))
|
||||
project["_tags"] = classify_project(project)
|
||||
|
||||
# Ergebnisse speichern
|
||||
save_outputs(projects, subgroups, group_info)
|
||||
|
||||
print("\n✅ Fertig! Alle Daten wurden gespeichert.")
|
||||
print(f" Nächster Schritt: Ergebnisse in project-audit/data/gitlab-inventory/ prüfen")
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if e.response.status_code == 401:
|
||||
print("\n❌ Authentifizierung fehlgeschlagen. Bitte Token prüfen (read_api Scope nötig).")
|
||||
elif e.response.status_code == 404:
|
||||
print(f"\n❌ Gruppe '{GROUP_PATH}' nicht gefunden. Pfad prüfen.")
|
||||
else:
|
||||
print(f"\n❌ HTTP-Fehler: {e}")
|
||||
sys.exit(1)
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"\n❌ Verbindung zu {GITLAB_URL} fehlgeschlagen. VPN aktiv?")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
INB-Analyse: Extrahiert Text aus den INB-PDFs und sucht nach Widerspruechen
|
||||
zwischen Hauptdokument und Anhaengen.
|
||||
|
||||
Fokus:
|
||||
- Expresstrassen (bekannter Widerspruch)
|
||||
- NAÄ (Netzausgeloeste Aenderungen)
|
||||
- Abrechnung / Trassenpreis
|
||||
- Fristen und Annahme-Prozesse
|
||||
- Schnittstellen-Definitionen
|
||||
"""
|
||||
import PyPDF2
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
DATA_DIR = Path("project-audit/data")
|
||||
|
||||
def extract_pdf(path):
|
||||
"""Extrahiert Text aus PDF."""
|
||||
text = ""
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
reader = PyPDF2.PdfReader(f)
|
||||
print(f" {path.name}: {len(reader.pages)} Seiten")
|
||||
for page in reader.pages:
|
||||
t = page.extract_text()
|
||||
if t:
|
||||
text += t + "\n"
|
||||
except Exception as e:
|
||||
print(f" FEHLER bei {path.name}: {e}")
|
||||
return text
|
||||
|
||||
# PDFs laden
|
||||
print("=== INB-2027 Dokumente laden ===")
|
||||
docs = {}
|
||||
for pdf in sorted(DATA_DIR.glob("INB-2027*.pdf")):
|
||||
docs[pdf.stem] = extract_pdf(pdf)
|
||||
|
||||
print(f"\nGesamt: {len(docs)} Dokumente, {sum(len(v) for v in docs.values())} Zeichen")
|
||||
|
||||
# Suchbegriffe die fuer pathOS relevant sind
|
||||
SEARCH_TERMS = [
|
||||
"Expresstrasse",
|
||||
"express",
|
||||
"Netzausgelöst",
|
||||
"netzausgelöst",
|
||||
"NAÄ",
|
||||
"implizit",
|
||||
"Annahme",
|
||||
"Frist",
|
||||
"Abrechnung",
|
||||
"Trassenpreis",
|
||||
"Schnittstelle",
|
||||
"TAF",
|
||||
"TAP",
|
||||
"elektronisch",
|
||||
"IT-System",
|
||||
"Portal",
|
||||
"Bestellung",
|
||||
"Stornierung",
|
||||
"Änderung",
|
||||
"Gelegenheitsverkehr",
|
||||
"20 Stunden",
|
||||
"20h",
|
||||
"Verkehrstag",
|
||||
"Tageswechsel",
|
||||
"Teilausfall",
|
||||
"Umleitung",
|
||||
]
|
||||
|
||||
print("\n=== Relevante Stellen pro Dokument ===")
|
||||
for doc_name, text in docs.items():
|
||||
print(f"\n--- {doc_name} ({len(text)} Zeichen) ---")
|
||||
found_terms = {}
|
||||
for term in SEARCH_TERMS:
|
||||
matches = [m.start() for m in re.finditer(re.escape(term), text, re.IGNORECASE)]
|
||||
if matches:
|
||||
found_terms[term] = len(matches)
|
||||
|
||||
if found_terms:
|
||||
for term, count in sorted(found_terms.items(), key=lambda x: -x[1]):
|
||||
print(f" {term:<25} {count:>3}x")
|
||||
else:
|
||||
print(" (keine relevanten Begriffe gefunden)")
|
||||
|
||||
# Detailsuche: Expresstrassen
|
||||
print("\n\n=== DETAIL: Expresstrassen ===")
|
||||
for doc_name, text in docs.items():
|
||||
matches = list(re.finditer(r'(?i)express.{0,200}', text))
|
||||
if matches:
|
||||
print(f"\n--- {doc_name} ---")
|
||||
for i, m in enumerate(matches[:5]):
|
||||
context = text[max(0, m.start()-50):m.start()+200].replace("\n", " ").strip()
|
||||
print(f" [{i+1}] ...{context}...")
|
||||
|
||||
# Detailsuche: NAÄ / Netzausgeloeste Aenderungen
|
||||
print("\n\n=== DETAIL: NAÄ / Netzausgelöste Änderungen ===")
|
||||
for doc_name, text in docs.items():
|
||||
matches = list(re.finditer(r'(?i)(netzausgelöst|NAÄ|netz.{0,5}ausgelöst).{0,200}', text))
|
||||
if matches:
|
||||
print(f"\n--- {doc_name} ---")
|
||||
for i, m in enumerate(matches[:5]):
|
||||
context = text[max(0, m.start()-30):m.start()+200].replace("\n", " ").strip()
|
||||
print(f" [{i+1}] ...{context}...")
|
||||
|
||||
# Detailsuche: Fristen / Annahme
|
||||
print("\n\n=== DETAIL: Fristen und Annahme ===")
|
||||
for doc_name, text in docs.items():
|
||||
matches = list(re.finditer(r'(?i)(Annahme|angenommen|Frist).{0,150}', text))
|
||||
if matches:
|
||||
print(f"\n--- {doc_name} ({len(matches)} Treffer, zeige 5) ---")
|
||||
for i, m in enumerate(matches[:5]):
|
||||
context = text[max(0, m.start()-20):m.start()+150].replace("\n", " ").strip()
|
||||
print(f" [{i+1}] ...{context}...")
|
||||
|
||||
# Detailsuche: Abrechnung / Trassenpreis
|
||||
print("\n\n=== DETAIL: Abrechnung / Trassenpreis ===")
|
||||
for doc_name, text in docs.items():
|
||||
matches = list(re.finditer(r'(?i)(Abrechnung|Trassenpreis|Entgelt|Faktur).{0,150}', text))
|
||||
if matches:
|
||||
print(f"\n--- {doc_name} ({len(matches)} Treffer, zeige 5) ---")
|
||||
for i, m in enumerate(matches[:5]):
|
||||
context = text[max(0, m.start()-20):m.start()+150].replace("\n", " ").strip()
|
||||
print(f" [{i+1}] ...{context}...")
|
||||
|
||||
# Export als Textdateien fuer spaetere Analyse
|
||||
print("\n\n=== Export als Text ===")
|
||||
for doc_name, text in docs.items():
|
||||
out_path = DATA_DIR / f"{doc_name}.txt"
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
print(f" {out_path.name}: {len(text)} Zeichen")
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Gezielte Suche in INB-Hauptdokument nach pathOS-relevanten Themen."""
|
||||
import re
|
||||
|
||||
with open("project-audit/data/INB-2027-Hauptdokument-data.txt", "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
|
||||
searches = [
|
||||
# Expresstrassen / Prioritaet
|
||||
("Expresstrasse|Expressangebot|Express-Trasse|Vorrangtrasse|vorrangig|Priorität.*Trasse", "Expresstrassen/Vorrang"),
|
||||
# Relevanz-Kategorien
|
||||
("sehr hohe|hohe Relevanz|mittlere Relevanz|geringe Relevanz|Relevanz", "Relevanz-Kategorien"),
|
||||
# 20h / Tageswechsel
|
||||
("20 Stunden|20h|Tageswechsel|Verkehrstag.*wechsel|über.*Mitternacht", "20h/Tageswechsel"),
|
||||
# NAÄ / implizite Annahme
|
||||
("netzausgelöst|NAÄ|implizit.*Annahme|automatisch.*Annahme|Annahme.*Frist", "NAÄ/Implizite Annahme"),
|
||||
# Abrechnung
|
||||
("Abrechnung.*Trasse|Trassenentgelt|Fakturierung|Rechnungsstellung", "Abrechnung"),
|
||||
# Stornierung
|
||||
("Stornierung.*Frist|Stornogebühr|Stornierungsentgelt", "Stornierung/Fristen"),
|
||||
# GelV spezifisch
|
||||
("Gelegenheitsverkehr.*Frist|GelV|ad.hoc", "GelV/Ad-hoc"),
|
||||
# Schnittstelle / Common Interface
|
||||
("Common Interface|Schnittstelle.*TAF|Schnittstelle.*TAP|XSD|XML.*Nachricht", "Schnittstelle/CI"),
|
||||
# Kapazitaet / Koordinierung
|
||||
("Kapazitätskonflikt|Koordinierungsverfahren|Überlastung", "Kapazitaet"),
|
||||
# Baubedingt
|
||||
("baubedingt|Baumaßnahme|Sperrung.*Strecke", "Baubedingt"),
|
||||
]
|
||||
|
||||
for pattern, label in searches:
|
||||
matches = list(re.finditer(pattern, text, re.IGNORECASE))
|
||||
if matches:
|
||||
print(f"\n{'='*80}")
|
||||
print(f" {label}: {len(matches)} Treffer")
|
||||
print(f"{'='*80}")
|
||||
for m in matches[:4]:
|
||||
start = max(0, m.start() - 60)
|
||||
end = min(len(text), m.start() + 250)
|
||||
ctx = text[start:end].replace("\n", " ").strip()
|
||||
# Markiere den Match
|
||||
print(f"\n ...{ctx}...")
|
||||
else:
|
||||
print(f"\n {label}: KEINE TREFFER")
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Sucht offene Abrechnungs-Tickets in TTTSol/TTTI."""
|
||||
import urllib.request, json, ssl, urllib.parse
|
||||
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
TOKEN = secrets.get("JIRA_TOKEN", "")
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
def search(jql):
|
||||
url = f"https://arija.jaas.service.deutschebahn.com/rest/api/2/search?jql={urllib.parse.quote(jql)}&maxResults=30&fields=key,summary,status,priority,assignee,updated,labels"
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
def print_issues(title, jql):
|
||||
print(f"\n--- {title} ---")
|
||||
r = search(jql)
|
||||
total = r.get("total", 0)
|
||||
print(f" Total: {total}")
|
||||
for i in r.get("issues", [])[:20]:
|
||||
f = i["fields"]
|
||||
s = f["status"]["name"]
|
||||
p = (f.get("priority") or {}).get("name", "?")
|
||||
a = (f.get("assignee") or {}).get("displayName", "Unassigned")
|
||||
labels = ", ".join(f.get("labels", []))
|
||||
summary = f["summary"][:70]
|
||||
print(f" {i['key']:<15} [{s:<14}] {p:<10} {summary}")
|
||||
if a != "Unassigned" or labels:
|
||||
extra = f"Assignee: {a}" if a != "Unassigned" else ""
|
||||
if labels:
|
||||
extra += f" Labels: {labels}"
|
||||
print(f" {'':15} {extra}")
|
||||
|
||||
# Abrechnung offen
|
||||
print_issues(
|
||||
"Abrechnung (offen, TTTSol + TTTI)",
|
||||
'text ~ "Abrechnung" AND (project = TTTSOL OR project = TTTI) AND statusCategory != Done ORDER BY priority DESC'
|
||||
)
|
||||
|
||||
# AC Trasse
|
||||
print_issues(
|
||||
"AC Trasse (alle Projekte, offen)",
|
||||
'text ~ "AC Trasse" AND statusCategory != Done ORDER BY priority DESC'
|
||||
)
|
||||
|
||||
# O2CCIB-6931 direkt (implizite Annahme)
|
||||
print_issues(
|
||||
"O2CCIB-6931 - Implizite Annahme NAÄ",
|
||||
'key = O2CCIB-6931'
|
||||
)
|
||||
|
||||
# TTTSOL-2184 direkt (implizite Annahme Highest)
|
||||
print_issues(
|
||||
"TTTSOL-2184 - Umgang mit impliziter Annahme",
|
||||
'key = TTTSOL-2184'
|
||||
)
|
||||
|
||||
# TTTSOL-1677 (20h Tool)
|
||||
print_issues(
|
||||
"TTTSOL-1677 - 20h Tool",
|
||||
'key = TTTSOL-1677'
|
||||
)
|
||||
|
||||
# TTTSOL-2035 (Abrechnung Zugnummer Umleitungsfall)
|
||||
print_issues(
|
||||
"TTTSOL-2035 - Abrechnung Zugnummer Umleitungsfall",
|
||||
'key = TTTSOL-2035'
|
||||
)
|
||||
@@ -0,0 +1,127 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Exportiert Fachliches Board und TTTI Board aus Jira
|
||||
#>
|
||||
|
||||
$JiraUrl = "https://arija.jaas.service.deutschebahn.com"
|
||||
$ApiBase = "$JiraUrl/rest"
|
||||
$OutputDir = "project-audit\data\jira-boards"
|
||||
|
||||
$Boards = @(
|
||||
@{ id = 2791; name = "Fachliches-Board" }
|
||||
@{ id = 1487; name = "TTTI-Board" }
|
||||
)
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
Write-Host "============================================================"
|
||||
Write-Host " pathOS Boards Export (Fachlich + TTTI)"
|
||||
Write-Host "============================================================"
|
||||
|
||||
$Token = Read-Host "Jira PAT"
|
||||
$Headers = @{ "Authorization" = "Bearer $Token"; "Content-Type" = "application/json" }
|
||||
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
|
||||
function Invoke-JiraApi($Uri) {
|
||||
try {
|
||||
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
return ($R.Content | ConvertFrom-Json)
|
||||
} catch {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($Board in $Boards) {
|
||||
$BId = $Board.id
|
||||
$BName = $Board.name
|
||||
Write-Host ""
|
||||
Write-Host ">> $BName (Board ID: $BId)" -ForegroundColor Yellow
|
||||
|
||||
# Board-Info
|
||||
$Info = Invoke-JiraApi "$ApiBase/agile/1.0/board/$BId"
|
||||
if ($Info) {
|
||||
Write-Host " Board: $($Info.name) (Projekt: $($Info.location.projectKey))"
|
||||
$Info | ConvertTo-Json -Depth 5 | Out-File "$OutputDir\$BName-info.json" -Encoding UTF8
|
||||
}
|
||||
|
||||
# Board-Config
|
||||
$Config = Invoke-JiraApi "$ApiBase/agile/1.0/board/$BId/configuration"
|
||||
if ($Config) {
|
||||
$Config | ConvertTo-Json -Depth 10 | Out-File "$OutputDir\$BName-config.json" -Encoding UTF8
|
||||
}
|
||||
|
||||
# Issues auf dem Board
|
||||
Write-Host " Lade Issues..." -NoNewline
|
||||
$Issues = @()
|
||||
$Start = 0
|
||||
do {
|
||||
$Uri = "$ApiBase/agile/1.0/board/$BId/issue?startAt=$Start&maxResults=100&fields=summary,status,issuetype,assignee,labels,components,fixVersions,priority,created,updated"
|
||||
$Result = Invoke-JiraApi $Uri
|
||||
if ($null -eq $Result) { break }
|
||||
$Issues += $Result.issues
|
||||
$Start += 100
|
||||
Write-Host "." -NoNewline
|
||||
} while ($Issues.Count -lt $Result.total -and $Issues.Count -lt 1000)
|
||||
Write-Host ""
|
||||
Write-Host " $($Issues.Count) Issues geladen" -ForegroundColor Green
|
||||
|
||||
$Issues | ConvertTo-Json -Depth 10 | Out-File "$OutputDir\$BName-issues.json" -Encoding UTF8
|
||||
|
||||
# Analyse
|
||||
$ByType = $Issues | Group-Object { $_.fields.issuetype.name }
|
||||
$ByStatus = $Issues | Group-Object { $_.fields.status.name }
|
||||
$ByLabel = @{}
|
||||
foreach ($I in $Issues) {
|
||||
if ($I.fields.labels) {
|
||||
foreach ($L in $I.fields.labels) { $ByLabel[$L] = ($ByLabel[$L]) + 1 }
|
||||
}
|
||||
}
|
||||
|
||||
$Done = ($Issues | Where-Object { $_.fields.status.name -match "Fertig|Done|Closed|Resolved|Geloest" }).Count
|
||||
$InProg = ($Issues | Where-Object { $_.fields.status.name -match "Implementation|In Bearbeitung|In Progress|Review|Validierung|Analysis" }).Count
|
||||
$Open = $Issues.Count - $Done - $InProg
|
||||
|
||||
Write-Host " Fertig: $Done | In Arbeit: $InProg | Offen: $Open"
|
||||
|
||||
# CSV
|
||||
$CsvData = $Issues | ForEach-Object {
|
||||
$Comps = if ($_.fields.components) { ($_.fields.components | ForEach-Object { $_.name }) -join ", " } else { "" }
|
||||
$Labels = if ($_.fields.labels) { $_.fields.labels -join ", " } else { "" }
|
||||
[PSCustomObject]@{
|
||||
Key = $_.key
|
||||
Typ = $_.fields.issuetype.name
|
||||
Status = $_.fields.status.name
|
||||
Prioritaet = if ($_.fields.priority) { $_.fields.priority.name } else { "" }
|
||||
Zusammenfassung = $_.fields.summary
|
||||
Komponenten = $Comps
|
||||
Labels = $Labels
|
||||
Assignee = if ($_.fields.assignee) { $_.fields.assignee.displayName } else { "" }
|
||||
Erstellt = $_.fields.created
|
||||
Aktualisiert = $_.fields.updated
|
||||
}
|
||||
}
|
||||
$CsvData | Export-Csv "$OutputDir\$BName-issues.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
|
||||
|
||||
# Markdown
|
||||
$Md = @()
|
||||
$Md += "# $BName Analyse"
|
||||
$Md += ""
|
||||
$Md += "> Board ID: $BId | Issues: $($Issues.Count) | Fertig: $Done | In Arbeit: $InProg | Offen: $Open"
|
||||
$Md += ""
|
||||
$Md += "## Nach Typ"
|
||||
foreach ($G in ($ByType | Sort-Object Count -Descending)) { $Md += "- $($G.Name): $($G.Count)" }
|
||||
$Md += ""
|
||||
$Md += "## Nach Status"
|
||||
foreach ($G in ($ByStatus | Sort-Object Count -Descending)) { $Md += "- $($G.Name): $($G.Count)" }
|
||||
$Md += ""
|
||||
$Md += "## Top Labels"
|
||||
foreach ($E in ($ByLabel.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 20)) { $Md += "- $($E.Key): $($E.Value)" }
|
||||
$Md -join "`n" | Out-File "$OutputDir\$BName-analyse.md" -Encoding UTF8
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
Write-Host " FERTIG! Ergebnisse in $OutputDir" -ForegroundColor Green
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Prueft BSSUPPORT Projekt-Struktur und sucht gefixt Bugs."""
|
||||
import urllib.request, json, ssl, urllib.parse
|
||||
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
TOKEN = secrets.get("JIRA_TOKEN", "")
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
def search(jql, max_results=30):
|
||||
url = f"https://arija.jaas.service.deutschebahn.com/rest/api/2/search?jql={urllib.parse.quote(jql)}&maxResults={max_results}&fields=key,summary,status,issuetype,updated,resolved,resolution,fixVersions,labels"
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except Exception as e:
|
||||
print(f" API-Fehler: {e}")
|
||||
return {"issues": [], "total": 0}
|
||||
|
||||
# 1. Alle kuerzlich aktualisierten Tickets
|
||||
print("=== BSSUPPORT: Letzte 30 aktualisierte Tickets ===")
|
||||
r = search("project = BSSUPPORT ORDER BY updated DESC")
|
||||
print(f"Total im Projekt: {r.get('total', 0)}")
|
||||
print()
|
||||
|
||||
types_seen = set()
|
||||
for i in r.get("issues", []):
|
||||
f = i["fields"]
|
||||
itype = f.get("issuetype", {}).get("name", "?")
|
||||
types_seen.add(itype)
|
||||
s = f["status"]["name"]
|
||||
res = (f.get("resolution") or {}).get("name", "")
|
||||
fv = ", ".join([v["name"] for v in f.get("fixVersions", [])])
|
||||
updated = f.get("updated", "")[:10]
|
||||
print(f" {i['key']:<18} [{itype:<15}] [{s:<14}] Res={res:<12} Upd={updated} {f['summary'][:50]}")
|
||||
if fv:
|
||||
print(f" {'':18} FixVersion: {fv}")
|
||||
|
||||
print(f"\nGesehene Issue-Typen: {sorted(types_seen)}")
|
||||
|
||||
# 2. Nur geschlossene/geloeste in letzten 2 Wochen
|
||||
print("\n\n=== BSSUPPORT: Geschlossen in letzten 14 Tagen ===")
|
||||
r2 = search("project = BSSUPPORT AND statusCategory = Done AND updated >= -14d ORDER BY updated DESC")
|
||||
print(f"Treffer: {r2.get('total', 0)}")
|
||||
for i in r2.get("issues", []):
|
||||
f = i["fields"]
|
||||
itype = f.get("issuetype", {}).get("name", "?")
|
||||
s = f["status"]["name"]
|
||||
res = (f.get("resolution") or {}).get("name", "")
|
||||
resolved = (f.get("resolved") or "")[:10]
|
||||
fv = ", ".join([v["name"] for v in f.get("fixVersions", [])])
|
||||
labels = ", ".join(f.get("labels", []))
|
||||
print(f" {i['key']:<18} [{itype:<15}] Res={res:<12} Resolved={resolved} {f['summary'][:55]}")
|
||||
if fv:
|
||||
print(f" {'':18} FixVersion: {fv}")
|
||||
if labels:
|
||||
print(f" {'':18} Labels: {labels}")
|
||||
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
Sucht BSSUPPORT-Tickets die in den letzten 2 Wochen durch Releases gefixt wurden.
|
||||
Nur Defects/Bugs.
|
||||
"""
|
||||
import urllib.request, json, ssl, urllib.parse
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
TOKEN = secrets.get("JIRA_TOKEN", "")
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
def search(jql, max_results=100):
|
||||
url = f"https://arija.jaas.service.deutschebahn.com/rest/api/2/search?jql={urllib.parse.quote(jql)}&maxResults={max_results}&fields=key,summary,status,priority,assignee,updated,resolved,issuetype,labels,fixVersions,resolution"
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except Exception as e:
|
||||
print(f" API-Fehler: {e}")
|
||||
return {"issues": [], "total": 0}
|
||||
|
||||
two_weeks_ago = (datetime.now() - timedelta(days=14)).strftime("%Y-%m-%d")
|
||||
|
||||
print(f"=== BSSUPPORT: Bugs/Defects gefixt seit {two_weeks_ago} ===")
|
||||
print()
|
||||
|
||||
# Suche 1: Resolved in letzten 2 Wochen, Typ Bug/Defect
|
||||
jql = f'project = BSSUPPORT AND issuetype in (Bug, Defect, Fehler) AND resolved >= "{two_weeks_ago}" ORDER BY resolved DESC'
|
||||
r = search(jql)
|
||||
print(f"Suche 1 (resolved >= {two_weeks_ago}, Bug/Defect): {r['total']} Treffer")
|
||||
for i in r.get("issues", []):
|
||||
f = i["fields"]
|
||||
s = f["status"]["name"]
|
||||
p = (f.get("priority") or {}).get("name", "?")
|
||||
res = (f.get("resolution") or {}).get("name", "?")
|
||||
resolved = f.get("resolved", "")[:10] if f.get("resolved") else "?"
|
||||
fix_versions = ", ".join([v["name"] for v in f.get("fixVersions", [])])
|
||||
print(f" {i['key']:<18} [{s:<14}] Res={res:<10} Resolved={resolved} {f['summary'][:55]}")
|
||||
if fix_versions:
|
||||
print(f" {'':18} FixVersion: {fix_versions}")
|
||||
|
||||
# Suche 2: Status Done/Geschlossen in letzten 2 Wochen (breiter)
|
||||
print()
|
||||
jql2 = f'project = BSSUPPORT AND issuetype in (Bug, Defect, Fehler, "Sub-task") AND statusCategory = Done AND updated >= "{two_weeks_ago}" ORDER BY updated DESC'
|
||||
r2 = search(jql2)
|
||||
print(f"Suche 2 (Done + updated >= {two_weeks_ago}): {r2['total']} Treffer")
|
||||
for i in r2.get("issues", [])[:30]:
|
||||
f = i["fields"]
|
||||
s = f["status"]["name"]
|
||||
itype = f.get("issuetype", {}).get("name", "?")
|
||||
res = (f.get("resolution") or {}).get("name", "?")
|
||||
fix_versions = ", ".join([v["name"] for v in f.get("fixVersions", [])])
|
||||
print(f" {i['key']:<18} [{itype:<10}] [{s:<14}] Res={res:<10} {f['summary'][:55]}")
|
||||
if fix_versions:
|
||||
print(f" {'':18} FixVersion: {fix_versions}")
|
||||
|
||||
# Suche 3: Alle Typen die "fix" im Resolution haben
|
||||
print()
|
||||
jql3 = f'project = BSSUPPORT AND resolution = Fixed AND resolved >= "{two_weeks_ago}" ORDER BY resolved DESC'
|
||||
r3 = search(jql3)
|
||||
print(f"Suche 3 (resolution=Fixed, alle Typen): {r3['total']} Treffer")
|
||||
for i in r3.get("issues", [])[:30]:
|
||||
f = i["fields"]
|
||||
itype = f.get("issuetype", {}).get("name", "?")
|
||||
resolved = f.get("resolved", "")[:10] if f.get("resolved") else "?"
|
||||
fix_versions = ", ".join([v["name"] for v in f.get("fixVersions", [])])
|
||||
print(f" {i['key']:<18} [{itype:<10}] Resolved={resolved} {f['summary'][:55]}")
|
||||
if fix_versions:
|
||||
print(f" {'':18} FixVersion: {fix_versions}")
|
||||
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
BSSUPPORT Bugs mit Releases in KW 18+19 (28.04 - 07.05.2026).
|
||||
Sucht Bugs die:
|
||||
- FixVersion enthalten (KTU oder Prod Release)
|
||||
- In den letzten 2 Kalenderwochen aktualisiert wurden
|
||||
- Status: Fertig/Geschlossen ODER ReTest (= im Release enthalten)
|
||||
"""
|
||||
import urllib.request, json, ssl, urllib.parse
|
||||
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
TOKEN = secrets.get("JIRA_TOKEN", "")
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
JIRA_URL = "https://arija.jaas.service.deutschebahn.com"
|
||||
|
||||
def search(jql, max_results=100):
|
||||
url = f"{JIRA_URL}/rest/api/2/search?jql={urllib.parse.quote(jql)}&maxResults={max_results}&fields=key,summary,status,issuetype,updated,resolved,resolution,fixVersions,labels,description"
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except Exception as e:
|
||||
print(f" API-Fehler: {e}")
|
||||
return {"issues": [], "total": 0}
|
||||
|
||||
# KW18 Start: Montag 28.04.2026
|
||||
START_DATE = "2026-04-28"
|
||||
|
||||
print(f"=== BSSUPPORT: Bugs mit Release-Bezug seit {START_DATE} (KW18+19) ===")
|
||||
print()
|
||||
|
||||
# Suche: Bugs mit FixVersion die in KW18/19 aktualisiert wurden
|
||||
jql = f'project = BSSUPPORT AND issuetype = Bug AND fixVersions is not EMPTY AND updated >= "{START_DATE}" ORDER BY fixVersions DESC, updated DESC'
|
||||
r = search(jql)
|
||||
print(f"Bugs mit FixVersion (aktualisiert seit {START_DATE}): {r.get('total', 0)}")
|
||||
print()
|
||||
|
||||
# Auch Bugs im ReTest (= im Release, noch nicht abgenommen)
|
||||
jql2 = f'project = BSSUPPORT AND issuetype = Bug AND status = ReTest AND updated >= "{START_DATE}" ORDER BY updated DESC'
|
||||
r2 = search(jql2)
|
||||
|
||||
# Auch geschlossene Bugs ohne FixVersion aber mit Release-Bezug im Label
|
||||
jql3 = f'project = BSSUPPORT AND issuetype = Bug AND statusCategory = Done AND updated >= "{START_DATE}" ORDER BY updated DESC'
|
||||
r3 = search(jql3)
|
||||
|
||||
# Alle zusammenfuehren (deduplizieren)
|
||||
all_issues = {}
|
||||
for issue_list in [r.get("issues", []), r2.get("issues", []), r3.get("issues", [])]:
|
||||
for i in issue_list:
|
||||
all_issues[i["key"]] = i
|
||||
|
||||
print(f"Gesamt (dedupliziert): {len(all_issues)} Bugs")
|
||||
print()
|
||||
|
||||
# Ausgabe als Tabelle
|
||||
print(f"{'Ticket':<18} {'Titel':<55} {'Release':<20} {'Status'}")
|
||||
print("=" * 110)
|
||||
|
||||
for key in sorted(all_issues.keys()):
|
||||
i = all_issues[key]
|
||||
f = i["fields"]
|
||||
s = f["status"]["name"]
|
||||
res = (f.get("resolution") or {}).get("name", "")
|
||||
fv = ", ".join([v["name"] for v in f.get("fixVersions", [])])
|
||||
summary = f["summary"][:54]
|
||||
|
||||
# Status-Anzeige
|
||||
if s == "Geschlossen" and res == "Fertig":
|
||||
status_display = "Fertig"
|
||||
elif s == "Geschlossen" and res:
|
||||
status_display = res
|
||||
elif s == "ReTest":
|
||||
status_display = "ReTest"
|
||||
else:
|
||||
status_display = s
|
||||
|
||||
# Nur KTU/Prod Releases oder geschlossene Bugs
|
||||
if fv or status_display in ["Fertig", "ReTest"]:
|
||||
link = f"{JIRA_URL}/browse/{key}"
|
||||
print(f" {key:<16} {summary:<55} {fv:<20} {status_display}")
|
||||
|
||||
print()
|
||||
print()
|
||||
print("=== Markdown-Tabelle ===")
|
||||
print()
|
||||
print("| Ticket | Titel | Release | Status |")
|
||||
print("|--------|-------|---------|--------|")
|
||||
|
||||
for key in sorted(all_issues.keys()):
|
||||
i = all_issues[key]
|
||||
f = i["fields"]
|
||||
s = f["status"]["name"]
|
||||
res = (f.get("resolution") or {}).get("name", "")
|
||||
fv = ", ".join([v["name"] for v in f.get("fixVersions", [])])
|
||||
summary = f["summary"][:65]
|
||||
|
||||
if s == "Geschlossen" and res == "Fertig":
|
||||
status_display = "✅ Fertig"
|
||||
elif s == "Geschlossen" and res:
|
||||
status_display = f"🔒 {res}"
|
||||
elif s == "ReTest":
|
||||
status_display = "🔄 ReTest"
|
||||
else:
|
||||
status_display = s
|
||||
|
||||
if fv or "Fertig" in status_display or "ReTest" in status_display:
|
||||
link = f"[{key}]({JIRA_URL}/browse/{key})"
|
||||
print(f"| {link} | {summary} | {fv} | {status_display} |")
|
||||
@@ -0,0 +1,154 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Bug-Verursachungs-Analyse (Bulk-Ansatz)
|
||||
Holt alle Bugs + Features der letzten 6 Monate in 2 Bulk-Abfragen
|
||||
und korreliert lokal.
|
||||
#>
|
||||
|
||||
$JiraUrl = "https://arija.jaas.service.deutschebahn.com"
|
||||
$ApiBase = "$JiraUrl/rest"
|
||||
$OutputDir = "project-audit\data\jira-bugs"
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
Write-Host "============================================================"
|
||||
Write-Host " Bug-Verursachungs-Analyse (Bulk)"
|
||||
Write-Host " 2 API-Calls statt 60+"
|
||||
Write-Host "============================================================"
|
||||
|
||||
$Token = $null
|
||||
$SecretsPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\.secrets"
|
||||
if (Test-Path $SecretsPath) {
|
||||
Get-Content $SecretsPath | ForEach-Object {
|
||||
if ($_ -match "^JIRA_TOKEN=(.+)$") {
|
||||
$val = $Matches[1].Trim()
|
||||
if ($val -ne "HIER_TOKEN_EINTRAGEN") { $Token = $val }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $Token) { $Token = Read-Host "Jira PAT" }
|
||||
$Headers = @{ "Authorization" = "Bearer $Token"; "Content-Type" = "application/json" }
|
||||
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
|
||||
function Invoke-JiraBulk {
|
||||
param([string]$Jql, [int]$MaxTotal = 5000)
|
||||
$AllIssues = @()
|
||||
$StartAt = 0
|
||||
$PageSize = 1000
|
||||
do {
|
||||
$Uri = "$ApiBase/api/2/search?jql=$([Uri]::EscapeDataString($Jql))&startAt=$StartAt&maxResults=$PageSize&fields=summary,created,resolutiondate,assignee,issuetype,project,reporter"
|
||||
Write-Host " Lade $StartAt..." -NoNewline
|
||||
try {
|
||||
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$Data = $R.Content | ConvertFrom-Json
|
||||
$AllIssues += $Data.issues
|
||||
$Total = $Data.total
|
||||
Write-Host " $($AllIssues.Count)/$Total" -ForegroundColor Green
|
||||
$StartAt += $PageSize
|
||||
Start-Sleep -Seconds 2
|
||||
} catch {
|
||||
$StatusCode = 0
|
||||
if ($_.Exception.Response) { $StatusCode = [int]$_.Exception.Response.StatusCode }
|
||||
if ($StatusCode -eq 429) {
|
||||
Write-Host " [429 - warte 30s]" -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds 30
|
||||
} else {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
break
|
||||
}
|
||||
}
|
||||
} while ($AllIssues.Count -lt $Total -and $AllIssues.Count -lt $MaxTotal)
|
||||
return $AllIssues
|
||||
}
|
||||
|
||||
$Projects = "O2C404, O2CCIB, O2CZERO"
|
||||
|
||||
# Abfrage 1: Alle Bugs (6 Monate)
|
||||
Write-Host ""
|
||||
Write-Host ">> Bugs (6 Monate)..." -ForegroundColor Yellow
|
||||
$BugJql = "project in ($Projects) AND issuetype = Bug AND created >= -180d ORDER BY created DESC"
|
||||
$Bugs = Invoke-JiraBulk -Jql $BugJql -MaxTotal 2000
|
||||
Write-Host " $($Bugs.Count) Bugs geladen"
|
||||
|
||||
# Abfrage 2: Abgeschlossene Features/Stories (6 Monate)
|
||||
Write-Host ""
|
||||
Write-Host ">> Features/Stories (6 Monate, abgeschlossen)..." -ForegroundColor Yellow
|
||||
$FeatureJql = "project in ($Projects) AND issuetype in (Feature, Enabler, Story, Aufgabe, Unteraufgabe) AND status in (Fertig, Done, Closed) AND resolved >= -180d ORDER BY resolved DESC"
|
||||
$Features = Invoke-JiraBulk -Jql $FeatureJql -MaxTotal 3000
|
||||
Write-Host " $($Features.Count) Features geladen"
|
||||
|
||||
# Lokale Analyse pro Team
|
||||
Write-Host ""
|
||||
Write-Host ">> Korrelationsanalyse..." -ForegroundColor Yellow
|
||||
|
||||
foreach ($ProjectKey in @("O2C404", "O2CCIB", "O2CZERO")) {
|
||||
$TeamName = switch ($ProjectKey) { "O2C404" { "Team 404" }; "O2CCIB" { "Team CIB" }; "O2CZERO" { "Team Zero" } }
|
||||
Write-Host ""
|
||||
Write-Host " === $TeamName ===" -ForegroundColor Cyan
|
||||
|
||||
$TeamBugs = $Bugs | Where-Object { $_.fields.project.key -eq $ProjectKey }
|
||||
$TeamFeatures = $Features | Where-Object { $_.fields.project.key -eq $ProjectKey }
|
||||
|
||||
Write-Host " $($TeamBugs.Count) Bugs, $($TeamFeatures.Count) Features"
|
||||
|
||||
# Korrelation: Wer hat in 14 Tagen vor Bug-Erstellung Features abgeschlossen?
|
||||
$Causation = @{}
|
||||
foreach ($Bug in $TeamBugs) {
|
||||
$BugCreated = [DateTime]::Parse($Bug.fields.created)
|
||||
$WindowStart = $BugCreated.AddDays(-14)
|
||||
|
||||
$RecentFeatures = $TeamFeatures | Where-Object {
|
||||
$_.fields.resolutiondate -and
|
||||
[DateTime]::Parse($_.fields.resolutiondate) -ge $WindowStart -and
|
||||
[DateTime]::Parse($_.fields.resolutiondate) -le $BugCreated
|
||||
}
|
||||
|
||||
foreach ($F in $RecentFeatures) {
|
||||
if ($F.fields.assignee) {
|
||||
$Dev = $F.fields.assignee.displayName
|
||||
if (-not $Causation[$Dev]) { $Causation[$Dev] = @{ Features = 0; BugKeys = @() } }
|
||||
if ($Causation[$Dev].BugKeys -notcontains $Bug.key) {
|
||||
$Causation[$Dev].BugKeys += $Bug.key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Feature-Count pro Dev
|
||||
foreach ($F in $TeamFeatures) {
|
||||
if ($F.fields.assignee) {
|
||||
$Dev = $F.fields.assignee.displayName
|
||||
if (-not $Causation[$Dev]) { $Causation[$Dev] = @{ Features = 0; BugKeys = @() } }
|
||||
$Causation[$Dev].Features++
|
||||
}
|
||||
}
|
||||
|
||||
# Ausgabe Top 10
|
||||
Write-Host " --- Potenzielle Bug-Verursachung (14-Tage-Fenster) ---"
|
||||
$Sorted = $Causation.GetEnumerator() | Sort-Object { $_.Value.BugKeys.Count } -Descending
|
||||
foreach ($E in ($Sorted | Select-Object -First 10)) {
|
||||
$UniqueBugs = $E.Value.BugKeys.Count
|
||||
$Feats = $E.Value.Features
|
||||
$Rate = if ($Feats -gt 0) { [Math]::Round($UniqueBugs / $Feats * 100, 0) } else { 0 }
|
||||
Write-Host (" {0,-30} Features:{1,4} | Bugs:{2,4} | Rate:{3,3}%" -f $E.Key, $Feats, $UniqueBugs, $Rate)
|
||||
}
|
||||
|
||||
# CSV
|
||||
$CsvData = $Sorted | ForEach-Object {
|
||||
[PSCustomObject]@{
|
||||
Team = $TeamName
|
||||
Developer = $_.Key
|
||||
Features = $_.Value.Features
|
||||
CorrelatedBugs = $_.Value.BugKeys.Count
|
||||
BugRate = if ($_.Value.Features -gt 0) { [Math]::Round($_.Value.BugKeys.Count / $_.Value.Features * 100, 0) } else { 0 }
|
||||
}
|
||||
}
|
||||
$CsvData | Export-Csv "$OutputDir\$ProjectKey-causation.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
Write-Host " FERTIG! API-Calls: ~$([Math]::Ceiling(($Bugs.Count + $Features.Count) / 1000) + 2)" -ForegroundColor Green
|
||||
Write-Host " Ergebnisse in $OutputDir" -ForegroundColor Green
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Systematischer Scan nach geschaeftskritischen Themen in Jira.
|
||||
Strategie: Suche nach Signalen die auf politische/regulatorische/kundenrelevante Risiken hindeuten.
|
||||
|
||||
Signale:
|
||||
1. Labels: TopThema, Konzernreporting, Taskforce-*, TTT_offenePunkte
|
||||
2. Prioritaet: Highest + Blocked
|
||||
3. Eskalation im Titel/Summary
|
||||
4. Kundenrelevant: DBFernverkehr, DBCargo, DBRegio, TTT_Kunde
|
||||
5. Regulatorisch: BNetzA, ERegG, SNB
|
||||
6. Lange offen + hohe Prio
|
||||
"""
|
||||
import urllib.request, json, ssl, urllib.parse
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
TOKEN = secrets.get("JIRA_TOKEN", "")
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
def search(jql, max_results=50):
|
||||
url = f"https://arija.jaas.service.deutschebahn.com/rest/api/2/search?jql={urllib.parse.quote(jql)}&maxResults={max_results}&fields=key,summary,status,priority,assignee,updated,created,labels,issuetype"
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except Exception as e:
|
||||
print(f" API-Fehler: {e}")
|
||||
return {"issues": [], "total": 0}
|
||||
|
||||
def print_issues(issues, max_show=15):
|
||||
for i in issues[:max_show]:
|
||||
f = i["fields"]
|
||||
s = f["status"]["name"]
|
||||
p = (f.get("priority") or {}).get("name", "?")
|
||||
labels = f.get("labels", [])
|
||||
key_labels = [l for l in labels if any(x in l for x in ["TopThema", "Konzern", "Taskforce", "Kunde", "offenePunkte", "Eskalation", "MUSS"])]
|
||||
label_str = " [" + ", ".join(key_labels) + "]" if key_labels else ""
|
||||
print(f" {i['key']:<15} [{s:<14}] {p:<10} {f['summary'][:60]}{label_str}")
|
||||
|
||||
print("=" * 100)
|
||||
print("SYSTEMATISCHER SCAN: Geschaeftskritische Risiken")
|
||||
print("=" * 100)
|
||||
|
||||
# 1. TopThema Label
|
||||
print("\n--- 1. Label 'TopThema' (alle offenen) ---")
|
||||
r = search('labels = TopThema AND statusCategory != Done ORDER BY priority DESC')
|
||||
print(f" Total: {r['total']}")
|
||||
print_issues(r["issues"])
|
||||
|
||||
# 2. Konzernreporting
|
||||
print("\n--- 2. Label 'Konzernreporting' (offen) ---")
|
||||
r = search('labels = Konzernreporting AND statusCategory != Done ORDER BY priority DESC')
|
||||
print(f" Total: {r['total']}")
|
||||
print_issues(r["issues"])
|
||||
|
||||
# 3. Taskforce-FplW (Fahrplanwechsel)
|
||||
print("\n--- 3. Label 'Taskforce-FplW' (offen) ---")
|
||||
r = search('labels = "Taskforce-FplW" AND statusCategory != Done ORDER BY priority DESC')
|
||||
print(f" Total: {r['total']}")
|
||||
print_issues(r["issues"])
|
||||
|
||||
# 4. Blocked + Highest/High
|
||||
print("\n--- 4. Blocked + Highest/High Prioritaet ---")
|
||||
r = search('status = Blocked AND priority in (Highest, High, Zeitkritisch, Hoch) AND (project = TTTSOL OR project = TTTI OR project = O2CCIB OR project = O2C404 OR project = O2CZERO) ORDER BY priority DESC')
|
||||
print(f" Total: {r['total']}")
|
||||
print_issues(r["issues"])
|
||||
|
||||
# 5. Eskalation im Titel
|
||||
print("\n--- 5. 'Eskalation' im Titel (offen) ---")
|
||||
r = search('summary ~ "Eskalation" AND statusCategory != Done ORDER BY updated DESC')
|
||||
print(f" Total: {r['total']}")
|
||||
print_issues(r["issues"])
|
||||
|
||||
# 6. Kundenrelevant (DBFernverkehr, DBCargo, DBRegio)
|
||||
print("\n--- 6. Kundenrelevant (DB Fernverkehr/Cargo/Regio, offen) ---")
|
||||
r = search('labels in (DBFernverkehr, DBCargo, DBRegio) AND statusCategory != Done ORDER BY priority DESC')
|
||||
print(f" Total: {r['total']}")
|
||||
print_issues(r["issues"])
|
||||
|
||||
# 7. TTT_Kunde Label
|
||||
print("\n--- 7. Label 'TTT_Kunde' (offen) ---")
|
||||
r = search('labels = TTT_Kunde AND statusCategory != Done ORDER BY priority DESC')
|
||||
print(f" Total: {r['total']}")
|
||||
print_issues(r["issues"])
|
||||
|
||||
# 8. TTT_offenePunkte + Highest
|
||||
print("\n--- 8. TTT_offenePunkte + Highest ---")
|
||||
r = search('labels = TTT_offenePunkte AND priority = Highest AND statusCategory != Done ORDER BY updated DESC')
|
||||
print(f" Total: {r['total']}")
|
||||
print_issues(r["issues"])
|
||||
|
||||
# 9. MUSS-Label (regulatorisch)
|
||||
print("\n--- 9. Label 'TTT_MUSS' (offen) ---")
|
||||
r = search('labels = TTT_MUSS AND statusCategory != Done ORDER BY priority DESC', 30)
|
||||
print(f" Total: {r['total']}")
|
||||
print_issues(r["issues"], 10)
|
||||
|
||||
# 10. Aelteste offene Highest-Tickets
|
||||
print("\n--- 10. Aelteste offene Highest-Tickets (>60 Tage) ---")
|
||||
r = search('priority = Highest AND statusCategory != Done AND (project = TTTSOL OR project = TTTI OR project = O2CCIB OR project = O2C404) AND created <= -60d ORDER BY created ASC')
|
||||
print(f" Total: {r['total']}")
|
||||
print_issues(r["issues"])
|
||||
|
||||
print("\n" + "=" * 100)
|
||||
@@ -0,0 +1,186 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Exportiert Jira-Daten fuer pathOS (O2CBS Projekt)
|
||||
.DESCRIPTION
|
||||
Zieht Boards, Sprints, Backlog-Items und Team-Zuordnungen.
|
||||
Kompatibel mit PowerShell 5.1.
|
||||
.USAGE
|
||||
powershell -ExecutionPolicy Bypass -File ".\project-audit\scripts\jira-export.ps1"
|
||||
#>
|
||||
|
||||
$JiraUrl = "https://arija.jaas.service.deutschebahn.com"
|
||||
$ProjectKey = "O2CBS"
|
||||
$ApiBase = "$JiraUrl/rest"
|
||||
$OutputDir = "project-audit\data\jira-export"
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
Write-Host "============================================================"
|
||||
Write-Host " pathOS Jira Export - Projekt $ProjectKey"
|
||||
Write-Host "============================================================"
|
||||
Write-Host ""
|
||||
|
||||
$Token = Read-Host "Jira PAT (Personal Access Token)"
|
||||
$Headers = @{ "Authorization" = "Bearer $Token"; "Content-Type" = "application/json" }
|
||||
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
|
||||
function Invoke-JiraApi {
|
||||
param([string]$Uri)
|
||||
try {
|
||||
$Resp = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
return ($Resp.Content | ConvertFrom-Json)
|
||||
} catch {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
# 1. Projekt-Info
|
||||
Write-Host ">> Projekt-Info..." -ForegroundColor Yellow
|
||||
$Project = Invoke-JiraApi "$ApiBase/api/2/project/$ProjectKey"
|
||||
if ($null -eq $Project) { Write-Host "Verbindung fehlgeschlagen. Token pruefen." -ForegroundColor Red; exit 1 }
|
||||
Write-Host " Projekt: $($Project.name) ($($Project.key))" -ForegroundColor Green
|
||||
|
||||
# 2. Boards finden
|
||||
Write-Host ">> Boards suchen..." -ForegroundColor Yellow
|
||||
$Boards = Invoke-JiraApi "$ApiBase/agile/1.0/board?projectKeyOrId=$ProjectKey&maxResults=50"
|
||||
if ($Boards -and $Boards.values) {
|
||||
Write-Host " $($Boards.values.Count) Boards gefunden" -ForegroundColor Green
|
||||
$Boards.values | ForEach-Object { Write-Host " - $($_.name) (ID: $($_.id), Typ: $($_.type))" }
|
||||
$Boards | ConvertTo-Json -Depth 5 | Out-File "$OutputDir\boards.json" -Encoding UTF8
|
||||
}
|
||||
|
||||
# 3. Aktuelle Sprints pro Board
|
||||
Write-Host ">> Sprints laden..." -ForegroundColor Yellow
|
||||
$AllSprints = @()
|
||||
if ($Boards -and $Boards.values) {
|
||||
foreach ($Board in $Boards.values) {
|
||||
$Sprints = Invoke-JiraApi "$ApiBase/agile/1.0/board/$($Board.id)/sprint?state=active,closed&maxResults=10"
|
||||
if ($Sprints -and $Sprints.values) {
|
||||
foreach ($S in $Sprints.values) {
|
||||
$S | Add-Member -NotePropertyName "boardName" -NotePropertyValue $Board.name -Force
|
||||
$S | Add-Member -NotePropertyName "boardId" -NotePropertyValue $Board.id -Force
|
||||
$AllSprints += $S
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Write-Host " $($AllSprints.Count) Sprints gefunden" -ForegroundColor Green
|
||||
$AllSprints | ConvertTo-Json -Depth 5 | Out-File "$OutputDir\sprints.json" -Encoding UTF8
|
||||
|
||||
# 4. Backlog-Items mit Team-Labels (JQL)
|
||||
Write-Host ">> Backlog-Items laden (letzte 2 PIs)..." -ForegroundColor Yellow
|
||||
$Jql = "project = $ProjectKey AND updated >= -90d ORDER BY updated DESC"
|
||||
$StartAt = 0
|
||||
$AllIssues = @()
|
||||
do {
|
||||
$SearchUri = "$ApiBase/api/2/search?jql=$([Uri]::EscapeDataString($Jql))&startAt=$StartAt&maxResults=100&fields=summary,status,issuetype,assignee,labels,components,fixVersions,priority,created,updated,customfield_10100"
|
||||
$Result = Invoke-JiraApi $SearchUri
|
||||
if ($null -eq $Result) { break }
|
||||
$AllIssues += $Result.issues
|
||||
$StartAt += 100
|
||||
Write-Host " $($AllIssues.Count) / $($Result.total) Issues geladen..."
|
||||
} while ($AllIssues.Count -lt $Result.total -and $AllIssues.Count -lt 1000)
|
||||
|
||||
Write-Host " $($AllIssues.Count) Issues geladen" -ForegroundColor Green
|
||||
$AllIssues | ConvertTo-Json -Depth 10 | Out-File "$OutputDir\issues.json" -Encoding UTF8
|
||||
|
||||
# 5. Komponenten (= Teams?)
|
||||
Write-Host ">> Komponenten laden..." -ForegroundColor Yellow
|
||||
$Components = Invoke-JiraApi "$ApiBase/api/2/project/$ProjectKey/components"
|
||||
if ($Components) {
|
||||
Write-Host " $($Components.Count) Komponenten" -ForegroundColor Green
|
||||
$Components | ForEach-Object { Write-Host " - $($_.name) (Lead: $(if ($_.lead) { $_.lead.displayName } else { '-' }))" }
|
||||
$Components | ConvertTo-Json -Depth 5 | Out-File "$OutputDir\components.json" -Encoding UTF8
|
||||
}
|
||||
|
||||
# 6. Versionen (Releases)
|
||||
Write-Host ">> Versionen laden..." -ForegroundColor Yellow
|
||||
$Versions = Invoke-JiraApi "$ApiBase/api/2/project/$ProjectKey/versions"
|
||||
if ($Versions) {
|
||||
$RecentVersions = $Versions | Where-Object { $_.released -eq $true -or $_.archived -eq $false } | Sort-Object -Property releaseDate -Descending | Select-Object -First 20
|
||||
Write-Host " $($Versions.Count) Versionen ($($RecentVersions.Count) aktuelle)" -ForegroundColor Green
|
||||
$Versions | ConvertTo-Json -Depth 5 | Out-File "$OutputDir\versions.json" -Encoding UTF8
|
||||
}
|
||||
|
||||
# 7. Analyse: Issues nach Team/Komponente
|
||||
Write-Host ""
|
||||
Write-Host ">> Analyse..." -ForegroundColor Yellow
|
||||
|
||||
$ByType = $AllIssues | Group-Object { $_.fields.issuetype.name }
|
||||
$ByStatus = $AllIssues | Group-Object { $_.fields.status.name }
|
||||
$ByComponent = @{}
|
||||
foreach ($Issue in $AllIssues) {
|
||||
if ($Issue.fields.components) {
|
||||
foreach ($Comp in $Issue.fields.components) {
|
||||
$ByComponent[$Comp.name] = ($ByComponent[$Comp.name]) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
$ByLabel = @{}
|
||||
foreach ($Issue in $AllIssues) {
|
||||
if ($Issue.fields.labels) {
|
||||
foreach ($Label in $Issue.fields.labels) {
|
||||
$ByLabel[$Label] = ($ByLabel[$Label]) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Ergebnis-Markdown
|
||||
$Md = @()
|
||||
$Md += "# Jira-Analyse pathOS ($ProjectKey)"
|
||||
$Md += ""
|
||||
$Md += "> Exportiert: $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
|
||||
$Md += "> Issues (letzte 90 Tage): $($AllIssues.Count)"
|
||||
$Md += ""
|
||||
$Md += "## Issues nach Typ"
|
||||
foreach ($G in ($ByType | Sort-Object Count -Descending)) { $Md += "- $($G.Name): $($G.Count)" }
|
||||
$Md += ""
|
||||
$Md += "## Issues nach Status"
|
||||
foreach ($G in ($ByStatus | Sort-Object Count -Descending)) { $Md += "- $($G.Name): $($G.Count)" }
|
||||
$Md += ""
|
||||
$Md += "## Issues nach Komponente (Team)"
|
||||
foreach ($E in ($ByComponent.GetEnumerator() | Sort-Object Value -Descending)) { $Md += "- $($E.Key): $($E.Value)" }
|
||||
$Md += ""
|
||||
$Md += "## Issues nach Label"
|
||||
foreach ($E in ($ByLabel.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 30)) { $Md += "- $($E.Key): $($E.Value)" }
|
||||
$Md += ""
|
||||
$Md += "## Boards"
|
||||
if ($Boards -and $Boards.values) { foreach ($B in $Boards.values) { $Md += "- $($B.name) (ID: $($B.id), Typ: $($B.type))" } }
|
||||
$Md += ""
|
||||
$Md += "## Aktuelle Sprints"
|
||||
foreach ($S in ($AllSprints | Sort-Object startDate -Descending | Select-Object -First 10)) {
|
||||
$Md += "- $($S.name) | Board: $($S.boardName) | Status: $($S.state) | $($S.startDate) - $($S.endDate)"
|
||||
}
|
||||
$Md += ""
|
||||
$Md += "## Letzte Releases"
|
||||
if ($RecentVersions) { foreach ($V in $RecentVersions) { $Md += "- $($V.name) | Released: $($V.released) | Datum: $($V.releaseDate)" } }
|
||||
|
||||
$Md -join "`n" | Out-File "$OutputDir\jira-analyse.md" -Encoding UTF8
|
||||
|
||||
# CSV fuer Issues
|
||||
$CsvData = $AllIssues | ForEach-Object {
|
||||
$Comps = if ($_.fields.components) { ($_.fields.components | ForEach-Object { $_.name }) -join ", " } else { "" }
|
||||
$Labels = if ($_.fields.labels) { $_.fields.labels -join ", " } else { "" }
|
||||
$Fix = if ($_.fields.fixVersions) { ($_.fields.fixVersions | ForEach-Object { $_.name }) -join ", " } else { "" }
|
||||
[PSCustomObject]@{
|
||||
Key = $_.key
|
||||
Typ = $_.fields.issuetype.name
|
||||
Status = $_.fields.status.name
|
||||
Prioritaet = if ($_.fields.priority) { $_.fields.priority.name } else { "" }
|
||||
Zusammenfassung = $_.fields.summary
|
||||
Komponenten = $Comps
|
||||
Labels = $Labels
|
||||
FixVersion = $Fix
|
||||
Assignee = if ($_.fields.assignee) { $_.fields.assignee.displayName } else { "" }
|
||||
Erstellt = $_.fields.created
|
||||
Aktualisiert = $_.fields.updated
|
||||
}
|
||||
}
|
||||
$CsvData | Export-Csv "$OutputDir\issues.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
Write-Host " FERTIG! Ergebnisse in $OutputDir" -ForegroundColor Green
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
@@ -0,0 +1,138 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Findet das aelteste und neueste Ticket pro Person (Tenure-Analyse)
|
||||
.NOTES
|
||||
BULK-Ansatz: Nur 2-3 API-Calls statt 70+
|
||||
Holt ALLE Issues der relevanten Projekte und gruppiert lokal nach Assignee
|
||||
#>
|
||||
|
||||
$JiraUrl = "https://arija.jaas.service.deutschebahn.com"
|
||||
$ApiBase = "$JiraUrl/rest"
|
||||
$OutputDir = "project-audit\data\jira-bugs"
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
Write-Host "============================================================"
|
||||
Write-Host " Tenure-Analyse (Bulk-Abfrage)"
|
||||
Write-Host " Strategie: Alle Issues holen, lokal nach Assignee gruppieren"
|
||||
Write-Host "============================================================"
|
||||
|
||||
$Token = $null
|
||||
$SecretsPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "..\.secrets"
|
||||
if (Test-Path $SecretsPath) {
|
||||
Get-Content $SecretsPath | ForEach-Object {
|
||||
if ($_ -match "^JIRA_TOKEN=(.+)$") {
|
||||
$val = $Matches[1].Trim()
|
||||
if ($val -ne "HIER_TOKEN_EINTRAGEN") { $Token = $val }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $Token) { $Token = Read-Host "Jira PAT" }
|
||||
$Headers = @{ "Authorization" = "Bearer $Token"; "Content-Type" = "application/json" }
|
||||
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
|
||||
function Invoke-JiraBulk {
|
||||
param([string]$Jql, [int]$MaxTotal = 5000)
|
||||
$AllIssues = @()
|
||||
$StartAt = 0
|
||||
$PageSize = 1000
|
||||
do {
|
||||
$Uri = "$ApiBase/api/2/search?jql=$([Uri]::EscapeDataString($Jql))&startAt=$StartAt&maxResults=$PageSize&fields=assignee,created,updated,project"
|
||||
Write-Host " Lade Issues $StartAt - $($StartAt + $PageSize)..." -NoNewline
|
||||
try {
|
||||
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$Data = $R.Content | ConvertFrom-Json
|
||||
$AllIssues += $Data.issues
|
||||
$Total = $Data.total
|
||||
Write-Host " $($AllIssues.Count)/$Total" -ForegroundColor Green
|
||||
$StartAt += $PageSize
|
||||
Start-Sleep -Seconds 2
|
||||
} catch {
|
||||
$StatusCode = 0
|
||||
if ($_.Exception.Response) { $StatusCode = [int]$_.Exception.Response.StatusCode }
|
||||
if ($StatusCode -eq 429) {
|
||||
Write-Host " [429 - warte 30s]" -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds 30
|
||||
} else {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
break
|
||||
}
|
||||
}
|
||||
} while ($AllIssues.Count -lt $Total -and $AllIssues.Count -lt $MaxTotal)
|
||||
return $AllIssues
|
||||
}
|
||||
|
||||
# EINE Bulk-Abfrage: Alle Issues aus allen Team-Projekten
|
||||
$Projects = "O2C404, O2CCIB, O2CZERO, O2COS, O2CDEVOPS, O2CSYS, O2CBS, O2CQST, TTTI"
|
||||
$Jql = "project in ($Projects) ORDER BY created ASC"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host ">> Bulk-Abfrage: Alle Issues aus $Projects" -ForegroundColor Yellow
|
||||
$AllIssues = Invoke-JiraBulk -Jql $Jql -MaxTotal 25000
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " Gesamt: $($AllIssues.Count) Issues geladen" -ForegroundColor Cyan
|
||||
|
||||
# Lokal nach Assignee gruppieren
|
||||
Write-Host ""
|
||||
Write-Host ">> Gruppiere nach Assignee..." -ForegroundColor Yellow
|
||||
|
||||
$ByAssignee = @{}
|
||||
foreach ($Issue in $AllIssues) {
|
||||
if ($Issue.fields.assignee) {
|
||||
$Name = $Issue.fields.assignee.displayName
|
||||
if (-not $ByAssignee[$Name]) {
|
||||
$ByAssignee[$Name] = @{ Issues = @(); FirstCreated = $null; LastUpdated = $null }
|
||||
}
|
||||
$Created = $Issue.fields.created.Substring(0, 10)
|
||||
$Updated = $Issue.fields.updated.Substring(0, 10)
|
||||
$ByAssignee[$Name].Issues += $Issue.key
|
||||
if (-not $ByAssignee[$Name].FirstCreated -or $Created -lt $ByAssignee[$Name].FirstCreated) {
|
||||
$ByAssignee[$Name].FirstCreated = $Created
|
||||
$ByAssignee[$Name].FirstTicket = $Issue.key
|
||||
$ByAssignee[$Name].FirstProject = $Issue.fields.project.key
|
||||
}
|
||||
if (-not $ByAssignee[$Name].LastUpdated -or $Updated -gt $ByAssignee[$Name].LastUpdated) {
|
||||
$ByAssignee[$Name].LastUpdated = $Updated
|
||||
$ByAssignee[$Name].LastTicket = $Issue.key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host " $($ByAssignee.Count) verschiedene Assignees gefunden"
|
||||
|
||||
# Ergebnisse aufbereiten
|
||||
$Results = @()
|
||||
foreach ($Entry in ($ByAssignee.GetEnumerator() | Sort-Object { $_.Value.FirstCreated })) {
|
||||
$Name = $Entry.Key
|
||||
$Data = $Entry.Value
|
||||
$DaysAgo = ((Get-Date) - [DateTime]::Parse($Data.LastUpdated)).Days
|
||||
$Status = if ($DaysAgo -le 30) { "Aktiv" } elseif ($DaysAgo -le 90) { "Wenig aktiv" } else { "INAKTIV ($DaysAgo Tage)" }
|
||||
|
||||
$Results += [PSCustomObject]@{
|
||||
Person = $Name
|
||||
FirstTicket = $Data.FirstTicket
|
||||
FirstDate = $Data.FirstCreated
|
||||
LastDate = $Data.LastUpdated
|
||||
LastTicket = $Data.LastTicket
|
||||
Status = $Status
|
||||
Project = $Data.FirstProject
|
||||
IssueCount = $Data.Issues.Count
|
||||
}
|
||||
}
|
||||
|
||||
# CSV speichern
|
||||
$Results | Export-Csv "$OutputDir\tenure.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
|
||||
|
||||
# Ausgabe
|
||||
Write-Host ""
|
||||
Write-Host "=== Sortiert nach Startdatum (Top 40) ==="
|
||||
$Results | Select-Object -First 40 | ForEach-Object {
|
||||
$Color = if ($_.Status -eq "Aktiv") { "Green" } elseif ($_.Status -match "INAKTIV") { "Red" } else { "Yellow" }
|
||||
Write-Host (" {0,-35} {1} bis {2} [{3}] ({4} Issues)" -f $_.Person, $_.FirstDate, $_.LastDate, $_.Status, $_.IssueCount) -ForegroundColor $Color
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "FERTIG! $($Results.Count) Personen in $OutputDir\tenure.csv" -ForegroundColor Green
|
||||
Write-Host "API-Calls: ~$([Math]::Ceiling($AllIssues.Count / 1000) + 1) (statt $($ByAssignee.Count * 2))" -ForegroundColor Cyan
|
||||
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
Sucht nach den drei kritischen Themen in Jira:
|
||||
1. Implizite Annahme NAÄ (Netzausgeloeste Aenderungen)
|
||||
2. 20h Zug
|
||||
3. Abrechnung / AC Trasse / TTT
|
||||
"""
|
||||
import urllib.request, json, ssl
|
||||
from pathlib import Path
|
||||
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
TOKEN = secrets.get("JIRA_TOKEN", "")
|
||||
if not TOKEN:
|
||||
print("FEHLER: JIRA_TOKEN nicht in .secrets!")
|
||||
exit(1)
|
||||
|
||||
JIRA_URL = "https://arija.jaas.service.deutschebahn.com"
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
def jira_search(jql, max_results=50):
|
||||
url = f"{JIRA_URL}/rest/api/2/search?jql={urllib.parse.quote(jql)}&maxResults={max_results}&fields=key,summary,status,priority,assignee,created,updated,issuetype,labels"
|
||||
req = urllib.request.Request(url)
|
||||
req.add_header("Authorization", f"Bearer {TOKEN}")
|
||||
req.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except Exception as e:
|
||||
print(f" Fehler: {e}")
|
||||
return None
|
||||
|
||||
import urllib.parse
|
||||
|
||||
print("=" * 80)
|
||||
print("KRITISCHE THEMEN - Jira-Suche")
|
||||
print("=" * 80)
|
||||
|
||||
# 1. Implizite Annahme / NAÄ
|
||||
print("\n--- 1. Implizite Annahme / NAÄ / Netzausgelöste Änderungen ---")
|
||||
queries_naa = [
|
||||
'text ~ "implizite Annahme" ORDER BY updated DESC',
|
||||
'text ~ "Netzausgelöste" ORDER BY updated DESC',
|
||||
'text ~ "NAÄ" ORDER BY updated DESC',
|
||||
'summary ~ "implizit" AND summary ~ "Annahme" ORDER BY updated DESC',
|
||||
]
|
||||
naa_issues = []
|
||||
for jql in queries_naa:
|
||||
result = jira_search(jql, 20)
|
||||
if result and result.get("issues"):
|
||||
for issue in result["issues"]:
|
||||
key = issue["key"]
|
||||
if key not in [i["key"] for i in naa_issues]:
|
||||
naa_issues.append(issue)
|
||||
|
||||
print(f" Gefunden: {len(naa_issues)} Issues")
|
||||
for issue in naa_issues[:15]:
|
||||
fields = issue["fields"]
|
||||
status = fields["status"]["name"]
|
||||
prio = fields.get("priority", {}).get("name", "?")
|
||||
assignee = fields.get("assignee", {})
|
||||
assignee_name = assignee.get("displayName", "Unassigned") if assignee else "Unassigned"
|
||||
print(f" {issue['key']:<15} [{status:<12}] {prio:<8} {fields['summary'][:60]}")
|
||||
print(f" {'':15} Assignee: {assignee_name} Updated: {fields['updated'][:10]}")
|
||||
|
||||
# 2. 20h Zug / TTTSol
|
||||
print("\n--- 2. 20h Zug ---")
|
||||
queries_20h = [
|
||||
'text ~ "20h Zug" ORDER BY updated DESC',
|
||||
'text ~ "20-Stunden" ORDER BY updated DESC',
|
||||
'project = TTTSol AND text ~ "20h" ORDER BY updated DESC',
|
||||
'text ~ "20h" AND text ~ "Zug" ORDER BY updated DESC',
|
||||
]
|
||||
zug_issues = []
|
||||
for jql in queries_20h:
|
||||
result = jira_search(jql, 20)
|
||||
if result and result.get("issues"):
|
||||
for issue in result["issues"]:
|
||||
key = issue["key"]
|
||||
if key not in [i["key"] for i in zug_issues]:
|
||||
zug_issues.append(issue)
|
||||
|
||||
print(f" Gefunden: {len(zug_issues)} Issues")
|
||||
for issue in zug_issues[:15]:
|
||||
fields = issue["fields"]
|
||||
status = fields["status"]["name"]
|
||||
prio = fields.get("priority", {}).get("name", "?")
|
||||
assignee = fields.get("assignee", {})
|
||||
assignee_name = assignee.get("displayName", "Unassigned") if assignee else "Unassigned"
|
||||
print(f" {issue['key']:<15} [{status:<12}] {prio:<8} {fields['summary'][:60]}")
|
||||
print(f" {'':15} Assignee: {assignee_name} Updated: {fields['updated'][:10]}")
|
||||
|
||||
# 3. Abrechnung / AC Trasse
|
||||
print("\n--- 3. Abrechnung / AC Trasse ---")
|
||||
queries_abr = [
|
||||
'text ~ "Abrechnung" AND (project = TTTSol OR project = TTTI) AND status != Done ORDER BY priority DESC',
|
||||
'text ~ "AC Trasse" ORDER BY updated DESC',
|
||||
'summary ~ "Abrechnung" AND status != Done ORDER BY priority DESC',
|
||||
]
|
||||
abr_issues = []
|
||||
for jql in queries_abr:
|
||||
result = jira_search(jql, 30)
|
||||
if result and result.get("issues"):
|
||||
for issue in result["issues"]:
|
||||
key = issue["key"]
|
||||
if key not in [i["key"] for i in abr_issues]:
|
||||
abr_issues.append(issue)
|
||||
|
||||
print(f" Gefunden: {len(abr_issues)} Issues")
|
||||
for issue in abr_issues[:20]:
|
||||
fields = issue["fields"]
|
||||
status = fields["status"]["name"]
|
||||
prio = fields.get("priority", {}).get("name", "?")
|
||||
assignee = fields.get("assignee", {})
|
||||
assignee_name = assignee.get("displayName", "Unassigned") if assignee else "Unassigned"
|
||||
labels = ", ".join(fields.get("labels", []))
|
||||
print(f" {issue['key']:<15} [{status:<12}] {prio:<8} {fields['summary'][:60]}")
|
||||
if labels:
|
||||
print(f" {'':15} Labels: {labels}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("ZUSAMMENFASSUNG")
|
||||
print("=" * 80)
|
||||
print(f" NAÄ/Implizite Annahme: {len(naa_issues)} Issues")
|
||||
print(f" 20h Zug: {len(zug_issues)} Issues")
|
||||
print(f" Abrechnung/AC Trasse: {len(abr_issues)} Issues")
|
||||
@@ -0,0 +1,149 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Exportiert Jira-Daten aller pathOS Team-Boards
|
||||
#>
|
||||
|
||||
$JiraUrl = "https://arija.jaas.service.deutschebahn.com"
|
||||
$ApiBase = "$JiraUrl/rest"
|
||||
$OutputDir = "project-audit\data\jira-teams"
|
||||
$TeamProjects = @(
|
||||
@{ key = "O2C404"; name = "Team 404 (Portal)" }
|
||||
@{ key = "O2CCIB"; name = "Team CIB (Prozesse/Backend)" }
|
||||
@{ key = "O2CZERO"; name = "Team Zero (TAF/TAP)" }
|
||||
@{ key = "O2COS"; name = "OPs Squad (Infrastruktur)" }
|
||||
@{ key = "O2CSYS"; name = "STeam/System (ehem.)" }
|
||||
@{ key = "O2CQST"; name = "QA/Test" }
|
||||
@{ key = "O2CDEVOPS"; name = "DevOps" }
|
||||
)
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
Write-Host "============================================================"
|
||||
Write-Host " pathOS Team-Boards Jira Export"
|
||||
Write-Host "============================================================"
|
||||
|
||||
$Token = Read-Host "Jira PAT"
|
||||
$Headers = @{ "Authorization" = "Bearer $Token"; "Content-Type" = "application/json" }
|
||||
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
|
||||
function Invoke-JiraApi($Uri) {
|
||||
try {
|
||||
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
return ($R.Content | ConvertFrom-Json)
|
||||
} catch {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
$Summary = @()
|
||||
|
||||
foreach ($Team in $TeamProjects) {
|
||||
$Key = $Team.key
|
||||
$Name = $Team.name
|
||||
Write-Host ""
|
||||
Write-Host ">> $Name ($Key)" -ForegroundColor Yellow
|
||||
|
||||
# Projekt-Info
|
||||
$Proj = Invoke-JiraApi "$ApiBase/api/2/project/$Key"
|
||||
if ($null -eq $Proj) { Write-Host " Nicht gefunden oder kein Zugriff" -ForegroundColor Red; continue }
|
||||
Write-Host " Projekt: $($Proj.name)"
|
||||
|
||||
# Issues (letzte 90 Tage)
|
||||
$Jql = "project = $Key AND updated >= -90d ORDER BY updated DESC"
|
||||
$Issues = @()
|
||||
$Start = 0
|
||||
do {
|
||||
$Uri = "$ApiBase/api/2/search?jql=$([Uri]::EscapeDataString($Jql))&startAt=$Start&maxResults=100&fields=summary,status,issuetype,assignee,labels,components,fixVersions,priority,created,updated,sprint"
|
||||
$Result = Invoke-JiraApi $Uri
|
||||
if ($null -eq $Result) { break }
|
||||
$Issues += $Result.issues
|
||||
$Start += 100
|
||||
} while ($Issues.Count -lt $Result.total -and $Issues.Count -lt 500)
|
||||
Write-Host " $($Issues.Count) Issues (90 Tage)"
|
||||
|
||||
# Analyse
|
||||
$ByType = $Issues | Group-Object { $_.fields.issuetype.name }
|
||||
$ByStatus = $Issues | Group-Object { $_.fields.status.name }
|
||||
|
||||
$Done = ($Issues | Where-Object { $_.fields.status.name -eq "Fertig" -or $_.fields.status.name -match "Done|Closed|Resolved" }).Count
|
||||
$InProgress = ($Issues | Where-Object { $_.fields.status.name -match "Implementation|In Bearbeitung|In Progress|Review|Validierung" }).Count
|
||||
$Open = $Issues.Count - $Done - $InProgress
|
||||
|
||||
$TeamInfo = [PSCustomObject]@{
|
||||
Team = $Name
|
||||
Key = $Key
|
||||
Total = $Issues.Count
|
||||
Done = $Done
|
||||
InProgress = $InProgress
|
||||
Open = $Open
|
||||
Features = ($ByType | Where-Object { $_.Name -eq "Feature" }).Count
|
||||
Enabler = ($ByType | Where-Object { $_.Name -eq "Enabler" }).Count
|
||||
Bugs = ($ByType | Where-Object { $_.Name -match "Bug|Fehler" }).Count
|
||||
Tasks = ($ByType | Where-Object { $_.Name -eq "Aufgabe" }).Count
|
||||
}
|
||||
$Summary += $TeamInfo
|
||||
|
||||
Write-Host " Fertig: $Done | In Arbeit: $InProgress | Offen: $Open"
|
||||
|
||||
# Issues als CSV speichern
|
||||
$CsvData = $Issues | ForEach-Object {
|
||||
[PSCustomObject]@{
|
||||
Key = $_.key
|
||||
Typ = $_.fields.issuetype.name
|
||||
Status = $_.fields.status.name
|
||||
Prioritaet = if ($_.fields.priority) { $_.fields.priority.name } else { "" }
|
||||
Zusammenfassung = $_.fields.summary
|
||||
Assignee = if ($_.fields.assignee) { $_.fields.assignee.displayName } else { "" }
|
||||
Labels = if ($_.fields.labels) { $_.fields.labels -join ", " } else { "" }
|
||||
Erstellt = $_.fields.created
|
||||
Aktualisiert = $_.fields.updated
|
||||
}
|
||||
}
|
||||
$CsvData | Export-Csv "$OutputDir\$Key-issues.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
|
||||
$Issues | ConvertTo-Json -Depth 10 | Out-File "$OutputDir\$Key-issues.json" -Encoding UTF8
|
||||
}
|
||||
|
||||
# Zusammenfassung
|
||||
Write-Host ""
|
||||
Write-Host ">> Zusammenfassung" -ForegroundColor Yellow
|
||||
$Summary | Format-Table -AutoSize
|
||||
|
||||
$Md = @()
|
||||
$Md += "# Jira Team-Boards Analyse pathOS"
|
||||
$Md += ""
|
||||
$Md += "> Exportiert: $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
|
||||
$Md += ""
|
||||
$Md += "| Team | Key | Issues (90d) | Fertig | In Arbeit | Offen | Features | Enabler | Bugs | Tasks |"
|
||||
$Md += "|------|-----|-------------|--------|-----------|-------|----------|---------|------|-------|"
|
||||
foreach ($T in $Summary) {
|
||||
$Md += "| $($T.Team) | $($T.Key) | $($T.Total) | $($T.Done) | $($T.InProgress) | $($T.Open) | $($T.Features) | $($T.Enabler) | $($T.Bugs) | $($T.Tasks) |"
|
||||
}
|
||||
$Md += ""
|
||||
|
||||
# Assignee-Analyse (wer arbeitet wo)
|
||||
$Md += "## Assignees pro Team"
|
||||
foreach ($Team in $TeamProjects) {
|
||||
$Key = $Team.key
|
||||
$JsonPath = "$OutputDir\$Key-issues.json"
|
||||
if (Test-Path $JsonPath) {
|
||||
$Issues = Get-Content $JsonPath -Raw | ConvertFrom-Json
|
||||
$Assignees = $Issues | Where-Object { $_.fields.assignee } | Group-Object { $_.fields.assignee.displayName } | Sort-Object Count -Descending
|
||||
if ($Assignees.Count -gt 0) {
|
||||
$Md += ""
|
||||
$Md += "### $($Team.name) ($Key)"
|
||||
foreach ($A in $Assignees) {
|
||||
$Md += "- $($A.Name): $($A.Count) Issues"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$Md -join "`n" | Out-File "$OutputDir\team-summary.md" -Encoding UTF8
|
||||
$Summary | Export-Csv "$OutputDir\team-summary.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
Write-Host " FERTIG! Ergebnisse in $OutputDir" -ForegroundColor Green
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
@@ -0,0 +1,88 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Test-Script fuer Jira API Queries - Debugging
|
||||
#>
|
||||
|
||||
$JiraUrl = "https://arija.jaas.service.deutschebahn.com"
|
||||
$ApiBase = "$JiraUrl/rest"
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
$Token = Read-Host "Jira PAT"
|
||||
$Headers = @{ "Authorization" = "Bearer $Token"; "Content-Type" = "application/json" }
|
||||
|
||||
Write-Host "=== Test 1: Einfacher Name ==="
|
||||
$Jql = "assignee = 'Ana Cvitkovic' ORDER BY created ASC"
|
||||
$EncodedJql = [Uri]::EscapeDataString($Jql)
|
||||
$Uri = "$ApiBase/api/2/search?jql=$EncodedJql&maxResults=1&fields=created,summary"
|
||||
Write-Host " JQL: $Jql"
|
||||
Write-Host " URI: $Uri"
|
||||
try {
|
||||
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$Data = $R.Content | ConvertFrom-Json
|
||||
Write-Host " OK: $($Data.total) Treffer" -ForegroundColor Green
|
||||
if ($Data.issues.Count -gt 0) {
|
||||
Write-Host " Erstes: $($Data.issues[0].key) - $($Data.issues[0].fields.created)"
|
||||
}
|
||||
} catch {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
if ($_.Exception.Response) {
|
||||
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
|
||||
$body = $reader.ReadToEnd()
|
||||
Write-Host " Response: $($body.Substring(0, [Math]::Min(500, $body.Length)))" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Test 2: Name mit Bindestrich ==="
|
||||
$Jql2 = "assignee = 'Dong-Won Han' ORDER BY created ASC"
|
||||
$EncodedJql2 = [Uri]::EscapeDataString($Jql2)
|
||||
$Uri2 = "$ApiBase/api/2/search?jql=$EncodedJql2&maxResults=1&fields=created,summary"
|
||||
Write-Host " JQL: $Jql2"
|
||||
try {
|
||||
$R2 = Invoke-WebRequest -Uri $Uri2 -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$Data2 = $R2.Content | ConvertFrom-Json
|
||||
Write-Host " OK: $($Data2.total) Treffer" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
if ($_.Exception.Response) {
|
||||
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
|
||||
$body = $reader.ReadToEnd()
|
||||
Write-Host " Response: $($body.Substring(0, [Math]::Min(500, $body.Length)))" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Test 3: Einfache Suche ohne Assignee ==="
|
||||
$Jql3 = "project = O2C404 ORDER BY created ASC"
|
||||
$EncodedJql3 = [Uri]::EscapeDataString($Jql3)
|
||||
$Uri3 = "$ApiBase/api/2/search?jql=$EncodedJql3&maxResults=1&fields=created,summary,assignee"
|
||||
Write-Host " JQL: $Jql3"
|
||||
try {
|
||||
$R3 = Invoke-WebRequest -Uri $Uri3 -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$Data3 = $R3.Content | ConvertFrom-Json
|
||||
Write-Host " OK: $($Data3.total) Treffer" -ForegroundColor Green
|
||||
if ($Data3.issues.Count -gt 0) {
|
||||
$Assignee = if ($Data3.issues[0].fields.assignee) { $Data3.issues[0].fields.assignee.displayName } else { "unassigned" }
|
||||
Write-Host " Erstes: $($Data3.issues[0].key) - Assignee: $Assignee"
|
||||
}
|
||||
} catch {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
if ($_.Exception.Response) {
|
||||
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
|
||||
$body = $reader.ReadToEnd()
|
||||
Write-Host " Response: $($body.Substring(0, [Math]::Min(500, $body.Length)))" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Test 4: currentUser ==="
|
||||
$Jql4 = "assignee = currentUser() ORDER BY created ASC"
|
||||
$Uri4 = "$ApiBase/api/2/search?jql=$([Uri]::EscapeDataString($Jql4))&maxResults=1&fields=created,summary"
|
||||
try {
|
||||
$R4 = Invoke-WebRequest -Uri $Uri4 -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
$Data4 = $R4.Content | ConvertFrom-Json
|
||||
Write-Host " OK: $($Data4.total) Treffer" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Velocity-Analyse ueber 12 Monate fuer alle pathOS Teams
|
||||
#>
|
||||
|
||||
$JiraUrl = "https://arija.jaas.service.deutschebahn.com"
|
||||
$ApiBase = "$JiraUrl/rest"
|
||||
$OutputDir = "project-audit\data\jira-velocity"
|
||||
|
||||
$TeamProjects = @(
|
||||
@{ key = "O2C404"; name = "Team 404" }
|
||||
@{ key = "O2CCIB"; name = "Team CIB" }
|
||||
@{ key = "O2CZERO"; name = "Team Zero" }
|
||||
@{ key = "O2COS"; name = "OPs Squad" }
|
||||
@{ key = "O2CDEVOPS"; name = "DevOps" }
|
||||
)
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
Write-Host "============================================================"
|
||||
Write-Host " pathOS Velocity-Analyse (12 Monate)"
|
||||
Write-Host "============================================================"
|
||||
|
||||
$Token = Read-Host "Jira PAT"
|
||||
$Headers = @{ "Authorization" = "Bearer $Token"; "Content-Type" = "application/json" }
|
||||
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
|
||||
function Invoke-JiraApi($Uri) {
|
||||
try {
|
||||
$R = Invoke-WebRequest -Uri $Uri -Headers $Headers -UseBasicParsing -ErrorAction Stop
|
||||
return ($R.Content | ConvertFrom-Json)
|
||||
} catch { return $null }
|
||||
}
|
||||
|
||||
$AllResults = @()
|
||||
|
||||
foreach ($Team in $TeamProjects) {
|
||||
$Key = $Team.key
|
||||
$Name = $Team.name
|
||||
Write-Host ""
|
||||
Write-Host ">> $Name ($Key)" -ForegroundColor Yellow
|
||||
|
||||
# Alle resolved/done Issues der letzten 12 Monate
|
||||
$Jql = "project = $Key AND status in (Fertig, Done, Closed, Resolved) AND resolved >= -365d ORDER BY resolved ASC"
|
||||
$Issues = @()
|
||||
$Start = 0
|
||||
do {
|
||||
$Uri = "$ApiBase/api/2/search?jql=$([Uri]::EscapeDataString($Jql))&startAt=$Start&maxResults=100&fields=summary,status,issuetype,resolutiondate,created"
|
||||
$Result = Invoke-JiraApi $Uri
|
||||
if ($null -eq $Result) { break }
|
||||
$Issues += $Result.issues
|
||||
$Start += 100
|
||||
Write-Host " $($Issues.Count) / $($Result.total)..." -NoNewline
|
||||
} while ($Issues.Count -lt $Result.total -and $Issues.Count -lt 3000)
|
||||
Write-Host ""
|
||||
Write-Host " $($Issues.Count) resolved Issues (12 Monate)" -ForegroundColor Green
|
||||
|
||||
# Nach Monat gruppieren
|
||||
$ByMonth = @{}
|
||||
$BugsByMonth = @{}
|
||||
$EnablerByMonth = @{}
|
||||
foreach ($I in $Issues) {
|
||||
$ResDate = $I.fields.resolutiondate
|
||||
if (-not $ResDate) { $ResDate = $I.fields.created }
|
||||
if ($ResDate) {
|
||||
try {
|
||||
$Date = [DateTime]::Parse($ResDate)
|
||||
$MK = $Date.ToString("yyyy-MM")
|
||||
$ByMonth[$MK] = ($ByMonth[$MK]) + 1
|
||||
$Type = $I.fields.issuetype.name
|
||||
if ($Type -eq "Bug" -or $Type -eq "Fehler") { $BugsByMonth[$MK] = ($BugsByMonth[$MK]) + 1 }
|
||||
if ($Type -eq "Enabler") { $EnablerByMonth[$MK] = ($EnablerByMonth[$MK]) + 1 }
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
# Ergebnis sammeln
|
||||
foreach ($MK in ($ByMonth.Keys | Sort-Object)) {
|
||||
$Total = $ByMonth[$MK]
|
||||
$Bugs = if ($BugsByMonth[$MK]) { $BugsByMonth[$MK] } else { 0 }
|
||||
$Enablers = if ($EnablerByMonth[$MK]) { $EnablerByMonth[$MK] } else { 0 }
|
||||
$AllResults += [PSCustomObject]@{
|
||||
Team = $Name
|
||||
Key = $Key
|
||||
Month = $MK
|
||||
Total = $Total
|
||||
Bugs = $Bugs
|
||||
Enabler = $Enablers
|
||||
BugRate = if ($Total -gt 0) { [Math]::Round(($Bugs / $Total) * 100, 0) } else { 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# CSV speichern
|
||||
$AllResults | Export-Csv "$OutputDir\velocity-12m.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
|
||||
|
||||
# Markdown
|
||||
$Md = @()
|
||||
$Md += "# Velocity-Analyse pathOS (12 Monate)"
|
||||
$Md += ""
|
||||
$Md += "> Exportiert: $(Get-Date -Format 'yyyy-MM-dd HH:mm')"
|
||||
$Md += ""
|
||||
$Md += "| Team | Monat | Gesamt | Bugs | Enabler | Bug% |"
|
||||
$Md += "|------|-------|--------|------|---------|------|"
|
||||
foreach ($R in $AllResults) {
|
||||
$Md += "| $($R.Team) | $($R.Month) | $($R.Total) | $($R.Bugs) | $($R.Enabler) | $($R.BugRate)% |"
|
||||
}
|
||||
$Md -join "`n" | Out-File "$OutputDir\velocity-12m.md" -Encoding UTF8
|
||||
|
||||
# Zusammenfassung pro Team
|
||||
Write-Host ""
|
||||
Write-Host "=== Zusammenfassung ==="
|
||||
foreach ($Team in $TeamProjects) {
|
||||
$TeamData = $AllResults | Where-Object { $_.Key -eq $Team.key }
|
||||
$TotalAll = ($TeamData | Measure-Object -Property Total -Sum).Sum
|
||||
$TotalBugs = ($TeamData | Measure-Object -Property Bugs -Sum).Sum
|
||||
$AvgMonth = if ($TeamData.Count -gt 0) { [Math]::Round($TotalAll / $TeamData.Count, 0) } else { 0 }
|
||||
Write-Host " $($Team.name): $TotalAll Issues (12M), Avg $AvgMonth/Monat, $TotalBugs Bugs ($([Math]::Round($TotalBugs/$TotalAll*100,0))%)"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
Write-Host " FERTIG! Ergebnisse in $OutputDir" -ForegroundColor Green
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
@@ -0,0 +1,10 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Fuehrt Push und Reorganisation nacheinander aus
|
||||
#>
|
||||
Write-Host "=== Schritt 1: Push ===" -ForegroundColor Cyan
|
||||
powershell -ExecutionPolicy Bypass -File ".\project-audit\scripts\confluence-push.ps1"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Schritt 2: Reorganisation ===" -ForegroundColor Cyan
|
||||
powershell -ExecutionPolicy Bypass -File ".\project-audit\scripts\confluence-reorganize.ps1"
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Stellt den Default Branch auf master um."""
|
||||
import urllib.request, json, ssl
|
||||
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
TOKEN = secrets.get("GITLAB_TOKEN_AUDIT", "")
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
url = "https://git.tech.rz.db.de/api/v4/projects/AndreKnie%2Fproject-audit"
|
||||
data = json.dumps({"default_branch": "master"}).encode("utf-8")
|
||||
req = urllib.request.Request(url, data=data, method="PUT")
|
||||
req.add_header("PRIVATE-TOKEN", TOKEN)
|
||||
req.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=15)
|
||||
result = json.loads(resp.read().decode("utf-8"))
|
||||
print(f"Default Branch jetzt: {result.get('default_branch', '?')}")
|
||||
print("Erfolgreich umgestellt!")
|
||||
except Exception as e:
|
||||
print(f"Fehler: {e}")
|
||||
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
SonarQube-Daten analysieren: Coverage und Smells pro Service/Team.
|
||||
Filtert auf master/main Branches und ordnet Services den Teams zu.
|
||||
"""
|
||||
import csv
|
||||
from collections import defaultdict
|
||||
|
||||
CSV_PATH = "project-audit/data/sonarqube/sonarqube-metrics.csv"
|
||||
|
||||
# Team-Zuordnung der Services
|
||||
TEAM_MAP = {
|
||||
"portal-ui": "404",
|
||||
"portal-middleware": "404",
|
||||
"portal-mw": "404",
|
||||
"steuerung-vertrieb": "CIB",
|
||||
"sv-": "CIB",
|
||||
"auftrags-verwaltung": "CIB",
|
||||
"av-": "CIB",
|
||||
"auftrag-service": "CIB",
|
||||
"archivierungsservice": "CIB",
|
||||
"common-interface": "Zero",
|
||||
"ci-": "Zero",
|
||||
"tadef": "Zero",
|
||||
"stammdaten": "Zero",
|
||||
"stb-": "Zero",
|
||||
"acat": "Zero",
|
||||
"vertragsdaten": "Zero",
|
||||
"vdv": "Zero",
|
||||
"ifp": "CIB",
|
||||
"kundendaten": "CIB",
|
||||
"kdv": "CIB",
|
||||
"rabatt": "CIB",
|
||||
"core-component": "Shared",
|
||||
"signature": "Shared",
|
||||
"logging": "Shared",
|
||||
}
|
||||
|
||||
def get_team(name):
|
||||
name_lower = name.lower()
|
||||
for pattern, team in TEAM_MAP.items():
|
||||
if pattern in name_lower:
|
||||
return team
|
||||
return "Andere"
|
||||
|
||||
# Daten laden (nur master/main)
|
||||
data = []
|
||||
with open(CSV_PATH, "r", encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f, delimiter=";", quotechar='"')
|
||||
for row in reader:
|
||||
name = row.get("Name", "")
|
||||
if name.endswith("-master") or name.endswith("-main"):
|
||||
lines = int(row.get("Lines", "0") or "0")
|
||||
if lines > 0: # Nur Projekte mit Code
|
||||
data.append({
|
||||
"name": name.replace("-master", "").replace("-main", ""),
|
||||
"lines": lines,
|
||||
"bugs": int(row.get("Bugs", "0") or "0"),
|
||||
"smells": int(row.get("CodeSmells", "0") or "0"),
|
||||
"coverage": float(row.get("Coverage", "0") or "0"),
|
||||
"duplications": float(row.get("Duplications", "0") or "0"),
|
||||
"qgate": row.get("QualityGate", "?"),
|
||||
"team": get_team(name),
|
||||
})
|
||||
|
||||
# Sortiert nach Lines
|
||||
data.sort(key=lambda x: x["lines"], reverse=True)
|
||||
|
||||
print(f"=== SonarQube Analyse (nur master/main, {len(data)} Projekte mit Code) ===")
|
||||
print()
|
||||
|
||||
# Top 20 nach Groesse
|
||||
print("--- Top 20 Projekte nach Groesse ---")
|
||||
print(f"{'Projekt':<40} {'Lines':>7} {'Bugs':>5} {'Smells':>7} {'Cov%':>6} {'QGate':>6} {'Team':<8}")
|
||||
print("-" * 85)
|
||||
for row in data[:20]:
|
||||
cov_str = f"{row['coverage']:.1f}" if row['coverage'] > 0 else "-"
|
||||
print(f"{row['name']:<40} {row['lines']:>7} {row['bugs']:>5} {row['smells']:>7} {cov_str:>6} {row['qgate']:>6} {row['team']:<8}")
|
||||
|
||||
# Team-Aggregation
|
||||
print()
|
||||
print("--- Aggregation pro Team ---")
|
||||
teams = defaultdict(lambda: {"lines": 0, "bugs": 0, "smells": 0, "cov_weighted": 0, "projects": 0, "qgate_ok": 0, "qgate_err": 0})
|
||||
for row in data:
|
||||
t = teams[row["team"]]
|
||||
t["lines"] += row["lines"]
|
||||
t["bugs"] += row["bugs"]
|
||||
t["smells"] += row["smells"]
|
||||
t["cov_weighted"] += row["coverage"] * row["lines"]
|
||||
t["projects"] += 1
|
||||
if row["qgate"] == "OK":
|
||||
t["qgate_ok"] += 1
|
||||
else:
|
||||
t["qgate_err"] += 1
|
||||
|
||||
print(f"{'Team':<10} {'Projekte':>8} {'Lines':>8} {'Bugs':>5} {'Smells':>7} {'Smells/1K':>9} {'Cov%':>6} {'QGate OK':>8}")
|
||||
print("-" * 75)
|
||||
for team in ["404", "CIB", "Zero", "Shared", "Andere"]:
|
||||
if team in teams:
|
||||
t = teams[team]
|
||||
smells_per_k = (t["smells"] / t["lines"] * 1000) if t["lines"] > 0 else 0
|
||||
avg_cov = (t["cov_weighted"] / t["lines"]) if t["lines"] > 0 else 0
|
||||
qgate_str = f"{t['qgate_ok']}/{t['projects']}"
|
||||
print(f"{team:<10} {t['projects']:>8} {t['lines']:>8} {t['bugs']:>5} {t['smells']:>7} {smells_per_k:>9.1f} {avg_cov:>6.1f} {qgate_str:>8}")
|
||||
|
||||
# Projekte ohne Coverage
|
||||
print()
|
||||
print("--- Projekte OHNE Coverage (0% oder keine Daten) ---")
|
||||
no_cov = [r for r in data if r["coverage"] == 0 and r["lines"] > 500]
|
||||
no_cov.sort(key=lambda x: x["lines"], reverse=True)
|
||||
for row in no_cov:
|
||||
print(f" {row['name']:<40} {row['lines']:>7} Lines {row['team']:<8}")
|
||||
|
||||
# Quality Gate Failures
|
||||
print()
|
||||
print("--- Quality Gate FAILED (ERROR) ---")
|
||||
failed = [r for r in data if r["qgate"] == "ERROR"]
|
||||
failed.sort(key=lambda x: x["lines"], reverse=True)
|
||||
for row in failed[:15]:
|
||||
cov_str = f"{row['coverage']:.1f}%" if row['coverage'] > 0 else "0%"
|
||||
print(f" {row['name']:<40} {row['lines']:>7} Lines Bugs={row['bugs']} Smells={row['smells']} Cov={cov_str} {row['team']}")
|
||||
@@ -0,0 +1,158 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Exportiert SonarQube Metriken fuer alle pathOS Projekte
|
||||
#>
|
||||
|
||||
$SonarUrl = "https://sonarqube.build.bsz-dev.cnp-test.comp.db.de"
|
||||
$OutputDir = "project-audit\data\sonarqube"
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
Write-Host "============================================================"
|
||||
Write-Host " SonarQube Export - pathOS"
|
||||
Write-Host " URL: $SonarUrl"
|
||||
Write-Host "============================================================"
|
||||
Write-Host ""
|
||||
Write-Host " Token erstellen: $SonarUrl/account/security"
|
||||
Write-Host ""
|
||||
|
||||
$SonarToken = Read-Host "SonarQube Token"
|
||||
$SonarAuth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${SonarToken}:"))
|
||||
$SonarHeaders = @{ "Authorization" = "Basic $SonarAuth" }
|
||||
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
|
||||
# Test Authentifizierung
|
||||
Write-Host ">> Teste Authentifizierung..." -NoNewline
|
||||
try {
|
||||
$AuthTest = Invoke-WebRequest -Uri "$SonarUrl/api/authentication/validate" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
|
||||
$AuthData = $AuthTest.Content | ConvertFrom-Json
|
||||
if ($AuthData.valid) {
|
||||
Write-Host " OK (authentifiziert)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " Token ungueltig!" -ForegroundColor Red
|
||||
Write-Host " Bitte Token unter $SonarUrl/account/security neu generieren."
|
||||
Write-Host " Token-Typ: 'User Token' (nicht 'Project Token')"
|
||||
exit 1
|
||||
}
|
||||
} catch {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
Write-Host " Versuche ohne Auth (falls anonymer Zugriff erlaubt)..."
|
||||
$SonarHeaders = @{}
|
||||
}
|
||||
|
||||
# Projekte laden - verschiedene Endpunkte probieren
|
||||
Write-Host ">> Lade Projekte..." -NoNewline
|
||||
$Projects = @()
|
||||
$Page = 1
|
||||
|
||||
# Versuch 1: projects/search (braucht Admin oder Browse-Rechte)
|
||||
try {
|
||||
$R = Invoke-WebRequest -Uri "$SonarUrl/api/projects/search?ps=100&p=1" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
|
||||
$Data = $R.Content | ConvertFrom-Json
|
||||
$Projects += $Data.components
|
||||
Write-Host " OK (projects/search)" -ForegroundColor Green
|
||||
# Rest laden
|
||||
while ($Projects.Count -lt $Data.paging.total) {
|
||||
$Page++
|
||||
$R = Invoke-WebRequest -Uri "$SonarUrl/api/projects/search?ps=100&p=$Page" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
|
||||
$Data = $R.Content | ConvertFrom-Json
|
||||
$Projects += $Data.components
|
||||
}
|
||||
} catch {
|
||||
Write-Host " 403 auf projects/search, versuche components/search..." -ForegroundColor Yellow
|
||||
# Versuch 2: components/search_projects
|
||||
try {
|
||||
$R = Invoke-WebRequest -Uri "$SonarUrl/api/components/search_projects?ps=100&p=1" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
|
||||
$Data = $R.Content | ConvertFrom-Json
|
||||
$Projects += $Data.components
|
||||
Write-Host " OK (components/search_projects)" -ForegroundColor Green
|
||||
while ($Projects.Count -lt $Data.paging.total) {
|
||||
$Page++
|
||||
$R = Invoke-WebRequest -Uri "$SonarUrl/api/components/search_projects?ps=100&p=$Page" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
|
||||
$Data = $R.Content | ConvertFrom-Json
|
||||
$Projects += $Data.components
|
||||
}
|
||||
} catch {
|
||||
Write-Host " Auch 403. Versuche components/search..." -ForegroundColor Yellow
|
||||
# Versuch 3: components/search mit Qualifier TRK
|
||||
try {
|
||||
$R = Invoke-WebRequest -Uri "$SonarUrl/api/components/search?qualifiers=TRK&ps=100&p=1" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
|
||||
$Data = $R.Content | ConvertFrom-Json
|
||||
$Projects += $Data.components
|
||||
Write-Host " OK (components/search)" -ForegroundColor Green
|
||||
while ($Projects.Count -lt $Data.paging.total) {
|
||||
$Page++
|
||||
$R = Invoke-WebRequest -Uri "$SonarUrl/api/components/search?qualifiers=TRK&ps=100&p=$Page" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
|
||||
$Data = $R.Content | ConvertFrom-Json
|
||||
$Projects += $Data.components
|
||||
}
|
||||
} catch {
|
||||
Write-Host ""
|
||||
Write-Host " Alle Endpunkte geben 403." -ForegroundColor Red
|
||||
Write-Host " Dein Account ist authentifiziert aber hat keine Projekt-Browse-Rechte ueber die API." -ForegroundColor Red
|
||||
Write-Host " Loesung: Bitte im OPs-Team anfragen, dich zur SonarQube-Gruppe 'bestellsystem1' hinzuzufuegen." -ForegroundColor Yellow
|
||||
Write-Host " Alternativ: Bekannte Projekt-Keys direkt abfragen (siehe unten)." -ForegroundColor Yellow
|
||||
|
||||
# Fallback: Bekannte Projekte direkt abfragen
|
||||
Write-Host ""
|
||||
Write-Host ">> Fallback: Bekannte Projekte direkt abfragen..." -ForegroundColor Yellow
|
||||
$KnownKeys = @(
|
||||
"steuerung-vertrieb", "auftrags-verwaltung-trasse", "portal-middleware", "portal-ui",
|
||||
"common-interface", "stammdaten-bereitstellung", "kundendaten-bereitstellung",
|
||||
"ifp-connector", "taftap-tdm-konverter", "vertragsdaten-verteiler",
|
||||
"archivierungsservice", "rabattnummern-bereitstellung", "tbv-absicherung-konverter"
|
||||
)
|
||||
foreach ($Key in $KnownKeys) {
|
||||
$Projects += [PSCustomObject]@{ key = $Key; name = $Key }
|
||||
}
|
||||
Write-Host " $($Projects.Count) bekannte Projekte" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
}
|
||||
Write-Host " $($Projects.Count) Projekte" -ForegroundColor Green
|
||||
|
||||
# Metriken pro Projekt
|
||||
Write-Host ">> Lade Metriken..."
|
||||
$Metrics = "ncloc,bugs,vulnerabilities,code_smells,coverage,duplicated_lines_density,sqale_rating,reliability_rating,security_rating,alert_status"
|
||||
$Results = @()
|
||||
|
||||
foreach ($P in $Projects) {
|
||||
Write-Host " $($P.key)..." -NoNewline
|
||||
try {
|
||||
$R = Invoke-WebRequest -Uri "$SonarUrl/api/measures/component?component=$($P.key)&metricKeys=$Metrics" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
|
||||
$MData = ($R.Content | ConvertFrom-Json).component.measures
|
||||
$Results += [PSCustomObject]@{
|
||||
Project = $P.key
|
||||
Name = $P.name
|
||||
Lines = ($MData | Where-Object { $_.metric -eq "ncloc" }).value
|
||||
Bugs = ($MData | Where-Object { $_.metric -eq "bugs" }).value
|
||||
Vulnerabilities = ($MData | Where-Object { $_.metric -eq "vulnerabilities" }).value
|
||||
CodeSmells = ($MData | Where-Object { $_.metric -eq "code_smells" }).value
|
||||
Coverage = ($MData | Where-Object { $_.metric -eq "coverage" }).value
|
||||
Duplications = ($MData | Where-Object { $_.metric -eq "duplicated_lines_density" }).value
|
||||
Maintainability = ($MData | Where-Object { $_.metric -eq "sqale_rating" }).value
|
||||
Reliability = ($MData | Where-Object { $_.metric -eq "reliability_rating" }).value
|
||||
Security = ($MData | Where-Object { $_.metric -eq "security_rating" }).value
|
||||
QualityGate = ($MData | Where-Object { $_.metric -eq "alert_status" }).value
|
||||
}
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
} catch { Write-Host " Fehler" -ForegroundColor Yellow }
|
||||
}
|
||||
|
||||
# Speichern
|
||||
$Results | Export-Csv "$OutputDir\sonarqube-metrics.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
|
||||
$Results | ConvertTo-Json -Depth 5 | Out-File "$OutputDir\sonarqube-metrics.json" -Encoding UTF8
|
||||
|
||||
$Md = @("# SonarQube Metriken pathOS", "", "| Projekt | Lines | Bugs | Vulns | Smells | Coverage | Dupl% | QGate |")
|
||||
$Md += "|---------|-------|------|-------|--------|----------|-------|-------|"
|
||||
foreach ($R in ($Results | Sort-Object { if ($_.Lines) { [int]$_.Lines } else { 0 } } -Descending)) {
|
||||
$Md += "| $($R.Name) | $($R.Lines) | $($R.Bugs) | $($R.Vulnerabilities) | $($R.CodeSmells) | $($R.Coverage)% | $($R.Duplications)% | $($R.QualityGate) |"
|
||||
}
|
||||
$Md -join "`n" | Out-File "$OutputDir\sonarqube-metrics.md" -Encoding UTF8
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
Write-Host " FERTIG! $($Results.Count) Projekte" -ForegroundColor Green
|
||||
Write-Host " Ergebnisse in $OutputDir" -ForegroundColor Green
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
@@ -0,0 +1,178 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Schritt 1: SonarQube URL aus GitLab Deployment-Repo finden
|
||||
Schritt 2: SonarQube Metriken exportieren (braucht SonarQube Token)
|
||||
#>
|
||||
|
||||
$GitLabUrl = "https://git.tech.rz.db.de"
|
||||
$GitLabApiBase = "$GitLabUrl/api/v4"
|
||||
$OutputDir = "project-audit\data\sonarqube"
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
Write-Host "============================================================"
|
||||
Write-Host " SonarQube Daten fuer pathOS"
|
||||
Write-Host "============================================================"
|
||||
|
||||
# === Schritt 1: URL aus GitLab finden ===
|
||||
Write-Host ""
|
||||
Write-Host ">> Schritt 1: SonarQube URL aus GitLab suchen..." -ForegroundColor Yellow
|
||||
|
||||
$GitLabToken = Read-Host "GitLab Token (read_api)"
|
||||
$GitLabHeaders = @{ "PRIVATE-TOKEN" = $GitLabToken }
|
||||
|
||||
# deployment-sonarqube Repo durchsuchen
|
||||
$RepoPath = [Uri]::EscapeDataString("bestellsystem1/infra/deployment-sonarqube")
|
||||
$SonarUrl = $null
|
||||
|
||||
foreach ($File in @("values.yaml", "helmfile.yaml", "Chart.yaml", "values-sonarqube.yaml")) {
|
||||
foreach ($Branch in @("main", "master")) {
|
||||
$Uri = "$GitLabApiBase/projects/$RepoPath/repository/files/$([Uri]::EscapeDataString($File))/raw?ref=$Branch"
|
||||
try {
|
||||
$Resp = Invoke-WebRequest -Uri $Uri -Headers $GitLabHeaders -UseBasicParsing -ErrorAction Stop
|
||||
$Content = $Resp.Content
|
||||
Write-Host " Gefunden: $File ($Branch)" -ForegroundColor Green
|
||||
|
||||
# URL suchen
|
||||
if ($Content -match "(https?://sonar[^\s""']+)") {
|
||||
$SonarUrl = $Matches[1]
|
||||
Write-Host " SonarQube URL: $SonarUrl" -ForegroundColor Green
|
||||
}
|
||||
if ($Content -match "host:\s*(.+)") {
|
||||
Write-Host " Host: $($Matches[1].Trim())"
|
||||
}
|
||||
if ($Content -match "ingress.*host.*:\s*(.+)") {
|
||||
Write-Host " Ingress Host: $($Matches[1].Trim())"
|
||||
}
|
||||
|
||||
# Datei speichern
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
$Content | Out-File "$OutputDir\deployment-$File" -Encoding UTF8
|
||||
} catch { continue }
|
||||
}
|
||||
}
|
||||
|
||||
# Auch in der Repo-Beschreibung suchen
|
||||
try {
|
||||
$RepoInfo = Invoke-WebRequest -Uri "$GitLabApiBase/projects/$RepoPath" -Headers $GitLabHeaders -UseBasicParsing -ErrorAction Stop
|
||||
$RepoData = $RepoInfo.Content | ConvertFrom-Json
|
||||
Write-Host " Repo-Beschreibung: $($RepoData.description)"
|
||||
} catch {}
|
||||
|
||||
# Bekannte CNP-URL-Muster probieren
|
||||
Write-Host ""
|
||||
Write-Host ">> Probiere bekannte URL-Muster..." -ForegroundColor Yellow
|
||||
$Candidates = @(
|
||||
"https://sonarqube.build.bsz-dev.cnp-test.comp.db.de"
|
||||
"https://sonarqube.bsz-dev.cnp-test.comp.db.de"
|
||||
"https://sonarqube.bsz-infra.cnp-test.comp.db.de"
|
||||
)
|
||||
foreach ($Url in $Candidates) {
|
||||
Write-Host " Teste $Url..." -NoNewline
|
||||
try {
|
||||
$R = Invoke-WebRequest -Uri "$Url/api/system/status" -UseBasicParsing -TimeoutSec 5 -ErrorAction Stop
|
||||
Write-Host " ERREICHBAR!" -ForegroundColor Green
|
||||
$SonarUrl = $Url
|
||||
break
|
||||
} catch {
|
||||
Write-Host " nicht erreichbar" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $SonarUrl) {
|
||||
Write-Host ""
|
||||
Write-Host "SonarQube URL nicht automatisch gefunden." -ForegroundColor Yellow
|
||||
Write-Host "Bitte manuell eingeben (oder Enter zum Ueberspringen):"
|
||||
$SonarUrl = Read-Host "SonarQube URL"
|
||||
if (-not $SonarUrl) {
|
||||
Write-Host "Uebersprungen. Bitte URL beim Team erfragen." -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
|
||||
# === Schritt 2: SonarQube Metriken ziehen ===
|
||||
Write-Host ""
|
||||
Write-Host ">> Schritt 2: SonarQube Metriken exportieren..." -ForegroundColor Yellow
|
||||
Write-Host " URL: $SonarUrl"
|
||||
Write-Host ""
|
||||
Write-Host " Du brauchst einen SonarQube Token."
|
||||
Write-Host " Erstellen unter: $SonarUrl/account/security"
|
||||
Write-Host " (Login mit GitLab SSO, dann My Account > Security > Generate Token)"
|
||||
Write-Host ""
|
||||
|
||||
$SonarToken = Read-Host "SonarQube Token (oder Enter zum Ueberspringen)"
|
||||
if (-not $SonarToken) {
|
||||
Write-Host "Uebersprungen." -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
$SonarAuth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${SonarToken}:"))
|
||||
$SonarHeaders = @{ "Authorization" = "Basic $SonarAuth" }
|
||||
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
|
||||
# Projekte auflisten
|
||||
Write-Host " Lade Projekte..." -NoNewline
|
||||
$Projects = @()
|
||||
$Page = 1
|
||||
do {
|
||||
try {
|
||||
$R = Invoke-WebRequest -Uri "$SonarUrl/api/projects/search?ps=100&p=$Page" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
|
||||
$Data = $R.Content | ConvertFrom-Json
|
||||
$Projects += $Data.components
|
||||
$Page++
|
||||
} catch {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
break
|
||||
}
|
||||
} while ($Projects.Count -lt $Data.paging.total)
|
||||
Write-Host " $($Projects.Count) Projekte" -ForegroundColor Green
|
||||
|
||||
# Metriken pro Projekt
|
||||
Write-Host " Lade Metriken..."
|
||||
$Metrics = "ncloc,bugs,vulnerabilities,code_smells,coverage,duplicated_lines_density,sqale_rating,reliability_rating,security_rating,alert_status"
|
||||
$Results = @()
|
||||
|
||||
foreach ($P in $Projects) {
|
||||
Write-Host " $($P.key)..." -NoNewline
|
||||
try {
|
||||
$R = Invoke-WebRequest -Uri "$SonarUrl/api/measures/component?component=$($P.key)&metricKeys=$Metrics" -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
|
||||
$MData = ($R.Content | ConvertFrom-Json).component.measures
|
||||
$Row = [PSCustomObject]@{
|
||||
Project = $P.key
|
||||
Name = $P.name
|
||||
Lines = ($MData | Where-Object { $_.metric -eq "ncloc" }).value
|
||||
Bugs = ($MData | Where-Object { $_.metric -eq "bugs" }).value
|
||||
Vulnerabilities = ($MData | Where-Object { $_.metric -eq "vulnerabilities" }).value
|
||||
CodeSmells = ($MData | Where-Object { $_.metric -eq "code_smells" }).value
|
||||
Coverage = ($MData | Where-Object { $_.metric -eq "coverage" }).value
|
||||
Duplications = ($MData | Where-Object { $_.metric -eq "duplicated_lines_density" }).value
|
||||
Maintainability = ($MData | Where-Object { $_.metric -eq "sqale_rating" }).value
|
||||
Reliability = ($MData | Where-Object { $_.metric -eq "reliability_rating" }).value
|
||||
Security = ($MData | Where-Object { $_.metric -eq "security_rating" }).value
|
||||
QualityGate = ($MData | Where-Object { $_.metric -eq "alert_status" }).value
|
||||
}
|
||||
$Results += $Row
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host " Fehler" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# Speichern
|
||||
$Results | Export-Csv "$OutputDir\sonarqube-metrics.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
|
||||
$Results | ConvertTo-Json -Depth 5 | Out-File "$OutputDir\sonarqube-metrics.json" -Encoding UTF8
|
||||
|
||||
# Markdown
|
||||
$Md = @("# SonarQube Metriken pathOS", "", "| Projekt | Lines | Bugs | Vulns | Smells | Coverage | Dupl% | QGate |")
|
||||
$Md += "|---------|-------|------|-------|--------|----------|-------|-------|"
|
||||
foreach ($R in ($Results | Sort-Object { [int]$_.Lines } -Descending)) {
|
||||
$Md += "| $($R.Name) | $($R.Lines) | $($R.Bugs) | $($R.Vulnerabilities) | $($R.CodeSmells) | $($R.Coverage)% | $($R.Duplications)% | $($R.QualityGate) |"
|
||||
}
|
||||
$Md -join "`n" | Out-File "$OutputDir\sonarqube-metrics.md" -Encoding UTF8
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
Write-Host " FERTIG! $($Results.Count) Projekte analysiert" -ForegroundColor Green
|
||||
Write-Host " Ergebnisse in $OutputDir" -ForegroundColor Green
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
@@ -0,0 +1,120 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Exportiert Top Code Smells pro Komponente aus SonarQube
|
||||
#>
|
||||
|
||||
$SonarUrl = "https://sonarqube.build.bsz-dev.cnp-test.comp.db.de"
|
||||
$OutputDir = "project-audit\data\sonarqube"
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
Write-Host "============================================================"
|
||||
Write-Host " SonarQube Smells Detail-Analyse"
|
||||
Write-Host "============================================================"
|
||||
|
||||
$SonarToken = Read-Host "SonarQube Token"
|
||||
$SonarAuth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${SonarToken}:"))
|
||||
$SonarHeaders = @{ "Authorization" = "Basic $SonarAuth" }
|
||||
|
||||
# Kern-Projekte (Master/Main Branch Keys)
|
||||
$Projects = @(
|
||||
@{ key = "steuerung-vertrieb-master"; name = "Steuerung Vertrieb"; team = "CIB" }
|
||||
@{ key = "portal-ui-master"; name = "Portal UI"; team = "404" }
|
||||
@{ key = "portal-middleware-master"; name = "Portal Middleware"; team = "404" }
|
||||
@{ key = "auftrags-verwaltung-trasse-master"; name = "Auftrags-Verwaltung"; team = "CIB" }
|
||||
@{ key = "common-interface-master"; name = "Common Interface"; team = "Zero" }
|
||||
@{ key = "taftap-tdm-konverter-main"; name = "TAF/TAP TDM Konverter"; team = "Zero" }
|
||||
@{ key = "archivierungsservice-master"; name = "Archivierungsservice"; team = "CIB" }
|
||||
@{ key = "stammdaten-bereitstellung-master"; name = "Stammdaten-Bereitstellung"; team = "Zero" }
|
||||
@{ key = "tadef-connector-main"; name = "TADEF Connector"; team = "Zero" }
|
||||
@{ key = "camunda-backup-wizard-main"; name = "Camunda Backup Wizard"; team = "CIB" }
|
||||
)
|
||||
|
||||
$AllSmells = @()
|
||||
|
||||
foreach ($P in $Projects) {
|
||||
Write-Host ""
|
||||
Write-Host ">> $($P.name) ($($P.team))..." -ForegroundColor Yellow
|
||||
|
||||
# Issues mit Typ CODE_SMELL laden
|
||||
$Issues = @()
|
||||
$Page = 1
|
||||
do {
|
||||
try {
|
||||
$Uri = "$SonarUrl/api/issues/search?componentKeys=$($P.key)&types=CODE_SMELL&statuses=OPEN,CONFIRMED&ps=100&p=$Page&facets=rules"
|
||||
$R = Invoke-WebRequest -Uri $Uri -Headers $SonarHeaders -UseBasicParsing -ErrorAction Stop
|
||||
$Data = $R.Content | ConvertFrom-Json
|
||||
$Issues += $Data.issues
|
||||
if ($Page -eq 1 -and $Data.facets) {
|
||||
# Rule-Facetten extrahieren (Top Smells nach Haeufigkeit)
|
||||
$RuleFacet = $Data.facets | Where-Object { $_.property -eq "rules" }
|
||||
if ($RuleFacet) {
|
||||
Write-Host " $($Data.total) Smells, Top Rules:"
|
||||
$TopRules = $RuleFacet.values | Sort-Object count -Descending | Select-Object -First 15
|
||||
foreach ($Rule in $TopRules) {
|
||||
$AllSmells += [PSCustomObject]@{
|
||||
Project = $P.name
|
||||
Team = $P.team
|
||||
Rule = $Rule.val
|
||||
Count = $Rule.count
|
||||
}
|
||||
Write-Host " $($Rule.val): $($Rule.count)"
|
||||
}
|
||||
}
|
||||
}
|
||||
$Page++
|
||||
} catch {
|
||||
Write-Host " FEHLER: $($_.Exception.Message)" -ForegroundColor Red
|
||||
break
|
||||
}
|
||||
} while ($Issues.Count -lt [Math]::Min($Data.total, 500) -and $Page -le 5)
|
||||
}
|
||||
|
||||
# Speichern
|
||||
$AllSmells | Export-Csv "$OutputDir\smells-by-rule.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
|
||||
|
||||
# Cross-Projekt Analyse: Gleiche Rules ueber Projekte
|
||||
Write-Host ""
|
||||
Write-Host "=== Cross-Projekt Analyse ===" -ForegroundColor Yellow
|
||||
$ByRule = $AllSmells | Group-Object Rule
|
||||
$CrossProject = $ByRule | Where-Object { $_.Count -gt 1 } | Sort-Object { ($_.Group | Measure-Object -Property Count -Sum).Sum } -Descending
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Rules die in mehreren Projekten auftreten:"
|
||||
foreach ($R in $CrossProject | Select-Object -First 20) {
|
||||
$TotalCount = ($R.Group | Measure-Object -Property Count -Sum).Sum
|
||||
$ProjectList = ($R.Group | ForEach-Object { "$($_.Project)($($_.Count))" }) -join ", "
|
||||
Write-Host " $($R.Name): $TotalCount total [$ProjectList]"
|
||||
}
|
||||
|
||||
# Markdown
|
||||
$Md = @("# SonarQube Smells Detail-Analyse", "")
|
||||
$Md += "## Top Smells pro Komponente"
|
||||
$Md += ""
|
||||
foreach ($P in $Projects) {
|
||||
$PSmells = $AllSmells | Where-Object { $_.Project -eq $P.name } | Sort-Object Count -Descending
|
||||
if ($PSmells.Count -eq 0) { continue }
|
||||
$Md += "### $($P.name) ($($P.team))"
|
||||
$Md += "| Rule | Count |"
|
||||
$Md += "|------|-------|"
|
||||
foreach ($S in $PSmells | Select-Object -First 10) {
|
||||
$Md += "| $($S.Rule) | $($S.Count) |"
|
||||
}
|
||||
$Md += ""
|
||||
}
|
||||
|
||||
$Md += "## Cross-Projekt: Gleiche Rules"
|
||||
$Md += "| Rule | Total | Projekte |"
|
||||
$Md += "|------|-------|----------|"
|
||||
foreach ($R in $CrossProject | Select-Object -First 20) {
|
||||
$TotalCount = ($R.Group | Measure-Object -Property Count -Sum).Sum
|
||||
$ProjectList = ($R.Group | ForEach-Object { "$($_.Project)($($_.Count))" }) -join ", "
|
||||
$Md += "| $($R.Name) | $TotalCount | $ProjectList |"
|
||||
}
|
||||
|
||||
$Md -join "`n" | Out-File "$OutputDir\smells-detail.md" -Encoding UTF8
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
Write-Host " FERTIG! Ergebnisse in $OutputDir" -ForegroundColor Green
|
||||
Write-Host "============================================================" -ForegroundColor Green
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Liest TrassenOrder User Research von SAB Confluence."""
|
||||
import urllib.request, json, ssl, re
|
||||
|
||||
secrets = {}
|
||||
with open("project-audit/.secrets", "r", encoding="utf-8-sig") as f:
|
||||
for line in f:
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.strip().split("=", 1)
|
||||
secrets[k.strip()] = v.strip()
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
CONF_TOKEN = secrets.get("CONFLUENCE_SAB", "")
|
||||
CONF_URL = "https://systelone-confluence.jaas.service.deutschebahn.com/rest/api"
|
||||
PAGE_ID = "85858484"
|
||||
|
||||
print(f"Token: ...{CONF_TOKEN[-4:]}")
|
||||
print(f"URL: {CONF_URL}/content/{PAGE_ID}")
|
||||
print()
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(f"{CONF_URL}/content/{PAGE_ID}?expand=body.storage")
|
||||
req.add_header("Authorization", f"Bearer {CONF_TOKEN}")
|
||||
resp = urllib.request.urlopen(req, context=ctx, timeout=30)
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
title = data.get("title", "?")
|
||||
body = data["body"]["storage"]["value"]
|
||||
print(f"Titel: {title}")
|
||||
|
||||
# HTML zu Text
|
||||
text = re.sub(r'<[^>]+>', '\n', body)
|
||||
text = re.sub(r'\n{3,}', '\n\n', text).strip()
|
||||
print(f"Laenge: {len(text)} Zeichen")
|
||||
print()
|
||||
print(text[:5000])
|
||||
|
||||
# Speichern
|
||||
with open("project-audit/data/trapo-user-research.txt", "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
print(f"\n\nGespeichert: project-audit/data/trapo-user-research.txt ({len(text)} Zeichen)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fehler: {e}")
|
||||
Reference in New Issue
Block a user