@@ -22,26 +22,60 @@ const maxZipSize = 50 * 1024 * 1024 // 50 MB
2222
2323// defaultIgnorePatterns are files/dirs excluded when zipping for upload.
2424var defaultIgnorePatterns = []string {
25+ // Version control
2526 ".git" ,
26- ".gitignore" ,
27+
28+ // CreateOS config
2729 ".createos.json" ,
28- "node_modules" ,
30+
31+ // Secrets and credentials
2932 ".env" ,
3033 ".env.*" ,
31- "__pycache__" ,
34+ "*.pem" ,
35+ "*.key" ,
36+ "*.p12" ,
37+ "*.pfx" ,
38+ "*.crt" ,
39+ "*.cer" ,
40+ "*.jks" ,
41+ ".npmrc" ,
42+ ".pypirc" ,
43+ "credentials.json" ,
44+ "service-account*.json" ,
45+
46+ // Dependencies
47+ "node_modules" ,
3248 ".venv" ,
3349 "venv" ,
50+ "vendor" , // Go
51+
52+ // Build artifacts
53+ "target" , // Rust
54+ "coverage" ,
55+ ".nyc_output" ,
56+ ".pytest_cache" ,
57+ "__pycache__" ,
58+
59+ // Database files
60+ "*.sqlite" ,
61+ "*.sqlite3" ,
62+ "*.db" ,
63+
64+ // Log files
65+ "*.log" ,
66+
67+ // Terraform state
68+ ".terraform" ,
69+ "terraform.tfstate" ,
70+ "terraform.tfstate.*" ,
71+
72+ // OS/editor noise
3473 ".DS_Store" ,
3574 "Thumbs.db" ,
3675 ".idea" ,
3776 ".vscode" ,
3877 "*.swp" ,
3978 "*.swo" ,
40- "target" , // Rust
41- "vendor" , // Go (optional, but common to exclude)
42- "dist" , // built output — may need to include for some projects
43- "coverage" ,
44- ".nyc_output" ,
4579}
4680
4781// NewDeployCommand returns the deploy command.
@@ -97,24 +131,44 @@ func NewDeployCommand() *cli.Command {
97131 return err
98132 }
99133
134+ // Validate flag/type combinations
135+ isVCS := project .Type == "vcs" || project .Type == "githubImport"
136+ if c .IsSet ("branch" ) && ! isVCS {
137+ return fmt .Errorf ("--branch is only supported for Git-connected projects (this project uses %q deployment)" , project .Type )
138+ }
139+ if c .IsSet ("dir" ) && project .Type != "upload" {
140+ return fmt .Errorf ("--dir is only supported for upload projects (this project uses %q deployment)" , project .Type )
141+ }
142+
100143 // Route based on project type
101144 switch {
102145 case c .IsSet ("image" ) || project .Type == "image" :
103146 return deployImage (c , client , project )
104147 case project .Type == "upload" :
105148 return deployUpload (c , client , project )
106- default :
107- // VCS (GitHub) projects and anything else
149+ case project .Type == "vcs" || project .Type == "githubImport" :
108150 return deployVCS (c , client , project )
151+ default :
152+ return fmt .Errorf ("unsupported project type %q — please deploy from the dashboard" , project .Type )
109153 }
110154 },
111155 }
112156}
113157
114- // deployVCS triggers a deployment from the latest commit on a branch.
158+ // deployVCS triggers a new deployment from the latest commit, optionally on a specific branch.
115159func deployVCS (c * cli.Context , client * api.APIClient , project * api.Project ) error {
116160 branch := c .String ("branch" )
117161
162+ if branch == "" && terminal .IsInteractive () {
163+ result , err := pterm .DefaultInteractiveTextInput .
164+ WithDefaultText ("Branch to deploy (leave empty for default branch)" ).
165+ Show ()
166+ if err != nil {
167+ return err
168+ }
169+ branch = strings .TrimSpace (result )
170+ }
171+
118172 branchLabel := "default branch"
119173 if branch != "" {
120174 branchLabel = branch
@@ -170,7 +224,9 @@ func deployUpload(c *cli.Context, client *api.APIClient, project *api.Project) e
170224 spinner .UpdateText ("Uploading..." )
171225
172226 // Close before uploading so the file is flushed
173- zipFile .Close () //nolint:errcheck
227+ if err := zipFile .Close (); err != nil { //nolint:govet
228+ return fmt .Errorf ("could not flush deployment package: %w" , err )
229+ }
174230
175231 deployment , err := client .UploadDeploymentZip (project .ID , zipFile .Name ())
176232 if err != nil {
@@ -211,57 +267,123 @@ func deployImage(c *cli.Context, client *api.APIClient, project *api.Project) er
211267 return waitForDeployment (client , project .ID , deployment )
212268}
213269
214- // waitForDeployment polls until the deployment succeeds, fails, or times out .
270+ // waitForDeployment streams build logs while building, then runtime logs on success .
215271func waitForDeployment (client * api.APIClient , projectID string , deployment * api.Deployment ) error {
216- spinner , _ := pterm . DefaultSpinner . Start ( fmt .Sprintf ( "Deploying (v%d)..." , deployment . VersionNumber ) )
272+ fmt .Println ( )
217273
218- timeout := time .After (5 * time .Minute )
219- ticker := time .NewTicker (3 * time .Second )
274+ timeout := time .After (10 * time .Minute )
275+ ticker := time .NewTicker (2 * time .Second )
220276 defer ticker .Stop ()
221277
278+ lastBuildLine := 0
279+ headerPrinted := false
280+
222281 for {
223282 select {
224283 case <- timeout :
225- spinner .Warning ("Deployment is still in progress — check back with: createos deployments logs" )
284+ fmt .Println ()
285+ pterm .Warning .Println ("Deployment is still in progress — check back with: createos deployments build-logs" )
226286 return nil
227287 case <- ticker .C :
228288 d , err := client .GetDeployment (projectID , deployment .ID )
229289 if err != nil {
230290 continue // transient error, keep polling
231291 }
232292
293+ if ! headerPrinted && d .VersionNumber > 0 {
294+ fmt .Printf (" Building v%d...\n \n " , d .VersionNumber )
295+ headerPrinted = true
296+ }
297+
298+ // Stream new build log lines
299+ buildLogs , err := client .GetDeploymentBuildLogs (projectID , deployment .ID )
300+ if err == nil {
301+ for _ , e := range buildLogs {
302+ if e .LineNumber > lastBuildLine {
303+ fmt .Println (e .Log )
304+ lastBuildLine = e .LineNumber
305+ }
306+ }
307+ }
308+
233309 switch d .Status {
234310 case "successful" , "running" , "active" , "deployed" :
235- spinner .Success (fmt .Sprintf ("Deployed (v%d)" , d .VersionNumber ))
311+ fmt .Println ()
312+ pterm .Success .Printf ("Deployed (v%d)\n " , d .VersionNumber )
236313 fmt .Println ()
237314 if d .Extra .Endpoint != "" {
238315 url := d .Extra .Endpoint
239316 if ! strings .HasPrefix (url , "http" ) {
240317 url = "https://" + url
241318 }
242319 pterm .Info .Printf ("Live at: %s\n " , url )
320+ fmt .Println ()
243321 }
244- fmt .Println ()
245- pterm .Println (pterm .Gray (" View logs: createos deployments logs" ))
246- pterm .Println (pterm .Gray (" Redeploy: createos deploy" ))
322+ // Stream initial runtime logs
323+ streamRuntimeLogs (client , projectID , deployment .ID )
247324 return nil
248325 case "failed" , "error" , "cancelled" :
249- spinner .Fail (fmt .Sprintf ("Deployment failed (v%d)" , d .VersionNumber ))
250326 fmt .Println ()
251- pterm .Println ( pterm . Gray ( " View build logs: createos deployments build-logs" ) )
327+ pterm .Error . Printf ( "Deployment failed (v%d) \n " , d . VersionNumber )
252328 return fmt .Errorf ("deployment %s failed with status: %s" , d .ID , d .Status )
253- default :
254- spinner .UpdateText (fmt .Sprintf ("Deploying (v%d) — %s..." , d .VersionNumber , d .Status ))
255329 }
256330 }
257331 }
258332}
259333
260- // createZip creates a zip archive of the directory, excluding default ignore patterns.
334+ // streamRuntimeLogs fetches and prints runtime logs after a successful deployment.
335+ func streamRuntimeLogs (client * api.APIClient , projectID , deploymentID string ) {
336+ logs , err := client .GetDeploymentLogs (projectID , deploymentID )
337+ if err != nil || logs == "" {
338+ pterm .Println (pterm .Gray (" View logs: createos deployments logs" ))
339+ pterm .Println (pterm .Gray (" Redeploy: createos deploy" ))
340+ return
341+ }
342+ fmt .Println (" Runtime logs:" )
343+ fmt .Println ()
344+ for _ , line := range strings .Split (strings .TrimRight (logs , "\n " ), "\n " ) {
345+ fmt .Println (" " + line )
346+ }
347+ fmt .Println ()
348+ pterm .Println (pterm .Gray (" Follow logs: createos deployments logs --follow" ))
349+ pterm .Println (pterm .Gray (" Redeploy: createos deploy" ))
350+ }
351+
352+ // loadGitignorePatterns reads .gitignore from srcDir and returns usable patterns.
353+ func loadGitignorePatterns (srcDir string ) []string {
354+ data , err := os .ReadFile (filepath .Join (srcDir , ".gitignore" )) //nolint:gosec
355+ if err != nil {
356+ return nil
357+ }
358+ var patterns []string
359+ for _ , line := range strings .Split (string (data ), "\n " ) {
360+ line = strings .TrimSpace (line )
361+ // Skip comments and empty lines
362+ if line == "" || strings .HasPrefix (line , "#" ) {
363+ continue
364+ }
365+ // Skip negations — we don't support re-including files
366+ if strings .HasPrefix (line , "!" ) {
367+ continue
368+ }
369+ // Strip trailing slash (directory marker) — we handle dirs via SkipDir
370+ line = strings .TrimSuffix (line , "/" )
371+ // Strip leading slash (root-anchored) — use basename matching
372+ line = strings .TrimPrefix (line , "/" )
373+ if line != "" {
374+ patterns = append (patterns , line )
375+ }
376+ }
377+ return patterns
378+ }
379+
380+ // createZip creates a zip archive of the directory, excluding default ignore patterns and .gitignore rules.
261381func createZip (w io.Writer , srcDir string ) error {
262382 zw := zip .NewWriter (w )
263383 defer zw .Close () //nolint:errcheck
264384
385+ ignorePatterns := append (defaultIgnorePatterns , loadGitignorePatterns (srcDir )... ) //nolint:gocritic
386+
265387 return filepath .Walk (srcDir , func (path string , info os.FileInfo , err error ) error {
266388 if err != nil {
267389 return err
@@ -277,10 +399,12 @@ func createZip(w io.Writer, srcDir string) error {
277399 return nil
278400 }
279401
280- // Check ignore patterns
402+ // Check ignore patterns against basename and full relative path
281403 baseName := filepath .Base (relPath )
282- for _ , pattern := range defaultIgnorePatterns {
283- if matched , _ := filepath .Match (pattern , baseName ); matched {
404+ for _ , pattern := range ignorePatterns {
405+ matchedBase , _ := filepath .Match (pattern , baseName )
406+ matchedRel , _ := filepath .Match (pattern , filepath .ToSlash (relPath ))
407+ if matchedBase || matchedRel {
284408 if info .IsDir () {
285409 return filepath .SkipDir
286410 }
0 commit comments