mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-06-17 04:26:23 -05:00
## Summary Adds a GitHub Action workflow that **automatically applies `Product-*` labels** to issues, reducing manual triage effort. ### How it works **Two-tier approach:** 1. **Deterministic mapping** — Parses the structured "Area(s) with issue?" dropdown from bug report templates and maps selections to the correct `Product-` label via a hardcoded lookup table. 2. **AI inference (Copilot fallback)** — When no product is resolved from the structured field (e.g., feature requests without the area field), calls GitHub Models API (`gpt-4.1-mini`) to infer the product from issue title + body. ### Trigger modes | Trigger | Use case | |---------|----------| | `issues: [opened]` | Auto-labels every new issue | | `workflow_dispatch` (single) | Test on one specific issue with dry-run | | `workflow_dispatch` (batch) | Process all open issues missing Product- labels | ### Safety features - **Label validation** — checks each label exists in the repo before applying - **Dry-run mode** — logs what would happen without modifying issues - **Concurrency control** — prevents duplicate runs - **Conservative AI prompt** — only labels products the issue is *primarily* about ### Testing performed Tested locally against 10 real issues with `Needs-Triage` and no `Product-*` label: - **6/10 resolved deterministically** (correct labels applied via `gh issue edit`) - **4/10 tested AI inference** via GitHub Models API: - #47482 (CmdPal Dock) → `Product-Command Palette` ✅ - #47474 (grab and move + fancy zones) → `Product-FancyZones` ✅ - #47476 (modular download) → `[]` (correctly abstained) ✅ - #47478 (Quick Access pinning) → improved prompt to avoid over-labeling ### Files changed | File | Purpose | |------|---------| | `.github/workflows/auto-label-product.yml` | The GitHub Action | | `.github/policies/resourceManagement.yml` | Removed redundant Workspaces-only regex rule | | `tools/Test-AutoLabelProduct.ps1` | Local PowerShell test script for dry-run testing | ### Mapping validated against actual repo labels Confirmed all label names in the mapping exist in the repo via `gh label list --search "Product-"`. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
194 lines
8.0 KiB
PowerShell
194 lines
8.0 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Test the auto-label-product workflow logic locally against real issues.
|
|
|
|
.DESCRIPTION
|
|
Fetches issues with "Needs-Triage" label but no "Product-*" label from the
|
|
PowerToys repo and simulates what the GitHub Action would do (without actually
|
|
applying labels). This lets you validate the mapping and AI inference before
|
|
merging the workflow.
|
|
|
|
.PARAMETER Apply
|
|
Actually apply the labels via `gh issue edit`. Requires gh auth.
|
|
|
|
.PARAMETER Limit
|
|
Number of issues to process (default: 10).
|
|
|
|
.EXAMPLE
|
|
# Dry run - see what would happen
|
|
.\Test-AutoLabelProduct.ps1
|
|
|
|
.EXAMPLE
|
|
# Apply labels to first 5 issues
|
|
.\Test-AutoLabelProduct.ps1 -Apply -Limit 5
|
|
|
|
.NOTES
|
|
Prerequisites:
|
|
- gh CLI authenticated: `gh auth login`
|
|
- PowerShell 7+
|
|
#>
|
|
|
|
param(
|
|
[switch]$Apply,
|
|
[int]$Limit = 10
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# ─── Mapping (must match the workflow) ────────────────────────────────────────
|
|
$AREA_TO_LABEL = @{
|
|
'Advanced Paste' = 'Product-Advanced Paste'
|
|
'Always on Top' = 'Product-Always On Top'
|
|
'Awake' = 'Product-Awake'
|
|
'ColorPicker' = 'Product-Color Picker'
|
|
'Command not found' = 'Product-CommandNotFound'
|
|
'Command Palette' = 'Product-Command Palette'
|
|
'Crop and Lock' = 'Product-CropAndLock'
|
|
'Environment Variables' = 'Product-Environment Variables'
|
|
'FancyZones' = 'Product-FancyZones'
|
|
'FancyZones Editor' = 'Product-FancyZones'
|
|
'File Locksmith' = 'Product-File Locksmith'
|
|
'File Explorer: Preview Pane' = 'Product-File Explorer'
|
|
'File Explorer: Thumbnail preview' = 'Product-File Explorer'
|
|
'Hosts File Editor' = 'Product-Hosts File Editor'
|
|
'Image Resizer' = 'Product-Image Resizer'
|
|
'Keyboard Manager' = 'Product-Keyboard Shortcut Manager'
|
|
'Light Switch' = 'Product-LightSwitch'
|
|
'Mouse Utilities' = 'Product-Mouse Utilities'
|
|
'Mouse Without Borders' = 'Product-Mouse Without Borders'
|
|
'New+' = 'Product-New+'
|
|
'Peek' = 'Product-Peek'
|
|
'Power Display' = 'Product-PowerDisplay'
|
|
'PowerRename' = 'Product-PowerRename'
|
|
'PowerToys Run' = 'Product-PowerToys Run'
|
|
'Quick Accent' = 'Product-Quick Accent'
|
|
'Registry Preview' = 'Product-Registry Preview'
|
|
'Screen ruler' = 'Product-Screen Ruler'
|
|
'Settings' = 'Product-Settings'
|
|
'Shortcut Guide' = 'Product-Shortcut Guide'
|
|
'TextExtractor' = 'Product-Text Extractor'
|
|
'Workspaces' = 'Product-Workspaces'
|
|
'ZoomIt' = 'Product-ZoomIt'
|
|
'General' = 'Product-General'
|
|
'Grab And Move' = 'Product-Grab And Move'
|
|
}
|
|
|
|
# Non-product areas (no label applied, AI fallback triggers)
|
|
$NON_PRODUCT_AREAS = @('Installer', 'System tray interaction', 'Welcome / PowerToys Tour window')
|
|
|
|
# ─── Fetch issues ────────────────────────────────────────────────────────────
|
|
Write-Host "`n🔍 Fetching issues with 'Needs-Triage' and no 'Product-*' label (limit: $Limit)..." -ForegroundColor Cyan
|
|
|
|
# gh search finds issues with Needs-Triage; we filter out those that already have Product- labels
|
|
$ghStderrPath = [System.IO.Path]::GetTempFileName()
|
|
try {
|
|
$issuesJson = gh issue list --repo microsoft/PowerToys --label "Needs-Triage" --limit 100 --json number,title,body,labels --state open 2> $ghStderrPath
|
|
$ghExitCode = $LASTEXITCODE
|
|
$ghErrorOutput = Get-Content -Path $ghStderrPath -Raw
|
|
}
|
|
finally {
|
|
Remove-Item -Path $ghStderrPath -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
if ($ghExitCode -ne 0) {
|
|
Write-Host "❌ Failed to fetch issues. Ensure 'gh auth login' is done." -ForegroundColor Red
|
|
if (-not [string]::IsNullOrWhiteSpace($ghErrorOutput)) {
|
|
Write-Host $ghErrorOutput
|
|
}
|
|
exit 1
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($ghErrorOutput)) {
|
|
Write-Host "⚠️ gh emitted stderr output while fetching issues:" -ForegroundColor Yellow
|
|
Write-Host $ghErrorOutput
|
|
}
|
|
|
|
$issues = $issuesJson | ConvertFrom-Json
|
|
|
|
# Filter: only issues WITHOUT any Product-* label
|
|
$issues = $issues | Where-Object {
|
|
$labels = $_.labels | ForEach-Object { $_.name }
|
|
-not ($labels | Where-Object { $_ -like 'Product-*' })
|
|
} | Select-Object -First $Limit
|
|
|
|
Write-Host "📋 Found $($issues.Count) issues to process.`n" -ForegroundColor Green
|
|
|
|
# ─── Process each issue ──────────────────────────────────────────────────────
|
|
$results = @()
|
|
|
|
foreach ($issue in $issues) {
|
|
$body = $issue.body
|
|
$title = $issue.title
|
|
$number = $issue.number
|
|
|
|
Write-Host "--- Issue #${number}: ${title} ---" -ForegroundColor Yellow
|
|
|
|
# Parse "Area(s) with issue?" field
|
|
$selectedAreas = @()
|
|
if ($body -match '### Area\(s\) with issue\?\s*\r?\n\r?\n([\s\S]*?)(?=\r?\n\r?\n###|\s*$)') {
|
|
$areaText = $Matches[1].Trim()
|
|
$selectedAreas = $areaText -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
|
}
|
|
|
|
if ($selectedAreas.Count -eq 0) {
|
|
Write-Host " ⚠️ No 'Area(s) with issue?' field found in body." -ForegroundColor DarkYellow
|
|
} else {
|
|
Write-Host " 📌 Areas selected: $($selectedAreas -join ', ')" -ForegroundColor DarkCyan
|
|
}
|
|
|
|
# Resolve labels
|
|
$resolvedLabels = @()
|
|
$unmapped = @()
|
|
|
|
foreach ($area in $selectedAreas) {
|
|
if ($AREA_TO_LABEL.ContainsKey($area)) {
|
|
$resolvedLabels += $AREA_TO_LABEL[$area]
|
|
} elseif ($area -notin $NON_PRODUCT_AREAS) {
|
|
$unmapped += $area
|
|
}
|
|
}
|
|
$resolvedLabels = $resolvedLabels | Sort-Object -Unique
|
|
|
|
if ($unmapped.Count -gt 0) {
|
|
Write-Host " ⚠️ Unmapped areas (need mapping update): $($unmapped -join ', ')" -ForegroundColor DarkYellow
|
|
}
|
|
|
|
if ($resolvedLabels.Count -eq 0) {
|
|
Write-Host " 🤖 No deterministic match → AI inference would trigger in workflow" -ForegroundColor Magenta
|
|
} else {
|
|
Write-Host " ✅ Would apply: $($resolvedLabels -join ', ')" -ForegroundColor Green
|
|
}
|
|
|
|
# Apply if requested
|
|
if ($Apply -and $resolvedLabels.Count -gt 0) {
|
|
foreach ($label in $resolvedLabels) {
|
|
Write-Host " 🏷️ Applying label: $label" -ForegroundColor White
|
|
gh issue edit $number --repo microsoft/PowerToys --add-label $label 2>&1 | Out-Null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Host " ❌ Failed to apply '$label' (may not exist in repo)" -ForegroundColor Red
|
|
}
|
|
}
|
|
}
|
|
|
|
$results += [PSCustomObject]@{
|
|
Issue = $number
|
|
Title = $title.Substring(0, [Math]::Min(60, $title.Length))
|
|
Areas = ($selectedAreas -join ', ')
|
|
Labels = ($resolvedLabels -join ', ')
|
|
NeedsAI = ($resolvedLabels.Count -eq 0)
|
|
}
|
|
Write-Host ""
|
|
}
|
|
|
|
# ─── Summary ─────────────────────────────────────────────────────────────────
|
|
Write-Host "`n═══ SUMMARY ═══" -ForegroundColor Cyan
|
|
$results | Format-Table -AutoSize -Wrap
|
|
|
|
$aiNeeded = ($results | Where-Object { $_.NeedsAI }).Count
|
|
$mapped = ($results | Where-Object { -not $_.NeedsAI }).Count
|
|
Write-Host "Deterministic: $mapped | AI fallback needed: $aiNeeded | Total: $($results.Count)" -ForegroundColor Cyan
|
|
|
|
if (-not $Apply) {
|
|
Write-Host "`n💡 This was a DRY RUN. Use -Apply to actually add labels." -ForegroundColor Yellow
|
|
}
|