Compare commits

..

10 Commits

30 changed files with 3044 additions and 46 deletions

194
gitea-shim/README.md Normal file
View File

@ -0,0 +1,194 @@
# Gitea GitHub SDK Shim
A compatibility layer that provides GitHub SDK interfaces (PyGitHub and Octokit) for Gitea repositories. This allows you to use existing GitHub-based code with Gitea instances with minimal changes.
## Installation
### Python
```bash
cd gitea-shim/python
pip install -e .
```
### JavaScript/TypeScript
```bash
cd gitea-shim/javascript
npm install
```
## Usage
### Python (PyGitHub Compatible)
Instead of using PyGitHub:
```python
# Old GitHub code
from github import Github
gh = Github(token)
repo = gh.get_repo("owner/repo")
```
Use the Gitea shim:
```python
# New Gitea code
from gitea_github_shim import GiteaGitHubShim
gh = GiteaGitHubShim(token, base_url="https://your-gitea-instance.com")
repo = gh.get_repo("owner/repo")
```
Or with environment variables:
```python
import os
os.environ['GITEA_URL'] = 'https://your-gitea-instance.com'
# Now you can use it just like GitHub
gh = GiteaGitHubShim(token) # URL is taken from env
repo = gh.get_repo("owner/repo")
```
### JavaScript/TypeScript (Octokit Compatible)
Instead of using Octokit:
```javascript
// Old GitHub code
const { Octokit } = require("@octokit/rest");
const octokit = new Octokit({ auth: token });
const repo = await octokit.rest.repos.get({ owner, repo });
```
Use the Gitea shim:
```javascript
// New Gitea code
const { GiteaOctokitShim } = require("gitea-shim");
const octokit = new GiteaOctokitShim({
auth: token,
baseUrl: "https://your-gitea-instance.com"
});
const repo = await octokit.rest.repos.get({ owner, repo });
```
## Supported APIs
### Repository Operations
- `get_repo()` / `rest.repos.get()` - Get repository
- `create_repo()` / `rest.repos.createForAuthenticatedUser()` - Create repository
- `repo.default_branch` - Get default branch
- `repo.create_fork()` / `rest.repos.createFork()` - Fork repository
### Pull Request Operations
- `repo.get_pull()` / `rest.pulls.get()` - Get pull request
- `repo.get_pulls()` / `rest.pulls.list()` - List pull requests
- `repo.create_pull()` / `rest.pulls.create()` - Create pull request
- `pr.update()` / `rest.pulls.update()` - Update pull request
- `pr.merge()` / `rest.pulls.merge()` - Merge pull request
### User Operations
- `get_user()` / `rest.users.getAuthenticated()` - Get user
- `user.get_repos()` - Get user repositories
- `user.add_to_following()` / `rest.users.follow()` - Follow user
### Activity Operations
- `user.get_starred()` / `rest.activity.listReposStarredByUser()` - Get starred repos
- `user.get_subscriptions()` / `rest.activity.listWatchedReposForUser()` - Get watched repos
## Environment Variables
- `GITEA_URL` - Base URL of your Gitea instance (e.g., `https://gitea.example.com`)
- `GITEA_TOKEN` - Personal access token for Gitea (you can still use `GITHUB_TOKEN` for compatibility)
## Migration Guide
### Step 1: Install the shim
```bash
# Python
pip install gitea-github-shim
# JavaScript
npm install gitea-github-shim
```
### Step 2: Update imports
Python:
```python
# Replace
from github import Github
# With
from gitea_github_shim import GiteaGitHubShim as Github
```
JavaScript:
```javascript
// Replace
const { Octokit } = require("@octokit/rest");
// With
const { GiteaOctokitShim: Octokit } = require("gitea-github-shim");
```
### Step 3: Update initialization
Add the Gitea URL when creating the client:
Python:
```python
gh = Github(token, base_url="https://your-gitea-instance.com")
```
JavaScript:
```javascript
const octokit = new Octokit({
auth: token,
baseUrl: "https://your-gitea-instance.com"
});
```
### Step 4: Handle API differences
Some features may not be available or work differently in Gitea:
1. **Draft PRs** - May not be supported
2. **Rate Limiting** - Gitea typically doesn't have rate limits
3. **GraphQL API** - Not available in Gitea
4. **Advanced Search** - Limited compared to GitHub
## Development
### Running Tests
```bash
# Python tests
cd gitea-shim/python
python -m pytest tests/
# JavaScript tests
cd gitea-shim/javascript
npm test
```
### Contributing
1. Check the `todo.md` file for pending tasks
2. Add tests for any new functionality
3. Update this README with new supported APIs
4. Submit a pull request
## Limitations
- Some GitHub-specific features are not available in Gitea
- API responses may have slight differences in structure
- Not all endpoints are implemented yet
- GraphQL is not supported (REST API only)
## License
MIT License - See LICENSE file for details

View File

@ -0,0 +1,216 @@
# Migration Guide for Workflows
This guide shows how to migrate your workflow files from direct GitHub API usage to the Gitea shim.
## Quick Migration
### Step 1: Add Import Path
Add this to the top of your workflow files:
```python
import sys
from pathlib import Path
# Add the gitea-shim to the path
gitea_shim_path = Path(__file__).parent.parent.parent.parent.parent / "gitea-shim" / "python"
sys.path.insert(0, str(gitea_shim_path))
try:
from gitea_shim import get_github_client, is_gitea_mode
except ImportError:
# Fallback to regular GitHub if shim is not available
from github import Github as get_github_client
is_gitea_mode = lambda: False
```
### Step 2: Replace GitHub Client Creation
**Before:**
```python
from github import Github
gh = Github(os.getenv("GITHUB_TOKEN"))
```
**After:**
```python
gh = get_github_client()
```
### Step 3: Add Logging (Optional)
Add logging to see which API you're using:
```python
log_key_value("API Mode", "Gitea" if is_gitea_mode() else "GitHub")
```
## Complete Example
### Before Migration
```python
"""Task decomposition workflow implementation."""
import os
from github import Github
import requests
from prometheus_swarm.workflows.base import Workflow
# ... other imports
class RepoSummarizerWorkflow(Workflow):
def setup(self):
"""Set up repository and workspace."""
check_required_env_vars(["GITHUB_TOKEN", "GITHUB_USERNAME"])
validate_github_auth(os.getenv("GITHUB_TOKEN"), os.getenv("GITHUB_USERNAME"))
# Get the default branch from GitHub
try:
gh = Github(os.getenv("GITHUB_TOKEN"))
self.context["repo_full_name"] = (
f"{self.context['repo_owner']}/{self.context['repo_name']}"
)
repo = gh.get_repo(
f"{self.context['repo_owner']}/{self.context['repo_name']}"
)
self.context["base"] = repo.default_branch
log_key_value("Default branch", self.context["base"])
except Exception as e:
log_error(e, "Failed to get default branch, using 'main'")
self.context["base"] = "main"
```
### After Migration
```python
"""Task decomposition workflow implementation."""
import os
import sys
from pathlib import Path
# Add the gitea-shim to the path
gitea_shim_path = Path(__file__).parent.parent.parent.parent.parent / "gitea-shim" / "python"
sys.path.insert(0, str(gitea_shim_path))
try:
from gitea_shim import get_github_client, is_gitea_mode
except ImportError:
# Fallback to regular GitHub if shim is not available
from github import Github as get_github_client
is_gitea_mode = lambda: False
import requests
from prometheus_swarm.workflows.base import Workflow
# ... other imports
class RepoSummarizerWorkflow(Workflow):
def setup(self):
"""Set up repository and workspace."""
check_required_env_vars(["GITHUB_TOKEN", "GITHUB_USERNAME"])
validate_github_auth(os.getenv("GITHUB_TOKEN"), os.getenv("GITHUB_USERNAME"))
# Get the default branch from GitHub/Gitea
try:
# Use the shim to get GitHub-compatible client
gh = get_github_client()
self.context["repo_full_name"] = (
f"{self.context['repo_owner']}/{self.context['repo_name']}"
)
repo = gh.get_repo(
f"{self.context['repo_owner']}/{self.context['repo_name']}"
)
self.context["base"] = repo.default_branch
log_key_value("Default branch", self.context["base"])
log_key_value("API Mode", "Gitea" if is_gitea_mode() else "GitHub")
except Exception as e:
log_error(e, "Failed to get default branch, using 'main'")
self.context["base"] = "main"
```
## Environment Configuration
### For GitHub (Default)
```bash
export GITHUB_TOKEN="your_github_token"
# USE_GITEA is not set or set to 'false'
```
### For Gitea
```bash
export USE_GITEA="true"
export GITEA_URL="http://your-gitea-instance:3000"
export GITEA_TOKEN="your_gitea_token" # or use GITHUB_TOKEN
```
## Testing the Migration
1. **Test with GitHub:**
```bash
export GITHUB_TOKEN="your_token"
python your_workflow.py
```
2. **Test with Gitea:**
```bash
export USE_GITEA="true"
export GITEA_URL="http://localhost:3000"
export GITHUB_TOKEN="your_token"
python your_workflow.py
```
3. **Run the shim test:**
```bash
cd gitea-shim/python
python test_shim.py
```
## Common Issues and Solutions
### Issue: Import Error
**Error:** `ModuleNotFoundError: No module named 'gitea_shim'`
**Solution:** Check that the path to the gitea-shim is correct in your import statement.
### Issue: Gitea Connection Failed
**Error:** `Connection refused` or `404 Not Found`
**Solution:**
1. Verify `GITEA_URL` is correct
2. Ensure Gitea is running
3. Check that your token has the right permissions
### Issue: Repository Not Found
**Error:** `Repository owner/repo not found`
**Solution:**
1. Verify the repository exists in your Gitea instance
2. Check that your token has access to the repository
3. Ensure the repository name is correct (case-sensitive)
### Issue: Authentication Failed
**Error:** `401 Unauthorized`
**Solution:**
1. Verify your token is valid
2. Check token permissions
3. Ensure `GITHUB_TOKEN` or `GITEA_TOKEN` is set correctly
## Benefits of Migration
1. **Code Reusability:** Same code works with both GitHub and Gitea
2. **Easy Testing:** Test against Gitea locally, deploy to GitHub
3. **Flexibility:** Switch between APIs without code changes
4. **Consistency:** Same interface regardless of backend
5. **Future-Proof:** Easy to add support for other Git hosting platforms
## Support
If you encounter issues during migration:
1. Check the test output: `python test_shim.py`
2. Verify environment variables are set correctly
3. Check the logs for detailed error messages
4. Ensure Gitea is running and accessible

178
gitea-shim/python/README.md Normal file
View File

@ -0,0 +1,178 @@
# Gitea GitHub Shim
A Python library that provides a GitHub-compatible interface for Gitea, allowing you to use the same code with both GitHub and Gitea APIs.
## Features
- **Drop-in replacement** for PyGitHub's `Github` class
- **Automatic switching** between GitHub and Gitea based on environment variables
- **GitHub-compatible interface** - minimal code changes required
- **Error handling** and fallback mechanisms
- **Comprehensive model wrappers** for repositories, users, and pull requests
## Installation
1. Clone this repository or copy the `gitea-shim/python` directory to your project
2. Install the required dependencies:
```bash
pip install gitea
```
## Configuration
The shim automatically detects whether to use GitHub or Gitea based on environment variables:
### GitHub Mode (Default)
```bash
export GITHUB_TOKEN="your_github_token"
# USE_GITEA is not set or set to 'false'
```
### Gitea Mode
```bash
export USE_GITEA="true"
export GITEA_URL="http://your-gitea-instance:3000"
export GITEA_TOKEN="your_gitea_token" # or use GITHUB_TOKEN
```
## Usage
### Basic Usage
```python
from gitea_shim import get_github_client
# This will automatically use GitHub or Gitea based on your environment
gh = get_github_client()
# Get a repository (works the same for both APIs)
repo = gh.get_repo("owner/repo")
# Get user information
user = gh.get_user()
# Create a pull request
pr = repo.create_pull(
title="My PR",
body="Description",
head="feature-branch",
base="main"
)
```
### Migration from PyGitHub
**Before (PyGitHub only):**
```python
from github import Github
gh = Github("your_token")
repo = gh.get_repo("owner/repo")
```
**After (with shim):**
```python
from gitea_shim import get_github_client
gh = get_github_client() # Works with both GitHub and Gitea
repo = gh.get_repo("owner/repo") # Same interface!
```
### Advanced Configuration
```python
from gitea_shim import config, is_gitea_mode, get_api_info
# Check which API you're using
if is_gitea_mode():
print("Using Gitea API")
else:
print("Using GitHub API")
# Get detailed API information
api_info = get_api_info()
print(f"API Type: {api_info['type']}")
print(f"URL: {api_info['url']}")
print(f"Token Set: {api_info['token_set']}")
```
## Supported Methods
### Client Methods
- `get_repo(full_name)` - Get repository by owner/repo name
- `get_user(login=None)` - Get user (authenticated or by login)
- `get_organization(login)` - Get organization
- `create_repo(name, **kwargs)` - Create new repository
- `search_repositories(query, **kwargs)` - Search repositories
- `search_users(query, **kwargs)` - Search users
- `get_api_status()` - Get API status
- `get_rate_limit()` - Get rate limit info
- `get_emojis()` - Get available emojis
- `get_gitignore_templates()` - Get gitignore templates
- `get_license_templates()` - Get license templates
### Repository Methods
- `get_pull(number)` - Get pull request by number
- `get_pulls(**kwargs)` - Get list of pull requests
- `create_pull(title, body, head, base, **kwargs)` - Create pull request
- `get_contents(path, ref=None)` - Get file contents
- `get_branches()` - Get all branches
- `get_branch(branch)` - Get specific branch
- `create_fork(organization=None)` - Fork repository
- `get_commits(**kwargs)` - Get commits
- `get_commit(sha)` - Get specific commit
### Pull Request Methods
- `update(**kwargs)` - Update pull request
- `merge(**kwargs)` - Merge pull request
- `get_commits()` - Get commits in PR
- `get_files()` - Get files changed in PR
- `create_review_comment(body, commit_id, path, position)` - Add review comment
- `create_issue_comment(body)` - Add issue comment
## Testing
Run the test suite to verify the shim works correctly:
```bash
cd gitea-shim/python
python test_shim.py
```
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `USE_GITEA` | Enable Gitea mode | `false` |
| `GITEA_URL` | Gitea instance URL | `http://localhost:3000` |
| `GITHUB_TOKEN` | GitHub/Gitea API token | Required |
| `GITEA_TOKEN` | Gitea-specific token (optional) | Uses `GITHUB_TOKEN` |
## Error Handling
The shim includes comprehensive error handling:
- **Graceful fallbacks** when Gitea methods don't exist
- **Compatible error messages** with GitHub's error format
- **Automatic retry logic** for transient failures
- **Detailed logging** for debugging
## Limitations
- Some GitHub-specific features may not be available in Gitea
- Rate limiting is mocked for Gitea (returns unlimited)
- Some advanced search features may differ between APIs
- Draft pull requests may not be supported in all Gitea versions
## Contributing
1. Fork the repository
2. Create a feature branch
3. Add tests for new functionality
4. Ensure all tests pass
5. Submit a pull request
## License
This project is licensed under the MIT License - see the LICENSE file for details.

View File

@ -0,0 +1,19 @@
"""Gitea GitHub Shim - A GitHub-compatible interface for Gitea."""
from .gitea_github_shim import GiteaGitHubShim
from .config import get_github_client, is_gitea_mode, get_api_info, config
from .models.repository import Repository
from .models.user import User
from .models.pull_request import PullRequest
__version__ = "0.1.0"
__all__ = [
"GiteaGitHubShim",
"get_github_client",
"is_gitea_mode",
"get_api_info",
"config",
"Repository",
"User",
"PullRequest"
]

View File

@ -0,0 +1,93 @@
"""Configuration for GitHub/Gitea API switching."""
import os
from typing import Optional
class APIConfig:
"""Configuration class for API switching between GitHub and Gitea."""
def __init__(self):
self.use_gitea = os.getenv('USE_GITEA', 'false').lower() == 'true'
self.gitea_url = os.getenv('GITEA_URL', 'http://localhost:3000')
self.github_token = os.getenv('GITHUB_TOKEN')
self.gitea_token = os.getenv('GITEA_TOKEN') or self.github_token
def get_client_class(self):
"""Get the appropriate client class based on configuration."""
if self.use_gitea:
try:
from gitea_github_shim import GiteaGitHubShim
return GiteaGitHubShim
except ImportError:
# Fallback to GitHub if Gitea shim is not available
from github import Github
return Github
else:
from github import Github
return Github
def get_client_kwargs(self):
"""Get the appropriate client initialization arguments."""
if self.use_gitea:
return {
'base_url_or_token': self.gitea_token,
'base_url': self.gitea_url
}
else:
# For GitHub, we need to pass the token as a positional argument
# and return empty kwargs since PyGitHub doesn't use keyword args for token
return {}
def get_client_args(self):
"""Get the positional arguments for client initialization."""
if self.use_gitea:
# Gitea shim uses keyword arguments
return []
else:
# GitHub uses positional argument for token
return [self.github_token] if self.github_token else []
# Global configuration instance
config = APIConfig()
def get_github_client():
"""
Get a GitHub-compatible client (either real GitHub or Gitea shim).
Returns:
GitHub-compatible client instance
"""
client_class = config.get_client_class()
args = config.get_client_args()
kwargs = config.get_client_kwargs()
if not config.github_token and not config.gitea_token:
raise ValueError(
"No API token found. Please set either GITHUB_TOKEN or GITEA_TOKEN environment variable."
)
return client_class(*args, **kwargs)
def is_gitea_mode():
"""Check if we're running in Gitea mode."""
return config.use_gitea
def get_api_info():
"""Get information about the current API configuration."""
if config.use_gitea:
return {
'type': 'gitea',
'url': config.gitea_url,
'token_set': bool(config.gitea_token)
}
else:
return {
'type': 'github',
'url': 'https://api.github.com',
'token_set': bool(config.github_token)
}

View File

@ -0,0 +1,251 @@
"""Main shim class that provides GitHub-compatible interface for Gitea."""
import os
from typing import Optional, Dict, Any, List
try:
# When running tests or using as a module
from models.repository import Repository
from models.user import User
from models.pull_request import PullRequest
except ImportError:
# When using as a package
from .models.repository import Repository
from .models.user import User
from .models.pull_request import PullRequest
# Mock the Gitea import for testing
try:
from gitea import Gitea
except ImportError:
# Mock Gitea for testing purposes
class Gitea:
def __init__(self, url, token):
self.url = url
self.token = token
class GiteaGitHubShim:
"""
A shim class that provides a GitHub-compatible interface for Gitea.
This class mimics the PyGitHub's Github class API, translating calls
to the py-gitea SDK.
"""
def __init__(self, base_url_or_token: Optional[str] = None,
base_url: Optional[str] = None,
timeout: int = 60,
per_page: int = 30):
"""
Initialize the Gitea client with GitHub-compatible interface.
Args:
base_url_or_token: If only one arg provided, treated as token (GitHub compat)
base_url: The Gitea instance URL
timeout: Request timeout in seconds
per_page: Number of items per page for pagination
"""
# Handle GitHub-style initialization where only token is provided
if base_url is None and base_url_or_token:
# In GitHub mode, we expect GITEA_URL to be set
self.token = base_url_or_token
self.base_url = os.getenv('GITEA_URL', 'http://localhost:3000')
else:
self.token = base_url_or_token
self.base_url = base_url or os.getenv('GITEA_URL', 'http://localhost:3000')
# Remove trailing slash from base URL
self.base_url = self.base_url.rstrip('/')
# Initialize the Gitea client
self._gitea = Gitea(self.base_url, self.token)
self.timeout = timeout
self.per_page = per_page
def get_repo(self, full_name_or_id: str) -> Repository:
"""
Get a repository by its full name (owner/repo) or ID.
Args:
full_name_or_id: Repository full name (owner/repo) or ID
Returns:
Repository object with GitHub-compatible interface
"""
if '/' in str(full_name_or_id):
# It's a full name (owner/repo)
owner, repo_name = full_name_or_id.split('/', 1)
try:
gitea_repo = self._gitea.get_repo(owner, repo_name)
except Exception as e:
# Handle case where repo doesn't exist
raise Exception(f"Repository {full_name_or_id} not found: {str(e)}")
else:
# It's an ID - Gitea doesn't support this directly
# We'd need to implement a search or listing mechanism
raise NotImplementedError("Getting repository by ID is not yet supported")
return Repository(gitea_repo, self._gitea)
def get_user(self, login: Optional[str] = None) -> User:
"""
Get a user by login name or get the authenticated user.
Args:
login: Username to get. If None, returns authenticated user.
Returns:
User object with GitHub-compatible interface
"""
if login is None:
# Get authenticated user
gitea_user = self._gitea.get_user()
else:
# Get specific user
gitea_user = self._gitea.get_user(login)
return User(gitea_user, self._gitea)
def get_organization(self, login: str):
"""
Get an organization by login name.
Args:
login: Organization name
Returns:
Organization object (not yet implemented)
"""
# Organizations in Gitea are similar to GitHub
gitea_org = self._gitea.get_org(login)
# TODO: Implement Organization model
return gitea_org
def create_repo(self, name: str, **kwargs) -> Repository:
"""
Create a new repository.
Args:
name: Repository name
**kwargs: Additional parameters (description, private, etc.)
Returns:
Repository object
"""
# Map GitHub parameters to Gitea parameters
gitea_params = {
'name': name,
'description': kwargs.get('description', ''),
'private': kwargs.get('private', False),
'auto_init': kwargs.get('auto_init', False),
'gitignores': kwargs.get('gitignore_template', ''),
'license': kwargs.get('license_template', ''),
'readme': kwargs.get('readme', '')
}
gitea_repo = self._gitea.create_repo(**gitea_params)
return Repository(gitea_repo, self._gitea)
def get_api_status(self) -> Dict[str, Any]:
"""Get API status information."""
# Gitea doesn't have a direct equivalent, return version info
try:
version = self._gitea.get_version()
return {
'status': 'good',
'version': version,
'api': 'gitea'
}
except Exception:
return {
'status': 'unknown',
'version': 'unknown',
'api': 'gitea'
}
def get_rate_limit(self) -> Dict[str, Any]:
"""
Get rate limit information.
Note: Gitea doesn't have rate limiting like GitHub, so we return
mock data indicating no limits.
"""
return {
'rate': {
'limit': 999999,
'remaining': 999999,
'reset': 0
}
}
def search_repositories(self, query: str, **kwargs) -> List[Repository]:
"""
Search repositories.
Args:
query: Search query
**kwargs: Additional search parameters
Returns:
List of Repository objects
"""
# Gitea search might have different parameters
gitea_repos = self._gitea.search_repos(query, **kwargs)
return [Repository(repo, self._gitea) for repo in gitea_repos]
def search_users(self, query: str, **kwargs) -> List[User]:
"""
Search users.
Args:
query: Search query
**kwargs: Additional search parameters
Returns:
List of User objects
"""
gitea_users = self._gitea.search_users(query, **kwargs)
return [User(user, self._gitea) for user in gitea_users]
def get_emojis(self) -> Dict[str, str]:
"""
Get available emojis.
Note: Gitea might not support this, return empty dict
"""
return {}
def get_gitignore_templates(self) -> List[str]:
"""
Get available gitignore templates.
Returns:
List of template names
"""
try:
templates = self._gitea.get_gitignore_templates()
return [t.name for t in templates]
except Exception:
return []
def get_license_templates(self) -> List[Dict[str, Any]]:
"""
Get available license templates.
Returns:
List of license template objects
"""
try:
licenses = self._gitea.get_license_templates()
return [
{
'key': lic.key,
'name': lic.name,
'url': lic.url,
'spdx_id': lic.spdx_id
}
for lic in licenses
]
except Exception:
return []

View File

@ -0,0 +1,7 @@
"""Model classes for Gitea GitHub shim."""
from .repository import Repository
from .pull_request import PullRequest
from .user import User
__all__ = ['Repository', 'PullRequest', 'User']

View File

@ -0,0 +1,266 @@
"""Pull Request model that provides GitHub-compatible interface for Gitea pull requests."""
from typing import Optional, Dict, Any, List
class PullRequest:
"""
Pull Request wrapper that provides GitHub-compatible interface for Gitea pull requests.
"""
def __init__(self, gitea_pr, gitea_repo, gitea_client):
"""
Initialize pull request wrapper.
Args:
gitea_pr: The Gitea pull request object
gitea_repo: The Gitea repository object
gitea_client: The Gitea client instance
"""
self._pr = gitea_pr
self._repo = gitea_repo
self._gitea = gitea_client
# Map Gitea attributes to GitHub attributes
self.number = gitea_pr.number
self.state = gitea_pr.state
self.title = gitea_pr.title
self.body = gitea_pr.body
self.created_at = gitea_pr.created_at
self.updated_at = gitea_pr.updated_at
self.closed_at = gitea_pr.closed_at
self.merged_at = gitea_pr.merged_at
self.merge_commit_sha = gitea_pr.merge_commit_sha
self.html_url = gitea_pr.html_url
self.diff_url = gitea_pr.diff_url
self.patch_url = gitea_pr.patch_url
self.mergeable = gitea_pr.mergeable
self.merged = gitea_pr.merged
self.draft = getattr(gitea_pr, 'draft', False) # Gitea might not have draft PRs
# User information
self.user = self._create_user_object(gitea_pr.user)
self.assignee = self._create_user_object(gitea_pr.assignee) if gitea_pr.assignee else None
self.assignees = [self._create_user_object(a) for a in getattr(gitea_pr, 'assignees', [])]
# Branch information
self.base = self._create_branch_info(gitea_pr.base)
self.head = self._create_branch_info(gitea_pr.head)
# Labels and milestone
self.labels = [self._create_label_object(l) for l in getattr(gitea_pr, 'labels', [])]
self.milestone = self._create_milestone_object(gitea_pr.milestone) if gitea_pr.milestone else None
def _create_user_object(self, gitea_user) -> Dict[str, Any]:
"""Create a GitHub-compatible user object."""
if not gitea_user:
return None
return {
'login': gitea_user.login,
'id': gitea_user.id,
'avatar_url': gitea_user.avatar_url,
'type': 'User'
}
def _create_branch_info(self, gitea_branch) -> Dict[str, Any]:
"""Create a GitHub-compatible branch info object."""
return {
'ref': gitea_branch.ref,
'sha': gitea_branch.sha,
'repo': {
'name': gitea_branch.repo.name,
'full_name': gitea_branch.repo.full_name,
'owner': self._create_user_object(gitea_branch.repo.owner)
} if gitea_branch.repo else None
}
def _create_label_object(self, gitea_label) -> Dict[str, Any]:
"""Create a GitHub-compatible label object."""
return {
'name': gitea_label.name,
'color': gitea_label.color,
'description': gitea_label.description
}
def _create_milestone_object(self, gitea_milestone) -> Dict[str, Any]:
"""Create a GitHub-compatible milestone object."""
return {
'title': gitea_milestone.title,
'description': gitea_milestone.description,
'state': gitea_milestone.state,
'number': gitea_milestone.id,
'due_on': gitea_milestone.due_on
}
def update(self, **kwargs) -> 'PullRequest':
"""
Update the pull request.
Args:
**kwargs: Fields to update (title, body, state, base, etc.)
Returns:
Updated PullRequest object
"""
# Map GitHub parameters to Gitea parameters
update_params = {}
if 'title' in kwargs:
update_params['title'] = kwargs['title']
if 'body' in kwargs:
update_params['body'] = kwargs['body']
if 'state' in kwargs:
update_params['state'] = kwargs['state']
if 'assignee' in kwargs:
update_params['assignee'] = kwargs['assignee']
if 'assignees' in kwargs:
update_params['assignees'] = kwargs['assignees']
if 'labels' in kwargs:
update_params['labels'] = kwargs['labels']
if 'milestone' in kwargs:
update_params['milestone'] = kwargs['milestone']
# Update using Gitea API
updated_pr = self._pr.update(**update_params)
# Return new PullRequest instance with updated data
return PullRequest(updated_pr, self._repo, self._gitea)
def merge(self, commit_title: Optional[str] = None,
commit_message: Optional[str] = None,
sha: Optional[str] = None,
merge_method: str = 'merge') -> Dict[str, Any]:
"""
Merge the pull request.
Args:
commit_title: Title for the merge commit
commit_message: Message for the merge commit
sha: SHA that must match the HEAD of the PR
merge_method: Merge method ('merge', 'squash', 'rebase')
Returns:
Merge result information
"""
# Map GitHub merge methods to Gitea
gitea_merge_style = {
'merge': 'merge',
'squash': 'squash',
'rebase': 'rebase'
}.get(merge_method, 'merge')
# Merge using Gitea API
result = self._pr.merge(
style=gitea_merge_style,
title=commit_title,
message=commit_message
)
return {
'sha': result.sha if hasattr(result, 'sha') else None,
'merged': True,
'message': 'Pull Request successfully merged'
}
def get_commits(self) -> List[Dict[str, Any]]:
"""
Get commits in the pull request.
Returns:
List of commit objects
"""
gitea_commits = self._pr.get_commits()
commits = []
for commit in gitea_commits:
commits.append({
'sha': commit.sha,
'commit': {
'message': commit.commit.message,
'author': {
'name': commit.commit.author.name,
'email': commit.commit.author.email,
'date': commit.commit.author.date
}
},
'html_url': commit.html_url
})
return commits
def get_files(self) -> List[Dict[str, Any]]:
"""
Get files changed in the pull request.
Returns:
List of file objects
"""
gitea_files = self._pr.get_files()
files = []
for file in gitea_files:
files.append({
'filename': file.filename,
'status': file.status,
'additions': file.additions,
'deletions': file.deletions,
'changes': file.changes,
'patch': file.patch,
'sha': file.sha,
'blob_url': file.blob_url,
'raw_url': file.raw_url
})
return files
def create_review_comment(self, body: str, commit_id: str, path: str,
position: int) -> Dict[str, Any]:
"""
Create a review comment on the pull request.
Args:
body: Comment body
commit_id: Commit SHA to comment on
path: File path to comment on
position: Line position in the diff
Returns:
Comment object
"""
comment = self._pr.create_review_comment(
body=body,
commit_id=commit_id,
path=path,
position=position
)
return {
'id': comment.id,
'body': comment.body,
'path': comment.path,
'position': comment.position,
'commit_id': comment.commit_id,
'user': self._create_user_object(comment.user),
'created_at': comment.created_at,
'updated_at': comment.updated_at
}
def create_issue_comment(self, body: str) -> Dict[str, Any]:
"""
Create a general comment on the pull request.
Args:
body: Comment body
Returns:
Comment object
"""
comment = self._pr.create_comment(body=body)
return {
'id': comment.id,
'body': comment.body,
'user': self._create_user_object(comment.user),
'created_at': comment.created_at,
'updated_at': comment.updated_at
}

View File

@ -0,0 +1,291 @@
"""Repository model that provides GitHub-compatible interface for Gitea repositories."""
from typing import Optional, List, Dict, Any
try:
# When running tests or using as a module
from models.pull_request import PullRequest
except ImportError:
# When using as a package
from .pull_request import PullRequest
class Repository:
"""
Repository wrapper that provides GitHub-compatible interface for Gitea repositories.
"""
def __init__(self, gitea_repo, gitea_client):
"""
Initialize repository wrapper.
Args:
gitea_repo: The Gitea repository object
gitea_client: The Gitea client instance
"""
self._repo = gitea_repo
self._gitea = gitea_client
# Map Gitea attributes to GitHub attributes
self.name = gitea_repo.name
self.full_name = gitea_repo.full_name
self.description = gitea_repo.description
self.private = gitea_repo.private
self.fork = gitea_repo.fork
self.created_at = gitea_repo.created_at
self.updated_at = gitea_repo.updated_at
self.pushed_at = gitea_repo.updated_at # Gitea doesn't have pushed_at
self.size = gitea_repo.size
self.stargazers_count = gitea_repo.stars_count
self.watchers_count = gitea_repo.watchers_count
self.forks_count = gitea_repo.forks_count
self.open_issues_count = gitea_repo.open_issues_count
self.default_branch = gitea_repo.default_branch
self.archived = gitea_repo.archived
self.html_url = gitea_repo.html_url
self.clone_url = gitea_repo.clone_url
self.ssh_url = gitea_repo.ssh_url
# Owner information
self.owner = self._create_owner_object(gitea_repo.owner)
def _create_owner_object(self, gitea_owner) -> Dict[str, Any]:
"""Create a GitHub-compatible owner object."""
return {
'login': gitea_owner.login,
'id': gitea_owner.id,
'avatar_url': gitea_owner.avatar_url,
'type': 'User' if not getattr(gitea_owner, 'is_organization', False) else 'Organization'
}
def get_pull(self, number: int) -> PullRequest:
"""
Get a pull request by number.
Args:
number: Pull request number
Returns:
PullRequest object
"""
gitea_pr = self._repo.get_pull(number)
return PullRequest(gitea_pr, self._repo, self._gitea)
def get_pulls(self, state: str = 'open', sort: str = 'created',
direction: str = 'desc', base: Optional[str] = None,
head: Optional[str] = None) -> List[PullRequest]:
"""
Get pull requests for the repository.
Args:
state: State of PRs to return ('open', 'closed', 'all')
sort: Sort field ('created', 'updated', 'popularity')
direction: Sort direction ('asc', 'desc')
base: Filter by base branch
head: Filter by head branch
Returns:
List of PullRequest objects
"""
# Map GitHub state to Gitea state
gitea_state = state
if state == 'all':
gitea_state = None # Gitea uses None for all
gitea_prs = self._repo.get_pulls(state=gitea_state)
# Convert to PullRequest objects
pulls = [PullRequest(pr, self._repo, self._gitea) for pr in gitea_prs]
# Apply additional filters if needed
if base:
pulls = [pr for pr in pulls if pr.base.ref == base]
if head:
pulls = [pr for pr in pulls if pr.head.ref == head]
return pulls
def create_pull(self, title: str, body: str, head: str, base: str,
draft: bool = False, **kwargs) -> PullRequest:
"""
Create a new pull request.
Args:
title: PR title
body: PR description
head: Source branch
base: Target branch
draft: Whether PR is a draft
**kwargs: Additional parameters
Returns:
PullRequest object
"""
# Create PR using Gitea API
gitea_pr = self._repo.create_pull_request(
title=title,
body=body,
head=head,
base=base
# Note: Gitea might not support draft PRs
)
return PullRequest(gitea_pr, self._repo, self._gitea)
def get_contents(self, path: str, ref: Optional[str] = None) -> Any:
"""
Get file contents from repository.
Args:
path: File path
ref: Git ref (branch, tag, commit SHA)
Returns:
File contents object
"""
# Get file contents using Gitea API
contents = self._repo.get_file_contents(path, ref=ref)
# Create GitHub-compatible response
return {
'name': contents.name,
'path': contents.path,
'sha': contents.sha,
'size': contents.size,
'type': contents.type,
'content': contents.content,
'encoding': contents.encoding,
'download_url': contents.download_url,
'html_url': contents.html_url
}
def get_branches(self) -> List[Dict[str, Any]]:
"""
Get all branches in the repository.
Returns:
List of branch objects
"""
gitea_branches = self._repo.get_branches()
branches = []
for branch in gitea_branches:
branches.append({
'name': branch.name,
'commit': {
'sha': branch.commit.id,
'url': branch.commit.url
},
'protected': branch.protected
})
return branches
def get_branch(self, branch: str) -> Dict[str, Any]:
"""
Get a specific branch.
Args:
branch: Branch name
Returns:
Branch object
"""
gitea_branch = self._repo.get_branch(branch)
return {
'name': gitea_branch.name,
'commit': {
'sha': gitea_branch.commit.id,
'url': gitea_branch.commit.url
},
'protected': gitea_branch.protected
}
def create_fork(self, organization: Optional[str] = None) -> 'Repository':
"""
Create a fork of the repository.
Args:
organization: Organization to fork to (optional)
Returns:
Repository object for the fork
"""
gitea_fork = self._repo.create_fork(organization=organization)
return Repository(gitea_fork, self._gitea)
def get_commits(self, sha: Optional[str] = None, path: Optional[str] = None,
since: Optional[str] = None, until: Optional[str] = None) -> List[Dict[str, Any]]:
"""
Get commits from the repository.
Args:
sha: Starting commit SHA or branch
path: Only commits containing this path
since: Only commits after this date
until: Only commits before this date
Returns:
List of commit objects
"""
# Note: Gitea API might have different parameters
gitea_commits = self._repo.get_commits()
commits = []
for commit in gitea_commits:
commits.append({
'sha': commit.sha,
'commit': {
'message': commit.commit.message,
'author': {
'name': commit.commit.author.name,
'email': commit.commit.author.email,
'date': commit.commit.author.date
},
'committer': {
'name': commit.commit.committer.name,
'email': commit.commit.committer.email,
'date': commit.commit.committer.date
}
},
'html_url': commit.html_url,
'author': commit.author,
'committer': commit.committer
})
return commits
def get_commit(self, sha: str) -> Dict[str, Any]:
"""
Get a specific commit.
Args:
sha: Commit SHA
Returns:
Commit object
"""
gitea_commit = self._repo.get_commit(sha)
return {
'sha': gitea_commit.sha,
'commit': {
'message': gitea_commit.commit.message,
'author': {
'name': gitea_commit.commit.author.name,
'email': gitea_commit.commit.author.email,
'date': gitea_commit.commit.author.date
},
'committer': {
'name': gitea_commit.commit.committer.name,
'email': gitea_commit.commit.committer.email,
'date': gitea_commit.commit.committer.date
}
},
'html_url': gitea_commit.html_url,
'author': gitea_commit.author,
'committer': gitea_commit.committer,
'stats': gitea_commit.stats,
'files': gitea_commit.files
}

View File

@ -0,0 +1,246 @@
"""User model that provides GitHub-compatible interface for Gitea users."""
from typing import Optional, List, Dict, Any
class User:
"""
User wrapper that provides GitHub-compatible interface for Gitea users.
"""
def __init__(self, gitea_user, gitea_client):
"""
Initialize user wrapper.
Args:
gitea_user: The Gitea user object
gitea_client: The Gitea client instance
"""
self._user = gitea_user
self._gitea = gitea_client
# Map Gitea attributes to GitHub attributes
self.login = gitea_user.login
self.id = gitea_user.id
self.avatar_url = gitea_user.avatar_url
self.html_url = gitea_user.html_url
self.type = 'User' # Could be 'User' or 'Organization'
self.name = gitea_user.full_name
self.company = getattr(gitea_user, 'company', None)
self.blog = getattr(gitea_user, 'website', None)
self.location = gitea_user.location
self.email = gitea_user.email
self.bio = getattr(gitea_user, 'description', None)
self.public_repos = getattr(gitea_user, 'public_repos', 0)
self.followers = getattr(gitea_user, 'followers_count', 0)
self.following = getattr(gitea_user, 'following_count', 0)
self.created_at = gitea_user.created
self.updated_at = getattr(gitea_user, 'last_login', gitea_user.created)
def get_repos(self, type: str = 'owner', sort: str = 'full_name',
direction: str = 'asc') -> List[Any]:
"""
Get repositories for the user.
Args:
type: Type of repos to return ('all', 'owner', 'member')
sort: Sort field ('created', 'updated', 'pushed', 'full_name')
direction: Sort direction ('asc', 'desc')
Returns:
List of Repository objects
"""
# Import here to avoid circular imports
try:
# When running tests or using as a module
from models.repository import Repository
except ImportError:
# When using as a package
from .repository import Repository
# Get repos using Gitea API
gitea_repos = self._user.get_repos()
# Convert to Repository objects
repos = [Repository(repo, self._gitea) for repo in gitea_repos]
# Apply sorting
if sort == 'full_name':
repos.sort(key=lambda r: r.full_name, reverse=(direction == 'desc'))
elif sort == 'created':
repos.sort(key=lambda r: r.created_at, reverse=(direction == 'desc'))
elif sort == 'updated':
repos.sort(key=lambda r: r.updated_at, reverse=(direction == 'desc'))
return repos
def get_starred(self) -> List[Any]:
"""
Get repositories starred by the user.
Returns:
List of Repository objects
"""
# Import here to avoid circular imports
try:
# When running tests or using as a module
from models.repository import Repository
except ImportError:
# When using as a package
from .repository import Repository
# Get starred repos using Gitea API
gitea_repos = self._user.get_starred()
# Convert to Repository objects
return [Repository(repo, self._gitea) for repo in gitea_repos]
def get_subscriptions(self) -> List[Any]:
"""
Get repositories watched by the user.
Returns:
List of Repository objects
"""
# Import here to avoid circular imports
try:
# When running tests or using as a module
from models.repository import Repository
except ImportError:
# When using as a package
from .repository import Repository
# Get watched repos using Gitea API
gitea_repos = self._user.get_subscriptions()
# Convert to Repository objects
return [Repository(repo, self._gitea) for repo in gitea_repos]
def get_orgs(self) -> List[Dict[str, Any]]:
"""
Get organizations the user belongs to.
Returns:
List of organization objects
"""
gitea_orgs = self._user.get_orgs()
orgs = []
for org in gitea_orgs:
orgs.append({
'login': org.username,
'id': org.id,
'avatar_url': org.avatar_url,
'description': org.description,
'type': 'Organization'
})
return orgs
def get_followers(self) -> List['User']:
"""
Get users following this user.
Returns:
List of User objects
"""
gitea_followers = self._user.get_followers()
# Convert to User objects
return [User(follower, self._gitea) for follower in gitea_followers]
def get_following(self) -> List['User']:
"""
Get users this user is following.
Returns:
List of User objects
"""
gitea_following = self._user.get_following()
# Convert to User objects
return [User(user, self._gitea) for user in gitea_following]
def has_in_following(self, following: 'User') -> bool:
"""
Check if this user follows another user.
Args:
following: User to check
Returns:
True if following, False otherwise
"""
return self._user.is_following(following.login)
def add_to_following(self, following: 'User') -> None:
"""
Follow another user.
Args:
following: User to follow
"""
self._user.follow(following.login)
def remove_from_following(self, following: 'User') -> None:
"""
Unfollow another user.
Args:
following: User to unfollow
"""
self._user.unfollow(following.login)
def get_keys(self) -> List[Dict[str, Any]]:
"""
Get SSH keys for the user.
Returns:
List of key objects
"""
gitea_keys = self._user.get_keys()
keys = []
for key in gitea_keys:
keys.append({
'id': key.id,
'key': key.key,
'title': key.title,
'created_at': key.created_at,
'read_only': getattr(key, 'read_only', True)
})
return keys
def create_repo(self, name: str, **kwargs) -> Any:
"""
Create a new repository for the user.
Args:
name: Repository name
**kwargs: Additional parameters
Returns:
Repository object
"""
# Import here to avoid circular imports
try:
# When running tests or using as a module
from models.repository import Repository
except ImportError:
# When using as a package
from .repository import Repository
# Map GitHub parameters to Gitea parameters
gitea_params = {
'name': name,
'description': kwargs.get('description', ''),
'private': kwargs.get('private', False),
'auto_init': kwargs.get('auto_init', False),
'gitignores': kwargs.get('gitignore_template', ''),
'license': kwargs.get('license_template', ''),
'readme': kwargs.get('readme', '')
}
gitea_repo = self._user.create_repo(**gitea_params)
return Repository(gitea_repo, self._gitea)

View File

@ -0,0 +1,36 @@
"""Setup configuration for gitea-github-shim package."""
from setuptools import setup, find_packages
with open("../../README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setup(
name="gitea-github-shim",
version="0.1.0",
author="Your Name",
author_email="your.email@example.com",
description="A compatibility layer that provides GitHub SDK interfaces for Gitea",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/yourusername/gitea-github-shim",
packages=find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries :: Python Modules",
],
python_requires=">=3.6",
install_requires=[
"py-gitea>=1.16.0",
],
extras_require={
"dev": [
"pytest>=6.0",
"mock>=4.0",
],
},
)

View File

@ -0,0 +1,51 @@
# Python Gitea-GitHub Shim Test Results
## Test Summary
All unit tests for the Python Gitea-GitHub shim are passing successfully!
### Test Execution
```bash
$ python3 -m unittest tests.test_gitea_github_shim -v
```
### Results: ✅ 17/17 tests passed
## Test Coverage
### GiteaGitHubShim Class (8 tests)
-`test_init_with_token_and_url` - Initialization with both token and URL
-`test_init_with_token_only` - GitHub compatibility mode (token only)
-`test_get_repo` - Getting repository by full name
-`test_get_user_authenticated` - Getting authenticated user
-`test_get_user_by_login` - Getting user by username
-`test_create_repo` - Creating a new repository
-`test_get_api_status` - Getting API status
-`test_get_rate_limit` - Getting rate limit info (mocked)
### Repository Model (3 tests)
-`test_repository_initialization` - Repository model initialization
-`test_get_pull` - Getting a pull request
-`test_create_pull` - Creating a pull request
### PullRequest Model (3 tests)
-`test_pull_request_initialization` - PR model initialization
-`test_update_pull_request` - Updating a pull request
-`test_merge_pull_request` - Merging a pull request
### User Model (3 tests)
-`test_user_initialization` - User model initialization
-`test_get_repos` - Getting user repositories
-`test_follow_user` - Following another user
## Key Features Tested
1. **Drop-in replacement** - The shim can be initialized just like PyGitHub
2. **Environment variable support** - GITEA_URL can be used for configuration
3. **Model compatibility** - Repository, PullRequest, and User models work as expected
4. **API mapping** - Gitea API calls are properly mapped to GitHub-style responses
5. **Error handling** - Proper handling of missing attributes with getattr()
## Next Steps
- Implement JavaScript/TypeScript Octokit shim
- Test with actual Gitea instance
- Add integration tests
- Implement remaining API endpoints (issues, activity, etc.)

View File

@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""Test script for the Gitea GitHub Shim."""
import os
import sys
from pathlib import Path
# Add current directory to path
current_dir = Path(__file__).parent
sys.path.insert(0, str(current_dir))
try:
from gitea_github_shim import GiteaGitHubShim
from config import get_github_client, is_gitea_mode, get_api_info
from utils import setup_api_environment, log_api_configuration
except ImportError as e:
print(f"Import error: {e}")
print("Make sure you're running from the correct directory")
sys.exit(1)
def test_shim_basic():
"""Test basic shim functionality."""
print("=== Testing Gitea GitHub Shim ===")
# Set up environment
setup_api_environment()
log_api_configuration()
# Get API info
api_info = get_api_info()
print(f"API Info: {api_info}")
# Test client creation
try:
client = get_github_client()
print(f"✓ Successfully created client: {type(client).__name__}")
# Test API status
try:
status = client.get_api_status()
print(f"✓ API Status: {status}")
except Exception as e:
print(f"⚠ API Status failed: {e}")
# Test rate limit
try:
rate_limit = client.get_rate_limit()
print(f"✓ Rate Limit: {rate_limit}")
except Exception as e:
print(f"⚠ Rate Limit failed: {e}")
except ValueError as e:
print(f"✗ Configuration error: {e}")
print(" Please set GITHUB_TOKEN or GITEA_TOKEN environment variable")
return False
except Exception as e:
print(f"✗ Failed to create client: {e}")
return False
return True
def test_repository_access():
"""Test repository access functionality."""
print("\n=== Testing Repository Access ===")
try:
client = get_github_client()
# Test with a known repository (GitHub's hello-world)
test_repo = "octocat/Hello-World"
try:
repo = client.get_repo(test_repo)
print(f"✓ Successfully accessed repository: {repo.full_name}")
print(f" - Default branch: {repo.default_branch}")
print(f" - Description: {repo.description}")
print(f" - Private: {repo.private}")
except Exception as e:
print(f"⚠ Repository access failed: {e}")
print(" This might be expected if using Gitea without the test repo")
except ValueError as e:
print(f"✗ Configuration error: {e}")
return False
except Exception as e:
print(f"✗ Repository test failed: {e}")
return False
return True
def test_user_access():
"""Test user access functionality."""
print("\n=== Testing User Access ===")
try:
client = get_github_client()
# Test getting authenticated user
try:
user = client.get_user()
print(f"✓ Successfully got authenticated user: {user.login}")
except Exception as e:
print(f"⚠ User access failed: {e}")
except ValueError as e:
print(f"✗ Configuration error: {e}")
return False
except Exception as e:
print(f"✗ User test failed: {e}")
return False
return True
def test_configuration():
"""Test configuration functionality."""
print("\n=== Testing Configuration ===")
try:
# Test API info
api_info = get_api_info()
print(f"✓ API Info: {api_info}")
# Test mode detection
mode = "Gitea" if is_gitea_mode() else "GitHub"
print(f"✓ API Mode: {mode}")
# Test client class detection
from config import config
client_class = config.get_client_class()
print(f"✓ Client Class: {client_class.__name__}")
return True
except Exception as e:
print(f"✗ Configuration test failed: {e}")
return False
def main():
"""Main test function."""
print("Gitea GitHub Shim Test Suite")
print("=" * 40)
# Check environment
print(f"USE_GITEA: {os.getenv('USE_GITEA', 'false')}")
print(f"GITEA_URL: {os.getenv('GITEA_URL', 'not set')}")
print(f"GITHUB_TOKEN: {'set' if os.getenv('GITHUB_TOKEN') else 'not set'}")
print(f"GITEA_TOKEN: {'set' if os.getenv('GITEA_TOKEN') else 'not set'}")
print()
# Run tests
tests = [
test_configuration,
test_shim_basic,
test_repository_access,
test_user_access,
]
passed = 0
total = len(tests)
for test in tests:
try:
if test():
passed += 1
except Exception as e:
print(f"✗ Test {test.__name__} failed with exception: {e}")
print(f"\n=== Test Results ===")
print(f"Passed: {passed}/{total}")
if passed == total:
print("🎉 All tests passed!")
return 0
else:
print("⚠ Some tests failed or had warnings")
if not os.getenv('GITHUB_TOKEN') and not os.getenv('GITEA_TOKEN'):
print("\n💡 To test with real API access, set GITHUB_TOKEN or GITEA_TOKEN")
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1 @@
"""Tests for Gitea GitHub shim."""

View File

@ -0,0 +1,642 @@
"""Unit tests for the Gitea GitHub shim."""
import unittest
from unittest.mock import Mock, MagicMock, patch
import os
from datetime import datetime
# Import the shim classes
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from gitea_github_shim import GiteaGitHubShim
from models.repository import Repository
from models.pull_request import PullRequest
from models.user import User
class TestGiteaGitHubShim(unittest.TestCase):
"""Test cases for GiteaGitHubShim class."""
def setUp(self):
"""Set up test fixtures."""
self.token = "test_token"
self.base_url = "https://gitea.example.com"
@patch('gitea_github_shim.Gitea')
def test_init_with_token_and_url(self, mock_gitea_class):
"""Test initialization with both token and URL."""
mock_gitea = Mock()
mock_gitea_class.return_value = mock_gitea
shim = GiteaGitHubShim(self.token, base_url=self.base_url)
self.assertEqual(shim.token, self.token)
self.assertEqual(shim.base_url, self.base_url)
mock_gitea_class.assert_called_once_with(self.base_url, self.token)
@patch('gitea_github_shim.Gitea')
def test_init_with_token_only(self, mock_gitea_class):
"""Test initialization with only token (GitHub compatibility mode)."""
mock_gitea = Mock()
mock_gitea_class.return_value = mock_gitea
with patch.dict(os.environ, {'GITEA_URL': 'https://env.gitea.com'}):
shim = GiteaGitHubShim(self.token)
self.assertEqual(shim.token, self.token)
self.assertEqual(shim.base_url, 'https://env.gitea.com')
mock_gitea_class.assert_called_once_with('https://env.gitea.com', self.token)
@patch('gitea_github_shim.Gitea')
def test_get_repo(self, mock_gitea_class):
"""Test getting a repository."""
# Set up mocks
mock_gitea = Mock()
mock_gitea_class.return_value = mock_gitea
mock_repo = Mock()
mock_repo.name = "test-repo"
mock_repo.full_name = "owner/test-repo"
mock_repo.description = "Test repository"
mock_repo.private = False
mock_repo.fork = False
mock_repo.created_at = datetime.now()
mock_repo.updated_at = datetime.now()
mock_repo.size = 1024
mock_repo.stars_count = 10
mock_repo.watchers_count = 5
mock_repo.forks_count = 2
mock_repo.open_issues_count = 3
mock_repo.default_branch = "main"
mock_repo.archived = False
mock_repo.html_url = "https://gitea.example.com/owner/test-repo"
mock_repo.clone_url = "https://gitea.example.com/owner/test-repo.git"
mock_repo.ssh_url = "git@gitea.example.com:owner/test-repo.git"
mock_owner = Mock()
mock_owner.login = "owner"
mock_owner.id = 1
mock_owner.avatar_url = "https://gitea.example.com/avatars/1"
mock_repo.owner = mock_owner
mock_gitea.get_repo.return_value = mock_repo
# Test
shim = GiteaGitHubShim(self.token, base_url=self.base_url)
repo = shim.get_repo("owner/test-repo")
# Assertions
self.assertIsInstance(repo, Repository)
self.assertEqual(repo.name, "test-repo")
self.assertEqual(repo.full_name, "owner/test-repo")
self.assertEqual(repo.default_branch, "main")
mock_gitea.get_repo.assert_called_once_with("owner", "test-repo")
@patch('gitea_github_shim.Gitea')
def test_get_user_authenticated(self, mock_gitea_class):
"""Test getting authenticated user."""
# Set up mocks
mock_gitea = Mock()
mock_gitea_class.return_value = mock_gitea
mock_user = Mock()
mock_user.login = "testuser"
mock_user.id = 1
mock_user.avatar_url = "https://gitea.example.com/avatars/1"
mock_user.html_url = "https://gitea.example.com/testuser"
mock_user.full_name = "Test User"
mock_user.location = "Earth"
mock_user.email = "test@example.com"
mock_user.created = datetime.now()
mock_gitea.get_user.return_value = mock_user
# Test
shim = GiteaGitHubShim(self.token, base_url=self.base_url)
user = shim.get_user()
# Assertions
self.assertIsInstance(user, User)
self.assertEqual(user.login, "testuser")
self.assertEqual(user.email, "test@example.com")
mock_gitea.get_user.assert_called_once_with()
@patch('gitea_github_shim.Gitea')
def test_get_user_by_login(self, mock_gitea_class):
"""Test getting user by login name."""
# Set up mocks
mock_gitea = Mock()
mock_gitea_class.return_value = mock_gitea
mock_user = Mock()
mock_user.login = "otheruser"
mock_user.id = 2
mock_user.avatar_url = "https://gitea.example.com/avatars/2"
mock_user.html_url = "https://gitea.example.com/otheruser"
mock_user.full_name = "Other User"
mock_user.location = "Mars"
mock_user.email = "other@example.com"
mock_user.created = datetime.now()
mock_gitea.get_user.return_value = mock_user
# Test
shim = GiteaGitHubShim(self.token, base_url=self.base_url)
user = shim.get_user("otheruser")
# Assertions
self.assertIsInstance(user, User)
self.assertEqual(user.login, "otheruser")
mock_gitea.get_user.assert_called_once_with("otheruser")
@patch('gitea_github_shim.Gitea')
def test_create_repo(self, mock_gitea_class):
"""Test creating a repository."""
# Set up mocks
mock_gitea = Mock()
mock_gitea_class.return_value = mock_gitea
mock_repo = Mock()
mock_repo.name = "new-repo"
mock_repo.full_name = "testuser/new-repo"
mock_repo.description = "New test repository"
mock_repo.private = True
mock_repo.fork = False
mock_repo.created_at = datetime.now()
mock_repo.updated_at = datetime.now()
mock_repo.size = 0
mock_repo.stars_count = 0
mock_repo.watchers_count = 0
mock_repo.forks_count = 0
mock_repo.open_issues_count = 0
mock_repo.default_branch = "main"
mock_repo.archived = False
mock_repo.html_url = "https://gitea.example.com/testuser/new-repo"
mock_repo.clone_url = "https://gitea.example.com/testuser/new-repo.git"
mock_repo.ssh_url = "git@gitea.example.com:testuser/new-repo.git"
mock_owner = Mock()
mock_owner.login = "testuser"
mock_owner.id = 1
mock_owner.avatar_url = "https://gitea.example.com/avatars/1"
mock_repo.owner = mock_owner
mock_gitea.create_repo.return_value = mock_repo
# Test
shim = GiteaGitHubShim(self.token, base_url=self.base_url)
repo = shim.create_repo(
"new-repo",
description="New test repository",
private=True,
auto_init=True,
gitignore_template="Python",
license_template="MIT"
)
# Assertions
self.assertIsInstance(repo, Repository)
self.assertEqual(repo.name, "new-repo")
self.assertEqual(repo.description, "New test repository")
self.assertTrue(repo.private)
mock_gitea.create_repo.assert_called_once_with(
name="new-repo",
description="New test repository",
private=True,
auto_init=True,
gitignores="Python",
license="MIT",
readme=""
)
@patch('gitea_github_shim.Gitea')
def test_get_api_status(self, mock_gitea_class):
"""Test getting API status."""
# Set up mocks
mock_gitea = Mock()
mock_gitea_class.return_value = mock_gitea
mock_gitea.get_version.return_value = "1.17.0"
# Test
shim = GiteaGitHubShim(self.token, base_url=self.base_url)
status = shim.get_api_status()
# Assertions
self.assertEqual(status['status'], 'good')
self.assertEqual(status['version'], '1.17.0')
self.assertEqual(status['api'], 'gitea')
@patch('gitea_github_shim.Gitea')
def test_get_rate_limit(self, mock_gitea_class):
"""Test getting rate limit (mock for Gitea)."""
# Set up mocks
mock_gitea = Mock()
mock_gitea_class.return_value = mock_gitea
# Test
shim = GiteaGitHubShim(self.token, base_url=self.base_url)
rate_limit = shim.get_rate_limit()
# Assertions
self.assertEqual(rate_limit['rate']['limit'], 999999)
self.assertEqual(rate_limit['rate']['remaining'], 999999)
self.assertEqual(rate_limit['rate']['reset'], 0)
class TestRepository(unittest.TestCase):
"""Test cases for Repository model."""
def setUp(self):
"""Set up test fixtures."""
self.mock_gitea = Mock()
self.mock_repo = Mock()
# Set up repository attributes
self.mock_repo.name = "test-repo"
self.mock_repo.full_name = "owner/test-repo"
self.mock_repo.description = "Test repository"
self.mock_repo.private = False
self.mock_repo.fork = False
self.mock_repo.created_at = datetime.now()
self.mock_repo.updated_at = datetime.now()
self.mock_repo.size = 1024
self.mock_repo.stars_count = 10
self.mock_repo.watchers_count = 5
self.mock_repo.forks_count = 2
self.mock_repo.open_issues_count = 3
self.mock_repo.default_branch = "main"
self.mock_repo.archived = False
self.mock_repo.html_url = "https://gitea.example.com/owner/test-repo"
self.mock_repo.clone_url = "https://gitea.example.com/owner/test-repo.git"
self.mock_repo.ssh_url = "git@gitea.example.com:owner/test-repo.git"
mock_owner = Mock()
mock_owner.login = "owner"
mock_owner.id = 1
mock_owner.avatar_url = "https://gitea.example.com/avatars/1"
self.mock_repo.owner = mock_owner
def test_repository_initialization(self):
"""Test Repository model initialization."""
repo = Repository(self.mock_repo, self.mock_gitea)
self.assertEqual(repo.name, "test-repo")
self.assertEqual(repo.full_name, "owner/test-repo")
self.assertEqual(repo.default_branch, "main")
self.assertEqual(repo.stargazers_count, 10)
self.assertEqual(repo.owner['login'], "owner")
def test_get_pull(self):
"""Test getting a pull request."""
# Set up mock PR
mock_pr = Mock()
mock_pr.number = 1
mock_pr.state = "open"
mock_pr.title = "Test PR"
mock_pr.body = "Test PR body"
mock_pr.created_at = datetime.now()
mock_pr.updated_at = datetime.now()
mock_pr.closed_at = None
mock_pr.merged_at = None
mock_pr.merge_commit_sha = None
mock_pr.html_url = "https://gitea.example.com/owner/test-repo/pulls/1"
mock_pr.diff_url = "https://gitea.example.com/owner/test-repo/pulls/1.diff"
mock_pr.patch_url = "https://gitea.example.com/owner/test-repo/pulls/1.patch"
mock_pr.mergeable = True
mock_pr.merged = False
mock_user = Mock()
mock_user.login = "contributor"
mock_user.id = 2
mock_user.avatar_url = "https://gitea.example.com/avatars/2"
mock_pr.user = mock_user
mock_pr.assignee = None
mock_pr.assignees = [] # Empty list of assignees
mock_pr.labels = [] # Empty list of labels
mock_base = Mock()
mock_base.ref = "main"
mock_base.sha = "abc123"
mock_base.repo = self.mock_repo
mock_pr.base = mock_base
mock_head = Mock()
mock_head.ref = "feature-branch"
mock_head.sha = "def456"
mock_head.repo = self.mock_repo
mock_pr.head = mock_head
mock_pr.milestone = None
self.mock_repo.get_pull.return_value = mock_pr
# Test
repo = Repository(self.mock_repo, self.mock_gitea)
pr = repo.get_pull(1)
# Assertions
self.assertIsInstance(pr, PullRequest)
self.assertEqual(pr.number, 1)
self.assertEqual(pr.title, "Test PR")
self.assertEqual(pr.state, "open")
self.mock_repo.get_pull.assert_called_once_with(1)
def test_create_pull(self):
"""Test creating a pull request."""
# Set up mock PR
mock_pr = Mock()
mock_pr.number = 2
mock_pr.state = "open"
mock_pr.title = "New feature"
mock_pr.body = "This adds a new feature"
mock_pr.created_at = datetime.now()
mock_pr.updated_at = datetime.now()
mock_pr.closed_at = None
mock_pr.merged_at = None
mock_pr.merge_commit_sha = None
mock_pr.html_url = "https://gitea.example.com/owner/test-repo/pulls/2"
mock_pr.diff_url = "https://gitea.example.com/owner/test-repo/pulls/2.diff"
mock_pr.patch_url = "https://gitea.example.com/owner/test-repo/pulls/2.patch"
mock_pr.mergeable = True
mock_pr.merged = False
mock_user = Mock()
mock_user.login = "contributor"
mock_user.id = 2
mock_user.avatar_url = "https://gitea.example.com/avatars/2"
mock_pr.user = mock_user
mock_pr.assignee = None
mock_pr.assignees = [] # Empty list of assignees
mock_pr.labels = [] # Empty list of labels
mock_base = Mock()
mock_base.ref = "main"
mock_base.sha = "abc123"
mock_base.repo = self.mock_repo
mock_pr.base = mock_base
mock_head = Mock()
mock_head.ref = "feature/new-feature"
mock_head.sha = "ghi789"
mock_head.repo = self.mock_repo
mock_pr.head = mock_head
mock_pr.milestone = None
self.mock_repo.create_pull_request.return_value = mock_pr
# Test
repo = Repository(self.mock_repo, self.mock_gitea)
pr = repo.create_pull(
title="New feature",
body="This adds a new feature",
head="feature/new-feature",
base="main"
)
# Assertions
self.assertIsInstance(pr, PullRequest)
self.assertEqual(pr.title, "New feature")
self.assertEqual(pr.base['ref'], "main")
self.assertEqual(pr.head['ref'], "feature/new-feature")
self.mock_repo.create_pull_request.assert_called_once_with(
title="New feature",
body="This adds a new feature",
head="feature/new-feature",
base="main"
)
class TestPullRequest(unittest.TestCase):
"""Test cases for PullRequest model."""
def setUp(self):
"""Set up test fixtures."""
self.mock_gitea = Mock()
self.mock_repo = Mock()
self.mock_pr = Mock()
# Set up PR attributes
self.mock_pr.number = 1
self.mock_pr.state = "open"
self.mock_pr.title = "Test PR"
self.mock_pr.body = "Test PR body"
self.mock_pr.created_at = datetime.now()
self.mock_pr.updated_at = datetime.now()
self.mock_pr.closed_at = None
self.mock_pr.merged_at = None
self.mock_pr.merge_commit_sha = None
self.mock_pr.html_url = "https://gitea.example.com/owner/test-repo/pulls/1"
self.mock_pr.diff_url = "https://gitea.example.com/owner/test-repo/pulls/1.diff"
self.mock_pr.patch_url = "https://gitea.example.com/owner/test-repo/pulls/1.patch"
self.mock_pr.mergeable = True
self.mock_pr.merged = False
mock_user = Mock()
mock_user.login = "contributor"
mock_user.id = 2
mock_user.avatar_url = "https://gitea.example.com/avatars/2"
self.mock_pr.user = mock_user
self.mock_pr.assignee = None
# Add missing attributes
self.mock_pr.assignees = [] # Empty list of assignees
self.mock_pr.labels = [] # Empty list of labels
mock_base = Mock()
mock_base.ref = "main"
mock_base.sha = "abc123"
mock_base.repo = Mock()
mock_base.repo.name = "test-repo"
mock_base.repo.full_name = "owner/test-repo"
mock_base.repo.owner = Mock()
mock_base.repo.owner.login = "owner"
mock_base.repo.owner.id = 1
mock_base.repo.owner.avatar_url = "https://gitea.example.com/avatars/1"
self.mock_pr.base = mock_base
mock_head = Mock()
mock_head.ref = "feature-branch"
mock_head.sha = "def456"
mock_head.repo = Mock()
mock_head.repo.name = "test-repo"
mock_head.repo.full_name = "contributor/test-repo"
mock_head.repo.owner = mock_user
self.mock_pr.head = mock_head
self.mock_pr.milestone = None
def test_pull_request_initialization(self):
"""Test PullRequest model initialization."""
pr = PullRequest(self.mock_pr, self.mock_repo, self.mock_gitea)
self.assertEqual(pr.number, 1)
self.assertEqual(pr.title, "Test PR")
self.assertEqual(pr.state, "open")
self.assertFalse(pr.merged)
self.assertEqual(pr.user['login'], "contributor")
self.assertEqual(pr.base['ref'], "main")
self.assertEqual(pr.head['ref'], "feature-branch")
def test_update_pull_request(self):
"""Test updating a pull request."""
# Set up mock for update
updated_pr = Mock()
updated_pr.number = 1
updated_pr.state = "open"
updated_pr.title = "Updated PR Title"
updated_pr.body = "Updated PR body"
updated_pr.created_at = self.mock_pr.created_at
updated_pr.updated_at = datetime.now()
updated_pr.closed_at = None
updated_pr.merged_at = None
updated_pr.merge_commit_sha = None
updated_pr.html_url = self.mock_pr.html_url
updated_pr.diff_url = self.mock_pr.diff_url
updated_pr.patch_url = self.mock_pr.patch_url
updated_pr.mergeable = True
updated_pr.merged = False
updated_pr.user = self.mock_pr.user
updated_pr.assignee = None
updated_pr.base = self.mock_pr.base
updated_pr.head = self.mock_pr.head
updated_pr.milestone = None
# Add missing attributes
updated_pr.assignees = [] # Empty list of assignees
updated_pr.labels = [] # Empty list of labels
self.mock_pr.update.return_value = updated_pr
# Test
pr = PullRequest(self.mock_pr, self.mock_repo, self.mock_gitea)
updated = pr.update(title="Updated PR Title", body="Updated PR body")
# Assertions
self.assertEqual(updated.title, "Updated PR Title")
self.assertEqual(updated.body, "Updated PR body")
self.mock_pr.update.assert_called_once_with(
title="Updated PR Title",
body="Updated PR body"
)
def test_merge_pull_request(self):
"""Test merging a pull request."""
# Set up mock for merge
merge_result = Mock()
merge_result.sha = "merged123"
self.mock_pr.merge.return_value = merge_result
# Test
pr = PullRequest(self.mock_pr, self.mock_repo, self.mock_gitea)
result = pr.merge(
commit_title="Merge PR #1",
commit_message="This merges the feature",
merge_method="squash"
)
# Assertions
self.assertEqual(result['sha'], "merged123")
self.assertTrue(result['merged'])
self.assertEqual(result['message'], "Pull Request successfully merged")
self.mock_pr.merge.assert_called_once_with(
style="squash",
title="Merge PR #1",
message="This merges the feature"
)
class TestUser(unittest.TestCase):
"""Test cases for User model."""
def setUp(self):
"""Set up test fixtures."""
self.mock_gitea = Mock()
self.mock_user = Mock()
# Set up user attributes
self.mock_user.login = "testuser"
self.mock_user.id = 1
self.mock_user.avatar_url = "https://gitea.example.com/avatars/1"
self.mock_user.html_url = "https://gitea.example.com/testuser"
self.mock_user.full_name = "Test User"
self.mock_user.location = "Earth"
self.mock_user.email = "test@example.com"
self.mock_user.created = datetime.now()
def test_user_initialization(self):
"""Test User model initialization."""
user = User(self.mock_user, self.mock_gitea)
self.assertEqual(user.login, "testuser")
self.assertEqual(user.name, "Test User")
self.assertEqual(user.email, "test@example.com")
self.assertEqual(user.location, "Earth")
self.assertEqual(user.type, "User")
def test_get_repos(self):
"""Test getting user repositories."""
# Set up mock repos
mock_repo1 = Mock()
mock_repo1.name = "repo1"
mock_repo1.full_name = "testuser/repo1"
mock_repo1.created_at = datetime.now()
mock_repo1.updated_at = datetime.now()
mock_repo2 = Mock()
mock_repo2.name = "repo2"
mock_repo2.full_name = "testuser/repo2"
mock_repo2.created_at = datetime.now()
mock_repo2.updated_at = datetime.now()
# Set other required attributes for Repository initialization
for repo in [mock_repo1, mock_repo2]:
repo.description = "Test repo"
repo.private = False
repo.fork = False
repo.size = 100
repo.stars_count = 1
repo.watchers_count = 1
repo.forks_count = 0
repo.open_issues_count = 0
repo.default_branch = "main"
repo.archived = False
repo.html_url = f"https://gitea.example.com/{repo.full_name}"
repo.clone_url = f"https://gitea.example.com/{repo.full_name}.git"
repo.ssh_url = f"git@gitea.example.com:{repo.full_name}.git"
repo.owner = self.mock_user
self.mock_user.get_repos.return_value = [mock_repo1, mock_repo2]
# Test
user = User(self.mock_user, self.mock_gitea)
repos = user.get_repos()
# Assertions
self.assertEqual(len(repos), 2)
self.assertEqual(repos[0].name, "repo1")
self.assertEqual(repos[1].name, "repo2")
self.mock_user.get_repos.assert_called_once()
def test_follow_user(self):
"""Test following another user."""
# Create another user to follow
other_user = Mock()
other_user.login = "otheruser"
# Test
user = User(self.mock_user, self.mock_gitea)
other = User(other_user, self.mock_gitea)
user.add_to_following(other)
# Assertions
self.mock_user.follow.assert_called_once_with("otheruser")
if __name__ == '__main__':
unittest.main()

121
gitea-shim/python/utils.py Normal file
View File

@ -0,0 +1,121 @@
"""Utility functions for GitHub/Gitea migration."""
import os
import logging
from typing import Optional, Dict, Any
logger = logging.getLogger(__name__)
def setup_api_environment():
"""
Set up environment variables for API configuration.
This function helps configure the environment for either GitHub or Gitea mode.
"""
# Check if we should use Gitea
use_gitea = os.getenv('USE_GITEA', 'false').lower() == 'true'
if use_gitea:
logger.info("Configuring for Gitea mode")
# Ensure GITEA_URL is set
if not os.getenv('GITEA_URL'):
os.environ['GITEA_URL'] = 'http://localhost:3000'
logger.info(f"Set GITEA_URL to {os.environ['GITEA_URL']}")
# Use GITEA_TOKEN if available, otherwise fall back to GITHUB_TOKEN
if not os.getenv('GITEA_TOKEN') and os.getenv('GITHUB_TOKEN'):
os.environ['GITEA_TOKEN'] = os.environ['GITHUB_TOKEN']
logger.info("Using GITHUB_TOKEN as GITEA_TOKEN")
else:
logger.info("Configuring for GitHub mode")
# Ensure GITHUB_TOKEN is set
if not os.getenv('GITHUB_TOKEN'):
logger.warning("GITHUB_TOKEN not set")
def validate_api_configuration() -> Dict[str, Any]:
"""
Validate the current API configuration.
Returns:
Dictionary with validation results
"""
use_gitea = os.getenv('USE_GITEA', 'false').lower() == 'true'
if use_gitea:
gitea_url = os.getenv('GITEA_URL')
gitea_token = os.getenv('GITEA_TOKEN') or os.getenv('GITHUB_TOKEN')
return {
'mode': 'gitea',
'url_set': bool(gitea_url),
'token_set': bool(gitea_token),
'url': gitea_url,
'valid': bool(gitea_url and gitea_token)
}
else:
github_token = os.getenv('GITHUB_TOKEN')
return {
'mode': 'github',
'url_set': True, # GitHub URL is fixed
'token_set': bool(github_token),
'url': 'https://api.github.com',
'valid': bool(github_token)
}
def migrate_github_imports():
"""
Helper function to show how to migrate GitHub imports.
This function doesn't actually do anything, but serves as documentation
for the migration process.
"""
migration_examples = {
'old_import': 'from github import Github',
'new_import': 'from gitea_shim import get_github_client',
'old_usage': 'gh = Github(token)',
'new_usage': 'gh = get_github_client()',
'old_repo': 'repo = gh.get_repo("owner/repo")',
'new_repo': 'repo = gh.get_repo("owner/repo") # Same interface!'
}
logger.info("Migration examples:")
for key, value in migration_examples.items():
logger.info(f"{key}: {value}")
def get_api_client_info() -> Dict[str, Any]:
"""
Get information about the current API client configuration.
Returns:
Dictionary with API client information
"""
try:
from .config import get_api_info
return get_api_info()
except ImportError:
return validate_api_configuration()
def log_api_configuration():
"""Log the current API configuration for debugging."""
config = get_api_client_info()
logger.info(f"API Configuration: {config}")
if not config.get('valid', False):
logger.warning("API configuration is invalid!")
if config['mode'] == 'gitea':
if not config['url_set']:
logger.error("GITEA_URL not set")
if not config['token_set']:
logger.error("GITEA_TOKEN or GITHUB_TOKEN not set")
else:
if not config['token_set']:
logger.error("GITHUB_TOKEN not set")

1
worker/.gitignore vendored
View File

@ -15,3 +15,4 @@ taskStateInfoKeypair.json
localKOIIDB.db localKOIIDB.db
metadata.json metadata.json
.npmrc .npmrc
**/*.log

View File

@ -124,7 +124,7 @@ environment: "TEST"
#################################### FOR UPDATING TASKS ONLY #################################### #################################### FOR UPDATING TASKS ONLY ####################################
## Old Task ID ## ## Old Task ID ##
task_id: "5bc74eTjGgNigupFBZXtfzAYVksPqSGBEVgRLubk7ak7" task_id: "A1UwX31uCMhZN4x9ZeH5xv3dzZcKLXsXytk6r7PzDLn3"
## Migration Description ## ## Migration Description ##
migrationDescription: "Log Reminder, Time Based Logic" migrationDescription: "Time Based Logic, Poll Task"

View File

@ -69,6 +69,7 @@ def start_task():
swarmBountyId=swarmBountyId, swarmBountyId=swarmBountyId,
repo_url=repo_url, repo_url=repo_url,
db=db, # Pass db instance db=db, # Pass db instance
podcall_signature=podcall_signature,
) )
return jsonify(result) return jsonify(result)
else: else:

View File

@ -1,8 +1,20 @@
import re import re
import requests import requests
from github import Github
import os import os
import logging import logging
import sys
from pathlib import Path
# Add the gitea-shim to the path
gitea_shim_path = Path(__file__).parent.parent.parent.parent.parent / "gitea-shim" / "python"
sys.path.insert(0, str(gitea_shim_path))
try:
from gitea_shim import get_github_client, is_gitea_mode
except ImportError:
# Fallback to regular GitHub if shim is not available
from github import Github as get_github_client
is_gitea_mode = lambda: False
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -14,7 +26,11 @@ def verify_pr_ownership(
expected_repo, expected_repo,
): ):
try: try:
gh = Github(os.environ.get("GITHUB_TOKEN")) # Use the shim to get GitHub-compatible client
gh = get_github_client()
# Log which API we're using
logger.info(f"Using {'Gitea' if is_gitea_mode() else 'GitHub'} API for PR verification")
match = re.match(r"https://github.com/([^/]+)/([^/]+)/pull/(\d+)", pr_url) match = re.match(r"https://github.com/([^/]+)/([^/]+)/pull/(\d+)", pr_url)
if not match: if not match:

View File

@ -11,7 +11,7 @@ from src.database.models import Submission
load_dotenv() load_dotenv()
def handle_task_creation(task_id, swarmBountyId, repo_url, db=None): def handle_task_creation(task_id, swarmBountyId, repo_url, db=None, podcall_signature=None):
"""Handle task creation request.""" """Handle task creation request."""
try: try:
if db is None: if db is None:
@ -22,6 +22,8 @@ def handle_task_creation(task_id, swarmBountyId, repo_url, db=None):
client=client, client=client,
prompts=PROMPTS, prompts=PROMPTS,
repo_url=repo_url, repo_url=repo_url,
podcall_signature=podcall_signature,
task_id=task_id,
) )
result = workflow.run() result = workflow.run()

View File

@ -19,7 +19,7 @@ class RepoClassificationPhase(WorkflowPhase):
super().__init__( super().__init__(
workflow=workflow, workflow=workflow,
prompt_name="classify_repository", prompt_name="classify_repository",
available_tools=["read_file", "list_files", "classify_repository"], available_tools=["read_file", "search_code", "list_directory_contents", "classify_repository"],
conversation_id=conversation_id, conversation_id=conversation_id,
name="Repository Classification", name="Repository Classification",
) )
@ -32,7 +32,8 @@ class ReadmeSectionGenerationPhase(WorkflowPhase):
prompt_name="generate_readme_section", prompt_name="generate_readme_section",
available_tools=[ available_tools=[
"read_file", "read_file",
"list_files", "search_code",
"list_directory_contents",
"create_readme_section", "create_readme_section",
], ],
conversation_id=conversation_id, conversation_id=conversation_id,
@ -56,7 +57,7 @@ class ReadmeReviewPhase(WorkflowPhase):
super().__init__( super().__init__(
workflow=workflow, workflow=workflow,
prompt_name="review_readme_file", prompt_name="review_readme_file",
available_tools=["read_file", "list_files", "review_readme_file"], available_tools=["read_file", "search_code", "list_directory_contents", "review_readme_file"],
conversation_id=conversation_id, conversation_id=conversation_id,
name="Readme Review", name="Readme Review",
) )
@ -67,7 +68,7 @@ class CreatePullRequestPhase(WorkflowPhase):
super().__init__( super().__init__(
workflow=workflow, workflow=workflow,
prompt_name="create_pr", prompt_name="create_pr",
available_tools=["read_file", "list_files", "create_pull_request_legacy"], available_tools=["read_file", "search_code", "list_directory_contents", "create_pull_request_legacy"],
conversation_id=conversation_id, conversation_id=conversation_id,
name="Create Pull Request", name="Create Pull Request",
) )

View File

@ -67,7 +67,7 @@ PROMPTS = {
"The content will be added automatically, your job is just to create a good title." "The content will be added automatically, your job is just to create a good title."
), ),
"create_pr": ( "create_pr": (
"You are creating a pull request for the file README_Prometheus.md you have generated. " "You are creating a pull request."
"The repository has been cloned to the current directory.\n" "The repository has been cloned to the current directory.\n"
"Use the `create_pull_request_legacy` tool to create the pull request.\n" "Use the `create_pull_request_legacy` tool to create the pull request.\n"
"IMPORTANT: Always use relative paths (e.g., 'src/file.py' not '/src/file.py')\n\n" "IMPORTANT: Always use relative paths (e.g., 'src/file.py' not '/src/file.py')\n\n"

View File

@ -1,7 +1,21 @@
"""Task decomposition workflow implementation.""" """Task decomposition workflow implementation."""
import os import os
from github import Github import sys
from pathlib import Path
# Add the gitea-shim to the path
gitea_shim_path = Path(__file__).parent.parent.parent.parent.parent / "gitea-shim" / "python"
sys.path.insert(0, str(gitea_shim_path))
try:
from gitea_shim import get_github_client, is_gitea_mode
except ImportError:
# Fallback to regular GitHub if shim is not available
from github import Github as get_github_client
is_gitea_mode = lambda: False
import requests
from prometheus_swarm.workflows.base import Workflow from prometheus_swarm.workflows.base import Workflow
from prometheus_swarm.utils.logging import log_section, log_key_value, log_error from prometheus_swarm.utils.logging import log_section, log_key_value, log_error
from src.workflows.repoSummarizer import phases from src.workflows.repoSummarizer import phases
@ -11,12 +25,16 @@ from prometheus_swarm.workflows.utils import (
validate_github_auth, validate_github_auth,
setup_repository, setup_repository,
) )
from kno_sdk import index_repo
from prometheus_swarm.tools.kno_sdk_wrapper.implementations import build_tools_wrapper
from src.workflows.repoSummarizer.prompts import PROMPTS from src.workflows.repoSummarizer.prompts import PROMPTS
from src.workflows.repoSummarizer.docs_sections import ( from src.workflows.repoSummarizer.docs_sections import (
DOCS_SECTIONS, DOCS_SECTIONS,
INITIAL_SECTIONS, INITIAL_SECTIONS,
FINAL_SECTIONS, FINAL_SECTIONS,
) )
from pathlib import Path
from prometheus_swarm.tools.git_operations.implementations import commit_and_push from prometheus_swarm.tools.git_operations.implementations import commit_and_push
@ -50,6 +68,8 @@ class RepoSummarizerWorkflow(Workflow):
client, client,
prompts, prompts,
repo_url, repo_url,
podcall_signature=None,
task_id=None,
): ):
# Extract owner and repo name from URL # Extract owner and repo name from URL
# URL format: https://github.com/owner/repo # URL format: https://github.com/owner/repo
@ -63,16 +83,38 @@ class RepoSummarizerWorkflow(Workflow):
repo_url=repo_url, repo_url=repo_url,
repo_owner=repo_owner, repo_owner=repo_owner,
repo_name=repo_name, repo_name=repo_name,
podcall_signature=podcall_signature,
task_id=task_id,
) )
def submit_draft_pr(self, pr_url):
"""Submit the draft PR."""
try:
response = requests.post(
f"http://host.docker.internal:30017/task/{self.task_id}/add-todo-draft-pr",
json={
"prUrl": pr_url,
"signature": self.podcall_signature,
"swarmBountyId": self.swarmBountyId,
"success": True,
"message": "",
},
)
except Exception as e:
log_error(e, "Failed to submit draft PR")
return {
"success": False,
"message": "Failed to submit draft PR",
"data": None,
}
def setup(self): def setup(self):
"""Set up repository and workspace.""" """Set up repository and workspace."""
check_required_env_vars(["GITHUB_TOKEN", "GITHUB_USERNAME"]) check_required_env_vars(["GITHUB_TOKEN", "GITHUB_USERNAME"])
validate_github_auth(os.getenv("GITHUB_TOKEN"), os.getenv("GITHUB_USERNAME")) validate_github_auth(os.getenv("GITHUB_TOKEN"), os.getenv("GITHUB_USERNAME"))
# Get the default branch from GitHub # Get the default branch from GiteaGitHubShim
try: try:
gh = Github(os.getenv("GITHUB_TOKEN")) gh = GiteaGitHubShim(os.getenv("GITHUB_TOKEN"), base_url=os.getenv("GITEA_URL"))
self.context["repo_full_name"] = ( self.context["repo_full_name"] = (
f"{self.context['repo_owner']}/{self.context['repo_name']}" f"{self.context['repo_owner']}/{self.context['repo_name']}"
) )
@ -82,6 +124,7 @@ class RepoSummarizerWorkflow(Workflow):
) )
self.context["base"] = repo.default_branch self.context["base"] = repo.default_branch
log_key_value("Default branch", self.context["base"]) log_key_value("Default branch", self.context["base"])
log_key_value("API Mode", "GiteaGitHubShim")
except Exception as e: except Exception as e:
log_error(e, "Failed to get default branch, using 'main'") log_error(e, "Failed to get default branch, using 'main'")
self.context["base"] = "main" self.context["base"] = "main"
@ -103,12 +146,22 @@ class RepoSummarizerWorkflow(Workflow):
# Enter repo directory # Enter repo directory
os.chdir(self.context["repo_path"]) os.chdir(self.context["repo_path"])
tools_build_result = self.build_tools_setup()
if not tools_build_result:
log_error(Exception("Failed to build tools setup"), "Failed to build tools setup")
return {
"success": False,
"message": "Failed to build tools setup",
"data": None,
}
# Configure Git user info # Configure Git user info
# setup_git_user_config(self.context["repo_path"]) # setup_git_user_config(self.context["repo_path"])
# Get current files for context # Get current files for context
def build_tools_setup(self):
index = index_repo(Path(self.context["repo_path"]))
tools = build_tools_wrapper(index)
return tools
def cleanup(self): def cleanup(self):
"""Cleanup workspace.""" """Cleanup workspace."""
# Make sure we're not in the repo directory before cleaning up # Make sure we're not in the repo directory before cleaning up
@ -139,7 +192,17 @@ class RepoSummarizerWorkflow(Workflow):
log_key_value("Branch created", self.context["head"]) log_key_value("Branch created", self.context["head"])
try: try:
commit_and_push(message="empty commit", allow_empty=True) commit_and_push(message="empty commit", allow_empty=True)
self.create_pull_request() draft_pr_result = self.create_pull_request()
if draft_pr_result.get("success"):
print("DRAFT PR RESULT", draft_pr_result)
self.submit_draft_pr(draft_pr_result.get("data").get("pr_url"))
else:
return {
"success": False,
"message": "Failed to create pull request",
"data": None,
}
except Exception as e: except Exception as e:
log_error(e, "Failed to commit and push") log_error(e, "Failed to commit and push")
return { return {
@ -260,6 +323,9 @@ class RepoSummarizerWorkflow(Workflow):
readme_result = generate_readme_section_phase.execute() readme_result = generate_readme_section_phase.execute()
# Check README Generation Result # Check README Generation Result
log_key_value("README RESULT", readme_result)
if not readme_result or not readme_result.get("success"): if not readme_result or not readme_result.get("success"):
log_error( log_error(
Exception(readme_result.get("error", "No result")), Exception(readme_result.get("error", "No result")),

View File

@ -7,8 +7,8 @@ PROMPTS = {
"and creating clear, structured documentation." "and creating clear, structured documentation."
), ),
"check_readme_file": ( "check_readme_file": (
"A pull request has been checked out for you. Review the file README_Prometheus.md in the repository " "Review the README_Prometheus.md in the repository and evaluate its quality and "
"and evaluate its quality and relevance to the repository.\n\n" "relevance to the repository.\n\n"
"Please analyze:\n" "Please analyze:\n"
"1. Is the README_Prometheus.md file related to this specific repository? (Does it describe the actual code " "1. Is the README_Prometheus.md file related to this specific repository? (Does it describe the actual code "
"and purpose of this repo?)\n" "and purpose of this repo?)\n"
@ -16,7 +16,7 @@ PROMPTS = {
"3. Is it comprehensive enough to help users understand and use the repository?\n" "3. Is it comprehensive enough to help users understand and use the repository?\n"
"4. Does it follow best practices for README documentation?\n\n" "4. Does it follow best practices for README documentation?\n\n"
"Use the `review_readme_file` tool to submit your findings.\n" "Use the `review_readme_file` tool to submit your findings.\n"
"IMPORTANT: Do not assume that an existing README is correct. " # "IMPORTANT: Do not assume that an existing README is correct. "
"Evaluate README_Prometheus.md against the codebase.\n" "Evaluate README_Prometheus.md against the codebase.\n"
"DO NOT consider the filename in your analysis, only the content.\n" "DO NOT consider the filename in your analysis, only the content.\n"
"STOP after submitting the review report." "STOP after submitting the review report."

View File

@ -1,7 +1,20 @@
"""Task decomposition workflow implementation.""" """Task decomposition workflow implementation."""
import os import os
from github import Github import sys
from pathlib import Path
# Add the gitea-shim to the path
gitea_shim_path = Path(__file__).parent.parent.parent.parent.parent / "gitea-shim" / "python"
sys.path.insert(0, str(gitea_shim_path))
try:
from gitea_shim import get_github_client, is_gitea_mode
except ImportError:
# Fallback to regular GitHub if shim is not available
from github import Github as get_github_client
is_gitea_mode = lambda: False
from prometheus_swarm.workflows.base import Workflow from prometheus_swarm.workflows.base import Workflow
from prometheus_swarm.utils.logging import log_section, log_key_value, log_error from prometheus_swarm.utils.logging import log_section, log_key_value, log_error
from src.workflows.repoSummarizerAudit import phases from src.workflows.repoSummarizerAudit import phases
@ -89,10 +102,15 @@ class repoSummarizerAuditWorkflow(Workflow):
self.context["github_token"] = os.getenv("GITHUB_TOKEN") self.context["github_token"] = os.getenv("GITHUB_TOKEN")
# Enter repo directory # Enter repo directory
os.chdir(self.context["repo_path"]) os.chdir(self.context["repo_path"])
gh = Github(self.context["github_token"])
# Use the shim to get GitHub-compatible client
gh = get_github_client()
repo = gh.get_repo(f"{self.context['repo_owner']}/{self.context['repo_name']}") repo = gh.get_repo(f"{self.context['repo_owner']}/{self.context['repo_name']}")
pr = repo.get_pull(self.context["pr_number"]) pr = repo.get_pull(self.context["pr_number"])
self.context["pr"] = pr self.context["pr"] = pr
log_key_value("API Mode", "Gitea" if is_gitea_mode() else "GitHub")
# Add remote for PR's repository and fetch the branch # Add remote for PR's repository and fetch the branch
os.system( os.system(
f"git remote add pr_source https://github.com/{pr.head.repo.full_name}" f"git remote add pr_source https://github.com/{pr.head.repo.full_name}"

View File

@ -2,13 +2,11 @@ import { getOrcaClient } from "@_koii/task-manager/extensions";
import { middleServerUrl, status } from "../utils/constant"; import { middleServerUrl, status } from "../utils/constant";
import { submissionJSONSignatureDecode } from "../utils/submissionJSONSignatureDecode"; import { submissionJSONSignatureDecode } from "../utils/submissionJSONSignatureDecode";
// import { status } from '../utils/constant' // import { status } from '../utils/constant'
export async function audit(cid: string, roundNumber: number, submitterKey: string): Promise<boolean | void> {
/** const TIMEOUT_MS = 180000; // 3 minutes in milliseconds
* Audit a submission const MAX_RETRIES = 3;
* This function should return true if the submission is correct, false otherwise
* The default implementation retrieves the proofs from IPFS async function auditWithTimeout(cid: string, roundNumber: number, submitterKey: string): Promise<boolean | void> {
* and sends them to your container for auditing
*/
let orcaClient; let orcaClient;
try { try {
orcaClient = await getOrcaClient(); orcaClient = await getOrcaClient();
@ -83,3 +81,28 @@ export async function audit(cid: string, roundNumber: number, submitterKey: stri
console.log("[AUDIT] Cleaning up resources"); console.log("[AUDIT] Cleaning up resources");
} }
} }
export async function audit(cid: string, roundNumber: number, submitterKey: string): Promise<boolean | void> {
let retries = 0;
while (retries < MAX_RETRIES) {
try {
const result = await Promise.race<boolean | void>([
auditWithTimeout(cid, roundNumber, submitterKey),
new Promise((_, reject) => setTimeout(() => reject(new Error("Audit timeout")), TIMEOUT_MS)),
]);
return result;
} catch (error) {
retries++;
console.log(`[AUDIT] Attempt ${retries} failed:`, error);
if (retries === MAX_RETRIES) {
console.log(`[AUDIT] Max retries (${MAX_RETRIES}) reached. Giving up.`);
return true; // Return true as a fallback
}
// Wait for a short time before retrying
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
}

View File

@ -54,6 +54,68 @@ export async function routes() {
const submitDistributionResult = await taskRunner.submitDistributionList(Number(roundNumber)); const submitDistributionResult = await taskRunner.submitDistributionList(Number(roundNumber));
res.status(200).json({ result: submitDistributionResult }); res.status(200).json({ result: submitDistributionResult });
}); });
app.post("/add-todo-draft-pr", async (req, res) => {
const signature = req.body.signature;
const prUrl = req.body.prUrl;
const swarmBountyId = req.body.swarmBountyId;
console.log("[TASK] req.body", req.body);
try {
const publicKey = await namespaceWrapper.getMainAccountPubkey();
const stakingKeypair = await namespaceWrapper.getSubmitterAccount();
if (!stakingKeypair) {
throw new Error("No staking key found");
}
const stakingKey = stakingKeypair.publicKey.toBase58();
const secretKey = stakingKeypair.secretKey;
if (!publicKey) {
throw new Error("No public key found");
}
const payload = await namespaceWrapper.verifySignature(signature, stakingKey);
if (!payload) {
throw new Error("Invalid signature");
}
console.log("[TASK] payload: ", payload);
const data = payload.data;
if (!data) {
throw new Error("No signature data found");
}
const jsonData = JSON.parse(data);
if (jsonData.taskId !== TASK_ID) {
throw new Error(`Invalid task ID from signature: ${jsonData.taskId}. Actual task ID: ${TASK_ID}`);
}
const middleServerPayload = {
taskId: jsonData.taskId,
swarmBountyId,
prUrl,
stakingKey,
publicKey,
action: "add-todo-draft-pr",
};
const middleServerSignature = await namespaceWrapper.payloadSigning(middleServerPayload, secretKey);
const middleServerResponse = await fetch(`${middleServerUrl}/summarizer/worker/add-todo-draft-pr`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ signature: middleServerSignature, stakingKey: stakingKey }),
});
console.log("[TASK] Add Draft PR Response: ", middleServerResponse);
if (middleServerResponse.status !== 200) {
throw new Error(`Posting to middle server failed: ${middleServerResponse.statusText}`);
}
res.status(200).json({ result: "Successfully saved PR" });
} catch (error) {
console.error("[TASK] Error adding PR to summarizer todo:", error);
// await namespaceWrapper.storeSet(`result-${roundNumber}`, status.SAVING_TODO_PR_FAILED);
res.status(400).json({ error: "Failed to save PR" });
}
});
app.post("/add-todo-pr", async (req, res) => { app.post("/add-todo-pr", async (req, res) => {
const signature = req.body.signature; const signature = req.body.signature;

View File

@ -57,6 +57,6 @@ export const actionMessage = {
export const defaultBountyMarkdownFile = export const defaultBountyMarkdownFile =
"https://raw.githubusercontent.com/koii-network/prometheus-swarm-bounties/master/README.md"; "https://raw.githubusercontent.com/koii-network/prometheus-swarm-bounties/master/README.md";
export const customReward = 1; // This should be in ROE! export const customReward = 400 * 10 ** 9; // This should be in ROE!
export const middleServerUrl = "https://builder247-test.dev1.koii.network"; export const middleServerUrl = "https://builder247-prod.dev.koii.network";

View File

@ -28,7 +28,7 @@ export async function task() {
taskId: TASK_ID, taskId: TASK_ID,
// roundNumber: roundNumber, // roundNumber: roundNumber,
action: "fetch-todo", action: "fetch-todo",
githubUsername: stakingKey, githubUsername: process.env.GITHUB_USERNAME,
stakingKey: stakingKey, stakingKey: stakingKey,
}, },
stakingKeypair.secretKey, stakingKeypair.secretKey,
@ -57,14 +57,16 @@ export async function task() {
// check if the response is 200 after all retries // check if the response is 200 after all retries
if (!requiredWorkResponse || requiredWorkResponse.status !== 200) { if (!requiredWorkResponse || requiredWorkResponse.status !== 200) {
return; // return;
continue;
} }
const requiredWorkResponseData = await requiredWorkResponse.json(); const requiredWorkResponseData = await requiredWorkResponse.json();
console.log("[TASK] requiredWorkResponseData: ", requiredWorkResponseData); console.log("[TASK] requiredWorkResponseData: ", requiredWorkResponseData);
// const uuid = uuidv4(); // const uuid = uuidv4();
const alreadyAssigned = await namespaceWrapper.storeGet(JSON.stringify(requiredWorkResponseData.data.id)); const alreadyAssigned = await namespaceWrapper.storeGet(JSON.stringify(requiredWorkResponseData.data.id));
if (alreadyAssigned) { if (alreadyAssigned) {
return; continue;
// return;
} else { } else {
await namespaceWrapper.storeSet(JSON.stringify(requiredWorkResponseData.data.id), "initialized"); await namespaceWrapper.storeSet(JSON.stringify(requiredWorkResponseData.data.id), "initialized");
} }
@ -90,30 +92,35 @@ export async function task() {
while (retryCount < maxRetries) { while (retryCount < maxRetries) {
try { try {
repoSummaryResponse = await Promise.race([ const podcallPromise = orcaClient.podCall(`worker-task`, {
orcaClient.podCall(`worker-task`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(jsonBody), body: JSON.stringify(jsonBody),
}), });
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout)),
]); const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Podcall timeout after 100 seconds")), timeout)
);
repoSummaryResponse = await Promise.race([podcallPromise, timeoutPromise]);
console.log("[TASK] repoSummaryResponse: ", repoSummaryResponse); console.log("[TASK] repoSummaryResponse: ", repoSummaryResponse);
break; // If successful, break the retry loop break; // If successful, break the retry loop
} catch (error) { } catch (error: any) {
console.log(`[TASK] Podcall attempt ${retryCount + 1} failed:`, error);
retryCount++; retryCount++;
if (retryCount === maxRetries) { if (retryCount === maxRetries) {
throw error; // If we've exhausted retries, throw the error throw new Error(`Podcall failed after ${maxRetries} attempts: ${error.message}`);
} }
console.log(`[TASK] Attempt ${retryCount} failed, retrying...`); console.log(`[TASK] Retrying in 10 seconds...`);
await new Promise((resolve) => setTimeout(resolve, 10000)); // Wait 10 seconds before retry await new Promise((resolve) => setTimeout(resolve, 10000)); // Wait 10 seconds before retry
} }
} }
} catch (error) { } catch (error) {
// await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ISSUE_SUMMARIZATION_FAILED); // await namespaceWrapper.storeSet(`result-${roundNumber}`, status.ISSUE_SUMMARIZATION_FAILED);
console.error("[TASK] EXECUTE TASK ERROR:", error); console.error("[TASK] EXECUTE TASK ERROR:", error);
continue;
} }
} catch (error) { } catch (error) {
console.error("[TASK] EXECUTE TASK ERROR:", error); console.error("[TASK] EXECUTE TASK ERROR:", error);