From a2ae944bd8ff629bd538177657205dbad6f55e6d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 04:54:21 +0000 Subject: [PATCH 1/5] Add TerraformRawExpression raw-HCL escape-hatch for cross-language expressions --- .../README.md | 27 ++++ .../TerraformProvisioningResource.cs | 2 + .../TerraformPublishingContext.cs | 5 +- .../TerraformRawExpression.cs | 101 +++++++++++++++ .../TerraformRawExpressionTests.cs | 117 ++++++++++++++++++ 5 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 src/EmmittJ.Aspire.Hosting.Terraform/TerraformRawExpression.cs create mode 100644 tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformRawExpressionTests.cs diff --git a/src/EmmittJ.Aspire.Hosting.Terraform/README.md b/src/EmmittJ.Aspire.Hosting.Terraform/README.md index 0a77023be..30f92c1b5 100644 --- a/src/EmmittJ.Aspire.Hosting.Terraform/README.md +++ b/src/EmmittJ.Aspire.Hosting.Terraform/README.md @@ -227,6 +227,33 @@ builder.AddProject("api") }); ``` +### Cross-language expression contract + +This integration reuses **Aspire's own expression system** as the language-neutral contract for wiring Terraform infrastructure, instead of introducing a parallel expression IR. Everything is lowered host-side into the C# Terraform AST: + +- **References & wiring** — resource-to-resource edges flow through Aspire value providers (`TerraformOutputReference`, `ReferenceExpression`) and are lowered by `ResolveValueProvider` / `ResolveReferenceExpression`. +- **External inputs** — Aspire `ParameterResource` values are surfaced as Terraform `variable` blocks (secret parameters are passed via `TF_VAR_*`, non-secret via `aspire.auto.tfvars`). + +#### Raw-HCL escape-hatch (`TerraformRawExpression`) + +Aspire's expression algebra is interpolation/concatenation only, so Terraform's *structured* operators (conditionals, `for` expressions, function calls, index/key access, and symbolic references such as `module.x.output`) have no direct counterpart. `TerraformRawExpression` is the escape-hatch: it is an Aspire value provider (`IValueProvider` + `IManifestExpressionProvider`) that carries a verbatim HCL fragment and renders **unquoted** (symbolic), instead of being emitted as a quoted string literal. + +Because it is a value provider, it can be embedded directly inside a `ReferenceExpression` hole and flows through the existing lowering: + +```csharp +// Inject a symbolic Terraform reference through the Aspire expression system. +var vnetId = TerraformRawExpression.Create("module.network.vnet_id"); + +app.PublishAsTerraform(infra => +{ + var value = infra.ResolveValueProvider(vnetId); // renders: module.network.vnet_id (unquoted) +}); +``` + +A plain `string` is treated as data and is quoted; use `TerraformRawExpression` only when an expression cannot be represented by the typed value providers. The contained HCL is rendered as-is and is not validated or escaped. + +> **Note:** The Aspire Type System (ATS) attributes used for generating non-C# SDKs (`[AspireExport]` / `[AspireDto]`) are not present in the Aspire version this integration targets (`13.0.0`). The cross-language *contract* above (value providers + the raw-HCL escape-hatch) is in place; surfacing it via ATS annotations is deferred until those attributes are available. + ### Publish-Only Execution Terraform file generation **only occurs during publish mode** (`aspire publish`), not during `dotnet run`. This ensures: diff --git a/src/EmmittJ.Aspire.Hosting.Terraform/TerraformProvisioningResource.cs b/src/EmmittJ.Aspire.Hosting.Terraform/TerraformProvisioningResource.cs index 5eea75881..9a5550172 100644 --- a/src/EmmittJ.Aspire.Hosting.Terraform/TerraformProvisioningResource.cs +++ b/src/EmmittJ.Aspire.Hosting.Terraform/TerraformProvisioningResource.cs @@ -954,6 +954,7 @@ public TerraformValue ResolveReferenceExpression(ReferenceExpression exp /// /// /// : Returned as-is + /// : Lowered to a verbatim (unquoted) HCL expression /// : Resolved via /// : Resolved via /// : Resolved via @@ -978,6 +979,7 @@ public TerraformValue ResolveValueProvider(object value) return value switch { string s => s, + TerraformRawExpression raw => raw.ToTerraformValue(), EndpointReference ep => ResolveEndpointReference(ep).ToReference(), EndpointReferenceExpression epExpr => ResolveEndpointReferenceExpression(epExpr), ParameterResource param => AddVariable(param).ToReference(), diff --git a/src/EmmittJ.Aspire.Hosting.Terraform/TerraformPublishingContext.cs b/src/EmmittJ.Aspire.Hosting.Terraform/TerraformPublishingContext.cs index aa3b0b27a..17a8237ee 100644 --- a/src/EmmittJ.Aspire.Hosting.Terraform/TerraformPublishingContext.cs +++ b/src/EmmittJ.Aspire.Hosting.Terraform/TerraformPublishingContext.cs @@ -326,6 +326,9 @@ private object ResolveInputValue(object inputValue) ContainerImageReference imageRef => ResolveContainerImageReference(imageRef), + TerraformRawExpression raw => + TerraformExpression.Raw(raw.Hcl), + string s => TerraformExpression.Literal(s), @@ -334,7 +337,7 @@ private object ResolveInputValue(object inputValue) _ => throw new NotSupportedException( $"Input value type '{inputValue.GetType().Name}' is not supported for module parameters. " + - $"Supported types: TerraformOutputReference, ParameterResource, ContainerImageReference, string, TerraformExpression") + $"Supported types: TerraformOutputReference, ParameterResource, ContainerImageReference, TerraformRawExpression, string, TerraformExpression") }; } diff --git a/src/EmmittJ.Aspire.Hosting.Terraform/TerraformRawExpression.cs b/src/EmmittJ.Aspire.Hosting.Terraform/TerraformRawExpression.cs new file mode 100644 index 000000000..b177c0dad --- /dev/null +++ b/src/EmmittJ.Aspire.Hosting.Terraform/TerraformRawExpression.cs @@ -0,0 +1,101 @@ +// Licensed under the MIT License. + +using Aspire.Hosting.ApplicationModel; +using EmmittJ.Terraform.Sdk; + +namespace EmmittJ.Aspire.Hosting.Terraform; + +/// +/// An Aspire value provider that carries a raw, verbatim HCL expression fragment. +/// +/// +/// +/// This type is the cross-language "escape-hatch" for Terraform's structured expression operators +/// (conditionals, for expressions, function calls, index/key access, references such as +/// module.x.output) that have no direct counterpart in Aspire's interpolation/concatenation-based +/// algebra. +/// +/// +/// Because it implements and , +/// it can be embedded directly inside a hole and flows through the +/// existing host-side lowering ( and +/// TerraformPublishingContext.ResolveInputValue). When lowered, the fragment is rendered +/// verbatim and unquoted via , so a reference like +/// module.network.vnet_id stays symbolic in the generated HCL instead of being emitted as a +/// quoted string literal. +/// +/// +/// The contained HCL is rendered as-is and is not validated or escaped. Prefer the typed value +/// providers (, ) where possible; +/// use this type only when an expression cannot otherwise be represented. +/// +/// +/// +/// +/// // Inject a symbolic Terraform reference through the Aspire expression system. +/// var moduleId = TerraformRawExpression.Create("module.network.vnet_id"); +/// +/// app.PublishAsTerraform(infra => +/// { +/// var value = infra.ResolveValueProvider(moduleId); // renders: module.network.vnet_id (unquoted) +/// }); +/// +/// +public sealed class TerraformRawExpression + : IManifestExpressionProvider, IValueProvider, IEquatable +{ + /// + /// Initializes a new instance of the class. + /// + /// The raw HCL expression fragment to render verbatim. + /// Thrown when is null or empty. + public TerraformRawExpression(string hcl) + { + ArgumentException.ThrowIfNullOrEmpty(hcl); + Hcl = hcl; + } + + /// + /// Gets the raw HCL expression fragment. + /// + public string Hcl { get; } + + /// + /// Creates a new from the specified HCL fragment. + /// + /// The raw HCL expression fragment to render verbatim. + /// A wrapping . + public static TerraformRawExpression Create(string hcl) => new(hcl); + + /// + /// Lowers this raw expression into the Terraform AST as a verbatim (unquoted) value. + /// + /// A that renders as the raw HCL fragment. + public TerraformValue ToTerraformValue() => TerraformExpression.Raw(Hcl); + + /// + /// Gets the manifest expression for this raw fragment, which is the verbatim HCL. + /// + public string ValueExpression => Hcl; + + /// + /// Gets the value of this raw expression. Returns the verbatim HCL fragment. + /// + /// A token to observe while waiting for the task to complete. + /// The raw HCL fragment. + public ValueTask GetValueAsync(CancellationToken cancellationToken = default) + => new(Hcl); + + /// + public bool Equals(TerraformRawExpression? other) + => other is not null && string.Equals(Hcl, other.Hcl, StringComparison.Ordinal); + + /// + public override bool Equals(object? obj) => Equals(obj as TerraformRawExpression); + + /// + public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Hcl); + + /// + public override string ToString() => Hcl; +} diff --git a/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformRawExpressionTests.cs b/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformRawExpressionTests.cs new file mode 100644 index 000000000..6cf8b5e78 --- /dev/null +++ b/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformRawExpressionTests.cs @@ -0,0 +1,117 @@ +// Licensed under the MIT License. + +using Aspire.Hosting.ApplicationModel; +using EmmittJ.Terraform.Sdk; + +namespace EmmittJ.Aspire.Hosting.Terraform.Tests; + +/// +/// Tests for , the cross-language raw-HCL escape-hatch, +/// and its lowering through the Aspire expression system into the Terraform AST. +/// +public class TerraformRawExpressionTests +{ + private static string Render(TerraformValue value) + { + var context = TerraformContext.Temporary(); + return string.Concat(value.ResolveNodes(context).Select(node => node.ToHcl(context))); + } + + private static TerraformProvisioningResource CreateProvisioningResource() + { + var builder = DistributedApplication.CreateBuilder(); + var environment = builder.AddTerraformEnvironment("env").Resource; + return new TerraformProvisioningResource("module", environment); + } + + [Fact] + public void Create_SetsHcl() + { + var raw = TerraformRawExpression.Create("module.network.vnet_id"); + + Assert.Equal("module.network.vnet_id", raw.Hcl); + } + + [Fact] + public void Constructor_ThrowsOnNullOrEmpty() + { + Assert.Throws(() => new TerraformRawExpression(null!)); + Assert.Throws(() => new TerraformRawExpression(string.Empty)); + } + + [Fact] + public void ValueExpression_ReturnsVerbatimHcl() + { + var raw = TerraformRawExpression.Create("var.enabled ? \"a\" : \"b\""); + + Assert.Equal("var.enabled ? \"a\" : \"b\"", ((IManifestExpressionProvider)raw).ValueExpression); + } + + [Fact] + public async Task GetValueAsync_ReturnsVerbatimHcl() + { + var raw = TerraformRawExpression.Create("module.network.vnet_id"); + + var value = await ((IValueProvider)raw).GetValueAsync(); + + Assert.Equal("module.network.vnet_id", value); + } + + [Fact] + public void Equality_IsBasedOnHcl() + { + var a = TerraformRawExpression.Create("module.x.id"); + var b = TerraformRawExpression.Create("module.x.id"); + var c = TerraformRawExpression.Create("module.y.id"); + + Assert.Equal(a, b); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + Assert.NotEqual(a, c); + } + + [Fact] + public void ToTerraformValue_RendersVerbatimUnquoted() + { + var raw = TerraformRawExpression.Create("module.network.vnet_id"); + + var rendered = Render(raw.ToTerraformValue()); + + // Symbolic reference must NOT be wrapped in quotes. + Assert.Equal("module.network.vnet_id", rendered); + } + + [Fact] + public void ResolveValueProvider_RawExpression_RendersSymbolic() + { + var infra = CreateProvisioningResource(); + var raw = TerraformRawExpression.Create("module.network.vnet_id"); + + var resolved = infra.ResolveValueProvider(raw); + + Assert.Equal("module.network.vnet_id", Render(resolved)); + } + + [Fact] + public void ResolveValueProvider_String_RendersQuotedLiteral() + { + var infra = CreateProvisioningResource(); + + var resolved = infra.ResolveValueProvider("module.network.vnet_id"); + + // A plain string is data, not an expression: it must be quoted. + Assert.Equal("\"module.network.vnet_id\"", Render(resolved)); + } + + [Fact] + public void ResolveValueProvider_RawExpression_EmbeddedInReferenceExpression_RendersSymbolic() + { + var infra = CreateProvisioningResource(); + var raw = TerraformRawExpression.Create("module.network.vnet_id"); + + // A single-hole ReferenceExpression carrying the raw provider resolves to the symbolic form. + var expression = ReferenceExpression.Create($"{raw}"); + var resolved = infra.ResolveValueProvider(expression); + + Assert.Equal("module.network.vnet_id", Render(resolved)); + } +} From 3139e83b13b14e349b9189189af316ef160b6221 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:30:05 +0000 Subject: [PATCH 2/5] Upgrade Aspire to 13.4.2 and migrate to IResourceContainerImageManager --- Directory.Packages.props | 10 +++++----- .../azure-container-apps/apphost/AppHost.csproj | 10 +++++----- playground/getting-started/GettingStarted.csproj | 4 ++-- src/EmmittJ.Aspire.Hosting.Terraform/README.md | 2 +- .../TerraformProvisioningResource.cs | 12 +++--------- .../EmmittJ.Terraform.Sdk.AppHost.csproj | 2 +- .../aspire-manifest.verified.json | 1 - .../aspire-manifest.verified.json | 1 - .../aspire-manifest.verified.json | 1 - .../aspire-manifest.verified.json | 1 - .../aspire-manifest.verified.json | 1 - 11 files changed, 17 insertions(+), 28 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index fd5226a50..f19c4876b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,11 +6,11 @@ - - - - - + + + + + diff --git a/playground/azure-container-apps/apphost/AppHost.csproj b/playground/azure-container-apps/apphost/AppHost.csproj index 5f66038ef..c2705dae6 100644 --- a/playground/azure-container-apps/apphost/AppHost.csproj +++ b/playground/azure-container-apps/apphost/AppHost.csproj @@ -1,6 +1,6 @@ - + Exe @@ -11,10 +11,10 @@ - - - - + + + + diff --git a/playground/getting-started/GettingStarted.csproj b/playground/getting-started/GettingStarted.csproj index 1d21bd2fd..2f7973092 100644 --- a/playground/getting-started/GettingStarted.csproj +++ b/playground/getting-started/GettingStarted.csproj @@ -1,6 +1,6 @@ - + Exe @@ -11,7 +11,7 @@ - + diff --git a/src/EmmittJ.Aspire.Hosting.Terraform/README.md b/src/EmmittJ.Aspire.Hosting.Terraform/README.md index 30f92c1b5..9bd877487 100644 --- a/src/EmmittJ.Aspire.Hosting.Terraform/README.md +++ b/src/EmmittJ.Aspire.Hosting.Terraform/README.md @@ -252,7 +252,7 @@ app.PublishAsTerraform(infra => A plain `string` is treated as data and is quoted; use `TerraformRawExpression` only when an expression cannot be represented by the typed value providers. The contained HCL is rendered as-is and is not validated or escaped. -> **Note:** The Aspire Type System (ATS) attributes used for generating non-C# SDKs (`[AspireExport]` / `[AspireDto]`) are not present in the Aspire version this integration targets (`13.0.0`). The cross-language *contract* above (value providers + the raw-HCL escape-hatch) is in place; surfacing it via ATS annotations is deferred until those attributes are available. +> **Note:** The Aspire Type System (ATS) attributes used for generating non-C# SDKs (`[AspireExport]` / `[AspireDto]`) are not present in the Aspire version this integration targets (`13.4.2`). The cross-language *contract* above (value providers + the raw-HCL escape-hatch) is in place; surfacing it via ATS annotations is deferred until those attributes are available. ### Publish-Only Execution diff --git a/src/EmmittJ.Aspire.Hosting.Terraform/TerraformProvisioningResource.cs b/src/EmmittJ.Aspire.Hosting.Terraform/TerraformProvisioningResource.cs index 9a5550172..3d3dbf107 100644 --- a/src/EmmittJ.Aspire.Hosting.Terraform/TerraformProvisioningResource.cs +++ b/src/EmmittJ.Aspire.Hosting.Terraform/TerraformProvisioningResource.cs @@ -268,17 +268,11 @@ private static async Task PushImageToRegistryAsync( IResource resource, PipelineStepContext context) { - var containerImageBuilder = context.Services.GetRequiredService(); + var containerImageManager = context.Services.GetRequiredService(); var registryEndpoint = await registry.Endpoint.GetValueAsync(context.CancellationToken).ConfigureAwait(false) ?? throw new InvalidOperationException("Failed to retrieve container registry endpoint."); - // Get the local image name (defaults to resource name) - if (!resource.TryGetContainerImageName(out var localImageName)) - { - localImageName = resource.Name.ToLowerInvariant(); - } - // Get the target tag using ContainerImageReference IValueProvider cir = new ContainerImageReference(resource); var targetTag = await cir.GetValueAsync(context.CancellationToken).ConfigureAwait(false); @@ -296,8 +290,8 @@ private static async Task PushImageToRegistryAsync( throw new InvalidOperationException($"Failed to get target tag for {resource.Name}"); } - await containerImageBuilder.TagImageAsync(localImageName, targetTag, context.CancellationToken).ConfigureAwait(false); - await containerImageBuilder.PushImageAsync(targetTag, context.CancellationToken).ConfigureAwait(false); + // Tags the local image with the registry-qualified remote name and pushes it. + await containerImageManager.PushImageAsync(resource, context.CancellationToken).ConfigureAwait(false); await pushTask.CompleteAsync( $"Successfully pushed **{resource.Name}** to `{targetTag}`", diff --git a/src/EmmittJ.Terraform.Sdk.AppHost/EmmittJ.Terraform.Sdk.AppHost.csproj b/src/EmmittJ.Terraform.Sdk.AppHost/EmmittJ.Terraform.Sdk.AppHost.csproj index 9503f6bb9..ab411ba81 100644 --- a/src/EmmittJ.Terraform.Sdk.AppHost/EmmittJ.Terraform.Sdk.AppHost.csproj +++ b/src/EmmittJ.Terraform.Sdk.AppHost/EmmittJ.Terraform.Sdk.AppHost.csproj @@ -1,5 +1,5 @@ - + Exe diff --git a/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.MultipleAnnotations_ContributeToSingleStack/aspire-manifest.verified.json b/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.MultipleAnnotations_ContributeToSingleStack/aspire-manifest.verified.json index 32d9fbb6a..f20dbf256 100644 --- a/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.MultipleAnnotations_ContributeToSingleStack/aspire-manifest.verified.json +++ b/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.MultipleAnnotations_ContributeToSingleStack/aspire-manifest.verified.json @@ -1,5 +1,4 @@ { - "$schema": "https://json.schemastore.org/aspire-8.0.json", "resources": { "terraform": { "error": "This resource does not support generation in the manifest." diff --git a/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.TerraformStack_GeneratesFileInDefaultLocation/aspire-manifest.verified.json b/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.TerraformStack_GeneratesFileInDefaultLocation/aspire-manifest.verified.json index 32d9fbb6a..f20dbf256 100644 --- a/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.TerraformStack_GeneratesFileInDefaultLocation/aspire-manifest.verified.json +++ b/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.TerraformStack_GeneratesFileInDefaultLocation/aspire-manifest.verified.json @@ -1,5 +1,4 @@ { - "$schema": "https://json.schemastore.org/aspire-8.0.json", "resources": { "terraform": { "error": "This resource does not support generation in the manifest." diff --git a/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.TerraformStack_WithComplexConfiguration_GeneratesValidHCL/aspire-manifest.verified.json b/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.TerraformStack_WithComplexConfiguration_GeneratesValidHCL/aspire-manifest.verified.json index 32d9fbb6a..f20dbf256 100644 --- a/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.TerraformStack_WithComplexConfiguration_GeneratesValidHCL/aspire-manifest.verified.json +++ b/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.TerraformStack_WithComplexConfiguration_GeneratesValidHCL/aspire-manifest.verified.json @@ -1,5 +1,4 @@ { - "$schema": "https://json.schemastore.org/aspire-8.0.json", "resources": { "terraform": { "error": "This resource does not support generation in the manifest." diff --git a/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.TerraformStack_WithOutputDirectory_GeneratesFileInSpecifiedLocation/aspire-manifest.verified.json b/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.TerraformStack_WithOutputDirectory_GeneratesFileInSpecifiedLocation/aspire-manifest.verified.json index 32d9fbb6a..f20dbf256 100644 --- a/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.TerraformStack_WithOutputDirectory_GeneratesFileInSpecifiedLocation/aspire-manifest.verified.json +++ b/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.TerraformStack_WithOutputDirectory_GeneratesFileInSpecifiedLocation/aspire-manifest.verified.json @@ -1,5 +1,4 @@ { - "$schema": "https://json.schemastore.org/aspire-8.0.json", "resources": { "terraform": { "error": "This resource does not support generation in the manifest." diff --git a/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.TerraformStack_WithParentConfiguration_UsesParentDirectory/aspire-manifest.verified.json b/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.TerraformStack_WithParentConfiguration_UsesParentDirectory/aspire-manifest.verified.json index 32d9fbb6a..f20dbf256 100644 --- a/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.TerraformStack_WithParentConfiguration_UsesParentDirectory/aspire-manifest.verified.json +++ b/tests/EmmittJ.Aspire.Hosting.Terraform.Tests/TerraformPipelineIntegrationTests.TerraformStack_WithParentConfiguration_UsesParentDirectory/aspire-manifest.verified.json @@ -1,5 +1,4 @@ { - "$schema": "https://json.schemastore.org/aspire-8.0.json", "resources": { "terraform": { "error": "This resource does not support generation in the manifest." From c6f8420a0dc5b86d9b5c0ce02a60da30d19fb73d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:31:03 +0000 Subject: [PATCH 3/5] Clarify push comment per review feedback --- .../TerraformProvisioningResource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EmmittJ.Aspire.Hosting.Terraform/TerraformProvisioningResource.cs b/src/EmmittJ.Aspire.Hosting.Terraform/TerraformProvisioningResource.cs index 3d3dbf107..47904779a 100644 --- a/src/EmmittJ.Aspire.Hosting.Terraform/TerraformProvisioningResource.cs +++ b/src/EmmittJ.Aspire.Hosting.Terraform/TerraformProvisioningResource.cs @@ -290,7 +290,7 @@ private static async Task PushImageToRegistryAsync( throw new InvalidOperationException($"Failed to get target tag for {resource.Name}"); } - // Tags the local image with the registry-qualified remote name and pushes it. + // Pushes the local image, which internally tags it with the registry-qualified remote name. await containerImageManager.PushImageAsync(resource, context.CancellationToken).ConfigureAwait(false); await pushTask.CompleteAsync( From 7b395a6a27f14aa5a33c5fbfca56bb1c05575ddd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:55:24 +0000 Subject: [PATCH 4/5] Update remaining dependencies to latest stable versions --- Directory.Packages.props | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f19c4876b..00396b0dc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,21 +11,21 @@ - - - - - - - + + + + + + + - - - - - + + + + + @@ -33,12 +33,12 @@ - - + + - + From 1cafa561ec52438aab5df7958557d2bc7a9eb76e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:58:07 +0000 Subject: [PATCH 5/5] Correct README: ATS attributes available in 13.4.2 behind analyzer flag --- src/EmmittJ.Aspire.Hosting.Terraform/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EmmittJ.Aspire.Hosting.Terraform/README.md b/src/EmmittJ.Aspire.Hosting.Terraform/README.md index 9bd877487..b2be761e8 100644 --- a/src/EmmittJ.Aspire.Hosting.Terraform/README.md +++ b/src/EmmittJ.Aspire.Hosting.Terraform/README.md @@ -252,7 +252,7 @@ app.PublishAsTerraform(infra => A plain `string` is treated as data and is quoted; use `TerraformRawExpression` only when an expression cannot be represented by the typed value providers. The contained HCL is rendered as-is and is not validated or escaped. -> **Note:** The Aspire Type System (ATS) attributes used for generating non-C# SDKs (`[AspireExport]` / `[AspireDto]`) are not present in the Aspire version this integration targets (`13.4.2`). The cross-language *contract* above (value providers + the raw-HCL escape-hatch) is in place; surfacing it via ATS annotations is deferred until those attributes are available. +> **Note:** The Aspire Type System (ATS) attributes used for generating non-C# SDKs (`[AspireExport]` / `[AspireDto]`) **are** available in the Aspire version this integration targets (`13.4.2`), in the `Aspire.Hosting` namespace. Their validation analyzer (`Aspire.Hosting.Integration.Analyzers`) is opt-in behind the `EnableAspireIntegrationAnalyzers` MSBuild property (default `false`). The cross-language *contract* above (value providers + the raw-HCL escape-hatch) is already in place. Surfacing the public builder API via ATS annotations is the remaining step: enabling the analyzer reports `ASPIREEXPORT008` for every extension method on the Aspire-exported `IResourceBuilder`, so each must be marked `[AspireExport]` (for ATS-compatible signatures) or `[AspireExportIgnore]`, and the domain types they expose (e.g. `TerraformOutputReference`, configuration/block types) must first be made ATS-compatible (`[AspireExport]` / `[AspireDto]`). ### Publish-Only Execution