<# .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 }