diff --git a/.gitignore b/.gitignore index 07c72f6412..b0810d95d4 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,4 @@ scripts/Brewfile.lock.json test/fixtures/**/go.sum .cursor .windsurf -.claude \ No newline at end of file +.claude diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea7c67a457..08919bfea0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -283,20 +283,14 @@ automatically be pulled into Snyk CLI as pull requests. ### CLI help command files (`help/cli-commands`) The Go CLI reads user-facing command help from markdown files under `help/cli-commands/`. These files are synced from -GitBook into this repository (see the `sync-cli-help-to-user-docs` workflow). At build time, the CLI embeds a manifest -of available help files (`cliv2/pkg/helpdocs/manifest.txt`) and uses it to decide whether to show legacy GitBook help -or native Cobra help for a given command. - -When you add, remove, or rename files in `help/cli-commands/`, regenerate the manifest and commit the result: - -```sh -make -C cliv2 helpdocs-manifest -git add cliv2/pkg/helpdocs/manifest.txt -``` - -`make -C cliv2 test` and `make build` run this target automatically, but you still need to commit the updated -`manifest.txt` when it changes. Go tests in `cliv2/pkg/helpdocs` verify that the manifest stays in sync with -`help/cli-commands/`. +GitBook into this repository (see the `sync-cli-help-to-user-docs` workflow). At **build** time, the Makefile copies +`help/cli-commands/` into `cliv2/internal/helpdocs/cli-commands/` so the Go embed can read them, then removes the copy +afterward. Go unit tests read `help/cli-commands/` from disk (or use a small in-memory fixture when that directory is +unavailable), so `make -C cliv2 test` does not run that copy step. The embedded filenames decide whether to show legacy +GitBook help or native Cobra help for a given command. + +When you add, remove, or rename files in `help/cli-commands/`, no extra manifest step is required. Help routing tests +pick up the changes on the next `make -C cliv2 test`; the shipped binary picks them up on the next `make build`. To test help routing locally after building: diff --git a/cliv2/.gitignore b/cliv2/.gitignore index bd3d19c8ad..543b08a8c2 100644 --- a/cliv2/.gitignore +++ b/cliv2/.gitignore @@ -12,3 +12,4 @@ _cache bin internal/embedded/_data /.bin/ +internal/helpdocs/cli-commands diff --git a/cliv2/Makefile b/cliv2/Makefile index 9668aa34c0..7c032dd98b 100644 --- a/cliv2/Makefile +++ b/cliv2/Makefile @@ -127,8 +127,8 @@ SIGN_SCRIPT = $(WORKING_DIR)/scripts/sign_$(_GO_OS).sh ISSIGNED_SCRIPT = $(WORKING_DIR)/scripts/issigned_$(_GO_OS).sh EMBEDDED_DATA_DIR = $(WORKING_DIR)/internal/embedded/_data HELPDOCS_DIR = $(WORKING_DIR)/internal/helpdocs -HELPDOCS_MANIFEST = $(HELPDOCS_DIR)/manifest.txt -HELPDOCS_SOURCE = $(WORKING_DIR)/../help/cli-commands/*.md +HELPDOCS_EMBED_DIR = $(HELPDOCS_DIR)/cli-commands +HELPDOCS_SOURCE = $(WORKING_DIR)/../help/cli-commands ifeq ($(GOHOSTOS), windows) SPECIAL_SHELL = powershell @@ -191,17 +191,23 @@ summary: .PHONY: configure configure: _validate-build-mode summary $(CACHE_DIR) $(CACHE_DIR)/variables.mk $(V1_DIRECTORY)/$(V1_EMBEDDED_FILE_OUTPUT) dependencies $(CACHE_DIR)/prepare-3rd-party-licenses -.PHONY: helpdocs-manifest -helpdocs-manifest: +.PHONY: _helpdocs-prepare _helpdocs-clean +_helpdocs-prepare: @set -e; \ - md_count=$$(ls $(HELPDOCS_SOURCE) 2>/dev/null | wc -l | tr -d ' '); \ + md_count=$$(ls $(HELPDOCS_SOURCE)/*.md 2>/dev/null | wc -l | tr -d ' '); \ if [ "$$md_count" -eq 0 ]; then \ - echo "$(LOG_PREFIX) ERROR: no .md files found in help/cli-commands ($(HELPDOCS_SOURCE))"; \ + echo "$(LOG_PREFIX) ERROR: no .md files found in $(HELPDOCS_SOURCE)"; \ exit 1; \ fi; \ - ls $(HELPDOCS_SOURCE) | xargs -n1 basename | sort > $(HELPDOCS_MANIFEST) + rm -rf $(HELPDOCS_EMBED_DIR)/*; \ + cp $(HELPDOCS_SOURCE)/*.md $(HELPDOCS_EMBED_DIR)/ -$(BUILD_DIR)/$(V2_EXECUTABLE_NAME): $(BUILD_DIR) $(SRCS) generate-ls-protocol-metadata $(HELPDOCS_MANIFEST) +_helpdocs-clean: + @git clean -fdX -- $(HELPDOCS_EMBED_DIR) + @git checkout -- $(HELPDOCS_EMBED_DIR) + +$(BUILD_DIR)/$(V2_EXECUTABLE_NAME): $(BUILD_DIR) $(SRCS) generate-ls-protocol-metadata + @$(MAKE) _helpdocs-prepare $(eval LS_PROTOCOL_VERSION := $(shell cat $(LS_PROTOCOL_VERSION_FILE))) $(eval LS_COMMIT_HASH := $(shell cat $(LS_COMMIT_HASH_FILE))) $(eval EXTRA_FLAGS := -X github.com/snyk/snyk-ls/application/config.Version=$(LS_COMMIT_HASH) -X github.com/snyk/snyk-ls/application/config.LsProtocolVersion=$(LS_PROTOCOL_VERSION) -X github.com/snyk/cli/cliv2/pkg/core.internalOS=$(GOOS) -X github.com/snyk/cli/cliv2/internal/embedded/cliv1.snykCLIVersion=$(CLI_V1_VERSION_TAG) -X github.com/snyk/cli-extension-iac/internal/commands/iactest.internalRulesClientURL=$(IAC_RULES_URL) -X github.com/snyk/cli/cliv2/internal/constants.StaticNodeJsBinary=$(STATIC_NODE_BINARY)) @@ -211,7 +217,9 @@ $(BUILD_DIR)/$(V2_EXECUTABLE_NAME): $(BUILD_DIR) $(SRCS) generate-ls-protocol-me @echo "$(LOG_PREFIX) EXTRA_FLAGS: " @$(foreach flag,$(filter -X%,$(subst -X ,$(space)-X,$(EXTRA_FLAGS))),echo "$(LOG_PREFIX) $(flag)";) @echo "$(LOG_PREFIX) GCFLAGS: $(GCFLAGS)" - @cd $(BUILD_ROOT_DIR) && CGO_ENABLED=$(CGO_ENABLED) GOEXPERIMENT=$(FIPS_CRYPTO_BACKEND) MS_GO_NOSYSTEMCRYPTO=$(MS_GO_NOSYSTEMCRYPTO) GOOS=$(_GO_OS) GOARCH=$(GOARCH) $(GOCMD) build -tags=application -ldflags="$(LDFLAGS) $(EXTRA_FLAGS)" $(GCFLAGS) -o $(BUILD_DIR)/$(V2_EXECUTABLE_NAME) ./cmd/cliv2 + @cd $(BUILD_ROOT_DIR) && CGO_ENABLED=$(CGO_ENABLED) GOEXPERIMENT=$(FIPS_CRYPTO_BACKEND) MS_GO_NOSYSTEMCRYPTO=$(MS_GO_NOSYSTEMCRYPTO) GOOS=$(_GO_OS) GOARCH=$(GOARCH) $(GOCMD) build -tags=application -ldflags="$(LDFLAGS) $(EXTRA_FLAGS)" $(GCFLAGS) -o $(BUILD_DIR)/$(V2_EXECUTABLE_NAME) ./cmd/cliv2 \ + || ($(MAKE) _helpdocs-clean; exit 1) + @$(MAKE) _helpdocs-clean .PHONY: fips fips: @@ -260,7 +268,7 @@ openboxtest: @$(GOCMD) test -cover ./... .PHONY: test -test: helpdocs-manifest openboxtest +test: openboxtest .PHONY: lint lint: $(TOOLS_BIN)/golangci-lint diff --git a/cliv2/internal/helpdocs/cli-commands/do-not-delete b/cliv2/internal/helpdocs/cli-commands/do-not-delete new file mode 100644 index 0000000000..7da51caa73 --- /dev/null +++ b/cliv2/internal/helpdocs/cli-commands/do-not-delete @@ -0,0 +1 @@ +Adding this file to the project will prevent IDE errors between builds \ No newline at end of file diff --git a/cliv2/internal/helpdocs/command_help.go b/cliv2/internal/helpdocs/command_help.go new file mode 100644 index 0000000000..5519dd92f0 --- /dev/null +++ b/cliv2/internal/helpdocs/command_help.go @@ -0,0 +1,88 @@ +package helpdocs + +import ( + "io/fs" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/rs/zerolog" +) + +var nonDocChars = regexp.MustCompile(`[^a-zA-Z0-9-]`) + +// CommandHelp indexes embedded or test-supplied CLI command help markdown files. +type CommandHelp struct { + files map[string]struct{} +} + +// NewCommandHelp builds a lookup by walking root on fsys for *.md files. +func NewCommandHelp(fsys fs.FS, root string) (*CommandHelp, error) { + docFiles, err := docFilesFromEmbed(fsys, root) + if err != nil { + return nil, err + } + + return &CommandHelp{files: docFiles}, nil +} + +// NewCommandHelpFromFiles builds a lookup from an existing filename set. +func NewCommandHelpFromFiles(files map[string]struct{}) *CommandHelp { + return &CommandHelp{files: files} +} + +// HasUserDoc reports whether legacy user-doc help should be shown for command segments. +// Empty segments -> true (top-level README via legacy help). +// Non-empty segments -> true only if a matching .md exists (README excluded). +func (h *CommandHelp) HasUserDoc(segments []string) bool { + return hasUserDoc(segments, h.files) +} + +func docFilesFromEmbed(fsys fs.FS, root string) (map[string]struct{}, error) { + files := make(map[string]struct{}) + err := fs.WalkDir(fsys, root, func(_ string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".md") { + return err + } + files[d.Name()] = struct{}{} + return nil + }) + if err != nil { + return nil, err + } + return files, nil +} + +// helpFileName mirrors src/cli/commands/help/index.ts findHelpFile() join + replace. +func helpFileName(segments []string) string { + joined := strings.Join(segments, "-") + cleaned := nonDocChars.ReplaceAllString(joined, "") + return cleaned + ".md" +} + +func hasUserDoc(segments []string, files map[string]struct{}) bool { + if len(segments) == 0 { + return true + } + if len(files) == 0 { + // Missing or empty embed at build time: prefer legacy help lookup. + return true + } + for len(segments) > 0 { + if _, ok := files[helpFileName(segments)]; ok { + return true + } + segments = segments[:len(segments)-1] + } + return false +} + +// commandHelpLogger returns a stderr logger when sourceDir is the snyk/cli help tree. +func commandHelpLogger(helpCLICommandsDir string) *zerolog.Logger { + repoRoot := filepath.Clean(filepath.Join(helpCLICommandsDir, "..", "..")) + if _, err := os.Stat(filepath.Join(repoRoot, "cliv2", "go.mod")); err != nil { + return nil + } + return new(zerolog.New(os.Stderr).With().Timestamp().Str("component", "helpdocs").Logger()) +} diff --git a/cliv2/internal/helpdocs/command_help_test.go b/cliv2/internal/helpdocs/command_help_test.go new file mode 100644 index 0000000000..994dc5f964 --- /dev/null +++ b/cliv2/internal/helpdocs/command_help_test.go @@ -0,0 +1,64 @@ +package helpdocs + +import ( + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" +) + +func Test_helpFileName(t *testing.T) { + assert.Equal(t, "container-test.md", helpFileName([]string{"container", "test"})) + assert.Equal(t, "iac-describe.md", helpFileName([]string{"iac", "describe"})) + assert.Equal(t, "secrets-test.md", helpFileName([]string{"secrets", "test"})) +} + +func Test_HasUserDoc(t *testing.T) { + help := CommandHelpForTest(t) + + tests := map[string]struct { + segments []string + want bool + }{ + "empty uses readme path": {segments: []string{}, want: true}, + "test command": {segments: []string{"test"}, want: true}, + "container test subcommand": {segments: []string{"container", "test"}, want: true}, + "iac describe subcommand": {segments: []string{"iac", "describe"}, want: true}, + "unknown command": {segments: []string{"rainmaker"}, want: false}, + "undocumented secrets test": {segments: []string{"secrets", "test"}, want: false}, + "redteam setup walks back to parent": {segments: []string{"redteam", "setup"}, want: true}, + "undocumented agent-scan": {segments: []string{"agent-scan"}, want: false}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.want, help.HasUserDoc(tc.segments)) + }) + } +} + +func Test_NewCommandHelpFromFS(t *testing.T) { + expected := fixtureCommandHelp() + + fsMap := fstest.MapFS{ + "do-not-delete": {Data: []byte("placeholder")}, + "nested/ignored.md": {Data: []byte("# nested")}, + } + for name := range fixtureCommandHelpFiles { + fsMap[name] = &fstest.MapFile{Data: []byte("# doc")} + } + + var help *CommandHelp + var err error + if help, err = NewCommandHelp(fsMap, "."); err != nil { + t.Fatal(err) + } + + for _, segments := range [][]string{ + {"test"}, + {"container", "test"}, + {"rainmaker"}, + } { + assert.Equal(t, expected.HasUserDoc(segments), help.HasUserDoc(segments), segments) + } +} diff --git a/cliv2/internal/helpdocs/embed.go b/cliv2/internal/helpdocs/embed.go new file mode 100644 index 0000000000..64da29dcac --- /dev/null +++ b/cliv2/internal/helpdocs/embed.go @@ -0,0 +1,21 @@ +package helpdocs + +import "embed" + +const cliCommandsDir = "cli-commands" + +//go:embed cli-commands +var cliCommands embed.FS + +var defaultCommandHelp *CommandHelp + +// DefaultCommandHelp returns the compile-time embedded CLI command help lookup. +func DefaultCommandHelp() *CommandHelp { + if defaultCommandHelp == nil { + var err error + if defaultCommandHelp, err = NewCommandHelp(cliCommands, cliCommandsDir); err != nil { + panic("helpdocs: index cli-commands" + err.Error()) + } + } + return defaultCommandHelp +} diff --git a/cliv2/internal/helpdocs/export_test.go b/cliv2/internal/helpdocs/export_test.go new file mode 100644 index 0000000000..5a2fa6bf03 --- /dev/null +++ b/cliv2/internal/helpdocs/export_test.go @@ -0,0 +1,8 @@ +package helpdocs + +import "testing" + +func CommandHelpForTest(t *testing.T) *CommandHelp { + t.Helper() + return CommandHelpFromRepo() +} diff --git a/cliv2/internal/helpdocs/fixture.go b/cliv2/internal/helpdocs/fixture.go new file mode 100644 index 0000000000..b2007e4998 --- /dev/null +++ b/cliv2/internal/helpdocs/fixture.go @@ -0,0 +1,42 @@ +package helpdocs + +import ( + "os" + "path/filepath" +) + +var fixtureCommandHelpFiles = map[string]struct{}{ + "test.md": {}, + "container.md": {}, + "container-test.md": {}, + "iac.md": {}, + "iac-describe.md": {}, + "redteam.md": {}, +} + +// fixtureCommandHelp returns a minimal doc index for unit tests without compile-time embed. +func fixtureCommandHelp() *CommandHelp { + return NewCommandHelpFromFiles(fixtureCommandHelpFiles) +} + +const repoCLICommandsDir = "../../../help/cli-commands" + +// CommandHelpFromRepo prefers help/cli-commands on disk, else fixtureCommandHelp. +func CommandHelpFromRepo() *CommandHelp { + sourceDir, err := filepath.Abs(repoCLICommandsDir) + if err != nil { + return fixtureCommandHelp() + } + matches, err := filepath.Glob(filepath.Join(sourceDir, "*.md")) + if err != nil || len(matches) == 0 { + return fixtureCommandHelp() + } + commandHelp, err := NewCommandHelp(os.DirFS(sourceDir), ".") + if err != nil { + if logger := commandHelpLogger(sourceDir); logger != nil { + logger.Warn().Err(err).Str("dir", sourceDir).Msg("failed to load command help from repo") + } + return fixtureCommandHelp() + } + return commandHelp +} diff --git a/cliv2/internal/helpdocs/helpdocs.go b/cliv2/internal/helpdocs/helpdocs.go deleted file mode 100644 index 4d021b14d6..0000000000 --- a/cliv2/internal/helpdocs/helpdocs.go +++ /dev/null @@ -1,71 +0,0 @@ -package helpdocs - -import ( - _ "embed" - "regexp" - "strings" -) - -//go:embed manifest.txt -var manifest string - -var docFiles map[string]struct{} - -var nonDocChars = regexp.MustCompile(`[^a-zA-Z0-9-]`) - -func init() { - docFiles = manifestFileSet(manifest) -} - -// manifestFileSet builds the doc filename set from manifest text. -// Trims trailing carriage returns so CRLF-checked-out manifests still match lookups. -func manifestFileSet(manifestText string) map[string]struct{} { - files := make(map[string]struct{}) - for _, line := range manifestLines(manifestText) { - files[line] = struct{}{} - } - return files -} - -func manifestLines(manifestText string) []string { - var lines []string - for _, line := range strings.Split(strings.TrimSpace(manifestText), "\n") { - line = strings.TrimSuffix(line, "\r") - if line != "" { - lines = append(lines, line) - } - } - return lines -} - -// helpFileName mirrors src/cli/commands/help/index.ts join + cleanse. -func helpFileName(segments []string) string { - joined := strings.Join(segments, "-") - cleaned := nonDocChars.ReplaceAllString(joined, "") - return cleaned + ".md" -} - -// HasUserDoc reports whether legacy user-doc help should be shown for command segments. -// Empty segments → true (top-level README via legacy help). -// Non-empty segments → true only if a matching .md exists during walk-back (README excluded). -func HasUserDoc(segments []string) bool { - return hasUserDoc(docFiles, segments) -} - -func hasUserDoc(files map[string]struct{}, segments []string) bool { - if len(segments) == 0 { - return true - } - if len(files) == 0 { - // Missing or empty manifest at build time: prefer legacy help lookup. - return true - } - args := append([]string(nil), segments...) - for len(args) > 0 { - if _, ok := files[helpFileName(args)]; ok { - return true - } - args = args[:len(args)-1] - } - return false -} diff --git a/cliv2/internal/helpdocs/helpdocs_test.go b/cliv2/internal/helpdocs/helpdocs_test.go deleted file mode 100644 index 8428089971..0000000000 --- a/cliv2/internal/helpdocs/helpdocs_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package helpdocs - -import ( - "os" - "path/filepath" - "sort" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func testDocFiles() map[string]struct{} { - names := []string{ - "README.md", - "test.md", - "container.md", - "container-test.md", - "iac-describe.md", - "redteam.md", - } - files := make(map[string]struct{}, len(names)) - for _, name := range names { - files[name] = struct{}{} - } - return files -} - -func Test_helpFileName(t *testing.T) { - assert.Equal(t, "container-test.md", helpFileName([]string{"container", "test"})) - assert.Equal(t, "iac-describe.md", helpFileName([]string{"iac", "describe"})) - assert.Equal(t, "secrets-test.md", helpFileName([]string{"secrets", "test"})) -} - -func Test_hasUserDoc(t *testing.T) { - files := testDocFiles() - - tests := map[string]struct { - segments []string - want bool - }{ - "empty uses readme path": {segments: []string{}, want: true}, - "test command": {segments: []string{"test"}, want: true}, - "container test subcommand": {segments: []string{"container", "test"}, want: true}, - "iac describe subcommand": {segments: []string{"iac", "describe"}, want: true}, - "unknown command": {segments: []string{"rainmaker"}, want: false}, - "undocumented secrets test": {segments: []string{"secrets", "test"}, want: false}, - "redteam setup walks back to parent": {segments: []string{"redteam", "setup"}, want: true}, - "undocumented agent-scan": {segments: []string{"agent-scan"}, want: false}, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - assert.Equal(t, tc.want, hasUserDoc(files, tc.segments)) - }) - } -} - -func Test_HasUserDoc_usesEmbeddedManifest(t *testing.T) { - assert.True(t, HasUserDoc([]string{"test"})) - assert.NotEmpty(t, docFiles) -} - -func Test_manifestFileSet_stripsCRLFLineEndings(t *testing.T) { - files := manifestFileSet("test.md\r\ncontainer-test.md\r\n") - - assert.True(t, hasUserDoc(files, []string{"test"})) - assert.True(t, hasUserDoc(files, []string{"container", "test"})) - assert.False(t, hasUserDoc(files, []string{"rainmaker"})) -} - -func Test_manifestMatchesHelpCLICommands(t *testing.T) { - helpDir := filepath.Join("..", "..", "..", "help", "cli-commands") - entries, err := os.ReadDir(helpDir) - require.NoError(t, err, "help/cli-commands must exist; run from repo root via go test ./pkg/helpdocs") - - var fromDisk []string - for _, entry := range entries { - if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") { - continue - } - fromDisk = append(fromDisk, entry.Name()) - } - sort.Strings(fromDisk) - - fromManifest := manifestEntries() - assert.Equal(t, fromDisk, fromManifest, - "embedded manifest.txt is out of sync with help/cli-commands; run: make -C cliv2 helpdocs-manifest") -} - -func manifestEntries() []string { - entries := manifestLines(manifest) - sort.Strings(entries) - return entries -} diff --git a/cliv2/internal/helpdocs/manifest.txt b/cliv2/internal/helpdocs/manifest.txt deleted file mode 100644 index 119fc0e94c..0000000000 --- a/cliv2/internal/helpdocs/manifest.txt +++ /dev/null @@ -1,32 +0,0 @@ -README.md -ai-red-teaming.md -aibom-test.md -aibom.md -apps.md -auth.md -code-test.md -code.md -config-environment.md -config.md -container-monitor.md -container-sbom.md -container-test.md -container.md -iac-capture.md -iac-describe.md -iac-report.md -iac-rules-init.md -iac-rules-push.md -iac-rules-test.md -iac-test.md -iac-update-exclude-policy.md -iac.md -ignore.md -log4shell.md -monitor.md -policy.md -redteam.md -sbom-monitor.md -sbom-test.md -sbom.md -test.md diff --git a/cliv2/internal/helprouting/helprouting.go b/cliv2/internal/helprouting/helprouting.go index c6190395c0..03af7d479a 100644 --- a/cliv2/internal/helprouting/helprouting.go +++ b/cliv2/internal/helprouting/helprouting.go @@ -24,6 +24,7 @@ func init() { type Router struct { LegacyHelp func() error OnHelpCalled func() + HasUserDoc func([]string) bool } // Help picks legacy user-doc help or Cobra help for the given context. @@ -42,7 +43,7 @@ func (r *Router) Help(c *cobra.Command, root *cobra.Command, argv []string) erro } segments := commandSegments(c, root, argv) - if helpdocs.HasUserDoc(segments) { + if r.hasUserDoc(segments) { return r.LegacyHelp() } @@ -51,13 +52,20 @@ func (r *Router) Help(c *cobra.Command, root *cobra.Command, argv []string) erro return r.LegacyHelp() } - if helpdocs.HasUserDoc(commandSegmentsFromCobra(target)) { + if r.hasUserDoc(commandSegmentsFromCobra(target)) { return r.LegacyHelp() } return renderCobraHelp(target) } +func (r *Router) hasUserDoc(segments []string) bool { + if r.HasUserDoc != nil { + return r.HasUserDoc(segments) + } + return helpdocs.DefaultCommandHelp().HasUserDoc(segments) +} + func commandSegments(c *cobra.Command, root *cobra.Command, argv []string) []string { if root == nil { return commandSegmentsFromCobra(c) diff --git a/cliv2/internal/helprouting/helprouting_test.go b/cliv2/internal/helprouting/helprouting_test.go index fad92b27ef..d4e09b173a 100644 --- a/cliv2/internal/helprouting/helprouting_test.go +++ b/cliv2/internal/helprouting/helprouting_test.go @@ -125,12 +125,15 @@ func Test_Router_Help(t *testing.T) { for _, tc := range append(documented, undocumented...) { t.Run(tc.name, func(t *testing.T) { + help := testCommandHelp(t) + legacyCalled := false router := &Router{ LegacyHelp: func() error { legacyCalled = true return nil }, + HasUserDoc: help.HasUserDoc, } cmd, buf := tc.command(t, root) @@ -162,9 +165,10 @@ func Test_commandSegmentsFromCobra(t *testing.T) { func Test_commandSegments(t *testing.T) { root := setupTestRoot(t) + help := testCommandHelp(t) assert.Equal(t, []string{"container"}, commandSegments(root, root, []string{"-h", "container"})) - assert.True(t, helpdocs.HasUserDoc([]string{"container"})) + assert.True(t, help.HasUserDoc([]string{"container"})) assert.Equal(t, []string{"container"}, commandSegments(root, root, []string{"--help=container"})) } @@ -223,3 +227,8 @@ func findCommand(t *testing.T, root *cobra.Command, path ...string) *cobra.Comma func helpSubcommand() *cobra.Command { return &cobra.Command{Use: "help"} } + +func testCommandHelp(t *testing.T) *helpdocs.CommandHelp { + t.Helper() + return helpdocs.CommandHelpFromRepo() +} diff --git a/cliv2/internal/utils/interactive_test.go b/cliv2/internal/utils/interactive_test.go index d308f2af0b..2b72ab1db6 100644 --- a/cliv2/internal/utils/interactive_test.go +++ b/cliv2/internal/utils/interactive_test.go @@ -11,8 +11,12 @@ import ( func Test_isTerminal_pipeIsNotATerminal(t *testing.T) { r, w, err := os.Pipe() require.NoError(t, err) - defer r.Close() - defer w.Close() + defer func(r *os.File) { + _ = r.Close() + }(r) + defer func(w *os.File) { + _ = w.Close() + }(w) assert.False(t, isTerminal(r), "read end of a pipe is not a terminal") assert.False(t, isTerminal(w), "write end of a pipe is not a terminal") @@ -21,7 +25,9 @@ func Test_isTerminal_pipeIsNotATerminal(t *testing.T) { func Test_isTerminal_regularFileIsNotATerminal(t *testing.T) { f, err := os.CreateTemp(t.TempDir(), "interactive-test") require.NoError(t, err) - defer f.Close() + defer func(f *os.File) { + _ = f.Close() + }(f) assert.False(t, isTerminal(f), "a regular file is not a terminal") } diff --git a/cliv2/pkg/core/main.go b/cliv2/pkg/core/main.go index dda97c1a2c..cf5993b888 100644 --- a/cliv2/pkg/core/main.go +++ b/cliv2/pkg/core/main.go @@ -1,9 +1,10 @@ package core // !!! This import needs to be the first import, please do not change this !!! +import _ "github.com/snyk/go-application-framework/pkg/networking/fips_enable" import ( + "github.com/snyk/cli/cliv2/internal/helpdocs" "github.com/snyk/cli/cliv2/internal/helprouting" - _ "github.com/snyk/go-application-framework/pkg/networking/fips_enable" ) import ( @@ -545,9 +546,11 @@ func mainWithErrorCode(additionalExts []workflow.ExtensionInit) int { var err error rInfo := runtimeinfo.New(runtimeinfo.WithName("snyk-cli"), runtimeinfo.WithVersion(cliv2.GetFullVersion())) + helpDocs := helpdocs.DefaultCommandHelp() helpRouter := &helprouting.Router{ LegacyHelp: runLegacyHelp, OnHelpCalled: func() { helpProvided = true }, + HasUserDoc: helpDocs.HasUserDoc, } rootCommand := prepareRootCommand(helpRouter) // omit the first arg which is always `snyk` diff --git a/cliv2/pkg/core/main_test.go b/cliv2/pkg/core/main_test.go index e2d7454afe..eeb58ca116 100644 --- a/cliv2/pkg/core/main_test.go +++ b/cliv2/pkg/core/main_test.go @@ -13,6 +13,7 @@ import ( "github.com/golang/mock/gomock" "github.com/rs/zerolog" + "github.com/snyk/cli/cliv2/internal/helpdocs" "github.com/snyk/cli/cliv2/internal/helprouting" "github.com/snyk/error-catalog-golang-public/code" "github.com/snyk/error-catalog-golang-public/snyk_errors" @@ -766,7 +767,9 @@ func loadJsonFile(t *testing.T, filename string) []byte { } func testHelpRouter() *helprouting.Router { + helpDocs := helpdocs.CommandHelpFromRepo() return &helprouting.Router{ LegacyHelp: func() error { return nil }, + HasUserDoc: helpDocs.HasUserDoc, } }