Skip to content

V3 candidate#167

Open
ocean wants to merge 56 commits into
masterfrom
v3-candidate
Open

V3 candidate#167
ocean wants to merge 56 commits into
masterfrom
v3-candidate

Conversation

@ocean
Copy link
Copy Markdown
Member

@ocean ocean commented May 12, 2026

Summary

Migrates the CLI framework from urfave/cli v1 to spf13/cobra, targeting a v3 major release. Full backwards compatibility with existing .ahoy.yml files is preserved throughout.

Changes

Framework migration

  • Replaced urfave/cli with spf13/cobra across the codebase
  • Removed Viper dependency after initial migration - flags handled directly via Cobra and spf13/pflag
  • Implemented custom pre-parser to retain single-dash flag support (-version, -help) for backwards compatibility

New features

  • Added ahoy config command group with two subcommands:
    • ahoy config validate - validates .ahoy.yml files with version mismatch detection and field-level checks
    • ahoy config init - refactored to use the native HTTP client (no external dependencies)
  • Added expandPath() for unified, consistent file path handling
  • Improved two-tier help output with per-command DESCRIPTION sections
  • Add new environment variables:
    • AHOY_CMD - absolute path to the running ahoy binary (via os.Executable()), so nested ahoy calls can reference $AHOY_CMD to guarantee version consistency.
    • AHOY_COMMAND_NAME - the name of the command being run (e.g. "test"), so wrapped scripts can detect they were invoked via ahoy and which command triggered them (resolves issue Add an AHOY_COMMAND environment variable to each running command #165).

Bug fixes

  • Fixed nil-pointer panics in edge-case command execution paths
  • Fixed silent error swallowing and incorrect exit codes on invalid flags
  • Fixed stderr-capture deadlock by replacing buffering with a goroutine drain
  • Fixed atomic file writes and URL scheme validation in downloadFile
  • Fixed AHOY_FILE and AHOY_VERBOSE environment variable wiring

Project structure

  • v3 code moved to repository root; v2 code preserved in v2/ subdirectory for continued support

CI

  • Added Homebrew coreutils to macOS runners so timeout works in BATS tests
  • Tests run against both v2 and root (v3) codebases

Backwards Compatibility

All existing .ahoy.yml configurations continue to work without modification. No breaking changes to command syntax or behaviour.

claude and others added 30 commits November 17, 2025 04:18
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.
ocean and others added 17 commits February 24, 2026 18:30
…ranches

ValidateConfig() was previously untested directly — all tests called
RunConfigValidate(), which aborts via getConfig() before ValidateConfig
is reached when YAML is invalid. This left four internal branches with
no unit-level coverage:

- Wrong API version (HasError = true, severity = "error")
- multiple_env_files feature on an old simulated version (warning)
- optional_imports feature on an old simulated version (HasError = true,
  severity = "error")
- command_aliases feature on an old simulated version (warning)

Each new test constructs a Config struct directly and pins simulateVersion
via a withSimulateVersion() helper so GetAhoyVersion() returns a
controlled value without rebuilding the binary. Also add a clean-config
test that asserts no issues for a valid v2 config.
The CI pipeline contract — that 'ahoy config validate' exits 1 when the
config has error-severity issues — was previously untested. A warning
from an old simulate-version was tested but warnings don't set HasError
and therefore exit 0.

Add a new test that creates a config using 'optional: true' (which
requires v2.2.0) and runs with --simulate-version v2.1.0, triggering an
error-severity 'optional_imports' issue. Asserts [ "$status" -eq 1 ] and
that "optional" appears in the output.

Also clarify the comment on the existing simulate-version test to make
explicit that command_aliases is a warning (exit 0), distinguishing it
from the new error-severity test.
Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
On Windows filepath.IsAbs returns false for paths like "/absolute/path"
because Windows requires a drive letter. The strings.HasPrefix("/") guard
is needed so cross-platform config files behave consistently.
…for stderr passthrough BATS test

Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
… potential v3 release (#166)

* Migrate CLI framework from urfave/cli to cobra and viper

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.

* Fix staticcheck S1011: use idiomatic slice append for aliases

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.

* Fix Bats integration tests: improve flag parsing and error handling

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.

* Fix single-dash flag handling for -version and -help

- 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%)

* Fix single-dash flag handling for -version and -help

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

* fix: Remove unused variable

* fix: Fix top level .ahoy.yml file

* Move v3 code to root, restore v2 code in subdirectory for support

Signed-off-by: Drew Robinson <drew.robinson@gmail.com>

* ci: Fix broken test, make sure tests are run on both versions still

Signed-off-by: Drew Robinson <drew.robinson@gmail.com>

* ci: Ignore all vendor files in format checking

Signed-off-by: Drew Robinson <drew.robinson@gmail.com>

* tests: Fix "no ahoy file" test in v2 (was finding parent dir ahoy file)

Signed-off-by: Drew Robinson <drew.robinson@gmail.com>

* Update DrevOps logo in README

* fix: Remove unneeded nil check

Signed-off-by: Drew Robinson <drew.robinson@gmail.com>

* docs: Update Readme

Signed-off-by: Drew Robinson <drew.robinson@gmail.com>

* feat: Improve top-level help output for two-tier help display

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.

* feat: Add per-command help template with DESCRIPTION section

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.

* tests: Update BATS tests for two-tier help formatting

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.

* tests: Add help interception tests from CodeRabbit review feedback

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.

* feat: Add expandPath() for unified path handling

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.

* feat: Add config validation engine

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

* feat: Refactor init command to use native HTTP client

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.

* feat: Add 'ahoy config' command group with validate and init subcommands

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."

* feat: Add hidden --simulate-version flag for validation testing

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.

* feat: Improve missing import error messages with diagnostic context

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.

* tests: Add unit tests for config validation, init, and expandPath

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.

* tests: Add BATS integration tests for 'ahoy config' command group

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).

* fix: Fix the file path usage in the tests

Signed-off-by: Drew Robinson <drew.robinson@gmail.com>

* fix: Fix critical nil-pointer panics, dead code, and unsafe file write

- 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.

* refactor: Remove Viper dependency, add AHOY_FILE and AHOY_VERBOSE env var support

Viper was configured (SetEnvPrefix, AutomaticEnv, BindPFlag, Set) but
never read back in production code — all values were consumed from Go
globals directly. The dependency added significant transitive packages
(fsnotify, afero, mapstructure, cast, go-toml, etc.) for zero benefit.

Replace the viper integration with two lightweight os.Getenv checks in
initFlags(), giving the same env var capability without the dependency:

  AHOY_FILE=path/to/.ahoy.yml   — equivalent to -f / --file
  AHOY_VERBOSE=true             — equivalent to -v / --verbose

Explicit CLI flags take precedence over environment variables.

Also update cli_parsing_test.go:
- TestEnvironmentVariableFlags: replace the empty viper assertion (which
  asserted nothing) with four real behavioural sub-tests covering both
  env vars and flag-precedence rules.
- TestMigrationCompatibility: replace the viper BindPFlag no-op with a
  genuine assertion that the persistent flags are registered on the root
  cobra command.

* fix: Remove dead versionFlagSet/helpFlagSet checks from BeforeCommand

main() already handles -version and -help (single-dash forms set by
initFlags) with os.Exit(0) calls before rootCmd.Execute() is ever
called, so the identical checks inside BeforeCommand's
PersistentPreRunE hook can never be reached.

Remove the two dead single-dash blocks, keeping only the --version and
--help double-dash paths that cobra's PersistentPreRunE actually runs.

* fix: Fix silent error swallowing and wrong exit code on invalid flag

- getSubCommands(): distinguish os.IsNotExist from other stat errors.
  Permission errors (EACCES) on an import file were silently treated
  as "file not found", making commands vanish from the CLI with no
  indication why. Non-IsNotExist errors now log a message so the user
  knows the file exists but cannot be read.

- getEnvironmentVars(): log a proper error when os.ReadFile fails on a
  file that was confirmed to exist. Previously a permission or I/O
  error returned nil silently, causing commands to fail with mysterious
  "variable not set" errors rather than a clear env-file read failure.

- commandHelpFunc / customHelpFunc: always write template execution
  errors to stderr. A template failure was silently truncating help
  output, leaving the user with no message and no usable help.
  The CLI_TEMPLATE_ERROR_DEBUG env var now only controls the verbose
  %#v dump, not whether the error is reported at all.

- main(): exit with code 1 (not 0) when an invalid flag is detected.
  Exiting 0 masked the failure as success, breaking CI pipelines that
  check exit codes. Update the BATS test to assert status 1.

* fix: Propagate YAML parse error detail through ConfigReport

Previously RunConfigValidate discarded the specific error returned by
getConfig() and stored only a generic "Fix YAML syntax errors" string
in Recommendations. Users with a syntax error saw no line/column
information to guide them to the problem.

Add a ParseError string field to ConfigReport, populated with the raw
error string when parsing fails. PrintConfigReport now prints it below
the "Invalid YAML" status line. The recommendation message is also
updated to include the error text directly.

Update TestRunConfigValidate_InvalidYAML to assert that:
- At least one recommendation exists and starts with "Fix YAML syntax error:"
- ParseError is non-empty when parsing fails

* fix: Correct ValidationIssue.Type comment to match assigned values

The comment listed four type values but only "version_mismatch" and
"missing_file" are ever assigned in the codebase. Remove the two
phantom values ("unknown_field", "syntax_error") so the comment
accurately documents the type's contract.

* fix: Validate URL scheme in downloadFile, replace live network test

- downloadFile() now rejects any URL whose scheme is not http or https
  before making a network request. Previously a file:// URL could read
  local filesystem paths via Go's HTTP client, and a relative URL or
  empty string produced a confusing internal error message.

- Replace TestDownloadFile_404Response (hit raw.githubusercontent.com)
  with httptest.NewServer-based equivalents. The live GitHub request
  fails in offline CI and firewalled environments; the new tests are
  fully hermetic. Three tests now cover: 200 OK success, 404 error, and
  rejection of disallowed URL schemes (file://, ftp://, empty string).

* tests: Add direct ValidateConfig unit tests for all four validation branches

ValidateConfig() was previously untested directly — all tests called
RunConfigValidate(), which aborts via getConfig() before ValidateConfig
is reached when YAML is invalid. This left four internal branches with
no unit-level coverage:

- Wrong API version (HasError = true, severity = "error")
- multiple_env_files feature on an old simulated version (warning)
- optional_imports feature on an old simulated version (HasError = true,
  severity = "error")
- command_aliases feature on an old simulated version (warning)

Each new test constructs a Config struct directly and pins simulateVersion
via a withSimulateVersion() helper so GetAhoyVersion() returns a
controlled value without rebuilding the binary. Also add a clean-config
test that asserts no issues for a valid v2 config.

* tests: Add BATS test asserting exit code 1 for error-severity validation

The CI pipeline contract — that 'ahoy config validate' exits 1 when the
config has error-severity issues — was previously untested. A warning
from an old simulate-version was tested but warnings don't set HasError
and therefore exit 0.

Add a new test that creates a config using 'optional: true' (which
requires v2.2.0) and runs with --simulate-version v2.1.0, triggering an
error-severity 'optional_imports' issue. Asserts [ "$status" -eq 1 ] and
that "optional" appears in the output.

Also clarify the comment on the existing simulate-version test to make
explicit that command_aliases is a warning (exit 0), distinguishing it
from the new error-severity test.

* chore: Remove commented-out debug lines

Signed-off-by: Drew Robinson <drew.robinson@gmail.com>

* fix: Make comments more accurate

Signed-off-by: Drew Robinson <drew.robinson@gmail.com>

* tests: Add BATS regression tests for stderr-capture deadlock

* fix: Document and refactor legacy single-dash flag pre-parser

* fix: Validate URL scheme and use atomic write in downloadFile

* fix: Update ValidationIssue.Type comment to match implementation

* fix: Replace stderr buffering with goroutine drain to prevent deadlock

* fix: Remove no-op SetFlagErrorFunc and improve SilenceErrors comment

* fix: Wire debug logger output to --verbose flag

* fix: Send deprecated ahoy init notice to stderr

* fix: Remove redundant absolute path check in expandPath

* style: Use standard hyphens in inline comments

* fix: Restore Unix-style absolute path check in expandPath

On Windows filepath.IsAbs returns false for paths like "/absolute/path"
because Windows requires a drive letter. The strings.HasPrefix("/") guard
is needed so cross-platform config files behave consistently.

* ci: Add Homebrew coreutils to macOS runners so timeout command works for stderr passthrough BATS test

Signed-off-by: Drew Robinson <drew.robinson@gmail.com>

---------

Signed-off-by: Drew Robinson <drew.robinson@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Important

Review skipped

Too many files!

This PR contains 196 files, which is 46 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d56cc1ae-f472-4bc7-a38b-13df041b31db

📥 Commits

Reviewing files that changed from the base of the PR and between 943ff2f and 985b7f7.

⛔ Files ignored due to path filters (7)
  • go.sum is excluded by !**/*.sum
  • v2/img/icon/ahoy-logo-square-blank-v2.png is excluded by !**/*.png
  • v2/img/icon/ahoy-logo-square-blank-v2.svg is excluded by !**/*.svg
  • v2/img/rect/ahoy-logo-rect-v2.png is excluded by !**/*.png
  • v2/img/rect/ahoy-logo-rect-v2.svg is excluded by !**/*.svg
  • v2/img/square/ahoy-logo-square-v2.png is excluded by !**/*.png
  • v2/img/square/ahoy-logo-square-v2.svg is excluded by !**/*.svg
📒 Files selected for processing (196)
  • .ahoy.yml
  • .github/workflows/build_and_test.yml
  • Dockerfile
  • Makefile
  • README.md
  • ahoy.go
  • ahoy_test.go
  • cli_parsing_test.go
  • config.go
  • config_init.go
  • config_init_test.go
  • config_validation.go
  • config_validation_test.go
  • description_test.go
  • examples/confirmation.ahoy.yml
  • examples/docker.ahoy.yml
  • examples/test.ahoy.yml
  • flag.go
  • go.mod
  • stringArray.go
  • stringArray_test.go
  • testdata/.env
  • testdata/.env.cmd
  • testdata/.env.local1
  • testdata/.env.local2
  • testdata/.env.multiple
  • testdata/.env.test
  • testdata/command-aliases.ahoy.yml
  • testdata/config.sh
  • testdata/descriptions-test.ahoy.yml
  • testdata/docker-overrides.ahoy.yml
  • testdata/docker.ahoy.yml
  • testdata/empty-imports.ahoy.yml
  • testdata/entrypoint-bash.ahoy.yml
  • testdata/entrypoint-php.ahoy.yml
  • testdata/entrypoint-prefix.ahoy.yml
  • testdata/env-multiple-global.ahoy.yml
  • testdata/env-multiple.ahoy.yml
  • testdata/env.ahoy.yml
  • testdata/invalid-yaml.ahoy.yml
  • testdata/missing-cmd.ahoy.yml
  • testdata/missing-imports.ahoy.yml
  • testdata/newer-features.ahoy.yml
  • testdata/non-optional-command.ahoy.yml
  • testdata/optional-command.ahoy.yml
  • testdata/override-base.ahoy.yml
  • testdata/simple.ahoy.yml
  • testdata/with-env-files.ahoy.yml
  • testdata/with-imports.ahoy.yml
  • tests/.ahoy.yml
  • tests/cli-flag-combinations.bats
  • tests/command-aliases.bats
  • tests/config-init.bats
  • tests/config-validate.bats
  • tests/cross-compile.bats
  • tests/descriptions.bats
  • tests/entrypoint.bats
  • tests/environment-variables.bats
  • tests/flags.bats
  • tests/migration-safety.bats
  • tests/missing-cmd.bats
  • tests/multiple-env-files.bats
  • tests/no-ahoy-file.bats
  • tests/optional-commands.bats
  • tests/simple.bats
  • tests/stderr-passthrough.bats
  • tests/sub-commands.bats
  • tests/verbose-flag.bats
  • tests/windows-binary-execution.bats
  • tests/windows-path-handling.bats
  • v2/.all-contributorsrc
  • v2/.circleci/config.yml
  • v2/.github/ISSUE_TEMPLATE/bug_report.md
  • v2/.github/ISSUE_TEMPLATE/feature_request.md
  • v2/.github/dependabot.yml
  • v2/.github/workflows/build_and_test.yml
  • v2/.github/workflows/codeql.yml
  • v2/.github/workflows/release.yml
  • v2/.gitignore
  • v2/.golangci.yml
  • v2/.goreleaser.yaml
  • v2/CODE_OF_CONDUCT.md
  • v2/FUNDING.yml
  • v2/LICENSE
  • v2/README.md
  • v2/README.md
  • v2/ahoy.go
  • v2/docs/Basic-Tips-and-Troubleshooting.md
  • v2/docs/Confirmation.md
  • v2/docs/Controlling-Execution.md
  • v2/docs/Home.md
  • v2/docs/Multiple-line-commands.md
  • v2/docs/index.md
  • v2/img/icon/ahoy-logo-square-blank-v2.webp
  • v2/img/icon/ahoy-logo-square-blank-v2@2x.webp
  • v2/img/rect/ahoy-logo-rect-v2.webp
  • v2/img/rect/ahoy-logo-rect-v2@2x.webp
  • v2/img/square/ahoy-logo-square-v2.webp
  • v2/img/square/ahoy-logo-square-v2@2x.webp
  • v2/tests/no-ahoy-file.bats
  • vendor/github.com/inconshreveable/mousetrap/LICENSE
  • vendor/github.com/inconshreveable/mousetrap/README.md
  • vendor/github.com/inconshreveable/mousetrap/trap_others.go
  • vendor/github.com/inconshreveable/mousetrap/trap_windows.go
  • vendor/github.com/spf13/cobra/.gitignore
  • vendor/github.com/spf13/cobra/.golangci.yml
  • vendor/github.com/spf13/cobra/.mailmap
  • vendor/github.com/spf13/cobra/CONDUCT.md
  • vendor/github.com/spf13/cobra/CONTRIBUTING.md
  • vendor/github.com/spf13/cobra/LICENSE.txt
  • vendor/github.com/spf13/cobra/MAINTAINERS
  • vendor/github.com/spf13/cobra/Makefile
  • vendor/github.com/spf13/cobra/README.md
  • vendor/github.com/spf13/cobra/SECURITY.md
  • vendor/github.com/spf13/cobra/active_help.go
  • vendor/github.com/spf13/cobra/args.go
  • vendor/github.com/spf13/cobra/bash_completions.go
  • vendor/github.com/spf13/cobra/bash_completionsV2.go
  • vendor/github.com/spf13/cobra/cobra.go
  • vendor/github.com/spf13/cobra/command.go
  • vendor/github.com/spf13/cobra/command_notwin.go
  • vendor/github.com/spf13/cobra/command_win.go
  • vendor/github.com/spf13/cobra/completions.go
  • vendor/github.com/spf13/cobra/fish_completions.go
  • vendor/github.com/spf13/cobra/flag_groups.go
  • vendor/github.com/spf13/cobra/powershell_completions.go
  • vendor/github.com/spf13/cobra/shell_completions.go
  • vendor/github.com/spf13/cobra/zsh_completions.go
  • vendor/github.com/spf13/pflag/.editorconfig
  • vendor/github.com/spf13/pflag/.gitignore
  • vendor/github.com/spf13/pflag/.golangci.yaml
  • vendor/github.com/spf13/pflag/.travis.yml
  • vendor/github.com/spf13/pflag/LICENSE
  • vendor/github.com/spf13/pflag/README.md
  • vendor/github.com/spf13/pflag/bool.go
  • vendor/github.com/spf13/pflag/bool_func.go
  • vendor/github.com/spf13/pflag/bool_slice.go
  • vendor/github.com/spf13/pflag/bytes.go
  • vendor/github.com/spf13/pflag/count.go
  • vendor/github.com/spf13/pflag/duration.go
  • vendor/github.com/spf13/pflag/duration_slice.go
  • vendor/github.com/spf13/pflag/errors.go
  • vendor/github.com/spf13/pflag/flag.go
  • vendor/github.com/spf13/pflag/float32.go
  • vendor/github.com/spf13/pflag/float32_slice.go
  • vendor/github.com/spf13/pflag/float64.go
  • vendor/github.com/spf13/pflag/float64_slice.go
  • vendor/github.com/spf13/pflag/func.go
  • vendor/github.com/spf13/pflag/golangflag.go
  • vendor/github.com/spf13/pflag/int.go
  • vendor/github.com/spf13/pflag/int16.go
  • vendor/github.com/spf13/pflag/int32.go
  • vendor/github.com/spf13/pflag/int32_slice.go
  • vendor/github.com/spf13/pflag/int64.go
  • vendor/github.com/spf13/pflag/int64_slice.go
  • vendor/github.com/spf13/pflag/int8.go
  • vendor/github.com/spf13/pflag/int_slice.go
  • vendor/github.com/spf13/pflag/ip.go
  • vendor/github.com/spf13/pflag/ip_slice.go
  • vendor/github.com/spf13/pflag/ipmask.go
  • vendor/github.com/spf13/pflag/ipnet.go
  • vendor/github.com/spf13/pflag/ipnet_slice.go
  • vendor/github.com/spf13/pflag/string.go
  • vendor/github.com/spf13/pflag/string_array.go
  • vendor/github.com/spf13/pflag/string_slice.go
  • vendor/github.com/spf13/pflag/string_to_int.go
  • vendor/github.com/spf13/pflag/string_to_int64.go
  • vendor/github.com/spf13/pflag/string_to_string.go
  • vendor/github.com/spf13/pflag/text.go
  • vendor/github.com/spf13/pflag/time.go
  • vendor/github.com/spf13/pflag/uint.go
  • vendor/github.com/spf13/pflag/uint16.go
  • vendor/github.com/spf13/pflag/uint32.go
  • vendor/github.com/spf13/pflag/uint64.go
  • vendor/github.com/spf13/pflag/uint8.go
  • vendor/github.com/spf13/pflag/uint_slice.go
  • vendor/gopkg.in/yaml.v2/.travis.yml
  • vendor/gopkg.in/yaml.v2/LICENSE
  • vendor/gopkg.in/yaml.v2/LICENSE.libyaml
  • vendor/gopkg.in/yaml.v2/NOTICE
  • vendor/gopkg.in/yaml.v2/README.md
  • vendor/gopkg.in/yaml.v2/apic.go
  • vendor/gopkg.in/yaml.v2/decode.go
  • vendor/gopkg.in/yaml.v2/emitterc.go
  • vendor/gopkg.in/yaml.v2/encode.go
  • vendor/gopkg.in/yaml.v2/parserc.go
  • vendor/gopkg.in/yaml.v2/readerc.go
  • vendor/gopkg.in/yaml.v2/resolve.go
  • vendor/gopkg.in/yaml.v2/scannerc.go
  • vendor/gopkg.in/yaml.v2/sorter.go
  • vendor/gopkg.in/yaml.v2/writerc.go
  • vendor/gopkg.in/yaml.v2/yaml.go
  • vendor/gopkg.in/yaml.v2/yamlh.go
  • vendor/gopkg.in/yaml.v2/yamlprivateh.go
  • vendor/modules.txt
  • windows_test.go

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch v3-candidate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 12, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgithub.com/​spf13/​cobra@​v1.10.195100100100100
Addedgithub.com/​spf13/​pflag@​v1.0.1097100100100100

View full report

@ocean
Copy link
Copy Markdown
Member Author

ocean commented May 12, 2026

@CodeRabbit help

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

CodeRabbit commands

These commands are invoked using PR/Issue comments.

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai rate limit to show your current review rate limit status.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai evaluate custom pre-merge check --instructions <custom-checks-instructions> --name <custom-checks-title> [--mode <error|warning>] to validate the custom pre-merge checks instructions. Defaults to error when --mode is omitted.
  • @coderabbitai ignore pre-merge checks to override pre-merge checks and get an approval on PR.
  • @coderabbitai run pre-merge checks to run pre-merge checks on the pull request.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve merge conflict to automatically resolve merge conflicts.
  • @coderabbitai autofix to automatically fix issues identified in unresolved review comments.
  • @coderabbitai resolve to resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai generate configuration to create a PR that adds the current resolved configuration as .coderabbit.yaml (or show it if already present).
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit configuration file (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, support, documentation and community

  • Visit our status page to check the current availability of CodeRabbit.
  • Create a ticket on our support page for assistance with any issues or questions.
  • Visit our documentation site for detailed information on how to use CodeRabbit.
  • Join our Discord community to connect with other users and get help from the community.
  • Follow us on X/Twitter for updates and announcements.

@ocean
Copy link
Copy Markdown
Member Author

ocean commented May 12, 2026

@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.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

@ocean, understood! I've noted that the configuration has been updated to exclude the root vendor/ directory — that should keep the review focused on the relevant source files. Kicking off a full review now.

✅ Actions performed

Full review triggered.

@AlexSkrypnyk
Copy link
Copy Markdown
Collaborator

I’ve tested this version on several projects and it works great. No regressions were spotted.

Thank you for working on this @ocean

@ocean
Copy link
Copy Markdown
Member Author

ocean commented May 17, 2026

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 ahoy@2) just in case anyone has any issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants