Skip to content
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2449217
RTECO-1648 - Implement jf agent apm command with JFrog Artifactory au…
udaykb2 Jul 27, 2026
9daad04
Merge branch 'main' into RTECO-1648-apm-support-implementation
udaykb2 Jul 28, 2026
ea04030
RTECO-1648 - Fix gosec findings in apm package (Go-Sec CI check)
udaykb2 Jul 28, 2026
859187e
RTECO-1648 - Bump jfrog-cli-core to the pushed RTECO-1648 commit
udaykb2 Jul 28, 2026
ff8d85e
RTECO-1648 - Drop direct-credential flags from apm, rename flagkit keys
udaykb2 Jul 28, 2026
0d839d2
RTECO-1648 - Replace regexp version parsing with a plain function
udaykb2 Jul 28, 2026
8f9e3b1
RTECO-1648 - Drop --server-id/--repo from apm, rename flagkit key, cl…
udaykb2 Jul 28, 2026
192cf73
RTECO-1648 - Fix apm.yml registries: block silently discarded with de…
udaykb2 Jul 28, 2026
700376f
Fix TestResolveRepoNameFromRegistry failing on Windows
udaykb2 Jul 30, 2026
f6e3616
Add AI help descriptions for APM commands (install, publish, update)
udaykb2 Jul 30, 2026
fabe9b1
Merge branch 'main' into RTECO-1648-apm-support-implementation
udaykb2 Jul 30, 2026
17333ab
RTECO-1648 - Replace AQL checksum lookup with HEAD, fix PR #518 revie…
udaykb2 Aug 1, 2026
f218bf8
Document apm passthrough capability and fix its --help handling
udaykb2 Aug 1, 2026
1d4c81e
Merge branch 'main' into RTECO-1648-apm-support-implementation
udaykb2 Aug 1, 2026
93293ae
Isolate AgentPackages const from the PackageTypes alignment group
udaykb2 Aug 1, 2026
e3d6c56
Add agent-apm entry to packageManagerConfigs
udaykb2 Aug 1, 2026
5dfbbf6
Fix 3 new CodeRabbit findings on PR #518
udaykb2 Aug 1, 2026
f28d7ac
Deduplicate repeated string literals and trim comments in apm code
udaykb2 Aug 1, 2026
40891e2
Fix apm publish artifact linkage in Artifactory build browser
udaykb2 Aug 2, 2026
727005d
Add comprehensive tests for BuildRegistryEntry and token generation
udaykb2 Aug 2, 2026
6964fe1
Fix APM access token generation: wrong endpoint and response field
udaykb2 Aug 2, 2026
db4844f
Detect APM validation failures that exit with code 0
udaykb2 Aug 2, 2026
86b9aa6
Resolve build-info repo name from --registry/default instead of host-…
udaykb2 Aug 2, 2026
bfdcddb
Unify apm install/publish build-info module IDs to name:version
udaykb2 Aug 2, 2026
dab7393
Fix data race sharing one HttpClientDetails across concurrent checksu…
udaykb2 Aug 3, 2026
cdbc586
Add local-zip fallback for publish checksum, matching cargo/ruby's pa…
udaykb2 Aug 3, 2026
5fd8b16
Fix build-info gaps in apm install/publish/update: dry-run, global,
udaykb2 Aug 3, 2026
5cc0db5
Merge branch 'main' into RTECO-1648-apm-support-implementation
udaykb2 Aug 4, 2026
e3b9a34
Give apm dependencies a single dev/prod/transitive scope, pnpm-style
udaykb2 Aug 4, 2026
04dcc73
Add real command examples to install/update help, matching publish's
udaykb2 Aug 4, 2026
20eccdf
Only log APM build-info skip messages when collection is enabled
udaykb2 Aug 4, 2026
207f450
Remove APM --global build-info skip special-casing
udaykb2 Aug 4, 2026
be0940c
Add apt command package from main for CLI compatibility
udaykb2 Aug 4, 2026
3a4ca0f
Merge main into RTECO-1648-apm-support-implementation
udaykb2 Aug 14, 2026
493ba9c
Remove separate folder for passthrough
udaykb2 Aug 14, 2026
2229d1c
RTECO-1648 - Bump min supported apm version to 0.23.0 and rename setu…
udaykb2 Aug 16, 2026
e2fe466
Merge branch 'main' into RTECO-1648-apm-support-implementation
udaykb2 Aug 16, 2026
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
43 changes: 43 additions & 0 deletions agent/apm/cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cli

import (
"github.com/jfrog/jfrog-cli-artifactory/agent/apm/commands/install"
"github.com/jfrog/jfrog-cli-artifactory/agent/apm/commands/publish"
"github.com/jfrog/jfrog-cli-artifactory/agent/apm/commands/update"
"github.com/jfrog/jfrog-cli-artifactory/cliutils/flagkit"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
)

// GetSubCommands returns the leaf commands for `jf agent apm`. Commands not listed here (e.g.
// "lock", which resolves but deploys nothing) fall through to the parent's passthrough handler.
func GetSubCommands() []components.Command {
return []components.Command{
{
Name: "install",
Flags: flagkit.GetCommandFlags(flagkit.AgentApm),
// SkipFlagParsing so apm-native flags (e.g. --frozen) that aren't in jf's own
// declared flag set above aren't rejected by urfave/cli before reaching apm.
// RunInstall extracts jf's own flags manually via ExtractApmSubcommandOptions.
SkipFlagParsing: true,
Description: "Install APM packages with JFrog Artifactory authentication.",
AIDescription: install.GetAIDescription(),
Action: install.RunInstall,
},
{
Name: "publish",
Flags: flagkit.GetCommandFlags(flagkit.AgentApm),
SkipFlagParsing: true,
Description: "Publish an APM package to JFrog Artifactory.",
AIDescription: publish.GetAIDescription(),
Action: publish.RunPublish,
},
{
Name: "update",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need to support update command for build info collection? can you please give an example here why ?

Flags: flagkit.GetCommandFlags(flagkit.AgentApm),
SkipFlagParsing: true,
Description: "Refresh APM dependencies to their latest matching refs, with build-info collection.",
AIDescription: update.GetAIDescription(),
Action: update.RunUpdate,
},
}
}
31 changes: 31 additions & 0 deletions agent/apm/cli/help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package cli

func GetDescription() string {
return "Agent Package Manager (APM) commands with JFrog Artifactory authentication."
}

func GetAIDescription() string {
return `Run apm against Artifactory-backed registries with credentials injected automatically. Dedicated subcommands install, publish, and update also collect build-info when --build-name and --build-number are set; every other apm command is forwarded with the same authenticated registry access but no build-info collection.

When to use:
- Running apm install / publish / update with Artifactory auth and optional build-info.
- Running any other apm command (lock, outdated, audit, doctor, view, marketplace, mcp, ...) through jf for authenticated registry access.

Prerequisites:
- apm CLI installed and on PATH.
- Registry configured via 'jf setup agent-apm' or an apm.yml registries: block.
- A configured JFrog Platform server (jf c add / jf login), or pass --server-id.

Common patterns:
$ jf agent apm install --build-name=my-build --build-number=1
$ jf agent apm publish --package my-org/my-package --build-name=my-build --build-number=1
$ jf agent apm update --yes --build-name=my-build --build-number=1
$ jf agent apm lock
$ jf agent apm outdated

Gotchas:
- Build-info is collected only by install, publish, and update, and only when both --build-name and --build-number are provided; publish it afterwards with 'jf rt build-publish'.
- 'jf agent apm <command> --help' shows that command's own help; 'apm --help' lists every native apm command reachable this way.

Related: jf setup agent-apm, jf agent apm install, jf agent apm publish, jf agent apm update, jf rt build-publish`
}
33 changes: 33 additions & 0 deletions agent/apm/commands/install/help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package install

func GetDescription() string {
return "Install APM packages with JFrog Artifactory authentication."
}

func GetAIDescription() string {
return `Install packages declared in apm.yml with authenticated access to Artifactory agentpackages repositories, and optionally record a build-info of installed dependencies.

When to use:
- Installing packages into an agent project that has apm.yml configured.
- Pulling private or curated packages from Artifactory.
- Capturing build-info for an install by passing --build-name and --build-number.

Prerequisites:
- apm CLI installed and on PATH.
- A registry declared in apm.yml's registries: block, or configured via 'jf setup agent-apm'.
- Read permission on the source Artifactory agentpackages repository.

Common patterns:
$ jf agent apm install
$ jf agent apm install my-org/my-package#1.0.0 --target claude
$ jf agent apm install "my-org/my-package#^1.0.0" --target claude --build-name=my-build --build-number=1
$ jf agent apm install --dev my-org/my-dev-tool#1.0.0
$ jf agent apm install --dry-run

Gotchas:
- A bare tag (#1.0.0) is an exact pin: apm update never moves it. Use a semver range (#^1.0.0, #~1.0.0) if later updates should pick up newer matching versions.
- --dry-run previews the install without changing anything and skips build-info (nothing real to record).
- Build-info is collected only when both --build-name and --build-number are provided; publish it afterwards with 'jf rt build-publish'.

Related: jf agent apm publish, jf agent apm update, jf setup agent-apm, jf rt build-publish`
}
129 changes: 129 additions & 0 deletions agent/apm/commands/install/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package install

import (
"fmt"
"os"
"path/filepath"
"strings"

apmcommon "github.com/jfrog/jfrog-cli-artifactory/agent/apm/common"
agentcommon "github.com/jfrog/jfrog-cli-artifactory/agent/common"
buildUtils "github.com/jfrog/jfrog-cli-core/v2/common/build"
"github.com/jfrog/jfrog-cli-core/v2/common/commands"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/jfrog/jfrog-client-go/utils/log"
)

// apmSubcommand is the apm subcommand this package always drives.
const apmSubcommand = "install"

// ApmInstallCommand runs `apm install` with JFrog Artifactory authentication and collects
// build-info from the resulting apm.lock.yaml. Never accepts --repo; a registry must already be
// declared via jf setup agent-apm or apm.yml's own registries: block.
type ApmInstallCommand struct {
args []string
serverDetails *config.ServerDetails
buildConfiguration *buildUtils.BuildConfiguration
}

func NewApmInstallCommand() *ApmInstallCommand {
return &ApmInstallCommand{}
}

func (c *ApmInstallCommand) SetArgs(args []string) *ApmInstallCommand {
c.args = args
return c
}

func (c *ApmInstallCommand) SetServerDetails(serverDetails *config.ServerDetails) *ApmInstallCommand {
c.serverDetails = serverDetails
return c
}

func (c *ApmInstallCommand) SetBuildConfiguration(buildConfiguration *buildUtils.BuildConfiguration) *ApmInstallCommand {
c.buildConfiguration = buildConfiguration
return c
}

func (c *ApmInstallCommand) CommandName() string {
return apmcommon.CommandNamePrefix + apmSubcommand
}

func (c *ApmInstallCommand) ServerDetails() (*config.ServerDetails, error) {
return c.serverDetails, nil
}

func (c *ApmInstallCommand) Run() error {
log.Info("Running apm install...")

if err := apmcommon.RunApmSubcommandWithAuth(apmSubcommand, c.args, c.serverDetails); err != nil {
return fmt.Errorf("run apm install: %w", err)
}

// Only mention / collect build-info when the user asked for it (--build-name/--build-number or env).
collectBuildInfo, err := apmcommon.ShouldCollectBuildInfo(c.buildConfiguration)
if err != nil {
log.Warn("apm install completed, but could not determine build-info collection state:", err.Error())
} else if collectBuildInfo {
if apmcommon.IsDryRunArg(c.args) {
log.Info("apm install: --dry-run - nothing was installed, skipping build-info recording.")
} else if workingDir, wdErr := os.Getwd(); wdErr != nil {
log.Warn("apm install completed, but could not determine working directory for build info:", wdErr.Error())
} else {
lockfileDir := workingDir
if rootDir := rootDirFromArgs(c.args); rootDir != "" {
lockfileDir = rootDir
if !filepath.IsAbs(lockfileDir) {
lockfileDir = filepath.Join(workingDir, lockfileDir)
}
}
lockfilePath := filepath.Join(lockfileDir, apmcommon.ApmLockfileName)
manifestPath := filepath.Join(workingDir, apmcommon.ApmManifestName)
if biErr := apmcommon.CollectAndSaveInstallBuildInfo(lockfilePath, manifestPath, c.serverDetails, c.buildConfiguration); biErr != nil {
log.Warn("apm install completed, but build info collection failed:", biErr.Error())
}
}
}

log.Info("apm install finished successfully.")
return nil
}

// rootDirFromArgs extracts the value of --root, which redirects apm_modules/ and apm.lock.yaml
// under DIR instead of the working directory (apm.yml and .apm/ still resolve from $PWD).
// Returns "" if --root isn't present.
func rootDirFromArgs(args []string) string {
for i, arg := range args {
if arg == "--root" && i+1 < len(args) {
return args[i+1]
}
if cut, ok := strings.CutPrefix(arg, "--root="); ok {
return cut
}
}
return ""
}

// RunInstall is the CLI action handler for `jf agent apm install`.
func RunInstall(c *components.Context) error {
if apmcommon.IsHelpRequest(c.Arguments) {
return apmcommon.RunApmCommand(nil, apmSubcommand, []string{apmcommon.HelpFlag})
}

opts, err := apmcommon.ExtractApmSubcommandOptions(c.Arguments)
if err != nil {
return err
}
serverDetails, err := agentcommon.GetServerDetails(c)
if err != nil {
return err
}

cmd := NewApmInstallCommand().
SetArgs(opts.RemainingArgs).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you mean by remaining args? it seems vague can you please change the name.

SetServerDetails(serverDetails).
SetBuildConfiguration(opts.BuildConfig)

return commands.ExecWithPackageManager(cmd, apmcommon.PackageManagerID)
}
26 changes: 26 additions & 0 deletions agent/apm/commands/install/install_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package install

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestRootDirFromArgs(t *testing.T) {
tests := []struct {
name string
args []string
want string
}{
{name: "--root with space form", args: []string{"--root", "./out"}, want: "./out"},
{name: "--root= form", args: []string{"--root=/tmp/build"}, want: "/tmp/build"},
{name: "no --root flag", args: []string{"--dry-run"}, want: ""},
{name: "--root as last arg with no value", args: []string{"--root"}, want: ""},
{name: "empty args", args: []string{}, want: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, rootDirFromArgs(tt.args))
})
}
}
80 changes: 80 additions & 0 deletions agent/apm/commands/passthrough/passthrough.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package passthrough

import (
apmcommon "github.com/jfrog/jfrog-cli-artifactory/agent/apm/common"
agentcommon "github.com/jfrog/jfrog-cli-artifactory/agent/common"
"github.com/jfrog/jfrog-cli-core/v2/common/commands"
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
"github.com/jfrog/jfrog-client-go/utils/log"
)

// ApmPassthroughCommand forwards any apm subcommand with auth environment injected.
type ApmPassthroughCommand struct {
subcmd string
args []string
serverDetails *config.ServerDetails
}

func NewApmPassthroughCommand() *ApmPassthroughCommand {
return &ApmPassthroughCommand{}
}

func (c *ApmPassthroughCommand) SetSubcmd(subcmd string) *ApmPassthroughCommand {
c.subcmd = subcmd
return c
}

func (c *ApmPassthroughCommand) SetArgs(args []string) *ApmPassthroughCommand {
c.args = args
return c
}

func (c *ApmPassthroughCommand) SetServerDetails(serverDetails *config.ServerDetails) *ApmPassthroughCommand {
c.serverDetails = serverDetails
return c
}

func (c *ApmPassthroughCommand) CommandName() string {
return apmcommon.CommandNamePrefix + c.subcmd
}

func (c *ApmPassthroughCommand) ServerDetails() (*config.ServerDetails, error) {
return c.serverDetails, nil
}

func (c *ApmPassthroughCommand) Run() error {
log.Info("Running apm " + apmcommon.SanitizeLogValue(c.subcmd) + "...")
return apmcommon.RunApmSubcommandWithAuth(c.subcmd, c.args, c.serverDetails)
}

// RunApmPassthroughDefault handles any `jf agent apm <subcmd>` not among install/publish/update.
// Auth always comes from the default configured JFrog server; passthrough takes no flags of its
// own, so nothing is extracted from c.Arguments beyond the subcommand.
func RunApmPassthroughDefault(c *components.Context) error {
if len(c.Arguments) == 0 {
return apmcommon.RunApmCommand(nil, apmcommon.HelpFlag, nil)
}

subcmd := c.Arguments[0]
if apmcommon.IsHelpRequest([]string{subcmd}) {
return apmcommon.RunApmCommand(nil, apmcommon.HelpFlag, nil)
}
// Show help without resolving server/auth, which a help request never needs. Forward the
// full remaining arg tail so nested commands like "deps why" get their own help, not "deps"'s.
if apmcommon.IsHelpRequest(c.Arguments[1:]) {
return apmcommon.RunApmCommand(nil, subcmd, c.Arguments[1:])
}

serverDetails, err := agentcommon.GetServerDetails(c)
if err != nil {
return err
}

cmd := NewApmPassthroughCommand().
SetSubcmd(subcmd).
SetArgs(c.Arguments[1:]).
SetServerDetails(serverDetails)

return commands.ExecWithPackageManager(cmd, apmcommon.PackageManagerID)
}
33 changes: 33 additions & 0 deletions agent/apm/commands/publish/help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package publish

func GetDescription() string {
return "Publish an APM package to JFrog Artifactory."
}

func GetAIDescription() string {
return `Publish an agent package to an Artifactory agentpackages repository with authenticated access, and optionally record a build-info of the published package.

When to use:
- Publishing custom agent packages (skills, tools, extensions) for reuse across projects.
- Creating versioned, reproducible deployments of agent components.
- Capturing build-info for a publish by passing --build-name and --build-number.

Prerequisites:
- apm CLI installed and on PATH.
- An apm.yml in the package directory (or a parent) declaring name, version, and description.
- Write permission on the Artifactory agentpackages repository.
- Registry configured via 'jf setup agent-apm' or an apm.yml registries: block.

Common patterns:
$ jf agent apm publish --package my-org/my-package
$ jf agent apm publish --package my-org/my-package --build-name=my-build --build-number=1
$ jf agent apm publish --package my-org/my-package --build-name=my-build --build-number=1 --module=my-module
$ jf agent apm publish --package my-org/my-package --dry-run

Gotchas:
- --package is required (owner/name); it is not inferred from a bare positional argument.
- --dry-run previews the upload without publishing and skips build-info.
- Build-info is collected only when both --build-name and --build-number are provided; optional --module groups packages under one module. Publish afterwards with 'jf rt build-publish'.

Related: jf agent apm install, jf agent apm update, jf setup agent-apm, jf rt build-publish`
}
Loading
Loading