microsoft / microsoft/aspire

JavaScriptPublishModeAnnotation and JavaScriptPublishMode are internal — downstream packages can't opt resources into the 'static-website' publish-mode shape

Open
#17,563 1 comment 2 reactions 0 assignees View on GitHub
area-integrations triage:bot-seen
Dominant language
C#
Stars
6.3k
Forks
991
Avg merge
2d 15h
Merged PRs (30d)
196

Description

### Is there an existing issue for this?

- [x] I have searched the existing issues

### Describe the bug

`Aspire.Hosting.JavaScript`'s `CreateDefaultJavaScriptAppBuilder` unconditionally calls `PublishAsDockerFile(...)` for every JS resource at publish time ([line 689 of `JavaScriptHostingExtensions.cs`](https://github.com/microsoft/aspire/blob/v13.3.5/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs#L689)). This attaches a `DockerfileBuildAnnotation` with `HasEntrypoint = false` and a `ContainerImageAnnotation` to the (now-swapped) `ExecutableContainerResource`.

The `HasEntrypoint` toggle is flipped to `true` only when a `JavaScriptPublishModeAnnotation` is present on the resource ([lines 862–870](https://github.com/microsoft/aspire/blob/v13.3.5/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs#L862-L870)):

```csharp
if (resource.TryGetLastAnnotation(out var dockerFileAnnotation))
{
dockerFileAnnotation.HasEntrypoint =
resource.TryGetLastAnnotation(out _);
}
```

`JavaScriptPublishModeAnnotation` and its companion enum `JavaScriptPublishMode` (values: `StaticWebsite`, `NodeServer`, `NpmScript`, `NextStandalone`) are declared `internal sealed` ([`JavaScriptPublishModeAnnotation.cs:8-28`](https://github.com/microsoft/aspire/blob/v13.3.5/src/Aspire.Hosting.JavaScript/JavaScriptPublishModeAnnotation.cs)). Only Aspire-internal extensions (`PublishAsStaticWebsite`, `PublishAsNodeServer`, `PublishAsNpmScript`, `PublishAsNextStandalone`) can set the annotation. There is no `InternalsVisibleTo` for any non-test assembly.

The downstream effect: if a JS resource targets a publish path that is not one of the four Aspire-blessed modes (in my case, Azure Static Web Apps — `Microsoft.Web/staticSites` content uploaded via the SWA CLI rather than a YARP-fronted container), the resource ends up in publish mode as a container with `HasEntrypoint = false`. Aspire's `validate-build-only-container-references` pipeline step then flags it as a "build-only container that is not consumed by another resource" and the pipeline aborts.

### How I encountered this

I'm experimenting with custom deployment integrations and currently working on one for Azure Static Web Apps — provides a `PublishAsAzureStaticWebApp(...)` extension on `IResourceBuilder` that ships Vite apps via `@azure/static-web-apps-cli`.

The first time I ran `aspire publish` against an AppHost using `builder.AddViteApp(...).PublishAsAzureStaticWebApp()`, the pipeline failed with:

```
Build-only container resource(s) 'portal' are not consumed by another resource and won't participate in publish or deploy. Reference them from another resource, for example using 'PublishWithContainerFiles' or 'PublishWithStaticFiles', or suppress this validation for the app by calling 'builder.Pipeline.DisableBuildOnlyContainerValidation()'.
```

I traced the cause to `CreateDefaultJavaScriptAppBuilder`'s unconditional `PublishAsDockerFile` and the `HasEntrypoint` toggle. My options were:

1. Call Aspire's `PublishAsStaticWebsite` from inside my own extension — but that forces the YARP container path (build inside Docker, ship YARP runtime image, expose port 5000), which is wrong for Azure SWA (the SWA service uploads `dist/` directly, no container is pushed or pulled).
2. Set `JavaScriptPublishModeAnnotation` myself — blocked because the annotation type is internal.
3. Call `builder.Pipeline.DisableBuildOnlyContainerValidation()` — works but app-wide (see workaround comparison below).
4. Strip the docker annotations on the affected resource — works, surgical, but brittle.

I went with (4). The relevant code in my integration:

```csharp
private static void StripDockerAnnotations(ResourceAnnotationCollection annotations)
{
var dockerAnnotationTypeNames = new[]
{
"Aspire.Hosting.ApplicationModel.DockerfileBuildAnnotation",
"Aspire.Hosting.ApplicationModel.ContainerImageAnnotation",
"Aspire.Hosting.ApplicationModel.DockerfileBuilderCallbackAnnotation",
"Aspire.Hosting.ApplicationModel.ContainerBuildOptionsCallbackAnnotation",
"Aspire.Hosting.ApplicationModel.ContainerFilesSourceAnnotation",
};

foreach (var annotation in annotations.Where(a => dockerAnnotationTypeNames.Contains(a.GetType().FullName)).ToList())
{
annotations.Remove(annotation);
}
}
```

It works, but it's brittle (annotation type names as strings) and fights the JS integration rather than cooperating with it.

### Expected Behavior

Either:

1. `JavaScriptPublishModeAnnotation` and `JavaScriptPublishMode` are made `public`, so downstream packages can attach the annotation and tell the JS integration "this resource is a static-website / node-server / etc.".
2. OR a new public extension method like `WithJavaScriptPublishMode(JavaScriptPublishMode mode)` on `IResourceBuilder` is exposed, allowing downstream packages to opt in without the rest of `PublishAsStaticWebsite`'s YARP-specific machinery.
3. OR `JavaScriptPublishMode` gains a `Custom` / `Skip` variant for downstream-owned publish targets that suppress the auto-Dockerfile.

The current state forces downstream packages into the fragile annotation-stripping workaround whenever the desired publish target isn't one of the four blessed modes.

### Steps To Reproduce

Conceptually:

```csharp
// External package (not Aspire.Hosting.JavaScript)
public static IResourceBuilder PublishAsMyCustomTarget(this IResourceBuilder builder)
where T : JavaScriptAppResource
{
// I want to mark this resource as having a non-Docker publish target.
// I can't:
// - Set JavaScriptPublishModeAnnotation (internal)
// - Use JavaScriptPublishMode enum (internal)
// So I'm forced to strip the auto-attached DockerfileBuildAnnotation etc.
return builder;
}
```

End-user code:

```csharp
builder.AddViteApp("portal", "/path/to/portal")
.WithPnpm()
.PublishAsMyCustomTarget(); // downstream package — wants non-Docker publish

builder.Build().Run(); // aspire publish: fails on validate-build-only-container-references
```

### Exceptions (if any)

```
Aspire.Hosting.DistributedApplicationException: Build-only container resource(s) 'portal' are not consumed by another resource and won't participate in publish or deploy. Reference them from another resource, for example using 'PublishWithContainerFiles' or 'PublishWithStaticFiles', or suppress this validation for the app by calling 'builder.Pipeline.DisableBuildOnlyContainerValidation()'.
at Aspire.Hosting.Pipelines.DistributedApplicationPipeline.ValidateBuildOnlyContainerReferences(DistributedApplicationModel model) in /_/src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs:line 451
```

### Aspire doctor output

```
Aspire Environment Check
========================

.NET SDK
✅ .NET 10.0.300 installed (x64)

Container Runtime
✅ Podman v5.8.2: running (auto-detected (only runtime running)) ← active

Environment
⚠️ HTTPS development certificate is only partially trusted
Set SSL_CERT_DIR in your shell profile: export SSL_CERT_DIR="$SSL_CERT_DIR:/home/kennethhoff/.aspnet/dev-certs/trust"
See: https://aka.ms/aspire-prerequisites#dev-certs
Details: The certificate is in the trusted store, but SSL_CERT_DIR is not configured to include
'/home/kennethhoff/.aspnet/dev-certs/trust'. Some applications may not trust the certificate.
'aspire run' will configure this automatically.

Summary: 2 passed, 1 warnings, 0 failed
```

(The dev-cert warning is unrelated to this bug — NixOS environment quirk; the certificate is trusted, just at a non-standard path.)

### Anything else?

- Aspire version: 13.3.5
- .NET SDK: 10.0.300 x64
- OS: Linux (NixOS dev shell)

#### Workaround comparison

The error message suggests `builder.Pipeline.DisableBuildOnlyContainerValidation()` as a workaround. I verified its implementation in [`DistributedApplicationPipelineExtensions.cs`](https://github.com/microsoft/aspire/blob/v13.3.5/src/Aspire.Hosting/Pipelines/DistributedApplicationPipelineExtensions.cs):

```csharp
public static IDistributedApplicationPipeline DisableBuildOnlyContainerValidation(this IDistributedApplicationPipeline pipeline)
{
pipeline.AddPipelineConfiguration(static context =>
{
var validationStep = context.Steps.SingleOrDefault(step =>
step.Name == DistributedApplicationPipeline.ValidateBuildOnlyContainerReferencesStepName);
validationStep?.RequiredBySteps.Clear();
return Task.CompletedTask;
});
return pipeline;
}
```

It clears `RequiredBySteps` on the single shared `validate-build-only-container-references` step, taking it out of the DAG entirely. Per Aspire's own xmldoc: *"This is an **application-wide** escape hatch for scenarios where the build-only container validation is too restrictive for a particular app."*

This is too coarse for a downstream package's needs:

| | Strip annotations (what I do) | `DisableBuildOnlyContainerValidation()` |
|---|---|---|
| Scope | Per-resource | App-wide |
| Other build-only containers in the same AppHost | Still validated | Validation off |
| User-facing surprises | None | Possible — silent orphan containers, unrelated to my integration, slip through |
| Code in downstream package | Brittle (string-typed annotation lookups) | One line |
| Cooperation with JS integration | None — undoes work the JS integration just did | None — neutralises an Aspire-internal safety net |

A public `JavaScriptPublishModeAnnotation` / `WithJavaScriptPublishMode(...)` would let downstream packages express intent cleanly, instead of either fighting the JS integration (strip) or globally weakening an Aspire safety net (disable).

#### Related observation

Even with `JavaScriptPublishMode.StaticWebsite` reachable from outside, `PublishAsStaticWebsite` itself isn't reusable for Azure SWA because it forces the YARP `wwwroot` container image at runtime — appropriate for self-hosted static-site scenarios, not for the SWA service which ingests `dist/` directly via its REST API. The `WithJavaScriptPublishMode(...)` proposal above would solve this generally; a downstream package would set the publish-mode annotation but skip the YARP container scaffolding.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.