From ab9d965393af4ab720c5ae11da9a9866f36da97f Mon Sep 17 00:00:00 2001 From: Dylan Tientcheu Date: Wed, 22 Apr 2026 12:33:55 +0200 Subject: [PATCH] chore: update CLI docs release automation Generate CLI command docs for the current docs site so releases stop opening PRs against the archived repo and publish nested MDX pages in the new docs tree. Made-with: Cursor --- .github/workflows/releases.yml | 2 +- Makefile | 27 ++++------- Taskfile.yml | 26 +++++------ cmd/docs/main.go | 10 ++-- internal/docs/docs.go | 10 +--- internal/docs/mdx.go | 78 ++++++++++++++++++++++++++----- internal/docs/mdx.tpl | 77 +++++++++++-------------------- internal/docs/mdx_test.go | 84 ++++++++++++++++++++++++++++++++++ 8 files changed, 206 insertions(+), 108 deletions(-) create mode 100644 internal/docs/mdx_test.go 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) +}