-
Notifications
You must be signed in to change notification settings - Fork 49
feat: support org-level issue templates from .github repo #440
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
Closed
+724
−5
Closed
Changes from all commits
Commits
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
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,175 @@ | ||
| """GitHub organization-level template fetching. | ||
|
|
||
| Checks for issue/PR templates at the organization level via the .github repo. | ||
| """ | ||
|
|
||
| import logging | ||
| import os | ||
| import re | ||
| from typing import List, Optional | ||
|
|
||
| import requests | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class GitHubTemplatesError(Exception): | ||
| """GitHub template fetch errors.""" | ||
|
|
||
| pass | ||
|
|
||
|
|
||
| class GitHubTemplatesFetcher: | ||
| """Fetches organization-level issue/PR templates from .github repos. | ||
|
|
||
| GitHub organizations can define default community health files (issue | ||
| templates, PR templates, CODEOWNERS, etc.) in a dedicated `.github` | ||
| repository that is inherited by all repositories in the organization. | ||
| """ | ||
|
|
||
| def __init__(self, token: Optional[str] = None): | ||
| """Initialize the fetcher. | ||
|
|
||
| Args: | ||
| token: GitHub personal access token (optional, defaults to | ||
| GITHUB_TOKEN environment variable) | ||
| """ | ||
| self.token = token or os.getenv("GITHUB_TOKEN") | ||
| self._api_base = "https://api.github.com" | ||
|
|
||
| @property | ||
| def configured(self) -> bool: | ||
| """Check if a GitHub token is available.""" | ||
| return bool(self.token) | ||
|
|
||
| def _request_headers(self) -> dict: | ||
| """Get default headers for GitHub API requests.""" | ||
| return { | ||
| "Authorization": f"Bearer {self.token}" if self.token else None, | ||
| "Accept": "application/vnd.github.v3+json", | ||
| "User-Agent": "agentready", | ||
| } | ||
|
|
||
| def _fetch_contents(self, owner: str, repo: str, path: str = "") -> List[dict]: | ||
| """Fetch directory contents from a GitHub repository. | ||
|
|
||
| Args: | ||
| owner: Repository owner/organization name | ||
| repo: Repository name | ||
| path: Path within the repository (defaults to root) | ||
|
|
||
| Returns: | ||
| List of file metadata dicts from GitHub API | ||
| """ | ||
| url = f"{self._api_base}/repos/{owner}/{repo}/contents/{path.lstrip('/')}" | ||
|
|
||
| try: | ||
| response = requests.get( | ||
| url, | ||
| headers={k: v for k, v in self._request_headers().items() if v}, | ||
| timeout=30, | ||
| ) | ||
| response.raise_for_status() | ||
| return response.json() | ||
| except requests.HTTPError as e: | ||
| if response.status_code == 404: | ||
| logger.debug("Path not found: %s/%s/%s", owner, repo, path) | ||
| return [] | ||
| elif response.status_code in (401, 403): | ||
| logger.debug("Unauthorized to access %s/%s/%s", owner, repo, path) | ||
| return [] | ||
| raise GitHubTemplatesError( | ||
| f"Failed to fetch contents: {owner}/{repo}/{path}" | ||
| ) from e | ||
| except requests.RequestException as e: | ||
| raise GitHubTemplatesError( | ||
| f"Request failed for {owner}/{repo}/{path}: {e}" | ||
| ) from e | ||
|
|
||
| def fetch_pr_templates(self, owner: str, repo_name: str = ".github") -> List[str]: | ||
| """Check for PR templates in an organization-level .github repo. | ||
|
|
||
| Args: | ||
| owner: GitHub organization name | ||
| repo_name: Repository name (defaults to '.github') | ||
|
|
||
| Returns: | ||
| List of PR template file paths found (e.g. | ||
| ['PULL_REQUEST_TEMPLATE.md']) | ||
| """ | ||
| if not self.configured: | ||
| return [] | ||
|
|
||
| contents = self._fetch_contents(owner, repo_name, ".github") | ||
| if not contents: | ||
| return [] | ||
|
|
||
| pr_filenames = { | ||
| "PULL_REQUEST_TEMPLATE.md": "PULL_REQUEST_TEMPLATE.md", | ||
| "pull_request_template.md": "PULL_REQUEST_TEMPLATE.md", | ||
| } | ||
|
|
||
| found = [] | ||
| for item in contents: | ||
| if item.get("type") == "file" and item.get("name") in pr_filenames: | ||
| found.append(pr_filenames[item["name"]]) | ||
|
|
||
| return sorted(set(found)) | ||
|
|
||
| def fetch_issue_templates( | ||
| self, owner: str, repo_name: str = ".github" | ||
| ) -> List[str]: | ||
| """Check for issue templates in an organization-level .github repo. | ||
|
|
||
| Args: | ||
| owner: GitHub organization name | ||
| repo_name: Repository name (defaults to '.github') | ||
|
|
||
| Returns: | ||
| List of issue template file paths found (e.g. | ||
| ['ISSUE_TEMPLATE/bug_report.md', 'ISSUE_TEMPLATE/feature_request.md']) | ||
| """ | ||
| if not self.configured: | ||
| return [] | ||
|
|
||
| contents = self._fetch_contents(owner, repo_name, ".github/ISSUE_TEMPLATE") | ||
| if not contents: | ||
| return [] | ||
|
|
||
| extensions = {".md", ".yml", ".yaml"} | ||
| found = [] | ||
| for item in contents: | ||
| name = item.get("name", "") | ||
| if item.get("type") == "file" and any( | ||
| name.endswith(ext) for ext in extensions | ||
| ): | ||
| found.append(f"ISSUE_TEMPLATE/{name}") | ||
|
|
||
| return sorted(found) | ||
|
|
||
| def extract_owner(self, url: str) -> str | None: | ||
| """Extract the owner/organization from a GitHub repository URL. | ||
|
|
||
| Supports HTTPS (https://github.com/owner/repo) and SSH | ||
| (git@github.com:owner/repo.git) formats. | ||
|
|
||
| Args: | ||
| url: Repository URL string | ||
|
|
||
| Returns: | ||
| Owner/organization name or None if not parseable | ||
| """ | ||
| if not url: | ||
| return None | ||
|
|
||
| # SSH format: git@github.com:owner/repo.git | ||
| ssh_match = re.match(r"git@github\.com:([^/]+)/.+?$", url) | ||
| if ssh_match: | ||
| return ssh_match.group(1) | ||
|
|
||
| # HTTPS: https://github.com/owner/repo.git or .gitignore etc | ||
| https_match = re.match(r"https://github\.com/([^/]+)/.+?$", url) | ||
| if https_match: | ||
| return https_match.group(1) | ||
|
|
||
| return None |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle GitHub API failures in the assessor instead of propagating exceptions.
fetch_pr_templates()/fetch_issue_templates()can raiseGitHubTemplatesError(e.g., 5xx/network), andassess()currently does not catch it. A transient GitHub outage can therefore fail this assessor run instead of degrading gracefully.Suggested guard pattern
As per coding guidelines, "Check for proper error handling (return skipped/error Finding, don't crash)."
Also applies to: 878-896
🤖 Prompt for AI Agents