diff --git a/README.md b/README.md index 5ecf9e4e..151092e8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # ![GIT Hooks + OpenAI - Generate GIT commit messages from OpenAI](https://raw.githubusercontent.com/guibranco/dotnet-aicommitmessage/main/docs/images/splash.png) 🧠 🤖 This tool generates AI-powered commit messages via Git hooks, automating meaningful message suggestions from OpenAI and others to improve commit quality and efficiency. @@ -249,6 +250,28 @@ When this option is enabled, the tool will: - Use fallback commit message generation (either the provided message or a placeholder) - Continue to work with branch name processing and issue number extraction +### Ignore API Errors + +In environments where API calls may occasionally fail due to network restrictions, timeouts, or temporary service issues, you can configure the tool to gracefully handle these errors instead of failing completely. Set the following environment variable: + +```bash +export DOTNET_AICOMMITMESSAGE_IGNORE_API_ERRORS=true +``` + +Or on Windows: + +```cmd +set DOTNET_AICOMMITMESSAGE_IGNORE_API_ERRORS=true +``` + +When this option is enabled, the tool will: +- Catch and suppress API-related exceptions (network errors, timeouts, authentication failures, etc.) +- Display a warning message when an API error occurs but is ignored +- Fall back to using the original commit message with branch name processing and issue number extraction +- Continue the commit process without interruption + +This is particularly useful in CI/CD pipelines or developer environments where occasional API issues shouldn't block the commit process. + ### Contributors diff --git a/Src/AiCommitMessage/Services/GenerateCommitMessageService.cs b/Src/AiCommitMessage/Services/GenerateCommitMessageService.cs index db769150..c2359a67 100644 --- a/Src/AiCommitMessage/Services/GenerateCommitMessageService.cs +++ b/Src/AiCommitMessage/Services/GenerateCommitMessageService.cs @@ -121,7 +121,18 @@ public string GenerateCommitMessage(GenerateCommitMessageOptions options) + (string.IsNullOrEmpty(diff) ? "" : diff); var model = EnvironmentLoader.LoadModelName(); - return GenerateWithModel(model, formattedMessage, branch, message, options.Debug); + + try + { + return GenerateWithModel(model, formattedMessage, branch, message, options.Debug); + } + catch (Exception ex) when (EnvironmentLoader.ShouldIgnoreApiErrors() && IsApiException(ex)) + { + Output.WarningLine( + "⚠️ AI API error occurred but was ignored due to DOTNET_AICOMMITMESSAGE_IGNORE_API_ERRORS setting. Falling back to original message." + ); + return PostProcess(message, branch, message); + } } private static string FilterPackageLockDiff(string diff) @@ -438,4 +449,20 @@ private static GitProvider GetGitProvider() return GitProvider.Unidentified; } -} + + /// + /// Determines whether the specified exception is an API-related exception. + /// + /// The exception to check. + /// true if the exception is API-related; otherwise, false. + private static bool IsApiException(Exception exception) + { + return exception is HttpRequestException + || exception is TaskCanceledException + || exception is InvalidOperationException + || exception is ClientResultException + || exception is RequestFailedException + || exception.InnerException is HttpRequestException + || exception.InnerException is TaskCanceledException; + } +} \ No newline at end of file diff --git a/Src/AiCommitMessage/Utility/EnvironmentLoader.cs b/Src/AiCommitMessage/Utility/EnvironmentLoader.cs index 4ff1763a..5d9bad5d 100644 --- a/Src/AiCommitMessage/Utility/EnvironmentLoader.cs +++ b/Src/AiCommitMessage/Utility/EnvironmentLoader.cs @@ -133,6 +133,13 @@ public static bool LoadOptionalEmoji() => public static bool IsApiDisabled() => bool.Parse(GetEnvironmentVariable("DOTNET_AICOMMITMESSAGE_DISABLE_API", "false")); + /// + /// Checks if API errors should be ignored via environment variable. + /// + /// true if API errors should be ignored, false otherwise. + public static bool ShouldIgnoreApiErrors() => + bool.Parse(GetEnvironmentVariable("DOTNET_AICOMMITMESSAGE_IGNORE_API_ERRORS", "false")); + /// /// Decrypts the specified encrypted text using Base64 decoding. /// diff --git a/Tests/AiCommitMessage.Tests/Integration/DisableApiIntegrationTests.cs b/Tests/AiCommitMessage.Tests/Integration/DisableApiIntegrationTests.cs index 27056935..90a86db0 100644 --- a/Tests/AiCommitMessage.Tests/Integration/DisableApiIntegrationTests.cs +++ b/Tests/AiCommitMessage.Tests/Integration/DisableApiIntegrationTests.cs @@ -77,4 +77,44 @@ public void SettingsService_Should_WorkCorrectly_When_ApiDisabled() ); } } -} + + /// + /// Tests the complete workflow when API errors are ignored. + /// + [Fact] + public void CompleteWorkflow_Should_WorkCorrectly_When_ApiErrorsIgnored() + { + // Arrange + Environment.SetEnvironmentVariable( + "DOTNET_AICOMMITMESSAGE_IGNORE_API_ERRORS", + "true", + EnvironmentVariableTarget.Process + ); + + var service = new GenerateCommitMessageService(); + var options = new GenerateCommitMessageOptions + { + Branch = "feature/285-ignore-api-errors", + Diff = "Added new environment variable support", + Message = "Add option to ignore API errors -skipai", // Use skipai to avoid actual API calls + }; + + try + { + // Act + var result = service.GenerateCommitMessage(options); + + // Assert + result.Should().Be("#285 Add option to ignore API errors"); + } + finally + { + // Cleanup + Environment.SetEnvironmentVariable( + "DOTNET_AICOMMITMESSAGE_IGNORE_API_ERRORS", + null, + EnvironmentVariableTarget.Process + ); + } + } +} \ No newline at end of file diff --git a/Tests/AiCommitMessage.Tests/Services/ApiErrorHandlingTests.cs b/Tests/AiCommitMessage.Tests/Services/ApiErrorHandlingTests.cs new file mode 100644 index 00000000..6b18790f --- /dev/null +++ b/Tests/AiCommitMessage.Tests/Services/ApiErrorHandlingTests.cs @@ -0,0 +1,69 @@ +using System.ClientModel; +using AiCommitMessage.Options; +using AiCommitMessage.Services; +using Azure; +using FluentAssertions; + +namespace AiCommitMessage.Tests.Services; + +/// +/// Tests for API error handling functionality in GenerateCommitMessageService. +/// +public class ApiErrorHandlingTests +{ + private readonly GenerateCommitMessageService _service; + + public ApiErrorHandlingTests() + { + _service = new GenerateCommitMessageService(); + } + + /// + /// Tests that the service handles API errors gracefully when ignore API errors is enabled. + /// + [Fact] + public void GenerateCommitMessage_Should_HandleApiErrors_When_IgnoreApiErrorsEnabled() + { + // Arrange + Environment.SetEnvironmentVariable( + "DOTNET_AICOMMITMESSAGE_IGNORE_API_ERRORS", + "true", + EnvironmentVariableTarget.Process + ); + Environment.SetEnvironmentVariable( + "DOTNET_AICOMMITMESSAGE_DISABLE_API", + "false", + EnvironmentVariableTarget.Process + ); + + var options = new GenerateCommitMessageOptions + { + Branch = "feature/285-test", + Diff = "Some diff", + Message = "Test commit -skipai", // Use skipai to avoid actual API calls but test the flow + }; + + try + { + // Act + var result = _service.GenerateCommitMessage(options); + + // Assert + result.Should().Be("#285 Test commit"); + } + finally + { + // Cleanup + Environment.SetEnvironmentVariable( + "DOTNET_AICOMMITMESSAGE_IGNORE_API_ERRORS", + null, + EnvironmentVariableTarget.Process + ); + Environment.SetEnvironmentVariable( + "DOTNET_AICOMMITMESSAGE_DISABLE_API", + null, + EnvironmentVariableTarget.Process + ); + } + } +} \ No newline at end of file diff --git a/Tests/AiCommitMessage.Tests/Utility/EnvironmentLoaderTests.cs b/Tests/AiCommitMessage.Tests/Utility/EnvironmentLoaderTests.cs index 201fab43..f1d82feb 100644 --- a/Tests/AiCommitMessage.Tests/Utility/EnvironmentLoaderTests.cs +++ b/Tests/AiCommitMessage.Tests/Utility/EnvironmentLoaderTests.cs @@ -88,4 +88,88 @@ public void IsApiDisabled_Should_ReturnFalse_When_EnvironmentVariableIsFalse() ); } } + + /// + /// Tests that ShouldIgnoreApiErrors returns false when the environment variable is not set. + /// + [Fact] + public void ShouldIgnoreApiErrors_Should_ReturnFalse_When_EnvironmentVariableNotSet() + { + // Arrange + Environment.SetEnvironmentVariable( + "DOTNET_AICOMMITMESSAGE_IGNORE_API_ERRORS", + null, + EnvironmentVariableTarget.Process + ); + + // Act + var result = EnvironmentLoader.ShouldIgnoreApiErrors(); + + // Assert + result.Should().BeFalse(); + } + + /// + /// Tests that ShouldIgnoreApiErrors returns true when the environment variable is set to "true". + /// + [Fact] + public void ShouldIgnoreApiErrors_Should_ReturnTrue_When_EnvironmentVariableIsTrue() + { + // Arrange + Environment.SetEnvironmentVariable( + "DOTNET_AICOMMITMESSAGE_IGNORE_API_ERRORS", + "true", + EnvironmentVariableTarget.Process + ); + + try + { + // Act + var result = EnvironmentLoader.ShouldIgnoreApiErrors(); + + // Assert + result.Should().BeTrue(); + } + finally + { + // Cleanup + Environment.SetEnvironmentVariable( + "DOTNET_AICOMMITMESSAGE_IGNORE_API_ERRORS", + null, + EnvironmentVariableTarget.Process + ); + } + } + + /// + /// Tests that ShouldIgnoreApiErrors returns false when the environment variable is set to "false". + /// + [Fact] + public void ShouldIgnoreApiErrors_Should_ReturnFalse_When_EnvironmentVariableIsFalse() + { + // Arrange + Environment.SetEnvironmentVariable( + "DOTNET_AICOMMITMESSAGE_IGNORE_API_ERRORS", + "false", + EnvironmentVariableTarget.Process + ); + + try + { + // Act + var result = EnvironmentLoader.ShouldIgnoreApiErrors(); + + // Assert + result.Should().BeFalse(); + } + finally + { + // Cleanup + Environment.SetEnvironmentVariable( + "DOTNET_AICOMMITMESSAGE_IGNORE_API_ERRORS", + null, + EnvironmentVariableTarget.Process + ); + } + } }