#!/usr/bin/env python3 """Create a Gitea issue. Parameterized over title/body and the target Gitea instance. Two instances are known out of the box: dadeschools -> gitea.dadeschools.net / Contractor / Timesheet prgs -> gitea.prgs.cc / Scaled-Tech-Consulting / Timesheet Auth is pulled from the macOS keychain via `git credential fill` for the chosen host -- no tokens on the command line. Examples: create_issue.py --title "Bug: blank PDF" --body "Blank on Safari" create_issue.py --remote prgs --title "Add tests" --body-file desc.md create_issue.py --host gitea.example.com --org Foo --repo Bar --title "..." """ import os import sys import argparse # Auto-execute using the project's local virtual environment Python PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) venv_python = os.path.join(PROJECT_ROOT, "venv", "bin", "python3") if os.path.exists(venv_python) and sys.executable != venv_python: os.execv(venv_python, [venv_python] + sys.argv) from gitea_auth import ( get_credentials, resolve_remote, add_remote_args, api_request, repo_api_url, ) import issue_workflow_labels def main(argv=None): parser = argparse.ArgumentParser(description="Create a Gitea issue.") add_remote_args(parser) parser.add_argument("--title", required=True, help="Issue title.") parser.add_argument("--body", default="", help="Issue body text.") parser.add_argument("--body-file", help="Read issue body from this file ('-' for stdin).") parser.add_argument("--label", action="append", default=[], help="Existing label name to apply; may be repeated.") parser.add_argument("--type-label", help="Issue type, e.g. feature or type:feature.") parser.add_argument("--status-label", help="Initial status, e.g. ready or status:ready.") parser.add_argument("--discussion", action="store_true", help="Apply type:discussion.") parser.add_argument("--require-workflow-labels", action="store_true", help="Fail unless one type:* and one status:* label are supplied.") args = parser.parse_args(argv) host, org, repo = resolve_remote(args) body = args.body if args.body_file: if args.body_file == "-": body = sys.stdin.read() else: with open(args.body_file, "r", encoding="utf-8") as fh: body = fh.read() requested_labels = issue_workflow_labels.labels_for_new_issue( issue_type=args.type_label, initial_status=args.status_label, extra_labels=args.label, discussion=args.discussion, ) label_assessment = issue_workflow_labels.assess_issue_labels( requested_labels, discussion=args.discussion, ) if args.require_workflow_labels and not label_assessment["valid"]: print( "Workflow label validation failed: " + "; ".join(label_assessment["errors"]), file=sys.stderr, ) return 1 user, password = get_credentials(host) if not user or not password: print(f"Could not get credentials for {host} " f"(no keychain entry? try a manual `git credential fill`).", file=sys.stderr) return 1 import base64 auth = f"Basic {base64.b64encode(f'{user}:{password}'.encode()).decode()}" base = repo_api_url(host, org, repo) url = f"{base}/issues" try: label_ids = [] if requested_labels: existing = api_request("GET", f"{base}/labels", auth) by_name = {lb["name"]: lb["id"] for lb in existing} missing = [name for name in requested_labels if name not in by_name] if missing: print(f"Missing labels: {missing}", file=sys.stderr) return 1 label_ids = [by_name[name] for name in requested_labels] data = api_request("POST", url, auth, {"title": args.title, "body": body}) if label_ids: api_request( "PUT", f"{base}/issues/{data.get('number')}/labels", auth, {"labels": label_ids}, ) print(f"Issue #{data.get('number')}: {data.get('html_url')}") if not label_assessment["valid"]: print( "Workflow label recommendation: " + "; ".join(label_assessment["errors"]), file=sys.stderr, ) return 0 except RuntimeError as e: print(f"Error: {e}", file=sys.stderr) return 1 if __name__ == "__main__": sys.exit(main())