diff --git a/cli/azd/.vscode/cspell-azd-dictionary.txt b/cli/azd/.vscode/cspell-azd-dictionary.txt index 1834e4f1526..bc02f9a1935 100644 --- a/cli/azd/.vscode/cspell-azd-dictionary.txt +++ b/cli/azd/.vscode/cspell-azd-dictionary.txt @@ -216,4 +216,5 @@ webfrontend westus2 wireinject yacspin +ymlt zerr diff --git a/cli/azd/.vscode/settings.json b/cli/azd/.vscode/settings.json index e07583a3933..f5a8eb31834 100644 --- a/cli/azd/.vscode/settings.json +++ b/cli/azd/.vscode/settings.json @@ -9,6 +9,8 @@ "go.lintTool": "golangci-lint", "go.testTimeout": "10m", "files.associations": { - "*.bicept": "go-template" - } + "*.bicept": "go-template", + "*.yamlt": "go-template", + "*.ymlt": "go-template" + } } diff --git a/cli/azd/pkg/pipeline/pipeline.go b/cli/azd/pkg/pipeline/pipeline.go index 474d9bdea42..b3eb43c57d5 100644 --- a/cli/azd/pkg/pipeline/pipeline.go +++ b/cli/azd/pkg/pipeline/pipeline.go @@ -5,6 +5,7 @@ package pipeline import ( "context" + "fmt" "maps" "path/filepath" "slices" @@ -161,8 +162,6 @@ func mergeProjectVariablesAndSecrets( const ( gitHubDisplayName string = "GitHub" azdoDisplayName string = "Azure DevOps" - gitHubLabel string = "github" - azdoLabel string = "azdo" envPersistedKey string = "AZD_PIPELINE_PROVIDER" defaultPipelineFileName string = "azure-dev.yml" gitHubDirectory string = ".github" @@ -175,3 +174,43 @@ var ( gitHubYml string = filepath.Join(gitHubWorkflowsDirectory, defaultPipelineFileName) azdoYml string = filepath.Join(azdoPipelinesDirectory, defaultPipelineFileName) ) + +type ciProviderType string + +const ( + ciProviderGitHubActions ciProviderType = "github" + ciProviderAzureDevOps ciProviderType = "azdo" +) + +func toCiProviderType(provider string) (ciProviderType, error) { + result := ciProviderType(provider) + if result == ciProviderGitHubActions || result == ciProviderAzureDevOps { + return result, nil + } + return "", fmt.Errorf("invalid ci provider type %s", provider) +} + +type infraProviderType string + +const ( + infraProviderBicep infraProviderType = "bicep" + infraProviderTerraform infraProviderType = "terraform" + infraProviderUndefined infraProviderType = "" +) + +func toInfraProviderType(provider string) (infraProviderType, error) { + result := infraProviderType(provider) + if result == infraProviderBicep || result == infraProviderTerraform || result == infraProviderUndefined { + return result, nil + } + return "", fmt.Errorf("invalid infra provider type %s", provider) +} + +type projectProperties struct { + CiProvider ciProviderType + InfraProvider infraProviderType + RepoRoot string + HasAppHost bool + BranchName string + AuthType PipelineAuthType +} diff --git a/cli/azd/pkg/pipeline/pipeline_manager.go b/cli/azd/pkg/pipeline/pipeline_manager.go index b9f16028496..daa4a3cee9a 100644 --- a/cli/azd/pkg/pipeline/pipeline_manager.go +++ b/cli/azd/pkg/pipeline/pipeline_manager.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "html/template" "log" "os" "path/filepath" @@ -656,7 +657,8 @@ func (pm *PipelineManager) pushGitRepo(ctx context.Context, gitRepoInfo *gitRepo // resolveProviderAndDetermine resolves the pipeline provider based on project configuration and environment, // or determines it if not already set. -func (pm *PipelineManager) resolveProviderAndDetermine(ctx context.Context, projectPath, repoRoot string) (string, error) { +func (pm *PipelineManager) resolveProviderAndDetermine( + ctx context.Context, projectPath, repoRoot string) (ciProviderType, error) { log.Printf("Loading project configuration from: %s", projectPath) prjConfig, err := project.Load(ctx, projectPath) if err != nil { @@ -667,13 +669,13 @@ func (pm *PipelineManager) resolveProviderAndDetermine(ctx context.Context, proj // 1) Check if provider is set on azure.yaml, it should override the `lastUsedProvider` if prjConfig.Pipeline.Provider != "" { log.Printf("Provider set in project configuration: %s", prjConfig.Pipeline.Provider) - return prjConfig.Pipeline.Provider, nil + return toCiProviderType(prjConfig.Pipeline.Provider) } // 2) Check if there is a persisted value from a previous run in the environment if lastUsedProvider, configExists := pm.env.LookupEnv(envPersistedKey); configExists { log.Printf("Using persisted provider from environment: %s", lastUsedProvider) - return lastUsedProvider, nil + return toCiProviderType(lastUsedProvider) } // 3) No config on azure.yaml or from previous run, so use the determineProvider logic @@ -704,12 +706,19 @@ func (pm *PipelineManager) initialize(ctx context.Context, override string) erro } // Use the provided pipeline provider if specified, otherwise resolve or determine the provider - pipelineProvider := strings.ToLower(override) - if pipelineProvider == "" { - pipelineProvider, err = pm.resolveProviderAndDetermine(ctx, projectPath, repoRoot) + var pipelineProvider ciProviderType + if override != "" { + p, err := toCiProviderType(strings.ToLower(override)) if err != nil { return err } + pipelineProvider = p + } else { + p, err := pm.resolveProviderAndDetermine(ctx, projectPath, repoRoot) + if err != nil { + return err + } + pipelineProvider = p } prjConfig, err := project.Load(ctx, projectPath) @@ -724,9 +733,40 @@ func (pm *PipelineManager) initialize(ctx context.Context, override string) erro defer func() { _ = infra.Cleanup() }() pm.infra = infra + hasAppHost := pm.importManager.HasAppHost(ctx, prjConfig) + + infraProvider, err := toInfraProviderType(string(pm.infra.Options.Provider)) + if err != nil { + return err + } + + // There are 2 possible options, for the git branch name, when running azd pipeline config: + // - There is not a git repo, so the branch name is empty. In this case, we default to "main". + // - There is a git repo and we can get the name of the current branch. + branchName := "main" + customBranchName, err := pm.gitCli.GetCurrentBranch(ctx, repoRoot) + // It is fine if we can't get the branch name, we will default to "main" + if err == nil { + branchName = customBranchName + } + + // default auth type for all providers + authType := AuthTypeFederated + if pm.args.PipelineAuthTypeName == "" && infraProvider == infraProviderTerraform { + // empty arg for auth and terraform forces client credentials, otherwise, it will be federated + authType = AuthTypeClientCredentials + } + // Check and prompt for missing CI/CD files if err := pm.checkAndPromptForProviderFiles( - ctx, repoRoot, pipelineProvider, string(pm.infra.Options.Provider)); err != nil { + ctx, projectProperties{ + CiProvider: pipelineProvider, + RepoRoot: repoRoot, + InfraProvider: infraProvider, + HasAppHost: hasAppHost, + BranchName: branchName, + AuthType: authType, + }); err != nil { return err } @@ -736,13 +776,13 @@ func (pm *PipelineManager) initialize(ctx context.Context, override string) erro } var scmProviderName, ciProviderName, displayName string - if pipelineProvider == azdoLabel { - scmProviderName = azdoLabel - ciProviderName = azdoLabel + if pipelineProvider == ciProviderAzureDevOps { + scmProviderName = string(ciProviderAzureDevOps) + ciProviderName = scmProviderName displayName = azdoDisplayName } else { - scmProviderName = gitHubLabel - ciProviderName = gitHubLabel + scmProviderName = string(ciProviderGitHubActions) + ciProviderName = scmProviderName displayName = gitHubDisplayName } log.Printf("Using pipeline provider: %s", output.WithHighLightFormat(displayName)) @@ -771,10 +811,10 @@ func (pm *PipelineManager) initialize(ctx context.Context, override string) erro func (pm *PipelineManager) savePipelineProviderToEnv( ctx context.Context, - provider string, + provider ciProviderType, env *environment.Environment, ) error { - env.DotenvSet(envPersistedKey, provider) + env.DotenvSet(envPersistedKey, string(provider)) err := pm.envManager.Save(ctx, env) if err != nil { return err @@ -783,37 +823,32 @@ func (pm *PipelineManager) savePipelineProviderToEnv( } func (pm *PipelineManager) checkAndPromptForProviderFiles( - ctx context.Context, repoRoot, pipelineProvider string, infraProvider string) error { - if pipelineProvider == "" { - log.Println("Pipeline provider is empty, no need to check for files.") - return nil - } + ctx context.Context, props projectProperties) error { + log.Printf("Checking for provider files for: %s", props.CiProvider) - log.Printf("Checking for provider files for: %s", pipelineProvider) - - providerFileChecks := map[string]struct { + providerFileChecks := map[ciProviderType]struct { ymlPath string dirPath string dirDisplayName string providerDisplayName string }{ - gitHubLabel: { - ymlPath: filepath.Join(repoRoot, gitHubYml), - dirPath: filepath.Join(repoRoot, gitHubWorkflowsDirectory), + ciProviderGitHubActions: { + ymlPath: filepath.Join(props.RepoRoot, gitHubYml), + dirPath: filepath.Join(props.RepoRoot, gitHubWorkflowsDirectory), dirDisplayName: gitHubWorkflowsDirectory, providerDisplayName: gitHubDisplayName, }, - azdoLabel: { - ymlPath: filepath.Join(repoRoot, azdoYml), - dirPath: filepath.Join(repoRoot, azdoPipelinesDirectory), + ciProviderAzureDevOps: { + ymlPath: filepath.Join(props.RepoRoot, azdoYml), + dirPath: filepath.Join(props.RepoRoot, azdoPipelinesDirectory), dirDisplayName: azdoPipelinesDirectory, providerDisplayName: azdoDisplayName, }, } - providerCheck, exists := providerFileChecks[pipelineProvider] + providerCheck, exists := providerFileChecks[props.CiProvider] if !exists { - errMsg := fmt.Sprintf("%s is not a known pipeline provider", pipelineProvider) + errMsg := fmt.Sprintf("%s is not a known pipeline provider", props.CiProvider) log.Println("Error:", errMsg) return fmt.Errorf(errMsg) } @@ -823,7 +858,7 @@ func (pm *PipelineManager) checkAndPromptForProviderFiles( if !osutil.FileExists(providerCheck.ymlPath) { log.Printf("%s YAML not found, prompting for creation", providerCheck.providerDisplayName) - if err := pm.promptForCiFiles(ctx, pipelineProvider, infraProvider, repoRoot); err != nil { + if err := pm.promptForCiFiles(ctx, props); err != nil { log.Println("Error prompting for CI files:", err) return err } @@ -838,14 +873,14 @@ func (pm *PipelineManager) checkAndPromptForProviderFiles( } if isEmpty { - if pipelineProvider == azdoLabel { + if props.CiProvider == ciProviderAzureDevOps { message := fmt.Sprintf( "%s provider selected, but %s is empty. Please add pipeline files and try again.", providerCheck.providerDisplayName, providerCheck.dirDisplayName) log.Println("Error:", message) return fmt.Errorf(message) } - if pipelineProvider == gitHubLabel { + if props.CiProvider == ciProviderGitHubActions { message := fmt.Sprintf( "%s provider selected, but %s is empty. Please add pipeline files.", providerCheck.providerDisplayName, providerCheck.dirDisplayName) @@ -855,23 +890,27 @@ func (pm *PipelineManager) checkAndPromptForProviderFiles( pm.console.Message(ctx, "") } - log.Printf("Provider files are present for: %s", pipelineProvider) + log.Printf("Provider files are present for: %s", props.CiProvider) return nil } // promptForCiFiles creates CI/CD files for the specified provider, confirming with the user before creation. -func (pm *PipelineManager) promptForCiFiles(ctx context.Context, pipelineProvider, infraProvider, repoRoot string) error { - paths := map[string]struct { +func (pm *PipelineManager) promptForCiFiles(ctx context.Context, props projectProperties) error { + paths := map[ciProviderType]struct { directory string yml string }{ - gitHubLabel: {filepath.Join(repoRoot, gitHubWorkflowsDirectory), filepath.Join(repoRoot, gitHubYml)}, - azdoLabel: {filepath.Join(repoRoot, azdoPipelinesDirectory), filepath.Join(repoRoot, azdoYml)}, + ciProviderGitHubActions: { + filepath.Join(props.RepoRoot, gitHubWorkflowsDirectory), filepath.Join(props.RepoRoot, gitHubYml), + }, + ciProviderAzureDevOps: { + filepath.Join(props.RepoRoot, azdoPipelinesDirectory), filepath.Join(props.RepoRoot, azdoYml), + }, } - providerPaths, exists := paths[pipelineProvider] + providerPaths, exists := paths[props.CiProvider] if !exists { - errMsg := fmt.Sprintf("Unknown provider: %s", pipelineProvider) + errMsg := fmt.Sprintf("Unknown provider: %s", props.CiProvider) log.Println("Error:", errMsg) return fmt.Errorf(errMsg) } @@ -908,17 +947,8 @@ func (pm *PipelineManager) promptForCiFiles(ctx context.Context, pipelineProvide } if !osutil.FileExists(providerPaths.yml) { - embedFilePath := fmt.Sprintf("pipeline/.%s/azure-dev.yml", pipelineProvider) - if infraProvider == "terraform" { - embedFilePath = fmt.Sprintf("pipeline/.%s/azure-dev-tf.yml", pipelineProvider) - } - contents, err := resources.PipelineFiles.ReadFile(embedFilePath) - if err != nil { - return fmt.Errorf("reading embedded file %s: %w", embedFilePath, err) - } - log.Printf("Creating file %s", providerPaths.yml) - if err := os.WriteFile(providerPaths.yml, contents, osutil.PermissionFile); err != nil { - return fmt.Errorf("creating file %s: %w", providerPaths.yml, err) + if err := generatePipelineDefinition(providerPaths.yml, props); err != nil { + return err } pm.console.Message(ctx, fmt.Sprintf( @@ -927,7 +957,6 @@ func (pm *PipelineManager) promptForCiFiles(ctx context.Context, pipelineProvide output.WithHighLightFormat(providerPaths.yml)), ) pm.console.Message(ctx, "") - } return nil @@ -938,7 +967,38 @@ func (pm *PipelineManager) promptForCiFiles(ctx context.Context, pipelineProvide return nil } -func (pm *PipelineManager) determineProvider(ctx context.Context, repoRoot string) (string, error) { +func generatePipelineDefinition(path string, props projectProperties) error { + embedFilePath := fmt.Sprintf("pipeline/.%s/azure-dev.ymlt", props.CiProvider) + tmpl, err := template. + New("azure-dev.yml"). + Option("missingkey=error"). + ParseFS(resources.PipelineFiles, embedFilePath) + if err != nil { + return fmt.Errorf("parsing embedded file %s: %w", embedFilePath, err) + } + builder := strings.Builder{} + err = tmpl.Execute(&builder, struct { + BranchName string + FedCredLogIn bool + InstallDotNetAspire bool + }{ + BranchName: props.BranchName, + FedCredLogIn: props.AuthType == AuthTypeFederated, + InstallDotNetAspire: props.HasAppHost, + }) + if err != nil { + return fmt.Errorf("executing template: %w", err) + } + + contents := []byte(builder.String()) + log.Printf("Creating file %s", path) + if err := os.WriteFile(path, contents, osutil.PermissionFile); err != nil { + return fmt.Errorf("creating file %s: %w", path, err) + } + return nil +} + +func (pm *PipelineManager) determineProvider(ctx context.Context, repoRoot string) (ciProviderType, error) { log.Printf("Checking for CI/CD YAML files in the repository root: %s", repoRoot) // Check for existence of official YAML files in the repo root @@ -957,22 +1017,22 @@ func (pm *PipelineManager) determineProvider(ctx context.Context, repoRoot strin case hasGitHubYml && !hasAzDevOpsYml: // GitHub Actions YAML found, Azure DevOps YAML not found log.Printf("Only GitHub Actions YAML found. Selecting GitHub Actions as the provider.") - return gitHubLabel, nil + return ciProviderGitHubActions, nil case hasAzDevOpsYml && !hasGitHubYml: // Azure DevOps YAML found, GitHub Actions YAML not found log.Printf("Only Azure DevOps YAML found. Selecting Azure DevOps as the provider.") - return azdoLabel, nil + return ciProviderAzureDevOps, nil default: // Default to GitHub Actions if no provider is specified log.Printf("Defaulting to GitHub Actions as the provider.") - return gitHubLabel, nil + return ciProviderGitHubActions, nil } } // promptForProvider prompts the user to select a CI/CD provider. -func (pm *PipelineManager) promptForProvider(ctx context.Context) (string, error) { +func (pm *PipelineManager) promptForProvider(ctx context.Context) (ciProviderType, error) { log.Printf("Prompting user to select a CI/CD provider.") pm.console.Message(ctx, "") choice, err := pm.console.Select(ctx, input.ConsoleOptions{ @@ -986,9 +1046,9 @@ func (pm *PipelineManager) promptForProvider(ctx context.Context) (string, error log.Printf("User selected choice: %d", choice) if choice == 0 { - return gitHubLabel, nil + return ciProviderGitHubActions, nil } else if choice == 1 { - return azdoLabel, nil + return ciProviderAzureDevOps, nil } return "", nil // This case should never occur with the current options. diff --git a/cli/azd/pkg/pipeline/pipeline_manager_test.go b/cli/azd/pkg/pipeline/pipeline_manager_test.go index f1c22fdf5c0..bd78b573533 100644 --- a/cli/azd/pkg/pipeline/pipeline_manager_test.go +++ b/cli/azd/pkg/pipeline/pipeline_manager_test.go @@ -25,6 +25,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/tools/github" "github.com/azure/azure-dev/cli/azd/test/mocks" "github.com/azure/azure-dev/cli/azd/test/mocks/mockenv" + "github.com/azure/azure-dev/cli/azd/test/snapshot" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -52,12 +53,12 @@ func Test_PipelineManager_Initialize(t *testing.T) { deleteYamlFiles(t, tempDir) - simulateUserInteraction(mockContext, gitHubLabel, true) + simulateUserInteraction(mockContext, ciProviderGitHubActions, true) manager, err := createPipelineManager(t, mockContext, azdContext, nil, nil) assert.NotNil(t, manager) - verifyProvider(t, manager, gitHubLabel, err) + verifyProvider(t, manager, ciProviderGitHubActions, err) // Execute the initialize method, which should trigger the confirmation prompt err = manager.initialize(ctx, "") @@ -74,7 +75,7 @@ func Test_PipelineManager_Initialize(t *testing.T) { deleteYamlFiles(t, tempDir) - simulateUserInteraction(mockContext, gitHubLabel, false) + simulateUserInteraction(mockContext, ciProviderGitHubActions, false) _, err := createPipelineManager(t, mockContext, azdContext, nil, nil) // No error for GitHub, just a message to the console @@ -88,7 +89,7 @@ func Test_PipelineManager_Initialize(t *testing.T) { deleteYamlFiles(t, tempDir) - simulateUserInteraction(mockContext, azdoLabel, false) + simulateUserInteraction(mockContext, ciProviderAzureDevOps, false) manager, err := createPipelineManager(t, mockContext, azdContext, nil, nil) assert.Nil(t, manager) @@ -102,7 +103,7 @@ func Test_PipelineManager_Initialize(t *testing.T) { deleteYamlFiles(t, tempDir) - simulateUserInteraction(mockContext, azdoLabel, true) + simulateUserInteraction(mockContext, ciProviderAzureDevOps, true) // Initialize the PipelineManager manager, err := createPipelineManager(t, mockContext, azdContext, nil, nil) @@ -111,7 +112,7 @@ func Test_PipelineManager_Initialize(t *testing.T) { // Execute the initialize method, which should trigger the confirmation prompt err = manager.initialize(ctx, "") - verifyProvider(t, manager, azdoLabel, err) + verifyProvider(t, manager, ciProviderAzureDevOps, err) // Check if the azure-dev.yml file was created in the expected path azdoYmlPath := filepath.Join(tempDir, azdoYml) @@ -123,10 +124,10 @@ func Test_PipelineManager_Initialize(t *testing.T) { mockContext = resetContext(tempDir, ctx) envValues := map[string]string{} - envValues[envPersistedKey] = azdoLabel + envValues[envPersistedKey] = string(ciProviderAzureDevOps) env := environment.NewWithValues("test-env", envValues) - simulateUserInteraction(mockContext, azdoLabel, false) + simulateUserInteraction(mockContext, ciProviderAzureDevOps, false) manager, err := createPipelineManager(t, mockContext, azdContext, env, nil) assert.Nil(t, manager) @@ -139,13 +140,13 @@ func Test_PipelineManager_Initialize(t *testing.T) { mockContext = resetContext(tempDir, ctx) envValues := map[string]string{} - envValues[envPersistedKey] = azdoLabel + envValues[envPersistedKey] = string(ciProviderAzureDevOps) env := environment.NewWithValues("test-env", envValues) - simulateUserInteraction(mockContext, azdoLabel, true) + simulateUserInteraction(mockContext, ciProviderAzureDevOps, true) manager, err := createPipelineManager(t, mockContext, azdContext, env, nil) - verifyProvider(t, manager, azdoLabel, err) + verifyProvider(t, manager, ciProviderAzureDevOps, err) deleteYamlFiles(t, tempDir) }) @@ -154,10 +155,10 @@ func Test_PipelineManager_Initialize(t *testing.T) { mockContext = resetContext(tempDir, ctx) envValues := map[string]string{} - envValues[envPersistedKey] = gitHubLabel + envValues[envPersistedKey] = string(ciProviderGitHubActions) env := environment.NewWithValues("test-env", envValues) - simulateUserInteraction(mockContext, gitHubLabel, false) + simulateUserInteraction(mockContext, ciProviderGitHubActions, false) _, err := createPipelineManager(t, mockContext, azdContext, env, nil) // No error for GitHub, just a message to the console @@ -171,14 +172,14 @@ func Test_PipelineManager_Initialize(t *testing.T) { mockContext = resetContext(tempDir, ctx) envValues := map[string]string{} - envValues[envPersistedKey] = gitHubLabel + envValues[envPersistedKey] = string(ciProviderGitHubActions) env := environment.NewWithValues("test-env", envValues) - simulateUserInteraction(mockContext, gitHubLabel, true) + simulateUserInteraction(mockContext, ciProviderGitHubActions, true) manager, err := createPipelineManager(t, mockContext, azdContext, env, nil) - verifyProvider(t, manager, gitHubLabel, err) + verifyProvider(t, manager, ciProviderGitHubActions, err) deleteYamlFiles(t, tempDir) }) @@ -186,7 +187,7 @@ func Test_PipelineManager_Initialize(t *testing.T) { // User provides an invalid provider name as an argument mockContext = resetContext(tempDir, ctx) - simulateUserInteraction(mockContext, gitHubLabel, true) + simulateUserInteraction(mockContext, ciProviderGitHubActions, true) args := &PipelineManagerArgs{ PipelineProvider: "other", @@ -194,7 +195,7 @@ func Test_PipelineManager_Initialize(t *testing.T) { manager, err := createPipelineManager(t, mockContext, azdContext, nil, args) assert.Nil(t, manager) - assert.EqualError(t, err, "other is not a known pipeline provider") + assert.EqualError(t, err, "invalid ci provider type other") deleteYamlFiles(t, tempDir) }) @@ -202,7 +203,7 @@ func Test_PipelineManager_Initialize(t *testing.T) { // User provides an invalid provider name in env mockContext = resetContext(tempDir, ctx) - simulateUserInteraction(mockContext, gitHubLabel, true) + simulateUserInteraction(mockContext, ciProviderGitHubActions, true) envValues := map[string]string{} envValues[envPersistedKey] = "other" @@ -210,20 +211,20 @@ func Test_PipelineManager_Initialize(t *testing.T) { manager, err := createPipelineManager(t, mockContext, azdContext, env, nil) assert.Nil(t, manager) - assert.EqualError(t, err, "other is not a known pipeline provider") + assert.EqualError(t, err, "invalid ci provider type other") deleteYamlFiles(t, tempDir) }) t.Run("unknown override value from yaml", func(t *testing.T) { mockContext = resetContext(tempDir, ctx) - simulateUserInteraction(mockContext, gitHubLabel, true) + simulateUserInteraction(mockContext, ciProviderGitHubActions, true) appendToAzureYaml(t, projectFileName, "pipeline:\n\r provider: other") manager, err := createPipelineManager(t, mockContext, azdContext, nil, nil) assert.Nil(t, manager) - assert.EqualError(t, err, "other is not a known pipeline provider") + assert.EqualError(t, err, "invalid ci provider type other") resetAzureYaml(t, projectFileName) deleteYamlFiles(t, tempDir) @@ -231,7 +232,7 @@ func Test_PipelineManager_Initialize(t *testing.T) { t.Run("override persisted value with yaml", func(t *testing.T) { mockContext = resetContext(tempDir, ctx) - simulateUserInteraction(mockContext, gitHubLabel, true) + simulateUserInteraction(mockContext, ciProviderGitHubActions, true) appendToAzureYaml(t, projectFileName, "pipeline:\n\r provider: fromYaml") @@ -241,7 +242,7 @@ func Test_PipelineManager_Initialize(t *testing.T) { manager, err := createPipelineManager(t, mockContext, azdContext, env, nil) assert.Nil(t, manager) - assert.EqualError(t, err, "fromYaml is not a known pipeline provider") + assert.EqualError(t, err, "invalid ci provider type fromYaml") resetAzureYaml(t, projectFileName) deleteYamlFiles(t, tempDir) @@ -249,7 +250,7 @@ func Test_PipelineManager_Initialize(t *testing.T) { t.Run("override persisted and yaml with arg", func(t *testing.T) { mockContext = resetContext(tempDir, ctx) - simulateUserInteraction(mockContext, gitHubLabel, true) + simulateUserInteraction(mockContext, ciProviderGitHubActions, true) appendToAzureYaml(t, projectFileName, "pipeline:\n\r provider: fromYaml") @@ -262,7 +263,7 @@ func Test_PipelineManager_Initialize(t *testing.T) { manager, err := createPipelineManager(t, mockContext, azdContext, env, args) assert.Nil(t, manager) - assert.EqualError(t, err, "arg is not a known pipeline provider") + assert.EqualError(t, err, "invalid ci provider type arg") resetAzureYaml(t, projectFileName) deleteYamlFiles(t, tempDir) @@ -270,22 +271,22 @@ func Test_PipelineManager_Initialize(t *testing.T) { t.Run("github directory only", func(t *testing.T) { mockContext = resetContext(tempDir, ctx) - simulateUserInteraction(mockContext, gitHubLabel, true) + simulateUserInteraction(mockContext, ciProviderGitHubActions, true) manager, err := createPipelineManager(t, mockContext, azdContext, nil, nil) - verifyProvider(t, manager, gitHubLabel, err) + verifyProvider(t, manager, ciProviderGitHubActions, err) deleteYamlFiles(t, tempDir) }) t.Run("azdo directory only", func(t *testing.T) { mockContext = resetContext(tempDir, ctx) - simulateUserInteraction(mockContext, azdoLabel, true) + simulateUserInteraction(mockContext, ciProviderAzureDevOps, true) manager, err := createPipelineManager(t, mockContext, azdContext, nil, nil) - verifyProvider(t, manager, azdoLabel, err) + verifyProvider(t, manager, ciProviderAzureDevOps, err) deleteYamlFiles(t, tempDir) }) @@ -294,7 +295,7 @@ func Test_PipelineManager_Initialize(t *testing.T) { createYamlFiles(t, tempDir) - simulateUserInteraction(mockContext, gitHubLabel, true) + simulateUserInteraction(mockContext, ciProviderGitHubActions, true) // Initialize the PipelineManager manager, err := createPipelineManager(t, mockContext, azdContext, nil, nil) @@ -304,7 +305,7 @@ func Test_PipelineManager_Initialize(t *testing.T) { // Execute the initialize method, which should trigger the provider selection prompt err = manager.initialize(ctx, "") - verifyProvider(t, manager, gitHubLabel, err) + verifyProvider(t, manager, ciProviderGitHubActions, err) deleteYamlFiles(t, tempDir) }) @@ -314,7 +315,7 @@ func Test_PipelineManager_Initialize(t *testing.T) { createYamlFiles(t, tempDir) - simulateUserInteraction(mockContext, azdoLabel, true) + simulateUserInteraction(mockContext, ciProviderAzureDevOps, true) // Initialize the PipelineManager manager, err := createPipelineManager(t, mockContext, azdContext, nil, nil) @@ -324,7 +325,7 @@ func Test_PipelineManager_Initialize(t *testing.T) { // Execute the initialize method, which should trigger the provider selection prompt err = manager.initialize(ctx, "") - verifyProvider(t, manager, azdoLabel, err) + verifyProvider(t, manager, ciProviderAzureDevOps, err) deleteYamlFiles(t, tempDir) }) @@ -333,25 +334,25 @@ func Test_PipelineManager_Initialize(t *testing.T) { mockContext = resetContext(tempDir, ctx) - simulateUserInteraction(mockContext, azdoLabel, true) + simulateUserInteraction(mockContext, ciProviderAzureDevOps, true) env := environment.New("test") args := &PipelineManagerArgs{ - PipelineProvider: azdoLabel, + PipelineProvider: string(ciProviderAzureDevOps), } manager, err := createPipelineManager(t, mockContext, azdContext, env, args) - verifyProvider(t, manager, azdoLabel, err) + verifyProvider(t, manager, ciProviderAzureDevOps, err) envValue, found := env.Dotenv()[envPersistedKey] assert.True(t, found) - assert.Equal(t, azdoLabel, envValue) + assert.Equal(t, ciProviderType(envValue), ciProviderAzureDevOps) // Calling function again with same env and without override arg should use the persisted err = manager.initialize(*mockContext.Context, "") - verifyProvider(t, manager, azdoLabel, err) + verifyProvider(t, manager, ciProviderAzureDevOps, err) deleteYamlFiles(t, tempDir) }) @@ -363,16 +364,16 @@ func Test_PipelineManager_Initialize(t *testing.T) { env := environment.New("test") args := &PipelineManagerArgs{ - PipelineProvider: azdoLabel, + PipelineProvider: string(ciProviderAzureDevOps), } manager, err := createPipelineManager(t, mockContext, azdContext, env, args) - verifyProvider(t, manager, azdoLabel, err) + verifyProvider(t, manager, ciProviderAzureDevOps, err) // Calling function again with same env and without override arg should use the persisted err = manager.initialize(*mockContext.Context, "") - verifyProvider(t, manager, azdoLabel, err) + verifyProvider(t, manager, ciProviderAzureDevOps, err) // Write yaml to override appendToAzureYaml(t, projectFileName, "pipeline:\n\r provider: github") @@ -380,30 +381,31 @@ func Test_PipelineManager_Initialize(t *testing.T) { // Calling function again with same env and without override arg should detect yaml change and override persisted err = manager.initialize(*mockContext.Context, "") - verifyProvider(t, manager, gitHubLabel, err) + verifyProvider(t, manager, ciProviderGitHubActions, err) // the persisted choice should be updated based on the value set on yaml envValue, found := env.Dotenv()[envPersistedKey] assert.True(t, found) - assert.Equal(t, gitHubLabel, envValue) + assert.Equal(t, ciProviderType(envValue), ciProviderGitHubActions) // Call again to check persisted(github) after one change (and yaml is still present) err = manager.initialize(*mockContext.Context, "") - verifyProvider(t, manager, gitHubLabel, err) + verifyProvider(t, manager, ciProviderGitHubActions, err) // Check argument override having yaml(github) config and persisted config(github) - err = manager.initialize(*mockContext.Context, azdoLabel) - verifyProvider(t, manager, azdoLabel, err) + expected := string(ciProviderAzureDevOps) + err = manager.initialize(*mockContext.Context, expected) + verifyProvider(t, manager, ciProviderAzureDevOps, err) // the persisted selection is now azdo(env) but yaml is github envValue, found = env.Dotenv()[envPersistedKey] assert.True(t, found) - assert.Equal(t, azdoLabel, envValue) + assert.Equal(t, expected, envValue) // persisted = azdo (per last run) and yaml = github, should return github // as yaml overrides a persisted run err = manager.initialize(*mockContext.Context, "") - verifyProvider(t, manager, gitHubLabel, err) + verifyProvider(t, manager, ciProviderGitHubActions, err) // reset state resetAzureYaml(t, projectFileName) @@ -412,6 +414,185 @@ func Test_PipelineManager_Initialize(t *testing.T) { }) } +func Test_promptForCiFiles(t *testing.T) { + t.Run("no files - github selected - no app host - fed Cred", func(t *testing.T) { + tempDir := t.TempDir() + path := filepath.Join(tempDir, gitHubWorkflowsDirectory) + err := os.MkdirAll(path, osutil.PermissionDirectory) + assert.NoError(t, err) + expectedPath := filepath.Join(tempDir, gitHubYml) + err = generatePipelineDefinition(expectedPath, projectProperties{ + CiProvider: ciProviderGitHubActions, + InfraProvider: infraProviderBicep, + RepoRoot: tempDir, + HasAppHost: false, + BranchName: "main", + AuthType: AuthTypeFederated, + }) + assert.NoError(t, err) + // should've created the pipeline + assert.FileExists(t, expectedPath) + // open the file and check the content + content, err := os.ReadFile(expectedPath) + assert.NoError(t, err) + snapshot.SnapshotT(t, normalizeEOL(content)) + }) + t.Run("no files - github selected - App host - fed Cred", func(t *testing.T) { + tempDir := t.TempDir() + path := filepath.Join(tempDir, gitHubWorkflowsDirectory) + err := os.MkdirAll(path, osutil.PermissionDirectory) + assert.NoError(t, err) + expectedPath := filepath.Join(tempDir, gitHubYml) + err = generatePipelineDefinition(expectedPath, projectProperties{ + CiProvider: ciProviderGitHubActions, + InfraProvider: infraProviderBicep, + RepoRoot: tempDir, + HasAppHost: true, + BranchName: "main", + AuthType: AuthTypeFederated, + }) + assert.NoError(t, err) + // should've created the pipeline + assert.FileExists(t, expectedPath) + // open the file and check the content + content, err := os.ReadFile(expectedPath) + assert.NoError(t, err) + snapshot.SnapshotT(t, normalizeEOL(content)) + }) + t.Run("no files - azdo selected - App host - fed Cred", func(t *testing.T) { + tempDir := t.TempDir() + path := filepath.Join(tempDir, gitHubWorkflowsDirectory) + err := os.MkdirAll(path, osutil.PermissionDirectory) + assert.NoError(t, err) + expectedPath := filepath.Join(tempDir, gitHubYml) + err = generatePipelineDefinition(expectedPath, projectProperties{ + CiProvider: ciProviderAzureDevOps, + InfraProvider: infraProviderBicep, + RepoRoot: tempDir, + HasAppHost: true, + BranchName: "main", + AuthType: AuthTypeFederated, + }) + assert.NoError(t, err) + // should've created the pipeline + assert.FileExists(t, expectedPath) + // open the file and check the content + content, err := os.ReadFile(expectedPath) + assert.NoError(t, err) + snapshot.SnapshotT(t, normalizeEOL(content)) + }) + t.Run("no files - github selected - no app host - client cred", func(t *testing.T) { + tempDir := t.TempDir() + path := filepath.Join(tempDir, gitHubWorkflowsDirectory) + err := os.MkdirAll(path, osutil.PermissionDirectory) + assert.NoError(t, err) + expectedPath := filepath.Join(tempDir, gitHubYml) + err = generatePipelineDefinition(expectedPath, projectProperties{ + CiProvider: ciProviderGitHubActions, + InfraProvider: infraProviderBicep, + RepoRoot: tempDir, + HasAppHost: false, + BranchName: "main", + AuthType: AuthTypeClientCredentials, + }) + assert.NoError(t, err) + // should've created the pipeline + assert.FileExists(t, expectedPath) + // open the file and check the content + content, err := os.ReadFile(expectedPath) + assert.NoError(t, err) + snapshot.SnapshotT(t, normalizeEOL(content)) + }) + t.Run("no files - github selected - branch name", func(t *testing.T) { + tempDir := t.TempDir() + path := filepath.Join(tempDir, gitHubWorkflowsDirectory) + err := os.MkdirAll(path, osutil.PermissionDirectory) + assert.NoError(t, err) + expectedPath := filepath.Join(tempDir, gitHubYml) + err = generatePipelineDefinition(expectedPath, projectProperties{ + CiProvider: ciProviderGitHubActions, + InfraProvider: infraProviderBicep, + RepoRoot: tempDir, + HasAppHost: false, + BranchName: "non-main", + AuthType: AuthTypeFederated, + }) + assert.NoError(t, err) + // should've created the pipeline + assert.FileExists(t, expectedPath) + // open the file and check the content + content, err := os.ReadFile(expectedPath) + assert.NoError(t, err) + snapshot.SnapshotT(t, normalizeEOL(content)) + }) + t.Run("no files - azdo selected - no app host - fed Cred", func(t *testing.T) { + tempDir := t.TempDir() + path := filepath.Join(tempDir, gitHubWorkflowsDirectory) + err := os.MkdirAll(path, osutil.PermissionDirectory) + assert.NoError(t, err) + expectedPath := filepath.Join(tempDir, gitHubYml) + err = generatePipelineDefinition(expectedPath, projectProperties{ + CiProvider: ciProviderAzureDevOps, + InfraProvider: infraProviderBicep, + RepoRoot: tempDir, + HasAppHost: false, + BranchName: "main", + AuthType: AuthTypeFederated, + }) + assert.NoError(t, err) + // should've created the pipeline + assert.FileExists(t, expectedPath) + // open the file and check the content + content, err := os.ReadFile(expectedPath) + assert.NoError(t, err) + snapshot.SnapshotT(t, normalizeEOL(content)) + }) + t.Run("no files - azdo selected - no app host - client cred", func(t *testing.T) { + tempDir := t.TempDir() + path := filepath.Join(tempDir, gitHubWorkflowsDirectory) + err := os.MkdirAll(path, osutil.PermissionDirectory) + assert.NoError(t, err) + expectedPath := filepath.Join(tempDir, gitHubYml) + err = generatePipelineDefinition(expectedPath, projectProperties{ + CiProvider: ciProviderAzureDevOps, + InfraProvider: infraProviderBicep, + RepoRoot: tempDir, + HasAppHost: false, + BranchName: "main", + AuthType: AuthTypeClientCredentials, + }) + assert.NoError(t, err) + // should've created the pipeline + assert.FileExists(t, expectedPath) + // open the file and check the content + content, err := os.ReadFile(expectedPath) + assert.NoError(t, err) + snapshot.SnapshotT(t, normalizeEOL(content)) + }) + t.Run("no files - azdo selected - branch name", func(t *testing.T) { + tempDir := t.TempDir() + path := filepath.Join(tempDir, gitHubWorkflowsDirectory) + err := os.MkdirAll(path, osutil.PermissionDirectory) + assert.NoError(t, err) + expectedPath := filepath.Join(tempDir, gitHubYml) + err = generatePipelineDefinition(expectedPath, projectProperties{ + CiProvider: ciProviderAzureDevOps, + InfraProvider: infraProviderBicep, + RepoRoot: tempDir, + HasAppHost: false, + BranchName: "non-main", + AuthType: AuthTypeFederated, + }) + assert.NoError(t, err) + // should've created the pipeline + assert.FileExists(t, expectedPath) + // open the file and check the content + content, err := os.ReadFile(expectedPath) + assert.NoError(t, err) + snapshot.SnapshotT(t, normalizeEOL(content)) + }) +} + func createPipelineManager( t *testing.T, mockContext *mocks.MockContext, @@ -493,6 +674,11 @@ func setupGitCliMocks(mockContext *mocks.MockContext, repoPath string) { }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { return exec.NewRunResult(0, repoPath, ""), nil }) + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "branch --show-current") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + return exec.NewRunResult(0, "main", ""), nil + }) } func resetAzureYaml(t *testing.T, projectFilePath string) { @@ -545,7 +731,7 @@ func resetContext(tempDir string, ctx context.Context) *mocks.MockContext { return newMockContext } -func createYamlFiles(t *testing.T, tempDir string, createOptions ...string) { +func createYamlFiles(t *testing.T, tempDir string, createOptions ...ciProviderType) { shouldCreateGitHub := true shouldCreateAzdo := true @@ -554,9 +740,9 @@ func createYamlFiles(t *testing.T, tempDir string, createOptions ...string) { shouldCreateAzdo = false for _, option := range createOptions { switch option { - case gitHubLabel: + case ciProviderGitHubActions: shouldCreateGitHub = true - case azdoLabel: + case ciProviderAzureDevOps: shouldCreateAzdo = true } } @@ -587,7 +773,7 @@ func createYamlFiles(t *testing.T, tempDir string, createOptions ...string) { } } -func deleteYamlFiles(t *testing.T, tempDir string, deleteOptions ...string) { +func deleteYamlFiles(t *testing.T, tempDir string, deleteOptions ...ciProviderType) { shouldDeleteGitHub := true shouldDeleteAzdo := true @@ -596,9 +782,9 @@ func deleteYamlFiles(t *testing.T, tempDir string, deleteOptions ...string) { shouldDeleteAzdo = false for _, option := range deleteOptions { switch option { - case gitHubLabel: + case ciProviderGitHubActions: shouldDeleteGitHub = true - case azdoLabel: + case ciProviderAzureDevOps: shouldDeleteAzdo = true } } @@ -619,13 +805,13 @@ func deleteYamlFiles(t *testing.T, tempDir string, deleteOptions ...string) { } } -func simulateUserInteraction(mockContext *mocks.MockContext, providerLabel string, createConfirmation bool) { +func simulateUserInteraction(mockContext *mocks.MockContext, providerLabel ciProviderType, createConfirmation bool) { var providerIndex int switch providerLabel { - case gitHubLabel: + case ciProviderGitHubActions: providerIndex = 0 - case azdoLabel: + case ciProviderAzureDevOps: providerIndex = 1 default: providerIndex = 0 @@ -644,17 +830,21 @@ func simulateUserInteraction(mockContext *mocks.MockContext, providerLabel strin }).Respond(createConfirmation) } -func verifyProvider(t *testing.T, manager *PipelineManager, providerLabel string, err error) { +func verifyProvider(t *testing.T, manager *PipelineManager, providerLabel ciProviderType, err error) { assert.NoError(t, err) switch providerLabel { - case gitHubLabel: + case ciProviderGitHubActions: assert.IsType(t, &GitHubScmProvider{}, manager.scmProvider) assert.IsType(t, &GitHubCiProvider{}, manager.ciProvider) - case azdoLabel: + case ciProviderAzureDevOps: assert.IsType(t, &AzdoScmProvider{}, manager.scmProvider) assert.IsType(t, &AzdoCiProvider{}, manager.ciProvider) default: t.Fatalf("%s is not a known pipeline provider", providerLabel) } } + +func normalizeEOL(input []byte) string { + return strings.ReplaceAll(string(input), "\r\n", "\n") +} diff --git a/cli/azd/resources/pipeline/.azdo/azure-dev.yml b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_azdo_selected_-_App_host_-_fed_Cred.snap similarity index 64% rename from cli/azd/resources/pipeline/.azdo/azure-dev.yml rename to cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_azdo_selected_-_App_host_-_fed_Cred.snap index 8bc0f94570c..0a096b1b77a 100644 --- a/cli/azd/resources/pipeline/.azdo/azure-dev.yml +++ b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_azdo_selected_-_App_host_-_fed_Cred.snap @@ -1,19 +1,15 @@ -# Run when commits are pushed to mainline branch (main or master) -# Set this to the mainline branch you are using +# Run when commits are pushed to main trigger: - main - - master - -# Azure Pipelines workflow to deploy to Azure using azd -# To configure required secrets and service connection for connecting to Azure, simply run `azd pipeline config --provider azdo` -# Task "Install azd" needs to install setup-azd extension for azdo - https://marketplace.visualstudio.com/items?itemName=ms-azuretools.azd -# See below for alternative task to install azd if you can't install above task in your organization pool: vmImage: ubuntu-latest steps: - - task: setup-azd@0 + # setup-azd@0 needs to be manually installed in your organization + # if you can't install it, you can use the below bash script to install azd + # and remove this step + - task: setup-azd@0 displayName: Install azd # If you can't install above task in your organization, you can comment it and uncomment below task to install azd @@ -28,6 +24,12 @@ steps: - pwsh: | azd config set auth.useAzCliAuth "true" displayName: Configure AZD to Use AZ CLI Authentication. + - task: Bash@3 + displayName: Install .NET Aspire workload + inputs: + targetType: 'inline' + script: | + dotnet workload install aspire - task: AzureCLI@2 displayName: Provision Infrastructure @@ -35,13 +37,14 @@ steps: azureSubscription: azconnection scriptType: bash scriptLocation: inlineScript + keepAzSessionActive: true inlineScript: | azd provision --no-prompt env: AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) AZURE_ENV_NAME: $(AZURE_ENV_NAME) AZURE_LOCATION: $(AZURE_LOCATION) - AZD_INITIAL_ENVIRONMENT_CONFIG: $(secrets.AZD_INITIAL_ENVIRONMENT_CONFIG) + AZD_INITIAL_ENVIRONMENT_CONFIG: $(AZD_INITIAL_ENVIRONMENT_CONFIG) - task: AzureCLI@2 displayName: Deploy Application @@ -49,9 +52,12 @@ steps: azureSubscription: azconnection scriptType: bash scriptLocation: inlineScript + keepAzSessionActive: true inlineScript: | azd deploy --no-prompt env: AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) AZURE_ENV_NAME: $(AZURE_ENV_NAME) - AZURE_LOCATION: $(AZURE_LOCATION) \ No newline at end of file + AZURE_LOCATION: $(AZURE_LOCATION) + + diff --git a/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_azdo_selected_-_branch_name.snap b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_azdo_selected_-_branch_name.snap new file mode 100644 index 00000000000..ae837d077ee --- /dev/null +++ b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_azdo_selected_-_branch_name.snap @@ -0,0 +1,56 @@ +# Run when commits are pushed to non-main +trigger: + - non-main + +pool: + vmImage: ubuntu-latest + +steps: + # setup-azd@0 needs to be manually installed in your organization + # if you can't install it, you can use the below bash script to install azd + # and remove this step + - task: setup-azd@0 + displayName: Install azd + + # If you can't install above task in your organization, you can comment it and uncomment below task to install azd + # - task: Bash@3 + # displayName: Install azd + # inputs: + # targetType: 'inline' + # script: | + # curl -fsSL https://aka.ms/install-azd.sh | bash + + # azd delegate auth to az to use service connection with AzureCLI@2 + - pwsh: | + azd config set auth.useAzCliAuth "true" + displayName: Configure AZD to Use AZ CLI Authentication. + - task: AzureCLI@2 + displayName: Provision Infrastructure + inputs: + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + keepAzSessionActive: true + inlineScript: | + azd provision --no-prompt + env: + AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) + AZURE_ENV_NAME: $(AZURE_ENV_NAME) + AZURE_LOCATION: $(AZURE_LOCATION) + AZD_INITIAL_ENVIRONMENT_CONFIG: $(AZD_INITIAL_ENVIRONMENT_CONFIG) + + - task: AzureCLI@2 + displayName: Deploy Application + inputs: + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + keepAzSessionActive: true + inlineScript: | + azd deploy --no-prompt + env: + AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) + AZURE_ENV_NAME: $(AZURE_ENV_NAME) + AZURE_LOCATION: $(AZURE_LOCATION) + + diff --git a/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_azdo_selected_-_no_app_host_-_client_cred.snap b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_azdo_selected_-_no_app_host_-_client_cred.snap new file mode 100644 index 00000000000..02e7ca228bd --- /dev/null +++ b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_azdo_selected_-_no_app_host_-_client_cred.snap @@ -0,0 +1,56 @@ +# Run when commits are pushed to main +trigger: + - main + +pool: + vmImage: ubuntu-latest + +steps: + # setup-azd@0 needs to be manually installed in your organization + # if you can't install it, you can use the below bash script to install azd + # and remove this step + - task: setup-azd@0 + displayName: Install azd + + # If you can't install above task in your organization, you can comment it and uncomment below task to install azd + # - task: Bash@3 + # displayName: Install azd + # inputs: + # targetType: 'inline' + # script: | + # curl -fsSL https://aka.ms/install-azd.sh | bash + + # azd delegate auth to az to use service connection with AzureCLI@2 + - pwsh: | + azd config set auth.useAzCliAuth "true" + displayName: Configure AZD to Use AZ CLI Authentication. + - task: AzureCLI@2 + displayName: Provision Infrastructure + inputs: + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + keepAzSessionActive: true + inlineScript: | + azd provision --no-prompt + env: + AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) + AZURE_ENV_NAME: $(AZURE_ENV_NAME) + AZURE_LOCATION: $(AZURE_LOCATION) + AZD_INITIAL_ENVIRONMENT_CONFIG: $(AZD_INITIAL_ENVIRONMENT_CONFIG) + + - task: AzureCLI@2 + displayName: Deploy Application + inputs: + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + keepAzSessionActive: true + inlineScript: | + azd deploy --no-prompt + env: + AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) + AZURE_ENV_NAME: $(AZURE_ENV_NAME) + AZURE_LOCATION: $(AZURE_LOCATION) + + diff --git a/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_azdo_selected_-_no_app_host_-_fed_Cred.snap b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_azdo_selected_-_no_app_host_-_fed_Cred.snap new file mode 100644 index 00000000000..02e7ca228bd --- /dev/null +++ b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_azdo_selected_-_no_app_host_-_fed_Cred.snap @@ -0,0 +1,56 @@ +# Run when commits are pushed to main +trigger: + - main + +pool: + vmImage: ubuntu-latest + +steps: + # setup-azd@0 needs to be manually installed in your organization + # if you can't install it, you can use the below bash script to install azd + # and remove this step + - task: setup-azd@0 + displayName: Install azd + + # If you can't install above task in your organization, you can comment it and uncomment below task to install azd + # - task: Bash@3 + # displayName: Install azd + # inputs: + # targetType: 'inline' + # script: | + # curl -fsSL https://aka.ms/install-azd.sh | bash + + # azd delegate auth to az to use service connection with AzureCLI@2 + - pwsh: | + azd config set auth.useAzCliAuth "true" + displayName: Configure AZD to Use AZ CLI Authentication. + - task: AzureCLI@2 + displayName: Provision Infrastructure + inputs: + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + keepAzSessionActive: true + inlineScript: | + azd provision --no-prompt + env: + AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) + AZURE_ENV_NAME: $(AZURE_ENV_NAME) + AZURE_LOCATION: $(AZURE_LOCATION) + AZD_INITIAL_ENVIRONMENT_CONFIG: $(AZD_INITIAL_ENVIRONMENT_CONFIG) + + - task: AzureCLI@2 + displayName: Deploy Application + inputs: + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + keepAzSessionActive: true + inlineScript: | + azd deploy --no-prompt + env: + AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) + AZURE_ENV_NAME: $(AZURE_ENV_NAME) + AZURE_LOCATION: $(AZURE_LOCATION) + + diff --git a/cli/azd/resources/pipeline/.github/azure-dev.yml b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_github_selected_-_App_host_-_fed_Cred.snap similarity index 65% rename from cli/azd/resources/pipeline/.github/azure-dev.yml rename to cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_github_selected_-_App_host_-_fed_Cred.snap index 6da1a44a44a..a9e5226c018 100644 --- a/cli/azd/resources/pipeline/.github/azure-dev.yml +++ b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_github_selected_-_App_host_-_fed_Cred.snap @@ -1,3 +1,4 @@ +# Run when commits are pushed to main on: workflow_dispatch: push: @@ -5,10 +6,6 @@ on: # Set this to the mainline branch you are using branches: - main - - master - -# GitHub Actions workflow to deploy to Azure using azd -# To configure required secrets for connecting to Azure, simply run `azd pipeline config` # Set up permissions for deploying with secretless Azure federated credentials # https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication @@ -16,6 +13,7 @@ permissions: id-token: write contents: read + jobs: build: runs-on: ubuntu-latest @@ -28,12 +26,12 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: Install azd uses: Azure/setup-azd@v1.0.0 + - name: Install .NET Aspire workload + run: dotnet workload install aspire - name: Log in with Azure (Federated Credentials) - if: ${{ env.AZURE_CLIENT_ID != '' }} run: | azd auth login ` --client-id "$Env:AZURE_CLIENT_ID" ` @@ -41,19 +39,6 @@ jobs: --tenant-id "$Env:AZURE_TENANT_ID" shell: pwsh - - name: Log in with Azure (Client Credentials) - if: ${{ env.AZURE_CREDENTIALS != '' }} - run: | - $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; - Write-Host "::add-mask::$($info.clientSecret)" - - azd auth login ` - --client-id "$($info.clientId)" ` - --client-secret "$($info.clientSecret)" ` - --tenant-id "$($info.tenantId)" - shell: pwsh - env: - AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} - name: Provision Infrastructure run: azd provision --no-prompt @@ -61,4 +46,5 @@ jobs: AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }} - name: Deploy Application - run: azd deploy --no-prompt \ No newline at end of file + run: azd deploy --no-prompt + diff --git a/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_github_selected_-_branch_name.snap b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_github_selected_-_branch_name.snap new file mode 100644 index 00000000000..1bb86cf7f14 --- /dev/null +++ b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_github_selected_-_branch_name.snap @@ -0,0 +1,47 @@ +# Run when commits are pushed to non-main +on: + workflow_dispatch: + push: + # Run when commits are pushed to mainline branch (main or master) + # Set this to the mainline branch you are using + branches: + - non-main + +# Set up permissions for deploying with secretless Azure federated credentials +# https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication +permissions: + id-token: write + contents: read + + +jobs: + build: + runs-on: ubuntu-latest + env: + AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install azd + uses: Azure/setup-azd@v1.0.0 + - name: Log in with Azure (Federated Credentials) + run: | + azd auth login ` + --client-id "$Env:AZURE_CLIENT_ID" ` + --federated-credential-provider "github" ` + --tenant-id "$Env:AZURE_TENANT_ID" + shell: pwsh + + + - name: Provision Infrastructure + run: azd provision --no-prompt + env: + AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }} + + - name: Deploy Application + run: azd deploy --no-prompt + diff --git a/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_github_selected_-_no_app_host_-_client_cred.snap b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_github_selected_-_no_app_host_-_client_cred.snap new file mode 100644 index 00000000000..011c8cf6966 --- /dev/null +++ b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_github_selected_-_no_app_host_-_client_cred.snap @@ -0,0 +1,47 @@ +# Run when commits are pushed to main +on: + workflow_dispatch: + push: + # Run when commits are pushed to mainline branch (main or master) + # Set this to the mainline branch you are using + branches: + - main + + + +jobs: + build: + runs-on: ubuntu-latest + env: + AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install azd + uses: Azure/setup-azd@v1.0.0 + - name: Log in with Azure (Client Credentials) + run: | + $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; + Write-Host "::add-mask::$($info.clientSecret)" + + azd auth login ` + --client-id "$($info.clientId)" ` + --client-secret "$($info.clientSecret)" ` + --tenant-id "$($info.tenantId)" + shell: pwsh + env: + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} + + + - name: Provision Infrastructure + run: azd provision --no-prompt + env: + AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }} + + - name: Deploy Application + run: azd deploy --no-prompt + diff --git a/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_github_selected_-_no_app_host_-_fed_Cred.snap b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_github_selected_-_no_app_host_-_fed_Cred.snap new file mode 100644 index 00000000000..624248d998d --- /dev/null +++ b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_github_selected_-_no_app_host_-_fed_Cred.snap @@ -0,0 +1,47 @@ +# Run when commits are pushed to main +on: + workflow_dispatch: + push: + # Run when commits are pushed to mainline branch (main or master) + # Set this to the mainline branch you are using + branches: + - main + +# Set up permissions for deploying with secretless Azure federated credentials +# https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication +permissions: + id-token: write + contents: read + + +jobs: + build: + runs-on: ubuntu-latest + env: + AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install azd + uses: Azure/setup-azd@v1.0.0 + - name: Log in with Azure (Federated Credentials) + run: | + azd auth login ` + --client-id "$Env:AZURE_CLIENT_ID" ` + --federated-credential-provider "github" ` + --tenant-id "$Env:AZURE_TENANT_ID" + shell: pwsh + + + - name: Provision Infrastructure + run: azd provision --no-prompt + env: + AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }} + + - name: Deploy Application + run: azd deploy --no-prompt + diff --git a/cli/azd/pkg/project/importer.go b/cli/azd/pkg/project/importer.go index d4c4933fced..3f5ec6bc5b4 100644 --- a/cli/azd/pkg/project/importer.go +++ b/cli/azd/pkg/project/importer.go @@ -99,6 +99,20 @@ func (im *ImportManager) ServiceStable(ctx context.Context, projectConfig *Proje return allServicesSlice, nil } +// HasAppHost returns true when there is one AppHost (Aspire) in the project. +func (im *ImportManager) HasAppHost(ctx context.Context, projectConfig *ProjectConfig) bool { + for _, svcConfig := range projectConfig.Services { + if svcConfig.Language == ServiceLanguageDotNet { + if canImport, err := im.dotNetImporter.CanImport(ctx, svcConfig.Path()); canImport { + return true + } else if err != nil { + log.Printf("error checking if %s is an app host project: %v", svcConfig.Path(), err) + } + } + } + return false +} + // defaultOptions for infra settings. These values are applied across provisioning providers. const ( DefaultModule = "main" diff --git a/cli/azd/resources/pipeline/.azdo/azure-dev.ymlt b/cli/azd/resources/pipeline/.azdo/azure-dev.ymlt new file mode 100644 index 00000000000..248b13545c1 --- /dev/null +++ b/cli/azd/resources/pipeline/.azdo/azure-dev.ymlt @@ -0,0 +1,65 @@ +{{define "azure-dev.yml" -}} +# Run when commits are pushed to {{.BranchName}} +trigger: + - {{.BranchName}} + +pool: + vmImage: ubuntu-latest + +steps: + # setup-azd@0 needs to be manually installed in your organization + # if you can't install it, you can use the below bash script to install azd + # and remove this step + - task: setup-azd@0 + displayName: Install azd + + # If you can't install above task in your organization, you can comment it and uncomment below task to install azd + # - task: Bash@3 + # displayName: Install azd + # inputs: + # targetType: 'inline' + # script: | + # curl -fsSL https://aka.ms/install-azd.sh | bash + + # azd delegate auth to az to use service connection with AzureCLI@2 + - pwsh: | + azd config set auth.useAzCliAuth "true" + displayName: Configure AZD to Use AZ CLI Authentication. +{{- if .InstallDotNetAspire}} + - task: Bash@3 + displayName: Install .NET Aspire workload + inputs: + targetType: 'inline' + script: | + dotnet workload install aspire +{{ end }} + - task: AzureCLI@2 + displayName: Provision Infrastructure + inputs: + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + keepAzSessionActive: true + inlineScript: | + azd provision --no-prompt + env: + AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) + AZURE_ENV_NAME: $(AZURE_ENV_NAME) + AZURE_LOCATION: $(AZURE_LOCATION) + AZD_INITIAL_ENVIRONMENT_CONFIG: $(AZD_INITIAL_ENVIRONMENT_CONFIG) + + - task: AzureCLI@2 + displayName: Deploy Application + inputs: + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + keepAzSessionActive: true + inlineScript: | + azd deploy --no-prompt + env: + AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) + AZURE_ENV_NAME: $(AZURE_ENV_NAME) + AZURE_LOCATION: $(AZURE_LOCATION) + +{{ end}} \ No newline at end of file diff --git a/cli/azd/resources/pipeline/.github/azure-dev.ymlt b/cli/azd/resources/pipeline/.github/azure-dev.ymlt new file mode 100644 index 00000000000..4f1a56d0235 --- /dev/null +++ b/cli/azd/resources/pipeline/.github/azure-dev.ymlt @@ -0,0 +1,69 @@ +{{define "azure-dev.yml" -}} +# Run when commits are pushed to {{.BranchName}} +on: + workflow_dispatch: + push: + # Run when commits are pushed to mainline branch (main or master) + # Set this to the mainline branch you are using + branches: + - {{.BranchName}} + +{{ if .FedCredLogIn -}} +# Set up permissions for deploying with secretless Azure federated credentials +# https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication +permissions: + id-token: write + contents: read +{{ end }} + +jobs: + build: + runs-on: ubuntu-latest + env: + AZURE_CLIENT_ID: ${{ "{{" }} vars.AZURE_CLIENT_ID {{ "}}" }} + AZURE_TENANT_ID: ${{ "{{" }} vars.AZURE_TENANT_ID {{ "}}" }} + AZURE_SUBSCRIPTION_ID: ${{ "{{" }} vars.AZURE_SUBSCRIPTION_ID {{ "}}" }} + AZURE_ENV_NAME: ${{ "{{" }} vars.AZURE_ENV_NAME {{ "}}" }} + AZURE_LOCATION: ${{ "{{" }} vars.AZURE_LOCATION {{ "}}" }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install azd + uses: Azure/setup-azd@v1.0.0 +{{- if .InstallDotNetAspire}} + - name: Install .NET Aspire workload + run: dotnet workload install aspire +{{ end }} +{{- if .FedCredLogIn }} + - name: Log in with Azure (Federated Credentials) + run: | + azd auth login ` + --client-id "$Env:AZURE_CLIENT_ID" ` + --federated-credential-provider "github" ` + --tenant-id "$Env:AZURE_TENANT_ID" + shell: pwsh +{{ end }} + +{{- if not .FedCredLogIn }} + - name: Log in with Azure (Client Credentials) + run: | + $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; + Write-Host "::add-mask::$($info.clientSecret)" + + azd auth login ` + --client-id "$($info.clientId)" ` + --client-secret "$($info.clientSecret)" ` + --tenant-id "$($info.tenantId)" + shell: pwsh + env: + AZURE_CREDENTIALS: ${{ "{{" }} secrets.AZURE_CREDENTIALS {{ "}}" }} +{{ end }} + + - name: Provision Infrastructure + run: azd provision --no-prompt + env: + AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ "{{" }} secrets.AZD_INITIAL_ENVIRONMENT_CONFIG {{ "}}" }} + + - name: Deploy Application + run: azd deploy --no-prompt +{{ end}} \ No newline at end of file