Skip to content

feat(extension): implicit plugin mechanism via circleci-<name> PATH lookup#1277

Draft
michael-webster wants to merge 3 commits intonextfrom
feat/implicit-extension-mechanism
Draft

feat(extension): implicit plugin mechanism via circleci-<name> PATH lookup#1277
michael-webster wants to merge 3 commits intonextfrom
feat/implicit-extension-mechanism

Conversation

@michael-webster
Copy link
Copy Markdown
Contributor

@michael-webster michael-webster commented May 5, 2026

Summary

  • Adds implicit extension support: any circleci-<name> binary in PATH is transparently invoked as circleci <name>, following git's plugin convention
  • New internal/extension package handles PATH lookup, environment injection, and exit code propagation
  • Only top-level unknown commands are intercepted — unknown subcommands within a group still produce the normal cobra error

How it works

When Cobra returns an unknown command "X" for "circleci" error, main.go intercepts it before printing and attempts to exec.LookPath("circleci-X"). If found, the binary is executed with:

  • All args after the command name passed through
  • CIRCLECI_TOKEN and CIRCLECI_HOST injected from the config file / env vars
  • CIRCLECI_VCS_TYPE, CIRCLECI_PROJECT_USERNAME, CIRCLECI_PROJECT_REPONAME injected from git remote (best-effort, silent on failure)
  • Full os.Environ() inherited so the extension gets everything the user had

If no matching binary exists, the original cobra error is shown unchanged.

Test plan

  • task test — all 464 tests pass
  • task check — lint clean (pre-existing license failure in cmd/.circleci/update_check.yml unrelated to this change)
  • Manually tested with a fake circleci-hello extension: args, CIRCLECI_TOKEN, and CIRCLECI_HOST all injected correctly
  • Manually tested circleci testsuite "ci tests" — dispatches to circleci-testsuite correctly, exit code propagated
  • Unknown commands with no matching extension still show the original cobra error
  • Unknown subcommands within a group (e.g. circleci pipeline foo) are not intercepted

Example running this:
image

🤖 Generated with Claude Code

webster and others added 2 commits May 5, 2026 14:56
…ookup

Unknown top-level commands transparently exec a matching circleci-<name>
binary from PATH, following git's plugin convention. The extension
receives CIRCLE_TOKEN, CIRCLE_URL, and best-effort project metadata
(VCS type, org, repo) via environment variables so it can call the
CircleCI API without reimplementing auth.

Only top-level unknown commands are intercepted — unknown subcommands
within a group (e.g. "circleci pipeline foo") still produce the normal
cobra error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ndling

- acceptance/extension_test.go: acceptance tests for extension dispatch,
  CIRCLE_TOKEN injection, unknown-command fallthrough, and exit code propagation
- buildEnv: replace duplicate env keys in-place instead of appending, so
  the injected token wins over any pre-existing CIRCLE_TOKEN in the shell env
- Run: use clierrors.New instead of fmt.Errorf for structured error output
- rootUnknownCommand: accept rootName param so binary renames (e.g. cci) work
- main.go: format CLIError from extension via the normal error formatter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@michael-webster michael-webster force-pushed the feat/implicit-extension-mechanism branch 2 times, most recently from 70d7b19 to 0bac55c Compare May 6, 2026 18:57
- Replace brittle Cobra error-string matching and hand-maintained
  globalFlagsWithValues allowlist with rootCmd.Find() + Flags().Args(),
  so extension dispatch is robust to new flags and Cobra version changes
- Return ErrExited from Run instead of calling os.Exit directly, so
  deferred cleanup runs and Run is testable
- Rename injected env vars CIRCLE_TOKEN/CIRCLE_URL → CIRCLECI_TOKEN/CIRCLECI_HOST
  (and VCS/project vars) to match the project's documented CIRCLECI_ prefix
- Extract skipUnlessShell helper in acceptance tests to consolidate the
  Windows skip guard in one place

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@michael-webster michael-webster force-pushed the feat/implicit-extension-mechanism branch from 0bac55c to feadafc Compare May 7, 2026 21:06
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.

1 participant