diff --git a/cli/azd/extensions/azure.ai.projects/internal/provisioning/foundry_provisioning_provider.go b/cli/azd/extensions/azure.ai.projects/internal/provisioning/foundry_provisioning_provider.go index df2981e15aa..b34a97ba7fd 100644 --- a/cli/azd/extensions/azure.ai.projects/internal/provisioning/foundry_provisioning_provider.go +++ b/cli/azd/extensions/azure.ai.projects/internal/provisioning/foundry_provisioning_provider.go @@ -51,9 +51,12 @@ const ( envKeyPrincipalID = "AZURE_PRINCIPAL_ID" ) -// deploymentNamePrefix is prepended to the azd environment name to form -// the ARM deployment name so re-runs update the same deployment record. -const deploymentNamePrefix = "azd-foundry-" +const ( + // deploymentNamePrefix is prepended to the azd environment name so re-runs + // update the same ARM deployment record. + deploymentNamePrefix = "azd-foundry-" + maxDeploymentNameLength = 64 +) // FoundryProvisioningProvider implements azdext.ProvisioningProvider for // the service whose host is FoundryProjectHost. By default it deploys @@ -1791,28 +1794,35 @@ func (p *FoundryProvisioningProvider) deploymentsClient(ctx context.Context) (*a return factory.NewDeploymentsClient(), nil } -// deploymentName is stable per azd env so re-runs update one record, plus a -// short hash of the project path so two projects sharing an env name (e.g. -// "dev") in the same subscription don't write the same deployment and read each -// other's outputs. +// deploymentName is stable per azd env and capped at ARM's 64-character limit. +// The project-path hash separates projects sharing an env name; an env-name hash +// preserves that identity when the readable env-name segment must be truncated. func (p *FoundryProvisioningProvider) deploymentName() string { - h := fnv.New32a() - _, _ = h.Write([]byte(p.projectPath)) - return fmt.Sprintf("%s%s-%08x", deploymentNamePrefix, p.envName, h.Sum32()) + pathHash := fnv.New32a() + _, _ = pathHash.Write([]byte(p.projectPath)) + name := fmt.Sprintf("%s%s-%08x", deploymentNamePrefix, p.envName, pathHash.Sum32()) + if len(name) <= maxDeploymentNameLength { + return name + } + + envHash := fnv.New32a() + _, _ = envHash.Write([]byte(p.envName)) + hashTail := fmt.Sprintf("-%08x-%08x", envHash.Sum32(), pathHash.Sum32()) + keep := maxDeploymentNameLength - len(deploymentNamePrefix) - len(hashTail) + return deploymentNamePrefix + p.envName[:keep] + hashTail } // brownfieldDeploymentName is deploymentName plus a "-brownfield" suffix, capped // at ARM's 64-character deployment-name limit. The trailing path hash and suffix // are preserved (uniqueness and intent); only the env-name portion is truncated. func (p *FoundryProvisioningProvider) brownfieldDeploymentName() string { - const maxLen = 64 name := p.deploymentName() + "-brownfield" - if len(name) <= maxLen { + if len(name) <= maxDeploymentNameLength { return name } const suffix = "-brownfield" hashTail := name[len(name)-len(suffix)-9 : len(name)-len(suffix)] // "-<8 hex>" - keep := maxLen - len(hashTail) - len(suffix) + keep := maxDeploymentNameLength - len(hashTail) - len(suffix) return name[:keep] + hashTail + suffix } diff --git a/cli/azd/extensions/azure.ai.projects/internal/provisioning/foundry_provisioning_provider_test.go b/cli/azd/extensions/azure.ai.projects/internal/provisioning/foundry_provisioning_provider_test.go index 059f61b9dc7..4eb58ccd033 100644 --- a/cli/azd/extensions/azure.ai.projects/internal/provisioning/foundry_provisioning_provider_test.go +++ b/cli/azd/extensions/azure.ai.projects/internal/provisioning/foundry_provisioning_provider_test.go @@ -410,6 +410,7 @@ func TestArmInputsToProto(t *testing.T) { func TestDeploymentName_StableForEnv(t *testing.T) { p := &FoundryProvisioningProvider{envName: "dev", projectPath: "/proj/a"} first := p.deploymentName() + assert.Equal(t, "azd-foundry-dev-accd5375", first, "names that already fit must not change") assert.Equal(t, first, p.deploymentName(), "same env+path is stable") assert.True(t, strings.HasPrefix(first, "azd-foundry-dev-"), "carries env and discriminator") @@ -421,6 +422,25 @@ func TestDeploymentName_StableForEnv(t *testing.T) { assert.NotEqual(t, first, other.deploymentName()) } +func TestDeploymentName_LongEnvironmentName(t *testing.T) { + projectPath := `C:\Users\zhihuan\source\repos\agent-framework-egress-control-responses` + p := &FoundryProvisioningProvider{ + envName: "agent-framework-egress-control-responses-dev", + projectPath: projectPath, + } + + name := p.deploymentName() + assert.Len(t, name, maxDeploymentNameLength) + assert.Equal(t, "azd-foundry-agent-framework-egress-control-res-269fe92a-aa9d4054", name) + + // Environment names that differ only beyond the retained prefix must not collide. + other := &FoundryProvisioningProvider{ + envName: "agent-framework-egress-control-responses-test", + projectPath: projectPath, + } + assert.NotEqual(t, name, other.deploymentName()) +} + func TestDeploymentOutputsResources_NilSafe(t *testing.T) { assert.Nil(t, deploymentOutputs(nil)) assert.Nil(t, deploymentResources(nil))