fix(project): preserve existing azure.yaml properties on AddService#8679
Conversation
AddService was the only mutating gRPC handler that saved the cached in-memory ProjectConfig without first reloading from disk. When the cache was stale relative to the on-disk azure.yaml (as in the `azd ai agent init` flow, which materializes/updates azure.yaml after azd resolves the cache), project.Save overwrote the file and dropped any properties absent from the cache - hooks, other top-level keys, and even pre-existing services. Reload the project config from disk before mutating and saving, mirroring the SetConfig*/SetServiceConfig*/UnsetConfig* handlers. Runtime event-handler state is preserved via reloadAndCacheProjectConfig -> CopyRuntimeStateTo. Adds a regression test that seeds a stale cache against an on-disk azure.yaml containing hooks, a custom top-level key, and an existing service, and asserts they all survive after AddService. Fixes #8678 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
📋 Prioritization NoteThanks for the contribution! The linked issue isn't in the current milestone yet. |
There was a problem hiding this comment.
Pull request overview
This PR fixes a correctness issue in the gRPC Project().AddService mutation path where saving a stale in-memory ProjectConfig could overwrite newer on-disk azure.yaml content (e.g., hooks, unknown top-level keys, and even pre-existing services). The change brings AddService in line with other mutating handlers by reloading the project config from disk before performing the mutation.
Changes:
- Reload project config from disk at the start of
projectService.AddServiceto avoid clobbering newerazure.yamlproperties. - Add a regression test that reproduces the stale-cache scenario and verifies preservation of hooks, unknown top-level keys, and existing services after
AddService.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| cli/azd/internal/grpcserver/project_service.go | Reloads and refreshes the cached ProjectConfig from disk before mutating/saving in AddService to prevent loss of on-disk properties. |
| cli/azd/internal/grpcserver/project_service_test.go | Adds a regression test ensuring AddService preserves existing azure.yaml hooks, unknown keys, and services even when the lazy cache is stale. |
Azure Dev CLI Install InstructionsInstall scriptsMacOS/Linux
bash: pwsh: WindowsPowerShell install MSI install Standalone Binary
MSI
Documentationlearn.microsoft.com documentationtitle: Azure Developer CLI reference
|
What
azd ai agent init(and any extension that adds a service via the gRPCProject().AddServiceAPI) was dropping existing top-levelazure.yamlproperties such ashooks.Root cause
projectService.AddService(cli/azd/internal/grpcserver/project_service.go) was the only mutating gRPC handler that saved the cached in-memoryProjectConfigwithout first reloading it from disk. Every other mutating handler (SetConfigSection,SetConfigValue,UnsetConfig,SetServiceConfigSection,SetServiceConfigValue,UnsetServiceConfig) reloads from disk (via the lossless raw-map path and/orreloadAndCacheProjectConfig).When the cached config was stale relative to the on-disk
azure.yaml— as happens in theinitflow, which materializes/updatesazure.yamlafter azd first resolveslazyProjectConfig—project.Savere-serialized the stale struct over the file and wiped everything not present in the cache:hooks, other top-level keys, and even pre-existing services.I confirmed this by reproduction: a fresh load → add service →
Saveis lossless, but a stale cache → add service →Saveclobbershooks, custom top-level keys, and existing services.Fix
AddServicenow reloads the project config from disk before reading/mutating/saving, mirroring the established pattern in the other handlers. Runtime event-handler state is preserved throughreloadAndCacheProjectConfig→CopyRuntimeStateTo, so hooks and extension-registered handlers keep working.Tests
Adds
Test_ProjectService_AddService_PreservesExistingProperties, which seeds a stale cache against an on-diskazure.yamlcontaininghooks, a custom top-level key, and an existing service, then asserts they all survive afterAddServicealongside the newly added service. The test fails without the fix (hooks wiped) and passes with it.Validation
go build ./...go test ./internal/grpcserver/...(full package)go vet,gofmt,golangci-lint run(0 issues),cspell— all cleanThis is a core-only change; the
azure.ai.agentsextension needs no edit (it already callsAddService), and the fix benefits every extension that adds services.Fixes #8678