Files
gitea-migration/contracts/gitea-api.md
S f32e200c64 feat: add API contracts
Document all Gitea REST API v1 and GitHub REST API endpoints used across
phases 1-9. Each endpoint includes: method, path, request/response schemas,
status codes, and which script uses it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:02:28 -06:00

8.3 KiB

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:

{
  "name": "string",
  "scopes": ["all"]
}

Response (201):

{
  "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):

{
  "id": 1,
  "login": "string",
  "email": "string",
  "is_admin": true
}

POST /orgs

Used in: Phase 1 Purpose: Create organization

Request:

{
  "username": "string",
  "full_name": "string",
  "description": "string",
  "visibility": "public"
}

Response (201):

{
  "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):

{
  "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):

{
  "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):

{
  "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):

{
  "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):

{
  "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):

{
  "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):

[
  {
    "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:

{
  "remote_address": "https://github.com/{owner}/{repo}.git",
  "remote_username": "string",
  "remote_password": "string (GitHub PAT)",
  "interval": "8h",
  "sync_on_commit": true
}

Response (201):

{
  "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:

{
  "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):

{
  "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):

{
  "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):

{
  "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):

{
  "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):

{
  "archived": true,
  "description": "[MOVED] Now at https://git.domain.com/org/repo — was: original description"
}

Request (un-archive):

{
  "archived": false
}

Response (200): Updated repository object