Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
496fb1e
first draft of issue labeler mcp using rag/llm
anannya03 Dec 18, 2025
db0c89d
second bunch of changes along with evaluation
anannya03 Dec 18, 2025
d227ef5
MCP Issues Label Prediction Modifications
anannya03 Jan 5, 2026
15c678c
deleted unnecessary files
anannya03 Jan 5, 2026
c56c319
Removed appSettings.json
anannya03 Jan 5, 2026
dcfdec7
Modified InitialTriage Rule for MCP and added a temp schedule job for…
anannya03 Jan 12, 2026
2404bc3
enable nullable
anannya03 Jan 12, 2026
b62250b
Merge branch 'add-mcp-labeler' into mcp-gh-issue-labeler
anannya03 Jan 13, 2026
562c596
GH Action change for label update
anannya03 Jan 13, 2026
cd28607
removed scheduleeventprocessing changes
anannya03 Jan 15, 2026
0f8a35d
code cleanup
anannya03 Jan 16, 2026
34ff588
Refactor ProcessScheduledEvent call in Program.cs
anannya03 Jan 16, 2026
fdcda54
test fix
anannya03 Jan 16, 2026
5835cf5
Merge branch 'mcp-gh-issue-labeler' of https://github.com/Azure/azure…
anannya03 Jan 16, 2026
63d46ce
deleted unnecessary test json
anannya03 Jan 16, 2026
0762ff9
Review comments fix - 1
anannya03 Jan 20, 2026
b7a8b49
resolved review comments- 2
anannya03 Jan 22, 2026
b212502
resolved eview comments- 3
anannya03 Jan 22, 2026
8e331b1
resolved review comments- 4
anannya03 Jan 22, 2026
cbe86ed
removed commented out code
anannya03 Jan 22, 2026
7219ceb
config driven server-team-mapping
anannya03 Jan 23, 2026
7c570a1
Few more changes
anannya03 Jan 23, 2026
c36f9ae
review comment resolution- 5
anannya03 Jan 25, 2026
d9fb86f
alphabetize the packages
anannya03 Jan 26, 2026
4388f55
sorted the usings
anannya03 Jan 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/event-processor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ jobs:
echo "::add-mask::$LABEL_SERVICE_API_KEY"
echo "LABEL_SERVICE_API_KEY=$LABEL_SERVICE_API_KEY" >> $GITHUB_ENV

APP_CONFIG_ENDPOINT=$(az keyvault secret show \
--vault-name issue-labeler \
-n app-config-endpoint \
-o tsv \
--query value)

echo "::add-mask::$APP_CONFIG_ENDPOINT"
echo "APP_CONFIG_ENDPOINT=$APP_CONFIG_ENDPOINT" >> $GITHUB_ENV

# To run github-event-processor built from source, for testing purposes, uncomment everything
# in between the Start/End-Build From Source comments and comment everything in between the
# Start/End-Install comments
Expand Down Expand Up @@ -95,6 +104,7 @@ jobs:
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LABEL_SERVICE_API_KEY: ${{ env.LABEL_SERVICE_API_KEY }}
APP_CONFIG_ENDPOINT: ${{ env.APP_CONFIG_ENDPOINT }}

- name: Archive github event data
uses: actions/upload-artifact@v6
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Azure.Sdk.Tools.GitHubEventProcessor.Configuration;
using Azure.Sdk.Tools.GitHubEventProcessor.Constants;
using Azure.Sdk.Tools.GitHubEventProcessor.EventProcessing;
using Azure.Sdk.Tools.GitHubEventProcessor.GitHubPayload;
using Azure.Sdk.Tools.GitHubEventProcessor.Utils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using NUnit.Framework;

namespace Azure.Sdk.Tools.GitHubEventProcessor.Tests.Static
{
[TestFixture]
[Parallelizable(ParallelScope.Children)]
public class McpIssueProcessingTests : ProcessingTestBase
{
/// <summary>
/// Test MCP InitialIssueTriage with various scenarios including:
/// - Predicted server and tool labels
/// - User-provided labels that conflict with predictions
/// - Code owner assignment
/// - Team notification comments when no owners found
/// - Customer-reported label logic
/// - needs-triage label removal
/// </summary>
/// <param name="rule">Rule being tested</param>
/// <param name="payloadFile">JSON payload file for the event</param>
/// <param name="ruleState">Whether InitialIssueTriage rule is on/off</param>
/// <param name="predictedLabels">Labels returned from AI triage service (comma-separated)</param>
/// <param name="userProvidedLabels">Labels already on the issue when opened (comma-separated)</param>
/// <param name="ownersWithAssignPermission">Owners with permission to be assigned (comma-separated)</param>
/// <param name="hasCodeownersEntry">Whether CODEOWNERS has entry for the labels</param>
/// <param name="isMemberOfOrg">Whether issue creator is member of microsoft org</param>
/// <param name="hasWriteOrAdmin">Whether issue creator has write/admin permission</param>
[Category("static")]
[NonParallelizable]

// Scenario: AI predicts server-azure.mcp label, no user labels, has code owner with permission
// Expected: server-azure.mcp label added, owner assigned, needs-team-attention added, comment posted
[TestCase(RulesConstants.InitialIssueTriage,
"Tests.JsonEventPayloads/McpIssueTriage_issue_opened_no_labels.json",
RuleState.On,
"server-azure.mcp",
"",
"McpOwner1",
true,
false,
false)]

// Scenario: AI predicts server-azure.mcp + tool-prompts.mcp, no owners found
// Expected: Both labels added, needs-team-triage added, team notification comment posted, customer-reported + question added
[TestCase(RulesConstants.InitialIssueTriage,
"Tests.JsonEventPayloads/McpIssueTriage_issue_opened_no_labels.json",
RuleState.On,
"server-azure.mcp, tools-prompts",
"",
null,
false,
false,
false)]

// Scenario: AI predicts server-azure.mcp, but user already added server-fabric.mcp (conflict)
// Expected: Only user's server-fabric.mcp kept, AI prediction ignored, no server-azure.mcp added
[TestCase(RulesConstants.InitialIssueTriage,
"Tests.JsonEventPayloads/McpIssueTriage_issue_opened_with_user_label.json",
RuleState.On,
"server-azure.mcp",
"server-fabric.mcp",
null,
false,
true,
true)]

// Scenario: AI predicts server-azure.mcp + tool-prompts.mcp, user added tool-prompts.mcp (partial match)
// Expected: server-azure.mcp added (prediction), tool-prompts.mcp kept (user choice), needs-triage removed if present
[TestCase(RulesConstants.InitialIssueTriage,
"Tests.JsonEventPayloads/McpIssueTriage_issue_opened_with_needs_triage.json",
RuleState.On,
"server-azure.mcp, tools-prompts",
"tools-prompts, needs-triage",
"McpOwner1",
true,
true,
false)]

// Scenario: AI predicts no labels (empty response)
// Expected: needs-triage label added, no other processing
[TestCase(RulesConstants.InitialIssueTriage,
"Tests.JsonEventPayloads/McpIssueTriage_issue_opened_no_labels.json",
RuleState.On,
"",
"",
null,
false,
false,
false)]

// Scenario: AI predicts server-fabric.mcp, multiple owners, only one has permission
// Expected: server-fabric.mcp added, owner with permission assigned, comment with all owners mentioned
[TestCase(RulesConstants.InitialIssueTriage,
"Tests.JsonEventPayloads/McpIssueTriage_issue_opened_no_labels.json",
RuleState.On,
"server-fabric.mcp",
"",
"McpOwner2",
true,
true,
true)]

// Scenario: Rule is disabled
// Expected: No processing, no updates
[TestCase(RulesConstants.InitialIssueTriage,
"Tests.JsonEventPayloads/McpIssueTriage_issue_opened_no_labels.json",
RuleState.Off,
"server-azure.mcp",
"",
null,
false,
false,
false)]

public async Task TestMcpInitialIssueTriage(
string rule,
string payloadFile,
RuleState ruleState,
string predictedLabels,
string userProvidedLabels,
string ownersWithAssignPermission,
bool hasCodeownersEntry,
bool isMemberOfOrg,
bool hasWriteOrAdmin)
{
var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var logger = loggerFactory.CreateLogger<McpIssueProcessing>();

var mockGitHubEventClient = new MockGitHubEventClient(OrgConstants.ProductHeaderName);
mockGitHubEventClient.RulesConfiguration.Rules[rule] = ruleState;
mockGitHubEventClient.UserHasPermissionsReturn = hasWriteOrAdmin;
mockGitHubEventClient.IsUserMemberOfOrgReturn = isMemberOfOrg;

var rawJson = TestHelpers.GetTestEventPayload(payloadFile);
var issueEventPayload = SimpleJsonSerializer.Deserialize<IssueEventGitHubPayload>(rawJson);

var expectedPredictedLabels = new List<string>();
if (!string.IsNullOrEmpty(predictedLabels))
{
expectedPredictedLabels = predictedLabels.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
}

mockGitHubEventClient.AIServiceLabels = expectedPredictedLabels;
mockGitHubEventClient.AIServiceAnswer = null;
mockGitHubEventClient.AIServiceAnswerType = null;

if (hasCodeownersEntry)
{
CodeOwnerUtils.ResetCodeOwnerEntries();
CodeOwnerUtils.codeOwnersFilePathOverride = "Tests.FakeCodeowners/McpCodeowners";
}

if (!string.IsNullOrEmpty(ownersWithAssignPermission))
{
var ownersWithPermission = ownersWithAssignPermission.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();

mockGitHubEventClient.OwnersWithAssignPermission = ownersWithPermission;
}

var mcpProcessor = new McpIssueProcessing(logger, CreateTestMcpConfiguration());

await mcpProcessor.ProcessIssueEvent(mockGitHubEventClient, issueEventPayload);

Assert.That(mockGitHubEventClient.RulesConfiguration.RuleEnabled(rule),
Is.EqualTo(ruleState == RuleState.On),
$"Rule '{rule}' enabled should have been {ruleState == RuleState.On}");

var totalUpdates = await mockGitHubEventClient.ProcessPendingUpdates(
issueEventPayload.Repository.Id,
issueEventPayload.Issue.Number);

if (ruleState == RuleState.Off)
{
Assert.That(totalUpdates, Is.EqualTo(0), "Expected no updates when rule is disabled");
}
else if (string.IsNullOrEmpty(predictedLabels))
{
Assert.That(mockGitHubEventClient.GetLabelsToAdd(), Does.Contain(TriageLabelConstants.NeedsTriage));

if (!isMemberOfOrg && !hasWriteOrAdmin)
{
Assert.That(mockGitHubEventClient.GetLabelsToAdd(), Does.Contain(TriageLabelConstants.CustomerReported));
Assert.That(mockGitHubEventClient.GetLabelsToAdd(), Does.Contain(TriageLabelConstants.Question));
}
}
else
{
var userLabels = string.IsNullOrEmpty(userProvidedLabels)
? new List<string>()
: userProvidedLabels.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();

var labelsToAdd = mockGitHubEventClient.GetLabelsToAdd();
var serverPredicted = expectedPredictedLabels.FirstOrDefault(l => l.StartsWith("server-", StringComparison.OrdinalIgnoreCase));
var toolPredicted = expectedPredictedLabels.FirstOrDefault(l => l.StartsWith("tools-", StringComparison.OrdinalIgnoreCase));

if (serverPredicted != null)
{
var userHasServerLabel = userLabels.Any(l => l.StartsWith("server-", StringComparison.OrdinalIgnoreCase));
if (!userHasServerLabel)
{
Assert.That(labelsToAdd, Does.Contain(serverPredicted),
$"Expected predicted server label '{serverPredicted}' to be added");
}
}

if (toolPredicted != null)
{
var userHasToolLabel = userLabels.Any(l => l.StartsWith("tools-", StringComparison.OrdinalIgnoreCase));
if (!userHasToolLabel)
{
Assert.That(labelsToAdd, Does.Contain(toolPredicted),
$"Expected predicted tool label '{toolPredicted}' to be added");
}
}

if (!string.IsNullOrEmpty(ownersWithAssignPermission) && hasCodeownersEntry)
{
Assert.That(labelsToAdd, Does.Contain(TriageLabelConstants.NeedsTeamAttention));
}
else if (serverPredicted != null || toolPredicted != null)
{
Assert.That(labelsToAdd, Does.Contain(TriageLabelConstants.NeedsTeamTriage));
}

if (userLabels.Contains(TriageLabelConstants.NeedsTriage, StringComparer.OrdinalIgnoreCase))
{
var labelsToRemove = mockGitHubEventClient.GetLabelsToRemove();
Assert.That(labelsToRemove, Does.Contain(TriageLabelConstants.NeedsTriage),
"Expected needs-triage to be removed when valid predicted labels exist");
}

if (!isMemberOfOrg && !hasWriteOrAdmin)
{
Assert.That(labelsToAdd, Does.Contain(TriageLabelConstants.CustomerReported));
Assert.That(labelsToAdd, Does.Contain(TriageLabelConstants.Question));
}
}
}

/// <summary>
/// Creates a test McpConfiguration with mock server team mappings.
/// </summary>
private static McpConfiguration CreateTestMcpConfiguration()
{
var configData = new Dictionary<string, string?>
{
{ "microsoft/mcp:ServerTeamMappings", "server-Azure.Mcp=@microsoft/azure-mcp;server-Fabric.Mcp=@microsoft/fabric-mcp" }
};

var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(configData)
.Build();

return new McpConfiguration(configuration);
}
}
}
Loading