Skip to content

feat: add support for user-doc-based help routing and manifest generation#6884

Open
robertolopezlopez wants to merge 1 commit into
mainfrom
feat/CLI-1474
Open

feat: add support for user-doc-based help routing and manifest generation#6884
robertolopezlopez wants to merge 1 commit into
mainfrom
feat/CLI-1474

Conversation

@robertolopezlopez

@robertolopezlopez robertolopezlopez commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

User description

Pull Request Submission Checklist

  • Follows CONTRIBUTING guidelines
  • Commit messages
    are release-note ready, emphasizing
    what was changed, not how.
  • Includes detailed description of changes
  • Contains risk assessment (Low | Medium | High)
  • Highlights breaking API changes (if applicable)
  • Links to automated tests covering new functionality
  • Includes manual testing instructions (if necessary)
  • Updates relevant GitBook documentation (PR link: ___)
  • Includes product update to be announced in the next stable release notes

What does this PR do?

Shows help generated by Cobra for CLI commands which don't have any GitBook help - existing GitBook help remains as source of truth.

Changes

Help routing now follows this order:

  1. GitBook user-doc exists -> existing markdown help (legacy CLI)
  2. No user-doc -> Cobra usage + flags
  3. Unknown command / root help -> generic README (unchanged)

How to find out: generic README contains How to get started. Cobra help contains Usage: and Flags:.

Before

snyk-macos-arm64 secrets test --help | head -20
CLI help
  Snyk CLI scans and monitors your projects for security vulnerabilities and license issues.

  For more information, visit the Snyk website https://snyk.io/

  For details, see the CLI documentation https://docs.snyk.io/snyk-cli

How to get started
  1. Authenticate by running snyk auth.
  2. Test your local project with snyk test.
  3. Get alerted for new vulnerabilities with snyk monitor.

Available commands
  To learn more about each Snyk CLI command, use the --help option, for example, snyk auth --help.

  Note: The help on the docs site is the same as the --help in the CLI.

  snyk auth
    Authenticate Snyk CLI with a Snyk account.

After

snyk-macos-arm64 secrets test --help           
Usage:
  snyk secrets test [flags]

Flags:
      --exclude string                        Ignores all issues originating from the specified file path.
  -h, --help                                  help for test
      --include-ignores                       Shows all discovered issues, including any that have been previously ignored.
      --json                                  Print results on the console as a JSON data structure.
      --json-file-output string               Save test output as a JSON data structure directly to the specified file, regardless of whether or not you use the --json option.
      --project-business-criticality string   Set the project business criticality project attribute to one or more values (comma-separated).
      --project-environment string            Set the project environment project attribute to one or more values (comma-separated).
      --project-lifecycle string              Set the project lifecycle project attribute to one or more values (comma-separated).
      --project-tags string                   Set the project tags to one or more values (comma-separated key value pairs withan "=" separator).
      --remote-repo-url string                Set or override the remote URL for the repository.
      --report                                Share results with the Snyk Web UI.
      --sarif                                 Return results in SARIF format.
      --sarif-file-output string              Save test output in SARIF format directly to the specified file, regardless of whether or not you use the --sarif option.
      --severity-threshold string             Report only vulnerabilities at the specified level or higher.
      --target-name string                    Used in Share Results to set or override the project name for the repository. 
      --target-reference string               Used in Share Results to specify a reference which differentiates this project, e.g. a branch name or version.

Global Flags:
      --DISABLE_ANALYTICS         
  -d, --debug                     
      --insecure                  
      --integration-name string   
      --log-level string           (default "debug")
      --max-attempts int          Maximum total network attempts, including the initial request (minimum: 1) (default -1)
      --org string                
      --proxy-noauth

Where should the reviewer start?

How should this be manually tested?

Setup

make build BUILD_MODE=public
BIN=./binary-releases/snyk-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/amd64/')

Adjust BIN for your platform if needed (e.g. snyk-macos-arm64, snyk-linux-amd64).

What changed

Help routing now follows this order:

  1. GitBook user-doc exists → existing markdown help (legacy CLI)
  2. No user-doc → Cobra usage + flags
  3. Unknown command / root help → generic README (unchanged)

Quick signal: generic README contains How to get started. Cobra help contains Usage: and Flags:.


1. Undocumented Go commands → Cobra help (the fix)

Command Expect
snyk secrets test --help Usage: + Flags:; not generic README
snyk agent-scan --help Cobra help
snyk depgraph --help Cobra help
snyk secrets test --bad-flag Same Cobra help as above (flag error keeps command context)
"$BIN" secrets test --help | head -20
"$BIN" secrets test --bad-flag 2>&1 | head -20

2. Documented commands → user-doc unchanged

Command Expect
snyk test --help Markdown starting with Test + Description
snyk container --help Container user-doc (test and continuously monitor container images…)
snyk iac --help Infrastructure as Code user-doc
snyk redteam setup --help Parent doc (redteam.md; walk-back)
"$BIN" test --help | head -10
"$BIN" -h container | head -10
"$BIN" iac -help | head -10
"$BIN" redteam setup --help | head -10

3. Root / unknown help → README unchanged

Command Expect
snyk --help Generic README (Snyk CLI scans and monitors…)
snyk help Same README
snyk help hello Same README (unknown subcommand)
snyk --help hello Same README

4. Legacy help flag forms (acceptance parity)

"$BIN" --help=iac | head -10           # IaC user-doc
"$BIN" --help container test | head -10 # container-test user-doc

Optional grep smoke checks

# Undocumented → must NOT show generic README
"$BIN" secrets test --help | grep -q "How to get started" && echo FAIL || echo PASS

# Undocumented → must show Cobra flags
"$BIN" secrets test --help | grep -q "^Flags:" && echo PASS || echo FAIL

# Documented → must still show user-doc
"$BIN" test --help | grep -q "^Test" && echo PASS || echo FAIL

# Legacy form
"$BIN" -h container | grep -q "test and continuously monitor container images" && echo PASS || echo FAIL

Regression check

Normal commands should still run (help routing only affects --help / flag errors):

"$BIN" --version

What's the product update that needs to be communicated to CLI users?


PR Type

Enhancement, Documentation, Tests


Description

  • Implement user-documentation-based help routing.

  • Prioritize Markdown help files over Cobra's native help.

  • Generate and embed a manifest of available help documentation.

  • Update tests and contribute guide for new help system.


Diagram Walkthrough

flowchart LR
  A["CLI Invoke"] --> B("Help Request");
  B --> C{Help Router};
  C -- "User Doc Available?" --> D["Show Legacy (Markdown) Help"];
  C -- "No User Doc" --> E["Show Cobra Help"];
  C -- "Unknown Command" --> D;
Loading

File Walkthrough

Relevant files
Tests
4 files
help.spec.ts
Update help command tests for new output format.                 
+3/-5     
main_test.go
Add tests for new help routing and setup.                               
+8/-1     
helpdocs_test.go
Add unit tests for help documentation lookup.                       
+94/-0   
helprouting_test.go
Add comprehensive tests for help routing scenarios.           
+209/-0 
Enhancement
5 files
main.go
Integrate new help routing logic into CLI core.                   
+20/-17 
helpdocs.go
Add logic for checking and identifying user documentation.
+55/-0   
helprouting.go
Implement core help routing logic and decision making.     
+172/-0 
Makefile
Add Makefile target for help docs manifest generation.     
+15/-2   
manifest.txt
Embed manifest of available user help documentation.         
+32/-0   
Documentation
1 files
CONTRIBUTING.md
Update contribution guide for CLI help commands.                 
+27/-0   

@robertolopezlopez robertolopezlopez self-assigned this Jun 5, 2026
@snyk-io

snyk-io Bot commented Jun 5, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@robertolopezlopez robertolopezlopez marked this pull request as ready for review June 5, 2026 13:47
@robertolopezlopez robertolopezlopez requested a review from a team as a code owner June 5, 2026 13:47
@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

@robertolopezlopez

Copy link
Copy Markdown
Contributor Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (636428d)

@robertolopezlopez

Copy link
Copy Markdown
Contributor Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (636428d)

Comment thread cliv2/pkg/core/help_routing.go Outdated
Comment on lines +22 to +30
var runLegacyHelp = func() error {
args := utils.RemoveSimilar(os.Args[1:], "--")
args = append(args, "--help")
return defaultCmd(args)
}

func legacyHelp() error {
return runLegacyHelp()
}

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.

maybe we can just simplify to have only runLegacyHelp

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.

done

Comment thread cliv2/pkg/core/help_routing.go Outdated
Comment thread cliv2/Makefile
@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

Comment thread cliv2/pkg/core/help_routing.go Outdated
}

args := helpTargetArgs(os.Args[1:])
if len(args) > 0 && args[0] == "help" {

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.

the "help" string is being used multiple times, please move to a constant

Comment thread cliv2/pkg/core/help_routing.go Outdated
Comment on lines +106 to +113
func resolveHelpCommand(root *cobra.Command) *cobra.Command {
if root == nil {
return nil
}
args := helpTargetArgs(os.Args[1:])
if len(args) == 0 {
return nil
}

@danskmt danskmt Jun 10, 2026

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.

should here check if os.Args have parameters before accessing? Well actually it kinda does that, so maybe ignore this comment

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.

https://pkg.go.dev/os#Args

Args hold the command-line arguments, starting with the program name.

also to double check what may happen... https://go.dev/play/p/C_KEdMptOeV

image

Comment on lines +141 to +153
for _, arg := range argv {
if isHelpFlag(arg) {
continue
}
if strings.HasPrefix(arg, "--help=") {
out = append(out, strings.TrimPrefix(arg, "--help="))
continue
}
if strings.HasPrefix(arg, "-") {
continue
}
out = append(out, arg)
}

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.

here you are using if hasPrefix of --help, but line 157 you have isHelpFlag function, maybe that should be used?

func isHelpFlag(arg string) bool {
	// -help is a legacy cliv1 flag
	return arg == "--help" || arg == "-h" || arg == "-help"
}

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.

isHelpFlag() and line 145 handle different help syntax.

input isHelpFlag() output helpTargetArgs() output as it is now with isHelpFlag()
["--help=iac"] false ["iac"] []
["--help", "container"] true (first element + continue) ["container"] []

I do not think isHelpFlag() is appropriate to use in line 145

Comment on lines +19 to +31
tests := map[string]struct {
argv []string
command func(t *testing.T, root *cobra.Command) (*cobra.Command, *bytes.Buffer)
wantLegacy bool
cobraOut []string
}{
"top-level --help uses legacy readme": {
argv: []string{"snyk", "--help"},
command: func(_ *testing.T, root *cobra.Command) (*cobra.Command, *bytes.Buffer) {
return root, nil
},
wantLegacy: true,
},

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.

Optional: this table of tests is mixing of using legacy and cobra. Maybe is worth changing to two separate table of tests: one with legacy docs and one with cobra

@robertolopezlopez robertolopezlopez Jun 10, 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.

Mmmh in my opinion checking the two boundaries is correct here, without split. Other tests such as Test_helpTargetArgs or Test_resolveHelpTargetForCobra are already split, but Test_help_routing stays on the top layer: given a help command, which help should be used?

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.

it was just more about having different scenarios a bit better split

Comment thread cliv2/pkg/core/main.go Outdated
rInfo := runtimeinfo.New(runtimeinfo.WithName("snyk-cli"), runtimeinfo.WithVersion(cliv2.GetFullVersion()))

rootCommand := prepareRootCommand()
globalRootCommand = rootCommand

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.

do we really need this globalRootCommand?

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.

You won here, and I extracted helprouting*.go to a separate package for better separation of concerns. No more globalRootCommand

Comment thread cliv2/pkg/core/main.go Outdated
err = defaultCmd(os.Args[1:])
case handleErrorShowHelp:
err = help(nil, []string{})
err = help(resolveHelpCommand(rootCommand), []string{})

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.

if have globalRootCommand, then why use 'rootCommand' here? Maybe could drop the global one?

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.

Yep, this is a design decision.

  • main.go#prepareRootCommand() returns the main snyk command.
  • line 538: store the main snyk command at package level
  • help_routing#help() makes use of it:
    • for root.Find(args) (line 65)
    • then, deciding legacy help or Cobra help.

in any case, while thinking over all this logic, I decided to refactor it :-D

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.

I extracted help routing logic to a separate package and globalRootCommand does not exist any more

@snyk-pr-review-bot

This comment has been minimized.

@robertolopezlopez

Copy link
Copy Markdown
Contributor Author

I got an interesting edge case to spice things up a bit: GAF has this workflow snyk ignore create - this one does not have documentation currently, but there is the older command snyk ignore that does have documentation.

Currently, snyk ignore create --help print out the older command help messages, and I'm wondering if it would be possible to print out the one generated by Cobra for this case.

This is NOT a blocker for this PR, just wondering if it's feasible to look into as well 😉 And I think it would be better with a separate PR, just wanted to bring it up since it looked related to #6892 & #6893, but those have a different approach.

I will check @CatalinSnyk

@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

Comment thread cliv2/pkg/helprouting/helprouting.go Outdated

const (
helpCommand = "help"
flagLong = "--help"

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.

maybe the name should be helpFlag? or just leave helpCommand and concat in the place, idk better reuse the helpCommand if possible, can even be a helper function commandToFlag

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.

yeah the naming is not perfect

@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

@robertolopezlopez

Copy link
Copy Markdown
Contributor Author

I got an interesting edge case to spice things up a bit: GAF has this workflow snyk ignore create - this one does not have documentation currently, but there is the older command snyk ignore that does have documentation.

Currently, snyk ignore create --help print out the older command help messages, and I'm wondering if it would be possible to print out the one generated by Cobra for this case.

This is NOT a blocker for this PR, just wondering if it's feasible to look into as well 😉 And I think it would be better with a separate PR, just wanted to bring it up since it looked related to #6892 & #6893, but those have a different approach.

@CatalinSnyk after checking carefully, I believe a good approach to fix this issue would be merging #6892 (which adds help/cli-commands/ignore-create.md) and run make build after... maybe it could even be done in that branch, I will ask

@robertolopezlopez

Copy link
Copy Markdown
Contributor Author

/describe

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Description updated to latest commit (ea08524)

@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

Comment thread cliv2/pkg/core/main.go Outdated
}

func runLegacyHelp() error {
return defaultCmd(append(utils.RemoveSimilar(os.Args[1:], "--"), "--help"))

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.

too many function calls in a single line, easy to misread. I'd suggest separating into variables

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.

ok ok I needed to do one extra modification in any case

Comment thread CONTRIBUTING.md

`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

@danskmt danskmt left a comment

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.

LGTM - left some few comments

@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot

This comment has been minimized.

Comment thread CONTRIBUTING.md
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.

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?

Comment thread CONTRIBUTING.md
### 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.

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Reviewer Guide 🔍

🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Incorrect Exit Code 🟠 [major]

On flag errors (e.g., unknown flags), handleErrorResult is handleErrorShowHelp. The code then invokes helpRouter.Help(), which returns nil after successfully printing Cobra usage. This overwrites the original err (the flag error) with nil. Consequently, when tearDown(err, ...) is called later, it derives a success exit code (0) instead of the standard error code for invalid input (e.g., 2), causing automated pipelines to ignore invalid CLI usage.

case handleErrorShowHelp:
	err = helpRouter.Help(nil, rootCommand, os.Args[1:])
Logic Contradiction 🟡 [minor]

The hasUserDoc logic includes a recursive walk-back that returns true if any parent command has a documentation file. For example, if iac.md exists but iac test-new (a native Go extension) does not, this function returns true. This causes the router to show the generic legacy iac help instead of the specific Cobra usage/flags for test-new, which contradicts the PR's stated goal of showing native help for commands without GitBook docs.

for len(args) > 0 {
	if _, ok := files[helpFileName(args)]; ok {
		return true
	}
	args = args[:len(args)-1]
}
📚 Repository Context Analyzed

This review considered 34 relevant code sections from 15 files (average relevance: 0.94)

🤖 Repository instructions applied (from AGENTS.md)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants