Update Skills from CopilotKit Source #32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Update Skills from CopilotKit Source | |
| on: | |
| schedule: | |
| # Daily at 6am UTC | |
| - cron: "0 6 * * *" | |
| workflow_dispatch: | |
| repository_dispatch: | |
| types: [copilotkit-release] | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| update-skills: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| has-changes: ${{ steps.check-changes.outputs.has-changes }} | |
| new-integrations: ${{ steps.detect-new.outputs.new-integrations }} | |
| strategy2-ran: ${{ steps.detect-new.outputs.strategy2-ran }} | |
| branch-name: ${{ steps.create-pr.outputs.branch-name }} | |
| steps: | |
| - name: Check out skills repo | |
| uses: actions/checkout@v4 | |
| with: | |
| path: skills-repo | |
| - name: Check out CopilotKit source repo | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: CopilotKit/CopilotKit | |
| path: copilotkit-repo | |
| ref: main | |
| # ────────────────────────────────────────────── | |
| # Strategy 1: Deterministic extraction | |
| # ────────────────────────────────────────────── | |
| - name: "Strategy 1: Extract public API surface" | |
| working-directory: skills-repo | |
| run: | | |
| # PLACEHOLDER: When extraction scripts exist, this step will: | |
| # - Find all @copilotkit/* package index.ts files in copilotkit-repo | |
| # - Grep for 'export' statements to build the public API surface | |
| # - Write results to skills/copilotkit-develop/references/api-surface.md | |
| # | |
| # Example future implementation: | |
| # for pkg in ../copilotkit-repo/packages/copilotkit/*/src/index.ts; do | |
| # grep -E '^export' "$pkg" >> extracted-api.txt | |
| # done | |
| echo "API surface extraction: placeholder (scripts not yet implemented)" | |
| - name: "Strategy 1: Extract error codes and deprecations" | |
| working-directory: skills-repo | |
| run: | | |
| # PLACEHOLDER: When extraction scripts exist, this step will: | |
| # - Grep copilotkit-repo source for error code patterns (CK_ERR_*, error classes) | |
| # - Grep for @deprecated JSDoc annotations | |
| # - Update skills/copilotkit-debug/references/ with error catalog | |
| # - Update skills/copilotkit-upgrade/references/ with deprecation list | |
| # | |
| # Example future implementation: | |
| # grep -rn '@deprecated' ../copilotkit-repo/packages/ > deprecations.txt | |
| # grep -rn 'class.*Error extends' ../copilotkit-repo/packages/ > error-classes.txt | |
| echo "Error/deprecation extraction: placeholder (scripts not yet implemented)" | |
| - name: "Strategy 1: Scan workspace structure" | |
| working-directory: skills-repo | |
| run: | | |
| # PLACEHOLDER: When extraction scripts exist, this step will: | |
| # - Parse Nx workspace configuration from copilotkit-repo | |
| # - Extract package dependency graph | |
| # - Update references with current package structure | |
| # | |
| # Example future implementation: | |
| # cat ../copilotkit-repo/nx.json | jq '.projects' > workspace-structure.json | |
| echo "Workspace structure scan: placeholder (scripts not yet implemented)" | |
| - name: "Strategy 1: Detect new integrations" | |
| id: detect-new | |
| working-directory: skills-repo | |
| run: | | |
| new_list=$(bash scripts/detect-new-integrations.sh \ | |
| ../copilotkit-repo \ | |
| skills/copilotkit-integrations/references/integrations 2>&1) || true | |
| if echo "$new_list" | grep -q "No new integrations detected"; then | |
| echo "No new integrations found." | |
| echo "new-integrations=" >> "$GITHUB_OUTPUT" | |
| echo "strategy2-ran=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "New integrations detected:" | |
| echo "$new_list" | |
| # Store as comma-separated for later steps | |
| csv=$(echo "$new_list" | tr '\n' ',' | sed 's/,$//') | |
| echo "new-integrations=${csv}" >> "$GITHUB_OUTPUT" | |
| echo "strategy2-ran=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| # ────────────────────────────────────────────── | |
| # Strategy 2: Claude API escalation for new integrations | |
| # ────────────────────────────────────────────── | |
| - name: "Strategy 2: Generate guides for new integrations" | |
| if: steps.detect-new.outputs.strategy2-ran == 'true' | |
| working-directory: skills-repo | |
| env: | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| run: | | |
| IFS=',' read -ra INTEGRATIONS <<< "${{ steps.detect-new.outputs.new-integrations }}" | |
| for integration in "${INTEGRATIONS[@]}"; do | |
| echo "==========================================" | |
| echo "Generating guide for: ${integration}" | |
| echo "==========================================" | |
| example_dir="../copilotkit-repo/examples/integrations/${integration}" | |
| guide_path="skills/copilotkit-integrations/references/integrations/${integration}.md" | |
| # Collect source files from the example directory into a temp file | |
| # (avoids shell variable size limits for large examples) | |
| example_files_tmp=$(mktemp) | |
| while IFS= read -r -d '' file; do | |
| rel_path="${file#${example_dir}/}" | |
| echo "--- FILE: ${rel_path} ---" >> "$example_files_tmp" | |
| head -c 50000 "$file" >> "$example_files_tmp" | |
| echo "" >> "$example_files_tmp" | |
| echo "--- END FILE ---" >> "$example_files_tmp" | |
| done < <(find "$example_dir" -type f \ | |
| \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \ | |
| -o -name "*.py" -o -name "*.toml" -o -name "*.yaml" -o -name "*.yml" \ | |
| -o -name "*.json" -o -name "*.md" -o -name "*.env.example" \ | |
| -o -name "Dockerfile" -o -name "Makefile" \) \ | |
| -not -path "*/node_modules/*" \ | |
| -not -path "*/.next/*" \ | |
| -not -path "*/__pycache__/*" \ | |
| -not -path "*/dist/*" \ | |
| -not -path "*/.git/*" \ | |
| -not -name "package-lock.json" \ | |
| -not -name "pnpm-lock.yaml" \ | |
| -print0 2>/dev/null) | |
| # Truncate if total is too large (keep under 100KB for API payload) | |
| if [ "$(wc -c < "$example_files_tmp")" -gt 100000 ]; then | |
| head -c 100000 "$example_files_tmp" > "${example_files_tmp}.trunc" | |
| mv "${example_files_tmp}.trunc" "$example_files_tmp" | |
| echo "WARNING: Truncated example files to 100KB for ${integration}" | |
| fi | |
| example_files=$(cat "$example_files_tmp") | |
| rm -f "$example_files_tmp" | |
| if [ -z "$example_files" ]; then | |
| echo "WARNING: No source files found in ${example_dir}, skipping." | |
| continue | |
| fi | |
| # Query CopilotKit docs MCP endpoint for relevant context | |
| doc_context="" | |
| doc_context=$(bash scripts/mcp-query.sh "${integration} integration guide setup" 5 2>/dev/null) || \ | |
| echo "WARNING: Could not fetch docs from mcp.copilotkit.ai for ${integration}" | |
| # Read an existing guide as a format reference | |
| reference_guide=$(cat skills/copilotkit-integrations/references/integrations/langgraph.md) | |
| # Build the Claude API request | |
| system_prompt=$(cat <<'SYSPROMPT' | |
| You are a technical writer for CopilotKit. Generate an integration guide in Markdown. | |
| OUTPUT FORMAT RULES: | |
| - Match the structure and tone of the reference guide provided | |
| - Start with a level-1 heading: "# <Framework Name> Integration" | |
| - Include sections: overview, prerequisites, dependencies, project structure, key files walkthrough, how it connects to CopilotKit, running the example | |
| - Use fenced code blocks with language tags | |
| - Be precise and concrete — reference actual file paths and code from the example | |
| TERMINOLOGY RULES (strict): | |
| - Public packages are @copilotkit/* — this is what users install and import | |
| - NEVER reference @copilotkitnext/* packages — these are internal | |
| - NEVER use deprecated v1 names: useCopilotAction (use useFrontendTool), CoAgents (use useAgent), CopilotTextarea (removed), useCopilotReadable (use useAgentContext) | |
| - Current v2 hooks: useAgent, useFrontendTool, useComponent, useAgentContext, useInterrupt, useSuggestions, useHumanInTheLoop | |
| - Current v2 components: CopilotChat, CopilotChatInput, CopilotChatMessageView | |
| - Current v2 runtime: CopilotRuntime, AgentRunner, BuiltInAgent | |
| SOURCE CONSTRAINT: | |
| - ONLY reference code that actually exists in the provided example files | |
| - Do NOT invent configuration, endpoints, or code patterns not present in the source | |
| - If something is unclear from the source, say so rather than guessing | |
| AG-UI PROTOCOL: | |
| - CopilotKit communicates with agent backends via the AG-UI protocol | |
| - AG-UI uses Server-Sent Events (SSE) for streaming agent state, messages, and tool calls | |
| - The frontend useAgent hook connects to the backend agent via AG-UI | |
| - Agent frameworks provide AG-UI adapters/transports that handle the protocol translation | |
| SYSPROMPT | |
| ) | |
| # Construct user message in a temp file (can be large) | |
| user_file=$(mktemp) | |
| cat > "$user_file" <<USERMSG | |
| Generate an integration guide for the "${integration}" framework with CopilotKit. | |
| Here is an existing guide for reference (match this format): | |
| ${reference_guide} | |
| Here are the source files from the example: | |
| ${example_files} | |
| Additional documentation context: | |
| ${doc_context:-No additional docs available.} | |
| USERMSG | |
| # Build JSON payload via temp files (avoids argument list too long on both jq and curl) | |
| system_file=$(mktemp) | |
| payload_file=$(mktemp) | |
| echo "$system_prompt" > "$system_file" | |
| jq -n \ | |
| --arg model "claude-sonnet-4-20250514" \ | |
| --rawfile system "$system_file" \ | |
| --rawfile user "$user_file" \ | |
| '{ | |
| model: $model, | |
| max_tokens: 8192, | |
| system: $system, | |
| messages: [{ role: "user", content: $user }] | |
| }' > "$payload_file" | |
| rm -f "$system_file" "$user_file" | |
| response=$(curl -sf "https://api.anthropic.com/v1/messages" \ | |
| -H "x-api-key: ${ANTHROPIC_API_KEY}" \ | |
| -H "anthropic-version: 2023-06-01" \ | |
| -H "content-type: application/json" \ | |
| --max-time 120 \ | |
| -d @"$payload_file") | |
| rm -f "$payload_file" | |
| if [ $? -ne 0 ] || [ -z "$response" ]; then | |
| echo "ERROR: Claude API call failed for ${integration}" | |
| continue | |
| fi | |
| # Extract the text content from the response | |
| guide_content=$(echo "$response" | jq -r '.content[0].text // empty') | |
| if [ -z "$guide_content" ]; then | |
| echo "ERROR: Empty response from Claude API for ${integration}" | |
| echo "Raw response: ${response}" | head -c 500 | |
| continue | |
| fi | |
| echo "$guide_content" > "$guide_path" | |
| echo "Wrote guide: ${guide_path} ($(wc -l < "$guide_path") lines)" | |
| done | |
| # ────────────────────────────────────────────── | |
| # PR creation | |
| # ────────────────────────────────────────────── | |
| - name: Check for changes | |
| id: check-changes | |
| working-directory: skills-repo | |
| run: | | |
| if git diff --quiet && [ -z "$(git status --porcelain)" ]; then | |
| echo "No changes detected." | |
| echo "has-changes=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Changes detected:" | |
| git status --short | |
| echo "has-changes=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Bump plugin.json patch version | |
| if: steps.check-changes.outputs.has-changes == 'true' | |
| working-directory: skills-repo | |
| run: | | |
| current=$(jq -r '.version' .claude-plugin/plugin.json) | |
| IFS='.' read -r major minor patch <<< "$current" | |
| new_version="${major}.${minor}.$((patch + 1))" | |
| jq --arg v "$new_version" '.version = $v' .claude-plugin/plugin.json > tmp.json \ | |
| && mv tmp.json .claude-plugin/plugin.json | |
| echo "Version bumped: ${current} -> ${new_version}" | |
| - name: Create branch and open PR | |
| id: create-pr | |
| if: steps.check-changes.outputs.has-changes == 'true' | |
| working-directory: skills-repo | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| branch_name="auto/update-skills" | |
| echo "branch-name=${branch_name}" >> "$GITHUB_OUTPUT" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| # Check if an existing auto-update PR branch exists | |
| existing_pr=$(gh pr list --head "$branch_name" --state open --json number --jq '.[0].number' || echo "") | |
| if [ -n "$existing_pr" ]; then | |
| # Existing PR — update the branch | |
| git fetch origin "$branch_name" 2>/dev/null || true | |
| git checkout "$branch_name" 2>/dev/null || git checkout -b "$branch_name" | |
| git merge main --no-edit || true | |
| git add -A | |
| git commit -m "Update skills from CopilotKit source ($(date +%Y-%m-%d))" || true | |
| git push origin "$branch_name" | |
| else | |
| # No existing PR — create new branch | |
| git checkout -b "$branch_name" | |
| git add -A | |
| git commit -m "Update skills from CopilotKit source ($(date +%Y-%m-%d))" | |
| git push origin "$branch_name" --force | |
| fi | |
| # Build PR body | |
| changed_files=$(git diff --name-only main..."$branch_name" || git diff --name-only HEAD~1) | |
| strategy2_ran="${{ steps.detect-new.outputs.strategy2-ran }}" | |
| new_integrations="${{ steps.detect-new.outputs.new-integrations }}" | |
| pr_body="## Auto-generated skills update" | |
| pr_body="${pr_body}\n\n**Last updated:** $(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| pr_body="${pr_body}\n**Trigger:** ${{ github.event_name }}" | |
| pr_body="${pr_body}\n\n### Changes\n\`\`\`\n${changed_files}\n\`\`\`" | |
| if [ "$strategy2_ran" = "true" ]; then | |
| integration_list=$(echo "$new_integrations" | tr ',' '\n' | sed 's/^/- /') | |
| pr_body="${pr_body}\n\n### New integrations (Strategy 2 - Claude-generated)" | |
| pr_body="${pr_body}\nThese guides were generated by Claude and need human review:" | |
| pr_body="${pr_body}\n${integration_list}" | |
| fi | |
| pr_body="${pr_body}\n\n---\n*This PR was auto-generated by the update-skills workflow. Do not auto-merge.*" | |
| pr_body=$(echo -e "$pr_body") | |
| # Determine labels | |
| labels="auto-generated" | |
| if [ "$strategy2_ran" = "true" ]; then | |
| labels="${labels},new-integration" | |
| fi | |
| if [ -n "$existing_pr" ]; then | |
| # Update existing PR body | |
| gh pr edit "$existing_pr" --body "$pr_body" | |
| echo "Updated existing PR #${existing_pr}" | |
| else | |
| # Create new PR | |
| gh pr create \ | |
| --title "Update skills from CopilotKit source" \ | |
| --body "$pr_body" \ | |
| --label "$labels" \ | |
| --base main | |
| fi | |
| # ────────────────────────────────────────────── | |
| # Smoke tests (runs on the PR branch) | |
| # ────────────────────────────────────────────── | |
| smoke-tests: | |
| runs-on: ubuntu-latest | |
| needs: update-skills | |
| if: needs.update-skills.outputs.has-changes == 'true' | |
| steps: | |
| - name: Check out skills repo (PR branch) | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.update-skills.outputs.branch-name }} | |
| - name: "Smoke test: Reference docs minimum length" | |
| run: | | |
| exit_code=0 | |
| for doc in skills/copilotkit-integrations/references/integrations/*.md; do | |
| [ -f "$doc" ] || continue | |
| lines=$(wc -l < "$doc") | |
| name=$(basename "$doc") | |
| if [ "$lines" -lt 50 ]; then | |
| echo "FAIL: ${name} has only ${lines} lines (minimum 50)" | |
| exit_code=1 | |
| else | |
| echo "OK: ${name} (${lines} lines)" | |
| fi | |
| done | |
| exit $exit_code | |
| - name: "Smoke test: Cross-reference SKILL.md hooks against api-surface" | |
| run: | | |
| # Validate that hooks/components mentioned in SKILL.md files | |
| # can be found in the API surface reference (if it exists). | |
| api_surface="skills/copilotkit-develop/references/api-surface.md" | |
| if [ ! -f "$api_surface" ]; then | |
| echo "SKIP: api-surface.md not found yet (placeholder step ran)" | |
| exit 0 | |
| fi | |
| exit_code=0 | |
| for skill_md in skills/*/SKILL.md; do | |
| [ -f "$skill_md" ] || continue | |
| skill_name=$(basename "$(dirname "$skill_md")") | |
| # Extract hook and component names (useXxx, CopilotXxx patterns) | |
| hooks=$(grep -oE '\b(use[A-Z][a-zA-Z]+|Copilot[A-Z][a-zA-Z]+)\b' "$skill_md" | sort -u) | |
| for hook in $hooks; do | |
| if ! grep -q "$hook" "$api_surface"; then | |
| echo "FAIL: ${skill_name}/SKILL.md references '${hook}' not found in api-surface.md" | |
| exit_code=1 | |
| fi | |
| done | |
| done | |
| if [ $exit_code -eq 0 ]; then | |
| echo "OK: All hook/component references validated." | |
| fi | |
| exit $exit_code |