diff --git a/contracts/gitea-api.md b/contracts/gitea-api.md new file mode 100644 index 0000000..02b8141 --- /dev/null +++ b/contracts/gitea-api.md @@ -0,0 +1,474 @@ +# API Contracts — Gitea Migration Toolkit + +All API calls used by the migration scripts. Every `gitea_api`, `gitea_backup_api`, and `github_api` call in the codebase MUST have a corresponding entry here. + +--- + +## Gitea REST API v1 + +Base URL: `${GITEA_INTERNAL_URL}/api/v1` (primary) or `${GITEA_BACKUP_INTERNAL_URL}/api/v1` (backup) +Auth: `Authorization: token ${GITEA_ADMIN_TOKEN}` (or `GITEA_BACKUP_ADMIN_TOKEN`) + +--- + +### POST /users/{username}/tokens + +**Used in**: Phase 1 (primary), Phase 2 (backup) +**Purpose**: Generate API token for admin user +**Auth**: Basic auth (`-u username:password`) — token doesn't exist yet + +**Request**: +```json +{ + "name": "string", + "scopes": ["all"] +} +``` + +**Response** (201): +```json +{ + "id": 1, + "name": "migration-token", + "sha1": "abc123...", + "token_last_eight": "abc12345" +} +``` + +**Notes**: `sha1` field contains the full token value — save this immediately, it's only returned once. + +--- + +### GET /user + +**Used in**: Preflight (validate token), Phase 1 post-check +**Purpose**: Verify the current authenticated user + +**Request**: No body + +**Response** (200): +```json +{ + "id": 1, + "login": "string", + "email": "string", + "is_admin": true +} +``` + +--- + +### POST /orgs + +**Used in**: Phase 1 +**Purpose**: Create organization + +**Request**: +```json +{ + "username": "string", + "full_name": "string", + "description": "string", + "visibility": "public" +} +``` + +**Response** (201): +```json +{ + "id": 1, + "name": "string", + "full_name": "string", + "visibility": "public" +} +``` + +--- + +### GET /orgs/{org} + +**Used in**: Phase 1 (idempotency check), Phase 1 post-check +**Purpose**: Check if organization exists + +**Request**: No body + +**Response** (200): +```json +{ + "id": 1, + "name": "string", + "full_name": "string" +} +``` + +**404**: Organization does not exist + +--- + +### POST /repos/migrate + +**Used in**: Phase 4 (import from GitHub), Phase 4 (Fedora pull mirrors) +**Purpose**: Migrate/mirror a repository from an external source + +**Request (GitHub import — primary)**: +```json +{ + "clone_addr": "https://github.com/{owner}/{repo}.git", + "auth_token": "string (GitHub PAT)", + "repo_owner": "string (Gitea org name)", + "repo_name": "string", + "service": "github", + "mirror": false, + "issues": true, + "labels": true, + "milestones": false, + "wiki": false +} +``` + +**Request (Fedora pull mirror)**: +```json +{ + "clone_addr": "http://{UNRAID_IP}:{PORT}/{org}/{repo}.git", + "auth_username": "string", + "auth_password": "string", + "repo_owner": "string (admin username on Fedora)", + "repo_name": "string", + "mirror": true, + "mirror_interval": "8h" +} +``` + +**Response** (201): +```json +{ + "id": 1, + "name": "string", + "full_name": "org/repo", + "empty": false, + "mirror": false, + "default_branch": "main" +} +``` + +**Notes**: Migration is async — poll `GET /repos/{owner}/{repo}` until `empty` is `false`. + +--- + +### GET /repos/{owner}/{repo} + +**Used in**: Phase 4 (idempotency + polling), Phase 4 post-check, Phase 5 post-check +**Purpose**: Get repository details + +**Request**: No body + +**Response** (200): +```json +{ + "id": 1, + "name": "string", + "full_name": "org/repo", + "empty": false, + "mirror": false, + "default_branch": "main", + "archived": false, + "description": "string" +} +``` + +**404**: Repository does not exist + +--- + +### DELETE /repos/{owner}/{repo} + +**Used in**: Phase 4 teardown +**Purpose**: Delete a repository + +**Request**: No body +**Response**: 204 No Content + +--- + +### GET /repos/{owner}/{repo}/commits + +**Used in**: Phase 4 post-check +**Purpose**: Verify repo has commits + +**Query params**: `?limit=1` + +**Response** (200): Array of commit objects (at least 1 if repo has content) + +--- + +### GET /repos/{owner}/{repo}/contents/{filepath} + +**Used in**: Phase 5 post-check (check `.gitea/workflows/` exists), Phase 9 (check security workflow exists) +**Purpose**: Get file or directory contents + +**Response** (200): File/directory metadata +**404**: Path does not exist + +--- + +### GET /admin/runners/registration-token + +**Used in**: Phase 3 +**Purpose**: Get registration token for new runners + +**Request**: No body + +**Response** (200): +```json +{ + "token": "string" +} +``` + +**Notes**: Requires admin-level API token. + +--- + +### GET /admin/runners + +**Used in**: Phase 3 post-check +**Purpose**: List all registered runners + +**Request**: No body + +**Response** (200): +```json +[ + { + "id": 1, + "name": "string", + "status": "online", + "labels": ["linux"] + } +] +``` + +--- + +### POST /repos/{owner}/{repo}/push_mirrors + +**Used in**: Phase 6 +**Purpose**: Create a push mirror to GitHub + +**Request**: +```json +{ + "remote_address": "https://github.com/{owner}/{repo}.git", + "remote_username": "string", + "remote_password": "string (GitHub PAT)", + "interval": "8h", + "sync_on_commit": true +} +``` + +**Response** (201): +```json +{ + "id": 1, + "remote_name": "string", + "remote_address": "string", + "interval": "8h", + "sync_on_commit": true +} +``` + +--- + +### GET /repos/{owner}/{repo}/push_mirrors + +**Used in**: Phase 6 (idempotency check), Phase 6 post-check +**Purpose**: List push mirrors for a repo + +**Response** (200): Array of push mirror objects + +--- + +### DELETE /repos/{owner}/{repo}/push_mirrors/{id} + +**Used in**: Phase 6 teardown +**Purpose**: Remove a push mirror + +**Response**: 204 No Content + +--- + +### POST /repos/{owner}/{repo}/push_mirrors-sync + +**Used in**: Phase 6 post-check +**Purpose**: Trigger immediate push mirror sync + +**Request**: No body +**Response**: 200 OK + +--- + +### POST /repos/{owner}/{repo}/branch_protections + +**Used in**: Phase 7, Phase 9 (update with status checks) +**Purpose**: Create branch protection rule + +**Request**: +```json +{ + "branch_name": "main", + "enable_push": false, + "enable_push_whitelist": false, + "require_signed_commits": false, + "enable_status_check": true, + "status_check_contexts": [], + "enable_approvals_whitelist": false, + "required_approvals": 1 +} +``` + +**Response** (201): Branch protection object + +--- + +### GET /repos/{owner}/{repo}/branch_protections + +**Used in**: Phase 7 (idempotency), Phase 7 post-check +**Purpose**: List all branch protection rules + +**Response** (200): Array of branch protection objects + +--- + +### GET /repos/{owner}/{repo}/branch_protections/{name} + +**Used in**: Phase 7 (check specific branch) +**Purpose**: Get a specific branch protection rule + +**Response** (200): Branch protection object +**404**: Rule does not exist + +--- + +### PATCH /repos/{owner}/{repo}/branch_protections/{name} + +**Used in**: Phase 9 (add status check contexts) +**Purpose**: Update branch protection rule + +**Request** (partial update): +```json +{ + "enable_status_check": true, + "status_check_contexts": ["semgrep", "trivy", "gitleaks"] +} +``` + +**Response** (200): Updated branch protection object + +--- + +### DELETE /repos/{owner}/{repo}/branch_protections/{name} + +**Used in**: Phase 7 teardown, Phase 9 teardown +**Purpose**: Delete branch protection rule + +**Response**: 204 No Content + +--- + +### GET /repos/{owner}/{repo}/actions/workflows + +**Used in**: Phase 5 post-check +**Purpose**: List workflows in repo + +**Response** (200): +```json +{ + "total_count": 1, + "workflows": [ + { + "id": "string", + "name": "string", + "path": ".gitea/workflows/ci.yml", + "state": "active" + } + ] +} +``` + +--- + +### GET /settings/api + +**Used in**: Phase 1 post-check (verify Actions enabled) +**Purpose**: Get instance API settings + +**Response** (200): Settings object with `has_actions` field + +--- + +## GitHub REST API + +Base URL: `https://api.github.com` +Auth: `Authorization: token ${GITHUB_TOKEN}` + +--- + +### GET /user + +**Used in**: Preflight +**Purpose**: Validate GitHub personal access token + +**Request**: No body + +**Response** (200): +```json +{ + "login": "string", + "id": 1 +} +``` + +**401**: Token invalid or expired + +--- + +### GET /repos/{owner}/{repo} + +**Used in**: Preflight (verify repos exist), Phase 8 (check archive status) +**Purpose**: Get repository details + +**Response** (200): +```json +{ + "name": "string", + "full_name": "owner/repo", + "archived": false, + "description": "string" +} +``` + +**404**: Repository does not exist + +--- + +### PATCH /repos/{owner}/{repo} + +**Used in**: Phase 8 (archive repos + update description), Phase 8 teardown (un-archive) +**Purpose**: Update repository settings + +**Request (archive)**: +```json +{ + "archived": true, + "description": "[MOVED] Now at https://git.domain.com/org/repo — was: original description" +} +``` + +**Request (un-archive)**: +```json +{ + "archived": false +} +``` + +**Response** (200): Updated repository object