diff --git a/.github/actions/generate-crd-api-reference/README.md b/.github/actions/generate-crd-api-reference/README.md new file mode 100644 index 0000000..99509ee --- /dev/null +++ b/.github/actions/generate-crd-api-reference/README.md @@ -0,0 +1,78 @@ +# generate-crd-api-reference + +Render a CRD API-reference markdown page from a **local** path of an operator's +Go types using [`crd-ref-docs`](https://github.com/elastic/crd-ref-docs). +Repo-agnostic; bundles its own rendering templates and post-processing fixups. +It does **not** clone — checking out sources is the caller's job. + +## Model + +The generator runs in the **operator** repo's CI: it reads the operator's own +Go types locally (no cross-repo read of private operator code), renders the +reference, and opens a PR in the **docs** repo. The only cross-repo access +needed is **write** to the docs repo, via `DOCS_TOKEN` (the operator's existing +broad-access token, e.g. `STAKATER_AB_REPOS`). + +## Turnkey usage (reusable workflow → PR in the docs repo) + +Add `crd-ref-docs.yaml` (your operator's ignore rules) to the **operator** repo +and a caller workflow: + +```yaml +name: Generate API Reference +on: + release: + types: [published] + workflow_dispatch: +jobs: + generate: + uses: stakater/.github/.github/workflows/generate-api-reference.yaml@main + with: + docs_repo: stakater/-docs + source_path: api// + config_file: crd-ref-docs.yaml + output_file: content/reference/api.md + secrets: + DOCS_TOKEN: ${{ secrets.STAKATER_AB_REPOS }} +``` + +## Direct action usage (render only, custom delivery) + +Check out whatever you need, then point the action at a local `source_path`: + +```yaml +- uses: actions/checkout@v5 # the operator repo — has the Go types + config +- uses: stakater/.github/.github/actions/generate-crd-api-reference@main + with: + source_path: api// + config_file: crd-ref-docs.yaml + output_file: content/reference/api.md +``` + +The action renders `output_file` from the local `source_path`; committing, +pushing, and PR policy are up to your workflow. + +## Run locally + +```bash +git clone --depth 1 https://github.com/stakater/.github /tmp/stakater-github +ACTION=/tmp/stakater-github/.github/actions/generate-crd-api-reference + +# from your operator repo root +SOURCE_PATH="$PWD/api//" \ +CONFIG_FILE="$PWD/crd-ref-docs.yaml" \ +OUTPUT_FILE="$PWD/api.md" \ +bash "$ACTION/generate-api-reference.sh" +``` + +Needs `go` and `perl` on PATH. Templates resolve automatically (sibling of the +script). + +## Notes + +- `source_path` may point at a single group (`api/group/v1alpha1`) or a parent + (`api/`) — `crd-ref-docs` recurses, so multi-group operators work unchanged. +- Two generic post-processing fixups keep output `mkdocs build --strict`-clean: + broken `map[string]Type` anchors are stripped to plain text, and a + `` line is prepended. +- Scratch (`.work/` locally, `RUNNER_TEMP` in CI) is never committed. diff --git a/.github/actions/generate-crd-api-reference/action.yml b/.github/actions/generate-crd-api-reference/action.yml new file mode 100644 index 0000000..8b08cbc --- /dev/null +++ b/.github/actions/generate-crd-api-reference/action.yml @@ -0,0 +1,64 @@ +name: Generate CRD API Reference +description: > + Render a CRD API-reference markdown page from a LOCAL path of an operator's Go + types using crd-ref-docs. Repo-agnostic; bundles its own rendering templates. + Checkout of source repos is the caller's responsibility — this action does not + clone. + +inputs: + source_path: + description: "Path (in the checkout) to the API types (e.g. api/group/v1alpha1, or api/ for all groups)." + required: true + config_file: + description: "Path (in the checkout) to crd-ref-docs.yaml." + required: false + default: "crd-ref-docs.yaml" + output_file: + description: "Path (in the checkout) to write the generated markdown." + required: false + default: "content/reference/api.md" + crd_ref_docs_version: + description: "crd-ref-docs version to install." + required: false + default: "v0.3.0" + go_version: + description: "Go version used to build crd-ref-docs." + required: false + default: "1.26" + working_directory: + description: "Directory that source_path/config_file/output_file are relative to." + required: false + default: "." + +outputs: + output_file: + description: "Absolute path to the generated markdown file." + value: ${{ steps.generate.outputs.output_file }} + +runs: + using: composite + steps: + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: ${{ inputs.go_version }} + + - name: Generate API reference + id: generate + shell: bash + working-directory: ${{ inputs.working_directory }} + env: + SOURCE_PATH_REL: ${{ inputs.source_path }} + CRD_REF_DOCS_VERSION: ${{ inputs.crd_ref_docs_version }} + CONFIG_FILE_REL: ${{ inputs.config_file }} + OUTPUT_FILE_REL: ${{ inputs.output_file }} + ACTION_PATH: ${{ github.action_path }} + run: | + set -euo pipefail + export SOURCE_PATH="$(pwd)/$SOURCE_PATH_REL" + export CONFIG_FILE="$(pwd)/$CONFIG_FILE_REL" + export OUTPUT_FILE="$(pwd)/$OUTPUT_FILE_REL" + export WORK_DIR="${RUNNER_TEMP:-$(pwd)/.work}/crd-ref" + # Script + templates live beside the action. + "$ACTION_PATH/generate-api-reference.sh" + echo "output_file=$OUTPUT_FILE" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/generate-crd-api-reference/crd-ref-templates/gv_details.tpl b/.github/actions/generate-crd-api-reference/crd-ref-templates/gv_details.tpl new file mode 100644 index 0000000..0e39c44 --- /dev/null +++ b/.github/actions/generate-crd-api-reference/crd-ref-templates/gv_details.tpl @@ -0,0 +1,68 @@ +{{- /* + Render types in hierarchical order: each container followed by the types it + contains. Root kinds first in declared order; any leftover types not + reached by traversal at the end, alphabetical. +*/ -}} + +{{- define "renderHierarchy" -}} +{{- $args := . -}} +{{- $type := index $args 0 -}} +{{- $visited := index $args 1 -}} +{{- $typeNames := index $args 2 -}} +{{- $allTypes := index $args 3 -}} +{{- if not (hasKey $visited $type.Name) }} +{{- $_ := set $visited $type.Name true }} + +{{ template "type" $type }} +{{- range $type.Members }} +{{- if .Type }} + {{- $memberType := .Type }} + {{- if not (hasKey $typeNames $memberType.Name) }} + {{- if $memberType.UnderlyingType }}{{- $memberType = $memberType.UnderlyingType }}{{- end }} + {{- if and $memberType $memberType.UnderlyingType }} + {{- if not (hasKey $typeNames $memberType.Name) }} + {{- $memberType = $memberType.UnderlyingType }} + {{- end }} + {{- end }} + {{- end }} + {{- if and $memberType (hasKey $typeNames $memberType.Name) }} + {{- $resolved := index $allTypes $memberType.Name }} + {{- template "renderHierarchy" (list $resolved $visited $typeNames $allTypes) }} + {{- end }} +{{- end }} +{{- end }} +{{- end }} +{{- end -}} + +{{- define "gvDetails" -}} +{{- $gv := . -}} + +## {{ $gv.GroupVersionString }} + +{{ $gv.Doc }} + +{{- if $gv.Kinds }} +### Resource Types +{{- range $gv.SortedKinds }} +- {{ $gv.TypeForKind . | markdownRenderTypeLink }} +{{- end }} +{{ end }} + +{{- $typeNames := dict -}} +{{- range $gv.SortedTypes -}} +{{- $_ := set $typeNames .Name true -}} +{{- end -}} + +{{- $visited := dict -}} + +{{ range $gv.SortedKinds }} +{{- template "renderHierarchy" (list ($gv.TypeForKind .) $visited $typeNames $gv.Types) }} +{{- end }} + +{{- range $gv.SortedTypes }} +{{- if not (hasKey $visited .Name) }} +{{- template "renderHierarchy" (list . $visited $typeNames $gv.Types) }} +{{- end }} +{{- end }} + +{{- end -}} diff --git a/.github/actions/generate-crd-api-reference/crd-ref-templates/gv_list.tpl b/.github/actions/generate-crd-api-reference/crd-ref-templates/gv_list.tpl new file mode 100644 index 0000000..a4d3dad --- /dev/null +++ b/.github/actions/generate-crd-api-reference/crd-ref-templates/gv_list.tpl @@ -0,0 +1,15 @@ +{{- define "gvList" -}} +{{- $groupVersions := . -}} + +# API Reference + +## Packages +{{- range $groupVersions }} +- {{ markdownRenderGVLink . }} +{{- end }} + +{{ range $groupVersions }} +{{ template "gvDetails" . }} +{{ end }} + +{{- end -}} diff --git a/.github/actions/generate-crd-api-reference/crd-ref-templates/type.tpl b/.github/actions/generate-crd-api-reference/crd-ref-templates/type.tpl new file mode 100644 index 0000000..7d89d04 --- /dev/null +++ b/.github/actions/generate-crd-api-reference/crd-ref-templates/type.tpl @@ -0,0 +1,49 @@ +{{- define "type" -}} +{{- $type := . -}} +{{- if markdownShouldRenderType $type -}} + +#### {{ $type.Name }} + +{{ if $type.IsAlias }}_Underlying type:_ _{{ markdownRenderTypeLink $type.UnderlyingType }}_{{ end }} + +{{ $type.Doc }} + +{{ if $type.Validation -}} +_Validation:_ +{{- range $type.Validation }} +- {{ . }} +{{- end }} +{{- end }} + +{{ if $type.References -}} +_Appears in:_ +{{- range $type.SortedReferences }} +- {{ markdownRenderTypeLink . }} +{{- end }} +{{- end }} + +{{ if $type.Members -}} +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +{{ if $type.GVK -}} +| `apiVersion` _string_ | `{{ $type.GVK.Group }}/{{ $type.GVK.Version }}` | | | +| `kind` _string_ | `{{ $type.GVK.Kind }}` | | | +{{ end -}} + +{{ range $type.Members -}} +| `{{ .Name }}` _{{ markdownRenderType .Type }}_ | {{ template "type_members" . }} | {{ markdownRenderDefault .Default }} | {{ range .Validation -}} {{ markdownRenderFieldDoc . }}
{{ end }} | +{{ end -}} + +{{ end -}} + +{{ if $type.EnumValues -}} +| Field | Description | +| --- | --- | +{{ range $type.EnumValues -}} +| `{{ .Name }}` | {{ markdownRenderFieldDoc .Doc }} | +{{ end -}} +{{ end -}} + + +{{- end -}} +{{- end -}} diff --git a/.github/actions/generate-crd-api-reference/crd-ref-templates/type_members.tpl b/.github/actions/generate-crd-api-reference/crd-ref-templates/type_members.tpl new file mode 100644 index 0000000..041758a --- /dev/null +++ b/.github/actions/generate-crd-api-reference/crd-ref-templates/type_members.tpl @@ -0,0 +1,8 @@ +{{- define "type_members" -}} +{{- $field := . -}} +{{- if eq $field.Name "metadata" -}} +Refer to Kubernetes API documentation for fields of `metadata`. +{{- else -}} +{{ markdownRenderFieldDoc $field.Doc }} +{{- end -}} +{{- end -}} diff --git a/.github/actions/generate-crd-api-reference/generate-api-reference.sh b/.github/actions/generate-crd-api-reference/generate-api-reference.sh new file mode 100755 index 0000000..7752b88 --- /dev/null +++ b/.github/actions/generate-crd-api-reference/generate-api-reference.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generic CRD API-reference renderer. Renders markdown from a LOCAL path of Go +# API types using crd-ref-docs, then applies generic post-processing. Checkout +# of any source repos is the caller's responsibility — this script never clones. +# Driven entirely by env vars so it can run from a composite action, a reusable +# workflow, or locally. + +# --- Required --- +: "${SOURCE_PATH:?SOURCE_PATH is required (local path to the API types, e.g. api/group.example.com/v1alpha1, or api/ for all groups)}" +: "${OUTPUT_FILE:?OUTPUT_FILE is required (absolute path to the markdown to write)}" +: "${CONFIG_FILE:?CONFIG_FILE is required (absolute path to crd-ref-docs.yaml)}" + +# --- Optional (with defaults) --- +CRD_REF_DOCS_VERSION="${CRD_REF_DOCS_VERSION:-v0.3.0}" + +# Templates ALWAYS resolved relative to this script so they travel with it. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEMPLATES_DIR="${TEMPLATES_DIR:-$SCRIPT_DIR/crd-ref-templates}" + +# Scratch area (tool binary only); never committed. +WORK_DIR="${WORK_DIR:-$PWD/.work}" +LOCALBIN="$WORK_DIR/bin" +CRD_REF_DOCS="$LOCALBIN/crd-ref-docs" + +mkdir -p "$LOCALBIN" "$(dirname "$OUTPUT_FILE")" + +# 1. Install crd-ref-docs (pinned) +if [ ! -x "$CRD_REF_DOCS" ]; then + echo "Installing crd-ref-docs $CRD_REF_DOCS_VERSION..." + GOBIN="$LOCALBIN" go install "github.com/elastic/crd-ref-docs@$CRD_REF_DOCS_VERSION" +fi + +# 2. Generate the reference from the local source path +"$CRD_REF_DOCS" \ + --source-path="$SOURCE_PATH" \ + --config="$CONFIG_FILE" \ + --templates-dir="$TEMPLATES_DIR" \ + --renderer=markdown \ + --output-path="$OUTPUT_FILE" + +# 3. Post-process (generic fixups for mkdocs strict builds) +# 3a. crd-ref-docs renders nested `map[string]Type` values as a link whose +# anchor contains brackets (`[map[string]X](#map[string]x)`) — an invalid +# anchor that fails `mkdocs build --strict`. Strip the link, keep the text. +perl -i -pe 's/\[(map\[string\][A-Za-z0-9]+)\]\(#map\[string\][a-z0-9]+\)/$1/g' "$OUTPUT_FILE" +# 3b. Silence markdownlint on the generated file. +sed -i '1i ' "$OUTPUT_FILE" + +echo "Wrote $OUTPUT_FILE (SOURCE_PATH=$SOURCE_PATH)" diff --git a/.github/workflows/generate-api-reference.yaml b/.github/workflows/generate-api-reference.yaml new file mode 100644 index 0000000..6fe73ba --- /dev/null +++ b/.github/workflows/generate-api-reference.yaml @@ -0,0 +1,105 @@ +name: Generate API Reference + +# Runs in the OPERATOR repo's context: reads the operator's own Go types locally +# (no cross-repo clone of private operator code), renders the API reference, and +# opens a PR in the docs repo. The only cross-repo access needed is WRITE to the +# docs repo, via DOCS_TOKEN. + +on: + workflow_call: + inputs: + docs_repo: + description: "Docs repository to write the reference into (owner/name)." + type: string + required: true + docs_ref: + description: "Docs repo ref to base the PR on." + type: string + required: false + default: "main" + source_path: + description: "Path in THIS (operator) repo to the API types (e.g. api/group/v1alpha1)." + type: string + required: true + config_file: + description: "Path in THIS (operator) repo to crd-ref-docs.yaml." + type: string + required: false + default: "crd-ref-docs.yaml" + output_file: + description: "Path in the DOCS repo to write the generated markdown." + type: string + required: false + default: "content/reference/api.md" + crd_ref_docs_version: + type: string + required: false + default: "v0.3.0" + go_version: + type: string + required: false + default: "1.26" + pr_branch: + type: string + required: false + default: "chore/regenerate-api-reference" + secrets: + DOCS_TOKEN: + description: "Token with write access to the docs repo (checkout + PR)." + required: true + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - name: Checkout operator repo + uses: actions/checkout@v5 + + - name: Checkout docs repo + uses: actions/checkout@v5 + with: + repository: ${{ inputs.docs_repo }} + ref: ${{ inputs.docs_ref }} + token: ${{ secrets.DOCS_TOKEN }} + path: docs-repo + + - name: Generate CRD API reference + uses: stakater/.github/.github/actions/generate-crd-api-reference@feat/api-gen-workflow + with: + source_path: ${{ inputs.source_path }} + config_file: ${{ inputs.config_file }} + output_file: docs-repo/${{ inputs.output_file }} + crd_ref_docs_version: ${{ inputs.crd_ref_docs_version }} + go_version: ${{ inputs.go_version }} + + - name: Create Pull Request + working-directory: docs-repo + env: + GH_TOKEN: ${{ secrets.DOCS_TOKEN }} + OUTPUT_FILE: ${{ inputs.output_file }} + OPERATOR_REPO: ${{ github.repository }} + OPERATOR_REF: ${{ github.ref_name }} + PR_BRANCH: ${{ inputs.pr_branch }} + run: | + set -euo pipefail + git config user.name "stakater-github-bot" + git config user.email "github-bot@stakater.com" + git checkout -B "$PR_BRANCH" + # Stage first so a brand-new (untracked) file counts as a change too; + # `git diff --cached` then covers add/modify/delete uniformly. + git add "$OUTPUT_FILE" + if git diff --cached --quiet -- "$OUTPUT_FILE"; then + echo "No changes in $OUTPUT_FILE; skipping PR." + exit 0 + fi + git commit -m "docs: regenerate API reference from $OPERATOR_REPO@$OPERATOR_REF" + # Dedicated bot branch: force-push (shallow single-branch checkout has + # no remote-tracking ref for a lease, so --force-with-lease is unsafe here). + git push --force origin "$PR_BRANCH" + # Create the PR only if one isn't already open for this branch. + if [ -z "$(gh pr list --head "$PR_BRANCH" --state open --json number -q '.[].number')" ]; then + gh pr create \ + --title "docs: regenerate API reference" \ + --body "Regenerated \`$OUTPUT_FILE\` from \`$OPERATOR_REPO@$OPERATOR_REF\`." \ + --head "$PR_BRANCH" + fi