Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions buildtools/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -2118,6 +2133,66 @@ 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 --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 {
return err
}

cmd := rubycommandexec.NewRubyCommand().
SetNativeTool(nativeTool).
SetArgs(filteredArgs).
SetServerID(serverID).
SetRepo(repo).
SetBuildConfiguration(buildConfiguration)

return commands.ExecWithPackageManager(cmd, project.Ruby.String())
}

// extractRubyRepoFromArgs extracts and consumes --repo <name> 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
Expand Down
33 changes: 33 additions & 0 deletions docs/buildtools/rubycommand/help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package rubycommand

var Usage = []string{"ruby <gem|bundle> <args> [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 <gem|bundle> <args>
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, --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 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`
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
11 changes: 8 additions & 3 deletions packagealias/dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tool>` 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

Expand Down
61 changes: 61 additions & 0 deletions ruby_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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 <gem|bundle>` 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)
}

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"))
}
4 changes: 4 additions & 0 deletions utils/cliutils/commandsflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const (
ConanConfig = "conan-config"
Conan = "conan"
Nix = "nix"
Ruby = "ruby"
Ping = "ping"
RtCurl = "rt-curl"
TemplateConsumer = "template-consumer"
Expand Down Expand Up @@ -2243,6 +2244,9 @@ var commandFlags = map[string][]string{
Nix: {
BuildName, BuildNumber, module, Project, serverId,
},
Ruby: {
BuildName, BuildNumber, module, Project, serverId, repo,
},
Stats: {
XrFormat, accessToken, serverId,
},
Expand Down
Loading