-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Automate OpenRouter API Key Distribution for External Recipe Contributors #3198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+2,559
−171
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
07a6a64
automate API keys
EbonyLouis 4429e0a
switching to sendgrid
EbonyLouis c01647b
Merge branch 'main' into api-key-automation
EbonyLouis 7b7028c
updates for sendgrid
EbonyLouis faaf24e
integrating the recipe scanner into the work Ebony started
iandouglas 98d9e40
fixing a non-sha release with a sha release hash
iandouglas ab4fad2
reverted back to a single docker container and put training data into…
iandouglas 320b1fd
pr comments addressed
EbonyLouis 2a217f6
updating flow for security scanner
EbonyLouis 1585e5e
add duplicate ID check
EbonyLouis 019959f
fix shell injection
EbonyLouis f0c6ead
removing ID
EbonyLouis 6a29f58
better error handling for email
EbonyLouis 0e4431d
updaing workflow to only re-scan recipes if a PR update changes a recipe
iandouglas a227768
pushing a newline character to a recipe to ensure it starts a re-scan
iandouglas 8c5dfae
updating recipe scanner to avoid a github injection problem
iandouglas a0c67d6
updating recipe scanner to skip a step if no recipes were added/changed
iandouglas 6ba1bf3
add git diff check for updating recipes
EbonyLouis b01c405
Merge branch 'api-key-automation' of github.com:block/goose into api-…
EbonyLouis 0fa561b
updated scanner to fix a broken link if the scan fails, and trying a …
iandouglas 5965071
trying to fix a printf error
iandouglas 13c0edd
reverting my pritnf changes
iandouglas File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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**: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}") | ||
EbonyLouis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.