V3 candidate#167
Conversation
This commit replaces the urfave/cli v1 library with the latest versions
of spf13/cobra and spf13/viper CLI frameworks while maintaining full
backwards compatibility with existing .ahoy.yml configuration files.
## Changes
### Core Implementation
- Replaced urfave/cli with cobra/viper throughout the codebase
- Updated ahoy.go:
- Converted cli.App to cobra.Command structure
- Migrated cli.Command to cobra.Command with proper field mappings:
* cmd.Name → cmd.Use
* cmd.Usage → cmd.Short
* cmd.Description → cmd.Long
* cmd.SkipFlagParsing → cmd.DisableFlagParsing
* cmd.Action → cmd.Run
- Implemented custom help template to maintain alias display functionality
- Added viper for configuration management with AHOY_ prefix
- Updated flag.go:
- Removed urfave/cli flag definitions
- Implemented initFlags to parse flags before cobra initialization
- Added viper integration for flag values
### Bug Fixes
- Fixed critical flag parsing issue where pflag was resetting variables
to default values. Solution: save parsed values before creating cobra
flags and use them as defaults.
### Testing
- Updated all test files to work with cobra:
- ahoy_test.go: Updated appRun helper and command structure
- cli_parsing_test.go: Converted to use cobra flag system
- description_test.go: Updated to use cobra command methods
- windows_test.go: Updated command name/usage accessors
- All 70+ tests pass successfully
### Dependencies
- Added: github.com/spf13/cobra v1.10.1
- Added: github.com/spf13/viper v1.21.0
- Added: github.com/spf13/pflag v1.0.10
- Removed: github.com/urfave/cli v1.22.9
- Updated vendor directory with new dependencies
## Backwards Compatibility
✅ All existing .ahoy.yml files work without modification
✅ All command-line flags function identically (-f, -v, etc.)
✅ Command aliases work correctly
✅ Environment variables with AHOY_ prefix supported
✅ Subcommand imports and overrides work as expected
✅ Custom entrypoints preserved
✅ Bash completion generation available via cobra's built-in system
## Testing Performed
- Unit tests: 70+ tests pass (0 failures)
- Integration: Tested with multiple .ahoy.yml configurations
- Aliases: Verified alias functionality works correctly
- Flags: Confirmed -f and -v flags operate as expected
- Completion: Bash completion script generation tested
## Migration Notes
The migration maintains the same user-facing API and behavior while
benefiting from cobra's more modern architecture and active maintenance.
No changes are required for existing ahoy users.
Replace loop-based append with direct slice append using spread operator:
- Before: for _, alias := range command.Aliases { completions = append(completions, alias) }
- After: completions = append(completions, command.Aliases...)
This is more idiomatic Go and addresses staticcheck warning S1011.
Major fixes: - Remove DisableFlagParsing in favor of FParseErrWhitelist to allow unknown flags while still parsing persistent flags correctly - Add flag normalization in initFlags() to support both single and double dash (--version and -version now both work) - Update BeforeCommand to exit cleanly for version and help flags - Add custom error handling in main() for unknown commands - Update NoArgsAction to properly format error messages Test results: 83/90 Bats tests passing (92% pass rate, up from 78/90 initially) Remaining issues are minor formatting differences in error messages and help output.
- Add global variables versionFlagSet and helpFlagSet to track flags parsed in initFlags() - Check these flags in main() after setupApp() to handle single-dash versions (-version, -help) that cobra doesn't natively support - Simplify BeforeCommand to avoid duplicate checks This fixes test compatibility where both -version and --version should work identically and exit with status 0. Test results: 84/90 Bats tests now passing (93.3%)
This commit fixes flag handling to support both single-dash and double-dash variants (e.g., -version and --version), improves bash completion flag handling, and updates test expectations to match the new output format. Changes: - Add bashCompletionFlagSet variable to properly track --generate-bash-completion flag - Handle bash completion flag in main() to exit with code 0 after printing completions - Update test expectations in tests/no-ahoy-file.bats to handle cosmetic output differences - All 90 Bats integration tests now pass
Migrate CLI framework from urfave/cli to cobra and viper
Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
Move v3 code to root, restore v2 code in subdirectory for future older version support
Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
Remove inline long descriptions from the command listing to keep it concise. Remove the redundant static ALIASES section since aliases are already shown inline next to each command name. Add a hint directing users to 'ahoy <command> --help' for detailed information. Add trimSpace template function for cleaner rendering.
Add commandHelpFunc that displays a NAME, DESCRIPTION (when present), USAGE, COMMANDS (for import subcommands), and ALIASES section when users run 'ahoy <command> --help'. Wire it into each command created by getCommands(). This completes the two-tier help system where the main listing stays concise and detailed help is available per command.
Update descriptions.bats to use looser matching that is resilient to tabwriter spacing changes. Add new tests for per-command help: description shown in DESCRIPTION section, multiline descriptions preserved, DESCRIPTION omitted when empty, aliases shown in per-command help. Remove redundant [ Aliases: ... ] assertion from command-aliases.bats since aliases are now shown inline only.
Add two tests suggested by CodeRabbit PR review on #164: - Verify 'ahoy <cmd> --help' shows help and does not execute the underlying command. - Verify '--help' after the '--' separator is passed through to the underlying command rather than being intercepted.
Add expandPath() helper to handle tilde expansion, absolute, and relative paths consistently. Update getSubCommands() and getCommands() to use expandPath() instead of inline prefix checks. Add simulateVersion global variable for use by the upcoming config validation system. Also improve getSubCommands() to log errors when an imported config file fails to parse, rather than silently discarding the error.
Port the schema validation system from the schema-validation branch. Includes ValidateConfig(), RunConfigValidate(), PrintConfigReport(), and supporting helpers for version comparison, feature support checking, and environment/import file status reporting. Key v3 adaptations: - Removed ValidateOptions struct (validation only runs via 'ahoy config validate') - Simplified RunConfigValidate() to accept configFile string only - Updated getConfig() calls to match v3 signature - Uses existing expandPath() helper added in previous commit
Port config_init.go from schema-validation branch, replacing the wget shell command with a native Go HTTP client using net/http. This removes the dependency on wget being installed on the host system. Introduces RunConfigInit() and downloadFile() as testable functions, and adapts initCommandAction() to use Cobra's command handler signature.
Introduce 'ahoy config' as a top-level command group containing: - 'ahoy config validate' — runs comprehensive config diagnostics - 'ahoy config init' — downloads an example .ahoy.yml (same as ahoy init) The legacy 'ahoy init' command is kept for backwards compatibility but now prints a deprecation notice directing users to 'ahoy config init'. The initCommandAction handler from config_init.go is now shared between both the deprecated 'ahoy init' and the new 'ahoy config init'. Also adds a hint to the main help output: "Run 'ahoy config validate' to check your configuration for issues."
Register --simulate-version as a hidden persistent flag in both the Cobra root command and the pre-parser (flag.go). When set, GetAhoyVersion() returns this value instead of the real version, allowing the config validation system to be tested against older Ahoy version behaviour without rebuilding the binary.
When a non-optional command has missing imports, the error message now lists the specific missing files by name and suggests solutions including 'ahoy config validate' for further diagnostics. When an optional import command is run on an Ahoy version that doesn't support the optional imports feature, a version-specific error message is shown with upgrade instructions. Update missing-cmd.bats to match the new enhanced error message format.
Add config_validation_test.go covering RunConfigValidate(), generateRecommendations(), checkEnvironmentFiles(), checkImportFiles(), compareVersions(), and VersionSupports(). Add config_init_test.go covering InitArgs struct, downloadFile() error handling, and fileExists() helper. Add TestExpandPath() to ahoy_test.go covering absolute, tilde, and relative paths. Add required test fixtures: testdata/invalid-yaml.ahoy.yml, testdata/with-imports.ahoy.yml, testdata/.env.test.
Add config-validate.bats with tests for: - Help output for ahoy config validate - Warning when no config file exists - Success with valid configuration - Detection of invalid YAML syntax - Detection of wrong API version - Environment file status reporting - Import file status reporting - Using -f flag to specify config file - --simulate-version flag for version testing Add config-init.bats with tests for: - Help output for ahoy config init - Downloading example config file - Prompt when existing .ahoy.yml found - Overwriting when user confirms - --force flag for non-interactive overwrite - Deprecation notice for legacy 'ahoy init' Also add FLAGS section to commandHelpFunc template so local flags (e.g. --force) are visible in per-command help output. Total BATS tests: 112 (up from 97).
Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
- fileExists(): return false on any os.Stat error, not just IsNotExist.
A non-IsNotExist error (EACCES, EIO) left info nil, causing a panic
on the info.IsDir() call.
- main(): check the error return from os.Pipe(). On file-descriptor
exhaustion the pipe returns nil handles; setting os.Stderr to a nil
*os.File causes any subsequent write to panic. Fall back to executing
without stderr capture when pipe creation fails.
- downloadFile(): write to a .tmp file and atomically rename to the
destination only after a successful, fully-flushed write. Previously
a mid-stream failure left a partial .ahoy.yml on disk, which would
appear to fileExists() as a valid file on the next run. Close() is
now called explicitly (not via defer) so buffered write errors on
NFS/SMB/Docker bind mounts are caught and returned rather than
silently swallowed.
- NoArgsAction(): collapse identical if/else branches and remove the
misleading comment "cobra will handle the unknown command error" —
both branches called logger("fatal") identically; cobra handled
nothing.
- getCommands() run closure: remove the self-contradicting debug
comment ("if we don't add an item... actually it's not!") left over
from a debugging session. Retain only the accurate bash $0/$@ note.
|
@CodeRabbit help |
ChatThere are 3 ways to chat with CodeRabbit:
CodeRabbit commands
Other keywords and placeholders
CodeRabbit configuration file (
|
|
@coderabbitai full review I've changed the configuration so you should ignore all the files in the root vendor/ directory now, which should reduce the file changes down to a manageable level. |
|
✅ Actions performedFull review triggered. |
Added BATS coverage for verbose flag positions and arg passthru.
…o subprocesses Sets AHOY_CMD to the resolved path of the running ahoy binary and AHOY_COMMAND_NAME to the name of the invoked command, making both available to every subprocess ahoy runs. Adds a testdata fixture and BATS integration tests to verify the behaviour.
- $AHOY_CMD contains a path to the actual ahoy executable so it can be used for substitution for running ahoy-driven subcommands with the exact same binary. - $AHOY_COMMAND_NAME contains the name of the ahoy command (within the .ahoy.yml file) that is being run, and therefore allows the subprocess to know it is being run by an ahoy parent process. Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
…bles into subprocesses" This reverts commit 1f78216.
… files... ... don't override the injected runtime values. In Go's os/exec, the last occurrence of a duplicate environment variable key takes precedence, and we want that to be the ones set by ahoy itself. Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
|
I’ve tested this version on several projects and it works great. No regressions were spotted. Thank you for working on this @ocean |
|
Thanks for that @AlexSkrypnyk - very glad it's all run smoothly for you. I'll release v3 today as the Drupal South Wellington 2026 release 😄 I'm also going to make v2 still available as a pinned version in Homebrew for a while (probably as |
yaml.v2 uses case-insensitive field matching as a fallback when no tags are present, but yaml.v3 (the current maintained version) requires exact matches. Without tags, migrating to yaml.v3 would silently leave AhoyAPI empty on every config file, causing spurious "unsupported API version" errors for all users. Add explicit yaml:"..." tags to every field on both Config and Command to make the structs safe for a future yaml.v3 migration.
…os.Exit os.Exit inside rootCmd.Execute() bypasses the pipe-drain cleanup in main(), which can cause in-flight subprocess stderr output to be silently truncated. Switching the command closure from Run to RunE and returning the subprocess error lets main() close the write-end of the pipe and wait for the drain goroutine to flush before exiting.
The block logged the sourcefile path and called os.Exit(0) whenever sourcefile was set, which in normal usage is always. This meant shell completion was printing the config file path instead of command names and exiting without running the pipe-drain cleanup in main(). Removing it lets the correct completion logic below run and avoids the os.Exit inside Execute().
… first
Previously getCommands() called logger("fatal") on the first invalid
command definition, meaning users had to fix errors one at a time with
no indication others existed. Each validation failure now logs as
[error] and continues, with a single [fatal] summary after all commands
are checked. Update BATS assertions to match the new output format.
getSubCommands() and getCommands() mutually recurse when processing imports. A circular reference (A imports B imports A) caused infinite recursion until the Go runtime panicked with a stack overflow. Track visited file paths in a package-level map reset by setupApp() at startup. The root config file is marked visited immediately so imports cannot loop back to it. When a circular reference is detected, a [warn] message is emitted and the file is skipped. Add a BATS test to verify no crash occurs and the warning is present. Also reset the map in the unit test that calls getSubCommands() directly.
os.Pipe() errors were silently discarded with _, leaving w as nil and causing a panic on the subsequent os.Stdout assignment. Add proper error handling that returns early and cleans up any partially created pipes. Add a deferred restore of os.Stdout/os.Stderr so they are recovered even if cmd.Execute() panics, preventing file descriptor leaks between tests.
There was a problem hiding this comment.
zizmor found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
…versions and enable Zizmor scanning Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
…bust Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
feat: Inject AHOY_CMD and AHOY_COMMAND_NAME environment variables into subprocesses
Summary
Migrates the CLI framework from
urfave/cliv1 tospf13/cobra, targeting a v3 major release. Full backwards compatibility with existing.ahoy.ymlfiles is preserved throughout.Changes
Framework migration
urfave/cliwithspf13/cobraacross the codebasespf13/pflag-version,-help) for backwards compatibilityNew features
ahoy configcommand group with two subcommands:ahoy config validate- validates.ahoy.ymlfiles with version mismatch detection and field-level checksahoy config init- refactored to use the native HTTP client (no external dependencies)expandPath()for unified, consistent file path handlingDESCRIPTIONsectionsAHOY_CMD- absolute path to the running ahoy binary (viaos.Executable()), so nestedahoycalls can reference$AHOY_CMDto guarantee version consistency.AHOY_COMMAND_NAME- the name of the command being run (e.g. "test"), so wrapped scripts can detect they were invoked viaahoyand which command triggered them (resolves issue Add an AHOY_COMMAND environment variable to each running command #165).Bug fixes
downloadFileAHOY_FILEandAHOY_VERBOSEenvironment variable wiringProject structure
v2/subdirectory for continued supportCI
coreutilsto macOS runners sotimeoutworks in BATS testsBackwards Compatibility
All existing
.ahoy.ymlconfigurations continue to work without modification. No breaking changes to command syntax or behaviour.