Azure / Azure/azure-dev

Empty service `env: {}` is dropped on save, so an explicit empty environment scope cannot be expressed

Open
#9,401 1 comment 0 reactions 0 assignees View on GitHub
area/engine
Dominant language
Go
Stars
569
Forks
364
Avg merge
2d 19h
Merged PRs (30d)
136

Description

## Problem

`azure.yaml` cannot express "this service declares an empty environment scope." An explicitly empty `env: {}` is silently dropped when the project is saved, so it is indistinguishable from a service that never declared `env:` at all.

## Root cause

`project.SaveConfig` re-parses the raw config map into a typed `ProjectConfig` and re-marshals that typed value:

```go
// cli/azd/pkg/project/project.go:272
func SaveConfig(ctx context.Context, config config.Config, projectFilePath string) error {
projectBytes, err := yaml.Marshal(config.Raw())
...
projectConfig, err := Parse(ctx, string(projectBytes))
...
return Save(ctx, projectConfig, projectFilePath)
}
```

`ServiceConfig.Environment` is a plain map with `omitempty`:

```go
// cli/azd/pkg/project/service_config.go:68
Environment osutil.ExpandableMap `yaml:"env,omitempty"`
```

`osutil.ExpandableMap` is `map[string]ExpandableString` with no custom `MarshalYAML`, so `omitempty` drops a zero-length map along with a nil one.

## Reproduction

Against the real `LoadConfig` / `SaveConfig`:

| Input | Written `azure.yaml` | Raw `Get("services.myagent.env")` |
| --- | --- | --- |
| `cfg.Set("services.myagent.env", map[string]any{})` | no `env:` key at all | `found = false` |
| `cfg.Set("services.myagent.env", map[string]any{"FOO": "${BAR}"})` | `env:` with the value | `found = true` |

## Why it matters

Extensions decide environment scoping by probing whether `env:` is present. In the Foundry extensions, `serviceEnvDeclared` reads `GetServiceConfigValue(Path: "env")` and feeds `ServiceRunContext.HasServiceEnvironment`. When that is false, the run and deploy paths fall back to forwarding the entire azd environment into the child process.

Because an explicit empty scope cannot survive a save, any generated service with no variables of its own reads as legacy and inherits everything, while a service that happens to declare one variable is fully isolated. Scoping ends up determined by an incidental template detail rather than by intent.

This blocked the empty-scope work in #9079: the call sites were changed to always write the section, then reverted in `88a6b76be` because the write never reached the file. The tests there passed only because the recording stub captured the RPC in memory and never round-tripped through `SaveConfig`.

## Possible directions

1. Change `ServiceConfig.Environment` to `*osutil.ExpandableMap`. With a pointer, `omitempty` drops only nil, so absent stays absent and explicitly empty round-trips. There are roughly nine field usages across `pkg/project`, `internal/cmd`, and the agents extension.
2. Give `ServiceConfig` custom marshalling that tracks whether `env:` was present in the source document.
3. Drop `omitempty`. Smallest change, but every service that never declared `env:` starts emitting `env: {}` on save, which is a visible and unwanted diff for existing projects.

Option 1 looks like the smallest change that preserves both behaviors exactly.

## Acceptance

- A raw config `Set` of an empty `env` section survives `SaveConfig` and is visible to a follow-up raw `Get`.
- A service that never declared `env:` still saves without an `env:` key.
- A test in `pkg/project` asserts against the written `azure.yaml` rather than a stub, since that is the layer that decides this.
- Foundry extension call sites can then declare an explicit empty scope again.

Contributor guide

Open the contributing guide

Research direction

Start with cli/azd/pkg/project/project.go SaveConfig and cli/azd/pkg/project/service_config.go, then trace the Environment usages in pkg/project, internal/cmd, and the agents extension. Add or run a pkg/project round-trip test using real LoadConfig/SaveConfig and verify empty env survives while absent env remains omitted; confirm the written azure.yaml and follow-up raw Get.

Written by the indexing model from the issue text.

Assessment

Tech stack
azure, go
Domain
cli, developer-experience
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.