From ed45caac026fdcce093aebc8cbfa1d1ae48e44ee Mon Sep 17 00:00:00 2001 From: kayx23 Date: Thu, 15 Oct 2026 14:08:43 +0800 Subject: [PATCH] docs: fix generated CRD reference self-links --- .github/workflows/golangci-lint.yml | 2 + Makefile | 4 ++ docs/assets/crd/config.yaml | 19 ++++++ docs/assets/template/type.tpl | 10 ++- docs/en/latest/reference/api-reference.md | 13 ++-- scripts/check-crd-doc-links.py | 74 +++++++++++++++++++++++ 6 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 scripts/check-crd-doc-links.py diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index a10b580b9..64faaf6e2 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -61,3 +61,5 @@ jobs: echo "docs/en/latest/reference/api-reference.md has changed. Please run 'make generate-crd-docs' and commit the updated file." exit 1 fi + - name: Check CRD Docs Links + run: make check-crd-doc-links diff --git a/Makefile b/Makefile index 260793e42..d78dfff0e 100644 --- a/Makefile +++ b/Makefile @@ -468,6 +468,10 @@ generate-crd-docs: manifests ## Generate CRD reference documentation in a single --max-depth=100 @echo "CRD reference documentation generated at $(CRD_DOCS_OUTPUT)" +.PHONY: check-crd-doc-links +check-crd-doc-links: ## Check links to headings in the generated CRD reference + @python3 scripts/check-crd-doc-links.py $(CRD_DOCS_OUTPUT) + .PHONY: generate-crd-docs-grouped generate-crd-docs-grouped: manifests ## Generate CRD reference documentation grouped by API group @mkdir -p docs/crd/groups diff --git a/docs/assets/crd/config.yaml b/docs/assets/crd/config.yaml index c224cc429..88e5ec16f 100644 --- a/docs/assets/crd/config.yaml +++ b/docs/assets/crd/config.yaml @@ -28,3 +28,22 @@ render: # Version of Kubernetes to use when generating links to Kubernetes API documentation. # NOTE: Quotes are required, otherwise the value will be intepreted as a number so versions ending with `0` like 1.30 would be covreted to "1.3" in results. kubernetesVersion: "1.30" + # Link types defined outside the generated API packages to their authoritative references. + # Without these mappings, crd-ref-docs treats them as local types and emits fragments for + # headings that are not part of this document. + knownTypes: + - name: Group + package: sigs.k8s.io/gateway-api/apis/v1 + link: https://pkg.go.dev/sigs.k8s.io/gateway-api@v1.6.0/apis/v1#Group + - name: Kind + package: sigs.k8s.io/gateway-api/apis/v1 + link: https://pkg.go.dev/sigs.k8s.io/gateway-api@v1.6.0/apis/v1#Kind + - name: ObjectName + package: sigs.k8s.io/gateway-api/apis/v1 + link: https://pkg.go.dev/sigs.k8s.io/gateway-api@v1.6.0/apis/v1#ObjectName + - name: SectionName + package: sigs.k8s.io/gateway-api/apis/v1 + link: https://pkg.go.dev/sigs.k8s.io/gateway-api@v1.6.0/apis/v1#SectionName + - name: Duration + package: time + link: https://pkg.go.dev/time#Duration diff --git a/docs/assets/template/type.tpl b/docs/assets/template/type.tpl index 57f6a4339..0c499c8fb 100644 --- a/docs/assets/template/type.tpl +++ b/docs/assets/template/type.tpl @@ -50,9 +50,15 @@ {{ end }} -{{ if $type.References -}} +{{ $renderedReferences := list -}} +{{ range $type.SortedReferences -}} +{{ if markdownShouldRenderType . -}} +{{ $renderedReferences = append $renderedReferences . -}} +{{ end -}} +{{ end -}} +{{ if $renderedReferences -}} _Appears in:_ -{{- range $type.SortedReferences }} +{{- range $renderedReferences }} - {{ markdownRenderTypeLink . }} {{- end }} {{- end }} diff --git a/docs/en/latest/reference/api-reference.md b/docs/en/latest/reference/api-reference.md index 84fc1a98c..830e6ed0b 100644 --- a/docs/en/latest/reference/api-reference.md +++ b/docs/en/latest/reference/api-reference.md @@ -234,10 +234,10 @@ _Base type:_ `LocalPolicyTargetReferenceWithSectionName` | Field | Description | | --- | --- | -| `group` _[Group](#group)_ | Group is the group of the target resource. | -| `kind` _[Kind](#kind)_ | Kind is kind of the target resource. | -| `name` _[ObjectName](#objectname)_ | Name is the name of the target resource. | -| `sectionName` _[SectionName](#sectionname)_ | SectionName is the name of a section within the target resource. When unspecified, this targetRef targets the entire resource. In the following resources, SectionName is interpreted as the following:

• Gateway: Listener name
• HTTPRoute: HTTPRouteRule name
• Service: Port name

If a SectionName is specified, but does not exist on the targeted object, the Policy must fail to attach, and the policy implementation should record a `ResolvedRefs` or similar Condition in the Policy's status. | +| `group` _[Group](https://pkg.go.dev/sigs.k8s.io/gateway-api@v1.6.0/apis/v1#Group)_ | Group is the group of the target resource. | +| `kind` _[Kind](https://pkg.go.dev/sigs.k8s.io/gateway-api@v1.6.0/apis/v1#Kind)_ | Kind is kind of the target resource. | +| `name` _[ObjectName](https://pkg.go.dev/sigs.k8s.io/gateway-api@v1.6.0/apis/v1#ObjectName)_ | Name is the name of the target resource. | +| `sectionName` _[SectionName](https://pkg.go.dev/sigs.k8s.io/gateway-api@v1.6.0/apis/v1#SectionName)_ | SectionName is the name of a section within the target resource. When unspecified, this targetRef targets the entire resource. In the following resources, SectionName is interpreted as the following:

• Gateway: Listener name
• HTTPRoute: HTTPRouteRule name
• Service: Port name

If a SectionName is specified, but does not exist on the targeted object, the Policy must fail to attach, and the policy implementation should record a `ResolvedRefs` or similar Condition in the Policy's status. | _Appears in:_ @@ -664,8 +664,7 @@ _Appears in:_ | `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#condition-v1-meta) array_ | | -_Appears in:_ -- [ConsumerStatus](#consumerstatus) + #### Timeout @@ -804,7 +803,7 @@ ActiveHealthCheck defines the active upstream health check configuration. | Field | Description | | --- | --- | | `type` _string_ | Type is the health check type. Can be `http`, `https`, or `tcp`. | -| `timeout` _[Duration](#duration)_ | Timeout sets health check timeout in seconds. | +| `timeout` _[Duration](https://pkg.go.dev/time#Duration)_ | Timeout sets health check timeout in seconds. | | `concurrency` _integer_ | Concurrency sets the number of targets to be checked at the same time. | | `host` _string_ | Host sets the upstream host. | | `port` _integer_ | Port sets the upstream port. | diff --git a/scripts/check-crd-doc-links.py b/scripts/check-crd-doc-links.py new file mode 100644 index 000000000..1e508dd41 --- /dev/null +++ b/scripts/check-crd-doc-links.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Verify that generated CRD reference self-links have matching headings.""" + +from __future__ import annotations + +import re +import sys +from collections import defaultdict +from pathlib import Path +from urllib.parse import unquote + + +HEADING_PATTERN = re.compile(r"^#{1,6}\s+(.+?)\s*#*\s*$") +SELF_LINK_PATTERN = re.compile(r"\]\(#([^)]+)\)") + + +def heading_slug(heading: str) -> str: + """Match Docusaurus' generated IDs for the plain headings in this file.""" + return re.sub(r"[^\w -]", "", heading.lower()).replace(" ", "-") + + +def main() -> int: + path = Path(sys.argv[1] if len(sys.argv) > 1 else "docs/en/latest/reference/api-reference.md") + lines = path.read_text(encoding="utf-8").splitlines() + + anchors: set[str] = set() + occurrences: defaultdict[str, int] = defaultdict(int) + for line in lines: + match = HEADING_PATTERN.match(line) + if not match: + continue + base = heading_slug(match.group(1)) + count = occurrences[base] + occurrences[base] += 1 + anchors.add(base if count == 0 else f"{base}-{count}") + + missing: list[tuple[int, str]] = [] + checked = 0 + for line_number, line in enumerate(lines, start=1): + for match in SELF_LINK_PATTERN.finditer(line): + checked += 1 + fragment = unquote(match.group(1)) + if fragment not in anchors: + missing.append((line_number, fragment)) + + if missing: + print(f"Broken generated CRD self-links in {path}:", file=sys.stderr) + for line_number, fragment in missing: + print(f" line {line_number}: #{fragment}", file=sys.stderr) + return 1 + + print(f"Generated CRD self-links: {checked} checked, all targets exist.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())