From 7056ebed88ee9d4384a77d25fbdbab1e669679fa Mon Sep 17 00:00:00 2001 From: Kevin Bost Date: Mon, 13 Apr 2026 11:32:29 -0700 Subject: [PATCH 1/2] Prefix MergeButler token environment variables Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/skills/doc-writer/SKILL.md | 6 +- .../Commands/PlatformServiceFactoryTests.cs | 61 +++++++++++++++++-- .../Mcp/PullRequestToolsTests.cs | 17 ++++-- MergeButler/Commands/EvaluateCommand.cs | 6 +- .../Commands/PlatformServiceFactory.cs | 17 ++++-- MergeButler/Mcp/PullRequestTools.cs | 12 ++-- README.md | 14 ++--- 7 files changed, 101 insertions(+), 32 deletions(-) diff --git a/.github/skills/doc-writer/SKILL.md b/.github/skills/doc-writer/SKILL.md index 294e064..9fe7526 100644 --- a/.github/skills/doc-writer/SKILL.md +++ b/.github/skills/doc-writer/SKILL.md @@ -90,7 +90,7 @@ MergeButler evaluate --config --pr --platform | `--config` | `-c` | Yes | Path to the YAML configuration file | | `--pr` | | Yes | URL of the pull request to evaluate | | `--platform` | `-p` | Yes | `GitHub` or `AzureDevOps` | -| `--token` | `-t` | No | Auth token. Defaults to `GITHUB_TOKEN` or `AZURE_DEVOPS_TOKEN` env var | +| `--token` | `-t` | No | Auth token. Defaults to `MERGEBUTLER__GITHUB_TOKEN` or `MERGEBUTLER__AZURE_DEVOPS_TOKEN` env var | | `--dry-run` | `-n` | No | Evaluate without submitting an approval | ### `config show` @@ -240,8 +240,8 @@ Use GitHub Copilot to evaluate the PR diff against a natural-language prompt. | Platform | Library | Token env var | |----------|---------|---------------| -| GitHub | [Octokit](https://github.com/octokit/octokit.net) | `GITHUB_TOKEN` | -| Azure DevOps | REST API | `AZURE_DEVOPS_TOKEN` | +| GitHub | [Octokit](https://github.com/octokit/octokit.net) | `MERGEBUTLER__GITHUB_TOKEN` | +| Azure DevOps | REST API | `MERGEBUTLER__AZURE_DEVOPS_TOKEN` | ## Documentation Maintenance diff --git a/MergeButler.Tests/Commands/PlatformServiceFactoryTests.cs b/MergeButler.Tests/Commands/PlatformServiceFactoryTests.cs index cd8bfa0..4d6585c 100644 --- a/MergeButler.Tests/Commands/PlatformServiceFactoryTests.cs +++ b/MergeButler.Tests/Commands/PlatformServiceFactoryTests.cs @@ -4,6 +4,16 @@ namespace MergeButler.Tests.Commands; public class PlatformServiceFactoryTests { + [Theory] + [InlineData(Platform.GitHub, PlatformServiceFactory.GitHubTokenEnvironmentVariable)] + [InlineData(Platform.AzureDevOps, PlatformServiceFactory.AzureDevOpsTokenEnvironmentVariable)] + public void GetTokenEnvironmentVariableName_ReturnsPrefixedName(Platform platform, string expected) + { + string result = PlatformServiceFactory.GetTokenEnvironmentVariableName(platform); + + Assert.Equal(expected, result); + } + [Theory] [InlineData("GitHub", Platform.GitHub)] [InlineData("github", Platform.GitHub)] @@ -35,13 +45,52 @@ public void ResolveToken_ProvidedToken_ReturnsProvided() Assert.Equal("my-token", result); } - [Fact] - public void ResolveToken_NullToken_FallsBackToEnvVar() + [Theory] + [InlineData(Platform.GitHub, PlatformServiceFactory.GitHubTokenEnvironmentVariable, "GITHUB_TOKEN")] + [InlineData(Platform.AzureDevOps, PlatformServiceFactory.AzureDevOpsTokenEnvironmentVariable, "AZURE_DEVOPS_TOKEN")] + public void ResolveToken_NullToken_FallsBackToPrefixedEnvVar(Platform platform, string prefixedEnvVar, string legacyEnvVar) + { + string? savedPrefixedToken = Environment.GetEnvironmentVariable(prefixedEnvVar); + string? savedLegacyToken = Environment.GetEnvironmentVariable(legacyEnvVar); + + try + { + Environment.SetEnvironmentVariable(prefixedEnvVar, "prefixed-token"); + Environment.SetEnvironmentVariable(legacyEnvVar, "legacy-token"); + + string? result = PlatformServiceFactory.ResolveToken(platform, null); + + Assert.Equal("prefixed-token", result); + } + finally + { + Environment.SetEnvironmentVariable(prefixedEnvVar, savedPrefixedToken); + Environment.SetEnvironmentVariable(legacyEnvVar, savedLegacyToken); + } + } + + [Theory] + [InlineData(Platform.GitHub, PlatformServiceFactory.GitHubTokenEnvironmentVariable, "GITHUB_TOKEN")] + [InlineData(Platform.AzureDevOps, PlatformServiceFactory.AzureDevOpsTokenEnvironmentVariable, "AZURE_DEVOPS_TOKEN")] + public void ResolveToken_NullToken_DoesNotUseLegacyEnvVar(Platform platform, string prefixedEnvVar, string legacyEnvVar) { - // If env var is not set, returns null - string? result = PlatformServiceFactory.ResolveToken(Platform.GitHub, null); - // Can't assert specific value since it depends on env, but shouldn't throw - Assert.True(result is null || result.Length > 0); + string? savedPrefixedToken = Environment.GetEnvironmentVariable(prefixedEnvVar); + string? savedLegacyToken = Environment.GetEnvironmentVariable(legacyEnvVar); + + try + { + Environment.SetEnvironmentVariable(prefixedEnvVar, null); + Environment.SetEnvironmentVariable(legacyEnvVar, "legacy-token"); + + string? result = PlatformServiceFactory.ResolveToken(platform, null); + + Assert.Null(result); + } + finally + { + Environment.SetEnvironmentVariable(prefixedEnvVar, savedPrefixedToken); + Environment.SetEnvironmentVariable(legacyEnvVar, savedLegacyToken); + } } [Fact] diff --git a/MergeButler.Tests/Mcp/PullRequestToolsTests.cs b/MergeButler.Tests/Mcp/PullRequestToolsTests.cs index af8aadd..869e5b0 100644 --- a/MergeButler.Tests/Mcp/PullRequestToolsTests.cs +++ b/MergeButler.Tests/Mcp/PullRequestToolsTests.cs @@ -1,3 +1,4 @@ +using MergeButler.Commands; using MergeButler.Mcp; namespace MergeButler.Tests.Mcp; @@ -8,9 +9,11 @@ public class PullRequestToolsTests public async Task GradePullRequest_NoToken_ReturnsError() { // Ensure env var is not set for this test - string? savedToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); + string? savedToken = Environment.GetEnvironmentVariable(PlatformServiceFactory.GitHubTokenEnvironmentVariable); + string? savedLegacyToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); try { + Environment.SetEnvironmentVariable(PlatformServiceFactory.GitHubTokenEnvironmentVariable, null); Environment.SetEnvironmentVariable("GITHUB_TOKEN", null); string result = await PullRequestTools.GradePullRequest( @@ -21,10 +24,12 @@ public async Task GradePullRequest_NoToken_ReturnsError() Assert.Contains("ERROR", result); Assert.Contains("token", result, StringComparison.OrdinalIgnoreCase); + Assert.Contains(PlatformServiceFactory.GitHubTokenEnvironmentVariable, result); } finally { - Environment.SetEnvironmentVariable("GITHUB_TOKEN", savedToken); + Environment.SetEnvironmentVariable(PlatformServiceFactory.GitHubTokenEnvironmentVariable, savedToken); + Environment.SetEnvironmentVariable("GITHUB_TOKEN", savedLegacyToken); } } @@ -42,9 +47,11 @@ await Assert.ThrowsAsync(() => [Fact] public async Task ApprovePullRequest_NoToken_ReturnsError() { - string? savedToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); + string? savedToken = Environment.GetEnvironmentVariable(PlatformServiceFactory.GitHubTokenEnvironmentVariable); + string? savedLegacyToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); try { + Environment.SetEnvironmentVariable(PlatformServiceFactory.GitHubTokenEnvironmentVariable, null); Environment.SetEnvironmentVariable("GITHUB_TOKEN", null); string result = await PullRequestTools.ApprovePullRequest( @@ -55,10 +62,12 @@ public async Task ApprovePullRequest_NoToken_ReturnsError() Assert.Contains("ERROR", result); Assert.Contains("token", result, StringComparison.OrdinalIgnoreCase); + Assert.Contains(PlatformServiceFactory.GitHubTokenEnvironmentVariable, result); } finally { - Environment.SetEnvironmentVariable("GITHUB_TOKEN", savedToken); + Environment.SetEnvironmentVariable(PlatformServiceFactory.GitHubTokenEnvironmentVariable, savedToken); + Environment.SetEnvironmentVariable("GITHUB_TOKEN", savedLegacyToken); } } diff --git a/MergeButler/Commands/EvaluateCommand.cs b/MergeButler/Commands/EvaluateCommand.cs index 52cd07e..78fef76 100644 --- a/MergeButler/Commands/EvaluateCommand.cs +++ b/MergeButler/Commands/EvaluateCommand.cs @@ -28,7 +28,8 @@ public static Command Create() Option tokenOption = new("--token", ["-t"]) { - Description = "Authentication token for the platform API. Defaults to GITHUB_TOKEN or AZURE_DEVOPS_TOKEN environment variable." + Description = "Authentication token for the platform API. Defaults to " + + PlatformServiceFactory.TokenEnvironmentVariableNames + " environment variable." }; Option dryRunOption = new("--dry-run", ["-n"]) @@ -110,7 +111,8 @@ internal static async Task ExecuteAsync( if (string.IsNullOrWhiteSpace(token)) { - output.WriteLine("Error: No authentication token provided. Use --token or set the appropriate environment variable."); + output.WriteLine( + $"Error: No authentication token provided. Use --token or set the {PlatformServiceFactory.GetTokenEnvironmentVariableName(platform)} environment variable."); return; } diff --git a/MergeButler/Commands/PlatformServiceFactory.cs b/MergeButler/Commands/PlatformServiceFactory.cs index a11087d..65a7ed5 100644 --- a/MergeButler/Commands/PlatformServiceFactory.cs +++ b/MergeButler/Commands/PlatformServiceFactory.cs @@ -9,12 +9,21 @@ namespace MergeButler.Commands; /// public static class PlatformServiceFactory { + public const string EnvironmentVariablePrefix = "MERGEBUTLER__"; + public const string GitHubTokenEnvironmentVariable = EnvironmentVariablePrefix + "GITHUB_TOKEN"; + public const string AzureDevOpsTokenEnvironmentVariable = EnvironmentVariablePrefix + "AZURE_DEVOPS_TOKEN"; + public const string TokenEnvironmentVariableNames = + GitHubTokenEnvironmentVariable + " or " + AzureDevOpsTokenEnvironmentVariable; + public static string? ResolveToken(Platform platform, string? providedToken) => - providedToken ?? platform switch + providedToken ?? Environment.GetEnvironmentVariable(GetTokenEnvironmentVariableName(platform)); + + public static string GetTokenEnvironmentVariableName(Platform platform) => + platform switch { - Platform.GitHub => Environment.GetEnvironmentVariable("GITHUB_TOKEN"), - Platform.AzureDevOps => Environment.GetEnvironmentVariable("AZURE_DEVOPS_TOKEN"), - _ => null + Platform.GitHub => GitHubTokenEnvironmentVariable, + Platform.AzureDevOps => AzureDevOpsTokenEnvironmentVariable, + _ => throw new ArgumentOutOfRangeException(nameof(platform), platform, "Unsupported platform.") }; public static IPullRequestService CreateService(Platform platform, string token) diff --git a/MergeButler/Mcp/PullRequestTools.cs b/MergeButler/Mcp/PullRequestTools.cs index 502ce09..21ee66a 100644 --- a/MergeButler/Mcp/PullRequestTools.cs +++ b/MergeButler/Mcp/PullRequestTools.cs @@ -13,12 +13,12 @@ public class PullRequestTools { [McpServerTool(Name = "grade_pull_request"), Description("Evaluate a pull request against MergeButler rules and return the grading result. " + - "This does NOT approve the PR — use approve_pull_request for that.")] + "This does NOT approve the PR — use approve_pull_request for that.")] public static async Task GradePullRequest( [Description("Full URL of the pull request (e.g. https://github.com/owner/repo/pull/42)")] string prUrl, [Description("Platform hosting the PR: GitHub, AzureDevOps, or azdo. If not specified, inferred from git remotes.")] string? platform = null, [Description("Path to MergeButler YAML config file. When omitted, the effective config is built from the default user and repo locations.")] string? configPath = null, - [Description("Auth token for the platform API. If not provided, falls back to GITHUB_TOKEN or AZURE_DEVOPS_TOKEN environment variable.")] string? token = null, + [Description("Auth token for the platform API. If not provided, falls back to " + PlatformServiceFactory.TokenEnvironmentVariableNames + " environment variable.")] string? token = null, CancellationToken cancellationToken = default) { Platform platformEnum; @@ -39,7 +39,7 @@ public static async Task GradePullRequest( token = PlatformServiceFactory.ResolveToken(platformEnum, token); if (string.IsNullOrWhiteSpace(token)) { - string envVar = platformEnum == Platform.GitHub ? "GITHUB_TOKEN" : "AZURE_DEVOPS_TOKEN"; + string envVar = PlatformServiceFactory.GetTokenEnvironmentVariableName(platformEnum); return $"ERROR: No authentication token provided. Please provide a token parameter or set the {envVar} environment variable."; } @@ -144,11 +144,11 @@ public static async Task GradePullRequest( [McpServerTool(Name = "approve_pull_request"), Description("Submit an approval on a pull request. " + - "Use grade_pull_request first to evaluate the PR before approving.")] + "Use grade_pull_request first to evaluate the PR before approving.")] public static async Task ApprovePullRequest( [Description("Full URL of the pull request (e.g. https://github.com/owner/repo/pull/42)")] string prUrl, [Description("Platform hosting the PR: GitHub, AzureDevOps, or azdo. If not specified, inferred from git remotes.")] string? platform = null, - [Description("Auth token for the platform API. If not provided, falls back to GITHUB_TOKEN or AZURE_DEVOPS_TOKEN environment variable.")] string? token = null, + [Description("Auth token for the platform API. If not provided, falls back to " + PlatformServiceFactory.TokenEnvironmentVariableNames + " environment variable.")] string? token = null, CancellationToken cancellationToken = default) { Platform platformEnum; @@ -169,7 +169,7 @@ public static async Task ApprovePullRequest( token = PlatformServiceFactory.ResolveToken(platformEnum, token); if (string.IsNullOrWhiteSpace(token)) { - string envVar = platformEnum == Platform.GitHub ? "GITHUB_TOKEN" : "AZURE_DEVOPS_TOKEN"; + string envVar = PlatformServiceFactory.GetTokenEnvironmentVariableName(platformEnum); return $"ERROR: No authentication token provided. Please provide a token parameter or set the {envVar} environment variable."; } diff --git a/README.md b/README.md index b89030b..184663a 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ MergeButler evaluate --config .mergebutler.yml --pr --platform --platform Date: Mon, 13 Apr 2026 11:37:51 -0700 Subject: [PATCH 2/2] Align test coverage with xUnit MTP v1 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 36c764d..efedb2b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,7 +19,7 @@ This would typically list all NuGet packages used within this solution. --> - + @@ -43,4 +43,4 @@ - \ No newline at end of file +