Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
265b4c1
feat(adminer): add Go AppHost example
aaronpowell Nov 11, 2026
32756d4
feat: add six Go AppHost examples
aaronpowell Nov 11, 2026
0c8803c
feat: add five more Go AppHost examples
aaronpowell Nov 11, 2026
ccce2b9
feat: add Go messaging AppHost examples
aaronpowell Nov 11, 2026
07404cb
feat(examples): add five Go AppHost counterparts
aaronpowell Nov 11, 2026
a43242c
feat(examples): add Loki and Tempo Go AppHosts
aaronpowell Nov 11, 2026
4aed0fb
feat(examples): add GoFeatureFlag and KurrentDB Go AppHosts
aaronpowell Nov 11, 2026
1ca4582
feat(examples): add Logto Go AppHost
aaronpowell Nov 11, 2026
4a58902
feat(examples): add SFTP Go AppHost
aaronpowell Nov 11, 2026
9871ebc
feat(examples): add Solr Go AppHost
aaronpowell Nov 11, 2026
d84e31f
feat(examples): add Listmonk Go AppHost
aaronpowell Nov 11, 2026
5705874
docs: document Meilisearch Go SDK blocker
aaronpowell Nov 11, 2026
15b747e
feat(examples): add MySQL extensions Go AppHost
aaronpowell Nov 11, 2026
f40c9e1
docs: document Redis Go SDK blocker
aaronpowell Nov 11, 2026
8f0bd6c
docs: document SQL Server Go SDK blocker
aaronpowell Nov 11, 2026
64e44e7
docs: document MCP Inspector Go SDK blocker
aaronpowell Nov 11, 2026
072ee91
test: cover affected Go AppHost test selection
aaronpowell Nov 11, 2026
9d2b5b9
docs: record additional Go AppHost blockers
aaronpowell Nov 11, 2026
c1d9073
test(k3s): validate existing Go AppHost parity
aaronpowell Nov 11, 2026
2caf2cf
feat(examples): add first batch of Go AppHost counterparts
aaronpowell Nov 11, 2026
257aefe
feat(examples): add third batch of Go AppHosts
aaronpowell Nov 11, 2026
bbd10ce
test: register Go AppHost batch coverage
aaronpowell Nov 11, 2026
2c68a27
feat(examples): add second batch of Go AppHost counterparts
aaronpowell Nov 11, 2026
d55cf3d
fix(examples): check Go endpoint wrapper errors
aaronpowell Nov 11, 2026
ad4220a
docs: clarify Go model validation gaps
aaronpowell Nov 11, 2026
bf20eee
Merge main into Go AppHost parity branch
aaronpowell Nov 11, 2026
58c83e5
Merge branch 'main' into aaronpowell-go-apphost-example-parity
aaronpowell Nov 12, 2026
9085b97
Fix SQL polyglot AppHost test restore environment (#2529)
aaronpowell Nov 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/setup-runtimes-caching/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ runs:
- uses: actions/setup-go@v7
name: Set up Go
with:
go-version: "^1.25.4"
go-version: "^1.26"
cache-dependency-path: |
./examples/golang/gin-api/go.sum

Expand Down
80 changes: 80 additions & 0 deletions docs/go-apphost-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Go AppHost parity gaps

These limitations were observed with Aspire SDK
`17.1.0-preview.1.26559.8`. The repository now pins
`17.0.0-preview.1.26555.11`; the ActiveMQ, Flare, Keycloak, and Zitadel
failures have also been confirmed with that version. The other cases have not
yet been revalidated against it:

- **RustFs:** `AddRustFsOptions.AccessKey` and `SecretKey` are generated as
`*aspire.ParameterResource` (pointers to interfaces). Passing addresses of
the two parameter resources compiles, but the generated `serializeValue`
does not recognize those pointers as resource handles. During AppHost model
construction, `addRustFs` fails with `INVALID_ARGUMENT`: `Parameter type
'IResourceBuilder\`1' must have [AspireDto] attribute to be deserialized from
JSON` for `accessKey`. The TypeScript example explicitly supplies both
credentials, so omitting them would not preserve its resource graph.
- **Umami:** `AddUmamiOptions.Secret` is likewise a `*aspire.ParameterResource`.
Passing the address of the explicitly configured `umami-secret` compiles,
but `aspire publish --apphost apphost.go --list-steps --non-interactive`
fails while constructing the model: `CommunityToolkit.Aspire.Hosting.Umami/addUmami`
reports `INVALID_ARGUMENT` for `secret`, with the same `IResourceBuilder\`1`
deserialization error as RustFs. Omitting the secret would not preserve the
TypeScript example.
- **Meilisearch:** `AddMeilisearchOptions.MasterKey` is also a pointer to a
parameter interface. An explicit `search-master-key` compiles but
`CommunityToolkit.Aspire.Hosting.Meilisearch/addMeilisearch` fails during
publish-model construction with `INVALID_ARGUMENT` for `masterKey` and
the same `IResourceBuilder\`1` deserialization error. The TypeScript
example explicitly supplies the key.
- **Redis extensions:** `AddRedisOptions.Password` is a pointer to a parameter
interface. Passing the explicit `redis-password` compiles, but
`Aspire.Hosting.Redis/addRedis` fails during publish-model construction with
`INVALID_ARGUMENT` for `password` and the same `IResourceBuilder\`1`
deserialization error. Generating a different password would change the
TypeScript example.
- **SQL Server extensions:** `AddSqlServerOptions.Password` has the same
pointer-to-parameter shape. The explicit `sql-password` compiles, but
`Aspire.Hosting.SqlServer/addSqlServer` reports `INVALID_ARGUMENT` for
`password` and the same `IResourceBuilder\`1` deserialization error during
publish-model construction. Omitting it changes the TypeScript example.
- **MCP Inspector:** `AddMcpInspectorOptions.ProxyToken` is a pointer to a
parameter interface. Passing the explicit `proxy-token` compiles, but
`CommunityToolkit.Aspire.Hosting.McpInspector/addMcpInspectorPolyglot` fails
during publish-model construction with `INVALID_ARGUMENT` for
`proxyToken` and the same `IResourceBuilder\`1` deserialization error.
- **Zitadel:** `AddZitadelOptions.Username`, `Password`, and `MasterKey` are
optional parameter-resource pointers. Supplying the credentials used by the
TypeScript example fails `CommunityToolkit.Aspire.Hosting.Zitadel/addZitadel`
with `INVALID_ARGUMENT` for `username` due to the same pointer-to-interface
serialization defect. Omitting them would change the example.
- **ActiveMQ:** `AddActiveMQOptions.UserName` is a pointer to a parameter
interface. `addActiveMQ` fails with `INVALID_ARGUMENT` for `userName` during
model construction when the TypeScript example's explicit credentials are
supplied. Its Go counterpart retains those credentials and is compile-tested.
- **Flare:** `WithPublicUrlsOptions.DashboardUrl` is a pointer to a parameter
interface. Passing the example's `flare-dashboard-url` causes
`CommunityToolkit.Aspire.Hosting.Flare/withPublicUrls` to fail with
`INVALID_ARGUMENT` for `dashboardUrl`; omitting it changes the configured
public URL.
- **Keycloak extensions:** The example's PostgreSQL `userName` and `password`
are parameter handles supplied to `addPostgres`. The generated Go options use
parameter-interface pointers; `Aspire.Hosting.PostgreSQL/addPostgres` fails
with `INVALID_ARGUMENT` for `userName`. Replacing the explicit credentials
changes the example.

The Python extensions Go example runs, but its publish-mode model fails:
`AddStreamlitApp` removes the Python resource before adding its Streamlit
replacement, while the publish-mode executable container with the same name
remains in the model. The resulting duplicate `dashboard` resource causes
`AddStreamlitApp` to fail. Its Go compilation test therefore does not run the
publish-model check; fixing the integration is outside this example-only work.

Floci's `WithServiceBus` is run-only by design and throws in publish mode, as
documented in the Floci README. Validate the example's Service Bus graph in run
mode rather than treating its publish-mode rejection as a Go SDK parity gap.

On Windows, the SQL Database Projects Go example compiles but its model fails
when `MSBuildLocator.RegisterDefaults()` cannot load native `hostfxr`. Its
publish-model test is disabled on Windows; this is an environment/native-loading
issue, not the parameter serialization issue above.
4 changes: 3 additions & 1 deletion docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ If you prefer not to use `devcontainer`, you can manually set up your developmen
- You'll also need [Gradle](https://gradle.org/install/) and [Apache Maven](https://maven.apache.org/download.cgi)
- [Bun](https://bun.sh)
- [Deno 2](https://deno.land/)
- [Go 1.23](https://golang.org/)
- [Go 1.26 or later](https://go.dev/dl/) (required for Go AppHosts)
- [Python 3](https://www.python.org/downloads/)
- [Uvicorn](https://www.uvicorn.org/)
- [Rust](https://www.rust-lang.org/tools/install)
Expand All @@ -44,3 +44,5 @@ The repository currently targets Aspire **17.0.0-preview.1.26555.11**, an unsupp
The C# AppHosts that host .NET projects now reference `Aspire.Hosting.Dotnet` and use `AddDotnetProject`; the TypeScript equivalents use `addDotnetProject`. `Aspire.Hosting.Dotnet` is centrally pinned to the same nightly version. The AppHosts still use `Aspire.AppHost.Sdk` for staged compatibility; Aspire 17 emits `ASPIRE012` for that obsolete SDK, and migrating to explicit `Aspire.Hosting.AppHost` references remains separate from this project-resource migration.

Install the matching CLI with `aspire.dev`'s installer and its explicit version option (`-Version 17.0.0-preview.1.26555.11` on Windows or `--version 17.0.0-preview.1.26555.11` on Linux/macOS), rather than selecting a moving daily channel. See the [upstream daily-build guide](https://github.com/microsoft/aspire/blob/main/docs/using-latest-daily.md) for installer instructions.

Go AppHosts are a preview feature. In a Go AppHost directory, run `aspire restore` to generate the SDK in `.aspire/modules`, then `go test ./...` to check compilation. Run the application with `aspire run`, rather than `go run`, so the CLI starts the AppHost server.
18 changes: 13 additions & 5 deletions eng/testing/select-affected-tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public static async Task<int> MainAsync(string[] args)
var packageToTests = BuildPackageToTests(packageRefs, nodeToTests);
var packageToProjects = BuildPackageToProjects(packageRefs);
var testInfraPackages = LoadTestInfraPackages(repoRoot);
var typeScriptAppHostToTests = BuildTypeScriptAppHostToTests(repoRoot, projectPaths);
var typeScriptAppHostToTests = BuildAppHostToTests(repoRoot, projectPaths, "TypeScriptAppHostTests.cs");
var goAppHostToTests = BuildAppHostToTests(repoRoot, projectPaths, "GoAppHostTests.cs");

foreach (var filePath in diffFiles)
{
Expand Down Expand Up @@ -165,13 +166,20 @@ public static async Task<int> MainAsync(string[] args)
continue;
}

if (TryGetTypeScriptAppHostTests(filePath, typeScriptAppHostToTests, out impactedTypeScriptTests))
if (TryGetAppHostTests(filePath, typeScriptAppHostToTests, out impactedTypeScriptTests))
{
selected.UnionWith(impactedTypeScriptTests);
reasons.Add($"Selected {impactedTypeScriptTests.Count} tests because {filePath} belongs to a TypeScript app host test fixture.");
continue;
}

if (TryGetAppHostTests(filePath, goAppHostToTests, out var impactedGoTests))
{
selected.UnionWith(impactedGoTests);
reasons.Add($"Selected {impactedGoTests.Count} tests because {filePath} belongs to a Go app host test fixture.");
continue;
}

if (TryGetExampleTests(filePath, projectPaths, nodeToTests, out var exampleName, out var impactedExampleTests))
{
selected.UnionWith(impactedExampleTests);
Expand Down Expand Up @@ -521,7 +529,7 @@ private static Dictionary<string, HashSet<string>> BuildPackageToProjects(Dictio
return appHostProjects.Count == 1 ? appHostProjects[0] : null;
}

private static Dictionary<string, HashSet<string>> BuildTypeScriptAppHostToTests(string repoRoot, List<string> projectPaths)
private static Dictionary<string, HashSet<string>> BuildAppHostToTests(string repoRoot, List<string> projectPaths, string testFileName)
{
var appHostToTests = new Dictionary<string, HashSet<string>>(StringComparer.Ordinal);
var testsRoot = Path.Combine(repoRoot, "tests");
Expand All @@ -530,7 +538,7 @@ private static Dictionary<string, HashSet<string>> BuildTypeScriptAppHostToTests
return appHostToTests;
}

foreach (var testFilePath in Directory.EnumerateFiles(testsRoot, "TypeScriptAppHostTests.cs", SearchOption.AllDirectories).OrderBy(static path => path, StringComparer.Ordinal))
foreach (var testFilePath in Directory.EnumerateFiles(testsRoot, testFileName, SearchOption.AllDirectories).OrderBy(static path => path, StringComparer.Ordinal))
{
var relativeTestFilePath = RelativePath(repoRoot, testFilePath);
var testProjectPath = NearestProject(relativeTestFilePath, projectPaths);
Expand Down Expand Up @@ -560,7 +568,7 @@ private static Dictionary<string, HashSet<string>> BuildTypeScriptAppHostToTests
return appHostToTests;
}

private static bool TryGetTypeScriptAppHostTests(
private static bool TryGetAppHostTests(
string filePath,
Dictionary<string, HashSet<string>> appHostToTests,
out HashSet<string> tests)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# ActiveMQ Go AppHost

This is the Go counterpart of the adjacent TypeScript AppHost. It configures
Classic and Artemis brokers, explicit credentials, ports, volumes, bind mounts,
and exported endpoint properties.

Use the Aspire 17 preview CLI matching `aspire.config.json` and Go 1.26 or later.
From this directory:

```shell
aspire restore --non-interactive
go build ./...
go vet ./...
aspire start --isolated --non-interactive
```

Running the brokers requires Docker.

## Preview SDK limitation

With Aspire `17.1.0-preview.1.26559.8`, compilation succeeds but model execution
fails at `addActiveMQ` with `INVALID_ARGUMENT` for `userName`. The generated Go
options use `*ParameterResource` for optional resource parameters, but their
serializer does not unwrap the pointer to the interface into a resource handle.
The explicit credentials are retained for parity rather than removing them or
modifying generated SDK files.
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package main

import (
"apphost/modules/aspire"
"log"
"os"
"path/filepath"
)

func main() {
builder, err := aspire.CreateBuilder()
check(err)

bindMountRoot, err := os.MkdirTemp("", "activemq-polyglot-")
check(err)
artemisDataPath := filepath.Join(bindMountRoot, "artemis-data")
artemisConfPath := filepath.Join(bindMountRoot, "artemis-conf")
check(os.MkdirAll(artemisDataPath, 0755))
check(os.MkdirAll(artemisConfPath, 0755))

password := builder.AddParameter("mq-password", &aspire.AddParameterOptions{
Value: aspire.StringPtr("admin"), Secret: aspire.BoolPtr(true),
})
user := builder.AddParameter("mq-user", &aspire.AddParameterOptions{
Value: aspire.StringPtr("admin"), PublishValueAsDefault: aspire.BoolPtr(true),
})
check(password.Err())
check(user.Err())

classic := builder.AddActiveMQ("classic", &aspire.AddActiveMQOptions{
UserName: &user, Password: &password, Port: aspire.Float64Ptr(36161),
Scheme: aspire.StringPtr("activemq"), WebPort: aspire.Float64Ptr(38161),
}).WithDataVolume(&aspire.WithDataVolumeOptions{Name: aspire.StringPtr("classic-data")}).
WithConfVolume(&aspire.WithConfVolumeOptions{Name: aspire.StringPtr("classic-conf")})
classic2 := builder.AddActiveMQ("classic2", &aspire.AddActiveMQOptions{
UserName: &user, Password: &password,
})
artemis := builder.AddActiveMQArtemis("artemis", &aspire.AddActiveMQArtemisOptions{
UserName: &user, Password: &password, Port: aspire.Float64Ptr(36162),
Scheme: aspire.StringPtr("tcp"), WebPort: aspire.Float64Ptr(38162),
}).WithDataBindMount(artemisDataPath).WithConfBindMount(artemisConfPath)
artemis2 := builder.AddActiveMQArtemis("artemis2", &aspire.AddActiveMQArtemisOptions{
UserName: &user, Password: &password,
}).WithDataVolume().WithConfVolume()

check(classic.Err())
check(classic2.Err())
check(artemis.Err())
check(artemis2.Err())
check(classic.PrimaryEndpoint().Err())
check(classic.Host().Err())
check(classic.Port().Err())
_ = classic.UriExpression()
_ = classic.ConnectionStringExpression()
check(classic2.PrimaryEndpoint().Err())
check(classic2.Host().Err())
check(classic2.Port().Err())
check(artemis.PrimaryEndpoint().Err())
check(artemis.Host().Err())
check(artemis.Port().Err())
_ = artemis.UriExpression()
_ = artemis.ConnectionStringExpression()
check(artemis2.PrimaryEndpoint().Err())
check(artemis2.Host().Err())
check(artemis2.Port().Err())

app, err := builder.Build()
check(err)
check(app.Run())
}

func check(err error) {
if err != nil {
log.Fatal(aspire.FormatError(err))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"appHost": {
"path": "apphost.go",
"language": "go"
},
"sdk": {
"version": "17.0.0-preview.1.26555.11"
},
"packages": {
"CommunityToolkit.Aspire.Hosting.ActiveMQ": "../../../src/CommunityToolkit.Aspire.Hosting.ActiveMQ/CommunityToolkit.Aspire.Hosting.ActiveMQ.csproj"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module apphost

go 1.26

require apphost/modules/aspire v0.0.0

replace apphost/modules/aspire => ./.aspire/modules
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package main

import (
"apphost/modules/aspire"
"log"
)

func main() {
builder, err := aspire.CreateBuilder()
if err != nil {
log.Fatal(aspire.FormatError(err))
}

adminer := builder.AddAdminer("adminer", &aspire.AddAdminerOptions{
Port: aspire.Float64Ptr(18080),
}).WithHostPort(aspire.Float64Ptr(18081))
if err := adminer.Err(); err != nil {
log.Fatal(aspire.FormatError(err))
}

app, err := builder.Build()
if err != nil {
log.Fatal(aspire.FormatError(err))
}
if err := app.Run(); err != nil {
log.Fatal(aspire.FormatError(err))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"appHost": {
"path": "apphost.go",
"language": "go"
},
"sdk": {
"version": "17.0.0-preview.1.26555.11"
},
"packages": {
"CommunityToolkit.Aspire.Hosting.Adminer": "../../../src/CommunityToolkit.Aspire.Hosting.Adminer/CommunityToolkit.Aspire.Hosting.Adminer.csproj"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module apphost

go 1.26

require apphost/modules/aspire v0.0.0

replace apphost/modules/aspire => ./.aspire/modules
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Azure extensions Go AppHost

This is the Go counterpart of the adjacent TypeScript AppHost. It configures
Azurite with blob, queue, and table ports 27000-27002, a data volume, and Azure
Storage Explorer for the blob resource.

Use the Aspire 17 preview CLI matching `aspire.config.json` and Go 1.26 or later.
From this directory:

```shell
aspire restore --non-interactive
go build ./...
go vet ./...
aspire start --isolated --non-interactive
```

Running the emulator and explorer requires Docker. `aspire publish
--non-interactive` validates the publish model without provisioning Azure
resources.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package main

import (
"apphost/modules/aspire"
"log"
)

func main() {
builder, err := aspire.CreateBuilder()
check(err)

storage := builder.AddAzureStorage("azure-storage").RunAsEmulator(&aspire.RunAsEmulatorOptions{
ConfigureContainer: func(azurite aspire.AzureStorageEmulatorResource) {
azurite.WithArgs([]string{"--disableProductStyleUrl"}).
WithBlobPort(27000).
WithQueuePort(27001).
WithTablePort(27002).
WithDataVolume(&aspire.WithDataVolumeOptions{Name: aspire.StringPtr("storage")})
check(azurite.Err())
},
})
check(storage.Err())
blobs := storage.AddBlobs("blobs").WithAzureStorageExplorer()
check(blobs.Err())

app, err := builder.Build()
check(err)
check(app.Run())
}

func check(err error) {
if err != nil {
log.Fatal(aspire.FormatError(err))
}
}
Loading
Loading