From e16ca8c905f3b9347400e9a1fbc28574e087ffe3 Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Tue, 28 Jul 2026 13:21:07 -0500 Subject: [PATCH 01/12] Add Elasticsearch 9 compatibility testing --- build/docker/elasticsearch/9.x/Dockerfile | 4 +++ .../Extensions/ElasticsearchExtensions.cs | 14 ++++++--- src/Exceptionless.AppHost/Program.cs | 31 ++++++++++++++++--- .../Exceptionless.Tests/AppWebHostFactory.cs | 20 ++++++++++-- 4 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 build/docker/elasticsearch/9.x/Dockerfile diff --git a/build/docker/elasticsearch/9.x/Dockerfile b/build/docker/elasticsearch/9.x/Dockerfile new file mode 100644 index 0000000000..8658600712 --- /dev/null +++ b/build/docker/elasticsearch/9.x/Dockerfile @@ -0,0 +1,4 @@ +# https://www.docker.elastic.co/ +FROM docker.elastic.co/elasticsearch/elasticsearch:9.4.2 + +RUN elasticsearch-plugin install -b mapper-size diff --git a/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs b/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs index ec5c5e4031..4f6cc8564c 100644 --- a/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs +++ b/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs @@ -60,7 +60,11 @@ public static IResourceBuilder AddElasticsearch(this IDis .PublishAsConnectionString(); } - public static IResourceBuilder WithKibana(this IResourceBuilder builder, Action>? configureContainer = null, string? containerName = null) + public static IResourceBuilder WithKibana( + this IResourceBuilder builder, + Action>? configureContainer = null, + string? containerName = null, + int? port = null) { ArgumentNullException.ThrowIfNull(builder); @@ -79,7 +83,7 @@ public static IResourceBuilder WithKibana(this IResourceB var resourceBuilder = builder.ApplicationBuilder.AddResource(resource) .WithImage(ElasticsearchContainerImageTags.KibanaImage, ElasticsearchContainerImageTags.Tag) .WithImageRegistry(ElasticsearchContainerImageTags.KibanaRegistry) - .WithHttpEndpoint(targetPort: KibanaPort, name: containerName) + .WithHttpEndpoint(targetPort: KibanaPort, port: port, name: containerName) .WithUrlForEndpoint(containerName, u => u.DisplayText = "Kibana") .WithEnvironment("xpack.security.enabled", "false") .WithEnvironment(ctx => @@ -134,9 +138,11 @@ public async Task CheckHealthAsync(HealthCheckContext context using var settings = new ElasticsearchClientSettings(new Uri(connectionString)); var client = new ElasticsearchClient(settings); - var response = await client.PingAsync(cancellationToken); + var response = await client.Cluster.HealthAsync( + request => request.WaitForStatus(Elastic.Clients.Elasticsearch.HealthStatus.Yellow), + cancellationToken); return response.IsValidResponse ? HealthCheckResult.Healthy() - : new HealthCheckResult(context.Registration.FailureStatus, $"Elasticsearch ping failed: {response.DebugInformation}"); + : new HealthCheckResult(context.Registration.FailureStatus, $"Elasticsearch cluster health check failed: {response.DebugInformation}"); } } diff --git a/src/Exceptionless.AppHost/Program.cs b/src/Exceptionless.AppHost/Program.cs index 51db032edf..1f972bcdf7 100644 --- a/src/Exceptionless.AppHost/Program.cs +++ b/src/Exceptionless.AppHost/Program.cs @@ -9,6 +9,12 @@ bool servicesOnly = HasArgument("--services-only"); bool ciE2E = HasArgument("--ci-e2e"); bool includeDevTools = !ciE2E; +int elasticsearchPort = GetPort("Elasticsearch:Port", 9200); +string elasticsearchImageTag = builder.Configuration["Elasticsearch:ImageTag"] ?? ElasticsearchContainerImageTags.Tag; +string elasticsearchContainerName = builder.Configuration["Elasticsearch:ContainerName"] ?? "Exceptionless-Elasticsearch"; +string elasticsearchDataVolume = builder.Configuration["Elasticsearch:DataVolume"] ?? "exceptionless.data.v1"; +string kibanaContainerName = builder.Configuration["Elasticsearch:KibanaContainerName"] ?? "Exceptionless-Kibana"; +int kibanaPort = GetPort("Elasticsearch:KibanaPort", 5601); int oldAppHttpPort = worktreePorts?.OldAppHttp ?? 7120; int oldAppPort = worktreePorts?.OldAppHttps ?? 7121; int oldAppLiveReloadPort = worktreePorts?.OldAppLiveReload ?? 35729; @@ -19,8 +25,9 @@ string exceptionlessServerUrl = worktreePorts?.ApiHttpsUrl ?? $"https://api-ex.dev.localhost:{DefaultApiHttpsPort}"; const string SharedEmailConnectionString = "smtp://localhost:1026"; -var elastic = builder.AddElasticsearch("Elasticsearch", port: 9200) - .WithDataVolume("exceptionless.data.v1") +var elastic = builder.AddElasticsearch("Elasticsearch", port: elasticsearchPort) + .WithImageTag(elasticsearchImageTag) + .WithDataVolume(elasticsearchDataVolume) .WithEndpointProxySupport(false); var storage = builder.AddAzureStorage("Storage") @@ -65,15 +72,17 @@ var ownedElastic = elastic; elastic = ownedElastic .WithLifetime(ContainerLifetime.Persistent) - .WithContainerName("Exceptionless-Elasticsearch"); + .WithContainerName(elasticsearchContainerName); if (!servicesOnly && includeDevTools) { elastic = elastic.WithKibana(b => b + .WithImageTag(elasticsearchImageTag) .WithLifetime(ContainerLifetime.Persistent) .WithEndpointProxySupport(false) - .WithContainerName("Exceptionless-Kibana") - .WithParentRelationship(ownedElastic)); + .WithContainerName(kibanaContainerName) + .WithParentRelationship(ownedElastic), + port: kibanaPort); } var ownedCache = cache; @@ -231,3 +240,15 @@ await builder.Build().RunAsync(); bool HasArgument(string name) => args.Any(arg => StringComparer.OrdinalIgnoreCase.Equals(arg, name) || StringComparer.OrdinalIgnoreCase.Equals(arg, name.TrimStart('-'))); + +int GetPort(string key, int defaultValue) +{ + string? value = builder.Configuration[key]; + if (String.IsNullOrWhiteSpace(value)) + return defaultValue; + + if (!Int32.TryParse(value, out int port) || port is < 1 or > 65535) + throw new InvalidOperationException($"Configuration value '{key}' must be a valid TCP port."); + + return port; +} diff --git a/tests/Exceptionless.Tests/AppWebHostFactory.cs b/tests/Exceptionless.Tests/AppWebHostFactory.cs index fa1d2f73ee..b80ebcc79a 100644 --- a/tests/Exceptionless.Tests/AppWebHostFactory.cs +++ b/tests/Exceptionless.Tests/AppWebHostFactory.cs @@ -21,7 +21,7 @@ namespace Exceptionless.Tests; public class AppWebHostFactory : WebApplicationFactory, IAsyncLifetime { - private const string SharedElasticsearchUrl = "http://localhost:9200"; + private static readonly string SharedElasticsearchUrl = GetSharedElasticsearchUrl(); private static readonly TimeSpan SharedElasticsearchStartupTimeout = TimeSpan.FromMinutes(3); private static int s_counter = -1; private static readonly Lazy> s_sharedAppHost = new(StartSharedAppHostAsync, LazyThreadSafetyMode.ExecutionAndPublication); @@ -58,16 +58,30 @@ private static async Task StartSharedAppHostAsync() return app; } + private static string GetSharedElasticsearchUrl() + { + const int defaultPort = 9200; + string? configuredPort = Environment.GetEnvironmentVariable("Elasticsearch__Port"); + if (String.IsNullOrWhiteSpace(configuredPort)) + return $"http://localhost:{defaultPort}"; + + if (!Int32.TryParse(configuredPort, out int port) || port is < 1 or > 65535) + throw new InvalidOperationException("Environment variable 'Elasticsearch__Port' must be a valid TCP port."); + + return $"http://localhost:{port}"; + } + private static async Task WaitForElasticsearchAsync(Uri elasticsearchUri) { - using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(1) }; + using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(2) }; var deadline = TimeProvider.System.GetUtcNow() + SharedElasticsearchStartupTimeout; + var healthUri = new Uri(elasticsearchUri, "/_cluster/health?wait_for_status=yellow&timeout=1s"); while (TimeProvider.System.GetUtcNow() < deadline) { try { - using var response = await client.GetAsync(elasticsearchUri); + using var response = await client.GetAsync(healthUri); if (response.StatusCode == HttpStatusCode.OK) return; } From 7da5914beec0585273d4fc1ad533866ec32bd49b Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Tue, 28 Jul 2026 14:16:07 -0500 Subject: [PATCH 02/12] Upgrade Elasticsearch stack to 9.4.2 --- .github/workflows/elasticsearch-docker-8.yml | 2 +- .github/workflows/elasticsearch-docker-9.yml | 46 +++++++++++++++++++ Dockerfile | 2 +- docker/docker-compose.apm.yml | 8 ++-- docker/docker-compose.dev.yml | 4 +- docker/docker-compose.yml | 4 +- k8s/elastic-monitor.yaml | 8 ++-- k8s/ex-dev-elasticsearch.yaml | 6 +-- k8s/ex-prod-elasticsearch.yaml | 6 +-- k8s/exceptionless/values.yaml | 2 +- samples/docker-compose.all-in-one.yml | 2 +- samples/docker-compose.yml | 4 +- .../Extensions/ElasticsearchExtensions.cs | 4 +- 13 files changed, 72 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/elasticsearch-docker-9.yml diff --git a/.github/workflows/elasticsearch-docker-8.yml b/.github/workflows/elasticsearch-docker-8.yml index 6f65a4ccd6..f8577d97c8 100644 --- a/.github/workflows/elasticsearch-docker-8.yml +++ b/.github/workflows/elasticsearch-docker-8.yml @@ -43,4 +43,4 @@ jobs: working-directory: build/docker/elasticsearch/8.x run: | VERSION=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' Dockerfile) - docker buildx build --platform linux/amd64,linux/arm64 --output "type=image,push=true" --file ./Dockerfile . --tag exceptionless/elasticsearch:$VERSION --tag exceptionless/elasticsearch:latest + docker buildx build --platform linux/amd64,linux/arm64 --output "type=image,push=true" --file ./Dockerfile . --tag exceptionless/elasticsearch:$VERSION diff --git a/.github/workflows/elasticsearch-docker-9.yml b/.github/workflows/elasticsearch-docker-9.yml new file mode 100644 index 0000000000..a16bd76f4b --- /dev/null +++ b/.github/workflows/elasticsearch-docker-9.yml @@ -0,0 +1,46 @@ +name: Elasticsearch 9.x Docker Image CI + +on: + push: + paths: + - "build/docker/elasticsearch/9.x/**" + - ".github/workflows/elasticsearch-docker-9.yml" + +jobs: + build: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') != true + + steps: + - uses: actions/checkout@v7 + - name: Setup .NET Core + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.301 + - name: Build Reason + env: + GITHUB_EVENT: ${{ toJson(github) }} + run: "echo ref: ${{github.ref}} event: ${{github.event_name}}" + - name: Build Version + run: | + dotnet tool install --global minver-cli --version 7.0.0 + version=$(minver --tag-prefix v) + echo "MINVERVERSIONOVERRIDE=$version" >> $GITHUB_ENV + echo "VERSION=$version" >> $GITHUB_ENV + echo "### Version: $version" >> $GITHUB_STEP_SUMMARY + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + - name: Login to DockerHub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + with: + platforms: linux/amd64,linux/arm64 + - name: Build custom Elasticsearch 9.x docker image + working-directory: build/docker/elasticsearch/9.x + run: | + VERSION=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' Dockerfile) + docker buildx build --platform linux/amd64,linux/arm64 --output "type=image,push=true" --file ./Dockerfile . --tag exceptionless/elasticsearch:$VERSION --tag exceptionless/elasticsearch:latest diff --git a/Dockerfile b/Dockerfile index 80cc955548..37f4570fb3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -100,7 +100,7 @@ ENTRYPOINT ["/app/app-docker-entrypoint.sh"] # completely self-contained -FROM exceptionless/elasticsearch:8.19.15 AS exceptionless +FROM exceptionless/elasticsearch:9.4.2 AS exceptionless WORKDIR /app COPY --from=job-publish /app/src/Exceptionless.Job/out ./ diff --git a/docker/docker-compose.apm.yml b/docker/docker-compose.apm.yml index bd55972eb9..9862cacd7d 100644 --- a/docker/docker-compose.apm.yml +++ b/docker/docker-compose.apm.yml @@ -2,7 +2,7 @@ version: "2.2" services: setup: - image: docker.elastic.co/elasticsearch/elasticsearch:8.19.15 + image: docker.elastic.co/elasticsearch/elasticsearch:9.4.2 volumes: - certs:/usr/share/elasticsearch/config/certs user: "0" @@ -53,7 +53,7 @@ services: depends_on: setup: condition: service_healthy - image: docker.elastic.co/elasticsearch/elasticsearch:8.19.15 + image: docker.elastic.co/elasticsearch/elasticsearch:9.4.2 volumes: - certs:/usr/share/elasticsearch/config/certs - esdata:/usr/share/elasticsearch/data @@ -98,7 +98,7 @@ services: depends_on: elasticsearch: condition: service_healthy - image: docker.elastic.co/kibana/kibana:8.19.15 + image: docker.elastic.co/kibana/kibana:9.4.2 volumes: - certs:/usr/share/kibana/config/certs ports: @@ -124,7 +124,7 @@ services: depends_on: elasticsearch: condition: service_healthy - image: docker.elastic.co/apm/apm-server:8.19.15 + image: docker.elastic.co/apm/apm-server:9.4.2 volumes: - certs:/usr/share/apm-server/certs ports: diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index a18d81b4cf..dd24a0dbbf 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -50,7 +50,7 @@ services: - appdata:/app/storage elasticsearch: - image: exceptionless/elasticsearch:8.19.15 + image: exceptionless/elasticsearch:9.4.2 environment: discovery.type: single-node xpack.security.enabled: "false" @@ -74,7 +74,7 @@ services: kibana: depends_on: - elasticsearch - image: docker.elastic.co/kibana/kibana:8.19.15 + image: docker.elastic.co/kibana/kibana:9.4.2 ports: - 5601:5601 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index c81594d41f..0c3db36ba7 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,6 +1,6 @@ services: elasticsearch: - image: exceptionless/elasticsearch:8.19.15 + image: exceptionless/elasticsearch:9.4.2 environment: node.name: elasticsearch cluster.name: exceptionless @@ -26,7 +26,7 @@ services: kibana: depends_on: - elasticsearch - image: docker.elastic.co/kibana/kibana:8.19.15 + image: docker.elastic.co/kibana/kibana:9.4.2 environment: xpack.security.enabled: "false" ports: diff --git a/k8s/elastic-monitor.yaml b/k8s/elastic-monitor.yaml index 943cde867d..c64e2a25b9 100644 --- a/k8s/elastic-monitor.yaml +++ b/k8s/elastic-monitor.yaml @@ -4,7 +4,7 @@ metadata: name: elastic-monitor namespace: elastic-system spec: - version: 8.19.15 + version: 9.4.2 podDisruptionBudget: {} nodeSets: - name: main @@ -228,7 +228,7 @@ metadata: name: kibana-monitor namespace: elastic-system spec: - version: 8.19.15 + version: 9.4.2 count: 1 http: tls: @@ -364,7 +364,7 @@ metadata: name: fleet-server namespace: elastic-system spec: - version: 8.19.15 + version: 9.4.2 kibanaRef: name: kibana-monitor elasticsearchRefs: @@ -388,7 +388,7 @@ metadata: name: elastic-agent namespace: elastic-system spec: - version: 8.19.15 + version: 9.4.2 kibanaRef: name: kibana-monitor fleetServerRef: diff --git a/k8s/ex-dev-elasticsearch.yaml b/k8s/ex-dev-elasticsearch.yaml index 46f14cc5eb..c93ffa74ed 100644 --- a/k8s/ex-dev-elasticsearch.yaml +++ b/k8s/ex-dev-elasticsearch.yaml @@ -14,8 +14,8 @@ metadata: name: ex-dev namespace: ex-dev spec: - version: 8.19.15 - image: exceptionless/elasticsearch:8.19.15 # https://github.com/exceptionless/Exceptionless/tree/main/build/docker/elasticsearch + version: 9.4.2 + image: exceptionless/elasticsearch:9.4.2 # https://github.com/exceptionless/Exceptionless/tree/main/build/docker/elasticsearch secureSettings: - secretName: ex-dev-snapshots http: @@ -68,7 +68,7 @@ metadata: name: ex-dev namespace: ex-dev spec: - version: 8.19.15 + version: 9.4.2 count: 1 elasticsearchRef: name: ex-dev diff --git a/k8s/ex-prod-elasticsearch.yaml b/k8s/ex-prod-elasticsearch.yaml index f159ae3046..65df870c7d 100644 --- a/k8s/ex-prod-elasticsearch.yaml +++ b/k8s/ex-prod-elasticsearch.yaml @@ -14,8 +14,8 @@ metadata: name: ex-prod namespace: ex-prod spec: - version: 8.19.15 - image: exceptionless/elasticsearch:8.19.15 # https://github.com/exceptionless/Exceptionless/tree/main/build/docker/elasticsearch + version: 9.4.2 + image: exceptionless/elasticsearch:9.4.2 # https://github.com/exceptionless/Exceptionless/tree/main/build/docker/elasticsearch monitoring: metrics: elasticsearchRefs: @@ -79,7 +79,7 @@ metadata: name: ex-prod namespace: ex-prod spec: - version: 8.19.15 + version: 9.4.2 count: 1 elasticsearchRef: name: ex-prod diff --git a/k8s/exceptionless/values.yaml b/k8s/exceptionless/values.yaml index 9b9febe39b..fea9395b89 100644 --- a/k8s/exceptionless/values.yaml +++ b/k8s/exceptionless/values.yaml @@ -35,7 +35,7 @@ elasticsearch: connectionString: image: repository: exceptionless/elasticsearch - tag: 8.19.15 + tag: 9.4.2 pullPolicy: IfNotPresent redis: diff --git a/samples/docker-compose.all-in-one.yml b/samples/docker-compose.all-in-one.yml index 5b1caf2d42..573d41f74d 100644 --- a/samples/docker-compose.all-in-one.yml +++ b/samples/docker-compose.all-in-one.yml @@ -20,7 +20,7 @@ services: kibana: depends_on: - elasticsearch - image: docker.elastic.co/kibana/kibana:8.19.15 + image: docker.elastic.co/kibana/kibana:9.4.2 ports: - 5601:5601 diff --git a/samples/docker-compose.yml b/samples/docker-compose.yml index d73e0518c9..4991f36e34 100644 --- a/samples/docker-compose.yml +++ b/samples/docker-compose.yml @@ -44,7 +44,7 @@ services: - ex_appdata:/app/storage elasticsearch: - image: exceptionless/elasticsearch:8.19.15 + image: exceptionless/elasticsearch:9.4.2 environment: discovery.type: single-node xpack.security.enabled: "false" @@ -58,7 +58,7 @@ services: kibana: depends_on: - elasticsearch - image: docker.elastic.co/kibana/kibana:8.19.15 + image: docker.elastic.co/kibana/kibana:9.4.2 ports: - 5601:5601 diff --git a/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs b/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs index 4f6cc8564c..f7584f2843 100644 --- a/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs +++ b/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs @@ -13,7 +13,7 @@ public static class ElasticsearchBuilderExtensions private const int KibanaPort = 5601; /// - /// Adds a Elasticsearch container to the application model. The default image is "docker.elastic.co/elasticsearch/elasticsearch". This version the package defaults to the 8.19.15 tag of the Elasticsearch container image + /// Adds a Elasticsearch container to the application model. The default image is "docker.elastic.co/elasticsearch/elasticsearch". This version the package defaults to the 9.4.2 tag of the Elasticsearch container image /// /// The . /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. @@ -125,7 +125,7 @@ internal static class ElasticsearchContainerImageTags public const string Image = "exceptionless/elasticsearch"; public const string KibanaRegistry = "docker.elastic.co"; public const string KibanaImage = "kibana/kibana"; - public const string Tag = "8.19.15"; + public const string Tag = "9.4.2"; } internal sealed class ElasticsearchConnectionHealthCheck(Func connectionStringFactory) : IHealthCheck From aa3c59c52fd0ebb84c70fc2686eb05dc6ce50b2a Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Tue, 28 Jul 2026 19:40:17 -0500 Subject: [PATCH 03/12] Address Elasticsearch upgrade review feedback --- .github/workflows/elasticsearch-docker-9.yml | 8 ++++++- .../Extensions/ElasticsearchExtensions.cs | 12 ++++++++--- .../Exceptionless.Tests/AppWebHostFactory.cs | 21 ++++++++++++++++++- .../AppWebHostFactoryTests.cs | 18 ++++++++++++++++ 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/.github/workflows/elasticsearch-docker-9.yml b/.github/workflows/elasticsearch-docker-9.yml index a16bd76f4b..b8c90d86ed 100644 --- a/.github/workflows/elasticsearch-docker-9.yml +++ b/.github/workflows/elasticsearch-docker-9.yml @@ -41,6 +41,12 @@ jobs: platforms: linux/amd64,linux/arm64 - name: Build custom Elasticsearch 9.x docker image working-directory: build/docker/elasticsearch/9.x + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | VERSION=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' Dockerfile) - docker buildx build --platform linux/amd64,linux/arm64 --output "type=image,push=true" --file ./Dockerfile . --tag exceptionless/elasticsearch:$VERSION --tag exceptionless/elasticsearch:latest + TAGS=(--tag "exceptionless/elasticsearch:$VERSION") + if [[ "$GITHUB_REF" == "refs/heads/$DEFAULT_BRANCH" ]]; then + TAGS+=(--tag "exceptionless/elasticsearch:latest") + fi + docker buildx build --platform linux/amd64,linux/arm64 --output "type=image,push=true" --file ./Dockerfile . "${TAGS[@]}" diff --git a/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs b/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs index f7584f2843..b96df31f93 100644 --- a/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs +++ b/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs @@ -141,8 +141,14 @@ public async Task CheckHealthAsync(HealthCheckContext context var response = await client.Cluster.HealthAsync( request => request.WaitForStatus(Elastic.Clients.Elasticsearch.HealthStatus.Yellow), cancellationToken); - return response.IsValidResponse - ? HealthCheckResult.Healthy() - : new HealthCheckResult(context.Registration.FailureStatus, $"Elasticsearch cluster health check failed: {response.DebugInformation}"); + bool isReady = response.IsValidResponse + && !response.TimedOut + && response.Status is Elastic.Clients.Elasticsearch.HealthStatus.Yellow or Elastic.Clients.Elasticsearch.HealthStatus.Green; + if (isReady) + return HealthCheckResult.Healthy(); + + return new HealthCheckResult( + context.Registration.FailureStatus, + $"Elasticsearch cluster health check failed. Timed out: {response.TimedOut}; status: {response.Status}. {response.DebugInformation}"); } } diff --git a/tests/Exceptionless.Tests/AppWebHostFactory.cs b/tests/Exceptionless.Tests/AppWebHostFactory.cs index b80ebcc79a..8b8c0c4f5b 100644 --- a/tests/Exceptionless.Tests/AppWebHostFactory.cs +++ b/tests/Exceptionless.Tests/AppWebHostFactory.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Net; +using System.Text.Json; using Aspire.Hosting; using Aspire.Hosting.Testing; using Exceptionless.Core; @@ -82,12 +83,16 @@ private static async Task WaitForElasticsearchAsync(Uri elasticsearchUri) try { using var response = await client.GetAsync(healthUri); - if (response.StatusCode == HttpStatusCode.OK) + using var document = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); + if (IsElasticsearchReady(response.StatusCode, document.RootElement)) return; } catch (HttpRequestException) { } + catch (JsonException) + { + } catch (TaskCanceledException) { } @@ -98,6 +103,20 @@ private static async Task WaitForElasticsearchAsync(Uri elasticsearchUri) throw new TimeoutException("Timed out waiting for the shared Elasticsearch container to be ready."); } + internal static bool IsElasticsearchReady(HttpStatusCode statusCode, JsonElement health) + { + if (statusCode != HttpStatusCode.OK) + return false; + + bool requestCompleted = health.TryGetProperty("timed_out", out var timedOut) + && timedOut.ValueKind == JsonValueKind.False; + bool clusterReady = health.TryGetProperty("status", out var status) + && status.ValueKind == JsonValueKind.String + && status.GetString() is "yellow" or "green"; + + return requestCompleted && clusterReady; + } + protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment(Environments.Development); diff --git a/tests/Exceptionless.Tests/AppWebHostFactoryTests.cs b/tests/Exceptionless.Tests/AppWebHostFactoryTests.cs index 7fef487faa..539785b681 100644 --- a/tests/Exceptionless.Tests/AppWebHostFactoryTests.cs +++ b/tests/Exceptionless.Tests/AppWebHostFactoryTests.cs @@ -1,4 +1,6 @@ +using System.Net; using System.Text; +using System.Text.Json; using Foundatio.Storage; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -7,6 +9,22 @@ namespace Exceptionless.Tests; public sealed class AppWebHostFactoryTests { + [Theory] + [InlineData(HttpStatusCode.OK, """{"timed_out":false,"status":"yellow"}""", true)] + [InlineData(HttpStatusCode.OK, """{"timed_out":false,"status":"green"}""", true)] + [InlineData(HttpStatusCode.OK, """{"timed_out":true,"status":"yellow"}""", false)] + [InlineData(HttpStatusCode.OK, """{"timed_out":false,"status":"red"}""", false)] + [InlineData(HttpStatusCode.OK, """{"timed_out":false,"status":1}""", false)] + [InlineData(HttpStatusCode.ServiceUnavailable, """{"timed_out":false,"status":"yellow"}""", false)] + public void IsElasticsearchReady_ClusterHealthResponse_ReturnsExpectedResult(HttpStatusCode statusCode, string json, bool expected) + { + using var document = JsonDocument.Parse(json); + + bool isReady = AppWebHostFactory.IsElasticsearchReady(statusCode, document.RootElement); + + Assert.Equal(expected, isReady); + } + [Fact] public async Task ConfigureWebHost_MultipleFactories_IsolatesFileStorageByAppScope() { From 1e1e8576002e807ae4c312467365f9f93995242f Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Tue, 28 Jul 2026 20:00:11 -0500 Subject: [PATCH 04/12] Protect Elasticsearch release image tags --- .github/workflows/elasticsearch-docker-9.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/elasticsearch-docker-9.yml b/.github/workflows/elasticsearch-docker-9.yml index b8c90d86ed..db5910edcf 100644 --- a/.github/workflows/elasticsearch-docker-9.yml +++ b/.github/workflows/elasticsearch-docker-9.yml @@ -45,8 +45,9 @@ jobs: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | VERSION=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' Dockerfile) - TAGS=(--tag "exceptionless/elasticsearch:$VERSION") if [[ "$GITHUB_REF" == "refs/heads/$DEFAULT_BRANCH" ]]; then - TAGS+=(--tag "exceptionless/elasticsearch:latest") + TAGS=(--tag "exceptionless/elasticsearch:$VERSION" --tag "exceptionless/elasticsearch:latest") + else + TAGS=(--tag "exceptionless/elasticsearch:$VERSION-$GITHUB_SHA") fi docker buildx build --platform linux/amd64,linux/arm64 --output "type=image,push=true" --file ./Dockerfile . "${TAGS[@]}" From d75931504ed5e31b8e6c1f5a26fabdb7232bb82e Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Tue, 28 Jul 2026 20:15:54 -0500 Subject: [PATCH 05/12] chore: update Elasticsearch stack to 9.4.4 --- Dockerfile | 2 +- build/docker/elasticsearch/9.x/Dockerfile | 2 +- docker/docker-compose.apm.yml | 8 ++++---- docker/docker-compose.dev.yml | 4 ++-- docker/docker-compose.yml | 4 ++-- k8s/elastic-monitor.yaml | 8 ++++---- k8s/ex-dev-elasticsearch.yaml | 6 +++--- k8s/ex-prod-elasticsearch.yaml | 6 +++--- k8s/exceptionless/values.yaml | 2 +- samples/docker-compose.all-in-one.yml | 2 +- samples/docker-compose.yml | 4 ++-- .../Extensions/ElasticsearchExtensions.cs | 4 ++-- 12 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Dockerfile b/Dockerfile index 37f4570fb3..e7a343ffc9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -100,7 +100,7 @@ ENTRYPOINT ["/app/app-docker-entrypoint.sh"] # completely self-contained -FROM exceptionless/elasticsearch:9.4.2 AS exceptionless +FROM exceptionless/elasticsearch:9.4.4 AS exceptionless WORKDIR /app COPY --from=job-publish /app/src/Exceptionless.Job/out ./ diff --git a/build/docker/elasticsearch/9.x/Dockerfile b/build/docker/elasticsearch/9.x/Dockerfile index 8658600712..e27831a5c2 100644 --- a/build/docker/elasticsearch/9.x/Dockerfile +++ b/build/docker/elasticsearch/9.x/Dockerfile @@ -1,4 +1,4 @@ # https://www.docker.elastic.co/ -FROM docker.elastic.co/elasticsearch/elasticsearch:9.4.2 +FROM docker.elastic.co/elasticsearch/elasticsearch:9.4.4 RUN elasticsearch-plugin install -b mapper-size diff --git a/docker/docker-compose.apm.yml b/docker/docker-compose.apm.yml index 9862cacd7d..b797b60a68 100644 --- a/docker/docker-compose.apm.yml +++ b/docker/docker-compose.apm.yml @@ -2,7 +2,7 @@ version: "2.2" services: setup: - image: docker.elastic.co/elasticsearch/elasticsearch:9.4.2 + image: docker.elastic.co/elasticsearch/elasticsearch:9.4.4 volumes: - certs:/usr/share/elasticsearch/config/certs user: "0" @@ -53,7 +53,7 @@ services: depends_on: setup: condition: service_healthy - image: docker.elastic.co/elasticsearch/elasticsearch:9.4.2 + image: docker.elastic.co/elasticsearch/elasticsearch:9.4.4 volumes: - certs:/usr/share/elasticsearch/config/certs - esdata:/usr/share/elasticsearch/data @@ -98,7 +98,7 @@ services: depends_on: elasticsearch: condition: service_healthy - image: docker.elastic.co/kibana/kibana:9.4.2 + image: docker.elastic.co/kibana/kibana:9.4.4 volumes: - certs:/usr/share/kibana/config/certs ports: @@ -124,7 +124,7 @@ services: depends_on: elasticsearch: condition: service_healthy - image: docker.elastic.co/apm/apm-server:9.4.2 + image: docker.elastic.co/apm/apm-server:9.4.4 volumes: - certs:/usr/share/apm-server/certs ports: diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index dd24a0dbbf..604325d68c 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -50,7 +50,7 @@ services: - appdata:/app/storage elasticsearch: - image: exceptionless/elasticsearch:9.4.2 + image: exceptionless/elasticsearch:9.4.4 environment: discovery.type: single-node xpack.security.enabled: "false" @@ -74,7 +74,7 @@ services: kibana: depends_on: - elasticsearch - image: docker.elastic.co/kibana/kibana:9.4.2 + image: docker.elastic.co/kibana/kibana:9.4.4 ports: - 5601:5601 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 0c3db36ba7..417e3751b5 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,6 +1,6 @@ services: elasticsearch: - image: exceptionless/elasticsearch:9.4.2 + image: exceptionless/elasticsearch:9.4.4 environment: node.name: elasticsearch cluster.name: exceptionless @@ -26,7 +26,7 @@ services: kibana: depends_on: - elasticsearch - image: docker.elastic.co/kibana/kibana:9.4.2 + image: docker.elastic.co/kibana/kibana:9.4.4 environment: xpack.security.enabled: "false" ports: diff --git a/k8s/elastic-monitor.yaml b/k8s/elastic-monitor.yaml index c64e2a25b9..eb409fcc8a 100644 --- a/k8s/elastic-monitor.yaml +++ b/k8s/elastic-monitor.yaml @@ -4,7 +4,7 @@ metadata: name: elastic-monitor namespace: elastic-system spec: - version: 9.4.2 + version: 9.4.4 podDisruptionBudget: {} nodeSets: - name: main @@ -228,7 +228,7 @@ metadata: name: kibana-monitor namespace: elastic-system spec: - version: 9.4.2 + version: 9.4.4 count: 1 http: tls: @@ -364,7 +364,7 @@ metadata: name: fleet-server namespace: elastic-system spec: - version: 9.4.2 + version: 9.4.4 kibanaRef: name: kibana-monitor elasticsearchRefs: @@ -388,7 +388,7 @@ metadata: name: elastic-agent namespace: elastic-system spec: - version: 9.4.2 + version: 9.4.4 kibanaRef: name: kibana-monitor fleetServerRef: diff --git a/k8s/ex-dev-elasticsearch.yaml b/k8s/ex-dev-elasticsearch.yaml index c93ffa74ed..ee61778a6f 100644 --- a/k8s/ex-dev-elasticsearch.yaml +++ b/k8s/ex-dev-elasticsearch.yaml @@ -14,8 +14,8 @@ metadata: name: ex-dev namespace: ex-dev spec: - version: 9.4.2 - image: exceptionless/elasticsearch:9.4.2 # https://github.com/exceptionless/Exceptionless/tree/main/build/docker/elasticsearch + version: 9.4.4 + image: exceptionless/elasticsearch:9.4.4 # https://github.com/exceptionless/Exceptionless/tree/main/build/docker/elasticsearch secureSettings: - secretName: ex-dev-snapshots http: @@ -68,7 +68,7 @@ metadata: name: ex-dev namespace: ex-dev spec: - version: 9.4.2 + version: 9.4.4 count: 1 elasticsearchRef: name: ex-dev diff --git a/k8s/ex-prod-elasticsearch.yaml b/k8s/ex-prod-elasticsearch.yaml index 65df870c7d..88457e15d1 100644 --- a/k8s/ex-prod-elasticsearch.yaml +++ b/k8s/ex-prod-elasticsearch.yaml @@ -14,8 +14,8 @@ metadata: name: ex-prod namespace: ex-prod spec: - version: 9.4.2 - image: exceptionless/elasticsearch:9.4.2 # https://github.com/exceptionless/Exceptionless/tree/main/build/docker/elasticsearch + version: 9.4.4 + image: exceptionless/elasticsearch:9.4.4 # https://github.com/exceptionless/Exceptionless/tree/main/build/docker/elasticsearch monitoring: metrics: elasticsearchRefs: @@ -79,7 +79,7 @@ metadata: name: ex-prod namespace: ex-prod spec: - version: 9.4.2 + version: 9.4.4 count: 1 elasticsearchRef: name: ex-prod diff --git a/k8s/exceptionless/values.yaml b/k8s/exceptionless/values.yaml index fea9395b89..8880328d4d 100644 --- a/k8s/exceptionless/values.yaml +++ b/k8s/exceptionless/values.yaml @@ -35,7 +35,7 @@ elasticsearch: connectionString: image: repository: exceptionless/elasticsearch - tag: 9.4.2 + tag: 9.4.4 pullPolicy: IfNotPresent redis: diff --git a/samples/docker-compose.all-in-one.yml b/samples/docker-compose.all-in-one.yml index 573d41f74d..0560b023cf 100644 --- a/samples/docker-compose.all-in-one.yml +++ b/samples/docker-compose.all-in-one.yml @@ -20,7 +20,7 @@ services: kibana: depends_on: - elasticsearch - image: docker.elastic.co/kibana/kibana:9.4.2 + image: docker.elastic.co/kibana/kibana:9.4.4 ports: - 5601:5601 diff --git a/samples/docker-compose.yml b/samples/docker-compose.yml index 4991f36e34..1d57db836e 100644 --- a/samples/docker-compose.yml +++ b/samples/docker-compose.yml @@ -44,7 +44,7 @@ services: - ex_appdata:/app/storage elasticsearch: - image: exceptionless/elasticsearch:9.4.2 + image: exceptionless/elasticsearch:9.4.4 environment: discovery.type: single-node xpack.security.enabled: "false" @@ -58,7 +58,7 @@ services: kibana: depends_on: - elasticsearch - image: docker.elastic.co/kibana/kibana:9.4.2 + image: docker.elastic.co/kibana/kibana:9.4.4 ports: - 5601:5601 diff --git a/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs b/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs index b96df31f93..e348e42e74 100644 --- a/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs +++ b/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs @@ -13,7 +13,7 @@ public static class ElasticsearchBuilderExtensions private const int KibanaPort = 5601; /// - /// Adds a Elasticsearch container to the application model. The default image is "docker.elastic.co/elasticsearch/elasticsearch". This version the package defaults to the 9.4.2 tag of the Elasticsearch container image + /// Adds a Elasticsearch container to the application model. The default image is "docker.elastic.co/elasticsearch/elasticsearch". This version the package defaults to the 9.4.4 tag of the Elasticsearch container image /// /// The . /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. @@ -125,7 +125,7 @@ internal static class ElasticsearchContainerImageTags public const string Image = "exceptionless/elasticsearch"; public const string KibanaRegistry = "docker.elastic.co"; public const string KibanaImage = "kibana/kibana"; - public const string Tag = "9.4.2"; + public const string Tag = "9.4.4"; } internal sealed class ElasticsearchConnectionHealthCheck(Func connectionStringFactory) : IHealthCheck From 74ab19f674186b989d8bf7f3479734da7deeb51e Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Tue, 28 Jul 2026 20:34:33 -0500 Subject: [PATCH 06/12] Test Elasticsearch branch images safely --- .github/workflows/build.yaml | 38 ++++++++++++++++++++ .github/workflows/elasticsearch-docker-9.yml | 3 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b548f1f58d..7ed7563452 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -83,6 +83,7 @@ jobs: timeout-minutes: 30 outputs: version: ${{ steps.version.outputs.version }} + elasticsearch_image_tag: ${{ steps.elasticsearch_image.outputs.tag }} should_publish: ${{ (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'dev-preview')) && secrets.DOCKER_USERNAME != '' && secrets.DOCKER_PASSWORD != '' }} is_prod_deploy: ${{ startsWith(github.ref, 'refs/tags/v') && github.event_name != 'pull_request' }} is_dev_deploy: ${{ (github.event_name == 'repository_dispatch' && github.event.action == 'preview') || (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'dev-preview')) }} @@ -129,9 +130,43 @@ jobs: echo "version=$version" >> $GITHUB_OUTPUT echo "### $version" >> $GITHUB_STEP_SUMMARY + - name: Resolve Elasticsearch image + id: elasticsearch_image + env: + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + version=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' build/docker/elasticsearch/9.x/Dockerfile) + tag=$version + + if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]] && + ! git diff --quiet "$PR_BASE_SHA"...HEAD -- build/docker/elasticsearch/9.x .github/workflows/elasticsearch-docker-9.yml; then + image_sha=$(sha256sum build/docker/elasticsearch/9.x/Dockerfile | cut -d " " -f 1) + tag="$version-sha256-$image_sha" + image="exceptionless/elasticsearch:$tag" + + for attempt in {1..30}; do + if docker manifest inspect "$image" > /dev/null 2>&1; then + break + fi + + if [[ "$attempt" -eq 30 ]]; then + echo "::error::Timed out waiting for $image to be published." + exit 1 + fi + + sleep 10 + done + fi + + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "### Elasticsearch image: exceptionless/elasticsearch:$tag" >> "$GITHUB_STEP_SUMMARY" + test-api: + needs: version runs-on: ubuntu-latest timeout-minutes: 30 + env: + Elasticsearch__ImageTag: ${{ needs.version.outputs.elasticsearch_image_tag }} steps: - name: Checkout @@ -223,8 +258,11 @@ jobs: run: echo "npm run test:integration" test-e2e: + needs: version runs-on: ubuntu-latest timeout-minutes: 45 + env: + Elasticsearch__ImageTag: ${{ needs.version.outputs.elasticsearch_image_tag }} steps: - name: Checkout diff --git a/.github/workflows/elasticsearch-docker-9.yml b/.github/workflows/elasticsearch-docker-9.yml index db5910edcf..fe9a697c63 100644 --- a/.github/workflows/elasticsearch-docker-9.yml +++ b/.github/workflows/elasticsearch-docker-9.yml @@ -48,6 +48,7 @@ jobs: if [[ "$GITHUB_REF" == "refs/heads/$DEFAULT_BRANCH" ]]; then TAGS=(--tag "exceptionless/elasticsearch:$VERSION" --tag "exceptionless/elasticsearch:latest") else - TAGS=(--tag "exceptionless/elasticsearch:$VERSION-$GITHUB_SHA") + IMAGE_SHA=$(sha256sum Dockerfile | cut -d " " -f 1) + TAGS=(--tag "exceptionless/elasticsearch:$VERSION-sha256-$IMAGE_SHA") fi docker buildx build --platform linux/amd64,linux/arm64 --output "type=image,push=true" --file ./Dockerfile . "${TAGS[@]}" From 1cfba7658596e1c276818b48e595f31bc5f6884b Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Tue, 28 Jul 2026 21:00:30 -0500 Subject: [PATCH 07/12] Address Elasticsearch upgrade migration feedback --- .github/workflows/build.yaml | 9 +++++ .github/workflows/elasticsearch-docker-9.yml | 7 ++-- docker/docker-compose.dev.yml | 2 ++ docker/docker-compose.yml | 2 ++ .../upgrading-self-hosted-instance.md | 35 +++++++++++++++++++ 5 files changed, 51 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7ed7563452..4be8ce4ea2 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -134,12 +134,21 @@ jobs: id: elasticsearch_image env: PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PUSH_BEFORE_SHA: ${{ github.event.before }} run: | version=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' build/docker/elasticsearch/9.x/Dockerfile) tag=$version + image_changed=false if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]] && ! git diff --quiet "$PR_BASE_SHA"...HEAD -- build/docker/elasticsearch/9.x .github/workflows/elasticsearch-docker-9.yml; then + image_changed=true + elif [[ "$GITHUB_EVENT_NAME" == "push" && "$GITHUB_REF" == "refs/heads/main" ]] && + ! git diff --quiet "$PUSH_BEFORE_SHA"..HEAD -- build/docker/elasticsearch/9.x .github/workflows/elasticsearch-docker-9.yml; then + image_changed=true + fi + + if [[ "$image_changed" == "true" ]]; then image_sha=$(sha256sum build/docker/elasticsearch/9.x/Dockerfile | cut -d " " -f 1) tag="$version-sha256-$image_sha" image="exceptionless/elasticsearch:$tag" diff --git a/.github/workflows/elasticsearch-docker-9.yml b/.github/workflows/elasticsearch-docker-9.yml index fe9a697c63..6fe30d0bb5 100644 --- a/.github/workflows/elasticsearch-docker-9.yml +++ b/.github/workflows/elasticsearch-docker-9.yml @@ -45,10 +45,9 @@ jobs: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | VERSION=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' Dockerfile) + IMAGE_SHA=$(sha256sum Dockerfile | cut -d " " -f 1) + TAGS=(--tag "exceptionless/elasticsearch:$VERSION-sha256-$IMAGE_SHA") if [[ "$GITHUB_REF" == "refs/heads/$DEFAULT_BRANCH" ]]; then - TAGS=(--tag "exceptionless/elasticsearch:$VERSION" --tag "exceptionless/elasticsearch:latest") - else - IMAGE_SHA=$(sha256sum Dockerfile | cut -d " " -f 1) - TAGS=(--tag "exceptionless/elasticsearch:$VERSION-sha256-$IMAGE_SHA") + TAGS+=(--tag "exceptionless/elasticsearch:$VERSION" --tag "exceptionless/elasticsearch:latest") fi docker buildx build --platform linux/amd64,linux/arm64 --output "type=image,push=true" --file ./Dockerfile . "${TAGS[@]}" diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 604325d68c..6752cf1a07 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -59,6 +59,8 @@ services: - 9200:9200 - 9300:9300 volumes: + # Complete the Elasticsearch 8.19 upgrade preflight before reusing this volume with Elasticsearch 9. + # See https://exceptionless.com/docs/self-hosting/upgrading-self-hosted-instance - esdata7:/usr/share/elasticsearch/data healthcheck: test: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 417e3751b5..11b3f2a705 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -11,6 +11,8 @@ services: ports: - 9200:9200 volumes: + # Complete the Elasticsearch 8.19 upgrade preflight before reusing this volume with Elasticsearch 9. + # See https://exceptionless.com/docs/self-hosting/upgrading-self-hosted-instance - esdata:/usr/share/elasticsearch/data healthcheck: test: diff --git a/docs/docs/self-hosting/upgrading-self-hosted-instance.md b/docs/docs/self-hosting/upgrading-self-hosted-instance.md index dd0974989f..2335c9e421 100644 --- a/docs/docs/self-hosting/upgrading-self-hosted-instance.md +++ b/docs/docs/self-hosting/upgrading-self-hosted-instance.md @@ -8,6 +8,41 @@ title: "Upgrading" **If you are upgrading from v1 or [v2](https://github.com/exceptionless/Exceptionless/releases/tag/v2.0.0) you will need to upgrade to [v3.0](https://github.com/exceptionless/Exceptionless/releases/tag/v3.0.0) before upgrading to the latest release.** +## Upgrading from v8 to v9 + +Exceptionless v9 uses Elasticsearch 9. Do not point an Elasticsearch 9 node at an existing data volume until the cluster has been prepared with Elasticsearch 8.19. Elasticsearch 9 can fail to start when incompatible indices created before Elasticsearch 8 remain. + +Use this upgrade path for an existing self-hosted installation: + +1. Take a current Elasticsearch snapshot or other verified backup and test that it can be restored. Elasticsearch does not support downgrading a data directory after it has been upgraded. +2. Upgrade Elasticsearch and Kibana to the latest 8.19.x patch release first, using the existing data volume. Do not start Elasticsearch 9 yet. +3. Stop the Exceptionless app and job services, but leave Elasticsearch and Kibana 8.19 running. This prevents writes while legacy indices are reindexed. +4. Open Kibana's **Upgrade Assistant** and resolve every critical issue. Reindex every active Exceptionless index created before Elasticsearch 8. Delete only indices you have confirmed are no longer needed; do not mark active Exceptionless indices as read-only. +5. If this data volume previously ran Elasticsearch 7, temporarily disable the GeoIP downloader while still on Elasticsearch 8.19. Elasticsearch deletes its downloaded `.geoip_databases` system index when this setting is disabled; Exceptionless data is not affected. + + ```bash + curl -fsS -X PUT "http://localhost:9200/_cluster/settings" \ + -H "Content-Type: application/json" \ + -d '{"persistent":{"ingest.geoip.downloader.enabled":false}}' + ``` + +6. Confirm that the deprecation API reports no critical issues: + + ```bash + curl -fsS "http://localhost:9200/_migration/deprecations?pretty" + ``` + +7. Stop Elasticsearch and Kibana 8.19 without deleting their data volume. Update the Elasticsearch and Kibana images to the v9 versions, start them, and verify cluster health before restarting the Exceptionless app and jobs. In Docker Compose, do not run `docker compose down -v` because `-v` deletes the data volume. +8. After Elasticsearch 9 is healthy, restore the default GeoIP downloader behavior: + + ```bash + curl -fsS -X PUT "http://localhost:9200/_cluster/settings" \ + -H "Content-Type: application/json" \ + -d '{"persistent":{"ingest.geoip.downloader.enabled":null}}' + ``` + +See Elastic's [prepare-to-upgrade guide](https://www.elastic.co/docs/deploy-manage/upgrade/prepare-to-upgrade) for the supported 8.x to 9.x upgrade requirements and Upgrade Assistant details. + ## Upgrading from v7.1 to v8 We simplified the self hosting process by integrating the UI into the existing app images. As such `exceptionless/ui` docker images are deprecated and we recommend using `exceptionless/app`. From 9a73c3b08de9771fb62a99b8a469f1179f3381bf Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Tue, 28 Jul 2026 21:21:54 -0500 Subject: [PATCH 08/12] Fix Elasticsearch candidate image isolation --- .github/workflows/build.yaml | 2 +- .github/workflows/elasticsearch-docker-9.yml | 7 ++--- src/Exceptionless.AppHost/Program.cs | 3 +- .../AppHostConfigurationTests.cs | 30 +++++++++++++++++++ 4 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 tests/Exceptionless.Tests/AppHostConfigurationTests.cs diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4be8ce4ea2..e1aaf50209 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -149,7 +149,7 @@ jobs: fi if [[ "$image_changed" == "true" ]]; then - image_sha=$(sha256sum build/docker/elasticsearch/9.x/Dockerfile | cut -d " " -f 1) + image_sha=$(git ls-files -- build/docker/elasticsearch/9.x .github/workflows/elasticsearch-docker-9.yml | sort | xargs sha256sum | sha256sum | cut -d " " -f 1) tag="$version-sha256-$image_sha" image="exceptionless/elasticsearch:$tag" diff --git a/.github/workflows/elasticsearch-docker-9.yml b/.github/workflows/elasticsearch-docker-9.yml index 6fe30d0bb5..a911616691 100644 --- a/.github/workflows/elasticsearch-docker-9.yml +++ b/.github/workflows/elasticsearch-docker-9.yml @@ -40,14 +40,13 @@ jobs: with: platforms: linux/amd64,linux/arm64 - name: Build custom Elasticsearch 9.x docker image - working-directory: build/docker/elasticsearch/9.x env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | - VERSION=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' Dockerfile) - IMAGE_SHA=$(sha256sum Dockerfile | cut -d " " -f 1) + VERSION=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' build/docker/elasticsearch/9.x/Dockerfile) + IMAGE_SHA=$(git ls-files -- build/docker/elasticsearch/9.x .github/workflows/elasticsearch-docker-9.yml | sort | xargs sha256sum | sha256sum | cut -d " " -f 1) TAGS=(--tag "exceptionless/elasticsearch:$VERSION-sha256-$IMAGE_SHA") if [[ "$GITHUB_REF" == "refs/heads/$DEFAULT_BRANCH" ]]; then TAGS+=(--tag "exceptionless/elasticsearch:$VERSION" --tag "exceptionless/elasticsearch:latest") fi - docker buildx build --platform linux/amd64,linux/arm64 --output "type=image,push=true" --file ./Dockerfile . "${TAGS[@]}" + docker buildx build --platform linux/amd64,linux/arm64 --output "type=image,push=true" --file build/docker/elasticsearch/9.x/Dockerfile build/docker/elasticsearch/9.x "${TAGS[@]}" diff --git a/src/Exceptionless.AppHost/Program.cs b/src/Exceptionless.AppHost/Program.cs index 1f972bcdf7..0917cd73d7 100644 --- a/src/Exceptionless.AppHost/Program.cs +++ b/src/Exceptionless.AppHost/Program.cs @@ -11,6 +11,7 @@ bool includeDevTools = !ciE2E; int elasticsearchPort = GetPort("Elasticsearch:Port", 9200); string elasticsearchImageTag = builder.Configuration["Elasticsearch:ImageTag"] ?? ElasticsearchContainerImageTags.Tag; +string kibanaImageTag = builder.Configuration["Elasticsearch:KibanaImageTag"] ?? ElasticsearchContainerImageTags.Tag; string elasticsearchContainerName = builder.Configuration["Elasticsearch:ContainerName"] ?? "Exceptionless-Elasticsearch"; string elasticsearchDataVolume = builder.Configuration["Elasticsearch:DataVolume"] ?? "exceptionless.data.v1"; string kibanaContainerName = builder.Configuration["Elasticsearch:KibanaContainerName"] ?? "Exceptionless-Kibana"; @@ -77,7 +78,7 @@ if (!servicesOnly && includeDevTools) { elastic = elastic.WithKibana(b => b - .WithImageTag(elasticsearchImageTag) + .WithImageTag(kibanaImageTag) .WithLifetime(ContainerLifetime.Persistent) .WithEndpointProxySupport(false) .WithContainerName(kibanaContainerName) diff --git a/tests/Exceptionless.Tests/AppHostConfigurationTests.cs b/tests/Exceptionless.Tests/AppHostConfigurationTests.cs new file mode 100644 index 0000000000..455dda2859 --- /dev/null +++ b/tests/Exceptionless.Tests/AppHostConfigurationTests.cs @@ -0,0 +1,30 @@ +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Testing; +using Xunit; + +namespace Exceptionless.Tests; + +public class AppHostConfigurationTests +{ + [Fact] + public async Task CreateAsync_WithSeparateElasticsearchAndKibanaOverrides_UsesIndependentImageTags() + { + const string elasticsearchImageTag = "9.4.4-sha256-candidate"; + const string kibanaImageTag = "9.4.4"; + var appHost = await DistributedApplicationTestingBuilder.CreateAsync( + [ + $"--Elasticsearch:ImageTag={elasticsearchImageTag}", + $"--Elasticsearch:KibanaImageTag={kibanaImageTag}" + ], + TestContext.Current.CancellationToken); + + var elasticsearch = Assert.Single(appHost.Resources.OfType()); + var kibana = Assert.Single(appHost.Resources.OfType()); + var elasticsearchImage = Assert.Single(elasticsearch.Annotations.OfType()); + var kibanaImage = Assert.Single(kibana.Annotations.OfType()); + + Assert.Equal(elasticsearchImageTag, elasticsearchImage.Tag); + Assert.Equal(kibanaImageTag, kibanaImage.Tag); + } +} From 1726e1d3405919da37e7c175a3d3e4aa277d08dd Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Tue, 28 Jul 2026 21:23:35 -0500 Subject: [PATCH 09/12] Wait for uncached Elasticsearch image builds --- .github/workflows/build.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e1aaf50209..a75a218da4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -153,12 +153,12 @@ jobs: tag="$version-sha256-$image_sha" image="exceptionless/elasticsearch:$tag" - for attempt in {1..30}; do + for attempt in {1..150}; do if docker manifest inspect "$image" > /dev/null 2>&1; then break fi - if [[ "$attempt" -eq 30 ]]; then + if [[ "$attempt" -eq 150 ]]; then echo "::error::Timed out waiting for $image to be published." exit 1 fi From 9c63bd7c7aa0da3ddf853ee0328711dc56797a91 Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Tue, 4 Aug 2026 08:59:49 -0500 Subject: [PATCH 10/12] chore: update Elasticsearch stack to 9.5.0 --- Dockerfile | 2 +- build/docker/elasticsearch/9.x/Dockerfile | 2 +- docker/docker-compose.apm.yml | 8 ++++---- docker/docker-compose.dev.yml | 4 ++-- docker/docker-compose.yml | 4 ++-- k8s/elastic-monitor.yaml | 8 ++++---- k8s/ex-dev-elasticsearch.yaml | 6 +++--- k8s/ex-prod-elasticsearch.yaml | 6 +++--- k8s/exceptionless/values.yaml | 2 +- samples/docker-compose.all-in-one.yml | 2 +- samples/docker-compose.yml | 4 ++-- .../Extensions/ElasticsearchExtensions.cs | 4 ++-- tests/Exceptionless.Tests/AppHostConfigurationTests.cs | 4 ++-- 13 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Dockerfile b/Dockerfile index e7a343ffc9..bc0a259ca0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -100,7 +100,7 @@ ENTRYPOINT ["/app/app-docker-entrypoint.sh"] # completely self-contained -FROM exceptionless/elasticsearch:9.4.4 AS exceptionless +FROM exceptionless/elasticsearch:9.5.0 AS exceptionless WORKDIR /app COPY --from=job-publish /app/src/Exceptionless.Job/out ./ diff --git a/build/docker/elasticsearch/9.x/Dockerfile b/build/docker/elasticsearch/9.x/Dockerfile index e27831a5c2..cc03ebce5a 100644 --- a/build/docker/elasticsearch/9.x/Dockerfile +++ b/build/docker/elasticsearch/9.x/Dockerfile @@ -1,4 +1,4 @@ # https://www.docker.elastic.co/ -FROM docker.elastic.co/elasticsearch/elasticsearch:9.4.4 +FROM docker.elastic.co/elasticsearch/elasticsearch:9.5.0 RUN elasticsearch-plugin install -b mapper-size diff --git a/docker/docker-compose.apm.yml b/docker/docker-compose.apm.yml index b797b60a68..220098afad 100644 --- a/docker/docker-compose.apm.yml +++ b/docker/docker-compose.apm.yml @@ -2,7 +2,7 @@ version: "2.2" services: setup: - image: docker.elastic.co/elasticsearch/elasticsearch:9.4.4 + image: docker.elastic.co/elasticsearch/elasticsearch:9.5.0 volumes: - certs:/usr/share/elasticsearch/config/certs user: "0" @@ -53,7 +53,7 @@ services: depends_on: setup: condition: service_healthy - image: docker.elastic.co/elasticsearch/elasticsearch:9.4.4 + image: docker.elastic.co/elasticsearch/elasticsearch:9.5.0 volumes: - certs:/usr/share/elasticsearch/config/certs - esdata:/usr/share/elasticsearch/data @@ -98,7 +98,7 @@ services: depends_on: elasticsearch: condition: service_healthy - image: docker.elastic.co/kibana/kibana:9.4.4 + image: docker.elastic.co/kibana/kibana:9.5.0 volumes: - certs:/usr/share/kibana/config/certs ports: @@ -124,7 +124,7 @@ services: depends_on: elasticsearch: condition: service_healthy - image: docker.elastic.co/apm/apm-server:9.4.4 + image: docker.elastic.co/apm/apm-server:9.5.0 volumes: - certs:/usr/share/apm-server/certs ports: diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 6752cf1a07..2b8518034b 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -50,7 +50,7 @@ services: - appdata:/app/storage elasticsearch: - image: exceptionless/elasticsearch:9.4.4 + image: exceptionless/elasticsearch:9.5.0 environment: discovery.type: single-node xpack.security.enabled: "false" @@ -76,7 +76,7 @@ services: kibana: depends_on: - elasticsearch - image: docker.elastic.co/kibana/kibana:9.4.4 + image: docker.elastic.co/kibana/kibana:9.5.0 ports: - 5601:5601 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 11b3f2a705..b1706174b1 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,6 +1,6 @@ services: elasticsearch: - image: exceptionless/elasticsearch:9.4.4 + image: exceptionless/elasticsearch:9.5.0 environment: node.name: elasticsearch cluster.name: exceptionless @@ -28,7 +28,7 @@ services: kibana: depends_on: - elasticsearch - image: docker.elastic.co/kibana/kibana:9.4.4 + image: docker.elastic.co/kibana/kibana:9.5.0 environment: xpack.security.enabled: "false" ports: diff --git a/k8s/elastic-monitor.yaml b/k8s/elastic-monitor.yaml index eb409fcc8a..f1c47ad253 100644 --- a/k8s/elastic-monitor.yaml +++ b/k8s/elastic-monitor.yaml @@ -4,7 +4,7 @@ metadata: name: elastic-monitor namespace: elastic-system spec: - version: 9.4.4 + version: 9.5.0 podDisruptionBudget: {} nodeSets: - name: main @@ -228,7 +228,7 @@ metadata: name: kibana-monitor namespace: elastic-system spec: - version: 9.4.4 + version: 9.5.0 count: 1 http: tls: @@ -364,7 +364,7 @@ metadata: name: fleet-server namespace: elastic-system spec: - version: 9.4.4 + version: 9.5.0 kibanaRef: name: kibana-monitor elasticsearchRefs: @@ -388,7 +388,7 @@ metadata: name: elastic-agent namespace: elastic-system spec: - version: 9.4.4 + version: 9.5.0 kibanaRef: name: kibana-monitor fleetServerRef: diff --git a/k8s/ex-dev-elasticsearch.yaml b/k8s/ex-dev-elasticsearch.yaml index ee61778a6f..6a8b1fd851 100644 --- a/k8s/ex-dev-elasticsearch.yaml +++ b/k8s/ex-dev-elasticsearch.yaml @@ -14,8 +14,8 @@ metadata: name: ex-dev namespace: ex-dev spec: - version: 9.4.4 - image: exceptionless/elasticsearch:9.4.4 # https://github.com/exceptionless/Exceptionless/tree/main/build/docker/elasticsearch + version: 9.5.0 + image: exceptionless/elasticsearch:9.5.0 # https://github.com/exceptionless/Exceptionless/tree/main/build/docker/elasticsearch secureSettings: - secretName: ex-dev-snapshots http: @@ -68,7 +68,7 @@ metadata: name: ex-dev namespace: ex-dev spec: - version: 9.4.4 + version: 9.5.0 count: 1 elasticsearchRef: name: ex-dev diff --git a/k8s/ex-prod-elasticsearch.yaml b/k8s/ex-prod-elasticsearch.yaml index 88457e15d1..de7848bce1 100644 --- a/k8s/ex-prod-elasticsearch.yaml +++ b/k8s/ex-prod-elasticsearch.yaml @@ -14,8 +14,8 @@ metadata: name: ex-prod namespace: ex-prod spec: - version: 9.4.4 - image: exceptionless/elasticsearch:9.4.4 # https://github.com/exceptionless/Exceptionless/tree/main/build/docker/elasticsearch + version: 9.5.0 + image: exceptionless/elasticsearch:9.5.0 # https://github.com/exceptionless/Exceptionless/tree/main/build/docker/elasticsearch monitoring: metrics: elasticsearchRefs: @@ -79,7 +79,7 @@ metadata: name: ex-prod namespace: ex-prod spec: - version: 9.4.4 + version: 9.5.0 count: 1 elasticsearchRef: name: ex-prod diff --git a/k8s/exceptionless/values.yaml b/k8s/exceptionless/values.yaml index 8880328d4d..8018140e5f 100644 --- a/k8s/exceptionless/values.yaml +++ b/k8s/exceptionless/values.yaml @@ -35,7 +35,7 @@ elasticsearch: connectionString: image: repository: exceptionless/elasticsearch - tag: 9.4.4 + tag: 9.5.0 pullPolicy: IfNotPresent redis: diff --git a/samples/docker-compose.all-in-one.yml b/samples/docker-compose.all-in-one.yml index 0560b023cf..622e74830e 100644 --- a/samples/docker-compose.all-in-one.yml +++ b/samples/docker-compose.all-in-one.yml @@ -20,7 +20,7 @@ services: kibana: depends_on: - elasticsearch - image: docker.elastic.co/kibana/kibana:9.4.4 + image: docker.elastic.co/kibana/kibana:9.5.0 ports: - 5601:5601 diff --git a/samples/docker-compose.yml b/samples/docker-compose.yml index 1d57db836e..7fcb4cc0de 100644 --- a/samples/docker-compose.yml +++ b/samples/docker-compose.yml @@ -44,7 +44,7 @@ services: - ex_appdata:/app/storage elasticsearch: - image: exceptionless/elasticsearch:9.4.4 + image: exceptionless/elasticsearch:9.5.0 environment: discovery.type: single-node xpack.security.enabled: "false" @@ -58,7 +58,7 @@ services: kibana: depends_on: - elasticsearch - image: docker.elastic.co/kibana/kibana:9.4.4 + image: docker.elastic.co/kibana/kibana:9.5.0 ports: - 5601:5601 diff --git a/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs b/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs index e348e42e74..4022e89583 100644 --- a/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs +++ b/src/Exceptionless.AppHost/Extensions/ElasticsearchExtensions.cs @@ -13,7 +13,7 @@ public static class ElasticsearchBuilderExtensions private const int KibanaPort = 5601; /// - /// Adds a Elasticsearch container to the application model. The default image is "docker.elastic.co/elasticsearch/elasticsearch". This version the package defaults to the 9.4.4 tag of the Elasticsearch container image + /// Adds a Elasticsearch container to the application model. The default image is "docker.elastic.co/elasticsearch/elasticsearch". This version the package defaults to the 9.5.0 tag of the Elasticsearch container image /// /// The . /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. @@ -125,7 +125,7 @@ internal static class ElasticsearchContainerImageTags public const string Image = "exceptionless/elasticsearch"; public const string KibanaRegistry = "docker.elastic.co"; public const string KibanaImage = "kibana/kibana"; - public const string Tag = "9.4.4"; + public const string Tag = "9.5.0"; } internal sealed class ElasticsearchConnectionHealthCheck(Func connectionStringFactory) : IHealthCheck diff --git a/tests/Exceptionless.Tests/AppHostConfigurationTests.cs b/tests/Exceptionless.Tests/AppHostConfigurationTests.cs index 455dda2859..5ec833ceaa 100644 --- a/tests/Exceptionless.Tests/AppHostConfigurationTests.cs +++ b/tests/Exceptionless.Tests/AppHostConfigurationTests.cs @@ -10,8 +10,8 @@ public class AppHostConfigurationTests [Fact] public async Task CreateAsync_WithSeparateElasticsearchAndKibanaOverrides_UsesIndependentImageTags() { - const string elasticsearchImageTag = "9.4.4-sha256-candidate"; - const string kibanaImageTag = "9.4.4"; + const string elasticsearchImageTag = "9.5.0-sha256-candidate"; + const string kibanaImageTag = "9.5.0"; var appHost = await DistributedApplicationTestingBuilder.CreateAsync( [ $"--Elasticsearch:ImageTag={elasticsearchImageTag}", From 3045b06c6dca0dc39070dc98cd9668c0e6a6fe54 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 5 Aug 2026 09:08:08 -0500 Subject: [PATCH 11/12] Fix dispatched Elasticsearch candidate selection --- .github/workflows/build.yaml | 7 +++++++ .github/workflows/preview-command.yml | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c1162046fd..4a523ad6ea 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -134,6 +134,7 @@ jobs: id: elasticsearch_image env: PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PREVIEW_BASE_SHA: ${{ github.event.client_payload.base_sha }} PUSH_BEFORE_SHA: ${{ github.event.before }} run: | version=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' build/docker/elasticsearch/9.x/Dockerfile) @@ -146,6 +147,12 @@ jobs: elif [[ "$GITHUB_EVENT_NAME" == "push" && "$GITHUB_REF" == "refs/heads/main" ]] && ! git diff --quiet "$PUSH_BEFORE_SHA"..HEAD -- build/docker/elasticsearch/9.x .github/workflows/elasticsearch-docker-9.yml; then image_changed=true + elif [[ "$GITHUB_EVENT_NAME" == "repository_dispatch" ]] && + ! git diff --quiet "${PREVIEW_BASE_SHA:-origin/main}"...HEAD -- build/docker/elasticsearch/9.x .github/workflows/elasticsearch-docker-9.yml; then + image_changed=true + elif [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]] && + ! git diff --quiet origin/main...HEAD -- build/docker/elasticsearch/9.x .github/workflows/elasticsearch-docker-9.yml; then + image_changed=true fi if [[ "$image_changed" == "true" ]]; then diff --git a/.github/workflows/preview-command.yml b/.github/workflows/preview-command.yml index fe94875393..e4dd285c7f 100644 --- a/.github/workflows/preview-command.yml +++ b/.github/workflows/preview-command.yml @@ -77,6 +77,7 @@ jobs: const headRepository = pullRequest.head.repo.full_name; const headRef = pullRequest.head.ref; const headSha = pullRequest.head.sha; + const baseSha = pullRequest.base.sha; const headLabel = pullRequest.head.label; const headShortSha = headSha.slice(0, 12); @@ -95,6 +96,7 @@ jobs: core.setOutput("head-ref", headRef); core.setOutput("head-label", headLabel); core.setOutput("head-sha", headSha); + core.setOutput("base-sha", baseSha); core.setOutput("head-short-sha", headShortSha); const previewLabel = "dev-preview"; @@ -142,6 +144,7 @@ jobs: HEAD_REF: ${{ steps.preview.outputs.head-ref }} HEAD_LABEL: ${{ steps.preview.outputs.head-label }} HEAD_SHA: ${{ steps.preview.outputs.head-sha }} + BASE_SHA: ${{ steps.preview.outputs.base-sha }} with: script: | await github.rest.repos.createDispatchEvent({ @@ -152,7 +155,8 @@ jobs: pr_number: Number(process.env.PR_NUMBER), head_ref: process.env.HEAD_REF, head_label: process.env.HEAD_LABEL, - head_sha: process.env.HEAD_SHA + head_sha: process.env.HEAD_SHA, + base_sha: process.env.BASE_SHA } }); From 52cc62c213bd35891471ff844fc00792ec4096e7 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 5 Aug 2026 09:13:34 -0500 Subject: [PATCH 12/12] Build fork Elasticsearch candidates locally --- .github/workflows/build.yaml | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4a523ad6ea..482668b30a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -84,6 +84,7 @@ jobs: outputs: version: ${{ steps.version.outputs.version }} elasticsearch_image_tag: ${{ steps.elasticsearch_image.outputs.tag }} + build_elasticsearch_image: ${{ steps.elasticsearch_image.outputs.build_locally }} should_publish: ${{ (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'dev-preview')) && secrets.DOCKER_USERNAME != '' && secrets.DOCKER_PASSWORD != '' }} is_prod_deploy: ${{ startsWith(github.ref, 'refs/tags/v') && github.event_name != 'pull_request' }} is_dev_deploy: ${{ (github.event_name == 'repository_dispatch' && github.event.action == 'preview') || (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'dev-preview')) }} @@ -134,12 +135,14 @@ jobs: id: elasticsearch_image env: PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PR_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} PREVIEW_BASE_SHA: ${{ github.event.client_payload.base_sha }} PUSH_BEFORE_SHA: ${{ github.event.before }} run: | version=$(sed -n 's/.*elasticsearch:\([^ ]*\).*/\1/p' build/docker/elasticsearch/9.x/Dockerfile) tag=$version image_changed=false + build_locally=false if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]] && ! git diff --quiet "$PR_BASE_SHA"...HEAD -- build/docker/elasticsearch/9.x .github/workflows/elasticsearch-docker-9.yml; then @@ -160,21 +163,26 @@ jobs: tag="$version-sha256-$image_sha" image="exceptionless/elasticsearch:$tag" - for attempt in {1..150}; do - if docker manifest inspect "$image" > /dev/null 2>&1; then - break - fi + if [[ "$GITHUB_EVENT_NAME" == "pull_request" && "$PR_HEAD_REPOSITORY" != "$GITHUB_REPOSITORY" ]]; then + build_locally=true + else + for attempt in {1..150}; do + if docker manifest inspect "$image" > /dev/null 2>&1; then + break + fi - if [[ "$attempt" -eq 150 ]]; then - echo "::error::Timed out waiting for $image to be published." - exit 1 - fi + if [[ "$attempt" -eq 150 ]]; then + echo "::error::Timed out waiting for $image to be published." + exit 1 + fi - sleep 10 - done + sleep 10 + done + fi fi echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "build_locally=$build_locally" >> "$GITHUB_OUTPUT" echo "### Elasticsearch image: exceptionless/elasticsearch:$tag" >> "$GITHUB_STEP_SUMMARY" test-api: @@ -190,6 +198,10 @@ jobs: with: ref: ${{ (github.event_name == 'repository_dispatch' && github.event.client_payload.head_sha) || github.event.pull_request.head.sha || github.sha }} + - name: Build fork Elasticsearch candidate locally + if: ${{ needs.version.outputs.build_elasticsearch_image == 'true' }} + run: docker build --tag "exceptionless/elasticsearch:${{ needs.version.outputs.elasticsearch_image_tag }}" --file build/docker/elasticsearch/9.x/Dockerfile build/docker/elasticsearch/9.x + - name: Setup .NET Core uses: actions/setup-dotnet@v5 with: @@ -284,6 +296,10 @@ jobs: - name: Checkout uses: actions/checkout@v6 + - name: Build fork Elasticsearch candidate locally + if: ${{ needs.version.outputs.build_elasticsearch_image == 'true' }} + run: docker build --tag "exceptionless/elasticsearch:${{ needs.version.outputs.elasticsearch_image_tag }}" --file build/docker/elasticsearch/9.x/Dockerfile build/docker/elasticsearch/9.x + - name: Setup .NET Core uses: actions/setup-dotnet@v5 with: