Skip to content

Commit 27af388

Browse files
fix: replace manifest.txt help with embedded user docs directory
1 parent eb3e20b commit 27af388

9 files changed

Lines changed: 166 additions & 136 deletions

File tree

.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: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -283,20 +283,13 @@ 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.
286+
GitBook into this repository (see the `sync-cli-help-to-user-docs` workflow). At build and test time, the Makefile
287+
copies `help/cli-commands/` into `cliv2/internal/helpdocs/cli-commands/` so the Go embed can read them, then removes
288+
the copy afterward. The embedded filenames decide whether to show legacy GitBook help or native Cobra help for a given
289+
command.
289290

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/`.
291+
When you add, remove, or rename files in `help/cli-commands/`, no extra manifest step is required — the embed picks up
292+
changes on the next `make build` or `make -C cliv2 test`.
300293

301294
To test help routing locally after building:
302295

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: 23 additions & 10 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,28 @@ 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 _helpdocs-clean
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)/*; \
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+
_helpdocs-clean:
206+
@rm -rf $(HELPDOCS_EMBED_DIR)/*
207+
@git checkout -- $(HELPDOCS_EMBED_DIR)/do-not-delete
208+
209+
$(BUILD_DIR)/$(V2_EXECUTABLE_NAME): $(BUILD_DIR) $(SRCS) generate-ls-protocol-metadata
210+
@$(MAKE) _helpdocs-prepare
211+
@($(MAKE) _build-v2-binary CLI_V1_VERSION_TAG="$(CLI_V1_VERSION_TAG)") || ($(MAKE) _helpdocs-clean; exit 1)
212+
@$(MAKE) _helpdocs-clean
213+
214+
.PHONY: _build-v2-binary
215+
_build-v2-binary:
205216
$(eval LS_PROTOCOL_VERSION := $(shell cat $(LS_PROTOCOL_VERSION_FILE)))
206217
$(eval LS_COMMIT_HASH := $(shell cat $(LS_COMMIT_HASH_FILE)))
207218
$(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))
@@ -257,10 +268,12 @@ generate: $(WORKING_DIR)/internal/httpauth/generated/httpauth_generated_mock.go
257268
.PHONY: openboxtest
258269
openboxtest:
259270
@echo "$(LOG_PREFIX) Running $@"
260-
@$(GOCMD) test -cover ./...
271+
@$(MAKE) _helpdocs-prepare
272+
@($(GOCMD) test -cover ./...) || ($(MAKE) _helpdocs-clean; exit 1)
273+
@$(MAKE) _helpdocs-clean
261274

262275
.PHONY: test
263-
test: helpdocs-manifest openboxtest
276+
test: openboxtest
264277

265278
.PHONY: lint
266279
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: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,64 @@
11
package helpdocs
22

33
import (
4-
_ "embed"
4+
"embed"
5+
"io/fs"
56
"regexp"
67
"strings"
78
)
89

9-
//go:embed manifest.txt
10-
var manifest string
10+
const cliCommandsDir = "cli-commands"
11+
12+
//go:embed cli-commands
13+
var cliCommands embed.FS
1114

1215
var docFiles map[string]struct{}
1316

1417
var nonDocChars = regexp.MustCompile(`[^a-zA-Z0-9-]`)
1518

1619
func init() {
17-
docFiles = manifestFileSet(manifest)
20+
docFiles = docFilesFromEmbed(cliCommands, cliCommandsDir)
1821
}
1922

20-
// manifestFileSet builds the doc filename set from manifest text.
21-
// Trims trailing carriage returns so CRLF-checked-out manifests still match lookups.
22-
func manifestFileSet(manifestText string) map[string]struct{} {
23+
func docFilesFromEmbed(fsys fs.FS, root string) map[string]struct{} {
2324
files := make(map[string]struct{})
24-
for _, line := range manifestLines(manifestText) {
25-
files[line] = struct{}{}
26-
}
27-
return files
28-
}
29-
30-
func manifestLines(manifestText string) []string {
31-
var lines []string
32-
for _, line := range strings.Split(strings.TrimSpace(manifestText), "\n") {
33-
line = strings.TrimSuffix(line, "\r")
34-
if line != "" {
35-
lines = append(lines, line)
25+
_ = fs.WalkDir(fsys, root, func(_ string, d fs.DirEntry, err error) error {
26+
if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".md") {
27+
return err
3628
}
37-
}
38-
return lines
29+
files[d.Name()] = struct{}{}
30+
return nil
31+
})
32+
return files
3933
}
4034

41-
// helpFileName mirrors src/cli/commands/help/index.ts join + cleanse.
35+
// helpFileName mirrors src/cli/commands/help/index.ts findHelpFile() join + replace
4236
func helpFileName(segments []string) string {
4337
joined := strings.Join(segments, "-")
4438
cleaned := nonDocChars.ReplaceAllString(joined, "")
4539
return cleaned + ".md"
4640
}
4741

4842
// HasUserDoc reports whether legacy user-doc help should be shown for command segments.
49-
// Empty segments true (top-level README via legacy help).
50-
// Non-empty segments true only if a matching .md exists during walk-back (README excluded).
43+
// Empty segments -> true (top-level README via legacy help).
44+
// Non-empty segments -> true only if a matching .md exists (README excluded).
5145
func HasUserDoc(segments []string) bool {
52-
return hasUserDoc(docFiles, segments)
46+
return hasUserDoc(segments, docFiles)
5347
}
5448

55-
func hasUserDoc(files map[string]struct{}, segments []string) bool {
49+
func hasUserDoc(segments []string, files map[string]struct{}) bool {
5650
if len(segments) == 0 {
5751
return true
5852
}
5953
if len(files) == 0 {
60-
// Missing or empty manifest at build time: prefer legacy help lookup.
54+
// Missing or empty embed at build time: prefer legacy help lookup.
6155
return true
6256
}
63-
args := append([]string(nil), segments...)
64-
for len(args) > 0 {
65-
if _, ok := files[helpFileName(args)]; ok {
57+
for len(segments) > 0 {
58+
if _, ok := files[helpFileName(segments)]; ok {
6659
return true
6760
}
68-
args = args[:len(args)-1]
61+
segments = segments[:len(segments)-1]
6962
}
7063
return false
7164
}
Lines changed: 91 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,91 @@
11
package helpdocs
22

33
import (
4+
"fmt"
45
"os"
56
"path/filepath"
6-
"sort"
7-
"strings"
87
"testing"
98

109
"github.com/stretchr/testify/assert"
1110
"github.com/stretchr/testify/require"
1211
)
1312

14-
func testDocFiles() map[string]struct{} {
15-
names := []string{
16-
"README.md",
17-
"test.md",
18-
"container.md",
19-
"container-test.md",
20-
"iac-describe.md",
21-
"redteam.md",
13+
const cliCommandsSourceDir = "../../../help/cli-commands"
14+
15+
var testCLICommandDocFiles map[string]struct{}
16+
17+
func TestMain(m *testing.M) {
18+
teardown, err := setupCLICommandsForTest()
19+
if err != nil {
20+
fmt.Fprintf(os.Stderr, "helpdocs TestMain setup: %v\n", err)
21+
os.Exit(1)
2222
}
23-
files := make(map[string]struct{}, len(names))
24-
for _, name := range names {
25-
files[name] = struct{}{}
23+
24+
testCLICommandDocFiles, err = loadCLICommandDocFilesFromDisk()
25+
if err != nil {
26+
teardown()
27+
fmt.Fprintf(os.Stderr, "helpdocs TestMain load doc files: %v\n", err)
28+
os.Exit(1)
2629
}
27-
return files
30+
31+
code := m.Run()
32+
teardown()
33+
os.Exit(code)
34+
}
35+
36+
// setupCLICommandsForTest copies GitBook-synced help markdown into the embed tree.
37+
// Teardown removes copied .md files and leaves the committed do-not-delete placeholder.
38+
func setupCLICommandsForTest() (func(), error) {
39+
embedDir, err := filepath.Abs(cliCommandsDir)
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
sourceDir, err := filepath.Abs(cliCommandsSourceDir)
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
sourceMatches, err := filepath.Glob(filepath.Join(sourceDir, "*.md"))
50+
if err != nil {
51+
return nil, err
52+
}
53+
if len(sourceMatches) == 0 {
54+
return nil, fmt.Errorf("expected .md files in %s", sourceDir)
55+
}
56+
57+
for _, match := range sourceMatches {
58+
data, err := os.ReadFile(match)
59+
if err != nil {
60+
return nil, err
61+
}
62+
dest := filepath.Join(embedDir, filepath.Base(match))
63+
if err := os.WriteFile(dest, data, 0o644); err != nil {
64+
return nil, err
65+
}
66+
}
67+
68+
return func() {
69+
matches, err := filepath.Glob(filepath.Join(embedDir, "*.md"))
70+
if err != nil {
71+
fmt.Fprintf(os.Stderr, "helpdocs test teardown: %v\n", err)
72+
return
73+
}
74+
for _, match := range matches {
75+
if err := os.Remove(match); err != nil {
76+
fmt.Fprintf(os.Stderr, "helpdocs test teardown: remove %s: %v\n", match, err)
77+
}
78+
}
79+
}, nil
80+
}
81+
82+
func loadCLICommandDocFilesFromDisk() (map[string]struct{}, error) {
83+
embedDir, err := filepath.Abs(cliCommandsDir)
84+
if err != nil {
85+
return nil, err
86+
}
87+
88+
return docFilesFromEmbed(os.DirFS(embedDir), "."), nil
2889
}
2990

3091
func Test_helpFileName(t *testing.T) {
@@ -33,9 +94,7 @@ func Test_helpFileName(t *testing.T) {
3394
assert.Equal(t, "secrets-test.md", helpFileName([]string{"secrets", "test"}))
3495
}
3596

36-
func Test_hasUserDoc(t *testing.T) {
37-
files := testDocFiles()
38-
97+
func Test_HasUserDoc(t *testing.T) {
3998
tests := map[string]struct {
4099
segments []string
41100
want bool
@@ -52,45 +111,28 @@ func Test_hasUserDoc(t *testing.T) {
52111

53112
for name, tc := range tests {
54113
t.Run(name, func(t *testing.T) {
55-
assert.Equal(t, tc.want, hasUserDoc(files, tc.segments))
114+
assert.Equal(t, tc.want, hasUserDoc(tc.segments, testCLICommandDocFiles))
56115
})
57116
}
58117
}
59118

60-
func Test_HasUserDoc_usesEmbeddedManifest(t *testing.T) {
61-
assert.True(t, HasUserDoc([]string{"test"}))
62-
assert.NotEmpty(t, docFiles)
63-
}
64-
65-
func Test_manifestFileSet_stripsCRLFLineEndings(t *testing.T) {
66-
files := manifestFileSet("test.md\r\ncontainer-test.md\r\n")
119+
func Test_HasUserDoc_usesEmbeddedCLICommands(t *testing.T) {
120+
require.NotEmpty(t, testCLICommandDocFiles)
121+
assert.Contains(t, testCLICommandDocFiles, "test.md")
122+
assert.True(t, hasUserDoc([]string{"test"}, testCLICommandDocFiles))
67123

68-
assert.True(t, hasUserDoc(files, []string{"test"}))
69-
assert.True(t, hasUserDoc(files, []string{"container", "test"}))
70-
assert.False(t, hasUserDoc(files, []string{"rainmaker"}))
124+
if len(docFiles) > 0 {
125+
assert.Equal(t, testCLICommandDocFiles, docFiles)
126+
assert.True(t, HasUserDoc([]string{"test"}))
127+
}
71128
}
72129

73-
func Test_manifestMatchesHelpCLICommands(t *testing.T) {
74-
helpDir := filepath.Join("..", "..", "..", "help", "cli-commands")
75-
entries, err := os.ReadDir(helpDir)
76-
require.NoError(t, err, "help/cli-commands must exist; run from repo root via go test ./pkg/helpdocs")
130+
func Test_docFilesFromEmbed(t *testing.T) {
131+
assert.True(t, hasUserDoc([]string{"test"}, testCLICommandDocFiles))
132+
assert.True(t, hasUserDoc([]string{"container", "test"}, testCLICommandDocFiles))
133+
assert.False(t, hasUserDoc([]string{"rainmaker"}, testCLICommandDocFiles))
77134

78-
var fromDisk []string
79-
for _, entry := range entries {
80-
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
81-
continue
82-
}
83-
fromDisk = append(fromDisk, entry.Name())
135+
if len(docFiles) > 0 {
136+
assert.Equal(t, testCLICommandDocFiles, docFilesFromEmbed(cliCommands, cliCommandsDir))
84137
}
85-
sort.Strings(fromDisk)
86-
87-
fromManifest := manifestEntries()
88-
assert.Equal(t, fromDisk, fromManifest,
89-
"embedded manifest.txt is out of sync with help/cli-commands; run: make -C cliv2 helpdocs-manifest")
90-
}
91-
92-
func manifestEntries() []string {
93-
entries := manifestLines(manifest)
94-
sort.Strings(entries)
95-
return entries
96138
}

0 commit comments

Comments
 (0)