158 lines
5.5 KiB
Python
158 lines
5.5 KiB
Python
#!/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())
|
|
|
|
|