From ceb8456c1dfe9ab938e38b98c060a35f81165228 Mon Sep 17 00:00:00 2001 From: agrasth Date: Mon, 29 Jun 2026 09:37:35 +0530 Subject: [PATCH 1/2] Register jf ruby command with flags, docs, Ghost Frog routing, and tests Wires the native RubyGems/Bundler support into the JFrog CLI: - Register `jf ruby` command in buildtools/cli.go with SkipFlagParsing - RubyCmd handler: extracts native tool, server-id, build details, delegates to RubyCommand via ExecWithPackageManager - Add Ruby flag set (BuildName, BuildNumber, module, Project, serverId) - Add docs/buildtools/rubycommand/help.go with usage examples - Route gem/bundle through `jf ruby` in packagealias/dispatch.go (Ghost Frog) - Add 5 integration tests (version passthrough, help bypass, error handling) Note: go.mod contains local replace directives for build-info-go and jfrog-cli-artifactory (development only, to be replaced at merge time). Co-authored-by: Cursor --- buildtools/cli.go | 55 +++++++++++++++++++++++++++++ docs/buildtools/rubycommand/help.go | 26 ++++++++++++++ go.mod | 2 ++ go.sum | 4 --- packagealias/dispatch.go | 11 ++++-- ruby_test.go | 53 +++++++++++++++++++++++++++ utils/cliutils/commandsflags.go | 4 +++ 7 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 docs/buildtools/rubycommand/help.go create mode 100644 ruby_test.go diff --git a/buildtools/cli.go b/buildtools/cli.go index 4daff389c..1a1620be2 100644 --- a/buildtools/cli.go +++ b/buildtools/cli.go @@ -5,6 +5,7 @@ import ( "fmt" conancommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/conan" nixcommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/nix" + rubycommandexec "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/ruby" "io/fs" "os" "os/exec" @@ -81,6 +82,7 @@ import ( "github.com/jfrog/jfrog-cli/docs/buildtools/pnpmconfig" "github.com/jfrog/jfrog-cli/docs/buildtools/poetry" "github.com/jfrog/jfrog-cli/docs/buildtools/poetryconfig" + "github.com/jfrog/jfrog-cli/docs/buildtools/rubycommand" uvcommand "github.com/jfrog/jfrog-cli/docs/buildtools/uvcommand" yarndocs "github.com/jfrog/jfrog-cli/docs/buildtools/yarn" "github.com/jfrog/jfrog-cli/docs/buildtools/yarnconfig" @@ -443,6 +445,19 @@ func GetCommands() []cli.Command { return cliutils.CreateConfigCmd(c, project.Ruby) }, }, + { + Name: "ruby", + Hidden: false, + Flags: cliutils.GetCommandFlags(cliutils.Ruby), + Usage: rubycommand.GetDescription(), + HelpName: corecommon.CreateUsage("ruby", rubycommand.GetDescription(), rubycommand.Usage), + UsageText: rubycommand.GetArguments(), + ArgsUsage: common.CreateEnvVars(), + SkipFlagParsing: true, + BashComplete: corecommon.CreateBashCompletionFunc(), + Category: buildToolsCategory, + Action: RubyCmd, + }, { Name: "npm-config", Flags: cliutils.GetCommandFlags(cliutils.NpmConfig), @@ -2118,6 +2133,46 @@ func NixCmd(c *cli.Context) error { return commands.ExecWithPackageManager(cmd, "nix") } +// RubyCmd wraps the native 'gem' and 'bundle' tools with Artifactory auth and +// build-info support. The first argument selects the native tool (gem/bundle); +// the remainder is passed through. Only --server-id and the build flags are +// interpreted by jf (config-less native flow, like 'jf uv'). +func RubyCmd(c *cli.Context) error { + if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { + return err + } + if c.NArg() < 1 { + return cliutils.WrongNumberOfArgumentsHandler(c) + } + + args := cliutils.ExtractCommand(c) + + // First arg selects the native tool: "gem" or "bundle". + nativeTool, remainingArgs := getCommandName(args) + + // Extract --server-id before passing args to the native tool. + var serverID string + var err error + remainingArgs, serverID, err = coreutils.ExtractServerIdFromCommand(remainingArgs) + if err != nil { + return fmt.Errorf("failed to extract server ID: %w", err) + } + + // Extract build flags (--build-name, --build-number, --module, --project). + filteredArgs, buildConfiguration, err := build.ExtractBuildDetailsFromArgs(remainingArgs) + if err != nil { + return err + } + + cmd := rubycommandexec.NewRubyCommand(). + SetNativeTool(nativeTool). + SetArgs(filteredArgs). + SetServerID(serverID). + SetBuildConfiguration(buildConfiguration) + + return commands.ExecWithPackageManager(cmd, project.Ruby.String()) +} + func pythonCmd(c *cli.Context, projectType project.ProjectType) error { if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { return err diff --git a/docs/buildtools/rubycommand/help.go b/docs/buildtools/rubycommand/help.go new file mode 100644 index 000000000..c50132546 --- /dev/null +++ b/docs/buildtools/rubycommand/help.go @@ -0,0 +1,26 @@ +package rubycommand + +var Usage = []string{"ruby [command options]"} + +func GetDescription() string { + return "Run native RubyGems (gem) and Bundler (bundle) commands with Artifactory authentication and build-info support." +} + +func GetArguments() string { + return ` ruby + Wraps the native 'gem' and 'bundle' tools. The first argument selects the + native tool; everything after it is passed straight through. Only + --build-name, --build-number, --module, --project and --server-id are + interpreted by jf. + + Authentication is injected automatically from your jf server config and + respects credentials you have already configured natively (Gemfile source, + .bundle/config, ~/.gem/credentials, BUNDLE_* / GEM_HOST_API_KEY env vars). + + Examples: + - jf ruby bundle install --build-name=my-build --build-number=1 + - jf ruby bundle update rake --server-id=my-rt + - jf ruby gem install rails --source https://server/artifactory/api/gems/gems-remote/ + - jf ruby gem build my_gem.gemspec --build-name=my-build --build-number=1 + - jf ruby gem push my_gem-1.0.0.gem --host https://server/artifactory/api/gems/gems-local/ --build-name=my-build --build-number=1` +} diff --git a/go.mod b/go.mod index 01570e815..825df3397 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,8 @@ replace ( github.com/CycloneDX/cyclonedx-go => github.com/CycloneDX/cyclonedx-go v0.10.0 // Should not be updated to 0.2.6 due to a bug (https://github.com/jfrog/jfrog-cli-core/pull/372) github.com/c-bata/go-prompt => github.com/c-bata/go-prompt v0.2.5 + github.com/jfrog/build-info-go => ../build-info-go + github.com/jfrog/jfrog-cli-artifactory => ../jfrog-cli-artifactory // Should not be updated to 0.2.0-beta.2 due to a bug (https://github.com/jfrog/jfrog-cli-core/pull/372) github.com/pkg/term => github.com/pkg/term v1.1.0 ) diff --git a/go.sum b/go.sum index 381622e55..668977831 100644 --- a/go.sum +++ b/go.sum @@ -394,8 +394,6 @@ github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= github.com/jfrog/archiver/v3 v3.6.3 h1:hkAmPjBw393tPmQ07JknLNWFNZjXdy2xFEnOW9wwOxI= github.com/jfrog/archiver/v3 v3.6.3/go.mod h1:5V9l+Fte30Y4qe9dUOAd3yNTf8lmtVNuhKNrvI8PMhg= -github.com/jfrog/build-info-go v1.13.1-0.20260615080618-42488b58c305 h1:q7/hTPm6ibQf45CztScTgPb8cAmKIeQ9im0ClISsq7Y= -github.com/jfrog/build-info-go v1.13.1-0.20260615080618-42488b58c305/go.mod h1:CYRUCvLKfyARjoJXLWAxce1qNUxTEtbRKAARkV42vpE= github.com/jfrog/froggit-go v1.22.0 h1:eeN5F8sOUo+h2cXkzArAu4nvSdjkDTAZtgqwrct70qg= github.com/jfrog/froggit-go v1.22.0/go.mod h1:wRDryqyp3oe+eHgME2mpnEQmO8XBECIPagFwj0nHmdI= github.com/jfrog/go-mockhttp v0.3.1 h1:/wac8v4GMZx62viZmv4wazB5GNKs+GxawuS1u3maJH8= @@ -406,8 +404,6 @@ github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYL github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260617073349-d68ee3120aa8 h1:FG+SfgPgrIuBHSos4sw4KNZq2MKxebbCZ6KZZRfaYcs= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260617073349-d68ee3120aa8/go.mod h1:p8yLtbmCxxQucIbLZKnWu0F+EDtj6NLXbRQCEK/nb6o= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260618051529-1b76b6ad2606 h1:hlc8XoqySjbrvKKjxswyXQ/q5I0Px9FcZpVZUTd+T3M= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260618051529-1b76b6ad2606/go.mod h1:VqV0Bed11HoBlugAEGa3RumbwnDVslEf0gKocTzLs9s= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260615072209-8ccac4f0072e h1:E3B8OyEkCsdEdGsZifTphBDUPrd00yKoemL9+l25Qj8= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260615072209-8ccac4f0072e/go.mod h1:9R90mhbczGXwW5EGlDs7F08ejQU/xdoDhYHMvzBiqgE= github.com/jfrog/jfrog-cli-evidence v0.9.5-0.20260601141509-8df6c9a4bc9b h1:V0FxnU3xh29y8yJHWymm6rPr1MrjG1DdPQlr3ckImwk= diff --git a/packagealias/dispatch.go b/packagealias/dispatch.go index 7adb2b405..b1e9cdaa2 100644 --- a/packagealias/dispatch.go +++ b/packagealias/dispatch.go @@ -116,10 +116,15 @@ func runJFMode(tool string, args []string) error { return fmt.Errorf("%s could not determine executable path: %w", ghostFrogLogPrefix, err) } - newArgs := make([]string, 0, len(os.Args)+1) + newArgs := make([]string, 0, len(os.Args)+2) newArgs = append(newArgs, execPath) // Use actual executable path - newArgs = append(newArgs, tool) // Add tool name as first argument - newArgs = append(newArgs, args...) // Add remaining arguments + // RubyGems (gem) and Bundler (bundle) are exposed under the single + // `jf ruby ` dispatcher rather than top-level `jf gem`/`jf bundle`. + if tool == "gem" || tool == "bundle" { + newArgs = append(newArgs, "ruby") + } + newArgs = append(newArgs, tool) // Add tool name as first argument + newArgs = append(newArgs, args...) // Add remaining arguments os.Args = newArgs diff --git a/ruby_test.go b/ruby_test.go new file mode 100644 index 000000000..ca66a2a0e --- /dev/null +++ b/ruby_test.go @@ -0,0 +1,53 @@ +package main + +import ( + "os/exec" + "testing" + + coretests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/stretchr/testify/assert" +) + +// These tests exercise the `jf ruby ` command wiring end-to-end without +// requiring a live Artifactory: help/version sub-commands bypass auth injection and +// Artifactory calls, so they validate the dispatcher, flag extraction and native exec. + +func rubyToolAvailable(t *testing.T, tool string) { + if _, err := exec.LookPath(tool); err != nil { + t.Skipf("'%s' is not installed; skipping ruby command test", tool) + } +} + +func TestRubyGemVersionPassthrough(t *testing.T) { + rubyToolAvailable(t, "gem") + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + // `jf ruby gem --version` must pass straight through to the gem binary. + assert.NoError(t, jfrogCli.Exec("ruby", "gem", "--version")) +} + +func TestRubyBundleVersionPassthrough(t *testing.T) { + rubyToolAvailable(t, "bundle") + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + assert.NoError(t, jfrogCli.Exec("ruby", "bundle", "--version")) +} + +func TestRubyGemHelpPassthrough(t *testing.T) { + rubyToolAvailable(t, "gem") + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + // `help` sub-command bypasses auth and must not error. + assert.NoError(t, jfrogCli.Exec("ruby", "gem", "help")) +} + +func TestRubyUnsupportedToolErrors(t *testing.T) { + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + // An unsupported native tool must be rejected by RubyCommand.Run. + err := jfrogCli.Exec("ruby", "npm", "install") + assert.Error(t, err) +} + +func TestRubyNoArgsErrors(t *testing.T) { + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + // `jf ruby` with no native tool must report a usage error, not panic. + err := jfrogCli.Exec("ruby") + assert.Error(t, err) +} diff --git a/utils/cliutils/commandsflags.go b/utils/cliutils/commandsflags.go index 4cf45994e..806a16aac 100644 --- a/utils/cliutils/commandsflags.go +++ b/utils/cliutils/commandsflags.go @@ -89,6 +89,7 @@ const ( ConanConfig = "conan-config" Conan = "conan" Nix = "nix" + Ruby = "ruby" Ping = "ping" RtCurl = "rt-curl" TemplateConsumer = "template-consumer" @@ -2243,6 +2244,9 @@ var commandFlags = map[string][]string{ Nix: { BuildName, BuildNumber, module, Project, serverId, }, + Ruby: { + BuildName, BuildNumber, module, Project, serverId, + }, Stats: { XrFormat, accessToken, serverId, }, From f9882130e4b6107c5412b579af8dc7fd850a5071 Mon Sep 17 00:00:00 2001 From: agrasth Date: Mon, 29 Jun 2026 11:52:30 +0530 Subject: [PATCH 2/2] feat: add --repo flag to jf ruby command The --repo flag specifies an Artifactory repository name and lets jf construct the full gems API URL from the server config, so users do not need to pass full Artifactory URLs for gem install/push commands. Also updates help text with --repo examples and adds integration test. Co-authored-by: Cursor --- buildtools/cli.go | 20 ++++++++++++++++++++ docs/buildtools/rubycommand/help.go | 15 +++++++++++---- ruby_test.go | 8 ++++++++ utils/cliutils/commandsflags.go | 2 +- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/buildtools/cli.go b/buildtools/cli.go index 1a1620be2..c528de271 100644 --- a/buildtools/cli.go +++ b/buildtools/cli.go @@ -2158,6 +2158,10 @@ func RubyCmd(c *cli.Context) error { return fmt.Errorf("failed to extract server ID: %w", err) } + // Extract --repo (Artifactory repository name for URL construction). + var repo string + remainingArgs, repo = extractRubyRepoFromArgs(remainingArgs) + // Extract build flags (--build-name, --build-number, --module, --project). filteredArgs, buildConfiguration, err := build.ExtractBuildDetailsFromArgs(remainingArgs) if err != nil { @@ -2168,11 +2172,27 @@ func RubyCmd(c *cli.Context) error { SetNativeTool(nativeTool). SetArgs(filteredArgs). SetServerID(serverID). + SetRepo(repo). SetBuildConfiguration(buildConfiguration) return commands.ExecWithPackageManager(cmd, project.Ruby.String()) } +// extractRubyRepoFromArgs extracts and consumes --repo from the args slice. +func extractRubyRepoFromArgs(args []string) (cleanArgs []string, repo string) { + for i := 0; i < len(args); i++ { + if args[i] == "--repo" && i+1 < len(args) { + repo = args[i+1] + i++ // skip the value + } else if strings.HasPrefix(args[i], "--repo=") { + repo = strings.TrimPrefix(args[i], "--repo=") + } else { + cleanArgs = append(cleanArgs, args[i]) + } + } + return cleanArgs, repo +} + func pythonCmd(c *cli.Context, projectType project.ProjectType) error { if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil { return err diff --git a/docs/buildtools/rubycommand/help.go b/docs/buildtools/rubycommand/help.go index c50132546..30f91c72b 100644 --- a/docs/buildtools/rubycommand/help.go +++ b/docs/buildtools/rubycommand/help.go @@ -10,17 +10,24 @@ func GetArguments() string { return ` ruby Wraps the native 'gem' and 'bundle' tools. The first argument selects the native tool; everything after it is passed straight through. Only - --build-name, --build-number, --module, --project and --server-id are - interpreted by jf. + --build-name, --build-number, --module, --project, --server-id and --repo + are interpreted by jf. Authentication is injected automatically from your jf server config and respects credentials you have already configured natively (Gemfile source, .bundle/config, ~/.gem/credentials, BUNDLE_* / GEM_HOST_API_KEY env vars). + The --repo flag specifies the Artifactory repository name and constructs + the full URL from your server config (eliminating the need to pass full + Artifactory URLs). When --repo is used with gem install/push, the + --source/--host argument is injected automatically. + Examples: - jf ruby bundle install --build-name=my-build --build-number=1 - jf ruby bundle update rake --server-id=my-rt + - jf ruby gem install rails --repo gems-virtual - jf ruby gem install rails --source https://server/artifactory/api/gems/gems-remote/ - - jf ruby gem build my_gem.gemspec --build-name=my-build --build-number=1 - - jf ruby gem push my_gem-1.0.0.gem --host https://server/artifactory/api/gems/gems-local/ --build-name=my-build --build-number=1` + - jf ruby gem push my_gem-1.0.0.gem --repo gems-local --build-name=my-build --build-number=1 + - jf ruby gem push my_gem-1.0.0.gem --host https://server/artifactory/api/gems/gems-local/ --build-name=my-build --build-number=1 + - jf ruby gem install rake --repo gems-virtual --server-id my-rt` } diff --git a/ruby_test.go b/ruby_test.go index ca66a2a0e..d0351b2ab 100644 --- a/ruby_test.go +++ b/ruby_test.go @@ -51,3 +51,11 @@ func TestRubyNoArgsErrors(t *testing.T) { err := jfrogCli.Exec("ruby") assert.Error(t, err) } + +func TestRubyRepoFlagPassthrough(t *testing.T) { + rubyToolAvailable(t, "gem") + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + // --repo is consumed by jf and should not be passed to gem. + // `gem help` should still work fine with --repo being stripped. + assert.NoError(t, jfrogCli.Exec("ruby", "gem", "help", "--repo", "gems-virtual")) +} diff --git a/utils/cliutils/commandsflags.go b/utils/cliutils/commandsflags.go index 806a16aac..fb8273625 100644 --- a/utils/cliutils/commandsflags.go +++ b/utils/cliutils/commandsflags.go @@ -2245,7 +2245,7 @@ var commandFlags = map[string][]string{ BuildName, BuildNumber, module, Project, serverId, }, Ruby: { - BuildName, BuildNumber, module, Project, serverId, + BuildName, BuildNumber, module, Project, serverId, repo, }, Stats: { XrFormat, accessToken, serverId,