diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..b6f569a2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +# If this file is renamed, the incrementing run attempt number will be reset. + +name: CI + +on: + push: + branches: [ "dev", "main" ] + pull_request: + branches: [ "dev", "main" ] + +env: + CI_BUILD_NUMBER_BASE: ${{ github.run_number }} + CI_TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} + CI_PUBLISH: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + +jobs: + build-windows: + name: Build (Windows) + runs-on: windows-latest + + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + - name: Setup + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + - name: Build and Publish + env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: pwsh + run: | + ./build/Build.Windows.ps1 + + build-linux: + name: Build (Linux) + runs-on: ubuntu-22.04 + + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + - name: Setup + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + - name: Configure Docker + run: | + docker run --privileged --rm linuxkit/binfmt:bebbae0c1100ebf7bf2ad4dfb9dfd719cf0ef132 + sudo service docker restart + - name: Build and Publish + env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} + shell: pwsh + run: | + ./build/Build.Linux.ps1 diff --git a/Build.Common.ps1 b/Build.Common.ps1 deleted file mode 100644 index 87f2d499..00000000 --- a/Build.Common.ps1 +++ /dev/null @@ -1,12 +0,0 @@ -function Get-SemVer($shortver) -{ - # This script originally (c) 2016 Serilog Contributors - license Apache 2.0 - $branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL]; - $suffix = @{ $true = ""; $false = ($branch.Substring(0, [math]::Min(10,$branch.Length)) -replace '[\/\+]','-').Trim("-")}[$branch -eq "main"] - - if ($suffix) { - $shortver + "-" + $suffix - } else { - $shortver - } -} diff --git a/Setup.ps1 b/Setup.ps1 deleted file mode 100644 index 2cef90f3..00000000 --- a/Setup.ps1 +++ /dev/null @@ -1,11 +0,0 @@ -$ErrorActionPreference = "Stop" - -$RequiredDotnetVersion = $(cat ./ci.global.json | convertfrom-json).sdk.version - -New-Item -ItemType Directory -Force "./build/" | Out-Null - -Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile "./build/installcli.ps1" -& ./build/installcli.ps1 -InstallDir "$pwd/.dotnetcli" -NoPath -Version $RequiredDotnetVersion -if ($LASTEXITCODE) { throw ".NET install failed" } - -$env:Path = "$pwd/.dotnetcli;$env:Path" diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index c0f95e9e..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,56 +0,0 @@ -version: 2025.1.{build} -skip_tags: true -image: -- Visual Studio 2022 -- Ubuntu2004 -environment: - DOCKER_TOKEN: - secure: QKr2YEuliXdFKe3jN7w97w== - DOCKER_USER: - 'datalustbuild' -test: off -artifacts: -- path: artifacts/seqcli-*.zip -- path: artifacts/seqcli-*.tar.gz -- path: artifacts/seqcli.*.nupkg -- path: artifacts/seqcli-*.md - -for: -- - matrix: - only: - - image: Visual Studio 2022 - - install: - - pwsh: ./Setup.ps1 - - build_script: - - pwsh: ./Build.ps1 - - deploy: - - - provider: NuGet - api_key: - secure: 8gHaCWoeZrbMxRKH09E/cwYxYVvkiJ9P/GXC8H4oNxoYZ2pQgeWzBGkOT9noYrBU - skip_symbols: true - artifact: /seqcli\..*\.nupkg/ - on: - branch: main - - - provider: GitHub - auth_token: - secure: Bo3ypKpKFxinjR9ShkNekNvkob2iklHJU+UlYyfHtcFFIAa58SV2TkEd0xWxz633 - artifact: /seqcli-.*\.(nupkg|zip|tar\.gz)/ - tag: v$(appveyor_build_version) - on: - branch: main -- - matrix: - only: - - image: Ubuntu2004 - - install: - - pwsh: ./setup.sh - - build_script: - - pwsh: $env:PATH = "$env:HOME/.dotnetcli:$env:PATH"; ./Build.Docker.ps1 diff --git a/baseversion b/baseversion new file mode 100644 index 00000000..103ea540 --- /dev/null +++ b/baseversion @@ -0,0 +1 @@ +2025.1 \ No newline at end of file diff --git a/build/Build.Common.ps1 b/build/Build.Common.ps1 new file mode 100644 index 00000000..711462c1 --- /dev/null +++ b/build/Build.Common.ps1 @@ -0,0 +1,16 @@ +function Get-SemVer() +{ + $branch = @{ $true = $env:CI_TARGET_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$NULL -ne $env:CI_TARGET_BRANCH]; + $revision = @{ $true = "{0:00000}" -f $([convert]::ToInt32($env:CI_BUILD_NUMBER_BASE, 10) + 2300); $false = "local" }[$NULL -ne $env:CI_BUILD_NUMBER_BASE] + $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)) -replace '([^a-zA-Z0-9\-]*)', '')-$revision"}[$branch -eq "main" -and $revision -ne "local"] + $commitHash = $(git rev-parse --short HEAD) + $buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""] + + $base = $(Get-Content ./baseversion).Trim() + + if ($suffix) { + $base + "." + $revision + "-" + $suffix + } else { + $revision + } +} diff --git a/Build.Docker.ps1 b/build/Build.Linux.ps1 similarity index 70% rename from Build.Docker.ps1 rename to build/Build.Linux.ps1 index e68c4381..aa2c188a 100644 --- a/Build.Docker.ps1 +++ b/build/Build.Linux.ps1 @@ -1,11 +1,9 @@ -Push-Location $PSScriptRoot +Push-Location $PSScriptRoot/../ -. ./Build.Common.ps1 +. ./build/Build.Common.ps1 -$IsCIBuild = $null -ne $env:APPVEYOR_BUILD_NUMBER -$IsPublishedBuild = ($env:APPVEYOR_REPO_BRANCH -eq "main" -or $env:APPVEYOR_REPO_BRANCH -eq "dev") -and $null -eq $env:APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH +$version = Get-SemVer -$version = Get-SemVer(@{ $true = $env:APPVEYOR_BUILD_VERSION; $false = "99.99.99" }[$env:APPVEYOR_BUILD_VERSION -ne $NULL]) $framework = "net9.0" $image = "datalust/seqcli" $archs = @( @@ -13,16 +11,13 @@ $archs = @( @{ rid = "arm64"; platform = "linux/arm64/v8" } ) -$endToEndVersion = "preview" - function Execute-Tests { & dotnet test ./test/SeqCli.Tests/SeqCli.Tests.csproj -c Release -f $framework /p:Configuration=Release /p:VersionPrefix=$version if ($LASTEXITCODE -ne 0) { exit 1 } cd ./test/SeqCli.EndToEnd/ - docker pull "datalust/seq:$endToEndVersion" - docker tag "datalust/seq:$endToEndVersion" datalust/seq:latest + docker pull datalust/seq:latest & dotnet run -f $framework -- --docker-server if ($LASTEXITCODE -ne 0) { @@ -35,6 +30,7 @@ function Execute-Tests function Build-DockerImage($arch) { $rid = "linux-$($arch.rid)" + & dotnet publish src/SeqCli/SeqCli.csproj -c Release -f $framework -r $rid --self-contained /p:VersionPrefix=$version /p:PublishSingleFile=true if($LASTEXITCODE -ne 0) { exit 2 } @@ -42,14 +38,19 @@ function Build-DockerImage($arch) if($LASTEXITCODE -ne 0) { exit 3 } } -function Publish-DockerImage($arch) +function Login-ToDocker() { $ErrorActionPreference = "SilentlyContinue" - if ($IsCIBuild) { - Write-Output "$env:DOCKER_TOKEN" | docker login -u $env:DOCKER_USER --password-stdin - if ($LASTEXITCODE) { exit 3 } - } + Write-Output "$env:DOCKER_TOKEN" | docker login -u $env:DOCKER_USER --password-stdin + if ($LASTEXITCODE) { exit 3 } + + $ErrorActionPreference = "Stop" +} + +function Publish-DockerImage($arch) +{ + $ErrorActionPreference = "SilentlyContinue" & docker push "$image-ci:$version-$($arch.rid)" if($LASTEXITCODE -ne 0) { exit 3 } @@ -76,13 +77,15 @@ Execute-Tests foreach ($arch in $archs) { Build-DockerImage($arch) +} - if ($IsPublishedBuild) { +if ("$($env:DOCKER_TOKEN)" -ne "") { + Login-ToDocker + + foreach ($arch in $archs) { Publish-DockerImage($arch) } -} -if ($IsPublishedBuild) { Publish-DockerManifest($archs) } diff --git a/Build.ps1 b/build/Build.Windows.ps1 similarity index 79% rename from Build.ps1 rename to build/Build.Windows.ps1 index caf8ff60..c36fe1f6 100644 --- a/Build.ps1 +++ b/build/Build.Windows.ps1 @@ -1,10 +1,11 @@ -Push-Location $PSScriptRoot +Push-Location $PSScriptRoot/../ -. ./Build.Common.ps1 +. ./build/Build.Common.ps1 $ErrorActionPreference = 'Stop' -$version = Get-SemVer(@{ $true = $env:APPVEYOR_BUILD_VERSION; $false = "99.99.99" }[$env:APPVEYOR_BUILD_VERSION -ne $NULL]) +$version = Get-SemVer + $framework = 'net9.0' $windowsTfmSuffix = '-windows' @@ -78,6 +79,26 @@ function Publish-Docs($version) if($LASTEXITCODE -ne 0) { throw "Build failed" } } +function Upload-NugetPackages +{ + # GitHub Actions will only supply this to branch builds and not PRs. We publish + # builds from any branch this action targets (i.e. main and dev). + + Write-Output "build: Publishing NuGet packages" + + foreach ($nupkg in Get-ChildItem artifacts/*.nupkg) { + & dotnet nuget push -k $env:NUGET_API_KEY -s https://api.nuget.org/v3/index.json "$nupkg" + if($LASTEXITCODE -ne 0) { throw "Publishing failed" } + } +} + +function Upload-GitHubRelease($version) +{ + Write-Output "build: Creating release for version $version" + + iex "gh release create v$version --title v$version --generate-notes $(get-item ./artifacts/*)" +} + function Remove-GlobalJson { if(Test-Path ./global.json) { rm ./global.json } @@ -104,6 +125,16 @@ Publish-Archives($version) Publish-DotNetTool($version) Execute-Tests($version) Publish-Docs($version) + +if ("$($env:NUGET_API_KEY)" -ne "") +{ + Upload-NugetPackages +} + +if ($env:CI_PUBLISH -eq "True") { + Upload-GitHubRelease($version) +} + Remove-GlobalJson Pop-Location diff --git a/docker-publish.ps1 b/docker-publish.ps1 deleted file mode 100644 index 99399c59..00000000 --- a/docker-publish.ps1 +++ /dev/null @@ -1,59 +0,0 @@ -param ( - [Parameter(Mandatory=$True)] - [string] $version -) - -$ErrorActionPreference = "Stop" - -$versionParts = $version.Split('.') - -$major = $versionParts[0] -$minor = $versionParts[1] -$isPre = $version.endswith("pre") - -$image = "datalust/seqcli" -$archs = @("x64", "arm64") -$publishImages = @() - -foreach ($arch in $archs) { - $ciImage = "$image-ci:$version-$arch" - $publishImage = "$($image):$version-$arch"; - - docker pull $ciImage - if ($LASTEXITCODE) { exit 1 } - - docker tag $ciImage $publishImage - if ($LASTEXITCODE) { exit 1 } - - docker push $publishImage - if ($LASTEXITCODE) { exit 1 } - - $publishImages += $publishImage -} - -$publishManifest = "$($image):$version" - -$pushTags = @($publishManifest) - -if ($isPre -eq $True) { - $pushTags += "$($image):preview" -} else { - $pushTags += "$($image):$major", "$($image):$major.$minor", "$($image):latest" -} - -$choices = "&Yes", "&No" -$decision = $Host.UI.PromptForChoice("Publishing ($publishManifest) as ($pushTags)", "Does this look right?", $choices, 1) -if ($decision -eq 0) { - foreach ($pushTag in $pushTags) { - Write-Host "Publishing $pushTag" - - echo "creating manifest $pushTag from $publishImages" - iex "docker manifest create $pushTag $publishImages" - if ($LASTEXITCODE) { exit 1 } - - docker manifest push $pushTag - if ($LASTEXITCODE) { exit 1 } - } -} else { - Write-Host "Cancelled" -} diff --git a/setup.sh b/setup.sh deleted file mode 100755 index 8477bc42..00000000 --- a/setup.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# Exit if any command fails -set -e -set -o pipefail - -sudo apt-get update || true -sudo apt-get install -y --no-install-recommends jq - -RequiredDotnetVersion=$(jq -r '.sdk.version' ci.global.json) - -curl https://dot.net/v1/dotnet-install.sh -sSfL --output dotnet-install.sh -chmod +x dotnet-install.sh -./dotnet-install.sh --install-dir $HOME/.dotnetcli --no-path --version $RequiredDotnetVersion -rm dotnet-install.sh - -docker run --privileged --rm linuxkit/binfmt:bebbae0c1100ebf7bf2ad4dfb9dfd719cf0ef132 -sudo service docker restart diff --git a/src/SeqCli/Cli/Commands/Sample/IngestCommand.cs b/src/SeqCli/Cli/Commands/Sample/IngestCommand.cs index 19171ecb..251c00a0 100644 --- a/src/SeqCli/Cli/Commands/Sample/IngestCommand.cs +++ b/src/SeqCli/Cli/Commands/Sample/IngestCommand.cs @@ -32,6 +32,7 @@ class IngestCommand : Command readonly BatchSizeFeature _batchSize; bool _quiet; + bool _setup; int _simulations = 1; public IngestCommand(SeqConnectionFactory connectionFactory) @@ -41,6 +42,7 @@ public IngestCommand(SeqConnectionFactory connectionFactory) _connection = Enable(); Options.Add("quiet", "Don't echo ingested events to `STDOUT`", _ => _quiet = true); + Options.Add("setup", "Configure sample dashboards, signals, users, and so on before starting ingestion", _ => _setup = true); Options.Add("simulations=", "Number of concurrent simulations to run; the default runs a single simulation", v => _simulations = int.Parse(v)); @@ -51,14 +53,27 @@ protected override async Task Run() { var (url, apiKey) = _connectionFactory.GetConnectionDetails(_connection); var batchSize = _batchSize.Value; - - if (!_confirm.TryConfirm($"This will send sample events to the Seq server at {url}.")) - { + + if (!_confirm.TryConfirm(_setup + ? $"This will apply sample configuration and send sample events to the Seq server at {url}." + : $"This will send sample events to the Seq server at {url}." + )) { await Console.Error.WriteLineAsync("Canceled by user."); return 1; } var connection = _connectionFactory.Connect(_connection); + + if (_setup) + { + var setupResult = await SetupCommand.ImportTemplates(connection); + + if (setupResult != 0) + { + return setupResult; + } + } + var simulations = Enumerable.Range(0, _simulations) .Select(_ => Simulation.RunAsync(connection, apiKey, batchSize, echoToStdout: !_quiet)) .ToList(); diff --git a/src/SeqCli/Cli/Commands/Sample/SetupCommand.cs b/src/SeqCli/Cli/Commands/Sample/SetupCommand.cs index 8905c97c..e801ca10 100644 --- a/src/SeqCli/Cli/Commands/Sample/SetupCommand.cs +++ b/src/SeqCli/Cli/Commands/Sample/SetupCommand.cs @@ -22,6 +22,7 @@ using SeqCli.Templates.Ast; using SeqCli.Templates.Import; using SeqCli.Util; +using Seq.Api; // ReSharper disable once UnusedType.Global @@ -58,6 +59,11 @@ protected override async Task Run() return 1; } + return await ImportTemplates(connection); + } + + internal static async Task ImportTemplates(SeqConnection connection) + { var templateArgs = new Dictionary(); templateArgs["ownerId"] = new JsonTemplateNull(); diff --git a/test/SeqCli.EndToEnd/Sample/SampleIngestTestCase.cs b/test/SeqCli.EndToEnd/Sample/SampleIngestTestCase.cs new file mode 100644 index 00000000..06e5020e --- /dev/null +++ b/test/SeqCli.EndToEnd/Sample/SampleIngestTestCase.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Seq.Api; +using SeqCli.EndToEnd.Support; +using Serilog; +using Xunit; + +namespace SeqCli.EndToEnd.Sample; + +public class SampleIngestTestCase : ICliTestCase +{ + public async Task ExecuteAsync(SeqConnection connection, ILogger logger, CliCommandRunner runner) + { + try + { + runner.Exec("sample ingest", "--setup --confirm", timeout: TimeSpan.FromSeconds(3)); + } + catch + { + // Ignored + } + + var events = await connection.Events.ListAsync(); + Assert.NotEmpty(events); + + var sampleWorkspace = (await connection.Workspaces.ListAsync(shared: true)) + .SingleOrDefault(w => w.Title == "Sample"); + + Assert.NotNull(sampleWorkspace); + } +} \ No newline at end of file diff --git a/test/SeqCli.EndToEnd/Support/CliCommandRunner.cs b/test/SeqCli.EndToEnd/Support/CliCommandRunner.cs index 0ee0c4b6..f7853d02 100644 --- a/test/SeqCli.EndToEnd/Support/CliCommandRunner.cs +++ b/test/SeqCli.EndToEnd/Support/CliCommandRunner.cs @@ -10,10 +10,10 @@ public class CliCommandRunner(TestConfiguration configuration) public ITestProcess? LastRunProcess { get; private set; } - public int Exec(string command, string? args = null, bool disconnected = false) + public int Exec(string command, string? args = null, bool disconnected = false, TimeSpan? timeout = null) { using var process = configuration.SpawnCliProcess(command, args, skipServerArg: disconnected); LastRunProcess = process; - return process.WaitForExit(DefaultExecTimeout); + return process.WaitForExit(timeout ?? DefaultExecTimeout); } } \ No newline at end of file diff --git a/test/SeqCli.EndToEnd/Support/TestConfiguration.cs b/test/SeqCli.EndToEnd/Support/TestConfiguration.cs index dd715813..1f33b97b 100644 --- a/test/SeqCli.EndToEnd/Support/TestConfiguration.cs +++ b/test/SeqCli.EndToEnd/Support/TestConfiguration.cs @@ -41,7 +41,7 @@ public CaptiveProcess SpawnServerProcess(string storagePath) { var containerName = Guid.NewGuid().ToString("n"); const string containerRuntime = "docker"; - return new CaptiveProcess(containerRuntime, $"run --name {containerName} -it --rm -e ACCEPT_EULA=Y -p {_serverListenPort}:80 datalust/seq:{imageTag}", stopCommandFullExePath: containerRuntime, stopCommandArgs: $"stop {containerName}"); + return new CaptiveProcess(containerRuntime, $"run --name {containerName} -d -e ACCEPT_EULA=Y -p {_serverListenPort}:80 datalust/seq:{imageTag}", stopCommandFullExePath: containerRuntime, stopCommandArgs: $"rm -f {containerName}"); } return new CaptiveProcess("seq", commandWithArgs);