diff --git a/.editorconfig b/.editorconfig index 6c43eef..0ae4778 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,280 +1,292 @@ root = true -# Top-most EditorConfig file # All files [*] charset = utf-8 -end_of_line = crlf -insert_final_newline = true -indent_style = tab +end_of_line = lf +indent_style = space indent_size = 4 +insert_final_newline = true trim_trailing_whitespace = true -# Microsoft .NET properties -csharp_style_expression_bodied_constructors = false:warning -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion -dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion - -# ReSharper properties -resharper_csharp_wrap_lines = false -resharper_local_function_body = expression_body -dotnet_style_operator_placement_when_wrapping = beginning_of_line -tab_width = 4 -dotnet_style_prefer_compound_assignment = true:suggestion -dotnet_diagnostic.CA1707.severity = none -dotnet_style_allow_statement_immediately_after_block_experimental = true:silent - -# C# files -[*.cs] -# Use file-scoped namespaces -csharp_style_namespace_declarations = file_scoped:suggestion - -# Prefer `var` when type is apparent -csharp_style_var_when_type_is_apparent = true:suggestion -# Prefer explicit type when type is not apparent -csharp_style_var_elsewhere = true:suggestion - -# Prefer expression-bodied members -csharp_style_expression_bodied_methods = true:suggestion - -# Prefer 'this.' qualification for members -dotnet_style_qualification_for_field = true:suggestion -dotnet_style_qualification_for_property = true:suggestion -dotnet_style_qualification_for_method = false:suggestion - -# Analyzer severity: treat warnings as errors for analyzers -dotnet_analyzer_diagnostic.severity = suggestion - -# Example: StyleCop rule as error (if StyleCop is installed) -dotnet_diagnostic.sa1101.severity = error - -# Suppress a specific warning (example: unused variable) -dotnet_diagnostic.cs0168.severity = none - -# Allow underscores in test method names -dotnet_diagnostic.CA1707.severity = none - -# Require 'using' directives to be inside namespaces -dotnet_sort_system_directives_first = true -dotnet_separate_import_directive_groups = true - -# Organize usings on save (supported in some editors) -csharp_indent_labels = one_less_than_current -csharp_using_directive_placement = outside_namespace:silent -csharp_prefer_simple_using_statement = true:suggestion -csharp_prefer_braces = true:silent -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_top_level_statements = true:silent -csharp_style_prefer_primary_constructors = true:suggestion -csharp_prefer_system_threading_lock = true:suggestion -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_expression_bodied_local_functions = false:silent -csharp_style_inlined_variable_declaration = true:suggestion -csharp_style_deconstructed_variable_declaration = true:suggestion -csharp_style_var_for_built_in_types = true:suggestion -#### Core EditorConfig Options #### - -# Indentation and spacing -indent_style = space -tab_width = 4 - -# New line preferences -insert_final_newline = false +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 -#### .NET Code Actions #### +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 -# Type members -dotnet_hide_advanced_members = false -dotnet_member_insertion_location = with_other_members_of_the_same_kind -dotnet_property_generation_behavior = prefer_throwing_properties +# JSON files +[*.json] +indent_size = 2 -# Symbol search -dotnet_search_reference_assemblies = true +# YAML files +[*.{yml,yaml}] +indent_size = 2 -#### .NET Coding Conventions #### +# PowerShell files +# PowerShell uses CRLF to maintain compatibility with Windows and PowerShell conventions +# This overrides the global end_of_line = lf setting and aligns with .gitattributes line 14 +[*.ps1] +indent_size = 4 +end_of_line = crlf +charset = utf-8-bom -# Organize usings -dotnet_separate_import_directive_groups = false -file_header_template = - -# this. and Me. preferences -dotnet_style_qualification_for_event = false:warning -dotnet_style_qualification_for_field = false -dotnet_style_qualification_for_method = false:warning -dotnet_style_qualification_for_property = false:warning - -# Language keywords vs BCL types preferences -dotnet_style_predefined_type_for_locals_parameters_members = true -dotnet_style_predefined_type_for_member_access = true - -# Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity -dotnet_style_parentheses_in_other_operators = never_if_unnecessary -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity - -# Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members - -# Expression-level preferences -dotnet_prefer_system_hash_code = true -dotnet_style_coalesce_expression = true -dotnet_style_collection_initializer = true -dotnet_style_explicit_tuple_names = true -dotnet_style_namespace_match_folder = true -dotnet_style_null_propagation = true -dotnet_style_object_initializer = true -dotnet_style_operator_placement_when_wrapping = beginning_of_line -dotnet_style_prefer_auto_properties = true -dotnet_style_prefer_collection_expression = when_types_loosely_match -dotnet_style_prefer_compound_assignment = true -dotnet_style_prefer_conditional_expression_over_assignment = true -dotnet_style_prefer_conditional_expression_over_return = true -dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed -dotnet_style_prefer_inferred_anonymous_type_member_names = true -dotnet_style_prefer_inferred_tuple_names = true -dotnet_style_prefer_is_null_check_over_reference_equality_method = true -dotnet_style_prefer_simplified_boolean_expressions = true -dotnet_style_prefer_simplified_interpolation = true - -# Field preferences -dotnet_style_readonly_field = true - -# Parameter preferences -dotnet_code_quality_unused_parameters = all - -# Suppression preferences -dotnet_remove_unnecessary_suppression_exclusions = none +# C# files +[*.cs] -# New line preferences -dotnet_style_allow_multiple_blank_lines_experimental = true -dotnet_style_allow_statement_immediately_after_block_experimental = true - -#### C# Coding Conventions #### - -# var preferences -csharp_style_var_elsewhere = true:warning -csharp_style_var_for_built_in_types = true:warning -csharp_style_var_when_type_is_apparent = true:warning - -# Expression-bodied members -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_constructors = true:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_lambdas = true:silent -csharp_style_expression_bodied_local_functions = false:silent -csharp_style_expression_bodied_methods = true:silent -csharp_style_expression_bodied_operators = true:silent -csharp_style_expression_bodied_properties = true:silent - -# Pattern matching preferences -csharp_style_pattern_matching_over_as_with_null_check = true -csharp_style_pattern_matching_over_is_with_cast_check = true -csharp_style_prefer_extended_property_pattern = true -csharp_style_prefer_not_pattern = true -csharp_style_prefer_pattern_matching = true -csharp_style_prefer_switch_expression = true - -# Null-checking preferences -csharp_style_conditional_delegate_call = true - -# Modifier preferences -csharp_prefer_static_anonymous_function = true -csharp_prefer_static_local_function = true -csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async -csharp_style_prefer_readonly_struct = true -csharp_style_prefer_readonly_struct_member = true - -# Code-block preferences -csharp_prefer_braces = true:warning -csharp_prefer_simple_using_statement = true:suggestion -csharp_prefer_system_threading_lock = true:suggestion -csharp_style_namespace_declarations = file_scoped:warning -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_primary_constructors = true:suggestion -csharp_style_prefer_top_level_statements = true:silent - -# Expression-level preferences -csharp_prefer_simple_default_expression = true -csharp_style_deconstructed_variable_declaration = true -csharp_style_implicit_object_creation_when_type_is_apparent = true -csharp_style_inlined_variable_declaration = true -csharp_style_prefer_index_operator = true -csharp_style_prefer_local_over_anonymous_function = true -csharp_style_prefer_null_check_over_type_check = true -csharp_style_prefer_range_operator = true -csharp_style_prefer_tuple_swap = true -csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion -csharp_style_prefer_utf8_string_literals = true -csharp_style_throw_expression = true -csharp_style_unused_value_assignment_preference = discard_variable -csharp_style_unused_value_expression_statement_preference = discard_variable - -# 'using' directive preferences -csharp_using_directive_placement = outside_namespace:silent +# SA0001 - Disable XML documentation file requirement +dotnet_diagnostic.SA0001.severity = none -# New line preferences -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true -csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true -csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true -csharp_style_allow_embedded_statements_on_same_line_experimental = true +# .NET Code Analysis Rules +# Enable .NET analyzers with conservative defaults +dotnet_analyzer_diagnostic.severity = suggestion -#### C# Formatting Rules #### +# IDE (Code Style) Rules +dotnet_diagnostic.IDE0005.severity = suggestion # Remove unnecessary usings +# Allow var usage - modern C# style +dotnet_diagnostic.IDE0007.severity = none # Use var instead of explicit type +dotnet_diagnostic.IDE0008.severity = none # Use explicit type instead of var +# Prefer expression bodies - suppress the suggestion to use block bodies instead +dotnet_diagnostic.IDE0022.severity = none # Expression bodies preferred + +# CA (Code Analysis) Rules - Set defaults +dotnet_diagnostic.CA1000.severity = warning +dotnet_diagnostic.CA1001.severity = warning +dotnet_diagnostic.CA1010.severity = warning +dotnet_diagnostic.CA1016.severity = warning +dotnet_diagnostic.CA1063.severity = warning +dotnet_diagnostic.CA1849.severity = warning # Call async methods when in async method + +# AsyncFixer Rules (all 5 rules explicitly configured) +dotnet_diagnostic.AsyncFixer01.severity = error # Unnecessary async/await +dotnet_diagnostic.AsyncFixer02.severity = error # Blocking synchronous operations inside async methods +dotnet_diagnostic.AsyncFixer03.severity = warning # Fire-and-forget async void +dotnet_diagnostic.AsyncFixer04.severity = error # Fire-and-forget async call inside using block +dotnet_diagnostic.AsyncFixer05.severity = suggestion # Downcasting from Task to Task + +# VSTHRD (Visual Studio Threading) Rules - Common rules explicitly configured +dotnet_diagnostic.VSTHRD100.severity = warning # Avoid async void methods +dotnet_diagnostic.VSTHRD101.severity = warning # Avoid unsupported async delegates +dotnet_diagnostic.VSTHRD102.severity = warning # Implement internal logic asynchronously +dotnet_diagnostic.VSTHRD103.severity = warning # Call async methods when in async method +dotnet_diagnostic.VSTHRD104.severity = warning # Offer async option +dotnet_diagnostic.VSTHRD105.severity = warning # Avoid method overloads that assume TaskScheduler.Current +dotnet_diagnostic.VSTHRD106.severity = warning # Use InvokeAsync to raise async events +dotnet_diagnostic.VSTHRD107.severity = warning # Await Task within using expression +dotnet_diagnostic.VSTHRD108.severity = warning # Assert thread affinity unconditionally +dotnet_diagnostic.VSTHRD109.severity = warning # Switch instead of assert in async methods +dotnet_diagnostic.VSTHRD110.severity = warning # Observe result of async calls +dotnet_diagnostic.VSTHRD111.severity = none # ConfigureAwait - not needed in library code targeting modern .NET +dotnet_diagnostic.VSTHRD112.severity = warning # Implement System.IAsyncDisposable +dotnet_diagnostic.VSTHRD114.severity = warning # Avoid returning null from a Task-returning method +dotnet_diagnostic.VSTHRD200.severity = suggestion # Use Async naming convention + +# Roslynator Rules - Common rules explicitly configured +dotnet_diagnostic.RCS1001.severity = suggestion # Add braces +dotnet_diagnostic.RCS1036.severity = none # Remove unnecessary blank line +dotnet_diagnostic.RCS1037.severity = suggestion # Remove trailing white-space +dotnet_diagnostic.RCS1138.severity = warning # Add summary to documentation comment +dotnet_diagnostic.RCS1140.severity = warning # Add exception to documentation comment +dotnet_diagnostic.RCS1141.severity = suggestion # Add parameter to documentation comment +dotnet_diagnostic.RCS1163.severity = warning # Unused parameter +dotnet_diagnostic.RCS1175.severity = suggestion # Unused this parameter +dotnet_diagnostic.RCS1180.severity = suggestion # Inline lazy initialization +dotnet_diagnostic.RCS1181.severity = suggestion # Convert comment to documentation comment +dotnet_diagnostic.RCS1186.severity = suggestion # Use Regex instance instead of static method +dotnet_diagnostic.RCS1197.severity = suggestion # Optimize StringBuilder.Append/AppendLine call +dotnet_diagnostic.RCS1214.severity = suggestion # Unnecessary interpolated string +dotnet_diagnostic.RCS1227.severity = suggestion # Validate arguments correctly + +# Meziantou Analyzer Rules +dotnet_diagnostic.MA0001.severity = suggestion # StringComparison missing +dotnet_diagnostic.MA0002.severity = suggestion # IEqualityComparer missing +dotnet_diagnostic.MA0003.severity = warning # Add parameter name to improve readability +dotnet_diagnostic.MA0004.severity = suggestion # Use Task.ConfigureAwait(false) +dotnet_diagnostic.MA0006.severity = warning # Use String.Equals instead of equality operator +dotnet_diagnostic.MA0007.severity = suggestion # Add comma after the last value +dotnet_diagnostic.MA0011.severity = suggestion # IFormatProvider is missing +dotnet_diagnostic.MA0016.severity = suggestion # Prefer returning collection abstraction instead of implementation +dotnet_diagnostic.MA0025.severity = warning # Implement the functionality instead of throwing NotImplementedException +dotnet_diagnostic.MA0026.severity = suggestion # Fix TODO comment +dotnet_diagnostic.MA0028.severity = warning # Optimize StringBuilder usage +dotnet_diagnostic.MA0029.severity = warning # Combine LINQ methods +dotnet_diagnostic.MA0036.severity = suggestion # Make class static +dotnet_diagnostic.MA0038.severity = suggestion # Make method static +dotnet_diagnostic.MA0040.severity = warning # Flow the cancellation token +dotnet_diagnostic.MA0048.severity = warning # File name must match type name +dotnet_diagnostic.MA0051.severity = warning # Method is too long +dotnet_diagnostic.MA0053.severity = suggestion # Make class sealed +dotnet_diagnostic.MA0056.severity = suggestion # Do not call overridable members in constructor +dotnet_diagnostic.MA0073.severity = suggestion # Avoid comparison with bool constant +dotnet_diagnostic.MA0076.severity = suggestion # Do not use implicit culture-sensitive ToString in interpolated strings + +# SonarAnalyzer Rules +dotnet_diagnostic.S1118.severity = suggestion # Utility classes should not have public constructors +dotnet_diagnostic.S1135.severity = none # (Disabled: overlaps with MA0026 "Fix TODO comment") +dotnet_diagnostic.S1199.severity = warning # Nested code blocks should not be used +dotnet_diagnostic.S2223.severity = warning # Non-constant static fields should not be visible +dotnet_diagnostic.S2259.severity = warning # Null pointers should not be dereferenced +dotnet_diagnostic.S2583.severity = warning # Conditionally executed code should be reachable +dotnet_diagnostic.S2589.severity = warning # Boolean expressions should not be gratuitous +dotnet_diagnostic.S2696.severity = suggestion # Instance members should not write to "static" fields +dotnet_diagnostic.S2933.severity = warning # Fields that are only assigned in the constructor should be "readonly" +dotnet_diagnostic.S2934.severity = warning # Property assignments should not be made for "readonly" fields +dotnet_diagnostic.S3215.severity = suggestion # "interface" instances should not be cast to concrete types +dotnet_diagnostic.S3216.severity = suggestion # "ConfigureAwait(false)" should be used in library code (especially for .NET Framework 4.6.2 / .NET Standard 2.0 targets) +dotnet_diagnostic.S3218.severity = suggestion # Inner class members should not shadow outer class "static" or type members +dotnet_diagnostic.S3236.severity = warning # Caller information arguments should not be provided explicitly +dotnet_diagnostic.S3242.severity = suggestion # Method parameters should be declared with base types +dotnet_diagnostic.S3247.severity = warning # Duplicate casts should not be made +dotnet_diagnostic.S3253.severity = suggestion # Constructor and destructor declarations should not be redundant +dotnet_diagnostic.S3257.severity = warning # Declarations and initializations should be as concise as possible +dotnet_diagnostic.S3358.severity = warning # Ternary operators should not be nested +dotnet_diagnostic.S3400.severity = warning # Methods should not return constants +dotnet_diagnostic.S3441.severity = warning # Redundant property names should be omitted in anonymous classes +dotnet_diagnostic.S3442.severity = warning # "abstract" classes should not have "public" constructors +dotnet_diagnostic.S3443.severity = warning # Type should not be examined on "System.Type" instances +dotnet_diagnostic.S3449.severity = suggestion # Right operands of shift operators should be integers +dotnet_diagnostic.S3451.severity = warning # Classes should not have only "private" constructors +dotnet_diagnostic.S3604.severity = warning # Member initializer values should not be redundant +dotnet_diagnostic.S3776.severity = suggestion # Cognitive Complexity of methods should not be too high +dotnet_diagnostic.S3881.severity = warning # "IDisposable" should be implemented correctly +dotnet_diagnostic.S3897.severity = suggestion # Classes that provide "Equals()" should implement "IEquatable" +dotnet_diagnostic.S3898.severity = warning # Value types should implement "IEquatable" +dotnet_diagnostic.S3902.severity = warning # "Assembly.GetExecutingAssembly" should not be called +dotnet_diagnostic.S3903.severity = warning # Types should be defined in named namespaces +dotnet_diagnostic.S3904.severity = warning # Assemblies should have version information +dotnet_diagnostic.S3925.severity = warning # "ISerializable" should be implemented correctly +dotnet_diagnostic.S3926.severity = warning # Deserialization methods should be provided for "OptionalField" members +dotnet_diagnostic.S3927.severity = warning # Serialization event handlers should be implemented correctly +dotnet_diagnostic.S4049.severity = suggestion # Properties should be preferred +dotnet_diagnostic.S4056.severity = suggestion # Overloads with a "CultureInfo" or an "IFormatProvider" parameter should be used +dotnet_diagnostic.S4136.severity = warning # Method overloads should be grouped together + +# SecurityCodeScan Rules +dotnet_diagnostic.SCS0005.severity = warning # Weak random number generator +dotnet_diagnostic.SCS0006.severity = warning # Weak hash algorithm +dotnet_diagnostic.SCS0015.severity = warning # Hardcoded password +dotnet_diagnostic.SCS0016.severity = warning # Controller method is vulnerable to CSRF +dotnet_diagnostic.SCS0017.severity = warning # Request validation disabled +dotnet_diagnostic.SCS0018.severity = warning # Path traversal +dotnet_diagnostic.SCS0019.severity = warning # OutputCache conflict +dotnet_diagnostic.SCS0020.severity = warning # SQL injection via EF raw query +dotnet_diagnostic.SCS0026.severity = warning # SQL injection via EF FromSqlRaw +dotnet_diagnostic.SCS0029.severity = warning # Cross-Site Scripting (XSS) +dotnet_diagnostic.SCS0031.severity = warning # SQL injection via EF ExecuteSqlRaw + +# Performance-critical rules for library code +dotnet_diagnostic.CA1062.severity = warning # Validate arguments of public methods +dotnet_diagnostic.CA1508.severity = warning # Avoid dead conditional code +dotnet_diagnostic.CA1510.severity = none # Disabled for multi-targeting: recommends ArgumentNullException.ThrowIfNull (not available on net462/netstandard2.0) +dotnet_diagnostic.CA1810.severity = warning # Initialize static fields inline +dotnet_diagnostic.CA1822.severity = suggestion # Mark members as static +dotnet_diagnostic.CA1825.severity = warning # Avoid zero-length array allocations +dotnet_diagnostic.CA1826.severity = warning # Use property instead of Linq Enumerable method +dotnet_diagnostic.CA1827.severity = warning # Do not use Count/LongCount when Any can be used +dotnet_diagnostic.CA1828.severity = warning # Do not use CountAsync/LongCountAsync when AnyAsync can be used +dotnet_diagnostic.CA1829.severity = warning # Use Length/Count property instead of Enumerable.Count method +dotnet_diagnostic.CA1851.severity = warning # Possible multiple enumerations of IEnumerable collection + +# Async/IAsyncEnumerable specific rules (CRITICAL for this library) +dotnet_diagnostic.CA2007.severity = warning # ConfigureAwait - enforce usage in library async code +dotnet_diagnostic.CA2012.severity = error # Use ValueTasks correctly +dotnet_diagnostic.CA2016.severity = warning # Forward CancellationToken parameter + +# Banned API Analyzer (RS0030) - Enforce async-first best practices +dotnet_diagnostic.RS0030.severity = error # Using banned API - treat as error # New line preferences -csharp_new_line_before_catch = true +csharp_new_line_before_open_brace = all csharp_new_line_before_else = true +csharp_new_line_before_catch = true csharp_new_line_before_finally = true -csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_open_brace = all +csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_between_query_expression_clauses = true # Indentation preferences -csharp_indent_block_contents = true -csharp_indent_braces = false csharp_indent_case_contents = true -csharp_indent_case_contents_when_block = true csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents_when_block = false # Space preferences csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false csharp_space_after_comma = true +csharp_space_before_comma = false csharp_space_after_dot = false -csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_before_dot = false csharp_space_after_semicolon_in_for_statement = true -csharp_space_around_binary_operators = before_and_after +csharp_space_before_semicolon_in_for_statement = false csharp_space_around_declaration_statements = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_before_comma = false -csharp_space_before_dot = false csharp_space_before_open_square_brackets = false -csharp_space_before_semicolon_in_for_statement = false csharp_space_between_empty_square_brackets = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_declaration_name_and_open_parenthesis = false -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_parentheses = false csharp_space_between_square_brackets = false +# Organize usings +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +# Code style rules +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion +dotnet_style_readonly_field = true:suggestion +csharp_prefer_braces = true:suggestion +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:suggestion + +# var preferences - prefer 'var' usage for modern C# style +csharp_style_var_when_type_is_apparent = true:silent +csharp_style_var_for_built_in_types = true:silent +csharp_style_var_elsewhere = true:silent + +# Expression preferences +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion + # Wrapping preferences +# Preserve manual line breaks and allow flexible parameter formatting +csharp_preserve_single_line_statements = false csharp_preserve_single_line_blocks = true -csharp_preserve_single_line_statements = true - -#### Naming styles #### -# Naming rules +# Line length guidance (not enforced by dotnet format, but used by some IDEs) +csharp_max_line_length = 120 +# Naming conventions dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i @@ -288,39 +300,186 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_m dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case # Symbol specifications - dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = # Naming styles +dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_auto_properties = true:silent -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_prefer_simplified_boolean_expressions = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:silent -dotnet_diagnostic.ca1707.severity = suggestion -dotnet_style_qualification_for_event = false:silent +# Disable file header requirements +dotnet_diagnostic.SA1633.severity = none +dotnet_diagnostic.SA1634.severity = none +dotnet_diagnostic.SA1635.severity = none +dotnet_diagnostic.SA1636.severity = none +dotnet_diagnostic.SA1637.severity = none +dotnet_diagnostic.SA1638.severity = none +dotnet_diagnostic.SA1639.severity = none +dotnet_diagnostic.SA1640.severity = none +dotnet_diagnostic.SA1641.severity = none +file_header_template = unset + +# Disable overly strict formatting rules globally +dotnet_diagnostic.SA1505.severity = none +dotnet_diagnostic.SA1508.severity = none +dotnet_diagnostic.SA1110.severity = none +dotnet_diagnostic.SA1009.severity = none +dotnet_diagnostic.SA1111.severity = none +dotnet_diagnostic.SA1500.severity = none +dotnet_diagnostic.SA1101.severity = none + +# Naming - error by default (strict) +dotnet_diagnostic.SA1300.severity = error +dotnet_diagnostic.IDE1006.severity = error +dotnet_diagnostic.CA1707.severity = error + +# Source code - strict rules +[src/**/*.cs] +# Documentation required +dotnet_diagnostic.SA1600.severity = warning +dotnet_diagnostic.SA1601.severity = warning +dotnet_diagnostic.SA1602.severity = warning + +# Library code should preserve synchronization context by default +# Consumers decide whether to use ConfigureAwait(false) when calling library methods +dotnet_diagnostic.MA0004.severity = none # Meziantou: Use ConfigureAwait +dotnet_diagnostic.S3216.severity = none # SonarAnalyzer: ConfigureAwait +dotnet_diagnostic.CA2007.severity = none # .NET Analyzer: Use ConfigureAwait + +# Test projects - relaxed naming, no doc requirements +[tests/**/*.cs] +# Allow Test_Method_Names_With_Underscores +dotnet_diagnostic.SA1300.severity = none +dotnet_diagnostic.IDE1006.severity = none +dotnet_diagnostic.CA1707.severity = none + +# Allow synchronous CancellationTokenSource.Cancel() in tests +dotnet_diagnostic.CA1849.severity = none + +# Relax async/await analyzer rules for tests +dotnet_diagnostic.AsyncFixer01.severity = none # Allow unnecessary async/await in tests +dotnet_diagnostic.AsyncFixer02.severity = none # Allow synchronous blocking in tests +dotnet_diagnostic.AsyncFixer05.severity = none # Allow downcasting in tests +dotnet_diagnostic.IDE0058.severity = none # Allow unused expression values in tests +dotnet_diagnostic.VSTHRD102.severity = none # Allow synchronous implementation in tests +dotnet_diagnostic.VSTHRD103.severity = none # Allow calling sync methods when async alternatives exist in tests +dotnet_diagnostic.VSTHRD104.severity = none # Allow missing async options in tests +dotnet_diagnostic.VSTHRD107.severity = none # Allow Task in using without await in tests +dotnet_diagnostic.VSTHRD114.severity = none # Allow returning null from Task methods in tests +dotnet_diagnostic.VSTHRD200.severity = none # Async suffix not required in test method names + +# Banned API Analyzer - Just warn in tests (allow for testing purposes) +dotnet_diagnostic.RS0030.severity = warning # Using banned API - warn instead of error in tests + +# Meziantou - Relax in tests +dotnet_diagnostic.MA0004.severity = none # ConfigureAwait not needed in tests +dotnet_diagnostic.MA0011.severity = none # IFormatProvider not critical in tests +dotnet_diagnostic.MA0026.severity = none # TODO comments OK in tests +dotnet_diagnostic.MA0040.severity = none # CancellationToken flow not critical in tests +dotnet_diagnostic.MA0048.severity = none # File name matching not critical in tests +dotnet_diagnostic.MA0051.severity = none # Method length OK in tests + +# SonarAnalyzer - Relax in tests +dotnet_diagnostic.S1118.severity = none # Utility class constructors OK in tests +dotnet_diagnostic.S1135.severity = none # TODO tags OK in tests +dotnet_diagnostic.S1144.severity = none # Unused private members OK in tests (reflection-based POCOs) +dotnet_diagnostic.S3216.severity = none # ConfigureAwait not needed in tests +dotnet_diagnostic.S3776.severity = none # Complexity OK in tests +dotnet_diagnostic.S4049.severity = none # Properties vs methods flexibility in tests +dotnet_diagnostic.S6966.severity = none # Async versions not available on all target frameworks + +# .NET Analyzer - Relax in tests +dotnet_diagnostic.CA2007.severity = none # ConfigureAwait not needed in tests + +# SecurityCodeScan - Relax in tests (but keep serious ones) +dotnet_diagnostic.SCS0005.severity = suggestion # Weak random OK for test data + +# No documentation required for tests +dotnet_diagnostic.SA1600.severity = none +dotnet_diagnostic.SA1601.severity = none +dotnet_diagnostic.SA1602.severity = none + +# Benchmark projects - relaxed naming, no doc requirements +[benchmarks/**/*.cs] +# Allow Benchmark_Method_Names +dotnet_diagnostic.SA1300.severity = none +dotnet_diagnostic.IDE1006.severity = none +dotnet_diagnostic.CA1707.severity = none + +# Relax async/await analyzer rules for benchmarks +dotnet_diagnostic.AsyncFixer01.severity = none # Allow unnecessary async/await in benchmarks + +# ConfigureAwait not needed in benchmarks +dotnet_diagnostic.MA0004.severity = none # Meziantou: Use ConfigureAwait +dotnet_diagnostic.S3216.severity = none # SonarAnalyzer: ConfigureAwait +dotnet_diagnostic.CA2007.severity = none # .NET Analyzer: Use ConfigureAwait + +# Banned API Analyzer - Just warn in benchmarks (allow for benchmarking purposes) +dotnet_diagnostic.RS0030.severity = warning # Using banned API - warn instead of error in benchmarks + +# No documentation required for benchmarks +dotnet_diagnostic.SA1600.severity = none +dotnet_diagnostic.SA1601.severity = none +dotnet_diagnostic.SA1602.severity = none + +# Suppress Meziantou, Sonar, and VSTHRD analyzers in benchmarks +dotnet_diagnostic.MA0002.severity = none +dotnet_diagnostic.MA0011.severity = none +dotnet_diagnostic.MA0016.severity = none +dotnet_diagnostic.MA0021.severity = none +dotnet_diagnostic.MA0036.severity = none +dotnet_diagnostic.MA0038.severity = none +dotnet_diagnostic.MA0040.severity = none +dotnet_diagnostic.MA0048.severity = none +dotnet_diagnostic.MA0051.severity = none +dotnet_diagnostic.MA0053.severity = none +dotnet_diagnostic.MA0076.severity = none +dotnet_diagnostic.S1118.severity = none +dotnet_diagnostic.S1144.severity = none +dotnet_diagnostic.S3776.severity = none +dotnet_diagnostic.S6966.severity = none +dotnet_diagnostic.VSTHRD200.severity = none + +# Example projects - relaxed naming, docs encouraged +[examples/**/*.cs] +# Allow Example_Method_Names +dotnet_diagnostic.SA1300.severity = none +dotnet_diagnostic.IDE1006.severity = none +dotnet_diagnostic.CA1707.severity = none -dotnet_diagnostic.ide0051.severity = none +# Documentation helpful but not required +dotnet_diagnostic.SA1600.severity = suggestion +dotnet_diagnostic.SA1601.severity = suggestion +dotnet_diagnostic.SA1602.severity = suggestion + +# Banned API Analyzer - Allow in examples for demonstration purposes +dotnet_diagnostic.RS0030.severity = none # Allow banned APIs in examples for demonstration + +# Suppress Meziantou, Sonar, and VSTHRD analyzers in examples +dotnet_diagnostic.CA2007.severity = none +dotnet_diagnostic.MA0002.severity = none +dotnet_diagnostic.MA0004.severity = none +dotnet_diagnostic.MA0011.severity = none +dotnet_diagnostic.MA0016.severity = none +dotnet_diagnostic.MA0021.severity = none +dotnet_diagnostic.MA0036.severity = none +dotnet_diagnostic.MA0038.severity = none +dotnet_diagnostic.MA0040.severity = none +dotnet_diagnostic.MA0048.severity = none +dotnet_diagnostic.MA0051.severity = none +dotnet_diagnostic.MA0053.severity = none +dotnet_diagnostic.MA0076.severity = none +dotnet_diagnostic.S1118.severity = none +dotnet_diagnostic.S1144.severity = none +dotnet_diagnostic.S3216.severity = none +dotnet_diagnostic.S3776.severity = none +dotnet_diagnostic.S6966.severity = none +dotnet_diagnostic.VSTHRD200.severity = none diff --git a/.github/workflows/docfx.yaml b/.github/workflows/docfx.yaml index d658d96..074c8b5 100644 --- a/.github/workflows/docfx.yaml +++ b/.github/workflows/docfx.yaml @@ -126,19 +126,84 @@ jobs: # Sorting descending by Stable places stable (1) before prerelease (0) of the same version. $stable = if ([string]::IsNullOrEmpty($Matches['prerelease'])) { 1 } else { 0 } [PSCustomObject]@{ - Tag = $t - Major = [int]$Matches['major'] - Minor = [int]$Matches['minor'] - Patch = [int]$Matches['patch'] - Stable = $stable + Tag = $t + Major = [int]$Matches['major'] + Minor = [int]$Matches['minor'] + Patch = [int]$Matches['patch'] + Stable = $stable + PreRelease = $Matches['prerelease'] } } } - # Sort descending by SemVer components so newest stable version comes first - $orderedTags = $taggedVersions | - Sort-Object -Property Major, Minor, Patch, Stable -Descending | - Select-Object -ExpandProperty Tag + # Sort descending by full SemVer precedence (Major, Minor, Patch, Stable, PreRelease) + # Convert to a mutable list so we can use a custom comparison for proper SemVer prerelease ordering. + $tagList = [System.Collections.Generic.List[object]]::new() + foreach ($item in $taggedVersions) { + [void]$tagList.Add($item) + } + + $comparison = [System.Comparison[object]]{ + param($a, $b) + + # Compare Major, Minor, Patch (descending) + if ($a.Major -ne $b.Major) { return [Math]::Sign($b.Major - $a.Major) } + if ($a.Minor -ne $b.Minor) { return [Math]::Sign($b.Minor - $a.Minor) } + if ($a.Patch -ne $b.Patch) { return [Math]::Sign($b.Patch - $a.Patch) } + + # Compare Stable flag (descending: stable=1 > prerelease=0) + if ($a.Stable -ne $b.Stable) { return [Math]::Sign($b.Stable - $a.Stable) } + + # At this point, Major/Minor/Patch/Stable are equal. + # If both are stable (no prerelease), they are equal for our purposes. + $aPre = [string]$a.PreRelease + $bPre = [string]$b.PreRelease + + if ([string]::IsNullOrEmpty($aPre) -and [string]::IsNullOrEmpty($bPre)) { return 0 } + + # Both should be prereleases when Stable is 0, but handle any unexpected cases gracefully. + if ([string]::IsNullOrEmpty($aPre) -and -not [string]::IsNullOrEmpty($bPre)) { return -1 } + if (-not [string]::IsNullOrEmpty($aPre) -and [string]::IsNullOrEmpty($bPre)) { return 1 } + + $aIds = $aPre -split '\.' + $bIds = $bPre -split '\.' + $maxLen = [Math]::Max($aIds.Length, $bIds.Length) + + for ($i = 0; $i -lt $maxLen; $i++) { + if ($i -ge $aIds.Length) { return -1 } # a has fewer identifiers -> lower precedence + if ($i -ge $bIds.Length) { return 1 } # b has fewer identifiers -> lower precedence + + $aId = $aIds[$i] + $bId = $bIds[$i] + + $aIsNum = [int]::TryParse($aId, [ref]([int]$null)) + $bIsNum = [int]::TryParse($bId, [ref]([int]$null)) + + if ($aIsNum -and $bIsNum) { + $aVal = [int]$aId + $bVal = [int]$bId + if ($aVal -ne $bVal) { return [Math]::Sign($bVal - $aVal) } + } + elseif ($aIsNum -and -not $bIsNum) { + # Numeric identifiers have lower precedence than non-numeric. + return 1 + } + elseif (-not $aIsNum -and $bIsNum) { + return -1 + } + else { + $cmp = [string]::CompareOrdinal($aId, $bId) + if ($cmp -ne 0) { return -$cmp } + } + } + + # All identifiers equal + return 0 + } + + $tagList.Sort($comparison) + # We implemented comparison for descending SemVer order directly in the comparison. + $orderedTags = $tagList | Select-Object -ExpandProperty Tag # Build the base path for this GitHub Pages project site: // # GITHUB_REPOSITORY is "owner/repo"; we need just the repo name. @@ -155,6 +220,66 @@ jobs: Set-Content -Path 'docfx_project/_site/versions.json' -Encoding utf8NoBOM Write-Host "Generated versions.json with $($versions.Count) version(s): $($versions | ForEach-Object { $_.version })" + - name: Clean up stale root files from gh-pages + # Before deploying the latest docs to the site root, remove any pre-existing + # root-level files and folders from the gh-pages branch (except the versions/ + # directory, CNAME, and .nojekyll) so that stale DocFX assets from a previous + # build do not linger on the live site. + # The versions/ folder is preserved so that all versioned docs remain accessible + # while the root is refreshed with the new build. + if: inputs.deploy_to_pages != false && inputs.deploy_as_latest != false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: pwsh + run: | + $branchExists = git ls-remote --heads origin gh-pages + if (-not $branchExists) { + Write-Host "ℹ️ gh-pages branch does not exist yet – skipping stale-file cleanup." + exit 0 + } + + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git remote set-url origin "https://x-access-token:$($env:GITHUB_TOKEN)@github.com/$($env:GITHUB_REPOSITORY).git" + + git fetch origin gh-pages + # Create a local tracking branch only if it does not already exist + git show-ref --verify --quiet refs/heads/gh-pages + if ($LASTEXITCODE -ne 0) { + git branch gh-pages origin/gh-pages + } + + $WORK_DIR = Join-Path $env:RUNNER_TEMP 'gh-pages-clean' + # Remove a leftover worktree from a previous failed run, if any + git worktree remove "$WORK_DIR" --force 2>&1 | Out-Null + if (Test-Path $WORK_DIR) { Remove-Item $WORK_DIR -Recurse -Force } + git worktree add "$WORK_DIR" gh-pages + + # Remove all root-level items EXCEPT: + # .git – Git metadata (worktree pointer file) + # CNAME – Custom domain config (if present) + # .nojekyll – Tells GitHub Pages not to run Jekyll + # versions/ – All versioned docs (v1.0.0/, latest/, etc.) + Get-ChildItem -Path $WORK_DIR -Force | Where-Object { + $_.Name -ne '.git' -and + $_.Name -ne 'CNAME' -and + $_.Name -ne '.nojekyll' -and + $_.Name -ne 'versions' + } | Remove-Item -Recurse -Force + + git -C "$WORK_DIR" add -A + git -C "$WORK_DIR" diff --cached --quiet + if ($LASTEXITCODE -ne 0) { + git -C "$WORK_DIR" commit ` + -m "chore: clean up stale root DocFX assets before redeploy [skip ci]" + git -C "$WORK_DIR" push origin HEAD:gh-pages + Write-Host "✅ Stale root files removed from gh-pages." + } else { + Write-Host "ℹ️ No stale files found in gh-pages root – nothing to clean." + } + + git worktree remove "$WORK_DIR" --force + - name: Compute destination directory # Determines the versioned subfolder name for the docs deployment (e.g. /v1.2.3/). # Uses the explicit 'version' input when provided; otherwise falls back to diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index d9c25e9..411e8d6 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -13,6 +13,8 @@ # for a maintainer to manually review and verify the changes before merging # - persist-credentials: false prevents the checkout token from being written to git config for subsequent git commands # (it does NOT, by itself, prevent steps from accessing github.token / GITHUB_TOKEN if you explicitly expose it) +# - After checkout, configuration files (.editorconfig, BannedSymbols.txt, etc.) are fetched from +# the main branch to prevent malicious PRs from disabling analyzers or bypassing code quality checks # - Default GITHUB_TOKEN permissions are restricted to read-only repository contents to limit impact if exposed name: PR Checks v3 (Gated) @@ -22,7 +24,7 @@ permissions: env: CODECOV_MINIMUM: 90 - + on: pull_request_target: # Runs from the main branch, not from PR branch branches: @@ -48,12 +50,26 @@ jobs: persist-credentials: false fetch-depth: 0 + - name: Fetch trusted gitleaks config from main + # Prevent PR from modifying .gitleaks.toml to bypass the scan + run: | + git fetch origin main --depth=1 + git checkout origin/main -- .gitleaks.toml 2>/dev/null || true + shell: bash + - name: Run gitleaks # gitleaks-action@v2 does not support pull_request_target, so invoke the CLI directly + # Pinned to a specific version with SHA256 checksum verification for supply-chain safety run: | GITLEAKS_VERSION="8.24.0" - curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ - | tar xz -C /usr/local/bin gitleaks + GITLEAKS_SHA256="cb49b7de5ee986510fe8666ca0273a6cc15eb82571f2f14832c9e8920751f3a4" + mkdir -p "$HOME/.local/bin" + TARBALL="$(mktemp)" + curl -sSfL -o "$TARBALL" "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + echo "${GITLEAKS_SHA256} ${TARBALL}" | sha256sum -c - || { echo "Checksum verification failed!"; exit 1; } + tar xzf "$TARBALL" -C "$HOME/.local/bin" gitleaks + rm -f "$TARBALL" + export PATH="$HOME/.local/bin:$PATH" gitleaks detect --source . --verbose --redact shell: bash @@ -75,6 +91,10 @@ jobs: persist-credentials: false - name: Fetch trusted configuration files from main branch + # Skip for Dependabot — its package-version bumps to protected files (e.g. + # Directory.Build.props) are legitimate and should not be overwritten by main's + # older versions. Dependabot's identity is GitHub-controlled and not spoofable. + if: github.event.pull_request.user.login != 'dependabot[bot]' run: | echo "Fetching configuration files from main branch to prevent malicious overrides..." @@ -120,6 +140,11 @@ jobs: echo "✅ Configuration files secured - using versions from main branch" - name: Detect protected configuration file changes + # Skip for Dependabot — its bumps to protected files (e.g. Directory.Build.props) + # are legitimate. The guard's threat model is human PR authors disabling analyzers + # in their own PRs; it does not apply to a trusted GitHub-controlled bot whose + # only action is package-version updates. + if: github.event.pull_request.user.login != 'dependabot[bot]' run: | echo "Checking for changes to protected configuration files in this PR..." @@ -187,7 +212,7 @@ jobs: # ============================================================================ # STAGE 1: Linux - .NET Core/5+ Tests with Coverage Gate # ============================================================================ - test-linux-core: + test-linux-core: name: "Stage 1: Linux Tests (.NET 5.0-10.0) + Coverage Gate" runs-on: ubuntu-latest needs: detect-projects @@ -201,6 +226,10 @@ jobs: persist-credentials: false - name: Fetch trusted configuration files from main branch + # Skip for Dependabot — its package-version bumps to protected files (e.g. + # Directory.Build.props) are legitimate and should not be overwritten by main's + # older versions. Dependabot's identity is GitHub-controlled and not spoofable. + if: github.event.pull_request.user.login != 'dependabot[bot]' run: | echo "Fetching configuration files from main branch to prevent malicious overrides..." @@ -245,6 +274,55 @@ jobs: echo "" echo "✅ Configuration files secured - using versions from main branch" + - name: Fetch trusted configuration files from main branch + # Skip for Dependabot — its package-version bumps to protected files (e.g. + # Directory.Build.props) are legitimate and should not be overwritten by main's + # older versions. Dependabot's identity is GitHub-controlled and not spoofable. + if: github.event.pull_request.user.login != 'dependabot[bot]' + run: | + echo "Fetching configuration files from main branch to prevent malicious overrides..." + + # Fetch the main branch + git fetch origin main:main-branch + + # List of configuration files that should come from trusted main branch + config_files=( + ".editorconfig" + "Directory.Build.props" + "Directory.Build.targets" + "BannedSymbols.txt" + "*.globalconfig" + "*.ruleset" + ".github/workflows/*.yml" + ".github/workflows/*.yaml" + ) + + # Copy each configuration file from main branch if it exists + for config_file in "${config_files[@]}"; do + # Handle glob patterns + if [[ "$config_file" == *"*"* ]]; then + # Find files matching the pattern in main branch + git ls-tree -r --name-only main-branch | grep -E "${config_file//\*/.*}" | while read -r file; do + if [ -n "$file" ]; then + echo " ✓ Copying $file from main branch" + mkdir -p "$(dirname "$file")" + git show "main-branch:$file" > "$file" || echo " ⚠️ Failed to copy $file" + fi + done + else + # Check if file exists in main branch + if git cat-file -e "main-branch:$config_file" 2>/dev/null; then + echo " ✓ Copying $config_file from main branch" + git show "main-branch:$config_file" > "$config_file" + else + echo " ℹ️ $config_file not found in main branch, skipping" + fi + fi + done + + echo "" + echo "✅ Configuration files secured - using versions from main branch" + # Fix for .NET 5.0 on Ubuntu 22.04+ - install libssl1.1 from the focal-security # repository so APT verifies the package via GPG instead of a plain wget download. - name: Install OpenSSL 1.1 for .NET 5.0 @@ -321,10 +399,17 @@ jobs: for proj in "${projects[@]}"; do echo "Building: $proj" - # Extract target frameworks from the project file - # Support both (single) and (multiple) - # Collapse newlines so multi-line values are handled correctly - frameworks=$(tr '\n' ' ' < "$proj" | grep -oP '\s*\K[^<]+' | tr ';' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -E '^(net(5\.0|6\.0|7\.0|8\.0|9\.0|10\.0)|netcoreapp[0-9.]+|netstandard[0-9.]+)$' || true) + # Extract target frameworks via MSBuild property evaluation. + # This handles multi-line XML, conditional property groups, + # and TFMs inherited from Directory.Build.props — all of which break grep-based parsing. + # Falls back from (multiple) to (single). + tfm_raw=$(dotnet msbuild "$proj" -noLogo -getProperty:TargetFrameworks 2>/dev/null \ + | grep -v '^[[:space:]]*$' | tail -n1 | sed 's/^TargetFrameworks[=:][[:space:]]*//' | tr -d '[:space:]') + if [ -z "$tfm_raw" ]; then + tfm_raw=$(dotnet msbuild "$proj" -noLogo -getProperty:TargetFramework 2>/dev/null \ + | grep -v '^[[:space:]]*$' | tail -n1 | sed 's/^TargetFramework[=:][[:space:]]*//' | tr -d '[:space:]') + fi + frameworks=$(printf '%s' "$tfm_raw" | tr ';' '\n' | grep -E '^(net(5\.0|6\.0|7\.0|8\.0|9\.0|10\.0)|netcoreapp[0-9.]+|netstandard[0-9.]+)$' || true) if [ -z "$frameworks" ]; then echo "⚠️ No Linux-compatible frameworks found in $proj" @@ -354,12 +439,20 @@ jobs: - name: Run tests with coverage (.NET Core 5.0 - 10.0) run: | - # Find all test projects (C#, VB.NET, F#) + # Find all test projects (C#, VB.NET, F#). + # Gracefully skip if there is no ./tests directory (e.g. template-publishing + # repos or library repos in early development that have no tests yet). + # The downstream coverage steps already handle the no-coverage-files case. + if [ ! -d ./tests ]; then + echo "ℹ️ No ./tests directory — skipping test stage." + exit 0 + fi + mapfile -d '' -t test_projects < <(find ./tests -type f \( -name "*.csproj" -o -name "*.vbproj" -o -name "*.fsproj" \) -print0) - + if [ ${#test_projects[@]} -eq 0 ]; then - echo "❌ No test projects found in ./tests directory!" - exit 1 + echo "ℹ️ No test projects found under ./tests — skipping test stage." + exit 0 fi echo "==========================================" @@ -373,9 +466,16 @@ jobs: echo "Testing project: $test_proj" echo "==========================================" - # Extract target frameworks from the project file - # Support both (single) and (multiple) - frameworks=$(grep -oP '\K[^<]+' "$test_proj" | tr ';' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -E '^(net(5\.0|6\.0|7\.0|8\.0|9\.0|10\.0)|netcoreapp3\.1)$' || true) + # Extract target frameworks via MSBuild property evaluation (handles multi-line XML + # and Directory.Build.props inheritance — both break grep-based parsing). + # Falls back from (multiple) to (single). + tfm_raw=$(dotnet msbuild "$test_proj" -noLogo -getProperty:TargetFrameworks 2>/dev/null \ + | grep -v '^[[:space:]]*$' | tail -n1 | sed 's/^TargetFrameworks[=:][[:space:]]*//' | tr -d '[:space:]') + if [ -z "$tfm_raw" ]; then + tfm_raw=$(dotnet msbuild "$test_proj" -noLogo -getProperty:TargetFramework 2>/dev/null \ + | grep -v '^[[:space:]]*$' | tail -n1 | sed 's/^TargetFramework[=:][[:space:]]*//' | tr -d '[:space:]') + fi + frameworks=$(printf '%s' "$tfm_raw" | tr ';' '\n' | grep -E '^(net(5\.0|6\.0|7\.0|8\.0|9\.0|10\.0)|netcoreapp3\.1)$' || true) if [ -z "$frameworks" ]; then echo "⊘ Skipping: No compatible .NET 5.0-10.0 target frameworks found" @@ -395,6 +495,7 @@ jobs: --configuration Release \ --framework "$fw" \ --collect:"XPlat Code Coverage" \ + --settings coverlet.runsettings \ --results-directory "./TestResults" \ --logger "console;verbosity=minimal" || exit 1 done <<< "$frameworks" @@ -552,6 +653,51 @@ jobs: Write-Host "" Write-Host "✅ Configuration files secured - using versions from main branch" + - name: Fetch trusted configuration files from main branch + shell: pwsh + run: | + Write-Host "Fetching configuration files from main branch to prevent malicious overrides..." + + # Fetch the main branch + git fetch origin main:main-branch + + # List of configuration files that should come from trusted main branch + $configFiles = @( + ".editorconfig", + "Directory.Build.props", + "Directory.Build.targets", + "BannedSymbols.txt" + ) + + # Copy each configuration file from main branch if it exists + foreach ($configFile in $configFiles) { + # Check if file exists in main branch + $exists = git cat-file -e "main-branch:$configFile" 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Host " ✓ Copying $configFile from main branch" + git show "main-branch:$configFile" | Out-File -FilePath $configFile -Encoding UTF8 -NoNewline + } else { + Write-Host " ℹ️ $configFile not found in main branch, skipping" + } + } + + # Handle glob patterns for .globalconfig, .ruleset, and workflow files + $globPatterns = @("*.globalconfig", "*.ruleset", ".github/workflows/*.yml", ".github/workflows/*.yaml") + foreach ($pattern in $globPatterns) { + $files = git ls-tree -r --name-only main-branch | Select-String -Pattern $pattern.Replace("*", ".*") + foreach ($file in $files) { + if ($file) { + Write-Host " ✓ Copying $file from main branch" + $dir = Split-Path -Parent $file + if ($dir) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } + git show "main-branch:$file" | Out-File -FilePath $file -Encoding UTF8 -NoNewline + } + } + } + + Write-Host "" + Write-Host "✅ Configuration files secured - using versions from main branch" + - name: Setup .NET uses: actions/setup-dotnet@v5 with: @@ -574,12 +720,19 @@ jobs: shell: pwsh run: | $ErrorActionPreference = 'Stop' - + + # Gracefully skip if there is no ./tests directory (e.g. template-publishing + # repos or library repos in early development that have no tests yet). + if (-not (Test-Path -Path './tests' -PathType Container)) { + Write-Host "ℹ️ No ./tests directory — skipping test stage." + exit 0 + } + $testProjects = @(Get-ChildItem -Path './tests/*' -Recurse -File -Include '*.csproj','*.vbproj','*.fsproj') - + if (@($testProjects).Count -eq 0) { - Write-Error "❌ No test projects found in ./tests directory!" - exit 1 + Write-Host "ℹ️ No test projects found under ./tests — skipping test stage." + exit 0 } Write-Host "==========================================" -ForegroundColor Cyan @@ -747,6 +900,10 @@ jobs: persist-credentials: false - name: Fetch trusted configuration files from main branch + # Skip for Dependabot — its package-version bumps to protected files (e.g. + # Directory.Build.props) are legitimate and should not be overwritten by main's + # older versions. Dependabot's identity is GitHub-controlled and not spoofable. + if: github.event.pull_request.user.login != 'dependabot[bot]' run: | echo "Fetching configuration files from main branch to prevent malicious overrides..." @@ -791,6 +948,55 @@ jobs: echo "" echo "✅ Configuration files secured - using versions from main branch" + - name: Fetch trusted configuration files from main branch + # Skip for Dependabot — its package-version bumps to protected files (e.g. + # Directory.Build.props) are legitimate and should not be overwritten by main's + # older versions. Dependabot's identity is GitHub-controlled and not spoofable. + if: github.event.pull_request.user.login != 'dependabot[bot]' + run: | + echo "Fetching configuration files from main branch to prevent malicious overrides..." + + # Fetch the main branch + git fetch origin main:main-branch + + # List of configuration files that should come from trusted main branch + config_files=( + ".editorconfig" + "Directory.Build.props" + "Directory.Build.targets" + "BannedSymbols.txt" + "*.globalconfig" + "*.ruleset" + ".github/workflows/*.yml" + ".github/workflows/*.yaml" + ) + + # Copy each configuration file from main branch if it exists + for config_file in "${config_files[@]}"; do + # Handle glob patterns + if [[ "$config_file" == *"*"* ]]; then + # Find files matching the pattern in main branch + git ls-tree -r --name-only main-branch | grep -E "${config_file//\*/.*}" | while read -r file; do + if [ -n "$file" ]; then + echo " ✓ Copying $file from main branch" + mkdir -p "$(dirname "$file")" + git show "main-branch:$file" > "$file" || echo " ⚠️ Failed to copy $file" + fi + done + else + # Check if file exists in main branch + if git cat-file -e "main-branch:$config_file" 2>/dev/null; then + echo " ✓ Copying $config_file from main branch" + git show "main-branch:$config_file" > "$config_file" + else + echo " ℹ️ $config_file not found in main branch, skipping" + fi + fi + done + + echo "" + echo "✅ Configuration files secured - using versions from main branch" + - name: Setup .NET uses: actions/setup-dotnet@v5 with: @@ -856,10 +1062,16 @@ jobs: for proj in "${projects[@]}"; do echo "Building: $proj" - # Extract target frameworks from the project file - # Support both (single) and (multiple) - # Trim whitespace from each framework before filtering - frameworks=$(tr -d '\n\r' < "$proj" | sed -n -E 's/.*[[:space:]]*>([^<]+)<\/TargetFrameworks?>.*/\1/p' | tr ';' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -E '^net(6\.0|7\.0|8\.0|9\.0|10\.0)$' || true) + # Extract target frameworks via MSBuild property evaluation (handles multi-line XML + # and Directory.Build.props inheritance). Filter to .NET 6+ for macOS ARM64 compatibility. + # Falls back from (multiple) to (single). + tfm_raw=$(dotnet msbuild "$proj" -noLogo -getProperty:TargetFrameworks 2>/dev/null \ + | grep -v '^[[:space:]]*$' | tail -n1 | sed 's/^TargetFrameworks[=:][[:space:]]*//' | tr -d '[:space:]') + if [ -z "$tfm_raw" ]; then + tfm_raw=$(dotnet msbuild "$proj" -noLogo -getProperty:TargetFramework 2>/dev/null \ + | grep -v '^[[:space:]]*$' | tail -n1 | sed 's/^TargetFramework[=:][[:space:]]*//' | tr -d '[:space:]') + fi + frameworks=$(printf '%s' "$tfm_raw" | tr ';' '\n' | grep -E '^net(6\.0|7\.0|8\.0|9\.0|10\.0)$' || true) if [ -z "$frameworks" ]; then echo "⚠️ No macOS ARM64-compatible frameworks found in $proj" @@ -889,15 +1101,22 @@ jobs: - name: Run tests (.NET 6.0 - 10.0 only - ARM64 compatible) run: | - # Find all test projects (C#, VB.NET, F#) + # Find all test projects (C#, VB.NET, F#). + # Gracefully skip if there is no ./tests directory (e.g. template-publishing + # repos or library repos in early development that have no tests yet). + if [ ! -d ./tests ]; then + echo "ℹ️ No ./tests directory — skipping test stage." + exit 0 + fi + test_projects=() while IFS= read -r -d '' file; do test_projects+=("$file") - done < <(find ./tests -type f \( -name "*.csproj" -o -name "*.vbproj" -o -name "*.fsproj" \) -print0) - + done < <(find ./tests -type f \( -name "*.csproj" -o -name "*.vbproj" -o -name "*.fsproj" \) -print0) + if [ ${#test_projects[@]} -eq 0 ]; then - echo "❌ No test projects found in ./tests directory!" - exit 1 + echo "ℹ️ No test projects found under ./tests — skipping test stage." + exit 0 fi echo "==========================================" @@ -911,11 +1130,16 @@ jobs: echo "Testing project: $test_proj" echo "==========================================" - # Extract target frameworks from the project file - # Support both (single) and (multiple) - # Only include .NET 6.0+ (ARM64 compatible on macOS) - # Normalize line endings to handle multi-line / elements - frameworks=$(tr -d '\n\r' < "$test_proj" | grep -Eo '[^<]+' | sed -E 's///' | tr ';' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -E '^net(6\.0|7\.0|8\.0|9\.0|10\.0)$' || true) + # Extract target frameworks via MSBuild property evaluation (handles multi-line XML + # and Directory.Build.props inheritance). Filter to .NET 6+ for macOS ARM64 compatibility. + # Falls back from (multiple) to (single). + tfm_raw=$(dotnet msbuild "$test_proj" -noLogo -getProperty:TargetFrameworks 2>/dev/null \ + | grep -v '^[[:space:]]*$' | tail -n1 | sed 's/^TargetFrameworks[=:][[:space:]]*//' | tr -d '[:space:]') + if [ -z "$tfm_raw" ]; then + tfm_raw=$(dotnet msbuild "$test_proj" -noLogo -getProperty:TargetFramework 2>/dev/null \ + | grep -v '^[[:space:]]*$' | tail -n1 | sed 's/^TargetFramework[=:][[:space:]]*//' | tr -d '[:space:]') + fi + frameworks=$(printf '%s' "$tfm_raw" | tr ';' '\n' | grep -E '^net(6\.0|7\.0|8\.0|9\.0|10\.0)$' || true) if [ -z "$frameworks" ]; then echo "⊘ Skipping: No compatible .NET 6.0-10.0 target frameworks found (ARM64 required)" @@ -936,6 +1160,7 @@ jobs: --configuration Release \ --framework "$fw" \ --collect:"XPlat Code Coverage" \ + --settings coverlet.runsettings \ --results-directory "./TestResults" \ --logger "console;verbosity=normal" || exit 1 done <<< "$frameworks" @@ -1058,6 +1283,10 @@ jobs: persist-credentials: false - name: Fetch trusted configuration files from main branch + # Skip for Dependabot — its package-version bumps to protected files (e.g. + # Directory.Build.props) are legitimate and should not be overwritten by main's + # older versions. Dependabot's identity is GitHub-controlled and not spoofable. + if: github.event.pull_request.user.login != 'dependabot[bot]' run: | echo "Fetching configuration files from main branch to prevent malicious overrides..." @@ -1102,6 +1331,55 @@ jobs: echo "" echo "✅ Configuration files secured - using versions from main branch" + - name: Fetch trusted configuration files from main branch + # Skip for Dependabot — its package-version bumps to protected files (e.g. + # Directory.Build.props) are legitimate and should not be overwritten by main's + # older versions. Dependabot's identity is GitHub-controlled and not spoofable. + if: github.event.pull_request.user.login != 'dependabot[bot]' + run: | + echo "Fetching configuration files from main branch to prevent malicious overrides..." + + # Fetch the main branch + git fetch origin main:main-branch + + # List of configuration files that should come from trusted main branch + config_files=( + ".editorconfig" + "Directory.Build.props" + "Directory.Build.targets" + "BannedSymbols.txt" + "*.globalconfig" + "*.ruleset" + ".github/workflows/*.yml" + ".github/workflows/*.yaml" + ) + + # Copy each configuration file from main branch if it exists + for config_file in "${config_files[@]}"; do + # Handle glob patterns + if [[ "$config_file" == *"*"* ]]; then + # Find files matching the pattern in main branch + git ls-tree -r --name-only main-branch | grep -E "${config_file//\*/.*}" | while read -r file; do + if [ -n "$file" ]; then + echo " ✓ Copying $file from main branch" + mkdir -p "$(dirname "$file")" + git show "main-branch:$file" > "$file" || echo " ⚠️ Failed to copy $file" + fi + done + else + # Check if file exists in main branch + if git cat-file -e "main-branch:$config_file" 2>/dev/null; then + echo " ✓ Copying $config_file from main branch" + git show "main-branch:$config_file" > "$config_file" + else + echo " ℹ️ $config_file not found in main branch, skipping" + fi + fi + done + + echo "" + echo "✅ Configuration files secured - using versions from main branch" + - name: Install DevSkim CLI run: dotnet tool install --global Microsoft.CST.DevSkim.CLI diff --git a/Directory.Build.props b/Directory.Build.props index 5e35560..b20a30e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,11 +6,54 @@ true latest true - + <_SkipUpgradeNetAnalyzersNuGetWarning>true - + true + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/coverlet.runsettings b/coverlet.runsettings index ca7a4cc..87e8fe4 100644 --- a/coverlet.runsettings +++ b/coverlet.runsettings @@ -5,7 +5,8 @@ cobertura - ExcludeFromCodeCoverage + **/bin/**/*.cs;**/obj/**/*.cs + ExcludeFromCodeCoverageAttribute diff --git a/scripts/Fix-BranchRuleset.ps1 b/scripts/Fix-BranchRuleset.ps1 deleted file mode 100644 index 4efdd8f..0000000 --- a/scripts/Fix-BranchRuleset.ps1 +++ /dev/null @@ -1,233 +0,0 @@ -<# -.SYNOPSIS - Fixes branch rulesets by disabling existing ones and recreating with the correct configuration. - -.DESCRIPTION - This script inspects the existing branch rulesets for a repository, disables all of them, - and renames any ruleset named "Protect main branch" to "Protect main branch (old)" so that - Setup-BranchRuleset.ps1 can create a fresh ruleset without conflicts. - - The script presents a plan of all changes before executing and prompts for confirmation. - -.PARAMETER Repository - The repository in owner/repo format. If not provided, uses the current repository. - -.EXAMPLE - .\Fix-BranchRuleset.ps1 - Inspects and fixes rulesets for the current repository - -.EXAMPLE - .\Fix-BranchRuleset.ps1 -Repository "Chris-Wolfgang/my-repo" - Inspects and fixes rulesets for a specific repository - -.NOTES - Requires: GitHub CLI (gh) authenticated with admin permissions - Install gh: https://cli.github.com/ -#> - -[CmdletBinding()] -param( - [Parameter()] - [string]$Repository = "{{GITHUB_USERNAME}}/{{REPO_NAME}}" -) - -# Check if gh CLI is installed -try { - $null = gh --version -} catch { - Write-Error "GitHub CLI (gh) is not installed or not in PATH." - Write-Host "Install from: https://cli.github.com/" -ForegroundColor Yellow - exit 1 -} - -# Check if authenticated -try { - $null = gh auth status 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Error "Not authenticated with GitHub CLI." - Write-Host "Run: gh auth login" -ForegroundColor Yellow - exit 1 - } -} catch { - Write-Error "Failed to check GitHub CLI authentication status." - exit 1 -} - -# Determine repository -if ($Repository -eq "{{GITHUB_USERNAME}}/{{REPO_NAME}}" -or -not $Repository) { - Write-Host "Detecting current repository..." -ForegroundColor Cyan - try { - $repoInfo = gh repo view --json nameWithOwner | ConvertFrom-Json - $Repository = $repoInfo.nameWithOwner - Write-Host "Using repository: $Repository" -ForegroundColor Green - } catch { - if ($Repository -eq "{{GITHUB_USERNAME}}/{{REPO_NAME}}") { - Write-Error "Could not detect repository. Please run the setup script first to replace placeholders, or specify -Repository parameter." - } else { - Write-Error "Could not detect repository. Please run from within a git repository or specify -Repository parameter." - } - exit 1 - } -} else { - Write-Host "Using specified repository: $Repository" -ForegroundColor Green -} - -# Fetch all rulesets -Write-Host "`nFetching existing rulesets..." -ForegroundColor Cyan - -try { - $rulesetsJson = gh api ` - -H "Accept: application/vnd.github+json" ` - -H "X-GitHub-Api-Version: 2022-11-28" ` - "/repos/$Repository/rulesets" ` - --paginate 2>&1 - - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to fetch rulesets: $rulesetsJson" - exit 1 - } - - $rulesets = $rulesetsJson | ConvertFrom-Json -} catch { - Write-Error "Failed to fetch rulesets: $($_.Exception.Message)" - exit 1 -} - -if (-not $rulesets -or $rulesets.Count -eq 0) { - Write-Host "No rulesets found for $Repository. Nothing to fix." -ForegroundColor Green - exit 0 -} - -# Build the plan -$plan = @() -$targetRulesetName = "Protect main branch" - -Write-Host "`nFound $($rulesets.Count) ruleset(s):" -ForegroundColor Cyan -Write-Host "" - -foreach ($ruleset in $rulesets) { - $status = if ($ruleset.enforcement -eq "disabled") { "disabled" } else { $ruleset.enforcement } - Write-Host " [$($ruleset.id)] $($ruleset.name) (enforcement: $status)" -ForegroundColor Gray - - $actions = @() - - # If this is the target name, rename it - if ($ruleset.name -eq $targetRulesetName) { - $actions += @{ - type = "rename" - description = "Rename '$($ruleset.name)' -> '$($ruleset.name) (old)'" - newName = "$($ruleset.name) (old)" - } - } - - # If not already disabled, disable it - if ($ruleset.enforcement -ne "disabled") { - $actions += @{ - type = "disable" - description = "Disable '$($ruleset.name)' (currently: $status)" - } - } - - if ($actions.Count -gt 0) { - $plan += @{ - ruleset = $ruleset - actions = $actions - } - } -} - -Write-Host "" - -# Present the plan -if ($plan.Count -eq 0) { - Write-Host "All rulesets are already disabled and none need renaming. Nothing to do." -ForegroundColor Green - exit 0 -} - -Write-Host "Planned changes:" -ForegroundColor Yellow -Write-Host "" - -$stepNumber = 1 -foreach ($item in $plan) { - foreach ($action in $item.actions) { - Write-Host " $stepNumber. $($action.description)" -ForegroundColor White - $stepNumber++ - } -} - -Write-Host "" - -# Prompt for confirmation -$response = Read-Host "Proceed with these changes? (y/N)" -if ($response -ne 'y' -and $response -ne 'Y') { - Write-Host "Cancelled. No changes were made." -ForegroundColor Yellow - exit 0 -} - -Write-Host "" - -# Execute the plan -$errors = 0 - -foreach ($item in $plan) { - $ruleset = $item.ruleset - $rulesetId = $ruleset.id - - # Build the update payload — apply rename and disable together in one API call - $updatePayload = @{} - - foreach ($action in $item.actions) { - switch ($action.type) { - "rename" { - $updatePayload["name"] = $action.newName - } - "disable" { - $updatePayload["enforcement"] = "disabled" - } - } - } - - if ($updatePayload.Count -gt 0) { - $descriptions = ($item.actions | ForEach-Object { $_.description }) -join " + " - Write-Host " Updating ruleset [$rulesetId]: $descriptions..." -ForegroundColor Cyan - - $jsonPayload = $updatePayload | ConvertTo-Json -Depth 5 - $tempFile = [System.IO.Path]::GetTempFileName() - $jsonPayload | Out-File -FilePath $tempFile -Encoding utf8NoBOM - - try { - $result = gh api ` - --method PUT ` - -H "Accept: application/vnd.github+json" ` - -H "X-GitHub-Api-Version: 2022-11-28" ` - "/repos/$Repository/rulesets/$rulesetId" ` - --input $tempFile 2>&1 - - if ($LASTEXITCODE -eq 0) { - Write-Host " Done." -ForegroundColor Green - } else { - Write-Host " Failed: $result" -ForegroundColor Red - $errors++ - } - } catch { - Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red - $errors++ - } finally { - if (Test-Path $tempFile) { - Remove-Item $tempFile -Force - } - } - } -} - -Write-Host "" - -if ($errors -gt 0) { - Write-Host "$errors action(s) failed. Review the errors above." -ForegroundColor Red - exit 1 -} else { - Write-Host "All changes applied successfully." -ForegroundColor Green - Write-Host "" - Write-Host "Next step: Run .\Setup-BranchRuleset.ps1 to create a fresh ruleset." -ForegroundColor Cyan - Write-Host "View rulesets at: https://github.com/$Repository/settings/rules" -ForegroundColor Cyan -} diff --git a/scripts/Setup-BranchRuleset.ps1 b/scripts/Setup-BranchRuleset.ps1 deleted file mode 100644 index 59b1266..0000000 --- a/scripts/Setup-BranchRuleset.ps1 +++ /dev/null @@ -1,294 +0,0 @@ -<# -.SYNOPSIS - Creates a branch protection ruleset for the main branch in the current repository. - -.DESCRIPTION - This script uses the GitHub CLI (gh) to create a repository ruleset that protects - the main branch with pull request requirements, required status checks, security - scanning rules, and automatic Copilot code review. - Run this locally after creating a new repo from the template. - - The script will prompt you to choose between single-developer or multi-developer - repository settings: - - Single Developer: No PR approvals required (you can merge your own PRs) - - Multi-Developer: Requires 1+ approval and code owner review - - The ruleset includes: - - Pull request reviews with configurable approval requirements - - Required status checks (tests, security scans) - - Force push and deletion protection - -.PARAMETER Repository - The repository in owner/repo format. If not provided, uses the current repository. - -.PARAMETER BranchName - The branch to protect. Default is "main". - -.EXAMPLE - .\Setup-BranchRuleset.ps1 - Creates the ruleset for the current repository with interactive prompts - -.EXAMPLE - .\Setup-BranchRuleset.ps1 -Repository "Chris-Wolfgang/my-repo" - Creates the ruleset for a specific repository - -.NOTES - Requires: GitHub CLI (gh) authenticated with sufficient permissions - Install gh: https://cli.github.com/ - - Required Permissions: - - Admin access to the repository, OR - - Write access with "Administration" permission enabled - - These permissions are necessary to create and modify repository rulesets. - - Note: Copilot code review is not supported through the rulesets API and must be - enabled manually in the GitHub repository UI after running this script. -#> - -[CmdletBinding()] -param( - [Parameter()] - [string]$Repository = "Chris-Wolfgang/Try-Pattern", - - [Parameter()] - [string]$BranchName = "main" -) - -# Check if gh CLI is installed -try { - $null = gh --version -} catch { - Write-Error "❌ GitHub CLI (gh) is not installed or not in PATH." - Write-Host "Install from: https://cli.github.com/" -ForegroundColor Yellow - exit 1 -} - -# Check if authenticated -try { - $null = gh auth status 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Error "❌ Not authenticated with GitHub CLI." - Write-Host "Run: gh auth login" -ForegroundColor Yellow - exit 1 - } -} catch { - Write-Error "❌ Failed to check GitHub CLI authentication status." - exit 1 -} - -# Determine repository -if ($Repository -eq "Chris-Wolfgang/Try-Pattern" -or -not $Repository) { - # Placeholders not replaced or no repository specified - auto-detect - Write-Host "🔍 Detecting current repository..." -ForegroundColor Cyan - try { - $repoInfo = gh repo view --json nameWithOwner | ConvertFrom-Json - $Repository = $repoInfo.nameWithOwner - Write-Host "✅ Using repository: $Repository" -ForegroundColor Green - } catch { - if ($Repository -eq "Chris-Wolfgang/Try-Pattern") { - Write-Error "❌ Could not detect repository. Please run the setup script (pwsh ./scripts/setup.ps1) first to replace placeholders, or specify -Repository parameter." - } else { - Write-Error "❌ Could not detect repository. Please run from within a git repository or specify -Repository parameter." - } - exit 1 - } -} else { - Write-Host "✅ Using specified repository: $Repository" -ForegroundColor Green -} - -Write-Host "`n🛡️ Setting up branch protection ruleset for: $Repository" -ForegroundColor Cyan -Write-Host "📌 Protected branch: $BranchName`n" -ForegroundColor Cyan - -# Check if ruleset already exists -Write-Host "🔍 Checking for existing rulesets..." -ForegroundColor Yellow -try { - $rulesetOutput = gh api ` - -H "Accept: application/vnd.github+json" ` - -H "X-GitHub-Api-Version: 2022-11-28" ` - "/repos/$Repository/rulesets" ` - --paginate ` - --jq '.[] | select(.name == "Protect main branch")' 2>&1 - - if ($LASTEXITCODE -ne 0) { - Write-Warning "⚠️ Could not check for existing rulesets (API returned exit code $LASTEXITCODE). Continuing..." - } elseif ($rulesetOutput) { - $matchingRulesets = $rulesetOutput | ConvertFrom-Json - $existingRuleset = $matchingRulesets | Select-Object -First 1 - - if ($existingRuleset) { - Write-Host "✅ Ruleset 'Protect main branch' already exists!" -ForegroundColor Green - Write-Host " View it at: https://github.com/$Repository/settings/rules" -ForegroundColor Cyan - $response = Read-Host "`nDo you want to continue anyway? This may fail. (y/N)" - if ($response -ne 'y' -and $response -ne 'Y') { - Write-Host "Exiting." -ForegroundColor Yellow - exit 0 - } - } - } else { - Write-Host "ℹ️ Ruleset 'Protect main branch' does not exist yet." -ForegroundColor Gray - } -} catch { - Write-Warning "⚠️ Could not check for existing rulesets: $($_.Exception.Message). Continuing..." -} - -# Prompt for repository type -Write-Host "`n👥 Repository Type Configuration" -ForegroundColor Cyan -Write-Host "" -Write-Host "Is this a single-developer or multi-developer repository?" -ForegroundColor Yellow -Write-Host "" -Write-Host " [1] Single Developer - No PR approvals required (you can merge your own PRs)" -ForegroundColor Gray -Write-Host " [2] Multi-Developer - Requires 1+ approval and code owner review" -ForegroundColor Gray -Write-Host "" -$repoTypeChoice = Read-Host "Enter your choice (1 or 2) [default: 1]" - -# Set defaults based on choice -$requireApprovals = 0 -$requireCodeOwnerReview = $false - -if ($repoTypeChoice -eq "2") { - $requireApprovals = 1 - $requireCodeOwnerReview = $true - Write-Host "✅ Configured for multi-developer repository (1 approval required)" -ForegroundColor Green -} else { - Write-Host "✅ Configured for single-developer repository (no approvals required)" -ForegroundColor Green -} - -# Create ruleset configuration -Write-Host "`n📝 Creating ruleset configuration..." -ForegroundColor Cyan - -$rulesetConfig = @{ - name = "Protect main branch" - target = "branch" - enforcement = "active" - conditions = @{ - ref_name = @{ - include = @("refs/heads/$BranchName") - exclude = @() - } - } - # No bypass actors allowed - all users (including admins) must follow branch protection rules - bypass_actors = @() - rules = @( - @{ - type = "pull_request" - parameters = @{ - required_approving_review_count = $requireApprovals - dismiss_stale_reviews_on_push = $true - require_code_owner_review = $requireCodeOwnerReview - require_last_push_approval = $false - required_review_thread_resolution = $true - } - }, - @{ - type = "required_status_checks" - parameters = @{ - strict_required_status_checks_policy = $true - # IMPORTANT: Workflows providing these required checks (specifically .github/workflows/pr.yaml) - # must NOT have path filters (paths/paths-ignore). If a workflow is path-filtered - # and doesn't run for a PR, GitHub will treat the required check as missing and - # block the merge. All required status checks must run on every PR. - required_status_checks = @( - @{ context = "Stage 1: Linux Tests (.NET 5.0-10.0) + Coverage Gate" }, - @{ context = "Stage 2: Windows Tests (.NET 5.0-10.0, Framework 4.6.2-4.8.1)" }, - @{ context = "Stage 3: macOS Tests (.NET 6.0-10.0)" }, - @{ context = "Security Scan (DevSkim)" } - ) - } - }, - # NOTE: code_scanning (CodeQL) is not included in this API-created ruleset because - # it requires a CodeQL workflow to be present and have run on the repo. Without prior - # analyses, the rule blocks all PRs. Add CodeQL integration separately if needed. - # NOTE: Copilot code review is not included in this API-created payload because - # it is not currently supported through the rulesets API. After the ruleset is - # created, enable Copilot code review settings manually in the GitHub repository UI. - @{ - type = "non_fast_forward" - }, - @{ - type = "deletion" - } - ) -} - -# Convert to JSON -$jsonConfig = $rulesetConfig | ConvertTo-Json -Depth 10 - -# Save to temporary file -$tempFile = [System.IO.Path]::GetTempFileName() -$jsonConfig | Out-File -FilePath $tempFile -Encoding utf8NoBOM - -try { - Write-Host "🚀 Creating branch ruleset..." -ForegroundColor Cyan - - # Create the ruleset - $response = gh api ` - --method POST ` - -H "Accept: application/vnd.github+json" ` - -H "X-GitHub-Api-Version: 2022-11-28" ` - "/repos/$Repository/rulesets" ` - --input $tempFile 2>&1 - - if ($LASTEXITCODE -eq 0) { - Write-Host "`n✅ Successfully created branch ruleset 'Protect main branch'!" -ForegroundColor Green - Write-Host "`n🛡️ Protection Rules Enabled:" -ForegroundColor Cyan - Write-Host " ✅ Pull requests required before merging" -ForegroundColor Gray - if ($requireApprovals -gt 0) { - Write-Host " ✅ Required approvals: $requireApprovals" -ForegroundColor Gray - Write-Host " ✅ Code owner review required" -ForegroundColor Gray - } else { - Write-Host " ✅ No approvals required (single-developer mode)" -ForegroundColor Gray - } - Write-Host " ✅ Required status checks (must pass before merging):" -ForegroundColor Gray - Write-Host " - Stage 1: Linux Tests (.NET 5.0-10.0) + Coverage Gate" -ForegroundColor DarkGray - Write-Host " - Stage 2: Windows Tests (.NET 5.0-10.0, Framework 4.6.2-4.8.1)" -ForegroundColor DarkGray - Write-Host " - Stage 3: macOS Tests (.NET 6.0-10.0)" -ForegroundColor DarkGray - Write-Host " - Security Scan (DevSkim)" -ForegroundColor DarkGray - Write-Host " ✅ Branches must be up to date before merging" -ForegroundColor Gray - Write-Host " ✅ Conversation resolution required before merging" -ForegroundColor Gray - Write-Host " ✅ Stale reviews dismissed when new commits are pushed" -ForegroundColor Gray - Write-Host " ⚠️ Copilot code review: enable manually in repository settings" -ForegroundColor Yellow - Write-Host " (Not yet supported through the rulesets API)" -ForegroundColor DarkGray - Write-Host " ✅ Force pushes blocked on $BranchName branch" -ForegroundColor Gray - Write-Host " ✅ Branch deletion prevented for $BranchName" -ForegroundColor Gray - Write-Host " ✅ No bypass allowed - all users must follow these rules" -ForegroundColor Gray - - Write-Host "`n🔗 View ruleset at:" -ForegroundColor Cyan - Write-Host " https://github.com/$Repository/settings/rules" -ForegroundColor Blue - } else { - Write-Error "❌ Failed to create ruleset" - Write-Host $response -ForegroundColor Red - - if ($response -like "*403*" -or $response -like "*Resource not accessible*") { - Write-Host "`n💡 This error usually means:" -ForegroundColor Yellow - Write-Host " 1. You don't have admin access to this repository, OR" -ForegroundColor Yellow - Write-Host " 2. Your GitHub authentication doesn't have the required scopes" -ForegroundColor Yellow - Write-Host "`n🔧 Try re-authenticating with:" -ForegroundColor Cyan - Write-Host " gh auth login" -ForegroundColor Gray - Write-Host " For more information about required scopes, see: https://cli.github.com/manual/gh_auth_login" -ForegroundColor Gray - } - - if ($response -like "*422*" -or $response -like "*Validation Failed*") { - Write-Host "`n💡 This validation error usually means:" -ForegroundColor Yellow - Write-Host " 1. The repository doesn't meet the requirements for rulesets (e.g., needs to be a GitHub Pro/Team/Enterprise repo)" -ForegroundColor Yellow - Write-Host " 2. Some configuration in the ruleset is invalid for this repository type" -ForegroundColor Yellow - Write-Host " 3. Required workflows or status checks might not exist yet" -ForegroundColor Yellow - Write-Host "`n🔧 Possible solutions:" -ForegroundColor Cyan - Write-Host " - Verify this is a GitHub Pro, Team, or Enterprise repository" -ForegroundColor Gray - Write-Host " - Check that the required workflows exist in .github/workflows/" -ForegroundColor Gray - Write-Host " - Ensure you have admin permissions on the repository" -ForegroundColor Gray - } - - exit 1 - } -} catch { - Write-Error "❌ An error occurred: $_" - exit 1 -} finally { - # Clean up temp file - if (Test-Path $tempFile) { - Remove-Item $tempFile -Force - } -} - -Write-Host "`n🎉 Setup complete!" -ForegroundColor Green diff --git a/src/Wolfgang.TryPattern/Wolfgang.TryPattern.csproj b/src/Wolfgang.TryPattern/Wolfgang.TryPattern.csproj index bf57193..57923ac 100644 --- a/src/Wolfgang.TryPattern/Wolfgang.TryPattern.csproj +++ b/src/Wolfgang.TryPattern/Wolfgang.TryPattern.csproj @@ -37,47 +37,6 @@ - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - +