mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-05-18 05:05:25 +02:00
Add auto-label-product GitHub Action for issue triage (#47485)
## 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>
This commit is contained in:
11
.github/policies/resourceManagement.yml
vendored
11
.github/policies/resourceManagement.yml
vendored
@@ -266,16 +266,5 @@ configuration:
|
||||
- addReply:
|
||||
reply: Hi! Your last comment indicates to our system, that you might want to contribute to this feature/fix this bug. Thank you! Please make us aware on our ["Would you like to contribute to PowerToys?" thread](https://github.com/microsoft/PowerToys/issues/28769), as we don't see all the comments. <br /><br />_I'm a bot (beep!) so please excuse any mistakes I may make_
|
||||
description:
|
||||
- if:
|
||||
- payloadType: Issues
|
||||
- isAction:
|
||||
action: Opened
|
||||
- bodyContains:
|
||||
pattern: 'Area\(s\) with issue\?\s*\nWorkspaces'
|
||||
isRegex: True
|
||||
then:
|
||||
- addLabel:
|
||||
label: Product-Workspaces
|
||||
description:
|
||||
onFailure:
|
||||
onSuccess:
|
||||
|
||||
275
.github/workflows/auto-label-product.yml
vendored
Normal file
275
.github/workflows/auto-label-product.yml
vendored
Normal file
@@ -0,0 +1,275 @@
|
||||
name: Auto Label Product on Issue Creation
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mode:
|
||||
description: 'single: label one issue, batch: label all issues missing Product- labels'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- single
|
||||
- batch
|
||||
default: single
|
||||
issue_number:
|
||||
description: 'Issue number (only used in single mode)'
|
||||
required: false
|
||||
type: number
|
||||
dry_run:
|
||||
description: 'If true, only log what labels would be applied without applying them'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
batch_limit:
|
||||
description: 'Max issues to process in batch mode (default: 50)'
|
||||
required: false
|
||||
type: number
|
||||
default: 50
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
models: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.inputs.issue_number || 'batch' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
label-product:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Auto-apply Product labels
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const isManual = context.eventName === 'workflow_dispatch';
|
||||
const dryRun = isManual ? (context.payload.inputs.dry_run === 'true') : false;
|
||||
const batchMode = isManual && context.payload.inputs.mode === 'batch';
|
||||
const batchLimit = isManual ? parseInt(context.payload.inputs.batch_limit || '50') : 1;
|
||||
|
||||
// Mapping from issue template "Area(s) with issue?" values to Product- labels
|
||||
const 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',
|
||||
};
|
||||
|
||||
const ALL_PRODUCT_LABELS = [...new Set(Object.values(AREA_TO_LABEL))].sort();
|
||||
|
||||
// ─── Collect issues to process ───
|
||||
let issues = [];
|
||||
if (batchMode) {
|
||||
// Fetch open issues that have no Product-* label
|
||||
core.info(`Batch mode: fetching up to ${batchLimit} issues without Product- labels...`);
|
||||
let page = 1;
|
||||
while (issues.length < batchLimit) {
|
||||
const { data } = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
per_page: 100,
|
||||
page: page++,
|
||||
sort: 'created',
|
||||
direction: 'desc',
|
||||
});
|
||||
if (data.length === 0) break;
|
||||
for (const issue of data) {
|
||||
if (issue.pull_request) continue; // skip PRs
|
||||
const hasProductLabel = issue.labels.some(l => l.name.startsWith('Product-'));
|
||||
if (!hasProductLabel) {
|
||||
issues.push(issue);
|
||||
if (issues.length >= batchLimit) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
core.info(`Found ${issues.length} issues to process.`);
|
||||
} else if (isManual) {
|
||||
const issueNumber = parseInt(context.payload.inputs.issue_number);
|
||||
if (!issueNumber) { core.setFailed('issue_number is required in single mode'); return; }
|
||||
const { data } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
issues = [data];
|
||||
} else {
|
||||
issues = [context.payload.issue];
|
||||
}
|
||||
|
||||
// ─── Process each issue ───
|
||||
const summaryRows = [];
|
||||
const labelExistsCache = new Map();
|
||||
|
||||
for (const issue of issues) {
|
||||
const body = issue.body || '';
|
||||
const title = issue.title || '';
|
||||
|
||||
// Parse the "Area(s) with issue?" field
|
||||
const areaMatch = body.match(/### Area\(s\) with issue\?\s*\r?\n\r?\n([\s\S]*?)(?=\r?\n\r?\n###|\r?\n*$)/);
|
||||
let selectedAreas = [];
|
||||
if (areaMatch) {
|
||||
const areaText = areaMatch[1].trim();
|
||||
selectedAreas = areaText.split(',').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
// Resolve labels from the structured field
|
||||
const resolvedLabels = new Set();
|
||||
for (const area of selectedAreas) {
|
||||
if (AREA_TO_LABEL[area]) {
|
||||
resolvedLabels.add(AREA_TO_LABEL[area]);
|
||||
}
|
||||
}
|
||||
|
||||
// AI fallback if no deterministic match
|
||||
if (resolvedLabels.size === 0) {
|
||||
core.info(`#${issue.number}: No deterministic match, trying AI inference...`);
|
||||
try {
|
||||
const prompt = `You are a GitHub issue triage assistant for the PowerToys project.
|
||||
Given the following issue title and body, determine which PowerToys product(s) this issue is PRIMARILY about.
|
||||
|
||||
Rules:
|
||||
- Only include products the issue is directly reporting a bug for or requesting a feature in.
|
||||
- Do NOT include products that are merely mentioned as examples or comparisons.
|
||||
- When in doubt, prefer fewer labels over more. One correct label is better than many guesses.
|
||||
- If the issue is about general PowerToys infrastructure (installer, settings app, system tray), use "Product-General" or "Product-Settings" as appropriate.
|
||||
|
||||
Respond with ONLY a JSON array of label strings from this list:
|
||||
${JSON.stringify(ALL_PRODUCT_LABELS)}
|
||||
|
||||
If you cannot determine the product, respond with an empty array: []
|
||||
|
||||
Issue title: ${title}
|
||||
|
||||
Issue body (first 2000 chars):
|
||||
${body.substring(0, 2000)}`;
|
||||
|
||||
const response = await fetch('https://models.github.ai/inference/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'openai/gpt-4.1-mini',
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
temperature: 0,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const content = data.choices?.[0]?.message?.content || '';
|
||||
const jsonMatch = content.match(/\[[\s\S]*?\]/);
|
||||
if (jsonMatch) {
|
||||
const inferred = JSON.parse(jsonMatch[0]);
|
||||
for (const label of inferred) {
|
||||
if (ALL_PRODUCT_LABELS.includes(label)) {
|
||||
resolvedLabels.add(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
core.info(`#${issue.number}: AI inferred: ${[...resolvedLabels].join(', ') || '(none)'}`);
|
||||
} else {
|
||||
core.warning(`#${issue.number}: AI inference failed (${response.status})`);
|
||||
}
|
||||
} catch (err) {
|
||||
core.warning(`#${issue.number}: AI error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedLabels.size === 0) {
|
||||
summaryRows.push([`#${issue.number}`, title.substring(0, 60), '(none)', 'skipped']);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate labels exist (cached to reduce API calls in batch mode)
|
||||
const labelsToApply = [];
|
||||
for (const label of resolvedLabels) {
|
||||
let exists = labelExistsCache.get(label);
|
||||
if (exists === undefined) {
|
||||
try {
|
||||
await github.rest.issues.getLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: label,
|
||||
});
|
||||
exists = true;
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
exists = false;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
labelExistsCache.set(label, exists);
|
||||
}
|
||||
if (exists) {
|
||||
labelsToApply.push(label);
|
||||
} else {
|
||||
core.warning(`Label "${label}" not found in repo, skipping.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (labelsToApply.length === 0) {
|
||||
summaryRows.push([`#${issue.number}`, title.substring(0, 60), '(labels not found)', 'skipped']);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply or dry-run
|
||||
if (dryRun) {
|
||||
core.info(`[DRY RUN] #${issue.number}: would apply ${labelsToApply.join(', ')}`);
|
||||
summaryRows.push([`#${issue.number}`, title.substring(0, 60), labelsToApply.join(', '), 'dry-run']);
|
||||
} else {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: labelsToApply,
|
||||
});
|
||||
core.info(`#${issue.number}: applied ${labelsToApply.join(', ')}`);
|
||||
summaryRows.push([`#${issue.number}`, title.substring(0, 60), labelsToApply.join(', '), 'applied']);
|
||||
}
|
||||
}
|
||||
|
||||
// Write job summary
|
||||
if (summaryRows.length > 0) {
|
||||
core.summary.addHeading(`Auto-Label Results (${dryRun ? 'Dry Run' : 'Applied'})`, 3);
|
||||
core.summary.addTable([
|
||||
[{data: 'Issue', header: true}, {data: 'Title', header: true}, {data: 'Labels', header: true}, {data: 'Status', header: true}],
|
||||
...summaryRows,
|
||||
]);
|
||||
await core.summary.write();
|
||||
}
|
||||
193
tools/Test-AutoLabelProduct.ps1
Normal file
193
tools/Test-AutoLabelProduct.ps1
Normal file
@@ -0,0 +1,193 @@
|
||||
<#
|
||||
.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
|
||||
}
|
||||
Reference in New Issue
Block a user