Skip to content

Commit f12f65d

Browse files
committed
feat: migrate CLI framework from urfave/cli to cobra + huh
- Replace urfave/cli/v2 with spf13/cobra v1.10.2 for CLI framework - Replace AlecAivazis/survey/v2 with charmbracelet/huh v0.8.0 for interactive forms - Auto-generated 'completion' command (bash, zsh, fish, powershell) - Migrate .goreleaser.yml: brews → homebrew_casks, dockers+docker_manifests → dockers_v2 - Update Dockerfile.goreleaser with TARGETPLATFORM ARG for dockers_v2 - Update all 14 command files, tests, golden files, and testscripts - Remove unused urfave/cli and survey dependencies via go mod tidy BREAKING CHANGE: CLI help output format changed (cobra conventions)
1 parent b43e7eb commit f12f65d

29 files changed

Lines changed: 803 additions & 1084 deletions

.goreleaser.yml

Lines changed: 27 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -59,22 +59,25 @@ changelog:
5959
- Merge pull request
6060
- Merge branch
6161

62-
brews:
62+
homebrew_casks:
6363
- name: apx
6464
repository:
6565
owner: infobloxopen
6666
name: homebrew-tap
6767
token: "{{ .Env.HOMEBREW_TAP_TOKEN }}"
6868
homepage: https://github.com/infobloxopen/apx
6969
description: "API Publishing eXperience CLI"
70-
license: Apache-2.0
71-
install: |
72-
bin.install "apx"
73-
(bash_completion/"apx").write `#{bin}/apx completion bash`
74-
(fish_completion/"apx.fish").write `#{bin}/apx completion fish`
75-
(zsh_completion/"_apx").write `#{bin}/apx completion zsh`
76-
test: |
77-
system "#{bin}/apx", "--version"
70+
hooks:
71+
post:
72+
install: |
73+
if OS.mac?
74+
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/apx"]
75+
end
76+
caveats: |
77+
The binary is not signed by Apple. On first run macOS may show a
78+
security warning. The post-install hook attempts to clear the
79+
quarantine flag automatically. If you still see the warning, run:
80+
xattr -dr com.apple.quarantine $(brew --caskroom)/apx
7881
7982
scoops:
8083
- name: apx
@@ -104,44 +107,24 @@ nfpms:
104107
dst: /etc/apx/apx.example.yaml
105108
type: config|noreplace
106109

107-
dockers:
108-
- image_templates:
109-
- "ghcr.io/infobloxopen/apx:{{ .Version }}-amd64"
110-
- "ghcr.io/infobloxopen/apx:latest-amd64"
110+
dockers_v2:
111+
- images:
112+
- "ghcr.io/infobloxopen/apx"
113+
tags:
114+
- "{{ .Version }}"
115+
- "{{ if not .IsNightly }}latest{{ end }}"
116+
platforms:
117+
- linux/amd64
118+
- linux/arm64
111119
dockerfile: Dockerfile.goreleaser
112-
use: buildx
113120
extra_files:
114121
- apx.example.yaml
115-
build_flag_templates:
116-
- "--platform=linux/amd64"
117-
- "--label=org.opencontainers.image.created={{.Date}}"
118-
- "--label=org.opencontainers.image.title={{.ProjectName}}"
119-
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
120-
- "--label=org.opencontainers.image.version={{.Version}}"
121-
- image_templates:
122-
- "ghcr.io/infobloxopen/apx:{{ .Version }}-arm64"
123-
- "ghcr.io/infobloxopen/apx:latest-arm64"
124-
dockerfile: Dockerfile.goreleaser
125-
use: buildx
126-
extra_files:
127-
- apx.example.yaml
128-
build_flag_templates:
129-
- "--platform=linux/arm64"
130-
- "--label=org.opencontainers.image.created={{.Date}}"
131-
- "--label=org.opencontainers.image.title={{.ProjectName}}"
132-
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
133-
- "--label=org.opencontainers.image.version={{.Version}}"
134-
goarch: arm64
135-
136-
docker_manifests:
137-
- name_template: "ghcr.io/infobloxopen/apx:{{ .Version }}"
138-
image_templates:
139-
- "ghcr.io/infobloxopen/apx:{{ .Version }}-amd64"
140-
- "ghcr.io/infobloxopen/apx:{{ .Version }}-arm64"
141-
- name_template: "ghcr.io/infobloxopen/apx:latest"
142-
image_templates:
143-
- "ghcr.io/infobloxopen/apx:latest-amd64"
144-
- "ghcr.io/infobloxopen/apx:latest-arm64"
122+
labels:
123+
"org.opencontainers.image.created": "{{.Date}}"
124+
"org.opencontainers.image.title": "{{.ProjectName}}"
125+
"org.opencontainers.image.revision": "{{.FullCommit}}"
126+
"org.opencontainers.image.version": "{{.Version}}"
127+
"org.opencontainers.image.source": "{{.GitURL}}"
145128

146129
release:
147130
github:

Dockerfile.goreleaser

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
FROM alpine:latest
22

3+
# dockers_v2 passes TARGETPLATFORM as a build arg (e.g. linux/amd64)
4+
ARG TARGETPLATFORM
5+
36
# Install necessary tools
47
RUN apk --no-cache add \
58
ca-certificates \
@@ -19,8 +22,8 @@ RUN apk add --no-cache go && \
1922
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest && \
2023
apk del go
2124

22-
# Copy the pre-built binary from GoReleaser
23-
COPY apx /usr/local/bin/apx
25+
# Copy the pre-built binary from GoReleaser (dockers_v2 places binaries under $TARGETPLATFORM/)
26+
COPY ${TARGETPLATFORM}/apx /usr/local/bin/apx
2427

2528
# Copy configuration examples
2629
COPY apx.example.yaml /etc/apx/apx.example.yaml

cmd/apx/commands/add.go

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,44 @@
11
package commands
22

33
import (
4-
"fmt"
54
"strings"
65

76
"github.com/infobloxopen/apx/internal/config"
87
"github.com/infobloxopen/apx/internal/ui"
9-
"github.com/urfave/cli/v2"
8+
"github.com/spf13/cobra"
109
)
1110

12-
// AddCommand returns the add command for adding dependencies
13-
func AddCommand() *cli.Command {
14-
return &cli.Command{
15-
Name: "add",
16-
Usage: "Add a dependency to apx.yaml and apx.lock",
17-
Description: `Add a schema module dependency to the project.
11+
func newAddCmd() *cobra.Command {
12+
return &cobra.Command{
13+
Use: "add <module-path>[@version]",
14+
Short: "Add a dependency to apx.yaml and apx.lock",
15+
Long: `Add a schema module dependency to the project.
1816
1917
The dependency is added to apx.yaml and the version is locked in apx.lock.
2018
2119
Examples:
2220
apx add proto/payments/ledger/v1@v1.2.3
2321
apx add proto/payments/wallet/v1 # Uses latest version
2422
apx add openapi/customer/accounts/v2@v2.0.0`,
25-
ArgsUsage: "<module-path>[@version]",
26-
Action: addAction,
23+
Args: cobra.ExactArgs(1),
24+
RunE: addAction,
2725
}
2826
}
2927

30-
func addAction(c *cli.Context) error {
31-
if c.NArg() == 0 {
32-
ui.Error("Module path required")
33-
return fmt.Errorf("usage: apx add <module-path>[@version]")
34-
}
35-
36-
arg := c.Args().First()
28+
func addAction(cmd *cobra.Command, args []string) error {
29+
arg := args[0]
3730

38-
// Parse module path and version
3931
var modulePath, version string
4032
if strings.Contains(arg, "@") {
4133
parts := strings.SplitN(arg, "@", 2)
4234
modulePath = parts[0]
4335
version = parts[1]
4436
} else {
4537
modulePath = arg
46-
version = "" // Will fetch latest
4738
}
4839

49-
// Create dependency manager
5040
mgr := config.NewDependencyManager("apx.yaml", "apx.lock")
5141

52-
// Add dependency
5342
if err := mgr.Add(modulePath, version); err != nil {
5443
ui.Error("Failed to add dependency: %v", err)
5544
return err

cmd/apx/commands/breaking.go

Lines changed: 18 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,83 +7,60 @@ import (
77

88
"github.com/infobloxopen/apx/internal/ui"
99
"github.com/infobloxopen/apx/internal/validator"
10-
"github.com/urfave/cli/v2"
10+
"github.com/spf13/cobra"
1111
)
1212

13-
// BreakingCommand returns the breaking changes command
14-
func BreakingCommand() *cli.Command {
15-
return &cli.Command{
16-
Name: "breaking",
17-
Usage: "Check for breaking changes in schema files",
18-
ArgsUsage: "[path]",
19-
Flags: []cli.Flag{
20-
&cli.StringFlag{
21-
Name: "against",
22-
Usage: "git reference or path to compare against",
23-
Required: true,
24-
},
25-
&cli.StringFlag{
26-
Name: "format",
27-
Aliases: []string{"f"},
28-
Usage: "Schema format (proto, openapi, avro, jsonschema, parquet)",
29-
},
30-
},
31-
Action: breakingAction,
13+
func newBreakingCmd() *cobra.Command {
14+
cmd := &cobra.Command{
15+
Use: "breaking [path]",
16+
Short: "Check for breaking changes in schema files",
17+
Args: cobra.MaximumNArgs(1),
18+
RunE: breakingAction,
3219
}
20+
cmd.Flags().String("against", "", "git reference or path to compare against")
21+
_ = cmd.MarkFlagRequired("against")
22+
cmd.Flags().StringP("format", "f", "", "Schema format (proto, openapi, avro, jsonschema, parquet)")
23+
return cmd
3324
}
3425

35-
func breakingAction(c *cli.Context) error {
36-
// Get path from args or default to current directory
26+
func breakingAction(cmd *cobra.Command, args []string) error {
3727
path := "."
38-
if c.Args().Len() > 0 {
39-
path = c.Args().First()
28+
if len(args) > 0 {
29+
path = args[0]
4030
}
4131

42-
// Get the baseline to compare against
43-
against := c.String("against")
44-
if against == "" {
45-
return fmt.Errorf("--against flag is required")
46-
}
32+
against, _ := cmd.Flags().GetString("against")
4733

48-
// Resolve absolute paths
4934
absPath, err := filepath.Abs(path)
5035
if err != nil {
5136
return fmt.Errorf("failed to resolve path: %w", err)
5237
}
5338

54-
// Check if path exists
5539
if _, err := os.Stat(absPath); os.IsNotExist(err) {
5640
return fmt.Errorf("path does not exist: %s", absPath)
5741
}
5842

59-
// Create toolchain resolver with default settings
6043
resolver := validator.NewToolchainResolver()
61-
62-
// Create validator
6344
v := validator.NewValidator(resolver)
6445

65-
// Detect or use specified format
6646
format := validator.FormatUnknown
67-
if formatStr := c.String("format"); formatStr != "" {
47+
if formatStr, _ := cmd.Flags().GetString("format"); formatStr != "" {
6848
format = validator.SchemaFormat(formatStr)
6949
} else {
7050
format = validator.DetectFormat(absPath)
7151
}
7252

73-
// Validate format
7453
if format == validator.FormatUnknown {
7554
return fmt.Errorf("could not detect schema format for: %s\nPlease specify format with --format flag", absPath)
7655
}
7756

78-
// Run breaking change detection
7957
ui.Info("Checking %s for breaking changes against: %s", format, against)
8058

81-
err = v.Breaking(absPath, against, format)
82-
if err != nil {
59+
if err := v.Breaking(absPath, against, format); err != nil {
8360
ui.Error("Breaking changes detected: %v", err)
8461
return err
8562
}
8663

87-
ui.Success(" No breaking changes detected")
64+
ui.Success("\u2713 No breaking changes detected")
8865
return nil
8966
}

cmd/apx/commands/catalog.go

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,28 @@ package commands
33
import (
44
"github.com/infobloxopen/apx/internal/config"
55
"github.com/infobloxopen/apx/internal/ui"
6-
"github.com/urfave/cli/v2"
6+
"github.com/spf13/cobra"
77
)
88

9-
// CatalogCommand returns the catalog command with subcommands
10-
func CatalogCommand() *cli.Command {
11-
return &cli.Command{
12-
Name: "catalog",
13-
Usage: "Catalog operations",
14-
Subcommands: []*cli.Command{
15-
{
16-
Name: "build",
17-
Usage: "Build module catalog",
18-
Action: catalogBuildAction,
19-
},
20-
},
9+
func newCatalogCmd() *cobra.Command {
10+
cmd := &cobra.Command{
11+
Use: "catalog",
12+
Short: "Catalog operations",
2113
}
14+
cmd.AddCommand(newCatalogBuildCmd())
15+
return cmd
2216
}
2317

24-
func catalogBuildAction(c *cli.Context) error {
25-
cfg, err := loadConfig(c)
18+
func newCatalogBuildCmd() *cobra.Command {
19+
return &cobra.Command{
20+
Use: "build",
21+
Short: "Build module catalog",
22+
RunE: catalogBuildAction,
23+
}
24+
}
25+
26+
func catalogBuildAction(cmd *cobra.Command, args []string) error {
27+
cfg, err := loadConfig(cmd)
2628
if err != nil {
2729
ui.Error("Failed to load config: %v", err)
2830
return err
@@ -32,7 +34,6 @@ func catalogBuildAction(c *cli.Context) error {
3234
}
3335

3436
func buildCatalog(cfg *config.Config) error {
35-
// TODO: Implement catalog building in internal/catalog package
3637
ui.Info("Building module catalog...")
3738
ui.Success("Module catalog built successfully")
3839
return nil

cmd/apx/commands/commands.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package commands
22

3-
// This file provides command constructors for the APX CLI.
4-
// Each command's implementation is in its own file (init.go, lint.go, etc.)
5-
// This separation keeps the codebase organized and maintainable.
6-
7-
// All command constructors - implementations are in separate files
3+
// This file previously held urfave/cli command constructors.
4+
// With the migration to cobra, all commands are registered in root.go
5+
// via NewRootCmd() → cmd.AddCommand(...).

cmd/apx/commands/common.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package commands
22

33
import (
4-
"github.com/infobloxopen/apx/internal/config"
5-
"github.com/urfave/cli/v2"
4+
"github.com/infobloxopen/apx/internal/config"
5+
"github.com/spf13/cobra"
66
)
77

8-
// loadConfig loads the configuration file
9-
func loadConfig(c *cli.Context) (*config.Config, error) {
10-
configPath := c.String("config")
8+
func loadConfig(cmd *cobra.Command) (*config.Config, error) {
9+
configPath, _ := cmd.Root().PersistentFlags().GetString("config")
10+
if configPath == "" {
11+
configPath = "apx.yaml"
12+
}
1113
return config.Load(configPath)
1214
}

0 commit comments

Comments
 (0)