Compare commits
2 commits
c16c9fff97
...
2af82d60d0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2af82d60d0 | ||
|
|
00b28d2f58 |
11 changed files with 227 additions and 208 deletions
95
.github/skills/gh-tickets/SKILL.md
vendored
95
.github/skills/gh-tickets/SKILL.md
vendored
|
|
@ -1,11 +1,11 @@
|
|||
---
|
||||
name: gh-tickets
|
||||
description: "Konventionen für GitHub-Issues in diesem Workspace: Aufgabentyp-Labels, Sortierung über das Project Board (Number-Feld 'Order') und CLI-Abfragen. Verwende diesen Skill immer dann, wenn Issues angelegt, abgefragt, sortiert oder vom nextstep-Router verarbeitet werden. Trigger-Phrasen: 'nächstes Ticket', 'Issue anlegen', 'Reihenfolge', 'Priorität', 'nextstep', 'Order', 'Board'."
|
||||
description: "Konventionen für Forgejo-Issues in diesem Workspace: Aufgabentyp-Labels, Sortierung über status:todo-Labels und CLI-Abfragen. Verwende diesen Skill immer dann, wenn Issues angelegt, abgefragt, sortiert oder vom nextstep-Router verarbeitet werden. Trigger-Phrasen: 'nächstes Ticket', 'Issue anlegen', 'Reihenfolge', 'Priorität', 'nextstep', 'Status', 'Board'."
|
||||
---
|
||||
|
||||
# Skill: GitHub Tickets (gh-tickets)
|
||||
# Skill: Forgejo Tickets (gh-tickets)
|
||||
|
||||
Dieses Dokument definiert die verbindlichen Konventionen für GitHub-Issues im Repository `jreinemann-euris/bollwerk`.
|
||||
Dieses Dokument definiert die verbindlichen Konventionen für Issues im Forgejo-Repository `bollwerkadmin/bollwerk` (`https://git.bollwerk.online`).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -49,83 +49,69 @@ Stoppe und fordere den User zur Zuordnung auf. Starte den Workflow **nicht** ohn
|
|||
|
||||
---
|
||||
|
||||
## 2. Sortierung über Project Board
|
||||
## 2. Sortierung über Status-Labels
|
||||
|
||||
Die Abarbeitungsreihenfolge wird über das **Project Board** gesteuert, nicht über Labels.
|
||||
Die Abarbeitungsreihenfolge wird über **Status-Labels** in Forgejo gesteuert.
|
||||
|
||||
### Board-Daten
|
||||
### Status-Labels
|
||||
|
||||
| Eigenschaft | Wert |
|
||||
| ------------ | ---------------------------------------------------- |
|
||||
| Projekt-Name | `Bollwerk` |
|
||||
| Projekt-Nr | `2` |
|
||||
| Owner | `jreinemann-euris` |
|
||||
| Sortierfeld | `Order` (Number-Feld) |
|
||||
| URL | https://github.com/users/jreinemann-euris/projects/2 |
|
||||
| Label | Bedeutung |
|
||||
| ------------------- | ------------------------------------------------- |
|
||||
| `status:todo` | Nächste offene Aufgabe (wird von next-ticket.ps1 gefunden) |
|
||||
| `status:in-progress`| Aktuell in Bearbeitung |
|
||||
| `status:done` | Abgeschlossen |
|
||||
| `status:backlog` | Im Backlog – wird erst nach todo-Items bearbeitet |
|
||||
|
||||
### Order-Feld Konventionen
|
||||
### Repository
|
||||
|
||||
- **Kleinere Zahl = höhere Priorität** (wird zuerst abgearbeitet)
|
||||
- **Jeder Order-Wert MUSS eindeutig sein** – keine zwei Items dürfen denselben Wert haben
|
||||
- Standardwerte: 10, 20, 30, 40, … (in 10er-Schritten, um Platz für Einfügungen zu lassen)
|
||||
- Ein dringend vorgezogenes Ticket bekommt einen Wert **zwischen** den bestehenden (z.B. 15 zwischen 10 und 20, oder 5 vor 10)
|
||||
- Wenn keine bestimmte Position vorgegeben ist, erhält das Ticket den **höchsten bestehenden Order-Wert + 10** (= wird am Ende eingefügt)
|
||||
- Items ohne Order-Wert sind ein **Fehler** und müssen sofort einen Order-Wert erhalten
|
||||
| Eigenschaft | Wert |
|
||||
| ------------ | ------------------------------------------------- |
|
||||
| URL | https://git.bollwerk.online/bollwerkadmin/bollwerk |
|
||||
| API-Base | https://git.bollwerk.online/api/v1 |
|
||||
| Token | `.github/skills/gh-tickets/forgejo-token.txt` (lokal, nicht committed) |
|
||||
|
||||
### Nächstes offenes Issue ermitteln
|
||||
|
||||
**Skript:** `.github/skills/gh-tickets/next-ticket.ps1`
|
||||
|
||||
```powershell
|
||||
# Nächstes Ticket (priority:high zuerst, dann Order aufsteigend):
|
||||
# Nächstes Ticket (priority:high zuerst, dann Issue-Nummer aufsteigend):
|
||||
& ".github/skills/gh-tickets/next-ticket.ps1"
|
||||
|
||||
# Bestimmtes Issue abfragen (Typ-Label prüfen):
|
||||
& ".github/skills/gh-tickets/next-ticket.ps1" -IssueNumber 98
|
||||
& ".github/skills/gh-tickets/next-ticket.ps1" -IssueNumber 128
|
||||
```
|
||||
|
||||
Ausgabe: `#68 [M] CRM: Erweiterte Kundensuche (Order: 120)`
|
||||
Ausgabe: `#128 [I] infra(forgejo): Projektlinks, Skills und Referenzen umstellen (Order: 128)`
|
||||
|
||||
### Issue zum Board hinzufügen
|
||||
|
||||
```powershell
|
||||
# Issue zum Board hinzufügen
|
||||
gh project item-add 2 --owner jreinemann-euris --url "https://github.com/jreinemann-euris/bollwerk/issues/<N>"
|
||||
|
||||
# Order-Wert setzen (erfordert Item-ID und Field-ID)
|
||||
gh project item-edit --id <ITEM_ID> --field-id <ORDER_FIELD_ID> --project-id <PROJECT_ID> --number <ORDER_VALUE>
|
||||
```
|
||||
|
||||
### Board-Status aktualisieren
|
||||
### Status aktualisieren
|
||||
|
||||
**Skript:** `.github/skills/gh-tickets/set-board-status.ps1`
|
||||
|
||||
```powershell
|
||||
# Status auf "In Progress" setzen
|
||||
& ".github/skills/gh-tickets/set-board-status.ps1" -IssueNumber 68 -Status InProgress
|
||||
& ".github/skills/gh-tickets/set-board-status.ps1" -IssueNumber 128 -Status InProgress
|
||||
|
||||
# Status auf "Done" setzen
|
||||
& ".github/skills/gh-tickets/set-board-status.ps1" -IssueNumber 68 -Status Done
|
||||
& ".github/skills/gh-tickets/set-board-status.ps1" -IssueNumber 128 -Status Done
|
||||
|
||||
# Status auf "Todo" zurücksetzen
|
||||
& ".github/skills/gh-tickets/set-board-status.ps1" -IssueNumber 68 -Status Todo
|
||||
& ".github/skills/gh-tickets/set-board-status.ps1" -IssueNumber 128 -Status Todo
|
||||
```
|
||||
|
||||
Gültige Status-Werte: `Todo`, `InProgress`, `Done`
|
||||
Gültige Status-Werte: `Todo`, `InProgress`, `Done`, `Backlog`
|
||||
|
||||
---
|
||||
|
||||
## 3. Issue anlegen (Checkliste)
|
||||
|
||||
Beim Anlegen eines neuen Issues **MÜSSEN alle 5 Schritte** durchgeführt werden:
|
||||
Beim Anlegen eines neuen Issues **MÜSSEN alle 3 Schritte** durchgeführt werden:
|
||||
|
||||
1. **Titel**: Beschreibend, ggf. mit Modulpräfix (z.B. `CRM:`, `feat(core):`)
|
||||
2. **Type-Label**: Genau eines von `migration`, `tech-decision`, `infrastructure`, `planning`, `test`
|
||||
3. **Weitere Labels**: Optional (z.B. `crm`, `enhancement`)
|
||||
4. **Board**: Issue **sofort** zum Project Board hinzufügen (ein Issue ohne Board-Eintrag existiert nicht für die Abarbeitung!)
|
||||
5. **Order**: **Eindeutigen** Order-Wert setzen. Vor dem Setzen die bestehenden Order-Werte abfragen und sicherstellen, dass der gewählte Wert noch nicht vergeben ist. Ohne spezifische Positionsvorgabe: höchsten bestehenden Wert + 10
|
||||
1. **Titel**: Beschreibend, ggf. mit Modulpräfix (z.B. `feat(core):`, `infra(forgejo):`)
|
||||
2. **Type-Label**: Genau eines von `migration`, `tech-decision`, `infrastructure`, `block-planning`, `feature`, `refactoring`, `planning`, `test`
|
||||
3. **Status-Label**: `status:todo` (sofort bearbeiten) oder `status:backlog` (später)
|
||||
|
||||
> **Kein Issue ohne Board + Order!** Ein Issue ohne Board-Eintrag wird bei „nächste Aufgabe" nicht gefunden.
|
||||
> **Kein Issue ohne Status-Label!** Ein Issue ohne `status:todo` oder `status:backlog` wird bei „nächste Aufgabe" nicht gefunden.
|
||||
|
||||
### Dringend vorgezogenes Issue anlegen ("als nächstes")
|
||||
|
||||
|
|
@ -134,21 +120,24 @@ Beim Anlegen eines neuen Issues **MÜSSEN alle 5 Schritte** durchgeführt werden
|
|||
Wenn während einer laufenden Aufgabe ein neues Issue entdeckt wird, das **direkt danach** bearbeitet werden soll:
|
||||
|
||||
```powershell
|
||||
# Issue anlegen und als nächstes Ticket einsortieren (niedrigster Order-Wert)
|
||||
& ".github/skills/gh-tickets/create-next-ticket.ps1" -Title "CRM: Bug in Kundensuche" -Labels "migration,crm" -Body "Beschreibung..."
|
||||
# Issue anlegen mit status:todo (wird als nächstes gefunden, niedrigste Nummer)
|
||||
& ".github/skills/gh-tickets/create-next-ticket.ps1" -Title "fix(server): Bug in Auth" -Labels "infrastructure" -Body "Beschreibung..."
|
||||
|
||||
# Issue ins Backlog legen
|
||||
& ".github/skills/gh-tickets/create-next-ticket.ps1" -Title "feat: Neues Feature" -Labels "feature" -Status Backlog
|
||||
```
|
||||
|
||||
Das Skript erledigt automatisch alle 5 Schritte: Issue anlegen, Board hinzufügen, Order auf niedrigsten Wert setzen.
|
||||
Das Skript erledigt automatisch: Issue anlegen + Status-Label setzen.
|
||||
**Trigger-Phrasen:** „als nächstes anlegen", „nächstes Ticket erstellen", „direkt danach bearbeiten"
|
||||
|
||||
---
|
||||
|
||||
## 4. Sortierung: Priorität + Order (zweistufig)
|
||||
## 4. Sortierung: Priorität + Issue-Nummer (zweistufig)
|
||||
|
||||
Die Abarbeitungsreihenfolge wird **zweistufig** bestimmt:
|
||||
|
||||
1. **Primär: Label `priority:high`** → Issues mit diesem Label kommen **immer zuerst**, unabhängig vom Order-Wert
|
||||
2. **Sekundär: Label `priority:low`** → Issues mit diesem Label kommen **immer zuletzt**, unabhängig vom Order-Wert
|
||||
3. **Tertiär: Order-Feld** → Innerhalb derselben Prioritätsstufe sortiert der Order-Wert aufsteigend (kleinere Zahl = früher dran)
|
||||
1. **Primär: Label `priority:high`** → Issues mit diesem Label kommen **immer zuerst**
|
||||
2. **Sekundär: Label `priority:low`** → Issues mit diesem Label kommen **immer zuletzt**
|
||||
3. **Tertiär: Issue-Nummer aufsteigend** → Niedrigere Nummer = früher erstellt = höhere Priorität
|
||||
|
||||
Beispiel: Ein Issue mit `priority:high` und Order 300 kommt **vor** einem Issue ohne Priorität mit Order 5. Ein normales Issue mit Order 999 kommt **vor** einem `priority:low`-Issue mit Order 1.
|
||||
Beispiel: Ein Issue #50 mit `priority:high` kommt **vor** Issue #10 ohne Priorität.
|
||||
|
|
|
|||
111
.github/skills/gh-tickets/create-next-ticket.ps1
vendored
111
.github/skills/gh-tickets/create-next-ticket.ps1
vendored
|
|
@ -1,20 +1,21 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Erstellt ein neues GitHub-Issue und fügt es zum Project Board hinzu.
|
||||
Erstellt ein neues Issue auf Forgejo und setzt das Status-Label.
|
||||
.DESCRIPTION
|
||||
Legt ein Issue an, fügt es zum Project Board hinzu, setzt den Board-Status
|
||||
und den Order-Wert.
|
||||
- Status "Todo" (Standard): Order = niedrigster Todo-Wert minus 1 → wird als nächstes Ticket eingereiht.
|
||||
- Status "Backlog": Order = höchster nicht-Done-Wert plus 10 → wird ans Ende gestellt.
|
||||
Legt ein Issue an und setzt ein Status-Label (status:todo oder status:backlog).
|
||||
Die Reihenfolge ergibt sich aus der Issue-Nummer (aufsteigend = ältere zuerst).
|
||||
- Status "Todo" (Standard): Label status:todo → wird als nächstes Ticket gefunden.
|
||||
- Status "Backlog": Label status:backlog → wird erst nach Todo-Items bearbeitet.
|
||||
.PARAMETER Title
|
||||
Titel des neuen Issues.
|
||||
.PARAMETER Body
|
||||
Body-Text des Issues (Markdown). Optional, Default: leer.
|
||||
.PARAMETER Labels
|
||||
Komma-separierte Labels (z.B. "migration,crm,enhancement"). Muss mindestens
|
||||
ein Type-Label enthalten: migration, tech-decision oder infrastructure.
|
||||
Komma-separierte Labels (z.B. "infrastructure,enhancement"). Muss mindestens
|
||||
ein Type-Label enthalten: migration, tech-decision, infrastructure, block-planning,
|
||||
feature, refactoring, planning oder test.
|
||||
.PARAMETER Status
|
||||
Board-Status des neuen Tickets: "Todo" (Standard) oder "Backlog".
|
||||
Status des neuen Tickets: "Todo" (Standard) oder "Backlog".
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Title,
|
||||
|
|
@ -24,13 +25,15 @@ param(
|
|||
[string]$Status = "Todo"
|
||||
)
|
||||
|
||||
$repo = "jreinemann-euris/bollwerk"
|
||||
$projectId = "PVT_kwHOCFqiJ84BXk9U"
|
||||
$orderFieldId = "PVTF_lAHOCFqiJ84BXk9UzhSw4jo"
|
||||
$statusFieldId = "PVTSSF_lAHOCFqiJ84BXk9UzhSw4es"
|
||||
$statusOptionMap = @{
|
||||
"Todo" = "f75ad846"
|
||||
"Backlog" = "4ce6ee37"
|
||||
$forgejoBase = "https://git.bollwerk.online/api/v1"
|
||||
$repo = "bollwerkadmin/bollwerk"
|
||||
$tokenFile = Join-Path $PSScriptRoot "forgejo-token.txt"
|
||||
$token = (Get-Content $tokenFile -Raw -ErrorAction Stop).Trim()
|
||||
$headers = @{ Authorization = "token $token"; "Content-Type" = "application/json" }
|
||||
|
||||
$statusLabelMap = @{
|
||||
"Todo" = "status:todo"
|
||||
"Backlog" = "status:backlog"
|
||||
}
|
||||
|
||||
# --- 1. Type-Label validieren ---
|
||||
|
|
@ -42,65 +45,27 @@ if (-not $hasType) {
|
|||
exit 1
|
||||
}
|
||||
|
||||
# --- 2. Issue anlegen ---
|
||||
$createArgs = @("issue", "create", "--repo", $repo, "--title", $Title, "--label", $Labels)
|
||||
if ($Body) {
|
||||
$createArgs += "--body"
|
||||
$createArgs += $Body
|
||||
}
|
||||
$issueUrl = & gh @createArgs 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Fehler beim Anlegen des Issues: $issueUrl"
|
||||
exit 1
|
||||
# --- 2. Alle Label-Namen zu IDs auflösen ---
|
||||
$allLabels = (Invoke-WebRequest -Uri "$forgejoBase/repos/$repo/labels?limit=50" -Headers $headers -UseBasicParsing).Content | ConvertFrom-Json
|
||||
$statusLabelName = $statusLabelMap[$Status]
|
||||
$allRequiredLabels = $labelList + $statusLabelName
|
||||
|
||||
$labelIds = @()
|
||||
foreach ($name in $allRequiredLabels) {
|
||||
$found = $allLabels | Where-Object { $_.name -eq $name } | Select-Object -First 1
|
||||
if ($found) { $labelIds += $found.id }
|
||||
else { Write-Warning "Label '$name' nicht gefunden – wird übersprungen." }
|
||||
}
|
||||
|
||||
$issueNumber = ($issueUrl -split '/')[-1]
|
||||
# --- 3. Issue anlegen ---
|
||||
$issueBody = @{ title = $Title; labels = $labelIds }
|
||||
if ($Body) { $issueBody["body"] = $Body }
|
||||
$issueJson = $issueBody | ConvertTo-Json
|
||||
|
||||
# --- 3. Zum Board hinzufügen ---
|
||||
$addResult = gh project item-add 2 --owner jreinemann-euris --url $issueUrl --format json 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Fehler beim Hinzufügen zum Board: $addResult"
|
||||
exit 1
|
||||
}
|
||||
$itemId = ($addResult | ConvertFrom-Json).id
|
||||
$issueResp = Invoke-WebRequest -Uri "$forgejoBase/repos/$repo/issues" -Method Post -Headers $headers -Body $issueJson -UseBasicParsing
|
||||
$issue = $issueResp.Content | ConvertFrom-Json
|
||||
$issueNumber = $issue.number
|
||||
$issueUrl = $issue.html_url
|
||||
|
||||
# --- 4. Order-Wert ermitteln ---
|
||||
$raw = gh project item-list 2 --owner jreinemann-euris --format json --limit 200 | ConvertFrom-Json
|
||||
$otherItems = $raw.items | Where-Object { $_.status -ne "Done" -and $_.id -ne $itemId }
|
||||
|
||||
if ($Status -eq "Todo") {
|
||||
# Als nächstes Ticket: niedrigster bestehender Wert minus 1
|
||||
$refOrder = $otherItems |
|
||||
ForEach-Object { [double]$_.order } |
|
||||
Where-Object { $_ -gt 0 } |
|
||||
Measure-Object -Minimum |
|
||||
Select-Object -ExpandProperty Minimum
|
||||
if ($null -eq $refOrder) { $refOrder = 10 }
|
||||
$newOrder = $refOrder - 1
|
||||
} else {
|
||||
# Backlog: ans Ende stellen (höchster bestehender Wert plus 10)
|
||||
$refOrder = $otherItems |
|
||||
ForEach-Object { [double]$_.order } |
|
||||
Where-Object { $_ -gt 0 } |
|
||||
Measure-Object -Maximum |
|
||||
Select-Object -ExpandProperty Maximum
|
||||
if ($null -eq $refOrder) { $refOrder = 0 }
|
||||
$newOrder = $refOrder + 10
|
||||
}
|
||||
|
||||
# --- 5. Order setzen ---
|
||||
gh project item-edit --project-id $projectId --id $itemId --field-id $orderFieldId --number $newOrder 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Fehler beim Setzen des Order-Werts."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- 6. Board-Status setzen ---
|
||||
$optionId = $statusOptionMap[$Status]
|
||||
gh project item-edit --project-id $projectId --id $itemId --field-id $statusFieldId --single-select-option-id $optionId 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Fehler beim Setzen des Board-Status."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "#$issueNumber $Title (Status: $Status, Order: $newOrder)"
|
||||
Write-Host "#$issueNumber $Title (Status: $Status)"
|
||||
Write-Host "URL: $issueUrl"
|
||||
|
|
|
|||
80
.github/skills/gh-tickets/next-ticket.ps1
vendored
80
.github/skills/gh-tickets/next-ticket.ps1
vendored
|
|
@ -1,56 +1,58 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Ermittelt das nächste offene Ticket aus dem GitHub Project Board.
|
||||
Ermittelt das nächste offene Ticket aus Forgejo (via status:todo Label).
|
||||
.DESCRIPTION
|
||||
Sortierung: priority:high (0) → normal (1) → priority:low (2), dann Order aufsteigend.
|
||||
Gibt Nummer, Typ-Label, Titel und Order aus.
|
||||
Sortierung: priority:high (0) → normal (1) → priority:low (2), dann Issue-Nummer aufsteigend.
|
||||
Gibt Nummer, Typ-Label, Titel und Order (= Issue-Nummer) aus.
|
||||
.PARAMETER IssueNumber
|
||||
Optional: Direkt eine Issue-Nummer abfragen statt Board-Suche.
|
||||
Optional: Direkt eine Issue-Nummer abfragen statt automatischer Suche.
|
||||
#>
|
||||
param([int]$IssueNumber)
|
||||
|
||||
$repo = "jreinemann-euris/bollwerk"
|
||||
$forgejoBase = "https://git.bollwerk.online/api/v1"
|
||||
$repo = "bollwerkadmin/bollwerk"
|
||||
$tokenFile = Join-Path $PSScriptRoot "forgejo-token.txt"
|
||||
$token = (Get-Content $tokenFile -Raw -ErrorAction Stop).Trim()
|
||||
$headers = @{ Authorization = "token $token" }
|
||||
|
||||
function Get-IssueType($labelNames) {
|
||||
if ($labelNames -contains "block-planning") { "[B]" }
|
||||
elseif ($labelNames -contains "migration") { "[M]" }
|
||||
elseif ($labelNames -contains "feature") { "[F]" }
|
||||
elseif ($labelNames -contains "refactoring") { "[F]" }
|
||||
elseif ($labelNames -contains "tech-decision") { "[T]" }
|
||||
elseif ($labelNames -contains "infrastructure") { "[I]" }
|
||||
elseif ($labelNames -contains "planning") { "[P]" }
|
||||
elseif ($labelNames -contains "test") { "[X]" }
|
||||
else { "[?]" }
|
||||
}
|
||||
|
||||
if ($IssueNumber -gt 0) {
|
||||
# Variante A: Explizite Issue-Nummer
|
||||
$json = gh issue view $IssueNumber --repo $repo --json number,title,labels | ConvertFrom-Json
|
||||
$labels = $json.labels | ForEach-Object { $_.name }
|
||||
$type = if ($labels -contains "block-planning") { "[B]" }
|
||||
elseif ($labels -contains "migration") { "[M]" }
|
||||
elseif ($labels -contains "feature") { "[F]" }
|
||||
elseif ($labels -contains "refactoring") { "[F]" }
|
||||
elseif ($labels -contains "tech-decision") { "[T]" }
|
||||
elseif ($labels -contains "infrastructure") { "[I]" }
|
||||
elseif ($labels -contains "planning") { "[P]" }
|
||||
elseif ($labels -contains "test") { "[X]" }
|
||||
else { "[?]" }
|
||||
Write-Host "#$($json.number) $type $($json.title)"
|
||||
# Variante A: Explizite Issue-Nummer → Forgejo REST API
|
||||
$url = "$forgejoBase/repos/$repo/issues/$IssueNumber"
|
||||
$issue = (Invoke-WebRequest -Uri $url -Headers $headers -UseBasicParsing).Content | ConvertFrom-Json
|
||||
$labelNames = $issue.labels | ForEach-Object { $_.name }
|
||||
$type = Get-IssueType $labelNames
|
||||
Write-Host "#$($issue.number) $type $($issue.title)"
|
||||
}
|
||||
else {
|
||||
# Variante B: Nächstes Ticket per Board-Query (client-seitig auf Todo gefiltert)
|
||||
$raw = gh project item-list 2 --owner jreinemann-euris --format json --limit 200 | ConvertFrom-Json
|
||||
if ($null -eq $raw -or $null -eq $raw.items) {
|
||||
Write-Host "Keine offenen Tickets gefunden."
|
||||
# Variante B: Nächstes Ticket per Forgejo-Label status:todo
|
||||
$url = "$forgejoBase/repos/$repo/issues?state=open&type=issues&labels=status%3Atodo&limit=50&sort=oldest"
|
||||
$issues = (Invoke-WebRequest -Uri $url -Headers $headers -UseBasicParsing).Content | ConvertFrom-Json
|
||||
if ($null -eq $issues -or $issues.Count -eq 0) {
|
||||
Write-Host "Keine offenen Tickets mit 'status:todo' gefunden."
|
||||
return
|
||||
}
|
||||
$todos = $raw.items | Where-Object { $_.status -eq "Todo" } | ForEach-Object {
|
||||
$labels = $_.labels
|
||||
$prio = if ($labels -contains "priority:high") { 0 }
|
||||
elseif ($labels -contains "priority:low") { 2 }
|
||||
else { 1 }
|
||||
$type = if ($labels -contains "block-planning") { "[B]" }
|
||||
elseif ($labels -contains "migration") { "[M]" }
|
||||
elseif ($labels -contains "feature") { "[F]" }
|
||||
elseif ($labels -contains "refactoring") { "[F]" }
|
||||
elseif ($labels -contains "tech-decision") { "[T]" }
|
||||
elseif ($labels -contains "infrastructure") { "[I]" }
|
||||
elseif ($labels -contains "planning") { "[P]" }
|
||||
elseif ($labels -contains "test") { "[X]" }
|
||||
else { "[?]" }
|
||||
$todos = $issues | ForEach-Object {
|
||||
$labelNames = $_.labels | ForEach-Object { $_.name }
|
||||
$prio = if ($labelNames -contains "priority:high") { 0 }
|
||||
elseif ($labelNames -contains "priority:low") { 2 }
|
||||
else { 1 }
|
||||
$type = Get-IssueType $labelNames
|
||||
[PSCustomObject]@{
|
||||
Number = $_.content.number
|
||||
Title = $_.content.title
|
||||
Order = if ($null -ne $_.order) { $_.order } else { 999999 }
|
||||
Number = $_.number
|
||||
Title = $_.title
|
||||
Order = $_.number # Issue-Nummer als Tiebreaker (aufsteigend = älteste zuerst)
|
||||
Prio = $prio
|
||||
Type = $type
|
||||
}
|
||||
|
|
|
|||
71
.github/skills/gh-tickets/set-board-status.ps1
vendored
71
.github/skills/gh-tickets/set-board-status.ps1
vendored
|
|
@ -1,61 +1,62 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Setzt den Board-Status eines Issues im GitHub Project Board.
|
||||
Setzt den Status eines Issues in Forgejo via Label (status:todo / status:in-progress / status:done / status:backlog).
|
||||
.DESCRIPTION
|
||||
Ermittelt die Item-ID des Issues im Board und setzt den Status
|
||||
auf den angegebenen Wert.
|
||||
Entfernt alle status:*-Labels und setzt das neue Status-Label.
|
||||
Akzeptierte Werte: "Todo", "InProgress" (oder "In Progress"), "Done", "Backlog".
|
||||
.PARAMETER IssueNumber
|
||||
Die Issue-Nummer (z.B. 68).
|
||||
.PARAMETER Status
|
||||
Der Zielstatus: "Todo", "InProgress", "In Progress" oder "Done".
|
||||
Der Zielstatus: "Todo", "InProgress", "In Progress", "Done" oder "Backlog".
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)][int]$IssueNumber,
|
||||
[Parameter(Mandatory)][string]$Status
|
||||
)
|
||||
|
||||
$projectId = "PVT_kwHOCFqiJ84BXk9U"
|
||||
$statusFieldId = "PVTSSF_lAHOCFqiJ84BXk9UzhSw4es"
|
||||
$forgejoBase = "https://git.bollwerk.online/api/v1"
|
||||
$repo = "bollwerkadmin/bollwerk"
|
||||
$tokenFile = Join-Path $PSScriptRoot "forgejo-token.txt"
|
||||
$token = (Get-Content $tokenFile -Raw -ErrorAction Stop).Trim()
|
||||
$headers = @{ Authorization = "token $token"; "Content-Type" = "application/json" }
|
||||
|
||||
# Normalisierung: "In Progress" → "InProgress" (robust gegen Agent-Varianten)
|
||||
# Normalisierung
|
||||
$normalizedStatus = $Status.Trim() -replace '\s+', ''
|
||||
# "InProgress" nach Kleinschreibung angleichen an Map-Schlüssel
|
||||
$statusMap = @{
|
||||
"Todo" = "f75ad846"
|
||||
"InProgress" = "47fc9ee4"
|
||||
"Done" = "98236657"
|
||||
"Backlog" = "4ce6ee37"
|
||||
|
||||
$labelMap = @{
|
||||
"Todo" = "status:todo"
|
||||
"InProgress" = "status:in-progress"
|
||||
"Done" = "status:done"
|
||||
"Backlog" = "status:backlog"
|
||||
}
|
||||
|
||||
if (-not $statusMap.ContainsKey($normalizedStatus)) {
|
||||
$valid = $statusMap.Keys -join ", "
|
||||
Write-Error "Ungültiger Status '$Status'. Gültige Werte: $valid (auch 'In Progress' für InProgress, 'Backlog' für Backlog)."
|
||||
if (-not $labelMap.ContainsKey($normalizedStatus)) {
|
||||
$valid = $labelMap.Keys -join ", "
|
||||
Write-Error "Ungültiger Status '$Status'. Gültige Werte: $valid (auch 'In Progress' für InProgress)."
|
||||
exit 1
|
||||
}
|
||||
$optionId = $statusMap[$normalizedStatus]
|
||||
$newLabel = $labelMap[$normalizedStatus]
|
||||
|
||||
# Item-ID im Board finden
|
||||
$raw = gh project item-list 2 --owner jreinemann-euris --format json --limit 200 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Fehler beim Abrufen des Project Boards: $raw"
|
||||
exit 1
|
||||
# Alle Labels des Issues abrufen
|
||||
$issueUrl = "$forgejoBase/repos/$repo/issues/$IssueNumber"
|
||||
$issue = (Invoke-WebRequest -Uri $issueUrl -Headers $headers -UseBasicParsing).Content | ConvertFrom-Json
|
||||
|
||||
# Alte status:*-Labels entfernen
|
||||
$statusLabelIds = $issue.labels | Where-Object { $_.name -like "status:*" } | ForEach-Object { $_.id }
|
||||
foreach ($id in $statusLabelIds) {
|
||||
Invoke-WebRequest -Uri "$forgejoBase/repos/$repo/issues/$IssueNumber/labels/$id" -Method Delete -Headers $headers -UseBasicParsing | Out-Null
|
||||
}
|
||||
$items = $raw | ConvertFrom-Json
|
||||
$item = $items.items | Where-Object { $null -ne $_.content -and $_.content.number -eq $IssueNumber } | Select-Object -First 1
|
||||
|
||||
if (-not $item) {
|
||||
Write-Error "Issue #$IssueNumber nicht im Project Board gefunden. Bitte zuerst mit 'gh project item-add' hinzufügen."
|
||||
# Neues Status-Label-ID ermitteln
|
||||
$allLabels = (Invoke-WebRequest -Uri "$forgejoBase/repos/$repo/labels?limit=50" -Headers $headers -UseBasicParsing).Content | ConvertFrom-Json
|
||||
$targetLabel = $allLabels | Where-Object { $_.name -eq $newLabel } | Select-Object -First 1
|
||||
if (-not $targetLabel) {
|
||||
Write-Error "Label '$newLabel' nicht auf Forgejo gefunden. Bitte zuerst anlegen."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$itemId = $item.id
|
||||
# Neues Status-Label setzen
|
||||
$body = "{`"labels`":[" + $targetLabel.id + "]}"
|
||||
Invoke-WebRequest -Uri "$forgejoBase/repos/$repo/issues/$IssueNumber/labels" -Method Post -Headers $headers -Body $body -UseBasicParsing | Out-Null
|
||||
|
||||
$editOutput = gh project item-edit --project-id $projectId --id $itemId --field-id $statusFieldId --single-select-option-id $optionId 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "Issue #$IssueNumber → $normalizedStatus"
|
||||
}
|
||||
else {
|
||||
Write-Error "Fehler beim Setzen des Status für Issue #${IssueNumber}: $editOutput"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "Issue #$IssueNumber → $normalizedStatus"
|
||||
|
|
|
|||
6
.github/skills/ship/SKILL.md
vendored
6
.github/skills/ship/SKILL.md
vendored
|
|
@ -1,11 +1,11 @@
|
|||
---
|
||||
name: ship
|
||||
description: "CoPuPi-Workflow: Commit, Mac-Build-Test, Push, GitHub-Pipeline beobachten. Verwende diesen Skill für alles rund um das Ausliefern von Code – git push, Pipeline-Status, CI-Fehler beheben, Version hochzählen. Trigger-Phrasen: 'ship', 'push', 'pipeline', 'CI', 'deployen', 'ausliefern', 'version bump'."
|
||||
description: "CoPuPi-Workflow: Commit, Mac-Build-Test, Push zu Forgejo. Verwende diesen Skill für alles rund um das Ausliefern von Code – git push, Version hochzählen. Trigger-Phrasen: 'ship', 'push', 'pipeline', 'CI', 'deployen', 'ausliefern', 'version bump'."
|
||||
---
|
||||
|
||||
# Skill: Ship (CoPuPi-Workflow)
|
||||
|
||||
Dieser Skill kapselt Tools und Konventionen für den Ship-Workflow – den Weg vom lokalen Commit bis zur grünen CI-Pipeline.
|
||||
Dieser Skill kapselt Tools und Konventionen für den Ship-Workflow – den Weg vom lokalen Commit bis zum Push nach Forgejo (`git.bollwerk.online`).
|
||||
|
||||
Der vollständige Workflow wird über `.github/prompts/ship.prompt.md` gesteuert.
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ Der vollständige Workflow wird über `.github/prompts/ship.prompt.md` gesteuert
|
|||
|
||||
### `watch-pipeline.ps1`
|
||||
|
||||
Beobachtet den neuesten GitHub Actions Run bis zum Abschluss. Gibt Status, Dauer und Warnungen aus.
|
||||
Beobachtet den neuesten Forgejo Actions Run bis zum Abschluss. Gibt Status, Dauer und Warnungen aus.
|
||||
|
||||
**Aufruf:**
|
||||
|
||||
|
|
|
|||
2
.github/skills/vps-deploy/SKILL.md
vendored
2
.github/skills/vps-deploy/SKILL.md
vendored
|
|
@ -216,6 +216,6 @@ Die SQLite-Datenbank wird unter `/opt/bollwerk/data/` auf dem Host gemountet und
|
|||
|
||||
- **1 GB RAM:** JVM-Heap auf 384 MB begrenzt. Kein Spielraum für weitere Dienste.
|
||||
- **Kein HTTPS:** Server läuft aktuell nur auf HTTP Port 8080. Für HTTPS → Caddy als Reverse Proxy einrichten.
|
||||
- **Kein CI/CD:** Deployment ist manuell (JAR bauen → scp → docker compose up). Ggf. GitHub Actions Pipeline ergänzen.
|
||||
- **Kein CI/CD:** Deployment ist manuell (JAR bauen → scp → docker compose up). Ggf. Forgejo Actions Pipeline ergänzen.
|
||||
- **Dockerfile lokal:** Das Dockerfile auf dem VPS (`/opt/bollwerk/Dockerfile`) ist ein schlankes Runtime-Only-Image. Das Multi-Stage-Dockerfile im Repo-Root ist für lokale Builds gedacht.
|
||||
- **SSH-Escape-Problem:** Beim Schreiben von Dateien via SSH-Heredoc werden JSON-Quotes zerstört. Dateien immer lokal erstellen und per `scp` hochladen.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import androidx.compose.foundation.lazy.items
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Person
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
|
|
@ -83,8 +82,10 @@ internal fun UserListScreen(
|
|||
|
||||
@Composable
|
||||
private fun UserListItem(user: UserListItemDto, unreadCount: Int, onClick: () -> Unit) {
|
||||
val statusText = formatOnlineStatus(user.lastSeen)
|
||||
ListItem(
|
||||
headlineContent = { Text(user.username) },
|
||||
supportingContent = statusText?.let { { Text(it, style = MaterialTheme.typography.bodySmall) } },
|
||||
leadingContent = {
|
||||
Icon(imageVector = Icons.Outlined.Person, contentDescription = null)
|
||||
},
|
||||
|
|
@ -97,3 +98,23 @@ private fun UserListItem(user: UserListItemDto, unreadCount: Int, onClick: () ->
|
|||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
private fun formatOnlineStatus(lastSeen: Long?): String? {
|
||||
if (lastSeen == null) return null
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastSeen < ONLINE_THRESHOLD_MS) return "Verbunden"
|
||||
val dt = java.util.Date(lastSeen)
|
||||
val cal = java.util.Calendar.getInstance().apply { time = dt }
|
||||
val todayCal = java.util.Calendar.getInstance()
|
||||
val isToday = cal.get(java.util.Calendar.YEAR) == todayCal.get(java.util.Calendar.YEAR) &&
|
||||
cal.get(java.util.Calendar.DAY_OF_YEAR) == todayCal.get(java.util.Calendar.DAY_OF_YEAR)
|
||||
val timeFmt = java.text.SimpleDateFormat("HH:mm", java.util.Locale.GERMAN)
|
||||
return if (isToday) {
|
||||
"zuletzt online um ${timeFmt.format(dt)}"
|
||||
} else {
|
||||
val dateFmt = java.text.SimpleDateFormat("dd.MM.", java.util.Locale.GERMAN)
|
||||
"zuletzt online am ${dateFmt.format(dt)} um ${timeFmt.format(dt)}"
|
||||
}
|
||||
}
|
||||
|
||||
private const val ONLINE_THRESHOLD_MS = 60_000L
|
||||
|
|
|
|||
|
|
@ -5,10 +5,13 @@ import androidx.lifecycle.viewModelScope
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import de.bollwerk.app.domain.repository.MessageRepository
|
||||
import de.bollwerk.shared.model.UserListItemDto
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -29,8 +32,30 @@ internal class UserListViewModel @Inject constructor(
|
|||
val unreadCounts: StateFlow<Map<String, Int>> = messageRepository.getUnreadCountsBySender()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
|
||||
|
||||
private var pollingJob: Job? = null
|
||||
|
||||
init {
|
||||
loadUsers()
|
||||
startPolling()
|
||||
}
|
||||
|
||||
private fun startPolling() {
|
||||
pollingJob = viewModelScope.launch {
|
||||
while (isActive) {
|
||||
delay(POLL_INTERVAL_MS)
|
||||
refreshStatuses()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshStatuses() {
|
||||
val result = messageRepository.fetchUsers()
|
||||
if (result.isSuccess) {
|
||||
val fresh = result.getOrDefault(emptyList())
|
||||
_uiState.value = _uiState.value.copy(users = fresh)
|
||||
} else {
|
||||
android.util.Log.w(TAG, "Online-Status-Polling fehlgeschlagen: ${result.exceptionOrNull()?.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadUsers() {
|
||||
|
|
@ -49,4 +74,9 @@ internal class UserListViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun retry() = loadUsers()
|
||||
|
||||
private companion object {
|
||||
const val POLL_INTERVAL_MS = 30_000L
|
||||
const val TAG = "UserListViewModel"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,14 @@ internal fun Route.userRoutes(userRepository: UserRepository, webSocketManager:
|
|||
HttpStatusCode.Unauthorized,
|
||||
ErrorResponse(status = 401, message = "Unauthorized")
|
||||
)
|
||||
val now = System.currentTimeMillis()
|
||||
val users = userRepository.listAll()
|
||||
.filter { it.id != principal.userId }
|
||||
.map { UserListItemDto(id = it.id, username = it.username) }
|
||||
.map { user ->
|
||||
val online = webSocketManager.isOnline(user.id)
|
||||
val lastSeen = if (online) now else webSocketManager.getLastSeen(user.id)
|
||||
UserListItemDto(id = user.id, username = user.username, lastSeen = lastSeen)
|
||||
}
|
||||
call.respond(HttpStatusCode.OK, users)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,17 +13,22 @@ import java.util.concurrent.CopyOnWriteArraySet
|
|||
internal class WebSocketManager {
|
||||
|
||||
private val sessions = ConcurrentHashMap<String, CopyOnWriteArraySet<WebSocketSession>>()
|
||||
private val lastSeenMap = ConcurrentHashMap<String, Long>()
|
||||
|
||||
fun addSession(userId: String, session: WebSocketSession) {
|
||||
sessions.getOrPut(userId) { CopyOnWriteArraySet() }.add(session)
|
||||
sessions.computeIfAbsent(userId) { CopyOnWriteArraySet() }.add(session)
|
||||
lastSeenMap[userId] = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
fun removeSession(userId: String, session: WebSocketSession) {
|
||||
sessions[userId]?.remove(session)
|
||||
lastSeenMap[userId] = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
fun isOnline(userId: String): Boolean = sessions[userId]?.isNotEmpty() == true
|
||||
|
||||
fun getLastSeen(userId: String): Long? = lastSeenMap[userId]
|
||||
|
||||
suspend fun notifyInventoryUpdated(userId: String, itemId: String) {
|
||||
val payload = buildJsonObject {
|
||||
put("type", "inventoryUpdated")
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@ import kotlinx.serialization.Serializable
|
|||
@Serializable
|
||||
data class UserListItemDto(
|
||||
val id: String,
|
||||
val username: String
|
||||
val username: String,
|
||||
val lastSeen: Long? = null
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue