From d9926d4db36cd5b4f12a33f341842452816bfbdb Mon Sep 17 00:00:00 2001 From: SornapudiSuresh Date: Mon, 27 Jul 2026 23:57:04 +0530 Subject: [PATCH 1/2] feat(commandsgen): generate CLI environment variables docs index Emit alphabetized environment-variables.mdx from implied-env options, plus special cases for TEMPORAL_GRPC_META_* and legacy TLS vars. Fixes #697 --- CONTRIBUTING.md | 7 +- cmd/gen-docs/main_test.go | 5 ++ internal/commandsgen/docs.go | 9 ++ internal/commandsgen/env_docs.go | 122 ++++++++++++++++++++++++++ internal/commandsgen/env_docs_test.go | 117 ++++++++++++++++++++++++ 5 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 internal/commandsgen/env_docs.go create mode 100644 internal/commandsgen/env_docs_test.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9eeea5b26..4a944305c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/cmd/gen-docs/main_test.go b/cmd/gen-docs/main_test.go index cdd68945c..f14e84c8e 100644 --- a/cmd/gen-docs/main_test.go +++ b/cmd/gen-docs/main_test.go @@ -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") + } } diff --git a/internal/commandsgen/docs.go b/internal/commandsgen/docs.go index d699c23f6..e7760401a 100644 --- a/internal/commandsgen/docs.go +++ b/internal/commandsgen/docs.go @@ -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 } diff --git a/internal/commandsgen/env_docs.go b/internal/commandsgen/env_docs.go new file mode 100644 index 000000000..cf202d47f --- /dev/null +++ b/internal/commandsgen/env_docs.go @@ -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 +} diff --git a/internal/commandsgen/env_docs_test.go b/internal/commandsgen/env_docs_test.go new file mode 100644 index 000000000..a03c48a8d --- /dev/null +++ b/internal/commandsgen/env_docs_test.go @@ -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") + } +} From aced855219dc09efccfd665687f80c487d8c8763 Mon Sep 17 00:00:00 2001 From: Lenny Chen Date: Tue, 28 Jul 2026 14:29:46 -0700 Subject: [PATCH 2/2] docs(commandsgen): separate legacy temporal env variables TEMPORAL_ENV and TEMPORAL_ENV_FILE configure the `temporal env` preset system, which stores named key-value presets in temporal.yaml. Listing them in the main table alongside TEMPORAL_PROFILE and TEMPORAL_CONFIG_FILE presented two different config systems as peers, and contradicted the note telling readers not to conflate shell variables with `temporal env` presets. Move both into a "Legacy temporal env variables" section under Special cases, mirroring the existing legacy TLS section, and reword the intro to state the relationship instead of denying it. --- internal/commandsgen/env_docs.go | 58 +++++++++++++++++++++------ internal/commandsgen/env_docs_test.go | 24 ++++++++++- 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/internal/commandsgen/env_docs.go b/internal/commandsgen/env_docs.go index cf202d47f..2a13c8d5e 100644 --- a/internal/commandsgen/env_docs.go +++ b/internal/commandsgen/env_docs.go @@ -14,6 +14,14 @@ type envVarEntry struct { Description string } +// legacyPresetEnvVars configure the `temporal env` preset system rather than the +// TOML config file. They get their own section so they are not read as peers of +// the profile-based settings, which use a different file in a different format. +var legacyPresetEnvVars = map[string]bool{ + "TEMPORAL_ENV": true, + "TEMPORAL_ENV_FILE": true, +} + // 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. @@ -27,6 +35,17 @@ func GenerateEnvVarDocsFile(commands Commands) []byte { return nil } + // Legacy `temporal env` variables are split out of the main table so readers + // do not mistake them for the current profile-based settings. + var current, legacyPreset []envVarEntry + for _, e := range entries { + if legacyPresetEnvVars[e.EnvVar] { + legacyPreset = append(legacyPreset, e) + } else { + current = append(current, e) + } + } + var buf bytes.Buffer buf.WriteString("---\n") buf.WriteString("id: environment-variables\n") @@ -44,20 +63,14 @@ func GenerateEnvVarDocsFile(commands Commands) []byte { 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("Setting a shell environment variable is not the same as storing a preset with\n") + buf.WriteString("[`temporal env`](/cli/command-reference/env). A shell variable configures the CLI process,\n") + buf.WriteString("while `temporal env` writes named key-value presets to a file. Two shell variables control\n") + buf.WriteString("which preset the CLI reads, and they are listed under\n") + buf.WriteString("[Legacy `temporal env` variables](#legacy-temporal-env-variables).\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") + writeEnvVarTable(&buf, current) buf.WriteString("## Special cases\n\n") buf.WriteString("### `TEMPORAL_GRPC_META_*`\n\n") @@ -76,9 +89,30 @@ func GenerateEnvVarDocsFile(commands Commands) []byte { buf.WriteString("| `TEMPORAL_TLS_CA_DATA` | `TEMPORAL_TLS_SERVER_CA_CERT_DATA` |\n") buf.WriteString("\n") + if len(legacyPreset) > 0 { + buf.WriteString("### Legacy `temporal env` variables\n\n") + buf.WriteString("These variables point the CLI at the `temporal env` preset system, which stores named\n") + buf.WriteString("key-value presets in `temporal.yaml`. Profiles in the TOML config file supersede that\n") + buf.WriteString("system: use `TEMPORAL_PROFILE` to select a profile and `TEMPORAL_CONFIG_FILE` to point at\n") + buf.WriteString("a `temporal.toml` file. The variables below still work.\n\n") + writeEnvVarTable(&buf, legacyPreset) + } + return buf.Bytes() } +// writeEnvVarTable renders entries as an MDX table of variable, flag, and description. +func writeEnvVarTable(buf *bytes.Buffer, entries []envVarEntry) { + 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") +} + func collectImpliedEnvVars(commands Commands) []envVarEntry { byEnv := make(map[string]envVarEntry) diff --git a/internal/commandsgen/env_docs_test.go b/internal/commandsgen/env_docs_test.go index a03c48a8d..4933c7b98 100644 --- a/internal/commandsgen/env_docs_test.go +++ b/internal/commandsgen/env_docs_test.go @@ -23,6 +23,15 @@ option-sets: type: string[] description: | HTTP headers for requests. + - name: env + type: string + description: Active environment name. + default: default + implied-env: TEMPORAL_ENV + - name: env-file + type: string + description: Path to environment settings file. + implied-env: TEMPORAL_ENV_FILE commands: - name: workflow summary: Workflow @@ -63,11 +72,12 @@ func TestGenerateEnvVarDocsFile(t *testing.T) { "| `TEMPORAL_ADDRESS` | `--address` |", "| `TEMPORAL_NAMESPACE` | `--namespace` |", "| `TEMPORAL_QUERY` | `--query` |", - "Do not confuse these shell environment variables", + "Setting a shell environment variable is not the same as storing a preset", "/cli/command-reference/env", "## Special cases", "`TEMPORAL_GRPC_META_*`", "| `TEMPORAL_TLS_CERT` | `TEMPORAL_TLS_CLIENT_CERT_PATH` |", + "### Legacy `temporal env` variables", } { if !strings.Contains(got, want) { t.Errorf("missing %q in:\n%s", want, got) @@ -81,6 +91,18 @@ func TestGenerateEnvVarDocsFile(t *testing.T) { if !(addr < ns && ns < query) { t.Errorf("expected alphabetical order ADDRESS < NAMESPACE < QUERY; got %d, %d, %d", addr, ns, query) } + + // Legacy `temporal env` variables belong in their own section, not the main + // table, so they are not read as peers of the profile-based settings. + specialCases := strings.Index(got, "## Special cases") + for _, legacy := range []string{"`TEMPORAL_ENV`", "`TEMPORAL_ENV_FILE`"} { + idx := strings.Index(got, legacy) + if idx == -1 { + t.Errorf("missing %q in:\n%s", legacy, got) + } else if idx < specialCases { + t.Errorf("%s appears in the main table at %d, want it after ## Special cases at %d", legacy, idx, specialCases) + } + } } func TestGenerateEnvVarDocsFileEmpty(t *testing.T) {