Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions cli/azd/.vscode/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ words:
- jsonschema
- rustc
- figspec
- vbauerster
languageSettings:
- languageId: go
ignoreRegExpList:
Expand Down Expand Up @@ -237,6 +238,9 @@ overrides:
- filename: pkg/project/service_target_dotnet_containerapp.go
words:
- IMAGENAME
- filename: internal/cmd/deploy.go
words:
- gctx
ignorePaths:
- "**/*_test.go"
- "**/mock*.go"
Expand Down
2 changes: 2 additions & 0 deletions cli/azd/extensions/azure.coding-agent/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g=
Expand Down Expand Up @@ -169,6 +170,7 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
Expand Down
242 changes: 164 additions & 78 deletions cli/azd/internal/cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,105 +238,90 @@ func (da *DeployAction) Run(ctx context.Context) (*actions.ActionResult, error)
deployResults := map[string]*project.ServiceDeployResult{}

err = da.projectConfig.Invoke(ctx, project.ProjectEventDeploy, projectEventArgs, func() error {
// Filter services based on target service name
var servicesToDeploy []*project.ServiceConfig
for _, svc := range stableServices {
stepMessage := fmt.Sprintf("Deploying service %s", svc.Name)
da.console.ShowSpinner(ctx, stepMessage, input.Step)

// Skip this service if both cases are true:
// 1. The user specified a service name
// 2. This service is not the one the user specified
if targetServiceName != "" && targetServiceName != svc.Name {
da.console.StopSpinner(ctx, stepMessage, input.StepSkipped)
continue
}
servicesToDeploy = append(servicesToDeploy, svc)
}

if alphaFeatureId, isAlphaFeature := alpha.IsFeatureKey(string(svc.Host)); isAlphaFeature {
// alpha feature on/off detection for host is done during initialization.
// This is just for displaying the warning during deployment.
da.console.WarnForFeature(ctx, alphaFeatureId)
}

// Initialize service context for tracking artifacts across operations
serviceContext := project.NewServiceContext()
// If no services to deploy, nothing to do
if len(servicesToDeploy) == 0 {
return nil
}

if da.flags.fromPackage != "" {
// --from-package set, skip packaging and create package artifact
err = serviceContext.Package.Add(&project.Artifact{
Kind: determineArtifactKind(da.flags.fromPackage),
Location: da.flags.fromPackage,
LocationKind: project.LocationKindLocal,
})
// Build a map of service names to services for dependency lookup
serviceMap := make(map[string]*project.ServiceConfig)
for _, svc := range servicesToDeploy {
serviceMap[svc.Name] = svc
}

if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return err
// Check if any service has dependencies (uses other services)
hasDependencies := false
for _, svc := range servicesToDeploy {
if len(svc.Uses) > 0 {
// Check if any dependency is another service (not a resource)
for _, dep := range svc.Uses {
if _, isService := serviceMap[dep]; isService {
hasDependencies = true
break
}
}
} else {
// --from-package not set, automatically package the application
_, err := async.RunWithProgress(
func(packageProgress project.ServiceProgress) {
progressMessage := fmt.Sprintf("Packaging service %s (%s)", svc.Name, packageProgress.Message)
da.console.ShowSpinner(ctx, progressMessage, input.Step)
},
func(progress *async.Progress[project.ServiceProgress]) (*project.ServicePackageResult, error) {
return da.serviceManager.Package(ctx, svc, serviceContext, progress, nil)
},
)

// do not stop progress here as next step is to publish
if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
}
if hasDependencies {
break
}
}

// Check if parallel deployment is enabled (default: enabled)
useParallelDeploy := os.Getenv("AZD_DISABLE_PARALLEL_DEPLOY") != "1"

if len(servicesToDeploy) == 1 || !useParallelDeploy {
// Single service or parallel disabled - deploy sequentially
for _, svc := range servicesToDeploy {
if err := da.deployService(ctx, svc, targetServiceName, deployResults); err != nil {
return err
}
}

_, err := async.RunWithProgress(
func(publishProgress project.ServiceProgress) {
progressMessage := fmt.Sprintf("Publishing service %s (%s)", svc.Name, publishProgress.Message)
da.console.ShowSpinner(ctx, progressMessage, input.Step)
},
func(progress *async.Progress[project.ServiceProgress]) (*project.ServicePublishResult, error) {
return da.serviceManager.Publish(ctx, svc, serviceContext, progress, nil)
},
)

// do not stop progress here as next step is to deploy
} else if hasDependencies {
// Services have dependencies - use dependency-aware deployment
da.console.Message(ctx, fmt.Sprintf(
"Deploying %d services with dependency awareness",
len(servicesToDeploy)))

parallelManager := NewParallelDeploymentManager(&da.serviceManager, 0)
serviceResults, err := parallelManager.DeployServicesWithDependencies(ctx, servicesToDeploy, serviceMap)
if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return err
return fmt.Errorf("parallel deployment failed: %w", err)
}

deployResult, err := async.RunWithProgress(
func(deployProgress project.ServiceProgress) {
progressMessage := fmt.Sprintf("Deploying service %s (%s)", svc.Name, deployProgress.Message)
da.console.ShowSpinner(ctx, progressMessage, input.Step)
},
func(progress *async.Progress[project.ServiceProgress]) (*project.ServiceDeployResult, error) {
return da.serviceManager.Deploy(ctx, svc, serviceContext, progress)
},
)

// Store results and report artifacts
for serviceName, serviceResult := range serviceResults {
deployResults[serviceName] = serviceResult
da.console.MessageUxItem(ctx, serviceResult.Artifacts)
}
} else {
// No dependencies - deploy all services in parallel
da.console.Message(ctx, fmt.Sprintf(
"Deploying %d services in parallel",
len(servicesToDeploy)))

parallelManager := NewParallelDeploymentManager(&da.serviceManager, 0)
serviceResults, err := parallelManager.DeployServices(ctx, servicesToDeploy)
if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return err
return fmt.Errorf("parallel deployment failed: %w", err)
}

// clean up for packages automatically created in temp dir
if da.flags.fromPackage == "" {
for _, artifact := range serviceContext.Package {
if strings.HasPrefix(artifact.Location, os.TempDir()) {
if err := os.RemoveAll(artifact.Location); err != nil {
log.Printf("failed to remove temporary package: %s : %s", artifact.Location, err)
}
}
}
// Store results and report artifacts
for serviceName, serviceResult := range serviceResults {
deployResults[serviceName] = serviceResult
da.console.MessageUxItem(ctx, serviceResult.Artifacts)
}

da.console.StopSpinner(ctx, stepMessage, input.GetStepResultFormat(err))
deployResults[svc.Name] = deployResult

// report deploy outputs
da.console.MessageUxItem(ctx, deployResult.Artifacts)
}

return nil
Expand Down Expand Up @@ -405,3 +390,104 @@ func GetCmdDeployHelpFooter(*cobra.Command) string {
),
})
}

// deployService deploys a single service sequentially
func (da *DeployAction) deployService(
ctx context.Context,
svc *project.ServiceConfig,
targetServiceName string,
deployResults map[string]*project.ServiceDeployResult,
) error {
stepMessage := fmt.Sprintf("Deploying service %s", svc.Name)
da.console.ShowSpinner(ctx, stepMessage, input.Step)

if alphaFeatureId, isAlphaFeature := alpha.IsFeatureKey(string(svc.Host)); isAlphaFeature {
// alpha feature on/off detection for host is done during initialization.
// This is just for displaying the warning during deployment.
da.console.WarnForFeature(ctx, alphaFeatureId)
}

// Initialize service context for tracking artifacts across operations
serviceContext := project.NewServiceContext()

if da.flags.fromPackage != "" {
// --from-package set, skip packaging and create package artifact
err := serviceContext.Package.Add(&project.Artifact{
Kind: determineArtifactKind(da.flags.fromPackage),
Location: da.flags.fromPackage,
LocationKind: project.LocationKindLocal,
})

if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return err
}
} else {
// --from-package not set, automatically package the application
_, err := async.RunWithProgress(
func(packageProgress project.ServiceProgress) {
progressMessage := fmt.Sprintf("Packaging service %s (%s)", svc.Name, packageProgress.Message)
da.console.ShowSpinner(ctx, progressMessage, input.Step)
},
func(progress *async.Progress[project.ServiceProgress]) (*project.ServicePackageResult, error) {
return da.serviceManager.Package(ctx, svc, serviceContext, progress, nil)
},
)

// do not stop progress here as next step is to publish
if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return err
}
}

_, err := async.RunWithProgress(
func(publishProgress project.ServiceProgress) {
progressMessage := fmt.Sprintf("Publishing service %s (%s)", svc.Name, publishProgress.Message)
da.console.ShowSpinner(ctx, progressMessage, input.Step)
},
func(progress *async.Progress[project.ServiceProgress]) (*project.ServicePublishResult, error) {
return da.serviceManager.Publish(ctx, svc, serviceContext, progress, nil)
},
)

// do not stop progress here as next step is to deploy
if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return err
}

deployResult, err := async.RunWithProgress(
func(deployProgress project.ServiceProgress) {
progressMessage := fmt.Sprintf("Deploying service %s (%s)", svc.Name, deployProgress.Message)
da.console.ShowSpinner(ctx, progressMessage, input.Step)
},
func(progress *async.Progress[project.ServiceProgress]) (*project.ServiceDeployResult, error) {
return da.serviceManager.Deploy(ctx, svc, serviceContext, progress)
},
)

if err != nil {
da.console.StopSpinner(ctx, stepMessage, input.StepFailed)
return err
}

// clean up for packages automatically created in temp dir
if da.flags.fromPackage == "" {
for _, artifact := range serviceContext.Package {
if strings.HasPrefix(artifact.Location, os.TempDir()) {
if err := os.RemoveAll(artifact.Location); err != nil {
log.Printf("failed to remove temporary package: %s : %s", artifact.Location, err)
}
}
}
}

da.console.StopSpinner(ctx, stepMessage, input.GetStepResultFormat(err))
deployResults[svc.Name] = deployResult

// report deploy outputs
da.console.MessageUxItem(ctx, deployResult.Artifacts)

return nil
}
Loading