diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 20d39933..0c5d5c66 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -45,7 +45,7 @@ jobs: - name: Docs checkout uses: actions/checkout@v4 with: - repository: algolia/doc + repository: algolia/docs-new path: docs fetch-depth: 0 token: ${{secrets.GORELEASER_GITHUB_TOKEN}} diff --git a/Makefile b/Makefile index a8a50e7e..641c639c 100644 --- a/Makefile +++ b/Makefile @@ -9,32 +9,23 @@ test: go test ./... -p 1 .PHONY: test -## Build & publish the old documentation -VARIATION ?= old -ifeq ($(VARIATION),old) -DOCS_FOLDER = docs -DOCS_GENERATED_PATH = app_data/cli/commands -DOCS_REPO_URL = git@github.com:algolia/doc.git -DOCS_BRANCH = master -DOCS_EXTENSION = yml -else ifeq ($(VARIATION),new) -DOCS_FOLDER = new-world-docs -DOCS_GENERATED_PATH = apps/docs/content/pages/tools/cli/commands -DOCS_REPO_URL = https://github.com/algolia/new-world-docs.git +## Build & publish the CLI documentation +DOCS_FOLDER ?= docs +DOCS_GENERATED_PATH = doc/tools/cli/commands +DOCS_REPO_URL = https://github.com/algolia/docs-new.git DOCS_BRANCH = main -DOCS_EXTENSION = mdx -endif docs: git clone $(DOCS_REPO_URL) "$@" .PHONY: docs-commands-data docs-commands-data: docs - git -C $(DOCS_FOLDER) pull git -C $(DOCS_FOLDER) checkout $(DOCS_BRANCH) - git -C $(DOCS_FOLDER) rm '$(DOCS_GENERATED_PATH)/*.$(DOCS_EXTENSION)' 2>/dev/null || true - go run ./cmd/docs --app_data-path $(DOCS_FOLDER)/$(DOCS_GENERATED_PATH) --target $(VARIATION) - git -C $(DOCS_FOLDER) add '$(DOCS_GENERATED_PATH)/*.$(DOCS_EXTENSION)' + git -C $(DOCS_FOLDER) pull --ff-only origin $(DOCS_BRANCH) + rm -rf "$(DOCS_FOLDER)/$(DOCS_GENERATED_PATH)" + mkdir -p "$(DOCS_FOLDER)/$(DOCS_GENERATED_PATH)" + go run ./cmd/docs --app_data-path "$(DOCS_FOLDER)/$(DOCS_GENERATED_PATH)" --target new + git -C $(DOCS_FOLDER) add -A "$(DOCS_GENERATED_PATH)" .PHONY: docs-pr docs-pr: docs-commands-data diff --git a/Taskfile.yml b/Taskfile.yml index b7394f52..c25a9a96 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -2,12 +2,12 @@ version: 3 output: prefixed dotenv: [.env] vars: - # The URL to the "old" Algolia docs repo - docs_remote: https://github.com/algolia/doc.git - # The temporary folder for the generated YML files + # The URL to the current Algolia docs repo + docs_remote: https://github.com/algolia/docs-new.git docs_local: docs - cli_ref_path: app_data/cli/commands - yml_folder: tmp + cli_ref_path: doc/tools/cli/commands + # The temporary folder for the generated MDX files + docs_output_dir: tmp tasks: build: desc: Build the binary @@ -103,23 +103,23 @@ tasks: internal: true cmd: git clone --depth=1 {{ .docs_remote }} {{ .docs_local }} generate-command-reference: - desc: Generate updated YML files for the CLI command reference + desc: Generate updated MDX files for the CLI command reference internal: true - cmd: go run ./cmd/docs --app_data-path {{ .yml_folder }} + cmd: go run ./cmd/docs --app_data-path {{ .docs_output_dir }} --target new update-command-reference: - desc: Add the updated YML files to the docs + desc: Add the updated MDX files to the docs summary: | This task clones the Algolia docs repo, - adds the updated CLI reference yml files to it, + adds the updated CLI reference mdx files to it, and pushes a new PR to the GitHub repo. internal: true cmds: - | git -C {{ .docs_local }} checkout -B chore/cli-$(git rev-parse --short HEAD) - git -C {{ .docs_local }} rm "{{ .cli_ref_path }}/*.yml" + rm -rf {{ .docs_local }}/{{ .cli_ref_path }} mkdir -p {{ .docs_local }}/{{ .cli_ref_path }} - mv {{ .yml_folder }}/*.yml {{ .docs_local }}/{{ .cli_ref_path }}/ - git -C {{ .docs_local }} add "{{ .cli_ref_path }}/*.yml" + mv {{ .docs_output_dir }}/* {{ .docs_local }}/{{ .cli_ref_path }}/ + git -C {{ .docs_local }} add -A {{ .cli_ref_path }} git -C {{ .docs_local }} commit --message 'chore: Update CLI command reference' env: GIT_COMMITTER_NAME: algolia-ci @@ -129,4 +129,4 @@ tasks: cleanup: desc: Cleanup the docs files internal: true - cmd: rm -rf {{ .docs_local }} {{ .yml_folder }} || true + cmd: rm -rf {{ .docs_local }} {{ .docs_output_dir }} || true diff --git a/cmd/docs/main.go b/cmd/docs/main.go index d9476351..fe279aeb 100644 --- a/cmd/docs/main.go +++ b/cmd/docs/main.go @@ -30,16 +30,16 @@ func run(args []string) error { "Path directory where you want generate documentation data files", ) help := flags.BoolP("help", "h", false, "Help about any command") - target := flags.StringP("target", "T", "old", "target old or new documentation website") - - if *target != "old" && *target != "new" { - return fmt.Errorf("error: --destination can only be 'old' or 'new' ('old' by default)") - } + target := flags.StringP("target", "T", "new", "target old or new documentation website") if err := flags.Parse(args); err != nil { return err } + if *target != "old" && *target != "new" { + return fmt.Errorf("error: --target can only be 'old' or 'new' ('new' by default)") + } + if *help { fmt.Fprintf(os.Stderr, "Usage of %s:\n\n%s", filepath.Base(args[0]), flags.FlagUsages()) return nil diff --git a/internal/docs/docs.go b/internal/docs/docs.go index 5962d97c..825306d8 100644 --- a/internal/docs/docs.go +++ b/internal/docs/docs.go @@ -118,15 +118,7 @@ func getCommands(cmd *cobra.Command) []Command { } command := newCommand(c) if c.HasAvailableSubCommands() { - for _, s := range c.Commands() { - sub := newCommand(s) - if s.HasAvailableSubCommands() { - for _, sus := range s.Commands() { - sub.SubCommands = append(sub.SubCommands, newCommand(sus)) - } - } - command.SubCommands = append(command.SubCommands, sub) - } + command.SubCommands = getCommands(c) } commands = append(commands, command) } diff --git a/internal/docs/mdx.go b/internal/docs/mdx.go index 5df70973..74bbd0a4 100644 --- a/internal/docs/mdx.go +++ b/internal/docs/mdx.go @@ -1,7 +1,7 @@ package docs import ( - "fmt" + _ "embed" "os" "path/filepath" "strings" @@ -10,31 +10,85 @@ import ( "github.com/spf13/cobra" ) +const mdxDocsRootSlug = "tools/cli/commands" + +//go:embed mdx.tpl +var mdxTemplate string + +type mdxPage struct { + Command + Slug string + SubPages []mdxPage +} + func GenMdxTree(cmd *cobra.Command, dir string) error { tpl, err := template.New("mdx.tpl").Funcs(template.FuncMap{ "getExamples": func(cmd Command) []Example { return cmd.ExamplesList() }, - }).ParseFiles("internal/docs/mdx.tpl") + }).Parse(mdxTemplate) if err != nil { return err } - commands := getCommands(cmd) + return writeMdxPageTree(tpl, dir, newMdxPage(describeCommand(cmd))) +} - for _, c := range commands { - c.Slug = strings.ReplaceAll(c.Name, " ", "-") - filename := filepath.Join(dir, fmt.Sprintf("%s.mdx", c.Slug)) - file, err := os.Create(filename) - if err != nil { - return err - } +func newMdxPage(cmd Command) mdxPage { + page := mdxPage{ + Command: cmd, + Slug: buildMdxSlug(commandPathParts(cmd)), + } + + for _, subCommand := range cmd.SubCommands { + page.SubPages = append(page.SubPages, newMdxPage(subCommand)) + } + + return page +} + +func writeMdxPageTree(tpl *template.Template, dir string, page mdxPage) error { + filename := filepath.Join(dir, commandOutputDir(page.Command), "index.mdx") + if err := os.MkdirAll(filepath.Dir(filename), 0o755); err != nil { + return err + } + + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + + if err := tpl.Execute(file, page); err != nil { + return err + } - err = tpl.Execute(file, c) - if err != nil { + for _, subPage := range page.SubPages { + if err := writeMdxPageTree(tpl, dir, subPage); err != nil { return err } } return nil } + +func commandPathParts(cmd Command) []string { + parts := strings.Fields(cmd.Name) + if len(parts) <= 1 { + return nil + } + + return parts[1:] +} + +func commandOutputDir(cmd Command) string { + return filepath.Join(commandPathParts(cmd)...) +} + +func buildMdxSlug(parts []string) string { + if len(parts) == 0 { + return mdxDocsRootSlug + } + + return mdxDocsRootSlug + "/" + strings.Join(parts, "/") +} diff --git a/internal/docs/mdx.tpl b/internal/docs/mdx.tpl index 1e6d45a3..2fe82cce 100644 --- a/internal/docs/mdx.tpl +++ b/internal/docs/mdx.tpl @@ -4,72 +4,49 @@ title: |- {{ .Name }} description: |- {{ .Description }} -slug: tools/cli/commands/{{ .Slug }} +slug: {{ .Slug }} --- -{{ if .SubCommands }}{{ range $subCommand := .SubCommands }}{{ if $subCommand.SubCommands }}{{ range $susCommand := $subCommand.SubCommands}} -## {{ $susCommand.Name }} - -{{ $susCommand.Description }} +{{ if .Description }} +{{ .Description }} +{{ end }} -### Usage +## Usage -`{{ $susCommand.Usage }}` +`{{ .Usage }}` -{{ $examples := getExamples $susCommand }}{{ if $examples }} -### Examples -{{ range $example := $examples }}{{ $example.Desc }} +{{ if .Aliases }} +## Aliases -```sh {{ if $example.WebCLICommand }}command="{{$example.WebCLICommand}}"{{ end }} -{{ $example.Code }} -``` -{{ end }}{{ end }}{{ range $flagKey, $flagSlice := $susCommand.Flags }}{{ if $flagSlice }} -### Flags -{{ range $flag := $flagSlice }} -- {{ if $flag.Shorthand }}`-{{ $flag.Shorthand }}`, {{ end }}`--{{ $flag.Name }}`: {{ $flag.Description }} -{{ end }}{{ end }}{{ end }}{{ end }} -{{ else }} -## {{ $subCommand.Name }} - -{{ $subCommand.Description }} +{{ range $alias := .Aliases -}} +- `{{ $alias }}` +{{ end }} +{{ end }} -### Usage +{{ if .SubPages }} +## Subcommands -`{{ $subCommand.Usage }}` +{{ range $subPage := .SubPages -}} +- [`{{ $subPage.Name }}`](/{{ $subPage.Slug }}): {{ $subPage.Description }} +{{ end }} +{{ end }} -{{ $examples := getExamples $subCommand }} -{{ if $examples }} -### Examples +{{ $examples := getExamples .Command }}{{ if $examples }} +## Examples {{ range $example := $examples }} {{ $example.Desc }} -```sh {{ if $example.WebCLICommand }}command="{{$example.WebCLICommand}}"{{ end }} +```sh{{ if $example.WebCLICommand }} command="{{$example.WebCLICommand}}"{{ end }} {{ $example.Code }} ``` {{ end }} {{ end }} -{{ range $flagKey, $flagSlice := $subCommand.Flags }} +{{ range $flagKey, $flagSlice := .Flags -}} {{ if $flagSlice }} -### Flags -{{ range $flag := $flagSlice }} -- {{ if $flag.Shorthand }}`-{{ $flag.Shorthand }}`, {{ end }}`--{{ $flag.Name }}`: {{ $flag.Description }} -{{ end }}{{ end }}{{ end }}{{ end}}{{ end }} -{{ else }}## {{ .Name }} - -### Usage - -`{{ .Usage }}` -{{ $examples := getExamples . }}{{ if $examples }} -### Examples -{{ range $example := $examples }} -{{ $example.Desc }} +## {{ $flagKey }} -```sh {{ if $example.WebCLICommand }}command="{{$example.WebCLICommand}}"{{ end }} -{{ $example.Code }} -``` -{{ end }}{{ end }} -{{ range $flagKey, $flagSlice := .Flags }} -### {{ $flagKey }} -{{ range $flag := $flagSlice }} +{{ range $flag := $flagSlice -}} - {{ if $flag.Shorthand }}`-{{ $flag.Shorthand }}`, {{ end }}`--{{ $flag.Name }}`: {{ $flag.Description }} -{{ end }}{{ end }}{{ end }} \ No newline at end of file +{{ end }} +{{ end }} +{{ end }} \ No newline at end of file diff --git a/internal/docs/mdx_test.go b/internal/docs/mdx_test.go new file mode 100644 index 00000000..e6e9b61a --- /dev/null +++ b/internal/docs/mdx_test.go @@ -0,0 +1,84 @@ +package docs + +import ( + "os" + "path/filepath" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" +) + +func TestGetCommandsRecursivelyIncludesNestedCommands(t *testing.T) { + root := &cobra.Command{Use: "algolia", Short: "Algolia CLI"} + one := &cobra.Command{Use: "one", Short: "Level one"} + two := &cobra.Command{Use: "two", Short: "Level two"} + three := &cobra.Command{Use: "three", Short: "Level three"} + four := &cobra.Command{ + Use: "four", + Short: "Level four", + RunE: func(*cobra.Command, []string) error { + return nil + }, + } + + three.AddCommand(four) + two.AddCommand(three) + one.AddCommand(two) + root.AddCommand(one) + + commands := getCommands(root) + require.Len(t, commands, 1) + require.Len(t, commands[0].SubCommands, 1) + require.Len(t, commands[0].SubCommands[0].SubCommands, 1) + require.Len(t, commands[0].SubCommands[0].SubCommands[0].SubCommands, 1) + require.Equal( + t, + "algolia one two three four", + commands[0].SubCommands[0].SubCommands[0].SubCommands[0].Name, + ) +} + +func TestGenMdxTreeWritesNestedCommandPages(t *testing.T) { + root := &cobra.Command{Use: "algolia", Short: "Algolia CLI"} + events := &cobra.Command{Use: "events", Short: "Manage events"} + sources := &cobra.Command{Use: "sources", Short: "Manage event sources"} + list := &cobra.Command{ + Use: "list", + Short: "List event sources", + Example: "# List sources\n$ algolia events sources list", + RunE: func(*cobra.Command, []string) error { + return nil + }, + } + list.Flags().StringP("format", "F", "json", "Output format") + + sources.AddCommand(list) + events.AddCommand(sources) + root.AddCommand(events) + + dir := t.TempDir() + require.NoError(t, GenMdxTree(root, dir)) + + rootContent := readTestFile(t, filepath.Join(dir, "index.mdx")) + require.Contains(t, rootContent, "slug: tools/cli/commands") + require.Contains(t, rootContent, "[`algolia events`](/tools/cli/commands/events)") + + eventsContent := readTestFile(t, filepath.Join(dir, "events", "index.mdx")) + require.Contains(t, eventsContent, "slug: tools/cli/commands/events") + require.Contains(t, eventsContent, "[`algolia events sources`](/tools/cli/commands/events/sources)") + + listContent := readTestFile(t, filepath.Join(dir, "events", "sources", "list", "index.mdx")) + require.Contains(t, listContent, "slug: tools/cli/commands/events/sources/list") + require.Contains(t, listContent, "`algolia events sources list [flags]`") + require.Contains(t, listContent, "`-F`, `--format`") +} + +func readTestFile(t *testing.T, path string) string { + t.Helper() + + content, err := os.ReadFile(path) + require.NoError(t, err) + + return string(content) +}