diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ddce77a6..5f8b0036 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,5 @@ -# Auto-request reviews for skill changes -# Any PR that modifies skills will automatically request reviews from these users - -/skills/ @stevekaliski-stripe @matv-stripe @sgr-stripe @jleong-stripe @tomchen-stripe -/providers/*/plugin/skills/ @stevekaliski-stripe @matv-stripe @sgr-stripe @jleong-stripe @tomchen-stripe +# Skills were previously reviewed here, but are now managed internally. +# No other directories were marked for CODEOWNER review, but if we do so +# in the future, the appropriate user list is: +# +# @anirudhgoyal-stripe @gusnguyen-stripe @jleong-stripe @johno-stripe @keshavc-stripe @markguan-stripe @matv-stripe @sgr-stripe @vncz-stripe diff --git a/.github/workflows/guard-skills.yml b/.github/workflows/guard-skills.yml new file mode 100644 index 00000000..38fca320 --- /dev/null +++ b/.github/workflows/guard-skills.yml @@ -0,0 +1,49 @@ +name: Guard synced skills + +on: + pull_request: + paths: + - 'skills/**' + - '!skills/README.md' + - 'providers/claude/plugin/skills/**' + - 'providers/cursor/plugin/skills/**' + +jobs: + block: + runs-on: ubuntu-latest + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ secrets.GH_APP_STRIPE_AI_SYNC_CLIENT_ID }} + private-key: ${{ secrets.GH_APP_STRIPE_AI_SYNC_PEM }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + permission-pull-requests: write + + - name: Build review body + id: message + uses: actions/github-script@v7 + with: + script: | + const author = context.payload.pull_request.user.login; + const isInternal = author.endsWith('-stripe'); + + const action = isInternal + ? 'To make lasting changes, apply them to the source. You can find instructions at [go/add-stripe-skill](http://go/add-stripe-skill).' + : 'These changes need to be applied internally. Tagging @stripe/developer-ai to take a look.'; + + const body = [ + 'This PR modifies files that are automatically synced from a centrally maintained copy at [docs.stripe.com/.well-known/skills](https://docs.stripe.com/.well-known/skills/index.json). Any changes made here will be overwritten by the next sync.', + '', + action, + ].join('\n'); + + core.setOutput('body', body); + + - name: Request changes on PR + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + PR_URL: ${{ github.event.pull_request.html_url }} + run: gh pr review "$PR_URL" --request-changes --body "${{ steps.message.outputs.body }}" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 940cf076..b015a4c9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -92,38 +92,6 @@ jobs: - name: Test run: pnpm run test - skills-sync-check: - name: Check - Skills in sync - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Verify provider skill directories match source - run: | - out_of_sync=false - - for provider in claude cursor; do - target="providers/$provider/plugin/skills" - - for skill_dir in skills/*/; do - skill_name="$(basename "$skill_dir")" - diff -r "skills/$skill_name" "$target/$skill_name" > /dev/null 2>&1 || { - echo "❌ $target/$skill_name is out of sync with skills/$skill_name" - diff -r "skills/$skill_name" "$target/$skill_name" || true - out_of_sync=true - } - done - done - - if [ "$out_of_sync" = true ]; then - echo "" - echo "Fix: run ./scripts/sync-skills.sh and commit the result." - exit 1 - fi - - echo "✅ All provider skill directories are in sync." python-build: name: Build - Python diff --git a/.github/workflows/sync-skills.yml b/.github/workflows/sync-skills.yml index 88b19f6e..0685fb0a 100644 --- a/.github/workflows/sync-skills.yml +++ b/.github/workflows/sync-skills.yml @@ -37,32 +37,7 @@ jobs: node-version: '20' - name: Run sync script - run: node skills/sync.js - - - name: Verify provider skill directories match source - run: | - out_of_sync=false - - for provider in claude cursor; do - target="providers/$provider/plugin/skills" - - for skill_dir in skills/*/; do - skill_name="$(basename "$skill_dir")" - diff -r "skills/$skill_name" "$target/$skill_name" > /dev/null 2>&1 || { - echo "❌ $target/$skill_name is out of sync with skills/$skill_name" - diff -r "skills/$skill_name" "$target/$skill_name" || true - out_of_sync=true - } - done - done - - if [ "$out_of_sync" = true ]; then - echo "" - echo "Fix: run ./scripts/sync-skills.sh and commit the result." - exit 1 - fi - - echo "✅ All provider skill directories are in sync." + run: node scripts/sync.js - name: Push to Github if: | @@ -70,7 +45,7 @@ jobs: !github.event.repository.fork shell: bash run: | - git add -u + git add ':(glob)**/skills/' # Skip if nothing changed if git diff --staged --quiet; then diff --git a/providers/README.md b/providers/README.md index 9b22caa7..463ee4ff 100644 --- a/providers/README.md +++ b/providers/README.md @@ -4,12 +4,8 @@ This directory contains plugins for different AI code editors. ## Skills -**Do not edit skill files in provider directories manually** +**Do not edit skill files in provider directories manually.** -Skills in `providers/*/plugin/skills/` are automatically synced from mcp.stripe.com via [this GitHub Action](https://github.com/stripe/agent-toolkit/blob/main/.github/workflows/sync-skills.yml). +Skills in `providers/*/plugin/skills/` are automatically synced from [docs.stripe.com/.well-known/skills](https://docs.stripe.com/.well-known/skills) via the [sync-skills workflow](/.github/workflows/sync-skills.yml). Any manual changes will be overwritten. -To manually trigger a sync: - -1. Go to https://github.com/stripe/agent-toolkit/actions/workflows/sync-skills.yml -2. Click "Run workflow" -3. Click the green "Run workflow" button +To manually trigger a sync, go to the [workflow page](https://github.com/stripe/agent-toolkit/actions/workflows/sync-skills.yml) and click "Run workflow". diff --git a/scripts/sync-skills.sh b/scripts/sync-skills.sh deleted file mode 100755 index 68f7f474..00000000 --- a/scripts/sync-skills.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# Copies skills/ to provider plugin directories. -# Run this after editing any file in skills/. - -set -e - -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" -SKILLS_DIR="$REPO_ROOT/skills" -TARGETS=( - "$REPO_ROOT/providers/claude/plugin/skills" - "$REPO_ROOT/providers/cursor/plugin/skills" -) - -for target in "${TARGETS[@]}"; do - # Remove old skill directories (preserve .gitkeep) - find "$target" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} + - - # Copy each skill directory - for skill_dir in "$SKILLS_DIR"/*/; do - [ -d "$skill_dir" ] || continue - cp -r "$skill_dir" "$target/" - done - - echo "Synced skills to $target" -done diff --git a/skills/sync.js b/scripts/sync.js similarity index 65% rename from skills/sync.js rename to scripts/sync.js index 488d7ce3..91d919a3 100644 --- a/skills/sync.js +++ b/scripts/sync.js @@ -8,7 +8,7 @@ const fetchText = (url) => { try { return execSync( `curl -sf --user-agent "github.com/stripe/ai/skills" "${url}"`, - { encoding: "utf8" } + { encoding: "utf8" }, ); } catch (err) { throw new Error(`Failed to fetch ${url}: ${err.message}`); @@ -24,17 +24,29 @@ const fetchManifest = () => { } }; +const cleanDirectory = async (dir) => { + const entries = await fs.readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.name === "README.md") continue; + await fs.rm(path.join(dir, entry.name), { recursive: true, force: true }); + } +}; + +const OUTPUT_LOCATIONS = [ + path.join(__dirname, "../skills"), + path.join(__dirname, "../providers/claude/plugin/skills"), + path.join(__dirname, "../providers/cursor/plugin/skills"), +]; + const run = async () => { const manifest = fetchManifest(); const skills = manifest.skills; console.log(`Found ${skills.length} skills`); - // Define all locations where skills should be written - const outputLocations = [ - __dirname, // skills/ (source of truth) - path.join(__dirname, "../providers/claude/plugin/skills"), - path.join(__dirname, "../providers/cursor/plugin/skills"), - ]; + for (const location of OUTPUT_LOCATIONS) { + await fs.mkdir(location, { recursive: true }); + await cleanDirectory(location); + } let errors = 0; for (const skill of skills) { @@ -51,7 +63,7 @@ const run = async () => { continue; } - for (const location of outputLocations) { + for (const location of OUTPUT_LOCATIONS) { const outputPath = path.join(location, skill.name, file); await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, content, "utf8"); @@ -67,6 +79,8 @@ const run = async () => { run().catch((err) => { console.error(err.message); - console.error("Encountered an error while fetching skills, skills will not be updated. Try triggering the workflow manually."); + console.error( + "Encountered an error while fetching skills, skills will not be updated. Try triggering the workflow manually.", + ); process.exit(1); }); diff --git a/skills/README.md b/skills/README.md index e5f7eec9..dc267ee5 100644 --- a/skills/README.md +++ b/skills/README.md @@ -9,4 +9,4 @@ Stripe has: - MCP Prompts - Agent skills -This folder is a collection of [agent skills](https://agentskills.io) to steer your agents to build optimal Stripe integrations. These are synced from Stripe servers through [this GitHub Action](https://github.com/stripe/agent-toolkit/blob/main/.github/workflows/sync-skills.yml). +This folder is a collection of [agent skills](https://agentskills.io) to steer your agents to build optimal Stripe integrations. These are synced automatically from [docs.stripe.com/.well-known/skills](https://docs.stripe.com/.well-known/skills) via the [sync-skills workflow](/.github/workflows/sync-skills.yml).