Skip to content

Commit fea4006

Browse files
Merge pull request #6919 from snyk/fix/CLI-1596
fix: replace manifest.txt help mechanism with embedded user docs directory
2 parents 9f95e5d + cf2d449 commit fea4006

17 files changed

Lines changed: 232 additions & 227 deletions

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,4 @@ scripts/Brewfile.lock.json
5555
test/fixtures/**/go.sum
5656
.cursor
5757
.windsurf
58-
.claude
58+
.claude

CONTRIBUTING.md

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -283,20 +283,14 @@ automatically be pulled into Snyk CLI as pull requests.
283283
### CLI help command files (`help/cli-commands`)
284284

285285
The Go CLI reads user-facing command help from markdown files under `help/cli-commands/`. These files are synced from
286-
GitBook into this repository (see the `sync-cli-help-to-user-docs` workflow). At build time, the CLI embeds a manifest
287-
of available help files (`cliv2/pkg/helpdocs/manifest.txt`) and uses it to decide whether to show legacy GitBook help
288-
or native Cobra help for a given command.
289-
290-
When you add, remove, or rename files in `help/cli-commands/`, regenerate the manifest and commit the result:
291-
292-
```sh
293-
make -C cliv2 helpdocs-manifest
294-
git add cliv2/pkg/helpdocs/manifest.txt
295-
```
296-
297-
`make -C cliv2 test` and `make build` run this target automatically, but you still need to commit the updated
298-
`manifest.txt` when it changes. Go tests in `cliv2/pkg/helpdocs` verify that the manifest stays in sync with
299-
`help/cli-commands/`.
286+
GitBook into this repository (see the `sync-cli-help-to-user-docs` workflow). At **build** time, the Makefile copies
287+
`help/cli-commands/` into `cliv2/internal/helpdocs/cli-commands/` so the Go embed can read them, then removes the copy
288+
afterward. Go unit tests read `help/cli-commands/` from disk (or use a small in-memory fixture when that directory is
289+
unavailable), so `make -C cliv2 test` does not run that copy step. The embedded filenames decide whether to show legacy
290+
GitBook help or native Cobra help for a given command.
291+
292+
When you add, remove, or rename files in `help/cli-commands/`, no extra manifest step is required. Help routing tests
293+
pick up the changes on the next `make -C cliv2 test`; the shipped binary picks them up on the next `make build`.
300294

301295
To test help routing locally after building:
302296

cliv2/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ _cache
1212
bin
1313
internal/embedded/_data
1414
/.bin/
15+
internal/helpdocs/cli-commands

cliv2/Makefile

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ SIGN_SCRIPT = $(WORKING_DIR)/scripts/sign_$(_GO_OS).sh
127127
ISSIGNED_SCRIPT = $(WORKING_DIR)/scripts/issigned_$(_GO_OS).sh
128128
EMBEDDED_DATA_DIR = $(WORKING_DIR)/internal/embedded/_data
129129
HELPDOCS_DIR = $(WORKING_DIR)/internal/helpdocs
130-
HELPDOCS_MANIFEST = $(HELPDOCS_DIR)/manifest.txt
131-
HELPDOCS_SOURCE = $(WORKING_DIR)/../help/cli-commands/*.md
130+
HELPDOCS_EMBED_DIR = $(HELPDOCS_DIR)/cli-commands
131+
HELPDOCS_SOURCE = $(WORKING_DIR)/../help/cli-commands
132132

133133
ifeq ($(GOHOSTOS), windows)
134134
SPECIAL_SHELL = powershell
@@ -191,17 +191,18 @@ summary:
191191
.PHONY: configure
192192
configure: _validate-build-mode summary $(CACHE_DIR) $(CACHE_DIR)/variables.mk $(V1_DIRECTORY)/$(V1_EMBEDDED_FILE_OUTPUT) dependencies $(CACHE_DIR)/prepare-3rd-party-licenses
193193

194-
.PHONY: helpdocs-manifest
195-
helpdocs-manifest:
194+
.PHONY: _helpdocs-prepare
195+
_helpdocs-prepare:
196196
@set -e; \
197-
md_count=$$(ls $(HELPDOCS_SOURCE) 2>/dev/null | wc -l | tr -d ' '); \
197+
md_count=$$(ls $(HELPDOCS_SOURCE)/*.md 2>/dev/null | wc -l | tr -d ' '); \
198198
if [ "$$md_count" -eq 0 ]; then \
199-
echo "$(LOG_PREFIX) ERROR: no .md files found in help/cli-commands ($(HELPDOCS_SOURCE))"; \
199+
echo "$(LOG_PREFIX) ERROR: no .md files found in $(HELPDOCS_SOURCE)"; \
200200
exit 1; \
201201
fi; \
202-
ls $(HELPDOCS_SOURCE) | xargs -n1 basename | sort > $(HELPDOCS_MANIFEST)
202+
rm -rf $(HELPDOCS_EMBED_DIR)/*.md; \
203+
cp $(HELPDOCS_SOURCE)/*.md $(HELPDOCS_EMBED_DIR)/
203204

204-
$(BUILD_DIR)/$(V2_EXECUTABLE_NAME): $(BUILD_DIR) $(SRCS) generate-ls-protocol-metadata $(HELPDOCS_MANIFEST)
205+
$(BUILD_DIR)/$(V2_EXECUTABLE_NAME): $(BUILD_DIR) $(SRCS) generate-ls-protocol-metadata _helpdocs-prepare
205206
$(eval LS_PROTOCOL_VERSION := $(shell cat $(LS_PROTOCOL_VERSION_FILE)))
206207
$(eval LS_COMMIT_HASH := $(shell cat $(LS_COMMIT_HASH_FILE)))
207208
$(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))
@@ -260,7 +261,7 @@ openboxtest:
260261
@$(GOCMD) test -cover ./...
261262

262263
.PHONY: test
263-
test: helpdocs-manifest openboxtest
264+
test: openboxtest
264265

265266
.PHONY: lint
266267
lint: $(TOOLS_BIN)/golangci-lint
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Adding this file to the project will prevent IDE errors between builds
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package helpdocs
2+
3+
import (
4+
"io/fs"
5+
"regexp"
6+
"strings"
7+
)
8+
9+
var nonDocChars = regexp.MustCompile(`[^a-zA-Z0-9-]`)
10+
11+
// CommandHelp indexes embedded or test-supplied CLI command help markdown files.
12+
type CommandHelp struct {
13+
files map[string]struct{}
14+
}
15+
16+
// NewCommandHelp builds a lookup by walking root on fsys for *.md files.
17+
func NewCommandHelp(fsys fs.FS, root string) (*CommandHelp, error) {
18+
docFiles, err := docFilesFromEmbed(fsys, root)
19+
if err != nil {
20+
return nil, err
21+
}
22+
23+
return &CommandHelp{files: docFiles}, nil
24+
}
25+
26+
// HasUserDoc reports whether legacy user-doc help should be shown for command segments.
27+
// Empty segments -> true (top-level README via legacy help).
28+
// Non-empty segments -> true only if a matching .md exists (README excluded).
29+
func (h *CommandHelp) HasUserDoc(segments []string) bool {
30+
return hasUserDoc(segments, h.files)
31+
}
32+
33+
func docFilesFromEmbed(fsys fs.FS, root string) (map[string]struct{}, error) {
34+
files := make(map[string]struct{})
35+
err := fs.WalkDir(fsys, root, func(_ string, d fs.DirEntry, err error) error {
36+
if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".md") {
37+
return err
38+
}
39+
files[d.Name()] = struct{}{}
40+
return nil
41+
})
42+
if err != nil {
43+
return nil, err
44+
}
45+
return files, nil
46+
}
47+
48+
// helpFileName mirrors src/cli/commands/help/index.ts findHelpFile() join + replace.
49+
func helpFileName(segments []string) string {
50+
joined := strings.Join(segments, "-")
51+
cleaned := nonDocChars.ReplaceAllString(joined, "")
52+
return cleaned + ".md"
53+
}
54+
55+
func hasUserDoc(segments []string, files map[string]struct{}) bool {
56+
if len(segments) == 0 {
57+
return true
58+
}
59+
if len(files) == 0 {
60+
// Missing or empty embed at build time: prefer legacy help lookup.
61+
return true
62+
}
63+
for len(segments) > 0 {
64+
if _, ok := files[helpFileName(segments)]; ok {
65+
return true
66+
}
67+
segments = segments[:len(segments)-1]
68+
}
69+
return false
70+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package helpdocs
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func Test_helpFileName(t *testing.T) {
11+
assert.Equal(t, "container-test.md", helpFileName([]string{"container", "test"}))
12+
assert.Equal(t, "iac-describe.md", helpFileName([]string{"iac", "describe"}))
13+
assert.Equal(t, "secrets-test.md", helpFileName([]string{"secrets", "test"}))
14+
}
15+
16+
func Test_HasUserDoc(t *testing.T) {
17+
help := CommandHelpForTest(t)
18+
19+
tests := map[string]struct {
20+
segments []string
21+
want bool
22+
}{
23+
"empty uses readme path": {segments: []string{}, want: true},
24+
"test command": {segments: []string{"test"}, want: true},
25+
"container test subcommand": {segments: []string{"container", "test"}, want: true},
26+
"iac describe subcommand": {segments: []string{"iac", "describe"}, want: true},
27+
"unknown command": {segments: []string{"rainmaker"}, want: false},
28+
"undocumented secrets test": {segments: []string{"secrets", "test"}, want: false},
29+
"redteam setup walks back to parent": {segments: []string{"redteam", "setup"}, want: true},
30+
"undocumented agent-scan": {segments: []string{"agent-scan"}, want: false},
31+
}
32+
33+
for name, tc := range tests {
34+
t.Run(name, func(t *testing.T) {
35+
assert.Equal(t, tc.want, help.HasUserDoc(tc.segments))
36+
})
37+
}
38+
}
39+
40+
func Test_NewCommandHelpFromFS(t *testing.T) {
41+
expected := FixtureCommandHelp()
42+
43+
help, err := NewCommandHelp(fixtureCommandHelpFS(), ".")
44+
require.NoError(t, err)
45+
46+
for _, segments := range [][]string{
47+
{"test"},
48+
{"container", "test"},
49+
{"rainmaker"},
50+
} {
51+
assert.Equal(t, expected.HasUserDoc(segments), help.HasUserDoc(segments), segments)
52+
}
53+
}

cliv2/internal/helpdocs/embed.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package helpdocs
2+
3+
import "embed"
4+
5+
const cliCommandsDir = "cli-commands"
6+
7+
//go:embed cli-commands
8+
var cliCommands embed.FS
9+
10+
var defaultCommandHelp *CommandHelp
11+
12+
// DefaultCommandHelp returns the compile-time embedded CLI command help lookup.
13+
func DefaultCommandHelp() *CommandHelp {
14+
if defaultCommandHelp == nil {
15+
var err error
16+
if defaultCommandHelp, err = NewCommandHelp(cliCommands, cliCommandsDir); err != nil {
17+
panic("helpdocs: index cli-commands: " + err.Error())
18+
}
19+
}
20+
return defaultCommandHelp
21+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package helpdocs
2+
3+
import "testing"
4+
5+
func CommandHelpForTest(t *testing.T) *CommandHelp {
6+
t.Helper()
7+
return FixtureCommandHelp()
8+
}

cliv2/internal/helpdocs/fixture.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package helpdocs
2+
3+
import "testing/fstest"
4+
5+
var fixtureCommandHelpFiles = map[string]struct{}{
6+
"test.md": {},
7+
"container.md": {},
8+
"container-test.md": {},
9+
"iac.md": {},
10+
"iac-describe.md": {},
11+
"redteam.md": {},
12+
}
13+
14+
func fixtureCommandHelpFS() fstest.MapFS {
15+
fsMap := fstest.MapFS{
16+
"do-not-delete": {Data: []byte("placeholder")},
17+
"nested/ignored.md": {Data: []byte("# nested")},
18+
}
19+
for name := range fixtureCommandHelpFiles {
20+
fsMap[name] = &fstest.MapFile{Data: []byte("# doc")}
21+
}
22+
return fsMap
23+
}
24+
25+
// FixtureCommandHelp returns a minimal CommandHelp built from fstest.MapFS for unit tests.
26+
func FixtureCommandHelp() *CommandHelp {
27+
help, err := NewCommandHelp(fixtureCommandHelpFS(), ".")
28+
if err != nil {
29+
panic("helpdocs: fixture command help: " + err.Error())
30+
}
31+
return help
32+
}

0 commit comments

Comments
 (0)