Files
coder/scripts/clidocgen/main.go
T
Nick VigilanteandCian Johnston 1cc230b43a refactor: extract docgen env prep into a shared package (#26827)
## What

`clidocgen` and the new `configdocgen` (coder/coder#26824) both carried
a byte-identical `prepareEnv()` that unsets `CODER_*` and pins
`CLIDOCGEN_*` / `TMPDIR` so generated docs don't embed the generating
host's home directory.

This extracts it to `scripts/docgenenv.Prepare()` and migrates
`clidocgen`.

## Why

Duplication flagged during review of #26824. `configdocgen` adopts the
shared helper in that PR, removing its copy.

## Risk

Behavior-preserving: regenerating the CLI reference (`make
docs/reference/cli/index.md`) yields no diff, and `make pre-commit`
passes (`lint/go`, `lint/ts`, `build`). A focused unit test pins the
`Prepare()` contract, and `_test.go` files are excluded from
`CLIDOCGEN_INPUTS` so test edits don't mark the generated docs stale.

<details>
<summary>CI status — blocked by an unrelated <code>main</code> breakage
(#24993)</summary>

All red checks on this PR are inherited from `main`, not caused by these
changes. This PR touches only `Makefile` and
`scripts/{clidocgen,docgenenv}`; it does not touch Helm.

`main` went red at `d0f68cb9b0` ("feat: add listenerset", #24993, merged
~18:26 UTC). The committed
`helm/coder/tests/testdata/listenerset*.golden` files don't match what
`helm template` renders, so:

- **`gen`** regenerates those goldens, and the unstaged-files check
fails.
- **`test-go-pg` (ubuntu-latest, pg-17) and `test-go-race-pg`** fail
only on `TestRenderChart/{coder,default}/listenerset[_redirect]` (golden
mismatch; the test prints "Run with -update to update golden files").
The same `test-go-pg` job passes on macOS and Windows, where the Helm
render test is skipped, and `scripts/docgenenv` reports `ok` on the
failing runners.

Base commit `14a61041d9` was green; `main` is red from `d0f68cb9b0`
onward. These checks clear once `main` is fixed and this branch is
updated. `fmt`, `lint`, `Storybook`, `check-build`, and `test-e2e` are
green.

</details>

---

🤖 Opened by Coder Agents on behalf of @nickvigilante.

---------

Co-authored-by: Cian Johnston <cian@coder.com>
2026-07-08 15:39:45 +00:00

176 lines
3.8 KiB
Go

package main
import (
"encoding/json"
"os"
"path/filepath"
"sort"
"github.com/coder/coder/v2/enterprise/cli"
"github.com/coder/coder/v2/scripts/atomicwrite"
"github.com/coder/coder/v2/scripts/docgenenv"
"github.com/coder/flog"
"github.com/coder/serpent"
)
// route is an individual page object in the docs manifest.json.
type route struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Path string `json:"path,omitempty"`
IconPath string `json:"icon_path,omitempty"`
State []string `json:"state,omitempty"`
Children []route `json:"children,omitempty"`
}
// manifest describes the entire documentation index.
type manifest struct {
Versions []string `json:"versions,omitempty"`
Routes []route `json:"routes,omitempty"`
}
func deleteEmptyDirs(dir string) error {
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
return nil
}
ents, err := os.ReadDir(path)
if err != nil {
return err
}
if len(ents) == 0 {
flog.Infof("deleting empty dir\t %v", path)
err = os.Remove(path)
if err != nil {
return err
}
}
return nil
})
}
func main() {
docgenenv.Prepare()
workdir, err := os.Getwd()
if err != nil {
flog.Fatalf("getwd: %v", err)
}
root := (&cli.RootCmd{})
// wroteMap indexes file paths to commands.
wroteMap := make(map[string]*serpent.Command)
var (
docsDir = filepath.Join(workdir, "docs")
cliMarkdownDir = filepath.Join(docsDir, "reference/cli")
)
if d := os.Getenv("DOCS_DIR"); d != "" {
docsDir = d
cliMarkdownDir = filepath.Join(docsDir, "reference/cli")
}
cmd, err := root.Command(root.EnterpriseSubcommands())
if err != nil {
flog.Fatalf("creating command: %v", err)
}
err = genTree(
cliMarkdownDir,
cmd,
wroteMap,
)
if err != nil {
flog.Fatalf("generating markdowns: %v", err)
}
// Delete old files
err = filepath.Walk(cliMarkdownDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
_, ok := wroteMap[path]
if !ok {
flog.Infof("deleting old doc\t %v", path)
if err := os.Remove(path); err != nil {
return err
}
}
return nil
})
if err != nil {
flog.Fatalf("deleting old docs: %v", err)
}
err = deleteEmptyDirs(cliMarkdownDir)
if err != nil {
flog.Fatalf("deleting empty dirs: %v", err)
}
// Update manifest
manifestPath := filepath.Join(docsDir, "manifest.json")
manifestByt, err := os.ReadFile(manifestPath)
if err != nil {
flog.Fatalf("reading manifest: %v", err)
}
var manifest manifest
err = json.Unmarshal(manifestByt, &manifest)
if err != nil {
flog.Fatalf("unmarshalling manifest: %v", err)
}
var found bool
for i := range manifest.Routes {
rt := &manifest.Routes[i]
if rt.Title != "Reference" {
continue
}
for j := range rt.Children {
child := &rt.Children[j]
if child.Title != "Command Line" {
continue
}
child.Children = nil
found = true
for path, cmd := range wroteMap {
relPath, err := filepath.Rel(docsDir, path)
if err != nil {
flog.Fatalf("getting relative path: %v", err)
}
child.Children = append(child.Children, route{
Title: fullName(cmd),
Description: cmd.Short,
Path: relPath,
})
}
// Sort children by title because wroteMap iteration is
// non-deterministic.
sort.Slice(child.Children, func(i, j int) bool {
return child.Children[i].Title < child.Children[j].Title
})
}
}
if !found {
flog.Fatalf("could not find Command Line route in manifest")
}
manifestByt, err = json.MarshalIndent(manifest, "", " ")
if err != nil {
flog.Fatalf("marshaling manifest: %v", err)
}
err = atomicwrite.File(manifestPath, manifestByt)
if err != nil {
flog.Fatalf("writing manifest: %v", err)
}
}