From 4c981dd8377d26c7101f3d2f68aaf0cf65875729 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Tue, 21 Apr 2026 17:44:14 +0300 Subject: [PATCH 01/16] Automate upstream release docs PRs via Renovate Adds a Renovate custom manager that watches version: pins in .github/upstream-projects.yaml for the four upstream ToolHive projects. When an upstream ships, Renovate opens a version-bump PR; a new workflow reacts to push source-verified content edits produced by the upstream-release-docs skill (three passes with docs-review), assigns reviewers from non-bot release contributors, and augments the PR body in a marker-delimited section. - .github/upstream-projects.yaml: source of truth for tracked projects - renovate.json: customManagers + packageRule (ignoreUnstable, rebaseWhen: never, recreateWhen: never, labels) - .github/workflows/upstream-release-docs.yml: pull_request + dispatch - scripts/upstream-release/detect-change.mjs: finds the changed project and asserts repo: hasn't been tampered with - scripts/upstream-release/apply-pin-files.mjs: rewrites @latest pins - docusaurus.config.ts: pins Registry Server Swagger at v1.2.1 Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/upstream-projects.yaml | 39 ++ .github/workflows/upstream-release-docs.yml | 409 +++++++++++++++++++ .gitignore | 5 + docusaurus.config.ts | 2 +- renovate.json | 24 ++ scripts/upstream-release/apply-pin-files.mjs | 79 ++++ scripts/upstream-release/detect-change.mjs | 103 +++++ 7 files changed, 660 insertions(+), 1 deletion(-) create mode 100644 .github/upstream-projects.yaml create mode 100644 .github/workflows/upstream-release-docs.yml create mode 100644 scripts/upstream-release/apply-pin-files.mjs create mode 100644 scripts/upstream-release/detect-change.mjs diff --git a/.github/upstream-projects.yaml b/.github/upstream-projects.yaml new file mode 100644 index 00000000..aee95379 --- /dev/null +++ b/.github/upstream-projects.yaml @@ -0,0 +1,39 @@ +# Human-maintained source-of-truth for upstream projects whose releases +# trigger doc-update PRs via .github/workflows/upstream-release-docs.yml. +# +# Bump `version:` to the last-documented tag. The workflow advances it +# automatically when a release lands and docs are updated, but you may +# also edit it by hand (e.g., to backfill or reset). +# +# The bot-maintained scan timestamps live separately in +# .github/upstream-scan-state.json so diffs to this file stay meaningful. + +projects: + - id: toolhive-registry-server + repo: stacklok/toolhive-registry-server + version: v1.2.1 + docs_paths: + - docs/toolhive/guides-registry + - docs/toolhive/concepts/registry-criteria.mdx + pin_files: + - path: docusaurus.config.ts + replace_latest: true + + - id: toolhive + repo: stacklok/toolhive + version: v0.22.0 + docs_paths: + - docs/toolhive/guides-cli + - docs/toolhive/guides-k8s + - docs/toolhive/guides-vmcp + + - id: toolhive-studio + repo: stacklok/toolhive-studio + version: v0.30.0 + docs_paths: + - docs/toolhive/guides-ui + + - id: toolhive-cloud-ui + repo: stacklok/toolhive-cloud-ui + version: v0.5.1 + docs_paths: [] diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml new file mode 100644 index 00000000..d8dd58aa --- /dev/null +++ b/.github/workflows/upstream-release-docs.yml @@ -0,0 +1,409 @@ +name: Upstream Release Docs + +# Reacts to Renovate-authored PRs that bump a `version:` in +# .github/upstream-projects.yaml. Runs the upstream-release-docs skill +# in multi-pass non-interactive mode to produce source-verified content +# edits, pushes them to the same PR branch, augments the PR body, and +# requests review from non-bot release contributors. +# +# Renovate is configured (renovate.json customManagers + packageRules) +# not to rebase these PRs, so we can push commits without force-push +# races. `rebaseWhen: never` + `recreateWhen: never` own that contract. +# +# Reference docs for stacklok/toolhive are handled separately by +# update-toolhive-reference.yml and are explicitly out of scope. + +on: + pull_request: + # `labeled` is included to close the race where Renovate's + # labels aren't yet on the PR at the `opened` event payload. + types: [opened, reopened, labeled] + paths: + - '.github/upstream-projects.yaml' + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to (re-)augment. Must be an open Renovate PR touching .github/upstream-projects.yaml.' + required: true + type: string + +permissions: + contents: write + pull-requests: write + +concurrency: + # Workflow-level group (not per-PR) so two simultaneous upstream + # releases don't run the skill in parallel on shared concept pages. + group: upstream-release-docs + cancel-in-progress: false + +jobs: + augment: + runs-on: ubuntu-latest + timeout-minutes: 90 + # Gate: triggered either by a Renovate-authored PR carrying the + # upstream-content label, OR by a manual workflow_dispatch retry. + # The per-event branches below resolve PR metadata from the right + # source. + if: | + github.event_name == 'workflow_dispatch' || + ( + github.event.pull_request.user.login == 'renovate[bot]' && + contains(github.event.pull_request.labels.*.name, 'upstream-content') + ) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Resolve PR number and head ref + id: pr + env: + EVENT: ${{ github.event_name }} + DISPATCH_PR: ${{ github.event.inputs.pr_number }} + EVENT_PR: ${{ github.event.pull_request.number }} + EVENT_HEAD_REF: ${{ github.event.pull_request.head.ref }} + run: | + if [ "$EVENT" = "workflow_dispatch" ]; then + PR_NUMBER="$DISPATCH_PR" + HEAD_REF=$(gh pr view "$PR_NUMBER" --json headRefName --jq .headRefName) + # Validate: must be a Renovate PR with the upstream-content label. + AUTHOR=$(gh pr view "$PR_NUMBER" --json author --jq '.author.login') + LABELS=$(gh pr view "$PR_NUMBER" --json labels --jq '[.labels[].name] | join(",")') + if [ "$AUTHOR" != "app/renovate" ] && [ "$AUTHOR" != "renovate[bot]" ]; then + echo "::error::PR #$PR_NUMBER is not authored by Renovate (author=$AUTHOR)." + exit 1 + fi + if ! echo "$LABELS" | grep -q 'upstream-content'; then + echo "::error::PR #$PR_NUMBER does not carry the upstream-content label (labels=$LABELS)." + exit 1 + fi + else + PR_NUMBER="$EVENT_PR" + HEAD_REF="$EVENT_HEAD_REF" + fi + echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + echo "head_ref=$HEAD_REF" >> "$GITHUB_OUTPUT" + + - name: Checkout PR branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + ref: ${{ steps.pr.outputs.head_ref }} + fetch-depth: 0 + + - name: Setup + uses: ./.github/actions/setup + + - name: Set up Git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Detect changed project + id: detect + run: node scripts/upstream-release/detect-change.mjs + + - name: Verify prev_tag exists upstream + env: + REPO: ${{ steps.detect.outputs.repo }} + PREV_TAG: ${{ steps.detect.outputs.prev_tag }} + run: | + # Sanity check: if the pinned prev_tag doesn't exist upstream + # (e.g., a fake v0.0.0 seed, a retagged release, a deleted tag), + # fail early rather than let the skill produce a confusing diff. + if ! gh api "repos/$REPO/git/refs/tags/$PREV_TAG" --silent 2>/dev/null; then + echo "::error::prev_tag $PREV_TAG does not exist in $REPO. The pinned version in .github/upstream-projects.yaml may be wrong or the upstream tag was deleted. Fix the pinned version and re-run." + exit 1 + fi + + - name: Shallow-clone upstream at new tag + id: clone + env: + REPO: ${{ steps.detect.outputs.repo }} + NEW_TAG: ${{ steps.detect.outputs.new_tag }} + run: | + SCRATCH=$(mktemp -d) + git clone --depth 1 --branch "$NEW_TAG" \ + "https://github.com/${REPO}.git" \ + "$SCRATCH/upstream" + echo "scratch_dir=$SCRATCH/upstream" >> "$GITHUB_OUTPUT" + + - name: Extract reviewers from release compare + id: reviewers + env: + REPO: ${{ steps.detect.outputs.repo }} + PREV: ${{ steps.detect.outputs.prev_tag }} + NEW: ${{ steps.detect.outputs.new_tag }} + run: | + # Capture stderr separately so we can surface a missing-compare + # situation in the PR body rather than silently dropping reviewers. + if COMPARE=$(gh api "repos/$REPO/compare/$PREV...$NEW" \ + --jq '[.commits[].author.login] | unique | .[]' 2>/dev/null); then + REVIEWERS=$(echo "$COMPARE" | + grep -Ev '(\[bot\]$|^github-actions|^stacklokbot$|^dependabot|^renovate|^copilot)' | + head -5 | paste -sd, -) + echo "compare_ok=true" >> "$GITHUB_OUTPUT" + else + REVIEWERS="" + echo "compare_ok=false" >> "$GITHUB_OUTPUT" + fi + echo "list=$REVIEWERS" >> "$GITHUB_OUTPUT" + echo "Reviewers: ${REVIEWERS:-}" + + - name: Read docs_paths hint + id: hints + env: + PROJECT_ID: ${{ steps.detect.outputs.id }} + run: | + HINTS=$(node -e " + const yaml = require('js-yaml'); + const fs = require('fs'); + const p = yaml.load(fs.readFileSync('.github/upstream-projects.yaml','utf8')).projects.find(x=>x.id===process.env.PROJECT_ID); + console.log(JSON.stringify(p?.docs_paths ?? [])); + ") + echo "docs_paths=$HINTS" >> "$GITHUB_OUTPUT" + + - name: Run upstream-release-docs skill (multi-pass) + id: skill + uses: anthropics/claude-code-action@38ec876110f9fbf8b950c79f534430740c3ac009 # v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + additional_permissions: | + actions: read + prompt: | + You are running in GitHub Actions with no interactive user. Follow + these steps exactly and do NOT ask clarifying questions -- proceed + best-effort at every decision point. + + PROJECT: ${{ steps.detect.outputs.id }} + REPO: ${{ steps.detect.outputs.repo }} + PREV_TAG: ${{ steps.detect.outputs.prev_tag }} + NEW_TAG: ${{ steps.detect.outputs.new_tag }} + CLONE: ${{ steps.clone.outputs.scratch_dir }} + DOCS_HINTS: ${{ steps.hints.outputs.docs_paths }} + + PASS 1 -- Initial content update: + Run /upstream-release-docs ${{ steps.detect.outputs.repo }} ${{ steps.detect.outputs.new_tag }} + Execute all 6 phases. Prefer reading source code from the + local clone at ${{ steps.clone.outputs.scratch_dir }} + instead of `gh api contents?ref=` -- it's already at + the tag and doesn't consume API quota. + For Phase 2 step 4 (context on major new features), SKIP + writing the "why"/consumer narrative and append one bullet + per gap to GAPS.md at repo root (create if missing). Each + bullet must name the feature and describe what context a + human needs to supply. + Follow the skill's own guidance on auto-generated reference + files (Phase 4 step 5, Phase 4 step 6) -- do not hand-edit + docs/toolhive/reference/cli/, static/api-specs/, or + docs/toolhive/reference/crds/. If a release genuinely needs + reference updates, note that in GAPS.md -- the separate + update-toolhive-reference.yml workflow handles those. + + PASS 2 -- Editorial re-review: + Run /docs-review over every file you changed in Pass 1 and + apply every actionable fix. Do NOT re-run upstream-release-docs; + you already have the source verification context in your + history. If docs-review surfaces a factual concern, re-verify + against source code at the tag before changing. + + PASS 3 -- Technical re-verification: + Re-run Phase 5 step 1 of /upstream-release-docs: re-verify + every factual claim in the changed files against source code + at the release tag. Fix any drift found. If no changes are + needed, say so explicitly. + Finally, re-run /docs-review one more time and apply any + remaining fixes. + + If at any point you conclude there are no doc-relevant changes + for this release (Phase 3 impact map is empty), stop and write + NO_CHANGES.md at repo root with a one-line explanation. Still + do not hand-edit any file. + + - name: Capture skill signal files + id: signals + run: | + GAPS_BODY="" + NO_CHANGES_BODY="" + if [ -f GAPS.md ]; then + GAPS_BODY=$(cat GAPS.md) + rm GAPS.md + fi + if [ -f NO_CHANGES.md ]; then + NO_CHANGES_BODY=$(cat NO_CHANGES.md) + rm NO_CHANGES.md + fi + + # Build full markdown fragments here so the PR body edit + # step can treat them as plain strings. + { + echo "note_block< [!NOTE]" + echo "> The skill reported no doc-relevant changes for this" + echo "> release. This PR only bumps the version reference" + echo "> and any pin_files substitutions." + echo ">" + echo "> $NO_CHANGES_BODY" + fi + echo "NOTE_EOF" + + echo "gaps_block<> "$GITHUB_OUTPUT" + + - name: Apply pin_files substitutions + env: + PROJECT_ID: ${{ steps.detect.outputs.id }} + NEW_TAG: ${{ steps.detect.outputs.new_tag }} + run: | + node scripts/upstream-release/apply-pin-files.mjs \ + --id "$PROJECT_ID" \ + --tag "$NEW_TAG" + + - name: Detect touches to auto-generated paths + id: autogen + run: | + git add -A + TOUCHED=$(git diff --cached --name-only -- \ + 'docs/toolhive/reference/cli/' \ + 'static/api-specs/' \ + 'docs/toolhive/reference/crds/' | paste -sd, - || true) + { + echo "note< [!WARNING]" + echo "> The skill touched files under auto-generated paths:" + echo "> \`$TOUCHED\`" + echo ">" + echo "> These are normally maintained by update-toolhive-reference.yml." + echo "> Review the changes carefully and revert if they should come" + echo "> from the reference workflow instead." + fi + echo "AUTOGEN_EOF" + } >> "$GITHUB_OUTPUT" + + - name: Commit and push content + id: push + env: + PROJECT_ID: ${{ steps.detect.outputs.id }} + NEW_TAG: ${{ steps.detect.outputs.new_tag }} + HEAD_REF: ${{ steps.pr.outputs.head_ref }} + run: | + git add -A + if git diff --cached --quiet; then + echo "No content changes to push." + echo "pushed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + git commit -m "Add upstream-release-docs content for $PROJECT_ID $NEW_TAG" + git push origin "HEAD:$HEAD_REF" + echo "pushed=true" >> "$GITHUB_OUTPUT" + + - name: Augment PR body (marker-delimited section) + # Runs even if earlier steps soft-failed so the augmentation + # survives partial failures; a subsequent workflow_dispatch + # retry will re-enter here. + if: always() && steps.detect.outputs.id != '' + env: + PR_NUMBER: ${{ steps.pr.outputs.number }} + PROJECT_ID: ${{ steps.detect.outputs.id }} + NEW_TAG: ${{ steps.detect.outputs.new_tag }} + PREV_TAG: ${{ steps.detect.outputs.prev_tag }} + REPO: ${{ steps.detect.outputs.repo }} + NOTE_BLOCK: ${{ steps.signals.outputs.note_block }} + GAPS_BLOCK: ${{ steps.signals.outputs.gaps_block }} + AUTOGEN_NOTE: ${{ steps.autogen.outputs.note }} + COMPARE_OK: ${{ steps.reviewers.outputs.compare_ok }} + run: | + START='' + END='' + + # Build our section. + { + echo "$START" + echo "" + echo "## Content additions by upstream-release-docs" + echo "" + echo "Source-verified against \`$REPO\` at tag \`$NEW_TAG\` (was \`$PREV_TAG\`). The \`upstream-release-docs\` and \`docs-review\` skills each ran twice (three total passes) before this update." + echo "" + if [ "$COMPARE_OK" != "true" ]; then + echo "> [!WARNING]" + echo "> Could not compare \`$PREV_TAG\` against \`$NEW_TAG\` upstream, so no reviewers were auto-assigned from release contributors. The pinned previous tag may have been retagged or deleted." + echo "" + fi + if [ -n "$NOTE_BLOCK" ]; then + echo "$NOTE_BLOCK" + echo "" + fi + if [ -n "$AUTOGEN_NOTE" ]; then + echo "$AUTOGEN_NOTE" + echo "" + fi + echo "### What's NOT in this PR" + echo "" + echo "Auto-generated reference files (\`docs/toolhive/reference/cli/\`, \`static/api-specs/\`, \`docs/toolhive/reference/crds/\`). Those are handled by \`update-toolhive-reference.yml\`. If this release has reference impact, that workflow opens a separate PR; merge it first, then rebase this one." + echo "" + echo "### Review guidance" + echo "" + echo "Unlike the reference-docs PR, this one contains hand-edited prose. Review for accuracy, not just style. If the \"Gaps needing human context\" section is populated, the skill deferred those sections to a human; fill them in before merging." + echo "" + if [ -n "$GAPS_BLOCK" ]; then + echo "$GAPS_BLOCK" + echo "" + fi + echo "Reviewers below are non-bot commit authors in the release range." + echo "" + echo "$END" + } > /tmp/section.md + + # Read existing body, replace or append our marked section. + EXISTING=$(gh pr view "$PR_NUMBER" --json body --jq .body) + if echo "$EXISTING" | grep -qF "$START"; then + # Replace existing section. + printf '%s\n' "$EXISTING" | awk -v start="$START" -v end="$END" -v repl_file=/tmp/section.md ' + BEGIN { in_section = 0 } + $0 == start { in_section = 1; while ((getline line < repl_file) > 0) print line; next } + $0 == end { if (in_section) { in_section = 0; next } } + !in_section { print } + ' > /tmp/pr-body.md + else + # Append. + { + printf '%s\n\n---\n\n' "$EXISTING" + cat /tmp/section.md + } > /tmp/pr-body.md + fi + + gh pr edit "$PR_NUMBER" --body-file /tmp/pr-body.md + + - name: Add reviewers + if: always() && steps.reviewers.outputs.list != '' + env: + PR_NUMBER: ${{ steps.pr.outputs.number }} + REVIEWERS: ${{ steps.reviewers.outputs.list }} + run: gh pr edit "$PR_NUMBER" --add-reviewer "$REVIEWERS" + + - name: Add needs-human-context label + if: always() && steps.signals.outputs.has_gaps == 'true' + env: + PR_NUMBER: ${{ steps.pr.outputs.number }} + run: gh pr edit "$PR_NUMBER" --add-label needs-human-context + + - name: Flag augmentation failure + # Runs only when a preceding step failed. Adds a label the + # operator can filter on and comments a retry pointer. + if: failure() + env: + PR_NUMBER: ${{ steps.pr.outputs.number }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + gh pr edit "$PR_NUMBER" --add-label upstream-docs-failed || true + gh pr comment "$PR_NUMBER" --body "Automated docs augmentation failed. Run: $RUN_URL + + Retry via the \`Upstream Release Docs\` workflow with \`pr_number=$PR_NUMBER\` once the underlying issue is resolved." || true diff --git a/.gitignore b/.gitignore index 62208381..92fc1040 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,8 @@ yarn-error.log* .vercel .idea .claude/settings.local.json + +# Signal files written by upstream-release-docs.yml for the skill to +# communicate gaps or no-op releases back to the workflow. Never committed. +/GAPS.md +/NO_CHANGES.md diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 89136d28..95fbde65 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -214,7 +214,7 @@ const config: Config = { }, { id: 'toolhive-registry-api', - spec: 'https://cdn.jsdelivr.net/gh/stacklok/toolhive-registry-server@latest/docs/thv-registry-api/swagger.yaml', + spec: 'https://cdn.jsdelivr.net/gh/stacklok/toolhive-registry-server@v1.2.1/docs/thv-registry-api/swagger.yaml', config: path.join(__dirname, 'src/redocly/redocly-toolhive.yaml'), }, ], diff --git a/renovate.json b/renovate.json index 3ba57eb1..576a3bb2 100644 --- a/renovate.json +++ b/renovate.json @@ -32,6 +32,15 @@ ], "datasourceTemplate": "docker", "versioningTemplate": "docker" + }, + { + "customType": "regex", + "description": "Track upstream ToolHive project versions for docs updates. PRs this opens are augmented by .github/workflows/upstream-release-docs.yml.", + "managerFilePatterns": ["/^\\.github/upstream-projects\\.yaml$/"], + "matchStrings": [ + "repo:\\s*(?[\\w.\\-/]+)\\s+version:\\s*(?v[\\w.+\\-]+)" + ], + "datasourceTemplate": "github-releases" } ], "packageRules": [ @@ -74,6 +83,21 @@ "internalChecksFilter": "strict", "prCreation": "not-pending", "labels": ["documentation", "mcp-guides"] + }, + { + "matchManagers": ["custom.regex"], + "matchFileNames": ["**/.github/upstream-projects.yaml"], + "schedule": ["at any time"], + "minimumReleaseAge": "24 hours", + "minimumReleaseAgeBehaviour": "timestamp-optional", + "ignoreUnstable": true, + "labels": ["autogen-docs", "upstream-content"], + "rebaseWhen": "never", + "recreateWhen": "never", + "commitMessageTopic": "{{depName}}", + "prBodyNotes": [ + "After this PR opens, `.github/workflows/upstream-release-docs.yml` adds source-verified content edits for the new release." + ] } ] } diff --git a/scripts/upstream-release/apply-pin-files.mjs b/scripts/upstream-release/apply-pin-files.mjs new file mode 100644 index 00000000..0f2218ed --- /dev/null +++ b/scripts/upstream-release/apply-pin-files.mjs @@ -0,0 +1,79 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Applies pin_files substitutions declared for a project in +// .github/upstream-projects.yaml. Called by the upstream-release-docs +// workflow after Renovate has already bumped the `version:` field. +// +// Usage: +// node apply-pin-files.mjs --id --tag +// +// pin_files entries supported: +// { path: '', replace_latest: true } +// Flips `@latest` (or `@vX.Y.Z`) to `@` +// so unrelated `@latest` strings elsewhere in the file are safe. + +import fs from 'node:fs'; +import yaml from 'js-yaml'; + +const PROJECTS_FILE = '.github/upstream-projects.yaml'; + +function parseArgs(argv) { + const args = {}; + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--id') { + args.id = argv[i + 1]; + i++; + } else if (argv[i] === '--tag') { + args.tag = argv[i + 1]; + i++; + } + } + return args; +} + +function applyPinFiles(project, newTag) { + for (const pin of project.pin_files ?? []) { + const filePath = pin.path; + if (!fs.existsSync(filePath)) { + console.error(`pin_files: skipping missing file ${filePath}`); + continue; + } + let content = fs.readFileSync(filePath, 'utf8'); + let updated = content; + if (pin.replace_latest) { + const repoEscaped = project.repo.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const pattern = new RegExp( + `(${repoEscaped})@(?:latest|v[\\w.\\-]+)`, + 'g' + ); + updated = updated.replace(pattern, `$1@${newTag}`); + } + if (updated !== content) { + fs.writeFileSync(filePath, updated); + console.log(`pin_files: updated ${filePath}`); + } + } +} + +function main() { + const { id, tag } = parseArgs(process.argv.slice(2)); + if (!id || !tag) { + console.error( + 'Usage: apply-pin-files.mjs --id --tag ' + ); + process.exit(1); + } + + const parsed = yaml.load(fs.readFileSync(PROJECTS_FILE, 'utf8')); + const project = parsed.projects.find((p) => p.id === id); + if (!project) { + console.error(`Unknown project id: ${id}`); + process.exit(1); + } + + applyPinFiles(project, tag); +} + +main(); diff --git a/scripts/upstream-release/detect-change.mjs b/scripts/upstream-release/detect-change.mjs new file mode 100644 index 00000000..ec92f056 --- /dev/null +++ b/scripts/upstream-release/detect-change.mjs @@ -0,0 +1,103 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Detects which project's version: changed between the PR branch and +// origin/main in .github/upstream-projects.yaml. Emits GITHUB_OUTPUT: +// +// id= +// repo= +// prev_tag= +// new_tag= +// +// Fails if: +// - zero projects changed +// - more than one project changed (Renovate is configured not to batch, +// but we fail loudly if that ever slips) +// - the `repo:` field for the changed project was also modified (a PR +// that edits both `repo:` and `version:` could point the workflow at +// a hostile clone URL; only Renovate version bumps should reach this) +// +// Set BASE_REF to override origin/main for local testing. + +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import yaml from 'js-yaml'; + +const PROJECTS_FILE = '.github/upstream-projects.yaml'; +const REPO_SHAPE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/; + +function emit(key, value) { + const out = process.env.GITHUB_OUTPUT; + const line = `${key}=${value}\n`; + if (out) { + fs.appendFileSync(out, line); + } else { + process.stdout.write(line); + } +} + +function loadFromRef(ref) { + const text = execFileSync('git', ['show', `${ref}:${PROJECTS_FILE}`], { + encoding: 'utf8', + }); + return yaml.load(text).projects; +} + +function main() { + const ref = process.env.BASE_REF || 'origin/main'; + const mainProjects = loadFromRef(ref); + const headProjects = yaml.load( + fs.readFileSync(PROJECTS_FILE, 'utf8') + ).projects; + + const changed = []; + for (const pr of headProjects) { + const base = mainProjects.find((m) => m.id === pr.id); + if (!base) continue; // new project added in this PR; not a version bump + if (base.version !== pr.version) { + if (base.repo !== pr.repo) { + console.error( + `Project ${pr.id} changed repo: ${base.repo} -> ${pr.repo}. ` + + `This workflow only handles version bumps; repo changes must be ` + + `reviewed and merged by a human before version updates proceed.` + ); + process.exit(1); + } + if (!REPO_SHAPE.test(pr.repo)) { + console.error(`Project ${pr.id} has malformed repo value: ${pr.repo}`); + process.exit(1); + } + changed.push({ + id: pr.id, + repo: pr.repo, + prev_tag: base.version, + new_tag: pr.version, + }); + } + } + + if (changed.length === 0) { + console.error( + 'No version changes detected in .github/upstream-projects.yaml' + ); + process.exit(1); + } + if (changed.length > 1) { + console.error( + `Multiple projects changed in one PR: ${changed + .map((c) => c.id) + .join(', ')}. Split the PR or dispatch the workflow per project.` + ); + process.exit(1); + } + + const c = changed[0]; + emit('id', c.id); + emit('repo', c.repo); + emit('prev_tag', c.prev_tag); + emit('new_tag', c.new_tag); + console.log(`Detected: ${c.id} ${c.prev_tag} -> ${c.new_tag}`); +} + +main(); From 6c77d204bbaf618de53050a7d4260e618566a284 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Tue, 21 Apr 2026 18:12:39 +0300 Subject: [PATCH 02/16] Add manual bootstrap mode to upstream-release-docs workflow workflow_dispatch now accepts either `pr_number` (retry an existing Renovate PR) or `project_id` + `new_tag` (bootstrap a fresh PR without waiting for Renovate). The bootstrap path branches off main, bumps the YAML via scripts/upstream-release/bump-yaml.mjs, creates the PR with the same labels Renovate applies, and feeds its number into the rest of the workflow so content augmentation runs identically. Useful for debugging, manual updates, and validating the pipeline end-to-end right after merge without waiting for an upstream release. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/upstream-release-docs.yml | 175 +++++++++++++++++--- scripts/upstream-release/bump-yaml.mjs | 90 ++++++++++ 2 files changed, 239 insertions(+), 26 deletions(-) create mode 100644 scripts/upstream-release/bump-yaml.mjs diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index d8dd58aa..77bc04c7 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -23,8 +23,16 @@ on: workflow_dispatch: inputs: pr_number: - description: 'PR number to (re-)augment. Must be an open Renovate PR touching .github/upstream-projects.yaml.' - required: true + description: 'Retry-only: PR number to re-augment (must be an open Renovate PR). Leave blank to bootstrap a new PR via project_id + new_tag.' + required: false + type: string + project_id: + description: 'Bootstrap a new PR: id from .github/upstream-projects.yaml (e.g. toolhive-registry-server). Requires new_tag.' + required: false + type: string + new_tag: + description: 'Bootstrap a new PR: the upstream tag to document (e.g. v1.3.0). Requires project_id.' + required: false type: string permissions: @@ -54,39 +62,154 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: + - name: Validate workflow_dispatch inputs + if: github.event_name == 'workflow_dispatch' + env: + PR_NUMBER_IN: ${{ github.event.inputs.pr_number }} + PROJECT_ID_IN: ${{ github.event.inputs.project_id }} + NEW_TAG_IN: ${{ github.event.inputs.new_tag }} + run: | + # Exactly one of: (pr_number) OR (project_id + new_tag). + if [ -n "$PR_NUMBER_IN" ] && { [ -n "$PROJECT_ID_IN" ] || [ -n "$NEW_TAG_IN" ]; }; then + echo "::error::Provide either pr_number (retry) or project_id + new_tag (bootstrap), not both." + exit 1 + fi + if [ -z "$PR_NUMBER_IN" ] && { [ -z "$PROJECT_ID_IN" ] || [ -z "$NEW_TAG_IN" ]; }; then + echo "::error::Bootstrap mode requires both project_id and new_tag." + exit 1 + fi + - name: Resolve PR number and head ref id: pr env: EVENT: ${{ github.event_name }} DISPATCH_PR: ${{ github.event.inputs.pr_number }} + PROJECT_ID_IN: ${{ github.event.inputs.project_id }} + NEW_TAG_IN: ${{ github.event.inputs.new_tag }} EVENT_PR: ${{ github.event.pull_request.number }} EVENT_HEAD_REF: ${{ github.event.pull_request.head.ref }} run: | - if [ "$EVENT" = "workflow_dispatch" ]; then - PR_NUMBER="$DISPATCH_PR" - HEAD_REF=$(gh pr view "$PR_NUMBER" --json headRefName --jq .headRefName) - # Validate: must be a Renovate PR with the upstream-content label. - AUTHOR=$(gh pr view "$PR_NUMBER" --json author --jq '.author.login') - LABELS=$(gh pr view "$PR_NUMBER" --json labels --jq '[.labels[].name] | join(",")') - if [ "$AUTHOR" != "app/renovate" ] && [ "$AUTHOR" != "renovate[bot]" ]; then - echo "::error::PR #$PR_NUMBER is not authored by Renovate (author=$AUTHOR)." - exit 1 - fi - if ! echo "$LABELS" | grep -q 'upstream-content'; then - echo "::error::PR #$PR_NUMBER does not carry the upstream-content label (labels=$LABELS)." - exit 1 - fi - else - PR_NUMBER="$EVENT_PR" - HEAD_REF="$EVENT_HEAD_REF" + case "$EVENT" in + pull_request) + PR_NUMBER="$EVENT_PR" + HEAD_REF="$EVENT_HEAD_REF" + MODE="react" + ;; + workflow_dispatch) + if [ -n "$DISPATCH_PR" ]; then + PR_NUMBER="$DISPATCH_PR" + HEAD_REF=$(gh pr view "$PR_NUMBER" --json headRefName --jq .headRefName) + AUTHOR=$(gh pr view "$PR_NUMBER" --json author --jq '.author.login') + LABELS=$(gh pr view "$PR_NUMBER" --json labels --jq '[.labels[].name] | join(",")') + if [ "$AUTHOR" != "app/renovate" ] && [ "$AUTHOR" != "renovate[bot]" ]; then + echo "::error::PR #$PR_NUMBER is not authored by Renovate (author=$AUTHOR). Retry mode is for Renovate PRs only; use bootstrap (project_id + new_tag) to create a new manual PR." + exit 1 + fi + if ! echo "$LABELS" | grep -q 'upstream-content'; then + echo "::error::PR #$PR_NUMBER does not carry the upstream-content label (labels=$LABELS)." + exit 1 + fi + MODE="retry" + else + # Bootstrap: branch and PR are created in the next step. + PR_NUMBER="" + HEAD_REF="" + MODE="bootstrap" + fi + ;; + esac + { + echo "number=$PR_NUMBER" + echo "head_ref=$HEAD_REF" + echo "mode=$MODE" + } >> "$GITHUB_OUTPUT" + echo "Mode: $MODE" + + # Bootstrap: a human manually dispatched with project_id + new_tag. + # Check out main, bump the YAML, create the PR, and emit its number + # + branch so the rest of the workflow proceeds as if Renovate had + # opened it. + - name: Checkout main for bootstrap + if: steps.pr.outputs.mode == 'bootstrap' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + ref: main + fetch-depth: 0 + + - name: Setup (bootstrap) + if: steps.pr.outputs.mode == 'bootstrap' + uses: ./.github/actions/setup + + - name: Set up Git (bootstrap) + if: steps.pr.outputs.mode == 'bootstrap' + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Bootstrap branch and PR + id: bootstrap + if: steps.pr.outputs.mode == 'bootstrap' + env: + PROJECT_ID: ${{ github.event.inputs.project_id }} + NEW_TAG: ${{ github.event.inputs.new_tag }} + ACTOR: ${{ github.actor }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + # Branch name intentionally distinct from Renovate's + # docs/upstream- so the two paths don't collide. + BRANCH="manual/upstream-${PROJECT_ID}-${NEW_TAG}" + if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then + echo "::error::Branch $BRANCH already exists on origin. Delete it or use retry mode with the existing PR's number." + exit 1 fi + git checkout -b "$BRANCH" + + node scripts/upstream-release/bump-yaml.mjs \ + --id "$PROJECT_ID" \ + --tag "$NEW_TAG" + + git add .github/upstream-projects.yaml + git commit -m "Bump $PROJECT_ID to $NEW_TAG" + git push origin "$BRANCH" + + # Open the PR with the same labels Renovate would apply, so + # downstream steps that read labels behave identically. + gh pr create \ + --base main \ + --head "$BRANCH" \ + --title "Update $PROJECT_ID to $NEW_TAG (manual dispatch)" \ + --body "Manually dispatched by @$ACTOR via workflow run $RUN_URL. + + This PR was created by the \`Upstream Release Docs\` workflow's bootstrap mode to document a release without waiting for Renovate. Content edits will be pushed as additional commits by the same workflow run." \ + --label autogen-docs \ + --label upstream-content + + PR_NUMBER=$(gh pr view "$BRANCH" --json number --jq .number) echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT" - echo "head_ref=$HEAD_REF" >> "$GITHUB_OUTPUT" + echo "head_ref=$BRANCH" >> "$GITHUB_OUTPUT" + + # Normalize the (possibly bootstrap-created) PR number + head_ref + # into a single step output so later steps reference one place. + - name: Resolve effective PR number + id: eff + env: + PR_FROM_RESOLVE: ${{ steps.pr.outputs.number }} + HEAD_FROM_RESOLVE: ${{ steps.pr.outputs.head_ref }} + PR_FROM_BOOTSTRAP: ${{ steps.bootstrap.outputs.number }} + HEAD_FROM_BOOTSTRAP: ${{ steps.bootstrap.outputs.head_ref }} + run: | + if [ -n "$PR_FROM_BOOTSTRAP" ]; then + echo "number=$PR_FROM_BOOTSTRAP" >> "$GITHUB_OUTPUT" + echo "head_ref=$HEAD_FROM_BOOTSTRAP" >> "$GITHUB_OUTPUT" + else + echo "number=$PR_FROM_RESOLVE" >> "$GITHUB_OUTPUT" + echo "head_ref=$HEAD_FROM_RESOLVE" >> "$GITHUB_OUTPUT" + fi - name: Checkout PR branch uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - ref: ${{ steps.pr.outputs.head_ref }} + ref: ${{ steps.eff.outputs.head_ref }} fetch-depth: 0 - name: Setup @@ -293,7 +416,7 @@ jobs: env: PROJECT_ID: ${{ steps.detect.outputs.id }} NEW_TAG: ${{ steps.detect.outputs.new_tag }} - HEAD_REF: ${{ steps.pr.outputs.head_ref }} + HEAD_REF: ${{ steps.eff.outputs.head_ref }} run: | git add -A if git diff --cached --quiet; then @@ -311,7 +434,7 @@ jobs: # retry will re-enter here. if: always() && steps.detect.outputs.id != '' env: - PR_NUMBER: ${{ steps.pr.outputs.number }} + PR_NUMBER: ${{ steps.eff.outputs.number }} PROJECT_ID: ${{ steps.detect.outputs.id }} NEW_TAG: ${{ steps.detect.outputs.new_tag }} PREV_TAG: ${{ steps.detect.outputs.prev_tag }} @@ -385,14 +508,14 @@ jobs: - name: Add reviewers if: always() && steps.reviewers.outputs.list != '' env: - PR_NUMBER: ${{ steps.pr.outputs.number }} + PR_NUMBER: ${{ steps.eff.outputs.number }} REVIEWERS: ${{ steps.reviewers.outputs.list }} run: gh pr edit "$PR_NUMBER" --add-reviewer "$REVIEWERS" - name: Add needs-human-context label if: always() && steps.signals.outputs.has_gaps == 'true' env: - PR_NUMBER: ${{ steps.pr.outputs.number }} + PR_NUMBER: ${{ steps.eff.outputs.number }} run: gh pr edit "$PR_NUMBER" --add-label needs-human-context - name: Flag augmentation failure @@ -400,7 +523,7 @@ jobs: # operator can filter on and comments a retry pointer. if: failure() env: - PR_NUMBER: ${{ steps.pr.outputs.number }} + PR_NUMBER: ${{ steps.eff.outputs.number }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | gh pr edit "$PR_NUMBER" --add-label upstream-docs-failed || true diff --git a/scripts/upstream-release/bump-yaml.mjs b/scripts/upstream-release/bump-yaml.mjs new file mode 100644 index 00000000..5eecd931 --- /dev/null +++ b/scripts/upstream-release/bump-yaml.mjs @@ -0,0 +1,90 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Bumps the `version:` field for the given project id in +// .github/upstream-projects.yaml. Used by the manual-dispatch +// bootstrap path where a human kicks off the workflow with an +// explicit project_id + new_tag instead of waiting for Renovate. +// +// The auto (Renovate-driven) path does NOT use this script — +// Renovate already bumps the version itself. +// +// Usage: +// node bump-yaml.mjs --id --tag +// +// Fails if the project is not in the YAML or the tag already +// matches (no-op — caller should not open a PR). + +import fs from 'node:fs'; +import yaml from 'js-yaml'; + +const PROJECTS_FILE = '.github/upstream-projects.yaml'; + +function parseArgs(argv) { + const args = {}; + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--id') { + args.id = argv[i + 1]; + i++; + } else if (argv[i] === '--tag') { + args.tag = argv[i + 1]; + i++; + } + } + return args; +} + +// Preserve comments and formatting by editing the raw text. +function bumpText(text, id, newTag) { + const lines = text.split('\n'); + let inProject = false; + let changed = false; + let previous = null; + for (let i = 0; i < lines.length; i++) { + const idMatch = lines[i].match(/^\s*-\s*id:\s*(\S+)/); + if (idMatch) { + inProject = idMatch[1] === id; + continue; + } + if (inProject && /^\s*version:\s*/.test(lines[i])) { + const m = lines[i].match(/version:\s*(\S+)/); + previous = m ? m[1] : null; + lines[i] = lines[i].replace(/(version:\s*)\S+/, `$1${newTag}`); + changed = true; + break; + } + } + if (!changed) { + throw new Error(`Did not find version: line for project id ${id}`); + } + return { text: lines.join('\n'), previous }; +} + +function main() { + const { id, tag } = parseArgs(process.argv.slice(2)); + if (!id || !tag) { + console.error('Usage: bump-yaml.mjs --id --tag '); + process.exit(1); + } + + const raw = fs.readFileSync(PROJECTS_FILE, 'utf8'); + const parsed = yaml.load(raw); + if (!parsed.projects.find((p) => p.id === id)) { + console.error(`Unknown project id: ${id}`); + process.exit(1); + } + + const { text, previous } = bumpText(raw, id, tag); + if (previous === tag) { + console.error( + `Project ${id} is already pinned at ${tag}; nothing to bump.` + ); + process.exit(1); + } + + fs.writeFileSync(PROJECTS_FILE, text); + console.log(`Bumped ${id}: ${previous} -> ${tag}`); +} + +main(); From 13ac7c511b69d7b6daf6bdc85bd9270b7946181b Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Tue, 21 Apr 2026 18:33:51 +0300 Subject: [PATCH 03/16] Unify toolhive reference and content docs into a single PR update-toolhive-reference.yml now also bumps the toolhive entry in .github/upstream-projects.yaml, applies pin_files substitutions, and applies the upstream-content label alongside autogen-docs. The PR it opens is then picked up by upstream-release-docs.yml (via its relaxed gate that accepts github-actions[bot] authors) which adds skill-generated content in a second commit. Renovate is disabled for stacklok/toolhive so it doesn't race the reference workflow. The other three tracked projects keep their Renovate-driven path unchanged. Net: one PR per stacklok/toolhive release instead of two, with the same review surface shape as Renovate-driven PRs for the other projects. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/upstream-projects.yaml | 24 ++++-- .../workflows/update-toolhive-reference.yml | 73 ++++++++++++++++--- .github/workflows/upstream-release-docs.yml | 29 +++++--- renovate.json | 7 ++ 4 files changed, 109 insertions(+), 24 deletions(-) diff --git a/.github/upstream-projects.yaml b/.github/upstream-projects.yaml index aee95379..8eefea0e 100644 --- a/.github/upstream-projects.yaml +++ b/.github/upstream-projects.yaml @@ -1,12 +1,24 @@ # Human-maintained source-of-truth for upstream projects whose releases -# trigger doc-update PRs via .github/workflows/upstream-release-docs.yml. +# trigger doc-update PRs. # -# Bump `version:` to the last-documented tag. The workflow advances it -# automatically when a release lands and docs are updated, but you may -# also edit it by hand (e.g., to backfill or reset). +# Two automation paths read this file: # -# The bot-maintained scan timestamps live separately in -# .github/upstream-scan-state.json so diffs to this file stay meaningful. +# - stacklok/toolhive releases: .github/workflows/update-toolhive-reference.yml +# is triggered by repository_dispatch (from the ToolHive release CI) or +# workflow_dispatch. It regenerates reference assets, bumps this YAML, +# applies pin_files, and opens a single PR. Renovate is explicitly +# disabled for this dep (see renovate.json) to avoid racing. +# +# - All other projects (registry-server, studio, cloud-ui): +# Renovate watches `repo:` + `version:` pairs here as a custom-regex +# manager and opens version-bump PRs automatically. +# .github/workflows/upstream-release-docs.yml reacts to those PRs and +# adds source-verified content edits via the upstream-release-docs +# skill. +# +# Either way, Renovate or the reference workflow bumps `version:`. You can +# also edit it by hand (e.g., to backfill or reset) and dispatch the +# upstream-release-docs workflow manually. projects: - id: toolhive-registry-server diff --git a/.github/workflows/update-toolhive-reference.yml b/.github/workflows/update-toolhive-reference.yml index 567e1753..526e0770 100644 --- a/.github/workflows/update-toolhive-reference.yml +++ b/.github/workflows/update-toolhive-reference.yml @@ -1,5 +1,17 @@ name: Update ToolHive Reference Docs +# Handles stacklok/toolhive releases end-to-end. Regenerates the +# machine-generated reference assets (CLI help, Swagger, CRD schemas), +# bumps the toolhive entry in .github/upstream-projects.yaml, applies +# pin_files substitutions, and opens a PR carrying the +# `upstream-content` label. The label triggers +# upstream-release-docs.yml to add skill-generated content edits to +# the same PR, producing one unified PR per toolhive release. +# +# The other three tracked projects (registry-server, studio, +# cloud-ui) don't produce reference artifacts this docs site consumes; +# they're handled by Renovate + upstream-release-docs.yml alone. + on: workflow_dispatch: inputs: @@ -18,7 +30,10 @@ permissions: pull-requests: write concurrency: - group: update-toolhive-reference + # Share the group with upstream-release-docs.yml so a toolhive + # release doesn't have the reference and content workflows racing + # against the same branch. + group: upstream-release-docs cancel-in-progress: false jobs: @@ -63,13 +78,47 @@ jobs: id: imports env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.get-version.outputs.version }} run: | chmod +x scripts/update-toolhive-reference.sh - if ! scripts/update-toolhive-reference.sh ${{ steps.get-version.outputs.version }}; then + if ! scripts/update-toolhive-reference.sh "$VERSION"; then echo "::error::Failed to update ToolHive reference docs" exit 1 fi + - name: Resolve tag for YAML bump + id: resolve + env: + INPUT_VERSION: ${{ steps.imports.outputs.version }} + FALLBACK_VERSION: ${{ steps.get-version.outputs.version }} + run: | + # scripts/update-toolhive-reference.sh emits `version` to + # GITHUB_OUTPUT after resolving `latest` to a concrete tag. + # Fall back to the raw input if something unexpected happened. + TAG="${INPUT_VERSION:-$FALLBACK_VERSION}" + if [ -z "$TAG" ] || [ "$TAG" = "latest" ]; then + echo "::error::Could not resolve a concrete tag for the YAML bump (got '$TAG')." + exit 1 + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + - name: Bump YAML + apply pin_files for toolhive + env: + TAG: ${{ steps.resolve.outputs.tag }} + run: | + # No-ops cleanly if already at this tag (rerun safety). + if ! node scripts/upstream-release/bump-yaml.mjs \ + --id toolhive --tag "$TAG" 2>/tmp/bump.err; then + if grep -q 'already pinned' /tmp/bump.err; then + echo "YAML already at $TAG; continuing." + else + cat /tmp/bump.err >&2 + exit 1 + fi + fi + node scripts/upstream-release/apply-pin-files.mjs \ + --id toolhive --tag "$TAG" + - name: Check for changes id: git-diff run: | @@ -124,33 +173,39 @@ jobs: if: steps.git-diff.outputs.changed == 'true' uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8 with: - branch: update-toolhive-reference-${{ steps.imports.outputs.version }} + branch: docs/upstream-toolhive-${{ steps.imports.outputs.version }} title: | - Update ToolHive reference docs for ${{ steps.imports.outputs.version }} + Update toolhive docs for ${{ steps.imports.outputs.version }} body: | This PR was auto-generated from ToolHive release **${{ steps.imports.outputs.version }}**.${{ steps.get-reviewer.outputs.assign_to != '' && format(' @{0} is tagged as the person who cut this release.', steps.get-reviewer.outputs.assign_to) || '' }} ${{ steps.new-crds.outputs.note_block }} ## What's in this PR - Only machine-generated reference files that are built from the ToolHive source code during the release process: + **Machine-generated reference files** (built from the ToolHive source at tag `${{ steps.imports.outputs.version }}`): - **CLI reference** (`docs/toolhive/reference/cli/`) - help text for every `thv` command - **REST API spec** (`static/api-specs/toolhive-api.yaml`) - OpenAPI/Swagger spec - **CRD reference** (`docs/toolhive/reference/crds/`, `static/api-specs/crds/`) - per-CRD MDX pages, extracted JSON schemas, and YAML examples - **Registry JSON schemas** (`static/api-specs/*.schema.json`) - registry validation schemas - ## What's NOT in this PR + **Version tracking**: + + - `.github/upstream-projects.yaml` - toolhive pin bumped to `${{ steps.imports.outputs.version }}` + - `pin_files` substitutions applied (e.g., `docusaurus.config.ts`) + + ## What will be added next - This PR does **not** contain guide, tutorial, or conceptual content. Any documentation updates beyond reference files are handled in separate, manually authored PRs. + The `upstream-release-docs.yml` workflow runs automatically when this PR opens and adds source-verified content edits to the guide/concept pages that describe this release. Wait for the content augmentation to complete before reviewing - look for a second commit from `github-actions[bot]` and a "Content additions by upstream-release-docs" section in the PR body. ## Review guidance - These files are generated directly from the ToolHive source and are not hand-edited. A quick scan for obvious issues is sufficient - this is intended to be merged shortly after the release is cut so the docs stay in sync. + Reference files are not hand-edited - a quick scan suffices. Content edits should be reviewed for accuracy against the release notes and source at the tag. commit-message: | - Update ToolHive reference docs for ${{ steps.imports.outputs.version }} + Update toolhive docs for ${{ steps.imports.outputs.version }} labels: | autogen-docs + upstream-content reviewers: ${{ steps.get-reviewer.outputs.assign_to }} delete-branch: true sign-commits: true diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index 77bc04c7..d9b89968 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -49,14 +49,21 @@ jobs: augment: runs-on: ubuntu-latest timeout-minutes: 90 - # Gate: triggered either by a Renovate-authored PR carrying the - # upstream-content label, OR by a manual workflow_dispatch retry. - # The per-event branches below resolve PR metadata from the right - # source. + # Gate: triggered either by a bot-authored PR carrying the + # upstream-content label, OR by a manual workflow_dispatch. + # + # Accepted authors: + # renovate[bot] - version-bump PRs for the three non-toolhive + # projects (see renovate.json customManager). + # github-actions[bot] - the toolhive reference workflow opens a PR + # from its own identity; we augment it. if: | github.event_name == 'workflow_dispatch' || ( - github.event.pull_request.user.login == 'renovate[bot]' && + ( + github.event.pull_request.user.login == 'renovate[bot]' || + github.event.pull_request.user.login == 'github-actions[bot]' + ) && contains(github.event.pull_request.labels.*.name, 'upstream-content') ) env: @@ -101,10 +108,14 @@ jobs: HEAD_REF=$(gh pr view "$PR_NUMBER" --json headRefName --jq .headRefName) AUTHOR=$(gh pr view "$PR_NUMBER" --json author --jq '.author.login') LABELS=$(gh pr view "$PR_NUMBER" --json labels --jq '[.labels[].name] | join(",")') - if [ "$AUTHOR" != "app/renovate" ] && [ "$AUTHOR" != "renovate[bot]" ]; then - echo "::error::PR #$PR_NUMBER is not authored by Renovate (author=$AUTHOR). Retry mode is for Renovate PRs only; use bootstrap (project_id + new_tag) to create a new manual PR." - exit 1 - fi + case "$AUTHOR" in + app/renovate|renovate[bot]|app/github-actions|github-actions[bot]) + ;; + *) + echo "::error::PR #$PR_NUMBER author '$AUTHOR' is not an accepted bot. Retry mode accepts renovate[bot] or github-actions[bot]; use bootstrap (project_id + new_tag) for a fresh manual PR." + exit 1 + ;; + esac if ! echo "$LABELS" | grep -q 'upstream-content'; then echo "::error::PR #$PR_NUMBER does not carry the upstream-content label (labels=$LABELS)." exit 1 diff --git a/renovate.json b/renovate.json index 576a3bb2..bc88fc52 100644 --- a/renovate.json +++ b/renovate.json @@ -98,6 +98,13 @@ "prBodyNotes": [ "After this PR opens, `.github/workflows/upstream-release-docs.yml` adds source-verified content edits for the new release." ] + }, + { + "description": "stacklok/toolhive releases are handled end-to-end by update-toolhive-reference.yml (reference regen + content), which bumps the YAML itself. Disable the Renovate path for this dep to avoid racing or opening a duplicate PR.", + "matchManagers": ["custom.regex"], + "matchFileNames": ["**/.github/upstream-projects.yaml"], + "matchDepNames": ["stacklok/toolhive"], + "enabled": false } ] } From 959968a5207261178873efcedfbd9b4a7b129ec2 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Tue, 21 Apr 2026 19:14:13 +0300 Subject: [PATCH 04/16] Address Copilot review feedback on PR #748 - Switch upstream-release scripts and the inline node snippet in the workflow from `js-yaml` to the already-declared `yaml` package, so the workflow does not rely on a transitive dependency that could disappear in a future install. Rewrites the `.load()` calls to `.parse()` to match the new package's API. - Filter out null author logins in the `gh api compare` reviewers extraction (`.commits[].author.login? // empty`) so a commit with an unlinked GitHub user cannot pass an empty value into --add-reviewer. - Replace the nested `$([ ... ] && ... || ...)` has_gaps expression with an explicit `if/else` block so the quoting stays obviously correct and resists accidental breakage in future edits. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/upstream-release-docs.yml | 12 ++++++++---- scripts/upstream-release/apply-pin-files.mjs | 4 ++-- scripts/upstream-release/bump-yaml.mjs | 4 ++-- scripts/upstream-release/detect-change.mjs | 6 +++--- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index d9b89968..2db997d6 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -270,7 +270,7 @@ jobs: # Capture stderr separately so we can surface a missing-compare # situation in the PR body rather than silently dropping reviewers. if COMPARE=$(gh api "repos/$REPO/compare/$PREV...$NEW" \ - --jq '[.commits[].author.login] | unique | .[]' 2>/dev/null); then + --jq '[.commits[].author.login? // empty] | unique | .[]' 2>/dev/null); then REVIEWERS=$(echo "$COMPARE" | grep -Ev '(\[bot\]$|^github-actions|^stacklokbot$|^dependabot|^renovate|^copilot)' | head -5 | paste -sd, -) @@ -288,9 +288,9 @@ jobs: PROJECT_ID: ${{ steps.detect.outputs.id }} run: | HINTS=$(node -e " - const yaml = require('js-yaml'); + const yaml = require('yaml'); const fs = require('fs'); - const p = yaml.load(fs.readFileSync('.github/upstream-projects.yaml','utf8')).projects.find(x=>x.id===process.env.PROJECT_ID); + const p = yaml.parse(fs.readFileSync('.github/upstream-projects.yaml','utf8')).projects.find(x=>x.id===process.env.PROJECT_ID); console.log(JSON.stringify(p?.docs_paths ?? [])); ") echo "docs_paths=$HINTS" >> "$GITHUB_OUTPUT" @@ -388,7 +388,11 @@ jobs: fi echo "GAPS_EOF" - echo "has_gaps=$([ -n "$GAPS_BODY" ] && echo true || echo false)" + if [ -n "$GAPS_BODY" ]; then + echo "has_gaps=true" + else + echo "has_gaps=false" + fi } >> "$GITHUB_OUTPUT" - name: Apply pin_files substitutions diff --git a/scripts/upstream-release/apply-pin-files.mjs b/scripts/upstream-release/apply-pin-files.mjs index 0f2218ed..fa9ce333 100644 --- a/scripts/upstream-release/apply-pin-files.mjs +++ b/scripts/upstream-release/apply-pin-files.mjs @@ -15,7 +15,7 @@ // so unrelated `@latest` strings elsewhere in the file are safe. import fs from 'node:fs'; -import yaml from 'js-yaml'; +import yaml from 'yaml'; const PROJECTS_FILE = '.github/upstream-projects.yaml'; @@ -66,7 +66,7 @@ function main() { process.exit(1); } - const parsed = yaml.load(fs.readFileSync(PROJECTS_FILE, 'utf8')); + const parsed = yaml.parse(fs.readFileSync(PROJECTS_FILE, 'utf8')); const project = parsed.projects.find((p) => p.id === id); if (!project) { console.error(`Unknown project id: ${id}`); diff --git a/scripts/upstream-release/bump-yaml.mjs b/scripts/upstream-release/bump-yaml.mjs index 5eecd931..edd755a9 100644 --- a/scripts/upstream-release/bump-yaml.mjs +++ b/scripts/upstream-release/bump-yaml.mjs @@ -17,7 +17,7 @@ // matches (no-op — caller should not open a PR). import fs from 'node:fs'; -import yaml from 'js-yaml'; +import yaml from 'yaml'; const PROJECTS_FILE = '.github/upstream-projects.yaml'; @@ -69,7 +69,7 @@ function main() { } const raw = fs.readFileSync(PROJECTS_FILE, 'utf8'); - const parsed = yaml.load(raw); + const parsed = yaml.parse(raw); if (!parsed.projects.find((p) => p.id === id)) { console.error(`Unknown project id: ${id}`); process.exit(1); diff --git a/scripts/upstream-release/detect-change.mjs b/scripts/upstream-release/detect-change.mjs index ec92f056..fa79702f 100644 --- a/scripts/upstream-release/detect-change.mjs +++ b/scripts/upstream-release/detect-change.mjs @@ -22,7 +22,7 @@ import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; -import yaml from 'js-yaml'; +import yaml from 'yaml'; const PROJECTS_FILE = '.github/upstream-projects.yaml'; const REPO_SHAPE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/; @@ -41,13 +41,13 @@ function loadFromRef(ref) { const text = execFileSync('git', ['show', `${ref}:${PROJECTS_FILE}`], { encoding: 'utf8', }); - return yaml.load(text).projects; + return yaml.parse(text).projects; } function main() { const ref = process.env.BASE_REF || 'origin/main'; const mainProjects = loadFromRef(ref); - const headProjects = yaml.load( + const headProjects = yaml.parse( fs.readFileSync(PROJECTS_FILE, 'utf8') ).projects; From d8229d1042652b8136cbb3e71ee4cac79a781fe4 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Tue, 21 Apr 2026 19:38:28 +0300 Subject: [PATCH 05/16] Chain reference workflow into content via workflow_call The unified toolhive path had a broken seam: peter-evans opens PRs with the default GITHUB_TOKEN, and GitHub intentionally does not trigger pull_request workflows on those PRs (recursion guard). Verified on PR #747 - no on-pr.yaml run fired. So the content workflow's pull_request trigger would never fire for toolhive PRs. Fix: add workflow_call trigger to upstream-release-docs.yml and call it as a dependent job from update-toolhive-reference.yml after peter-evans completes. The Renovate path is unaffected - Renovate is a GitHub App identity whose PRs DO trigger workflows. Also: - Distinct concurrency groups per workflow (upstream-release-docs-reference vs -content) so the workflow_call from reference to content doesn't deadlock on a shared group. - Drop the "What will be added next" section from the peter-evans body; the content workflow appends its own section below. - Fix leading-whitespace leak in the bootstrap PR body by using a heredoc + sed strip instead of an inline quoted string. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../workflows/update-toolhive-reference.yml | 35 +++++++---- .github/workflows/upstream-release-docs.yml | 61 +++++++++++++------ 2 files changed, 67 insertions(+), 29 deletions(-) diff --git a/.github/workflows/update-toolhive-reference.yml b/.github/workflows/update-toolhive-reference.yml index 526e0770..73f49eda 100644 --- a/.github/workflows/update-toolhive-reference.yml +++ b/.github/workflows/update-toolhive-reference.yml @@ -30,15 +30,17 @@ permissions: pull-requests: write concurrency: - # Share the group with upstream-release-docs.yml so a toolhive - # release doesn't have the reference and content workflows racing - # against the same branch. - group: upstream-release-docs + # Distinct from upstream-release-docs-content so that when this + # workflow calls the content workflow via workflow_call, the nested + # run doesn't block waiting for this one's concurrency slot. + group: upstream-release-docs-reference cancel-in-progress: false jobs: update-reference: runs-on: ubuntu-latest + outputs: + pr_number: ${{ steps.cpr.outputs.pull-request-number }} steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 @@ -170,6 +172,7 @@ jobs: fi - name: Create Pull Request + id: cpr if: steps.git-diff.outputs.changed == 'true' uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8 with: @@ -194,13 +197,7 @@ jobs: - `.github/upstream-projects.yaml` - toolhive pin bumped to `${{ steps.imports.outputs.version }}` - `pin_files` substitutions applied (e.g., `docusaurus.config.ts`) - ## What will be added next - - The `upstream-release-docs.yml` workflow runs automatically when this PR opens and adds source-verified content edits to the guide/concept pages that describe this release. Wait for the content augmentation to complete before reviewing - look for a second commit from `github-actions[bot]` and a "Content additions by upstream-release-docs" section in the PR body. - - ## Review guidance - - Reference files are not hand-edited - a quick scan suffices. Content edits should be reviewed for accuracy against the release notes and source at the tag. + A follow-up commit from `upstream-release-docs.yml` will add source-verified content edits to the guide/concept pages and a "Content additions by upstream-release-docs" section below. Wait for it to land before reviewing. commit-message: | Update toolhive docs for ${{ steps.imports.outputs.version }} labels: | @@ -209,3 +206,19 @@ jobs: reviewers: ${{ steps.get-reviewer.outputs.assign_to }} delete-branch: true sign-commits: true + + # Chains into the content workflow via workflow_call. We can't rely + # on the `pull_request: opened` event because peter-evans opens the + # PR with the default GITHUB_TOKEN, which by design does not trigger + # further workflow runs. Calling the reusable workflow directly is + # the supported escape hatch. + augment-content: + needs: update-reference + if: needs.update-reference.outputs.pr_number != '' + uses: ./.github/workflows/upstream-release-docs.yml + with: + pr_number: ${{ needs.update-reference.outputs.pr_number }} + secrets: inherit + permissions: + contents: write + pull-requests: write diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index 2db997d6..35359ae8 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -34,36 +34,45 @@ on: description: 'Bootstrap a new PR: the upstream tag to document (e.g. v1.3.0). Requires project_id.' required: false type: string + # Reusable-workflow entry point. Called by update-toolhive-reference.yml + # after it opens its PR via peter-evans — PRs opened with the default + # GITHUB_TOKEN do not fire pull_request workflows, so we cross the seam + # via workflow_call instead of the event chain. + workflow_call: + inputs: + pr_number: + description: 'PR number to augment.' + required: true + type: string permissions: contents: write pull-requests: write concurrency: - # Workflow-level group (not per-PR) so two simultaneous upstream - # releases don't run the skill in parallel on shared concept pages. - group: upstream-release-docs + # Distinct from update-toolhive-reference.yml's group so a workflow_call + # from that workflow (which holds its own group) doesn't deadlock when + # this one also tries to acquire the same group. + group: upstream-release-docs-content cancel-in-progress: false jobs: augment: runs-on: ubuntu-latest timeout-minutes: 90 - # Gate: triggered either by a bot-authored PR carrying the - # upstream-content label, OR by a manual workflow_dispatch. - # - # Accepted authors: - # renovate[bot] - version-bump PRs for the three non-toolhive - # projects (see renovate.json customManager). - # github-actions[bot] - the toolhive reference workflow opens a PR - # from its own identity; we augment it. + # Gate: three accepted entry points. + # - pull_request: Renovate-authored PR with upstream-content label. + # (The toolhive reference workflow can't reach here because PRs + # opened with the default GITHUB_TOKEN don't fire pull_request + # events; the reference workflow reaches us via workflow_call.) + # - workflow_dispatch: human manually retries or bootstraps. + # - workflow_call: the reference workflow invokes us with pr_number. if: | github.event_name == 'workflow_dispatch' || + github.event_name == 'workflow_call' || ( - ( - github.event.pull_request.user.login == 'renovate[bot]' || - github.event.pull_request.user.login == 'github-actions[bot]' - ) && + github.event_name == 'pull_request' && + github.event.pull_request.user.login == 'renovate[bot]' && contains(github.event.pull_request.labels.*.name, 'upstream-content') ) env: @@ -95,6 +104,7 @@ jobs: NEW_TAG_IN: ${{ github.event.inputs.new_tag }} EVENT_PR: ${{ github.event.pull_request.number }} EVENT_HEAD_REF: ${{ github.event.pull_request.head.ref }} + CALL_PR: ${{ inputs.pr_number }} run: | case "$EVENT" in pull_request) @@ -102,6 +112,13 @@ jobs: HEAD_REF="$EVENT_HEAD_REF" MODE="react" ;; + workflow_call) + # Invoked by update-toolhive-reference.yml after it opens + # its PR. Trust the caller; we already share a repository. + PR_NUMBER="$CALL_PR" + HEAD_REF=$(gh pr view "$PR_NUMBER" --json headRefName --jq .headRefName) + MODE="react" + ;; workflow_dispatch) if [ -n "$DISPATCH_PR" ]; then PR_NUMBER="$DISPATCH_PR" @@ -185,13 +202,21 @@ jobs: # Open the PR with the same labels Renovate would apply, so # downstream steps that read labels behave identically. + # Heredoc so the YAML indent doesn't leak into the PR body. + cat > /tmp/bootstrap-body.md < Date: Tue, 21 Apr 2026 19:43:13 +0300 Subject: [PATCH 06/16] Host Registry Server Swagger locally instead of via jsdelivr Per review feedback from Dan: pull the Swagger file from the upstream repo at the pinned tag into static/api-specs/ and reference the local file in docusaurus.config.ts, matching how ToolHive's own Swagger is handled. Keeps the docs build offline-friendly and makes the pin implicit in git history rather than a CDN URL template. - New scripts/upstream-release/sync-assets.mjs helper copies declared files from the shallow clone into the docs repo. No-op when a project declares no assets. - .github/upstream-projects.yaml gains an `assets:` field per project. - Replaced the `pin_files` entry for registry-server with assets. - static/api-specs/toolhive-registry-api.yaml seeded from upstream v1.2.1 (1971 lines). Regenerated automatically on every bump. - Content workflow runs sync-assets right after the shallow clone. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/upstream-projects.yaml | 11 +- .github/workflows/upstream-release-docs.yml | 9 + docusaurus.config.ts | 2 +- scripts/upstream-release/sync-assets.mjs | 85 + static/api-specs/toolhive-registry-api.yaml | 1971 +++++++++++++++++++ 5 files changed, 2074 insertions(+), 4 deletions(-) create mode 100644 scripts/upstream-release/sync-assets.mjs create mode 100644 static/api-specs/toolhive-registry-api.yaml diff --git a/.github/upstream-projects.yaml b/.github/upstream-projects.yaml index 8eefea0e..9aadf6c0 100644 --- a/.github/upstream-projects.yaml +++ b/.github/upstream-projects.yaml @@ -27,9 +27,14 @@ projects: docs_paths: - docs/toolhive/guides-registry - docs/toolhive/concepts/registry-criteria.mdx - pin_files: - - path: docusaurus.config.ts - replace_latest: true + # Files copied from the upstream repo at the pinned tag whenever + # version: bumps. Source paths are relative to the upstream repo + # root; destination paths are relative to this repo root. Keeping + # the swagger locally (rather than referencing via jsdelivr) lets + # the docs build offline and keeps the pin implicit in git. + assets: + - source: docs/thv-registry-api/swagger.yaml + destination: static/api-specs/toolhive-registry-api.yaml - id: toolhive repo: stacklok/toolhive diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index 35359ae8..72c32abf 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -285,6 +285,15 @@ jobs: "$SCRATCH/upstream" echo "scratch_dir=$SCRATCH/upstream" >> "$GITHUB_OUTPUT" + - name: Sync declared assets from upstream clone + env: + PROJECT_ID: ${{ steps.detect.outputs.id }} + CLONE_DIR: ${{ steps.clone.outputs.scratch_dir }} + run: | + node scripts/upstream-release/sync-assets.mjs \ + --id "$PROJECT_ID" \ + --clone "$CLONE_DIR" + - name: Extract reviewers from release compare id: reviewers env: diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 95fbde65..0be1ed9f 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -214,7 +214,7 @@ const config: Config = { }, { id: 'toolhive-registry-api', - spec: 'https://cdn.jsdelivr.net/gh/stacklok/toolhive-registry-server@v1.2.1/docs/thv-registry-api/swagger.yaml', + spec: 'static/api-specs/toolhive-registry-api.yaml', config: path.join(__dirname, 'src/redocly/redocly-toolhive.yaml'), }, ], diff --git a/scripts/upstream-release/sync-assets.mjs b/scripts/upstream-release/sync-assets.mjs new file mode 100644 index 00000000..05c2edaa --- /dev/null +++ b/scripts/upstream-release/sync-assets.mjs @@ -0,0 +1,85 @@ +#!/usr/bin/env node +// SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// Copies declared upstream assets from the shallow-clone directory into +// the docs repo for the given project. Declared per-project in +// .github/upstream-projects.yaml under `assets:`: +// +// assets: +// - source: +// destination: +// +// Usage: +// node sync-assets.mjs --id --clone +// +// No-op if the project declares no assets. + +import fs from 'node:fs'; +import path from 'node:path'; +import yaml from 'yaml'; + +const PROJECTS_FILE = '.github/upstream-projects.yaml'; + +function parseArgs(argv) { + const args = {}; + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--id') { + args.id = argv[i + 1]; + i++; + } else if (argv[i] === '--clone') { + args.clone = argv[i + 1]; + i++; + } + } + return args; +} + +function main() { + const { id, clone } = parseArgs(process.argv.slice(2)); + if (!id || !clone) { + console.error( + 'Usage: sync-assets.mjs --id --clone ' + ); + process.exit(1); + } + if (!fs.existsSync(clone) || !fs.statSync(clone).isDirectory()) { + console.error(`Clone directory not found: ${clone}`); + process.exit(1); + } + + const parsed = yaml.parse(fs.readFileSync(PROJECTS_FILE, 'utf8')); + const project = parsed.projects.find((p) => p.id === id); + if (!project) { + console.error(`Unknown project id: ${id}`); + process.exit(1); + } + + const assets = project.assets ?? []; + if (assets.length === 0) { + console.log(`No assets declared for ${id}; nothing to sync.`); + return; + } + + for (const asset of assets) { + const { source, destination } = asset; + if (!source || !destination) { + console.error( + `Asset entry missing source or destination: ${JSON.stringify(asset)}` + ); + process.exit(1); + } + const srcPath = path.join(clone, source); + if (!fs.existsSync(srcPath)) { + console.error( + `Source not found in clone: ${source} (resolved to ${srcPath})` + ); + process.exit(1); + } + fs.mkdirSync(path.dirname(destination), { recursive: true }); + fs.copyFileSync(srcPath, destination); + console.log(`assets: synced ${source} -> ${destination}`); + } +} + +main(); diff --git a/static/api-specs/toolhive-registry-api.yaml b/static/api-specs/toolhive-registry-api.yaml new file mode 100644 index 00000000..2ee43748 --- /dev/null +++ b/static/api-specs/toolhive-registry-api.yaml @@ -0,0 +1,1971 @@ +components: + schemas: + github_com_stacklok_toolhive-registry-server_internal_config.FilterConfig: + description: Filtering rules + properties: + names: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_config.NameFilterConfig' + tags: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_config.TagFilterConfig' + type: object + github_com_stacklok_toolhive-registry-server_internal_config.NameFilterConfig: + properties: + exclude: + items: + type: string + type: array + uniqueItems: false + include: + items: + type: string + type: array + uniqueItems: false + type: object + github_com_stacklok_toolhive-registry-server_internal_config.SourceType: + description: git, api, file, managed, kubernetes + enum: + - git + - api + - file + - managed + - kubernetes + type: string + x-enum-varnames: + - SourceTypeGit + - SourceTypeAPI + - SourceTypeFile + - SourceTypeManaged + - SourceTypeKubernetes + github_com_stacklok_toolhive-registry-server_internal_config.TagFilterConfig: + properties: + exclude: + items: + type: string + type: array + uniqueItems: false + include: + items: + type: string + type: array + uniqueItems: false + type: object + github_com_stacklok_toolhive-registry-server_internal_service.CreationType: + description: API or CONFIG + enum: + - API + - CONFIG + type: string + x-enum-varnames: + - CreationTypeAPI + - CreationTypeCONFIG + github_com_stacklok_toolhive-registry-server_internal_service.EntryVersionInfo: + properties: + createdAt: + type: string + description: + type: string + title: + type: string + updatedAt: + type: string + version: + type: string + type: object + github_com_stacklok_toolhive-registry-server_internal_service.RegistryEntriesResponse: + properties: + entries: + items: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.RegistryEntryInfo' + type: array + uniqueItems: false + type: object + github_com_stacklok_toolhive-registry-server_internal_service.RegistryEntryInfo: + properties: + entryType: + type: string + name: + type: string + sourceName: + type: string + version: + type: string + type: object + github_com_stacklok_toolhive-registry-server_internal_service.RegistryInfo: + properties: + claims: + additionalProperties: {} + type: object + createdAt: + type: string + creationType: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.CreationType' + name: + type: string + sources: + items: + type: string + type: array + uniqueItems: false + updatedAt: + type: string + type: object + github_com_stacklok_toolhive-registry-server_internal_service.Skill: + properties: + _meta: + additionalProperties: {} + type: object + allowedTools: + items: + type: string + type: array + uniqueItems: false + compatibility: + type: string + createdAt: + type: string + description: + type: string + icons: + items: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.SkillIcon' + type: array + uniqueItems: false + id: + type: string + isLatest: + type: boolean + license: + type: string + metadata: + additionalProperties: {} + type: object + name: + type: string + namespace: + type: string + packages: + items: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.SkillPackage' + type: array + uniqueItems: false + repository: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.SkillRepository' + status: + type: string + title: + type: string + updatedAt: + type: string + version: + type: string + type: object + github_com_stacklok_toolhive-registry-server_internal_service.SkillIcon: + properties: + label: + type: string + size: + type: string + src: + type: string + type: + type: string + type: object + github_com_stacklok_toolhive-registry-server_internal_service.SkillPackage: + properties: + commit: + type: string + digest: + type: string + identifier: + type: string + mediaType: + type: string + ref: + type: string + registryType: + type: string + subfolder: + type: string + url: + type: string + type: object + github_com_stacklok_toolhive-registry-server_internal_service.SkillRepository: + properties: + type: + type: string + url: + type: string + type: object + github_com_stacklok_toolhive-registry-server_internal_service.SourceEntriesResponse: + properties: + entries: + items: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.SourceEntryInfo' + type: array + uniqueItems: false + type: object + github_com_stacklok_toolhive-registry-server_internal_service.SourceEntryInfo: + properties: + claims: + additionalProperties: {} + type: object + entryType: + type: string + name: + type: string + versions: + items: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.EntryVersionInfo' + type: array + uniqueItems: false + type: object + github_com_stacklok_toolhive-registry-server_internal_service.SourceInfo: + properties: + claims: + additionalProperties: {} + description: Authorization claims + type: object + createdAt: + type: string + creationType: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.CreationType' + filterConfig: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_config.FilterConfig' + name: + type: string + sourceConfig: + description: Type-specific source configuration + sourceType: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_config.SourceType' + syncSchedule: + description: Sync interval string + type: string + syncStatus: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.SourceSyncStatus' + type: + description: MANAGED, FILE, REMOTE, KUBERNETES + type: string + updatedAt: + type: string + type: object + github_com_stacklok_toolhive-registry-server_internal_service.SourceListResponse: + properties: + sources: + items: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.SourceInfo' + type: array + uniqueItems: false + type: object + github_com_stacklok_toolhive-registry-server_internal_service.SourceSyncStatus: + properties: + attemptCount: + description: Number of sync attempts + type: integer + lastAttempt: + description: Last sync attempt + type: string + lastSyncTime: + description: Last successful sync + type: string + message: + description: Status or error message + type: string + phase: + description: complete, syncing, failed + type: string + serverCount: + description: Number of servers in registry + type: integer + skillCount: + description: Number of skills in registry + type: integer + type: object + internal_api_v1.meResponse: + properties: + roles: + items: + type: string + type: array + uniqueItems: false + subject: + type: string + type: object + internal_api_v1.publishEntryRequest: + properties: + claims: + additionalProperties: {} + type: object + server: + $ref: '#/components/schemas/v0.ServerJSON' + skill: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.Skill' + type: object + internal_api_v1.registryListResponse: + properties: + registries: + items: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.RegistryInfo' + type: array + uniqueItems: false + type: object + internal_api_v1.updateEntryClaimsRequest: + properties: + claims: + additionalProperties: {} + type: object + type: object + internal_api_x_skills.SkillListMetadata: + properties: + count: + type: integer + nextCursor: + type: string + type: object + internal_api_x_skills.SkillListResponse: + properties: + metadata: + $ref: '#/components/schemas/internal_api_x_skills.SkillListMetadata' + skills: + items: + $ref: '#/components/schemas/registry.Skill' + type: array + uniqueItems: false + type: object + model.Argument: + properties: + choices: + items: + type: string + type: array + uniqueItems: false + default: + type: string + description: + type: string + format: + $ref: '#/components/schemas/model.Format' + isRepeated: + type: boolean + isRequired: + type: boolean + isSecret: + type: boolean + name: + example: --port + type: string + placeholder: + type: string + type: + $ref: '#/components/schemas/model.ArgumentType' + value: + type: string + valueHint: + example: file_path + type: string + variables: + additionalProperties: + $ref: '#/components/schemas/model.Input' + type: object + type: object + model.ArgumentType: + enum: + - positional + - named + example: positional + type: string + x-enum-varnames: + - ArgumentTypePositional + - ArgumentTypeNamed + model.Format: + enum: + - string + - number + - boolean + - filepath + type: string + x-enum-varnames: + - FormatString + - FormatNumber + - FormatBoolean + - FormatFilePath + model.Icon: + properties: + mimeType: + example: image/png + type: string + sizes: + items: + type: string + type: array + uniqueItems: false + src: + example: https://example.com/icon.png + format: uri + maxLength: 255 + type: string + theme: + type: string + type: object + model.Input: + properties: + choices: + items: + type: string + type: array + uniqueItems: false + default: + type: string + description: + type: string + format: + $ref: '#/components/schemas/model.Format' + isRequired: + type: boolean + isSecret: + type: boolean + placeholder: + type: string + value: + type: string + type: object + model.KeyValueInput: + properties: + choices: + items: + type: string + type: array + uniqueItems: false + default: + type: string + description: + type: string + format: + $ref: '#/components/schemas/model.Format' + isRequired: + type: boolean + isSecret: + type: boolean + name: + example: SOME_VARIABLE + type: string + placeholder: + type: string + value: + type: string + variables: + additionalProperties: + $ref: '#/components/schemas/model.Input' + type: object + type: object + model.Package: + properties: + environmentVariables: + description: EnvironmentVariables are set when running the package + items: + $ref: '#/components/schemas/model.KeyValueInput' + type: array + uniqueItems: false + fileSha256: + description: FileSHA256 is the SHA-256 hash for integrity verification (required + for mcpb, optional for others) + example: fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce + pattern: ^[a-f0-9]{64}$ + type: string + identifier: + description: |- + Identifier is the package identifier: + - For NPM/PyPI/NuGet: package name or ID + - For OCI: full image reference (e.g., "ghcr.io/owner/repo:v1.0.0") + - For MCPB: direct download URL + example: '@modelcontextprotocol/server-brave-search' + minLength: 1 + type: string + packageArguments: + description: PackageArguments are passed to the package's binary + items: + $ref: '#/components/schemas/model.Argument' + type: array + uniqueItems: false + registryBaseUrl: + description: RegistryBaseURL is the base URL of the package registry (used + by npm, pypi, nuget; not used by oci, mcpb) + example: https://registry.npmjs.org + format: uri + type: string + registryType: + description: RegistryType indicates how to download packages (e.g., "npm", + "pypi", "oci", "nuget", "mcpb") + example: npm + minLength: 1 + type: string + runtimeArguments: + description: RuntimeArguments are passed to the package's runtime command + (e.g., docker, npx) + items: + $ref: '#/components/schemas/model.Argument' + type: array + uniqueItems: false + runtimeHint: + description: RunTimeHint suggests the appropriate runtime for the package + example: npx + type: string + transport: + $ref: '#/components/schemas/model.Transport' + version: + description: Version is the package version (required for npm, pypi, nuget; + optional for mcpb; not used by oci where version is in the identifier) + example: 1.0.2 + minLength: 1 + type: string + type: object + model.Repository: + properties: + id: + example: b94b5f7e-c7c6-d760-2c78-a5e9b8a5b8c9 + type: string + source: + example: github + type: string + subfolder: + example: src/everything + type: string + url: + example: https://github.com/modelcontextprotocol/servers + format: uri + type: string + type: object + model.Status: + enum: + - active + - deprecated + - deleted + type: string + x-enum-varnames: + - StatusActive + - StatusDeprecated + - StatusDeleted + model.Transport: + description: Transport is required and specifies the transport protocol configuration + properties: + headers: + items: + $ref: '#/components/schemas/model.KeyValueInput' + type: array + uniqueItems: false + type: + example: stdio + type: string + url: + example: https://api.example.com/mcp + type: string + variables: + additionalProperties: + $ref: '#/components/schemas/model.Input' + type: object + type: object + registry.Skill: + properties: + _meta: + additionalProperties: {} + description: Meta is an opaque payload with extended meta data details of + the skill. + type: object + allowedTools: + description: |- + AllowedTools is the list of tools that the skill is compatible with. + This is experimental. + items: + type: string + type: array + uniqueItems: false + compatibility: + description: Compatibility is the environment requirements of the skill. + type: string + description: + description: Description is the description of the skill. + type: string + icons: + description: Icons is the list of icons for the skill. + items: + $ref: '#/components/schemas/registry.SkillIcon' + type: array + uniqueItems: false + license: + description: License is the SPDX license identifier of the skill. + type: string + metadata: + additionalProperties: {} + description: |- + Metadata is the official metadata of the skill as reported in the + SKILL.md file. + type: object + name: + description: |- + Name is the name of the skill. + The format is that of identifiers, e.g. "my-skill". + type: string + namespace: + description: |- + Namespace is the namespace of the skill. + The format is reverse-DNS, e.g. "io.github.user". + type: string + packages: + description: Packages is the list of packages for the skill. + items: + $ref: '#/components/schemas/registry.SkillPackage' + type: array + uniqueItems: false + repository: + $ref: '#/components/schemas/registry.SkillRepository' + status: + description: |- + Status is the status of the skill. + Can be one of "active", "deprecated", or "archived". + type: string + title: + description: |- + Title is the title of the skill. + This is for human consumption, not an identifier. + type: string + version: + description: |- + Version is the version of the skill. + Any non-empty string is valid, but ideally it should be either a + semantic version or a commit hash. + type: string + type: object + registry.SkillIcon: + properties: + label: + description: Label is the label of the icon. + type: string + size: + description: Size is the size of the icon. + type: string + src: + description: Src is the source of the icon. + type: string + type: + description: Type is the type of the icon. + type: string + type: object + registry.SkillPackage: + properties: + commit: + description: Commit is the commit of the package. + type: string + digest: + description: Digest is the digest of the package. + type: string + identifier: + description: Identifier is the OCI identifier of the package. + type: string + mediaType: + description: MediaType is the media type of the package. + type: string + ref: + description: Ref is the reference of the package. + type: string + registryType: + description: |- + RegistryType is the type of registry the package is from. + Can be "oci" or "git". + type: string + subfolder: + description: Subfolder is the subfolder of the package. + type: string + url: + description: URL is the URL of the package. + type: string + type: object + registry.SkillRepository: + description: Repository is the source repository of the skill. + properties: + type: + description: Type is the type of the repository. + type: string + url: + description: URL is the URL of the repository. + type: string + type: object + v0.Metadata: + properties: + count: + type: integer + nextCursor: + type: string + type: object + v0.RegistryExtensions: + properties: + isLatest: + type: boolean + publishedAt: + format: date-time + type: string + status: + $ref: '#/components/schemas/model.Status' + statusChangedAt: + format: date-time + type: string + statusMessage: + type: string + updatedAt: + format: date-time + type: string + type: object + v0.ResponseMeta: + properties: + io.modelcontextprotocol.registry/official: + $ref: '#/components/schemas/v0.RegistryExtensions' + type: object + v0.ServerJSON: + properties: + $schema: + example: https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json + format: uri + minLength: 1 + type: string + _meta: + $ref: '#/components/schemas/v0.ServerMeta' + description: + example: MCP server providing weather data and forecasts via OpenWeatherMap + API + maxLength: 100 + minLength: 1 + type: string + icons: + items: + $ref: '#/components/schemas/model.Icon' + type: array + uniqueItems: false + name: + example: io.github.user/weather + maxLength: 200 + minLength: 3 + pattern: ^[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+$ + type: string + packages: + items: + $ref: '#/components/schemas/model.Package' + type: array + uniqueItems: false + remotes: + items: + $ref: '#/components/schemas/model.Transport' + type: array + uniqueItems: false + repository: + $ref: '#/components/schemas/model.Repository' + title: + example: Weather API + maxLength: 100 + minLength: 1 + type: string + version: + example: 1.0.2 + type: string + websiteUrl: + example: https://modelcontextprotocol.io/examples + format: uri + type: string + type: object + v0.ServerListResponse: + properties: + metadata: + $ref: '#/components/schemas/v0.Metadata' + servers: + items: + $ref: '#/components/schemas/v0.ServerResponse' + type: array + uniqueItems: false + type: object + v0.ServerMeta: + properties: + io.modelcontextprotocol.registry/publisher-provided: + additionalProperties: {} + type: object + type: object + v0.ServerResponse: + properties: + _meta: + $ref: '#/components/schemas/v0.ResponseMeta' + server: + $ref: '#/components/schemas/v0.ServerJSON' + type: object + securitySchemes: + BearerAuth: + description: 'OAuth 2.0 Bearer token authentication. Format: "Bearer {token}"' + in: header + name: Authorization + type: apiKey +externalDocs: + description: "" + url: "" +info: + contact: + url: https://github.com/stacklok/toolhive + description: |- + API for accessing MCP server registry data and deployed server information + This API provides endpoints to query the MCP (Model Context Protocol) server registry, + get information about available servers, and check the status of deployed servers. + + Authentication is required by default. Use Bearer token authentication with a valid + OAuth/OIDC access token. The /.well-known/oauth-protected-resource endpoint provides + OAuth discovery metadata (RFC 9728). + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + title: ToolHive Registry API + version: "0.1" +openapi: 3.1.0 +paths: + /openapi.json: + get: + description: Get the OpenAPI 3.1.0 specification for this API + responses: + "200": + content: + application/json: + schema: + type: object + description: OpenAPI 3.1.0 specification + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal Server Error + summary: OpenAPI specification + tags: + - system + /registry/{registryName}/v0.1/servers: + get: + description: Get a list of available servers from a specific registry + parameters: + - description: Registry name + in: path + name: registryName + required: true + schema: + type: string + - description: Pagination cursor for retrieving next set of results + in: query + name: cursor + schema: + type: string + - description: Maximum number of items to return + in: query + name: limit + schema: + type: integer + - description: Search servers by name (substring match) + in: query + name: search + schema: + type: string + - description: Filter by version ('latest' for latest version, or an exact version + like '1.2.3') + in: query + name: version + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/v0.ServerListResponse' + description: OK + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "401": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Unauthorized + "404": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Registry not found + security: + - BearerAuth: [] + summary: List servers in specific registry + tags: + - registry + /registry/{registryName}/v0.1/servers/{serverName}/versions: + get: + description: Returns all available versions for a specific MCP server from a + specific registry + parameters: + - description: Registry name + in: path + name: registryName + required: true + schema: + type: string + - description: URL-encoded server name (e.g., \ + in: path + name: serverName + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/v0.ServerListResponse' + description: A list of all versions for the server + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "401": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Unauthorized + "404": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Server not found + security: + - BearerAuth: [] + summary: List all versions of an MCP server in specific registry + tags: + - registry + /registry/{registryName}/v0.1/servers/{serverName}/versions/{version}: + get: + description: |- + Returns detailed information about a specific version of an MCP server from a specific registry. + Use the special version `latest` to get the latest version. + parameters: + - description: Registry name + in: path + name: registryName + required: true + schema: + type: string + - description: URL-encoded server name (e.g., \ + in: path + name: serverName + required: true + schema: + type: string + - description: URL-encoded version to retrieve (e.g., \ + in: path + name: version + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/v0.ServerResponse' + description: Detailed server information + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "401": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Unauthorized + "404": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Server or version not found + security: + - BearerAuth: [] + summary: Get specific MCP server version in specific registry + tags: + - registry + /registry/{registryName}/v0.1/x/dev.toolhive/skills: + get: + description: List skills in a registry (paginated, latest versions). + parameters: + - description: Registry name + in: path + name: registryName + required: true + schema: + type: string + - description: Filter by name/description substring + in: query + name: search + schema: + type: string + - description: Filter by status (comma-separated, e.g. active,deprecated) + in: query + name: status + schema: + type: string + - description: Max results (default 50, max 100) + in: query + name: limit + schema: + type: integer + - description: Pagination cursor + in: query + name: cursor + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/internal_api_x_skills.SkillListResponse' + description: List of skills + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + security: + - BearerAuth: [] + summary: List skills in registry + tags: + - skills + /registry/{registryName}/v0.1/x/dev.toolhive/skills/{namespace}/{name}: + get: + description: Get the latest version of a skill by namespace and name. + parameters: + - description: Registry name + in: path + name: registryName + required: true + schema: + type: string + - description: Skill namespace (reverse-DNS) + in: path + name: namespace + required: true + schema: + type: string + - description: Skill name + in: path + name: name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/registry.Skill' + description: Skill details + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "404": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Skill not found + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + security: + - BearerAuth: [] + summary: Get latest skill version + tags: + - skills + /registry/{registryName}/v0.1/x/dev.toolhive/skills/{namespace}/{name}/versions: + get: + description: List all versions of a skill. + parameters: + - description: Registry name + in: path + name: registryName + required: true + schema: + type: string + - description: Skill namespace (reverse-DNS) + in: path + name: namespace + required: true + schema: + type: string + - description: Skill name + in: path + name: name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/internal_api_x_skills.SkillListResponse' + description: List of skill versions + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "404": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Skill not found + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + security: + - BearerAuth: [] + summary: List skill versions + tags: + - skills + /registry/{registryName}/v0.1/x/dev.toolhive/skills/{namespace}/{name}/versions/{version}: + get: + description: Get a specific version of a skill. + parameters: + - description: Registry name + in: path + name: registryName + required: true + schema: + type: string + - description: Skill namespace (reverse-DNS) + in: path + name: namespace + required: true + schema: + type: string + - description: Skill name + in: path + name: name + required: true + schema: + type: string + - description: Skill version + in: path + name: version + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/registry.Skill' + description: Skill details + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "404": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Skill or version not found + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + security: + - BearerAuth: [] + summary: Get specific skill version + tags: + - skills + /v1/entries: + post: + description: Publish a new server or skill entry. Exactly one of 'server' or + 'skill' must be provided. + requestBody: + content: + application/json: + schema: + oneOf: + - type: object + - $ref: '#/components/schemas/internal_api_v1.publishEntryRequest' + description: Entry to publish (server or skill) + summary: request + description: Entry to publish (server or skill) + required: true + responses: + "201": + content: + application/json: + schema: + type: object + description: Published entry (server or skill) + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "409": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Conflict + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + summary: Publish entry + tags: + - v1 + /v1/entries/{type}/{name}/claims: + put: + description: Update claims for a published entry name + parameters: + - description: Entry Type (server or skill) + in: path + name: type + required: true + schema: + type: string + - description: Entry Name + in: path + name: name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + oneOf: + - type: object + - $ref: '#/components/schemas/internal_api_v1.updateEntryClaimsRequest' + description: Claims to set + summary: request + description: Claims to set + required: true + responses: + "204": + description: No Content + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "403": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Forbidden + "404": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Not found + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + "503": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: No managed source available + summary: Update entry claims + tags: + - v1 + /v1/entries/{type}/{name}/versions/{version}: + delete: + description: Delete a published entry version + parameters: + - description: Entry Type (server or skill) + in: path + name: type + required: true + schema: + type: string + - description: Entry Name + in: path + name: name + required: true + schema: + type: string + - description: Version + in: path + name: version + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "204": + description: No Content + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "404": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Not found + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + summary: Delete published entry + tags: + - v1 + /v1/me: + get: + description: Returns the authenticated caller's identity and roles + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/internal_api_v1.meResponse' + description: Caller identity and roles + "401": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Unauthorized + summary: Get current user info + tags: + - v1 + /v1/registries: + get: + description: List all registries + requestBody: + content: + application/json: + schema: + type: object + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/internal_api_v1.registryListResponse' + description: Registries list + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + summary: List registries + tags: + - v1 + /v1/registries/{name}: + delete: + description: Delete a registry by name + parameters: + - description: Registry Name + in: path + name: name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "204": + description: Registry deleted + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "403": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Cannot modify config-created registry + "404": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Registry not found + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + summary: Delete registry + tags: + - v1 + get: + description: Get a registry by name + parameters: + - description: Registry Name + in: path + name: name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.RegistryInfo' + description: Registry details + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "404": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Registry not found + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + summary: Get registry + tags: + - v1 + put: + description: Create a new registry or update an existing one + parameters: + - description: Registry Name + in: path + name: name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.RegistryInfo' + description: Registry updated + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.RegistryInfo' + description: Registry created + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "403": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Cannot modify config-created registry + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + summary: Create or update registry + tags: + - v1 + /v1/registries/{name}/entries: + get: + description: List all entries for a registry + parameters: + - description: Registry Name + in: path + name: name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.RegistryEntriesResponse' + description: Registry entries + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "404": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Registry not found + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + summary: List registry entries + tags: + - v1 + /v1/sources: + get: + description: List all sources + requestBody: + content: + application/json: + schema: + type: object + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.SourceListResponse' + description: Sources list + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + summary: List sources + tags: + - v1 + /v1/sources/{name}: + delete: + description: Delete a source by name + parameters: + - description: Source Name + in: path + name: name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "204": + description: Source deleted + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "403": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Cannot modify config-created source + "404": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Source not found + "409": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Source in use + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + summary: Delete source + tags: + - v1 + get: + description: Get a source by name + parameters: + - description: Source Name + in: path + name: name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.SourceInfo' + description: Source details + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "404": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Source not found + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + summary: Get source + tags: + - v1 + put: + description: Create a new source or update an existing one + parameters: + - description: Source Name + in: path + name: name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.SourceInfo' + description: Source updated + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.SourceInfo' + description: Source created + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "403": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Cannot modify config-created source + "409": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Managed source limit reached + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + summary: Create or update source + tags: + - v1 + /v1/sources/{name}/entries: + get: + description: List all entries for a source + parameters: + - description: Source Name + in: path + name: name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/github_com_stacklok_toolhive-registry-server_internal_service.SourceEntriesResponse' + description: Source entries + "400": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Bad request + "404": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Source not found + "500": + content: + application/json: + schema: + additionalProperties: + type: string + type: object + description: Internal server error + summary: List source entries + tags: + - v1 From 0aea8d8a29977a48d0e84a1f2e07187dc14b7c81 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Tue, 21 Apr 2026 20:15:34 +0300 Subject: [PATCH 07/16] Unify toolhive under the Renovate-driven content pipeline Collapses update-toolhive-reference.yml into upstream-release-docs.yml so all four tracked projects share one trigger path, one workflow, one PR per release. Renovate now watches toolhive's version: the same as the others; the content workflow runs a conditional reference-regen step for toolhive only. Also generalizes the `assets:` schema to support three source types: - source: (file-in-clone copy) - release_asset: (gh release download) - release_asset: , extract: tar-gz (download + extract) toolhive's swagger and CLI-docs tarball move from the shell script into declarative `assets:` entries. The shell script shrinks to just the CRD MDX generation + toolhive-core schema download (the parts that need custom transforms). It reuses the shallow clone the content workflow already makes. Changes: - Delete .github/workflows/update-toolhive-reference.yml - Remove workflow_call + dual concurrency groups from the content workflow (no chain seam needed without a second workflow) - Remove the Renovate "disable for stacklok/toolhive" packageRule - Extend sync-assets.mjs with release_asset + extract support - Slim update-toolhive-reference.sh; accept TOOLHIVE_CLONE_DIR to reuse the workflow's shallow clone Trade-off: toolhive reference docs now inherit Renovate's 24h minimumReleaseAge. Previously they published within minutes of a release. Acceptable for content coherence; set minimumReleaseAge: 0 per-dep if sub-hour latency becomes needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/upstream-projects.yaml | 34 +-- .../workflows/update-toolhive-reference.yml | 224 ------------------ .github/workflows/upstream-release-docs.yml | 78 +++--- renovate.json | 9 +- scripts/update-toolhive-reference.sh | 93 +++----- scripts/upstream-release/sync-assets.mjs | 145 ++++++++++-- 6 files changed, 217 insertions(+), 366 deletions(-) delete mode 100644 .github/workflows/update-toolhive-reference.yml diff --git a/.github/upstream-projects.yaml b/.github/upstream-projects.yaml index 9aadf6c0..32cde546 100644 --- a/.github/upstream-projects.yaml +++ b/.github/upstream-projects.yaml @@ -1,24 +1,20 @@ # Human-maintained source-of-truth for upstream projects whose releases # trigger doc-update PRs. # -# Two automation paths read this file: +# Renovate watches `repo:` + `version:` pairs here as a custom-regex +# manager (see renovate.json) and opens a version-bump PR whenever an +# upstream releases a new tag. .github/workflows/upstream-release-docs.yml +# reacts to those PRs, shallow-clones the upstream, and adds +# source-verified content edits via the upstream-release-docs skill. # -# - stacklok/toolhive releases: .github/workflows/update-toolhive-reference.yml -# is triggered by repository_dispatch (from the ToolHive release CI) or -# workflow_dispatch. It regenerates reference assets, bumps this YAML, -# applies pin_files, and opens a single PR. Renovate is explicitly -# disabled for this dep (see renovate.json) to avoid racing. +# For `stacklok/toolhive` specifically, the same workflow also +# regenerates machine-generated reference assets (CLI help, Swagger, +# CRD schemas) via scripts/update-toolhive-reference.sh before the +# skill runs. # -# - All other projects (registry-server, studio, cloud-ui): -# Renovate watches `repo:` + `version:` pairs here as a custom-regex -# manager and opens version-bump PRs automatically. -# .github/workflows/upstream-release-docs.yml reacts to those PRs and -# adds source-verified content edits via the upstream-release-docs -# skill. -# -# Either way, Renovate or the reference workflow bumps `version:`. You can -# also edit it by hand (e.g., to backfill or reset) and dispatch the -# upstream-release-docs workflow manually. +# You can also edit `version:` by hand (to backfill or reset) and +# dispatch the upstream-release-docs workflow manually in bootstrap +# or retry mode. projects: - id: toolhive-registry-server @@ -43,6 +39,12 @@ projects: - docs/toolhive/guides-cli - docs/toolhive/guides-k8s - docs/toolhive/guides-vmcp + assets: + - release_asset: swagger.yaml + destination: static/api-specs/toolhive-api.yaml + - release_asset: thv-cli-docs.tar.gz + destination: docs/toolhive/reference/cli + extract: tar-gz - id: toolhive-studio repo: stacklok/toolhive-studio diff --git a/.github/workflows/update-toolhive-reference.yml b/.github/workflows/update-toolhive-reference.yml deleted file mode 100644 index 73f49eda..00000000 --- a/.github/workflows/update-toolhive-reference.yml +++ /dev/null @@ -1,224 +0,0 @@ -name: Update ToolHive Reference Docs - -# Handles stacklok/toolhive releases end-to-end. Regenerates the -# machine-generated reference assets (CLI help, Swagger, CRD schemas), -# bumps the toolhive entry in .github/upstream-projects.yaml, applies -# pin_files substitutions, and opens a PR carrying the -# `upstream-content` label. The label triggers -# upstream-release-docs.yml to add skill-generated content edits to -# the same PR, producing one unified PR per toolhive release. -# -# The other three tracked projects (registry-server, studio, -# cloud-ui) don't produce reference artifacts this docs site consumes; -# they're handled by Renovate + upstream-release-docs.yml alone. - -on: - workflow_dispatch: - inputs: - version: - description: 'ToolHive version to update reference docs for' - required: true - default: 'latest' - assign_to: - description: 'GitHub username to request PR review from (optional)' - required: false - repository_dispatch: - types: [published-release] - -permissions: - contents: write - pull-requests: write - -concurrency: - # Distinct from upstream-release-docs-content so that when this - # workflow calls the content workflow via workflow_call, the nested - # run doesn't block waiting for this one's concurrency slot. - group: upstream-release-docs-reference - cancel-in-progress: false - -jobs: - update-reference: - runs-on: ubuntu-latest - outputs: - pr_number: ${{ steps.cpr.outputs.pull-request-number }} - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup - uses: ./.github/actions/setup - - - name: Set up Git - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - - - name: Determine version - id: get-version - run: | - if [[ "${{ github.event_name }}" == "repository_dispatch" ]]; then - echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT - else - echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT - fi - - - name: Determine reviewer - id: get-reviewer - run: | - if [[ "${{ github.event_name }}" == "repository_dispatch" ]]; then - REVIEWER="${{ github.event.client_payload.assign_to }}" - else - REVIEWER="${{ github.event.inputs.assign_to }}" - fi - # Filter out github-actions bot and stacklokbot (can't be requested as reviewers) - if [[ "$REVIEWER" =~ ^github-actions ]] || [[ "$REVIEWER" == "stacklokbot" ]]; then - REVIEWER="" - fi - echo "assign_to=$REVIEWER" >> $GITHUB_OUTPUT - - - name: Run update script - id: imports - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ steps.get-version.outputs.version }} - run: | - chmod +x scripts/update-toolhive-reference.sh - if ! scripts/update-toolhive-reference.sh "$VERSION"; then - echo "::error::Failed to update ToolHive reference docs" - exit 1 - fi - - - name: Resolve tag for YAML bump - id: resolve - env: - INPUT_VERSION: ${{ steps.imports.outputs.version }} - FALLBACK_VERSION: ${{ steps.get-version.outputs.version }} - run: | - # scripts/update-toolhive-reference.sh emits `version` to - # GITHUB_OUTPUT after resolving `latest` to a concrete tag. - # Fall back to the raw input if something unexpected happened. - TAG="${INPUT_VERSION:-$FALLBACK_VERSION}" - if [ -z "$TAG" ] || [ "$TAG" = "latest" ]; then - echo "::error::Could not resolve a concrete tag for the YAML bump (got '$TAG')." - exit 1 - fi - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - - - name: Bump YAML + apply pin_files for toolhive - env: - TAG: ${{ steps.resolve.outputs.tag }} - run: | - # No-ops cleanly if already at this tag (rerun safety). - if ! node scripts/upstream-release/bump-yaml.mjs \ - --id toolhive --tag "$TAG" 2>/tmp/bump.err; then - if grep -q 'already pinned' /tmp/bump.err; then - echo "YAML already at $TAG; continuing." - else - cat /tmp/bump.err >&2 - exit 1 - fi - fi - node scripts/upstream-release/apply-pin-files.mjs \ - --id toolhive --tag "$TAG" - - - name: Check for changes - id: git-diff - run: | - git add . - if git diff --cached --quiet; then - echo "No changes to commit." - echo "changed=false" >> $GITHUB_OUTPUT - else - echo "Changes detected." - echo "changed=true" >> $GITHUB_OUTPUT - fi - - - name: Detect newly added CRDs - id: new-crds - if: steps.git-diff.outputs.changed == 'true' - run: | - # A new upstream CRD results in an added *.schema.json under - # static/api-specs/crds/. New CRDs publish automatically with - # schema-derived prose; flag them so reviewers can decide whether - # to add a hand-written override in scripts/lib/crd-intros.mjs. - NEW=() - while IFS= read -r schema_file; do - [ -z "$schema_file" ] && continue - kind=$(jq -r '."x-kubernetes-kind"' "$schema_file") - NEW+=("$kind") - done < <(git diff --cached --name-only --diff-filter=A -- 'static/api-specs/crds/*.schema.json') - - if [ ${#NEW[@]} -gt 0 ]; then - { - echo "note_block< [!NOTE]" - echo "> **New CRD(s) in this release.** The following Kind(s) were" - echo "> added upstream and have been auto-published with" - echo "> schema-derived prose:" - echo ">" - for kind in "${NEW[@]}"; do - echo "> - \`$kind\`" - done - echo ">" - echo "> This does not block the release. To polish the page(s)," - echo "> add an override entry in \`scripts/lib/crd-intros.mjs\`" - echo "> (all fields optional) and push to this branch, or land a" - echo "> follow-up PR after merge." - echo "EOF" - } >> $GITHUB_OUTPUT - else - echo "note_block=" >> $GITHUB_OUTPUT - fi - - - name: Create Pull Request - id: cpr - if: steps.git-diff.outputs.changed == 'true' - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8 - with: - branch: docs/upstream-toolhive-${{ steps.imports.outputs.version }} - title: | - Update toolhive docs for ${{ steps.imports.outputs.version }} - body: | - This PR was auto-generated from ToolHive release **${{ steps.imports.outputs.version }}**.${{ steps.get-reviewer.outputs.assign_to != '' && format(' @{0} is tagged as the person who cut this release.', steps.get-reviewer.outputs.assign_to) || '' }} - ${{ steps.new-crds.outputs.note_block }} - - ## What's in this PR - - **Machine-generated reference files** (built from the ToolHive source at tag `${{ steps.imports.outputs.version }}`): - - - **CLI reference** (`docs/toolhive/reference/cli/`) - help text for every `thv` command - - **REST API spec** (`static/api-specs/toolhive-api.yaml`) - OpenAPI/Swagger spec - - **CRD reference** (`docs/toolhive/reference/crds/`, `static/api-specs/crds/`) - per-CRD MDX pages, extracted JSON schemas, and YAML examples - - **Registry JSON schemas** (`static/api-specs/*.schema.json`) - registry validation schemas - - **Version tracking**: - - - `.github/upstream-projects.yaml` - toolhive pin bumped to `${{ steps.imports.outputs.version }}` - - `pin_files` substitutions applied (e.g., `docusaurus.config.ts`) - - A follow-up commit from `upstream-release-docs.yml` will add source-verified content edits to the guide/concept pages and a "Content additions by upstream-release-docs" section below. Wait for it to land before reviewing. - commit-message: | - Update toolhive docs for ${{ steps.imports.outputs.version }} - labels: | - autogen-docs - upstream-content - reviewers: ${{ steps.get-reviewer.outputs.assign_to }} - delete-branch: true - sign-commits: true - - # Chains into the content workflow via workflow_call. We can't rely - # on the `pull_request: opened` event because peter-evans opens the - # PR with the default GITHUB_TOKEN, which by design does not trigger - # further workflow runs. Calling the reusable workflow directly is - # the supported escape hatch. - augment-content: - needs: update-reference - if: needs.update-reference.outputs.pr_number != '' - uses: ./.github/workflows/upstream-release-docs.yml - with: - pr_number: ${{ needs.update-reference.outputs.pr_number }} - secrets: inherit - permissions: - contents: write - pull-requests: write diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index 72c32abf..2403c19b 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -1,17 +1,24 @@ name: Upstream Release Docs # Reacts to Renovate-authored PRs that bump a `version:` in -# .github/upstream-projects.yaml. Runs the upstream-release-docs skill -# in multi-pass non-interactive mode to produce source-verified content -# edits, pushes them to the same PR branch, augments the PR body, and -# requests review from non-bot release contributors. +# .github/upstream-projects.yaml. For all four tracked projects +# (toolhive, toolhive-registry-server, toolhive-studio, +# toolhive-cloud-ui), the flow is identical: # -# Renovate is configured (renovate.json customManagers + packageRules) -# not to rebase these PRs, so we can push commits without force-push -# races. `rebaseWhen: never` + `recreateWhen: never` own that contract. +# 1. Renovate opens a version-bump PR +# 2. This workflow fires on the PR event, detects which project +# changed, shallow-clones the upstream at the new tag +# 3. For toolhive specifically, regenerates reference assets +# (CLI help, Swagger, CRD schemas) via the existing +# scripts/update-toolhive-reference.sh +# 4. Copies declared upstream assets into static/ +# 5. Runs the upstream-release-docs skill (3 passes) to produce +# source-verified content edits +# 6. Commits everything to the PR branch, augments the PR body, +# assigns reviewers from non-bot release contributors # -# Reference docs for stacklok/toolhive are handled separately by -# update-toolhive-reference.yml and are explicitly out of scope. +# Renovate is configured with rebaseWhen: never + recreateWhen: never +# so we can push commits without force-push races. on: pull_request: @@ -34,42 +41,26 @@ on: description: 'Bootstrap a new PR: the upstream tag to document (e.g. v1.3.0). Requires project_id.' required: false type: string - # Reusable-workflow entry point. Called by update-toolhive-reference.yml - # after it opens its PR via peter-evans — PRs opened with the default - # GITHUB_TOKEN do not fire pull_request workflows, so we cross the seam - # via workflow_call instead of the event chain. - workflow_call: - inputs: - pr_number: - description: 'PR number to augment.' - required: true - type: string permissions: contents: write pull-requests: write concurrency: - # Distinct from update-toolhive-reference.yml's group so a workflow_call - # from that workflow (which holds its own group) doesn't deadlock when - # this one also tries to acquire the same group. - group: upstream-release-docs-content + # Workflow-level group so two simultaneous upstream releases don't + # run the skill in parallel on shared concept pages. + group: upstream-release-docs cancel-in-progress: false jobs: augment: runs-on: ubuntu-latest timeout-minutes: 90 - # Gate: three accepted entry points. + # Gate: two accepted entry points. # - pull_request: Renovate-authored PR with upstream-content label. - # (The toolhive reference workflow can't reach here because PRs - # opened with the default GITHUB_TOKEN don't fire pull_request - # events; the reference workflow reaches us via workflow_call.) # - workflow_dispatch: human manually retries or bootstraps. - # - workflow_call: the reference workflow invokes us with pr_number. if: | github.event_name == 'workflow_dispatch' || - github.event_name == 'workflow_call' || ( github.event_name == 'pull_request' && github.event.pull_request.user.login == 'renovate[bot]' && @@ -104,7 +95,6 @@ jobs: NEW_TAG_IN: ${{ github.event.inputs.new_tag }} EVENT_PR: ${{ github.event.pull_request.number }} EVENT_HEAD_REF: ${{ github.event.pull_request.head.ref }} - CALL_PR: ${{ inputs.pr_number }} run: | case "$EVENT" in pull_request) @@ -112,13 +102,6 @@ jobs: HEAD_REF="$EVENT_HEAD_REF" MODE="react" ;; - workflow_call) - # Invoked by update-toolhive-reference.yml after it opens - # its PR. Trust the caller; we already share a repository. - PR_NUMBER="$CALL_PR" - HEAD_REF=$(gh pr view "$PR_NUMBER" --json headRefName --jq .headRefName) - MODE="react" - ;; workflow_dispatch) if [ -n "$DISPATCH_PR" ]; then PR_NUMBER="$DISPATCH_PR" @@ -285,14 +268,31 @@ jobs: "$SCRATCH/upstream" echo "scratch_dir=$SCRATCH/upstream" >> "$GITHUB_OUTPUT" - - name: Sync declared assets from upstream clone + - name: Sync declared assets env: PROJECT_ID: ${{ steps.detect.outputs.id }} CLONE_DIR: ${{ steps.clone.outputs.scratch_dir }} + REPO: ${{ steps.detect.outputs.repo }} + NEW_TAG: ${{ steps.detect.outputs.new_tag }} run: | node scripts/upstream-release/sync-assets.mjs \ --id "$PROJECT_ID" \ - --clone "$CLONE_DIR" + --clone "$CLONE_DIR" \ + --repo "$REPO" \ + --tag "$NEW_TAG" + + # toolhive ships CRD manifests in its repo (not as release assets) + # and needs custom MDX generation. It also pulls toolhive-core + # JSON schemas via a go.mod-derived version. Both are handled + # here; reuses the shallow clone to skip a redundant clone. + - name: Regenerate ToolHive CRD + core schema reference + if: steps.detect.outputs.id == 'toolhive' + env: + NEW_TAG: ${{ steps.detect.outputs.new_tag }} + TOOLHIVE_CLONE_DIR: ${{ steps.clone.outputs.scratch_dir }} + run: | + chmod +x scripts/update-toolhive-reference.sh + scripts/update-toolhive-reference.sh "$NEW_TAG" - name: Extract reviewers from release compare id: reviewers diff --git a/renovate.json b/renovate.json index bc88fc52..37ac9afb 100644 --- a/renovate.json +++ b/renovate.json @@ -96,15 +96,8 @@ "recreateWhen": "never", "commitMessageTopic": "{{depName}}", "prBodyNotes": [ - "After this PR opens, `.github/workflows/upstream-release-docs.yml` adds source-verified content edits for the new release." + "After this PR opens, `.github/workflows/upstream-release-docs.yml` adds source-verified content edits for the new release. For `stacklok/toolhive`, the same workflow also regenerates reference docs (CLI help, Swagger, CRD schemas)." ] - }, - { - "description": "stacklok/toolhive releases are handled end-to-end by update-toolhive-reference.yml (reference regen + content), which bumps the YAML itself. Disable the Renovate path for this dep to avoid racing or opening a duplicate PR.", - "matchManagers": ["custom.regex"], - "matchFileNames": ["**/.github/upstream-projects.yaml"], - "matchDepNames": ["stacklok/toolhive"], - "enabled": false } ] } diff --git a/scripts/update-toolhive-reference.sh b/scripts/update-toolhive-reference.sh index 4ce40e63..336c02f2 100755 --- a/scripts/update-toolhive-reference.sh +++ b/scripts/update-toolhive-reference.sh @@ -2,31 +2,42 @@ # SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc. # SPDX-License-Identifier: Apache-2.0 +# Generates the toolhive-specific reference artifacts that don't fit +# the declarative `assets:` schema in .github/upstream-projects.yaml: +# +# - Per-CRD MDX reference pages (from toolhive's operator Helm chart) +# - Per-CRD JSON schemas + examples + index +# - toolhive-core JSON schemas (derived from toolhive's go.mod pin) +# +# The simpler asset handling (CLI help tarball + Swagger) now lives in +# .github/upstream-projects.yaml under assets: and is handled by +# scripts/upstream-release/sync-assets.mjs. +# +# Usage: +# scripts/update-toolhive-reference.sh +# +# Env: +# TOOLHIVE_CLONE_DIR Path to an already-checked-out toolhive repo at +# the target tag. If unset, the script clones into +# a temp directory (backward compat for ad-hoc runs). + set -euo pipefail REPO_ROOT=$(git rev-parse --show-toplevel) cd "$REPO_ROOT" -DOCS_DIR="./docs" STATIC_DIR="./static" -CLI_DOCS_DST="${DOCS_DIR}/toolhive/reference/cli" -API_SPEC_DST="${STATIC_DIR}/api-specs/toolhive-api.yaml" REGISTRY_SCHEMA_DST="${STATIC_DIR}/api-specs/toolhive-legacy-registry.schema.json" UPSTREAM_REGISTRY_SCHEMA_DST="${STATIC_DIR}/api-specs/upstream-registry.schema.json" REGISTRY_META_SCHEMA_DST="${STATIC_DIR}/api-specs/publisher-provided.schema.json" SKILL_SCHEMA_DST="${STATIC_DIR}/api-specs/skill.schema.json" -if [ ! -d "$DOCS_DIR" ]; then - echo "Docs directory does not exist: $DOCS_DIR" - exit 1 -fi if [ ! -d "$STATIC_DIR" ]; then echo "Static directory does not exist: $STATIC_DIR" exit 1 fi -# Check if gh CLI is installed if ! command -v gh >/dev/null 2>&1; then echo "Error: 'gh' is required but not installed." exit 1 @@ -34,7 +45,6 @@ fi VERSION=$(echo "${1:-}" | tr -cd '[:alnum:].-') -# Resolve to actual tag if "latest" or unset if [ -z "$VERSION" ] || [ "$VERSION" = "latest" ]; then echo "No tag specified or 'latest' specified, resolving latest release..." VERSION=$(gh release view --repo stacklok/toolhive --json tagName --jq '.tagName') @@ -43,44 +53,21 @@ else echo "Using specified tag: $VERSION" fi -# Output the release version for use in CI workflows (if running in GitHub Actions) -if [ -n "${GITHUB_OUTPUT:-}" ]; then - echo "version=$VERSION" >> "$GITHUB_OUTPUT" -fi - DOWNLOAD_DIR=$(mktemp -d) trap 'rm -rf "$DOWNLOAD_DIR"' EXIT -## ToolHive assets -echo "Downloading ToolHive release assets for ${VERSION}..." - -gh release download "$VERSION" \ - --repo stacklok/toolhive \ - --pattern "thv-cli-docs.tar.gz" \ - --pattern "swagger.yaml" \ - --dir "$DOWNLOAD_DIR" - -## CLI reference -echo "Updating ToolHive CLI reference documentation in ${CLI_DOCS_DST}" -# Remove existing CLI reference documentation files in case we remove any commands -rm -f "${CLI_DOCS_DST}"/thv_*.md -tar -xzf "${DOWNLOAD_DIR}/thv-cli-docs.tar.gz" -C "${CLI_DOCS_DST}" -echo "CLI reference documentation updated successfully" - -## API reference -echo "Updating ToolHive API reference at ${API_SPEC_DST}" -cp "${DOWNLOAD_DIR}/swagger.yaml" "${API_SPEC_DST}" -echo "API reference updated successfully" +## Clone toolhive at the tag (or reuse the one the caller already has) +if [ -n "${TOOLHIVE_CLONE_DIR:-}" ] && [ -d "$TOOLHIVE_CLONE_DIR" ]; then + echo "Reusing caller-provided toolhive clone: $TOOLHIVE_CLONE_DIR" +else + TOOLHIVE_CLONE_DIR="${DOWNLOAD_DIR}/toolhive" + echo "Cloning ToolHive at ${VERSION} into ${TOOLHIVE_CLONE_DIR}..." + git clone --depth 1 --branch "$VERSION" \ + https://github.com/stacklok/toolhive.git \ + "$TOOLHIVE_CLONE_DIR" +fi ## CRD API reference -# Fetch the CRD manifests for this release and generate per-CRD schema files -# and MDX pages. CRDs live in the operator Helm chart inside the repo, so we -# clone the tag shallowly rather than using release assets. -echo "Fetching ToolHive CRD manifests for ${VERSION}..." -TOOLHIVE_CLONE_DIR="${DOWNLOAD_DIR}/toolhive" -git clone --depth 1 --branch "$VERSION" \ - https://github.com/stacklok/toolhive.git \ - "$TOOLHIVE_CLONE_DIR" CRD_SRC="${TOOLHIVE_CLONE_DIR}/deploy/charts/operator-crds/files/crds" echo "Extracting CRD schemas and examples..." @@ -90,10 +77,10 @@ echo "Generating CRD reference pages..." node "${REPO_ROOT}/scripts/generate-crd-pages.mjs" echo "CRD API reference updated successfully" -## Derive toolhive-core version from go.mod at the tagged commit +## toolhive-core version + schemas echo "Determining toolhive-core version from go.mod at tag ${VERSION}..." -CORE_VERSION=$(gh api "repos/stacklok/toolhive/contents/go.mod?ref=${VERSION}" \ - --jq '.content' | base64 -d | grep 'github.com/stacklok/toolhive-core' | awk '{print $2}' | head -1) +CORE_VERSION=$(grep 'github.com/stacklok/toolhive-core' "${TOOLHIVE_CLONE_DIR}/go.mod" \ + | awk '{print $2}' | head -1) if [ -z "$CORE_VERSION" ]; then echo "Warning: Could not determine toolhive-core version from go.mod; falling back to latest release" @@ -101,9 +88,7 @@ if [ -z "$CORE_VERSION" ]; then fi echo "Using toolhive-core version: ${CORE_VERSION}" -## toolhive-core schema assets echo "Downloading toolhive-core schema assets for ${CORE_VERSION}..." - gh release download "$CORE_VERSION" \ --repo stacklok/toolhive-core \ --pattern "toolhive-legacy-registry.schema.json" \ @@ -113,19 +98,13 @@ gh release download "$CORE_VERSION" \ --dir "$DOWNLOAD_DIR" cp "${DOWNLOAD_DIR}/toolhive-legacy-registry.schema.json" "${REGISTRY_SCHEMA_DST}" -echo "Registry JSON schema updated successfully" - cp "${DOWNLOAD_DIR}/upstream-registry.schema.json" "${UPSTREAM_REGISTRY_SCHEMA_DST}" -echo "Upstream registry JSON schema updated successfully" +cp "${DOWNLOAD_DIR}/publisher-provided.schema.json" "${REGISTRY_META_SCHEMA_DST}" +cp "${DOWNLOAD_DIR}/skill.schema.json" "${SKILL_SCHEMA_DST}" +echo "toolhive-core JSON schemas updated successfully" # Bundle the upstream schema to resolve remote $ref references echo "Bundling upstream registry schema (resolving remote references)..." node "${REPO_ROOT}/scripts/bundle-upstream-schema.mjs" -cp "${DOWNLOAD_DIR}/publisher-provided.schema.json" "${REGISTRY_META_SCHEMA_DST}" -echo "Registry extensions JSON schema updated successfully" - -cp "${DOWNLOAD_DIR}/skill.schema.json" "${SKILL_SCHEMA_DST}" -echo "Skill JSON schema updated successfully" - -echo "Release processing completed for: $VERSION (toolhive-core: $CORE_VERSION)" \ No newline at end of file +echo "Release processing completed for: $VERSION (toolhive-core: $CORE_VERSION)" diff --git a/scripts/upstream-release/sync-assets.mjs b/scripts/upstream-release/sync-assets.mjs index 05c2edaa..bc1213b9 100644 --- a/scripts/upstream-release/sync-assets.mjs +++ b/scripts/upstream-release/sync-assets.mjs @@ -2,20 +2,32 @@ // SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc. // SPDX-License-Identifier: Apache-2.0 -// Copies declared upstream assets from the shallow-clone directory into -// the docs repo for the given project. Declared per-project in -// .github/upstream-projects.yaml under `assets:`: +// Copies declared upstream assets into the docs repo for the given +// project. Assets are declared per-project in +// .github/upstream-projects.yaml under `assets:`. Three source kinds: // -// assets: -// - source: -// destination: +// - source: # file in the shallow clone +// destination: +// +// - release_asset: # GitHub release asset +// destination: +// +// - release_asset: +// destination: +// extract: tar-gz # extract tarball into dir // // Usage: -// node sync-assets.mjs --id --clone +// node sync-assets.mjs --id --clone [--repo ] +// +// `--clone` is required for `source:` entries (copies from the clone). +// `--repo` defaults to the project's repo from the YAML and is used +// for `release_asset:` downloads. // // No-op if the project declares no assets. +import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; import yaml from 'yaml'; @@ -30,21 +42,102 @@ function parseArgs(argv) { } else if (argv[i] === '--clone') { args.clone = argv[i + 1]; i++; + } else if (argv[i] === '--repo') { + args.repo = argv[i + 1]; + i++; + } else if (argv[i] === '--tag') { + args.tag = argv[i + 1]; + i++; } } return args; } -function main() { - const { id, clone } = parseArgs(process.argv.slice(2)); - if (!id || !clone) { +function syncFromClone(asset, cloneDir) { + const { source, destination } = asset; + const srcPath = path.join(cloneDir, source); + if (!fs.existsSync(srcPath)) { + console.error( + `Source not found in clone: ${source} (resolved to ${srcPath})` + ); + process.exit(1); + } + fs.mkdirSync(path.dirname(destination), { recursive: true }); + fs.copyFileSync(srcPath, destination); + console.log(`assets: synced ${source} -> ${destination}`); +} + +function syncFromReleaseAsset(asset, repo, tag) { + const { release_asset: assetName, destination, extract } = asset; + if (!tag) { console.error( - 'Usage: sync-assets.mjs --id --clone ' + `release_asset entries require --tag; got: ${JSON.stringify(asset)}` ); process.exit(1); } - if (!fs.existsSync(clone) || !fs.statSync(clone).isDirectory()) { - console.error(`Clone directory not found: ${clone}`); + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sync-assets-')); + try { + // gh release download writes the asset verbatim into --dir. + execFileSync( + 'gh', + [ + 'release', + 'download', + tag, + '--repo', + repo, + '--pattern', + assetName, + '--dir', + tmp, + ], + { stdio: 'inherit' } + ); + const downloadedPath = path.join(tmp, assetName); + if (!fs.existsSync(downloadedPath)) { + console.error( + `Download did not produce expected file: ${downloadedPath}` + ); + process.exit(1); + } + if (extract === 'tar-gz') { + fs.mkdirSync(destination, { recursive: true }); + // Strip pre-existing contents so removed upstream files are reflected. + for (const entry of fs.readdirSync(destination)) { + if (entry.startsWith('thv_') || entry.endsWith('.md')) { + fs.rmSync(path.join(destination, entry), { + recursive: true, + force: true, + }); + } + } + execFileSync('tar', ['-xzf', downloadedPath, '-C', destination], { + stdio: 'inherit', + }); + console.log( + `assets: extracted release_asset ${assetName} -> ${destination}/` + ); + } else if (extract) { + console.error(`Unsupported extract directive: ${extract}`); + process.exit(1); + } else { + fs.mkdirSync(path.dirname(destination), { recursive: true }); + fs.copyFileSync(downloadedPath, destination); + console.log( + `assets: downloaded release_asset ${assetName} -> ${destination}` + ); + } + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } +} + +function main() { + const { id, clone, repo: repoArg, tag } = parseArgs(process.argv.slice(2)); + if (!id) { + console.error( + 'Usage: sync-assets.mjs --id --clone [--tag ] [--repo ]' + ); process.exit(1); } @@ -61,24 +154,32 @@ function main() { return; } + const repo = repoArg || project.repo; + const resolvedTag = tag || project.version; + for (const asset of assets) { - const { source, destination } = asset; - if (!source || !destination) { + if (!asset.destination) { console.error( - `Asset entry missing source or destination: ${JSON.stringify(asset)}` + `Asset entry missing destination: ${JSON.stringify(asset)}` ); process.exit(1); } - const srcPath = path.join(clone, source); - if (!fs.existsSync(srcPath)) { + if (asset.source) { + if (!clone) { + console.error( + `Asset with source: needs --clone. Entry: ${JSON.stringify(asset)}` + ); + process.exit(1); + } + syncFromClone(asset, clone); + } else if (asset.release_asset) { + syncFromReleaseAsset(asset, repo, resolvedTag); + } else { console.error( - `Source not found in clone: ${source} (resolved to ${srcPath})` + `Asset entry has neither source nor release_asset: ${JSON.stringify(asset)}` ); process.exit(1); } - fs.mkdirSync(path.dirname(destination), { recursive: true }); - fs.copyFileSync(srcPath, destination); - console.log(`assets: synced ${source} -> ${destination}`); } } From 1fe69e17327d83a78eabc5f3a25c2e96a5f5a593 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Tue, 21 Apr 2026 21:00:31 +0300 Subject: [PATCH 08/16] Consume toolhive's new release assets, delete the shell script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assumes stacklok/toolhive#4982 has landed. That PR ships three new release assets on toolhive: - thv-crds.tar.gz (the 13 CRD YAMLs) - thv-cli-docs.tar.gz (already existed) - toolhive-*.schema.json (re-exported from toolhive-core at the go.mod-pinned version) And retires the repository_dispatch chain that previously triggered our reference-regen workflow. On our side, the shell wrapper script collapses entirely: - .github/upstream-projects.yaml: toolhive gains five new release_asset entries (swagger, CLI docs tarball with extract, and the four core schemas). sync-assets.mjs handles all of them declaratively. - .github/workflows/upstream-release-docs.yml: the old "run the shell script" step becomes an inline "download + extract CRD tarball, run our three Node helpers" step. Runs only for project_id=toolhive. - A new "Commit regenerated reference assets" step lands the regen as its own commit, so the autogen-detect step below sees only skill- introduced touches (prevents false-positive warnings). - scripts/update-toolhive-reference.sh deleted. Net effect after #4982 merges: - Zero repo clones for CRD processing (tarball replaces clone) - Zero cross-repo downloads (toolhive re-exports core schemas) - Zero shell scripts for release-doc regen - Three Node helpers stay (extract-crd-schemas, generate-crd-pages, bundle-upstream-schema) — they produce docs-specific outputs and genuinely belong on our side. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/upstream-projects.yaml | 20 +++- .github/workflows/upstream-release-docs.yml | 91 ++++++++++------ scripts/update-toolhive-reference.sh | 110 -------------------- 3 files changed, 77 insertions(+), 144 deletions(-) delete mode 100755 scripts/update-toolhive-reference.sh diff --git a/.github/upstream-projects.yaml b/.github/upstream-projects.yaml index 32cde546..1ac705af 100644 --- a/.github/upstream-projects.yaml +++ b/.github/upstream-projects.yaml @@ -7,10 +7,11 @@ # reacts to those PRs, shallow-clones the upstream, and adds # source-verified content edits via the upstream-release-docs skill. # -# For `stacklok/toolhive` specifically, the same workflow also -# regenerates machine-generated reference assets (CLI help, Swagger, -# CRD schemas) via scripts/update-toolhive-reference.sh before the -# skill runs. +# For `stacklok/toolhive` specifically, most reference artifacts are +# declared as `assets:` below (synced by sync-assets.mjs) — plus one +# workflow-step in upstream-release-docs.yml that downloads the CRD +# manifests tarball and runs extract-crd-schemas.mjs + generate-crd- +# pages.mjs to produce our opinionated per-CRD MDX pages. # # You can also edit `version:` by hand (to backfill or reset) and # dispatch the upstream-release-docs workflow manually in bootstrap @@ -45,6 +46,17 @@ projects: - release_asset: thv-cli-docs.tar.gz destination: docs/toolhive/reference/cli extract: tar-gz + # toolhive-core schemas re-exported by toolhive at release time + # (see stacklok/toolhive#4982). Previously downloaded from the + # toolhive-core repo via a go.mod-derived version lookup. + - release_asset: toolhive-legacy-registry.schema.json + destination: static/api-specs/toolhive-legacy-registry.schema.json + - release_asset: upstream-registry.schema.json + destination: static/api-specs/upstream-registry.schema.json + - release_asset: publisher-provided.schema.json + destination: static/api-specs/publisher-provided.schema.json + - release_asset: skill.schema.json + destination: static/api-specs/skill.schema.json - id: toolhive-studio repo: stacklok/toolhive-studio diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index 2403c19b..aea43ee4 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -8,10 +8,14 @@ name: Upstream Release Docs # 1. Renovate opens a version-bump PR # 2. This workflow fires on the PR event, detects which project # changed, shallow-clones the upstream at the new tag -# 3. For toolhive specifically, regenerates reference assets -# (CLI help, Swagger, CRD schemas) via the existing -# scripts/update-toolhive-reference.sh -# 4. Copies declared upstream assets into static/ +# 3. Syncs declared upstream assets into static/ via sync-assets.mjs +# (release-asset downloads, tarball extractions, file-from-clone +# copies — see `assets:` in upstream-projects.yaml) +# 4. For toolhive specifically, downloads the CRD manifests tarball +# and runs extract-crd-schemas.mjs + generate-crd-pages.mjs + +# bundle-upstream-schema.mjs to produce our opinionated reference +# MDX (the CRD tarball and toolhive-core schemas are shipped as +# release assets by stacklok/toolhive#4982) # 5. Runs the upstream-release-docs skill (3 passes) to produce # source-verified content edits # 6. Commits everything to the PR branch, augments the PR body, @@ -281,18 +285,44 @@ jobs: --repo "$REPO" \ --tag "$NEW_TAG" - # toolhive ships CRD manifests in its repo (not as release assets) - # and needs custom MDX generation. It also pulls toolhive-core - # JSON schemas via a go.mod-derived version. Both are handled - # here; reuses the shallow clone to skip a redundant clone. - - name: Regenerate ToolHive CRD + core schema reference + # toolhive ships CRD manifests as a release asset tarball (see + # stacklok/toolhive#4982). We extract it to a temp dir and run our + # opinionated transforms: extract per-CRD JSON schemas + examples, + # generate MDX reference pages. Bundling the upstream-registry + # schema (resolves the remote MCP-server $refs for our JSON-Schema + # viewer) also runs here. + - name: Extract CRDs + generate reference MDX (toolhive) if: steps.detect.outputs.id == 'toolhive' env: + REPO: ${{ steps.detect.outputs.repo }} NEW_TAG: ${{ steps.detect.outputs.new_tag }} - TOOLHIVE_CLONE_DIR: ${{ steps.clone.outputs.scratch_dir }} run: | - chmod +x scripts/update-toolhive-reference.sh - scripts/update-toolhive-reference.sh "$NEW_TAG" + TMP=$(mktemp -d) + gh release download "$NEW_TAG" --repo "$REPO" \ + --pattern "thv-crds.tar.gz" --dir "$TMP" + mkdir -p "$TMP/crds" + tar -xzf "$TMP/thv-crds.tar.gz" -C "$TMP/crds" + TOOLHIVE_CRD_DIR="$TMP/crds" node scripts/extract-crd-schemas.mjs + node scripts/generate-crd-pages.mjs + node scripts/bundle-upstream-schema.mjs + rm -rf "$TMP" + + # Commit the regenerated reference assets (sync-assets output + + # toolhive CRD MDX if applicable) before the skill runs. This + # keeps the skill's content commit clean and lets the autogen- + # detect step below distinguish skill touches from our own + # legitimate regen writes. + - name: Commit regenerated reference assets + env: + PROJECT_ID: ${{ steps.detect.outputs.id }} + NEW_TAG: ${{ steps.detect.outputs.new_tag }} + run: | + git add -A + if git diff --cached --quiet; then + echo "No reference changes for $PROJECT_ID $NEW_TAG." + else + git commit -m "Regenerate reference assets for $PROJECT_ID $NEW_TAG" + fi - name: Extract reviewers from release compare id: reviewers @@ -362,9 +392,10 @@ jobs: Follow the skill's own guidance on auto-generated reference files (Phase 4 step 5, Phase 4 step 6) -- do not hand-edit docs/toolhive/reference/cli/, static/api-specs/, or - docs/toolhive/reference/crds/. If a release genuinely needs - reference updates, note that in GAPS.md -- the separate - update-toolhive-reference.yml workflow handles those. + docs/toolhive/reference/crds/. Those are regenerated from + release assets by earlier steps in this workflow; if a + release genuinely needs hand-written reference updates, + note that in GAPS.md. PASS 2 -- Editorial re-review: Run /docs-review over every file you changed in Pass 1 and @@ -440,6 +471,8 @@ jobs: - name: Detect touches to auto-generated paths id: autogen + # Runs AFTER the skill and AFTER the regen commit above, so the + # staged diff represents skill-introduced changes only. run: | git add -A TOUCHED=$(git diff --cached --name-only -- \ @@ -453,29 +486,31 @@ jobs: echo "> The skill touched files under auto-generated paths:" echo "> \`$TOUCHED\`" echo ">" - echo "> These are normally maintained by update-toolhive-reference.yml." - echo "> Review the changes carefully and revert if they should come" - echo "> from the reference workflow instead." + echo "> These paths are regenerated from release assets earlier" + echo "> in this workflow. Review the skill's changes and revert" + echo "> them if they should come from the regen step instead." fi echo "AUTOGEN_EOF" } >> "$GITHUB_OUTPUT" - - name: Commit and push content + - name: Commit and push id: push env: PROJECT_ID: ${{ steps.detect.outputs.id }} NEW_TAG: ${{ steps.detect.outputs.new_tag }} HEAD_REF: ${{ steps.eff.outputs.head_ref }} run: | + # Stage any skill content and add a content commit if non-empty. + # A regen commit from the earlier step may also be waiting. git add -A - if git diff --cached --quiet; then - echo "No content changes to push." - echo "pushed=false" >> "$GITHUB_OUTPUT" - exit 0 + if ! git diff --cached --quiet; then + git commit -m "Add upstream-release-docs content for $PROJECT_ID $NEW_TAG" + else + echo "No skill content changes to commit." fi - git commit -m "Add upstream-release-docs content for $PROJECT_ID $NEW_TAG" + # Push whatever local commits are ahead of the remote — regen + # only, content only, or both. Empty push is a no-op. git push origin "HEAD:$HEAD_REF" - echo "pushed=true" >> "$GITHUB_OUTPUT" - name: Augment PR body (marker-delimited section) # Runs even if earlier steps soft-failed so the augmentation @@ -517,13 +552,9 @@ jobs: echo "$AUTOGEN_NOTE" echo "" fi - echo "### What's NOT in this PR" - echo "" - echo "Auto-generated reference files (\`docs/toolhive/reference/cli/\`, \`static/api-specs/\`, \`docs/toolhive/reference/crds/\`). Those are handled by \`update-toolhive-reference.yml\`. If this release has reference impact, that workflow opens a separate PR; merge it first, then rebase this one." - echo "" echo "### Review guidance" echo "" - echo "Unlike the reference-docs PR, this one contains hand-edited prose. Review for accuracy, not just style. If the \"Gaps needing human context\" section is populated, the skill deferred those sections to a human; fill them in before merging." + echo "Machine-generated reference files under \`docs/toolhive/reference/cli/\`, \`static/api-specs/\`, and \`docs/toolhive/reference/crds/\` are regenerated from upstream release assets (separate commit, titled \"Regenerate reference assets\") and should be spot-checked only. The \"Add upstream-release-docs content\" commit contains hand-edited prose; review that one for accuracy, not just style. If the \"Gaps needing human context\" section is populated, the skill deferred those sections to a human; fill them in before merging." echo "" if [ -n "$GAPS_BLOCK" ]; then echo "$GAPS_BLOCK" diff --git a/scripts/update-toolhive-reference.sh b/scripts/update-toolhive-reference.sh deleted file mode 100755 index 336c02f2..00000000 --- a/scripts/update-toolhive-reference.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/bin/bash -# SPDX-FileCopyrightText: Copyright 2026 Stacklok, Inc. -# SPDX-License-Identifier: Apache-2.0 - -# Generates the toolhive-specific reference artifacts that don't fit -# the declarative `assets:` schema in .github/upstream-projects.yaml: -# -# - Per-CRD MDX reference pages (from toolhive's operator Helm chart) -# - Per-CRD JSON schemas + examples + index -# - toolhive-core JSON schemas (derived from toolhive's go.mod pin) -# -# The simpler asset handling (CLI help tarball + Swagger) now lives in -# .github/upstream-projects.yaml under assets: and is handled by -# scripts/upstream-release/sync-assets.mjs. -# -# Usage: -# scripts/update-toolhive-reference.sh -# -# Env: -# TOOLHIVE_CLONE_DIR Path to an already-checked-out toolhive repo at -# the target tag. If unset, the script clones into -# a temp directory (backward compat for ad-hoc runs). - -set -euo pipefail - -REPO_ROOT=$(git rev-parse --show-toplevel) -cd "$REPO_ROOT" - -STATIC_DIR="./static" - -REGISTRY_SCHEMA_DST="${STATIC_DIR}/api-specs/toolhive-legacy-registry.schema.json" -UPSTREAM_REGISTRY_SCHEMA_DST="${STATIC_DIR}/api-specs/upstream-registry.schema.json" -REGISTRY_META_SCHEMA_DST="${STATIC_DIR}/api-specs/publisher-provided.schema.json" -SKILL_SCHEMA_DST="${STATIC_DIR}/api-specs/skill.schema.json" - -if [ ! -d "$STATIC_DIR" ]; then - echo "Static directory does not exist: $STATIC_DIR" - exit 1 -fi - -if ! command -v gh >/dev/null 2>&1; then - echo "Error: 'gh' is required but not installed." - exit 1 -fi - -VERSION=$(echo "${1:-}" | tr -cd '[:alnum:].-') - -if [ -z "$VERSION" ] || [ "$VERSION" = "latest" ]; then - echo "No tag specified or 'latest' specified, resolving latest release..." - VERSION=$(gh release view --repo stacklok/toolhive --json tagName --jq '.tagName') - echo "Resolved to: $VERSION" -else - echo "Using specified tag: $VERSION" -fi - -DOWNLOAD_DIR=$(mktemp -d) -trap 'rm -rf "$DOWNLOAD_DIR"' EXIT - -## Clone toolhive at the tag (or reuse the one the caller already has) -if [ -n "${TOOLHIVE_CLONE_DIR:-}" ] && [ -d "$TOOLHIVE_CLONE_DIR" ]; then - echo "Reusing caller-provided toolhive clone: $TOOLHIVE_CLONE_DIR" -else - TOOLHIVE_CLONE_DIR="${DOWNLOAD_DIR}/toolhive" - echo "Cloning ToolHive at ${VERSION} into ${TOOLHIVE_CLONE_DIR}..." - git clone --depth 1 --branch "$VERSION" \ - https://github.com/stacklok/toolhive.git \ - "$TOOLHIVE_CLONE_DIR" -fi - -## CRD API reference -CRD_SRC="${TOOLHIVE_CLONE_DIR}/deploy/charts/operator-crds/files/crds" - -echo "Extracting CRD schemas and examples..." -TOOLHIVE_CRD_DIR="$CRD_SRC" node "${REPO_ROOT}/scripts/extract-crd-schemas.mjs" - -echo "Generating CRD reference pages..." -node "${REPO_ROOT}/scripts/generate-crd-pages.mjs" -echo "CRD API reference updated successfully" - -## toolhive-core version + schemas -echo "Determining toolhive-core version from go.mod at tag ${VERSION}..." -CORE_VERSION=$(grep 'github.com/stacklok/toolhive-core' "${TOOLHIVE_CLONE_DIR}/go.mod" \ - | awk '{print $2}' | head -1) - -if [ -z "$CORE_VERSION" ]; then - echo "Warning: Could not determine toolhive-core version from go.mod; falling back to latest release" - CORE_VERSION=$(gh release view --repo stacklok/toolhive-core --json tagName --jq '.tagName') -fi -echo "Using toolhive-core version: ${CORE_VERSION}" - -echo "Downloading toolhive-core schema assets for ${CORE_VERSION}..." -gh release download "$CORE_VERSION" \ - --repo stacklok/toolhive-core \ - --pattern "toolhive-legacy-registry.schema.json" \ - --pattern "upstream-registry.schema.json" \ - --pattern "publisher-provided.schema.json" \ - --pattern "skill.schema.json" \ - --dir "$DOWNLOAD_DIR" - -cp "${DOWNLOAD_DIR}/toolhive-legacy-registry.schema.json" "${REGISTRY_SCHEMA_DST}" -cp "${DOWNLOAD_DIR}/upstream-registry.schema.json" "${UPSTREAM_REGISTRY_SCHEMA_DST}" -cp "${DOWNLOAD_DIR}/publisher-provided.schema.json" "${REGISTRY_META_SCHEMA_DST}" -cp "${DOWNLOAD_DIR}/skill.schema.json" "${SKILL_SCHEMA_DST}" -echo "toolhive-core JSON schemas updated successfully" - -# Bundle the upstream schema to resolve remote $ref references -echo "Bundling upstream registry schema (resolving remote references)..." -node "${REPO_ROOT}/scripts/bundle-upstream-schema.mjs" - -echo "Release processing completed for: $VERSION (toolhive-core: $CORE_VERSION)" From ef496840146ccd3a1a2536d9dba21cfd82726b9d Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Tue, 21 Apr 2026 21:22:40 +0300 Subject: [PATCH 09/16] Refine reference-asset language and attribute gaps to PR authors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two improvements from Dan Barr's review of PR #748: 1. Terminology: "regenerate" was overloaded. It now reads accurately: - "sync" for release-asset downloads / file copies (swagger, CLI tarball, 4 core schemas) — nothing is actually recreated - "regenerate" reserved for the toolhive CRD MDX step which truly transforms upstream manifests into our opinionated layout - "refresh" for the combined commit that may do both The commit that lands reference updates is now titled "Refresh reference assets for ". 2. Gap attribution: the skill's GAPS.md format now includes the PR number and @-mentions the PR author (non-bots only). Previously a gap routed to "everyone in the reviewer pool" via a label; now each gap directly pings the engineer who built the feature. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/upstream-release-docs.yml | 54 +++++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index aea43ee4..ff4ce7a8 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -307,12 +307,12 @@ jobs: node scripts/bundle-upstream-schema.mjs rm -rf "$TMP" - # Commit the regenerated reference assets (sync-assets output + - # toolhive CRD MDX if applicable) before the skill runs. This - # keeps the skill's content commit clean and lets the autogen- - # detect step below distinguish skill touches from our own - # legitimate regen writes. - - name: Commit regenerated reference assets + # Commit the refreshed reference assets (synced release-asset + # files + regenerated toolhive CRD MDX if applicable) before the + # skill runs. This keeps the skill's content commit clean and + # lets the autogen-detect step below distinguish skill touches + # from our own legitimate refresh writes. + - name: Commit refreshed reference assets env: PROJECT_ID: ${{ steps.detect.outputs.id }} NEW_TAG: ${{ steps.detect.outputs.new_tag }} @@ -321,7 +321,7 @@ jobs: if git diff --cached --quiet; then echo "No reference changes for $PROJECT_ID $NEW_TAG." else - git commit -m "Regenerate reference assets for $PROJECT_ID $NEW_TAG" + git commit -m "Refresh reference assets for $PROJECT_ID $NEW_TAG" fi - name: Extract reviewers from release compare @@ -387,15 +387,26 @@ jobs: For Phase 2 step 4 (context on major new features), SKIP writing the "why"/consumer narrative and append one bullet per gap to GAPS.md at repo root (create if missing). Each - bullet must name the feature and describe what context a - human needs to supply. + bullet MUST: + - Name the feature + - Reference the PR that introduced it, using the PR + number you found in Phase 2 deep-dive + - @-mention the PR author by their GitHub handle (skip + this for bot authors like renovate[bot] or + github-actions[bot]) + - Describe what context a human needs to supply + Format: `- Feature X (PR #123 by @alice): needs a user + story explaining who this is for and the expected + consumer workflow.` + This routes gaps to the engineer who built the feature + rather than the whole reviewer pool. Follow the skill's own guidance on auto-generated reference files (Phase 4 step 5, Phase 4 step 6) -- do not hand-edit docs/toolhive/reference/cli/, static/api-specs/, or - docs/toolhive/reference/crds/. Those are regenerated from - release assets by earlier steps in this workflow; if a - release genuinely needs hand-written reference updates, - note that in GAPS.md. + docs/toolhive/reference/crds/. Those are synced or + regenerated from release assets by earlier steps in this + workflow; if a release genuinely needs hand-written + reference updates, note that in GAPS.md. PASS 2 -- Editorial re-review: Run /docs-review over every file you changed in Pass 1 and @@ -471,8 +482,8 @@ jobs: - name: Detect touches to auto-generated paths id: autogen - # Runs AFTER the skill and AFTER the regen commit above, so the - # staged diff represents skill-introduced changes only. + # Runs AFTER the skill and AFTER the refresh commit above, so + # the staged diff represents skill-introduced changes only. run: | git add -A TOUCHED=$(git diff --cached --name-only -- \ @@ -486,9 +497,10 @@ jobs: echo "> The skill touched files under auto-generated paths:" echo "> \`$TOUCHED\`" echo ">" - echo "> These paths are regenerated from release assets earlier" - echo "> in this workflow. Review the skill's changes and revert" - echo "> them if they should come from the regen step instead." + echo "> These paths are synced or regenerated from release" + echo "> assets earlier in this workflow. Review the skill's" + echo "> changes and revert them if they should come from the" + echo "> refresh step instead." fi echo "AUTOGEN_EOF" } >> "$GITHUB_OUTPUT" @@ -501,14 +513,14 @@ jobs: HEAD_REF: ${{ steps.eff.outputs.head_ref }} run: | # Stage any skill content and add a content commit if non-empty. - # A regen commit from the earlier step may also be waiting. + # A refresh commit from the earlier step may also be waiting. git add -A if ! git diff --cached --quiet; then git commit -m "Add upstream-release-docs content for $PROJECT_ID $NEW_TAG" else echo "No skill content changes to commit." fi - # Push whatever local commits are ahead of the remote — regen + # Push whatever local commits are ahead of the remote — refresh # only, content only, or both. Empty push is a no-op. git push origin "HEAD:$HEAD_REF" @@ -554,7 +566,7 @@ jobs: fi echo "### Review guidance" echo "" - echo "Machine-generated reference files under \`docs/toolhive/reference/cli/\`, \`static/api-specs/\`, and \`docs/toolhive/reference/crds/\` are regenerated from upstream release assets (separate commit, titled \"Regenerate reference assets\") and should be spot-checked only. The \"Add upstream-release-docs content\" commit contains hand-edited prose; review that one for accuracy, not just style. If the \"Gaps needing human context\" section is populated, the skill deferred those sections to a human; fill them in before merging." + echo "Machine-generated reference files under \`docs/toolhive/reference/cli/\`, \`static/api-specs/\`, and \`docs/toolhive/reference/crds/\` are synced or regenerated from upstream release assets (separate commit, titled \"Refresh reference assets\") and should be spot-checked only. The \"Add upstream-release-docs content\" commit contains hand-edited prose; review that one for accuracy, not just style. If the \"Gaps needing human context\" section is populated, the skill deferred those sections to a human; fill them in before merging." echo "" if [ -n "$GAPS_BLOCK" ]; then echo "$GAPS_BLOCK" From ab0dc1f64b1bb86543c6ac15608190457c2f0be2 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Tue, 21 Apr 2026 21:53:28 +0300 Subject: [PATCH 10/16] Derive bootstrap base from dispatching branch, not a new input Simpler than adding a base_ref input: workflow_dispatch always runs against `github.ref_name` (the branch passed to --ref). For production, that's main. For pre-merge testing, dispatch from the feature branch with `gh workflow run --ref feat/branch` and the bootstrap flow branches from, bumps, and opens its PR against that branch. - Bootstrap checkout uses github.ref_name - Bootstrap PR's --base uses github.ref_name - eff step emits base_ref for detect-change.mjs to use as BASE_REF - pull_request trigger: event.pull_request.base.ref - workflow_dispatch retry: gh pr view --json baseRefName - workflow_dispatch bootstrap: github.ref_name Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/upstream-release-docs.yml | 51 ++++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index ff4ce7a8..c18536a0 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -141,14 +141,16 @@ jobs: echo "Mode: $MODE" # Bootstrap: a human manually dispatched with project_id + new_tag. - # Check out main, bump the YAML, create the PR, and emit its number - # + branch so the rest of the workflow proceeds as if Renovate had + # Check out the dispatching branch (main in production; any branch + # for pre-merge testing — just dispatch via `gh workflow run --ref + # `), bump the YAML, create the PR, and emit its number + + # branch so the rest of the workflow proceeds as if Renovate had # opened it. - - name: Checkout main for bootstrap + - name: Checkout dispatching branch for bootstrap if: steps.pr.outputs.mode == 'bootstrap' uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - ref: main + ref: ${{ github.ref_name }} fetch-depth: 0 - name: Setup (bootstrap) @@ -167,6 +169,7 @@ jobs: env: PROJECT_ID: ${{ github.event.inputs.project_id }} NEW_TAG: ${{ github.event.inputs.new_tag }} + BASE_REF: ${{ github.ref_name }} ACTOR: ${{ github.actor }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | @@ -200,7 +203,7 @@ jobs: sed -i 's/^ //' /tmp/bootstrap-body.md gh pr create \ - --base main \ + --base "$BASE_REF" \ --head "$BRANCH" \ --title "Update $PROJECT_ID to $NEW_TAG (manual dispatch)" \ --body-file /tmp/bootstrap-body.md \ @@ -211,23 +214,43 @@ jobs: echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT" echo "head_ref=$BRANCH" >> "$GITHUB_OUTPUT" - # Normalize the (possibly bootstrap-created) PR number + head_ref - # into a single step output so later steps reference one place. - - name: Resolve effective PR number + # Normalize the (possibly bootstrap-created) PR number, head_ref, + # and base_ref into a single step output so later steps reference + # one place. base_ref drives detect-change.mjs (the baseline to + # diff against) and the CRD-regen paths. + - name: Resolve effective PR metadata id: eff env: + EVENT: ${{ github.event_name }} PR_FROM_RESOLVE: ${{ steps.pr.outputs.number }} HEAD_FROM_RESOLVE: ${{ steps.pr.outputs.head_ref }} PR_FROM_BOOTSTRAP: ${{ steps.bootstrap.outputs.number }} HEAD_FROM_BOOTSTRAP: ${{ steps.bootstrap.outputs.head_ref }} + BASE_FROM_EVENT: ${{ github.event.pull_request.base.ref }} + DISPATCH_REF_NAME: ${{ github.ref_name }} run: | if [ -n "$PR_FROM_BOOTSTRAP" ]; then - echo "number=$PR_FROM_BOOTSTRAP" >> "$GITHUB_OUTPUT" - echo "head_ref=$HEAD_FROM_BOOTSTRAP" >> "$GITHUB_OUTPUT" + # Bootstrap mode: the PR was just created with --base set to + # the dispatching branch (main in production; feat branch + # when testing pre-merge). + NUMBER="$PR_FROM_BOOTSTRAP" + HEAD="$HEAD_FROM_BOOTSTRAP" + BASE="$DISPATCH_REF_NAME" + elif [ "$EVENT" = "pull_request" ]; then + NUMBER="$PR_FROM_RESOLVE" + HEAD="$HEAD_FROM_RESOLVE" + BASE="$BASE_FROM_EVENT" else - echo "number=$PR_FROM_RESOLVE" >> "$GITHUB_OUTPUT" - echo "head_ref=$HEAD_FROM_RESOLVE" >> "$GITHUB_OUTPUT" + # workflow_dispatch retry: look up the PR's base_ref. + NUMBER="$PR_FROM_RESOLVE" + HEAD="$HEAD_FROM_RESOLVE" + BASE=$(gh pr view "$NUMBER" --json baseRefName --jq .baseRefName) fi + { + echo "number=$NUMBER" + echo "head_ref=$HEAD" + echo "base_ref=$BASE" + } >> "$GITHUB_OUTPUT" - name: Checkout PR branch uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 @@ -245,6 +268,10 @@ jobs: - name: Detect changed project id: detect + env: + # Pass the PR's actual base (main in production; dispatching + # branch when bootstrap was run via gh workflow run --ref). + BASE_REF: origin/${{ steps.eff.outputs.base_ref }} run: node scripts/upstream-release/detect-change.mjs - name: Verify prev_tag exists upstream From ad4ba66af03c1e1a1760b3b9cf41f8e23f476788 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Tue, 21 Apr 2026 21:59:04 +0300 Subject: [PATCH 11/16] Drop labels; gate via bot-author + paths filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Labels were doing two jobs — neither worth the friction of pre-creating the labels in every repo that adopts this: 1. Gating which PRs trigger augmentation. Replaced with the existing pull_request paths: filter (only YAML edits) + user.login check (must be renovate[bot]). Human PRs editing the YAML are out of scope; they should be reviewed normally without skill augmentation. 2. Human filtering of "failed" / "needs-context" PRs. Replaced with PR body sections and a failure-path PR comment that carries the run URL. Changes: - Drop labels from Renovate packageRule in renovate.json. - Drop --label from bootstrap gh pr create. - Drop "Add needs-human-context label" step (gaps already shown in PR body). - Simplify failure step to comment-only; drop upstream-docs-failed label. The PR comment already includes the run URL + retry hint. - Drop labeled trigger type; no longer needed without label-race concerns. - Drop label check from workflow_dispatch retry validation. Also unblocks the first real dispatch, which failed on "upstream-content not found" since the label was never pre-created. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/upstream-release-docs.yml | 42 +++++---------------- renovate.json | 1 - 2 files changed, 10 insertions(+), 33 deletions(-) diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index c18536a0..8854954e 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -26,9 +26,7 @@ name: Upstream Release Docs on: pull_request: - # `labeled` is included to close the race where Renovate's - # labels aren't yet on the PR at the `opened` event payload. - types: [opened, reopened, labeled] + types: [opened, reopened] paths: - '.github/upstream-projects.yaml' workflow_dispatch: @@ -61,14 +59,16 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 90 # Gate: two accepted entry points. - # - pull_request: Renovate-authored PR with upstream-content label. + # - pull_request: Renovate-authored PR touching upstream-projects.yaml + # (the `paths:` filter on the trigger already narrows to YAML edits). # - workflow_dispatch: human manually retries or bootstraps. + # Human-authored PRs that happen to edit the YAML are out of scope and + # should be reviewed normally without skill augmentation. if: | github.event_name == 'workflow_dispatch' || ( github.event_name == 'pull_request' && - github.event.pull_request.user.login == 'renovate[bot]' && - contains(github.event.pull_request.labels.*.name, 'upstream-content') + github.event.pull_request.user.login == 'renovate[bot]' ) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -111,7 +111,6 @@ jobs: PR_NUMBER="$DISPATCH_PR" HEAD_REF=$(gh pr view "$PR_NUMBER" --json headRefName --jq .headRefName) AUTHOR=$(gh pr view "$PR_NUMBER" --json author --jq '.author.login') - LABELS=$(gh pr view "$PR_NUMBER" --json labels --jq '[.labels[].name] | join(",")') case "$AUTHOR" in app/renovate|renovate[bot]|app/github-actions|github-actions[bot]) ;; @@ -120,10 +119,6 @@ jobs: exit 1 ;; esac - if ! echo "$LABELS" | grep -q 'upstream-content'; then - echo "::error::PR #$PR_NUMBER does not carry the upstream-content label (labels=$LABELS)." - exit 1 - fi MODE="retry" else # Bootstrap: branch and PR are created in the next step. @@ -190,8 +185,6 @@ jobs: git commit -m "Bump $PROJECT_ID to $NEW_TAG" git push origin "$BRANCH" - # Open the PR with the same labels Renovate would apply, so - # downstream steps that read labels behave identically. # Heredoc so the YAML indent doesn't leak into the PR body. cat > /tmp/bootstrap-body.md <> "$GITHUB_OUTPUT" @@ -490,12 +481,6 @@ jobs: echo "$GAPS_BODY" fi echo "GAPS_EOF" - - if [ -n "$GAPS_BODY" ]; then - echo "has_gaps=true" - else - echo "has_gaps=false" - fi } >> "$GITHUB_OUTPUT" - name: Apply pin_files substitutions @@ -631,21 +616,14 @@ jobs: REVIEWERS: ${{ steps.reviewers.outputs.list }} run: gh pr edit "$PR_NUMBER" --add-reviewer "$REVIEWERS" - - name: Add needs-human-context label - if: always() && steps.signals.outputs.has_gaps == 'true' - env: - PR_NUMBER: ${{ steps.eff.outputs.number }} - run: gh pr edit "$PR_NUMBER" --add-label needs-human-context - - - name: Flag augmentation failure - # Runs only when a preceding step failed. Adds a label the - # operator can filter on and comments a retry pointer. + - name: Comment on augmentation failure + # Runs only when a preceding step failed. Comments a retry + # pointer on the PR so a human can see the run URL. if: failure() env: PR_NUMBER: ${{ steps.eff.outputs.number }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | - gh pr edit "$PR_NUMBER" --add-label upstream-docs-failed || true gh pr comment "$PR_NUMBER" --body "Automated docs augmentation failed. Run: $RUN_URL Retry via the \`Upstream Release Docs\` workflow with \`pr_number=$PR_NUMBER\` once the underlying issue is resolved." || true diff --git a/renovate.json b/renovate.json index 37ac9afb..5ea18e23 100644 --- a/renovate.json +++ b/renovate.json @@ -91,7 +91,6 @@ "minimumReleaseAge": "24 hours", "minimumReleaseAgeBehaviour": "timestamp-optional", "ignoreUnstable": true, - "labels": ["autogen-docs", "upstream-content"], "rebaseWhen": "never", "recreateWhen": "never", "commitMessageTopic": "{{depName}}", From 875ae260f1d7da753bd8806f42d6f63771cf01a9 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Tue, 21 Apr 2026 22:05:10 +0300 Subject: [PATCH 12/16] Inline node setup after PR-branch checkout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `./.github/actions/setup` composite starts with its own actions/checkout that defaults to the dispatching ref, so calling it after "Checkout PR branch" clobbers the PR-branch working tree with the dispatching branch's content. detect-change.mjs then compared the dispatching branch's YAML against itself and reported "No version changes detected" — blocking the first dispatch. Fix: inline the setup-node + cache + npm ci steps after the PR-branch checkout instead of calling the composite. The composite is still used in the bootstrap flow where re-checkout of the dispatching branch is harmless. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/scheduled_tasks.lock | 1 + .github/workflows/upstream-release-docs.yml | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 .claude/scheduled_tasks.lock diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 00000000..d063e969 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"7a40abde-bf3c-4e1a-b06e-d6b485b2143d","pid":77615,"acquiredAt":1776797977279} \ No newline at end of file diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index 8854954e..bb921d7a 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -249,8 +249,25 @@ jobs: ref: ${{ steps.eff.outputs.head_ref }} fetch-depth: 0 - - name: Setup - uses: ./.github/actions/setup + # NOTE: we inline the node/deps setup rather than calling the + # ./.github/actions/setup composite because that composite starts + # with its own actions/checkout that overwrites the PR-branch + # checkout above with the dispatching branch. + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: '24.15.0' + + - name: Cache dependencies + id: cache + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ./node_modules + key: modules-${{ hashFiles('package-lock.json') }} + + - name: Install dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: npm ci - name: Set up Git run: | From 5be8d39d24cf1244c8d490303e3dec9b1333a9f1 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Tue, 21 Apr 2026 22:05:30 +0300 Subject: [PATCH 13/16] Don't track .claude/scheduled_tasks.lock --- .claude/scheduled_tasks.lock | 1 - .gitignore | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 .claude/scheduled_tasks.lock diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock deleted file mode 100644 index d063e969..00000000 --- a/.claude/scheduled_tasks.lock +++ /dev/null @@ -1 +0,0 @@ -{"sessionId":"7a40abde-bf3c-4e1a-b06e-d6b485b2143d","pid":77615,"acquiredAt":1776797977279} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 92fc1040..a599175b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ yarn-error.log* # communicate gaps or no-op releases back to the workflow. Never committed. /GAPS.md /NO_CHANGES.md +.claude/scheduled_tasks.lock From a126d5a86ebb6cbcfb3d64c94ddde8ea6bb8c660 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Tue, 21 Apr 2026 22:08:39 +0300 Subject: [PATCH 14/16] Restore id-token: write for claude-code-action OIDC Removed earlier as 'unused'. anthropics/claude-code-action@v1 needs id-token: write to fetch an OIDC token. Skill step was failing with "Could not fetch an OIDC token" on the first real dispatch. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/upstream-release-docs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index bb921d7a..8e21ad4e 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -47,6 +47,8 @@ on: permissions: contents: write pull-requests: write + # Required by anthropics/claude-code-action@v1 for OIDC token exchange. + id-token: write concurrency: # Workflow-level group so two simultaneous upstream releases don't From c818f936275bc32a5268ea7b04df891cb56992b7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Apr 2026 19:09:22 +0000 Subject: [PATCH 15/16] Bump toolhive to v0.23.1 --- .github/upstream-projects.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/upstream-projects.yaml b/.github/upstream-projects.yaml index 1ac705af..6456d929 100644 --- a/.github/upstream-projects.yaml +++ b/.github/upstream-projects.yaml @@ -35,7 +35,7 @@ projects: - id: toolhive repo: stacklok/toolhive - version: v0.22.0 + version: v0.23.1 docs_paths: - docs/toolhive/guides-cli - docs/toolhive/guides-k8s From 0450ab83fe28f0ec526a34594982b79588312a6d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Apr 2026 19:09:40 +0000 Subject: [PATCH 16/16] Refresh reference assets for toolhive v0.23.1 --- .../reference/crds/embeddingserver.mdx | 4 ++-- .../reference/crds/mcpexternalauthconfig.mdx | 4 ++-- docs/toolhive/reference/crds/mcpgroup.mdx | 4 ++-- .../toolhive/reference/crds/mcpoidcconfig.mdx | 4 ++-- docs/toolhive/reference/crds/mcpregistry.mdx | 4 ++-- .../reference/crds/mcpremoteproxy.mdx | 4 ++-- docs/toolhive/reference/crds/mcpserver.mdx | 4 ++-- .../reference/crds/mcpserverentry.mdx | 4 ++-- .../reference/crds/mcptelemetryconfig.mdx | 4 ++-- .../toolhive/reference/crds/mcptoolconfig.mdx | 4 ++-- .../virtualmcpcompositetooldefinition.mdx | 4 ++-- .../reference/crds/virtualmcpserver.mdx | 4 ++-- .../crds/embeddingservers.example.yaml | 2 +- .../crds/embeddingservers.schema.json | 2 +- static/api-specs/crds/index.json | 24 +++++++++---------- .../crds/mcpexternalauthconfigs.example.yaml | 2 +- .../crds/mcpexternalauthconfigs.schema.json | 2 +- static/api-specs/crds/mcpgroups.example.yaml | 2 +- static/api-specs/crds/mcpgroups.schema.json | 2 +- .../crds/mcpoidcconfigs.example.yaml | 2 +- .../api-specs/crds/mcpoidcconfigs.schema.json | 2 +- .../api-specs/crds/mcpregistries.example.yaml | 2 +- .../api-specs/crds/mcpregistries.schema.json | 2 +- .../crds/mcpremoteproxies.example.yaml | 2 +- .../crds/mcpremoteproxies.schema.json | 2 +- .../crds/mcpserverentries.example.yaml | 2 +- .../crds/mcpserverentries.schema.json | 2 +- static/api-specs/crds/mcpservers.example.yaml | 2 +- static/api-specs/crds/mcpservers.schema.json | 2 +- .../crds/mcptelemetryconfigs.example.yaml | 2 +- .../crds/mcptelemetryconfigs.schema.json | 2 +- .../crds/mcptoolconfigs.example.yaml | 2 +- .../api-specs/crds/mcptoolconfigs.schema.json | 2 +- ...almcpcompositetooldefinitions.example.yaml | 2 +- ...ualmcpcompositetooldefinitions.schema.json | 2 +- .../crds/virtualmcpservers.example.yaml | 2 +- .../crds/virtualmcpservers.schema.json | 2 +- static/api-specs/toolhive-api.yaml | 18 +++++++------- 38 files changed, 69 insertions(+), 69 deletions(-) diff --git a/docs/toolhive/reference/crds/embeddingserver.mdx b/docs/toolhive/reference/crds/embeddingserver.mdx index b47e7051..ef1139db 100644 --- a/docs/toolhive/reference/crds/embeddingserver.mdx +++ b/docs/toolhive/reference/crds/embeddingserver.mdx @@ -8,13 +8,13 @@ toc_max_heading_level: 4 `EmbeddingServer` defines a containerized embedding model server managed by the ToolHive operator. The [VirtualMCPServer](./virtualmcpserver.mdx) optimizer references an `EmbeddingServer` to generate vector embeddings for tool discovery. -**API:** `toolhive.stacklok.dev/v1alpha1` +**API:** `toolhive.stacklok.dev/v1beta1` · **Scope:** Namespaced · **Short names:** `emb`, `embedding` ## Example ```yaml title="embeddingserver.yaml" -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: EmbeddingServer metadata: name: my-embeddingserver diff --git a/docs/toolhive/reference/crds/mcpexternalauthconfig.mdx b/docs/toolhive/reference/crds/mcpexternalauthconfig.mdx index 21386526..6d277afc 100644 --- a/docs/toolhive/reference/crds/mcpexternalauthconfig.mdx +++ b/docs/toolhive/reference/crds/mcpexternalauthconfig.mdx @@ -8,13 +8,13 @@ toc_max_heading_level: 4 `MCPExternalAuthConfig` configures how an MCP server or proxy authenticates to external services via token exchange or an embedded authorization server. It is referenced by [MCPServer](./mcpserver.mdx), [MCPRemoteProxy](./mcpremoteproxy.mdx), [MCPServerEntry](./mcpserverentry.mdx), and [VirtualMCPServer](./virtualmcpserver.mdx). -**API:** `toolhive.stacklok.dev/v1alpha1` +**API:** `toolhive.stacklok.dev/v1beta1` · **Scope:** Namespaced · **Short names:** `extauth`, `mcpextauth` ## Example ```yaml title="mcpexternalauthconfig.yaml" -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPExternalAuthConfig metadata: name: my-mcpexternalauthconfig diff --git a/docs/toolhive/reference/crds/mcpgroup.mdx b/docs/toolhive/reference/crds/mcpgroup.mdx index 58858fdc..afaef5a0 100644 --- a/docs/toolhive/reference/crds/mcpgroup.mdx +++ b/docs/toolhive/reference/crds/mcpgroup.mdx @@ -8,13 +8,13 @@ toc_max_heading_level: 4 `MCPGroup` is a grouping construct for backend workloads. Other resources reference an `MCPGroup` by name to join a shared pool - for example, a [VirtualMCPServer](./virtualmcpserver.mdx) aggregates the tools exposed by every member of its referenced group. -**API:** `toolhive.stacklok.dev/v1alpha1` +**API:** `toolhive.stacklok.dev/v1beta1` · **Scope:** Namespaced · **Short names:** `mcpg`, `mcpgroup` ## Example ```yaml title="mcpgroup.yaml" -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPGroup metadata: name: my-mcpgroup diff --git a/docs/toolhive/reference/crds/mcpoidcconfig.mdx b/docs/toolhive/reference/crds/mcpoidcconfig.mdx index bd1b3abb..6378e1dc 100644 --- a/docs/toolhive/reference/crds/mcpoidcconfig.mdx +++ b/docs/toolhive/reference/crds/mcpoidcconfig.mdx @@ -8,13 +8,13 @@ toc_max_heading_level: 4 `MCPOIDCConfig` defines OIDC authentication settings that can be shared across multiple MCP workloads. [MCPServer](./mcpserver.mdx), [MCPRemoteProxy](./mcpremoteproxy.mdx), and [VirtualMCPServer](./virtualmcpserver.mdx) reference an `MCPOIDCConfig` via `spec.oidcConfigRef` to validate incoming tokens. -**API:** `toolhive.stacklok.dev/v1alpha1` +**API:** `toolhive.stacklok.dev/v1beta1` · **Scope:** Namespaced · **Short names:** `mcpoidc` ## Example ```yaml title="mcpoidcconfig.yaml" -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPOIDCConfig metadata: name: my-mcpoidcconfig diff --git a/docs/toolhive/reference/crds/mcpregistry.mdx b/docs/toolhive/reference/crds/mcpregistry.mdx index bce1f42a..65d494ed 100644 --- a/docs/toolhive/reference/crds/mcpregistry.mdx +++ b/docs/toolhive/reference/crds/mcpregistry.mdx @@ -8,13 +8,13 @@ toc_max_heading_level: 4 `MCPRegistry` deploys a [ToolHive Registry Server](../../guides-registry/intro.mdx) in the cluster. The operator watches `MCPRegistry` resources and provisions the Registry Server, its PostgreSQL backing, and the configured sources (Git, ConfigMap, URL, or Kubernetes discovery) that populate its catalog of MCP server definitions. -**API:** `toolhive.stacklok.dev/v1alpha1` +**API:** `toolhive.stacklok.dev/v1beta1` · **Scope:** Namespaced · **Short names:** `mcpreg`, `registry` ## Example ```yaml title="mcpregistry.yaml" -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPRegistry metadata: name: my-mcpregistry diff --git a/docs/toolhive/reference/crds/mcpremoteproxy.mdx b/docs/toolhive/reference/crds/mcpremoteproxy.mdx index c163513d..65c82f03 100644 --- a/docs/toolhive/reference/crds/mcpremoteproxy.mdx +++ b/docs/toolhive/reference/crds/mcpremoteproxy.mdx @@ -8,13 +8,13 @@ toc_max_heading_level: 4 `MCPRemoteProxy` fronts a remote MCP server (reachable over HTTPS) with the same authentication, telemetry, and tool-filtering features that the operator applies to containerized servers. Use this when you want to apply ToolHive policies to a third-party hosted MCP endpoint. -**API:** `toolhive.stacklok.dev/v1alpha1` +**API:** `toolhive.stacklok.dev/v1beta1` · **Scope:** Namespaced · **Short names:** `rp`, `mcprp` ## Example ```yaml title="mcpremoteproxy.yaml" -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPRemoteProxy metadata: name: my-mcpremoteproxy diff --git a/docs/toolhive/reference/crds/mcpserver.mdx b/docs/toolhive/reference/crds/mcpserver.mdx index 4489bfb7..9792fc40 100644 --- a/docs/toolhive/reference/crds/mcpserver.mdx +++ b/docs/toolhive/reference/crds/mcpserver.mdx @@ -8,13 +8,13 @@ toc_max_heading_level: 4 `MCPServer` defines a containerized MCP server managed by the ToolHive Kubernetes operator. The operator watches `MCPServer` resources and reconciles them into a running, proxied MCP server with the configured transport, authentication, telemetry, and tool filtering. -**API:** `toolhive.stacklok.dev/v1alpha1` +**API:** `toolhive.stacklok.dev/v1beta1` · **Scope:** Namespaced · **Short names:** `mcpserver`, `mcpservers` ## Example ```yaml title="mcpserver.yaml" -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPServer metadata: name: my-mcpserver diff --git a/docs/toolhive/reference/crds/mcpserverentry.mdx b/docs/toolhive/reference/crds/mcpserverentry.mdx index 60f07127..cb611bd8 100644 --- a/docs/toolhive/reference/crds/mcpserverentry.mdx +++ b/docs/toolhive/reference/crds/mcpserverentry.mdx @@ -8,13 +8,13 @@ toc_max_heading_level: 4 `MCPServerEntry` declares a remote MCP server as a first-class member of an [MCPGroup](./mcpgroup.mdx) without running a full [MCPRemoteProxy](./mcpremoteproxy.mdx). Entries appear in registry listings and participate in group-scoped aggregations like a [VirtualMCPServer](./virtualmcpserver.mdx). -**API:** `toolhive.stacklok.dev/v1alpha1` +**API:** `toolhive.stacklok.dev/v1beta1` · **Scope:** Namespaced · **Short names:** `mcpentry` ## Example ```yaml title="mcpserverentry.yaml" -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPServerEntry metadata: name: my-mcpserverentry diff --git a/docs/toolhive/reference/crds/mcptelemetryconfig.mdx b/docs/toolhive/reference/crds/mcptelemetryconfig.mdx index 23479459..b81e7bec 100644 --- a/docs/toolhive/reference/crds/mcptelemetryconfig.mdx +++ b/docs/toolhive/reference/crds/mcptelemetryconfig.mdx @@ -8,13 +8,13 @@ toc_max_heading_level: 4 `MCPTelemetryConfig` defines telemetry settings that can be shared across multiple MCP workloads. [MCPServer](./mcpserver.mdx), [MCPRemoteProxy](./mcpremoteproxy.mdx), and [VirtualMCPServer](./virtualmcpserver.mdx) reference a single `MCPTelemetryConfig` to emit traces and metrics to a common backend. -**API:** `toolhive.stacklok.dev/v1alpha1` +**API:** `toolhive.stacklok.dev/v1beta1` · **Scope:** Namespaced · **Short names:** `mcpotel` ## Example ```yaml title="mcptelemetryconfig.yaml" -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPTelemetryConfig metadata: name: my-mcptelemetryconfig diff --git a/docs/toolhive/reference/crds/mcptoolconfig.mdx b/docs/toolhive/reference/crds/mcptoolconfig.mdx index e20fe728..fe0c696c 100644 --- a/docs/toolhive/reference/crds/mcptoolconfig.mdx +++ b/docs/toolhive/reference/crds/mcptoolconfig.mdx @@ -8,13 +8,13 @@ toc_max_heading_level: 4 `MCPToolConfig` defines tool-filtering and renaming rules that can be shared across MCP workloads. [MCPServer](./mcpserver.mdx), [MCPRemoteProxy](./mcpremoteproxy.mdx), and [VirtualMCPServer](./virtualmcpserver.mdx) reference an `MCPToolConfig` via `spec.toolConfigRef` to customize the tool surface their clients see. -**API:** `toolhive.stacklok.dev/v1alpha1` +**API:** `toolhive.stacklok.dev/v1beta1` · **Scope:** Namespaced · **Short names:** `tc`, `toolconfig` ## Example ```yaml title="mcptoolconfig.yaml" -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPToolConfig metadata: name: my-mcptoolconfig diff --git a/docs/toolhive/reference/crds/virtualmcpcompositetooldefinition.mdx b/docs/toolhive/reference/crds/virtualmcpcompositetooldefinition.mdx index fc942327..07ca47c7 100644 --- a/docs/toolhive/reference/crds/virtualmcpcompositetooldefinition.mdx +++ b/docs/toolhive/reference/crds/virtualmcpcompositetooldefinition.mdx @@ -8,13 +8,13 @@ toc_max_heading_level: 4 `VirtualMCPCompositeToolDefinition` defines a reusable composite tool workflow - a sequence of backend tool calls exposed to clients as a single high-level tool. Referenced by a [VirtualMCPServer](./virtualmcpserver.mdx) via `spec.config.compositeToolRefs`. -**API:** `toolhive.stacklok.dev/v1alpha1` +**API:** `toolhive.stacklok.dev/v1beta1` · **Scope:** Namespaced · **Short names:** `vmcpctd`, `compositetool` ## Example ```yaml title="virtualmcpcompositetooldefinition.yaml" -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: VirtualMCPCompositeToolDefinition metadata: name: my-virtualmcpcompositetooldefinition diff --git a/docs/toolhive/reference/crds/virtualmcpserver.mdx b/docs/toolhive/reference/crds/virtualmcpserver.mdx index 4d83ff49..b305ae6a 100644 --- a/docs/toolhive/reference/crds/virtualmcpserver.mdx +++ b/docs/toolhive/reference/crds/virtualmcpserver.mdx @@ -8,13 +8,13 @@ toc_max_heading_level: 4 `VirtualMCPServer` (vMCP) aggregates the backend workloads belonging to an [MCPGroup](./mcpgroup.mdx) into a single endpoint. Clients see one MCP server; the operator handles tool aggregation, conflict resolution, auth, and optional composite tool workflows behind the scenes. -**API:** `toolhive.stacklok.dev/v1alpha1` +**API:** `toolhive.stacklok.dev/v1beta1` · **Scope:** Namespaced · **Short names:** `vmcp`, `virtualmcp` ## Example ```yaml title="virtualmcpserver.yaml" -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: VirtualMCPServer metadata: name: my-virtualmcpserver diff --git a/static/api-specs/crds/embeddingservers.example.yaml b/static/api-specs/crds/embeddingservers.example.yaml index 46b7e686..34b19103 100644 --- a/static/api-specs/crds/embeddingservers.example.yaml +++ b/static/api-specs/crds/embeddingservers.example.yaml @@ -1,4 +1,4 @@ -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: EmbeddingServer metadata: name: my-embeddingserver diff --git a/static/api-specs/crds/embeddingservers.schema.json b/static/api-specs/crds/embeddingservers.schema.json index 8ddc9780..5b6d2253 100644 --- a/static/api-specs/crds/embeddingservers.schema.json +++ b/static/api-specs/crds/embeddingservers.schema.json @@ -4,7 +4,7 @@ "description": "EmbeddingServer is the Schema for the embeddingservers API", "x-kubernetes-group": "toolhive.stacklok.dev", "x-kubernetes-kind": "EmbeddingServer", - "x-kubernetes-version": "v1alpha1", + "x-kubernetes-version": "v1beta1", "x-kubernetes-plural": "embeddingservers", "x-kubernetes-short-names": [ "emb", diff --git a/static/api-specs/crds/index.json b/static/api-specs/crds/index.json index cd98ea98..df6b84a2 100644 --- a/static/api-specs/crds/index.json +++ b/static/api-specs/crds/index.json @@ -3,7 +3,7 @@ "kind": "EmbeddingServer", "plural": "embeddingservers", "group": "toolhive.stacklok.dev", - "version": "v1alpha1", + "version": "v1beta1", "shortNames": [ "emb", "embedding" @@ -25,7 +25,7 @@ "kind": "MCPExternalAuthConfig", "plural": "mcpexternalauthconfigs", "group": "toolhive.stacklok.dev", - "version": "v1alpha1", + "version": "v1beta1", "shortNames": [ "extauth", "mcpextauth" @@ -68,7 +68,7 @@ "kind": "MCPGroup", "plural": "mcpgroups", "group": "toolhive.stacklok.dev", - "version": "v1alpha1", + "version": "v1beta1", "shortNames": [ "mcpg", "mcpgroup" @@ -109,7 +109,7 @@ "kind": "MCPOIDCConfig", "plural": "mcpoidcconfigs", "group": "toolhive.stacklok.dev", - "version": "v1alpha1", + "version": "v1beta1", "shortNames": [ "mcpoidc" ], @@ -142,7 +142,7 @@ "kind": "MCPRegistry", "plural": "mcpregistries", "group": "toolhive.stacklok.dev", - "version": "v1alpha1", + "version": "v1beta1", "shortNames": [ "mcpreg", "registry" @@ -157,7 +157,7 @@ "kind": "MCPRemoteProxy", "plural": "mcpremoteproxies", "group": "toolhive.stacklok.dev", - "version": "v1alpha1", + "version": "v1beta1", "shortNames": [ "rp", "mcprp" @@ -204,7 +204,7 @@ "kind": "MCPServerEntry", "plural": "mcpserverentries", "group": "toolhive.stacklok.dev", - "version": "v1alpha1", + "version": "v1beta1", "shortNames": [ "mcpentry" ], @@ -231,7 +231,7 @@ "kind": "MCPServer", "plural": "mcpservers", "group": "toolhive.stacklok.dev", - "version": "v1alpha1", + "version": "v1beta1", "shortNames": [ "mcpserver", "mcpservers" @@ -278,7 +278,7 @@ "kind": "MCPTelemetryConfig", "plural": "mcptelemetryconfigs", "group": "toolhive.stacklok.dev", - "version": "v1alpha1", + "version": "v1beta1", "shortNames": [ "mcpotel" ], @@ -311,7 +311,7 @@ "kind": "MCPToolConfig", "plural": "mcptoolconfigs", "group": "toolhive.stacklok.dev", - "version": "v1alpha1", + "version": "v1beta1", "shortNames": [ "tc", "toolconfig" @@ -345,7 +345,7 @@ "kind": "VirtualMCPCompositeToolDefinition", "plural": "virtualmcpcompositetooldefinitions", "group": "toolhive.stacklok.dev", - "version": "v1alpha1", + "version": "v1beta1", "shortNames": [ "vmcpctd", "compositetool" @@ -367,7 +367,7 @@ "kind": "VirtualMCPServer", "plural": "virtualmcpservers", "group": "toolhive.stacklok.dev", - "version": "v1alpha1", + "version": "v1beta1", "shortNames": [ "vmcp", "virtualmcp" diff --git a/static/api-specs/crds/mcpexternalauthconfigs.example.yaml b/static/api-specs/crds/mcpexternalauthconfigs.example.yaml index 77320fba..aef10ac3 100644 --- a/static/api-specs/crds/mcpexternalauthconfigs.example.yaml +++ b/static/api-specs/crds/mcpexternalauthconfigs.example.yaml @@ -1,4 +1,4 @@ -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPExternalAuthConfig metadata: name: my-mcpexternalauthconfig diff --git a/static/api-specs/crds/mcpexternalauthconfigs.schema.json b/static/api-specs/crds/mcpexternalauthconfigs.schema.json index b929346d..31241e25 100644 --- a/static/api-specs/crds/mcpexternalauthconfigs.schema.json +++ b/static/api-specs/crds/mcpexternalauthconfigs.schema.json @@ -4,7 +4,7 @@ "description": "MCPExternalAuthConfig is the Schema for the mcpexternalauthconfigs API.\nMCPExternalAuthConfig resources are namespace-scoped and can only be referenced by\nMCPServer resources within the same namespace. Cross-namespace references\nare not supported for security and isolation reasons.", "x-kubernetes-group": "toolhive.stacklok.dev", "x-kubernetes-kind": "MCPExternalAuthConfig", - "x-kubernetes-version": "v1alpha1", + "x-kubernetes-version": "v1beta1", "x-kubernetes-plural": "mcpexternalauthconfigs", "x-kubernetes-short-names": [ "extauth", diff --git a/static/api-specs/crds/mcpgroups.example.yaml b/static/api-specs/crds/mcpgroups.example.yaml index f0ce93e3..1c6dd73a 100644 --- a/static/api-specs/crds/mcpgroups.example.yaml +++ b/static/api-specs/crds/mcpgroups.example.yaml @@ -1,4 +1,4 @@ -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPGroup metadata: name: my-mcpgroup diff --git a/static/api-specs/crds/mcpgroups.schema.json b/static/api-specs/crds/mcpgroups.schema.json index f1f5a3c4..e827b3a1 100644 --- a/static/api-specs/crds/mcpgroups.schema.json +++ b/static/api-specs/crds/mcpgroups.schema.json @@ -4,7 +4,7 @@ "description": "MCPGroup is the Schema for the mcpgroups API", "x-kubernetes-group": "toolhive.stacklok.dev", "x-kubernetes-kind": "MCPGroup", - "x-kubernetes-version": "v1alpha1", + "x-kubernetes-version": "v1beta1", "x-kubernetes-plural": "mcpgroups", "x-kubernetes-short-names": [ "mcpg", diff --git a/static/api-specs/crds/mcpoidcconfigs.example.yaml b/static/api-specs/crds/mcpoidcconfigs.example.yaml index d59addeb..6e394fe1 100644 --- a/static/api-specs/crds/mcpoidcconfigs.example.yaml +++ b/static/api-specs/crds/mcpoidcconfigs.example.yaml @@ -1,4 +1,4 @@ -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPOIDCConfig metadata: name: my-mcpoidcconfig diff --git a/static/api-specs/crds/mcpoidcconfigs.schema.json b/static/api-specs/crds/mcpoidcconfigs.schema.json index 669d97df..158e894b 100644 --- a/static/api-specs/crds/mcpoidcconfigs.schema.json +++ b/static/api-specs/crds/mcpoidcconfigs.schema.json @@ -4,7 +4,7 @@ "description": "MCPOIDCConfig is the Schema for the mcpoidcconfigs API.\nMCPOIDCConfig resources are namespace-scoped and can only be referenced by\nMCPServer resources within the same namespace. Cross-namespace references\nare not supported for security and isolation reasons.", "x-kubernetes-group": "toolhive.stacklok.dev", "x-kubernetes-kind": "MCPOIDCConfig", - "x-kubernetes-version": "v1alpha1", + "x-kubernetes-version": "v1beta1", "x-kubernetes-plural": "mcpoidcconfigs", "x-kubernetes-short-names": [ "mcpoidc" diff --git a/static/api-specs/crds/mcpregistries.example.yaml b/static/api-specs/crds/mcpregistries.example.yaml index 5e941635..4ae55608 100644 --- a/static/api-specs/crds/mcpregistries.example.yaml +++ b/static/api-specs/crds/mcpregistries.example.yaml @@ -1,4 +1,4 @@ -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPRegistry metadata: name: my-mcpregistry diff --git a/static/api-specs/crds/mcpregistries.schema.json b/static/api-specs/crds/mcpregistries.schema.json index ff151c13..89915cf4 100644 --- a/static/api-specs/crds/mcpregistries.schema.json +++ b/static/api-specs/crds/mcpregistries.schema.json @@ -4,7 +4,7 @@ "description": "MCPRegistry is the Schema for the mcpregistries API", "x-kubernetes-group": "toolhive.stacklok.dev", "x-kubernetes-kind": "MCPRegistry", - "x-kubernetes-version": "v1alpha1", + "x-kubernetes-version": "v1beta1", "x-kubernetes-plural": "mcpregistries", "x-kubernetes-short-names": [ "mcpreg", diff --git a/static/api-specs/crds/mcpremoteproxies.example.yaml b/static/api-specs/crds/mcpremoteproxies.example.yaml index f4ec6f8a..4e06a587 100644 --- a/static/api-specs/crds/mcpremoteproxies.example.yaml +++ b/static/api-specs/crds/mcpremoteproxies.example.yaml @@ -1,4 +1,4 @@ -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPRemoteProxy metadata: name: my-mcpremoteproxy diff --git a/static/api-specs/crds/mcpremoteproxies.schema.json b/static/api-specs/crds/mcpremoteproxies.schema.json index 20424e68..a5587643 100644 --- a/static/api-specs/crds/mcpremoteproxies.schema.json +++ b/static/api-specs/crds/mcpremoteproxies.schema.json @@ -4,7 +4,7 @@ "description": "MCPRemoteProxy is the Schema for the mcpremoteproxies API\nIt enables proxying remote MCP servers with authentication, authorization, audit logging, and tool filtering", "x-kubernetes-group": "toolhive.stacklok.dev", "x-kubernetes-kind": "MCPRemoteProxy", - "x-kubernetes-version": "v1alpha1", + "x-kubernetes-version": "v1beta1", "x-kubernetes-plural": "mcpremoteproxies", "x-kubernetes-short-names": [ "rp", diff --git a/static/api-specs/crds/mcpserverentries.example.yaml b/static/api-specs/crds/mcpserverentries.example.yaml index 2a7384f3..cdc8eb58 100644 --- a/static/api-specs/crds/mcpserverentries.example.yaml +++ b/static/api-specs/crds/mcpserverentries.example.yaml @@ -1,4 +1,4 @@ -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPServerEntry metadata: name: my-mcpserverentry diff --git a/static/api-specs/crds/mcpserverentries.schema.json b/static/api-specs/crds/mcpserverentries.schema.json index 8acc33ba..ed37c587 100644 --- a/static/api-specs/crds/mcpserverentries.schema.json +++ b/static/api-specs/crds/mcpserverentries.schema.json @@ -4,7 +4,7 @@ "description": "MCPServerEntry is the Schema for the mcpserverentries API.\nIt declares a remote MCP server endpoint for vMCP discovery and routing\nwithout deploying any infrastructure.", "x-kubernetes-group": "toolhive.stacklok.dev", "x-kubernetes-kind": "MCPServerEntry", - "x-kubernetes-version": "v1alpha1", + "x-kubernetes-version": "v1beta1", "x-kubernetes-plural": "mcpserverentries", "x-kubernetes-short-names": [ "mcpentry" diff --git a/static/api-specs/crds/mcpservers.example.yaml b/static/api-specs/crds/mcpservers.example.yaml index b02c28a8..470f1903 100644 --- a/static/api-specs/crds/mcpservers.example.yaml +++ b/static/api-specs/crds/mcpservers.example.yaml @@ -1,4 +1,4 @@ -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPServer metadata: name: my-mcpserver diff --git a/static/api-specs/crds/mcpservers.schema.json b/static/api-specs/crds/mcpservers.schema.json index 148bceb6..395fb731 100644 --- a/static/api-specs/crds/mcpservers.schema.json +++ b/static/api-specs/crds/mcpservers.schema.json @@ -4,7 +4,7 @@ "description": "MCPServer is the Schema for the mcpservers API", "x-kubernetes-group": "toolhive.stacklok.dev", "x-kubernetes-kind": "MCPServer", - "x-kubernetes-version": "v1alpha1", + "x-kubernetes-version": "v1beta1", "x-kubernetes-plural": "mcpservers", "x-kubernetes-short-names": [ "mcpserver", diff --git a/static/api-specs/crds/mcptelemetryconfigs.example.yaml b/static/api-specs/crds/mcptelemetryconfigs.example.yaml index e630b142..d1a26f4a 100644 --- a/static/api-specs/crds/mcptelemetryconfigs.example.yaml +++ b/static/api-specs/crds/mcptelemetryconfigs.example.yaml @@ -1,4 +1,4 @@ -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPTelemetryConfig metadata: name: my-mcptelemetryconfig diff --git a/static/api-specs/crds/mcptelemetryconfigs.schema.json b/static/api-specs/crds/mcptelemetryconfigs.schema.json index 4aecc789..4f299679 100644 --- a/static/api-specs/crds/mcptelemetryconfigs.schema.json +++ b/static/api-specs/crds/mcptelemetryconfigs.schema.json @@ -4,7 +4,7 @@ "description": "MCPTelemetryConfig is the Schema for the mcptelemetryconfigs API.\nMCPTelemetryConfig resources are namespace-scoped and can only be referenced by\nMCPServer resources within the same namespace. Cross-namespace references\nare not supported for security and isolation reasons.", "x-kubernetes-group": "toolhive.stacklok.dev", "x-kubernetes-kind": "MCPTelemetryConfig", - "x-kubernetes-version": "v1alpha1", + "x-kubernetes-version": "v1beta1", "x-kubernetes-plural": "mcptelemetryconfigs", "x-kubernetes-short-names": [ "mcpotel" diff --git a/static/api-specs/crds/mcptoolconfigs.example.yaml b/static/api-specs/crds/mcptoolconfigs.example.yaml index 9b8de9ca..d911f3a6 100644 --- a/static/api-specs/crds/mcptoolconfigs.example.yaml +++ b/static/api-specs/crds/mcptoolconfigs.example.yaml @@ -1,4 +1,4 @@ -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPToolConfig metadata: name: my-mcptoolconfig diff --git a/static/api-specs/crds/mcptoolconfigs.schema.json b/static/api-specs/crds/mcptoolconfigs.schema.json index fbe41e6b..9efb42fc 100644 --- a/static/api-specs/crds/mcptoolconfigs.schema.json +++ b/static/api-specs/crds/mcptoolconfigs.schema.json @@ -4,7 +4,7 @@ "description": "MCPToolConfig is the Schema for the mcptoolconfigs API.\nMCPToolConfig resources are namespace-scoped and can only be referenced by\nMCPServer resources within the same namespace. Cross-namespace references\nare not supported for security and isolation reasons.", "x-kubernetes-group": "toolhive.stacklok.dev", "x-kubernetes-kind": "MCPToolConfig", - "x-kubernetes-version": "v1alpha1", + "x-kubernetes-version": "v1beta1", "x-kubernetes-plural": "mcptoolconfigs", "x-kubernetes-short-names": [ "tc", diff --git a/static/api-specs/crds/virtualmcpcompositetooldefinitions.example.yaml b/static/api-specs/crds/virtualmcpcompositetooldefinitions.example.yaml index e588ae95..cfae92d1 100644 --- a/static/api-specs/crds/virtualmcpcompositetooldefinitions.example.yaml +++ b/static/api-specs/crds/virtualmcpcompositetooldefinitions.example.yaml @@ -1,4 +1,4 @@ -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: VirtualMCPCompositeToolDefinition metadata: name: my-virtualmcpcompositetooldefinition diff --git a/static/api-specs/crds/virtualmcpcompositetooldefinitions.schema.json b/static/api-specs/crds/virtualmcpcompositetooldefinitions.schema.json index c878929c..c2320024 100644 --- a/static/api-specs/crds/virtualmcpcompositetooldefinitions.schema.json +++ b/static/api-specs/crds/virtualmcpcompositetooldefinitions.schema.json @@ -4,7 +4,7 @@ "description": "VirtualMCPCompositeToolDefinition is the Schema for the virtualmcpcompositetooldefinitions API\nVirtualMCPCompositeToolDefinition defines reusable composite workflows that can be referenced\nby multiple VirtualMCPServer instances", "x-kubernetes-group": "toolhive.stacklok.dev", "x-kubernetes-kind": "VirtualMCPCompositeToolDefinition", - "x-kubernetes-version": "v1alpha1", + "x-kubernetes-version": "v1beta1", "x-kubernetes-plural": "virtualmcpcompositetooldefinitions", "x-kubernetes-short-names": [ "vmcpctd", diff --git a/static/api-specs/crds/virtualmcpservers.example.yaml b/static/api-specs/crds/virtualmcpservers.example.yaml index 664a53df..20167256 100644 --- a/static/api-specs/crds/virtualmcpservers.example.yaml +++ b/static/api-specs/crds/virtualmcpservers.example.yaml @@ -1,4 +1,4 @@ -apiVersion: toolhive.stacklok.dev/v1alpha1 +apiVersion: toolhive.stacklok.dev/v1beta1 kind: VirtualMCPServer metadata: name: my-virtualmcpserver diff --git a/static/api-specs/crds/virtualmcpservers.schema.json b/static/api-specs/crds/virtualmcpservers.schema.json index 03f5db70..d380fbd3 100644 --- a/static/api-specs/crds/virtualmcpservers.schema.json +++ b/static/api-specs/crds/virtualmcpservers.schema.json @@ -4,7 +4,7 @@ "description": "VirtualMCPServer is the Schema for the virtualmcpservers API\nVirtualMCPServer aggregates multiple backend MCPServers into a unified endpoint", "x-kubernetes-group": "toolhive.stacklok.dev", "x-kubernetes-kind": "VirtualMCPServer", - "x-kubernetes-version": "v1alpha1", + "x-kubernetes-version": "v1beta1", "x-kubernetes-plural": "virtualmcpservers", "x-kubernetes-short-names": [ "vmcp", diff --git a/static/api-specs/toolhive-api.yaml b/static/api-specs/toolhive-api.yaml index 5fc9d245..e6c07a3e 100644 --- a/static/api-specs/toolhive-api.yaml +++ b/static/api-specs/toolhive-api.yaml @@ -31,7 +31,7 @@ components: description: Version is the schema version of the registry type: string type: object - github_com_stacklok_toolhive_cmd_thv-operator_api_v1alpha1.RateLimitBucket: + github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.RateLimitBucket: description: |- PerUser token bucket configuration for this tool. +optional @@ -47,15 +47,15 @@ components: refillPeriod: $ref: '#/components/schemas/v1.Duration' type: object - github_com_stacklok_toolhive_cmd_thv-operator_api_v1alpha1.RateLimitConfig: + github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.RateLimitConfig: description: |- RateLimitConfig contains the CRD rate limiting configuration. When set, rate limiting middleware is added to the proxy middleware chain. properties: perUser: - $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1alpha1.RateLimitBucket' + $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.RateLimitBucket' shared: - $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1alpha1.RateLimitBucket' + $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.RateLimitBucket' tools: description: |- Tools defines per-tool rate limit overrides. @@ -65,11 +65,11 @@ components: +listMapKey=name +optional items: - $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1alpha1.ToolRateLimitConfig' + $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.ToolRateLimitConfig' type: array uniqueItems: false type: object - github_com_stacklok_toolhive_cmd_thv-operator_api_v1alpha1.ToolRateLimitConfig: + github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.ToolRateLimitConfig: properties: name: description: |- @@ -78,9 +78,9 @@ components: +kubebuilder:validation:MinLength=1 type: string perUser: - $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1alpha1.RateLimitBucket' + $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.RateLimitBucket' shared: - $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1alpha1.RateLimitBucket' + $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.RateLimitBucket' type: object github_com_stacklok_toolhive_pkg_audit.Config: description: |- @@ -1213,7 +1213,7 @@ components: type: array uniqueItems: false rate_limit_config: - $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1alpha1.RateLimitConfig' + $ref: '#/components/schemas/github_com_stacklok_toolhive_cmd_thv-operator_api_v1beta1.RateLimitConfig' rate_limit_namespace: description: RateLimitNamespace is the Kubernetes namespace for Redis key derivation.