Skip to content

Commit 2622b87

Browse files
committed
Improved usability for skills commands
1 parent f1be918 commit 2622b87

File tree

13 files changed

+519
-75
lines changed

13 files changed

+519
-75
lines changed

cli/cli.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ func GetJfrogCliArtifactoryApp() components.App {
3535
})
3636
app.Subcommands = append(app.Subcommands, components.Namespace{
3737
Name: "skills",
38+
Aliases: []string{"skill"},
3839
Description: "Skills commands.",
40+
Hidden: true,
3941
Commands: skillsCLI.GetCommands(),
4042
Category: "Command Namespaces",
4143
})

cliutils/flagkit/flags.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -500,13 +500,16 @@ const (
500500
// Skills commands keys
501501
SkillsPublish = "skills-publish"
502502
SkillsInstall = "skills-install"
503+
SkillsSearch = "skills-search"
503504

504505
// Skills-specific flags
505-
version = "version"
506-
installPath = "path"
507-
signingKey = "signing-key"
508-
keyAlias = "key-alias"
509-
skillsQuiet = "skills-" + quiet
506+
version = "version"
507+
installPath = "path"
508+
signingKey = "signing-key"
509+
keyAlias = "key-alias"
510+
skillsQuiet = "skills-" + quiet
511+
propSearch = "prop"
512+
skillsFormat = "skills-" + Format
510513
)
511514

512515
var commandFlags = map[string][]string{
@@ -832,10 +835,13 @@ var commandFlags = map[string][]string{
832835
Format, OrderBy, FilterBy, OrderAsc, Limit, Offset, Includes, Project,
833836
},
834837
SkillsPublish: {
835-
url, user, password, accessToken, serverId, version, signingKey, keyAlias, skillsQuiet,
838+
url, user, password, accessToken, serverId, repo, version, signingKey, keyAlias, skillsQuiet,
836839
},
837840
SkillsInstall: {
838-
url, user, password, accessToken, serverId, version, installPath, skillsQuiet,
841+
url, user, password, accessToken, serverId, repo, version, installPath, skillsQuiet,
842+
},
843+
SkillsSearch: {
844+
url, user, password, accessToken, serverId, repo, skillsFormat, propSearch,
839845
},
840846
}
841847

@@ -1143,7 +1149,9 @@ var flagsMap = map[string]components.Flag{
11431149
installPath: components.NewStringFlag(installPath, "Custom install path for the skill. Default: current directory.", components.SetMandatoryFalse()),
11441150
signingKey: components.NewStringFlag(signingKey, "Path to PGP private key for signing evidence. Overrides EVD_SIGNING_KEY_PATH env var.", components.SetMandatoryFalse()),
11451151
keyAlias: components.NewStringFlag(keyAlias, "Alias for the signing key. Overrides EVD_KEY_ALIAS env var.", components.SetMandatoryFalse()),
1146-
skillsQuiet: components.NewBoolFlag(quiet, "[Default: $CI] Set to true to skip interactive prompts.", components.WithBoolDefaultValueFalse()),
1152+
skillsQuiet: components.NewBoolFlag(quiet, "[Default: $CI] Set to true to skip interactive prompts.", components.WithBoolDefaultValueFalse()),
1153+
skillsFormat: components.NewStringFlag(Format, "Output format: \"table\" (default) or \"json\".", components.SetMandatoryFalse()),
1154+
propSearch: components.NewBoolFlag(propSearch, "Use Artifactory property search (skill.name) instead of Skills API search.", components.WithBoolDefaultValueFalse()),
11471155
}
11481156

11491157
func GetCommandFlags(cmdKey string) []components.Flag {

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ replace github.com/gfleury/go-bitbucket-v1 => github.com/gfleury/go-bitbucket-v1
207207

208208
replace github.com/ktrysmt/go-bitbucket => github.com/ktrysmt/go-bitbucket v0.9.80
209209

210-
replace github.com/jfrog/jfrog-client-go => github.com/bhanurp/jfrog-client-go v1.49.1-0.20260305021800-71cf43b6e283
210+
// replace github.com/jfrog/jfrog-client-go => ../jfrog-client-go
211211

212212
// replace github.com/jfrog/jfrog-cli-core/v2 => github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251026182600-8a8c0428f538
213+
214+
replace github.com/jfrog/jfrog-client-go => github.com/bhanurp/jfrog-client-go v1.49.1-0.20260305114046-2b0ff03b6f49

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE=
9898
github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc=
9999
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
100100
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
101-
github.com/bhanurp/jfrog-client-go v1.49.1-0.20260305021800-71cf43b6e283 h1:YQ+U69LTJO5MriIPyevWeGJpSMWQXIJRNsDzzQpcF7Q=
102-
github.com/bhanurp/jfrog-client-go v1.49.1-0.20260305021800-71cf43b6e283/go.mod h1:sCE06+GngPoyrGO0c+vmhgMoVSP83UMNiZnIuNPzU8U=
101+
github.com/bhanurp/jfrog-client-go v1.49.1-0.20260305114046-2b0ff03b6f49 h1:5BguVDGfQR4O2pRGRl3tbDQOpA+0E9nEP688/Yjsw6o=
102+
github.com/bhanurp/jfrog-client-go v1.49.1-0.20260305114046-2b0ff03b6f49/go.mod h1:sCE06+GngPoyrGO0c+vmhgMoVSP83UMNiZnIuNPzU8U=
103103
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
104104
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
105105
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=

skills/cli/cli.go

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,32 @@ import (
44
"github.com/jfrog/jfrog-cli-artifactory/cliutils/flagkit"
55
"github.com/jfrog/jfrog-cli-artifactory/skills/commands/install"
66
"github.com/jfrog/jfrog-cli-artifactory/skills/commands/publish"
7+
"github.com/jfrog/jfrog-cli-artifactory/skills/commands/search"
78
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
89
)
910

1011
func GetCommands() []components.Command {
1112
return []components.Command{
1213
{
13-
Name: "publish",
14-
Flags: flagkit.GetCommandFlags(flagkit.SkillsPublish),
15-
Description: "Publish a skill to Artifactory.\n" +
16-
" After uploading, evidence is signed and attached to the artifact.\n" +
17-
" Provide a PGP private key via --signing-key (or EVD_SIGNING_KEY_PATH env var)\n" +
18-
" and an alias via --key-alias (or EVD_KEY_ALIAS env var).\n" +
19-
" If no key is provided, the upload succeeds but evidence creation is skipped.",
20-
Arguments: getPublishArguments(),
21-
Action: publish.RunPublish,
14+
Name: "publish",
15+
Flags: flagkit.GetCommandFlags(flagkit.SkillsPublish),
16+
Description: "Publish a skill to Artifactory. Signs and attaches evidence if a signing key is provided.",
17+
Arguments: getPublishArguments(),
18+
Action: publish.RunPublish,
2219
},
2320
{
24-
Name: "install",
25-
Flags: flagkit.GetCommandFlags(flagkit.SkillsInstall),
26-
Description: "Install a skill from Artifactory.\n" +
27-
" Evidence verification uses --use-artifactory-keys to pull the publisher's\n" +
28-
" public key from Artifactory automatically. No local signing keys are needed.\n" +
29-
" If verification fails, an interactive prompt lets you proceed or abort;\n" +
30-
" in CI/quiet mode the install fails automatically.",
31-
Arguments: getInstallArguments(),
32-
Action: install.RunInstall,
21+
Name: "install",
22+
Flags: flagkit.GetCommandFlags(flagkit.SkillsInstall),
23+
Description: "Install a skill from Artifactory. Verifies evidence using Artifactory keys automatically.",
24+
Arguments: getInstallArguments(),
25+
Action: install.RunInstall,
26+
},
27+
{
28+
Name: "search",
29+
Flags: flagkit.GetCommandFlags(flagkit.SkillsSearch),
30+
Description: "Search for skills across Artifactory repositories.",
31+
Arguments: getSearchArguments(),
32+
Action: search.RunSearch,
3333
},
3434
}
3535
}
@@ -40,9 +40,14 @@ func getPublishArguments() []components.Argument {
4040
Name: "path",
4141
Description: "Path to the skill folder containing SKILL.md.",
4242
},
43+
}
44+
}
45+
46+
func getSearchArguments() []components.Argument {
47+
return []components.Argument{
4348
{
44-
Name: "repo",
45-
Description: "Skills repository key in Artifactory.",
49+
Name: "query",
50+
Description: "Skill name or search term.",
4651
},
4752
}
4853
}
@@ -53,9 +58,5 @@ func getInstallArguments() []components.Argument {
5358
Name: "slug",
5459
Description: "Skill name/slug to install.",
5560
},
56-
{
57-
Name: "repo",
58-
Description: "Skills repository key in Artifactory.",
59-
},
6061
}
6162
}

skills/commands/install/install.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,12 @@ func unzipFile(src, dest string) error {
208208
_ = r.Close()
209209
}()
210210

211-
if err := os.MkdirAll(dest, 0755); err != nil {
211+
if err := os.MkdirAll(dest, 0750); err != nil {
212212
return err
213213
}
214214

215215
for _, f := range r.File {
216+
// #nosec G305 -- path traversal is checked immediately below
216217
fpath := filepath.Join(dest, f.Name)
217218

218219
if !strings.HasPrefix(filepath.Clean(fpath), filepath.Clean(dest)+string(os.PathSeparator)) {
@@ -226,7 +227,8 @@ func unzipFile(src, dest string) error {
226227
continue
227228
}
228229

229-
if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil {
230+
// #nosec G301 -- skill files need to be readable
231+
if err := os.MkdirAll(filepath.Dir(fpath), 0750); err != nil {
230232
return err
231233
}
232234

@@ -255,12 +257,14 @@ func extractFile(f *zip.File, dest string) error {
255257
_ = outFile.Close()
256258
}()
257259

260+
// #nosec G110 -- skill zip files are size-bounded by Artifactory upload limits
258261
_, err = io.Copy(outFile, rc)
259262
return err
260263
}
261264

262265
func copyDir(src, dst string) error {
263-
if err := os.MkdirAll(dst, 0755); err != nil {
266+
// #nosec G301 -- skill files need to be readable
267+
if err := os.MkdirAll(dst, 0750); err != nil {
264268
return err
265269
}
266270

@@ -293,6 +297,7 @@ func copyFile(src, dst string) error {
293297
_ = in.Close()
294298
}()
295299

300+
// #nosec G304 -- dst is constructed from validated unzip output path
296301
out, err := os.Create(dst)
297302
if err != nil {
298303
return err
@@ -307,25 +312,30 @@ func copyFile(src, dst string) error {
307312

308313
// RunInstall is the CLI action for `jf skills install`.
309314
func RunInstall(c *components.Context) error {
310-
if c.GetNumberOfArgs() < 2 {
311-
return fmt.Errorf("usage: jf skills install <slug> <repo> [options]")
315+
if c.GetNumberOfArgs() < 1 {
316+
return fmt.Errorf("usage: jf skills install <slug> [--repo <repo>] [options]")
312317
}
313318

314319
slug := c.GetArgumentAt(0)
315-
repoKey := c.GetArgumentAt(1)
316320

317321
serverDetails, err := common.GetServerDetails(c)
318322
if err != nil {
319323
return err
320324
}
321325

326+
quiet := common.IsQuiet(c)
327+
repoKey, err := common.ResolveRepo(serverDetails, c.GetStringFlagValue("repo"), quiet)
328+
if err != nil {
329+
return err
330+
}
331+
322332
cmd := NewInstallCommand().
323333
SetServerDetails(serverDetails).
324334
SetRepoKey(repoKey).
325335
SetSlug(slug).
326336
SetVersion(c.GetStringFlagValue("version")).
327337
SetInstallPath(c.GetStringFlagValue("path")).
328-
SetQuiet(common.IsQuiet(c))
338+
SetQuiet(quiet)
329339

330340
return cmd.Run()
331341
}

skills/commands/publish/publish.go

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
1818
"github.com/jfrog/jfrog-client-go/artifactory"
1919
"github.com/jfrog/jfrog-client-go/artifactory/services"
20-
serviceutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils"
20+
2121
"github.com/jfrog/jfrog-client-go/utils/log"
2222
)
2323

@@ -131,11 +131,8 @@ func (pc *PublishCommand) Run() error {
131131
return fmt.Errorf("failed to compute SHA256: %w", err)
132132
}
133133

134-
targetProps := fmt.Sprintf("skill.name=%s;skill.description=%s;skill.version=%s",
135-
slug, escapePropertyValue(meta.Description), version)
136-
137134
target := fmt.Sprintf("%s/%s/%s/", pc.repoKey, slug, version)
138-
if err := pc.upload(zipPath, target, targetProps); err != nil {
135+
if err := pc.upload(zipPath, target); err != nil {
139136
return fmt.Errorf("upload failed: %w", err)
140137
}
141138

@@ -260,6 +257,7 @@ func zipSkillFolder(skillDir, slug, version string) (string, error) {
260257
}
261258

262259
zipPath := filepath.Join(tmpDir, fmt.Sprintf("%s-%s.zip", slug, version))
260+
// #nosec G304 -- path is constructed from controlled temp directory and user-provided slug
263261
zipFile, err := os.Create(zipPath)
264262
if err != nil {
265263
return "", fmt.Errorf("failed to create zip file: %w", err)
@@ -306,7 +304,7 @@ func zipSkillFolder(skillDir, slug, version string) (string, error) {
306304
return err
307305
}
308306

309-
// #nosec G304 -- Path is constructed from user-provided skill directory
307+
// #nosec G304,G122 -- path is from user-provided skill directory via filepath.Walk
310308
file, err := os.Open(path)
311309
if err != nil {
312310
return err
@@ -358,7 +356,7 @@ func computeSHA256(path string) (string, error) {
358356
return hex.EncodeToString(h.Sum(nil)), nil
359357
}
360358

361-
func (pc *PublishCommand) upload(zipPath, target, targetProps string) error {
359+
func (pc *PublishCommand) upload(zipPath, target string) error {
362360
serviceManager, err := utils.CreateUploadServiceManager(pc.serverDetails, 1, 3, 0, false, nil)
363361
if err != nil {
364362
return err
@@ -368,14 +366,6 @@ func (pc *PublishCommand) upload(zipPath, target, targetProps string) error {
368366
uploadParams.Pattern = zipPath
369367
uploadParams.Target = target
370368
uploadParams.Flat = true
371-
props := serviceutils.NewProperties()
372-
for _, prop := range strings.Split(targetProps, ";") {
373-
parts := strings.SplitN(prop, "=", 2)
374-
if len(parts) == 2 {
375-
props.AddProperty(parts[0], parts[1])
376-
}
377-
}
378-
uploadParams.TargetProps = props
379369

380370
_, _, err = serviceManager.UploadFiles(artifactory.UploadServiceOptions{}, uploadParams)
381371
return err
@@ -441,20 +431,13 @@ func (pc *PublishCommand) attachEvidence(slug, version, sha256Hex string) {
441431
log.Info("Evidence successfully attached.")
442432
}
443433

444-
func escapePropertyValue(val string) string {
445-
val = strings.ReplaceAll(val, ";", "\\;")
446-
val = strings.ReplaceAll(val, "=", "\\=")
447-
return val
448-
}
449-
450434
// RunPublish is the CLI action for `jf skills publish`.
451435
func RunPublish(c *components.Context) error {
452-
if c.GetNumberOfArgs() < 2 {
453-
return fmt.Errorf("usage: jf skills publish <path-to-skill-folder> <repo> [options]")
436+
if c.GetNumberOfArgs() < 1 {
437+
return fmt.Errorf("usage: jf skills publish <path-to-skill-folder> [--repo <repo>] [options]")
454438
}
455439

456440
skillDir := c.GetArgumentAt(0)
457-
repoKey := c.GetArgumentAt(1)
458441
absDir, err := filepath.Abs(skillDir)
459442
if err != nil {
460443
return fmt.Errorf("invalid skill path: %w", err)
@@ -470,14 +453,20 @@ func RunPublish(c *components.Context) error {
470453
return err
471454
}
472455

456+
quiet := common.IsQuiet(c)
457+
repoKey, err := common.ResolveRepo(serverDetails, c.GetStringFlagValue("repo"), quiet)
458+
if err != nil {
459+
return err
460+
}
461+
473462
cmd := NewPublishCommand().
474463
SetServerDetails(serverDetails).
475464
SetRepoKey(repoKey).
476465
SetSkillDir(absDir).
477466
SetVersion(c.GetStringFlagValue("version")).
478467
SetSigningKey(c.GetStringFlagValue("signing-key")).
479468
SetKeyAlias(c.GetStringFlagValue("key-alias")).
480-
SetQuiet(common.IsQuiet(c))
469+
SetQuiet(quiet)
481470

482471
return cmd.Run()
483472
}

0 commit comments

Comments
 (0)