From 5d5103db37f8ef015a3a1cbcd1a89195b03dee13 Mon Sep 17 00:00:00 2001 From: Alejandro Ponce Date: Thu, 23 Oct 2025 12:06:41 +0300 Subject: [PATCH 1/2] Add automated daily workflow for ToolHive model updates - New GitHub Actions workflow runs daily at midnight UTC to check for ToolHive API changes - Enhanced generate_toolhive_models.sh to automatically manage thv serve lifecycle (starts/stops with cleanup) - Script now detects timestamp-only changes and skips updates when no meaningful changes exist - Outputs specific filenames where changes were detected for better visibility - Workflow creates PRs automatically only when meaningful changes are found - Pinned toolhive-actions version to v0.0.3 for consistency across workflows --- .github/workflows/integration-tests.yml | 2 +- .github/workflows/update-thv-models.yml | 101 ++++++++++++++++++ Taskfile.yml | 2 +- generate_toolhive_models.sh | 14 --- scripts/generate_toolhive_models.sh | 135 ++++++++++++++++++++++++ 5 files changed, 238 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/update-thv-models.yml delete mode 100755 generate_toolhive_models.sh create mode 100755 scripts/generate_toolhive_models.sh diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index e32ec20..324b17e 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -29,7 +29,7 @@ jobs: tags: mcp-optimizer:latest - name: Install ToolHive - uses: StacklokLabs/toolhive-actions/install@v0 + uses: StacklokLabs/toolhive-actions/install@6a095f99aa2fd6cd92cf0bb94bdf509b99820c06 # v0.0.3 - name: Run ToolHive server run: | diff --git a/.github/workflows/update-thv-models.yml b/.github/workflows/update-thv-models.yml new file mode 100644 index 0000000..93e3a13 --- /dev/null +++ b/.github/workflows/update-thv-models.yml @@ -0,0 +1,101 @@ +name: Update ToolHive Models + +on: + schedule: + # Run every day at midnight UTC + - cron: '0 0 * * *' + workflow_dispatch: # Allow manual trigger + +permissions: + contents: write + pull-requests: write + issues: write + +concurrency: + group: update-thv-models + cancel-in-progress: true + +jobs: + update-models: + name: Update ToolHive API Models + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Install ToolHive + uses: StacklokLabs/toolhive-actions/install@6a095f99aa2fd6cd92cf0bb94bdf509b99820c06 # v0.0.3 + + - name: Install uv + uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1 + with: + enable-cache: true + python-version: '3.13' + + - name: Install Task + uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 # v2.0.0 + with: + version: 3.44.1 + + - name: Install Dependencies + run: task install + + - name: Generate ToolHive Models + run: task generate-thv-models + + - name: Create Pull Request + id: create-pr + uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: | + Update ToolHive API models + + Automated update of ToolHive API models from OpenAPI specification. + branch: update-thv-models + delete-branch: true + title: 'chore: Update ToolHive API models' + body: | + ## Summary + This PR updates the ToolHive API models generated from the latest OpenAPI specification. + + ## Changes + - Updated Pydantic models in `src/mcp_optimizer/toolhive/api_models/` + + ## Notes + - This PR was automatically generated by the [update-thv-models workflow](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + - The models are generated using `datamodel-codegen` from ToolHive's OpenAPI endpoint + + 🤖 Generated with [GitHub Actions](https://github.com/features/actions) + labels: | + automated + dependencies + + - name: Notify on Failure + if: failure() + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const runUrl = `${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}`; + + // Create an issue to notify about the failure + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: '⚠️ ToolHive Model Update Workflow Failed', + body: `## Workflow Failure Alert + + The automated ToolHive model update workflow has failed. + + **Details:** + - Workflow: \`${{ github.workflow }}\` + - Run ID: \`${{ github.run_id }}\` + - Triggered by: \`${{ github.event_name }}\` + - Branch: \`${{ github.ref_name }}\` + + **Action Required:** + Please investigate the failure and fix any issues with the model generation process. + + [View Failed Workflow Run](${runUrl})`, + labels: ['automated', 'bug', 'ci-failure'] + }); diff --git a/Taskfile.yml b/Taskfile.yml index c38fdb3..da8ee1a 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -63,7 +63,7 @@ tasks: generate-thv-models: desc: Generate Pydantic models from Toolhive's OpenAPI specification cmds: - - ./generate_toolhive_models.sh + - ./scripts/generate_toolhive_models.sh deps: - install diff --git a/generate_toolhive_models.sh b/generate_toolhive_models.sh deleted file mode 100755 index a431426..0000000 --- a/generate_toolhive_models.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -OUTPUT_DIR="src/mcp_optimizer/toolhive/api_models" -rm -r $OUTPUT_DIR -uv run datamodel-codegen \ - --url http://127.0.0.1:8080/api/openapi.json \ - --output $OUTPUT_DIR \ - --input-file-type openapi \ - --use-standard-collections \ - --use-subclass-enum \ - --snake-case-field \ - --collapse-root-models \ - --target-python-version 3.13 \ - --output-model-type pydantic_v2.BaseModel diff --git a/scripts/generate_toolhive_models.sh b/scripts/generate_toolhive_models.sh new file mode 100755 index 0000000..fee293b --- /dev/null +++ b/scripts/generate_toolhive_models.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash + +set -euo pipefail + +OUTPUT_DIR="src/mcp_optimizer/toolhive/api_models" +TEMP_DIR="$(mktemp -d)" +THV_PID="" +MANAGE_THV="${MANAGE_THV:-true}" + +# Cleanup function +cleanup() { + local exit_code=$? + if [ -n "$THV_PID" ] && [ "$MANAGE_THV" = "true" ]; then + # Check if process exists before attempting to kill + if kill -0 "$THV_PID" 2>/dev/null; then + echo "Stopping thv serve (PID: $THV_PID)..." + kill "$THV_PID" 2>/dev/null || true + wait "$THV_PID" 2>/dev/null || true + fi + fi + rm -rf "$TEMP_DIR" + exit $exit_code +} + +trap cleanup EXIT INT TERM + +# Start thv serve if we're managing it +if [ "$MANAGE_THV" = "true" ]; then + echo "Starting thv serve --openapi on port 8080..." + thv serve --openapi --port 8080 & + THV_PID=$! + + echo "Waiting for thv serve to be ready..." + MAX_ATTEMPTS=30 + for i in $(seq 1 $MAX_ATTEMPTS); do + if curl -s --max-time 5 http://127.0.0.1:8080/api/openapi.json > /dev/null 2>&1; then + echo "thv serve is ready!" + break + fi + if [ $i -eq $MAX_ATTEMPTS ]; then + echo "ERROR: thv serve did not become ready" + exit 1 + fi + sleep 1 + done +fi + +# Save current models to temp directory for comparison +if [ -d "$OUTPUT_DIR" ]; then + echo "Backing up current models..." + cp -r "$OUTPUT_DIR" "$TEMP_DIR/backup" +fi + +# Remove old models (with safety check) +if [ -n "$OUTPUT_DIR" ] && [ "$OUTPUT_DIR" != "/" ]; then + rm -rf "$OUTPUT_DIR" +else + echo "ERROR: Invalid OUTPUT_DIR value" + exit 1 +fi + +# Generate new models +echo "Generating models from OpenAPI specification..." +uv run datamodel-codegen \ + --url http://127.0.0.1:8080/api/openapi.json \ + --output "$OUTPUT_DIR" \ + --input-file-type openapi \ + --use-standard-collections \ + --use-subclass-enum \ + --snake-case-field \ + --collapse-root-models \ + --target-python-version 3.13 \ + --output-model-type pydantic_v2.BaseModel + +# Check if there are meaningful changes (excluding timestamp) +if [ -d "$TEMP_DIR/backup" ]; then + echo "Checking for meaningful changes..." + + # Create copies with timestamp lines removed for comparison + mkdir -p "$TEMP_DIR/new" "$TEMP_DIR/old" + + # Process new files + if [ -d "$OUTPUT_DIR" ] && [ -n "$(ls -A "$OUTPUT_DIR"/*.py 2>/dev/null)" ]; then + for file in "$OUTPUT_DIR"/*.py; do + if [ -f "$file" ]; then + filename=$(basename "$file") + grep --text -v "^# timestamp:" "$file" > "$TEMP_DIR/new/$filename" || true + fi + done + fi + + # Process old files + for file in "$TEMP_DIR/backup"/*.py; do + if [ -f "$file" ]; then + filename=$(basename "$file") + grep --text -v "^# timestamp:" "$file" > "$TEMP_DIR/old/$filename" || true + fi + done + + # Compare directories and collect changed files + CHANGED_FILES=() + for file in "$TEMP_DIR/new"/*.py; do + filename=$(basename "$file") + old_file="$TEMP_DIR/old/$filename" + + if [ ! -f "$old_file" ]; then + CHANGED_FILES+=("$filename (new file)") + elif ! diff "$file" "$old_file" > /dev/null 2>&1; then + CHANGED_FILES+=("$filename") + fi + done + + # Check for deleted files + for file in "$TEMP_DIR/old"/*.py; do + filename=$(basename "$file") + new_file="$TEMP_DIR/new/$filename" + + if [ ! -f "$new_file" ]; then + CHANGED_FILES+=("$filename (deleted)") + fi + done + + if [ ${#CHANGED_FILES[@]} -eq 0 ]; then + echo "No meaningful changes detected (only timestamp updated)" + echo "Restoring original files..." + rm -rf "$OUTPUT_DIR" + mv "$TEMP_DIR/backup" "$OUTPUT_DIR" + exit 0 + else + echo "Meaningful changes detected in ${#CHANGED_FILES[@]} file(s):" + printf ' - %s\n' "${CHANGED_FILES[@]}" + fi +fi + +echo "Model generation complete!" From fd37c880426da75062c256af44daa919d6434a11 Mon Sep 17 00:00:00 2001 From: Alejandro Ponce Date: Thu, 23 Oct 2025 13:59:53 +0300 Subject: [PATCH 2/2] comments from review --- .github/workflows/update-thv-models.yml | 14 +++++++++++++- scripts/generate_toolhive_models.sh | 6 +++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-thv-models.yml b/.github/workflows/update-thv-models.yml index 93e3a13..d779310 100644 --- a/.github/workflows/update-thv-models.yml +++ b/.github/workflows/update-thv-models.yml @@ -43,7 +43,19 @@ jobs: - name: Generate ToolHive Models run: task generate-thv-models + - name: Check for Changes + id: check-changes + run: | + if git diff --quiet && git diff --cached --quiet; then + echo "No changes detected" + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "Changes detected" + echo "has_changes=true" >> $GITHUB_OUTPUT + fi + - name: Create Pull Request + if: steps.check-changes.outputs.has_changes == 'true' id: create-pr uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 with: @@ -52,7 +64,7 @@ jobs: Update ToolHive API models Automated update of ToolHive API models from OpenAPI specification. - branch: update-thv-models + branch: update-thv-models-${{ github.run_id }} delete-branch: true title: 'chore: Update ToolHive API models' body: | diff --git a/scripts/generate_toolhive_models.sh b/scripts/generate_toolhive_models.sh index fee293b..2a01f1c 100755 --- a/scripts/generate_toolhive_models.sh +++ b/scripts/generate_toolhive_models.sh @@ -33,14 +33,18 @@ if [ "$MANAGE_THV" = "true" ]; then echo "Waiting for thv serve to be ready..." MAX_ATTEMPTS=30 for i in $(seq 1 $MAX_ATTEMPTS); do - if curl -s --max-time 5 http://127.0.0.1:8080/api/openapi.json > /dev/null 2>&1; then + # Check if endpoint returns valid JSON with expected content + response=$(curl -s --max-time 5 http://127.0.0.1:8080/api/openapi.json 2>&1) + if [ $? -eq 0 ] && echo "$response" | python3 -m json.tool > /dev/null 2>&1 && echo "$response" | grep -q "openapi"; then echo "thv serve is ready!" break fi if [ $i -eq $MAX_ATTEMPTS ]; then echo "ERROR: thv serve did not become ready" + echo "Last response: $response" exit 1 fi + echo "Attempt $i/$MAX_ATTEMPTS: Waiting for OpenAPI endpoint..." sleep 1 done fi