From 42ba2e991d13d3c3abf3f876217a848300aa2751 Mon Sep 17 00:00:00 2001 From: Rachael Graham Date: Tue, 22 Sep 2026 11:36:45 -0500 Subject: [PATCH 1/5] Fix ref docs workflow: bump crd-ref-docs to v0.2.0 for Go 1.27 The nightly reference-docs job has failed since 2026-09-22 with x/tools@v0.19.0/internal/tokeninternal/tokeninternal.go:64:9: invalid array length -delta * delta (constant -256 of type int64) Nothing changed in kagent or the docs. Setup Go asks for 'stable', so GitHub runners picked up Go 1.27.1 and rebuilt crd-ref-docs against it. crd-ref-docs v0.1.0 depends on golang.org/x/tools v0.19.0, whose internal/tokeninternal carries a compile-time size assertion on go/token.File; Go 1.27 changed that struct, so the assertion fails and the step dies before generating anything. x/tools removed the file in v0.35.0, which crd-ref-docs v0.2.0 depends on, so the bump clears it. Verified under a real Go 1.27.1 toolchain against kagent main's go/api/v1alpha3: v0.1.0 reproduces the failure, v0.2.0 and v0.3.0 both generate. All 74 headings are identical across v0.1.0, v0.2.0 and v0.3.0, so the provider pages that link to crd-ref-docs anchors (#modelconfigspec, #openaiconfig, ...) are unaffected. v0.2.0 is the near-drop-in at 6 bytes of output difference: it collapses blank lines inside Go doc comments, flattening

to
in seven prose-heavy fields, and gains accurate items:MinLength on array fields. v0.3.0 was left on the table because it appends Required: {} / Optional: {} to all 244 Validation rows, which Hextra renders as literal braces. The rest of the job is Go 1.27-clean: helm-docs v1.14.2 has no x/tools dependency, and both CLI binaries build and smoke-test. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Rachael Graham --- .github/workflows/update-ref-docs.yaml | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-ref-docs.yaml b/.github/workflows/update-ref-docs.yaml index d3b6a9cb..bf9390e7 100644 --- a/.github/workflows/update-ref-docs.yaml +++ b/.github/workflows/update-ref-docs.yaml @@ -70,6 +70,23 @@ concurrency: # provider pages link to crd-ref-docs anchors (#modelconfigspec, #openaiconfig, # ...), so keep that renderer and its anchor naming. # +# crd-ref-docs is pinned at v0.2.0, and the floor is not negotiable: Setup Go +# below asks for 'stable', so every `go run @` here is rebuilt +# against whatever Go the runner has that morning. v0.1.0 depends on +# golang.org/x/tools v0.19.0, whose internal/tokeninternal carries a +# compile-time size assertion on go/token.File. Go 1.27 changed that struct, so +# from 2026-09-22 the build died with "invalid array length -delta * delta" +# before generating anything. x/tools dropped the file in v0.35.0, which is +# what v0.2.0 depends on. Do not pin back below v0.2.0. +# +# v0.3.0 also builds, and was left on the table on purpose: it appends +# `Required: {}` / `Optional: {}` to all 244 rows of the Validation column, +# which Hextra renders as literal braces. All 74 headings are identical across +# v0.1.0, v0.2.0 and v0.3.0, so the provider-page anchors above survive either +# choice. The one thing v0.2.0 costs is paragraph breaks inside long Go doc +# comments -- it collapses blank lines, so

flattens to
in +# about seven prose-heavy fields. +# # Frontmatter lives in the heredocs below, not in the generated pages: every # run overwrites it. Descriptions follow the 1.x convention of an imperative # verb and a full sentence. @@ -516,7 +533,7 @@ jobs: cat crd-ref-docs-config.yaml # Generate API docs - go run github.com/elastic/crd-ref-docs@v0.1.0 \ + go run github.com/elastic/crd-ref-docs@v0.2.0 \ --source-path="$GITHUB_WORKSPACE/kagent/go/api/v1alpha3/" \ --renderer=markdown \ --output-path ./ \ @@ -610,7 +627,7 @@ jobs: cat crd-ref-docs-config.yaml # Generate KMCP API docs - go run github.com/elastic/crd-ref-docs@v0.1.0 \ + go run github.com/elastic/crd-ref-docs@v0.2.0 \ --source-path="$GITHUB_WORKSPACE/kmcp/api/v1alpha1/" \ --renderer=markdown \ --output-path ./ \ From 6decf73305f682d686c8af0fcef9448fc43fb426 Mon Sep 17 00:00:00 2001 From: Rachael Graham Date: Tue, 22 Sep 2026 11:41:02 -0500 Subject: [PATCH 2/5] Mark required fields in the API reference; bump setup-go to v5 Takes crd-ref-docs to v0.3.0 rather than v0.2.0. Both clear the Go 1.27 build failure; v0.3.0 additionally marks which fields are required, which nothing before it did. The reference currently gives a reader no way to tell a required field from an optional one -- 67 required fields across the v1alpha3 types, none of them marked. v0.3.0 pays for that by stamping all 244 rows of the Validation column with `Required: \{\}` or `Optional: \{\}`, which Hextra renders as a literal `{}`. Both generation steps now normalize the markers: optional is the default, so its 177 markers are dropped outright, and the 67 required ones become bold text. Diffed the normalized v0.3.0 output against v0.2.0 on kagent main's go/api/v1alpha3: all 67 changed lines are explained purely by the added marker, with no other difference. So this is the v0.2.0 output plus required-field marking and nothing else. Verified the normalization block runs verbatim as the workflow renders it, and that the markers survive the angle-bracket pass that follows. Also bumps actions/setup-go from v4 to v5, off deprecated Node 20. Two other workflows in this repo already run setup-go@v5, so this one was the last v4. actions/checkout is left alone deliberately: it is 8 call sites across 6 workflows and belongs in its own change. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Rachael Graham --- .github/workflows/update-ref-docs.yaml | 65 ++++++++++++++++++++------ 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/.github/workflows/update-ref-docs.yaml b/.github/workflows/update-ref-docs.yaml index bf9390e7..a394d7a7 100644 --- a/.github/workflows/update-ref-docs.yaml +++ b/.github/workflows/update-ref-docs.yaml @@ -70,22 +70,27 @@ concurrency: # provider pages link to crd-ref-docs anchors (#modelconfigspec, #openaiconfig, # ...), so keep that renderer and its anchor naming. # -# crd-ref-docs is pinned at v0.2.0, and the floor is not negotiable: Setup Go +# crd-ref-docs is pinned at v0.3.0, and the floor is not negotiable: Setup Go # below asks for 'stable', so every `go run @` here is rebuilt # against whatever Go the runner has that morning. v0.1.0 depends on # golang.org/x/tools v0.19.0, whose internal/tokeninternal carries a # compile-time size assertion on go/token.File. Go 1.27 changed that struct, so # from 2026-09-22 the build died with "invalid array length -delta * delta" -# before generating anything. x/tools dropped the file in v0.35.0, which is -# what v0.2.0 depends on. Do not pin back below v0.2.0. +# before generating anything. x/tools dropped the file in v0.35.0, so v0.2.0 is +# the oldest release that builds at all. Do not pin back below it. # -# v0.3.0 also builds, and was left on the table on purpose: it appends -# `Required: {}` / `Optional: {}` to all 244 rows of the Validation column, -# which Hextra renders as literal braces. All 74 headings are identical across -# v0.1.0, v0.2.0 and v0.3.0, so the provider-page anchors above survive either -# choice. The one thing v0.2.0 costs is paragraph breaks inside long Go doc -# comments -- it collapses blank lines, so

flattens to
in -# about seven prose-heavy fields. +# All 74 headings are identical across v0.1.0, v0.2.0 and v0.3.0, so the +# provider-page anchors above survive any of them. The versions differ only in +# the table bodies: +# +# - v0.2.0 collapses blank lines inside Go doc comments, so

+# flattens to
in about seven prose-heavy fields. v0.3.0 does the +# same; there is no version that both builds and keeps those breaks. +# - v0.3.0 marks required fields, which nothing before it did. That is the +# reason for preferring it: the reference had no way to tell a required +# field from an optional one. It pays for that by stamping all 244 rows of +# the Validation column, so the generation steps below normalize the +# markers -- see the comment there. # # Frontmatter lives in the heredocs below, not in the generated pages: every # run overwrites it. Descriptions follow the 1.x convention of an imperative @@ -141,7 +146,7 @@ jobs: path: website - name: Setup Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: 'stable' cache: false @@ -533,7 +538,7 @@ jobs: cat crd-ref-docs-config.yaml # Generate API docs - go run github.com/elastic/crd-ref-docs@v0.2.0 \ + go run github.com/elastic/crd-ref-docs@v0.3.0 \ --source-path="$GITHUB_WORKSPACE/kagent/go/api/v1alpha3/" \ --renderer=markdown \ --output-path ./ \ @@ -548,6 +553,23 @@ jobs: # Remove the temporary config file so it is not included in the PR rm -f crd-ref-docs-config.yaml + # crd-ref-docs stamps every row of the Validation column with + # `Required: \{\}` or `Optional: \{\}`. Hextra renders those escaped + # braces as a literal `{}`, and optional is the default anyway, so drop + # the optional marker outright and render the required one as bold + # text. Marking required fields is the only thing v0.3.0 adds over + # v0.2.0; without this normalization it adds 244 rows of noise instead. + echo "Normalizing required/optional field markers..." + python3 - <<'PY_MARKERS' + from pathlib import Path + + path = Path("out.md") + text = path.read_text() + text = text.replace("Optional: \\{\\}
", "") + text = text.replace("Required: \\{\\}
", "**Required**
") + path.write_text(text) + PY_MARKERS + # Fix problematic angle brackets in the generated markdown. # Goldmark (unsafe: true) would otherwise treat stray <...> as raw HTML # and silently swallow it, so convert bare angle brackets to HTML @@ -627,7 +649,7 @@ jobs: cat crd-ref-docs-config.yaml # Generate KMCP API docs - go run github.com/elastic/crd-ref-docs@v0.2.0 \ + go run github.com/elastic/crd-ref-docs@v0.3.0 \ --source-path="$GITHUB_WORKSPACE/kmcp/api/v1alpha1/" \ --renderer=markdown \ --output-path ./ \ @@ -642,6 +664,23 @@ jobs: # Remove the temporary config file so it is not included in the PR rm -f crd-ref-docs-config.yaml + # crd-ref-docs stamps every row of the Validation column with + # `Required: \{\}` or `Optional: \{\}`. Hextra renders those escaped + # braces as a literal `{}`, and optional is the default anyway, so drop + # the optional marker outright and render the required one as bold + # text. Marking required fields is the only thing v0.3.0 adds over + # v0.2.0; without this normalization it adds 244 rows of noise instead. + echo "Normalizing required/optional field markers..." + python3 - <<'PY_MARKERS' + from pathlib import Path + + path = Path("out.md") + text = path.read_text() + text = text.replace("Optional: \\{\\}
", "") + text = text.replace("Required: \\{\\}
", "**Required**
") + path.write_text(text) + PY_MARKERS + # Fix problematic angle brackets (see the kagent step for rationale). echo "Fixing problematic angle brackets in generated markdown..." sed -i 's/
/__BR_TAG__/g' "./out.md" From 1a8bf2d5f23c376ec86fa863f652b761f17d91b2 Mon Sep 17 00:00:00 2001 From: Rachael Graham Date: Tue, 22 Sep 2026 11:53:35 -0500 Subject: [PATCH 3/5] Generate reference docs from a kagent release tag, not from main This job ran nightly against kagent main, which meant the 1.x reference described code that no released kagent contained, while the install commands on those same pages pinned an older release. That is not hypothetical. The 2026-09-22 run documented KagentHarnessCompaction and KagentHarnessSummarizer, neither of which exists at v1.0.0-alpha1, on pages whose `helm install --version` line says alpha1. The same run moved the Agent Substrate conref to 0.2.0-beta5; alpha1 pins beta4. A reader following those pages would have installed alpha1 and read about a tree they did not have. Changes: - Drops the nightly schedule. The workflow now runs on workflow_dispatch or a repository_dispatch of type kagent-release, and at no other time. A scheduled run can only ask what kagent looks like now, and that answer always drifts ahead of what anyone can install. - Adds a resolve job that picks the release to document -- an explicit version from either dispatch shape, otherwise kagent's latest release -- and validates it: version shape, tag existence, and a refusal to document any 0.x version, since 0.x is frozen at v0.10.1 and a 0.x dispatch would overwrite 1.x content with 0.x values. - Pins both checkouts to release tags. kmcp gets its own latest release rather than anything derived from the kagent tag, since it is unversioned in the docs and released on its own cadence. - Writes versions/kagent.md from the resolved tag. It was editorial because `git describe` on main yields a dev tag, an objection that only held while this ran from main. Leaving it manual now would let the install command drift from the reference pages regenerated beside it. The 1.x span is replaced with the same surgery used for agent-substrate.md, including the assertion that the frozen 0.x span came through byte-identical. - Resolves the runtime image digests at the release tag instead of reading versions/kagent.md. That read was correct only while the conref was updated ahead of the run; now that the same run writes it, reading it here would pin the images one release behind. Verified the rendered conref surgery against the real versions/kagent.md (1.x span moves to 1.0.0-alpha2, 0.x span untouched, no trailing newline gained) and the resolve step against the live GitHub API: it picks v1.0.0-alpha2, and rejects a 0.x version, an absent tag, and a non-version string. The release lives in kagent-dev/kagent, so `on: release` cannot fire in this repo. Until kagent's tag.yaml sends the dispatch, cut a release and then run this workflow from the Actions tab. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Rachael Graham --- .github/workflows/update-ref-docs.yaml | 224 ++++++++++++++++++++----- 1 file changed, 180 insertions(+), 44 deletions(-) diff --git a/.github/workflows/update-ref-docs.yaml b/.github/workflows/update-ref-docs.yaml index a394d7a7..33618d37 100644 --- a/.github/workflows/update-ref-docs.yaml +++ b/.github/workflows/update-ref-docs.yaml @@ -1,12 +1,26 @@ name: Update Reference Documentation on: - workflow_dispatch: # Allow manual triggers - schedule: - # Nightly at 06:30 UTC: regenerate API/Helm docs from the latest kagent/kmcp main so docs don't drift from code. - - cron: '30 6 * * *' + workflow_dispatch: + inputs: + version: + description: 'kagent release to generate from, e.g. 1.0.0-alpha2. Defaults to kagent''s latest release.' + required: false + # For kagent's release workflow to fire directly. Send {"version": "1.0.0-alpha2"} + # in client_payload, or omit it to use kagent's latest release. + repository_dispatch: + types: [kagent-release] + +# There is deliberately NO schedule here. A nightly run can only ever ask "what +# does kagent look like now", and the answer drifts ahead of what anyone can +# install. This workflow runs when a release happens and at no other time. +# +# The release lives in kagent-dev/kagent, so `on: release` cannot fire in this +# repo -- kagent has to tell us. Until its tag.yaml sends the dispatch, cut a +# release and then run this workflow from the Actions tab. -# Queue overlapping runs (e.g. nightly cron + a manual dispatch) so they don't force-push the same PR branch concurrently. +# Queue overlapping runs (e.g. a re-dispatch racing the first) so they don't +# force-push the same PR branch concurrently. concurrency: group: update-ref-docs cancel-in-progress: false @@ -28,11 +42,24 @@ concurrency: # title and body say so, because that case needs the guides re-tested rather # than a routine review. # -# Retargeted from 0.x to 1.x on 2026-09-11. This job generates from kagent -# main, so it can only ever be correct for the tree that main actually ships. +# This job generates from a kagent RELEASE TAG, never from main, and that is +# the whole point of its trigger shape. It ran nightly against main until +# 2026-09-22, which meant the 1.x reference described code no released kagent +# contained while the install commands on the same pages pinned an older +# release. That is not hypothetical: the 2026-09-22 run documented +# KagentHarnessCompaction and KagentHarnessSummarizer, both added after +# v1.0.0-alpha1, on pages whose `helm install --version` line said alpha1. The +# same run moved the Agent Substrate conref to 0.2.0-beta5, which alpha1 does +# not pin either. +# +# So the tag is resolved first and everything follows from it: both checkouts, +# every version conref, the runtime image digests, and the CLI binaries. A +# reader who installs the version these pages name gets the tree these pages +# describe. +# +# Retargeted from 0.x to 1.x on 2026-09-11, back when this still tracked main. # main dropped go/api/v1alpha2 in kagent 7bf6a6cd (#2696) on 2026-09-04 and -# now ships v1alpha3, which is what 1.x documents. Between that removal and -# this retarget the job failed every night on the missing v1alpha2 directory. +# now ships v1alpha3, which is what 1.x documents. # # 0.x is FROZEN and is no longer generated here. Its committed pages # (0.x/resources/api-ref.md, 0.x/resources/helm.md and 0.x/resources/cli/) all @@ -96,7 +123,78 @@ concurrency: # run overwrites it. Descriptions follow the 1.x convention of an imperative # verb and a full sentence. jobs: + # Decides WHICH kagent release this run documents, and whether there is + # anything to do. Split into its own job so the answer is available to the + # checkout steps below, which run before any script could compute it. + resolve: + runs-on: ubuntu-latest + outputs: + kagent_tag: ${{ steps.pick.outputs.kagent_tag }} + kagent_version: ${{ steps.pick.outputs.kagent_version }} + kmcp_tag: ${{ steps.pick.outputs.kmcp_tag }} + proceed: ${{ steps.pick.outputs.proceed }} + steps: + - name: Resolve the kagent release to document + id: pick + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OWNER: ${{ github.repository_owner }} + REQUESTED: ${{ github.event.inputs.version || github.event.client_payload.version || '' }} + run: | + set -euo pipefail + + # An explicit version wins, from either dispatch shape. Otherwise take + # kagent's latest release. `gh release view` with no tag returns the + # release marked Latest, which excludes drafts but INCLUDES + # prereleases -- deliberate while 1.0 is still on alpha tags. + if [ -n "$REQUESTED" ]; then + KAGENT_TAG="v${REQUESTED#v}" + echo "Version requested explicitly: $KAGENT_TAG" + else + KAGENT_TAG=$(gh release view --repo "$OWNER/kagent" --json tagName -q .tagName) + echo "Using kagent's latest release: $KAGENT_TAG" + fi + + KAGENT_VERSION="${KAGENT_TAG#v}" + if ! printf '%s' "$KAGENT_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then + echo "Error: resolved '$KAGENT_VERSION', which is not a version. Refusing to generate." + exit 1 + fi + + # Refuse to document a 0.x release. 0.x is frozen at v0.10.1 and its + # pages are regenerated by hand from that tag; a dispatch naming a 0.x + # version would otherwise overwrite 1.x content with 0.x values. + case "$KAGENT_VERSION" in + 0.*) echo "Error: $KAGENT_VERSION is a 0.x release. 0.x is frozen; see the header comment."; exit 1 ;; + esac + + # Prove the tag exists before four later steps assume it does. + if ! gh api "repos/$OWNER/kagent/git/ref/tags/$KAGENT_TAG" >/dev/null 2>&1; then + echo "Error: $OWNER/kagent has no tag $KAGENT_TAG." + exit 1 + fi + + # kmcp is unversioned in the docs and released on its own cadence, so + # it gets its own latest release rather than anything derived from the + # kagent tag. Still a release, not main, for the same reason. + KMCP_TAG=$(gh release view --repo "$OWNER/kmcp" --json tagName -q .tagName) + echo "Using kmcp's latest release: $KMCP_TAG" + + { + echo "kagent_tag=$KAGENT_TAG" + echo "kagent_version=$KAGENT_VERSION" + echo "kmcp_tag=$KMCP_TAG" + } >> "$GITHUB_OUTPUT" + + # Every trigger this workflow has is deliberate, so every run proceeds. + # Re-running the same release is fine and sometimes necessary -- to + # pick up images that were still pushing, or to regenerate after a + # docs-side change like a renderer bump. + echo "proceed=true" >> "$GITHUB_OUTPUT" + generate-api-docs: + needs: resolve + if: needs.resolve.outputs.proceed == 'true' runs-on: ubuntu-latest permissions: @@ -105,6 +203,11 @@ jobs: # Target Hugo content paths (relative to the website checkout). env: + # The release this run documents. Every conref, both checkouts, the + # runtime image digests and the CLI binaries derive from it. + KAGENT_VERSION: ${{ needs.resolve.outputs.kagent_version }} + KAGENT_TAG: ${{ needs.resolve.outputs.kagent_tag }} + KMCP_TAG: ${{ needs.resolve.outputs.kmcp_tag }} KAGENT_API_PAGE: docs-site/content/kagent/1.x/reference/api-ref.md KMCP_API_PAGE: docs-site/content/kmcp/reference/api-ref.md HELM_PAGE: docs-site/content/kagent/1.x/reference/helm.md @@ -127,16 +230,20 @@ jobs: KMCP_CLI_URL_PREFIX: /docs/kmcp/reference/cli steps: + # Both checkouts are pinned to a release tag. Checking out main here is + # what let the reference describe unreleased code; see the header comment. - name: Checkout kagent repository uses: actions/checkout@v4 with: repository: ${{ github.repository_owner }}/kagent + ref: ${{ needs.resolve.outputs.kagent_tag }} path: kagent - name: Checkout kmcp repository uses: actions/checkout@v4 with: repository: ${{ github.repository_owner }}/kmcp + ref: ${{ needs.resolve.outputs.kmcp_tag }} path: kmcp - name: Checkout docs repository @@ -170,13 +277,12 @@ jobs: # is NOT a source: it is gitignored and generated from Chart-template.yaml # by envsubst, so a checkout never carries a useful value. # - # Deliberately not automated: kagent.md names the release the docs - # DESCRIBE, while `git describe` on main yields a dev tag. That stayed - # editorial through 1.0 and should stay editorial now, for a second reason - # it did not have before -- the runtime image step below resolves its - # digests at the tag this conref names, so this one file is what decides - # which release the whole 1.x doc set pins to. Bumping it by hand is the - # release switch; everything else follows on the next run. + # kagent.md was editorial until 2026-09-22, because `git describe` on main + # yields a dev tag and this job ran from main. Generating from a release + # tag removes that objection: the tag IS the release the docs describe, so + # the conref is now written from it like every other value here. Keeping + # it manual under the new trigger would only let the install command drift + # away from the reference pages regenerated beside it. # # Also not automated: jaeger / loki / tempo / otel-collector are pinned # nowhere in kagent, so the only source is "latest upstream", which would @@ -268,22 +374,12 @@ jobs: run: | set -euo pipefail - KAGENT_CONREF="$VERSIONS_DIR/kagent.md" - if [ ! -f "$KAGENT_CONREF" ]; then - echo "Error: expected conref $KAGENT_CONREF" - exit 1 - fi - - # The 1.x span of: - # {{< version include-if="0.x" >}}0.10.1{{< /version >}}{{< version include-if="1.x" >}}1.0.0-alpha1{{< /version >}} - IMAGE_TAG=$(sed -E 's|.*include-if="1\.x" >\}\}([^{]*)\{\{< /version >\}\}.*|\1|' "$KAGENT_CONREF") - - if ! printf '%s' "$IMAGE_TAG" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then - echo "Error: read the image tag as '$IMAGE_TAG' from $KAGENT_CONREF, which is not a version." - echo "The conref's shape likely changed. Fix the extraction here rather than editing the conref by hand." - exit 1 - fi - echo "Resolving runtime images at tag $IMAGE_TAG (from versions/kagent.md)" + # The release being documented, not whatever versions/kagent.md holds. + # This step used to read the conref, which worked only while that file + # was editorial and updated ahead of the run. Now that the same run + # writes it, reading it here would pin the images one release behind. + IMAGE_TAG="$KAGENT_VERSION" + echo "Resolving runtime images at tag $IMAGE_TAG (the release being documented)" # Anonymous pull token; these packages are public. Deliberately not # crane or `docker buildx imagetools`: neither is on this runner, and @@ -316,12 +412,13 @@ jobs: digest=$(ghcr_digest "$repo" "$IMAGE_TAG" || true) - # An unresolved digest is NOT fatal. versions/kagent.md is editorial - # and may legitimately name a release whose images are not pushed - # yet. Writing an empty conref would blank a `workload.image` line - # on four pages, so the conref is left alone and the miss is - # surfaced in the PR body instead -- same shape as the kmcp - # cross-check above. + # An unresolved digest is NOT fatal. A GitHub release can be + # published before its images finish pushing, so a run triggered by + # that release can legitimately arrive early. Writing an empty + # conref would blank a `workload.image` line on four pages, so the + # conref is left alone and the miss is surfaced in the PR body + # instead -- same shape as the kmcp cross-check above. Re-dispatch + # the workflow once the images land. if ! printf '%s' "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$'; then echo "::warning::Could not resolve a digest for ${repo}:${IMAGE_TAG}. Leaving its conref unchanged." UNRESOLVED="${UNRESOLVED}\`${repo}:${IMAGE_TAG}\` " @@ -472,6 +569,42 @@ jobs: echo " agent-substrate.md: unchanged ($SUBSTRATE_VERSION)" fi + # kagent.md is version-split the same way, and 0.x is FROZEN at the + # v0.10.1 value. This is the conref that decides which release the 1.x + # install commands name, so writing it from the tag we generated + # everything else from is what keeps the pages self-consistent. + KAGENT_CONREF="$VERSIONS_DIR/kagent.md" + if [ ! -f "$KAGENT_CONREF" ]; then + echo "Error: expected conref $KAGENT_CONREF" + exit 1 + fi + + KAGENT_BEFORE=$(cat "$KAGENT_CONREF") + KAGENT_AFTER=$(printf '%s' "$KAGENT_BEFORE" \ + | sed -E "s|(include-if=\"1\.x\" >\}\})[^{]*(\{\{< /version >\}\})|\1${KAGENT_VERSION}\2|") + printf '%s' "$KAGENT_AFTER" > "$KAGENT_CONREF" + + if ! printf '%s' "$KAGENT_AFTER" | grep -qF "include-if=\"1.x\" >}}${KAGENT_VERSION}{{< /version >}}"; then + echo "Error: the 1.x span of kagent.md did not take the new value." + echo " before: $KAGENT_BEFORE" + echo " after: $KAGENT_AFTER" + exit 1 + fi + KAGENT_BEFORE_0X=$(printf '%s' "$KAGENT_BEFORE" | sed -E 's|(\{\{< version include-if="1\.x").*||') + KAGENT_AFTER_0X=$(printf '%s' "$KAGENT_AFTER" | sed -E 's|(\{\{< version include-if="1\.x").*||') + if [ "$KAGENT_BEFORE_0X" != "$KAGENT_AFTER_0X" ]; then + echo "Error: the frozen 0.x span of kagent.md changed. Refusing to continue." + echo " before: $KAGENT_BEFORE_0X" + echo " after: $KAGENT_AFTER_0X" + exit 1 + fi + + if [ "$KAGENT_BEFORE" != "$KAGENT_AFTER" ]; then + echo " kagent.md 1.x span -> $KAGENT_VERSION (0.x span untouched)" + else + echo " kagent.md: unchanged ($KAGENT_VERSION)" + fi + - name: Flag whether the conrefs moved run: | set -euo pipefail @@ -907,15 +1040,17 @@ jobs: signoff: true title: "Update kagent 1.x and kmcp reference docs${{ env.CONREFS_CHANGED && ' (version conrefs changed)' || '' }}" body: | - Automated update of the kagent 1.x API and Helm references, the kmcp API reference, and the version conrefs, based on the latest commits: - - **kagent**: [`${{ env.KAGENT_COMMIT }}`](https://github.com/${{ github.repository_owner }}/kagent/commit/${{ env.KAGENT_COMMIT }}) - - **kmcp**: [`${{ env.KMCP_COMMIT }}`](https://github.com/${{ github.repository_owner }}/kmcp/commit/${{ env.KMCP_COMMIT }}) + Regenerates the kagent 1.x API and Helm references, the kmcp API reference, and the version conrefs at a released tag. Everything below was generated from these, and nothing here was read from either repository's `main`: + - **kagent**: [`${{ env.KAGENT_TAG }}`](https://github.com/${{ github.repository_owner }}/kagent/releases/tag/${{ env.KAGENT_TAG }}) (`${{ env.KAGENT_COMMIT }}`) + - **kmcp**: [`${{ env.KMCP_TAG }}`](https://github.com/${{ github.repository_owner }}/kmcp/releases/tag/${{ env.KMCP_TAG }}) (`${{ env.KMCP_COMMIT }}`) + + `versions/kagent.md` moves to `${{ env.KAGENT_VERSION }}` in this run, so the `helm install --version` lines on the 1.x pages and the reference content beside them describe the same release. ${{ env.CONREF_NOTE }} ${{ env.RUNTIME_IMAGE_NOTE }} - Version conrefs in this run. Each is read from the same place kagent's own build reads it, except the two runtime images, which are resolved from the registry because a digest exists only once the image is pushed: + Version conrefs in this run. Each is read from the same place kagent's own build reads it, at the tag above, except the two runtime images, which are resolved from the registry because a digest exists only once the image is pushed: | Conref | Value | Source | | ------ | ----- | ------ | @@ -923,8 +1058,9 @@ jobs: | `kmcp` | `${{ env.KMCP_VERSION }}` | `go/go.mod` require | | `kagent-tools` | `${{ env.KAGENT_TOOLS_VERSION }}` | `helm/kagent/Chart-template.yaml` | | `max-kube` (both committed copies) | `${{ env.MAX_KUBE }}` | `Makefile` `KIND_IMAGE_VERSION` | - | `runtime-image` | `${{ env.RUNTIME_IMAGE }}` | GHCR, at the tag in `versions/kagent.md` | - | `runtime-image-claude` | `${{ env.RUNTIME_IMAGE_CLAUDE }}` | GHCR, at the tag in `versions/kagent.md` | + | `kagent` (1.x span only) | `${{ env.KAGENT_VERSION }}` | the release tag this run documents | + | `runtime-image` | `${{ env.RUNTIME_IMAGE }}` | GHCR, at `${{ env.KAGENT_TAG }}` | + | `runtime-image-claude` | `${{ env.RUNTIME_IMAGE_CLAUDE }}` | GHCR, at `${{ env.KAGENT_TAG }}` | Unchanged conref values are normal: most runs touch only the generated reference pages. From 5f79c7fcf910449e556552b1f3b6388b75e83e79 Mon Sep 17 00:00:00 2001 From: Rachael Graham Date: Tue, 22 Sep 2026 12:01:00 -0500 Subject: [PATCH 4/5] Split the kmcp conref: what kagent bundles vs kmcp's latest release versions/kmcp.md was one file answering two different questions, and it only looked correct because kagent happened to pin kmcp's newest release. kmcp 0.4.0 broke the tie -- kagent alpha1 and alpha2 both pin 0.3.0 -- so one value cannot serve both readers any more: versions/kmcp.md what kagent bundles, from go/go.mod. Read by the kagent 1.x support matrix, which describes the release kagent ships. Stays 0.3.0. versions/kmcp-latest.md kmcp's newest release. Read by the standalone kmcp quickstart and install guide, both of which render it inside sample CLI output reading "No version specified, using latest: v...". That line is a claim about what kmcp resolves to, so kagent's pin is wrong there. Bumping the single conref to 0.4.0 instead would have made the kagent support matrix claim alpha2 bundles kmcp 0.4.0, which it does not -- the same drift this branch removes for kagent itself. The cross-check step that used to warn about this divergence now writes both values and reports the split, since it is deliberate. An unreadable kmcp release leaves kmcp-latest.md alone rather than blanking it, the same way an unresolved image digest already does. Verified with a Hugo build: both kmcp pages render 0.4.0 and the kagent support matrix still renders 0.3.0. Ran the rendered conref step against a copy of the real versions/ directory with alpha2 values -- both version-split files take the new 1.x value with the frozen 0.x span byte-identical, and no conref gains a trailing newline. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Rachael Graham --- .github/workflows/update-ref-docs.yaml | 54 ++++++++++++++----- .../kagent-docs/versions/kmcp-latest.md | 1 + .../content/kmcp/deploy/install-controller.md | 2 +- docs-site/content/kmcp/quickstart.md | 2 +- 4 files changed, 43 insertions(+), 16 deletions(-) create mode 100644 docs-site/assets/kagent-docs/versions/kmcp-latest.md diff --git a/.github/workflows/update-ref-docs.yaml b/.github/workflows/update-ref-docs.yaml index 33618d37..ed6e0b0f 100644 --- a/.github/workflows/update-ref-docs.yaml +++ b/.github/workflows/update-ref-docs.yaml @@ -446,30 +446,49 @@ jobs: run: | set -euo pipefail - # versions/kmcp.md is reused by BOTH the kagent 1.x support matrix and - # the standalone kmcp install guides, which want subtly different - # things: what kagent bundles, versus what kmcp last released. The two - # agree today (both 0.3.0). This job writes the kagent-pinned value, - # because the guides that install kmcp inside kagent outnumber the - # standalone ones and must match the bundled chart. A divergence is - # surfaced here rather than silently resolved -- if it ever fires, the - # real fix is to split the conref by version the way - # versions/agent-substrate.md already does. + # kagent's pinned kmcp and kmcp's own latest release are two different + # facts, and each has pages that need it: + # + # versions/kmcp.md what kagent bundles, from go/go.mod. + # Read by the kagent 1.x support matrix, + # which describes the release kagent ships. + # versions/kmcp-latest.md kmcp's newest release. Read by the + # standalone kmcp quickstart and install + # guide, both of which render it inside + # sample CLI output reading "No version + # specified, using latest: v...". That line + # is a claim about what kmcp resolves to, so + # kagent's pin would be wrong there. + # + # They were one file until 2026-09-22, carrying the kagent-pinned + # value, and agreed only because kagent happened to pin kmcp's newest + # release. kmcp 0.4.0 broke the tie: kagent alpha1 and alpha2 both pin + # 0.3.0. Splitting is what the single-file version of this step + # already recommended doing if the two ever diverged. KMCP_LATEST=$(gh release view \ --repo "${{ github.repository_owner }}/kmcp" \ --json tagName -q .tagName 2>/dev/null | sed 's/^v//' || true) + # Empty would blank the conref and break the sample output on two + # pages, so an unreadable release leaves kmcp-latest.md alone. if [ -z "$KMCP_LATEST" ]; then - echo "Note: could not read kmcp's latest release; skipping the cross-check." + echo "::warning::Could not read kmcp's latest release. Leaving versions/kmcp-latest.md unchanged." + echo "KMCP_LATEST=" >> $GITHUB_ENV echo "KMCP_MISMATCH=" >> $GITHUB_ENV exit 0 fi + echo "KMCP_LATEST=$KMCP_LATEST" >> $GITHUB_ENV + + # A divergence is now expected and handled, so it is reported rather + # than warned about. It is still worth saying out loud: it means the + # kagent support matrix and the kmcp guides name different versions on + # purpose, which looks like a bug to anyone who has not read this. if [ "$KMCP_LATEST" != "$KMCP_VERSION" ]; then - echo "::warning::kagent pins kmcp $KMCP_VERSION but kmcp's latest release is $KMCP_LATEST. Writing the pinned value; the standalone kmcp install guides may now understate." - echo "KMCP_MISMATCH=kagent pins \`$KMCP_VERSION\`, kmcp's latest release is \`$KMCP_LATEST\`. The pinned value was written. Consider splitting \`versions/kmcp.md\` by version." >> $GITHUB_ENV + echo "kmcp split: kagent pins $KMCP_VERSION, kmcp's latest release is $KMCP_LATEST. Writing both." + echo "KMCP_MISMATCH=\`versions/kmcp.md\` is \`$KMCP_VERSION\` (what kagent bundles) and \`versions/kmcp-latest.md\` is \`$KMCP_LATEST\` (kmcp's newest release). The two naming different versions is expected -- the kagent support matrix and the standalone kmcp guides are answering different questions." >> $GITHUB_ENV else - echo "kmcp cross-check OK: pinned and latest release both $KMCP_VERSION" + echo "kmcp: kagent's pin and kmcp's latest release are both $KMCP_VERSION." echo "KMCP_MISMATCH=" >> $GITHUB_ENV fi @@ -503,6 +522,12 @@ jobs: echo "Plain-value conrefs:" write_conref "$VERSIONS_DIR/kmcp.md" "$KMCP_VERSION" + # Only when it resolved; see the cross-check step above. + if [ -n "${KMCP_LATEST:-}" ]; then + write_conref "$VERSIONS_DIR/kmcp-latest.md" "$KMCP_LATEST" + else + echo " kmcp-latest.md: SKIPPED, kmcp's latest release was unreadable. Left at $(cat "$VERSIONS_DIR/kmcp-latest.md")" + fi write_conref "$VERSIONS_DIR/kagent-tools.md" "$KAGENT_TOOLS_VERSION" write_conref "$VERSIONS_DIR/max-kube.md" "$MAX_KUBE" write_conref "$KUBE_VERSION_FILE" "$MAX_KUBE" @@ -1055,7 +1080,8 @@ jobs: | Conref | Value | Source | | ------ | ----- | ------ | | `agent-substrate` (1.x span only) | `${{ env.SUBSTRATE_VERSION }}` | `go/go.mod` replace target | - | `kmcp` | `${{ env.KMCP_VERSION }}` | `go/go.mod` require | + | `kmcp` | `${{ env.KMCP_VERSION }}` | `go/go.mod` require (what kagent bundles) | + | `kmcp-latest` | `${{ env.KMCP_LATEST }}` | kmcp's latest release (what the kmcp guides show) | | `kagent-tools` | `${{ env.KAGENT_TOOLS_VERSION }}` | `helm/kagent/Chart-template.yaml` | | `max-kube` (both committed copies) | `${{ env.MAX_KUBE }}` | `Makefile` `KIND_IMAGE_VERSION` | | `kagent` (1.x span only) | `${{ env.KAGENT_VERSION }}` | the release tag this run documents | diff --git a/docs-site/assets/kagent-docs/versions/kmcp-latest.md b/docs-site/assets/kagent-docs/versions/kmcp-latest.md new file mode 100644 index 00000000..60a2d3e9 --- /dev/null +++ b/docs-site/assets/kagent-docs/versions/kmcp-latest.md @@ -0,0 +1 @@ +0.4.0 \ No newline at end of file diff --git a/docs-site/content/kmcp/deploy/install-controller.md b/docs-site/content/kmcp/deploy/install-controller.md index 31d074aa..f694bf46 100644 --- a/docs-site/content/kmcp/deploy/install-controller.md +++ b/docs-site/content/kmcp/deploy/install-controller.md @@ -42,7 +42,7 @@ The kmcp controller manages the lifecycle of MCP servers that are defined in an Example output: ```sh 🚀 Deploying KMCP controller to cluster... - No version specified, using latest: v{{< reuse "kagent-docs/versions/kmcp.md" >}} + No version specified, using latest: v{{< reuse "kagent-docs/versions/kmcp-latest.md" >}} Release "kmcp" does not exist. Installing it now. NAME: kmcp LAST DEPLOYED: Wed Jul 30 18:41:01 2025 diff --git a/docs-site/content/kmcp/quickstart.md b/docs-site/content/kmcp/quickstart.md index d6da5741..64453887 100644 --- a/docs-site/content/kmcp/quickstart.md +++ b/docs-site/content/kmcp/quickstart.md @@ -104,7 +104,7 @@ With your first FastMCP Python server up and running, you can now deploy it to a Example output: ```sh 🚀 Deploying KMCP controller to cluster... - No version specified, using latest: v{{< reuse "kagent-docs/versions/kmcp.md" >}} + No version specified, using latest: v{{< reuse "kagent-docs/versions/kmcp-latest.md" >}} Release "kmcp" does not exist. Installing it now. NAME: kmcp LAST DEPLOYED: Wed Jul 30 18:41:01 2025 From 072c52b9cf17eecf6d3283436d5c8d0e93d8769d Mon Sep 17 00:00:00 2001 From: Rachael Graham Date: Tue, 22 Sep 2026 12:14:59 -0500 Subject: [PATCH 5/5] Trim the workflow comments added by this branch Cut 34 of the 131 comment lines this branch added, against 129 lines of code. The file was already 29% comments before this branch and is 30% now, rather than 32%. What went: a two-column table describing the kmcp split, which restated the two filenames and their values; three header paragraphs about generating from a tag, where one carries the rule and the concrete failure; the "Retargeted from 0.x on 2026-09-11" history, superseded by the tag change; and the restatements in the kagent.md and digest notes. What stayed is the set that stops a specific regression: why there is no schedule, why the digest step no longer reads versions/kagent.md (doing so now would pin images one release behind, silently), why 0.x dispatches are refused, and the crd-ref-docs pin floor. No logic changed. Re-ran the resolve step against the live API and the rendered conref step against a copy of the real versions/ directory to confirm the trim did not disturb either. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Rachael Graham --- .github/workflows/update-ref-docs.yaml | 77 +++++++------------------- 1 file changed, 21 insertions(+), 56 deletions(-) diff --git a/.github/workflows/update-ref-docs.yaml b/.github/workflows/update-ref-docs.yaml index ed6e0b0f..1e6aafd6 100644 --- a/.github/workflows/update-ref-docs.yaml +++ b/.github/workflows/update-ref-docs.yaml @@ -11,13 +11,10 @@ on: repository_dispatch: types: [kagent-release] -# There is deliberately NO schedule here. A nightly run can only ever ask "what -# does kagent look like now", and the answer drifts ahead of what anyone can -# install. This workflow runs when a release happens and at no other time. -# -# The release lives in kagent-dev/kagent, so `on: release` cannot fire in this -# repo -- kagent has to tell us. Until its tag.yaml sends the dispatch, cut a -# release and then run this workflow from the Actions tab. +# There is deliberately NO schedule: a nightly run can only ask what kagent +# looks like now, which drifts ahead of what anyone can install. The release +# lives in kagent-dev/kagent, so `on: release` cannot fire here -- kagent has to +# tell us. Until its tag.yaml sends the dispatch, run this from the Actions tab. # Queue overlapping runs (e.g. a re-dispatch racing the first) so they don't # force-push the same PR branch concurrently. @@ -42,24 +39,10 @@ concurrency: # title and body say so, because that case needs the guides re-tested rather # than a routine review. # -# This job generates from a kagent RELEASE TAG, never from main, and that is -# the whole point of its trigger shape. It ran nightly against main until -# 2026-09-22, which meant the 1.x reference described code no released kagent -# contained while the install commands on the same pages pinned an older -# release. That is not hypothetical: the 2026-09-22 run documented -# KagentHarnessCompaction and KagentHarnessSummarizer, both added after -# v1.0.0-alpha1, on pages whose `helm install --version` line said alpha1. The -# same run moved the Agent Substrate conref to 0.2.0-beta5, which alpha1 does -# not pin either. -# -# So the tag is resolved first and everything follows from it: both checkouts, -# every version conref, the runtime image digests, and the CLI binaries. A -# reader who installs the version these pages name gets the tree these pages -# describe. -# -# Retargeted from 0.x to 1.x on 2026-09-11, back when this still tracked main. -# main dropped go/api/v1alpha2 in kagent 7bf6a6cd (#2696) on 2026-09-04 and -# now ships v1alpha3, which is what 1.x documents. +# This job generates from a kagent RELEASE TAG, never from main. It ran nightly +# against main until 2026-09-22, and the 2026-09-22 run is what that cost: +# KagentHarnessCompaction and KagentHarnessSummarizer documented on pages whose +# `helm install --version` line said alpha1, a release neither type exists in. # # 0.x is FROZEN and is no longer generated here. Its committed pages # (0.x/resources/api-ref.md, 0.x/resources/helm.md and 0.x/resources/cli/) all @@ -277,12 +260,9 @@ jobs: # is NOT a source: it is gitignored and generated from Chart-template.yaml # by envsubst, so a checkout never carries a useful value. # - # kagent.md was editorial until 2026-09-22, because `git describe` on main - # yields a dev tag and this job ran from main. Generating from a release - # tag removes that objection: the tag IS the release the docs describe, so - # the conref is now written from it like every other value here. Keeping - # it manual under the new trigger would only let the install command drift - # away from the reference pages regenerated beside it. + # kagent.md was editorial while this ran from main, where `git describe` + # only yields a dev tag. A release tag IS the release the docs describe, + # so it is written from that like everything else here. # # Also not automated: jaeger / loki / tempo / otel-collector are pinned # nowhere in kagent, so the only source is "latest upstream", which would @@ -412,13 +392,11 @@ jobs: digest=$(ghcr_digest "$repo" "$IMAGE_TAG" || true) - # An unresolved digest is NOT fatal. A GitHub release can be - # published before its images finish pushing, so a run triggered by - # that release can legitimately arrive early. Writing an empty + # NOT fatal: a release can be published before its images finish + # pushing, so a run triggered by it can arrive early. An empty # conref would blank a `workload.image` line on four pages, so the - # conref is left alone and the miss is surfaced in the PR body - # instead -- same shape as the kmcp cross-check above. Re-dispatch - # the workflow once the images land. + # value is left alone and the miss goes in the PR body instead. + # Re-dispatch once the images land. if ! printf '%s' "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$'; then echo "::warning::Could not resolve a digest for ${repo}:${IMAGE_TAG}. Leaving its conref unchanged." UNRESOLVED="${UNRESOLVED}\`${repo}:${IMAGE_TAG}\` " @@ -446,25 +424,12 @@ jobs: run: | set -euo pipefail - # kagent's pinned kmcp and kmcp's own latest release are two different - # facts, and each has pages that need it: - # - # versions/kmcp.md what kagent bundles, from go/go.mod. - # Read by the kagent 1.x support matrix, - # which describes the release kagent ships. - # versions/kmcp-latest.md kmcp's newest release. Read by the - # standalone kmcp quickstart and install - # guide, both of which render it inside - # sample CLI output reading "No version - # specified, using latest: v...". That line - # is a claim about what kmcp resolves to, so - # kagent's pin would be wrong there. - # - # They were one file until 2026-09-22, carrying the kagent-pinned - # value, and agreed only because kagent happened to pin kmcp's newest - # release. kmcp 0.4.0 broke the tie: kagent alpha1 and alpha2 both pin - # 0.3.0. Splitting is what the single-file version of this step - # already recommended doing if the two ever diverged. + # Two different facts, split into two conrefs in 2026-09-22 when kmcp + # 0.4.0 broke the tie that had made them look like one (kagent alpha1 + # and alpha2 both pin 0.3.0). kmcp.md is what kagent bundles, for the + # 1.x support matrix. kmcp-latest.md is kmcp's newest release, for the + # standalone kmcp guides, which render it inside sample CLI output + # reading "using latest: v..." -- a claim kagent's pin would falsify. KMCP_LATEST=$(gh release view \ --repo "${{ github.repository_owner }}/kmcp" \ --json tagName -q .tagName 2>/dev/null | sed 's/^v//' || true)