Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions cmd/commands/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import (
"github.com/Drafteame/draft/internal/pkg/log"
)

const forceFlagUsage = "Bypass .deployignore checks and deploy regardless"

// NewFuncCmd returns a deploy function command for the given environment.
func NewFuncCmd(env deployaction.EnvConfig) *cobra.Command {
stage := env.Stage()
return &cobra.Command{
var force bool
cmd := &cobra.Command{
Use: fmt.Sprintf("deploy:func:%s <service|path> <function-name> [function-name...]", stage),
Short: fmt.Sprintf("Deploy one or more Lambda functions to %s", stage),
Long: fmt.Sprintf(`Package and deploy one or more Lambda functions to %s.
Expand All @@ -24,22 +27,25 @@ All functions must belong to the same service; the service is packaged once.
Examples:
draft deploy:func:%s gamestats storegamestats
draft deploy:func:%s gamestats storegamestats otherlambda
draft deploy:func:%s services/gamestats storegamestats`, stage, stage, stage, stage),
draft deploy:func:%s services/gamestats`, stage, stage, stage, stage),
Args: cobra.MinimumNArgs(2),
Run: func(c *cobra.Command, args []string) {
common.ChDir(c)

if err := deployaction.DeployFunction(env, args[0], args[1:]); err != nil {
if err := deployaction.DeployFunction(env, args[0], args[1:], force); err != nil {
log.Exitf(1, "deploy:func:%s failed: %v", stage, err)
}
},
}
cmd.Flags().BoolVar(&force, "force", false, forceFlagUsage)
return cmd
}

// NewServiceCmd returns a deploy service command for the given environment.
func NewServiceCmd(env deployaction.EnvConfig) *cobra.Command {
stage := env.Stage()
return &cobra.Command{
var force bool
cmd := &cobra.Command{
Use: fmt.Sprintf("deploy:%s <service|path> [service|path...]", stage),
Short: fmt.Sprintf("Deploy one or more services to %s", stage),
Long: fmt.Sprintf(`Deploy Serverless services to %s without changing directories.
Expand All @@ -56,7 +62,7 @@ Examples:
Run: func(c *cobra.Command, args []string) {
common.ChDir(c)

results := deployaction.DeployService(env, args)
results := deployaction.DeployService(env, args, force)

hasError := false
if len(results) > 1 {
Expand All @@ -76,4 +82,6 @@ Examples:
}
},
}
cmd.Flags().BoolVar(&force, "force", false, forceFlagUsage)
return cmd
}
54 changes: 54 additions & 0 deletions internal/actions/deploy/deployignore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package deploy

import (
"fmt"
"path/filepath"
"strings"

"github.com/Drafteame/draft/internal/pkg/files"
)

// shouldSkipForStage decides whether a service should be skipped for the given
// stage based on its .deployignore file. The semantics mirror
// scripts/deploy/check_deploy_ignore.sh in the api repo:
//
// - File missing -> deploy (skip=false)
// - File present but empty/whitespace -> skip every stage
// - File with content -> one stage per line; skip if the
// current stage matches any non-empty trimmed line (case-insensitive).
//
// An empty stage is treated as "unknown" and never matches a listed stage; an
// empty file still skips everything in that case (preserves the legacy blunt
// behaviour for callers that do not yet pass a stage).
func shouldSkipForStage(absPath, stage string) (skip bool, reason string, err error) {
ignorePath := filepath.Join(absPath, ".deployignore")
if !files.Exists(ignorePath) {
return false, "", nil
}

content, err := files.Read(ignorePath)
if err != nil {
return false, "", fmt.Errorf("failed to read %s: %w", ignorePath, err)
}

stages := make([]string, 0)
for _, line := range strings.Split(string(content), "\n") {
trimmed := strings.TrimSpace(line)
if trimmed != "" {
stages = append(stages, trimmed)
}
}

if len(stages) == 0 {
return true, ".deployignore is empty (skipping all stages)", nil
}

stageLC := strings.ToLower(stage)
for _, s := range stages {
if strings.ToLower(s) == stageLC {
return true, fmt.Sprintf("stage %q listed in .deployignore", stage), nil
}
}

return false, "", nil
}
21 changes: 14 additions & 7 deletions internal/actions/deploy/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,30 @@ const deployRegion = "us-east-2"

// DeployFunction packages and deploys one or more Lambda functions using the given env config.
// serviceArg can be a service name or a path. All functions must belong to the same service.
func DeployFunction(env EnvConfig, serviceArg string, functionNames []string) error {
// If force is true, .deployignore is ignored.
func DeployFunction(env EnvConfig, serviceArg string, functionNames []string, force bool) error {
absPath, err := resolveService(serviceArg)
if err != nil {
return err
}

skip, err := validateServiceDir(absPath)
if err != nil {
if err := validateServiceDir(absPath); err != nil {
return err
}
if skip {
log.Warnf("Skipping %s: .deployignore found", absPath)
return nil
}

stage := env.Stage()

if !force {
skip, reason, err := shouldSkipForStage(absPath, stage)
if err != nil {
return err
}
if skip {
log.Warnf("Skipping %s: %s (use --force to override)", absPath, reason)
return nil
}
}

log.Info("Fetching AWS Account ID...")
accountID, err := aws.GetAccountID(env.Profile)
if err != nil {
Expand Down
43 changes: 24 additions & 19 deletions internal/actions/deploy/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ type DeployResult struct {
}

// DeployService deploys one or more services (by name or path) using the given env config.
func DeployService(env EnvConfig, args []string) []DeployResult {
// If force is true, .deployignore is ignored.
func DeployService(env EnvConfig, args []string, force bool) []DeployResult {
results := make([]DeployResult, 0, len(args))

log.Info("Fetching AWS Account ID...")
Expand All @@ -40,21 +41,27 @@ func DeployService(env EnvConfig, args []string) []DeployResult {
continue
}

err = deployServiceToDir(env, absPath, accountID)
err = deployServiceToDir(env, absPath, accountID, force)
results = append(results, DeployResult{Name: arg, Err: err})
}

return results
}

func deployServiceToDir(env EnvConfig, absPath, accountID string) error {
skip, err := validateServiceDir(absPath)
if err != nil {
func deployServiceToDir(env EnvConfig, absPath, accountID string, force bool) error {
if err := validateServiceDir(absPath); err != nil {
return err
}
if skip {
log.Warnf("Skipping %s: .deployignore found", absPath)
return nil

if !force {
skip, reason, err := shouldSkipForStage(absPath, env.Stage())
if err != nil {
return err
}
if skip {
log.Warnf("Skipping %s: %s (use --force to override)", absPath, reason)
return nil
}
}

stage := env.Stage()
Expand All @@ -73,24 +80,22 @@ func deployServiceToDir(env EnvConfig, absPath, accountID string) error {
absPath, stage, accountID, slsParams, syncSecretsDry,
)

_, err = exec.Command(script, exec.WithStdout(os.Stdout), exec.WithStderr(os.Stderr))
if err != nil {
if _, err := exec.Command(script, exec.WithStdout(os.Stdout), exec.WithStderr(os.Stderr)); err != nil {
return fmt.Errorf("deploy failed: %w", err)
}

log.Successf("✓ Deployed: %s", absPath)
return nil
}

// validateServiceDir checks whether a directory is eligible for deployment.
// Returns (true, nil) if the directory should be skipped (.deployignore present),
// (false, err) if serverless.yml is missing, or (false, nil) if ready to deploy.
func validateServiceDir(absPath string) (skip bool, err error) {
if files.Exists(filepath.Join(absPath, ".deployignore")) {
return true, nil
}
// validateServiceDir asserts that absPath looks like a deployable serverless
// service. It returns nil when serverless.yml is present and a descriptive
// error otherwise. .deployignore handling is done separately by
// shouldSkipForStage so that --force can bypass the skip without bypassing
// this structural check.
func validateServiceDir(absPath string) error {
if !files.Exists(filepath.Join(absPath, "serverless.yml")) {
return false, fmt.Errorf("serverless.yml not found in %s", absPath)
return fmt.Errorf("serverless.yml not found in %s", absPath)
}
return false, nil
return nil
}