docs(ISSUE-WP-0006): Forgejo-only language and projection boundary
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 34s

Prefer Forgejo as the self-hosted forge product. Keep Gitea only as the
Gitea-compatible API identifier (module backends/gitea, type string
gitea). FORGEJO_TOKEN is preferred; GITEA_* remains a deprecated alias.

INTENT/SCOPE quote ACT-ADR-005: issue-core is not the fleet ops claim
queue. Connector docs describe repo work record → hub index → optional
Forgejo projection, not activity-core → issue-core → harness.

Assistant: grok
Assistant-Session: 01a09dc6-3f0d-7c93-8b11-8e83c0623d49
This commit is contained in:
tegwick 2026-09-14 04:52:58 +02:00
parent f9d276dadf
commit ee9b85215d
34 changed files with 410 additions and 263 deletions

View file

@ -2,12 +2,12 @@
issue-core external issue-tracker connector
Backend-agnostic CLI, library, and optional REST surface for operating on
third-party issue trackers (Gitea/Forgejo, local SQLite). Not the origin of
fleet work records (see INTENT.md / SCOPE.md).
third-party issue trackers (Forgejo via Gitea-compatible API, local SQLite).
Not the origin of fleet work records (see INTENT.md / SCOPE.md).
Shipped:
- Unified issue model and IssueBackend plugin architecture
- Local SQLite + Gitea backends; bidirectional sync CLI
- Local SQLite + Forgejo backends; bidirectional sync CLI
- CLI (`issue` / `issue-core`) and optional FastAPI REST (`issue serve`)
"""

View file

@ -7,5 +7,5 @@ API regardless of the underlying issue tracking system.
Available Backends:
- local: SQLite-based local backend for offline use
- gitea: Gitea API backend for GitHub-compatible systems
- gitea: Forgejo backend (Gitea-compatible API)
"""

View file

@ -1,12 +1,11 @@
"""
Gitea Backend
Forgejo backend (Gitea-compatible API).
A backend implementation for Gitea issue tracking systems.
This backend provides integration with Gitea API for remote issue management.
Talks to the self-hosted Forgejo HTTP API. Gitea is not a second supported
product. The backend type string remains `gitea`.
Features:
- Full Gitea API integration
- GitHub-compatible operations
- Full Forgejo / Gitea-compatible API integration
- Remote synchronization
- Authentication support
- Rate limiting compliance

View file

@ -1,8 +1,9 @@
"""
Gitea Backend Implementation
Forgejo backend (Gitea-compatible API).
Provides integration with Gitea API for remote issue tracking.
This backend adapts the Gitea API to our unified issue model.
Talks to the self-hosted Forgejo HTTP API, which is Gitea-compatible.
Gitea is not a second supported product. The backend type string remains
`gitea` for compatibility.
"""
import requests
@ -16,7 +17,7 @@ from ...core.models import Issue, Label, User, Milestone, Comment, IssueState, P
class GiteaAPIError(Exception):
"""Gitea API specific errors."""
"""Forgejo (Gitea-compatible API) errors."""
pass
@ -26,7 +27,7 @@ class GiteaRateLimitError(GiteaAPIError):
class GiteaBackend(RemoteBackend, SyncableBackend):
"""Gitea API backend for remote issue tracking."""
"""Forgejo backend using the Gitea-compatible HTTP API."""
def __init__(self):
self.base_url: Optional[str] = None
@ -44,7 +45,7 @@ class GiteaBackend(RemoteBackend, SyncableBackend):
supports_webhooks=True,
supports_real_time=False,
max_labels_per_issue=None,
max_assignees_per_issue=10 # Gitea typical limit
max_assignees_per_issue=10 # Forgejo / Gitea-compatible typical limit
)
@property
@ -56,7 +57,7 @@ class GiteaBackend(RemoteBackend, SyncableBackend):
return self._capabilities
def connect(self, config: Dict[str, Any]) -> None:
"""Connect to Gitea API."""
"""Connect to Forgejo (Gitea-compatible API)."""
self.base_url = config['base_url'].rstrip('/')
self.token = config['token']
self.owner = config['owner']
@ -71,10 +72,10 @@ class GiteaBackend(RemoteBackend, SyncableBackend):
# Test connection
if not self.test_connection():
raise GiteaAPIError("Failed to connect to Gitea API")
raise GiteaAPIError("Failed to connect to Forgejo (Gitea-compatible API)")
def disconnect(self) -> None:
"""Disconnect from Gitea API."""
"""Disconnect from Forgejo."""
self.session.close()
self.base_url = None
self.token = None
@ -195,20 +196,20 @@ class GiteaBackend(RemoteBackend, SyncableBackend):
if issue.milestone:
data['milestone'] = int(issue.milestone.backend_id) if issue.milestone.backend_id else None
# Gitea expects numeric label IDs on issue create/update. Name-only
# labels are preserved in issue-core metadata but omitted from the API
# payload until a label-resolution step exists.
label_ids = []
for label in issue.labels:
if not label.backend_id:
continue
try:
label_ids.append(int(label.backend_id))
except (TypeError, ValueError):
continue
if label_ids:
data['labels'] = label_ids
# Gitea expects numeric label IDs on issue create/update. Name-only
# labels are preserved in issue-core metadata but omitted from the API
# payload until a label-resolution step exists.
label_ids = []
for label in issue.labels:
if not label.backend_id:
continue
try:
label_ids.append(int(label.backend_id))
except (TypeError, ValueError):
continue
if label_ids:
data['labels'] = label_ids
return data
# Issue CRUD Operations

View file

@ -12,6 +12,23 @@ from .utils import (
echo_error, echo_warning, echo_info, confirm_action
)
# Preferred FORGEJO_*; GITEA_* remain deprecated aliases (ISSUE-WP-0006).
_FORGE_TOKEN_ENV_NAMES = (
'FORGEJO_TOKEN',
'FORGEJO_API_TOKEN',
'GITEA_API_TOKEN',
'GITEA_TOKEN',
)
def _forge_token_from_env():
"""Return (env_name, token) for the first set Forgejo/legacy token var."""
for name in _FORGE_TOKEN_ENV_NAMES:
value = os.getenv(name)
if value:
return name, value
return None, None
@click.group()
def backend_group():
@ -46,14 +63,19 @@ def add_backend(ctx, name, backend_type):
'db_path': str(db_path)
}
elif backend_type == 'gitea':
base_url = click.prompt('Gitea base URL (e.g., https://git.example.com)')
base_url = click.prompt('Forgejo base URL (Gitea-compatible API)')
owner = click.prompt('Repository owner/organization')
repo = click.prompt('Repository name')
# Check for API token in environment variable first
env_token = os.getenv('GITEA_API_TOKEN')
env_name, env_token = _forge_token_from_env()
if env_token:
click.echo(f"Using API token from GITEA_API_TOKEN environment variable")
if env_name in ('GITEA_API_TOKEN', 'GITEA_TOKEN'):
click.echo(
f"Using API token from {env_name} "
"(deprecated alias; prefer FORGEJO_TOKEN)"
)
else:
click.echo(f"Using API token from {env_name} environment variable")
token = env_token
else:
token = click.prompt('Access token', hide_input=True)