diff --git a/.editorconfig b/.editorconfig index fb98354..351c4f2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,9 +7,12 @@ root = true [*] charset = utf-8 indent_style = space -indent_size = 2 +indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +end_of_line = crlf [*.{cs,js,ts,sql,tql}] @@ -21,15 +24,42 @@ csharp_space_between_method_call_parameter_list_parentheses = true csharp_space_between_method_declaration_parameter_list_parentheses = true csharp_space_after_keywords_in_control_flow_statements = false csharp_space_between_parentheses = control_flow_statements -# Motive: May be weird at first, but it improve readability. +csharp_space_around_binary_operators = before_and_after +# Motive: May be weird at first, but it improves readability. csharp_style_prefer_primary_constructors = false:suggestion # Primary constructors should be used only for very simple classes. May be record is a good choice. +csharp_indent_labels = no_change +# When using goto, labels should be explicitly positioned based on the algorithm. + +csharp_using_directive_placement = outside_namespace:silent +# Rather standard placement of using in C#. + +csharp_indent_case_contents_when_block = false; +# switch case block don't need another indent. + +csharp_prefer_braces = true:silent + +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent + +csharp_style_prefer_top_level_statements = true:suggestion +# Applies to Main(). + +csharp_style_namespace_declarations=file_scoped:suggestion +#Motive: Less useless space. + # internal and private fields should be _camelCase dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields -dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style dotnet_naming_symbols.private_internal_fields.applicable_kinds = field dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal @@ -51,17 +81,20 @@ dotnet_diagnostic.CA1816.severity = none csharp_prefer_simple_using_statement = false:none # Motive: Most of the time, the 'not simple' using scope is better. -# CA1031: Do not catch general exception types. -dotnet_diagnostic.CA1031.severity = none - # IDE0057: Use range operator. csharp_style_prefer_range_operator = false:suggestion # Motive: Use it if you want but this is should not show a message. +csharp_style_namespace_declarations = file_scoped:warning +# Motive: Less whitespace for editable files (this doesn't apply to generated code). + # IDE0060: Remove unused parameter dotnet_code_quality_unused_parameters = all:silent # Motive: Emit messages where the parameter are necessary. +# CA1031: Do not catch general exception types. +dotnet_diagnostic.CA1031.severity = none + # IDE0040: Add accessibility modifiers dotnet_style_require_accessibility_modifiers = omit_if_default:silent # Motive: 'private' is one more word that can be omitted. @@ -112,6 +145,5 @@ dotnet_diagnostic.VSTHRD101.severity = error # VSTHRD003: Avoid awaiting foreign Tasks dotnet_diagnostic.VSTHRD003.severity = none - # /Signature-Code .editorconfig diff --git a/CK.DB.User.UserGitLab.AuthScope/CK.DB.User.UserGitLab.AuthScope.csproj b/CK.DB.User.UserGitLab.AuthScope/CK.DB.User.UserGitLab.AuthScope.csproj index 59d7152..d933174 100644 --- a/CK.DB.User.UserGitLab.AuthScope/CK.DB.User.UserGitLab.AuthScope.csproj +++ b/CK.DB.User.UserGitLab.AuthScope/CK.DB.User.UserGitLab.AuthScope.csproj @@ -1,17 +1,13 @@ - net6.0 + net8.0 This package adds a ScopeSetId column to CK.tUserGitLab table. - true - + - - - - + \ No newline at end of file diff --git a/CK.DB.User.UserGitLab.AuthScope/IUserGitLabInfo.cs b/CK.DB.User.UserGitLab.AuthScope/IUserGitLabInfo.cs index 21698e9..f8def95 100644 --- a/CK.DB.User.UserGitLab.AuthScope/IUserGitLabInfo.cs +++ b/CK.DB.User.UserGitLab.AuthScope/IUserGitLabInfo.cs @@ -1,19 +1,18 @@ using CK.Core; using System; -namespace CK.DB.User.UserGitLab.AuthScope +namespace CK.DB.User.UserGitLab.AuthScope; + +/// +/// Extends with ScopeSet identifier. +/// +public interface IUserGitLabInfo : UserGitLab.IUserGitLabInfo { /// - /// Extends with ScopeSet identifier. + /// Gets or sets the scope set identifier. + /// Note that the ScopeSetId is intrinsic: a new ScopeSetId is acquired + /// and set only when a new UserGitLab is created (by copy from + /// the default one - the ScopeSet of the UserGitLab 0). /// - public interface IUserGitLabInfo : UserGitLab.IUserGitLabInfo - { - /// - /// Gets or sets the scope set identifier. - /// Note that the ScopeSetId is intrinsic: a new ScopeSetId is acquired - /// and set only when a new UserGitLab is created (by copy from - /// the default one - the ScopeSet of the UserGitLab 0). - /// - int ScopeSetId { get; set; } - } + int ScopeSetId { get; set; } } diff --git a/CK.DB.User.UserGitLab.AuthScope/Package.cs b/CK.DB.User.UserGitLab.AuthScope/Package.cs index 66e9eba..f883d98 100644 --- a/CK.DB.User.UserGitLab.AuthScope/Package.cs +++ b/CK.DB.User.UserGitLab.AuthScope/Package.cs @@ -5,58 +5,57 @@ using System; using System.Threading.Tasks; -namespace CK.DB.User.UserGitLab.AuthScope +namespace CK.DB.User.UserGitLab.AuthScope; + +/// +/// Package that adds AuthScope support to GitLab authentication. +/// +[SqlPackage( Schema = "CK", ResourcePath = "Res" )] +[Versions( "1.0.0" )] +[SqlObjectItem( "transform:sUserGitLabUCL, transform:sUserGitLabDestroy" )] +public class Package : SqlPackage { + AuthScopeSetTable _scopeSetTable; + UserGitLabTable _googleTable; + + void StObjConstruct( AuthScopeSetTable scopeSetTable, UserGitLabTable googleTable ) + { + _scopeSetTable = scopeSetTable; + _googleTable = googleTable; + } + + /// + /// Gets the . + /// + public UserGitLabTable UserGitLabTable => _googleTable; + /// - /// Package that adds AuthScope support to GitLab authentication. + /// Gets the . /// - [SqlPackage( Schema = "CK", ResourcePath = "Res" )] - [Versions("1.0.0")] - [SqlObjectItem( "transform:sUserGitLabUCL, transform:sUserGitLabDestroy" )] - public class Package : SqlPackage + public AuthScopeSetTable AuthScopeSetTable => _scopeSetTable; + + /// + /// Reads the of a user. + /// + /// The call context to use. + /// The user identifier. + /// The scope set or null if the user is not a GitLab user. + public Task ReadScopeSetAsync( ISqlCallContext ctx, int userId ) { - AuthScopeSetTable _scopeSetTable; - UserGitLabTable _googleTable; - - void StObjConstruct( AuthScopeSetTable scopeSetTable, UserGitLabTable googleTable ) - { - _scopeSetTable = scopeSetTable; - _googleTable = googleTable; - } - - /// - /// Gets the . - /// - public UserGitLabTable UserGitLabTable => _googleTable; - - /// - /// Gets the . - /// - public AuthScopeSetTable AuthScopeSetTable => _scopeSetTable; - - /// - /// Reads the of a user. - /// - /// The call context to use. - /// The user identifier. - /// The scope set or null if the user is not a GitLab user. - public Task ReadScopeSetAsync( ISqlCallContext ctx, int userId ) - { - if( userId <= 0 ) throw new ArgumentException( nameof( userId ) ); - var cmd = _scopeSetTable.CreateReadCommand( $"select ScopeSetId from CK.tUserGitLab where UserId = {userId}" ); - return _scopeSetTable.RawReadAuthScopeSetAsync( ctx, cmd ); - } - - /// - /// Reads the default that is the template for new users. - /// - /// The call context to use. - /// The default scope set. - public Task ReadDefaultScopeSetAsync( ISqlCallContext ctx ) - { - var cmd = _scopeSetTable.CreateReadCommand( "select ScopeSetId from CK.tUserGitLab where UserId = 0" ); - return _scopeSetTable.RawReadAuthScopeSetAsync( ctx, cmd ); - } + if( userId <= 0 ) throw new ArgumentException( nameof( userId ) ); + var cmd = _scopeSetTable.CreateReadCommand( $"select ScopeSetId from CK.tUserGitLab where UserId = {userId}" ); + return _scopeSetTable.RawReadAuthScopeSetAsync( ctx, cmd ); + } + /// + /// Reads the default that is the template for new users. + /// + /// The call context to use. + /// The default scope set. + public Task ReadDefaultScopeSetAsync( ISqlCallContext ctx ) + { + var cmd = _scopeSetTable.CreateReadCommand( "select ScopeSetId from CK.tUserGitLab where UserId = 0" ); + return _scopeSetTable.RawReadAuthScopeSetAsync( ctx, cmd ); } + } diff --git a/CK.DB.User.UserGitLab/CK.DB.User.UserGitLab.csproj b/CK.DB.User.UserGitLab/CK.DB.User.UserGitLab.csproj index 7d46723..2fed23d 100644 --- a/CK.DB.User.UserGitLab/CK.DB.User.UserGitLab.csproj +++ b/CK.DB.User.UserGitLab/CK.DB.User.UserGitLab.csproj @@ -1,16 +1,12 @@ - net6.0 + net8.0 This package defines tUserGitLab table. - true - + - - - - + \ No newline at end of file diff --git a/CK.DB.User.UserGitLab/IUserGitLabInfo.cs b/CK.DB.User.UserGitLab/IUserGitLabInfo.cs index a7b903a..a838e4c 100644 --- a/CK.DB.User.UserGitLab/IUserGitLabInfo.cs +++ b/CK.DB.User.UserGitLab/IUserGitLabInfo.cs @@ -1,16 +1,14 @@ using CK.Core; -namespace CK.DB.User.UserGitLab +namespace CK.DB.User.UserGitLab; + +/// +/// Holds information stored for a GitLab user. +/// +public interface IUserGitLabInfo : IPoco { /// - /// Holds information stored for a GitLab user. + /// Gets or sets the GitLab account identifier. /// - public interface IUserGitLabInfo : IPoco - { - /// - /// Gets or sets the GitLab account identifier. - /// - string GitLabAccountId { get; set; } - } - + string GitLabAccountId { get; set; } } diff --git a/CK.DB.User.UserGitLab/Package.cs b/CK.DB.User.UserGitLab/Package.cs index 7a144e7..fc5ed5a 100644 --- a/CK.DB.User.UserGitLab/Package.cs +++ b/CK.DB.User.UserGitLab/Package.cs @@ -1,17 +1,16 @@ using CK.Core; -namespace CK.DB.User.UserGitLab +namespace CK.DB.User.UserGitLab; + +/// +/// Package that adds GitLab authentication support for users. +/// +[SqlPackage( Schema = "CK", ResourcePath = "Res" )] +[Versions( "1.0.0" )] +[SqlObjectItem( "transform:vUserAuthProvider" )] +public class Package : SqlPackage { - /// - /// Package that adds GitLab authentication support for users. - /// - [SqlPackage( Schema = "CK", ResourcePath = "Res" )] - [Versions("1.0.0")] - [SqlObjectItem( "transform:vUserAuthProvider" )] - public class Package : SqlPackage + void StObjConstruct( Actor.Package actorPackage, Auth.Package authPackage ) { - void StObjConstruct( Actor.Package actorPackage, Auth.Package authPackage ) - { - } } } diff --git a/CK.DB.User.UserGitLab/UserGitLabTable.Sync.cs b/CK.DB.User.UserGitLab/UserGitLabTable.Sync.cs index 5fec54a..cda8627 100644 --- a/CK.DB.User.UserGitLab/UserGitLabTable.Sync.cs +++ b/CK.DB.User.UserGitLab/UserGitLabTable.Sync.cs @@ -2,89 +2,88 @@ using CK.SqlServer; using CK.Core; -namespace CK.DB.User.UserGitLab +namespace CK.DB.User.UserGitLab; + +public abstract partial class UserGitLabTable { - public abstract partial class UserGitLabTable + /// + /// Creates or updates a user entry for this provider. + /// This is the "binding account" feature since it binds an external identity to + /// an already existing user that may already be registered into other authencation providers. + /// + /// The call context to use. + /// The acting actor identifier. + /// The user identifier that must be registered. + /// Provider specific data: the poco. + /// Optionnaly configures Create, Update only or WithLogin behavior. + /// The result. + public UCLResult CreateOrUpdateGitLabUser( ISqlCallContext ctx, int actorId, int userId, IUserGitLabInfo info, UCLMode mode = UCLMode.CreateOrUpdate ) { - /// - /// Creates or updates a user entry for this provider. - /// This is the "binding account" feature since it binds an external identity to - /// an already existing user that may already be registered into other authencation providers. - /// - /// The call context to use. - /// The acting actor identifier. - /// The user identifier that must be registered. - /// Provider specific data: the poco. - /// Optionnaly configures Create, Update only or WithLogin behavior. - /// The result. - public UCLResult CreateOrUpdateGitLabUser( ISqlCallContext ctx, int actorId, int userId, IUserGitLabInfo info, UCLMode mode = UCLMode.CreateOrUpdate ) - { - return UserGitLabUCL( ctx, actorId, userId, info, mode ); - } + return UserGitLabUCL( ctx, actorId, userId, info, mode ); + } - /// - /// Challenges data to identify a user. - /// Note that a successful challenge may have side effects such as updating claims, access tokens or other data - /// related to the user and this provider. - /// - /// The call context to use. - /// The payload to challenge. - /// Set it to false to avoid login side-effect (such as updating the LastLoginTime) on success. - /// The login result. - public LoginResult LoginUser( ISqlCallContext ctx, IUserGitLabInfo info, bool actualLogin = true ) - { - var mode = actualLogin - ? UCLMode.UpdateOnly | UCLMode.WithActualLogin - : UCLMode.UpdateOnly | UCLMode.WithCheckLogin; - var r = UserGitLabUCL( ctx, 1, 0, info, mode ); - return r.LoginResult; - } + /// + /// Challenges data to identify a user. + /// Note that a successful challenge may have side effects such as updating claims, access tokens or other data + /// related to the user and this provider. + /// + /// The call context to use. + /// The payload to challenge. + /// Set it to false to avoid login side-effect (such as updating the LastLoginTime) on success. + /// The login result. + public LoginResult LoginUser( ISqlCallContext ctx, IUserGitLabInfo info, bool actualLogin = true ) + { + var mode = actualLogin + ? UCLMode.UpdateOnly | UCLMode.WithActualLogin + : UCLMode.UpdateOnly | UCLMode.WithCheckLogin; + var r = UserGitLabUCL( ctx, 1, 0, info, mode ); + return r.LoginResult; + } - /// - /// Destroys a GitLabUser for a user. - /// - /// The call context to use. - /// The acting actor identifier. - /// The user identifier for which GitLab account information must be destroyed. - /// The awaitable. - [SqlProcedure( "sUserGitLabDestroy" )] - public abstract void DestroyGitLabUser( ISqlCallContext ctx, int actorId, int userId ); + /// + /// Destroys a GitLabUser for a user. + /// + /// The call context to use. + /// The acting actor identifier. + /// The user identifier for which GitLab account information must be destroyed. + /// The awaitable. + [SqlProcedure( "sUserGitLabDestroy" )] + public abstract void DestroyGitLabUser( ISqlCallContext ctx, int actorId, int userId ); - /// - /// Finds a user by its GitLab account identifier. - /// Returns null if no such user exists. - /// - /// The call context to use. - /// The google account identifier. - /// A or null if not found. - public IdentifiedUserInfo? FindKnownUserInfo( ISqlCallContext ctx, string googleAccountId ) + /// + /// Finds a user by its GitLab account identifier. + /// Returns null if no such user exists. + /// + /// The call context to use. + /// The google account identifier. + /// A or null if not found. + public IdentifiedUserInfo? FindKnownUserInfo( ISqlCallContext ctx, string googleAccountId ) + { + using( var c = CreateReaderCommand( googleAccountId ) ) { - using( var c = CreateReaderCommand( googleAccountId ) ) - { - return ctx[Database].ExecuteSingleRow( c, r => r == null - ? null - : DoCreateUserUnfo( googleAccountId, r ) ); - } + return ctx[Database].ExecuteSingleRow( c, r => r == null + ? null + : DoCreateUserUnfo( googleAccountId, r ) ); } + } - /// - /// Raw call to manage GitLabUser. Since this should not be used directly, it is protected. - /// Actual implementation of the centralized update, create or login procedure. - /// - /// The call context to use. - /// The acting actor identifier. - /// The user identifier for which a GitLab account must be created or updated. - /// User information to create or update. - /// Configures Create, Update only or WithCheck/ActualLogin behavior. - /// The result. - [SqlProcedure( "sUserGitLabUCL" )] - protected abstract UCLResult UserGitLabUCL( - ISqlCallContext ctx, - int actorId, - int userId, - [ParameterSource]IUserGitLabInfo info, - UCLMode mode ); + /// + /// Raw call to manage GitLabUser. Since this should not be used directly, it is protected. + /// Actual implementation of the centralized update, create or login procedure. + /// + /// The call context to use. + /// The acting actor identifier. + /// The user identifier for which a GitLab account must be created or updated. + /// User information to create or update. + /// Configures Create, Update only or WithCheck/ActualLogin behavior. + /// The result. + [SqlProcedure( "sUserGitLabUCL" )] + protected abstract UCLResult UserGitLabUCL( + ISqlCallContext ctx, + int actorId, + int userId, + [ParameterSource] IUserGitLabInfo info, + UCLMode mode ); - } } diff --git a/CK.DB.User.UserGitLab/UserGitLabTable.cs b/CK.DB.User.UserGitLab/UserGitLabTable.cs index 1b40a98..2aba6a4 100644 --- a/CK.DB.User.UserGitLab/UserGitLabTable.cs +++ b/CK.DB.User.UserGitLab/UserGitLabTable.cs @@ -8,215 +8,214 @@ using CK.DB.Auth; using System.Diagnostics.CodeAnalysis; -namespace CK.DB.User.UserGitLab +namespace CK.DB.User.UserGitLab; + +/// +/// GitLab authentication provider. +/// +[SqlTable( "tUserGitLab", Package = typeof( Package ) )] +[Versions( "2.0.1" )] +[SqlObjectItem( "transform:sUserDestroy" )] +public abstract partial class UserGitLabTable : SqlTable, IGenericAuthenticationProvider { + [AllowNull] + IPocoFactory _infoFactory; + /// - /// GitLab authentication provider. + /// Gets "GitLab" that is the name of the GitLab provider. /// - [SqlTable( "tUserGitLab", Package = typeof( Package ) )] - [Versions( "2.0.1" )] - [SqlObjectItem( "transform:sUserDestroy" )] - public abstract partial class UserGitLabTable : SqlTable, IGenericAuthenticationProvider + public string ProviderName => "GitLab"; + + void StObjConstruct( IPocoFactory infoFactory ) { - [AllowNull] - IPocoFactory _infoFactory; + _infoFactory = infoFactory; + } - /// - /// Gets "GitLab" that is the name of the GitLab provider. - /// - public string ProviderName => "GitLab"; + public bool CanCreatePayload => true; - void StObjConstruct( IPocoFactory infoFactory ) - { - _infoFactory = infoFactory; - } + object IGenericAuthenticationProvider.CreatePayload() => _infoFactory.Create(); - public bool CanCreatePayload => true; - - object IGenericAuthenticationProvider.CreatePayload() => _infoFactory.Create(); - - IUserGitLabInfo IGenericAuthenticationProvider.CreatePayload() => _infoFactory.Create(); - - /// - /// Creates a poco. - /// - /// A new instance. - public T CreateUserInfo() where T : IUserGitLabInfo => (T)_infoFactory.Create(); - - /// - /// Creates or updates a user entry for this provider. - /// This is the "binding account" feature since it binds an external identity to - /// an already existing user that may already be registered into other authencation providers. - /// - /// The call context to use. - /// The acting actor identifier. - /// The user identifier that must be registered. - /// Provider specific data: the poco. - /// Optionnaly configures Create, Update only or WithLogin behavior. - /// Optional cancellation token. - /// The result. - public async Task CreateOrUpdateGitLabUserAsync( ISqlCallContext ctx, int actorId, int userId, IUserGitLabInfo info, UCLMode mode = UCLMode.CreateOrUpdate, CancellationToken cancellationToken = default ) - { - var r = await GitLabUserUCLAsync( ctx, actorId, userId, info, mode, cancellationToken ).ConfigureAwait( false ); - return r; - } + IUserGitLabInfo IGenericAuthenticationProvider.CreatePayload() => _infoFactory.Create(); - /// - /// Challenges data to identify a user. - /// Note that a successful challenge may have side effects such as updating claims, access tokens or other data - /// related to the user and this provider. - /// - /// The call context to use. - /// The payload to challenge. - /// Set it to false to avoid login side-effect (such as updating the LastLoginTime) on success. - /// Optional cancellation token. - /// The . - public async Task LoginUserAsync( ISqlCallContext ctx, IUserGitLabInfo info, bool actualLogin = true, CancellationToken cancellationToken = default ) - { - var mode = actualLogin - ? UCLMode.UpdateOnly | UCLMode.WithActualLogin - : UCLMode.UpdateOnly | UCLMode.WithCheckLogin; - var r = await GitLabUserUCLAsync( ctx, 1, 0, info, mode, cancellationToken ).ConfigureAwait( false ); - return r.LoginResult; - } + /// + /// Creates a poco. + /// + /// A new instance. + public T CreateUserInfo() where T : IUserGitLabInfo => (T)_infoFactory.Create(); - /// - /// Destroys a GitLabUser for a user. - /// - /// The call context to use. - /// The acting actor identifier. - /// The user identifier for which GitLab account information must be destroyed. - /// Optional cancellation token. - /// The awaitable. - [SqlProcedure( "sUserGitLabDestroy" )] - public abstract Task DestroyGitLabUserAsync( ISqlCallContext ctx, int actorId, int userId, CancellationToken cancellationToken = default ); - - /// - /// Raw call to manage GitLabUser. Since this should not be used directly, it is protected. - /// Actual implementation of the centralized update, create or login procedure. - /// - /// The call context to use. - /// The acting actor identifier. - /// The user identifier for which a GitLab account must be created or updated. - /// User information to create or update. - /// Configures Create, Update only or WithCheck/ActualLogin behavior. - /// Optional cancellation token. - /// The result. - [SqlProcedure( "sUserGitLabUCL" )] - protected abstract Task GitLabUserUCLAsync( - ISqlCallContext ctx, - int actorId, - int userId, - [ParameterSource]IUserGitLabInfo info, - UCLMode mode, - CancellationToken cancellationToken ); - - /// - /// Finds a user by its GitLab account identifier. - /// Returns null if no such user exists. - /// - /// The call context to use. - /// The google account identifier. - /// Optional cancellation token. - /// A object or null if not found. - public async Task?> FindKnownUserInfoAsync( ISqlCallContext ctx, string googleAccountId, CancellationToken cancellationToken = default ) - { - using( var c = CreateReaderCommand( googleAccountId ) ) - { - return await ctx[Database].ExecuteSingleRowAsync( c, r => r == null - ? null - : DoCreateUserUnfo( googleAccountId, r ), cancellationToken ) - .ConfigureAwait( false ); - } - } + /// + /// Creates or updates a user entry for this provider. + /// This is the "binding account" feature since it binds an external identity to + /// an already existing user that may already be registered into other authencation providers. + /// + /// The call context to use. + /// The acting actor identifier. + /// The user identifier that must be registered. + /// Provider specific data: the poco. + /// Optionnaly configures Create, Update only or WithLogin behavior. + /// Optional cancellation token. + /// The result. + public async Task CreateOrUpdateGitLabUserAsync( ISqlCallContext ctx, int actorId, int userId, IUserGitLabInfo info, UCLMode mode = UCLMode.CreateOrUpdate, CancellationToken cancellationToken = default ) + { + var r = await GitLabUserUCLAsync( ctx, actorId, userId, info, mode, cancellationToken ).ConfigureAwait( false ); + return r; + } - /// - /// Creates a the reader command parametrized with the GitLab account identifier. - /// Single-row returned columns are defined by . - /// - /// GitLab account identifier to look for. - /// A ready to use reader command. - SqlCommand CreateReaderCommand( string googleAccountId ) - { - StringBuilder b = new StringBuilder( "select " ); - AppendUserInfoColumns( b ).Append( " from CK.tUserGitLab where GitLabAccountId=@A" ); - var c = new SqlCommand( b.ToString() ); - c.Parameters.Add( new SqlParameter( "@A", googleAccountId ) ); - return c; - } + /// + /// Challenges data to identify a user. + /// Note that a successful challenge may have side effects such as updating claims, access tokens or other data + /// related to the user and this provider. + /// + /// The call context to use. + /// The payload to challenge. + /// Set it to false to avoid login side-effect (such as updating the LastLoginTime) on success. + /// Optional cancellation token. + /// The . + public async Task LoginUserAsync( ISqlCallContext ctx, IUserGitLabInfo info, bool actualLogin = true, CancellationToken cancellationToken = default ) + { + var mode = actualLogin + ? UCLMode.UpdateOnly | UCLMode.WithActualLogin + : UCLMode.UpdateOnly | UCLMode.WithCheckLogin; + var r = await GitLabUserUCLAsync( ctx, 1, 0, info, mode, cancellationToken ).ConfigureAwait( false ); + return r.LoginResult; + } - IdentifiedUserInfo DoCreateUserUnfo( string googleAccountId, SqlDataRow r ) - { - var info = _infoFactory.Create(); - info.GitLabAccountId = googleAccountId; - FillUserGitLabInfo( info, r, 1 ); - return new IdentifiedUserInfo( r.GetInt32( 0 ), info ); - } + /// + /// Destroys a GitLabUser for a user. + /// + /// The call context to use. + /// The acting actor identifier. + /// The user identifier for which GitLab account information must be destroyed. + /// Optional cancellation token. + /// The awaitable. + [SqlProcedure( "sUserGitLabDestroy" )] + public abstract Task DestroyGitLabUserAsync( ISqlCallContext ctx, int actorId, int userId, CancellationToken cancellationToken = default ); - /// - /// Adds the columns name to read. - /// - /// The string builder. - /// The string builder. - protected virtual StringBuilder AppendUserInfoColumns( StringBuilder b ) - { - var props = _infoFactory.PocoClassType.GetProperties().Where( p => p.Name != nameof( IUserGitLabInfo.GitLabAccountId ) ); - return props.Any() ? b.Append( "UserId, " ).AppendStrings( props.Select( p => p.Name ) ) : b.Append( "UserId " ); - } + /// + /// Raw call to manage GitLabUser. Since this should not be used directly, it is protected. + /// Actual implementation of the centralized update, create or login procedure. + /// + /// The call context to use. + /// The acting actor identifier. + /// The user identifier for which a GitLab account must be created or updated. + /// User information to create or update. + /// Configures Create, Update only or WithCheck/ActualLogin behavior. + /// Optional cancellation token. + /// The result. + [SqlProcedure( "sUserGitLabUCL" )] + protected abstract Task GitLabUserUCLAsync( + ISqlCallContext ctx, + int actorId, + int userId, + [ParameterSource] IUserGitLabInfo info, + UCLMode mode, + CancellationToken cancellationToken ); - /// - /// Fill UserInfo properties from reader. - /// - /// The info to fill. - /// The record. - /// The index of the first column. - /// The updated index. - protected virtual int FillUserGitLabInfo( IUserGitLabInfo info, SqlDataRow r, int idx ) + /// + /// Finds a user by its GitLab account identifier. + /// Returns null if no such user exists. + /// + /// The call context to use. + /// The google account identifier. + /// Optional cancellation token. + /// A object or null if not found. + public async Task?> FindKnownUserInfoAsync( ISqlCallContext ctx, string googleAccountId, CancellationToken cancellationToken = default ) + { + using( var c = CreateReaderCommand( googleAccountId ) ) { - var props = _infoFactory.PocoClassType.GetProperties().Where( p => p.Name != nameof( IUserGitLabInfo.GitLabAccountId ) ); - foreach( var p in props ) - { - p.SetValue( info, r.GetValue( idx++ ) ); - } - return idx; + return await ctx[Database].ExecuteSingleRowAsync( c, r => r == null + ? null + : DoCreateUserUnfo( googleAccountId, r ), cancellationToken ) + .ConfigureAwait( false ); } + } + + /// + /// Creates a the reader command parametrized with the GitLab account identifier. + /// Single-row returned columns are defined by . + /// + /// GitLab account identifier to look for. + /// A ready to use reader command. + SqlCommand CreateReaderCommand( string googleAccountId ) + { + StringBuilder b = new StringBuilder( "select " ); + AppendUserInfoColumns( b ).Append( " from CK.tUserGitLab where GitLabAccountId=@A" ); + var c = new SqlCommand( b.ToString() ); + c.Parameters.Add( new SqlParameter( "@A", googleAccountId ) ); + return c; + } - #region IGenericAuthenticationProvider explicit implementation. + IdentifiedUserInfo DoCreateUserUnfo( string googleAccountId, SqlDataRow r ) + { + var info = _infoFactory.Create(); + info.GitLabAccountId = googleAccountId; + FillUserGitLabInfo( info, r, 1 ); + return new IdentifiedUserInfo( r.GetInt32( 0 ), info ); + } - UCLResult IGenericAuthenticationProvider.CreateOrUpdateUser( ISqlCallContext ctx, int actorId, int userId, object payload, UCLMode mode ) - { - IUserGitLabInfo info = _infoFactory.ExtractPayload( payload ); - return CreateOrUpdateGitLabUser( ctx, actorId, userId, info, mode ); - } + /// + /// Adds the columns name to read. + /// + /// The string builder. + /// The string builder. + protected virtual StringBuilder AppendUserInfoColumns( StringBuilder b ) + { + var props = _infoFactory.PocoClassType.GetProperties().Where( p => p.Name != nameof( IUserGitLabInfo.GitLabAccountId ) ); + return props.Any() ? b.Append( "UserId, " ).AppendStrings( props.Select( p => p.Name ) ) : b.Append( "UserId " ); + } - LoginResult IGenericAuthenticationProvider.LoginUser( ISqlCallContext ctx, object payload, bool actualLogin ) + /// + /// Fill UserInfo properties from reader. + /// + /// The info to fill. + /// The record. + /// The index of the first column. + /// The updated index. + protected virtual int FillUserGitLabInfo( IUserGitLabInfo info, SqlDataRow r, int idx ) + { + var props = _infoFactory.PocoClassType.GetProperties().Where( p => p.Name != nameof( IUserGitLabInfo.GitLabAccountId ) ); + foreach( var p in props ) { - IUserGitLabInfo info = _infoFactory.ExtractPayload( payload ); - return LoginUser( ctx, info, actualLogin ); + p.SetValue( info, r.GetValue( idx++ ) ); } + return idx; + } - Task IGenericAuthenticationProvider.CreateOrUpdateUserAsync( ISqlCallContext ctx, int actorId, int userId, object payload, UCLMode mode, CancellationToken cancellationToken ) - { - IUserGitLabInfo info = _infoFactory.ExtractPayload( payload ); - return CreateOrUpdateGitLabUserAsync( ctx, actorId, userId, info, mode, cancellationToken ); - } + #region IGenericAuthenticationProvider explicit implementation. - Task IGenericAuthenticationProvider.LoginUserAsync( ISqlCallContext ctx, object payload, bool actualLogin, CancellationToken cancellationToken ) - { - IUserGitLabInfo info = _infoFactory.ExtractPayload( payload ); - return LoginUserAsync( ctx, info, actualLogin, cancellationToken ); - } + UCLResult IGenericAuthenticationProvider.CreateOrUpdateUser( ISqlCallContext ctx, int actorId, int userId, object payload, UCLMode mode ) + { + IUserGitLabInfo info = _infoFactory.ExtractPayload( payload ); + return CreateOrUpdateGitLabUser( ctx, actorId, userId, info, mode ); + } - void IGenericAuthenticationProvider.DestroyUser( ISqlCallContext ctx, int actorId, int userId, string? schemeSuffix ) - { - DestroyGitLabUser( ctx, actorId, userId ); - } + LoginResult IGenericAuthenticationProvider.LoginUser( ISqlCallContext ctx, object payload, bool actualLogin ) + { + IUserGitLabInfo info = _infoFactory.ExtractPayload( payload ); + return LoginUser( ctx, info, actualLogin ); + } - Task IGenericAuthenticationProvider.DestroyUserAsync( ISqlCallContext ctx, int actorId, int userId, string? schemeSuffix, CancellationToken cancellationToken ) - { - return DestroyGitLabUserAsync( ctx, actorId, userId, cancellationToken ); - } + Task IGenericAuthenticationProvider.CreateOrUpdateUserAsync( ISqlCallContext ctx, int actorId, int userId, object payload, UCLMode mode, CancellationToken cancellationToken ) + { + IUserGitLabInfo info = _infoFactory.ExtractPayload( payload ); + return CreateOrUpdateGitLabUserAsync( ctx, actorId, userId, info, mode, cancellationToken ); + } - #endregion + Task IGenericAuthenticationProvider.LoginUserAsync( ISqlCallContext ctx, object payload, bool actualLogin, CancellationToken cancellationToken ) + { + IUserGitLabInfo info = _infoFactory.ExtractPayload( payload ); + return LoginUserAsync( ctx, info, actualLogin, cancellationToken ); + } + + void IGenericAuthenticationProvider.DestroyUser( ISqlCallContext ctx, int actorId, int userId, string? schemeSuffix ) + { + DestroyGitLabUser( ctx, actorId, userId ); } + + Task IGenericAuthenticationProvider.DestroyUserAsync( ISqlCallContext ctx, int actorId, int userId, string? schemeSuffix, CancellationToken cancellationToken ) + { + return DestroyGitLabUserAsync( ctx, actorId, userId, cancellationToken ); + } + + #endregion } diff --git a/CodeCakeBuilder/Abstractions/Artifact.cs b/CodeCakeBuilder/Abstractions/Artifact.cs index ef4aed4..537e0c5 100644 --- a/CodeCakeBuilder/Abstractions/Artifact.cs +++ b/CodeCakeBuilder/Abstractions/Artifact.cs @@ -1,69 +1,68 @@ using System; -namespace CodeCake.Abstractions +namespace CodeCake.Abstractions; + +/// +/// An artifact is produced by a solution and can be of any type. +/// +public readonly struct Artifact : IEquatable { /// - /// An artifact is produced by a solution and can be of any type. + /// Gets the artifact type. /// - public readonly struct Artifact : IEquatable - { - /// - /// Gets the artifact type. - /// - public string Type { get; } + public string Type { get; } - /// - /// Gets the artifact name. - /// - public string Name { get; } + /// + /// Gets the artifact name. + /// + public string Name { get; } - /// - /// Initializes a new . - /// - /// Artifact type. - /// Artifact name. - public Artifact( string type, string name ) - { - Type = type ?? throw new ArgumentNullException( nameof( type ) ); - Name = name ?? throw new ArgumentNullException( nameof( name ) ); - } + /// + /// Initializes a new . + /// + /// Artifact type. + /// Artifact name. + public Artifact( string type, string name ) + { + Type = type ?? throw new ArgumentNullException( nameof( type ) ); + Name = name ?? throw new ArgumentNullException( nameof( name ) ); + } - /// - /// Checks equality. - /// - /// The other artifact. - /// True when equals, false otherwise. - public bool Equals( Artifact other ) => Type == other.Type && Name == other.Name; + /// + /// Checks equality. + /// + /// The other artifact. + /// True when equals, false otherwise. + public bool Equals( Artifact other ) => Type == other.Type && Name == other.Name; - /// - /// Overridden to call . - /// - /// An object. - /// True the object is an Artifact that is equal to this one, false otherwise. - public override bool Equals( object? obj ) => obj is Artifact a ? Equals( a ) : false; + /// + /// Overridden to call . + /// + /// An object. + /// True the object is an Artifact that is equal to this one, false otherwise. + public override bool Equals( object? obj ) => obj is Artifact a ? Equals( a ) : false; - /// - /// Overridden to combine and . - /// - /// The hash code. - public override int GetHashCode() => Type.GetHashCode() ^ Name.GetHashCode(); + /// + /// Overridden to combine and . + /// + /// The hash code. + public override int GetHashCode() => Type.GetHashCode() ^ Name.GetHashCode(); - /// - /// Implements == operator. - /// - /// First artifact. - /// Second artifact. - /// True if they are equal. - public static bool operator ==( Artifact x, Artifact y ) => x.Equals( y ); + /// + /// Implements == operator. + /// + /// First artifact. + /// Second artifact. + /// True if they are equal. + public static bool operator ==( Artifact x, Artifact y ) => x.Equals( y ); - /// - /// Implements != operator. - /// - /// First artifact. - /// Second artifact. - /// True if they are not equal. - public static bool operator !=( Artifact x, Artifact y ) => !x.Equals( y ); + /// + /// Implements != operator. + /// + /// First artifact. + /// Second artifact. + /// True if they are not equal. + public static bool operator !=( Artifact x, Artifact y ) => !x.Equals( y ); - public override string ToString() => $"{Type}:{Name}"; - } + public override string ToString() => $"{Type}:{Name}"; } diff --git a/CodeCakeBuilder/Abstractions/ArtifactFeed.cs b/CodeCakeBuilder/Abstractions/ArtifactFeed.cs index 3db056c..9cdd7f6 100644 --- a/CodeCakeBuilder/Abstractions/ArtifactFeed.cs +++ b/CodeCakeBuilder/Abstractions/ArtifactFeed.cs @@ -2,46 +2,45 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace CodeCake.Abstractions +namespace CodeCake.Abstractions; + +/// +/// Abstract feed associated to a . +/// +public abstract class ArtifactFeed { + protected ArtifactFeed( ArtifactType t ) + { + ArtifactType = t; + } + /// - /// Abstract feed associated to a . + /// Gets the artifact type that handles this feed. /// - public abstract class ArtifactFeed - { - protected ArtifactFeed( ArtifactType t ) - { - ArtifactType = t; - } - - /// - /// Gets the artifact type that handles this feed. - /// - public ArtifactType ArtifactType { get; } - - /// - /// Gets the cake context. - /// - protected ICakeContext Cake => ArtifactType.GlobalInfo.Cake; - - /// - /// Name used to print the feed in the logs. - /// - public abstract string Name { get; } - - /// - /// Creates a list of object from a set of . - /// - /// Set of artifacts. - /// The set of push information. - public abstract Task> CreatePushListAsync( IEnumerable artifacts ); - - /// - /// Pushes a set of artifacts previously computed by . - /// - /// The instances to push (that necessary target this feed). - /// The awaitable. - public abstract Task PushAsync( IEnumerable pushes ); + public ArtifactType ArtifactType { get; } + + /// + /// Gets the cake context. + /// + protected ICakeContext Cake => ArtifactType.GlobalInfo.Cake; + + /// + /// Name used to print the feed in the logs. + /// + public abstract string Name { get; } + + /// + /// Creates a list of object from a set of . + /// + /// Set of artifacts. + /// The set of push information. + public abstract Task> CreatePushListAsync( IEnumerable artifacts ); + + /// + /// Pushes a set of artifacts previously computed by . + /// + /// The instances to push (that necessary target this feed). + /// The awaitable. + public abstract Task PushAsync( IEnumerable pushes ); - } } diff --git a/CodeCakeBuilder/Abstractions/ArtifactInstance.cs b/CodeCakeBuilder/Abstractions/ArtifactInstance.cs index 558ed47..7e9268a 100644 --- a/CodeCakeBuilder/Abstractions/ArtifactInstance.cs +++ b/CodeCakeBuilder/Abstractions/ArtifactInstance.cs @@ -1,73 +1,72 @@ using CSemVer; using System; -namespace CodeCake.Abstractions +namespace CodeCake.Abstractions; + +/// +/// Defines the instance of an : its is known. +/// +public readonly struct ArtifactInstance : IEquatable { /// - /// Defines the instance of an : its is known. + /// Initializes a new . /// - public readonly struct ArtifactInstance : IEquatable + /// The artifact. + /// The version. Can not be null. + public ArtifactInstance( Artifact a, SVersion version ) { - /// - /// Initializes a new . - /// - /// The artifact. - /// The version. Can not be null. - public ArtifactInstance( Artifact a, SVersion version ) - { - Artifact = a; - Version = version ?? throw new ArgumentNullException( nameof( version ) ); - } + Artifact = a; + Version = version ?? throw new ArgumentNullException( nameof( version ) ); + } - /// - /// Initializes a new . - /// - /// The artifact type. Can not be null. - /// The artifact name. Can not be null. - /// The version. Can not be null. - public ArtifactInstance( string type, string name, SVersion version ) - : this( new Artifact( type, name ), version ) - { - } + /// + /// Initializes a new . + /// + /// The artifact type. Can not be null. + /// The artifact name. Can not be null. + /// The version. Can not be null. + public ArtifactInstance( string type, string name, SVersion version ) + : this( new Artifact( type, name ), version ) + { + } - /// - /// Gets the artifact. - /// - public Artifact Artifact { get; } + /// + /// Gets the artifact. + /// + public Artifact Artifact { get; } - /// - /// Gets the artifact version. - /// - public SVersion Version { get; } + /// + /// Gets the artifact version. + /// + public SVersion Version { get; } - /// - /// Checks equality. - /// - /// The other instance. - /// True when equals, false otherwise. - public bool Equals( ArtifactInstance other ) => Artifact == other.Artifact && Version == other.Version; + /// + /// Checks equality. + /// + /// The other instance. + /// True when equals, false otherwise. + public bool Equals( ArtifactInstance other ) => Artifact == other.Artifact && Version == other.Version; - public override bool Equals( object? obj ) => obj is ArtifactInstance a ? Equals( a ) : false; + public override bool Equals( object? obj ) => obj is ArtifactInstance a ? Equals( a ) : false; - public override int GetHashCode() => Version.GetHashCode() ^ Artifact.GetHashCode(); + public override int GetHashCode() => Version.GetHashCode() ^ Artifact.GetHashCode(); - public override string ToString() => $"{Artifact}/{Version.ToNormalizedString()}"; + public override string ToString() => $"{Artifact}/{Version.ToNormalizedString()}"; - /// - /// Implements == operator. - /// - /// First artifact instance. - /// Second artifact instance. - /// True if they are equal. - public static bool operator ==( ArtifactInstance x, ArtifactInstance y ) => x.Equals( y ); + /// + /// Implements == operator. + /// + /// First artifact instance. + /// Second artifact instance. + /// True if they are equal. + public static bool operator ==( ArtifactInstance x, ArtifactInstance y ) => x.Equals( y ); - /// - /// Implements != operator. - /// - /// First artifact instance. - /// Second artifact instance. - /// True if they are not equal. - public static bool operator !=( ArtifactInstance x, ArtifactInstance y ) => !x.Equals( y ); + /// + /// Implements != operator. + /// + /// First artifact instance. + /// Second artifact instance. + /// True if they are not equal. + public static bool operator !=( ArtifactInstance x, ArtifactInstance y ) => !x.Equals( y ); - } } diff --git a/CodeCakeBuilder/Abstractions/ArtifactPush.cs b/CodeCakeBuilder/Abstractions/ArtifactPush.cs index b5935d3..87d760f 100644 --- a/CodeCakeBuilder/Abstractions/ArtifactPush.cs +++ b/CodeCakeBuilder/Abstractions/ArtifactPush.cs @@ -1,46 +1,44 @@ using CSemVer; using System.Collections.Generic; -namespace CodeCake.Abstractions +namespace CodeCake.Abstractions; + +/// +/// Represents the required push of a into a . +/// This class may be specialized if Feed per Artifact data must be generated and circulate between +/// the +/// and . +/// +public class ArtifactPush { /// - /// Represents the required push of a into a . - /// This class may be specialized if Feed per Artifact data must be generated and circulate between - /// the - /// and . + /// Initializes a new instance. /// - public class ArtifactPush + /// The artifact. + /// The target feed. + public ArtifactPush( ILocalArtifact local, ArtifactFeed feed ) { - /// - /// Initializes a new instance. - /// - /// The artifact. - /// The target feed. - public ArtifactPush( ILocalArtifact local, ArtifactFeed feed ) - { - LocalArtifact = local; - Feed = feed; - } - - /// - /// Gets the artifact. - /// - public ILocalArtifact LocalArtifact { get; } + LocalArtifact = local; + Feed = feed; + } - /// - /// Gets the artifact name. - /// - public string Name => LocalArtifact.ArtifactInstance.Artifact.Name; + /// + /// Gets the artifact. + /// + public ILocalArtifact LocalArtifact { get; } - /// - /// Gets the artifact's version. - /// - public SVersion Version => LocalArtifact.ArtifactInstance.Version; + /// + /// Gets the artifact name. + /// + public string Name => LocalArtifact.ArtifactInstance.Artifact.Name; - /// - /// Gets the feed. - /// - public ArtifactFeed Feed { get; } - } + /// + /// Gets the artifact's version. + /// + public SVersion Version => LocalArtifact.ArtifactInstance.Version; + /// + /// Gets the feed. + /// + public ArtifactFeed Feed { get; } } diff --git a/CodeCakeBuilder/Abstractions/ArtifactType.cs b/CodeCakeBuilder/Abstractions/ArtifactType.cs index a024be3..beded96 100644 --- a/CodeCakeBuilder/Abstractions/ArtifactType.cs +++ b/CodeCakeBuilder/Abstractions/ArtifactType.cs @@ -5,151 +5,150 @@ using System.Linq; using System.Threading.Tasks; -namespace CodeCake.Abstractions +namespace CodeCake.Abstractions; + +/// +/// Abstract artifact Type that handles and defines +/// how can be published into those feeds. +/// +public abstract class ArtifactType { + readonly StandardGlobalInfo _globalInfo; + readonly string _typeName; + List? _feeds; + List? _artifacts; + List? _pushes; + /// - /// Abstract artifact Type that handles and defines - /// how can be published into those feeds. + /// Initializes a new artifact type and adds it into + /// the . /// - public abstract class ArtifactType + /// The global object. + /// Name of the type. + protected ArtifactType( StandardGlobalInfo globalInfo, string typeName ) { - readonly StandardGlobalInfo _globalInfo; - readonly string _typeName; - List? _feeds; - List? _artifacts; - List? _pushes; - - /// - /// Initializes a new artifact type and adds it into - /// the . - /// - /// The global object. - /// Name of the type. - protected ArtifactType( StandardGlobalInfo globalInfo, string typeName ) - { - _globalInfo = globalInfo; - _typeName = typeName; - } + _globalInfo = globalInfo; + _typeName = typeName; + } - /// - /// Gets the artifact type name. - /// - public string TypeName => _typeName; + /// + /// Gets the artifact type name. + /// + public string TypeName => _typeName; - /// - /// Gets the global . - /// - public StandardGlobalInfo GlobalInfo => _globalInfo; + /// + /// Gets the global . + /// + public StandardGlobalInfo GlobalInfo => _globalInfo; - /// - /// Gets a mutable list of all the target feeds into which artifacts of this type should be pushed. - /// - /// The list of feeds. - public IList GetTargetFeeds( bool reset = false ) + /// + /// Gets a mutable list of all the target feeds into which artifacts of this type should be pushed. + /// + /// The list of feeds. + public IList GetTargetFeeds( bool reset = false ) + { + if( _feeds == null || reset ) { - if( _feeds == null || reset ) + _feeds = new List(); + if( GlobalInfo.LocalFeedPath != null ) { - _feeds = new List(); - if( GlobalInfo.LocalFeedPath != null ) + foreach( var f in GetLocalFeeds() ) { - foreach( var f in GetLocalFeeds() ) + GlobalInfo.Cake.Information( $"Adding local feed {f.Name}." ); + if( f.ArtifactType != this ) { - GlobalInfo.Cake.Information( $"Adding local feed {f.Name}." ); - if( f.ArtifactType != this ) - { - throw new InvalidOperationException( $"Feed type mismatch." ); - } - _feeds.Add( f ); + throw new InvalidOperationException( $"Feed type mismatch." ); } + _feeds.Add( f ); } - if( GlobalInfo.PushToRemote ) + } + if( GlobalInfo.PushToRemote ) + { + foreach( ArtifactFeed f in GetRemoteFeeds() ) { - foreach( ArtifactFeed f in GetRemoteFeeds() ) + GlobalInfo.Cake.Information( $"Adding remote feed: {f.Name}" ); + if( f.ArtifactType != this ) { - GlobalInfo.Cake.Information( $"Adding remote feed: {f.Name}" ); - if( f.ArtifactType != this ) - { - throw new InvalidOperationException( $"Feed type mismatch." ); - } - _feeds.Add( f ); + throw new InvalidOperationException( $"Feed type mismatch." ); } + _feeds.Add( f ); } } - return _feeds; } + return _feeds; + } - /// - /// Gets a mutable list of all locally produced artifacts of this type. - /// - /// True to recompute a list. - /// The set of artifacts. - public IList GetArtifacts( bool reset = false ) + /// + /// Gets a mutable list of all locally produced artifacts of this type. + /// + /// True to recompute a list. + /// The set of artifacts. + public IList GetArtifacts( bool reset = false ) + { + if( _artifacts == null || reset ) { - if( _artifacts == null || reset ) + _artifacts = new List(); + foreach( var a in GetLocalArtifacts() ) { - _artifacts = new List(); - foreach( var a in GetLocalArtifacts() ) + if( a.ArtifactInstance.Artifact.Type != _typeName ) { - if( a.ArtifactInstance.Artifact.Type != _typeName ) - { - throw new InvalidOperationException( $"Artifact type mismatch: expected '{_typeName}' but got '{a.ArtifactInstance.Artifact.Type}'." ); - } - _artifacts.Add( a ); + throw new InvalidOperationException( $"Artifact type mismatch: expected '{_typeName}' but got '{a.ArtifactInstance.Artifact.Type}'." ); } + _artifacts.Add( a ); } - return _artifacts; } + return _artifacts; + } - /// - /// Gets a mutable list of all the pushes of artifacts into target feeds for this type. - /// - /// The set of pushes. - public async Task> GetPushListAsync( bool reset = false ) + /// + /// Gets a mutable list of all the pushes of artifacts into target feeds for this type. + /// + /// The set of pushes. + public async Task> GetPushListAsync( bool reset = false ) + { + if( _pushes == null || reset ) { - if( _pushes == null || reset ) + _pushes = new List(); + var locals = GetArtifacts(); + var tasks = GetTargetFeeds().Select( f => f.CreatePushListAsync( locals ) ).ToArray(); + foreach( var p in await Task.WhenAll( tasks ) ) { - _pushes = new List(); - var locals = GetArtifacts(); - var tasks = GetTargetFeeds().Select( f => f.CreatePushListAsync( locals ) ).ToArray(); - foreach( var p in await Task.WhenAll( tasks ) ) - { - _pushes.AddRange( p ); - } + _pushes.AddRange( p ); } - return _pushes; } + return _pushes; + } - /// - /// Must push all the required artifacts into all the target feeds. - /// This uses the by default. - /// - /// Push details: defaults to the result of . - public async Task PushAsync( IEnumerable? pushes = null ) - { - if( pushes == null ) pushes = await GetPushListAsync(); - var tasks = pushes.GroupBy( p => p.Feed ).Select( g => g.Key.PushAsync( g ) ).ToArray(); - await Task.WhenAll( tasks ); - } + /// + /// Must push all the required artifacts into all the target feeds. + /// This uses the by default. + /// + /// Push details: defaults to the result of . + public async Task PushAsync( IEnumerable? pushes = null ) + { + if( pushes == null ) pushes = await GetPushListAsync(); + var tasks = pushes.GroupBy( p => p.Feed ).Select( g => g.Key.PushAsync( g ) ).ToArray(); + await Task.WhenAll( tasks ); + } - /// - /// Must get the remote target feeds into which artifacts of this - /// type should be pushed. - /// - /// A set of remote feed. - protected abstract IEnumerable GetRemoteFeeds(); + /// + /// Must get the remote target feeds into which artifacts of this + /// type should be pushed. + /// + /// A set of remote feed. + protected abstract IEnumerable GetRemoteFeeds(); - /// - /// Must get the local target feeds into which artifacts of this - /// type should be pushed. - /// - /// A set of local feeds. - protected abstract IEnumerable GetLocalFeeds(); + /// + /// Must get the local target feeds into which artifacts of this + /// type should be pushed. + /// + /// A set of local feeds. + protected abstract IEnumerable GetLocalFeeds(); - /// - /// Must get the locally produced artifacts of this type. - /// - /// A set of local artifacts. - protected abstract IEnumerable GetLocalArtifacts(); + /// + /// Must get the locally produced artifacts of this type. + /// + /// A set of local artifacts. + protected abstract IEnumerable GetLocalArtifacts(); - } } diff --git a/CodeCakeBuilder/Abstractions/ICIPublishWorkflow.cs b/CodeCakeBuilder/Abstractions/ICIPublishWorkflow.cs index b3524e5..f9db7cd 100644 --- a/CodeCakeBuilder/Abstractions/ICIPublishWorkflow.cs +++ b/CodeCakeBuilder/Abstractions/ICIPublishWorkflow.cs @@ -1,12 +1,11 @@ -namespace CodeCake.Abstractions +namespace CodeCake.Abstractions; + +interface ICIPublishWorkflow { - interface ICIPublishWorkflow - { - /// - /// Pack the solution: it produce the artifacts. - /// - void Pack(); + /// + /// Pack the solution: it produce the artifacts. + /// + void Pack(); - ArtifactType ArtifactType { get; } - } + ArtifactType ArtifactType { get; } } diff --git a/CodeCakeBuilder/Abstractions/ICIWorkflow.cs b/CodeCakeBuilder/Abstractions/ICIWorkflow.cs index f407304..1fa0abd 100644 --- a/CodeCakeBuilder/Abstractions/ICIWorkflow.cs +++ b/CodeCakeBuilder/Abstractions/ICIWorkflow.cs @@ -1,20 +1,19 @@ -namespace CodeCake.Abstractions +namespace CodeCake.Abstractions; + +public interface ICIWorkflow { - public interface ICIWorkflow - { - /// - /// Try to clean the folder, for example by deleting bin & obj. - /// - void Clean(); + /// + /// Try to clean the folder, for example by deleting bin & obj. + /// + void Clean(); - /// - /// Build the solution. - /// - void Build(); + /// + /// Build the solution. + /// + void Build(); - /// - /// Run the unit tests of the solution. - /// - void Test(); - } + /// + /// Run the unit tests of the solution. + /// + void Test(); } diff --git a/CodeCakeBuilder/Abstractions/ILocalArtifact.cs b/CodeCakeBuilder/Abstractions/ILocalArtifact.cs index b8a0d3a..cb198d9 100644 --- a/CodeCakeBuilder/Abstractions/ILocalArtifact.cs +++ b/CodeCakeBuilder/Abstractions/ILocalArtifact.cs @@ -1,16 +1,15 @@ -namespace CodeCake.Abstractions +namespace CodeCake.Abstractions; + +/// +/// Defines a locally produced artifact. +/// This exposes an instance (with the version) even if it is the exact same version for all +/// the artifact inside a repository: exposing the version here MAY enable, if needed, a "local" +/// version definition for an artifact that COULD differ from the repository's one. +/// +public interface ILocalArtifact { /// - /// Defines a locally produced artifact. - /// This exposes an instance (with the version) even if it is the exact same version for all - /// the artifact inside a repository: exposing the version here MAY enable, if needed, a "local" - /// version definition for an artifact that COULD differ from the repository's one. + /// Gets the artifact instance that is produced. /// - public interface ILocalArtifact - { - /// - /// Gets the artifact instance that is produced. - /// - ArtifactInstance ArtifactInstance { get; } - } + ArtifactInstance ArtifactInstance { get; } } diff --git a/CodeCakeBuilder/Build.CreateStandardGlobalInfo.cs b/CodeCakeBuilder/Build.CreateStandardGlobalInfo.cs index 187ebdc..c74f744 100644 --- a/CodeCakeBuilder/Build.CreateStandardGlobalInfo.cs +++ b/CodeCakeBuilder/Build.CreateStandardGlobalInfo.cs @@ -2,85 +2,84 @@ using Cake.Common.Diagnostics; using SimpleGitVersion; -namespace CodeCake +namespace CodeCake; + +public partial class Build { - public partial class Build + /// + /// Creates a new initialized by the + /// current environment. + /// + /// A new info object. + StandardGlobalInfo CreateStandardGlobalInfo() { - /// - /// Creates a new initialized by the - /// current environment. - /// - /// A new info object. - StandardGlobalInfo CreateStandardGlobalInfo() + var result = new StandardGlobalInfo( Cake, Cake.GetRepositoryInfo().FinalBuildInfo ); + // By default: + if( result.IsValid ) { - var result = new StandardGlobalInfo( Cake, Cake.GetRepositoryInfo().FinalBuildInfo ); - // By default: - if( result.IsValid ) + // gitInfo is valid: it is either ci or a release build. + var v = result.BuildInfo.Version; + // If a /LocalFeed/ directory exists above, we publish the packages in it. + var localFeedRoot = Cake.FindSiblingDirectoryAbove( Cake.Environment.WorkingDirectory.FullPath, "LocalFeed" ); + if( localFeedRoot != null ) { - // gitInfo is valid: it is either ci or a release build. - var v = result.BuildInfo.Version; - // If a /LocalFeed/ directory exists above, we publish the packages in it. - var localFeedRoot = Cake.FindSiblingDirectoryAbove( Cake.Environment.WorkingDirectory.FullPath, "LocalFeed" ); - if( localFeedRoot != null ) + if( v.AsCSVersion == null ) { - if( v.AsCSVersion == null ) + if( v.Prerelease.EndsWith( ".local" ) ) { - if( v.Prerelease.EndsWith( ".local" ) ) - { - // Local releases must not be pushed on any remote and are copied to LocalFeed/Local - // feed (if LocalFeed/ directory above exists). - result.IsLocalCIRelease = true; - result.LocalFeedPath = System.IO.Path.Combine( localFeedRoot, "Local" ); - } - else - { - // CI build versions are routed to LocalFeed/CI - result.LocalFeedPath = System.IO.Path.Combine( localFeedRoot, "CI" ); - } + // Local releases must not be pushed on any remote and are copied to LocalFeed/Local + // feed (if LocalFeed/ directory above exists). + result.IsLocalCIRelease = true; + result.LocalFeedPath = System.IO.Path.Combine( localFeedRoot, "Local" ); } else { - // Release or prerelease go to LocalFeed/Release - result.LocalFeedPath = System.IO.Path.Combine( localFeedRoot, "Release" ); + // CI build versions are routed to LocalFeed/CI + result.LocalFeedPath = System.IO.Path.Combine( localFeedRoot, "CI" ); } - System.IO.Directory.CreateDirectory( result.LocalFeedPath ); } - else result.IsLocalCIRelease = v.Prerelease.EndsWith( ".local" ); - - // Creating the right remote feed. - if( !result.IsLocalCIRelease - && (Cake.InteractiveMode() == InteractiveMode.NoInteraction - || Cake.ReadInteractiveOption( "PushToRemote", "Push to Remote feeds?", 'Y', 'N' ) == 'Y') ) + else { - result.PushToRemote = true; + // Release or prerelease go to LocalFeed/Release + result.LocalFeedPath = System.IO.Path.Combine( localFeedRoot, "Release" ); } + System.IO.Directory.CreateDirectory( result.LocalFeedPath ); + } + else result.IsLocalCIRelease = v.Prerelease.EndsWith( ".local" ); + + // Creating the right remote feed. + if( !result.IsLocalCIRelease + && (Cake.InteractiveMode() == InteractiveMode.NoInteraction + || Cake.ReadInteractiveOption( "PushToRemote", "Push to Remote feeds?", 'Y', 'N' ) == 'Y') ) + { + result.PushToRemote = true; + } + } + else + { + if( Cake.InteractiveMode() != InteractiveMode.NoInteraction + && Cake.ReadInteractiveOption( "PublishDirtyRepo", "Repository is not ready to be published. Proceed anyway?", 'Y', 'N' ) == 'Y' ) + { + Cake.Warning( "Unable to compute a valid version, but you choose to continue..." ); + result.IgnoreNoArtifactsToProduce = true; } else { - if( Cake.InteractiveMode() != InteractiveMode.NoInteraction - && Cake.ReadInteractiveOption( "PublishDirtyRepo", "Repository is not ready to be published. Proceed anyway?", 'Y', 'N' ) == 'Y' ) + // On Appveyor, we let the build run: this gracefully handles Pull Requests. + if( Cake.AppVeyor().IsRunningOnAppVeyor ) { - Cake.Warning( "Unable to compute a valid version, but you choose to continue..." ); result.IgnoreNoArtifactsToProduce = true; } else { - // On Appveyor, we let the build run: this gracefully handles Pull Requests. - if( Cake.AppVeyor().IsRunningOnAppVeyor ) - { - result.IgnoreNoArtifactsToProduce = true; - } - else - { - Cake.TerminateWithError( "Repository is not ready to be published." ); - } + Cake.TerminateWithError( "Repository is not ready to be published." ); } - // When the gitInfo is not valid, we do not try to push any packages, even if the build continues - // (either because the user choose to continue or if we are on the CI server). - // We don't need to worry about feeds here. } - return result; + // When the gitInfo is not valid, we do not try to push any packages, even if the build continues + // (either because the user choose to continue or if we are on the CI server). + // We don't need to worry about feeds here. } - + return result; } + } diff --git a/CodeCakeBuilder/Build.cs b/CodeCakeBuilder/Build.cs index 2b214e3..55a57d0 100644 --- a/CodeCakeBuilder/Build.cs +++ b/CodeCakeBuilder/Build.cs @@ -2,76 +2,75 @@ using Cake.Core; using Cake.Core.Diagnostics; -namespace CodeCake -{ +namespace CodeCake; + - /// - /// Sample build "script". - /// Build scripts can be decorated with AddPath attributes that inject existing paths into the PATH environment variable. - /// +/// +/// Sample build "script". +/// Build scripts can be decorated with AddPath attributes that inject existing paths into the PATH environment variable. +/// - public partial class Build : CodeCakeHost +public partial class Build : CodeCakeHost +{ + public Build() { - public Build() - { - Cake.Log.Verbosity = Verbosity.Diagnostic; + Cake.Log.Verbosity = Verbosity.Diagnostic; + + StandardGlobalInfo globalInfo = CreateStandardGlobalInfo() + .AddDotnet() + .SetCIBuildTag(); + + Task( "Check-Repository" ) + .Does( () => + { + globalInfo.TerminateIfShouldStop(); + } ); - StandardGlobalInfo globalInfo = CreateStandardGlobalInfo() - .AddDotnet() - .SetCIBuildTag(); + Task( "Clean" ) + .IsDependentOn( "Check-Repository" ) + .Does( () => + { + globalInfo.GetDotnetSolution().Clean(); + Cake.CleanDirectories( globalInfo.ReleasesFolder.ToString() ); - Task( "Check-Repository" ) - .Does( () => - { - globalInfo.TerminateIfShouldStop(); - } ); + } ); - Task( "Clean" ) - .IsDependentOn( "Check-Repository" ) - .Does( () => - { - globalInfo.GetDotnetSolution().Clean(); - Cake.CleanDirectories( globalInfo.ReleasesFolder.ToString() ); - - } ); + Task( "Build" ) + .IsDependentOn( "Check-Repository" ) + .IsDependentOn( "Clean" ) + .Does( () => + { + globalInfo.GetDotnetSolution().Build(); + } ); - Task( "Build" ) - .IsDependentOn( "Check-Repository" ) - .IsDependentOn( "Clean" ) - .Does( () => - { - globalInfo.GetDotnetSolution().Build(); - } ); + Task( "Unit-Testing" ) + .IsDependentOn( "Build" ) + .WithCriteria( () => Cake.InteractiveMode() == InteractiveMode.NoInteraction + || Cake.ReadInteractiveOption( "RunUnitTests", "Run Unit Tests?", 'Y', 'N' ) == 'Y' ) + .Does( () => + { - Task( "Unit-Testing" ) - .IsDependentOn( "Build" ) - .WithCriteria( () => Cake.InteractiveMode() == InteractiveMode.NoInteraction - || Cake.ReadInteractiveOption( "RunUnitTests", "Run Unit Tests?", 'Y', 'N' ) == 'Y' ) - .Does( () => - { - - globalInfo.GetDotnetSolution().Test(); - } ); + globalInfo.GetDotnetSolution().SolutionTest(); + } ); - Task( "Create-NuGet-Packages" ) - .WithCriteria( () => globalInfo.IsValid ) - .IsDependentOn( "Unit-Testing" ) - .Does( () => - { - globalInfo.GetDotnetSolution().Pack(); - } ); + Task( "Create-NuGet-Packages" ) + .WithCriteria( () => globalInfo.IsValid ) + .IsDependentOn( "Unit-Testing" ) + .Does( () => + { + globalInfo.GetDotnetSolution().Pack(); + } ); - Task( "Push-Artifacts" ) - .IsDependentOn( "Create-NuGet-Packages" ) - .WithCriteria( () => globalInfo.IsValid ) - .Does( async () => - { - await globalInfo.PushArtifactsAsync(); - } ); + Task( "Push-Artifacts" ) + .IsDependentOn( "Create-NuGet-Packages" ) + .WithCriteria( () => globalInfo.IsValid ) + .Does( async () => + { + await globalInfo.PushArtifactsAsync(); + } ); - // The Default task for this script can be set here. - Task( "Default" ) - .IsDependentOn( "Push-Artifacts" ); - } + // The Default task for this script can be set here. + Task( "Default" ) + .IsDependentOn( "Push-Artifacts" ); } } diff --git a/CodeCakeBuilder/CodeCakeBuilder.csproj b/CodeCakeBuilder/CodeCakeBuilder.csproj index 3ffdef4..4d0d3c3 100644 --- a/CodeCakeBuilder/CodeCakeBuilder.csproj +++ b/CodeCakeBuilder/CodeCakeBuilder.csproj @@ -1,23 +1,24 @@ - net6.0 + net8.0 Exe false false false false + annotations - + - + - + diff --git a/CodeCakeBuilder/Program.cs b/CodeCakeBuilder/Program.cs index 76b49cd..adbb42f 100644 --- a/CodeCakeBuilder/Program.cs +++ b/CodeCakeBuilder/Program.cs @@ -2,31 +2,30 @@ using System.Linq; using System.Threading.Tasks; -namespace CodeCake +namespace CodeCake; + +class Program { - class Program - { - /// - /// Basic parameter that sets the solution directory as being the current directory - /// instead of using the default lookup to "Solution/Builder/bin/[Configuration]/[targetFramework]" folder. - /// Check of this argument uses . - /// - const string SolutionDirectoryIsCurrentDirectoryParameter = "SolutionDirectoryIsCurrentDirectory"; + /// + /// Basic parameter that sets the solution directory as being the current directory + /// instead of using the default lookup to "Solution/Builder/bin/[Configuration]/[targetFramework]" folder. + /// Check of this argument uses . + /// + const string SolutionDirectoryIsCurrentDirectoryParameter = "SolutionDirectoryIsCurrentDirectory"; - /// - /// CodeCakeBuilder entry point. This is a default, simple, implementation that can - /// be extended as needed. - /// - /// The command line arguments. - /// An error code (typically negative), 0 on success. - static async Task Main( string[] args ) - { - string? solutionDirectory = args.Contains( SolutionDirectoryIsCurrentDirectoryParameter, StringComparer.OrdinalIgnoreCase ) - ? Environment.CurrentDirectory - : null; - var app = new CodeCakeApplication( solutionDirectory ); - RunResult result = await app.RunAsync( args.Where( a => !StringComparer.OrdinalIgnoreCase.Equals( a, SolutionDirectoryIsCurrentDirectoryParameter ) ) ); - return result.ReturnCode; - } + /// + /// CodeCakeBuilder entry point. This is a default, simple, implementation that can + /// be extended as needed. + /// + /// The command line arguments. + /// An error code (typically negative), 0 on success. + static async Task Main( string[] args ) + { + string? solutionDirectory = args.Contains( SolutionDirectoryIsCurrentDirectoryParameter, StringComparer.OrdinalIgnoreCase ) + ? Environment.CurrentDirectory + : null; + var app = new CodeCakeApplication( solutionDirectory ); + RunResult result = await app.RunAsync( args.Where( a => !StringComparer.OrdinalIgnoreCase.Equals( a, SolutionDirectoryIsCurrentDirectoryParameter ) ) ); + return result.ReturnCode; } } diff --git a/CodeCakeBuilder/StandardGlobalInfo.cs b/CodeCakeBuilder/StandardGlobalInfo.cs index 885eeac..929bc6d 100644 --- a/CodeCakeBuilder/StandardGlobalInfo.cs +++ b/CodeCakeBuilder/StandardGlobalInfo.cs @@ -15,247 +15,245 @@ using System.Net.Http; using System.Threading.Tasks; -namespace CodeCake +namespace CodeCake; + +/// +/// Exposes global state information for the build script. +/// +public class StandardGlobalInfo { + readonly ICakeContext _ctx; + readonly HashSet _solutions = new HashSet(); + List? _artifactPushes; + bool _ignoreNoArtifactsToProduce; + + static StandardGlobalInfo() + { + SharedHttpClient = new HttpClient(); + } + + public StandardGlobalInfo( ICakeContext ctx, ICommitBuildInfo finalBuildInfo ) + { + _ctx = ctx; + BuildInfo = finalBuildInfo; + ReleasesFolder = "CodeCakeBuilder/Releases"; + Directory.CreateDirectory( ReleasesFolder ); + } + + public void RegisterSolution( ICIWorkflow solution ) + { + _solutions.Add( solution ); + } + /// - /// Exposes global state information for the build script. + /// Gets the Cake context. /// - public class StandardGlobalInfo - { - readonly ICakeContext _ctx; - readonly HashSet _solutions = new HashSet(); - List? _artifactPushes; - bool _ignoreNoArtifactsToProduce; + public ICakeContext Cake => _ctx; - static StandardGlobalInfo() - { - SharedHttpClient = new HttpClient(); - } + /// + /// Gets the for this commit. + /// This holds the and of this commit. + /// If is the then this is not really a + /// valid build. + /// + public ICommitBuildInfo BuildInfo { get; } - public StandardGlobalInfo( ICakeContext ctx, ICommitBuildInfo finalBuildInfo ) - { - _ctx = ctx; - BuildInfo = finalBuildInfo; - ReleasesFolder = "CodeCakeBuilder/Releases"; - Directory.CreateDirectory( ReleasesFolder ); - } + /// + /// Gets whether the is not the . + /// + public bool IsValid => BuildInfo.IsValid(); + + IEnumerable SolutionProducingArtifacts => Solutions.OfType(); + + /// + /// Gets the set of of the that have been registered. + /// + public IEnumerable ArtifactTypes => SolutionProducingArtifacts.Select( p => p.ArtifactType ); + + /// + /// Gets the release folder: "CodeCakeBuilder/Releases". + /// + public NormalizedPath ReleasesFolder { get; } + + /// + /// Gets whether this is a purely local build. + /// + public bool IsLocalCIRelease { get; set; } + + /// + /// Shared http client. + /// See: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/ + /// Do not add any default on it. + /// + public static readonly HttpClient SharedHttpClient; - public void RegisterSolution( ICIWorkflow solution ) + /// + /// Gets whether artifacts should be pushed to remote feeds. + /// + public bool PushToRemote { get; set; } + + /// + /// Gets or sets the local feed path. + /// Can be null if no local feed exists or if local feed should be ignored. + /// + public string? LocalFeedPath { get; set; } + + /// + /// Gets or sets whether should be ignored. + /// Defaults to false: by default if there is no packages to produce is true. + /// + public bool IgnoreNoArtifactsToProduce + { + get { - _solutions.Add( solution ); + return Cake.Argument( "IgnoreNoArtifactsToProduce", 'N' ) == 'Y' || _ignoreNoArtifactsToProduce; } - - /// - /// Gets the Cake context. - /// - public ICakeContext Cake => _ctx; - - /// - /// Gets the for this commit. - /// This holds the and of this commit. - /// If is the then this is not really a - /// valid build. - /// - public ICommitBuildInfo BuildInfo { get; } - - /// - /// Gets whether the is not the . - /// - public bool IsValid => BuildInfo.IsValid(); - - IEnumerable SolutionProducingArtifacts => Solutions.OfType(); - - /// - /// Gets the set of of the that have been registered. - /// - public IEnumerable ArtifactTypes => SolutionProducingArtifacts.Select( p => p.ArtifactType ); - - /// - /// Gets the release folder: "CodeCakeBuilder/Releases". - /// - public NormalizedPath ReleasesFolder { get; } - - /// - /// Gets whether this is a purely local build. - /// - public bool IsLocalCIRelease { get; set; } - - /// - /// Shared http client. - /// See: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/ - /// Do not add any default on it. - /// - public static readonly HttpClient SharedHttpClient; - - /// - /// Gets whether artifacts should be pushed to remote feeds. - /// - public bool PushToRemote { get; set; } - - /// - /// Gets or sets the local feed path. - /// Can be null if no local feed exists or if local feed should be ignored. - /// - public string? LocalFeedPath { get; set; } - - /// - /// Gets or sets whether should be ignored. - /// Defaults to false: by default if there is no packages to produce is true. - /// - public bool IgnoreNoArtifactsToProduce + set { - get - { - return Cake.Argument( "IgnoreNoArtifactsToProduce", 'N' ) == 'Y' || _ignoreNoArtifactsToProduce; - } - set - { - _ignoreNoArtifactsToProduce = value; - } + _ignoreNoArtifactsToProduce = value; } + } + + /// + /// Gets whether is empty: all artifacts are already available in all + /// artifact repositories: unless is set to true, there is nothing to + /// do and is true. + /// + public bool NoArtifactsToProduce => !GetArtifactPushList().Any(); - /// - /// Gets whether is empty: all artifacts are already available in all - /// artifact repositories: unless is set to true, there is nothing to - /// do and is true. - /// - public bool NoArtifactsToProduce => !GetArtifactPushList().Any(); - - /// - /// Gets a read only list of all the pushes of artifacts for all . - /// - /// - /// True to recompute the list from all - /// (without reseting them). - /// - /// The set of pushes. - public IReadOnlyList GetArtifactPushList( bool reset = false ) + /// + /// Gets a read only list of all the pushes of artifacts for all . + /// + /// + /// True to recompute the list from all + /// (without reseting them). + /// + /// The set of pushes. + public IReadOnlyList GetArtifactPushList( bool reset = false ) + { + if( _artifactPushes == null || reset ) { - if( _artifactPushes == null || reset ) + _artifactPushes = new List(); + var tasks = ArtifactTypes.Select( f => f.GetPushListAsync() ).ToArray(); + Task.WaitAll( tasks ); + foreach( var p in tasks.Select( t => t.Result ) ) { - _artifactPushes = new List(); - var tasks = ArtifactTypes.Select( f => f.GetPushListAsync() ).ToArray(); - Task.WaitAll( tasks ); - foreach( var p in tasks.Select( t => t.Result ) ) - { - _artifactPushes.AddRange( p ); - } + _artifactPushes.AddRange( p ); } - return _artifactPushes; } + return _artifactPushes; + } - /// - /// Gets whether there is no artifact to produce and is false. - /// - public bool ShouldStop => NoArtifactsToProduce && !IgnoreNoArtifactsToProduce; + /// + /// Gets whether there is no artifact to produce and is false. + /// + public bool ShouldStop => NoArtifactsToProduce && !IgnoreNoArtifactsToProduce; - public IReadOnlyCollection Solutions => _solutions; + public IReadOnlyCollection Solutions => _solutions; - #region Memory key support. + #region Memory key support. - string MemoryFilePath => $"CodeCakeBuilder/MemoryKey.{BuildInfo.CommitSha}.txt"; + string MemoryFilePath => $"CodeCakeBuilder/MemoryKey.{BuildInfo.CommitSha}.txt"; - public void WriteCommitMemoryKey( NormalizedPath key ) - { - if( BuildInfo.IsValid() ) File.AppendAllLines( MemoryFilePath, new[] { key.ToString() } ); - } + public void WriteCommitMemoryKey( NormalizedPath key ) + { + if( BuildInfo.IsValid() ) File.AppendAllLines( MemoryFilePath, new[] { key.ToString() } ); + } - public bool CheckCommitMemoryKey( NormalizedPath key ) + public bool CheckCommitMemoryKey( NormalizedPath key ) + { + bool done = File.Exists( MemoryFilePath ) + ? Array.IndexOf( File.ReadAllLines( MemoryFilePath ), key.Path ) >= 0 + : false; + if( done ) { - bool done = File.Exists( MemoryFilePath ) - ? Array.IndexOf( File.ReadAllLines( MemoryFilePath ), key.Path ) >= 0 - : false; - if( done ) + if( !BuildInfo.IsValid() ) + { + Cake.Information( $"Zero commit. Key exists but is ignored: {key}" ); + done = false; + } + else { - if( !BuildInfo.IsValid() ) - { - Cake.Information( $"Zero commit. Key exists but is ignored: {key}" ); - done = false; - } - else - { - Cake.Information( $"Key exists on this commit: {key}" ); - } + Cake.Information( $"Key exists on this commit: {key}" ); } - return done; } + return done; + } + + #endregion + + /// + /// Simply calls on each + /// with their correct typed artifacts. + /// + public Task PushArtifactsAsync( IEnumerable? pushes = null ) + { + if( pushes == null ) pushes = GetArtifactPushList(); + return Task.WhenAll( ArtifactTypes.Select( t => t.PushAsync( pushes.Where( a => a.Feed.ArtifactType == t ) ) ) ); + } - #endregion + /// + /// Tags the build when running on Appveyor or AzureDevOps (GitLab does not support this). + /// Note that if is true, " (Skipped)" is appended. + /// + /// This info object (allowing fluent syntax). + public StandardGlobalInfo SetCIBuildTag() + { + string AddSkipped( string s ) => ShouldStop ? s + " (Skipped)" : s; - /// - /// Simply calls on each - /// with their correct typed artifacts. - /// - public Task PushArtifactsAsync( IEnumerable? pushes = null ) + string ComputeAzurePipelineUpdateBuildVersion( ICommitBuildInfo buildInfo ) { - if( pushes == null ) pushes = GetArtifactPushList(); - return Task.WhenAll( ArtifactTypes.Select( t => t.PushAsync( pushes.Where( a => a.Feed.ArtifactType == t ) ) ) ); + // Azure (formerly VSTS, formerly VSO) analyzes the stdout to set its build number. + // On clash, the default Azure/VSTS/VSO build number is used: to ensure that the actual + // version will be always be available we need to inject a uniquifier. + string buildVersion = AddSkipped( $"{buildInfo.Version}_{DateTime.UtcNow:yyyyMMdd-HHmmss}" ); + Cake.Information( $"Using Azure build number: {buildVersion}" ); + return $"##vso[build.updatebuildnumber]{buildVersion}"; } - /// - /// Tags the build when running on Appveyor or AzureDevOps (GitLab does not support this). - /// Note that if is true, " (Skipped)" is appended. - /// - /// This info object (allowing fluent syntax). - public StandardGlobalInfo SetCIBuildTag() + void AzurePipelineUpdateBuildVersion( string buildInstruction ) { - string AddSkipped( string s ) => ShouldStop ? s + " (Skipped)" : s; - - string ComputeAzurePipelineUpdateBuildVersion( ICommitBuildInfo buildInfo ) - { - // Azure (formerly VSTS, formerly VSO) analyzes the stdout to set its build number. - // On clash, the default Azure/VSTS/VSO build number is used: to ensure that the actual - // version will be always be available we need to inject a uniquifier. - string buildVersion = AddSkipped( $"{buildInfo.Version}_{DateTime.UtcNow:yyyyMMdd-HHmmss}" ); - Cake.Information( $"Using Azure build number: {buildVersion}" ); - return $"##vso[build.updatebuildnumber]{buildVersion}"; - } + Console.WriteLine(); + Console.WriteLine( buildInstruction ); + Console.WriteLine(); + } - void AzurePipelineUpdateBuildVersion( string buildInstruction ) + IAppVeyorProvider appVeyor = Cake.AppVeyor(); + IAzurePipelinesProvider azure = Cake.AzurePipelines(); + try + { + if( appVeyor.IsRunningOnAppVeyor ) { - Console.WriteLine(); - Console.WriteLine( buildInstruction ); - Console.WriteLine(); + appVeyor.UpdateBuildVersion( AddSkipped( BuildInfo.Version.ToString() ) ); } - IAppVeyorProvider appVeyor = Cake.AppVeyor(); - IAzurePipelinesProvider azure = Cake.AzurePipelines(); - try - { - if( appVeyor.IsRunningOnAppVeyor ) - { - appVeyor.UpdateBuildVersion( AddSkipped( BuildInfo.Version.ToString() ) ); - } - - if( azure.IsRunningOnAzurePipelines ) - { - string azureVersion = ComputeAzurePipelineUpdateBuildVersion( BuildInfo ); - AzurePipelineUpdateBuildVersion( azureVersion ); - } - } - catch( Exception e ) + if( azure.IsRunningOnAzurePipelines ) { - Cake.Warning( "Could not set the Build Version!" ); - Cake.Warning( e ); + string azureVersion = ComputeAzurePipelineUpdateBuildVersion( BuildInfo ); + AzurePipelineUpdateBuildVersion( azureVersion ); } - - return this; } - - /// - /// Terminates the script with success if is true. - /// (Calls Cake.TerminateWithSuccess.) - /// - /// This info object (allowing fluent syntax). - public StandardGlobalInfo TerminateIfShouldStop() + catch( Exception e ) { - if( ShouldStop ) - { - Cake.TerminateWithSuccess( "All packages from this commit are already available. Build skipped." ); - } - return this; + Cake.Warning( "Could not set the Build Version!" ); + Cake.Warning( e ); } + return this; + } + + /// + /// Terminates the script with success if is true. + /// (Calls Cake.TerminateWithSuccess.) + /// + /// This info object (allowing fluent syntax). + public StandardGlobalInfo TerminateIfShouldStop() + { + if( ShouldStop ) + { + Cake.TerminateWithSuccess( "All packages from this commit are already available. Build skipped." ); + } + return this; } } diff --git a/CodeCakeBuilder/dotnet/Build.NuGetArtifactType.cs b/CodeCakeBuilder/dotnet/Build.NuGetArtifactType.cs index 89fea38..35ae52a 100644 --- a/CodeCakeBuilder/dotnet/Build.NuGetArtifactType.cs +++ b/CodeCakeBuilder/dotnet/Build.NuGetArtifactType.cs @@ -9,103 +9,102 @@ using System.Linq; using static CodeCake.Build; -namespace CodeCake +namespace CodeCake; + + +public partial class DotnetSolution : ICIPublishWorkflow { + private ArtifactType? _artifactType; - public partial class DotnetSolution : ICIPublishWorkflow + public ArtifactType ArtifactType { - private ArtifactType? _artifactType; - - public ArtifactType ArtifactType + get { - get - { - if( _artifactType == null ) _artifactType = new NuGetArtifactType( _globalInfo, this ); - return _artifactType; - } + if( _artifactType == null ) _artifactType = new NuGetArtifactType( _globalInfo, this ); + return _artifactType; } + } - public void Pack() + public void Pack() + { + var nugetInfo = _globalInfo.ArtifactTypes.OfType().Single(); + var settings = new DotNetPackSettings().AddVersionArguments( _globalInfo.BuildInfo, c => { - var nugetInfo = _globalInfo.ArtifactTypes.OfType().Single(); - var settings = new DotNetPackSettings().AddVersionArguments( _globalInfo.BuildInfo, c => - { - c.NoBuild = true; - // Includes the .pdb in package. - c.IncludeSymbols = true; - c.Configuration = _globalInfo.BuildInfo.BuildConfiguration; - c.OutputDirectory = _globalInfo.ReleasesFolder.Path; - } ); - foreach( var p in nugetInfo.GetNuGetArtifacts() ) - { - _globalInfo.Cake.Information( p.ArtifactInstance ); - _globalInfo.Cake.DotNetPack( p.Project.Path.FullPath, settings ); - } + c.NoBuild = true; + // Includes the .pdb in package. + c.IncludeSymbols = true; + c.Configuration = _globalInfo.BuildInfo.BuildConfiguration; + c.OutputDirectory = _globalInfo.ReleasesFolder.Path; + } ); + foreach( var p in nugetInfo.GetNuGetArtifacts() ) + { + _globalInfo.Cake.Information( p.ArtifactInstance ); + _globalInfo.Cake.DotNetPack( p.Project.Path.FullPath, settings ); } } +} - public partial class Build +public partial class Build +{ + /// + /// Implements NuGet package handling. + /// + public class NuGetArtifactType : ArtifactType { - /// - /// Implements NuGet package handling. - /// - public class NuGetArtifactType : ArtifactType - { - readonly DotnetSolution _solution; + readonly DotnetSolution _solution; - public class NuGetArtifact : ILocalArtifact + public class NuGetArtifact : ILocalArtifact + { + public NuGetArtifact( SolutionProject p, SVersion v ) { - public NuGetArtifact( SolutionProject p, SVersion v ) - { - Project = p; - ArtifactInstance = new ArtifactInstance( "NuGet", p.Name, v ); - } + Project = p; + ArtifactInstance = new ArtifactInstance( "NuGet", p.Name, v ); + } - public ArtifactInstance ArtifactInstance { get; } + public ArtifactInstance ArtifactInstance { get; } - public SolutionProject Project { get; } - } + public SolutionProject Project { get; } + } - public NuGetArtifactType( StandardGlobalInfo globalInfo, DotnetSolution solution ) - : base( globalInfo, "NuGet" ) - { - _solution = solution; - } + public NuGetArtifactType( StandardGlobalInfo globalInfo, DotnetSolution solution ) + : base( globalInfo, "NuGet" ) + { + _solution = solution; + } - /// - /// Downcasts the mutable list as a set of . - /// - /// The set of NuGet artifacts. - public IEnumerable GetNuGetArtifacts() => GetArtifacts().Cast(); - - /// - /// Gets the remote target feeds. - /// - /// The set of remote NuGet feeds (in practice at most one). - protected override IEnumerable GetRemoteFeeds() - {if( GlobalInfo.BuildInfo.Version.PackageQuality >= CSemVer.PackageQuality.ReleaseCandidate ) yield return new RemoteFeed( this, "nuget.org", "https://api.nuget.org/v3/index.json", "NUGET_ORG_PUSH_API_KEY" ); -if( GlobalInfo.BuildInfo.Version.PackageQuality <= CSemVer.PackageQuality.Stable ) yield return new SignatureVSTSFeed( this, "Signature-OpenSource","NetCore3", "Feeds"); + /// + /// Downcasts the mutable list as a set of . + /// + /// The set of NuGet artifacts. + public IEnumerable GetNuGetArtifacts() => GetArtifacts().Cast(); + + /// + /// Gets the remote target feeds. + /// + /// The set of remote NuGet feeds (in practice at most one). + protected override IEnumerable GetRemoteFeeds() + {if( GlobalInfo.BuildInfo.Version.PackageQuality >= CSemVer.PackageQuality.ReleaseCandidate ) yield return new RemoteFeed( this, "nuget.org", "https://api.nuget.org/v3/index.json", "NUGET_ORG_PUSH_API_KEY" ); +yield return new SignatureVSTSFeed( this, "Signature-OpenSource","NetCore3", "Feeds"); } - /// - /// Gets the local target feeds. - /// - /// The set of remote NuGet feeds (in practice at most one). - protected override IEnumerable GetLocalFeeds() - { - return new NuGetHelper.NuGetFeed[] { - new NugetLocalFeed( this, GlobalInfo.LocalFeedPath ) - }; - } + /// + /// Gets the local target feeds. + /// + /// The set of remote NuGet feeds (in practice at most one). + protected override IEnumerable GetLocalFeeds() + { + return new NuGetHelper.NuGetFeed[] { + new NugetLocalFeed( this, GlobalInfo.LocalFeedPath ) + }; + } - protected override IEnumerable GetLocalArtifacts() - { - return _solution.ProjectsToPublish.Select( p => new NuGetArtifact( p, GlobalInfo.BuildInfo.Version ) ); - } + protected override IEnumerable GetLocalArtifacts() + { + return _solution.ProjectsToPublish.Select( p => new NuGetArtifact( p, GlobalInfo.BuildInfo.Version ) ); } } } diff --git a/CodeCakeBuilder/dotnet/Build.NuGetHelper.cs b/CodeCakeBuilder/dotnet/Build.NuGetHelper.cs index a3b7683..f6fa7cc 100644 --- a/CodeCakeBuilder/dotnet/Build.NuGetHelper.cs +++ b/CodeCakeBuilder/dotnet/Build.NuGetHelper.cs @@ -20,531 +20,531 @@ using System.Threading; using System.Threading.Tasks; -namespace CodeCake +namespace CodeCake; + +public partial class Build { - public partial class Build + public static class NuGetHelper { - public static class NuGetHelper + static readonly SourceCacheContext _sourceCache; + static readonly List> _providers; + static readonly ISettings _settings; + static readonly PackageProviderProxy _sourceProvider; + static readonly List _vstsFeeds; + static ILogger _logger; + + /// + /// Implements a IPackageSourceProvider that mixes sources from NuGet.config settings + /// and sources that are used by the build chain. + /// + class PackageProviderProxy : IPackageSourceProvider { - static readonly SourceCacheContext _sourceCache; - static readonly List> _providers; - static readonly ISettings _settings; - static readonly PackageProviderProxy _sourceProvider; - static readonly List _vstsFeeds; - static ILogger _logger; + readonly IPackageSourceProvider _fromSettings; + readonly Lazy> _sources; + int _definedSourceCount; - /// - /// Implements a IPackageSourceProvider that mixes sources from NuGet.config settings - /// and sources that are used by the build chain. - /// - class PackageProviderProxy : IPackageSourceProvider + public PackageProviderProxy( ISettings settings ) { - readonly IPackageSourceProvider _fromSettings; - readonly Lazy> _sources; - int _definedSourceCount; + _fromSettings = new PackageSourceProvider( settings ); + _sources = new Lazy>( () => new List( _fromSettings.LoadPackageSources() ) ); + } - public PackageProviderProxy( ISettings settings ) + public PackageSource FindOrCreateFromUrl( string name, string urlV3 ) + { + if( string.IsNullOrEmpty( urlV3 ) || (!new Uri( urlV3 ).IsFile && !urlV3.EndsWith( "/v3/index.json" )) ) { - _fromSettings = new PackageSourceProvider( settings ); - _sources = new Lazy>( () => new List( _fromSettings.LoadPackageSources() ) ); + throw new ArgumentException( "Feed requires a /v3/index.json url.", nameof( urlV3 ) ); } - - public PackageSource FindOrCreateFromUrl( string name, string urlV3 ) + if( string.IsNullOrWhiteSpace( name ) ) { - if( string.IsNullOrEmpty( urlV3 ) || (!new Uri( urlV3 ).IsFile && !urlV3.EndsWith( "/v3/index.json" )) ) - { - throw new ArgumentException( "Feed requires a /v3/index.json url.", nameof( urlV3 ) ); - } - if( string.IsNullOrWhiteSpace( name ) ) - { - throw new ArgumentNullException( nameof( name ) ); - } - var exists = _sources.Value.FirstOrDefault( s => !s.IsLocal && s.Source == urlV3 ); - if( exists != null ) return exists; - exists = new PackageSource( urlV3, "CCB-" + name ); - _sources.Value.Insert( _definedSourceCount++, exists ); - return exists; + throw new ArgumentNullException( nameof( name ) ); } + var exists = _sources.Value.FirstOrDefault( s => !s.IsLocal && s.Source == urlV3 ); + if( exists != null ) return exists; + exists = new PackageSource( urlV3, "CCB-" + name ); + _sources.Value.Insert( _definedSourceCount++, exists ); + return exists; + } - public PackageSource FindOrCreateFromLocalPath( string localPath ) - { - if( string.IsNullOrWhiteSpace( localPath ) ) throw new ArgumentNullException( nameof( localPath ) ); - NormalizedPath path = System.IO.Path.GetFullPath( localPath ); - var exists = _sources.Value.FirstOrDefault( s => s.IsLocal && new NormalizedPath( s.Source ) == path ); - if( exists != null ) return exists; - exists = new PackageSource( path, "CCB-" + path.LastPart ); - _sources.Value.Insert( _definedSourceCount++, exists ); - return exists; - } - - string IPackageSourceProvider.ActivePackageSourceName => _fromSettings.ActivePackageSourceName; - - string IPackageSourceProvider.DefaultPushSource => _fromSettings.DefaultPushSource; - - event EventHandler IPackageSourceProvider.PackageSourcesChanged { add { } remove { } } + public PackageSource FindOrCreateFromLocalPath( string localPath ) + { + if( string.IsNullOrWhiteSpace( localPath ) ) throw new ArgumentNullException( nameof( localPath ) ); + NormalizedPath path = System.IO.Path.GetFullPath( localPath ); + var exists = _sources.Value.FirstOrDefault( s => s.IsLocal && new NormalizedPath( s.Source ) == path ); + if( exists != null ) return exists; + exists = new PackageSource( path, "CCB-" + path.LastPart ); + _sources.Value.Insert( _definedSourceCount++, exists ); + return exists; + } - /// - /// Gets all the sources. - /// - /// - public IEnumerable LoadPackageSources() => _sources.Value; + string IPackageSourceProvider.ActivePackageSourceName => _fromSettings.ActivePackageSourceName; - bool IPackageSourceProvider.IsPackageSourceEnabled( string name ) => true; + string IPackageSourceProvider.DefaultPushSource => _fromSettings.DefaultPushSource; - void IPackageSourceProvider.SaveActivePackageSource( PackageSource source ) - { - throw new NotSupportedException( "Should not be called in this scenario." ); - } + event EventHandler IPackageSourceProvider.PackageSourcesChanged { add { } remove { } } - void IPackageSourceProvider.SavePackageSources( IEnumerable sources ) - { - throw new NotSupportedException( "Should not be called in this scenario." ); - } + /// + /// Gets all the sources. + /// + /// + public IEnumerable LoadPackageSources() => _sources.Value; - PackageSource? IPackageSourceProvider.GetPackageSourceByName( string name ) => _sources.Value.FirstOrDefault( s => s.Name == name ); + bool IPackageSourceProvider.IsPackageSourceEnabled( string name ) => true; - PackageSource? IPackageSourceProvider.GetPackageSourceBySource( string source ) => _sources.Value.FirstOrDefault( s => s.Source == source ); + void IPackageSourceProvider.SaveActivePackageSource( PackageSource source ) + { + throw new NotSupportedException( "Should not be called in this scenario." ); + } - void IPackageSourceProvider.RemovePackageSource( string name ) - { - throw new NotSupportedException( "Should not be called in this scenario." ); - } + void IPackageSourceProvider.SavePackageSources( IEnumerable sources ) + { + throw new NotSupportedException( "Should not be called in this scenario." ); + } - void IPackageSourceProvider.EnablePackageSource( string name ) - { - throw new NotSupportedException( "Should not be called in this scenario." ); - } + PackageSource? IPackageSourceProvider.GetPackageSourceByName( string name ) => _sources.Value.FirstOrDefault( s => s.Name == name ); - void IPackageSourceProvider.DisablePackageSource( string name ) - { - throw new NotSupportedException( "Should not be called in this scenario." ); - } + PackageSource? IPackageSourceProvider.GetPackageSourceBySource( string source ) => _sources.Value.FirstOrDefault( s => s.Source == source ); - void IPackageSourceProvider.UpdatePackageSource( PackageSource source, bool updateCredentials, bool updateEnabled ) - { - throw new NotSupportedException( "Should not be called in this scenario." ); - } + void IPackageSourceProvider.RemovePackageSource( string name ) + { + throw new NotSupportedException( "Should not be called in this scenario." ); + } - void IPackageSourceProvider.AddPackageSource( PackageSource source ) - { - throw new NotSupportedException( "Should not be called in this scenario." ); - } + void IPackageSourceProvider.EnablePackageSource( string name ) + { + throw new NotSupportedException( "Should not be called in this scenario." ); + } - public IReadOnlyList LoadAuditSources() - { - throw new NotSupportedException( "Should not be called in this scenario." ); - } + void IPackageSourceProvider.DisablePackageSource( string name ) + { + throw new NotSupportedException( "Should not be called in this scenario." ); } - static NuGetHelper() + void IPackageSourceProvider.UpdatePackageSource( PackageSource source, bool updateCredentials, bool updateEnabled ) { - _settings = Settings.LoadDefaultSettings( Environment.CurrentDirectory ); - _sourceProvider = new PackageProviderProxy( _settings ); - _vstsFeeds = new List(); - // Setting "NoCache" (?) here is required to be able to retry a push after a - // failure. Without it, the PUT is canceled. - _sourceCache = new SourceCacheContext().WithRefreshCacheTrue(); - _providers = new List>(); - _providers.AddRange( Repository.Provider.GetCoreV3() ); + throw new NotSupportedException( "Should not be called in this scenario." ); } - class Logger : ILogger + void IPackageSourceProvider.AddPackageSource( PackageSource source ) { - readonly ICakeContext _ctx; - readonly object _lock; + throw new NotSupportedException( "Should not be called in this scenario." ); + } - public Logger( ICakeContext ctx ) - { - _ctx = ctx; - _lock = new object(); - } + public IReadOnlyList LoadAuditSources() + { + throw new NotSupportedException( "Should not be called in this scenario." ); + } + } - public void LogDebug( string data ) { lock( _lock ) _ctx.Debug( $"NuGet: {data}" ); } - public void LogVerbose( string data ) { lock( _lock ) _ctx.Verbose( $"NuGet: {data}" ); } - public void LogInformation( string data ) { lock( _lock ) _ctx.Information( $"NuGet: {data}" ); } - public void LogMinimal( string data ) { lock( _lock ) _ctx.Information( $"NuGet: {data}" ); } - public void LogWarning( string data ) { lock( _lock ) _ctx.Warning( $"NuGet: {data}" ); } - public void LogError( string data ) { lock( _lock ) _ctx.Error( $"NuGet: {data}" ); } - public void LogSummary( string data ) { lock( _lock ) _ctx.Information( $"NuGet: {data}" ); } - public void LogInformationSummary( string data ) { lock( _lock ) _ctx.Information( $"NuGet: {data}" ); } - public void Log( NuGet.Common.LogLevel level, string data ) { lock( _lock ) _ctx.Information( $"NuGet ({level}): {data}" ); } - public Task LogAsync( NuGet.Common.LogLevel level, string data ) - { - Log( level, data ); - return System.Threading.Tasks.Task.CompletedTask; - } + static NuGetHelper() + { + _settings = Settings.LoadDefaultSettings( Environment.CurrentDirectory ); + _sourceProvider = new PackageProviderProxy( _settings ); + _vstsFeeds = new List(); + // Setting "NoCache" (?) here is required to be able to retry a push after a + // failure. Without it, the PUT is canceled. + _sourceCache = new SourceCacheContext().WithRefreshCacheTrue(); + _providers = new List>(); + _providers.AddRange( Repository.Provider.GetCoreV3() ); + } - public void Log( ILogMessage message ) - { - lock( _lock ) _ctx.Information( $"NuGet ({message.Level}) - Code: {message.Code} - Project: {message.ProjectPath} - {message.Message}" ); - } + class Logger : ILogger + { + readonly ICakeContext _ctx; + readonly object _lock; - public Task LogAsync( ILogMessage message ) - { - Log( message ); - return System.Threading.Tasks.Task.CompletedTask; - } + public Logger( ICakeContext ctx ) + { + _ctx = ctx; + _lock = new object(); } - static ILogger InitializeAndGetLogger( ICakeContext ctx ) + public void LogDebug( string data ) { lock( _lock ) _ctx.Debug( $"NuGet: {data}" ); } + public void LogVerbose( string data ) { lock( _lock ) _ctx.Verbose( $"NuGet: {data}" ); } + public void LogInformation( string data ) { lock( _lock ) _ctx.Information( $"NuGet: {data}" ); } + public void LogMinimal( string data ) { lock( _lock ) _ctx.Information( $"NuGet: {data}" ); } + public void LogWarning( string data ) { lock( _lock ) _ctx.Warning( $"NuGet: {data}" ); } + public void LogError( string data ) { lock( _lock ) _ctx.Error( $"NuGet: {data}" ); } + public void LogSummary( string data ) { lock( _lock ) _ctx.Information( $"NuGet: {data}" ); } + public void LogInformationSummary( string data ) { lock( _lock ) _ctx.Information( $"NuGet: {data}" ); } + public void Log( NuGet.Common.LogLevel level, string data ) { lock( _lock ) _ctx.Information( $"NuGet ({level}): {data}" ); } + public Task LogAsync( NuGet.Common.LogLevel level, string data ) { - if( _logger == null ) - { - ctx.Information( $"Initializing with sources:" ); - foreach( var s in _sourceProvider.LoadPackageSources() ) - { - ctx.Information( $"{s.Name} => {s.Source}" ); - } - _logger = new Logger( ctx ); + Log( level, data ); + return System.Threading.Tasks.Task.CompletedTask; + } - } - return _logger; + public void Log( ILogMessage message ) + { + lock( _lock ) _ctx.Information( $"NuGet ({message.Level}) - Code: {message.Code} - Project: {message.ProjectPath} - {message.Message}" ); } - class Creds : ICredentialProvider + public Task LogAsync( ILogMessage message ) { - private readonly ICakeContext _ctx; + Log( message ); + return System.Threading.Tasks.Task.CompletedTask; + } + } - public Creds( ICakeContext ctx ) + static ILogger InitializeAndGetLogger( ICakeContext ctx ) + { + if( _logger == null ) + { + ctx.Information( $"Initializing with sources:" ); + foreach( var s in _sourceProvider.LoadPackageSources() ) { - _ctx = ctx; + ctx.Information( $"{s.Name} => {s.Source}" ); } + _logger = new Logger( ctx ); + + } + return _logger; + } + + class Creds : ICredentialProvider + { + private readonly ICakeContext _ctx; + + public Creds( ICakeContext ctx ) + { + _ctx = ctx; + } - public string Id { get; } = "Cake"; - - public Task GetAsync( - Uri uri, - IWebProxy proxy, - CredentialRequestType type, - string message, - bool isRetry, - bool nonInteractive, - CancellationToken cancellationToken ) => - System.Threading.Tasks.Task.FromResult( - new CredentialResponse( - new NetworkCredential( - "CKli", - _ctx.InteractiveEnvironmentVariable( - _vstsFeeds.Single( p => new Uri( p.Url ).ToString() == uri.ToString() ).SecretKeyName - ) + public string Id { get; } = "Cake"; + + public Task GetAsync( + Uri uri, + IWebProxy proxy, + CredentialRequestType type, + string message, + bool isRetry, + bool nonInteractive, + CancellationToken cancellationToken ) => + System.Threading.Tasks.Task.FromResult( + new CredentialResponse( + new NetworkCredential( + "CKli", + _ctx.InteractiveEnvironmentVariable( + _vstsFeeds.Single( p => new Uri( p.Url ).ToString() == uri.ToString() ).SecretKeyName ) ) - ); - } + ) + ); + } + /// + /// Base class for NuGet feeds. + /// + public abstract class NuGetFeed : ArtifactFeed + { + readonly PackageSource _packageSource; + readonly SourceRepository _sourceRepository; + readonly AsyncLazy _updater; /// - /// Base class for NuGet feeds. + /// Initialize a new remote feed. + /// Its final is the one of the existing feed if it appears in the existing + /// sources (from NuGet configuration files) or "CCB-" if this is + /// an unexisting source (CCB is for CodeCakeBuilder). /// - public abstract class NuGetFeed : ArtifactFeed + /// The central NuGet handler. + /// Name of the feed. + /// Must be a v3/index.json url otherwise an argument exception is thrown. + protected NuGetFeed( NuGetArtifactType type, string name, string urlV3 ) + : this( type, _sourceProvider.FindOrCreateFromUrl( name, urlV3 ) ) { - readonly PackageSource _packageSource; - readonly SourceRepository _sourceRepository; - readonly AsyncLazy _updater; - /// - /// Initialize a new remote feed. - /// Its final is the one of the existing feed if it appears in the existing - /// sources (from NuGet configuration files) or "CCB-" if this is - /// an unexisting source (CCB is for CodeCakeBuilder). - /// - /// The central NuGet handler. - /// Name of the feed. - /// Must be a v3/index.json url otherwise an argument exception is thrown. - protected NuGetFeed( NuGetArtifactType type, string name, string urlV3 ) - : this( type, _sourceProvider.FindOrCreateFromUrl( name, urlV3 ) ) + if( this is VSTSFeed f ) { - if( this is VSTSFeed f ) + if( HttpHandlerResourceV3.CredentialService == null ) { - if( HttpHandlerResourceV3.CredentialService == null ) - { - HttpHandlerResourceV3.CredentialService = new Lazy( - () => new CredentialService( - providers: new AsyncLazy>( - () => System.Threading.Tasks.Task.FromResult>( - new List { new Creds( Cake ) } ) - ), - nonInteractive: true, - handlesDefaultCredentials: true ) - ); - } - _vstsFeeds.Add( f ); + HttpHandlerResourceV3.CredentialService = new Lazy( + () => new CredentialService( + providers: new AsyncLazy>( + () => System.Threading.Tasks.Task.FromResult>( + new List { new Creds( Cake ) } ) + ), + nonInteractive: true, + handlesDefaultCredentials: true ) + ); } + _vstsFeeds.Add( f ); } + } - /// - /// Initialize a new local feed. - /// Its final is the one of the existing feed if it appears in the existing - /// sources (from NuGet configuration files) or "CCB-GetDirectoryName(localPath)" if this is - /// an unexisting source (CCB is for CodeCakeBuilder). - /// - /// The central NuGet handler. - /// Local path. - protected NuGetFeed( NuGetArtifactType type, string localPath ) - : this( type, _sourceProvider.FindOrCreateFromLocalPath( localPath ) ) - { - } + /// + /// Initialize a new local feed. + /// Its final is the one of the existing feed if it appears in the existing + /// sources (from NuGet configuration files) or "CCB-GetDirectoryName(localPath)" if this is + /// an unexisting source (CCB is for CodeCakeBuilder). + /// + /// The central NuGet handler. + /// Local path. + protected NuGetFeed( NuGetArtifactType type, string localPath ) + : this( type, _sourceProvider.FindOrCreateFromLocalPath( localPath ) ) + { + } - NuGetFeed( NuGetArtifactType type, PackageSource s ) - : base( type ) + NuGetFeed( NuGetArtifactType type, PackageSource s ) + : base( type ) + { + _packageSource = s; + _sourceRepository = new SourceRepository( _packageSource, _providers ); + _updater = new AsyncLazy( async () => { - _packageSource = s; - _sourceRepository = new SourceRepository( _packageSource, _providers ); - _updater = new AsyncLazy( async () => - { - var r = await _sourceRepository.GetResourceAsync(); - // TODO: Update for next NuGet version? - // r.Settings = _settings; - return r; - } ); - } + var r = await _sourceRepository.GetResourceAsync(); + // TODO: Update for next NuGet version? + // r.Settings = _settings; + return r; + } ); + } + + /// + /// Must provide the secret key name that must be found in the environment variables. + /// Without it push is skipped. + /// + public abstract string SecretKeyName { get; } + + /// + /// The url of the source. Can be a local path. + /// + public string Url => _packageSource.Source; + + /// + /// Gets whether this is a local feed (a directory). + /// + public bool IsLocal => _packageSource.IsLocal; + + /// + /// Gets the source name. + /// If the source appears in NuGet configuration files, it is the configured name of the source, otherwise + /// it is prefixed with "CCB-" (CCB is for CodeCakeBuilder). + /// + public override string Name => _packageSource.Name; - /// - /// Must provide the secret key name that must be found in the environment variables. - /// Without it push is skipped. - /// - public abstract string SecretKeyName { get; } - - /// - /// The url of the source. Can be a local path. - /// - public string Url => _packageSource.Source; - - /// - /// Gets whether this is a local feed (a directory). - /// - public bool IsLocal => _packageSource.IsLocal; - - /// - /// Gets the source name. - /// If the source appears in NuGet configuration files, it is the configured name of the source, otherwise - /// it is prefixed with "CCB-" (CCB is for CodeCakeBuilder). - /// - public override string Name => _packageSource.Name; - - /// - /// Creates a list of push entries from a set of local artifacts into this feed. - /// - /// Local artifacts. - /// The set of push into this feed. - public override async Task> CreatePushListAsync( IEnumerable artifacts ) + /// + /// Creates a list of push entries from a set of local artifacts into this feed. + /// + /// Local artifacts. + /// The set of push into this feed. + public override async Task> CreatePushListAsync( IEnumerable artifacts ) + { + var result = new List(); + var logger = InitializeAndGetLogger( Cake ); + MetadataResource meta = await _sourceRepository.GetResourceAsync(); + foreach( var p in artifacts ) { - var result = new List(); - var logger = InitializeAndGetLogger( Cake ); - MetadataResource meta = await _sourceRepository.GetResourceAsync(); - foreach( var p in artifacts ) + var pId = new PackageIdentity( p.ArtifactInstance.Artifact.Name, new NuGetVersion( p.ArtifactInstance.Version.ToNormalizedString() ) ); + if( await meta.Exists( pId, _sourceCache, logger, CancellationToken.None ) ) { - var pId = new PackageIdentity( p.ArtifactInstance.Artifact.Name, new NuGetVersion( p.ArtifactInstance.Version.ToNormalizedString() ) ); - if( await meta.Exists( pId, _sourceCache, logger, CancellationToken.None ) ) - { - Cake.Debug( $" ==> Feed '{Name}' already contains {p.ArtifactInstance}." ); - } - else - { - Cake.Debug( $"Package {p.ArtifactInstance} must be published to remote feed '{Name}'." ); - result.Add( new ArtifactPush( p, this ) ); - } + Cake.Debug( $" ==> Feed '{Name}' already contains {p.ArtifactInstance}." ); } - return result; - } - - /// - /// Pushes a set of packages from .nupkg files that must exist in . - /// - /// The instances to push (that necessary target this feed). - /// The awaitable. - public override async Task PushAsync( IEnumerable pushes ) - { - string apiKey = null; - if( !_packageSource.IsLocal ) + else { - apiKey = ResolveAPIKey(); - if( string.IsNullOrEmpty( apiKey ) ) - { - Cake.Information( $"Could not resolve API key. Push to '{Name}' => '{Url}' is skipped." ); - return; - } + Cake.Debug( $"Package {p.ArtifactInstance} must be published to remote feed '{Name}'." ); + result.Add( new ArtifactPush( p, this ) ); } - Cake.Information( $"Pushing packages to '{Name}' => '{Url}'." ); - var logger = InitializeAndGetLogger( Cake ); - var updater = await _updater; - var names = pushes.Select( p => p.Name + "." + p.Version.WithBuildMetaData( null ).ToNormalizedString() ); - var fullPaths = names.Select( n => ArtifactType.GlobalInfo.ReleasesFolder.AppendPart( n + ".nupkg" ).ToString() ); - - await updater.Push( - fullPaths.ToList(), - string.Empty, // no Symbol source. - 20, //20 seconds timeout - disableBuffering: false, - getApiKey: endpoint => apiKey, - getSymbolApiKey: symbolsEndpoint => null, - noServiceEndpoint: false, - skipDuplicate: true, - symbolPackageUpdateResource: null, - log: logger ); - await OnAllArtifactsPushedAsync( pushes ); } - - /// - /// Called once all the packages are pushed. - /// Does nothing at this level. - /// - /// The instances to push (that necessary target this feed). - /// The awaitable. - protected virtual Task OnAllArtifactsPushedAsync( IEnumerable pushes ) - { - return System.Threading.Tasks.Task.CompletedTask; - } - - /// - /// Must resolve the API key required to push the package. - /// - /// The secret (that can be null or empty). - protected abstract string? ResolveAPIKey(); + return result; } - } - - /// - /// A basic VSTS feed uses "VSTS" for the API key and does not handle views. - /// The https://github.com/Microsoft/artifacts-credprovider must be installed. - /// A Personal Access Token, environment variable - /// must be defined and contains the token. - /// If this SecretKeyName is not defined or empty, push is skipped. - /// - class VSTSFeed : NuGetHelper.NuGetFeed - { - string? _azureFeedPAT; /// - /// Initialize a new remote VSTS feed. + /// Pushes a set of packages from .nupkg files that must exist in . /// - /// Name of the feed. - /// Must be a v3/index.json url otherwise an argument exception is thrown. - /// The secret key name. When null or empty, push is skipped. - public VSTSFeed( NuGetArtifactType t, string name, string urlV3, string secretKeyName ) - : base( t, name, urlV3 ) + /// The instances to push (that necessary target this feed). + /// The awaitable. + public override async Task PushAsync( IEnumerable pushes ) { - SecretKeyName = secretKeyName; + string apiKey = null; + if( !_packageSource.IsLocal ) + { + apiKey = ResolveAPIKey(); + if( string.IsNullOrEmpty( apiKey ) ) + { + Cake.Information( $"Could not resolve API key. Push to '{Name}' => '{Url}' is skipped." ); + return; + } + } + Cake.Information( $"Pushing packages to '{Name}' => '{Url}'." ); + var logger = InitializeAndGetLogger( Cake ); + var updater = await _updater; + var names = pushes.Select( p => p.Name + "." + p.Version.WithBuildMetaData( null ).ToNormalizedString() ); + var fullPaths = names.Select( n => ArtifactType.GlobalInfo.ReleasesFolder.AppendPart( n + ".nupkg" ).ToString() ); + + await updater.Push( + fullPaths.ToList(), + string.Empty, // no Symbol source. + 20, //20 seconds timeout + disableBuffering: false, + getApiKey: endpoint => apiKey, + getSymbolApiKey: symbolsEndpoint => null, + noServiceEndpoint: false, + skipDuplicate: true, + symbolPackageUpdateResource: null, + log: logger ); + await OnAllArtifactsPushedAsync( pushes ); } /// - /// Gets the name of the environment variable that must contain the - /// Personal Access Token that allows push to this feed. - /// The https://github.com/Microsoft/artifacts-credprovider VSS_NUGET_EXTERNAL_FEED_ENDPOINTS - /// will be dynamically generated. + /// Called once all the packages are pushed. + /// Does nothing at this level. /// - public override string SecretKeyName { get; } + /// The instances to push (that necessary target this feed). + /// The awaitable. + protected virtual Task OnAllArtifactsPushedAsync( IEnumerable pushes ) + { + return System.Threading.Tasks.Task.CompletedTask; + } /// - /// Looks up for the environment variable that is required to promote packages. - /// If this variable is empty or not defined, push is skipped. + /// Must resolve the API key required to push the package. /// - /// The Cake context. - /// The "VSTS" API key or null to skip the push. - protected override string? ResolveAPIKey() - { - _azureFeedPAT = Cake.InteractiveEnvironmentVariable( SecretKeyName ); - if( string.IsNullOrWhiteSpace( _azureFeedPAT ) ) - { - Cake.Warning( $"No {SecretKeyName} environment variable found." ); - _azureFeedPAT = null; - } - // The API key for the Credential Provider must be "VSTS". - return _azureFeedPAT != null ? "VSTS" : null; - } + /// The secret (that can be null or empty). + protected abstract string? ResolveAPIKey(); } + } + + /// + /// A basic VSTS feed uses "VSTS" for the API key and does not handle views. + /// The https://github.com/Microsoft/artifacts-credprovider must be installed. + /// A Personal Access Token, environment variable + /// must be defined and contains the token. + /// If this SecretKeyName is not defined or empty, push is skipped. + /// + class VSTSFeed : NuGetHelper.NuGetFeed + { + string? _azureFeedPAT; /// - /// A SignatureVSTSFeed handles Stable, Latest, Preview and CI Azure feed views with - /// package promotion based on the published version. - /// The secret key name is built by : - /// "AZURE_FEED_" + Organization.ToUpperInvariant().Replace( '-', '_' ).Replace( ' ', '_' ) + "_PAT". + /// Initialize a new remote VSTS feed. /// - class SignatureVSTSFeed : VSTSFeed + /// Name of the feed. + /// Must be a v3/index.json url otherwise an argument exception is thrown. + /// The secret key name. When null or empty, push is skipped. + public VSTSFeed( NuGetArtifactType t, string name, string urlV3, string secretKeyName ) + : base( t, name, urlV3 ) { - /// - /// Builds the standardized secret key name from the organization name: this is - /// the Personal Access Token that allows push packages. - /// - /// Organization name. - /// The secret key name to use. - public static string GetSecretKeyName( string organization ) - => "AZURE_FEED_" + organization.ToUpperInvariant() - .Replace( '-', '_' ) - .Replace( ' ', '_' ) - + "_PAT"; + SecretKeyName = secretKeyName; + } - /// - /// Initialize a new SignatureVSTSFeed. - /// Its is set to "-" - /// (and may be prefixed with "CCB-" if it doesn't correspond to a source defined in the NuGet.config settings. - /// - /// Name of the organization. - /// Identifier of the feed in Azure, inside the organization. - public SignatureVSTSFeed( NuGetArtifactType t, string organization, string feedName, string? projectName ) - : base( t, organization + "-" + feedName, - projectName != null - ? $"https://pkgs.dev.azure.com/{organization}/{projectName}/_packaging/{feedName}/nuget/v3/index.json" - : $"https://pkgs.dev.azure.com/{organization}/_packaging/{feedName}/nuget/v3/index.json", - GetSecretKeyName( organization ) ) + /// + /// Gets the name of the environment variable that must contain the + /// Personal Access Token that allows push to this feed. + /// The https://github.com/Microsoft/artifacts-credprovider VSS_NUGET_EXTERNAL_FEED_ENDPOINTS + /// will be dynamically generated. + /// + public override string SecretKeyName { get; } + + /// + /// Looks up for the environment variable that is required to promote packages. + /// If this variable is empty or not defined, push is skipped. + /// + /// The Cake context. + /// The "VSTS" API key or null to skip the push. + protected override string? ResolveAPIKey() + { + _azureFeedPAT = Cake.InteractiveEnvironmentVariable( SecretKeyName ); + if( string.IsNullOrWhiteSpace( _azureFeedPAT ) ) { - Organization = organization; - FeedName = feedName; - ProjectName = projectName; + Cake.Warning( $"No {SecretKeyName} environment variable found." ); + _azureFeedPAT = null; } + // The API key for the Credential Provider must be "VSTS". + return _azureFeedPAT != null ? "VSTS" : null; + } + } - /// - /// Gets the organization name. - /// - public string Organization { get; } + /// + /// A SignatureVSTSFeed handles Stable, Latest, Preview and CI Azure feed views with + /// package promotion based on the published version. + /// The secret key name is built by : + /// "AZURE_FEED_" + Organization.ToUpperInvariant().Replace( '-', '_' ).Replace( ' ', '_' ) + "_PAT". + /// + class SignatureVSTSFeed : VSTSFeed + { + /// + /// Builds the standardized secret key name from the organization name: this is + /// the Personal Access Token that allows push packages. + /// + /// Organization name. + /// The secret key name to use. + public static string GetSecretKeyName( string organization ) + => "AZURE_FEED_" + organization.ToUpperInvariant() + .Replace( '-', '_' ) + .Replace( ' ', '_' ) + + "_PAT"; - /// - /// Gets the feed identifier. - /// - public string FeedName { get; } + /// + /// Initialize a new SignatureVSTSFeed. + /// Its is set to "-" + /// (and may be prefixed with "CCB-" if it doesn't correspond to a source defined in the NuGet.config settings. + /// + /// Name of the organization. + /// Identifier of the feed in Azure, inside the organization. + public SignatureVSTSFeed( NuGetArtifactType t, string organization, string feedName, string? projectName ) + : base( t, organization + "-" + feedName, + projectName != null + ? $"https://pkgs.dev.azure.com/{organization}/{projectName}/_packaging/{feedName}/nuget/v3/index.json" + : $"https://pkgs.dev.azure.com/{organization}/_packaging/{feedName}/nuget/v3/index.json", + GetSecretKeyName( organization ) ) + { + Organization = organization; + FeedName = feedName; + ProjectName = projectName; + } - public string? ProjectName { get; } + /// + /// Gets the organization name. + /// + public string Organization { get; } - /// - /// Implements Package promotion in @CI, @Exploratory, @Preview, @Latest and @Stable views. - /// - /// The Cake context. - /// The set of artifacts to promote. - /// The awaitable. - protected override async Task OnAllArtifactsPushedAsync( IEnumerable pushes ) + /// + /// Gets the feed identifier. + /// + public string FeedName { get; } + + public string? ProjectName { get; } + + /// + /// Implements Package promotion in @CI, @Exploratory, @Preview, @Latest and @Stable views. + /// + /// The Cake context. + /// The set of artifacts to promote. + /// The awaitable. + protected override async Task OnAllArtifactsPushedAsync( IEnumerable pushes ) + { + var basicAuth = Convert.ToBase64String( Encoding.ASCII.GetBytes( ":" + Cake.InteractiveEnvironmentVariable( SecretKeyName ) ) ); + foreach( var p in pushes ) { - var basicAuth = Convert.ToBase64String( Encoding.ASCII.GetBytes( ":" + Cake.InteractiveEnvironmentVariable( SecretKeyName ) ) ); - foreach( var p in pushes ) + foreach( var view in p.Version.PackageQuality.GetAllQualities() ) { - foreach( var view in p.Version.PackageQuality.GetAllQualities() ) + var url = ProjectName != null ? + $"https://pkgs.dev.azure.com/{Organization}/{ProjectName}/_apis/packaging/feeds/{FeedName}/nuget/packagesBatch?api-version=5.0-preview.1" + : $"https://pkgs.dev.azure.com/{Organization}/_apis/packaging/feeds/{FeedName}/nuget/packagesBatch?api-version=5.0-preview.1"; + using( HttpRequestMessage req = new HttpRequestMessage( HttpMethod.Post, url ) ) { - var url = ProjectName != null ? - $"https://pkgs.dev.azure.com/{Organization}/{ProjectName}/_apis/packaging/feeds/{FeedName}/nuget/packagesBatch?api-version=5.0-preview.1" - : $"https://pkgs.dev.azure.com/{Organization}/_apis/packaging/feeds/{FeedName}/nuget/packagesBatch?api-version=5.0-preview.1"; - using( HttpRequestMessage req = new HttpRequestMessage( HttpMethod.Post, url ) ) + req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue( "Basic", basicAuth ); + var body = GetPromotionJSONBody( p.Name, p.Version.ToNormalizedString(), view.ToString() ); + req.Content = new StringContent( body, Encoding.UTF8, "application/json" ); + using( var m = await StandardGlobalInfo.SharedHttpClient.SendAsync( req ) ) { - req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue( "Basic", basicAuth ); - var body = GetPromotionJSONBody( p.Name, p.Version.ToNormalizedString(), view.ToString() ); - req.Content = new StringContent( body, Encoding.UTF8, "application/json" ); - using( var m = await StandardGlobalInfo.SharedHttpClient.SendAsync( req ) ) + if( m.IsSuccessStatusCode ) + { + Cake.Information( $"Package '{p.Name}' promoted to view '@{view}'." ); + } + else { - if( m.IsSuccessStatusCode ) - { - Cake.Information( $"Package '{p.Name}' promoted to view '@{view}'." ); - } - else - { - Cake.Error( $"Package '{p.Name}' promotion to view '@{view}' failed." ); - // Throws! - m.EnsureSuccessStatusCode(); - } + Cake.Error( $"Package '{p.Name}' promotion to view '@{view}' failed." ); + // Throws! + m.EnsureSuccessStatusCode(); } } } } } + } - static string GetPromotionJSONBody( string packageName, string packageVersion, string viewId, bool npm = false ) - { - var bodyFormat = @"{ + static string GetPromotionJSONBody( string packageName, string packageVersion, string viewId, bool npm = false ) + { + var bodyFormat = @"{ ""data"": { ""viewId"": ""{viewId}"" }, @@ -555,69 +555,68 @@ static string GetPromotionJSONBody( string packageName, string packageVersion, s ""protocolType"": ""{NuGetOrNpm}"" }] }"; - return bodyFormat.Replace( "{NuGetOrNpm}", npm ? "Npm" : "NuGet" ) - .Replace( "{viewId}", viewId ) - .Replace( "{packageName}", packageName ) - .Replace( "{packageVersion}", packageVersion ); - } - + return bodyFormat.Replace( "{NuGetOrNpm}", npm ? "Npm" : "NuGet" ) + .Replace( "{viewId}", viewId ) + .Replace( "{packageName}", packageName ) + .Replace( "{packageVersion}", packageVersion ); } + } + + /// + /// A remote feed where push is controlled by its . + /// + class RemoteFeed : NuGetHelper.NuGetFeed + { /// - /// A remote feed where push is controlled by its . + /// Initialize a new remote feed. + /// The push is controlled by an API key name that is the name of an environment variable + /// that must contain the actual API key to push packages. /// - class RemoteFeed : NuGetHelper.NuGetFeed + /// Name of the feed. + /// Must be a v3/index.json url otherwise an argument exception is thrown. + /// The secret key name. + public RemoteFeed( NuGetArtifactType t, string name, string urlV3, string secretKeyName ) + : base( t, name, urlV3 ) { - /// - /// Initialize a new remote feed. - /// The push is controlled by an API key name that is the name of an environment variable - /// that must contain the actual API key to push packages. - /// - /// Name of the feed. - /// Must be a v3/index.json url otherwise an argument exception is thrown. - /// The secret key name. - public RemoteFeed( NuGetArtifactType t, string name, string urlV3, string secretKeyName ) - : base( t, name, urlV3 ) - { - SecretKeyName = secretKeyName; - } - - /// - /// Gets or sets the push API key name. - /// This is the environment variable name that must contain the NuGet API key required to push. - /// - public override string SecretKeyName { get; } - - /// - /// Resolves the API key from environment variable. - /// - /// The Cake context. - /// The API key or null. - protected override string? ResolveAPIKey() - { - if( String.IsNullOrEmpty( SecretKeyName ) ) - { - Cake.Information( $"Remote feed '{Name}' APIKeyName is null or empty." ); - return null; - } - return Cake.InteractiveEnvironmentVariable( SecretKeyName ); - } + SecretKeyName = secretKeyName; } /// - /// Local feed. Pushes are always possible. + /// Gets or sets the push API key name. + /// This is the environment variable name that must contain the NuGet API key required to push. + /// + public override string SecretKeyName { get; } + + /// + /// Resolves the API key from environment variable. /// - class NugetLocalFeed : NuGetHelper.NuGetFeed + /// The Cake context. + /// The API key or null. + protected override string? ResolveAPIKey() { - public NugetLocalFeed( NuGetArtifactType t, string path ) - : base( t, path ) + if( String.IsNullOrEmpty( SecretKeyName ) ) { + Cake.Information( $"Remote feed '{Name}' APIKeyName is null or empty." ); + return null; } + return Cake.InteractiveEnvironmentVariable( SecretKeyName ); + } + } - public override string SecretKeyName => null; - - protected override string? ResolveAPIKey() => null; + /// + /// Local feed. Pushes are always possible. + /// + class NugetLocalFeed : NuGetHelper.NuGetFeed + { + public NugetLocalFeed( NuGetArtifactType t, string path ) + : base( t, path ) + { } + + public override string SecretKeyName => null; + + protected override string? ResolveAPIKey() => null; } } diff --git a/CodeCakeBuilder/dotnet/DotnetSolution.cs b/CodeCakeBuilder/dotnet/DotnetSolution.cs index 9e4b5a4..600ab68 100644 --- a/CodeCakeBuilder/dotnet/DotnetSolution.cs +++ b/CodeCakeBuilder/dotnet/DotnetSolution.cs @@ -16,199 +16,215 @@ using System.Linq; using System.Xml.Linq; -namespace CodeCake +namespace CodeCake; + + +public static class StandardGlobalInfoDotnetExtension { + public static StandardGlobalInfo AddDotnet( this StandardGlobalInfo globalInfo ) + { + DotnetSolution sln = DotnetSolution.FromSolutionInCurrentWorkingDirectory( globalInfo ); + globalInfo.RegisterSolution( sln ); + return globalInfo; + } - public static class StandardGlobalInfoDotnetExtension + public static DotnetSolution GetDotnetSolution( this StandardGlobalInfo globalInfo ) { - public static StandardGlobalInfo AddDotnet( this StandardGlobalInfo globalInfo ) - { - DotnetSolution sln = DotnetSolution.FromSolutionInCurrentWorkingDirectory( globalInfo ); - globalInfo.RegisterSolution( sln ); - return globalInfo; - } + return globalInfo.Solutions.OfType().Single(); + } +} - public static DotnetSolution GetDotnetSolution( this StandardGlobalInfo globalInfo ) - { - return globalInfo.Solutions.OfType().Single(); - } +public partial class DotnetSolution : ICIWorkflow +{ + readonly StandardGlobalInfo _globalInfo; + public readonly string SolutionFileName; + public readonly IReadOnlyList Projects; + public readonly IReadOnlyList ProjectsToPublish; + + DotnetSolution( StandardGlobalInfo globalInfo, + string solutionFileName, + IReadOnlyList projects, + IReadOnlyList projectsToPublish ) + { + _globalInfo = globalInfo; + SolutionFileName = solutionFileName; + Projects = projects; + ProjectsToPublish = projectsToPublish; } - public partial class DotnetSolution : ICIWorkflow + /// + /// Create a DotnetSolution from the sln. + /// + /// The StandardGlobalInfo + /// + /// The predicate used to filter the project to publish. By default the predicate is p => !p.Path.Segments.Contains + /// + /// + public static DotnetSolution FromSolutionInCurrentWorkingDirectory( StandardGlobalInfo globalInfo ) { - readonly StandardGlobalInfo _globalInfo; - public readonly string SolutionFileName; - public readonly IReadOnlyList Projects; - public readonly IReadOnlyList ProjectsToPublish; - - DotnetSolution( StandardGlobalInfo globalInfo, - string solutionFileName, - IReadOnlyList projects, - IReadOnlyList projectsToPublish ) - { - _globalInfo = globalInfo; - SolutionFileName = solutionFileName; - Projects = projects; - ProjectsToPublish = projectsToPublish; - } + string solutionFileName = System.IO.Path.GetFileName( + globalInfo.Cake.GetFiles( "*.sln", + new GlobberSettings + { + Predicate = p => !System.IO.Path.GetFileName( p.Path.FullPath ) + .EndsWith( ".local.sln", StringComparison.OrdinalIgnoreCase ) + } ).Single().FullPath + ); + var sln = globalInfo.Cake.ParseSolution( solutionFileName ); + + var projects = sln + .Projects + .Where( p => p is not SolutionFolder && p.Name != "CodeCakeBuilder" ) + .ToList(); + var projectsToPublish = projects.Where( + p => ((bool?)XDocument.Load( p.Path.FullPath ) + .Root! + .Elements( "PropertyGroup" ) + .Elements( "IsPackable" ).LastOrDefault() ?? true) == true + ) + .ToList(); + + return new DotnetSolution( globalInfo, solutionFileName, projects, projectsToPublish ); + } - /// - /// Create a DotnetSolution from the sln. - /// - /// The StandardGlobalInfo - /// - /// The predicate used to filter the project to publish. By default the predicate is p => !p.Path.Segments.Contains - /// - /// - public static DotnetSolution FromSolutionInCurrentWorkingDirectory( StandardGlobalInfo globalInfo ) - { - string solutionFileName = System.IO.Path.GetFileName( - globalInfo.Cake.GetFiles( "*.sln", - new GlobberSettings - { - Predicate = p => !System.IO.Path.GetFileName( p.Path.FullPath ) - .EndsWith( ".local.sln", StringComparison.OrdinalIgnoreCase ) - } ).Single().FullPath - ); - var sln = globalInfo.Cake.ParseSolution( solutionFileName ); - - var projects = sln - .Projects - .Where( p => !(p is SolutionFolder) - && p.Name != "CodeCakeBuilder" ) - .ToList(); - var projectsToPublish = projects.Where( - p => ((bool?)XDocument.Load( p.Path.FullPath ) - .Root! - .Elements( "PropertyGroup" ) - .Elements( "IsPackable" ).LastOrDefault() ?? true) == true - ) - .ToList(); - - return new DotnetSolution( globalInfo, solutionFileName, projects, projectsToPublish ); - } + /// + /// Cleans the bin/obj of the projects and the TestResults xml. + /// + public void Clean() + { + _globalInfo.Cake.CleanDirectories( Projects.Select( p => p.Path.GetDirectory().Combine( "bin" ) ) ); + _globalInfo.Cake.CleanDirectories( Projects.Select( p => p.Path.GetDirectory().Combine( "obj" ) ) ); + _globalInfo.Cake.DeleteFiles( "Tests/**/TestResult*.xml" ); + } - /// - /// Cleans the bin/obj of the projects and the TestResults xml. - /// - public void Clean() + /// + /// Builds the solution without "CodeCakeBuilder" project itself and + /// optionally other projects. + /// + /// Optional project names (without path nor .csproj extension). + public void Build( params string[] excludedProjectsName ) + { + using( var tempSln = _globalInfo.Cake.CreateTemporarySolutionFile( SolutionFileName ) ) { - _globalInfo.Cake.CleanDirectories( Projects.Select( p => p.Path.GetDirectory().Combine( "bin" ) ) ); - _globalInfo.Cake.CleanDirectories( Projects.Select( p => p.Path.GetDirectory().Combine( "obj" ) ) ); - _globalInfo.Cake.DeleteFiles( "Tests/**/TestResult*.xml" ); + var exclude = new List( excludedProjectsName ) { "CodeCakeBuilder" }; + tempSln.ExcludeProjectsFromBuild( [.. exclude] ); + _globalInfo.Cake.DotNetBuild( tempSln.FullPath.FullPath, + new DotNetBuildSettings().AddVersionArguments( _globalInfo.BuildInfo, s => + { + s.Configuration = _globalInfo.BuildInfo.BuildConfiguration; + } ) ); } + } - /// - /// Builds the solution without "CodeCakeBuilder" project itself and - /// optionally other projects. - /// - /// Optional project names (without path nor .csproj extension). - public void Build( params string[] excludedProjectsName ) + /// + /// Simply 'dotnet test --no-restore --no-build' the solution. + /// + public void SolutionTest() + { + var memKey = $"Test:{SolutionFileName}"; + if( !_globalInfo.CheckCommitMemoryKey( memKey ) ) { - using( var tempSln = _globalInfo.Cake.CreateTemporarySolutionFile( SolutionFileName ) ) + var options = new DotNetTestSettings() { - var exclude = new List( excludedProjectsName ) { "CodeCakeBuilder" }; - tempSln.ExcludeProjectsFromBuild( exclude.ToArray() ); - _globalInfo.Cake.DotNetBuild( tempSln.FullPath.FullPath, - new DotNetBuildSettings().AddVersionArguments( _globalInfo.BuildInfo, s => - { - s.Configuration = _globalInfo.BuildInfo.BuildConfiguration; - } ) ); - } + Configuration = _globalInfo.BuildInfo.BuildConfiguration, + NoRestore = true, + NoBuild = true, + Loggers = ["trx"] + }; + _globalInfo.Cake.DotNetTest( null, options ); } + _globalInfo.WriteCommitMemoryKey( memKey ); + } - public void Test( IEnumerable? testProjects = null ) - { - if( testProjects == null ) - { - testProjects = Projects.Where( p => p.Name.EndsWith( ".Tests" ) ); - } + [Obsolete( "Use the simpler SolutionTest() that simply 'dotnet test --no-restore --no-build' the solution." )] + public void Test( IEnumerable? testProjects = null ) + { + testProjects ??= Projects.Where( p => p.Name.EndsWith( ".Tests" ) ); - foreach( SolutionProject project in testProjects ) + foreach( SolutionProject project in testProjects ) + { + NormalizedPath projectPath = project.Path.GetDirectory().FullPath; + NormalizedPath binDir = projectPath.AppendPart( "bin" ).AppendPart( _globalInfo.BuildInfo.BuildConfiguration ); + NormalizedPath objDir = projectPath.AppendPart( "obj" ); + string assetsJson = File.ReadAllText( objDir.AppendPart( "project.assets.json" ) ); + bool isNunitLite = assetsJson.Contains( "NUnitLite" ); + bool isVSTest = assetsJson.Contains( "Microsoft.NET.Test.Sdk" ); + foreach( NormalizedPath buildDir in + Directory.GetDirectories( binDir ) + .Where( p => Directory.EnumerateFiles( p ).Any() ) + ) { - NormalizedPath projectPath = project.Path.GetDirectory().FullPath; - NormalizedPath binDir = projectPath.AppendPart( "bin" ).AppendPart( _globalInfo.BuildInfo.BuildConfiguration ); - NormalizedPath objDir = projectPath.AppendPart( "obj" ); - string assetsJson = File.ReadAllText( objDir.AppendPart( "project.assets.json" ) ); - bool isNunitLite = assetsJson.Contains( "NUnitLite" ); - bool isVSTest = assetsJson.Contains( "Microsoft.NET.Test.Sdk" ); - foreach( NormalizedPath buildDir in - Directory.GetDirectories( binDir ) - .Where( p => Directory.EnumerateFiles( p ).Any() ) - ) + string framework = buildDir.LastPart; + bool isNetFramework = framework.StartsWith( "net" ) && framework.Length == 6 && int.TryParse( framework.AsSpan( 3 ), out var _ ); + string fileWithoutExtension = buildDir.AppendPart( project.Name ); + string testBinariesPath = ""; + if( isNunitLite ) { - string framework = buildDir.LastPart; - bool isNetFramework = framework.StartsWith( "net" ) && framework.Length == 6 && int.TryParse( framework.Substring( 3 ), out var _ ); - string fileWithoutExtension = buildDir.AppendPart( project.Name ); - string testBinariesPath = ""; - if( isNunitLite ) + // Using NUnitLite. + if( isNetFramework && File.Exists( (testBinariesPath = fileWithoutExtension + ".exe") ) ) { - // Using NUnitLite. - if( isNetFramework && File.Exists( (testBinariesPath = fileWithoutExtension + ".exe") ) ) - { - _globalInfo.Cake.Information( $"Testing via NUnitLite ({framework}): {testBinariesPath}" ); - if( !_globalInfo.CheckCommitMemoryKey( testBinariesPath ) ) - { - _globalInfo.Cake.NUnit3( new[] { testBinariesPath }, new NUnit3Settings - { - Results = new[] { new NUnit3Result() { FileName = FilePath.FromString( projectPath.AppendPart( "TestResult.Net461.xml" ) ) } } - } ); - } - } - else + _globalInfo.Cake.Information( $"Testing via NUnitLite ({framework}): {testBinariesPath}" ); + if( !_globalInfo.CheckCommitMemoryKey( testBinariesPath ) ) { - testBinariesPath = fileWithoutExtension + ".dll"; - _globalInfo.Cake.Information( $"Testing via NUnitLite ({framework}): {testBinariesPath}" ); - if( !_globalInfo.CheckCommitMemoryKey( testBinariesPath ) ) + _globalInfo.Cake.NUnit3( new[] { testBinariesPath }, new NUnit3Settings { - _globalInfo.Cake.DotNetExecute( testBinariesPath ); - } + Results = new[] { new NUnit3Result() { FileName = FilePath.FromString( projectPath.AppendPart( "TestResult.Net461.xml" ) ) } } + } ); } } - if( isVSTest ) + else { testBinariesPath = fileWithoutExtension + ".dll"; - // VS Tests - _globalInfo.Cake.Information( $"Testing via VSTest ({framework}): {testBinariesPath}" ); + _globalInfo.Cake.Information( $"Testing via NUnitLite ({framework}): {testBinariesPath}" ); if( !_globalInfo.CheckCommitMemoryKey( testBinariesPath ) ) { - var options = new DotNetTestSettings() - { - Configuration = _globalInfo.BuildInfo.BuildConfiguration, - Framework = framework, - NoRestore = true, - NoBuild = true, - Loggers = new List() { "trx" } - }; - if( _globalInfo.Cake.Environment.GetEnvironmentVariable( "DisableNodeReUse" ) != null ) - { - options.ArgumentCustomization = args => args.Append( "/nodeReuse:false" ); - } - _globalInfo.Cake.DotNetTest( projectPath, options ); + _globalInfo.Cake.DotNetExecute( testBinariesPath ); } } + } + if( isVSTest ) + { + testBinariesPath = fileWithoutExtension + ".dll"; + // VS Tests + _globalInfo.Cake.Information( $"Testing via VSTest ({framework}): {testBinariesPath}" ); + if( !_globalInfo.CheckCommitMemoryKey( testBinariesPath ) ) + { + var options = new DotNetTestSettings() + { + Configuration = _globalInfo.BuildInfo.BuildConfiguration, + Framework = framework, + NoRestore = true, + NoBuild = true, + Loggers = ["trx"] + }; + if( _globalInfo.Cake.Environment.GetEnvironmentVariable( "DisableNodeReUse" ) != null ) + { + options.ArgumentCustomization = args => args.Append( "/nodeReuse:false" ); + } + _globalInfo.Cake.DotNetTest( projectPath, options ); + } + } - if( !isVSTest && !isNunitLite ) + if( !isVSTest && !isNunitLite ) + { + testBinariesPath = fileWithoutExtension + ".dll"; + _globalInfo.Cake.Information( "Testing via NUnit: {0}", testBinariesPath ); + if( !_globalInfo.CheckCommitMemoryKey( testBinariesPath ) ) { - testBinariesPath = fileWithoutExtension + ".dll"; - _globalInfo.Cake.Information( "Testing via NUnit: {0}", testBinariesPath ); - if( !_globalInfo.CheckCommitMemoryKey( testBinariesPath ) ) + _globalInfo.Cake.NUnit( new[] { testBinariesPath }, new NUnitSettings() { - _globalInfo.Cake.NUnit( new[] { testBinariesPath }, new NUnitSettings() - { - Framework = "v4.5", - ResultsFile = FilePath.FromString( projectPath.AppendPart( "TestResult.Net461.xml" ) ) - } ); + Framework = "v4.5", + ResultsFile = FilePath.FromString( projectPath.AppendPart( "TestResult.Net461.xml" ) ) + } ); - } } - _globalInfo.WriteCommitMemoryKey( testBinariesPath ); } + _globalInfo.WriteCommitMemoryKey( testBinariesPath ); } } + } - void ICIWorkflow.Build() => Build(); + void ICIWorkflow.Build() => Build(); - void ICIWorkflow.Test() => Test(); - } + void ICIWorkflow.Test() => SolutionTest(); } diff --git a/Directory.Build.props b/Directory.Build.props index 2943ffc..462c47c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,6 +9,8 @@ strict + + enable true true @@ -62,16 +64,16 @@ - + - + - +