mirror of
https://github.com/PlaneQuery/OpenAirframes.git
synced 2026-07-12 05:06:37 +02:00
FEATURE: Add contributions framework. Fix and improve daily adsb release using Github actions for map reduce.
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Approve a community submission and create a PR.
|
||||
|
||||
This script is called by the GitHub Actions workflow when the 'approved'
|
||||
label is added to a validated submission issue.
|
||||
|
||||
Usage:
|
||||
python -m src.contributions.approve_submission --issue-number 123 --issue-body "..." --author "username" --author-id 12345
|
||||
|
||||
Environment variables:
|
||||
GITHUB_TOKEN: GitHub API token with repo write permissions
|
||||
GITHUB_REPOSITORY: owner/repo
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from .schema import extract_json_from_issue_body, extract_contributor_name_from_issue_body, parse_and_validate
|
||||
from .contributor import (
|
||||
generate_contributor_uuid,
|
||||
generate_submission_filename,
|
||||
compute_content_hash,
|
||||
)
|
||||
|
||||
|
||||
def github_api_request(
|
||||
method: str,
|
||||
endpoint: str,
|
||||
data: dict | None = None,
|
||||
accept: str = "application/vnd.github.v3+json"
|
||||
) -> dict:
|
||||
"""Make a GitHub API request."""
|
||||
token = os.environ.get("GITHUB_TOKEN")
|
||||
repo = os.environ.get("GITHUB_REPOSITORY")
|
||||
|
||||
if not token or not repo:
|
||||
raise EnvironmentError("GITHUB_TOKEN and GITHUB_REPOSITORY must be set")
|
||||
|
||||
url = f"https://api.github.com/repos/{repo}{endpoint}"
|
||||
headers = {
|
||||
"Authorization": f"token {token}",
|
||||
"Accept": accept,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
body = json.dumps(data).encode() if data else None
|
||||
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req) as response:
|
||||
return json.loads(response.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
error_body = e.read().decode() if e.fp else ""
|
||||
print(f"GitHub API error: {e.code} {e.reason}: {error_body}", file=sys.stderr)
|
||||
raise
|
||||
|
||||
|
||||
def add_issue_comment(issue_number: int, body: str) -> None:
|
||||
"""Add a comment to a GitHub issue."""
|
||||
github_api_request("POST", f"/issues/{issue_number}/comments", {"body": body})
|
||||
|
||||
|
||||
def get_default_branch_sha() -> str:
|
||||
"""Get the SHA of the default branch (main)."""
|
||||
ref = github_api_request("GET", "/git/ref/heads/main")
|
||||
return ref["object"]["sha"]
|
||||
|
||||
|
||||
def create_branch(branch_name: str, sha: str) -> None:
|
||||
"""Create a new branch from a SHA."""
|
||||
try:
|
||||
github_api_request("POST", "/git/refs", {
|
||||
"ref": f"refs/heads/{branch_name}",
|
||||
"sha": sha,
|
||||
})
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 422: # Branch exists
|
||||
# Delete and recreate
|
||||
try:
|
||||
github_api_request("DELETE", f"/git/refs/heads/{branch_name}")
|
||||
except urllib.error.HTTPError:
|
||||
pass
|
||||
github_api_request("POST", "/git/refs", {
|
||||
"ref": f"refs/heads/{branch_name}",
|
||||
"sha": sha,
|
||||
})
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def create_or_update_file(path: str, content: str, message: str, branch: str) -> None:
|
||||
"""Create or update a file in the repository."""
|
||||
content_b64 = base64.b64encode(content.encode()).decode()
|
||||
github_api_request("PUT", f"/contents/{path}", {
|
||||
"message": message,
|
||||
"content": content_b64,
|
||||
"branch": branch,
|
||||
})
|
||||
|
||||
|
||||
def create_pull_request(title: str, head: str, base: str, body: str) -> dict:
|
||||
"""Create a pull request."""
|
||||
return github_api_request("POST", "/pulls", {
|
||||
"title": title,
|
||||
"head": head,
|
||||
"base": base,
|
||||
"body": body,
|
||||
})
|
||||
|
||||
|
||||
def add_labels_to_issue(issue_number: int, labels: list[str]) -> None:
|
||||
"""Add labels to an issue or PR."""
|
||||
github_api_request("POST", f"/issues/{issue_number}/labels", {"labels": labels})
|
||||
|
||||
|
||||
def process_submission(
|
||||
issue_number: int,
|
||||
issue_body: str,
|
||||
author_username: str,
|
||||
author_id: int,
|
||||
) -> bool:
|
||||
"""
|
||||
Process an approved submission and create a PR.
|
||||
|
||||
Args:
|
||||
issue_number: The GitHub issue number
|
||||
issue_body: The issue body text
|
||||
author_username: The GitHub username of the issue author
|
||||
author_id: The numeric GitHub user ID
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
# Extract and validate JSON
|
||||
json_str = extract_json_from_issue_body(issue_body)
|
||||
if not json_str:
|
||||
add_issue_comment(issue_number, "❌ Could not extract JSON from submission.")
|
||||
return False
|
||||
|
||||
data, errors = parse_and_validate(json_str)
|
||||
if errors:
|
||||
error_list = "\n".join(f"- {e}" for e in errors)
|
||||
add_issue_comment(issue_number, f"❌ **Validation Failed**\n\n{error_list}")
|
||||
return False
|
||||
|
||||
# Normalize to list
|
||||
submissions = data if isinstance(data, list) else [data]
|
||||
|
||||
# Generate contributor UUID from GitHub ID
|
||||
contributor_uuid = generate_contributor_uuid(author_id)
|
||||
|
||||
# Extract contributor name from issue form (or default to GitHub username)
|
||||
contributor_name = extract_contributor_name_from_issue_body(issue_body)
|
||||
if not contributor_name:
|
||||
contributor_name = f"@{author_username}"
|
||||
|
||||
# Add metadata to each submission
|
||||
now = datetime.now(timezone.utc)
|
||||
date_str = now.strftime("%Y-%m-%d")
|
||||
timestamp_str = now.isoformat()
|
||||
|
||||
for submission in submissions:
|
||||
submission["contributor_uuid"] = contributor_uuid
|
||||
submission["contributor_name"] = contributor_name
|
||||
submission["creation_timestamp"] = timestamp_str
|
||||
|
||||
# Generate unique filename
|
||||
content_json = json.dumps(submissions, indent=2, sort_keys=True)
|
||||
content_hash = compute_content_hash(content_json)
|
||||
filename = generate_submission_filename(author_username, date_str, content_hash)
|
||||
file_path = f"community/{filename}"
|
||||
|
||||
# Create branch
|
||||
branch_name = f"community-submission-{issue_number}"
|
||||
default_sha = get_default_branch_sha()
|
||||
create_branch(branch_name, default_sha)
|
||||
|
||||
# Create file
|
||||
commit_message = f"Add community submission from @{author_username} (closes #{issue_number})"
|
||||
create_or_update_file(file_path, content_json, commit_message, branch_name)
|
||||
|
||||
# Create PR
|
||||
pr_body = f"""## Community Submission
|
||||
|
||||
Adds {len(submissions)} submission(s) from @{author_username}.
|
||||
|
||||
**File:** `{file_path}`
|
||||
**Contributor UUID:** `{contributor_uuid}`
|
||||
|
||||
Closes #{issue_number}
|
||||
|
||||
---
|
||||
|
||||
### Submissions
|
||||
```json
|
||||
{content_json}
|
||||
```"""
|
||||
|
||||
pr = create_pull_request(
|
||||
title=f"Community submission: {filename}",
|
||||
head=branch_name,
|
||||
base="main",
|
||||
body=pr_body,
|
||||
)
|
||||
|
||||
# Add labels to PR
|
||||
add_labels_to_issue(pr["number"], ["community", "auto-generated"])
|
||||
|
||||
# Comment on original issue
|
||||
add_issue_comment(
|
||||
issue_number,
|
||||
f"✅ **Submission Approved**\n\n"
|
||||
f"PR #{pr['number']} has been created to add your submission.\n\n"
|
||||
f"**File:** `{file_path}`\n"
|
||||
f"**Your Contributor UUID:** `{contributor_uuid}`\n\n"
|
||||
f"The PR will be merged by a maintainer."
|
||||
)
|
||||
|
||||
print(f"Created PR #{pr['number']} for submission")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Approve community submission and create PR")
|
||||
parser.add_argument("--issue-number", type=int, required=True, help="GitHub issue number")
|
||||
parser.add_argument("--issue-body", required=True, help="Issue body text")
|
||||
parser.add_argument("--author", required=True, help="Issue author username")
|
||||
parser.add_argument("--author-id", type=int, required=True, help="Issue author numeric ID")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
success = process_submission(
|
||||
issue_number=args.issue_number,
|
||||
issue_body=args.issue_body,
|
||||
author_username=args.author,
|
||||
author_id=args.author_id,
|
||||
)
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user