From 8deb18c1d9b4d4e060782d166d6670405cc0661b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 18:04:29 +0000 Subject: [PATCH 1/4] Initial plan From f6d5aeb9417aa57c9875baf9d01b80dfe63a5c3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 18:08:54 +0000 Subject: [PATCH 2/4] Modernize CI workflow to use dotnet CLI directly Co-authored-by: egil <105649+egil@users.noreply.github.com> --- .github/workflows/ci.yml | 78 +++++++++++++++++++++++++++++++++++----- .gitignore | 1 + 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d0bca6..0971118 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,29 +5,89 @@ on: [push, pull_request] env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + DOCS_PATH: ${{ secrets.DOCS_PATH }} + DOCS_BRANCH: ${{ secrets.DOCS_BRANCH }} jobs: + can_document: + runs-on: ubuntu-latest + outputs: + value: ${{ steps.check_job.outputs.value }} + steps: + - name: Checks whether documentation can be built + id: check_job + run: | + echo "value: ${{ env.DOCS_PATH != null && github.ref == env.DOCS_BRANCH }}" + echo "value=${{ env.DOCS_PATH != null && github.ref == env.DOCS_BRANCH }}" >> "${GITHUB_OUTPUT}" + + documentation: + needs: [can_document] + runs-on: ubuntu-latest + if: needs.can_document.outputs.value == 'true' + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: "14.x" + registry-url: 'https://registry.npmjs.org' + + - name: Install Dependencies + run: | + cd $DOCS_PATH + npm install + + - name: Deploy Doclet + run: | + cd $DOCS_PATH + npx pilet publish --fresh --url https://feed.piral.cloud/api/v1/pilet/anglesharp --api-key ${{ secrets.PIRAL_FEED_KEY }} + linux: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x - name: Build - run: ./build.sh + run: dotnet build src/AngleSharp.Diffing.sln --configuration Release + + - name: Test + run: dotnet test src/AngleSharp.Diffing.sln --configuration Release --no-build windows: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x - name: Build + run: dotnet build src/AngleSharp.Diffing.sln --configuration Release + + - name: Test + run: dotnet test src/AngleSharp.Diffing.sln --configuration Release --no-build + + - name: Publish to NuGet + if: github.ref == 'refs/heads/main' run: | - if ($env:GITHUB_REF -eq "refs/heads/master") { - .\build.ps1 -Target Publish - } elseif ($env:GITHUB_REF -eq "refs/heads/devel") { - .\build.ps1 -Target PrePublish - } else { - .\build.ps1 + if ($env:NUGET_API_KEY) { + dotnet pack src/AngleSharp.Diffing.sln --configuration Release --no-build --output nupkgs + dotnet nuget push "nupkgs/*.nupkg" --api-key $env:NUGET_API_KEY --source https://api.nuget.org/v3/index.json --skip-duplicate } diff --git a/.gitignore b/.gitignore index e87b082..9cef900 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. From 1e96e60293b6839993df378663c6d2697878eba8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Sep 2025 08:27:13 +0000 Subject: [PATCH 3/4] Update build.sh to use modern dotnet CLI instead of deprecated Cake/mono Co-authored-by: egil <105649+egil@users.noreply.github.com> --- .github/workflows/ci.yml | 74 ++------------------ build.sh | 142 ++++++++++++++++++++------------------- 2 files changed, 81 insertions(+), 135 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0971118..bd40446 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,64 +5,16 @@ on: [push, pull_request] env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} - DOCS_PATH: ${{ secrets.DOCS_PATH }} - DOCS_BRANCH: ${{ secrets.DOCS_BRANCH }} jobs: - can_document: - runs-on: ubuntu-latest - outputs: - value: ${{ steps.check_job.outputs.value }} - steps: - - name: Checks whether documentation can be built - id: check_job - run: | - echo "value: ${{ env.DOCS_PATH != null && github.ref == env.DOCS_BRANCH }}" - echo "value=${{ env.DOCS_PATH != null && github.ref == env.DOCS_BRANCH }}" >> "${GITHUB_OUTPUT}" - - documentation: - needs: [can_document] - runs-on: ubuntu-latest - if: needs.can_document.outputs.value == 'true' - - steps: - - uses: actions/checkout@v4 - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: "14.x" - registry-url: 'https://registry.npmjs.org' - - - name: Install Dependencies - run: | - cd $DOCS_PATH - npm install - - - name: Deploy Doclet - run: | - cd $DOCS_PATH - npx pilet publish --fresh --url https://feed.piral.cloud/api/v1/pilet/anglesharp --api-key ${{ secrets.PIRAL_FEED_KEY }} - linux: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Setup dotnet - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Build - run: dotnet build src/AngleSharp.Diffing.sln --configuration Release - - - name: Test - run: dotnet test src/AngleSharp.Diffing.sln --configuration Release --no-build + run: ./build.sh windows: runs-on: windows-latest @@ -70,24 +22,12 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup dotnet - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Build - run: dotnet build src/AngleSharp.Diffing.sln --configuration Release - - - name: Test - run: dotnet test src/AngleSharp.Diffing.sln --configuration Release --no-build - - - name: Publish to NuGet - if: github.ref == 'refs/heads/main' run: | - if ($env:NUGET_API_KEY) { - dotnet pack src/AngleSharp.Diffing.sln --configuration Release --no-build --output nupkgs - dotnet nuget push "nupkgs/*.nupkg" --api-key $env:NUGET_API_KEY --source https://api.nuget.org/v3/index.json --skip-duplicate + if ($env:GITHUB_REF -eq "refs/heads/master") { + .\build.ps1 -Target Publish + } elseif ($env:GITHUB_REF -eq "refs/heads/devel") { + .\build.ps1 -Target PrePublish + } else { + .\build.ps1 } diff --git a/build.sh b/build.sh index c62ce5c..a64344d 100755 --- a/build.sh +++ b/build.sh @@ -1,93 +1,99 @@ #!/usr/bin/env bash ############################################################### -# This is the Cake bootstrapper script that is responsible for -# downloading Cake and all specified tools from NuGet. +# This is a modern .NET build script that replaces the legacy +# Cake-based build system to avoid mono dependency on Linux. ############################################################### +set -eo pipefail + # 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 +SRC_DIR=$SCRIPT_DIR/src +SOLUTION_FILE=$SRC_DIR/AngleSharp.Diffing.sln # Define default arguments. -SCRIPT="build.cake" TARGET="Default" CONFIGURATION="Release" -VERBOSITY="verbose" -DRYRUN= +VERBOSITY="minimal" SHOW_VERSION=false -SCRIPT_ARGUMENTS=() # Parse arguments. -for i in "$@"; do +while [[ $# -gt 0 ]]; 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") ;; + -t|--target) TARGET="$2"; shift 2 ;; + -c|--configuration) CONFIGURATION="$2"; shift 2 ;; + -v|--verbosity) VERBOSITY="$2"; shift 2 ;; + --version) SHOW_VERSION=true; shift ;; + --) shift; break ;; + *) shift ;; esac - shift done -# Make sure the tools folder exist. -if [ ! -d $TOOLS_DIR ]; then - mkdir $TOOLS_DIR +# Show dotnet version if requested +if $SHOW_VERSION; then + dotnet --version + exit 0 fi -# 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 +echo "Building AngleSharp.Diffing with target '$TARGET' and configuration '$CONFIGURATION'" -# 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 - fi +# Ensure we have dotnet CLI available +if ! command -v dotnet &> /dev/null; then + echo "Error: dotnet CLI is not installed or not in PATH" + exit 1 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 - fi -fi +echo "Using .NET SDK version: $(dotnet --version)" -# 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 +# Change to source directory +cd $SRC_DIR -# Make sure that Cake has been installed. -if [ ! -f $CAKE_EXE ]; then - echo "Could not find Cake.exe at '$CAKE_EXE'." - exit 1 -fi +# Build based on target +case $TARGET in + "Clean") + echo "Cleaning build artifacts..." + dotnet clean $SOLUTION_FILE --configuration $CONFIGURATION --verbosity $VERBOSITY + ;; + "Restore"|"Restore-Packages") + echo "Restoring NuGet packages..." + dotnet restore $SOLUTION_FILE --verbosity $VERBOSITY + ;; + "Build") + echo "Building solution..." + dotnet build $SOLUTION_FILE --configuration $CONFIGURATION --verbosity $VERBOSITY + ;; + "Test"|"Run-Unit-Tests") + echo "Running tests..." + dotnet test $SOLUTION_FILE --configuration $CONFIGURATION --verbosity $VERBOSITY + ;; + "Package"|"Create-Package") + echo "Building and creating packages..." + dotnet build $SOLUTION_FILE --configuration $CONFIGURATION --verbosity $VERBOSITY + dotnet pack $SOLUTION_FILE --configuration $CONFIGURATION --no-build --verbosity $VERBOSITY + ;; + "Publish"|"Publish-Package") + echo "Building, testing, and publishing packages..." + dotnet build $SOLUTION_FILE --configuration $CONFIGURATION --verbosity $VERBOSITY + dotnet test $SOLUTION_FILE --configuration $CONFIGURATION --no-build --verbosity $VERBOSITY + dotnet pack $SOLUTION_FILE --configuration $CONFIGURATION --no-build --verbosity $VERBOSITY + + # Publish to NuGet if API key is available + if [ ! -z "$NUGET_API_KEY" ]; then + echo "Publishing packages to NuGet..." + for nupkg in $(find . -name "*.nupkg" -not -path "*/bin/Debug/*"); do + echo "Publishing $nupkg" + dotnet nuget push "$nupkg" --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate + done + else + echo "NUGET_API_KEY not set, skipping NuGet publish" + fi + ;; + "Default"|*) + echo "Running default build (build + test + package)..." + dotnet build $SOLUTION_FILE --configuration $CONFIGURATION --verbosity $VERBOSITY + dotnet test $SOLUTION_FILE --configuration $CONFIGURATION --no-build --verbosity $VERBOSITY + dotnet pack $SOLUTION_FILE --configuration $CONFIGURATION --no-build --verbosity $VERBOSITY + ;; +esac -# 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 +echo "Build completed successfully!" \ No newline at end of file From 6d14d3a657b09e3bef77aaac4b2871010f842394 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Sep 2025 09:08:51 +0000 Subject: [PATCH 4/4] Upgrade build system from dotnet CLI to Nuke build automation system Co-authored-by: egil <105649+egil@users.noreply.github.com> --- .gitignore | 8 +- .nuke/build.schema.json | 134 ++++++++++ .nuke/parameters.json | 4 + build.cake | 11 - build.cmd | 7 + build.ps1 | 117 ++++----- build.sh | 140 ++++------- 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 - 20 files changed, 1288 insertions(+), 368 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/.gitignore b/.gitignore index 9cef900..ec5269c 100644 --- a/.gitignore +++ b/.gitignore @@ -347,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 a64344d..320669f 100755 --- a/build.sh +++ b/build.sh @@ -1,99 +1,67 @@ #!/usr/bin/env bash -############################################################### -# This is a modern .NET build script that replaces the legacy -# Cake-based build system to avoid mono dependency on Linux. -############################################################### + +bash --version 2>&1 | head -n 1 set -eo pipefail +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) -# Define directories. -SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) -SRC_DIR=$SCRIPT_DIR/src -SOLUTION_FILE=$SRC_DIR/AngleSharp.Diffing.sln +########################################################################### +# CONFIGURATION +########################################################################### -# Define default arguments. -TARGET="Default" -CONFIGURATION="Release" -VERBOSITY="minimal" -SHOW_VERSION=false +BUILD_PROJECT_FILE="$SCRIPT_DIR/nuke/_build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" -# Parse arguments. -while [[ $# -gt 0 ]]; do - case $1 in - -t|--target) TARGET="$2"; shift 2 ;; - -c|--configuration) CONFIGURATION="$2"; shift 2 ;; - -v|--verbosity) VERBOSITY="$2"; shift 2 ;; - --version) SHOW_VERSION=true; shift ;; - --) shift; break ;; - *) shift ;; - esac -done +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" +DOTNET_CHANNEL="STS" -# Show dotnet version if requested -if $SHOW_VERSION; then - dotnet --version - exit 0 -fi +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_NOLOGO=1 -echo "Building AngleSharp.Diffing with target '$TARGET' and configuration '$CONFIGURATION'" +########################################################################### +# EXECUTION +########################################################################### -# Ensure we have dotnet CLI available -if ! command -v dotnet &> /dev/null; then - echo "Error: dotnet CLI is not installed or not in PATH" - exit 1 -fi +function FirstJsonValue { + perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" +} -echo "Using .NET SDK version: $(dotnet --version)" +# 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" -# Change to source directory -cd $SRC_DIR - -# Build based on target -case $TARGET in - "Clean") - echo "Cleaning build artifacts..." - dotnet clean $SOLUTION_FILE --configuration $CONFIGURATION --verbosity $VERBOSITY - ;; - "Restore"|"Restore-Packages") - echo "Restoring NuGet packages..." - dotnet restore $SOLUTION_FILE --verbosity $VERBOSITY - ;; - "Build") - echo "Building solution..." - dotnet build $SOLUTION_FILE --configuration $CONFIGURATION --verbosity $VERBOSITY - ;; - "Test"|"Run-Unit-Tests") - echo "Running tests..." - dotnet test $SOLUTION_FILE --configuration $CONFIGURATION --verbosity $VERBOSITY - ;; - "Package"|"Create-Package") - echo "Building and creating packages..." - dotnet build $SOLUTION_FILE --configuration $CONFIGURATION --verbosity $VERBOSITY - dotnet pack $SOLUTION_FILE --configuration $CONFIGURATION --no-build --verbosity $VERBOSITY - ;; - "Publish"|"Publish-Package") - echo "Building, testing, and publishing packages..." - dotnet build $SOLUTION_FILE --configuration $CONFIGURATION --verbosity $VERBOSITY - dotnet test $SOLUTION_FILE --configuration $CONFIGURATION --no-build --verbosity $VERBOSITY - dotnet pack $SOLUTION_FILE --configuration $CONFIGURATION --no-build --verbosity $VERBOSITY - - # Publish to NuGet if API key is available - if [ ! -z "$NUGET_API_KEY" ]; then - echo "Publishing packages to NuGet..." - for nupkg in $(find . -name "*.nupkg" -not -path "*/bin/Debug/*"); do - echo "Publishing $nupkg" - dotnet nuget push "$nupkg" --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate - done - else - echo "NUGET_API_KEY not set, skipping NuGet publish" + # 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 - ;; - "Default"|*) - echo "Running default build (build + test + package)..." - dotnet build $SOLUTION_FILE --configuration $CONFIGURATION --verbosity $VERBOSITY - dotnet test $SOLUTION_FILE --configuration $CONFIGURATION --no-build --verbosity $VERBOSITY - dotnet pack $SOLUTION_FILE --configuration $CONFIGURATION --no-build --verbosity $VERBOSITY - ;; -esac + fi + + # 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 + +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + +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 -echo "Build completed successfully!" \ 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