diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index c970d1fd..be3085fd 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Code checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Go @@ -26,4 +26,18 @@ jobs: version: v0.184.0 args: release env: - GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} + - name: Docs checkout + uses: actions/checkout@v3 + with: + repository: algolia/cli-docs + path: docs + fetch-depth: 0 + token: ${{secrets.GORELEASER_GITHUB_TOKEN}} + - name: Update docs + env: + GIT_COMMITTER_NAME: algolia-ci + GIT_AUTHOR_NAME: algolia-ci + GIT_COMMITTER_EMAIL: noreply@algolia.com + GIT_AUTHOR_EMAIL: noreply@algolia.com + run: make docs-bump \ No newline at end of file diff --git a/Makefile b/Makefile index 15a93a7e..b11e3550 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,18 @@ .PHONY: test test: - go test ./... \ No newline at end of file + go test ./... + +## Documentation related tasks + +docs: + git clone https://github.com/algolia/cli-docs.git "$@" + +.PHONY: docs-bump +docs-bump: docs + git -C docs pull + git -C docs rm 'algolia_*.md' 2>/dev/null || true + go run ./cmd/docs --doc-path docs + rm -f docs/*.bak + git -C docs add 'algolia*.md' + git -C docs commit -m 'update docs' || true + git -C docs push diff --git a/cmd/docs/main.go b/cmd/docs/main.go new file mode 100644 index 00000000..c3001ddb --- /dev/null +++ b/cmd/docs/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/spf13/pflag" + + "github.com/algolia/cli/internal/docs" + "github.com/algolia/cli/pkg/cmd/root" + "github.com/algolia/cli/pkg/cmdutil" + "github.com/algolia/cli/pkg/config" + "github.com/algolia/cli/pkg/iostreams" +) + +func main() { + if err := run(os.Args); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run(args []string) error { + flags := pflag.NewFlagSet("", pflag.ContinueOnError) + dir := flags.StringP("doc-path", "", "", "Path directory where you want generate doc files") + help := flags.BoolP("help", "h", false, "Help about any command") + + if err := flags.Parse(args); err != nil { + return err + } + + if *help { + fmt.Fprintf(os.Stderr, "Usage of %s:\n\n%s", filepath.Base(args[0]), flags.FlagUsages()) + return nil + } + + if *dir == "" { + return fmt.Errorf("error: --doc-path not set") + } + + ios, _, _, _ := iostreams.Test() + rootCmd := root.NewRootCmd(&cmdutil.Factory{ + IOStreams: ios, + Config: &config.Config{}, + }) + rootCmd.InitDefaultHelpCmd() + + if err := os.MkdirAll(*dir, 0755); err != nil { + return err + } + + if err := docs.GenMarkdownTree(rootCmd, *dir, linkHandler); err != nil { + return err + } + + return nil + +} + +func linkHandler(name string) string { + return fmt.Sprintf("./%s", strings.TrimSuffix(name, ".md")) +} diff --git a/go.mod b/go.mod index b88f3d0b..519bae54 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/muesli/termenv v0.9.0 github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.2.1 + github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.9.0 github.com/stretchr/testify v1.7.0 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 diff --git a/internal/docs/markdown.go b/internal/docs/markdown.go new file mode 100644 index 00000000..b7056459 --- /dev/null +++ b/internal/docs/markdown.go @@ -0,0 +1,197 @@ +package docs + +import ( + "fmt" + "html/template" + "io" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +func printOptions(w io.Writer, cmd *cobra.Command) error { + flags := cmd.NonInheritedFlags() + flags.SetOutput(w) + if flags.HasAvailableFlags() { + fmt.Fprint(w, "### Options\n\n") + if err := printFlagsHTML(w, flags); err != nil { + return err + } + fmt.Fprint(w, "\n\n") + } + + parentFlags := cmd.InheritedFlags() + parentFlags.SetOutput(w) + if hasNonHelpFlags(parentFlags) { + fmt.Fprint(w, "### Options inherited from parent commands\n\n") + if err := printFlagsHTML(w, parentFlags); err != nil { + return err + } + fmt.Fprint(w, "\n\n") + } + return nil +} + +func hasNonHelpFlags(fs *pflag.FlagSet) (found bool) { + fs.VisitAll(func(f *pflag.Flag) { + if !f.Hidden && f.Name != "help" { + found = true + } + }) + return +} + +type flagView struct { + Name string + Varname string + Shorthand string + Usage string +} + +var flagsTemplate = ` +
{{ range . }} +
{{ if .Shorthand }}-{{.Shorthand}}, {{ end -}} + --{{.Name}}{{ if .Varname }} <{{.Varname}}>{{ end }}
+
{{.Usage}}
+{{ end }}
+` + +var tpl = template.Must(template.New("flags").Parse(flagsTemplate)) + +func printFlagsHTML(w io.Writer, fs *pflag.FlagSet) error { + var flags []flagView + fs.VisitAll(func(f *pflag.Flag) { + if f.Hidden || f.Name == "help" { + return + } + varname, usage := pflag.UnquoteUsage(f) + flags = append(flags, flagView{ + Name: f.Name, + Varname: varname, + Shorthand: f.Shorthand, + Usage: usage, + }) + }) + return tpl.Execute(w, flags) +} + +type CmdView struct { + Title string + Parent string + HasChildren bool +} + +var headerTemplate = `--- +layout: default +title: {{.Title}}{{if .HasChildren}} +has_children: true +{{end}} +{{if .Parent}}parent: {{.Parent}}{{end}} +--- +` + +var headerTpl = template.Must(template.New("header").Parse(headerTemplate)) + +func printHeaderMarkdown(w io.Writer, cmd *cobra.Command) error { + // Do no put everything under the "algolia" command + if cmd.Name() == "algolia" { + return headerTpl.Execute(w, &CmdView{ + Title: cmd.CommandPath(), + Parent: "", + HasChildren: false, + }) + } + + var parent string + if cmd.HasParent() { + parent = cmd.Parent().CommandPath() + if parent == "algolia" { + parent = "" + } + } + + return headerTpl.Execute(w, &CmdView{ + Title: cmd.CommandPath(), + Parent: parent, + HasChildren: cmd.HasSubCommands(), + }) +} + +func GenMarkdown(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error { + // Markdown header (with parent page if applicable) + if err := printHeaderMarkdown(w, cmd); err != nil { + return err + } + + fmt.Fprintf(w, "## %s\n\n", cmd.CommandPath()) + + hasLong := cmd.Long != "" + if !hasLong { + fmt.Fprintf(w, "%s\n\n", cmd.Short) + } + if cmd.Runnable() { + fmt.Fprintf(w, "```\n%s\n```\n\n", cmd.UseLine()) + } + if hasLong { + fmt.Fprintf(w, "%s\n\n", cmd.Long) + } + + if cmd.Commands() != nil && len(cmd.Commands()) > 0 { + fmt.Fprint(w, "### Commands\n\n") + for _, subcmd := range cmd.Commands() { + if !subcmd.IsAvailableCommand() { + continue + } + fmt.Fprintf(w, "* [%s](%s)\n", subcmd.CommandPath(), linkHandler(cmdDocsPath(subcmd))) + } + } + + if err := printOptions(w, cmd); err != nil { + return err + } + + if len(cmd.Example) > 0 { + fmt.Fprint(w, "### Examples\n\n{% highlight bash %}{% raw %}\n") + fmt.Fprint(w, cmd.Example) + fmt.Fprint(w, "{% endraw %}{% endhighlight %}\n\n") + } + + if cmd.HasParent() { + p := cmd.Parent() + fmt.Fprint(w, "### See also\n\n") + fmt.Fprintf(w, "* [%s](%s)\n", p.CommandPath(), linkHandler(cmdDocsPath(p))) + } + + return nil +} + +func GenMarkdownTree(cmd *cobra.Command, dir string, linkHandler func(string) string) error { + for _, c := range cmd.Commands() { + if c.Hidden { + continue + } + + if err := GenMarkdownTree(c, dir, linkHandler); err != nil { + return err + } + } + + filename := filepath.Join(dir, cmdDocsPath(cmd)) + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + if err := GenMarkdown(cmd, f, linkHandler); err != nil { + return err + } + return nil +} + +func cmdDocsPath(c *cobra.Command) string { + return strings.ReplaceAll(c.CommandPath(), " ", "_") + ".md" +} diff --git a/pkg/cmd/open/open.go b/pkg/cmd/open/open.go index 47c87672..7e1717ce 100644 --- a/pkg/cmd/open/open.go +++ b/pkg/cmd/open/open.go @@ -84,16 +84,16 @@ func NewOpenCmd(f *cmdutil.Factory) *cobra.Command { Long: `The open command provices shortcuts to quickly let you open pages to Algolia within your browser. 'algolia open --list' for a list of supported shortcuts.`, Example: heredoc.Doc(` # Display the list of supported shortcuts - algolia open --list + $ algolia open --list # Open the dashboard for the current application - algolia open dashboard - + $ algolia open dashboard + # Open the API reference - algolia open api - + $ algolia open api + # Open the documentation - algolia open docs + $ algolia open docs `), RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 0 {