diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md new file mode 100644 index 0000000..28b4d6a --- /dev/null +++ b/API_DOCUMENTATION.md @@ -0,0 +1,933 @@ +# API Documentation + +## Overview + +This document describes the APIs utilized by the Redmine-Gitea Sync Server, including both external APIs (Redmine and Gitea) and the server's own endpoints. + +## Table of Contents + +- [Server Endpoints](#server-endpoints) +- [Redmine API Integration](#redmine-api-integration) +- [Gitea API Integration](#gitea-api-integration) +- [Webhook Payloads](#webhook-payloads) +- [Data Mapping](#data-mapping) + +--- + +## Server Endpoints + +### Health Check + +**Endpoint:** `GET /health` + +**Description:** Returns server health status and configuration information. + +**Response:** +```json +{ + "status": "ok", + "uptime": 12345.67, + "timestamp": "2025-12-17T01:49:47Z", + "cache": { + "size": 5, + "ttl": 30000 + }, + "config": { + "redmine": { + "url": "https://redmine.example.com", + "connected": true + }, + "gitea": { + "url": "https://gitea.example.com", + "owner": "username", + "connected": true + }, + "sync": { + "enableLabels": true, + "enableMilestones": true, + "enableAttachments": false, + "enableCustomFields": true, + "retryAttempts": 3, + "retryDelay": 1000 + } + } +} +``` + +### Status Check + +**Endpoint:** `GET /status` + +**Description:** Tests connectivity to both Redmine and Gitea platforms. + +**Response:** +```json +{ + "server": "running", + "uptime": 12345.67, + "cache": { + "size": 5, + "ttl": 30000 + }, + "connections": { + "redmine": "connected", + "gitea": "connected" + } +} +``` + +**Possible Connection States:** +- `connected`: Successfully connected to platform +- `disconnected`: Failed to connect to platform +- `unknown`: Connection not yet tested + +### Clear Cache + +**Endpoint:** `POST /cache/clear` + +**Description:** Manually clears the sync cache. Useful for troubleshooting sync issues. + +**Response:** +```json +{ + "message": "Cache cleared", + "timestamp": "2025-12-17T01:49:47Z" +} +``` + +### Redmine Webhook + +**Endpoint:** `POST /webhook/redmine` + +**Description:** Receives webhook notifications from Redmine for issue updates. + +**Expected Payload:** See [Redmine Webhook Payload](#redmine-webhook-payload) + +**Response:** `200 OK` (always returns immediately) + +**Processing:** +1. Validates payload structure +2. Checks sync cache to prevent loops +3. Retrieves or creates corresponding Gitea issue +4. Syncs metadata (status, assignee, labels, milestone) +5. Syncs journals as comments +6. Updates cache + +### Gitea Webhook + +**Endpoint:** `POST /webhook/gitea` + +**Description:** Receives webhook notifications from Gitea for issue updates. + +**Expected Payload:** See [Gitea Webhook Payload](#gitea-webhook-payload) + +**Response:** `200 OK` (always returns immediately) + +**Processing:** +1. Validates payload structure +2. Checks sync cache to prevent loops +3. Retrieves or creates corresponding Redmine issue +4. Syncs metadata (status, assignee, due date) +5. Syncs comments as journals +6. Updates cache + +--- + +## Redmine API Integration + +### Base Configuration + +**Base URL:** Configured via `REDMINE_API_URL` +**Authentication:** API Key via `X-Redmine-API-Key` header +**Format:** JSON + +### Endpoints Used + +#### Get Issue + +**Method:** `GET /issues/{id}.json` + +**Parameters:** +- `include`: `attachments,journals,watchers` + +**Response Example:** +```json +{ + "issue": { + "id": 31, + "project": { + "id": 2, + "identifier": "ai-smuggling", + "name": "AI Smuggling" + }, + "tracker": { + "id": 1, + "name": "Bug" + }, + "status": { + "id": 2, + "name": "In Progress" + }, + "priority": { + "id": 2, + "name": "Normal" + }, + "author": { + "id": 1, + "login": "admin" + }, + "assignee": { + "id": 1, + "login": "admin" + }, + "subject": "Issue title", + "description": "Issue description", + "start_date": "2025-12-16", + "due_date": "2025-12-19", + "estimated_hours": 5.0, + "custom_fields": [], + "journals": [], + "attachments": [] + } +} +``` + +#### Create Issue + +**Method:** `POST /issues.json` + +**Payload:** +```json +{ + "issue": { + "project_id": "ai-smuggling", + "tracker_id": 1, + "status_id": 2, + "priority_id": 2, + "subject": "Issue title", + "description": "Issue description", + "assigned_to_id": 1, + "start_date": "2025-12-16", + "due_date": "2025-12-19", + "estimated_hours": 5.0 + } +} +``` + +**Response:** +```json +{ + "issue": { + "id": 32, + "project": {...}, + "subject": "Issue title", + ... + } +} +``` + +#### Update Issue + +**Method:** `PUT /issues/{id}.json` + +**Payload:** +```json +{ + "issue": { + "subject": "Updated title", + "description": "Updated description", + "status_id": 5, + "assigned_to_id": 2, + "due_date": "2025-12-20", + "notes": "Adding a comment" + } +} +``` + +#### Search Issues + +**Method:** `GET /issues.json` + +**Parameters:** +- `project_id`: Project identifier +- `subject`: Subject search (supports `~` for partial match) +- `limit`: Maximum results (default: 100) + +**Response:** +```json +{ + "issues": [ + { + "id": 31, + "subject": "Issue title", + ... + } + ], + "total_count": 1, + "offset": 0, + "limit": 100 +} +``` + +#### Get Project + +**Method:** `GET /projects/{id}.json` + +**Response:** +```json +{ + "project": { + "id": 2, + "identifier": "ai-smuggling", + "name": "AI Smuggling", + "description": "Project description", + "created_on": "2025-12-15T08:37:45.000Z" + } +} +``` + +#### Get Trackers + +**Method:** `GET /trackers.json` + +**Response:** +```json +{ + "trackers": [ + { + "id": 1, + "name": "Bug" + }, + { + "id": 2, + "name": "Feature" + } + ] +} +``` + +#### Get Issue Statuses + +**Method:** `GET /issue_statuses.json` + +**Response:** +```json +{ + "issue_statuses": [ + { + "id": 1, + "name": "New", + "is_closed": false + }, + { + "id": 5, + "name": "Closed", + "is_closed": true + } + ] +} +``` + +#### Get Priorities + +**Method:** `GET /enumerations/issue_priorities.json` + +**Response:** +```json +{ + "issue_priorities": [ + { + "id": 1, + "name": "Low" + }, + { + "id": 2, + "name": "Normal" + } + ] +} +``` + +#### Get Versions (Milestones) + +**Method:** `GET /projects/{id}/versions.json` + +**Response:** +```json +{ + "versions": [ + { + "id": 1, + "name": "Version 1.0", + "description": "First release", + "status": "open", + "due_date": "2025-12-31" + } + ] +} +``` + +--- + +## Gitea API Integration + +### Base Configuration + +**Base URL:** Configured via `GITEA_API_URL` +**Authentication:** Token via `Authorization: token {TOKEN}` header +**Format:** JSON + +### Endpoints Used + +#### Get Issue + +**Method:** `GET /api/v1/repos/{owner}/{repo}/issues/{index}` + +**Response Example:** +```json +{ + "id": 27, + "number": 27, + "title": "Issue title", + "body": "Issue description", + "state": "open", + "user": { + "id": 1, + "login": "tombomb122", + "full_name": "Thomas Scott" + }, + "assignee": { + "id": 1, + "login": "tombomb122" + }, + "labels": [], + "milestone": null, + "created_at": "2025-12-17T01:49:47Z", + "updated_at": "2025-12-17T01:49:47Z", + "due_date": null, + "comments": 0 +} +``` + +#### Search Issues + +**Method:** `GET /api/v1/repos/{owner}/{repo}/issues` + +**Parameters:** +- `state`: `open`, `closed`, or `all` +- `labels`: Comma-separated label names +- `q`: Search query +- `limit`: Maximum results +- `page`: Page number + +**Response:** +```json +[ + { + "id": 27, + "number": 27, + "title": "Issue title", + ... + } +] +``` + +#### Create Issue + +**Method:** `POST /api/v1/repos/{owner}/{repo}/issues` + +**Payload:** +```json +{ + "title": "Issue title", + "body": "Issue description", + "assignee": "username", + "milestone": 1, + "labels": [1, 2, 3], + "due_date": "2025-12-19T00:00:00Z" +} +``` + +**Response:** +```json +{ + "id": 28, + "number": 28, + "title": "Issue title", + ... +} +``` + +#### Update Issue + +**Method:** `PATCH /api/v1/repos/{owner}/{repo}/issues/{index}` + +**Payload:** +```json +{ + "title": "Updated title", + "body": "Updated description", + "state": "closed", + "assignee": "username", + "milestone": 2, + "due_date": "2025-12-20T00:00:00Z" +} +``` + +#### Get Comments + +**Method:** `GET /api/v1/repos/{owner}/{repo}/issues/{index}/comments` + +**Response:** +```json +[ + { + "id": 1, + "body": "Comment text", + "user": { + "login": "username" + }, + "created_at": "2025-12-17T01:49:47Z", + "updated_at": "2025-12-17T01:49:47Z" + } +] +``` + +#### Add Comment + +**Method:** `POST /api/v1/repos/{owner}/{repo}/issues/{index}/comments` + +**Payload:** +```json +{ + "body": "Comment text" +} +``` + +**Response:** +```json +{ + "id": 2, + "body": "Comment text", + "user": {...}, + "created_at": "2025-12-17T02:00:00Z" +} +``` + +#### Get Labels + +**Method:** `GET /api/v1/repos/{owner}/{repo}/labels` + +**Response:** +```json +[ + { + "id": 1, + "name": "bug", + "color": "#ff0000", + "description": "Bug label" + } +] +``` + +#### Create Label + +**Method:** `POST /api/v1/repos/{owner}/{repo}/labels` + +**Payload:** +```json +{ + "name": "bug", + "color": "#ff0000", + "description": "Bug label" +} +``` + +#### Set Issue Labels + +**Method:** `PUT /api/v1/repos/{owner}/{repo}/issues/{index}/labels` + +**Payload:** +```json +{ + "labels": [1, 2, 3] +} +``` + +#### Get Milestones + +**Method:** `GET /api/v1/repos/{owner}/{repo}/milestones` + +**Parameters:** +- `state`: `open`, `closed`, or `all` + +**Response:** +```json +[ + { + "id": 1, + "title": "Version 1.0", + "description": "First release", + "state": "open", + "due_on": "2025-12-31T00:00:00Z" + } +] +``` + +#### Create Milestone + +**Method:** `POST /api/v1/repos/{owner}/{repo}/milestones` + +**Payload:** +```json +{ + "title": "Version 1.0", + "description": "First release", + "due_on": "2025-12-31T00:00:00Z" +} +``` + +#### Get Repository + +**Method:** `GET /api/v1/repos/{owner}/{repo}` + +**Response:** +```json +{ + "id": 1, + "name": "AI_Smuggling", + "full_name": "tombomb122/AI_Smuggling", + "description": "Repository description", + "owner": {...}, + "created_at": "2025-12-15T08:40:06Z" +} +``` + +--- + +## Webhook Payloads + +### Redmine Webhook Payload + +**Event:** Issue Update + +```json +{ + "payload": { + "action": "updated", + "issue": { + "id": 31, + "subject": "Issue title", + "description": "Issue description", + "created_on": "2025-12-16T14:06:41.000Z", + "updated_on": "2025-12-16T14:07:37.000Z", + "start_date": "2025-12-16", + "due_date": "2025-12-19", + "estimated_hours": 5.0, + "project": { + "id": 2, + "identifier": "ai-smuggling", + "name": "AI Smuggling" + }, + "status": { + "id": 2, + "name": "In Progress" + }, + "tracker": { + "id": 1, + "name": "Bug" + }, + "priority": { + "id": 2, + "name": "Normal" + }, + "author": { + "id": 1, + "login": "admin" + }, + "assignee": { + "id": 1, + "login": "admin" + } + }, + "journal": { + "id": 14, + "notes": "Comment text", + "created_on": "2025-12-16T14:07:37.000Z", + "author": { + "id": 1, + "login": "admin" + }, + "details": [ + { + "property": "attr", + "prop_key": "status_id", + "old_value": "1", + "value": "2" + } + ] + }, + "url": "https://redmine.example.com/issues/31" + } +} +``` + +### Gitea Webhook Payload + +**Event:** Issue Update + +```json +{ + "action": "opened", + "number": 27, + "issue": { + "id": 27, + "number": 27, + "title": "Issue title", + "body": "Issue description", + "state": "open", + "user": { + "id": 1, + "login": "tombomb122", + "full_name": "Thomas Scott" + }, + "assignee": { + "id": 1, + "login": "tombomb122" + }, + "labels": [], + "milestone": null, + "created_at": "2025-12-17T01:49:47Z", + "updated_at": "2025-12-17T01:49:47Z", + "due_date": null + }, + "repository": { + "id": 1, + "name": "AI_Smuggling", + "full_name": "tombomb122/AI_Smuggling", + "owner": { + "login": "tombomb122" + } + }, + "sender": { + "id": 1, + "login": "tombomb122" + } +} +``` + +**Event:** Issue Comment + +```json +{ + "action": "created", + "issue": {...}, + "comment": { + "id": 1, + "body": "Comment text", + "user": { + "login": "tombomb122" + }, + "created_at": "2025-12-17T02:00:00Z", + "updated_at": "2025-12-17T02:00:00Z" + }, + "repository": {...}, + "sender": {...} +} +``` + +--- + +## Data Mapping + +### Status Mapping + +#### Redmine → Gitea + +| Redmine Status ID | Redmine Status Name | Gitea State | +|-------------------|---------------------|-------------| +| 1 | New | open | +| 2 | In Progress | open | +| 3 | Resolved | open | +| 4 | Feedback | open | +| 5 | Closed | closed | +| 6 | Rejected | closed | + +#### Gitea → Redmine + +| Gitea State | Redmine Status ID | Redmine Status Name | +|-------------|-------------------|---------------------| +| open | 2 | In Progress | +| closed | 5 | Closed | + +### Priority Mapping + +| Redmine Priority ID | Redmine Priority Name | Gitea Label | +|---------------------|----------------------|-------------| +| 1 | Low | Low Priority | +| 2 | Normal | Normal Priority | +| 3 | High | High Priority | +| 4 | Urgent | Urgent | +| 5 | Immediate | Immediate | + +### Tracker Mapping + +| Redmine Tracker ID | Redmine Tracker Name | Gitea Label | +|--------------------|---------------------|-------------| +| 1 | Bug | bug | +| 2 | Feature | feature | +| 3 | Support | support | + +### Issue Title Format + +**Redmine → Gitea:** +``` +Redmine #{id}: {subject} +``` +Example: `Redmine #31: Fix login bug` + +**Gitea → Redmine:** +The original Gitea title is used, but when synced back to Gitea, it gets the Redmine prefix. + +### Issue Description Format + +**Gitea → Redmine:** +``` +[Gitea Issue #{number}] +Author: {username} +Created at: {timestamp} + +{original body} +``` + +**Redmine → Gitea:** +``` +{original description} + +--- +**Custom Fields:** +- Field1: Value1 +- Field2: Value2 +``` + +### Comment/Journal Format + +**Redmine Journal → Gitea Comment:** +``` +[Redmine Journal #{id}] +**{author}** commented: + +{notes} + +**Changes:** +- field_name: old_value → new_value +``` + +**Gitea Comment → Redmine Journal:** +``` +[Gitea Comment #{id}] +{username} commented: + +{body} +``` + +--- + +## Error Codes + +### HTTP Status Codes + +- `200 OK`: Request successful +- `400 Bad Request`: Invalid payload or parameters +- `401 Unauthorized`: Invalid or missing API credentials +- `403 Forbidden`: Insufficient permissions +- `404 Not Found`: Resource not found +- `422 Unprocessable Entity`: Validation errors +- `500 Internal Server Error`: Server error + +### Redmine Errors + +Common Redmine API errors: +- Invalid API key +- Project not found +- Issue not found +- Insufficient permissions +- Validation errors + +### Gitea Errors + +Common Gitea API errors: +- Invalid token +- Repository not found +- Issue not found +- Label already exists +- Rate limit exceeded + +--- + +## Rate Limiting + +### Redmine +- No explicit rate limit in API +- Server configuration may impose limits +- Consider implementing client-side throttling + +### Gitea +- Rate limits depend on instance configuration +- Default: Varies by instance +- The sync server implements retry logic with exponential backoff + +--- + +## Best Practices + +1. **Authentication**: Securely store API keys and tokens +2. **Error Handling**: Always check response status codes +3. **Retry Logic**: Implement exponential backoff for transient failures +4. **Webhooks**: Validate webhook signatures in production +5. **Logging**: Log all API interactions for debugging +6. **Testing**: Test sync in development environment first +7. **Monitoring**: Monitor API usage and error rates + +--- + +## Troubleshooting + +### Common API Issues + +**Issue: 401 Unauthorized** +- Verify API key/token is correct +- Check token has required permissions +- Ensure token hasn't expired + +**Issue: 404 Not Found** +- Verify resource exists (project, issue, repository) +- Check project/repository identifiers are correct +- Ensure proper URL formatting + +**Issue: 422 Validation Error** +- Review required fields +- Check data types match API expectations +- Validate date formats + +**Issue: Rate Limited** +- Implement retry with backoff +- Reduce sync frequency +- Contact administrator for increased limits + +### Debug Tips + +1. Enable verbose logging: `LOG_VERBOSE=true` +2. Use API testing tools (Postman, curl) to verify credentials +3. Check server logs for detailed error messages +4. Verify network connectivity to both platforms +5. Test individual API endpoints before full sync + +--- + +## References + +- [Redmine API Documentation](https://www.redmine.org/projects/redmine/wiki/Rest_api) +- [Gitea API Documentation](https://docs.gitea.com/api/1.20/) +- [Redmine Webhooks](https://www.redmine.org/projects/redmine/wiki/Webhooks) +- [Gitea Webhooks](https://docs.gitea.com/usage/webhooks) \ No newline at end of file