name: Validate Recipe PR on: pull_request_target: types: [opened, synchronize, reopened] paths: - 'documentation/src/pages/recipes/data/recipes/**' permissions: contents: read pull-requests: write issues: write jobs: validate-recipe: runs-on: ubuntu-latest env: PROVIDER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} steps: - name: Checkout PR uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@v3 with: node-version: '20' - name: Install and Configure goose run: | mkdir -p /home/runner/.local/bin curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh \ | CONFIGURE=false INSTALL_PATH=/home/runner/.local/bin bash echo "/home/runner/.local/bin" >> $GITHUB_PATH mkdir -p ~/.config/goose cat < ~/.config/goose/config.yaml GOOSE_PROVIDER: openrouter GOOSE_MODEL: "anthropic/claude-sonnet-4" keyring: false EOF - name: Check if recipe files changed in this push id: recipe_changes run: | set -e echo "🔍 Checking if recipe files were modified in this push..." # Get the list of changed files in this specific push (added/modified only, not deleted) if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.action }}" = "synchronize" ]; then # For synchronize events, check files changed since the previous commit echo "📝 Synchronize event - checking files changed since previous commit" CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.before }}..${{ github.event.after }}) else # For opened/reopened, check all files in the PR (compare PR head against base) echo "📝 PR opened/reopened - checking all files in PR" CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}) fi echo "Changed files in this push:" echo "$CHANGED_FILES" echo "" # Check if any recipe files were changed if echo "$CHANGED_FILES" | grep -q "^documentation/src/pages/recipes/data/recipes/"; then echo "recipe_files_changed=true" >> "$GITHUB_OUTPUT" echo "✅ Recipe files were modified in this push - proceeding with validation" else echo "recipe_files_changed=false" >> "$GITHUB_OUTPUT" echo "â„šī¸ No recipe files were modified in this push - skipping validation" fi - name: Find recipe files in PR (new or modified) id: find_changed_recipes if: steps.recipe_changes.outputs.recipe_files_changed == 'true' run: | set -e echo "Looking for recipe files in PR (new or modified)..." # Get the list of changed/new files in this PR (added/modified only, not deleted) if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.action }}" = "synchronize" ]; then # For synchronize events, check files changed since the previous commit echo "📝 Synchronize event - checking files changed/added since previous commit" CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.before }}..${{ github.event.after }}) else # For opened/reopened, check all files in the PR (compare PR head against base) echo "📝 PR opened/reopened - checking all new/modified files in PR" CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}) fi # Filter for recipe files only that were changed or added RECIPE_FILES=$(echo "$CHANGED_FILES" | grep "^documentation/src/pages/recipes/data/recipes/" | grep -E "\.(yaml|yml)$" || true) if [ -z "$RECIPE_FILES" ]; then echo "No changed recipe files found in PR" echo "validation_status=no_files" >> $GITHUB_OUTPUT exit 1 fi echo "Found changed recipe files:" echo "$RECIPE_FILES" # Save recipe file paths for validation step echo "$RECIPE_FILES" > /tmp/changed_recipe_files.txt - name: Validate changed recipe files id: validate if: steps.recipe_changes.outputs.recipe_files_changed == 'true' run: | set -e # Read the list of changed recipe files RECIPE_FILES=$(cat /tmp/changed_recipe_files.txt) ALL_VALID=true VALIDATION_OUTPUT="" # First pass: Basic YAML validation while IFS= read -r RECIPE_FILE; do if [ -f "$RECIPE_FILE" ]; then BASE_RECIPE_FILENAME=$(basename "$RECIPE_FILE") echo "🔍 Validating: $BASE_RECIPE_FILENAME" if OUTPUT=$(goose recipe validate "$RECIPE_FILE" 2>&1); then echo "✅ Valid: $BASE_RECIPE_FILENAME" VALIDATION_OUTPUT="${VALIDATION_OUTPUT}✅ $BASE_RECIPE_FILENAME: VALID\n" else echo "❌ Invalid: $BASE_RECIPE_FILENAME" echo "$OUTPUT" VALIDATION_OUTPUT="${VALIDATION_OUTPUT}❌ $BASE_RECIPE_FILENAME: INVALID\n\`\`\`\n$OUTPUT\n\`\`\`\n" ALL_VALID=false fi fi done < /tmp/changed_recipe_files.txt # Second pass: Check for duplicate filenames if [ "$ALL_VALID" = true ]; then echo "🔍 Checking for duplicate filenames..." # Check for duplicate filenames first SEEN_FILENAMES="" while IFS= read -r RECIPE_FILE; do if [ -f "$RECIPE_FILE" ]; then FILENAME=$(basename "$RECIPE_FILE" .yaml) FILENAME=$(basename "$FILENAME" .yml) echo "📋 Checking filename: '$FILENAME'" # Check if we've seen this filename before in this PR if echo "$SEEN_FILENAMES" | grep -q "^$FILENAME$"; then echo "❌ Duplicate filename '$FILENAME' found in this PR" VALIDATION_OUTPUT="${VALIDATION_OUTPUT}❌ Duplicate filename '$FILENAME' found in this PR\n" ALL_VALID=false else SEEN_FILENAMES="$SEEN_FILENAMES\n$FILENAME" fi # Check if this is a new file or an update to existing file # Get list of changed files in this PR compared to base branch CHANGED_FILES=$(git diff --name-only --diff-filter=AM ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} | grep "^$RECIPE_FILE$" || true) EXISTING_FILES=$(find documentation/src/pages/recipes/data/recipes/ -name "$FILENAME.yaml" -o -name "$FILENAME.yml" | grep -v "^$RECIPE_FILE$" || true) if [ -n "$EXISTING_FILES" ] && [ -z "$CHANGED_FILES" ]; then # File exists in repo but is not being modified - this is a new duplicate echo "❌ Recipe filename '$FILENAME' already exists:" echo "$EXISTING_FILES" VALIDATION_OUTPUT="${VALIDATION_OUTPUT}❌ $RECIPE_FILE: Filename '$FILENAME' already exists in: $EXISTING_FILES\n" ALL_VALID=false elif [ -n "$EXISTING_FILES" ] && [ -n "$CHANGED_FILES" ]; then # File exists and is being modified - this is an update echo "✅ Updating existing recipe: '$FILENAME'" else # File doesn't exist - this is a new recipe echo "✅ New recipe filename '$FILENAME' is unique" fi echo "✅ Filename '$FILENAME' validation complete" fi done < /tmp/changed_recipe_files.txt fi # Save validation output for use in comment echo "$VALIDATION_OUTPUT" > /tmp/validation_output.txt if [ "$ALL_VALID" = true ]; then echo "validation_status=valid" >> $GITHUB_OUTPUT else echo "validation_status=invalid" >> $GITHUB_OUTPUT fi - name: Comment validation results if: steps.recipe_changes.outputs.recipe_files_changed == 'true' uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); const status = '${{ steps.validate.outputs.validation_status }}'; let comment; if (status === 'no_files') { comment = `❌ **Recipe Validation Failed** No recipe files found in the correct location! 📁 **Please add your recipe to**: \`documentation/src/pages/recipes/data/recipes/your-recipe-id.yaml\` **Example**: If your recipe ID is \`web-scraper\`, create: \`documentation/src/pages/recipes/data/recipes/web-scraper.yaml\``; } else if (status === 'valid') { comment = `✅ **Recipe Validation Passed** Your recipe(s) are valid and ready for review! 🔍 **Next Steps**: 1. Our team will review your recipe 2. If approved, we'll run a security scan 3. Once merged, you'll receive $10 in OpenRouter credits (if email provided) Thanks for contributing to the goose Recipe Cookbook! 🎉`; } else { // Read validation details from file let validationDetails = ''; try { validationDetails = fs.readFileSync('/tmp/validation_output.txt', 'utf8'); } catch (e) { validationDetails = 'See workflow logs for details.'; } comment = `❌ **Recipe Validation Failed** Please fix the validation errors and push your changes: ${validationDetails} 📚 Check our [Recipe Guide](https://block.github.io/goose/recipes) for help with the correct format.`; } await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.payload.pull_request.number, body: comment }); - name: Set validation status if: always() run: | # Check if recipe files were changed in this PR if [ "${{ steps.recipe_changes.outputs.recipe_files_changed }}" = "false" ]; then # No recipe files were modified in this PR - validation skipped echo "â„šī¸ No recipe files in PR - validation skipped" exit 0 fi VALIDATION_STATUS="${{ steps.validate.outputs.validation_status }}" if [ "$VALIDATION_STATUS" = "valid" ]; then echo "✅ All recipes are valid" exit 0 else echo "❌ Recipe validation failed" exit 1 fi