diff --git a/.vscode/cspell.global.yaml b/.vscode/cspell.global.yaml index 94465ad0248..94ec7df49b0 100644 --- a/.vscode/cspell.global.yaml +++ b/.vscode/cspell.global.yaml @@ -35,6 +35,7 @@ ignoreWords: - azurewebsites - azcore - Azdo + - azdignore - azdrelease - aztfmod - azurecaf @@ -59,9 +60,11 @@ ignoreWords: - dapr - databricks - dedb + - denormal - devcenter - devcontainer - dnsz + - dotignore - evgd - evgs - evgt @@ -71,6 +74,8 @@ ignoreWords: - fdfp - fics - Frontdoor + - funcignore + - gitcli - golobby - graphsdk - hndl @@ -158,6 +163,7 @@ ignoreWords: - vwan - wafrg - westus + - webappignore - Wans - apim - Retryable @@ -176,7 +182,9 @@ ignoreWords: - venv - webapprouting - zipdeploy + - zipignore - appinsights + - pycache useGitignore: true dictionaryDefinitions: - name: gitHubUserAliases diff --git a/cli/azd/.vscode/launch.json b/cli/azd/.vscode/launch.json index c4f0bfbfaef..e31a252056c 100644 --- a/cli/azd/.vscode/launch.json +++ b/cli/azd/.vscode/launch.json @@ -55,4 +55,4 @@ "default": "auth login --use-device-code" } ] -} +} \ No newline at end of file diff --git a/cli/azd/pkg/dotignore/dotignore.go b/cli/azd/pkg/dotignore/dotignore.go new file mode 100644 index 00000000000..d47048cc47e --- /dev/null +++ b/cli/azd/pkg/dotignore/dotignore.go @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package dotignore + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/denormal/go-gitignore" +) + +// ReadDotIgnoreFile reads the ignore file located at the root of the project directory. +// If the ignoreFileName is blank or the file is not found, it returns nil, nil. +func ReadDotIgnoreFile(projectDir string, ignoreFileName string) (gitignore.GitIgnore, error) { + // Return nil if the ignoreFileName is empty + if ignoreFileName == "" { + return nil, nil + } + + ignoreFilePath := filepath.Join(projectDir, ignoreFileName) + if _, err := os.Stat(ignoreFilePath); os.IsNotExist(err) { + // Return nil if the ignore file does not exist + return nil, nil + } + + ignoreMatcher, err := gitignore.NewFromFile(ignoreFilePath) + if err != nil { + return nil, fmt.Errorf("error reading %s file at %s: %w", ignoreFileName, ignoreFilePath, err) + } + + return ignoreMatcher, nil +} + +// ShouldIgnore determines whether a file or directory should be ignored based on the provided ignore matcher. +func ShouldIgnore(relativePath string, isDir bool, ignoreMatcher gitignore.GitIgnore) bool { + if ignoreMatcher != nil { + if match := ignoreMatcher.Relative(relativePath, isDir); match != nil && match.Ignore() { + return true + } + } + return false +} diff --git a/cli/azd/pkg/project/framework_service_npm.go b/cli/azd/pkg/project/framework_service_npm.go index 18d5f5f90a2..ff97c8c95a4 100644 --- a/cli/azd/pkg/project/framework_service_npm.go +++ b/cli/azd/pkg/project/framework_service_npm.go @@ -131,10 +131,10 @@ func (np *npmProject) Package( packageSource, packageDest, buildForZipOptions{ - excludeConditions: []excludeDirEntryCondition{ - excludeNodeModules, - }, - }); err != nil { + excludeConditions: []excludeDirEntryCondition{excludeNodeModules}, + }, + serviceConfig, + ); err != nil { return nil, fmt.Errorf("packaging for %s: %w", serviceConfig.Name, err) } diff --git a/cli/azd/pkg/project/framework_service_python.go b/cli/azd/pkg/project/framework_service_python.go index e700020cb93..de2d478a576 100644 --- a/cli/azd/pkg/project/framework_service_python.go +++ b/cli/azd/pkg/project/framework_service_python.go @@ -131,11 +131,10 @@ func (pp *pythonProject) Package( packageSource, packageDest, buildForZipOptions{ - excludeConditions: []excludeDirEntryCondition{ - excludeVirtualEnv, - excludePyCache, - }, - }); err != nil { + excludeConditions: []excludeDirEntryCondition{excludeVirtualEnv, excludePyCache}, + }, + serviceConfig, + ); err != nil { return nil, fmt.Errorf("packaging for %s: %w", serviceConfig.Name, err) } diff --git a/cli/azd/pkg/project/project_utils.go b/cli/azd/pkg/project/project_utils.go index fdce5438556..3e35ed773e7 100644 --- a/cli/azd/pkg/project/project_utils.go +++ b/cli/azd/pkg/project/project_utils.go @@ -9,34 +9,36 @@ import ( "path/filepath" "time" + "github.com/azure/azure-dev/cli/azd/pkg/dotignore" "github.com/azure/azure-dev/cli/azd/pkg/rzip" "github.com/otiai10/copy" ) -// CreateDeployableZip creates a zip file of a folder, recursively. +// createDeployableZip creates a zip file of a folder, recursively. // Returns the path to the created zip file or an error if it fails. func createDeployableZip(projectName string, appName string, path string) (string, error) { - // TODO: should probably avoid picking up files that weren't meant to be deployed (ie, local .env files, etc..) + // Create the output zip file path filePath := filepath.Join(os.TempDir(), fmt.Sprintf("%s-%s-azddeploy-%d.zip", projectName, appName, time.Now().Unix())) zipFile, err := os.Create(filePath) if err != nil { return "", fmt.Errorf("failed when creating zip package to deploy %s: %w", appName, err) } - if err := rzip.CreateFromDirectory(path, zipFile); err != nil { - // if we fail here just do our best to close things out and cleanup + // Zip the directory without any exclusions (they've already been handled in buildForZip) + err = rzip.CreateFromDirectory(path, zipFile) + if err != nil { zipFile.Close() os.Remove(zipFile.Name()) return "", err } + // Close the zip file and return the path if err := zipFile.Close(); err != nil { - // may fail but, again, we'll do our best to cleanup here. os.Remove(zipFile.Name()) return "", err } - return zipFile.Name(), nil + return filePath, nil } // excludeDirEntryCondition resolves when a file or directory should be considered or not as part of build, when build is a @@ -48,17 +50,43 @@ type buildForZipOptions struct { excludeConditions []excludeDirEntryCondition } -// buildForZip is use by projects which build strategy is to only copy the source code into a folder which is later -// zipped for packaging. For example Python and Node framework languages. buildForZipOptions provides the specific -// details for each language which should not be ever copied. -func buildForZip(src, dst string, options buildForZipOptions) error { +// buildForZip is used by projects to prepare a directory for +// zipping, excluding files based on the ignore file and other conditions. +func buildForZip(src, dst string, options buildForZipOptions, serviceConfig *ServiceConfig) error { + // Lookup the appropriate ignore file name based on the service kind (Host) + ignoreFileName := GetIgnoreFileNameByKind(serviceConfig.Host) + + // Read and honor the specified ignore file if it exists + ignoreMatcher, err := dotignore.ReadDotIgnoreFile(src, ignoreFileName) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("reading %s file: %w", ignoreFileName, err) + } + + // Temporary array to build exclude conditions dynamically + tempExcludeConditions := []excludeDirEntryCondition{} + + // If there's no .ignore file, add the provided excludeConditions + if ignoreMatcher == nil { + tempExcludeConditions = append(tempExcludeConditions, options.excludeConditions...) + } else { + // If there's a .ignore file, apply ignoreMatcher only + tempExcludeConditions = append(tempExcludeConditions, func(path string, file os.FileInfo) bool { + relativePath, err := filepath.Rel(src, path) + if err == nil && dotignore.ShouldIgnore(relativePath, file.IsDir(), ignoreMatcher) { + return true + } + return false + }) + } - // these exclude conditions applies to all projects - options.excludeConditions = append(options.excludeConditions, globalExcludeAzdFolder) + // Always append the global exclusions (e.g., .azure folder) + tempExcludeConditions = append(tempExcludeConditions, globalExcludeAzdFolder) + // Copy the source directory to the destination, applying the final exclude conditions return copy.Copy(src, dst, copy.Options{ Skip: func(srcInfo os.FileInfo, src, dest string) (bool, error) { - for _, checkExclude := range options.excludeConditions { + // Apply exclude conditions (either the default or the ignoreMatcher) + for _, checkExclude := range tempExcludeConditions { if checkExclude(src, srcInfo) { return true, nil } diff --git a/cli/azd/pkg/project/service_target.go b/cli/azd/pkg/project/service_target.go index fea7b41e0e2..a21c799b833 100644 --- a/cli/azd/pkg/project/service_target.go +++ b/cli/azd/pkg/project/service_target.go @@ -94,6 +94,19 @@ type ServiceTarget interface { ) ([]string, error) } +// GetIgnoreFileNameByKind returns the appropriate ignore file name (e.g., .funcignore, .webappignore) +// based on the service target kind. +func GetIgnoreFileNameByKind(kind ServiceTargetKind) string { + switch kind { + case AzureFunctionTarget: + return ".funcignore" + case AppServiceTarget: + return ".webappignore" + default: + return "" + } +} + // NewServiceDeployResult is a helper function to create a new ServiceDeployResult func NewServiceDeployResult( relatedResourceId string, diff --git a/cli/azd/pkg/rzip/rzip.go b/cli/azd/pkg/rzip/rzip.go index 34f5191954b..2b4336398a6 100644 --- a/cli/azd/pkg/rzip/rzip.go +++ b/cli/azd/pkg/rzip/rzip.go @@ -12,47 +12,77 @@ import ( "strings" ) +// CreateFromDirectory compresses a directory into a zip file. func CreateFromDirectory(source string, buf *os.File) error { w := zip.NewWriter(buf) + defer w.Close() + + // Walk through the source directory err := filepath.WalkDir(source, func(path string, info fs.DirEntry, err error) error { if err != nil { return err } - if info.IsDir() { - return nil - } - fileInfo, err := info.Info() + // Fetch file info (Lstat avoids following symlinks) + fileInfo, err := os.Lstat(path) if err != nil { return err } - header := &zip.FileHeader{ - Name: strings.Replace( - strings.TrimPrefix( - strings.TrimPrefix(path, source), - string(filepath.Separator)), "\\", "/", -1), - Modified: fileInfo.ModTime(), - Method: zip.Deflate, + // Skip symbolic links + if fileInfo.Mode()&os.ModeSymlink != 0 { + return nil } - f, err := w.CreateHeader(header) - if err != nil { - return err + // Create relative path and normalize it for zip + relativePath := filepath.ToSlash(strings.TrimPrefix(strings.TrimPrefix(path, source), string(filepath.Separator))) + + // Handle directories by adding a trailing slash + if fileInfo.IsDir() { + relativePath += "/" } - in, err := os.Open(path) + + // Create zip header from the file info + header, err := zip.FileInfoHeader(fileInfo) if err != nil { return err } - _, err = io.Copy(f, in) + + header.Name = relativePath + header.Modified = fileInfo.ModTime() + + // Compress files (leave directories uncompressed) + if !fileInfo.IsDir() { + header.Method = zip.Deflate + } + + // Write the header to the zip + writer, err := w.CreateHeader(header) if err != nil { return err } + + // Write the file's content if it's not a directory + if !fileInfo.IsDir() { + if err := writeFileToZip(writer, path); err != nil { + return err + } + } + return nil }) + + return err +} + +// writeFileToZip writes the contents of a file to the zip writer. +func writeFileToZip(writer io.Writer, filePath string) error { + file, err := os.Open(filePath) if err != nil { return err } + defer file.Close() - return w.Close() + _, err = io.Copy(writer, file) + return err } diff --git a/cli/azd/test/functional/package_test.go b/cli/azd/test/functional/package_test.go index acb22b3596e..d217dded3f8 100644 --- a/cli/azd/test/functional/package_test.go +++ b/cli/azd/test/functional/package_test.go @@ -1,9 +1,11 @@ package cli_test import ( + "archive/zip" "fmt" "os" "path/filepath" + "strings" "testing" "github.com/azure/azure-dev/cli/azd/pkg/osutil" @@ -160,3 +162,285 @@ func Test_CLI_Package(t *testing.T) { require.NoError(t, err) require.Contains(t, packageResult.Stdout, fmt.Sprintf("Package Output: %s", os.TempDir())) } + +/* +Test_CLI_Package_ZipIgnore + +This test verifies that the packaging logic correctly handles the inclusion and exclusion of files +based on the presence or absence of `.webappignore` and `.funcignore` files. The following scenarios are covered: + +1. Node_App_Service_With_Webappignore + - Verifies that `node_modules` is included when `.webappignore` is present, + and that specific files like `logs/log.txt` are excluded as per the rules defined in the `.webappignore` file. + +2. Python_App_Service_With_Webappignore + - Verifies that Python-specific files like `__pycache__` and `.venv` are included when `.webappignore` is present, + and files like `logs/log.txt` are excluded based on the rules in the `.webappignore`. + +3. Python_App_Service_With_Pycache_Excluded + - Verifies that `__pycache__` is excluded when a `.webappignore` file explicitly contains a rule to exclude it, + while other directories like `.venv` are included since there is no exclusion rule for them. + +4. Function_App_With_Funcignore + - Verifies that a Function App respects the `.funcignore` file, ensuring that `logs/log.txt` is excluded + as per the rules defined in `.funcignore`. + +5. Node_App_Service_Without_Webappignore + - Verifies that `node_modules` is excluded when no `.webappignore` is present, + and that files like `logs/log.txt` are included since no exclusion rules apply without the `.webappignore`. + +6. Python_App_Service_Without_Webappignore + - Verifies that Python-specific files like `__pycache__` + and `.venv` are excluded by default when no `.webappignore` is present, + and that files like `logs/log.txt` are included. + +7. Function_App_Without_Funcignore + - Verifies that when no `.funcignore` file is present, no exclusions are applied, and files such as `logs/log.txt` + are included in the package. + +For each scenario, the test simulates the presence or absence of the relevant + `.ignore` files and checks the contents of the resulting + zip package to ensure the correct files are included or excluded as expected. +*/ + +func Test_CLI_Package_ZipIgnore(t *testing.T) { + t.Parallel() + ctx, cancel := newTestContext(t) + defer cancel() + + // Set this to true if you want to print the directory and zip contents for debugging + printDebug := false + + // Create a temporary directory for the project + dir := tempDirWithDiagnostics(t) + + // Set up the CLI with the appropriate working directory and environment variables + cli := azdcli.NewCLI(t) + cli.WorkingDirectory = dir + cli.Env = append(os.Environ(), "AZURE_LOCATION=eastus2") + + // Copy the sample project to the app directory + err := copySample(dir, "dotignore") + require.NoError(t, err, "failed expanding sample") + + // Print directory contents for debugging if printDebug is true + if printDebug { + printDirContents(t, "service_node", filepath.Join(dir, "src", "service_node")) + printDirContents(t, "service_python", filepath.Join(dir, "src", "service_python")) + printDirContents(t, "service_python_pycache", filepath.Join(dir, "src", "service_python_pycache")) + printDirContents(t, "service_function", filepath.Join(dir, "src", "service_function")) + } + + // Run the init command to initialize the project + _, err = cli.RunCommandWithStdIn( + ctx, + "Use code in the current directory\n"+ + "Confirm and continue initializing my app\n"+ + "appdb\n"+ + "TESTENV\n", + "init", + ) + require.NoError(t, err) + + // Define the scenarios to test + scenarios := []struct { + name string + description string + serviceName string // This is the actual service name used in the directory + expectedFiles map[string]bool + shouldDeleteIgnoreFile bool // Flag to simulate the absence of .webappignore or .funcignore + }{ + { + name: "Node_App_Service_With_Webappignore", + description: "Verifies that node_modules are included when " + + ".webappignore is present, and logs/log.txt is excluded.", + serviceName: "service_node", + shouldDeleteIgnoreFile: false, + expectedFiles: map[string]bool{ + "testfile.js": true, + "node_modules/some_package/index.js": true, // Included because .webappignore is present + "logs/log.txt": false, // Excluded by .webappignore + }, + }, + { + name: "Python_App_Service_With_Webappignore", + description: "Verifies that __pycache__ and .venv are included when " + + " .webappignore is present, and logs/log.txt is excluded.", + serviceName: "service_python", + shouldDeleteIgnoreFile: false, + expectedFiles: map[string]bool{ + "testfile.py": true, + "__pycache__/testcache.txt": true, // Included because .webappignore is present + ".venv/pyvenv.cfg": true, // Included because .webappignore is present + "logs/log.txt": false, // Excluded by .webappignore + }, + }, + { + name: "Python_App_Service_With_Pycache_Excluded", + description: "Verifies that __pycache__ is excluded when .webappignore has a rule to exclude it.", + serviceName: "service_python_pycache", + shouldDeleteIgnoreFile: false, + expectedFiles: map[string]bool{ + "testfile.py": true, + "__pycache__/testcache.txt": false, // Excluded by .webappignore rule + ".venv/pyvenv.cfg": true, // Included because no exclusion rule + "logs/log.txt": false, // Excluded by .webappignore + }, + }, + { + name: "Function_App_With_Funcignore", + description: "Verifies that logs/log.txt is excluded when .funcignore is present.", + serviceName: "service_function", + shouldDeleteIgnoreFile: false, + expectedFiles: map[string]bool{ + "testfile.py": true, + "__pycache__/testcache.txt": true, + ".venv/pyvenv.cfg": true, + "logs/log.txt": false, // Excluded by .funcignore + }, + }, + { + name: "Node_App_Service_Without_Webappignore", + description: "Verifies that node_modules is excluded when .webappignore is not present.", + serviceName: "service_node", + shouldDeleteIgnoreFile: true, + expectedFiles: map[string]bool{ + "testfile.js": true, + "node_modules/some_package/index.js": false, // Excluded because no .webappignore + "logs/log.txt": true, // Included because no .webappignore + }, + }, + { + name: "Python_App_Service_Without_Webappignore", + description: "Verifies that __pycache__ and .venv are excluded when .webappignore is not present.", + serviceName: "service_python", + shouldDeleteIgnoreFile: true, + expectedFiles: map[string]bool{ + "testfile.py": true, + "__pycache__/testcache.txt": false, // Excluded because no .webappignore + ".venv/pyvenv.cfg": false, // Excluded because no .webappignore + "logs/log.txt": true, // Included because no .webappignore + }, + }, + { + name: "Function_App_Without_Funcignore", + description: "Verifies that logs/log.txt is included when .funcignore is not present.", + serviceName: "service_function", + shouldDeleteIgnoreFile: true, + expectedFiles: map[string]bool{ + "testfile.py": true, + "__pycache__/testcache.txt": false, + ".venv/pyvenv.cfg": false, + "logs/log.txt": true, // Included because no .funcignore + }, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + // Print the scenario description + t.Logf("Scenario: %s - %s", scenario.name, scenario.description) + + // If we're simulating the absence of the ignore file, delete it + if scenario.shouldDeleteIgnoreFile { + os.Remove(filepath.Join(dir, "src", scenario.serviceName, ".webappignore")) + os.Remove(filepath.Join(dir, "src", scenario.serviceName, ".funcignore")) + } + + // Run the package command and specify an output path + outputDir := filepath.Join(dir, "dist_"+strings.ReplaceAll(scenario.name, " ", "_")) + err = os.Mkdir(outputDir, 0755) // Ensure the directory exists + require.NoError(t, err) + + _, err = cli.RunCommand(ctx, "package", "--output-path", outputDir) + require.NoError(t, err) + + // Check contents of Service package + checkServicePackage(t, outputDir, scenario.serviceName, scenario.expectedFiles, printDebug) + + // Clean up generated zip files and ignore files + os.RemoveAll(outputDir) + }) + } +} + +// Helper function to check service package contents +func checkServicePackage(t *testing.T, distPath, serviceName string, expectedFiles map[string]bool, printDebug bool) { + zipFilePath := findServiceZipFile(t, distPath, serviceName) + if printDebug { + printZipContents(t, serviceName, zipFilePath) // Print the contents of the zip file if printDebug is true + } + zipReader, err := zip.OpenReader(zipFilePath) + require.NoError(t, err) + defer zipReader.Close() + + checkZipContents(t, zipReader, expectedFiles, serviceName) +} + +// Helper function to find the zip file by service name +func findServiceZipFile(t *testing.T, distPath, serviceName string) string { + files, err := os.ReadDir(distPath) + require.NoError(t, err) + + for _, file := range files { + if filepath.Ext(file.Name()) == ".zip" && strings.Contains(file.Name(), serviceName) { + return filepath.Join(distPath, file.Name()) + } + } + + t.Fatalf("Zip file for service '%s' not found", serviceName) + return "" +} + +// Helper function to print directory contents for debugging +func printDirContents(t *testing.T, serviceName, dir string) { + t.Logf("[%s] Listing directory: %s", serviceName, dir) + files, err := os.ReadDir(dir) + require.NoError(t, err) + for _, file := range files { + t.Logf("[%s] Found: %s", serviceName, file.Name()) + if file.IsDir() { + printDirContents(t, serviceName, + filepath.Join(dir, file.Name())) // Recursive call to list sub-directory contents + } + } +} + +// Helper function to print the contents of a zip file +func printZipContents(t *testing.T, serviceName, zipFilePath string) { + t.Logf("[%s] Listing contents of zip file: %s", serviceName, zipFilePath) + zipReader, err := zip.OpenReader(zipFilePath) + require.NoError(t, err) + defer zipReader.Close() + + for _, file := range zipReader.File { + t.Logf("[%s] Found in zip: %s", serviceName, file.Name) + } +} + +// Helper function to check zip contents against expected files +func checkZipContents(t *testing.T, zipReader *zip.ReadCloser, expectedFiles map[string]bool, serviceName string) { + foundFiles := make(map[string]bool) + + for _, file := range zipReader.File { + // Normalize the file name to use forward slashes + normalizedFileName := strings.ReplaceAll(file.Name, "\\", "/") + foundFiles[normalizedFileName] = true + } + + for expectedFile, shouldExist := range expectedFiles { + // Normalize the expected file name to use forward slashes + normalizedExpectedFile := strings.ReplaceAll(expectedFile, "\\", "/") + if shouldExist { + if !foundFiles[normalizedExpectedFile] { + t.Errorf("[%s] Expected file '%s' to be included in the package but it was not found", + serviceName, normalizedExpectedFile) + } + } else { + if foundFiles[normalizedExpectedFile] { + t.Errorf("[%s] Expected file '%s' to be excluded from the package but it was found", + serviceName, normalizedExpectedFile) + } + } + } +} diff --git a/cli/azd/test/functional/testdata/samples/dotignore/.gitignore b/cli/azd/test/functional/testdata/samples/dotignore/.gitignore new file mode 100644 index 00000000000..c3e5b3d6481 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/.gitignore @@ -0,0 +1,3 @@ +!src/**/__pycache__/ +!src/**/.venv/ +!src/**/node_modules/ diff --git a/cli/azd/test/functional/testdata/samples/dotignore/azure.yaml b/cli/azd/test/functional/testdata/samples/dotignore/azure.yaml new file mode 100644 index 00000000000..4461b426be1 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/azure.yaml @@ -0,0 +1,20 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: dotignoretest +services: + service_python: + project: src/service_python + language: python + host: appservice + service_node: + project: src/service_node + language: js + host: appservice + service_function: + project: src/service_function + language: python + host: function + service_python_pycache: + project: src/service_python_pycache + language: python + host: appservice \ No newline at end of file diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/.funcignore b/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/.funcignore new file mode 100644 index 00000000000..6d2b2ed9cc7 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/.funcignore @@ -0,0 +1 @@ +logs/log.txt \ No newline at end of file diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/.venv/pyvenv.cfg b/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/.venv/pyvenv.cfg new file mode 100644 index 00000000000..2bc7ab42877 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/.venv/pyvenv.cfg @@ -0,0 +1 @@ +home = /usr/local/bin/python diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/__pycache__/testcache.txt b/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/__pycache__/testcache.txt new file mode 100644 index 00000000000..d24b1df1aca --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/__pycache__/testcache.txt @@ -0,0 +1 @@ +binary data diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/logs/log.txt b/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/logs/log.txt new file mode 100644 index 00000000000..c3463ec70c8 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/logs/log.txt @@ -0,0 +1 @@ +Log data... diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/testfile.py b/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/testfile.py new file mode 100644 index 00000000000..7df869a15e7 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/testfile.py @@ -0,0 +1 @@ +print("Hello, World!") diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/.webappignore b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/.webappignore new file mode 100644 index 00000000000..6d2b2ed9cc7 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/.webappignore @@ -0,0 +1 @@ +logs/log.txt \ No newline at end of file diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/logs/log.txt b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/logs/log.txt new file mode 100644 index 00000000000..c3463ec70c8 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/logs/log.txt @@ -0,0 +1 @@ +Log data... diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/node_modules/.package-lock.json b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/node_modules/.package-lock.json new file mode 100644 index 00000000000..16586a85525 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/node_modules/.package-lock.json @@ -0,0 +1,12 @@ +{ + "name": "sample-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/some_package": { + "version": "1.0.0", + "license": "ISC" + } + } +} diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/node_modules/some_package/index.js b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/node_modules/some_package/index.js new file mode 100644 index 00000000000..e5367a98907 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/node_modules/some_package/index.js @@ -0,0 +1,5 @@ +// some_package/index.js + +module.exports = function() { + console.log("This is some_package!"); +}; diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/node_modules/some_package/package.json b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/node_modules/some_package/package.json new file mode 100644 index 00000000000..5fdf2c80fab --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/node_modules/some_package/package.json @@ -0,0 +1,8 @@ +{ + "name": "some_package", + "version": "1.0.0", + "description": "A mock package for testing", + "main": "index.js", + "author": "", + "license": "ISC" +} diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/package-lock.json b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/package-lock.json new file mode 100644 index 00000000000..29713cf7e85 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/package-lock.json @@ -0,0 +1,21 @@ +{ + "name": "sample-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sample-service", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "some_package": "^1.0.0" + }, + "devDependencies": {} + }, + "node_modules/some_package": { + "version": "1.0.0", + "license": "ISC" + } + } +} diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/package.json b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/package.json new file mode 100644 index 00000000000..e42ab6f7265 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/package.json @@ -0,0 +1,17 @@ +{ + "name": "sample-service", + "version": "1.0.0", + "description": "A sample Node.js service for testing purposes.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "echo \"Building the project...\"", + "start": "node index.js" + }, + "author": "Your Name", + "license": "MIT", + "dependencies": { + "some_package": "^1.0.0" + }, + "devDependencies": {} +} diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/testfile.js b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/testfile.js new file mode 100644 index 00000000000..184dfcc9987 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/testfile.js @@ -0,0 +1 @@ +console.log("Hello, World!"); diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/.venv/lib64/python3.10/site-packages/_yaml/__init__.py b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/.venv/lib64/python3.10/site-packages/_yaml/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/.venv/pyvenv.cfg b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/.venv/pyvenv.cfg new file mode 100644 index 00000000000..2bc7ab42877 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/.venv/pyvenv.cfg @@ -0,0 +1 @@ +home = /usr/local/bin/python diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/.webappignore b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/.webappignore new file mode 100644 index 00000000000..6d2b2ed9cc7 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/.webappignore @@ -0,0 +1 @@ +logs/log.txt \ No newline at end of file diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/__pycache__/testcache.txt b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/__pycache__/testcache.txt new file mode 100644 index 00000000000..d24b1df1aca --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/__pycache__/testcache.txt @@ -0,0 +1 @@ +binary data diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/logs/log.txt b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/logs/log.txt new file mode 100644 index 00000000000..c3463ec70c8 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/logs/log.txt @@ -0,0 +1 @@ +Log data... diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/testfile.py b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/testfile.py new file mode 100644 index 00000000000..7df869a15e7 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/testfile.py @@ -0,0 +1 @@ +print("Hello, World!") diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/.venv/pyvenv.cfg b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/.venv/pyvenv.cfg new file mode 100644 index 00000000000..2bc7ab42877 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/.venv/pyvenv.cfg @@ -0,0 +1 @@ +home = /usr/local/bin/python diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/.webappignore b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/.webappignore new file mode 100644 index 00000000000..bb379adcb0b --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/.webappignore @@ -0,0 +1,2 @@ +__pycache__/ +logs/log.txt \ No newline at end of file diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/__pycache__/testcache.txt b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/__pycache__/testcache.txt new file mode 100644 index 00000000000..d24b1df1aca --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/__pycache__/testcache.txt @@ -0,0 +1 @@ +binary data diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/logs/log.txt b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/logs/log.txt new file mode 100644 index 00000000000..c3463ec70c8 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/logs/log.txt @@ -0,0 +1 @@ +Log data... diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/testfile.py b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/testfile.py new file mode 100644 index 00000000000..7df869a15e7 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/testfile.py @@ -0,0 +1 @@ +print("Hello, World!") diff --git a/go.mod b/go.mod index 164e3b7cb42..02d70f2e421 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/bradleyjkemp/cupaloy/v2 v2.8.0 github.com/buger/goterm v1.0.4 github.com/cli/browser v1.1.0 + github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817 github.com/drone/envsubst v1.0.3 github.com/fatih/color v1.13.0 github.com/gofrs/flock v0.8.1 @@ -77,6 +78,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/go.sum b/go.sum index 1bfc6810097..2222c7404f7 100644 --- a/go.sum +++ b/go.sum @@ -175,9 +175,13 @@ github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWH github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/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/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=