Skip to content

Commit 20b4b96

Browse files
Mpdreamzclaude
andauthored
Add MCP and API smoke tests to the integration suite on CI (#3356)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 908493a commit 20b4b96

25 files changed

Lines changed: 284 additions & 92 deletions

.github/workflows/ci.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ jobs:
185185

186186
integration:
187187
runs-on: docs-builder-latest-16
188+
environment: integration-tests
189+
permissions:
190+
contents: read
191+
id-token: write
188192
steps:
189193
- uses: actions/checkout@v6
190194

@@ -195,5 +199,22 @@ jobs:
195199
- name: Install Aspire workload
196200
run: dotnet workload install aspire
197201

202+
- name: Configure AWS credentials
203+
uses: aws-actions/configure-aws-credentials@v4
204+
with:
205+
role-to-assume: arn:aws:iam::197730964718:role/elastic-docs-v3-integration-tests
206+
aws-region: us-east-1
207+
208+
- name: Fetch ES credentials
209+
run: |
210+
ES_URL=$(aws ssm get-parameter --name /elastic-docs-v3/prod/docs-elasticsearch-github-actions-readonly-url --with-decryption --query Parameter.Value --output text)
211+
ES_KEY=$(aws ssm get-parameter --name /elastic-docs-v3/prod/docs-elasticsearch-github-actions-readonly-api-key --with-decryption --query Parameter.Value --output text)
212+
echo "::add-mask::$ES_URL"
213+
echo "::add-mask::$ES_KEY"
214+
dotnet user-secrets set "DocumentationElasticUrl" "$ES_URL" --project aspire
215+
dotnet user-secrets set "DocumentationElasticApiKey" "$ES_KEY" --project aspire
216+
198217
- name: Integration Tests
218+
env:
219+
ENVIRONMENT: prod
199220
run: dotnet run --project build -c release -- integrate

aspire/AppHost.cs

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -54,50 +54,57 @@ internal static async Task Run(
5454
.WaitForCompletion(cloneAll)
5555
.WithParentRelationship(cloneAll);
5656

57-
var elasticsearchLocal = builder.AddElasticsearch(ElasticsearchLocal)
58-
.WithEnvironment("LICENSE", "trial");
59-
if (!startElasticsearch)
60-
elasticsearchLocal = elasticsearchLocal.WithExplicitStart();
57+
IResourceBuilder<ElasticsearchResource>? elasticsearchLocal = null;
58+
if (startElasticsearch)
59+
elasticsearchLocal = builder.AddElasticsearch(ElasticsearchLocal)
60+
.WithEnvironment("LICENSE", "trial");
6161

6262
var elasticsearchRemote = builder.AddExternalService(ElasticsearchRemote, elasticsearchUrl);
6363

64-
var api = builder.AddProject<Projects.Elastic_Documentation_Api>(Api)
64+
// Read ENVIRONMENT and DOCS_BUILD_TYPE from the host process (injected by CI or set locally).
65+
// Index name pattern: docs-{type}.semantic-{env}-latest
66+
var rawEnvironment = Environment.GetEnvironmentVariable("ENVIRONMENT");
67+
var serviceEnvironment = string.IsNullOrWhiteSpace(rawEnvironment) ? "prod" : rawEnvironment;
68+
var rawBuildType = Environment.GetEnvironmentVariable("DOCS_BUILD_TYPE");
69+
var buildType = string.IsNullOrWhiteSpace(rawBuildType) ? "assembler" : rawBuildType;
70+
71+
var api = builder.AddProject<Projects.Elastic_Documentation_Api>(Api, launchProfileName: "http")
6572
.WithArgs(GlobalArguments)
66-
.WithEnvironment("ENVIRONMENT", "dev")
73+
.WithEnvironment("ENVIRONMENT", serviceEnvironment)
74+
.WithEnvironment("DOCS_BUILD_TYPE", buildType)
6775
.WithEnvironment("LLM_GATEWAY_FUNCTION_URL", llmUrl)
68-
.WithEnvironment("LLM_GATEWAY_SERVICE_ACCOUNT_KEY_PATH", llmServiceAccountPath);
76+
.WithEnvironment("LLM_GATEWAY_SERVICE_ACCOUNT_KEY_PATH", llmServiceAccountPath)
77+
.WithHttpHealthCheck("/docs/_api/health");
6978

7079
// ReSharper disable once RedundantAssignment
7180
api = startElasticsearch
7281
? api
73-
.WithReference(elasticsearchLocal)
74-
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchLocal.GetEndpoint("http"))
75-
.WithEnvironment(context => context.EnvironmentVariables["DOCUMENTATION_ELASTIC_PASSWORD"] = elasticsearchLocal.Resource.PasswordParameter)
76-
.WithParentRelationship(elasticsearchLocal)
77-
.WaitFor(elasticsearchLocal)
78-
.WithExplicitStart()
82+
.WithReference(elasticsearchLocal!)
83+
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchLocal!.GetEndpoint("http"))
84+
.WithEnvironment(context => context.EnvironmentVariables["DOCUMENTATION_ELASTIC_PASSWORD"] = elasticsearchLocal!.Resource.PasswordParameter)
85+
.WithParentRelationship(elasticsearchLocal!)
86+
.WaitFor(elasticsearchLocal!)
7987
: api.WithReference(elasticsearchRemote)
8088
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchUrl)
81-
.WithEnvironment("DOCUMENTATION_ELASTIC_APIKEY", elasticsearchApiKey)
82-
.WithExplicitStart();
89+
.WithEnvironment("DOCUMENTATION_ELASTIC_APIKEY", elasticsearchApiKey);
8390

8491
var mcp = builder.AddProject<Projects.Elastic_Documentation_Mcp_Remote>(RemoteMcp)
8592
.WithArgs(GlobalArguments)
86-
.WithEnvironment("ENVIRONMENT", "dev");
93+
.WithEnvironment("ENVIRONMENT", serviceEnvironment)
94+
.WithEnvironment("DOCS_BUILD_TYPE", buildType)
95+
.WithHttpHealthCheck("/docs/_mcp/health");
8796

8897
// ReSharper disable once RedundantAssignment
8998
mcp = startElasticsearch
9099
? mcp
91-
.WithReference(elasticsearchLocal)
92-
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchLocal.GetEndpoint("http"))
93-
.WithEnvironment(context => context.EnvironmentVariables["DOCUMENTATION_ELASTIC_PASSWORD"] = elasticsearchLocal.Resource.PasswordParameter)
94-
.WithParentRelationship(elasticsearchLocal)
95-
.WaitFor(elasticsearchLocal)
96-
.WithExplicitStart()
100+
.WithReference(elasticsearchLocal!)
101+
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchLocal!.GetEndpoint("http"))
102+
.WithEnvironment(context => context.EnvironmentVariables["DOCUMENTATION_ELASTIC_PASSWORD"] = elasticsearchLocal!.Resource.PasswordParameter)
103+
.WithParentRelationship(elasticsearchLocal!)
104+
.WaitFor(elasticsearchLocal!)
97105
: mcp.WithReference(elasticsearchRemote)
98106
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchUrl)
99-
.WithEnvironment("DOCUMENTATION_ELASTIC_APIKEY", elasticsearchApiKey)
100-
.WithExplicitStart();
107+
.WithEnvironment("DOCUMENTATION_ELASTIC_APIKEY", elasticsearchApiKey);
101108

102109
var indexElasticsearch = builder.AddProject<Projects.docs_builder>(ElasticsearchIngest)
103110
.WithArgs(["assembler", "index", .. GlobalArguments])
@@ -107,11 +114,11 @@ internal static async Task Run(
107114
// ReSharper disable once RedundantAssignment
108115
indexElasticsearch = startElasticsearch
109116
? indexElasticsearch
110-
.WaitFor(elasticsearchLocal)
111-
.WithReference(elasticsearchLocal)
112-
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchLocal.GetEndpoint("http"))
113-
.WithEnvironment(context => context.EnvironmentVariables["DOCUMENTATION_ELASTIC_PASSWORD"] = elasticsearchLocal.Resource.PasswordParameter)
114-
.WithParentRelationship(elasticsearchLocal)
117+
.WaitFor(elasticsearchLocal!)
118+
.WithReference(elasticsearchLocal!)
119+
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchLocal!.GetEndpoint("http"))
120+
.WithEnvironment(context => context.EnvironmentVariables["DOCUMENTATION_ELASTIC_PASSWORD"] = elasticsearchLocal!.Resource.PasswordParameter)
121+
.WithParentRelationship(elasticsearchLocal!)
115122
: indexElasticsearch
116123
.WithReference(elasticsearchRemote)
117124
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchUrl)
@@ -129,16 +136,16 @@ internal static async Task Run(
129136

130137
serveStatic = startElasticsearch
131138
? serveStatic
132-
.WithReference(elasticsearchLocal)
133-
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchLocal.GetEndpoint("http"))
134-
.WithEnvironment(context => context.EnvironmentVariables["DOCUMENTATION_ELASTIC_PASSWORD"] = elasticsearchLocal.Resource.PasswordParameter)
139+
.WithReference(elasticsearchLocal!)
140+
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchLocal!.GetEndpoint("http"))
141+
.WithEnvironment(context => context.EnvironmentVariables["DOCUMENTATION_ELASTIC_PASSWORD"] = elasticsearchLocal!.Resource.PasswordParameter)
135142
: serveStatic
136143
.WithReference(elasticsearchRemote)
137144
.WithEnvironment("DOCUMENTATION_ELASTIC_URL", elasticsearchUrl)
138145
.WithEnvironment("DOCUMENTATION_ELASTIC_APIKEY", elasticsearchApiKey);
139146

140147
// ReSharper disable once RedundantAssignment
141-
serveStatic = startElasticsearch ? serveStatic.WaitFor(elasticsearchLocal) : serveStatic.WaitFor(buildAll);
148+
serveStatic = startElasticsearch ? serveStatic.WaitFor(elasticsearchLocal!) : serveStatic.WaitFor(buildAll);
142149

143150
await builder.Build().RunAsync(ct);
144151
}

docs-builder.slnx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,7 @@
8484
</Folder>
8585
<Folder Name="/tests-integration/">
8686
<File Path="tests-integration/Directory.Build.props" />
87-
<Project Path="tests-integration/Elastic.Assembler.IntegrationTests/Elastic.Assembler.IntegrationTests.csproj" />
88-
<Project Path="tests-integration/Elastic.Documentation.Api.IntegrationTests/Elastic.Documentation.Api.IntegrationTests.csproj" />
87+
<Project Path="tests-integration/Elastic.Documentation.IntegrationTests/Elastic.Documentation.IntegrationTests.csproj" />
8988
<Project Path="tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj" />
9089
<Project Path="tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj" />
9190
<Project Path="tests-integration/Elastic.ContentDateEnrichment.IntegrationTests/Elastic.ContentDateEnrichment.IntegrationTests.csproj" />
@@ -95,6 +94,7 @@
9594
<Project Path="tests/authoring/authoring.fsproj" />
9695
<Project Path="tests/Elastic.ApiExplorer.Tests/Elastic.ApiExplorer.Tests.csproj" />
9796
<Project Path="tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj" />
97+
<Project Path="tests/Elastic.Documentation.Api.Tests/Elastic.Documentation.Api.Tests.csproj" />
9898
<Project Path="tests/Elastic.Documentation.Build.Tests/Elastic.Documentation.Build.Tests.csproj" />
9999
<Project Path="tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj" />
100100
<Project Path="tests/Elastic.Documentation.LegacyDocs.Tests/Elastic.Documentation.LegacyDocs.Tests.csproj" />

src/api/Elastic.Documentation.Api/Program.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,17 @@
2222
_ = builder.AddDefaultHealthChecks();
2323
_ = builder.AddDocsApiOpenTelemetry();
2424

25-
// Configure Kestrel to listen on port 8080 (standard container port)
26-
_ = builder.WebHost.ConfigureKestrel(serverOptions =>
25+
// Only hardcode port 8080 when not running under Aspire/orchestration.
26+
// Use builder.Configuration so both ASPNETCORE_* and DOTNET_* prefix variants are covered.
27+
if (string.IsNullOrEmpty(builder.Configuration["HTTP_PORTS"])
28+
&& string.IsNullOrEmpty(builder.Configuration["HTTPS_PORTS"])
29+
&& string.IsNullOrEmpty(builder.Configuration["URLS"]))
2730
{
28-
serverOptions.ListenAnyIP(8080);
29-
});
31+
_ = builder.WebHost.ConfigureKestrel(serverOptions =>
32+
{
33+
serverOptions.ListenAnyIP(8080);
34+
});
35+
}
3036

3137
var environment = Environment.GetEnvironmentVariable("ENVIRONMENT");
3238
Console.WriteLine($"Docs Environment: {environment}");

src/api/Elastic.Documentation.Mcp.Remote/Program.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,17 @@
3030
_ = builder.Services.ConfigureOpenTelemetryTracerProvider(t =>
3131
t.AddSource(McpToolTelemetry.McpToolSourceName));
3232

33-
// Configure Kestrel to listen on port 8080 (standard container port)
34-
_ = builder.WebHost.ConfigureKestrel(serverOptions =>
33+
// Only hardcode port 8080 when not running under Aspire/orchestration.
34+
// Use builder.Configuration so both ASPNETCORE_* and DOTNET_* prefix variants are covered.
35+
if (string.IsNullOrEmpty(builder.Configuration["HTTP_PORTS"])
36+
&& string.IsNullOrEmpty(builder.Configuration["HTTPS_PORTS"])
37+
&& string.IsNullOrEmpty(builder.Configuration["URLS"]))
3538
{
36-
serverOptions.ListenAnyIP(8080);
37-
});
39+
_ = builder.WebHost.ConfigureKestrel(serverOptions =>
40+
{
41+
serverOptions.ListenAnyIP(8080);
42+
});
43+
}
3844

3945
var environment = Environment.GetEnvironmentVariable("ENVIRONMENT");
4046
Console.WriteLine($"Docs Environment: {environment}");

tests-integration/Elastic.Assembler.IntegrationTests/AssembleFixture.cs renamed to tests-integration/Elastic.Documentation.IntegrationTests/AssembleFixture.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@
55
using Aspire.Hosting;
66
using Aspire.Hosting.ApplicationModel;
77
using Aspire.Hosting.Testing;
8+
using Elastic.Documentation.Aspire;
89
using Elastic.Documentation.ServiceDefaults;
910
using InMemLogger;
1011
using Microsoft.Extensions.Configuration;
1112
using Microsoft.Extensions.DependencyInjection;
1213
using Microsoft.Extensions.Logging;
1314
using static Elastic.Documentation.Aspire.ResourceNames;
1415

15-
[assembly: CaptureConsole, AssemblyFixture(typeof(Elastic.Assembler.IntegrationTests.DocumentationFixture))]
16+
[assembly: CaptureConsole, AssemblyFixture(typeof(Elastic.Documentation.IntegrationTests.DocumentationFixture))]
1617

17-
namespace Elastic.Assembler.IntegrationTests;
18+
namespace Elastic.Documentation.IntegrationTests;
1819

1920
public static class DistributedApplicationExtensions
2021
{
@@ -90,6 +91,14 @@ public async ValueTask InitializeAsync()
9091
_ = await DistributedApplication.ResourceNotifications
9192
.WaitForResourceHealthyAsync(AssemblerServe, cancellationToken: TestContext.Current.CancellationToken)
9293
.WaitAsync(TimeSpan.FromMinutes(3), TestContext.Current.CancellationToken);
94+
95+
_ = await DistributedApplication.ResourceNotifications
96+
.WaitForResourceHealthyAsync(ResourceNames.Api, cancellationToken: TestContext.Current.CancellationToken)
97+
.WaitAsync(TimeSpan.FromMinutes(3), TestContext.Current.CancellationToken);
98+
99+
_ = await DistributedApplication.ResourceNotifications
100+
.WaitForResourceHealthyAsync(RemoteMcp, cancellationToken: TestContext.Current.CancellationToken)
101+
.WaitAsync(TimeSpan.FromMinutes(3), TestContext.Current.CancellationToken);
93102
}
94103
catch (Exception e)
95104
{
@@ -99,6 +108,10 @@ public async ValueTask InitializeAsync()
99108
}
100109
}
101110

111+
public HttpClient CreateApiClient() => DistributedApplication.CreateHttpClient(ResourceNames.Api);
112+
113+
public HttpClient CreateMcpClient() => DistributedApplication.CreateHttpClient(RemoteMcp);
114+
102115
private async ValueTask ValidateExitCode(string resourceName)
103116
{
104117
var eventResource = await DistributedApplication.ResourceNotifications.WaitForResourceAsync(resourceName, _ => true);

tests-integration/Elastic.Assembler.IntegrationTests/AssemblerConfigurationTests.cs renamed to tests-integration/Elastic.Documentation.IntegrationTests/AssemblerConfigurationTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
using Microsoft.Extensions.Logging.Abstractions;
1212
using Nullean.ScopedFileSystem;
1313

14-
namespace Elastic.Assembler.IntegrationTests;
14+
namespace Elastic.Documentation.IntegrationTests;
1515

1616
public class PublicOnlyAssemblerConfigurationTests
1717
{
@@ -130,7 +130,7 @@ public void ReadsVersions()
130130
public ValueTask DisposeAsync()
131131
{
132132
GC.SuppressFinalize(this);
133-
if (TestContext.Current.TestState?.Result is TestResult.Passed)
133+
if (TestContext.Current.TestState?.Result is not TestResult.Failed)
134134
return default;
135135
foreach (var resource in _fixture.InMemoryLogger.RecordedLogs)
136136
_output.WriteLine(resource.Message);

tests-integration/Elastic.Assembler.IntegrationTests/DocsSyncTests.cs renamed to tests-integration/Elastic.Documentation.IntegrationTests/DocsSyncTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
using OpenTelemetry;
2222
using OpenTelemetry.Trace;
2323

24-
namespace Elastic.Assembler.IntegrationTests;
24+
namespace Elastic.Documentation.IntegrationTests;
2525

2626
public class DocsSyncTests
2727
{

tests-integration/Elastic.Assembler.IntegrationTests/Elastic.Assembler.IntegrationTests.csproj renamed to tests-integration/Elastic.Documentation.IntegrationTests/Elastic.Documentation.IntegrationTests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
<ProjectReference Include="$(SolutionRoot)\aspire\aspire.csproj" />
1313
<ProjectReference Include="$(SolutionRoot)\src\Elastic.Documentation.ServiceDefaults\Elastic.Documentation.ServiceDefaults.csproj" />
1414
<ProjectReference Include="$(SolutionRoot)\src\api\Elastic.Documentation.Api\Elastic.Documentation.Api.csproj" />
15+
<ProjectReference Include="$(SolutionRoot)\src\api\Elastic.Documentation.Mcp.Remote\Elastic.Documentation.Mcp.Remote.csproj" />
1516
<ProjectReference Include="$(SolutionRoot)\src\Elastic.Markdown\Elastic.Markdown.csproj" />
1617
</ItemGroup>
1718
<ItemGroup>
1819
<PackageReference Include="FakeItEasy" />
1920
<PackageReference Include="AngleSharp" />
2021
<PackageReference Include="Aspire.Hosting.Testing"/>
2122
<PackageReference Include="InMemoryLogger"/>
23+
<PackageReference Include="ModelContextProtocol" />
2224
<PackageReference Include="OpenTelemetry" />
2325
<PackageReference Include="OpenTelemetry.Exporter.InMemory" />
2426
</ItemGroup>

tests-integration/Elastic.Assembler.IntegrationTests/NavigationBuildingTests.cs renamed to tests-integration/Elastic.Documentation.IntegrationTests/NavigationBuildingTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
using Nullean.ScopedFileSystem;
2525
using RazorSlices;
2626

27-
namespace Elastic.Assembler.IntegrationTests;
27+
namespace Elastic.Documentation.IntegrationTests;
2828

2929
public class NavigationBuildingTests(DocumentationFixture fixture, ITestOutputHelper output) : IAsyncLifetime
3030
{
@@ -169,7 +169,7 @@ private static IEnumerable<string> GetAllNavigationUrls(INavigationItem item)
169169
public ValueTask DisposeAsync()
170170
{
171171
GC.SuppressFinalize(this);
172-
if (TestContext.Current.TestState?.Result is TestResult.Passed)
172+
if (TestContext.Current.TestState?.Result is not TestResult.Failed)
173173
return default;
174174
foreach (var resource in fixture.InMemoryLogger.RecordedLogs)
175175
output.WriteLine(resource.Message);

0 commit comments

Comments
 (0)