Cleanup: Remove Recipe Key Flow (#6015)
This commit is contained in:
@@ -16,9 +16,7 @@ body:
|
|||||||
|
|
||||||
🪄 **What Happens After?**
|
🪄 **What Happens After?**
|
||||||
- If accepted, we'll publish your recipe to the [goose recipes cookbook](https://block.github.io/goose/recipes)
|
- If accepted, we'll publish your recipe to the [goose recipes cookbook](https://block.github.io/goose/recipes)
|
||||||
- You'll receive OpenRouter **LLM API credits** as a thank you!
|
|
||||||
- Your GitHub handle will be displayed and linked on the recipe card
|
- Your GitHub handle will be displayed and linked on the recipe card
|
||||||
- If you provide an email below, we'll email you your credits when your recipe is approved and merged.
|
|
||||||
- If the YAML has any issues, goose will comment with validation errors so you can fix and resubmit.
|
- If the YAML has any issues, goose will comment with validation errors so you can fix and resubmit.
|
||||||
|
|
||||||
🧪 **Pro Tip:** You can test your recipe locally in your terminal with:
|
🧪 **Pro Tip:** You can test your recipe locally in your terminal with:
|
||||||
@@ -86,15 +84,6 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: email
|
|
||||||
attributes:
|
|
||||||
label: Your Email (optional)
|
|
||||||
description: If your recipe is approved, we'll email your LLM API credits here.
|
|
||||||
placeholder: yourname@example.com
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
|
|||||||
@@ -31,6 +31,3 @@ Before:
|
|||||||
|
|
||||||
After:
|
After:
|
||||||
|
|
||||||
### Submitting a Recipe?
|
|
||||||
<!-- For Recipe Cookbook Submissions ONLY: Include your email below to receive $10 OpenRouter credits once approved and merged -->
|
|
||||||
**Email**:
|
|
||||||
|
|||||||
@@ -1,229 +0,0 @@
|
|||||||
import os
|
|
||||||
import requests
|
|
||||||
import re
|
|
||||||
import email_validator
|
|
||||||
from sendgrid import SendGridAPIClient
|
|
||||||
from sendgrid.helpers.mail import Mail
|
|
||||||
from python_http_client.exceptions import HTTPError
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Environment variable validation
|
|
||||||
required_envs = ["GITHUB_TOKEN", "GITHUB_SHA", "GITHUB_REPOSITORY", "PROVISIONING_API_KEY", "EMAIL_API_KEY"]
|
|
||||||
missing = [env for env in required_envs if env not in os.environ]
|
|
||||||
if missing:
|
|
||||||
print(f"❌ Missing environment variables: {', '.join(missing)}")
|
|
||||||
exit(2)
|
|
||||||
|
|
||||||
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
|
|
||||||
GITHUB_SHA = os.environ["GITHUB_SHA"]
|
|
||||||
REPO_NAME = os.environ["GITHUB_REPOSITORY"]
|
|
||||||
PROVISIONING_API_KEY = os.environ["PROVISIONING_API_KEY"]
|
|
||||||
SENDGRID_API_KEY = os.environ["EMAIL_API_KEY"]
|
|
||||||
|
|
||||||
pr_number = get_pr_number_from_sha()
|
|
||||||
pr_data = fetch_pr_body(pr_number, GITHUB_TOKEN, REPO_NAME)
|
|
||||||
pr_body = pr_data.get("body", "")
|
|
||||||
|
|
||||||
email = extract_email(pr_body, REPO_NAME, pr_number, GITHUB_TOKEN)
|
|
||||||
print(f"📬 Found email: {email}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
api_key = provision_api_key(PROVISIONING_API_KEY)
|
|
||||||
print("✅ API key generated!")
|
|
||||||
|
|
||||||
if not send_email(email, api_key, SENDGRID_API_KEY):
|
|
||||||
print("❌ Email failed to send. Exiting without PR comment.")
|
|
||||||
exit(2)
|
|
||||||
|
|
||||||
comment_on_pr(GITHUB_TOKEN, REPO_NAME, pr_number, email)
|
|
||||||
|
|
||||||
except Exception as err:
|
|
||||||
print(f"❌ An error occurred: {err}")
|
|
||||||
exit(2)
|
|
||||||
|
|
||||||
def get_pr_number_from_sha():
|
|
||||||
token = os.getenv("GITHUB_TOKEN")
|
|
||||||
repo = os.getenv("GITHUB_REPOSITORY")
|
|
||||||
sha = os.getenv("GITHUB_SHA")
|
|
||||||
|
|
||||||
url = f"https://api.github.com/repos/{repo}/commits/{sha}/pulls"
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"token {token}",
|
|
||||||
"Accept": "application/vnd.github.groot-preview+json"
|
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
pr_data = response.json()
|
|
||||||
if pr_data:
|
|
||||||
return pr_data[0]["number"]
|
|
||||||
else:
|
|
||||||
raise Exception("No PR found for this SHA")
|
|
||||||
|
|
||||||
def fetch_pr_body(pr_number, github_token, repo_full_name):
|
|
||||||
print("🔍 Fetching PR body...")
|
|
||||||
pr_url = f"https://api.github.com/repos/{repo_full_name}/pulls/{pr_number}"
|
|
||||||
try:
|
|
||||||
pr_resp = requests.get(
|
|
||||||
pr_url,
|
|
||||||
headers={"Authorization": f"Bearer {github_token}"}
|
|
||||||
)
|
|
||||||
pr_resp.raise_for_status()
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
print("❌ Failed to fetch PR body:", str(e))
|
|
||||||
raise
|
|
||||||
return pr_resp.json()
|
|
||||||
|
|
||||||
def extract_email_from_text(text):
|
|
||||||
email_match = re.search(r"\*\*Email\*\*:\s*([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})", text)
|
|
||||||
if email_match:
|
|
||||||
return email_match.group(1)
|
|
||||||
email_match = re.search(r"[Ee]mail:\s*([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})", text)
|
|
||||||
if email_match:
|
|
||||||
return email_match.group(1)
|
|
||||||
email_match = re.search(r"\b([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})\b", text)
|
|
||||||
if email_match:
|
|
||||||
return email_match.group(1)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def fetch_pr_comments(repo_full_name, pr_number, github_token):
|
|
||||||
comments_url = f"https://api.github.com/repos/{repo_full_name}/issues/{pr_number}/comments"
|
|
||||||
try:
|
|
||||||
comments_resp = requests.get(
|
|
||||||
comments_url,
|
|
||||||
headers={"Authorization": f"Bearer {github_token}"}
|
|
||||||
)
|
|
||||||
comments_resp.raise_for_status()
|
|
||||||
return comments_resp.json()
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
print(f"⚠️ Failed to fetch PR comments: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def validate_email_address(email):
|
|
||||||
try:
|
|
||||||
valid_email = email_validator.validate_email(email)
|
|
||||||
normalized_email = valid_email.email
|
|
||||||
print(f"✅ Email validation passed: {normalized_email}")
|
|
||||||
return normalized_email
|
|
||||||
except email_validator.EmailNotValidError as e:
|
|
||||||
print(f"❌ Email validation failed: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def extract_email(pr_body, repo_full_name, pr_number, github_token):
|
|
||||||
print("🔍 Searching for email in PR body...")
|
|
||||||
email = extract_email_from_text(pr_body)
|
|
||||||
if email:
|
|
||||||
print(f"📧 Found email in PR body: {email}")
|
|
||||||
validated_email = validate_email_address(email)
|
|
||||||
if validated_email:
|
|
||||||
return validated_email
|
|
||||||
else:
|
|
||||||
print("⚠️ Email in PR body is invalid, checking comments...")
|
|
||||||
|
|
||||||
print("🔍 No valid email found in PR body, checking comments...")
|
|
||||||
comments = fetch_pr_comments(repo_full_name, pr_number, github_token)
|
|
||||||
for comment in comments:
|
|
||||||
comment_body = comment.get("body", "")
|
|
||||||
email = extract_email_from_text(comment_body)
|
|
||||||
if email:
|
|
||||||
print(f"📧 Found email in comment by {comment.get('user', {}).get('login', 'unknown')}: {email}")
|
|
||||||
validated_email = validate_email_address(email)
|
|
||||||
if validated_email:
|
|
||||||
return validated_email
|
|
||||||
else:
|
|
||||||
print("⚠️ Email in comment is invalid, continuing search...")
|
|
||||||
|
|
||||||
print("❌ No valid email found in PR body or comments. Skipping key issuance.")
|
|
||||||
exit(2)
|
|
||||||
|
|
||||||
def provision_api_key(provisioning_api_key):
|
|
||||||
print("🔐 Creating OpenRouter key...")
|
|
||||||
try:
|
|
||||||
key_resp = requests.post(
|
|
||||||
"https://openrouter.ai/api/v1/keys",
|
|
||||||
headers={
|
|
||||||
"Authorization": f"Bearer {provisioning_api_key}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
json={
|
|
||||||
"name": "goose contributor",
|
|
||||||
"label": "goose-cookbook",
|
|
||||||
"limit": 10.0
|
|
||||||
}
|
|
||||||
)
|
|
||||||
key_resp.raise_for_status()
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
print("❌ Failed to provision API key:", str(e))
|
|
||||||
raise
|
|
||||||
key = key_resp.json().get("key")
|
|
||||||
if not key:
|
|
||||||
print("❌ API response did not include a key.")
|
|
||||||
exit(2)
|
|
||||||
return key
|
|
||||||
|
|
||||||
def send_email(email, api_key, sendgrid_api_key):
|
|
||||||
print("📤 Sending email via SendGrid...")
|
|
||||||
try:
|
|
||||||
sg = SendGridAPIClient(sendgrid_api_key)
|
|
||||||
from_email = "goose team <goose@opensource.block.xyz>"
|
|
||||||
subject = "🎉 Your goose contributor API key"
|
|
||||||
html_content = f"""
|
|
||||||
<p>Thank you for contributing to the <strong>goose recipe cookbook</strong>!</p>
|
|
||||||
<p>🎉 Here's your <strong>$10 OpenRouter API key</strong>:</p>
|
|
||||||
<pre style="background-color:#f4f4f4;padding:10px;border-radius:6px;"><code>{api_key}</code></pre>
|
|
||||||
<p>To use this in goose (CLI or Desktop):</p>
|
|
||||||
<ul>
|
|
||||||
<li>Go to your <strong>Provider Settings</strong></li>
|
|
||||||
<li>Select <strong>OpenRouter</strong> from the provider list</li>
|
|
||||||
<li>Paste your API key</li>
|
|
||||||
</ul>
|
|
||||||
<p>📚 Full setup instructions:<br>
|
|
||||||
<a href="https://block.github.io/goose/docs/getting-started/providers/#configure-provider">
|
|
||||||
https://block.github.io/goose/docs/getting-started/providers/#configure-provider</a></p>
|
|
||||||
<p>Happy coding!<br>– the goose team</p>
|
|
||||||
"""
|
|
||||||
message = Mail(
|
|
||||||
from_email=from_email,
|
|
||||||
to_emails=email,
|
|
||||||
subject=subject,
|
|
||||||
html_content=html_content
|
|
||||||
)
|
|
||||||
response = sg.send(message)
|
|
||||||
print(f"✅ Email sent successfully! Status code: {response.status_code}")
|
|
||||||
if response.status_code >= 300:
|
|
||||||
print(f"⚠️ Warning: Unexpected status code {response.status_code}")
|
|
||||||
print(f"Response body: {response.body}")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
except HTTPError as e:
|
|
||||||
print(f"❌ SendGrid HTTP error {e.status_code}: {e.body}")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Unexpected error sending email: {type(e).__name__}: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def comment_on_pr(github_token, repo_full_name, pr_number, email):
|
|
||||||
print("💬 Commenting on PR...")
|
|
||||||
comment_url = f"https://api.github.com/repos/{repo_full_name}/issues/{pr_number}/comments"
|
|
||||||
try:
|
|
||||||
comment_resp = requests.post(
|
|
||||||
comment_url,
|
|
||||||
headers={
|
|
||||||
"Authorization": f"Bearer {github_token}",
|
|
||||||
"Accept": "application/vnd.github+json"
|
|
||||||
},
|
|
||||||
json={
|
|
||||||
"body": f"✅ $10 OpenRouter API key sent to `{email}`. Thanks for your contribution to the goose cookbook!"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
comment_resp.raise_for_status()
|
|
||||||
print("✅ Confirmation comment added to PR.")
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
print("❌ Failed to comment on PR:", str(e))
|
|
||||||
raise
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
name: Send API Key on PR Merge
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [closed]
|
|
||||||
paths:
|
|
||||||
- 'documentation/src/pages/recipes/data/recipes/**'
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
send-api-key:
|
|
||||||
if: github.event.pull_request.merged == true
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repo at merge commit
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Check if recipe files were added or modified in merged PR
|
|
||||||
id: recipe_changes
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
echo "🔍 Checking if recipe files were added or modified in merged PR..."
|
|
||||||
|
|
||||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
|
||||||
echo "Merge commit: $MERGE_COMMIT"
|
|
||||||
|
|
||||||
# Get parent commit of the merge
|
|
||||||
PARENT_COMMIT=$(git rev-parse "$MERGE_COMMIT^1")
|
|
||||||
echo "Parent commit: $PARENT_COMMIT"
|
|
||||||
|
|
||||||
# Get list of added or modified files in the PR (ignore deletions)
|
|
||||||
CHANGED_FILES=$(git diff --name-only --diff-filter=AM "$PARENT_COMMIT" "$MERGE_COMMIT")
|
|
||||||
|
|
||||||
echo "Files added/modified in merged PR:"
|
|
||||||
echo "$CHANGED_FILES"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Detect recipe changes only
|
|
||||||
if echo "$CHANGED_FILES" | grep -q "^documentation/src/pages/recipes/data/recipes/"; then
|
|
||||||
echo "recipe_files_changed=true" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "✅ Recipe files were added/modified in merged PR - proceeding with API key sending"
|
|
||||||
else
|
|
||||||
echo "recipe_files_changed=false" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "ℹ️ No recipe files were added/modified in merged PR - skipping API key sending"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
if: steps.recipe_changes.outputs.recipe_files_changed == 'true'
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
|
|
||||||
- name: Install dependencies and run email script
|
|
||||||
if: steps.recipe_changes.outputs.recipe_files_changed == 'true'
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
GITHUB_SHA: ${{ github.sha }}
|
|
||||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
||||||
PROVISIONING_API_KEY: ${{ secrets.PROVISIONING_API_KEY }}
|
|
||||||
EMAIL_API_KEY: ${{ secrets.SENDGRID_API_KEY }}
|
|
||||||
run: |
|
|
||||||
pip install requests sendgrid email-validator
|
|
||||||
python .github/scripts/send_key.py
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
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
|
|
||||||
Reference in New Issue
Block a user