From 6f4f1fc3e9c3334f2e4344461c55d625604e795a Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Tue, 21 Oct 2025 15:36:29 -0700 Subject: [PATCH 1/2] Invoke project events during commands --- cli/azd/cmd/build.go | 69 ++++--- cli/azd/cmd/package.go | 144 ++++++++------ cli/azd/cmd/restore.go | 74 ++++--- .../microsoft.azd.demo/internal/cmd/listen.go | 32 ++- cli/azd/internal/cmd/deploy.go | 185 ++++++++++-------- cli/azd/internal/cmd/publish.go | 179 +++++++++-------- cli/azd/pkg/project/project_manager.go | 6 +- 7 files changed, 394 insertions(+), 295 deletions(-) diff --git a/cli/azd/cmd/build.go b/cli/azd/cmd/build.go index 1167b3266d8..20720fb9c0a 100644 --- a/cli/azd/cmd/build.go +++ b/cli/azd/cmd/build.go @@ -162,44 +162,57 @@ func (ba *buildAction) Run(ctx context.Context) (*actions.ActionResult, error) { return nil, err } - buildResults := map[string]*project.ServiceBuildResult{} stableServices, err := ba.importManager.ServiceStable(ctx, ba.projectConfig) if err != nil { return nil, err } - for _, svc := range stableServices { - stepMessage := fmt.Sprintf("Building service %s", svc.Name) - ba.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 { - ba.console.StopSpinner(ctx, stepMessage, input.StepSkipped) - continue - } + projectEventArgs := project.ProjectLifecycleEventArgs{ + Project: ba.projectConfig, + } - buildResult, err := async.RunWithProgress( - func(buildProgress project.ServiceProgress) { - progressMessage := fmt.Sprintf("Building service %s (%s)", svc.Name, buildProgress.Message) - ba.console.ShowSpinner(ctx, progressMessage, input.Step) - }, - func(progress *async.Progress[project.ServiceProgress]) (*project.ServiceBuildResult, error) { - return ba.serviceManager.Build(ctx, svc, nil, progress) - }, - ) + buildResults := map[string]*project.ServiceBuildResult{} - if err != nil { - ba.console.StopSpinner(ctx, stepMessage, input.StepFailed) - return nil, err + err = ba.projectConfig.Invoke(ctx, project.ProjectEventBuild, projectEventArgs, func() error { + for _, svc := range stableServices { + stepMessage := fmt.Sprintf("Building service %s", svc.Name) + ba.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 { + ba.console.StopSpinner(ctx, stepMessage, input.StepSkipped) + continue + } + + buildResult, err := async.RunWithProgress( + func(buildProgress project.ServiceProgress) { + progressMessage := fmt.Sprintf("Building service %s (%s)", svc.Name, buildProgress.Message) + ba.console.ShowSpinner(ctx, progressMessage, input.Step) + }, + func(progress *async.Progress[project.ServiceProgress]) (*project.ServiceBuildResult, error) { + return ba.serviceManager.Build(ctx, svc, nil, progress) + }, + ) + + if err != nil { + ba.console.StopSpinner(ctx, stepMessage, input.StepFailed) + return err + } + + ba.console.StopSpinner(ctx, stepMessage, input.StepDone) + buildResults[svc.Name] = buildResult + + // report build outputs + ba.console.MessageUxItem(ctx, buildResult.Artifacts) } - ba.console.StopSpinner(ctx, stepMessage, input.StepDone) - buildResults[svc.Name] = buildResult + return nil + }) - // report build outputs - ba.console.MessageUxItem(ctx, buildResult.Artifacts) + if err != nil { + return nil, err } if ba.formatter.Kind() == output.JsonFormat { diff --git a/cli/azd/cmd/package.go b/cli/azd/cmd/package.go index 4e672207557..68ff285d5e6 100644 --- a/cli/azd/cmd/package.go +++ b/cli/azd/cmd/package.go @@ -142,81 +142,95 @@ func (pa *packageAction) Run(ctx context.Context) (*actions.ActionResult, error) return nil, err } - packageResults := map[string]*project.ServicePackageResult{} - serviceTable, err := pa.importManager.ServiceStable(ctx, pa.projectConfig) if err != nil { return nil, err } + serviceCount := len(serviceTable) - for index, svc := range serviceTable { - // TODO(ellismg): We need to figure out what packaging an containerized dotnet app means. For now, just skip it. - // We "package" the app during deploy when we call `dotnet publish /p:PublishProfile=DefaultContainer` to build - // and push the container image. - // - // Doing this skip here means that during `azd up` we don't show output like: - // /* cSpell:disable */ - // - // Packaging services (azd package) - // - // (✓) Done: Packaging service basketservice - // - Package Output: /var/folders/6n/sxbj12js5ksg6ztn0kslqp400000gn/T/azd472091284 - // - // (✓) Done: Packaging service catalogservice - // - Package Output: /var/folders/6n/sxbj12js5ksg6ztn0kslqp400000gn/T/azd2265185954 - // - // (✓) Done: Packaging service frontend - // - Package Output: /var/folders/6n/sxbj12js5ksg6ztn0kslqp400000gn/T/azd2956031596 - // - // /* cSpell:enable */ - // Which is nice - since the above is not the package that we publish (instead it's the raw output of - // `dotnet publish`, as if you were going to run on App Service.). - // - // With .NET 8, we'll be able to build just the container image, by setting ContainerArchiveOutputPath - // as a property when we run `dotnet publish`. If we set this to the filepath of a tgz (doesn't need to exist) - // the the action will just produce a container image and save it to that tgz, as `docker save` would have. It will - // not push the container image. - // - // It's probably right for us to think about "package" for a containerized application as meaning "produce the tgz" - // of the image, as would be done by `docker save` and then do this for both DotNetContainerAppTargets and - // ContainerAppTargets. - if svc.Host == project.DotNetContainerAppTarget { - continue - } - stepMessage := fmt.Sprintf("Packaging service %s", svc.Name) - pa.console.ShowSpinner(ctx, stepMessage, input.Step) + projectEventArgs := project.ProjectLifecycleEventArgs{ + Project: pa.projectConfig, + } - // 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 { - pa.console.StopSpinner(ctx, stepMessage, input.StepSkipped) - continue - } + packageResults := map[string]*project.ServicePackageResult{} - options := &project.PackageOptions{OutputPath: pa.flags.outputPath} - packageResult, err := async.RunWithProgress( - func(packageProgress project.ServiceProgress) { - progressMessage := fmt.Sprintf("Packaging service %s (%s)", svc.Name, packageProgress.Message) - pa.console.ShowSpinner(ctx, progressMessage, input.Step) - }, - func(progress *async.Progress[project.ServiceProgress]) (*project.ServicePackageResult, error) { - return pa.serviceManager.Package(ctx, svc, nil, progress, options) - }, - ) - pa.console.StopSpinner(ctx, stepMessage, input.GetStepResultFormat(err)) - - if err != nil { - return nil, err - } - packageResults[svc.Name] = packageResult + err = pa.projectConfig.Invoke(ctx, project.ProjectEventPackage, projectEventArgs, func() error { + for index, svc := range serviceTable { + // TODO(ellismg): We need to figure out what packaging an containerized dotnet app means. For now, just skip it. + // We "package" the app during deploy when we call `dotnet publish /p:PublishProfile=DefaultContainer` to build + // and push the container image. + // + // Doing this skip here means that during `azd up` we don't show output like: + // /* cSpell:disable */ + // + // Packaging services (azd package) + // + // (✓) Done: Packaging service basketservice + // - Package Output: /var/folders/6n/sxbj12js5ksg6ztn0kslqp400000gn/T/azd472091284 + // + // (✓) Done: Packaging service catalogservice + // - Package Output: /var/folders/6n/sxbj12js5ksg6ztn0kslqp400000gn/T/azd2265185954 + // + // (✓) Done: Packaging service frontend + // - Package Output: /var/folders/6n/sxbj12js5ksg6ztn0kslqp400000gn/T/azd2956031596 + // + // /* cSpell:enable */ + // Which is nice - since the above is not the package that we publish (instead it's the raw output of + // `dotnet publish`, as if you were going to run on App Service.). + // + // With .NET 8, we'll be able to build just the container image, by setting ContainerArchiveOutputPath + // as a property when we run `dotnet publish`. If we set this to the filepath of a tgz (doesn't need to exist) + // the the action will just produce a container image and save it to that tgz, as `docker save` would have. It will + // not push the container image. + // + // It's probably right for us to think about "package" for a containerized application as meaning "produce the tgz" + // of the image, as would be done by `docker save` and then do this for both DotNetContainerAppTargets and + // ContainerAppTargets. + if svc.Host == project.DotNetContainerAppTarget { + continue + } + + stepMessage := fmt.Sprintf("Packaging service %s", svc.Name) + pa.console.ShowSpinner(ctx, stepMessage, input.Step) - // report package output - pa.console.MessageUxItem(ctx, packageResult.Artifacts) - if index < serviceCount-1 { - pa.console.Message(ctx, "") + // 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 { + pa.console.StopSpinner(ctx, stepMessage, input.StepSkipped) + continue + } + + options := &project.PackageOptions{OutputPath: pa.flags.outputPath} + packageResult, err := async.RunWithProgress( + func(packageProgress project.ServiceProgress) { + progressMessage := fmt.Sprintf("Packaging service %s (%s)", svc.Name, packageProgress.Message) + pa.console.ShowSpinner(ctx, progressMessage, input.Step) + }, + func(progress *async.Progress[project.ServiceProgress]) (*project.ServicePackageResult, error) { + return pa.serviceManager.Package(ctx, svc, nil, progress, options) + }, + ) + pa.console.StopSpinner(ctx, stepMessage, input.GetStepResultFormat(err)) + + if err != nil { + return err + } + packageResults[svc.Name] = packageResult + + // report package output + pa.console.MessageUxItem(ctx, packageResult.Artifacts) + if index < serviceCount-1 { + pa.console.Message(ctx, "") + } } + + return nil + }) + + if err != nil { + return nil, err } if pa.formatter.Kind() == output.JsonFormat { diff --git a/cli/azd/cmd/restore.go b/cli/azd/cmd/restore.go index 6667812ba0a..bea9dc31b77 100644 --- a/cli/azd/cmd/restore.go +++ b/cli/azd/cmd/restore.go @@ -154,44 +154,60 @@ func (ra *restoreAction) Run(ctx context.Context) (*actions.ActionResult, error) return nil, err } - restoreResults := map[string]*project.ServiceRestoreResult{} stableServices, err := ra.importManager.ServiceStable(ctx, ra.projectConfig) if err != nil { return nil, err } - for _, svc := range stableServices { - stepMessage := fmt.Sprintf("Restoring service %s", svc.Name) - ra.console.ShowSpinner(ctx, stepMessage, input.Step) + projectEventArgs := project.ProjectLifecycleEventArgs{ + Project: ra.projectConfig, + } - // 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 { - ra.console.StopSpinner(ctx, stepMessage, input.StepSkipped) - continue - } + restoreResults := map[string]*project.ServiceRestoreResult{} - // Initialize service context for restore operation - serviceContext := &project.ServiceContext{} - - restoreResult, err := async.RunWithProgress( - func(buildProgress project.ServiceProgress) { - progressMessage := fmt.Sprintf("Building service %s (%s)", svc.Name, buildProgress.Message) - ra.console.ShowSpinner(ctx, progressMessage, input.Step) - }, - func(progress *async.Progress[project.ServiceProgress]) (*project.ServiceRestoreResult, error) { - return ra.serviceManager.Restore(ctx, svc, serviceContext, progress) - }, - ) - - if err != nil { - ra.console.StopSpinner(ctx, stepMessage, input.StepFailed) - return nil, err + err = ra.projectConfig.Invoke(ctx, project.ProjectEventRestore, projectEventArgs, func() error { + for _, svc := range stableServices { + stepMessage := fmt.Sprintf("Restoring service %s", svc.Name) + ra.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 { + ra.console.StopSpinner(ctx, stepMessage, input.StepSkipped) + continue + } + + // Initialize service context for restore operation + serviceContext := &project.ServiceContext{} + + restoreResult, err := async.RunWithProgress( + func(buildProgress project.ServiceProgress) { + progressMessage := fmt.Sprintf("Building service %s (%s)", svc.Name, buildProgress.Message) + ra.console.ShowSpinner(ctx, progressMessage, input.Step) + }, + func(progress *async.Progress[project.ServiceProgress]) (*project.ServiceRestoreResult, error) { + return ra.serviceManager.Restore(ctx, svc, serviceContext, progress) + }, + ) + + if err != nil { + ra.console.StopSpinner(ctx, stepMessage, input.StepFailed) + return err + } + + ra.console.StopSpinner(ctx, stepMessage, input.StepDone) + restoreResults[svc.Name] = restoreResult + + // report restore output + ra.console.MessageUxItem(ctx, restoreResult.Artifacts) } - ra.console.StopSpinner(ctx, stepMessage, input.StepDone) - restoreResults[svc.Name] = restoreResult + return nil + }) + + if err != nil { + return nil, err } if ra.formatter.Kind() == output.JsonFormat { diff --git a/cli/azd/extensions/microsoft.azd.demo/internal/cmd/listen.go b/cli/azd/extensions/microsoft.azd.demo/internal/cmd/listen.go index 202e2bd919f..d23fd3f2119 100644 --- a/cli/azd/extensions/microsoft.azd.demo/internal/cmd/listen.go +++ b/cli/azd/extensions/microsoft.azd.demo/internal/cmd/listen.go @@ -41,17 +41,43 @@ func newListenCommand() *cobra.Command { return nil }). + WithProjectEventHandler("predeploy", func(ctx context.Context, args *azdext.ProjectEventArgs) error { + for i := 1; i <= 20; i++ { + fmt.Printf("%d. Doing important predeploy project work in extension...\n", i) + time.Sleep(250 * time.Millisecond) + } + + return nil + }). + WithProjectEventHandler("postdeploy", func(ctx context.Context, args *azdext.ProjectEventArgs) error { + for i := 1; i <= 20; i++ { + fmt.Printf("%d. Doing important postdeploy project work in extension...\n", i) + time.Sleep(250 * time.Millisecond) + } + + return nil + }). WithServiceEventHandler("prepackage", func(ctx context.Context, args *azdext.ServiceEventArgs) error { for i := 1; i <= 20; i++ { - fmt.Printf("%d. Doing important work in extension...\n", i) + fmt.Printf("%d. Doing important prepackage service work in extension...\n", i) + time.Sleep(250 * time.Millisecond) + } + + return nil + }, &azdext.ServerEventOptions{ + // Optionally filter your subscription by service host and/or language + Host: "containerapp", + }). + WithServiceEventHandler("postpackage", func(ctx context.Context, args *azdext.ServiceEventArgs) error { + for i := 1; i <= 20; i++ { + fmt.Printf("%d. Doing important postpackage service work in extension...\n", i) time.Sleep(250 * time.Millisecond) } return nil }, &azdext.ServerEventOptions{ // Optionally filter your subscription by service host and/or language - Host: "containerapp", - Language: "python", + Host: "containerapp", }) // Start listening for events diff --git a/cli/azd/internal/cmd/deploy.go b/cli/azd/internal/cmd/deploy.go index 2a61f1f13ae..1fdeee2dc0a 100644 --- a/cli/azd/internal/cmd/deploy.go +++ b/cli/azd/internal/cmd/deploy.go @@ -226,122 +226,135 @@ func (da *DeployAction) Run(ctx context.Context) (*actions.ActionResult, error) startTime := time.Now() - deployResults := map[string]*project.ServiceDeployResult{} stableServices, err := da.importManager.ServiceStable(ctx, da.projectConfig) if err != nil { return nil, err } - for _, svc := range stableServices { - stepMessage := fmt.Sprintf("Deploying service %s", svc.Name) - da.console.ShowSpinner(ctx, stepMessage, input.Step) + projectEventArgs := project.ProjectLifecycleEventArgs{ + Project: da.projectConfig, + } - // 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 - } + deployResults := map[string]*project.ServiceDeployResult{} - 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) - } + err = da.projectConfig.Invoke(ctx, project.ProjectEventDeploy, projectEventArgs, func() error { + 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 + } - // Initialize service context for tracking artifacts across operations - serviceContext := &project.ServiceContext{} + 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) + } - 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, - }) + // Initialize service context for tracking artifacts across operations + serviceContext := &project.ServiceContext{} - if err != nil { - da.console.StopSpinner(ctx, stepMessage, input.StepFailed) - return nil, err + 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 + packageResult, 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 + } + + // Append package artifacts + if err := serviceContext.Package.Add(packageResult.Artifacts...); err != nil { + return err + } } - } else { - // --from-package not set, automatically package the application - packageResult, err := async.RunWithProgress( - func(packageProgress project.ServiceProgress) { - progressMessage := fmt.Sprintf("Packaging service %s (%s)", svc.Name, packageProgress.Message) + + publishResult, 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.ServicePackageResult, error) { - return da.serviceManager.Package(ctx, svc, serviceContext, progress, nil) + 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 publish + // do not stop progress here as next step is to deploy if err != nil { da.console.StopSpinner(ctx, stepMessage, input.StepFailed) - return nil, err + return err } - // Append package artifacts - if err := serviceContext.Package.Add(packageResult.Artifacts...); err != nil { - return nil, err - } - } + // Append publish artifacts + serviceContext.Publish.Add(publishResult.Artifacts...) - publishResult, 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 nil, err - } - - // Append publish artifacts - serviceContext.Publish.Add(publishResult.Artifacts...) - - 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) - }, - ) + 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 nil, err - } + if err != nil { + da.console.StopSpinner(ctx, stepMessage, input.StepFailed) + return err + } - // Append deploy artifacts - serviceContext.Deploy.Add(deployResult.Artifacts...) + // Append deploy artifacts + serviceContext.Deploy.Add(deployResult.Artifacts...) - // 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) + // 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) } - da.console.StopSpinner(ctx, stepMessage, input.GetStepResultFormat(err)) - deployResults[svc.Name] = deployResult + return nil + }) - // report deploy outputs - da.console.MessageUxItem(ctx, deployResult.Artifacts) + if err != nil { + return nil, err } aspireDashboardUrl := apphost.AspireDashboardUrl(ctx, da.env, da.alphaFeatureManager) diff --git a/cli/azd/internal/cmd/publish.go b/cli/azd/internal/cmd/publish.go index 5dce4906e97..39cdbd6f3dc 100644 --- a/cli/azd/internal/cmd/publish.go +++ b/cli/azd/internal/cmd/publish.go @@ -231,122 +231,135 @@ func (pa *PublishAction) Run(ctx context.Context) (*actions.ActionResult, error) startTime := time.Now() - publishResults := map[string]*project.ServicePublishResult{} stableServices, err := pa.importManager.ServiceStable(ctx, pa.projectConfig) if err != nil { return nil, err } - for _, svc := range stableServices { - stepMessage := fmt.Sprintf("Publishing service %s", svc.Name) - pa.console.ShowSpinner(ctx, stepMessage, input.Step) + projectEventArgs := project.ProjectLifecycleEventArgs{ + Project: pa.projectConfig, + } - // 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 { - pa.console.StopSpinner(ctx, stepMessage, input.StepSkipped) - continue - } + publishResults := map[string]*project.ServicePublishResult{} - 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 publishing. - pa.console.WarnForFeature(ctx, alphaFeatureId) - } + err = pa.projectConfig.Invoke(ctx, project.ProjectEventPublish, projectEventArgs, func() error { + for _, svc := range stableServices { + stepMessage := fmt.Sprintf("Publishing service %s", svc.Name) + pa.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 { + pa.console.StopSpinner(ctx, stepMessage, input.StepSkipped) + continue + } - if !pa.supportsPublish(ctx, svc) { - pa.console.StopSpinner(ctx, stepMessage, input.StepSkipped) + 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 publishing. + pa.console.WarnForFeature(ctx, alphaFeatureId) + } - var message string - if svc.Host == project.DotNetContainerAppTarget { - message = "'publish' does not currently support Aspire projects" - } else { - message = fmt.Sprintf( - "'publish' only supports '%s' and '%s' services, but '%s' has host type '%s'", - project.ContainerAppTarget, project.AksTarget, svc.Name, svc.Host) + if !pa.supportsPublish(ctx, svc) { + pa.console.StopSpinner(ctx, stepMessage, input.StepSkipped) + + var message string + if svc.Host == project.DotNetContainerAppTarget { + message = "'publish' does not currently support Aspire projects" + } else { + message = fmt.Sprintf( + "'publish' only supports '%s' and '%s' services, but '%s' has host type '%s'", + project.ContainerAppTarget, project.AksTarget, svc.Name, svc.Host) + } + + pa.console.MessageUxItem(ctx, &ux.WarningMessage{ + Description: message, + }) + continue } - pa.console.MessageUxItem(ctx, &ux.WarningMessage{ - Description: message, - }) - continue - } + // Initialize service context for tracking artifacts across operations + serviceContext := &project.ServiceContext{} - // Initialize service context for tracking artifacts across operations - serviceContext := &project.ServiceContext{} + if pa.flags.FromPackage != "" { + // --from-package set, skip packaging and create package artifact + err = serviceContext.Package.Add(&project.Artifact{ + Kind: determineArtifactKind(pa.flags.FromPackage), + Location: pa.flags.FromPackage, + LocationKind: project.LocationKindLocal, + }) - if pa.flags.FromPackage != "" { - // --from-package set, skip packaging and create package artifact - err = serviceContext.Package.Add(&project.Artifact{ - Kind: determineArtifactKind(pa.flags.FromPackage), - Location: pa.flags.FromPackage, - LocationKind: project.LocationKindLocal, - }) + if err != nil { + pa.console.StopSpinner(ctx, stepMessage, input.StepFailed) + return err + } + } else { + // --from-package not set, automatically package the application + packageResult, err := async.RunWithProgress( + func(packageProgress project.ServiceProgress) { + progressMessage := fmt.Sprintf("Packaging service %s (%s)", svc.Name, packageProgress.Message) + pa.console.ShowSpinner(ctx, progressMessage, input.Step) + }, + func(progress *async.Progress[project.ServiceProgress]) (*project.ServicePackageResult, error) { + return pa.serviceManager.Package(ctx, svc, serviceContext, progress, nil) + }, + ) + + if err != nil { + pa.console.StopSpinner(ctx, stepMessage, input.StepFailed) + return err + } - if err != nil { - pa.console.StopSpinner(ctx, stepMessage, input.StepFailed) - return nil, err + // Append package artifacts + if err := serviceContext.Package.Add(packageResult.Artifacts...); err != nil { + pa.console.StopSpinner(ctx, stepMessage, input.StepFailed) + return err + } } - } else { - // --from-package not set, automatically package the application - packageResult, err := async.RunWithProgress( - func(packageProgress project.ServiceProgress) { - progressMessage := fmt.Sprintf("Packaging service %s (%s)", svc.Name, packageProgress.Message) + + publishResult, err := async.RunWithProgress( + func(publishProgress project.ServiceProgress) { + progressMessage := fmt.Sprintf("Publishing service %s (%s)", svc.Name, publishProgress.Message) pa.console.ShowSpinner(ctx, progressMessage, input.Step) }, - func(progress *async.Progress[project.ServiceProgress]) (*project.ServicePackageResult, error) { - return pa.serviceManager.Package(ctx, svc, serviceContext, progress, nil) + func(progress *async.Progress[project.ServiceProgress]) (*project.ServicePublishResult, error) { + return pa.serviceManager.Publish(ctx, svc, serviceContext, progress, publishOptions) }, ) if err != nil { pa.console.StopSpinner(ctx, stepMessage, input.StepFailed) - return nil, err + return err } - // Append package artifacts - if err := serviceContext.Package.Add(packageResult.Artifacts...); err != nil { + if err := serviceContext.Publish.Add(publishResult.Artifacts...); err != nil { pa.console.StopSpinner(ctx, stepMessage, input.StepFailed) - return nil, err + return err } - } - - publishResult, err := async.RunWithProgress( - func(publishProgress project.ServiceProgress) { - progressMessage := fmt.Sprintf("Publishing service %s (%s)", svc.Name, publishProgress.Message) - pa.console.ShowSpinner(ctx, progressMessage, input.Step) - }, - func(progress *async.Progress[project.ServiceProgress]) (*project.ServicePublishResult, error) { - return pa.serviceManager.Publish(ctx, svc, serviceContext, progress, publishOptions) - }, - ) - - if err != nil { - pa.console.StopSpinner(ctx, stepMessage, input.StepFailed) - return nil, err - } - if err := serviceContext.Publish.Add(publishResult.Artifacts...); err != nil { - pa.console.StopSpinner(ctx, stepMessage, input.StepFailed) - return nil, err - } - - // clean up for packages automatically created in temp dir - if pa.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) + // clean up for packages automatically created in temp dir + if pa.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) + } } } } + + pa.console.StopSpinner(ctx, stepMessage, input.GetStepResultFormat(err)) + + publishResults[svc.Name] = publishResult + pa.console.MessageUxItem(ctx, publishResult.Artifacts) } - pa.console.StopSpinner(ctx, stepMessage, input.GetStepResultFormat(err)) + return nil + }) - publishResults[svc.Name] = publishResult - pa.console.MessageUxItem(ctx, publishResult.Artifacts) + if err != nil { + return nil, err } if pa.formatter.Kind() == output.JsonFormat { diff --git a/cli/azd/pkg/project/project_manager.go b/cli/azd/pkg/project/project_manager.go index 6dcf232a17a..e939f3da5f9 100644 --- a/cli/azd/pkg/project/project_manager.go +++ b/cli/azd/pkg/project/project_manager.go @@ -17,8 +17,12 @@ import ( ) const ( - ProjectEventDeploy ext.Event = "deploy" ProjectEventProvision ext.Event = "provision" + ProjectEventRestore ext.Event = "restore" + ProjectEventBuild ext.Event = "build" + ProjectEventPackage ext.Event = "package" + ProjectEventPublish ext.Event = "publish" + ProjectEventDeploy ext.Event = "deploy" ) var ( From 022110ef608b71f13d3d6619247547dd39b78b8b Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 22 Oct 2025 09:53:14 -0700 Subject: [PATCH 2/2] fixes lint issues --- cli/azd/cmd/package.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/azd/cmd/package.go b/cli/azd/cmd/package.go index 68ff285d5e6..4e42a5f6c8a 100644 --- a/cli/azd/cmd/package.go +++ b/cli/azd/cmd/package.go @@ -181,12 +181,12 @@ func (pa *packageAction) Run(ctx context.Context) (*actions.ActionResult, error) // // With .NET 8, we'll be able to build just the container image, by setting ContainerArchiveOutputPath // as a property when we run `dotnet publish`. If we set this to the filepath of a tgz (doesn't need to exist) - // the the action will just produce a container image and save it to that tgz, as `docker save` would have. It will - // not push the container image. + // the the action will just produce a container image and save it to that tgz, as `docker save` would have. + // It will not push the container image. // - // It's probably right for us to think about "package" for a containerized application as meaning "produce the tgz" - // of the image, as would be done by `docker save` and then do this for both DotNetContainerAppTargets and - // ContainerAppTargets. + // It's probably right for us to think about "package" for a containerized application as meaning + // "produce the tgz" of the image, as would be done by `docker save` and then do this for both + // DotNetContainerAppTargets and ContainerAppTargets. if svc.Host == project.DotNetContainerAppTarget { continue }