Skip to content

[Bug]: Stable NuGet version discrepancy #157

[Bug]: Stable NuGet version discrepancy

[Bug]: Stable NuGet version discrepancy #157

# New BSD 3-Clause License (https://github.com/Krypton-Suite/Standard-Toolkit/blob/master/LICENSE)
# Modifications by Peter Wagner (aka Wagnerp), Simon Coghlan (aka Smurf-IV), tobitege et al. 2025 - 2026. All rights reserved.
name: Auto-label issue areas
on:
issues:
types:
- opened
- edited
permissions:
issues: write
jobs:
label-areas:
runs-on: ubuntu-latest
steps:
- name: Extract and apply area labels
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
const issue = context.payload.issue;
const issueBody = issue.body || '';
const issueTitle = issue.title || '';
// Extract labels once for reuse
const labels = (issue.labels || []).map(
label => typeof label === 'string' ? label : label.name
);
// Helper function to check if labels contain a pattern (handles emojis)
const hasLabelMatching = (pattern) => {
const patternLower = pattern.toLowerCase();
return labels.some(label => {
const labelLower = label.toLowerCase();
// Check if label contains the pattern (handles emoji prefixes like '🪲 bug')
return labelLower.includes(patternLower);
});
};
// Auto-prefix issue titles based on template type
// Only do this for newly opened issues (not edits)
if (context.payload.action === 'opened') {
let titlePrefix = null;
// Check for Bug Report template (handles '🪲 bug' label)
const hasBugLabel = hasLabelMatching('bug');
const hasBugReportFields = issueBody.includes('### Steps to Reproduce');
if ((hasBugLabel || hasBugReportFields) && !issueTitle.startsWith('[Bug]: ')) {
titlePrefix = '[Bug]: ';
}
// Check for Feature Request template (handles '✨ new feature', '💡 suggestion' labels)
else if ((hasLabelMatching('enhancement') || hasLabelMatching('new feature') || hasLabelMatching('suggestion') ||
issueBody.includes('### Feature Description')) &&
!issueTitle.startsWith('[Feature Request]: ')) {
titlePrefix = '[Feature Request]: ';
}
// Check for Other Issues template
else if ((labels.includes('discussion') || labels.includes('other') ||
issueBody.includes('### Please describe your issue')) &&
!issueTitle.startsWith('[Other Issues]: ')) {
titlePrefix = '[Other Issues]: ';
}
// Check for Post a Question template (handles '❔ question' label)
else if ((hasLabelMatching('question') ||
issueBody.includes('### What do you want to ask?')) &&
!issueTitle.startsWith('[Question]: ')) {
titlePrefix = '[Question]: ';
}
if (titlePrefix) {
const newTitle = `${titlePrefix}${issueTitle}`;
try {
await github.rest.issues.update({
owner,
repo,
issue_number: issue.number,
title: newTitle,
});
console.log(`Updated issue title to: ${newTitle}`);
} catch (error) {
console.warn(`Failed to update issue title: ${error.message}`);
}
}
}
// Map area names to label search patterns
// The workflow will dynamically find matching labels from the repository
const areaToLabelPattern = {
'Docking': 'area:docking',
'Navigator': 'area:navigator',
'Ribbon': 'area:ribbon',
'Toolkit': 'area:toolkit',
'Workspace': 'area:workspace'
};
// Fetch all labels from the repository to find exact matches (handles emojis)
let allLabels = [];
try {
const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
owner,
repo,
per_page: 100
});
allLabels = repoLabels.map(label => label.name);
console.log(`Fetched ${allLabels.length} labels from repository`);
} catch (error) {
console.warn(`Failed to fetch labels: ${error.message}`);
// Fallback to direct mapping if label fetch fails
}
// Create a mapping from area names to actual label names (with emojis if present)
const areaToLabel = {};
for (const [area, pattern] of Object.entries(areaToLabelPattern)) {
let matchedLabel = null;
if (allLabels.length > 0) {
// Try to find exact case-insensitive match first
matchedLabel = allLabels.find(label =>
label.toLowerCase() === pattern.toLowerCase()
);
// If no exact match, try to find label that contains the pattern (handles emojis)
// e.g., "🎯 area:docking" should match pattern "area:docking"
if (!matchedLabel) {
matchedLabel = allLabels.find(label => {
const labelLower = label.toLowerCase();
const patternLower = pattern.toLowerCase();
// Match if label contains the pattern (handles emoji prefixes)
return labelLower.includes(patternLower);
});
}
// Also try matching by extracting just alphanumeric/colon chars (for fuzzy matching)
if (!matchedLabel) {
const patternClean = pattern.toLowerCase().replace(/[^\w:]/g, '');
matchedLabel = allLabels.find(label => {
const labelClean = label.toLowerCase().replace(/[^\w:]/g, '');
return labelClean === patternClean;
});
}
}
// If still no match, use the pattern as-is (will fail gracefully later)
areaToLabel[area] = matchedLabel || pattern;
if (matchedLabel && matchedLabel !== pattern) {
console.log(`Matched "${area}" to label "${matchedLabel}" (pattern was "${pattern}")`);
} else if (!matchedLabel) {
console.log(`Using pattern "${pattern}" for area "${area}" (no label match found)`);
}
}
// Extract "Areas Affected" field from issue body (only for bug reports)
// GitHub issue forms store dropdown selections as markdown lists
// Pattern matches: ### Areas Affected followed by bulleted list items
// Check for bug label (handles '🪲 bug' label)
const isBugReport = hasLabelMatching('bug') || issueBody.includes('### Steps to Reproduce');
if (!isBugReport) {
console.log('Not a bug report - skipping area label assignment.');
return;
}
// Try multiple patterns to find "Areas Affected" field
// GitHub formats dropdowns differently - try various patterns
let areasAffectedMatch = null;
const patterns = [
/###\s+Areas Affected\s*\n+([\s\S]*?)(?=\n###|$)/i,
/###\s+Areas Affected\s*([\s\S]*?)(?=\n###|$)/i,
/Areas Affected\s*\n+([\s\S]*?)(?=\n###|$)/i,
/Areas Affected\s*([\s\S]*?)(?=\n###|$)/i
];
for (const pattern of patterns) {
areasAffectedMatch = issueBody.match(pattern);
if (areasAffectedMatch) {
console.log(`Matched pattern: ${pattern}`);
break;
}
}
if (!areasAffectedMatch) {
console.log('No "Areas Affected" field found in issue body.');
console.log('Issue body preview:', issueBody.substring(0, 500));
return;
}
const areasText = areasAffectedMatch[1].trim();
console.log(`Raw areas text: "${areasText}"`);
// Check if field is empty or indicates no selection
if (!areasText ||
areasText.toLowerCase() === 'no selection' ||
areasText.toLowerCase() === 'none' ||
areasText === '_No response_' ||
areasText === '') {
console.log('No areas selected (field is empty).');
return;
}
// Extract bullet list items (lines starting with - or *)
// Handle both single and multiple selections
let selectedAreas = [];
// First, try splitting by newlines (for multiple selections)
const lines = areasText.split(/\r?\n/);
console.log(`Split into ${lines.length} lines`);
for (const line of lines) {
// Remove bullet markers (-, *, •) and trim
let cleaned = line.replace(/^[-*•]\s*/, '').trim();
// Also handle cases where there might be extra whitespace or formatting
cleaned = cleaned.replace(/^-\s*/, '').trim();
console.log(`Processing line: "${line}" -> "${cleaned}"`);
// Check if this is a valid area
if (cleaned.length > 0 &&
!cleaned.match(/^<!--/) &&
!cleaned.match(/^###/) &&
areaToLabel[cleaned] !== undefined) {
console.log(`Found valid area: ${cleaned}`);
selectedAreas.push(cleaned);
}
}
// If no areas found from splitting, check if the whole text is a single area
if (selectedAreas.length === 0) {
const trimmed = areasText.trim();
console.log(`No areas from splitting, checking whole text: "${trimmed}"`);
if (areaToLabel[trimmed] !== undefined) {
console.log(`Found single area: ${trimmed}`);
selectedAreas.push(trimmed);
}
}
if (!selectedAreas.length) {
console.log('No areas selected.');
return;
}
console.log(`Found selected areas: ${selectedAreas.join(', ')}`);
// Map areas to labels
const labelsToAdd = selectedAreas
.map(area => areaToLabel[area] || area)
.filter(label => label); // Remove any undefined/null values
if (!labelsToAdd.length) {
console.log('No valid labels to add.');
return;
}
// Get existing labels on the issue (reuse already extracted labels)
const existingLabels = labels;
// Only add labels that aren't already present
const newLabels = labelsToAdd.filter(label => !existingLabels.includes(label));
if (!newLabels.length) {
console.log('All area labels are already present on the issue.');
return;
}
console.log(`Adding labels: ${newLabels.join(', ')}`);
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issue.number,
labels: newLabels,
});
console.log(`Successfully added ${newLabels.length} label(s) to issue #${issue.number}`);
} catch (error) {
console.error(`Failed to add labels: ${error.message}`);
// If some labels don't exist, try adding them one by one
for (const label of newLabels) {
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: issue.number,
labels: [label],
});
console.log(`Added label: ${label}`);
} catch (labelError) {
console.warn(`Failed to add label "${label}": ${labelError.message}`);
}
}
}