diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 25a6648..c060ec8 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -174,6 +174,29 @@ jobs:
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
+ # Packs both libraries and publishes the package consumer from those packages with Native AOT,
+ # so the source generator is proven to reach a consumer through the package (buildTransitive).
+ - name: "Pack and run the package consumer with Native AOT"
+ shell: pwsh
+ run: |
+ dotnet pack src/LocalStack.Client/LocalStack.Client.csproj -c Release -o artifacts/packages
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ dotnet pack src/LocalStack.Client.Extensions/LocalStack.Client.Extensions.csproj -c Release -o artifacts/packages
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ $version = dotnet msbuild src/LocalStack.Client.Extensions/LocalStack.Client.Extensions.csproj -getProperty:Version
+ $frameworks = (dotnet msbuild src/LocalStack.Client/LocalStack.Client.csproj -getProperty:LocalStackModernTargetFrameworks).Split(';', [StringSplitOptions]::RemoveEmptyEntries)
+ # A fresh package cache, so a same-version package from an earlier run cannot be reused.
+ $env:NUGET_PACKAGES = Join-Path $env:RUNNER_TEMP 'package-consumer-nuget'
+ $project = 'tests/LocalStack.Client.PackageConsumer.Tests/LocalStack.Client.PackageConsumer.Tests.csproj'
+ $rid = '${{ runner.os == 'Windows' && 'win-x64' || 'linux-x64' }}'
+ $exe = if ($IsWindows) { 'LocalStack.Client.PackageConsumer.Tests.exe' } else { 'LocalStack.Client.PackageConsumer.Tests' }
+ foreach ($framework in $frameworks) {
+ dotnet publish $project -c Release -r $rid -o "artifacts/package-consumer/$framework" "-p:LocalStackConsumerTargetFramework=$framework" "-p:LocalStackVersion=$version"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ & "./artifacts/package-consumer/$framework/$exe"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ }
+
continuous-deployment:
name: "Continuous Deployment"
runs-on: ubuntu-22.04
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2363240..12bca02 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,7 +38,7 @@ Planned as **3.0.0** for both packages; not yet published.
### Build, tests and CI
- The repository now builds with the .NET 11 SDK pinned in `global.json`; test projects and CI cover `net10.0` and `net11.0`.
-- Added the xUnit v3 `LocalStack.Client.NativeAot.Tests` lane and the `native-aot-tests` CI job, which publish with `PublishAot` and run native executables on Windows and Linux for both modern targets; Linux also runs live S3/SQS round trips against LocalStack 4.14.0. Cake skips this project.
+- Added the xUnit v3 `LocalStack.Client.NativeAot.Tests` lane and the `native-aot-tests` CI job, which publish with `PublishAot` and run native executables on Windows and Linux for both modern targets; Linux also runs live S3/SQS round trips against LocalStack 4.14.0. Cake skips this project. The same job packs both libraries and publishes `LocalStack.Client.PackageConsumer.Tests` from those packages with Native AOT on both operating systems.
- With the .NET 11 SDK, .NET Framework (`net472`) tests run on Windows only; Cake skips them elsewhere with a warning, and CI no longer installs Mono on macOS.
- Refreshed build, test, and analyzer dependencies, including Cake.Frosting 6.3.0, NuGet.Versioning 7.9.0, SourceLink 10.0.402, Testcontainers and Testcontainers.LocalStack 4.16.0, and `System.Security.Cryptography.Pkcs` 10.0.13. Restore resolves Microsoft.Build.Tasks.Git 10.0.402 and SSH.NET 2026.0.0; repository-wide `NuGetAuditMode=all` remains enabled.
- Test and sandbox consumers use centrally managed Microsoft.Extensions 10.0.13. Refreshed 112 AWS service SDK pins and retained six already-latest entries, with no catalog additions or removals. Core-only projects resolve 4.0.102.6; Extensions and service-package consumers resolve 4.0.102.11 under their dependency ranges.
diff --git a/docs/engineering/quality-gates.md b/docs/engineering/quality-gates.md
index 6b5b8ed..448f9a8 100644
--- a/docs/engineering/quality-gates.md
+++ b/docs/engineering/quality-gates.md
@@ -29,6 +29,6 @@ The [main CI workflow](../../.github/workflows/ci-cd.yml) ignores Markdown-only
In the main CI workflow only the Linux leg runs functional tests. Its test step receives the Actions secret `LOCALSTACK_AUTH_TOKEN`, an organization secret shared with this repository. A pull-request run that cannot read that secret (a fork, a Dependabot run without its own copy, or any run before the secret exists) excludes the token-gated tests through the Cake flag and writes a workflow warning. A push run without the secret still runs them, so it fails at the fixture instead of passing. LocalStack expects a workspace CI Auth Token in CI, not a developer's personal token.
-CI's separate `native-aot-tests` job publishes/runs both targets on `windows-latest` (`win-x64`) and `ubuntu-22.04` (`linux-x64`). Linux waits for LocalStack 4.14.0 health and sets `LOCALSTACK_NATIVE_AOT_HOST`; live cases otherwise skip. The [project](../../tests/LocalStack.Client.NativeAot.Tests/LocalStack.Client.NativeAot.Tests.csproj) and pinned xUnit v3 AOT package define the runner; this lane qualifies the [Native AOT boundary](../architecture/platform-and-packaging.md#native-aot-and-trimming), not every service/deployment.
+CI's separate `native-aot-tests` job publishes/runs both targets on `windows-latest` (`win-x64`) and `ubuntu-22.04` (`linux-x64`). Linux waits for LocalStack 4.14.0 health and sets `LOCALSTACK_NATIVE_AOT_HOST`; live cases otherwise skip. The [project](../../tests/LocalStack.Client.NativeAot.Tests/LocalStack.Client.NativeAot.Tests.csproj) and pinned xUnit v3 AOT package define the runner; this lane qualifies the [Native AOT boundary](../architecture/platform-and-packaging.md#native-aot-and-trimming), not every service/deployment. The same job then packs both libraries into `artifacts/packages` and publishes [`LocalStack.Client.PackageConsumer.Tests`](../../tests/LocalStack.Client.PackageConsumer.Tests/) from those packages for every modern target framework with a fresh NuGet cache; that project is outside the solution and isolated from the repository's build props, so it proves the generator reaches a consumer through the package's `buildTransitive` target on both operating systems.
The badge-update step is `continue-on-error`, and its action logs a non-200 Gist response without turning that response into a failed step. Neither a badge nor a fixed test count in prose proves that the current revision passed. Report the actual command, target frameworks, result, and any tests deliberately not run. Do not infer NuGet publication, Native AOT support, or full LocalStack service behavior from a successful build or client-construction test.
diff --git a/tests/LocalStack.Client.PackageConsumer.Tests/Directory.Build.props b/tests/LocalStack.Client.PackageConsumer.Tests/Directory.Build.props
new file mode 100644
index 0000000..0c0e3c1
--- /dev/null
+++ b/tests/LocalStack.Client.PackageConsumer.Tests/Directory.Build.props
@@ -0,0 +1,3 @@
+
+
+
diff --git a/tests/LocalStack.Client.PackageConsumer.Tests/Directory.Build.targets b/tests/LocalStack.Client.PackageConsumer.Tests/Directory.Build.targets
new file mode 100644
index 0000000..f175977
--- /dev/null
+++ b/tests/LocalStack.Client.PackageConsumer.Tests/Directory.Build.targets
@@ -0,0 +1,3 @@
+
+
+
diff --git a/tests/LocalStack.Client.PackageConsumer.Tests/Directory.Packages.props b/tests/LocalStack.Client.PackageConsumer.Tests/Directory.Packages.props
new file mode 100644
index 0000000..6242951
--- /dev/null
+++ b/tests/LocalStack.Client.PackageConsumer.Tests/Directory.Packages.props
@@ -0,0 +1,6 @@
+
+
+
+ false
+
+
diff --git a/tests/LocalStack.Client.PackageConsumer.Tests/LocalStack.Client.PackageConsumer.Tests.csproj b/tests/LocalStack.Client.PackageConsumer.Tests/LocalStack.Client.PackageConsumer.Tests.csproj
new file mode 100644
index 0000000..f1a2436
--- /dev/null
+++ b/tests/LocalStack.Client.PackageConsumer.Tests/LocalStack.Client.PackageConsumer.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+
+ Exe
+ $(LocalStackConsumerTargetFramework)
+ true
+ enable
+ enable
+ true
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/LocalStack.Client.PackageConsumer.Tests/Program.cs b/tests/LocalStack.Client.PackageConsumer.Tests/Program.cs
new file mode 100644
index 0000000..32976b1
--- /dev/null
+++ b/tests/LocalStack.Client.PackageConsumer.Tests/Program.cs
@@ -0,0 +1,44 @@
+using Amazon.S3;
+using Amazon.SQS;
+using LocalStack.Client.Contracts;
+using LocalStack.Client.Extensions;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+if (System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported)
+{
+ Console.Error.WriteLine("Expected a Native AOT executable.");
+ return 1;
+}
+
+var configuration = new ConfigurationBuilder()
+ .SetBasePath(AppContext.BaseDirectory)
+ .AddJsonFile("appsettings.json")
+ .Build();
+
+// The consumer contract, unchanged from earlier versions.
+var services = new ServiceCollection();
+services.AddLocalStack(configuration);
+services.AddAwsService();
+using var provider = services.BuildServiceProvider();
+var s3 = provider.GetRequiredService();
+
+// A Type-based API: served by the source generator that arrives through the package.
+var session = provider.GetRequiredService();
+using var sqs = (IAmazonSQS)session.CreateClientByInterface(typeof(IAmazonSQS));
+string serviceId = provider.GetRequiredService().ExtractServiceMetadata(typeof(AmazonSQSClient)).ServiceId;
+
+if (!((AmazonS3Config)s3.Config).ForcePathStyle || serviceId != "SQS" || sqs.Config.ProxyPort != 4566)
+{
+ Console.Error.WriteLine($"Unexpected configuration: pathStyle={((AmazonS3Config)s3.Config).ForcePathStyle} serviceId={serviceId} sqsPort={sqs.Config.ProxyPort}");
+ return 1;
+}
+
+if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LOCALSTACK_NATIVE_AOT_HOST")))
+{
+ await s3.ListBucketsAsync();
+ await sqs.ListQueuesAsync(new Amazon.SQS.Model.ListQueuesRequest());
+}
+
+Console.WriteLine("Package consumer passed.");
+return 0;
diff --git a/tests/LocalStack.Client.PackageConsumer.Tests/appsettings.json b/tests/LocalStack.Client.PackageConsumer.Tests/appsettings.json
new file mode 100644
index 0000000..d9bab94
--- /dev/null
+++ b/tests/LocalStack.Client.PackageConsumer.Tests/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "LocalStack": {
+ "UseLocalStack": true,
+ "Session": {
+ "RegionName": "eu-central-1"
+ },
+ "Config": {
+ "LocalStackHost": "localhost",
+ "EdgePort": 4566
+ }
+ }
+}
diff --git a/tests/LocalStack.Client.PackageConsumer.Tests/nuget.config b/tests/LocalStack.Client.PackageConsumer.Tests/nuget.config
new file mode 100644
index 0000000..25a4f42
--- /dev/null
+++ b/tests/LocalStack.Client.PackageConsumer.Tests/nuget.config
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+