mirror of
https://github.com/block/goose.git
synced 2026-04-29 03:59:36 +00:00
265 lines
No EOL
12 KiB
YAML
265 lines
No EOL
12 KiB
YAML
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 <<EOF > ~/.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 |