Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ and create a PR in the Documentation repo with the corresponding updates. To gen

go run ./cmd/gen-docs -input internal/temporalcli/commands.yaml -input cliext/option-sets.yaml -output dist/docs

This will auto-generate a new set of docs to `dist/docs/`. If a new root command is added, a new file will be automatically generated, like `temporal activity` and `activity.mdx`.
This will auto-generate a new set of docs to `dist/docs/`, including an
`environment-variables.mdx` index of shell env vars derived from `implied-env`.
If a new root command is added, a new file will be automatically generated, like `temporal activity` and `activity.mdx`.

When generating cloud CLI docs with `-subdir`, the env-var index is intentionally
omitted so a subsequent run does not overwrite the main CLI page.

## Inject additional build-time information

Expand Down
5 changes: 5 additions & 0 deletions cmd/gen-docs/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,9 @@ func TestGenDocsMultipleInputs(t *testing.T) {
if _, err := os.Stat(workflowPath); os.IsNotExist(err) {
t.Fatal("workflow.mdx was not generated")
}

envVarsPath := filepath.Join(outputDir, "environment-variables.mdx")
if _, err := os.Stat(envVarsPath); os.IsNotExist(err) {
t.Fatal("environment-variables.mdx was not generated")
}
}
9 changes: 9 additions & 0 deletions internal/commandsgen/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ func GenerateDocsFiles(commands Commands, subdirNames []string) (map[string][]by
for key, buf := range w.fileMap {
finalMap[key] = buf.Bytes()
}

// Emit the env-var index only for full CLI docs generation. Cloud-cli runs
// use -subdir and would otherwise overwrite this page with a partial list.
if len(subdirNames) == 0 {
if envDocs := GenerateEnvVarDocsFile(commands); envDocs != nil {
finalMap["environment-variables"] = envDocs
}
}

return finalMap, nil
}

Expand Down
122 changes: 122 additions & 0 deletions internal/commandsgen/env_docs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package commandsgen

import (
"bytes"
"fmt"
"sort"
"strings"
)

// envVarEntry is one documented shell environment variable.
type envVarEntry struct {
EnvVar string
Flag string
Description string
}

// GenerateEnvVarDocsFile builds an MDX index of shell environment variables
// derived from options with implied-env, plus a few special cases that are not
// simple 1:1 flag mappings.
//
// Returns nil when there are no implied-env options (so callers can skip writing
// a file, e.g. during cloud-cli -subdir generation that would otherwise overwrite
// the main CLI env-var page).
func GenerateEnvVarDocsFile(commands Commands) []byte {
entries := collectImpliedEnvVars(commands)
if len(entries) == 0 {
return nil
}

var buf bytes.Buffer
buf.WriteString("---\n")
buf.WriteString("id: environment-variables\n")
buf.WriteString("title: Temporal CLI environment variables reference\n")
buf.WriteString("sidebar_label: environment variables\n")
buf.WriteString("description: Shell environment variables that configure Temporal CLI flags.\n")
buf.WriteString("toc_max_heading_level: 4\n")
buf.WriteString("keywords:\n")
buf.WriteString(" - temporal cli\n")
buf.WriteString(" - environment variables\n")
buf.WriteString(" - configuration\n")
buf.WriteString("tags:\n")
buf.WriteString(" - Temporal CLI\n")
buf.WriteString("---\n\n")
buf.WriteString(autoGeneratedNotice)

buf.WriteString("This page lists shell environment variables that configure the Temporal CLI.\n\n")
buf.WriteString(":::tip Shell environment variables\n\n")
buf.WriteString("Do not confuse these shell environment variables with [`temporal env`](/cli/command-reference/env) presets.\n")
buf.WriteString("Shell variables configure the CLI process; `temporal env` stores named key-value presets.\n\n")
buf.WriteString(":::\n\n")
buf.WriteString("When both a flag and its environment variable are set, the flag takes precedence.\n\n")

buf.WriteString("| Environment variable | Flag | Description |\n")
buf.WriteString("|----------------------|------|-------------|\n")
for _, e := range entries {
desc := encodeJSONExample(e.Description)
desc = strings.ReplaceAll(desc, "|", "\\|")
buf.WriteString(fmt.Sprintf("| `%s` | `--%s` | %s |\n", e.EnvVar, e.Flag, desc))
}
buf.WriteString("\n")

buf.WriteString("## Special cases\n\n")
buf.WriteString("### `TEMPORAL_GRPC_META_*`\n\n")
buf.WriteString("HTTP headers for requests can be set with `--grpc-meta KEY=VALUE` (repeatable), or via\n")
buf.WriteString("environment variables named `TEMPORAL_GRPC_META_[name]`, where `[name]` is the header name.\n\n")
buf.WriteString("### Legacy TLS variables\n\n")
buf.WriteString("For compatibility, the following legacy TLS environment variables still work and override\n")
buf.WriteString("values loaded from the config file or the preferred TLS variables above:\n\n")
buf.WriteString("| Legacy variable | Preferred equivalent |\n")
buf.WriteString("|-----------------|----------------------|\n")
buf.WriteString("| `TEMPORAL_TLS_CERT` | `TEMPORAL_TLS_CLIENT_CERT_PATH` |\n")
buf.WriteString("| `TEMPORAL_TLS_CERT_DATA` | `TEMPORAL_TLS_CLIENT_CERT_DATA` |\n")
buf.WriteString("| `TEMPORAL_TLS_KEY` | `TEMPORAL_TLS_CLIENT_KEY_PATH` |\n")
buf.WriteString("| `TEMPORAL_TLS_KEY_DATA` | `TEMPORAL_TLS_CLIENT_KEY_DATA` |\n")
buf.WriteString("| `TEMPORAL_TLS_CA` | `TEMPORAL_TLS_SERVER_CA_CERT_PATH` |\n")
buf.WriteString("| `TEMPORAL_TLS_CA_DATA` | `TEMPORAL_TLS_SERVER_CA_CERT_DATA` |\n")
buf.WriteString("\n")

return buf.Bytes()
}

func collectImpliedEnvVars(commands Commands) []envVarEntry {
byEnv := make(map[string]envVarEntry)

add := func(o Option) {
if o.ImpliedEnv == "" || o.Hidden {
return
}
// First definition wins; later duplicates (same env across option sets) are ignored.
if _, exists := byEnv[o.ImpliedEnv]; exists {
return
}
byEnv[o.ImpliedEnv] = envVarEntry{
EnvVar: o.ImpliedEnv,
Flag: o.Name,
Description: o.Description,
}
}

for _, set := range commands.OptionSets {
if set.ExternalPackage != "" {
continue
}
for _, o := range set.Options {
add(o)
}
}
for _, cmd := range commands.CommandList {
for _, o := range cmd.Options {
add(o)
}
}

entries := make([]envVarEntry, 0, len(byEnv))
for _, e := range byEnv {
entries = append(entries, e)
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].EnvVar < entries[j].EnvVar
})
return entries
}
117 changes: 117 additions & 0 deletions internal/commandsgen/env_docs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package commandsgen

import (
"strings"
"testing"
)

const envVarFixture = `
option-sets:
- name: client
options:
- name: address
type: string
description: Temporal Service gRPC endpoint.
default: localhost:7233
implied-env: TEMPORAL_ADDRESS
- name: namespace
type: string
description: Temporal Service Namespace.
default: default
implied-env: TEMPORAL_NAMESPACE
- name: grpc-meta
type: string[]
description: |
HTTP headers for requests.
commands:
- name: workflow
summary: Workflow
description: Manage Workflows.
- name: workflow list
summary: List
description: List Workflows.
option-sets:
- client
options:
- name: query
type: string
description: Visibility query.
implied-env: TEMPORAL_QUERY
docs:
keywords:
- workflow
description-header: Manage Workflows
tags:
- Workflows
`

func TestGenerateEnvVarDocsFile(t *testing.T) {
cmds, err := ParseCommands([]byte(envVarFixture))
if err != nil {
t.Fatalf("ParseCommands: %v", err)
}

docs := GenerateEnvVarDocsFile(cmds)
if docs == nil {
t.Fatal("expected env var docs, got nil")
}
got := string(docs)

for _, want := range []string{
"id: environment-variables",
"| Environment variable | Flag | Description |",
"| `TEMPORAL_ADDRESS` | `--address` |",
"| `TEMPORAL_NAMESPACE` | `--namespace` |",
"| `TEMPORAL_QUERY` | `--query` |",
"Do not confuse these shell environment variables",
"/cli/command-reference/env",
"## Special cases",
"`TEMPORAL_GRPC_META_*`",
"| `TEMPORAL_TLS_CERT` | `TEMPORAL_TLS_CLIENT_CERT_PATH` |",
} {
if !strings.Contains(got, want) {
t.Errorf("missing %q in:\n%s", want, got)
}
}

// Alphabetized: ADDRESS before NAMESPACE before QUERY
addr := strings.Index(got, "`TEMPORAL_ADDRESS`")
ns := strings.Index(got, "`TEMPORAL_NAMESPACE`")
query := strings.Index(got, "`TEMPORAL_QUERY`")
if !(addr < ns && ns < query) {
t.Errorf("expected alphabetical order ADDRESS < NAMESPACE < QUERY; got %d, %d, %d", addr, ns, query)
}
}

func TestGenerateEnvVarDocsFileEmpty(t *testing.T) {
cmds, err := ParseCommands([]byte(splitFixture))
if err != nil {
t.Fatalf("ParseCommands: %v", err)
}
if docs := GenerateEnvVarDocsFile(cmds); docs != nil {
t.Fatalf("expected nil when no implied-env options, got:\n%s", docs)
}
}

func TestGenerateDocsFilesIncludesEnvVarsWithoutSubdir(t *testing.T) {
cmds, err := ParseCommands([]byte(envVarFixture))
if err != nil {
t.Fatalf("ParseCommands: %v", err)
}

docs, err := GenerateDocsFiles(cmds, nil)
if err != nil {
t.Fatalf("GenerateDocsFiles: %v", err)
}
if _, ok := docs["environment-variables"]; !ok {
t.Fatalf("expected environment-variables page, got keys: %v", keys(docs))
}

docsSub, err := GenerateDocsFiles(cmds, []string{"workflow"})
if err != nil {
t.Fatalf("GenerateDocsFiles with subdir: %v", err)
}
if _, ok := docsSub["environment-variables"]; ok {
t.Fatal("environment-variables must not be emitted when -subdir is set")
}
}