#!/usr/bin/env python3 """ Upload metadata.json to teacher-only repository via Gitea API. """ import argparse import base64 import json import os import sys import urllib.error import urllib.request from pathlib import Path from urllib.parse import urlparse def detect_host(server_url: str, external_host: str | None) -> str: """Detect the Gitea host to use for API calls. If server_url uses internal name (like 'gitea'), use external_host instead. """ parsed = urlparse(server_url) raw_host = parsed.netloc or parsed.path.split("/")[0] host = raw_host if raw_host.lower().startswith("gitea"): if not external_host: raise ValueError( f"Server URL uses internal name '{raw_host}' but EXTERNAL_GITEA_HOST is not set. " "Please configure EXTERNAL_GITEA_HOST in .env and run sync_runner_config.sh" ) host = external_host return host def main() -> int: parser = argparse.ArgumentParser(description="Upload metadata.json to course metadata repo") parser.add_argument("--metadata-file", required=True) parser.add_argument("--metadata-repo", required=True, help="owner/repo of metadata store") parser.add_argument("--branch", default="main") parser.add_argument("--student-repo", required=True) parser.add_argument("--run-id", required=True) parser.add_argument("--commit-sha", required=True) parser.add_argument("--workflow", required=True, choices=["grade", "objective", "llm"]) parser.add_argument("--server-url", required=True) parser.add_argument("--external-host") parser.add_argument("--assignment-id", help="Assignment ID (e.g., hw1)") args = parser.parse_args() token = os.environ.get("METADATA_TOKEN") if not token: print("METADATA_TOKEN is not set", file=sys.stderr) return 1 path = Path(args.metadata_file) if not path.is_file(): print(f"metadata file not found: {path}", file=sys.stderr) return 0 try: owner, repo_name = args.metadata_repo.split("/", 1) except ValueError: print(f"Invalid metadata repo: {args.metadata_repo}", file=sys.stderr) return 1 # Extract student ID from student repo name # student repo format: hw1-stu_20250001 or hw1-stu_student1 student_id = args.student_repo.split("/")[-1] # Get repo name # Auto-detect assignment ID from student repo if not provided assignment_id = args.assignment_id if not assignment_id: # Try to extract from student_repo format: hw1-stu_xxx repo_name_part = args.student_repo.split("/")[-1] if "-stu_" in repo_name_part: assignment_id = repo_name_part.split("-stu_")[0] elif "-template" in repo_name_part: assignment_id = repo_name_part.split("-template")[0] elif "-tests" in repo_name_part: assignment_id = repo_name_part.split("-tests")[0] else: assignment_id = "unknown" # New path structure: {assignment_id}/{student_id}/{workflow}_{run_id}_{sha}.json target_path = f"{assignment_id}/{student_id}/{args.workflow}_{args.run_id}_{args.commit_sha[:7]}.json" host = detect_host(args.server_url, args.external_host) api_url = f"http://{host}/api/v1/repos/{owner}/{repo_name}/contents/{target_path}" message = f"Upload {args.workflow} metadata for {args.student_repo} {args.commit_sha}" # Check if file exists to determine if we need to update (PUT) or create (POST) get_req = urllib.request.Request( api_url, headers={"Authorization": f"token {token}"}, method="GET" ) sha = None try: with urllib.request.urlopen(get_req) as resp: existing_file = json.loads(resp.read().decode()) # API may return a list (directory contents) or dict (single file) if isinstance(existing_file, dict): sha = existing_file.get("sha") print(f"File exists, updating (sha: {sha})") elif isinstance(existing_file, list): # Response is a directory listing, file doesn't exist at this exact path print(f"Path is a directory or file not found in expected format") else: print(f"Unexpected response type: {type(existing_file)}") except urllib.error.HTTPError as e: if e.code != 404: print(f"Error checking file existence: {e}", file=sys.stderr) return 1 # File doesn't exist, proceed with creation content = base64.b64encode(path.read_bytes()).decode() payload = { "content": content, "message": message, "branch": args.branch } if sha: payload["sha"] = sha data = json.dumps(payload).encode() req = urllib.request.Request( api_url, data=data, headers={ "Authorization": f"token {token}", "Content-Type": "application/json", }, method="PUT" if sha else "POST", ) try: with urllib.request.urlopen(req, timeout=30) as resp: resp_body = resp.read().decode() print(resp_body) except urllib.error.HTTPError as exc: print(f"Metadata upload failed: {exc.status} {exc.reason}", file=sys.stderr) print(exc.read().decode(), file=sys.stderr) return 1 except urllib.error.URLError as exc: print(f"Metadata upload failed: {exc}", file=sys.stderr) return 1 print(f"✅ Metadata stored at {args.metadata_repo}:{target_path}") return 0 if __name__ == "__main__": raise SystemExit(main())