diff --git a/src/Cake.Common.Tests/Unit/Build/GitLabCI/Data/GitLabCIBuildInfoTests.cs b/src/Cake.Common.Tests/Unit/Build/GitLabCI/Data/GitLabCIBuildInfoTests.cs index 86ebe58d8e..14b9ace302 100644 --- a/src/Cake.Common.Tests/Unit/Build/GitLabCI/Data/GitLabCIBuildInfoTests.cs +++ b/src/Cake.Common.Tests/Unit/Build/GitLabCI/Data/GitLabCIBuildInfoTests.cs @@ -2,7 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Cake.Common.Build.GitLabCI.Data; using Cake.Common.Tests.Fixtures.Build; +using NSubstitute; using Xunit; namespace Cake.Common.Tests.Unit.Build.GitLabCI.Data @@ -362,5 +364,41 @@ public void Should_Return_Correct_Value() Assert.Equal(42, result); } } + + public sealed class TheSourceProperty + { + // Values taken from https://docs.gitlab.com/ee/ci/jobs/job_rules.html#ci_pipeline_source-predefined-variable + [Theory] + [InlineData("", null)] + [InlineData("unknown_source", null)] + [InlineData("api", GitLabCIPipelineSource.Api)] + [InlineData("chat", GitLabCIPipelineSource.Chat)] + [InlineData("external", GitLabCIPipelineSource.External)] + [InlineData("external_pull_request_event", GitLabCIPipelineSource.ExternalPullRequestEvent)] + [InlineData("merge_request_event", GitLabCIPipelineSource.MergeRequestEvent)] + [InlineData("ondemand_dast_scan", GitLabCIPipelineSource.OnDemandDastScan)] + [InlineData("ondemand_dast_validation", GitLabCIPipelineSource.OnDemandDastValidation)] + [InlineData("parent_pipeline", GitLabCIPipelineSource.ParentPipeline)] + [InlineData("pipeline", GitLabCIPipelineSource.Pipeline)] + [InlineData("push", GitLabCIPipelineSource.Push)] + [InlineData("schedule", GitLabCIPipelineSource.Schedule)] + [InlineData("security_orchestration_policy", GitLabCIPipelineSource.SecurityOrchestrationPolicy)] + [InlineData("trigger", GitLabCIPipelineSource.Trigger)] + [InlineData("web", GitLabCIPipelineSource.Web)] + [InlineData("webide", GitLabCIPipelineSource.WebIde)] + public void Should_Return_Correct_Value(string pipelineSourceEnvironmentVariable, GitLabCIPipelineSource? expectedSource) + { + // Given + var fixture = new GitLabCIInfoFixture(); + fixture.Environment.GetEnvironmentVariable("CI_PIPELINE_SOURCE").Returns(pipelineSourceEnvironmentVariable); + var info = fixture.CreateBuildInfo(); + + // When + var result = info.Source; + + // Then + Assert.Equal(expectedSource, result); + } + } } } diff --git a/src/Cake.Common/Build/GitLabCI/Data/GitLabCIBuildInfo.cs b/src/Cake.Common/Build/GitLabCI/Data/GitLabCIBuildInfo.cs index d97d18c5ef..c811bcf48d 100644 --- a/src/Cake.Common/Build/GitLabCI/Data/GitLabCIBuildInfo.cs +++ b/src/Cake.Common/Build/GitLabCI/Data/GitLabCIBuildInfo.cs @@ -131,5 +131,13 @@ public GitLabCIBuildInfo(ICakeEnvironment environment) /// The email address of the user. /// public string UserEmail => GetEnvironmentString("GITLAB_USER_EMAIL"); + + /// + /// Gets how the pipeline was triggered. + /// + /// + /// The trigger of the pipeline as or null if the trigger could not be determined. + /// + public GitLabCIPipelineSource? Source => GetEnvironmentEnum("CI_PIPELINE_SOURCE"); } } diff --git a/src/Cake.Common/Build/GitLabCI/Data/GitLabCIPipelineSource.cs b/src/Cake.Common/Build/GitLabCI/Data/GitLabCIPipelineSource.cs new file mode 100644 index 0000000000..0d2b3ea8d6 --- /dev/null +++ b/src/Cake.Common/Build/GitLabCI/Data/GitLabCIPipelineSource.cs @@ -0,0 +1,91 @@ +using System.Runtime.Serialization; + +namespace Cake.Common.Build.GitLabCI.Data; + +/// +/// Enumerates possible triggers of a GitLab CI pipeline. +/// +/// CI_PIPELINE_SOURCE predefined variable (GitLab Documentation) +public enum GitLabCIPipelineSource +{ + /// + /// Pipeline var triggered by the pipelines API. + /// + Api, + + /// + /// Pipeline was created using a GitLab ChatOps command. + /// + Chat, + + /// + /// Pipeline was created using a CI service other than GitLab + /// + External, + + /// + /// Pipeline was created because an external pull request on GitHub was created or updated. + /// + [EnumMember(Value = "external_pull_request_event")] + ExternalPullRequestEvent, + + /// + /// Pipeline was created because a merge request was created or updated. + /// + [EnumMember(Value = "merge_request_event")] + MergeRequestEvent, + + /// + /// Pipeline is an on-demand DAST scan pipeline. + /// + [EnumMember(Value = "ondemand_dast_scan")] + OnDemandDastScan, + + /// + /// Pipeline is an on-demand DAST validation pipeline. + /// + [EnumMember(Value = "ondemand_dast_validation")] + OnDemandDastValidation, + + /// + /// Pipeline was created by a parent pipeline. + /// + [EnumMember(Value = "parent_pipeline")] + ParentPipeline, + + /// + /// Pipeline was created by another pipeline + /// + Pipeline, + + /// + /// Pipelune was triggered by a Git push event. + /// + Push, + + /// + /// Pipeline was triggered by a schedule. + /// + Schedule, + + /// + /// Pipeline is a security orchestration policy pipeline. + /// + [EnumMember(Value = "security_orchestration_policy")] + SecurityOrchestrationPolicy, + + /// + /// Pipeline was created using a trigger token. + /// + Trigger, + + /// + /// Pipeline was created by selecting New Pipeline in the GitLab UI. + /// + Web, + + /// + /// Pipeline was created using the Web IDE. + /// + WebIde +} \ No newline at end of file diff --git a/src/Cake.Common/Build/GitLabCI/GitLabCIInfo.cs b/src/Cake.Common/Build/GitLabCI/GitLabCIInfo.cs index bcbb7a7251..03a3bd2dc7 100644 --- a/src/Cake.Common/Build/GitLabCI/GitLabCIInfo.cs +++ b/src/Cake.Common/Build/GitLabCI/GitLabCIInfo.cs @@ -3,6 +3,9 @@ // See the LICENSE file in the project root for more information. using System; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; using Cake.Core; namespace Cake.Common.Build.GitLabCI @@ -99,5 +102,48 @@ protected bool GetEnvironmentBoolean(string primaryVariable, string secondaryVar { return GetEnvironmentBoolean(primaryVariable) ? GetEnvironmentBoolean(primaryVariable) : GetEnvironmentBoolean(secondaryVariable); } + + /// + /// Gets an environment variable as an enum. + /// + /// + /// By default, the environment variable value is presumed to be identical to the enum value name. + /// To define a mapping between environment variable value and enum name, apply the attribue in the enum definition. + /// + /// Parsing is case-insensitive. + /// + /// + /// The primary environment variable name. + /// The type of the enum to return. + /// + /// The environment variable value converted to the corresponding value of or + /// null if the variable is not set or the value could not be converted to the the specified enum type. + /// + protected TEnum? GetEnvironmentEnum(string variable) where TEnum : struct, Enum + { + var value = GetEnvironmentString(variable); + if (!string.IsNullOrWhiteSpace(value)) + { + // Instead of using Enum.TryParse(), enumerate the enum fields using reflection. + // This defining enums where the environment variable value differs from the name of the corresponding enum member: + // - If the enum member has a [EnumMember] attribute, use the value defined by the attribute instead of the enum member name + // - Otherwise, use the enum member name (matching the behavior of Enum.TryParse()) + foreach (var field in typeof(TEnum).GetFields().Where(fi => fi.FieldType == typeof(TEnum))) + { + var enumMemberName = field.Name; + if (field.GetCustomAttribute() is { Value: { } customName } && !string.IsNullOrEmpty(customName)) + { + enumMemberName = customName; + } + + if (StringComparer.OrdinalIgnoreCase.Equals(enumMemberName, value)) + { + return (TEnum?)field.GetValue(null); + } + } + } + + return null; + } } } \ No newline at end of file