Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
07a6a64
automate API keys
EbonyLouis Jul 1, 2025
4429e0a
switching to sendgrid
EbonyLouis Jul 1, 2025
c01647b
Merge branch 'main' into api-key-automation
EbonyLouis Jul 28, 2025
7b7028c
updates for sendgrid
EbonyLouis Jul 28, 2025
faaf24e
integrating the recipe scanner into the work Ebony started
iandouglas Aug 26, 2025
98d9e40
fixing a non-sha release with a sha release hash
iandouglas Aug 26, 2025
ab4fad2
reverted back to a single docker container and put training data into…
iandouglas Aug 27, 2025
320b1fd
pr comments addressed
EbonyLouis Aug 28, 2025
2a217f6
updating flow for security scanner
EbonyLouis Aug 28, 2025
1585e5e
add duplicate ID check
EbonyLouis Aug 28, 2025
019959f
fix shell injection
EbonyLouis Aug 28, 2025
f0c6ead
removing ID
EbonyLouis Aug 28, 2025
6a29f58
better error handling for email
EbonyLouis Aug 28, 2025
0e4431d
updaing workflow to only re-scan recipes if a PR update changes a recipe
iandouglas Aug 28, 2025
a227768
pushing a newline character to a recipe to ensure it starts a re-scan
iandouglas Aug 28, 2025
8c5dfae
updating recipe scanner to avoid a github injection problem
iandouglas Aug 28, 2025
a0c67d6
updating recipe scanner to skip a step if no recipes were added/changed
iandouglas Aug 28, 2025
6ba1bf3
add git diff check for updating recipes
EbonyLouis Aug 28, 2025
b01c405
Merge branch 'api-key-automation' of github.com:block/goose into api-…
EbonyLouis Aug 28, 2025
0fa561b
updated scanner to fix a broken link if the scan fails, and trying a …
iandouglas Aug 28, 2025
5965071
trying to fix a printf error
iandouglas Aug 28, 2025
13c0edd
reverting my pritnf changes
iandouglas Aug 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Pull Request Description

<!-- Describe your changes here -->

---

<!-- For Recipe Cookbook Submissions ONLY: Include your email below to receive $10 OpenRouter credits once approved & merged -->
**Email**:
228 changes: 228 additions & 0 deletions .github/scripts/send_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
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 fetch_pr_body(pr_url, github_token):
print("🔍 Fetching PR body...")
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):
"""Extract email from text using various patterns"""
# Try PR template format: "**Email**: email@example.com"
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)

# Try other common email patterns
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)

# Try general email pattern
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(pr_url, github_token):
"""Fetch all comments on the PR"""
# Convert PR URL to comments URL
comments_url = pr_url.replace("/pulls/", "/issues/") + "/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):
"""Validate email address format and deliverability"""
try:
# Validate and get normalized email
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, pr_url, github_token):
"""Extract and validate email from PR body and comments"""
print("🔍 Searching for email in PR body...")

# First check 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...")

# Check PR comments
comments = fetch_pr_comments(pr_url, 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...")

# No valid email found anywhere
print("❌ No valid email found in PR body or comments. Skipping key issuance.")
exit(0)

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
return key_resp.json()["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>Thanks for contributing to the Goose Recipe Cookbook!</p>
<p>Here's your <strong>$10 OpenRouter API key</strong>:</p>
<p><code>{api_key}</code></p>
<p>Happy vibe-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}")

# Check for potential issues even on "success"
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:
# Specific SendGrid HTTP errors
status_code = e.status_code
error_body = e.body

if status_code == 401:
print("❌ SendGrid authentication failed - invalid API key")
elif status_code == 403:
print("❌ SendGrid authorization failed - API key lacks permissions")
elif status_code == 429:
print("❌ SendGrid rate limit exceeded - too many requests")
elif status_code == 400:
print(f"❌ SendGrid bad request - invalid email data: {error_body}")
elif status_code >= 500:
print(f"❌ SendGrid server error ({status_code}) - try again later")
else:
print(f"❌ SendGrid HTTP error {status_code}: {error_body}")

print(f"Full error details: {e}")
return False

except ValueError as e:
print(f"❌ Invalid email format or API key: {e}")
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

def main():
# Load environment variables
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
PR_URL = os.environ["GITHUB_API_URL"]
PROVISIONING_API_KEY = os.environ["PROVISIONING_API_KEY"]
SENDGRID_API_KEY = os.environ["EMAIL_API_KEY"]

pr_data = fetch_pr_body(PR_URL, GITHUB_TOKEN)
pr_body = pr_data.get("body", "")
pr_number = pr_data["number"]
repo_full_name = pr_data["base"]["repo"]["full_name"]

email = extract_email(pr_body, PR_URL, GITHUB_TOKEN)
print(f"📬 Found email: {email}")

try:
api_key = provision_api_key(PROVISIONING_API_KEY)
print("✅ API key generated!")

if send_email(email, api_key, SENDGRID_API_KEY):
comment_on_pr(GITHUB_TOKEN, repo_full_name, pr_number, email)
except Exception as err:
print(f"❌ An error occurred: {err}")

if __name__ == "__main__":
main()
136 changes: 0 additions & 136 deletions .github/workflows/create-recipe-pr.yml

This file was deleted.

Loading
Loading