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..2a13c8d5e --- /dev/null +++ b/internal/commandsgen/env_docs.go @@ -0,0 +1,156 @@ +package commandsgen + +import ( + "bytes" + "fmt" + "sort" + "strings" +) + +// envVarEntry is one documented shell environment variable. +type envVarEntry struct { + EnvVar string + Flag string + 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. +// +// 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 + } + + // 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") + 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("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") + + writeEnvVarTable(&buf, current) + + 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") + + 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) + + 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..4933c7b98 --- /dev/null +++ b/internal/commandsgen/env_docs_test.go @@ -0,0 +1,139 @@ +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. + - 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 + 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` |", + "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) + } + } + + // 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) + } + + // 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) { + 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") + } +}