Skip to content
Merged
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
27 changes: 27 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,33 @@ User-facing documentation is [available on GitBook](https://docs.snyk.io/feature
`snyk help` output must also be [edited on GitBook](https://docs.snyk.io/features/snyk-cli/commands). Changes will
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'd prefer the other way around, that the code defines the public website docs, but I guess that's just preference.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Anything in here which audits which commands don't have any help documents?


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/`.

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.

so every make build will create this manifest.txt, even in the CI runs? Just wondering if that could create any kind of impact regarding this

@robertolopezlopez robertolopezlopez Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes it will execute during make build

it won't have any impact and actually it is good. The commands with user help will be added to manifest.txt and the integrity test (helpdocs_test.go#Test_manifestMatchesHelpCLICommands) will not complain. If make buiLd (or directly make -C cliv2 helpdocs-manifest) is not run after adding some user help, the test will fail.

This test is a safeguard so we never forget to add commands with user docs to manifest.txt.

The updated manifest.txt is necessary as it is embedded into helpdocs.go#manifest which feeds docFiles in init() > docFiles > used in HasUserDoc() which is core part of the logic for discerning user docs / Cobra / default


To test help routing locally after building:

```sh
./binary-releases/snyk-macos-arm64 help test # documented command → GitBook help
./binary-releases/snyk-macos-arm64 help agent-scan # undocumented command → Cobra help
```

Adjust the binary path for your platform (see [Building](#building)).

## Creating a branch

Create a new branch before making any changes. Make sure to give it a descriptive name so that you can find it later.
Expand Down
17 changes: 15 additions & 2 deletions cliv2/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ HASH_STRING = $(HASH)$(HASH_ALGORITHM)
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

ifeq ($(GOHOSTOS), windows)
SPECIAL_SHELL = powershell
Expand Down Expand Up @@ -188,7 +191,17 @@ 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

$(BUILD_DIR)/$(V2_EXECUTABLE_NAME): $(BUILD_DIR) $(SRCS) generate-ls-protocol-metadata
.PHONY: helpdocs-manifest
helpdocs-manifest:
@set -e; \
md_count=$$(ls $(HELPDOCS_SOURCE) 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))"; \
exit 1; \
fi; \
ls $(HELPDOCS_SOURCE) | xargs -n1 basename | sort > $(HELPDOCS_MANIFEST)

$(BUILD_DIR)/$(V2_EXECUTABLE_NAME): $(BUILD_DIR) $(SRCS) generate-ls-protocol-metadata $(HELPDOCS_MANIFEST)
Comment thread
danskmt marked this conversation as resolved.
$(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))
Expand Down Expand Up @@ -247,7 +260,7 @@ openboxtest:
@$(GOCMD) test -cover ./...

.PHONY: test
test: openboxtest
test: helpdocs-manifest openboxtest

.PHONY: lint
lint: $(TOOLS_BIN)/golangci-lint
Expand Down
71 changes: 71 additions & 0 deletions cliv2/internal/helpdocs/helpdocs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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
}
96 changes: 96 additions & 0 deletions cliv2/internal/helpdocs/helpdocs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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
}
32 changes: 32 additions & 0 deletions cliv2/internal/helpdocs/manifest.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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
Loading