From 4de430bac8649d686d8c9dafa832a677e6b6f02b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Sep 2025 08:19:56 +0000 Subject: [PATCH 1/4] chore: Add comprehensive GitHub Copilot instructions for AngleSharp.Diffing development (#53) * Initial plan * Add comprehensive GitHub Copilot instructions for AngleSharp.Diffing Co-authored-by: egil <105649+egil@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: egil <105649+egil@users.noreply.github.com> --- .github/copilot-instructions.md | 196 ++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..1a58d6a --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,196 @@ +# AngleSharp.Diffing - HTML Comparison Library + +AngleSharp.Diffing is a .NET library for comparing AngleSharp control nodes and test nodes to identify differences between HTML DOM trees. This library targets .NET Standard 2.0 and provides a fluent API for HTML comparison and diffing. + +**Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.** + +## Working Effectively + +### Prerequisites and Setup +- Install .NET 8.0+ SDK (already available in GitHub Actions environments) +- Mono is NOT required for standard development (only for legacy Cake build system) + +### Bootstrap, Build, and Test +Execute these commands in order from the repository root: + +```bash +cd src +dotnet restore # ~11 seconds - restores NuGet packages +dotnet build # ~15 seconds - compiles library and tests +dotnet test # ~5 seconds - runs 521 tests. NEVER CANCEL. +``` + +**CRITICAL TIMING EXPECTATIONS:** +- `dotnet restore`: 11 seconds - NEVER CANCEL. Set timeout to 60+ seconds minimum. +- `dotnet build`: 15 seconds - NEVER CANCEL. Set timeout to 60+ seconds minimum. +- `dotnet test`: 5 seconds - runs 521 tests. NEVER CANCEL. Set timeout to 30+ seconds minimum. +- Complete clean rebuild: ~20 seconds - NEVER CANCEL. Set timeout to 60+ seconds minimum. + +### Alternative Build Methods +**PRIMARY METHOD (Recommended):** Use dotnet CLI commands as shown above. + +**LEGACY METHOD (Not Recommended):** The repository includes Cake build scripts (`build.sh`/`build.ps1`) but they require Mono and may fail. Only use if specifically needed: +```bash +# Install Mono first (if needed) +sudo apt-get update && sudo apt-get install -y mono-complete +./build.sh # May fail - use dotnet commands instead +``` + +### Release Builds and Packaging +```bash +cd src +dotnet build --configuration Release # ~15 seconds +dotnet pack --configuration Release # ~8 seconds - creates NuGet packages +``` + +**Package Output:** Creates `.nupkg` and `.snupkg` files in `src/AngleSharp.Diffing/bin/Release/` + +## Validation and Testing + +### Manual Validation Requirements +**ALWAYS** validate changes by creating and running a test program to exercise the library: + +```csharp +using AngleSharp.Diffing; + +// Test basic HTML diffing functionality +var control = @"

hello world

"; +var test = @"

hello world

"; + +var diffs = DiffBuilder + .Compare(control) + .WithTest(test) + .Build() + .ToList(); + +Console.WriteLine($"Found {diffs.Count} differences:"); +foreach (var diff in diffs) +{ + Console.WriteLine($"- {diff.GetType().Name}: {diff.Result} {diff.Target}"); +} +``` + +### Code Quality and Standards +- **Code Analysis:** Project has strict analysis rules enabled (`EnforceCodeStyleInBuild=true`) +- **EditorConfig:** Follow the existing `.editorconfig` settings (4-space indentation, CRLF line endings) +- **Nullable References:** Enabled - handle null values appropriately +- **Build Warnings:** Zero warnings expected - treat warnings as errors for critical types + +### Test Suite Information +- **Test Framework:** xUnit with Shouldly assertions +- **Test Count:** 521 tests covering core diffing functionality +- **Test Runtime:** ~5 seconds total +- **Test Files Location:** `src/AngleSharp.Diffing.Tests/` + +## Navigation and Architecture + +### Key Directories and Files +``` +src/ +├── AngleSharp.Diffing/ # Main library project +│ ├── Core/ # Core diffing engine and interfaces +│ ├── Extensions/ # Extension methods +│ ├── Strategies/ # Diffing strategy implementations +│ └── DiffBuilder.cs # Main fluent API entry point +├── AngleSharp.Diffing.Tests/ # Test project +│ ├── Core/ # Core functionality tests +│ └── Strategies/ # Strategy-specific tests +└── AngleSharp.Diffing.sln # Solution file +``` + +### Important Classes and APIs +- **`DiffBuilder`**: Main entry point with fluent API (`DiffBuilder.Compare(control).WithTest(test).Build()`) +- **`IDiff`**: Base interface for all difference types +- **`HtmlDifferenceEngine`**: Core comparison engine +- **Diff Types**: `AttrDiff`, `NodeDiff`, `MissingNodeDiff`, `UnexpectedNodeDiff`, etc. + +### Dependencies +- **AngleSharp 1.1.2**: Core HTML parsing library +- **AngleSharp.Css 1.0.0-beta.144**: CSS support +- **Target Framework**: .NET Standard 2.0 (library), .NET 8.0 (tests) + +## Common Development Tasks + +### Making Code Changes +1. **Always build and test first** to establish baseline: `dotnet build && dotnet test` +2. Make your changes to source files in `src/AngleSharp.Diffing/` +3. **Immediately test after changes**: `dotnet build && dotnet test` +4. **Manual validation**: Create a test program to exercise your changes +5. **Pre-commit validation**: Ensure no build warnings or test failures + +### Debugging and Investigation +- **Test filtering**: `dotnet test --filter "TestMethodName"` to run specific tests +- **Verbose builds**: `dotnet build --verbosity normal` to see detailed output +- **Key test files**: `DiffBuilderTest.cs` shows basic API usage patterns + +### CI/Build Requirements +- **GitHub Actions**: Runs on both Linux and Windows +- **Zero warnings policy**: Build must complete without warnings +- **All tests must pass**: 521/521 tests required +- **Code formatting**: Enforced via EditorConfig and analyzers + +## Getting Started Examples + +### Basic HTML Comparison +```csharp +var control = "

Expected content

"; +var test = "

Actual content

"; + +var diffs = DiffBuilder + .Compare(control) + .WithTest(test) + .Build(); +``` + +### Advanced Configuration +```csharp +var diffs = DiffBuilder + .Compare(control) + .WithTest(test) + .WithOptions(options => { + // Configure diffing strategies + }) + .Build(); +``` + +### Working with Results +```csharp +foreach (var diff in diffs) +{ + switch (diff.Result) + { + case DiffResult.Different: + case DiffResult.Missing: + case DiffResult.Unexpected: + // Handle different types of differences + break; + } +} +``` + +## Troubleshooting + +### Common Issues +- **Build failures**: Ensure you're in the `src/` directory +- **Cake build failures**: Use `dotnet` commands instead of `build.sh`/`build.ps1` +- **Test failures**: Check for environment-specific issues or recent changes +- **Package restore issues**: Clear NuGet caches with `dotnet nuget locals all --clear` + +### Quick Reset Commands +```bash +# Clean and rebuild everything +cd src +dotnet clean +rm -rf */bin */obj +dotnet restore +dotnet build +dotnet test +``` + +## Performance Expectations +- **Development builds**: ~15 seconds +- **Test execution**: ~5 seconds for full suite +- **Package creation**: ~8 seconds +- **CI builds**: Allow 60+ seconds timeout minimum for each step + +**CRITICAL**: NEVER CANCEL builds or tests that appear slow. Wait at least 60 seconds for builds and 30 seconds for tests before considering alternatives. \ No newline at end of file From 600656dcaca8c7f8d3cbf5eabfa883001dcf33b0 Mon Sep 17 00:00:00 2001 From: RiRiSharp <30719799+RiRiSharp@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:51:45 +0200 Subject: [PATCH 2/4] feat: Unmatched `:ignore`'ed attributes does not contribute diffs * Ignore unmatched :ignore attributes * Add more tests * Add test to cover situation where no ignore attribute was requested * Add :ignore filter approach * Fix Linux pipeline * Create IgnoreAttributeStrategy and make IgnoreAttributeComparer obsolete * Revert "Fix Linux pipeline" This reverts commit 25975588e119f3fcb2b53ebf74bc065a6ee9bd8c. --- .../DiffBuilderTest.cs | 41 +++++++++++++++- .../IgnoreAttributeComparerTest.cs | 8 ++-- .../TestData/IgnoreAttributeTestData.cs | 45 ++++++++++++++++++ .../Core/HtmlDifferenceEngine.cs | 1 + src/AngleSharp.Diffing/Core/SourceMap.cs | 2 +- ...iffingStrategyPipelineBuilderExtensions.cs | 3 +- .../IgnoreAttributeComparer.cs | 10 +++- .../IgnoreAttributeStrategy.cs | 47 +++++++++++++++++++ 8 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 src/AngleSharp.Diffing.Tests/TestData/IgnoreAttributeTestData.cs create mode 100644 src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeStrategy.cs diff --git a/src/AngleSharp.Diffing.Tests/DiffBuilderTest.cs b/src/AngleSharp.Diffing.Tests/DiffBuilderTest.cs index 9769990..e9ad09d 100644 --- a/src/AngleSharp.Diffing.Tests/DiffBuilderTest.cs +++ b/src/AngleSharp.Diffing.Tests/DiffBuilderTest.cs @@ -1,3 +1,8 @@ +using AngleSharp.Diffing.Strategies; +using AngleSharp.Diffing.Strategies.AttributeStrategies; +using AngleSharp.Diffing.Strategies.TextNodeStrategies; +using AngleSharp.Diffing.TestData; + namespace AngleSharp.Diffing; @@ -116,4 +121,38 @@ public void Test006(string control, string test) diffs.ShouldBeEmpty(); } -} + + [Theory(DisplayName = + "When a control element has ':ignore', elements with and without that attribute should return empty diffs")] + [MemberData(nameof(IgnoreAttributeTestData.ControlAndHtmlData), MemberType = typeof(IgnoreAttributeTestData))] + public void Test007(string controlHtml, string testHtml) + { + var diffs = DiffBuilder.Compare(controlHtml).WithTest(testHtml).Build(); + Assert.Empty(diffs); + } + + [Theory(DisplayName = + "When a control element has ':ignore', but IgnoreAttributeComparer is not active, diffs should be found")] + [MemberData(nameof(IgnoreAttributeTestData.ControlHtmlAndDiffData), MemberType = typeof(IgnoreAttributeTestData))] + public void Test008(string controlHtml, string testHtml, DiffResult expectedDiffResult) + { + var diffs = DiffBuilder + .Compare(controlHtml) + .WithTest(testHtml) + .WithOptions(a => a // Most important thing to note here is we do not have a ignore attribute comparer + .AddSearchingNodeMatcher() + .AddMatcher(AttributeNameMatcher.Match, StrategyType.Generalized) + .AddElementComparer(enforceTagClosing: false) + .AddMatcher(PostfixedAttributeMatcher.Match, StrategyType.Specialized) + .AddComparer(AttributeComparer.Compare, StrategyType.Generalized) + .AddClassAttributeComparer() + .AddBooleanAttributeComparer(BooleanAttributeComparision.Strict) + .AddStyleAttributeComparer()) + .Build() + .ToList(); + + Assert.Single(diffs); + Assert.Equal(DiffTarget.Attribute, diffs[0].Target); + Assert.Equal(expectedDiffResult, diffs[0].Result); + } +} \ No newline at end of file diff --git a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/IgnoreAttributeComparerTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/IgnoreAttributeComparerTest.cs index a59efc3..34bb922 100644 --- a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/IgnoreAttributeComparerTest.cs +++ b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/IgnoreAttributeComparerTest.cs @@ -17,7 +17,7 @@ public void Test000(CompareResult currentResult) @"

", "foo" ); - IgnoreAttributeComparer + IgnoreAttributeStrategy .Compare(comparison, currentResult) .ShouldBe(currentResult); } @@ -30,8 +30,8 @@ public void Test003() @"

", "foo" ); - IgnoreAttributeComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different); - IgnoreAttributeComparer.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same); + IgnoreAttributeStrategy.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different); + IgnoreAttributeStrategy.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same); } [Fact(DisplayName = "When a attribute does contain have the ':ignore' postfix, Same is returned")] @@ -42,6 +42,6 @@ public void Test004() @"

", "foo" ); - IgnoreAttributeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Same); + IgnoreAttributeStrategy.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Same); } } diff --git a/src/AngleSharp.Diffing.Tests/TestData/IgnoreAttributeTestData.cs b/src/AngleSharp.Diffing.Tests/TestData/IgnoreAttributeTestData.cs new file mode 100644 index 0000000..daded07 --- /dev/null +++ b/src/AngleSharp.Diffing.Tests/TestData/IgnoreAttributeTestData.cs @@ -0,0 +1,45 @@ +namespace AngleSharp.Diffing.TestData; + +internal static class IgnoreAttributeTestData +{ + public static TheoryData ControlAndHtmlData() + { + var theoryData = new TheoryData(); + foreach (var (controlHtml, expectedHtml, _) in TestCases) + { + theoryData.Add(controlHtml, expectedHtml); + } + + return theoryData; + } + + public static TheoryData ControlHtmlAndDiffData() + { + var theoryData = new TheoryData(); + foreach (var (controlHtml, expectedHtml, expectedDiffResult) in TestCases) + { + theoryData.Add(controlHtml, expectedHtml, expectedDiffResult); + } + + return theoryData; + } + + private static readonly IEnumerable<(string controlHtml, string expectedHtml, DiffResult expectedDiffResult)> + TestCases = + [ + ("
", "
", DiffResult.Different), + ("
", "
", DiffResult.Different), + ("
", "
", DiffResult.Different), + ("
", "
", DiffResult.Missing), + ("", "", DiffResult.Different), + ("", "", DiffResult.Different), + ("", "", DiffResult.Different), + ("", "", DiffResult.Missing), + ("", "", DiffResult.Different), + ("", "", DiffResult.Missing), + ("", "", DiffResult.Different), + ("", "", DiffResult.Missing), + ("", "", DiffResult.Different), + ("", "", DiffResult.Missing), + ]; +} \ No newline at end of file diff --git a/src/AngleSharp.Diffing/Core/HtmlDifferenceEngine.cs b/src/AngleSharp.Diffing/Core/HtmlDifferenceEngine.cs index 575ca96..35734aa 100644 --- a/src/AngleSharp.Diffing/Core/HtmlDifferenceEngine.cs +++ b/src/AngleSharp.Diffing/Core/HtmlDifferenceEngine.cs @@ -1,4 +1,5 @@ using AngleSharp.Diffing.Core.Diffs; +using AngleSharp.Diffing.Strategies.AttributeStrategies; namespace AngleSharp.Diffing.Core; diff --git a/src/AngleSharp.Diffing/Core/SourceMap.cs b/src/AngleSharp.Diffing/Core/SourceMap.cs index 4f99c8d..4b30d12 100644 --- a/src/AngleSharp.Diffing/Core/SourceMap.cs +++ b/src/AngleSharp.Diffing/Core/SourceMap.cs @@ -70,7 +70,7 @@ public IEnumerable GetUnmatched() { foreach (var source in _sources.Values) { - if (!_matched.Contains(source.Attribute.Name)) + if (IsUnmatched(source.Attribute.Name)) yield return source; } } diff --git a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/DiffingStrategyPipelineBuilderExtensions.cs b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/DiffingStrategyPipelineBuilderExtensions.cs index 17b8223..ca9f18d 100644 --- a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/DiffingStrategyPipelineBuilderExtensions.cs +++ b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/DiffingStrategyPipelineBuilderExtensions.cs @@ -23,6 +23,7 @@ public static IDiffingStrategyCollection IgnoreDiffAttributes(this IDiffingStrat public static IDiffingStrategyCollection AddAttributeNameMatcher(this IDiffingStrategyCollection builder) { builder.AddMatcher(AttributeNameMatcher.Match, StrategyType.Generalized); + builder.AddMatcher(IgnoreAttributeStrategy.Match, StrategyType.Generalized); return builder; } @@ -33,7 +34,7 @@ public static IDiffingStrategyCollection AddAttributeComparer(this IDiffingStrat { builder.AddMatcher(PostfixedAttributeMatcher.Match, StrategyType.Specialized); builder.AddComparer(AttributeComparer.Compare, StrategyType.Generalized); - builder.AddComparer(IgnoreAttributeComparer.Compare, StrategyType.Specialized); + builder.AddComparer(IgnoreAttributeStrategy.Compare, StrategyType.Specialized); return builder; } diff --git a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeComparer.cs b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeComparer.cs index c74f619..eb42457 100644 --- a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeComparer.cs +++ b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeComparer.cs @@ -3,6 +3,7 @@ /// /// Represents the ignore attribute comparer. /// +[Obsolete("Has been moved to IgnoreAttributeStrategy")] public static class IgnoreAttributeComparer { private const string DIFF_IGNORE_POSTFIX = ":ignore"; @@ -15,8 +16,13 @@ public static CompareResult Compare(in AttributeComparison comparison, CompareRe if (currentDecision.IsSameOrSkip) return currentDecision; - return comparison.Control.Attribute.Name.EndsWith(DIFF_IGNORE_POSTFIX, StringComparison.OrdinalIgnoreCase) + return IsIgnoreAttribute(comparison.Control.Attribute) ? CompareResult.Same : currentDecision; } -} + + private static bool IsIgnoreAttribute(IAttr source) + { + return source.Name.EndsWith(DIFF_IGNORE_POSTFIX, StringComparison.OrdinalIgnoreCase); + } +} \ No newline at end of file diff --git a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeStrategy.cs b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeStrategy.cs new file mode 100644 index 0000000..84d24b6 --- /dev/null +++ b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeStrategy.cs @@ -0,0 +1,47 @@ +namespace AngleSharp.Diffing.Strategies.AttributeStrategies; + +/// +/// Ignore Attribute matcher strategy. +/// +public static class IgnoreAttributeStrategy +{ + private const string DIFF_IGNORE_POSTFIX = ":ignore"; + + /// + /// The ignore attribute comparer. + /// + public static CompareResult Compare(in AttributeComparison comparison, CompareResult currentDecision) + { + if (currentDecision.IsSameOrSkip) + return currentDecision; + + return IsIgnoreAttribute(comparison.Control.Attribute) + ? CompareResult.Same + : currentDecision; + } + + /// + /// Attribute name matcher strategy. + /// + public static IEnumerable Match(IDiffContext context, SourceMap controlSources, SourceMap testSources) + { + if (controlSources is null) + throw new ArgumentNullException(nameof(controlSources)); + if (testSources is null) + throw new ArgumentNullException(nameof(testSources)); + + foreach (var control in controlSources.GetUnmatched()) + { + // An unmatched :ignore attribute can just be matched with itself if it isn't + // matched with a "test" attribute of the same name already. + // this means an ignored attribute is ignored even if it does not appear in the test html. + if (control.Attribute.Name.EndsWith(DIFF_IGNORE_POSTFIX, StringComparison.OrdinalIgnoreCase)) + yield return new AttributeComparison(control, control); + } + } + + private static bool IsIgnoreAttribute(IAttr source) + { + return source.Name.EndsWith(DIFF_IGNORE_POSTFIX, StringComparison.OrdinalIgnoreCase); + } +} \ No newline at end of file From 64342438322a3b25738c51bb21186b0c083b93ba Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Sep 2025 09:20:47 +0000 Subject: [PATCH 3/4] ci: Upgrade build system to Nuke following AngleSharp ecosystem standards (#51) * Initial plan * Modernize CI workflow to use dotnet CLI directly Co-authored-by: egil <105649+egil@users.noreply.github.com> * Update build.sh to use modern dotnet CLI instead of deprecated Cake/mono Co-authored-by: egil <105649+egil@users.noreply.github.com> * Upgrade build system from dotnet CLI to Nuke build automation system Co-authored-by: egil <105649+egil@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: egil <105649+egil@users.noreply.github.com> --- .github/workflows/ci.yml | 4 +- .gitignore | 9 +- .nuke/build.schema.json | 134 ++++++++++ .nuke/parameters.json | 4 + build.cake | 11 - build.cmd | 7 + build.ps1 | 117 ++++----- build.sh | 130 ++++------ nuke/.editorconfig | 14 ++ nuke/Build.cs | 270 ++++++++++++++++++++ nuke/Configuration.cs | 16 ++ nuke/Directory.Build.props | 11 + nuke/Directory.Build.targets | 9 + nuke/Extensions/StringExtensions.cs | 80 ++++++ nuke/ReleaseNotes.cs | 81 ++++++ nuke/ReleaseNotesParser.cs | 146 +++++++++++ nuke/SemVersion.cs | 378 ++++++++++++++++++++++++++++ nuke/_build.csproj | 21 ++ src/AngleSharp.Diffing.sln | 3 +- tools/anglesharp.cake | 200 --------------- tools/packages.config | 6 - 21 files changed, 1289 insertions(+), 362 deletions(-) create mode 100644 .nuke/build.schema.json create mode 100644 .nuke/parameters.json delete mode 100644 build.cake create mode 100755 build.cmd create mode 100644 nuke/.editorconfig create mode 100644 nuke/Build.cs create mode 100644 nuke/Configuration.cs create mode 100644 nuke/Directory.Build.props create mode 100644 nuke/Directory.Build.targets create mode 100644 nuke/Extensions/StringExtensions.cs create mode 100644 nuke/ReleaseNotes.cs create mode 100644 nuke/ReleaseNotesParser.cs create mode 100644 nuke/SemVersion.cs create mode 100644 nuke/_build.csproj delete mode 100644 tools/anglesharp.cake delete mode 100644 tools/packages.config diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d0bca6..bd40446 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Build run: ./build.sh @@ -20,7 +20,7 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Build run: | diff --git a/.gitignore b/.gitignore index e87b082..ec5269c 100644 --- a/.gitignore +++ b/.gitignore @@ -189,6 +189,7 @@ PublishScripts/ # NuGet Packages *.nupkg +nupkgs/ # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. @@ -346,4 +347,10 @@ ASALocalRun/ # BeatPulse healthcheck temp database healthchecksdb -*.ncrunchsolution \ No newline at end of file +*.ncrunchsolution + +# Nuke build artifacts +.nuke/temp/ +bin/ +nuke/bin/ +nuke/obj/ \ No newline at end of file diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 0000000..7a3c92d --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,134 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions": { + "Host": { + "type": "string", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "CreatePackage", + "Default", + "Package", + "PrePublish", + "Publish", + "PublishPackage", + "PublishPreRelease", + "PublishRelease", + "Restore", + "RunUnitTests" + ] + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { + "properties": { + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "description": "Host for execution. Default is 'automatic'", + "$ref": "#/definitions/Host" + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Verbosity": { + "description": "Logging verbosity during build execution. Default is 'Normal'", + "$ref": "#/definitions/Verbosity" + } + } + } + }, + "allOf": [ + { + "properties": { + "Configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", + "enum": [ + "Debug", + "Release" + ] + }, + "ReleaseNotesFilePath": { + "type": "string", + "description": "ReleaseNotesFilePath - To determine the SemanticVersion" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + } + } + }, + { + "$ref": "#/definitions/NukeBuild" + } + ] +} diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 0000000..9e30d9f --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "src/AngleSharp.Diffing.sln" +} \ No newline at end of file diff --git a/build.cake b/build.cake deleted file mode 100644 index a2de43c..0000000 --- a/build.cake +++ /dev/null @@ -1,11 +0,0 @@ -var target = Argument("target", "Default"); -var projectName = "AngleSharp.Diffing"; -var solutionName = "AngleSharp.Diffing"; -var frameworks = new Dictionary -{ - { "netstandard2.0", "netstandard2.0" }, -}; - -#load tools/anglesharp.cake - -RunTarget(target); \ No newline at end of file diff --git a/build.cmd b/build.cmd new file mode 100755 index 0000000..6ba3512 --- /dev/null +++ b/build.cmd @@ -0,0 +1,7 @@ +:; set -eo pipefail +:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +:; ${SCRIPT_DIR}/build.sh "$@" +:; exit $? + +@ECHO OFF +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index 6f27bfd..fcbabaf 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,81 +1,74 @@ +[CmdletBinding()] Param( - [string]$Script = "build.cake", - [string]$Target = "Default", - [ValidateSet("Release", "Debug")] - [string]$Configuration = "Release", - [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] - [string]$Verbosity = "Verbose", - [switch]$Experimental, - [switch]$WhatIf, - [switch]$Mono, - [switch]$SkipToolPackageRestore, [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] - [string[]]$ScriptArgs + [string[]]$BuildArguments ) -$PSScriptRoot = split-path -parent $MyInvocation.MyCommand.Definition; -$UseDryRun = ""; -$UseMono = ""; -$TOOLS_DIR = Join-Path $PSScriptRoot "tools" -$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" -$NUGET_OLD_EXE = Join-Path $TOOLS_DIR "nuget_old.exe" -$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" -$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -$NUGET_OLD_URL = "https://dist.nuget.org/win-x86-commandline/v3.5.0/nuget.exe" +Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" -# Should we use experimental build of Roslyn? -$UseExperimental = ""; -if ($Experimental.IsPresent) { - $UseExperimental = "--experimental" -} +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent -# Is this a dry run? -if ($WhatIf.IsPresent) { - $UseDryRun = "--dryrun" -} +########################################################################### +# CONFIGURATION +########################################################################### -# Should we use mono? -if ($Mono.IsPresent) { - $UseMono = "--mono" -} +$BuildProjectFile = "$PSScriptRoot\nuke\_build.csproj" +$TempDirectory = "$PSScriptRoot\\.nuke\temp" -# Try download NuGet.exe if do not exist. -if (!(Test-Path $NUGET_EXE)) { - (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) -} +$DotNetGlobalFile = "$PSScriptRoot\\global.json" +$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" +$DotNetChannel = "STS" -# Try download NuGet.exe if do not exist. -if (!(Test-Path $NUGET_OLD_URL)) { - (New-Object System.Net.WebClient).DownloadFile($NUGET_OLD_URL, $NUGET_OLD_EXE) -} +$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 +$env:DOTNET_NOLOGO = 1 -# Make sure NuGet (latest) exists where we expect it. -if (!(Test-Path $NUGET_EXE)) { - Throw "Could not find nuget.exe" +########################################################################### +# EXECUTION +########################################################################### + +function ExecSafe([scriptblock] $cmd) { + & $cmd + if ($LASTEXITCODE) { exit $LASTEXITCODE } } -# Make sure NuGet (v3.5.0) exists where we expect it. -if (!(Test-Path $NUGET_OLD_EXE)) { - Throw "Could not find nuget_old.exe" +# If dotnet CLI is installed globally and it matches requested version, use for execution +if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` + $(dotnet --version) -and $LASTEXITCODE -eq 0) { + $env:DOTNET_EXE = (Get-Command "dotnet").Path } +else { + # Download install script + $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" + New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + + # If global.json exists, load expected version + if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } + } -# Restore tools from NuGet? -if (-Not $SkipToolPackageRestore.IsPresent) -{ - Push-Location - Set-Location $TOOLS_DIR - Invoke-Expression "$NUGET_EXE install -ExcludeVersion" - Pop-Location - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE + # Install by channel or version + $DotNetDirectory = "$TempDirectory\dotnet-win" + if (!(Test-Path variable:DotNetVersion)) { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + } else { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } } + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + $env:PATH = "$DotNetDirectory;$env:PATH" } -# Make sure that Cake has been installed. -if (!(Test-Path $CAKE_EXE)) { - Throw "Could not find Cake.exe" +Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" + +if (Test-Path env:NUKE_ENTERPRISE_TOKEN) { + & $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null + & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null } -# Start Cake -Invoke-Expression "$CAKE_EXE `"$Script`" --target=`"$Target`" --configuration=`"$Configuration`" --verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs" -exit $LASTEXITCODE \ No newline at end of file +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } \ No newline at end of file diff --git a/build.sh b/build.sh index c62ce5c..320669f 100755 --- a/build.sh +++ b/build.sh @@ -1,93 +1,67 @@ #!/usr/bin/env bash -############################################################### -# This is the Cake bootstrapper script that is responsible for -# downloading Cake and all specified tools from NuGet. -############################################################### -# Define directories. -SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) -TOOLS_DIR=$SCRIPT_DIR/tools -NUGET_EXE=$TOOLS_DIR/nuget.exe -NUGET_OLD_EXE=$TOOLS_DIR/nuget_old.exe -CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe +bash --version 2>&1 | head -n 1 -# Define default arguments. -SCRIPT="build.cake" -TARGET="Default" -CONFIGURATION="Release" -VERBOSITY="verbose" -DRYRUN= -SHOW_VERSION=false -SCRIPT_ARGUMENTS=() +set -eo pipefail +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) -# Parse arguments. -for i in "$@"; do - case $1 in - -s|--script) SCRIPT="$2"; shift ;; - -t|--target) TARGET="$2"; shift ;; - -c|--configuration) CONFIGURATION="$2"; shift ;; - -v|--verbosity) VERBOSITY="$2"; shift ;; - -d|--dryrun) DRYRUN="--dryrun" ;; - --version) SHOW_VERSION=true ;; - --) shift; SCRIPT_ARGUMENTS+=("$@"); break ;; - *) SCRIPT_ARGUMENTS+=("$1") ;; - esac - shift -done +########################################################################### +# CONFIGURATION +########################################################################### -# Make sure the tools folder exist. -if [ ! -d $TOOLS_DIR ]; then - mkdir $TOOLS_DIR -fi +BUILD_PROJECT_FILE="$SCRIPT_DIR/nuke/_build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" -# Make sure that packages.config exist. -if [ ! -f $TOOLS_DIR/packages.config ]; then - echo "Downloading packages.config..." - curl -Lsfo $TOOLS_DIR/packages.config http://cakebuild.net/bootstrapper/packages - if [ $? -ne 0 ]; then - echo "An error occured while downloading packages.config." - exit 1 - fi -fi +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" +DOTNET_CHANNEL="STS" + +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_NOLOGO=1 + +########################################################################### +# EXECUTION +########################################################################### + +function FirstJsonValue { + perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" +} -# Download NuGet (v3.5.0) if it does not exist. -if [ ! -f $NUGET_OLD_EXE ]; then - echo "Downloading NuGet..." - curl -Lsfo $NUGET_OLD_EXE https://dist.nuget.org/win-x86-commandline/v3.5.0/nuget.exe - if [ $? -ne 0 ]; then - echo "An error occured while downloading nuget.exe." - exit 1 +# If dotnet CLI is installed globally and it matches requested version, use for execution +if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then + export DOTNET_EXE="$(command -v dotnet)" +else + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # If global.json exists, load expected version + if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then + DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") + if [[ "$DOTNET_VERSION" == "" ]]; then + unset DOTNET_VERSION + fi fi -fi -# Download NuGet (latest) if it does not exist. -if [ ! -f $NUGET_EXE ]; then - echo "Downloading NuGet..." - curl -Lsfo $NUGET_EXE https://dist.nuget.org/win-x86-commandline/latest/nuget.exe - if [ $? -ne 0 ]; then - echo "An error occured while downloading nuget.exe." - exit 1 + # Install by channel or version + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + if [[ -z ${DOTNET_VERSION+x} ]]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path fi + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" + export PATH="$DOTNET_DIRECTORY:$PATH" fi -# Restore tools from NuGet. -pushd $TOOLS_DIR >/dev/null -mono $NUGET_EXE install -ExcludeVersion -if [ $? -ne 0 ]; then - echo "Could not restore NuGet packages." - exit 1 -fi -popd >/dev/null +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" -# Make sure that Cake has been installed. -if [ ! -f $CAKE_EXE ]; then - echo "Could not find Cake.exe at '$CAKE_EXE'." - exit 1 +if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then + "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true + "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true fi -# Start Cake -if $SHOW_VERSION; then - exec mono $CAKE_EXE --version -else - exec mono $CAKE_EXE $SCRIPT --verbosity=$VERBOSITY --configuration=$CONFIGURATION --target=$TARGET $DRYRUN "${SCRIPT_ARGUMENTS[@]}" -fi \ No newline at end of file +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" \ No newline at end of file diff --git a/nuke/.editorconfig b/nuke/.editorconfig new file mode 100644 index 0000000..597b2b5 --- /dev/null +++ b/nuke/.editorconfig @@ -0,0 +1,14 @@ +root = false + +[*] +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.csproj] +indent_size = 2 + +[*.json] +indent_size = 2 \ No newline at end of file diff --git a/nuke/Build.cs b/nuke/Build.cs new file mode 100644 index 0000000..cb2754b --- /dev/null +++ b/nuke/Build.cs @@ -0,0 +1,270 @@ +using Microsoft.Build.Exceptions; +using Nuke.Common; +using Nuke.Common.CI.GitHubActions; +using Nuke.Common.IO; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tools.DotNet; +using Nuke.Common.Tools.GitHub; +using Nuke.Common.Tools.NuGet; +using Nuke.Common.Utilities.Collections; +using Octokit; +using Octokit.Internal; +using Serilog; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Nuke.Common.Tooling; + +using static Nuke.Common.Tools.DotNet.DotNetTasks; +using static Nuke.Common.Tools.NuGet.NuGetTasks; + +using Project = Nuke.Common.ProjectModel.Project; + +class Build : NukeBuild +{ + /// Support plugins are available for: + /// - JetBrains ReSharper https://nuke.build/resharper + /// - JetBrains Rider https://nuke.build/rider + /// - Microsoft VisualStudio https://nuke.build/visualstudio + /// - Microsoft VSCode https://nuke.build/vscode + + public static int Main () => Execute(x => x.RunUnitTests); + + [Nuke.Common.Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] + readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; + + [Nuke.Common.Parameter("ReleaseNotesFilePath - To determine the SemanticVersion")] + readonly AbsolutePath ReleaseNotesFilePath = RootDirectory / "CHANGELOG.md"; + + [Solution] + readonly Solution Solution; + + string TargetProjectName => "AngleSharp.Diffing"; + + string TargetLibName => $"{TargetProjectName}"; + + AbsolutePath SourceDirectory => RootDirectory / "src"; + + AbsolutePath BuildDirectory => SourceDirectory / TargetProjectName / "bin" / Configuration; + + AbsolutePath ResultDirectory => RootDirectory / "bin" / Version; + + AbsolutePath NugetDirectory => ResultDirectory / "nuget"; + + GitHubActions GitHubActions => GitHubActions.Instance; + + Project TargetProject { get; set; } + + // Note: The ChangeLogTasks from Nuke itself look buggy. So using the Cake source code. + IReadOnlyList ChangeLog { get; set; } + + ReleaseNotes LatestReleaseNotes { get; set; } + + SemVersion SemVersion { get; set; } + + string Version { get; set; } + + IReadOnlyCollection TargetFrameworks { get; set; } + + protected override void OnBuildInitialized() + { + var parser = new ReleaseNotesParser(); + + Log.Debug("Reading ChangeLog {FilePath}...", ReleaseNotesFilePath); + ChangeLog = parser.Parse(File.ReadAllText(ReleaseNotesFilePath)); + ChangeLog.NotNull("ChangeLog / ReleaseNotes could not be read!"); + + LatestReleaseNotes = ChangeLog.First(); + LatestReleaseNotes.NotNull("LatestVersion could not be read!"); + + Log.Debug("Using LastestVersion from ChangeLog: {LatestVersion}", LatestReleaseNotes.Version); + SemVersion = LatestReleaseNotes.SemVersion; + Version = LatestReleaseNotes.Version.ToString(); + + if (GitHubActions != null) + { + Log.Debug("Add Version Postfix if under CI - GithubAction(s)..."); + + var buildNumber = GitHubActions.RunNumber; + + if (ScheduledTargets.Contains(Default)) + { + Version = $"{Version}-ci.{buildNumber}"; + } + else if (ScheduledTargets.Contains(PrePublish)) + { + Version = $"{Version}-beta.{buildNumber}"; + } + } + + Log.Information("Building version: {Version}", Version); + + TargetProject = Solution.GetProject(TargetLibName); + TargetProject.NotNull("TargetProject could not be loaded!"); + + TargetFrameworks = TargetProject.GetTargetFrameworks(); + TargetFrameworks.NotNull("No TargetFramework(s) found to build for!"); + + Log.Information("Target Framework(s): {Frameworks}", TargetFrameworks); + } + + Target Clean => _ => _ + .Before(Restore) + .Executes(() => + { + SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(x => x.DeleteDirectory()); + }); + + Target Restore => _ => _ + .Executes(() => + { + DotNetRestore(s => s + .SetProjectFile(Solution)); + }); + + Target Compile => _ => _ + .DependsOn(Restore) + .Executes(() => + { + DotNetBuild(s => s + .SetProjectFile(Solution) + .SetConfiguration(Configuration) + .SetContinuousIntegrationBuild(IsServerBuild) + .EnableNoRestore()); + }); + + Target RunUnitTests => _ => _ + .DependsOn(Compile) + .Executes(() => + { + DotNetTest(s => s + .SetProjectFile(Solution) + .SetConfiguration(Configuration) + .EnableNoRestore() + .EnableNoBuild() + .When(_ => GitHubActions.Instance is not null, x => x.SetLoggers("GitHubActions")) + ); + }); + + Target CreatePackage => _ => _ + .DependsOn(Compile) + .Executes(() => + { + DotNetPack(s => s + .SetProject(TargetProject) + .SetConfiguration(Configuration) + .SetVersion(Version) + .SetOutputDirectory(NugetDirectory) + .EnableIncludeSymbols() + .SetSymbolPackageFormat(DotNetSymbolPackageFormat.snupkg) + .EnableNoRestore() + .EnableNoBuild()); + }); + + Target PublishPackage => _ => _ + .DependsOn(CreatePackage) + .DependsOn(RunUnitTests) + .Executes(() => + { + var apiKey = Environment.GetEnvironmentVariable("NUGET_API_KEY"); + + if (apiKey.IsNullOrEmpty()) + { + throw new BuildAbortedException("Could not resolve the NuGet API key."); + } + + foreach (var nupkg in NugetDirectory.GlobFiles("*.nupkg")) + { + DotNetNuGetPush(s => s + .SetTargetPath(nupkg) + .SetSource("https://api.nuget.org/v3/index.json") + .SetApiKey(apiKey)); + } + }); + + Target PublishPreRelease => _ => _ + .DependsOn(PublishPackage) + .Executes(() => + { + string gitHubToken; + + if (GitHubActions != null) + { + gitHubToken = GitHubActions.Token; + } + else + { + gitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); + } + + if (gitHubToken.IsNullOrEmpty()) + { + throw new BuildAbortedException("Could not resolve GitHub token."); + } + + var credentials = new Credentials(gitHubToken); + + GitHubTasks.GitHubClient = new GitHubClient( + new ProductHeaderValue(nameof(NukeBuild)), + new InMemoryCredentialStore(credentials)); + + GitHubTasks.GitHubClient.Repository.Release + .Create("AngleSharp", TargetProjectName, new NewRelease(Version) + { + Name = Version, + Body = String.Join(Environment.NewLine, LatestReleaseNotes.Notes), + Prerelease = true, + TargetCommitish = "devel", + }); + }); + + Target PublishRelease => _ => _ + .DependsOn(PublishPackage) + .Executes(() => + { + string gitHubToken; + + if (GitHubActions != null) + { + gitHubToken = GitHubActions.Token; + } + else + { + gitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); + } + + if (gitHubToken.IsNullOrEmpty()) + { + throw new BuildAbortedException("Could not resolve GitHub token."); + } + + var credentials = new Credentials(gitHubToken); + + GitHubTasks.GitHubClient = new GitHubClient( + new ProductHeaderValue(nameof(NukeBuild)), + new InMemoryCredentialStore(credentials)); + + GitHubTasks.GitHubClient.Repository.Release + .Create("AngleSharp", TargetProjectName, new NewRelease(Version) + { + Name = Version, + Body = String.Join(Environment.NewLine, LatestReleaseNotes.Notes), + Prerelease = false, + TargetCommitish = "main", + }); + }); + + Target Package => _ => _ + .DependsOn(RunUnitTests) + .DependsOn(CreatePackage); + + Target Default => _ => _ + .DependsOn(Package); + + Target Publish => _ => _ + .DependsOn(PublishRelease); + + Target PrePublish => _ => _ + .DependsOn(PublishPreRelease); +} \ No newline at end of file diff --git a/nuke/Configuration.cs b/nuke/Configuration.cs new file mode 100644 index 0000000..84b66ca --- /dev/null +++ b/nuke/Configuration.cs @@ -0,0 +1,16 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Nuke.Common.Tooling; + +[TypeConverter(typeof(TypeConverter))] +public class Configuration : Enumeration +{ + public static Configuration Debug = new Configuration { Value = nameof(Debug) }; + public static Configuration Release = new Configuration { Value = nameof(Release) }; + + public static implicit operator string(Configuration configuration) + { + return configuration.Value; + } +} \ No newline at end of file diff --git a/nuke/Directory.Build.props b/nuke/Directory.Build.props new file mode 100644 index 0000000..63333c1 --- /dev/null +++ b/nuke/Directory.Build.props @@ -0,0 +1,11 @@ + + + + + + false + false + false + + + \ No newline at end of file diff --git a/nuke/Directory.Build.targets b/nuke/Directory.Build.targets new file mode 100644 index 0000000..3f61047 --- /dev/null +++ b/nuke/Directory.Build.targets @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/nuke/Extensions/StringExtensions.cs b/nuke/Extensions/StringExtensions.cs new file mode 100644 index 0000000..5dd4b54 --- /dev/null +++ b/nuke/Extensions/StringExtensions.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +// ReSharper disable once CheckNamespace +/// +/// Contains extension methods for . +/// +/// +/// Original from Cake build tool source: +/// https://github.com/cake-build/cake/blob/9828d7b246d332054896e52ba56983822feb3f05/src/Cake.Core/Extensions/StringExtensions.cs +/// +public static class StringExtensions +{ + /// + /// Quotes the specified . + /// + /// The string to quote. + /// A quoted string. + public static string Quote(this string value) + { + if (!IsQuoted(value)) + { + value = string.Concat("\"", value, "\""); + } + + return value; + } + + /// + /// Unquote the specified . + /// + /// The string to unquote. + /// An unquoted string. + public static string UnQuote(this string value) + { + if (IsQuoted(value)) + { + value = value.Trim('"'); + } + + return value; + } + + /// + /// Splits the into lines. + /// + /// The string to split. + /// The lines making up the provided string. + public static string[] SplitLines(this string content) + { + content = NormalizeLineEndings(content); + return content.Split(new[] { "\r\n" }, StringSplitOptions.None); + } + + /// + /// Normalizes the line endings in a . + /// + /// The string to normalize line endings in. + /// A with normalized line endings. + public static string NormalizeLineEndings(this string value) + { + if (value != null) + { + value = value.Replace("\r\n", "\n"); + value = value.Replace("\r", string.Empty); + return value.Replace("\n", "\r\n"); + } + + return string.Empty; + } + + private static bool IsQuoted(this string value) + { + return value.StartsWith("\"", StringComparison.OrdinalIgnoreCase) + && value.EndsWith("\"", StringComparison.OrdinalIgnoreCase); + } +} \ No newline at end of file diff --git a/nuke/ReleaseNotes.cs b/nuke/ReleaseNotes.cs new file mode 100644 index 0000000..01dcdad --- /dev/null +++ b/nuke/ReleaseNotes.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; + +/// +/// Represent release notes. +/// +/// +/// Original from Cake build tool source: +/// https://github.com/cake-build/cake/blob/9828d7b246d332054896e52ba56983822feb3f05/src/Cake.Common/ReleaseNotes.cs +/// +public sealed class ReleaseNotes +{ + private readonly List _notes; + + /// + /// Gets the version. + /// + /// The version. + public SemVersion SemVersion { get; } + + /// + /// Gets the version. + /// + /// The version. + public Version Version { get; } + + /// + /// Gets the release notes. + /// + /// The release notes. + public IReadOnlyList Notes => _notes; + + /// + /// Gets the raw text of the line that was extracted from. + /// + /// The raw text of the Version line. + public string RawVersionLine { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The semantic version. + /// The notes. + /// The raw text of the version line. + public ReleaseNotes(SemVersion semVersion, IEnumerable notes, string rawVersionLine) + : this( + semVersion?.AssemblyVersion ?? throw new ArgumentNullException(nameof(semVersion)), + semVersion, + notes, + rawVersionLine) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The version. + /// The notes. + /// The raw text of the version line. + public ReleaseNotes(Version version, IEnumerable notes, string rawVersionLine) + : this( + version ?? throw new ArgumentNullException(nameof(version)), + new SemVersion(version.Major, version.Minor, version.Build), + notes, + rawVersionLine) + { + } + + private ReleaseNotes(Version version, SemVersion semVersion, IEnumerable notes, string rawVersionLine) + { + Version = version ?? throw new ArgumentNullException(nameof(version)); + SemVersion = semVersion ?? throw new ArgumentNullException(nameof(semVersion)); + RawVersionLine = rawVersionLine; + _notes = new List(notes ?? Enumerable.Empty()); + } +} \ No newline at end of file diff --git a/nuke/ReleaseNotesParser.cs b/nuke/ReleaseNotesParser.cs new file mode 100644 index 0000000..d911c90 --- /dev/null +++ b/nuke/ReleaseNotesParser.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Build.Exceptions; + +/// +/// The release notes parser. +/// +/// +/// Original from Cake build tool source: +/// https://github.com/cake-build/cake/blob/9828d7b246d332054896e52ba56983822feb3f05/src/Cake.Common/ReleaseNotesParser.cs +/// +public sealed class ReleaseNotesParser +{ + /// + /// Parses all release notes. + /// + /// The content. + /// All release notes. + public IReadOnlyList Parse(string content) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + var lines = content.SplitLines(); + if (lines.Length > 0) + { + var line = lines[0].Trim(); + + if (line.StartsWith("#", StringComparison.OrdinalIgnoreCase)) + { + return ParseComplexFormat(lines); + } + + if (line.StartsWith("*", StringComparison.OrdinalIgnoreCase)) + { + return ParseSimpleFormat(lines); + } + } + + throw new BuildAbortedException("Unknown release notes format."); + } + + private IReadOnlyList ParseComplexFormat(string[] lines) + { + var lineIndex = 0; + var result = new List(); + + while (true) + { + if (lineIndex >= lines.Length) + { + break; + } + + // Create release notes. + var semVer = SemVersion.Zero; + var version = SemVersion.TryParse(lines[lineIndex], out semVer); + if (!version) + { + throw new BuildAbortedException("Could not parse version from release notes header."); + } + + var rawVersionLine = lines[lineIndex]; + + // Increase the line index. + lineIndex++; + + // Parse content. + var notes = new List(); + while (true) + { + // Sanity checks. + if (lineIndex >= lines.Length) + { + break; + } + + if (lines[lineIndex].StartsWith("#", StringComparison.OrdinalIgnoreCase)) + { + break; + } + + // Get the current line. + var line = (lines[lineIndex] ?? string.Empty).Trim('*').Trim(); + if (!string.IsNullOrWhiteSpace(line)) + { + notes.Add(line); + } + + lineIndex++; + } + + result.Add(new ReleaseNotes(semVer, notes, rawVersionLine)); + } + + return result.OrderByDescending(x => x.SemVersion).ToArray(); + } + + private IReadOnlyList ParseSimpleFormat(string[] lines) + { + var lineIndex = 0; + var result = new List(); + + while (true) + { + if (lineIndex >= lines.Length) + { + break; + } + + // Trim the current line. + var line = (lines[lineIndex] ?? string.Empty).Trim('*', ' '); + if (string.IsNullOrWhiteSpace(line)) + { + lineIndex++; + continue; + } + + // Parse header. + var semVer = SemVersion.Zero; + var version = SemVersion.TryParse(lines[lineIndex], out semVer); + if (!version) + { + throw new BuildAbortedException("Could not parse version from release notes header."); + } + + // Parse the description. + line = line.Substring(semVer.ToString().Length).Trim('-', ' '); + + // Add the release notes to the result. + result.Add(new ReleaseNotes(semVer, new[] { line }, line)); + + lineIndex++; + } + + return result.OrderByDescending(x => x.SemVersion).ToArray(); + } +} \ No newline at end of file diff --git a/nuke/SemVersion.cs b/nuke/SemVersion.cs new file mode 100644 index 0000000..4a9a715 --- /dev/null +++ b/nuke/SemVersion.cs @@ -0,0 +1,378 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; + +/// +/// Class for representing semantic versions. +/// +/// +/// Original from Cake build tool source: +/// https://github.com/cake-build/cake/blob/9828d7b246d332054896e52ba56983822feb3f05/src/Cake.Common/SemanticVersion.cs +/// +public class SemVersion : IComparable, IComparable, IEquatable +{ + /// + /// Gets the default version of a SemanticVersion. + /// + public static SemVersion Zero { get; } = new SemVersion(0, 0, 0, null, null, "0.0.0"); + + /// + /// Regex property for parsing a semantic version number. + /// + public static readonly Regex SemVerRegex = + new Regex( + @"(?0|(?:[1-9]\d*))(?:\.(?0|(?:[1-9]\d*))(?:\.(?0|(?:[1-9]\d*)))?(?:\-(?[0-9A-Z\.-]+))?(?:\+(?[0-9A-Z\.-]+))?)?", + RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase); + + /// + /// Gets the major number of the version. + /// + public int Major { get; } + + /// + /// Gets the minor number of the version. + /// + public int Minor { get; } + + /// + /// Gets the patch number of the version. + /// + public int Patch { get; } + + /// + /// Gets the prerelease of the version. + /// + public string PreRelease { get; } + + /// + /// Gets the meta of the version. + /// + public string Meta { get; } + + /// + /// Gets a value indicating whether semantic version is a prerelease or not. + /// + public bool IsPreRelease { get; } + + /// + /// Gets a value indicating whether semantic version has meta or not. + /// + public bool HasMeta { get; } + + /// + /// Gets the VersionString of the semantic version. + /// + public string VersionString { get; } + + /// + /// Gets the AssemblyVersion of the semantic version. + /// + public Version AssemblyVersion { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Major number. + /// Minor number. + /// Patch number. + /// Prerelease string. + /// Meta string. + public SemVersion(int major, int minor, int patch, string preRelease = null, string meta = null) : this(major, + minor, patch, preRelease, meta, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Major number. + /// Minor number. + /// Patch number. + /// Prerelease string. + /// Meta string. + /// The complete version number. + public SemVersion(int major, int minor, int patch, string preRelease, string meta, string versionString) + { + Major = major; + Minor = minor; + Patch = patch; + AssemblyVersion = new Version(major, minor, patch); + IsPreRelease = !string.IsNullOrEmpty(preRelease); + HasMeta = !string.IsNullOrEmpty(meta); + PreRelease = IsPreRelease ? preRelease : null; + Meta = HasMeta ? meta : null; + + if (!string.IsNullOrEmpty(versionString)) + { + VersionString = versionString; + } + else + { + var sb = new StringBuilder(); + sb.AppendFormat(CultureInfo.InvariantCulture, "{0}.{1}.{2}", Major, Minor, Patch); + + if (IsPreRelease) + { + sb.AppendFormat(CultureInfo.InvariantCulture, "-{0}", PreRelease); + } + + if (HasMeta) + { + sb.AppendFormat(CultureInfo.InvariantCulture, "+{0}", Meta); + } + + VersionString = sb.ToString(); + } + } + + /// + /// Method which tries to parse a semantic version string. + /// + /// the version that should be parsed. + /// the out parameter the parsed version should be stored in. + /// Returns a boolean indicating if the parse was successful. + public static bool TryParse(string version, + out SemVersion semVersion) + { + semVersion = Zero; + + if (string.IsNullOrEmpty(version)) + { + return false; + } + + var match = SemVerRegex.Match(version); + if (!match.Success) + { + return false; + } + + if (!int.TryParse( + match.Groups["Major"].Value, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var major) || + !int.TryParse( + match.Groups["Minor"].Value, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var minor) || + !int.TryParse( + match.Groups["Patch"].Value, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var patch)) + { + return false; + } + + semVersion = new SemVersion( + major, + minor, + patch, + match.Groups["PreRelease"]?.Value, + match.Groups["Meta"]?.Value, + version); + + return true; + } + + /// + /// Checks if two SemVersion objects are equal. + /// + /// the other SemVersion want to test equality to. + /// A boolean indicating whether the objecst we're equal or not. + public bool Equals(SemVersion other) + { + return other is object + && Major == other.Major + && Minor == other.Minor + && Patch == other.Patch + && string.Equals(PreRelease, other.PreRelease, StringComparison.OrdinalIgnoreCase) + && string.Equals(Meta, other.Meta, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Compares to SemVersion objects to and another. + /// + /// The SemVersion object we compare with. + /// Return 0 if the objects are identical, 1 if the version is newer and -1 if the version is older. + public int CompareTo(SemVersion other) + { + if (other is null) + { + return 1; + } + + if (Equals(other)) + { + return 0; + } + + if (Major > other.Major) + { + return 1; + } + + if (Major < other.Major) + { + return -1; + } + + if (Minor > other.Minor) + { + return 1; + } + + if (Minor < other.Minor) + { + return -1; + } + + if (Patch > other.Patch) + { + return 1; + } + + if (Patch < other.Patch) + { + return -1; + } + + if (IsPreRelease != other.IsPreRelease) + { + return other.IsPreRelease ? 1 : -1; + } + + switch (StringComparer.InvariantCultureIgnoreCase.Compare(PreRelease, other.PreRelease)) + { + case 1: + return 1; + + case -1: + return -1; + + default: + { + return (string.IsNullOrEmpty(Meta) != string.IsNullOrEmpty(other.Meta)) + ? string.IsNullOrEmpty(Meta) ? 1 : -1 + : StringComparer.InvariantCultureIgnoreCase.Compare(Meta, other.Meta); + } + } + } + + /// + /// Compares to SemVersion objects to and another. + /// + /// The object we compare with. + /// Return 0 if the objects are identical, 1 if the version is newer and -1 if the version is older. + public int CompareTo(object obj) + { + return (obj is SemVersion semVersion) + ? CompareTo(semVersion) + : -1; + } + + /// + /// Equals-method for the SemVersion class. + /// + /// the other SemVersion want to test equality to. + /// A boolean indicating whether the objecst we're equal or not. + public override bool Equals(object obj) + { + return (obj is SemVersion semVersion) + && Equals(semVersion); + } + + /// + /// Method for getting the hashcode of the SemVersion object. + /// + /// The hashcode of the SemVersion object. + public override int GetHashCode() + { + unchecked + { + var hashCode = Major; + hashCode = (hashCode * 397) ^ Minor; + hashCode = (hashCode * 397) ^ Patch; + hashCode = (hashCode * 397) ^ + (PreRelease != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(PreRelease) : 0); + hashCode = (hashCode * 397) ^ (Meta != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Meta) : 0); + return hashCode; + } + } + + /// + /// Returns the string representation of an SemVersion object. + /// + /// The string representation of the object. + public override string ToString() + { + int[] verParts = { Major, Minor, Patch }; + string ver = string.Join(".", verParts); + return $"{ver}{(IsPreRelease ? "-" : string.Empty)}{PreRelease}{Meta}"; + } + + /// + /// The greater than-operator for the SemVersion class. + /// + /// first SemVersion. + /// second. SemVersion. + /// A value indicating if the operand1 was greater than operand2. + public static bool operator >(SemVersion operand1, SemVersion operand2) + => operand1 is { } && operand1.CompareTo(operand2) == 1; + + /// + /// The less than-operator for the SemVersion class. + /// + /// first SemVersion. + /// second. SemVersion. + /// A value indicating if the operand1 was less than operand2. + public static bool operator <(SemVersion operand1, SemVersion operand2) + => operand1 is { } + ? operand1.CompareTo(operand2) == -1 + : operand2 is { }; + + /// + /// The greater than or equal to-operator for the SemVersion class. + /// + /// first SemVersion. + /// second. SemVersion. + /// A value indicating if the operand1 was greater than or equal to operand2. + public static bool operator >=(SemVersion operand1, SemVersion operand2) + => operand1 is { } + ? operand1.CompareTo(operand2) >= 0 + : operand2 is null; + + /// + /// The lesser than or equal to-operator for the SemVersion class. + /// + /// first SemVersion. + /// second. SemVersion. + /// A value indicating if the operand1 was lesser than or equal to operand2. + public static bool operator <=(SemVersion operand1, SemVersion operand2) + => operand1 is null || operand1.CompareTo(operand2) <= 0; + + /// + /// The equal to-operator for the SemVersion class. + /// + /// first SemVersion. + /// second. SemVersion. + /// A value indicating if the operand1 was equal to operand2. + public static bool operator ==(SemVersion operand1, SemVersion operand2) + => operand1?.Equals(operand2) ?? operand2 is null; + + /// + /// The not equal to-operator for the SemVersion class. + /// + /// first SemVersion. + /// second. SemVersion. + /// A value indicating if the operand1 was not equal to operand2. + public static bool operator !=(SemVersion operand1, SemVersion operand2) + => !(operand1?.Equals(operand2) ?? operand2 is null); +} \ No newline at end of file diff --git a/nuke/_build.csproj b/nuke/_build.csproj new file mode 100644 index 0000000..1c79a37 --- /dev/null +++ b/nuke/_build.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + + CS0649;CS0169 + .. + .. + 1 + + + + + + + + + + + \ No newline at end of file diff --git a/src/AngleSharp.Diffing.sln b/src/AngleSharp.Diffing.sln index 83fb1ab..fe70bc8 100644 --- a/src/AngleSharp.Diffing.sln +++ b/src/AngleSharp.Diffing.sln @@ -10,9 +10,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E8E3C8B4-92C3-4DB7-B920-D28651E24A57}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig - ..\tools\anglesharp.cake = ..\tools\anglesharp.cake - ..\build.cake = ..\build.cake ..\build.ps1 = ..\build.ps1 + ..\build.sh = ..\build.sh ..\CHANGELOG.md = ..\CHANGELOG.md Directory.Build.props = Directory.Build.props ..\README.md = ..\README.md diff --git a/tools/anglesharp.cake b/tools/anglesharp.cake deleted file mode 100644 index 3457063..0000000 --- a/tools/anglesharp.cake +++ /dev/null @@ -1,200 +0,0 @@ -#addin nuget:?package=Cake.FileHelpers&version=4.0.1 -#addin nuget:?package=Octokit&version=0.32.0 -using Octokit; - -var configuration = Argument("configuration", "Release"); -var isRunningOnUnix = IsRunningOnUnix(); -var isRunningOnWindows = IsRunningOnWindows(); -var isRunningOnGitHubActions = BuildSystem.GitHubActions.IsRunningOnGitHubActions; -var releaseNotes = ParseReleaseNotes("./CHANGELOG.md"); -var version = releaseNotes.RawVersionLine.Substring(2); -var buildDir = Directory($"./src/{projectName}/bin") + Directory(configuration); -var buildResultDir = Directory("./bin") + Directory(version); -var nugetRoot = buildResultDir + Directory("nuget"); - -if (isRunningOnGitHubActions) -{ - var buildNumber = BuildSystem.GitHubActions.Environment.Workflow.RunNumber; - - if (target == "Default") - { - version = $"{version}-ci-{buildNumber}"; - } - else if (target == "PrePublish") - { - version = $"{version}-alpha-{buildNumber}"; - } -} - -if (!isRunningOnWindows) -{ - frameworks.Remove("net46"); - frameworks.Remove("net461"); - frameworks.Remove("net472"); -} - -// Initialization -// ---------------------------------------- - -Setup(_ => -{ - Information($"Building version {version} of {projectName}."); - Information("For the publish target the following environment variables need to be set:"); - Information("- NUGET_API_KEY"); - Information("- GITHUB_API_TOKEN"); -}); - -// Tasks -// ---------------------------------------- - -Task("Clean") - .Does(() => - { - CleanDirectories(new DirectoryPath[] { buildDir, buildResultDir, nugetRoot }); - }); - -Task("Restore-Packages") - .IsDependentOn("Clean") - .Does(() => - { - NuGetRestore($"./src/{solutionName}.sln", new NuGetRestoreSettings - { - ToolPath = "tools/nuget.exe", - }); - }); - -Task("Build") - .IsDependentOn("Restore-Packages") - .Does(() => - { - var buildSettings = new DotNetCoreMSBuildSettings(); - buildSettings.WarningCodesAsMessage.Add("CS1591"); // During CI builds we don't want the output polluted by "warning CS1591: Missing XML comment" warnings - - ReplaceRegexInFiles("./src/Directory.Build.props", "(?<=)(.+?)(?=)", version); - DotNetCoreBuild($"./src/{solutionName}.sln", new DotNetCoreBuildSettings - { - Configuration = configuration, - MSBuildSettings = buildSettings - }); - }); - -Task("Run-Unit-Tests") - .IsDependentOn("Build") - .Does(() => - { - var settings = new DotNetCoreTestSettings - { - Configuration = configuration, - NoBuild = false, - }; - - if (isRunningOnGitHubActions) - { - settings.TestAdapterPath = Directory("."); - settings.Loggers.Add("GitHubActions"); - } - - DotNetCoreTest($"./src/{solutionName}.Tests/", settings); - }); - -Task("Copy-Files") - .IsDependentOn("Build") - .Does(() => - { - foreach (var item in frameworks) - { - var targetDir = nugetRoot + Directory("lib") + Directory(item.Key); - CreateDirectory(targetDir); - CopyFiles(new FilePath[] - { - buildDir + Directory(item.Value) + File($"{projectName}.dll"), - buildDir + Directory(item.Value) + File($"{projectName}.xml"), - }, targetDir); - } - - CopyFiles(new FilePath[] { - $"src/{projectName}.nuspec", - "logo.png" - }, nugetRoot); - }); - -Task("Create-Package") - .IsDependentOn("Build") - .Does(() => - { - var settings = new DotNetCorePackSettings - { - Configuration = configuration, - NoBuild = false, - OutputDirectory = nugetRoot, - ArgumentCustomization = args => args.Append("--include-symbols").Append("-p:SymbolPackageFormat=snupkg") - }; - - DotNetCorePack($"./src/{solutionName}/", settings); - }); - -Task("Publish-Package") - .IsDependentOn("Create-Package") - .IsDependentOn("Run-Unit-Tests") - .Does(() => - { - var apiKey = EnvironmentVariable("NUGET_API_KEY"); - - if (String.IsNullOrEmpty(apiKey)) - { - throw new InvalidOperationException("Could not resolve the NuGet API key."); - } - - foreach (var nupkg in GetFiles(nugetRoot.Path.FullPath + "/*.nupkg")) - { - NuGetPush(nupkg, new NuGetPushSettings - { - Source = "https://nuget.org/api/v2/package", - ApiKey = apiKey, - SkipDuplicate = true - }); - } - }); - -Task("Publish-Release") - .IsDependentOn("Publish-Package") - .IsDependentOn("Run-Unit-Tests") - .Does(() => - { - var githubToken = EnvironmentVariable("GITHUB_TOKEN"); - - if (String.IsNullOrEmpty(githubToken)) - { - throw new InvalidOperationException("Could not resolve GitHub token."); - } - - var github = new GitHubClient(new ProductHeaderValue("AngleSharpCakeBuild")) - { - Credentials = new Credentials(githubToken), - }; - - var newRelease = github.Repository.Release; - newRelease.Create("AngleSharp", projectName, new NewRelease("v" + version) - { - Name = version, - Body = String.Join(Environment.NewLine, releaseNotes.Notes), - Prerelease = releaseNotes.Version.ToString() != version, - TargetCommitish = "master", - }).Wait(); - }); - -// Targets -// ---------------------------------------- - -Task("Package") - .IsDependentOn("Run-Unit-Tests") - .IsDependentOn("Create-Package"); - -Task("Default") - .IsDependentOn("Package"); - -Task("Publish") - .IsDependentOn("Publish-Release"); - -Task("PrePublish") - .IsDependentOn("Publish-Package"); diff --git a/tools/packages.config b/tools/packages.config deleted file mode 100644 index e714645..0000000 --- a/tools/packages.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 03215d477284c02c066b2395c5da1a08362f03a1 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Mon, 15 Sep 2025 09:23:33 +0000 Subject: [PATCH 4/4] docs: Update CHANGELOG for version 1.1.0 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72e8386..4824e71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.1.0 + +- An ':ignore' marked attributes will no longer show up as a diff if the ignored attribute was NOT found in the test html. By [@RiRiSharp](https://github.com/RiRiSharp). + # 1.0.0 - Enabled using `diff:ignoreAttributes` and `diff:ignoreChildren` together on the same element.