Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion cli/azd/cmd/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,23 @@ func registerCommonDependencies(container *ioc.NestedContainer) {
return security.NewManager(cwd)
})

container.MustRegisterSingleton(repository.NewInitializer)
container.MustRegisterSingleton(func(
console input.Console,
gitCli *git.Cli,
dotnetCli *dotnet.Cli,
features *alpha.FeatureManager,
lazyEnvManager *lazy.Lazy[environment.Manager],
transport policy.Transporter,
) *repository.Initializer {
return repository.NewInitializerWithRepositoryStatusChecker(
console,
gitCli,
dotnetCli,
features,
lazyEnvManager,
repository.NewGitHubRepositoryStatusChecker(transport),
)
})
container.MustRegisterSingleton(alpha.NewFeaturesManager)
container.MustRegisterSingleton(config.NewUserConfigManager)
container.MustRegisterSingleton(config.NewManager)
Expand Down
8 changes: 7 additions & 1 deletion cli/azd/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ func (i *initAction) Run(ctx context.Context) (_ *actions.ActionResult, retErr e
// or pass "." to use the current directory (preserving existing behavior).
createdProjectDir := ""
originalWd := wd
cleanupProjectDir := false

if isTemplateInit {
targetDir, err := i.resolveTargetDirectory(wd)
Expand Down Expand Up @@ -308,7 +309,7 @@ func (i *initAction) Run(ctx context.Context) (_ *actions.ActionResult, retErr e
// Only remove the directory if we created it — don't delete
// pre-existing directories the user pointed at.
defer func() {
if retErr != nil {
if retErr != nil || cleanupProjectDir {
_ = os.Chdir(originalWd)
if !dirExistedBefore {
_ = os.RemoveAll(createdProjectDir)
Expand Down Expand Up @@ -424,6 +425,11 @@ func (i *initAction) Run(ctx context.Context) (_ *actions.ActionResult, retErr e
tracing.SetUsageAttributes(fields.InitMethod.String("template"))
template, err := i.initializeTemplate(ctx, azdCtx)
if err != nil {
if errors.Is(err, repository.ErrArchivedTemplateDeclined) {
cleanupProjectDir = true
i.console.Message(ctx, output.WithWarningFormat("CANCELLED: Initialization stopped."))
return nil, nil
}
return nil, err
}

Expand Down
43 changes: 43 additions & 0 deletions cli/azd/cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/internal/agent/consent"
"github.com/azure/azure-dev/cli/azd/internal/repository"
"github.com/azure/azure-dev/cli/azd/pkg/account"
"github.com/azure/azure-dev/cli/azd/pkg/alpha"
"github.com/azure/azure-dev/cli/azd/pkg/config"
Expand Down Expand Up @@ -154,6 +155,48 @@ func TestInitNoPromptRequiresMode(t *testing.T) {
})
}

func TestInitArchivedTemplateDeclinedCleansCreatedDirectory(t *testing.T) {
mockContext := mocks.NewMockContext(t.Context())
mockContext.Console.SetTerminal(true)
mockContext.Console.WhenConfirm(func(options input.ConsoleOptions) bool {
return options.Message == "Continue using this archived template?" &&
options.DefaultValue == false
}).Respond(false)

flags := &initFlags{
templatePath: "Azure-Samples/todo-csharp-sql-swa-func",
global: &internal.GlobalCommandOptions{},
}
flags.EnvironmentName = "archive-test"

action := setupInitAction(t, mockContext, flags)
action.repoInitializer = repository.NewInitializerWithRepositoryStatusChecker(
mockContext.Console,
action.gitCli,
nil,
nil,
nil,
archivedRepositoryStatusChecker{},
)

wd, err := os.Getwd()
require.NoError(t, err)
targetDir := filepath.Join(wd, "todo-csharp-sql-swa-func")

result, err := action.Run(t.Context())

require.NoError(t, err)
require.Nil(t, result)
require.Contains(t, strings.Join(mockContext.Console.Output(), "\n"), "CANCELLED: Initialization stopped.")
require.NoDirExists(t, targetDir)
}

type archivedRepositoryStatusChecker struct{}

func (archivedRepositoryStatusChecker) Check(context.Context, string) (*repository.RepositoryStatus, error) {
return &repository.RepositoryStatus{Archived: true}, nil
}

func TestInitFailFastMissingEnvNonInteractive(t *testing.T) {
t.Run("NoLongerFailsWhenNoPromptWithTemplateAndNoEnv", func(t *testing.T) {
mockContext := mocks.NewMockContext(t.Context())
Expand Down
14 changes: 14 additions & 0 deletions cli/azd/docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,20 @@ specific version of the tool installed on the machine.
| `AZD_PACK_TOOL_PATH` | The `pack` tool override path. The direct path to `pack` or `pack.exe`. |
| `AZD_COPILOT_CLI_PATH` | The Copilot CLI tool override path. When set, skips automatic download and uses the specified path. |

### GitHub Repository Access

These GitHub-compatible variables are used when `azd init --template` checks repository metadata before cloning.
Metadata requests are unauthenticated when no matching token is set.

| Variable | Description |
| --- | --- |
| `GH_TOKEN` | Token used to request repository metadata from `github.com` and GitHub Enterprise Cloud `*.ghe.com` hosts. Takes precedence over `GITHUB_TOKEN`. |
| `GITHUB_TOKEN` | Token used to request repository metadata from `github.com` and `*.ghe.com` when `GH_TOKEN` is not set. |
| `GH_HOST` | GitHub Enterprise host recognized for repository metadata checks. |
| `GITHUB_SERVER_URL` | GitHub server URL recognized for repository metadata checks when `GH_HOST` is not set. |
| `GH_ENTERPRISE_TOKEN` | Token used to request repository metadata from a recognized GitHub Enterprise Server host. Takes precedence over `GITHUB_ENTERPRISE_TOKEN`. |
| `GITHUB_ENTERPRISE_TOKEN` | Token used for a recognized GitHub Enterprise Server host when `GH_ENTERPRISE_TOKEN` is not set. |

## Extension Configuration

| Variable | Description |
Expand Down
1 change: 1 addition & 0 deletions cli/azd/internal/cmd/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,7 @@ func Test_PackageLevelErrorsMapped(t *testing.T) {
"ErrUnsupportedScriptType": "pkg/ext: hook script validation, caught before command level",

// Errors that are always caught/handled before reaching MapError
"ErrArchivedTemplateDeclined": "caught in cmd/init.go and converted to a successful cancellation result",
"ErrEnsureEnvPreReqBicepCompileFailed": "caught in cmd/env.go and cmd/up.go before reaching telemetry",
"ErrAzdOperationsNotEnabled": "caught in pkg/project/dotnet_importer.go before reaching telemetry",
"ErrAzCliSecretNotFound": "caught in pkg/cmdsubst before reaching telemetry",
Expand Down
96 changes: 84 additions & 12 deletions cli/azd/internal/repository/initializer.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type Initializer struct {
dotnetCli *dotnet.Cli
features *alpha.FeatureManager
lazyEnvManager *lazy.Lazy[environment.Manager]
statusChecker RepositoryStatusChecker
}

func NewInitializer(
Expand All @@ -66,6 +67,25 @@ func NewInitializer(
}
}

// NewInitializerWithRepositoryStatusChecker creates an initializer that checks repository metadata before cloning.
func NewInitializerWithRepositoryStatusChecker(
console input.Console,
gitCli *git.Cli,
dotnetCli *dotnet.Cli,
features *alpha.FeatureManager,
lazyEnvManager *lazy.Lazy[environment.Manager],
statusChecker RepositoryStatusChecker,
) *Initializer {
initializer := NewInitializer(console, gitCli, dotnetCli, features, lazyEnvManager)
initializer.statusChecker = statusChecker
return initializer
}

var (
// ErrArchivedTemplateDeclined indicates that the user chose not to initialize from an archived repository.
ErrArchivedTemplateDeclined = errors.New("archived template repository declined by user")
)

// Initializes a local repository in the project directory from a remote repository or local template directory.
//
// A confirmation prompt is displayed for any existing files to be overwritten.
Expand All @@ -76,18 +96,6 @@ func (i *Initializer) Initialize(
templateBranch string) error {
var err error

staging, err := os.MkdirTemp("", "az-dev-template")

if err != nil {
return fmt.Errorf("creating temp folder: %w", err)
}

// Attempt to remove the temporary directory we cloned the template into, but don't fail the
// overall operation if we can't.
defer func() {
_ = os.RemoveAll(staging)
}()

target := azdCtx.ProjectDirectory()

templateUrl, err := templates.Absolute(template.RepositoryPath)
Expand All @@ -107,6 +115,21 @@ func (i *Initializer) Initialize(
}
}

if err := i.confirmArchivedTemplate(ctx, templateUrl); err != nil {
return err
}

staging, err := os.MkdirTemp("", "az-dev-template")
if err != nil {
return fmt.Errorf("creating temp folder: %w", err)
}

// Attempt to remove the temporary directory we cloned the template into, but don't fail the
// overall operation if we can't.
defer func() {
_ = os.RemoveAll(staging)
}()

var stepMessage string
if templates.IsLocalPath(templateUrl) {
stepMessage = fmt.Sprintf(
Expand Down Expand Up @@ -184,6 +207,55 @@ func (i *Initializer) Initialize(
return nil
}

func (i *Initializer) confirmArchivedTemplate(ctx context.Context, templateURL string) error {
if i.statusChecker == nil {
return nil
}

status, err := i.statusChecker.Check(ctx, templateURL)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return err
Comment thread
vhvb1989 marked this conversation as resolved.
}
log.Printf("unable to verify template repository archive status: %v", err)
return nil
}
if status == nil || !status.Archived {
return nil
}

i.console.Message(
ctx,
output.WithWarningFormat(
"WARNING: This template repository is archived and no longer maintained.",
),
)
i.console.Message(
ctx,
"It may not receive dependency updates, compatibility fixes, or security patches.\n",
)

if i.console.IsNoPromptMode() {
return fmt.Errorf(
"template repository %s is archived and requires confirmation; rerun without --no-prompt",
templateURL,
)
}

confirmed, err := i.console.Confirm(ctx, input.ConsoleOptions{
Message: "Continue using this archived template?",
DefaultValue: false,
})
if err != nil {
return err
}
if !confirmed {
return ErrArchivedTemplateDeclined
}

return nil
}

func (i *Initializer) fetchCode(
ctx context.Context,
templateUrl string,
Expand Down
Loading
Loading