-
Notifications
You must be signed in to change notification settings - Fork 687
feat: add support for user-doc-based help routing and manifest generation #6884
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| 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. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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/`. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so every
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes it will execute during 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 ( This test is a safeguard so we never forget to add commands with user docs to The updated |
||
|
|
||
| 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. | ||
|
|
||
| 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 | ||
| } |
| 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 | ||
| } |
| 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 |
There was a problem hiding this comment.
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.