Skip to content

🤖 feat: generate API reference docs from Go types#24

Merged
ThomasK33 merged 3 commits intomainfrom
docs-y855
Feb 10, 2026
Merged

🤖 feat: generate API reference docs from Go types#24
ThomasK33 merged 3 commits intomainfrom
docs-y855

Conversation

@ThomasK33
Copy link
Member

@ThomasK33 ThomasK33 commented Feb 10, 2026

Summary

This PR integrates elastic/crd-ref-docs to generate API reference markdown pages from Go API types, replacing hand-maintained reference content with deterministic generated output.

Background

The API docs under docs/reference/api/ could drift from source types in api/. This change adds a generated-doc workflow aligned with existing vendored-tool patterns so docs stay current and reviewable.

Implementation

  • Added Go tool directives for executable tooling, including github.com/elastic/crd-ref-docs.
  • Added CRD ref docs config and custom markdown template:
    • hack/crd-ref-docs/config.yaml
    • hack/crd-ref-docs/templates/markdown/gv_list.tpl
  • Added generator script:
    • hack/update-reference-docs.sh
  • Wired generation into build workflows:
    • Makefile: docs-reference, docs-reference-check, and docs-check dependency update.
    • .github/workflows/ci.yaml: verify generated API docs are up to date in lint job; include docgen paths in change detection.
  • Regenerated API reference docs:
    • docs/reference/api/codercontrolplane.md
    • docs/reference/api/coderworkspace.md
    • docs/reference/api/codertemplate.md
  • Removed legacy tools pin file superseded by tool directives:
    • internal/deps/controllergen_tools.go

Validation

  • make test
  • make build
  • make lint
  • go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.10
  • Vendor idempotence check: go mod tidy && go mod vendor (no unexpected module/vendor drift)
  • Docs determinism check: regenerated API reference docs twice and verified stable checksums

Risks

  • Low for runtime behavior: changes are documentation/tooling focused.
  • Moderate for contributor workflow: API type changes now require regenerated reference docs; CI enforcement ensures drift cannot merge.

📋 Implementation Plan

Integrate elastic/crd-ref-docs to generate API reference Markdown

Context / Why

Today our API reference pages under docs/reference/api/ are hand-written and can drift from the actual Go API types in api/. We want to:

  • Generate those reference docs from the Go type definitions.
  • Keep the output deterministic (repeatable) and easy to update via make.
  • Prefer the repo’s existing “vendored tool + GOFLAGS=-mod=vendor go run ./vendor/...” workflow.
  • (Optionally) enforce “docs are up to date” in CI when API types change.

Evidence (what we verified)

In-repo

  • Makefile: docs-check only runs mkdocs build --strict; there is no doc generation target today.
  • mkdocs.yml: Reference nav links point directly at:
    • docs/reference/api/codercontrolplane.md
    • docs/reference/api/coderworkspace.md
    • docs/reference/api/codertemplate.md
  • Current reference docs format is consistent and simple: “API identity” bullets + Spec/Status tables + Source section.
  • Go API types:
    • CRD types: api/v1alpha1/* with +groupName=coder.com in api/v1alpha1/doc.go.
    • Aggregated API types: api/aggregation/v1alpha1/* with +groupName=aggregation.coder.com in api/aggregation/v1alpha1/doc.go.
    • Root objects are marked with +kubebuilder:object:root=true.
  • Tooling patterns:
    • internal/deps/controllergen_tools.go (build tag tools) pins tool dependencies so they are vendored.
    • hack/update-manifests.sh and hack/update-codegen.sh run vendored tools via GOFLAGS=-mod=vendor go run ./vendor/... and include fail-fast assertions.
    • go.mod targets Go 1.25.7, so we can use Go 1.24+ tool directives to pin these tools without a tools.go file.

Upstream (crd-ref-docs)

  • Latest release is github.com/elastic/crd-ref-docs v0.3.0.
  • CLI supports --renderer=markdown, --templates-dir, and --template-value key/value injection.
  • Output modes are only single and group (no built-in “one file per Kind”), and the template entrypoint executed is named gvList.

Implementation plan

1) Track tool dependencies via Go 1.24+ tool directives (no tools.go file)

Instead of adding (or extending) a build-tagged tools.go file with blank imports, we can pin executable tooling directly in go.mod using the tool directive (supported because this repo’s go.mod already targets Go 1.25.x).

Files to change

  • go.mod, go.sum, vendor/
  • (Cleanup) delete internal/deps/controllergen_tools.go once its tool deps are represented in go.mod.

Edits

  • Add a tool block in go.mod (either manually or via go get -tool ...) that includes:
    • github.com/elastic/crd-ref-docs
    • sigs.k8s.io/controller-runtime/tools/setup-envtest (already used in Makefile)
    • sigs.k8s.io/controller-tools/cmd/controller-gen (already used in hack/update-manifests.sh)
    • k8s.io/code-generator/cmd/deepcopy-gen (already used in hack/update-codegen.sh; optional but recommended for consistency)

Example desired shape:

tool (
	github.com/elastic/crd-ref-docs
	k8s.io/code-generator/cmd/deepcopy-gen
	sigs.k8s.io/controller-runtime/tools/setup-envtest
	sigs.k8s.io/controller-tools/cmd/controller-gen
)
  • Pin versions with minimal churn:

    • Prefer to manually add the tool (...) block so existing tool versions in require remain unchanged.
    • Add the new tool requirement for github.com/elastic/crd-ref-docs v0.3.0.
      • Option A (explicit): add the require github.com/elastic/crd-ref-docs v0.3.0 line yourself.
      • Option B (Go-managed): run go get -tool github.com/elastic/crd-ref-docs@v0.3.0 (Go will add/update the tool directive + require).
  • Then run the standard vendoring workflow:

    • go mod tidy
    • go mod vendor
    • git diff --exit-code -- go.mod go.sum vendor/
  • Verify vendoring picked up the tool packages we execute from ./vendor/... today:

    • vendor/github.com/elastic/crd-ref-docs/
    • vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/
    • vendor/sigs.k8s.io/controller-runtime/tools/setup-envtest/
    • vendor/k8s.io/code-generator/cmd/deepcopy-gen/
  • Delete internal/deps/controllergen_tools.go once the tool block is in place (so we no longer rely on a tools file).

Why this approach

  • Avoids tools.go boilerplate and build tags just to pin tools.
  • Makes tooling dependencies explicit in go.mod.
  • Preserves the repo’s offline/deterministic workflow (-mod=vendor + vendored tool sources).

2) Add crd-ref-docs config + custom Markdown templates (match our current per-Kind pages)

Because crd-ref-docs can’t emit “one file per Kind” directly, we’ll:

  • run it once per page (Kind) and
  • use a custom gvList template that renders exactly one Kind selected via --template-value=kind=....

New files to add

  • hack/crd-ref-docs/config.yaml
  • hack/crd-ref-docs/templates/markdown/*.tpl

2.1 Config (hack/crd-ref-docs/config.yaml)

Keep it minimal and conservative; avoid ignoring Spec/Status structs (we need them to render tables).

Suggested starting config:

processor:
  # Skip *List root objects (we don’t publish reference pages for list types).
  ignoreTypes:
    - ".*List$"

  # Preserve multi-line comments if we add them later.
  useRawDocstring: true

render:
  # Used only if we later decide to link Kubernetes core types.
  kubernetesVersion: "1.35"

2.2 Templates

Create a small set of templates in hack/crd-ref-docs/templates/markdown/ with a required gvList definition.

Key goals of the template:

  • Output exactly the format we already publish in docs/reference/api/*.md.
  • Fail fast if the selected Kind isn’t found or required template values are missing.
  • Render only spec.* and status.* fields.

Skeleton (single file is fine; split into multiple *.tpl files if preferred):

{{- define "gvList" -}}
{{- $kind := markdownTemplateValue "kind" -}}
{{- if eq $kind "" }}{{ fail "missing --template-value=kind=<Kind>" }}{{ end -}}

{{- /* Find the first GV that contains this Kind */ -}}
{{- $t := "" -}}
{{- $gv := "" -}}
{{- range . -}}
  {{- $candidate := .TypeForKind $kind -}}
  {{- if $candidate -}}
    {{- $t = $candidate -}}
    {{- $gv = . -}}
  {{- end -}}
{{- end -}}
{{- if not $t }}{{ fail (printf "kind %q not found in source tree" $kind) }}{{ end -}}

<!-- Code generated by hack/update-reference-docs.sh using github.com/elastic/crd-ref-docs. DO NOT EDIT. -->

# `{{ $kind }}`

## API identity

- Group/version: `{{ $t.GVK.Group }}/{{ $t.GVK.Version }}`
- Kind: `{{ $t.GVK.Kind }}`
- Resource: `{{ lower $kind }}s`
- Scope: namespaced

{{- /* Extract spec/status fields */ -}}
{{- $specField := "" -}}
{{- $statusField := "" -}}
{{- range $t.Members -}}
  {{- if eq .Name "spec" }}{{- $specField = . -}}{{- end -}}
  {{- if eq .Name "status" }}{{- $statusField = . -}}{{- end -}}
{{- end -}}
{{- if not $specField }}{{ fail "expected root type to contain a spec field" }}{{ end -}}
{{- if not $statusField }}{{ fail "expected root type to contain a status field" }}{{ end -}}

## Spec

| Field | Type | Description |
| --- | --- | --- |
{{- range $specField.Type.Members -}}
| `spec.{{ .Name }}` | `{{ template "typeName" .Type }}` | {{ markdownRenderFieldDoc .Doc }} |
{{- end }}

## Status

| Field | Type | Description |
| --- | --- | --- |
{{- range $statusField.Type.Members -}}
| `status.{{ .Name }}` | `{{ template "typeName" .Type }}` | {{ markdownRenderFieldDoc .Doc }} |
{{- end }}

## Source

- Go type: `{{ markdownTemplateValue "goType" }}`
{{- with markdownTemplateValue "generatedCRD" }}
{{- if ne . "" }}- Generated CRD: `{{ . }}`{{ end -}}
{{- end }}
{{- with markdownTemplateValue "storage" }}
{{- if ne . "" }}- Storage implementation: `{{ . }}`{{ end -}}
{{- end }}
{{- with markdownTemplateValue "apiServiceManifest" }}
{{- if ne . "" }}- APIService registration manifest: `{{ . }}`{{ end -}}
{{- end }}

{{- end -}}

{{- /* Helper: keep output Go-ish and stable (extend as needed) */ -}}
{{- define "typeName" -}}
{{- $t := . -}}
{{- if not $t -}}<unknown>{{- else if eq $t.Package "" -}}
{{- $t.Name -}}
{{- else if eq $t.Package "k8s.io/apimachinery/pkg/apis/meta/v1" -}}
metav1.{{ $t.Name }}
{{- else -}}
{{- $t.Name -}}
{{- end -}}
{{- end -}}

Notes:

  • This intentionally renders a flat table of direct spec/status members (matching our current docs). If we later introduce nested structs we can extend the template to recurse or switch to the richer default templates.
  • We compute Resource as lower(kind) + "s" and default scope to namespaced (matches current reality). If we add cluster-scoped APIs, we can either (a) add +kubebuilder:resource:scope=Cluster markers and parse them in-template, or (b) pass scope= as a template value from the script.

3) Add a generator script (hack/update-reference-docs.sh)

New file

  • hack/update-reference-docs.sh

Behavior

  • Mirrors existing hack/update-*.sh scripts:
    • set -euo pipefail
    • verify expected directories exist
    • run vendored tool via GOFLAGS=-mod=vendor go run ./vendor/...

Core logic (shape)

#!/usr/bin/env bash
set -euo pipefail

SCRIPT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

assert_dir() { [[ -d "$1" ]] || { echo "assertion failed: expected dir $1" >&2; exit 1; }; }
assert_file() { [[ -f "$1" ]] || { echo "assertion failed: expected file $1" >&2; exit 1; }; }

assert_dir "${SCRIPT_ROOT}/docs/reference/api"
assert_file "${SCRIPT_ROOT}/hack/crd-ref-docs/config.yaml"
assert_dir "${SCRIPT_ROOT}/hack/crd-ref-docs/templates/markdown"

CRD_REF_DOCS_PKG="./vendor/github.com/elastic/crd-ref-docs"

generate_kind() {
  local source_path="$1" out_path="$2" kind="$3"
  shift 3

  GOFLAGS=-mod=vendor go run "${CRD_REF_DOCS_PKG}" \
    --renderer=markdown \
    --config="${SCRIPT_ROOT}/hack/crd-ref-docs/config.yaml" \
    --templates-dir="${SCRIPT_ROOT}/hack/crd-ref-docs/templates/markdown" \
    --source-path="${SCRIPT_ROOT}/${source_path}" \
    --output-mode=single \
    --output-path="${SCRIPT_ROOT}/${out_path}" \
    --template-value="kind=${kind}" \
    "$@"
}

cd "${SCRIPT_ROOT}"

# CRD API
generate_kind "api/v1alpha1" "docs/reference/api/codercontrolplane.md" "CoderControlPlane" \
  --template-value="goType=api/v1alpha1/codercontrolplane_types.go" \
  --template-value="generatedCRD=config/crd/bases/coder.com_codercontrolplanes.yaml"

# Aggregated API
generate_kind "api/aggregation/v1alpha1" "docs/reference/api/coderworkspace.md" "CoderWorkspace" \
  --template-value="goType=api/aggregation/v1alpha1/types.go" \
  --template-value="storage=internal/aggregated/storage/workspace.go" \
  --template-value="apiServiceManifest=deploy/apiserver-apiservice.yaml"

generate_kind "api/aggregation/v1alpha1" "docs/reference/api/codertemplate.md" "CoderTemplate" \
  --template-value="goType=api/aggregation/v1alpha1/types.go" \
  --template-value="storage=internal/aggregated/storage/template.go" \
  --template-value="apiServiceManifest=deploy/apiserver-apiservice.yaml"

4) Wire it into make

Files to change

  • Makefile

Targets to add

  • docs-reference: generate the API reference docs.
  • docs-reference-check: verify docs are up-to-date (no diff after generation).

Suggested Makefile shape:

.PHONY: docs-reference docs-reference-check

docs-reference: $(VENDOR_STAMP)
	bash ./hack/update-reference-docs.sh

docs-reference-check: docs-reference
	git diff --exit-code -- docs/reference/api/

# Optional: ensure doc pages are generated before mkdocs build.
docs-check: docs-reference-check
	@command -v mkdocs >/dev/null || (echo "mkdocs not found; use nix develop" && exit 1)
	mkdocs build --strict

If we want to avoid pulling Go + vendor into a docs-only workflow, keep docs-check as-is and document that contributors should run make docs-reference when updating API types.


5) Make the generated nature explicit + regenerate docs

Files to change (generated output)

  • docs/reference/api/codercontrolplane.md
  • docs/reference/api/coderworkspace.md
  • docs/reference/api/codertemplate.md

Steps

  • Run bash ./hack/update-reference-docs.sh and commit the updated Markdown.
  • Ensure each generated file contains the “Code generated … DO NOT EDIT.” comment at the top.

6) (Recommended) CI enforcement so drift can’t land

Goal: If Go API types change, CI should fail unless docs/reference/api/*.md is regenerated.

Files to change

  • .github/workflows/ci.yaml

Approach

  • In the existing lint job (after vendoring check), add:
- name: Verify API reference docs are up to date
  run: |
    bash ./hack/update-reference-docs.sh
    git diff --exit-code -- docs/reference/api/
  • Update paths-filter “go” filter to also include the docgen inputs/outputs so CI runs when those change:
    • hack/update-reference-docs.sh
    • hack/crd-ref-docs/**
    • docs/reference/api/**

This keeps enforcement lightweight (no mkdocs / Python needed).


Validation / acceptance criteria

  • bash ./hack/update-reference-docs.sh runs successfully and produces deterministic output (second run yields no diff).
  • make verify-vendor passes after adding crd-ref-docs and updating vendor/.
  • make test and make build still pass.
  • If CI enforcement is added: CI fails when API types drift from docs/reference/api/*.md, and passes when regenerated.
Alternatives considered (kept brief)
  • Use crd-ref-docs output-mode=group and publish one file per API group (coder.com.md, aggregation.coder.com.md).

    • Pros: single invocation, scales better.
    • Cons: changes our current per-Kind pages + mkdocs nav; harder to preserve current doc structure.
  • Use default crd-ref-docs markdown templates.

    • Pros: richer output (nested types, validation/defaults).
    • Cons: larger diffs vs our current minimal docs; doesn’t match our current UX.

Generated with mux • Model: openai:gpt-5.3-codex • Thinking: xhigh • Cost: $0.45

Integrate elastic/crd-ref-docs into the project workflow to generate API
reference Markdown from Go types, wire generation checks into Make/CI, and
vendor the required tooling dependencies.

---

_Generated with `mux` • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `$0.45`_

<!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=0.45 -->
@ThomasK33
Copy link
Member Author

@codex review

Please review this PR for readiness.

Adjust the custom crd-ref-docs markdown template to emit exactly one trailing
newline in generated API reference pages, then regenerate the docs so
docs-quality markdown lint passes.

---

_Generated with `mux` • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `$0.45`_

<!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=0.45 -->
@ThomasK33
Copy link
Member Author

@codex review

Addressed docs-quality lint issues (trailing newline / blank-line formatting) and pushed a fix. Please re-review.

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. What shall we delve into next?

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Add an explicit AGENTS.md pattern note to run `make docs-reference` when
changing API structs in the core API type files, and to avoid merging without
updated generated docs under `docs/reference/api`.

---

_Generated with `mux` • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: `$0.45`_

<!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=0.45 -->
@ThomasK33 ThomasK33 added this pull request to the merge queue Feb 10, 2026
@ThomasK33
Copy link
Member Author

Merged via the queue into main with commit c1fbc5b Feb 10, 2026
10 checks passed
@ThomasK33 ThomasK33 deleted the docs-y855 branch February 10, 2026 09:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant