Add test to verify sample AppHost publish manifests - #1693
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an integration test that snapshots the Aspire publish manifest output for each project-based sample AppHost, ensuring publish-manifest changes are surfaced in CI via committed aspire-manifest.json files.
Changes:
- Add
ManifestTeststo generate publish manifests in-process and compare to committed snapshots (with normalization and cleanup). - Commit/refresh
aspire-manifest.jsonsnapshots alongside sample AppHosts. - Ignore transient
aspire-manifest.g.jsonoutput in.gitignore.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/SamplesIntegrationTests/ManifestTests.cs | New xUnit theory to generate and snapshot-compare publish manifests (with normalization, config neutralization, and cleanup). |
| .gitignore | Ignores transient aspire-manifest.g.json generated by the manifest tests. |
| samples/volume-mount/VolumeMount.AppHost/aspire-manifest.json | Adds committed expected publish manifest snapshot. |
| samples/orleans-voting/OrleansVoting.AppHost/aspire-manifest.json | Adds committed expected publish manifest snapshot. |
| samples/Metrics/MetricsApp.AppHost/aspire-manifest.json | Adds committed expected publish manifest snapshot (includes neutralized OTLP settings). |
| samples/health-checks-ui/HealthChecksUI.AppHost/aspire-manifest.json | Refreshes expected manifest snapshot to current publish output. |
| samples/database-migrations/DatabaseMigrations.AppHost/aspire-manifest.json | Adds committed expected publish manifest snapshot. |
| samples/database-containers/DatabaseContainers.AppHost/aspire-manifest.json | Adds committed expected publish manifest snapshot. |
| samples/custom-resources/CustomResources.AppHost/aspire-manifest.json | Adds committed expected publish manifest snapshot (includes unsupported-resource error entries). |
| samples/client-apps-integration/ClientAppsIntegration.AppHost/aspire-manifest.json | Adds committed expected publish manifest snapshot. |
| samples/aspire-with-node/AspireWithNode.AppHost/aspire-manifest.json | Refreshes expected manifest snapshot to current publish output. |
| samples/aspire-with-javascript/AspireJavaScript.AppHost/aspire-manifest.json | Adds committed expected publish manifest snapshot. |
| samples/aspire-with-azure-functions/ImageGallery.AppHost/aspire-manifest.json | Adds committed expected publish manifest snapshot. |
| samples/aspire-shop/AspireShop.AppHost/aspire-manifest.json | Adds committed expected publish manifest snapshot. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }, | ||
| "angular": { | ||
| "type": "dockerfile.v0", | ||
| "path": "../AspireJavaScript.Angular/Dockerfile", | ||
| "context": "../AspireJavaScript.Angular", | ||
| "env": { | ||
| "NODE_ENV": "development", | ||
| "services__weatherapi__http__0": "{weatherapi.bindings.http.url}", | ||
| "services__weatherapi__https__0": "{weatherapi.bindings.https.url}", | ||
| "PORT": "{angular.bindings.http.targetPort}" | ||
| }, | ||
| "bindings": { | ||
| "http": { | ||
| "scheme": "http", | ||
| "protocol": "tcp", | ||
| "transport": "http", | ||
| "targetPort": 8000, | ||
| "external": true | ||
| } | ||
| } | ||
| }, | ||
| "react": { | ||
| "type": "dockerfile.v0", | ||
| "path": "../AspireJavaScript.React/Dockerfile", | ||
| "context": "../AspireJavaScript.React", | ||
| "env": { | ||
| "NODE_ENV": "development", | ||
| "services__weatherapi__http__0": "{weatherapi.bindings.http.url}", | ||
| "services__weatherapi__https__0": "{weatherapi.bindings.https.url}", | ||
| "BROWSER": "none", | ||
| "PORT": "{react.bindings.http.targetPort}" | ||
| }, | ||
| "bindings": { | ||
| "http": { | ||
| "scheme": "http", | ||
| "protocol": "tcp", | ||
| "transport": "http", | ||
| "targetPort": 8001, | ||
| "external": true | ||
| } | ||
| } | ||
| }, | ||
| "vue": { | ||
| "type": "dockerfile.v0", | ||
| "path": "../AspireJavaScript.Vue/Dockerfile", | ||
| "context": "../AspireJavaScript.Vue", | ||
| "env": { | ||
| "NODE_ENV": "development", | ||
| "services__weatherapi__http__0": "{weatherapi.bindings.http.url}", | ||
| "services__weatherapi__https__0": "{weatherapi.bindings.https.url}", | ||
| "PORT": "{vue.bindings.http.targetPort}" | ||
| }, | ||
| "bindings": { | ||
| "http": { | ||
| "scheme": "http", | ||
| "protocol": "tcp", | ||
| "transport": "http", | ||
| "targetPort": 8002, | ||
| "external": true | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } No newline at end of file | ||
| "angular": { No newline at end of file |
There was a problem hiding this comment.
Good catch — the truncated/invalid JSON was a symptom, not just a bad snapshot. aspire publish was actually failing for this sample: under the current Aspire.Hosting.JavaScript, combining an explicit run script (runScriptName / WithRunScript) with an existing hand-authored Dockerfile is rejected unless the container entrypoint is set explicitly, so the publishing pipeline threw partway through writing the angular resource. The in-process host stops on that failure without faulting RunAsync, so the test wrote the truncated manifest and still passed.
Fixed by setting the nginx entrypoint explicitly on the angular/react/vue apps:
.PublishAsDockerFile(container => container.WithEntrypoint("/docker-entrypoint.sh"));The manifest now regenerates complete and valid (all five resources). I also hardened ManifestTests to fail when publish produces a missing/truncated/invalid manifest — and to route the AppHost logs to the test output so the underlying publish error is visible — so a broken sample can't silently pass again.
Adds ManifestTests that generates the Aspire publish manifest for each project-based sample AppHost and compares it against a committed aspire-manifest.json snapshot, so changes that affect publish output are caught in CI. - Generates manifests in-process via DistributedApplicationTestingBuilder (--operation publish --publisher manifest), writing into each AppHost directory so every path is a machine-independent relative path. - Fails the test when publish produces a missing, truncated, or otherwise invalid manifest. A failed publishing pipeline stops the in-process host without faulting RunAsync, so the AppHost logs are routed to the test output to surface the underlying publish error. - Normalizes the machine-specific volume-name SHA-256 prefix and line endings. - Neutralizes ambient OTLP/dashboard configuration during generation with a fixed, non-empty placeholder that reliably overrides user secrets, so the manifests are reproducible and no real OTLP API key can leak into a snapshot. - Cleans up generated byproducts (aspire-manifest.g.json, *.module.bicep, *.Dockerfile) without touching committed source files. - Supports regeneration via ASPIRE_UPDATE_MANIFESTS=true, refused on CI. Sets an explicit container entrypoint on the aspire-with-javascript angular/react/vue apps so they publish with their existing nginx Dockerfiles; Aspire.Hosting.JavaScript now rejects an explicit run script combined with an existing Dockerfile unless the entrypoint is set explicitly. Adds expected manifests for all 11 project-based sample AppHosts and ignores the transient aspire-manifest.g.json. Resolves #572 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 557d9ca1-0e59-4e3a-8cee-ddc5f3e44449
648edf1 to
51582cb
Compare
Summary
Adds a test that generates the Aspire publish manifest for each project-based sample AppHost and compares it against a committed
aspire-manifest.jsonsnapshot, so any change that affects publish output is caught in CI. Closes #572.As noted in the issue, this is the cheapest way to get coverage of publish output: any change that impacts a manifest now requires the committed "expected" manifest to be regenerated, which surfaces the change in the diff for review.
What's included
tests/SamplesIntegrationTests/ManifestTests.cs— a theory over every*.AppHost.dllreferenced by the test project (12 AppHosts).aspire-manifest.jsoncommitted next to each AppHost (9 new, 3 stale ones refreshed). This matches the existingmanifestlaunch profile /GenerateAspireManifestconvention of writing the manifest into the AppHost directory..gitignore— ignores the transientaspire-manifest.g.jsonthe test generates.How it works
DistributedApplicationTestingBuilderusing--operation publish --publisher manifest— no Docker required.../Foo/Foo.csproj).<sha256>placeholder, plus line endings.ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL/AppHost:OtlpApiKeyinto a container's environment) so manifests are reproducible regardless of a contributor's environment or user secrets, and so an OTLP API key can never leak into a committed snapshot.aspire-manifest.g.json,*.module.bicep,*.Dockerfile) without ever deleting committed source files.Regenerating the expected manifests
When a change intentionally affects a manifest, regenerate locally and commit the result:
Update mode is refused when
CIis set, so the verification can never be silently skipped on CI. The failure message points contributors at these steps.Notes for reviewers
- The expected manifests were generated on Windows; manifest paths use forward slashes and all machine-specific tokens are normalized, so they match Linux CI output. CI runs the test suite on
- A few resources legitimately emit
- Scope is the 12 project-based AppHosts referenced by the test project; file-based AppHost samples (
trueubuntu-latest.{"error":"This resource does not support generation in the manifest."}(Docker Compose environment / custom resources). That's the current publish output and is snapshotted as-is for change detection.apphost.cs/apphost.mts) aren't referenced and aren't covered.