Skip to content

Commit 1412d63

Browse files
feat: addressed suggested changes
1 parent 7ef9004 commit 1412d63

5 files changed

Lines changed: 190 additions & 61 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
.claude
22
.createos.json
33
.env.*
4+
cos
5+
build.sh

cmd/deploy/deploy.go

Lines changed: 153 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,60 @@ const maxZipSize = 50 * 1024 * 1024 // 50 MB
2222

2323
// defaultIgnorePatterns are files/dirs excluded when zipping for upload.
2424
var 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.
115159
func 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.
215271
func 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.
261381
func 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
}

cmd/deployments/retrigger.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func newDeploymentRetriggerCommand() *cli.Command {
2828
return err
2929
}
3030

31-
if err := client.RetriggerDeployment(projectID, deploymentID); err != nil {
31+
if _, err := client.RetriggerDeployment(projectID, deploymentID, ""); err != nil {
3232
return err
3333
}
3434

cmd/root/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func NewApp() *cli.App {
8383
}
8484

8585
cmd := c.Args().First()
86-
if cmd == "" || cmd == "login" || cmd == "logout" || cmd == "version" || cmd == "ask" || cmd == "init" || cmd == "upgrade" {
86+
if cmd == "" || cmd == "login" || cmd == "logout" || cmd == "version" || cmd == "ask" || cmd == "upgrade" {
8787
return nil
8888
}
8989

internal/api/methods.go

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -258,17 +258,42 @@ func (c *APIClient) GetDeploymentBuildLogs(projectID, deploymentID string) ([]Bu
258258
return result.Data, nil
259259
}
260260

261-
// RetriggerDeployment triggers a new deployment run.
262-
func (c *APIClient) RetriggerDeployment(projectID, deploymentID string) error {
263-
resp, err := c.Client.R().
264-
Post("/v1/projects/" + projectID + "/deployments/" + deploymentID + "/retrigger")
261+
// RetriggerDeployment triggers a new deployment run and returns the new deployment.
262+
// branch is optional — pass empty string to use the branch from the existing deployment.
263+
func (c *APIClient) RetriggerDeployment(projectID, deploymentID, branch string) (*Deployment, error) {
264+
var result Response[Deployment]
265+
req := c.Client.R().SetResult(&result)
266+
if branch != "" {
267+
req = req.SetQueryParam("branch", branch)
268+
}
269+
resp, err := req.Post("/v1/projects/" + projectID + "/deployments/" + deploymentID + "/retrigger")
265270
if err != nil {
266-
return err
271+
return nil, err
267272
}
268273
if resp.IsError() {
269-
return ParseAPIError(resp.StatusCode(), resp.Body())
274+
return nil, ParseAPIError(resp.StatusCode(), resp.Body())
270275
}
271-
return nil
276+
return &result.Data, nil
277+
}
278+
279+
// TriggerLatestDeployment triggers a new deployment from the latest commit.
280+
// branch is optional — passed as a query param; omit to use the project's default branch.
281+
func (c *APIClient) TriggerLatestDeployment(projectID, branch string) (*Deployment, error) {
282+
var result Response[struct {
283+
ID string `json:"id"`
284+
}]
285+
req := c.Client.R().SetResult(&result)
286+
if branch != "" {
287+
req = req.SetQueryParam("branch", branch)
288+
}
289+
resp, err := req.Post("/v1/projects/" + projectID + "/trigger-latest")
290+
if err != nil {
291+
return nil, err
292+
}
293+
if resp.IsError() {
294+
return nil, ParseAPIError(resp.StatusCode(), resp.Body())
295+
}
296+
return c.GetDeployment(projectID, result.Data.ID)
272297
}
273298

274299
// CancelDeployment cancels a running deployment.
@@ -676,36 +701,14 @@ func (c *APIClient) CreateDeployment(projectID string, body map[string]any) (*De
676701
return &result.Data, nil
677702
}
678703

679-
// TriggerLatestDeployment triggers a new deployment from the latest commit on
680-
// the given branch. If branch is empty the repository's default branch is used.
681-
// Only available for VCS projects.
682-
func (c *APIClient) TriggerLatestDeployment(projectID, branch string) (*Deployment, error) {
683-
body := map[string]any{}
684-
if branch != "" {
685-
body["branch"] = branch
686-
}
687-
var result Response[Deployment]
688-
resp, err := c.Client.R().
689-
SetResult(&result).
690-
SetBody(body).
691-
Post("/v1/projects/" + projectID + "/deployments/trigger-latest")
692-
if err != nil {
693-
return nil, err
694-
}
695-
if resp.IsError() {
696-
return nil, ParseAPIError(resp.StatusCode(), resp.Body())
697-
}
698-
return &result.Data, nil
699-
}
700-
701704
// UploadDeploymentZip creates a new deployment by uploading a ZIP file.
702705
// Only available for upload-type projects.
703706
func (c *APIClient) UploadDeploymentZip(projectID, zipPath string) (*Deployment, error) {
704707
var result Response[Deployment]
705708
resp, err := c.Client.R().
706709
SetResult(&result).
707710
SetFile("file", zipPath).
708-
Post("/v1/projects/" + projectID + "/deployments/upload/zip")
711+
Put("/v1/projects/" + projectID + "/deployments/upload-zip")
709712
if err != nil {
710713
return nil, err
711714
}

0 commit comments

Comments
 (0)