diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 418623a7..a35a3351 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,15 @@ First, update [commands.yml](temporalcli/commandsgen/commands.yml) following the This will expect every non-parent command to have a `run` method, so for new commands developers will have to implement `run` on the new command in a separate file before it will compile. +Once a command is updated, the CI will automatically generate new docs +and create a PR in the Documentation repo with the corresponding updates. To generate these docs locally, run: + + go run ./temporalcli/internal/cmd/gen-docs + +This will auto-generate a new set of docs to `temporalcli/docs/`. If a new root command is added, a new file will be automatically generated, like `temporal activity` and `activity.mdx`. + ## Inject additional build-time information + To add build-time information to the version string printed by the binary, use go build -ldflags "-X github.com/temporalio/cli/temporalcli.buildInfo=" diff --git a/temporalcli/commandsgen/commands.yml b/temporalcli/commandsgen/commands.yml index a3055241..b5ab4231 100644 --- a/temporalcli/commandsgen/commands.yml +++ b/temporalcli/commandsgen/commands.yml @@ -213,6 +213,20 @@ commands: ``` option-sets: - client + docs: + description-header: >- + Learn how to use Temporal Activity commands for completing or failing + Activity Executions in your Workflow. Optimize your Temporal Workflow + management effectively. + keywords: + - activity + - activity complete + - activity execution + - activity fail + - cli reference + - cli-feature + - command-line-interface-cli + - temporal cli - name: temporal activity complete summary: Complete an Activity @@ -308,6 +322,20 @@ commands: and Query creation. option-sets: - client + docs: + description-header: >- + Use Temporal CLI to manage multiple Workflow Executions with Batch + Jobs that can Cancel, Signal, or Terminate Workflows. Filter and monitor + Batch Jobs effectively. + keywords: + - batch + - batch describe + - batch list + - batch terminate + - cli reference + - cli-feature + - command-line-interface-cli + - temporal cli - name: temporal batch describe summary: Show batch job progress @@ -382,6 +410,22 @@ commands: Temporal CLI checks for an `--env` option first, then checks for the `TEMPORAL_ENV` environment variable. If neither is set, the CLI uses the "default" environment. + docs: + description-header: >- + Temporal CLI 'env' commands allow the configuration, setting, deleting, + and listing of environmental properties, making it easy to manage Temporal + Server instances. + keywords: + - cli reference + - command-line-interface-cli + - configuration + - env + - env delete + - env get + - env list + - env set + - environment + - temporal cli - name: temporal env delete summary: Delete an environment or environment property @@ -495,6 +539,33 @@ commands: ``` option-sets: - client + docs: + description-header: >- + Operator commands in Temporal allow actions on Namespaces, Search Attributes, + and Clusters using specific subcommands. Execute with + "temporal operator [command] [subcommand] [options]". + keywords: + - cli reference + - cluster + - cluster health + - cluster list + - cluster remove + - cluster upsert + - command-line-interface-cli + - describe + - namespace + - namespace create + - namespace delete + - namespace describe + - namespace list + - operator + - search attribute + - search attribute create + - search attribute list + - search attribute remove + - system + - temporal cli + - update - name: temporal operator cluster summary: Manage a Temporal Cluster @@ -1100,6 +1171,26 @@ commands: ``` option-sets: - client + docs: + description-header: >- + Temporal's Schedule commands allow users to create, update, and manage + Workflow Executions seamlessly for automation, supporting commands for + creation, backfill, deletion, and more. + keywords: + - backfill + - cli reference + - command-line-interface-cli + - schedule + - schedule backfill + - schedule create + - schedule delete + - schedule describe + - schedule list + - schedule toggle + - schedule trigger + - schedule update + - temporal cli + - updates - name: temporal schedule backfill summary: Backfill past actions @@ -1339,6 +1430,17 @@ commands: temporal server start-dev \ --ui-port 3000 ``` + docs: + description-header: >- + Manage your Temporal Server easily with CLI commands. Start a local + server using `temporal server start-dev` and access the Web UI at + http://localhost:8233. Customize with multiple options. + keywords: + - cli reference + - command-line-interface-cli + - server + - server start-dev + - temporal cli - name: temporal server start-dev summary: Start Temporal development server @@ -1467,6 +1569,19 @@ commands: ``` option-sets: - client + docs: + description-header: >- + Temporal Task Queue commands facilitate operations like describing + poller info, displaying partitions, fetching compatible Build IDs, + and determining Build ID reachability for effective Workflow and + Activity management. + keywords: + - cli reference + - command-line-interface-cli + - list partitions + - task queue + - task queue describe + - temporal cli - name: temporal task-queue describe summary: Show active Workers @@ -2181,6 +2296,43 @@ commands: ``` option-sets: - client + docs: + description-header: >- + Temporal Workflow commands enable operations on Workflow Executions, + such as cancel, count, delete, describe, execute, list, query, reset, + reset-batch, show, signal, stack, start, terminate, trace, and update, + enhancing efficiency and control. + keywords: + - call stack + - cancellation + - child workflows + - cli reference + - command-line-interface-cli + - event history + - query + - resets-feature + - signals + - signals-feature + - stack trace + - temporal cli + - termination + - workflow + - workflow cancel + - workflow count + - workflow delete + - workflow describe + - workflow execute + - workflow execution + - workflow list + - workflow query + - workflow reset + - workflow reset-batch + - workflow show + - workflow signal + - workflow stack + - workflow start + - workflow terminate + - workflow trace - name: temporal workflow cancel summary: Send cancellation to Workflow Execution diff --git a/temporalcli/commandsgen/docs.go b/temporalcli/commandsgen/docs.go new file mode 100644 index 00000000..e7fef0ce --- /dev/null +++ b/temporalcli/commandsgen/docs.go @@ -0,0 +1,117 @@ +package commandsgen + +import ( + "bytes" + "fmt" + "sort" + "strings" +) + +type DocsFile struct { + FileName string +} + +func GenerateDocsFiles(commands Commands) (map[string][]byte, error) { + optionSetMap := make(map[string]OptionSets) + for i, optionSet := range commands.OptionSets { + optionSetMap[optionSet.Name] = commands.OptionSets[i] + } + + w := &docWriter{fileMap: make(map[string]*bytes.Buffer), optionSetMap: optionSetMap} + + // sort by parent command (activity, batch, etc) + for _, cmd := range commands.CommandList { + if err := cmd.writeDoc(w); err != nil { + return nil, fmt.Errorf("failed writing docs for command %s: %w", cmd.FullName, err) + } + } + + // Format and return + var finalMap = make(map[string][]byte) + for key, buf := range w.fileMap { + finalMap[key] = buf.Bytes() + } + return finalMap, nil +} + +type docWriter struct { + fileMap map[string]*bytes.Buffer + optionSetMap map[string]OptionSets + optionsStack [][]Option +} + +func (c *Command) writeDoc(w *docWriter) error { + commandLength := len(strings.Split(c.FullName, " ")) + w.processOptions(c) + + // If this is a root command, write a new file + if commandLength == 2 { + w.writeCommand(c) + } else if commandLength > 2 { + w.writeSubcommand(c) + } + return nil +} + +func (w *docWriter) writeCommand(c *Command) { + fileName := strings.Split(c.FullName, " ")[1] + w.fileMap[fileName] = &bytes.Buffer{} + w.fileMap[fileName].WriteString("---\n") + w.fileMap[fileName].WriteString("id: " + fileName + "\n") + w.fileMap[fileName].WriteString("title: " + c.FullName + "\n") + w.fileMap[fileName].WriteString("sidebar_label: " + c.FullName + "\n") + w.fileMap[fileName].WriteString("description: " + c.Docs.DescriptionHeader + "\n") + w.fileMap[fileName].WriteString("toc_max_heading_level: 4\n") + w.fileMap[fileName].WriteString("keywords:\n") + for _, keyword := range c.Docs.Keywords { + w.fileMap[fileName].WriteString(" - " + keyword + "\n") + } + // tags are the same as Keywords, but with `-` instead of ` ` + w.fileMap[fileName].WriteString("tags:\n") + for _, keyword := range c.Docs.Keywords { + w.fileMap[fileName].WriteString(" - " + strings.ReplaceAll(keyword, " ", "-") + "\n") + } + w.fileMap[fileName].WriteString("---\n\n") +} + +func (w *docWriter) writeSubcommand(c *Command) { + fileName := strings.Split(c.FullName, " ")[1] + subCommand := strings.Join(strings.Split(c.FullName, " ")[2:], "") + w.fileMap[fileName].WriteString("## " + subCommand + "\n\n") + w.fileMap[fileName].WriteString(c.Description + "\n\n") + w.fileMap[fileName].WriteString("Use the following options to change the behavior of this command.\n\n") + + // gather options from command and all options aviailable from parent commands + var allOptions = make([]Option, 0) + for _, options := range w.optionsStack { + allOptions = append(allOptions, options...) + } + + // alphabetize options + sort.Slice(allOptions, func(i, j int) bool { + return allOptions[i].Name < allOptions[j].Name + }) + + for _, option := range allOptions { + w.fileMap[fileName].WriteString(fmt.Sprintf("## %s\n\n", option.Name)) + w.fileMap[fileName].WriteString(option.Description + "\n\n") + + } +} + +func (w *docWriter) processOptions(c *Command) { + // Pop options from stack if we are moving up a level + if len(w.optionsStack) >= len(strings.Split(c.FullName, " ")) { + w.optionsStack = w.optionsStack[:len(w.optionsStack)-1] + } + var options []Option + options = append(options, c.Options...) + + // Maintain stack of options available from parent commands + for _, set := range c.OptionSets { + optionSetOptions := w.optionSetMap[set].Options + options = append(options, optionSetOptions...) + } + + w.optionsStack = append(w.optionsStack, options) +} diff --git a/temporalcli/commandsgen/parse.go b/temporalcli/commandsgen/parse.go index a6b8dede..d4aea876 100644 --- a/temporalcli/commandsgen/parse.go +++ b/temporalcli/commandsgen/parse.go @@ -44,6 +44,13 @@ type ( IgnoreMissingEnv bool `yaml:"ignores-missing-env"` Options []Option `yaml:"options"` OptionSets []string `yaml:"option-sets"` + Docs Docs `yaml:"docs"` + } + + // Docs represents docs-only information that is not used in CLI generation. + Docs struct { + Keywords []string `yaml:"keywords"` + DescriptionHeader string `yaml:"description-header"` } // OptionSets represents the structure of option sets. @@ -125,6 +132,15 @@ func (c *Command) processSection() error { return fmt.Errorf("missing description for command: %s", c.FullName) } + if len(c.NamePath) == 2 { + if c.Docs.Keywords == nil { + return fmt.Errorf("missing keywords for root command: %s", c.FullName) + } + if c.Docs.DescriptionHeader == "" { + return fmt.Errorf("missing description for root command: %s", c.FullName) + } + } + // Strip trailing newline for description c.Description = strings.TrimRight(c.Description, "\n") diff --git a/temporalcli/internal/cmd/gen-docs/main.go b/temporalcli/internal/cmd/gen-docs/main.go new file mode 100644 index 00000000..94ff20ba --- /dev/null +++ b/temporalcli/internal/cmd/gen-docs/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + "runtime" + + "github.com/temporalio/cli/temporalcli/commandsgen" +) + +func main() { + if err := run(); err != nil { + log.Fatal(err) + } +} + +func run() error { + // Get commands dir + _, file, _, _ := runtime.Caller(0) + docsDir := filepath.Join(file, "../../../../docs/") + + err := os.MkdirAll(docsDir, os.ModePerm) + if err != nil { + log.Fatalf("Error creating directory: %v", err) + } + + // Parse markdown + cmds, err := commandsgen.ParseCommands() + if err != nil { + return fmt.Errorf("failed parsing markdown: %w", err) + } + + // Generate docs + b, err := commandsgen.GenerateDocsFiles(cmds) + if err != nil { + return err + } + + // Write + for filename, content := range b { + filePath := filepath.Join(docsDir, filename+".mdx") + if err := os.WriteFile(filePath, content, 0644); err != nil { + return fmt.Errorf("failed writing file: %w", err) + } + } + + return nil +}