From 6da6dd2479d1e690912b18d895e0aa8990a4640b Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Wed, 12 Aug 2026 00:32:20 +0000 Subject: [PATCH 1/3] Warn before using archived templates Detect archived GitHub repositories before cloning templates and require an explicit, default-No confirmation. Preserve safe non-interactive behavior and document GitHub metadata authentication variables. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 254ebc9e-ea83-47b8-b307-309ba40685f4 --- cli/azd/cmd/container.go | 18 +- cli/azd/cmd/init.go | 7 +- cli/azd/cmd/init_test.go | 42 ++++ cli/azd/docs/environment-variables.md | 14 ++ cli/azd/internal/repository/initializer.go | 96 +++++++-- .../internal/repository/repository_status.go | 174 +++++++++++++++ .../repository/repository_status_test.go | 199 ++++++++++++++++++ docs/reference/environment-variables.md | 10 + 8 files changed, 546 insertions(+), 14 deletions(-) create mode 100644 cli/azd/internal/repository/repository_status.go create mode 100644 cli/azd/internal/repository/repository_status_test.go diff --git a/cli/azd/cmd/container.go b/cli/azd/cmd/container.go index 4aa90b41155..1e838b570d4 100644 --- a/cli/azd/cmd/container.go +++ b/cli/azd/cmd/container.go @@ -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) diff --git a/cli/azd/cmd/init.go b/cli/azd/cmd/init.go index adace1e28f4..76b790b3fa0 100644 --- a/cli/azd/cmd/init.go +++ b/cli/azd/cmd/init.go @@ -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) @@ -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) @@ -424,6 +425,10 @@ 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 + return initCancelledResult(), nil + } return nil, err } diff --git a/cli/azd/cmd/init_test.go b/cli/azd/cmd/init_test.go index b9549f964d1..79616dfffa3 100644 --- a/cli/azd/cmd/init_test.go +++ b/cli/azd/cmd/init_test.go @@ -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" @@ -154,6 +155,47 @@ 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 == "Do you want to 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.Equal(t, "Init cancelled.", result.Message.Header) + 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()) diff --git a/cli/azd/docs/environment-variables.md b/cli/azd/docs/environment-variables.md index 9a22784c156..c880bd4014d 100644 --- a/cli/azd/docs/environment-variables.md +++ b/cli/azd/docs/environment-variables.md @@ -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`. Takes precedence over `GITHUB_TOKEN`. | +| `GITHUB_TOKEN` | Token used to request repository metadata from `github.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 host. Takes precedence over `GITHUB_ENTERPRISE_TOKEN`. | +| `GITHUB_ENTERPRISE_TOKEN` | Token used to request repository metadata from a recognized GitHub Enterprise host when `GH_ENTERPRISE_TOKEN` is not set. | + ## Extension Configuration | Variable | Description | diff --git a/cli/azd/internal/repository/initializer.go b/cli/azd/internal/repository/initializer.go index b7a9d5fdb40..cca0d91b637 100644 --- a/cli/azd/internal/repository/initializer.go +++ b/cli/azd/internal/repository/initializer.go @@ -48,6 +48,7 @@ type Initializer struct { dotnetCli *dotnet.Cli features *alpha.FeatureManager lazyEnvManager *lazy.Lazy[environment.Manager] + statusChecker RepositoryStatusChecker } func NewInitializer( @@ -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. @@ -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) @@ -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( @@ -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 + } + 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 is no longer actively maintained.", + ), + ) + i.console.Message( + ctx, + "It may not receive dependency updates, compatibility fixes, security patches, or support.\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: "Do you want to 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, diff --git a/cli/azd/internal/repository/repository_status.go b/cli/azd/internal/repository/repository_status.go new file mode 100644 index 00000000000..3be5df6e031 --- /dev/null +++ b/cli/azd/internal/repository/repository_status.go @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package repository + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" +) + +const ( + maxRepositoryMetadataSize = 1 << 20 + repositoryMetadataTimeout = 5 * time.Second +) + +// RepositoryStatus contains hosting-provider metadata relevant to template initialization. +type RepositoryStatus struct { + Archived bool +} + +// RepositoryStatusChecker retrieves repository metadata when the hosting provider supports it. +type RepositoryStatusChecker interface { + // Check returns nil when the repository host is not supported. + Check(ctx context.Context, repositoryURL string) (*RepositoryStatus, error) +} + +type githubRepositoryStatusChecker struct { + transport policy.Transporter + hosts map[string]string + requestTimeout time.Duration +} + +// NewGitHubRepositoryStatusChecker creates a checker for GitHub-hosted repositories. +func NewGitHubRepositoryStatusChecker(transport policy.Transporter) RepositoryStatusChecker { + if transport == nil { + transport = http.DefaultClient + } + + hosts := map[string]string{ + "github.com": firstSetEnvironmentVariable("GH_TOKEN", "GITHUB_TOKEN"), + } + enterpriseToken := firstSetEnvironmentVariable("GH_ENTERPRISE_TOKEN", "GITHUB_ENTERPRISE_TOKEN") + if host := normalizeGitHubHost(os.Getenv("GH_HOST")); host != "" && host != "github.com" { + hosts[host] = enterpriseToken + } + if serverURL := os.Getenv("GITHUB_SERVER_URL"); serverURL != "" { + if parsed, err := url.Parse(serverURL); err == nil { + if host := normalizeGitHubHost(parsed.Hostname()); host != "" && host != "github.com" { + hosts[host] = enterpriseToken + } + } + } + + return &githubRepositoryStatusChecker{ + transport: transport, + hosts: hosts, + requestTimeout: repositoryMetadataTimeout, + } +} + +func (c *githubRepositoryStatusChecker) Check( + ctx context.Context, + repositoryURL string, +) (*RepositoryStatus, error) { + host, slug, ok := parseGitHubRepositoryURL(repositoryURL, c.hosts) + if !ok { + return nil, nil + } + + apiURL := fmt.Sprintf("https://%s/api/v3/repos/%s", host, slug) + if host == "github.com" { + apiURL = fmt.Sprintf("https://api.github.com/repos/%s", slug) + } + + requestCtx, cancel := context.WithTimeout(ctx, c.requestTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(requestCtx, http.MethodGet, apiURL, nil) + if err != nil { + return nil, fmt.Errorf("creating GitHub repository metadata request: %w", err) + } + + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("User-Agent", "azd") + if token := c.hosts[host]; token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + + resp, err := c.transport.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting GitHub repository metadata: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("requesting GitHub repository metadata: HTTP %d", resp.StatusCode) + } + + var metadata struct { + Archived bool `json:"archived"` + } + if err := json.NewDecoder(io.LimitReader(resp.Body, maxRepositoryMetadataSize)).Decode(&metadata); err != nil { + return nil, fmt.Errorf("decoding GitHub repository metadata: %w", err) + } + + return &RepositoryStatus{Archived: metadata.Archived}, nil +} + +func parseGitHubRepositoryURL( + repositoryURL string, + knownHosts map[string]string, +) (host string, slug string, ok bool) { + var path string + + if after, ok0 := strings.CutPrefix(repositoryURL, "git@"); ok0 { + hostAndPath := after + host, path, ok = strings.Cut(hostAndPath, ":") + if !ok { + return "", "", false + } + } else { + parsed, err := url.Parse(repositoryURL) + if err != nil { + return "", "", false + } + + switch parsed.Scheme { + case "http", "https", "ssh", "git": + default: + return "", "", false + } + + host = parsed.Hostname() + path = parsed.Path + } + + host = normalizeGitHubHost(host) + if _, known := knownHosts[host]; !known { + return "", "", false + } + + path = strings.Trim(strings.TrimSpace(path), "/") + path = strings.TrimSuffix(path, ".git") + parts := strings.Split(path, "/") + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", false + } + + return host, url.PathEscape(parts[0]) + "/" + url.PathEscape(parts[1]), true +} + +func normalizeGitHubHost(host string) string { + host = strings.ToLower(strings.TrimSpace(host)) + return strings.TrimPrefix(host, "www.") +} + +func firstSetEnvironmentVariable(names ...string) string { + for _, name := range names { + if token := os.Getenv(name); token != "" { + return token + } + } + + return "" +} diff --git a/cli/azd/internal/repository/repository_status_test.go b/cli/azd/internal/repository/repository_status_test.go new file mode 100644 index 00000000000..5aac1fbe50a --- /dev/null +++ b/cli/azd/internal/repository/repository_status_test.go @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package repository + +import ( + "context" + "errors" + "net/http" + "testing" + "time" + + "github.com/azure/azure-dev/cli/azd/pkg/input" + "github.com/azure/azure-dev/cli/azd/test/mocks" + "github.com/azure/azure-dev/cli/azd/test/mocks/mockhttp" + "github.com/azure/azure-dev/cli/azd/test/mocks/mockinput" + "github.com/stretchr/testify/require" +) + +func TestGitHubRepositoryStatusChecker(t *testing.T) { + t.Run("ArchivedGitHubRepository", func(t *testing.T) { + mockTransport := mockhttp.NewMockHttpUtil() + mockTransport.When(func(req *http.Request) bool { + return req.Method == http.MethodGet && + req.URL.String() == "https://api.github.com/repos/Azure-Samples/todo-csharp-sql-swa-func" + }).RespondFn(func(req *http.Request) (*http.Response, error) { + return mocks.CreateHttpResponseWithBody(req, http.StatusOK, map[string]any{ + "archived": true, + }) + }) + + checker := NewGitHubRepositoryStatusChecker(mockTransport) + status, err := checker.Check( + t.Context(), + "https://github.com/Azure-Samples/todo-csharp-sql-swa-func.git", + ) + + require.NoError(t, err) + require.NotNil(t, status) + require.True(t, status.Archived) + }) + + t.Run("GitHubEnterpriseRepository", func(t *testing.T) { + t.Setenv("GH_HOST", "github.contoso.com") + mockTransport := mockhttp.NewMockHttpUtil() + mockTransport.When(func(req *http.Request) bool { + return req.URL.String() == "https://github.contoso.com/api/v3/repos/contoso/template" + }).RespondFn(func(req *http.Request) (*http.Response, error) { + return mocks.CreateHttpResponseWithBody(req, http.StatusOK, map[string]any{ + "archived": false, + }) + }) + + checker := NewGitHubRepositoryStatusChecker(mockTransport) + status, err := checker.Check(t.Context(), "git@github.contoso.com:contoso/template.git") + + require.NoError(t, err) + require.NotNil(t, status) + require.False(t, status.Archived) + }) + + t.Run("UnsupportedRepositoryHost", func(t *testing.T) { + checker := NewGitHubRepositoryStatusChecker(mockhttp.NewMockHttpUtil()) + status, err := checker.Check(t.Context(), "https://gitlab.com/contoso/template") + + require.NoError(t, err) + require.Nil(t, status) + }) + + t.Run("MetadataRequestFailure", func(t *testing.T) { + mockTransport := mockhttp.NewMockHttpUtil() + mockTransport.When(func(req *http.Request) bool { + return true + }).RespondFn(func(req *http.Request) (*http.Response, error) { + return mocks.CreateEmptyHttpResponse(req, http.StatusForbidden) + }) + + checker := NewGitHubRepositoryStatusChecker(mockTransport) + status, err := checker.Check(t.Context(), "https://github.com/contoso/template") + + require.ErrorContains(t, err, "HTTP 403") + require.Nil(t, status) + }) + + t.Run("MetadataRequestTimeout", func(t *testing.T) { + mockTransport := mockhttp.NewMockHttpUtil() + mockTransport.When(func(req *http.Request) bool { + return true + }).RespondFn(func(req *http.Request) (*http.Response, error) { + <-req.Context().Done() + return nil, req.Context().Err() + }) + + checker := NewGitHubRepositoryStatusChecker(mockTransport).(*githubRepositoryStatusChecker) + checker.requestTimeout = time.Millisecond + status, err := checker.Check(t.Context(), "https://github.com/contoso/template") + + require.ErrorIs(t, err, context.DeadlineExceeded) + require.Nil(t, status) + }) +} + +func TestInitializerConfirmArchivedTemplate(t *testing.T) { + t.Run("ActiveRepositoryContinuesWithoutPrompt", func(t *testing.T) { + console := mockinput.NewMockConsole() + initializer := &Initializer{ + console: console, + statusChecker: &fakeRepositoryStatusChecker{status: &RepositoryStatus{}}, + } + + err := initializer.confirmArchivedTemplate(t.Context(), "https://github.com/contoso/template") + + require.NoError(t, err) + require.Empty(t, console.Output()) + }) + + t.Run("ArchivedRepositoryAccepted", func(t *testing.T) { + console := mockinput.NewMockConsole() + console.WhenConfirm(func(options input.ConsoleOptions) bool { + require.Equal(t, "Do you want to continue using this archived template?", options.Message) + require.Equal(t, false, options.DefaultValue) + return true + }).Respond(true) + initializer := &Initializer{ + console: console, + statusChecker: &fakeRepositoryStatusChecker{status: &RepositoryStatus{Archived: true}}, + } + + err := initializer.confirmArchivedTemplate(t.Context(), "https://github.com/contoso/template") + + require.NoError(t, err) + require.Contains(t, console.Output()[0], "WARNING:") + require.Contains(t, console.Output()[0], "no longer actively maintained") + require.Contains(t, console.Output()[1], "security patches") + }) + + t.Run("ArchivedRepositoryDeclined", func(t *testing.T) { + console := mockinput.NewMockConsole() + console.WhenConfirm(func(options input.ConsoleOptions) bool { + return options.DefaultValue == false + }).Respond(false) + initializer := &Initializer{ + console: console, + statusChecker: &fakeRepositoryStatusChecker{status: &RepositoryStatus{Archived: true}}, + } + + err := initializer.confirmArchivedTemplate(t.Context(), "https://github.com/contoso/template") + + require.ErrorIs(t, err, ErrArchivedTemplateDeclined) + }) + + t.Run("ArchivedRepositoryNoPrompt", func(t *testing.T) { + console := mockinput.NewMockConsole() + console.SetNoPromptMode(true) + initializer := &Initializer{ + console: console, + statusChecker: &fakeRepositoryStatusChecker{status: &RepositoryStatus{Archived: true}}, + } + + err := initializer.confirmArchivedTemplate(t.Context(), "https://github.com/contoso/template") + + require.ErrorContains(t, err, "requires confirmation") + require.ErrorContains(t, err, "rerun without --no-prompt") + }) + + t.Run("MetadataFailureContinues", func(t *testing.T) { + console := mockinput.NewMockConsole() + initializer := &Initializer{ + console: console, + statusChecker: &fakeRepositoryStatusChecker{err: errors.New("rate limited")}, + } + + err := initializer.confirmArchivedTemplate(t.Context(), "https://github.com/contoso/template") + + require.NoError(t, err) + require.Empty(t, console.Output()) + }) + + t.Run("CancellationStopsInitialization", func(t *testing.T) { + console := mockinput.NewMockConsole() + initializer := &Initializer{ + console: console, + statusChecker: &fakeRepositoryStatusChecker{err: context.Canceled}, + } + + err := initializer.confirmArchivedTemplate(t.Context(), "https://github.com/contoso/template") + + require.ErrorIs(t, err, context.Canceled) + }) +} + +type fakeRepositoryStatusChecker struct { + status *RepositoryStatus + err error +} + +func (f *fakeRepositoryStatusChecker) Check(context.Context, string) (*RepositoryStatus, error) { + return f.status, f.err +} diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md index 241eda4f1cb..a81a680b846 100644 --- a/docs/reference/environment-variables.md +++ b/docs/reference/environment-variables.md @@ -35,6 +35,16 @@ Override the path to external tools that azd invokes: | `AZD_GH_TOOL_PATH` | Path to the GitHub CLI binary | | `AZD_PACK_TOOL_PATH` | Path to the Cloud Native Buildpacks (`pack`) binary | +## GitHub Repository Access + +Used by `azd init --template` when checking GitHub repository metadata before cloning: + +| Variable | Description | +|---|---| +| `GH_TOKEN` / `GITHUB_TOKEN` | Authenticate repository metadata requests to `github.com` | +| `GH_HOST` / `GITHUB_SERVER_URL` | Identify a GitHub Enterprise host for repository metadata checks | +| `GH_ENTERPRISE_TOKEN` / `GITHUB_ENTERPRISE_TOKEN` | Authenticate repository metadata requests to the recognized GitHub Enterprise host | + ## Build Configuration | Variable | Description | From 6bab0d81cef354d27134761aba5fbb5143308f21 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Wed, 12 Aug 2026 01:01:27 +0000 Subject: [PATCH 2/3] Document archived template cancellation mapping Mark the archived-template decline sentinel as handled by azd init so the package-level telemetry mapping guard recognizes the intentional control-flow error. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 254ebc9e-ea83-47b8-b307-309ba40685f4 --- cli/azd/internal/cmd/errors_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/azd/internal/cmd/errors_test.go b/cli/azd/internal/cmd/errors_test.go index 86ecf94754d..f4d51f03d1b 100644 --- a/cli/azd/internal/cmd/errors_test.go +++ b/cli/azd/internal/cmd/errors_test.go @@ -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", From 85a1e20ca266b7ff6efa1301c3e579ec087a6995 Mon Sep 17 00:00:00 2001 From: Victor Vazquez Date: Wed, 12 Aug 2026 16:35:04 +0000 Subject: [PATCH 3/3] Confirm archived template in functional test Update the template initialization functional test to explicitly accept the archived repository warning before supplying the environment name. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 254ebc9e-ea83-47b8-b307-309ba40685f4 --- cli/azd/test/functional/init_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/azd/test/functional/init_test.go b/cli/azd/test/functional/init_test.go index 99b195e3898..8f22dad3789 100644 --- a/cli/azd/test/functional/init_test.go +++ b/cli/azd/test/functional/init_test.go @@ -309,9 +309,10 @@ func Test_CLI_Init_CanUseTemplate(t *testing.T) { cli.WorkingDirectory = dir cli.Env = append(os.Environ(), "AZURE_LOCATION=eastus2") + // The template is archived, so accept the warning before providing the environment name. _, err := cli.RunCommandWithStdIn( ctx, - "TESTENV\n", + "y\nTESTENV\n", "init", "--template", "cosmos-dotnet-core-todo-app",