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
156 changes: 156 additions & 0 deletions internal/commandsgen/env_docs.go
Original file line number Diff line number Diff line change
@@ -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
}
139 changes: 139 additions & 0 deletions internal/commandsgen/env_docs_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}