Skip to content

Commit c6a0b63

Browse files
authored
feat: add .zeaburignore support for deployment filtering (#162)
2 parents 5f6479c + bf32eb5 commit c6a0b63

File tree

2 files changed

+241
-7
lines changed

2 files changed

+241
-7
lines changed

internal/util/pack.go

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,28 +38,72 @@ func PackZipWithoutGitIgnoreFiles() ([]byte, error) {
3838
return flate.NewWriter(out, flate.BestCompression)
3939
})
4040

41-
ignoreObject, err := gitignore.CompileIgnoreFile("./.gitignore")
41+
// .zeaburignore has higher priority than .gitignore
42+
// Try to load .zeaburignore first, fallback to .gitignore if not exists
43+
var ignoreObject *gitignore.GitIgnore
44+
var err error
45+
46+
ignoreObject, err = gitignore.CompileIgnoreFile("./.zeaburignore")
4247
if err != nil {
43-
if !errors.Is(err, fs.ErrNotExist) {
44-
fmt.Println("Error compiling .gitignore file:", err)
48+
if errors.Is(err, fs.ErrNotExist) {
49+
// .zeaburignore not found, try .gitignore
50+
ignoreObject, err = gitignore.CompileIgnoreFile("./.gitignore")
51+
if err != nil {
52+
if !errors.Is(err, fs.ErrNotExist) {
53+
fmt.Println("Error compiling .gitignore file:", err)
54+
}
55+
ignoreObject = nil
56+
}
57+
} else {
58+
fmt.Println("Error compiling .zeaburignore file:", err)
59+
ignoreObject = nil
4560
}
4661
}
4762

4863
err = filepath.Walk(".", func(path string, info fs.FileInfo, err error) error {
4964
if err != nil {
50-
fmt.Println("Error accessing path:", path, err)
51-
return err
65+
// Skip files/directories that cannot be accessed (e.g., symlinks to non-existent targets)
66+
if info != nil && info.IsDir() {
67+
return filepath.SkipDir
68+
}
69+
return nil
5270
}
5371

5472
if path == "." {
5573
return nil
5674
}
5775

58-
if strings.HasPrefix(path, ".git") {
76+
// Skip .git directory but not .gitignore or other .git* files
77+
if path == ".git" || strings.HasPrefix(path, ".git"+string(filepath.Separator)) {
78+
if info != nil && info.IsDir() {
79+
return filepath.SkipDir
80+
}
5981
return nil
6082
}
6183

62-
if ignoreObject != nil && ignoreObject.MatchesPath(path) {
84+
// Check ignore patterns before processing
85+
if ignoreObject != nil {
86+
// Normalize path separators to forward slashes for cross-platform gitignore matching
87+
checkPath := filepath.ToSlash(path)
88+
// For directories, we need to check with trailing slash for proper gitignore matching
89+
if info.IsDir() {
90+
checkPath = checkPath + "/"
91+
}
92+
93+
if ignoreObject.MatchesPath(checkPath) {
94+
// Skip ignored files/directories entirely
95+
if info.IsDir() {
96+
return filepath.SkipDir
97+
}
98+
return nil
99+
}
100+
}
101+
102+
// Skip symlinks to avoid "is a directory" errors
103+
if info.Mode()&os.ModeSymlink != 0 {
104+
if info.IsDir() {
105+
return filepath.SkipDir
106+
}
63107
return nil
64108
}
65109

internal/util/pack_test.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package util_test
2+
3+
import (
4+
"archive/zip"
5+
"bytes"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
"testing"
10+
11+
"github.com/zeabur/cli/internal/util"
12+
)
13+
14+
func TestPackZipWithZeaburIgnore(t *testing.T) {
15+
// Create a temporary directory for testing
16+
tmpDir, err := os.MkdirTemp("", "zeabur-pack-test-")
17+
if err != nil {
18+
t.Fatalf("Failed to create temp dir: %v", err)
19+
}
20+
defer os.RemoveAll(tmpDir)
21+
22+
// Change to temp directory
23+
originalDir, err := os.Getwd()
24+
if err != nil {
25+
t.Fatalf("Failed to get current dir: %v", err)
26+
}
27+
defer func() {
28+
if err := os.Chdir(originalDir); err != nil {
29+
t.Errorf("Failed to restore directory: %v", err)
30+
}
31+
}()
32+
33+
if err := os.Chdir(tmpDir); err != nil {
34+
t.Fatalf("Failed to change to temp dir: %v", err)
35+
}
36+
37+
// Create test files and directories
38+
testFiles := map[string]string{
39+
"main.go": "package main",
40+
"README.md": "# Test Project",
41+
".agent/skills/test.txt": "should be ignored",
42+
".agents/config.json": "should be ignored",
43+
"src/app.go": "package src",
44+
".gitignore": "*.log\n",
45+
"test.log": "log content",
46+
".git/config": "[core]\n",
47+
}
48+
49+
for path, content := range testFiles {
50+
dir := filepath.Dir(path)
51+
if dir != "." {
52+
if err := os.MkdirAll(dir, 0755); err != nil {
53+
t.Fatalf("Failed to create dir %s: %v", dir, err)
54+
}
55+
}
56+
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
57+
t.Fatalf("Failed to create file %s: %v", path, err)
58+
}
59+
}
60+
61+
// Create .zeaburignore file
62+
zeaburignore := `.agent/
63+
.agents/
64+
.cursor/
65+
`
66+
if err := os.WriteFile(".zeaburignore", []byte(zeaburignore), 0644); err != nil {
67+
t.Fatalf("Failed to create .zeaburignore: %v", err)
68+
}
69+
70+
// Pack the zip
71+
zipBytes, err := util.PackZipWithoutGitIgnoreFiles()
72+
if err != nil {
73+
t.Fatalf("PackZipWithoutGitIgnoreFiles failed: %v", err)
74+
}
75+
76+
// Read the zip and check contents
77+
zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
78+
if err != nil {
79+
t.Fatalf("Failed to read zip: %v", err)
80+
}
81+
82+
filesInZip := make(map[string]bool)
83+
for _, file := range zipReader.File {
84+
filesInZip[file.Name] = true
85+
t.Logf("File in zip: %s", file.Name)
86+
}
87+
88+
// Check that expected files are included
89+
expectedFiles := []string{"main.go", "README.md", "src/app.go", ".zeaburignore", ".gitignore"}
90+
for _, file := range expectedFiles {
91+
if !filesInZip[file] {
92+
t.Errorf("Expected file %s not found in zip", file)
93+
}
94+
}
95+
96+
// Check that .zeaburignore patterns are excluded
97+
excludedByZeaburIgnore := []string{".agent/skills/test.txt", ".agents/config.json", ".agent/", ".agents/"}
98+
for _, file := range excludedByZeaburIgnore {
99+
if filesInZip[file] {
100+
t.Errorf("File %s should be excluded by .zeaburignore but found in zip", file)
101+
}
102+
}
103+
104+
// When .zeaburignore exists, .gitignore patterns are NOT applied
105+
// So test.log should be included in the zip
106+
if !filesInZip["test.log"] {
107+
t.Errorf("Expected test.log to be included in zip when .zeaburignore takes precedence over .gitignore")
108+
}
109+
110+
// Check that .git directory is excluded
111+
for path := range filesInZip {
112+
if strings.HasPrefix(path, ".git/") || strings.HasPrefix(path, ".git\\") {
113+
t.Errorf("File %s should be excluded (.git directory) but found in zip", path)
114+
}
115+
}
116+
}
117+
118+
func TestPackZipWithoutZeaburIgnore(t *testing.T) {
119+
// Create a temporary directory for testing
120+
tmpDir, err := os.MkdirTemp("", "zeabur-pack-test-")
121+
if err != nil {
122+
t.Fatalf("Failed to create temp dir: %v", err)
123+
}
124+
defer os.RemoveAll(tmpDir)
125+
126+
// Change to temp directory
127+
originalDir, err := os.Getwd()
128+
if err != nil {
129+
t.Fatalf("Failed to get current dir: %v", err)
130+
}
131+
defer func() {
132+
if err := os.Chdir(originalDir); err != nil {
133+
t.Errorf("Failed to restore directory: %v", err)
134+
}
135+
}()
136+
137+
if err := os.Chdir(tmpDir); err != nil {
138+
t.Fatalf("Failed to change to temp dir: %v", err)
139+
}
140+
141+
// Create test files
142+
testFiles := map[string]string{
143+
"main.go": "package main",
144+
"README.md": "# Test Project",
145+
"test.log": "log content",
146+
}
147+
148+
for path, content := range testFiles {
149+
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
150+
t.Fatalf("Failed to create file %s: %v", path, err)
151+
}
152+
}
153+
154+
// Create .gitignore file (no .zeaburignore)
155+
gitignore := "*.log\n"
156+
if err := os.WriteFile(".gitignore", []byte(gitignore), 0644); err != nil {
157+
t.Fatalf("Failed to create .gitignore: %v", err)
158+
}
159+
160+
// Pack the zip
161+
zipBytes, err := util.PackZipWithoutGitIgnoreFiles()
162+
if err != nil {
163+
t.Fatalf("PackZipWithoutGitIgnoreFiles failed: %v", err)
164+
}
165+
166+
// Read the zip and check contents
167+
zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes)))
168+
if err != nil {
169+
t.Fatalf("Failed to read zip: %v", err)
170+
}
171+
172+
filesInZip := make(map[string]bool)
173+
for _, file := range zipReader.File {
174+
filesInZip[file.Name] = true
175+
t.Logf("File in zip: %s", file.Name)
176+
}
177+
178+
// Check that expected files are included
179+
expectedFiles := []string{"main.go", "README.md", ".gitignore"}
180+
for _, file := range expectedFiles {
181+
if !filesInZip[file] {
182+
t.Errorf("Expected file %s not found in zip", file)
183+
}
184+
}
185+
186+
// Check that .gitignore patterns are respected (should fallback to .gitignore)
187+
if filesInZip["test.log"] {
188+
t.Errorf("File test.log should be excluded by .gitignore but found in zip")
189+
}
190+
}

0 commit comments

Comments
 (0)