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
145 changes: 145 additions & 0 deletions buildtools/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package buildtools
import (
"errors"
"fmt"
alpinecommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/alpine"
conancommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/conan"
nixcommand "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/nix"
"io/fs"
Expand Down Expand Up @@ -68,6 +69,7 @@ import (
huggingfaceuploaddocs "github.com/jfrog/jfrog-cli/docs/buildtools/huggingfaceupload"
mvndoc "github.com/jfrog/jfrog-cli/docs/buildtools/mvn"
"github.com/jfrog/jfrog-cli/docs/buildtools/mvnconfig"
"github.com/jfrog/jfrog-cli/docs/buildtools/apkcommand"
"github.com/jfrog/jfrog-cli/docs/buildtools/nix"
"github.com/jfrog/jfrog-cli/docs/buildtools/npmcommand"
"github.com/jfrog/jfrog-cli/docs/buildtools/npmconfig"
Expand Down Expand Up @@ -430,6 +432,18 @@ func GetCommands() []cli.Command {
Category: buildToolsCategory,
Action: NixCmd,
},
{
Name: "apk",
Flags: cliutils.GetCommandFlags(cliutils.Apk),
Usage: apkcommand.GetDescription(),
HelpName: corecommon.CreateUsage("apk", apkcommand.GetDescription(), apkcommand.Usage),
UsageText: apkcommand.GetArguments(),
ArgsUsage: common.CreateEnvVars(),
SkipFlagParsing: true,
BashComplete: corecommon.CreateBashCompletionFunc("config", "upload", "add", "upgrade", "update", "fetch", "search", "del", "info"),
Category: buildToolsCategory,
Action: ApkCmd,
},
{
Name: "ruby-config",
Flags: cliutils.GetCommandFlags(cliutils.RubyConfig),
Expand Down Expand Up @@ -2118,6 +2132,137 @@ func NixCmd(c *cli.Context) error {
return commands.ExecWithPackageManager(cmd, "nix")
}

// ApkCmd dispatches jf apk subcommands: config, upload, and native apk operations.
func ApkCmd(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)
subcmd, remainingArgs := getCommandName(args)

var (
serverID string
err error
)
remainingArgs, serverID, err = coreutils.ExtractServerIdFromCommand(remainingArgs)
if err != nil {
return errorutils.CheckErrorf("failed to extract --server-id: %w", err)
}

remainingArgs, repoKey, err := coreutils.ExtractStringOptionFromArgs(remainingArgs, "repo")
if err != nil {
return errorutils.CheckErrorf("failed to extract --repo: %w", err)
}
remainingArgs, alpineVersion, err := coreutils.ExtractStringOptionFromArgs(remainingArgs, "alpine-version")
if err != nil {
return errorutils.CheckErrorf("failed to extract --alpine-version: %w", err)
}
remainingArgs, username, err := coreutils.ExtractStringOptionFromArgs(remainingArgs, "user")
if err != nil {
return errorutils.CheckErrorf("failed to extract --user: %w", err)
}
remainingArgs, password, err := coreutils.ExtractStringOptionFromArgs(remainingArgs, "password")
if err != nil {
return errorutils.CheckErrorf("failed to extract --password: %w", err)
}

var serverDetails *coreConfig.ServerDetails
if serverID != "" {
serverDetails, err = coreConfig.GetSpecificConfig(serverID, false, false)
} else {
serverDetails, err = coreConfig.GetDefaultServerConf()
}
if err != nil {
log.Warn("No JFrog server configured — skipping credential injection. Run: jf c add")
}

switch subcmd {
case "config":
return apkConfigSubCmd(remainingArgs, serverDetails, repoKey, alpineVersion, username, password)
case "upload":
return apkUploadSubCmd(c, remainingArgs, serverDetails, repoKey, alpineVersion, username, password)
default:
filteredArgs, buildConfiguration, err := build.ExtractBuildDetailsFromArgs(remainingArgs)
if err != nil {
return err
}
cmd := alpinecommand.NewApkCommand(subcmd).
SetArgs(filteredArgs).
SetServerDetails(serverDetails).
SetBuildConfiguration(buildConfiguration).
SetRepo(repoKey).
SetAlpineVersion(alpineVersion).
SetUsername(username).
SetPassword(password)
return commands.ExecWithPackageManager(cmd, "apk")
}
}

// apkConfigSubCmd handles jf apk config: downloads the Artifactory RSA public key and
// optionally writes it to disk.
func apkConfigSubCmd(args []string, serverDetails *coreConfig.ServerDetails, repoKey, alpineVersion, username, password string) error {
args, branch, err := coreutils.ExtractStringOptionFromArgs(args, "branch")
if err != nil {
return errorutils.CheckErrorf("failed to extract --branch: %w", err)
}
if branch == "" {
branch = "main"
}
_, applyFlag, err := coreutils.ExtractBoolFlagFromArgs(args, "apply")
if err != nil {
return errorutils.CheckErrorf("failed to extract --apply: %w", err)
}
cmd := alpinecommand.NewApkConfigCommand().
SetServerDetails(serverDetails).
SetRepo(repoKey).
SetAlpineVersion(alpineVersion).
SetBranch(branch).
SetUsername(username).
SetPassword(password).
SetApply(applyFlag)
return commands.ExecWithPackageManager(cmd, "apk")
}

// apkUploadSubCmd handles jf apk upload: publishes a local .apk file to Artifactory.
func apkUploadSubCmd(c *cli.Context, args []string, serverDetails *coreConfig.ServerDetails, repoKey, alpineVersion, username, password string) error {
filteredArgs, buildConfiguration, err := build.ExtractBuildDetailsFromArgs(args)
if err != nil {
return err
}

filteredArgs, branch, err := coreutils.ExtractStringOptionFromArgs(filteredArgs, "branch")
if err != nil {
return errorutils.CheckErrorf("failed to extract --branch: %w", err)
}
if branch == "" {
branch = "main"
}
filteredArgs, arch, err := coreutils.ExtractStringOptionFromArgs(filteredArgs, "arch")
if err != nil {
return errorutils.CheckErrorf("failed to extract --arch: %w", err)
}

if len(filteredArgs) != 1 {
return cliutils.WrongNumberOfArgumentsHandler(c)
}
filePath := filteredArgs[0]

cmd := alpinecommand.NewApkUploadCommand(filePath).
SetServerDetails(serverDetails).
SetBuildConfiguration(buildConfiguration).
SetRepo(repoKey).
SetAlpineVersion(alpineVersion).
SetBranch(branch).
SetArch(arch).
SetUsername(username).
SetPassword(password)
return commands.ExecWithPackageManager(cmd, "apk")
}

func pythonCmd(c *cli.Context, projectType project.ProjectType) error {
if show, err := cliutils.ShowCmdHelpIfNeeded(c, c.Args()); show || err != nil {
return err
Expand Down
82 changes: 82 additions & 0 deletions docs/buildtools/apkcommand/help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package apkcommand

var Usage = []string{
"apk config --server-id <id> --repo <repo-key> --alpine-version <vX.Y> [--branch <name>]",
"apk upload <file.apk> --repo <repo-key> --alpine-version <vX.Y> [--branch <name>] [--arch <arch>] [jf-flags]",
"apk add <packages...> [jf-flags]",
"apk upgrade [packages...] [jf-flags]",
"apk update [jf-flags]",
"apk fetch <packages...> [jf-flags]",
"apk search <pattern> [jf-flags]",
"apk del <packages...>",
"apk info [packages...]",
}

// GetDescription returns the short command description shown in jf --help.
func GetDescription() string {
return "Manage Alpine packages via Artifactory: configure trust, upload packages, and run native apk commands with credential injection and Build Info capture."
}

// GetAIDescription returns the extended description used by AI-assisted help.
func GetAIDescription() string {
return `jf apk provides three modes of operation for working with Artifactory Alpine repositories:

1. jf apk config — Bootstrap RSA key trust and repository URL.
Downloads the Artifactory RSA public key for the target Alpine repo,
writes it to /etc/apk/keys/, and adds the repo URL to /etc/apk/repositories.

2. jf apk upload — Publish a local .apk file to Artifactory.
Performs a direct REST PUT (no native apk binary required). Infers arch
and package metadata from the filename. Sets Artifactory properties and
records a Build Info artifact.

3. jf apk <native-subcommand> — Wrap native apk commands with Artifactory
credentials and Build Info capture.
Injects HTTP_AUTH into the apk subprocess environment.
For 'add' and 'upgrade', diffs the installed package list before/after
to record dependencies into a Build Info alpine module.`
}

// GetArguments returns the argument reference shown in jf apk --help.
func GetArguments() string {
return ` apk subcommand
Subcommands:
config — bootstrap RSA key trust and repository URL for an Artifactory Alpine repo
upload — publish a local .apk file to Artifactory
add — install packages (Build Info + HTTP_AUTH)
upgrade — upgrade packages (Build Info + HTTP_AUTH)
update — refresh index (HTTP_AUTH only)
fetch — download .apk files (HTTP_AUTH only)
search — search index (HTTP_AUTH only)
del — remove packages (passthrough)
info — query package info (passthrough)

Common flags (all subcommands):
--server-id JFrog server config ID (from jf c add). Default: active server.
--repo Artifactory Alpine repository key.
--alpine-version Alpine release, e.g. v3.20.
--user Override Artifactory username.
--password Override Artifactory password or token.

config-specific flags:
--branch Alpine repo branch (main|community|edge|<custom>). Default: main.

upload-specific flags:
--branch Alpine repo branch. Default: main.
--arch CPU architecture. Default: inferred from filename.
--build-name Build Info name. Env fallback: JFROG_CLI_BUILD_NAME.
--build-number Build Info number. Env fallback: JFROG_CLI_BUILD_NUMBER.
--project JFrog Projects key. Env fallback: JFROG_CLI_BUILD_PROJECT.

add/upgrade flags:
--build-name Build Info name.
--build-number Build Info number.
--project JFrog Projects key.

Examples:
jf apk config --server-id my-server --repo my-alpine-repo --alpine-version v3.20
jf apk upload ./myapp-1.0.0-r0.x86_64.apk --repo my-alpine-repo --alpine-version v3.20
jf apk add curl bash --server-id my-server --repo my-alpine-repo \
--build-name ci-image --build-number 42
jf apk upgrade --server-id my-server --repo my-alpine-repo`
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ require (
github.com/buger/jsonparser v1.2.0
github.com/gocarina/gocsv v0.0.0-20260607070740-0735908c6461
github.com/jfrog/archiver/v3 v3.6.3
github.com/jfrog/build-info-go v1.13.1-0.20260615080618-42488b58c305
github.com/jfrog/build-info-go v1.13.1-0.20260621212604-330cfe272ba6
github.com/jfrog/gofrog v1.7.6
github.com/jfrog/jfrog-cli-application v1.0.2-0.20260617073349-d68ee3120aa8
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260618051529-1b76b6ad2606
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260621212942-c4258681e69a
github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260615072209-8ccac4f0072e
github.com/jfrog/jfrog-cli-evidence v0.9.5-0.20260601141509-8df6c9a4bc9b
github.com/jfrog/jfrog-cli-platform-services v1.10.1-0.20260601140139-4cefb6add7b7
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -394,8 +394,8 @@ 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/build-info-go v1.13.1-0.20260621212604-330cfe272ba6 h1:q+1E4BWMzR053ipbG14E7JyNynVcK38meITma8PDc8U=
github.com/jfrog/build-info-go v1.13.1-0.20260621212604-330cfe272ba6/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 +406,8 @@ 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-artifactory v0.8.1-0.20260621212942-c4258681e69a h1:Ht4Hqv/XPgSdVLueKHBxU3iPGxMK9PkpWus5vpqw8yU=
github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260621212942-c4258681e69a/go.mod h1:REu8s+cg/Ro/gdBw7mgAUX3nXskYhLjzaFEv5QJOl4g=
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
16 changes: 16 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"
Apk = "apk"
Ping = "ping"
RtCurl = "rt-curl"
TemplateConsumer = "template-consumer"
Expand Down Expand Up @@ -161,6 +162,10 @@ const (
serverIdNpm = "server-id-npm"
disableTokenRefresh = "disable-token-refresh"

// Alpine (apk) flag keys — repo and branch reuse the existing flag constants.
apkAlpineVersion = "alpine-version"
apkArch = "arch"

passwordStdin = "password-stdin"
accessTokenStdin = "access-token-stdin"

Expand Down Expand Up @@ -776,6 +781,14 @@ var flagsMap = map[string]cli.Flag{
Name: serverId,
Usage: "[Optional] Server ID configured using the 'jf config' command. Used in native mode (JFROG_RUN_NATIVE=true) to identify the JFrog server for usage reporting.` `",
},
apkAlpineVersion: cli.StringFlag{
Name: apkAlpineVersion,
Usage: "[Optional] Alpine release version, e.g. v3.20.` `",
},
apkArch: cli.StringFlag{
Name: apkArch,
Usage: "[Optional] CPU architecture override (x86_64, aarch64, armhf, …). Inferred from filename by default.` `",
},
passwordStdin: cli.BoolFlag{
Name: passwordStdin,
Usage: "[Default: false] Set to true to provide the password via stdin.` `",
Expand Down Expand Up @@ -2243,6 +2256,9 @@ var commandFlags = map[string][]string{
Nix: {
BuildName, BuildNumber, module, Project, serverId,
},
Apk: {
serverId, repo, apkAlpineVersion, branch, apkArch, BuildName, BuildNumber, module, Project, user, password,
},
Stats: {
XrFormat, accessToken, serverId,
},
Expand Down
Loading