diff --git a/cli/azd/extensions/azure.ai.agents/CHANGELOG.md b/cli/azd/extensions/azure.ai.agents/CHANGELOG.md index d3f82e0d74a..1996dff882e 100644 --- a/cli/azd/extensions/azure.ai.agents/CHANGELOG.md +++ b/cli/azd/extensions/azure.ai.agents/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.1.32-preview (2026-05-18) +- [[#8223]](https://github.com/Azure/azure-dev/pull/8223) Add `.agentignore` support for controlling which files are excluded from agent code-deploy ZIP packaging. Uses `.gitignore` syntax with sensible defaults generated during `azd ai agent init`. - [[#8222]](https://github.com/Azure/azure-dev/pull/8222) Add post-init validation to check .NET runtime compatibility with project TargetFramework and show guidance when mismatched. - [[#7865]](https://github.com/Azure/azure-dev/pull/7865) Improve `azd ai agent invoke` trace ID handling for consistent responses, including deduping comma-folded request IDs. - [[#8184]](https://github.com/Azure/azure-dev/pull/8184) Default `azd ai agent show` output to table format. diff --git a/cli/azd/extensions/azure.ai.agents/cspell.yaml b/cli/azd/extensions/azure.ai.agents/cspell.yaml index c1cd9010e90..6bd1ce25540 100644 --- a/cli/azd/extensions/azure.ai.agents/cspell.yaml +++ b/cli/azd/extensions/azure.ai.agents/cspell.yaml @@ -28,6 +28,7 @@ words: - westeurope # Project terms - ABAC + - agentignore - ADLS - agentserver - aiservices diff --git a/cli/azd/extensions/azure.ai.agents/go.mod b/cli/azd/extensions/azure.ai.agents/go.mod index bfad84820b0..40b835c9590 100644 --- a/cli/azd/extensions/azure.ai.agents/go.mod +++ b/cli/azd/extensions/azure.ai.agents/go.mod @@ -28,6 +28,8 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) +require github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817 + require ( dario.cat/mergo v1.0.2 // indirect github.com/AlecAivazis/survey/v2 v2.3.7 // indirect @@ -58,6 +60,7 @@ require ( github.com/clipperhouse/displaywidth v0.9.0 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.5.0 // indirect + github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/go-logr/logr v1.4.3 // indirect diff --git a/cli/azd/extensions/azure.ai.agents/go.sum b/cli/azd/extensions/azure.ai.agents/go.sum index e7b4fc1dace..e3c47521f5c 100644 --- a/cli/azd/extensions/azure.ai.agents/go.sum +++ b/cli/azd/extensions/azure.ai.agents/go.sum @@ -17,8 +17,6 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthoriza github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0/go.mod h1:/pz8dyNQe+Ey3yBp/XuYz7oqX8YDNWVpPB0hH3XWfbc= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2 h1:qiir/pptnHqp6hV8QwV+IExYIf6cPsXBfUDUXQ27t2Y= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2/go.mod h1:jVRrRDLCOuif95HDYC23ADTMlvahB7tMdl519m9Iyjc= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.8.0 h1:ZMGAqCZov8+7iFUPWKVcTaLgNXUeTlz20sIuWkQWNfg= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.8.0/go.mod h1:BElPQ/GZtrdQ2i5uDZw3OKLE1we75W0AEWyeBR1TWQA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices/v2 v2.0.0 h1:pxphC/uRZKNHNPbZ0duDDgKkefju2F03OkG5xF6byHQ= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices/v2 v2.0.0/go.mod h1:twcwRey+l1znKBL5TEzYiZMtiVkWfM7Pq8a9vY04xYc= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.3.0-beta.3 h1:4qfc7os3wRQcl+ImfeH9z0abWJzuV9IGcN1B9olmPTU= @@ -106,10 +104,14 @@ github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817 h1:0nsrg//Dc7xC74H/TZ5sYR8uk4UQRNjsw8zejqH5a4Q= +github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817/go.mod h1:C/+sI4IFnEpCn6VQ3GIPEp+FrQnQw+YQP3+n+GdGq7o= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 1c944f613c7..ead410f60bb 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -586,7 +586,10 @@ agents are unique by name within a project, so deploying with an existing name creates a new version of that existing agent instead of a separate agent. Use --agent-name to choose a unique Foundry agent name when initializing from -a reusable sample or manifest.`, +a reusable sample or manifest. + +A default .agentignore file is generated to control which files are excluded +from code-deploy ZIP packaging (uses .gitignore syntax).`, Example: ` # Initialize from an agent manifest azd ai agent init -m ./agent.manifest.yaml @@ -1835,6 +1838,15 @@ func writeAgentDefinitionFile(targetDir string, agentManifest *agent_yaml.AgentM } fmt.Println(output.WithGrayFormat("Processed agent.yaml at %s", filePath)) + + // Generate .agentignore if it doesn't already exist + agentIgnorePath := filepath.Join(targetDir, ".agentignore") + if _, err := os.Stat(agentIgnorePath); os.IsNotExist(err) { + if err := os.WriteFile(agentIgnorePath, []byte(project.DefaultAgentIgnoreContent()), osutil.PermissionFile); err != nil { + return fmt.Errorf("writing .agentignore: %w", err) + } + } + return nil } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go index 1ded95ab710..7fd77b738c9 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go @@ -21,6 +21,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/azure/azure-dev/cli/azd/pkg/ux" "github.com/fatih/color" "google.golang.org/protobuf/types/known/structpb" @@ -819,6 +820,14 @@ func (a *InitFromCodeAction) writeDefinitionToSrcDir(definition *agent_yaml.Cont return "", fmt.Errorf("writing definition to file: %w", err) } + // Generate .agentignore if it doesn't already exist + agentIgnorePath := filepath.Join(srcDir, ".agentignore") + if _, err := os.Stat(agentIgnorePath); os.IsNotExist(err) { + if err := os.WriteFile(agentIgnorePath, []byte(project.DefaultAgentIgnoreContent()), osutil.PermissionFile); err != nil { + return "", fmt.Errorf("writing .agentignore: %w", err) + } + } + return definitionPath, nil } diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/agentignore.go b/cli/azd/extensions/azure.ai.agents/internal/project/agentignore.go new file mode 100644 index 00000000000..2d79b3db354 --- /dev/null +++ b/cli/azd/extensions/azure.ai.agents/internal/project/agentignore.go @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package project + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + gitignore "github.com/denormal/go-gitignore" +) + +const ( + agentIgnoreFileName = ".agentignore" + agentIgnoreMaxSize = 1 << 20 // 1 MB +) + +// defaultExclusionsContent is used as the matcher when no .agentignore file exists. +// Generated from DefaultAgentIgnoreContent() to maintain a single source of truth. +var defaultExclusionsContent = DefaultAgentIgnoreContent() + +// utf8BOM is the byte order mark that some Windows editors prepend to UTF-8 files. +var utf8BOM = []byte{0xEF, 0xBB, 0xBF} + +// agentIgnoreMatcher provides path matching for agent code deploy packaging. +type agentIgnoreMatcher struct { + ignore gitignore.GitIgnore // from .agentignore file or defaults + hasUserIgnore bool +} + +// newAgentIgnoreMatcher creates a matcher by reading .agentignore from srcDir. +// If no .agentignore exists, defaults are used. +func newAgentIgnoreMatcher(ctx context.Context, srcDir string) (*agentIgnoreMatcher, error) { + _ = ctx // reserved for future cancellation support + m := &agentIgnoreMatcher{} + + // Try to load user's .agentignore + ig, err := loadAgentIgnore(ctx, srcDir) + if err != nil { + return nil, err + } + + if ig != nil { + m.ignore = ig + m.hasUserIgnore = true + } else { + // No .agentignore file — use defaults + m.ignore = gitignore.New( + strings.NewReader(defaultExclusionsContent), + srcDir, + nil, + ) + } + + return m, nil +} + +// ShouldExclude returns true if the given path should be excluded from the ZIP. +// relPath is the path relative to srcDir using forward slashes. +// isDir indicates whether the path is a directory. +func (m *agentIgnoreMatcher) ShouldExclude(relPath string, isDir bool) bool { + match := m.ignore.Relative(relPath, isDir) + if match != nil && match.Ignore() { + return true + } + return false +} + +// loadAgentIgnore reads an .agentignore file from srcDir. +// Returns nil, nil if no file exists. +func loadAgentIgnore(ctx context.Context, srcDir string) (gitignore.GitIgnore, error) { + _ = ctx // reserved for future cancellation support + path := filepath.Join(srcDir, agentIgnoreFileName) + info, err := os.Lstat(path) + if errors.Is(err, os.ErrNotExist) { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("reading %s: %w", agentIgnoreFileName, err) + } + if !info.Mode().IsRegular() { + return nil, fmt.Errorf("%s must be a regular file", agentIgnoreFileName) + } + if info.Size() > agentIgnoreMaxSize { + return nil, fmt.Errorf("%s exceeds maximum size (%d bytes)", agentIgnoreFileName, agentIgnoreMaxSize) + } + + f, err := os.Open(path) //nolint:gosec // path is constructed from a known directory + constant filename + if err != nil { + return nil, fmt.Errorf("reading %s: %w", agentIgnoreFileName, err) + } + defer f.Close() + + data, err := io.ReadAll(io.LimitReader(f, agentIgnoreMaxSize+1)) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", agentIgnoreFileName, err) + } + if int64(len(data)) > agentIgnoreMaxSize { + return nil, fmt.Errorf("%s exceeds maximum size (%d bytes)", agentIgnoreFileName, agentIgnoreMaxSize) + } + + // Strip UTF-8 BOM + data = bytes.TrimPrefix(data, utf8BOM) + + return gitignore.New(bytes.NewReader(data), srcDir, nil), nil +} + +// DefaultAgentIgnoreContent returns the default .agentignore file content +// that should be generated during `azd ai agent init`. +func DefaultAgentIgnoreContent() string { + return `# Files excluded from agent code deployment packaging. +# Uses .gitignore syntax. +# Note: only the root .agentignore is read; subdirectory files are not supported. +# +# To include a file that is excluded by default, use negation: !filename + +# azd tooling files +agent.yaml +agent.manifest.yaml +azure.yaml +.agentignore + +# Security / secrets +.env +.env.* +.azure/ +.git/ + +# Python +__pycache__/ +.venv/ +venv/ +*.pyc +*.pyo +.mypy_cache/ +.pytest_cache/ + +# .NET +bin/ +obj/ +*.user +*.suo +.vs/ + +# Node +node_modules/ + +# Docker (not used in code deploy) +Dockerfile +.dockerignore +` +} diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/agentignore_test.go b/cli/azd/extensions/azure.ai.agents/internal/project/agentignore_test.go new file mode 100644 index 00000000000..54a96448b45 --- /dev/null +++ b/cli/azd/extensions/azure.ai.agents/internal/project/agentignore_test.go @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package project + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAgentIgnore_NoFile_UsesDefaults(t *testing.T) { + dir := t.TempDir() + + m, err := newAgentIgnoreMatcher(t.Context(), dir) + require.NoError(t, err) + require.False(t, m.hasUserIgnore) + + // Default exclusions + require.True(t, m.ShouldExclude("__pycache__", true)) + require.True(t, m.ShouldExclude(".venv", true)) + require.True(t, m.ShouldExclude("venv", true)) + require.True(t, m.ShouldExclude("node_modules", true)) + require.True(t, m.ShouldExclude("bin", true)) + require.True(t, m.ShouldExclude("obj", true)) + require.True(t, m.ShouldExclude(".vs", true)) + require.True(t, m.ShouldExclude(".git", true)) + require.True(t, m.ShouldExclude(".mypy_cache", true)) + require.True(t, m.ShouldExclude(".pytest_cache", true)) + require.True(t, m.ShouldExclude("foo.pyc", false)) + require.True(t, m.ShouldExclude("bar.pyo", false)) + require.True(t, m.ShouldExclude("x.user", false)) + require.True(t, m.ShouldExclude("y.suo", false)) + require.True(t, m.ShouldExclude("agent.yaml", false)) + require.True(t, m.ShouldExclude("agent.manifest.yaml", false)) + require.True(t, m.ShouldExclude("azure.yaml", false)) + require.True(t, m.ShouldExclude(".agentignore", false)) + require.True(t, m.ShouldExclude(".env", false)) + require.True(t, m.ShouldExclude(".env.local", false)) + require.True(t, m.ShouldExclude(".azure", true)) + require.True(t, m.ShouldExclude("Dockerfile", false)) + require.True(t, m.ShouldExclude(".dockerignore", false)) + + // Should NOT exclude normal files + require.False(t, m.ShouldExclude("main.py", false)) + require.False(t, m.ShouldExclude("requirements.txt", false)) + require.False(t, m.ShouldExclude("src", true)) +} + +func TestAgentIgnore_UserFileOverridesDefaults(t *testing.T) { + dir := t.TempDir() + // User only excludes *.log — defaults like __pycache__ should NOT apply + err := os.WriteFile(filepath.Join(dir, ".agentignore"), []byte("*.log\n"), 0600) + require.NoError(t, err) + + m, err := newAgentIgnoreMatcher(t.Context(), dir) + require.NoError(t, err) + require.True(t, m.hasUserIgnore) + + // User-specified exclusion works + require.True(t, m.ShouldExclude("debug.log", false)) + + // Default exclusions no longer apply (user took control) + require.False(t, m.ShouldExclude("__pycache__", true)) + require.False(t, m.ShouldExclude("node_modules", true)) + require.False(t, m.ShouldExclude("foo.pyc", false)) + + // Security files are also user-configurable now — not excluded unless user lists them + require.False(t, m.ShouldExclude(".env", false)) + require.False(t, m.ShouldExclude(".git", true)) +} + +func TestAgentIgnore_NegationWorks(t *testing.T) { + dir := t.TempDir() + // Exclude all .txt but keep important.txt + content := "*.txt\n!important.txt\n" + err := os.WriteFile(filepath.Join(dir, ".agentignore"), []byte(content), 0600) + require.NoError(t, err) + + m, err := newAgentIgnoreMatcher(t.Context(), dir) + require.NoError(t, err) + + require.True(t, m.ShouldExclude("notes.txt", false)) + require.False(t, m.ShouldExclude("important.txt", false)) +} + +func TestAgentIgnore_SymlinkRejected(t *testing.T) { + dir := t.TempDir() + target := filepath.Join(dir, "real_ignore") + err := os.WriteFile(target, []byte("*.log\n"), 0600) + require.NoError(t, err) + + link := filepath.Join(dir, ".agentignore") + err = os.Symlink(target, link) + if err != nil { + t.Skip("symlinks not supported on this platform") + } + + _, err = newAgentIgnoreMatcher(t.Context(), dir) + require.Error(t, err) + require.Contains(t, err.Error(), "must be a regular file") +} + +func TestAgentIgnore_UTF8BOM(t *testing.T) { + dir := t.TempDir() + // Write file with UTF-8 BOM + content := append([]byte{0xEF, 0xBB, 0xBF}, []byte("*.log\n")...) + err := os.WriteFile(filepath.Join(dir, ".agentignore"), content, 0600) + require.NoError(t, err) + + m, err := newAgentIgnoreMatcher(t.Context(), dir) + require.NoError(t, err) + + require.True(t, m.ShouldExclude("app.log", false)) +} + +func TestAgentIgnore_EmptyFile(t *testing.T) { + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, ".agentignore"), []byte(""), 0600) + require.NoError(t, err) + + m, err := newAgentIgnoreMatcher(t.Context(), dir) + require.NoError(t, err) + require.True(t, m.hasUserIgnore) + + // Nothing excluded — empty file means include everything + require.False(t, m.ShouldExclude("main.py", false)) + require.False(t, m.ShouldExclude("__pycache__", true)) + require.False(t, m.ShouldExclude(".env", false)) +} + +func TestDefaultAgentIgnoreContent(t *testing.T) { + content := DefaultAgentIgnoreContent() + require.Contains(t, content, "__pycache__/") + require.Contains(t, content, "node_modules/") + require.Contains(t, content, "agent.yaml") + require.Contains(t, content, ".agentignore") + require.Contains(t, content, "bin/") + require.Contains(t, content, ".env") + require.Contains(t, content, ".azure/") + require.Contains(t, content, ".git/") + require.Contains(t, content, "Dockerfile") + require.Contains(t, content, ".dockerignore") +} diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go index e5b3ae430af..8d00e3eb353 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go @@ -384,7 +384,7 @@ func (p *AgentServiceTargetProvider) Package( // Code deploy: ZIP the source directory if p.isCodeDeployAgent() { progress("Packaging code") - zipPath, sha256Hex, err := p.packageCodeDeploy(serviceConfig) + zipPath, sha256Hex, err := p.packageCodeDeploy(ctx, serviceConfig) if err != nil { return nil, exterrors.Internal(exterrors.OpContainerPackage, fmt.Sprintf("code packaging failed: %s", err)) } @@ -1057,7 +1057,7 @@ func (p *AgentServiceTargetProvider) deployHostedAgent( // packageCodeDeploy creates a ZIP archive of the agent source code, writes it to a temp file, // and computes its SHA-256. Returns the temp file path and SHA-256 hex string. -func (p *AgentServiceTargetProvider) packageCodeDeploy(serviceConfig *azdext.ServiceConfig) (string, string, error) { +func (p *AgentServiceTargetProvider) packageCodeDeploy(ctx context.Context, serviceConfig *azdext.ServiceConfig) (string, string, error) { // Source directory is the service's relative path srcDir := filepath.Dir(p.agentDefinitionPath) @@ -1076,30 +1076,14 @@ func (p *AgentServiceTargetProvider) packageCodeDeploy(serviceConfig *azdext.Ser } } - // Exclusion patterns - // TODO: support a .azdignore or similar ignore file for user-configurable exclusions. - excludeDirs := map[string]bool{ - "__pycache__": true, - ".venv": true, - "venv": true, - ".git": true, - "node_modules": true, - ".mypy_cache": true, - ".pytest_cache": true, - ".azure": true, - // .NET directories (for remote_build, exclude build artifacts) - "bin": true, - "obj": true, - ".vs": true, - } - excludeExts := map[string]bool{ - ".pyc": true, - ".pyo": true, - ".user": true, - ".suo": true, - } - excludeFiles := map[string]bool{ - ".env": true, + // Load .agentignore (or use defaults if no file exists) + ignoreMatcher, err := newAgentIgnoreMatcher(ctx, srcDir) + if err != nil { + return "", "", exterrors.Dependency( + exterrors.CodeInvalidFilePath, + fmt.Sprintf("failed to load %s: %s", agentIgnoreFileName, err), + "check that .agentignore is a valid file with gitignore syntax", + ) } // Create temp file and write ZIP directly to it while computing SHA-256 @@ -1141,9 +1125,14 @@ func (p *AgentServiceTargetProvider) packageCodeDeploy(serviceConfig *azdext.Ser // Normalize to forward slashes for ZIP relPath = filepath.ToSlash(relPath) + // Skip symlinked directories to avoid traversing outside the project root + if d.IsDir() && d.Type()&fs.ModeSymlink != 0 { + return filepath.SkipDir + } + // Check directory exclusions if d.IsDir() { - if excludeDirs[d.Name()] { + if ignoreMatcher.ShouldExclude(relPath, true) { return filepath.SkipDir } return nil @@ -1154,18 +1143,8 @@ func (p *AgentServiceTargetProvider) packageCodeDeploy(serviceConfig *azdext.Ser return nil } - // Check file extension exclusions - if excludeExts[filepath.Ext(path)] { - return nil - } - - // Check file name exclusions (.env, .env.*) - if excludeFiles[d.Name()] || strings.HasPrefix(d.Name(), ".env.") { - return nil - } - - // Skip agent.yaml itself from the ZIP (metadata is sent separately) - if d.Name() == "agent.yaml" { + // Check file exclusions + if ignoreMatcher.ShouldExclude(relPath, false) { return nil } @@ -1645,6 +1624,8 @@ func (p *AgentServiceTargetProvider) waitForAgentActive( const confirmCount = 2 // consecutive times a terminal status must be seen deadline := time.Now().Add(pollTimeout) + maxAttempts := int(pollTimeout / pollInterval) + attempt := 0 progress("Waiting for agent to become active") var consecutiveActive int @@ -1658,6 +1639,9 @@ func (p *AgentServiceTargetProvider) waitForAgentActive( case <-time.After(pollInterval): } + attempt++ + progress(fmt.Sprintf("Polling agent status (%d/%d)", attempt, maxAttempts)) + versionResp, err := agentClient.GetAgentVersion(ctx, agentName, version, agentAPIVersion) if err != nil { fmt.Fprintf(os.Stderr, " Warning: poll failed: %s\n", err)