• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CyberDuck79 / duckfile / 17530388730

07 Sep 2025 03:21PM UTC coverage: 79.757% (+3.6%) from 76.188%
17530388730

Pull #63

github

CyberDuck79
test(coverage): attempt to exclude some file and function from coverage
Pull Request #63: feat(template): add submodules support with recursive fetch and tests

16 of 22 new or added lines in 4 files covered. (72.73%)

6 existing lines in 2 files now uncovered.

2427 of 3043 relevant lines covered (79.76%)

9.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.45
/cmd/duck/root.go
1
package main
2

3
import (
4
        "fmt"
5
        "os"
6
        "strings"
7

8
        "github.com/CyberDuck79/duckfile/internal/config"
9
        "github.com/CyberDuck79/duckfile/internal/log"
10
        "github.com/CyberDuck79/duckfile/internal/run"
11
        "github.com/spf13/cobra"
12
)
13

14
// test seam - updated to include security config
15
var runExec = func(cfg *config.DuckConf, targetName string, passthrough []string, securityCfg *config.SecurityConfig, trackCommitHashFlag *bool, autoUpdateOnChangeFlag *bool) error {
1✔
16
        return run.Exec(cfg, targetName, passthrough, securityCfg, trackCommitHashFlag, autoUpdateOnChangeFlag)
1✔
17
}
1✔
18

19
// Version is injected at build time with: go build -ldflags "-X main.Version=<version>"
20
// Defaults to "dev" when not set.
21
var Version = "dev"
22

23
// Global variable to store config file path from --config flag
24
var configPath string
25

26
var rootCmd = &cobra.Command{
27
        Use:   "duck [target] -- [target_args...]",
28
        Short: "Universal remote templating for DevOps tools",
29
        Long: `Duck fetches, renders, and executes remote Git templates with deterministic caching.
30

31
USAGE
32
  duck [target] [flags]                 Execute target (or default if none specified)
33
  duck [target] -- [args...] [flags]   Execute target and pass args to underlying tool
34
  duck [command] [flags]                Run a specific command
35

36
COMMANDS
37
  add         Add a new target to existing duck.yaml
38
  clean       Purge cached objects and per-target directories  
39
  init        Interactive wizard to create a duck.yaml
40
  list        List targets defined in duck.yaml
41
  sync        Sync templates into cache without executing
42
  version     Print duck version
43
  help        Help about any command
44

45
FLAGS
46
  -h, --help                     Show help for duck
47
  -c, --config string            Path to duck config file (default: auto-discover)
48
  --log-level string             Log level: error, warn, info, debug
49
  --track-commit-hash            Enable commit hash validation
50
  --no-track-commit-hash         Disable commit hash validation
51
  --auto-update-on-change        Auto-update when commit hash changes
52
  --no-auto-update-on-change     Disable auto-update on commit changes
53
  --allowed-hosts strings        Comma-separated allowed Git hosts
54
  --denied-hosts strings         Comma-separated denied Git hosts
55
  --strict-hosts                 Fail if no host restrictions configured
56

57
ENVIRONMENT VARIABLES
58
  DUCK_CONFIG                    Path to config file (overrides auto-discovery)
59
  DUCK_LOG_LEVEL                 Default log level
60
  DUCK_TRACK_COMMIT_HASH         Enable commit hash tracking (true/false)
61
  DUCK_AUTO_UPDATE_ON_CHANGE     Enable auto-update behavior (true/false)
62
  DUCK_ALLOWED_HOSTS             Comma-separated allowed hosts
63
  DUCK_DENIED_HOSTS              Comma-separated denied hosts
64
  DUCK_STRICT_MODE               Enable strict host validation (true/false)
65

66
EXAMPLES
67
  duck                           Execute default target
68
  duck build                     Execute 'build' target  
69
  duck --config prod.yaml build  Use custom config file
70
  duck test -- --verbose         Execute 'test' with args
71
  duck sync                      Sync all templates
72
  duck --log-level=debug build   Execute with debug logging
73

74
Use "duck [command] --help" for more information about a command.`,
75
        SilenceUsage:       true,
76
        SilenceErrors:      true,
77
        DisableFlagParsing: true, // Disable to handle custom parsing with -- separator
78
        Args:               cobra.ArbitraryArgs,
79
        RunE: func(cmd *cobra.Command, args []string) error {
32✔
80
                // Manual parsing for target and passthrough args due to -- separator
32✔
81
                var (
32✔
82
                        target             string
32✔
83
                        binArgs            []string
32✔
84
                        logLevel           string
32✔
85
                        trackCommitHash    *bool
32✔
86
                        autoUpdateOnChange *bool
32✔
87
                )
32✔
88

32✔
89
                // Find "--" separator
32✔
90
                sepIdx := -1
32✔
91
                for i, a := range args {
80✔
92
                        if a == "--" {
50✔
93
                                sepIdx = i
2✔
94
                                break
2✔
95
                        }
96
                }
97

98
                duckArgs := args
32✔
99
                if sepIdx != -1 {
34✔
100
                        duckArgs = args[:sepIdx]
2✔
101
                        binArgs = args[sepIdx+1:]
2✔
102
                }
2✔
103

104
                // Parse our custom flags from duckArgs
105
                allowedHosts := []string{}
32✔
106
                deniedHosts := []string{}
32✔
107
                strictMode := false
32✔
108

32✔
109
                for i := 0; i < len(duckArgs); i++ {
71✔
110
                        arg := duckArgs[i]
39✔
111
                        switch {
39✔
112
                        case strings.HasPrefix(arg, "--config="):
1✔
113
                                configPath = arg[len("--config="):]
1✔
114
                        case arg == "--config" && i+1 < len(duckArgs):
3✔
115
                                configPath = duckArgs[i+1]
3✔
116
                                i++ // skip next arg
3✔
117
                        case arg == "-c" && i+1 < len(duckArgs):
1✔
118
                                configPath = duckArgs[i+1]
1✔
119
                                i++ // skip next arg
1✔
120
                        case strings.HasPrefix(arg, "--log-level="):
1✔
121
                                logLevel = arg[len("--log-level="):]
1✔
122
                        case arg == "--log-level" && i+1 < len(duckArgs):
1✔
123
                                logLevel = duckArgs[i+1]
1✔
124
                                i++ // skip next arg
1✔
125
                        case arg == "--track-commit-hash":
8✔
126
                                trackTrue := true
8✔
127
                                trackCommitHash = &trackTrue
8✔
128
                        case arg == "--no-track-commit-hash":
2✔
129
                                trackFalse := false
2✔
130
                                trackCommitHash = &trackFalse
2✔
131
                        case arg == "--auto-update-on-change":
3✔
132
                                updateTrue := true
3✔
133
                                autoUpdateOnChange = &updateTrue
3✔
134
                        case arg == "--no-auto-update-on-change":
3✔
135
                                updateFalse := false
3✔
136
                                autoUpdateOnChange = &updateFalse
3✔
137
                        case strings.HasPrefix(arg, "--allowed-hosts="):
1✔
138
                                hostStr := arg[len("--allowed-hosts="):]
1✔
139
                                allowedHosts = strings.Split(hostStr, ",")
1✔
140
                        case arg == "--allowed-hosts" && i+1 < len(duckArgs):
1✔
141
                                hostStr := duckArgs[i+1]
1✔
142
                                allowedHosts = strings.Split(hostStr, ",")
1✔
143
                                i++ // skip next arg
1✔
144
                        case strings.HasPrefix(arg, "--denied-hosts="):
1✔
145
                                hostStr := arg[len("--denied-hosts="):]
1✔
146
                                deniedHosts = strings.Split(hostStr, ",")
1✔
147
                        case arg == "--denied-hosts" && i+1 < len(duckArgs):
1✔
148
                                hostStr := duckArgs[i+1]
1✔
149
                                deniedHosts = strings.Split(hostStr, ",")
1✔
150
                                i++ // skip next arg
1✔
151
                        case arg == "--strict-hosts":
1✔
152
                                strictMode = true
1✔
153
                        case arg == "-h" || arg == "--help":
2✔
154
                                return cmd.Help()
2✔
155
                        case !strings.HasPrefix(arg, "-"):
4✔
156
                                // This is the target name
4✔
157
                                if target == "" {
8✔
158
                                        target = arg
4✔
159
                                }
4✔
160
                        }
161
                }
162

163
                // 1. load config
164
                cfg, err := loadConfig()
30✔
165
                if err != nil {
32✔
166
                        return err
2✔
167
                }
2✔
168

169
                // 2. Resolve and set log level
170
                logLevelStr := config.ResolveLogLevel(logLevel, cfg)
28✔
171
                effectiveLogLevel, err := log.ParseLevel(logLevelStr)
28✔
172
                if err != nil {
28✔
173
                        return fmt.Errorf("invalid log level: %w", err)
×
174
                }
×
175
                log.SetLevel(effectiveLogLevel)
28✔
176

28✔
177
                // 3. If no target or explicit legacy "default", translate to configured default key
28✔
178
                if target == "" || target == "default" {
52✔
179
                        target = cfg.Default
24✔
180
                }
24✔
181

182
                // 4. Build security configuration with enhanced precedence system
183
                securityCfg, err := config.BuildSecurityConfigWithPrecedence(allowedHosts, deniedHosts, strictMode)
28✔
184
                if err != nil {
28✔
185
                        return fmt.Errorf("failed to build security configuration: %w", err)
×
186
                }
×
187

188
                // 5. execute with security validation
189
                return runExec(cfg, target, binArgs, securityCfg, trackCommitHash, autoUpdateOnChange)
28✔
190
        },
191
}
192

193
func init() {
1✔
194
        rootCmd.Version = Version
1✔
195

1✔
196
        // Add persistent flag available to all subcommands
1✔
197
        rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "",
1✔
198
                "Path to duck config file (default: auto-discover duck.yaml, duck.yml, .duck.yaml, .duck.yml)")
1✔
199

1✔
200
        // Set custom help template only for root command
1✔
201
        rootCmd.SetHelpTemplate(`{{.Long}}
1✔
202
`)
1✔
203
}
1✔
204

205
func main() {
×
206
        if err := rootCmd.Execute(); err != nil {
×
207
                fmt.Fprintln(os.Stderr, "error:", err)
×
208
                os.Exit(1)
×
209
        }
×
210
}
211

212
func loadConfig() (*config.DuckConf, error) {
59✔
213
        // Load .env file first (before any config loading)
59✔
214
        // This allows .env variables to be used in config resolution
59✔
215
        if err := config.LoadEnvFileIfExists(log.Infof); err != nil {
59✔
216
                return nil, fmt.Errorf("environment setup failed: %w", err)
×
217
        }
×
218

219
        var cfgFile string
59✔
220

59✔
221
        // Priority 1: --config flag (highest precedence)
59✔
222
        if configPath != "" {
65✔
223
                cfgFile = configPath
6✔
224
                // Validate file exists when explicitly specified
6✔
225
                if _, err := os.Stat(cfgFile); err != nil {
7✔
226
                        return nil, fmt.Errorf("config file %q not found: %w", cfgFile, err)
1✔
227
                }
1✔
228
        } else {
53✔
229
                // Priority 2: DUCK_CONFIG environment variable
53✔
230
                if envConfigPath := os.Getenv("DUCK_CONFIG"); envConfigPath != "" {
55✔
231
                        cfgFile = envConfigPath
2✔
232
                        // Validate file exists when explicitly specified
2✔
233
                        if _, err := os.Stat(cfgFile); err != nil {
3✔
234
                                return nil, fmt.Errorf("config file from DUCK_CONFIG %q not found: %w", cfgFile, err)
1✔
235
                        }
1✔
236
                } else {
51✔
237
                        // Priority 3: Auto-discovery (lowest precedence)
51✔
238
                        configFiles := []string{"duck.yaml", "duck.yml", ".duck.yaml", ".duck.yml"}
51✔
239
                        for _, f := range configFiles {
102✔
240
                                if _, err := os.Stat(f); err == nil {
102✔
241
                                        cfgFile = f
51✔
242
                                        break
51✔
243
                                }
244
                        }
245
                        if cfgFile == "" {
51✔
UNCOV
246
                                return nil, fmt.Errorf("no config file found (tried: %v). Use --config to specify a custom path", configFiles)
×
UNCOV
247
                        }
×
248
                }
249
        }
250

251
        log.Debugf("Loading configuration from %s", cfgFile)
57✔
252
        // Load config from the determined file path
57✔
253
        cfg, err := config.Load(cfgFile)
57✔
254
        if err != nil {
57✔
UNCOV
255
                return nil, fmt.Errorf("failed to load config from %q: %w", cfgFile, err)
×
UNCOV
256
        }
×
257
        log.Debugf("Successfully loaded configuration with %d targets", len(cfg.Targets))
57✔
258

57✔
259
        return cfg, nil
57✔
260
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc