Skip to content

Commit e411d0c

Browse files
jongioCopilot
andcommitted
fix: strip UTF-8 BOM from funcignore and fix negation pattern handling (Azure#7311)
* fix: strip UTF-8 BOM from funcignore and fix negation pattern handling The go-gitignore library does not handle UTF-8 BOM (byte order mark) that some Windows editors (e.g. Notepad) prepend to files. When a .funcignore file has a BOM, the invisible prefix bytes become part of the first pattern, causing it to never match. This broke the node_modules exclusion detection in resolveFunctionAppRemoteBuild, leading to incorrect remoteBuild validation results. Changes: - Strip UTF-8 BOM from .funcignore content before parsing in both resolveFunctionAppRemoteBuild (validation) and createDeployableZip (packaging) for consistent behavior - Switch createDeployableZip from gitignore.NewFromFile to manual read + gitignore.New for consistency with validation code path - Fix negation pattern handling in createDeployableZip: check match.Ignore() instead of just match != nil, so negation patterns (e.g. !node_modules/important) correctly include files - Add comprehensive BOM-handling tests and stripUTF8BOM unit tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: clean up temp zip file on ignore file read error Close and remove the temporary zip file when reading the ignore file fails with a non-NotExist error, preventing file handle leaks and orphaned temp files (especially on Windows). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: retrigger CI (Windows extension test flake) The previous Windows BuildCLI failure was a known file-locking flake in Test_CLI_Extension_ForceInstall — unrelated to this PR's changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: merge CustomerScenarios into JavaScriptMatrix test Addresses review feedback from @weikanglim. The CustomerScenarios test was duplicating 8 of 9 cases from JavaScriptMatrix. Merged Case 9 (true + no funcignore) into JavaScriptMatrix and added customer case comments for traceability. Removed the duplicate test function. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f2a971e commit e411d0c

3 files changed

Lines changed: 295 additions & 16 deletions

File tree

cli/azd/pkg/project/project_utils.go

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package project
55

66
import (
7+
"bytes"
78
"errors"
89
"fmt"
910
"io/fs"
@@ -30,12 +31,19 @@ func createDeployableZip(svc *ServiceConfig, root string) (string, error) {
3031
ignoreFile := svc.Host.IgnoreFile()
3132
var ignorer gitignore.GitIgnore
3233
if ignoreFile != "" {
33-
ig, err := gitignore.NewFromFile(filepath.Join(root, ignoreFile))
34-
if !errors.Is(err, fs.ErrNotExist) && err != nil {
34+
ignoreFilePath := filepath.Join(root, ignoreFile)
35+
contents, err := os.ReadFile(ignoreFilePath)
36+
if errors.Is(err, fs.ErrNotExist) {
37+
// no ignore file, use defaults below
38+
} else if err != nil {
39+
zipFile.Close()
40+
os.Remove(zipFile.Name()) //nolint:gosec // G703: zipFile.Name() is our own temp file, not user-controlled
3541
return "", fmt.Errorf("reading ignore file: %w", err)
42+
} else {
43+
// Strip UTF-8 BOM if present, then parse from in-memory contents.
44+
contents = stripUTF8BOM(contents)
45+
ignorer = gitignore.New(bytes.NewReader(contents), root, nil)
3646
}
37-
38-
ignorer = ig
3947
}
4048

4149
// apply exclusions for zip deployment
@@ -68,9 +76,11 @@ func createDeployableZip(svc *ServiceConfig, root string) (string, error) {
6876
}
6977

7078
// apply exclusions from ignore file
71-
if ignorer != nil && ignorer.Absolute(src, isDir) != nil {
72-
return false, nil
73-
} else if ignorer == nil { // default exclusions without ignorefile control
79+
if ignorer != nil {
80+
if match := ignorer.Absolute(src, isDir); match != nil && match.Ignore() {
81+
return false, nil
82+
}
83+
} else { // default exclusions without ignorefile control
7484
if svc.Language == ServiceLanguagePython {
7585
if isDir {
7686
// check for .venv containing pyvenv.cfg
@@ -112,3 +122,12 @@ func createDeployableZip(svc *ServiceConfig, root string) (string, error) {
112122

113123
return zipFile.Name(), nil
114124
}
125+
126+
// utf8BOM is the byte order mark that some Windows editors prepend to UTF-8 files.
127+
var utf8BOM = []byte{0xEF, 0xBB, 0xBF}
128+
129+
// stripUTF8BOM removes a leading UTF-8 BOM from the given byte slice if present.
130+
// The BOM breaks gitignore pattern parsing because the invisible bytes become part of the first pattern.
131+
func stripUTF8BOM(data []byte) []byte {
132+
return bytes.TrimPrefix(data, utf8BOM)
133+
}

cli/azd/pkg/project/service_target_functionapp.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ func resolveFunctionAppRemoteBuild(serviceConfig *ServiceConfig) (remoteBuild bo
4444
return false, fmt.Errorf("reading ignore file: %w", err)
4545
}
4646

47+
// Strip UTF-8 BOM if present. Some Windows editors (e.g. Notepad) prepend a BOM which
48+
// causes the gitignore parser to treat the first pattern as having invisible prefix bytes,
49+
// breaking pattern matching.
50+
ignoreFileContents = stripUTF8BOM(ignoreFileContents)
51+
4752
// Parse from in-memory contents so we don't hold an open file handle (important on Windows temp dirs).
4853
ignore := gitignore.New(bytes.NewReader(ignoreFileContents), serviceConfig.Path(), nil)
4954

0 commit comments

Comments
 (0)