658 lines
14 KiB
Markdown
658 lines
14 KiB
Markdown
# 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)
|
|
Derived at runtime from container IPs:
|
|
- `GITEA_INTERNAL_URL=http://${UNRAID_GITEA_IP}:3000`
|
|
- `GITEA_BACKUP_INTERNAL_URL=http://${FEDORA_GITEA_IP}:3000`
|
|
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.
|
|
|
|
---
|
|
|
|
### DELETE /users/{username}/tokens/{token_name}
|
|
|
|
**Used in**: Phase 1, Phase 2 (idempotent re-run safety)
|
|
**Purpose**: Delete a stale API token before regenerating
|
|
**Auth**: Basic auth (`-u username:password`)
|
|
|
|
**Request**: No body
|
|
**Response**: 204 No Content
|
|
**404**: Token does not exist (safe to ignore)
|
|
|
|
**Notes**: Gitea returns 409 Conflict if you try to POST a token with a name that already exists. Deleting first makes token generation idempotent.
|
|
|
|
---
|
|
|
|
### 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 (migration polling), Phase 4 post-check, Phase 6 post-check (compare HEAD SHAs)
|
|
**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 (idempotency check), Phase 5 post-check, Phase 5 teardown, Phase 9 (idempotency check), Phase 9 post-check, Phase 9 teardown
|
|
**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"]
|
|
}
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
### GET /repos/{owner}/{repo}/actions/runners/registration-token
|
|
|
|
**Used in**: `manage_runner.sh` (repo-scoped runner registration)
|
|
**Purpose**: Get registration token scoped to a specific repo (vs admin-level global token)
|
|
|
|
**Request**: No body
|
|
|
|
**Response** (200):
|
|
```json
|
|
{
|
|
"token": "string"
|
|
}
|
|
```
|
|
|
|
**Notes**: Used when `repos` field in `runners.conf` is set to a specific repo name instead of `all`. Requires write access to the repo.
|
|
|
|
---
|
|
|
|
### 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
|
|
**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), Phase 9 teardown (clear 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
|
|
**Purpose**: Delete branch protection rule
|
|
|
|
**Response**: 204 No Content
|
|
|
|
---
|
|
|
|
### GET /settings/api
|
|
|
|
**Used in**: Phase 1 post-check, Phase 2 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 (mark as mirror + update description), Phase 8 teardown (restore settings)
|
|
**Purpose**: Update repository settings
|
|
|
|
**Request (mark as mirror)**:
|
|
```json
|
|
{
|
|
"description": "[MIRROR] Offsite backup — primary at https://git.domain.com/org/repo — was: original description",
|
|
"homepage": "https://git.domain.com/org/repo",
|
|
"has_wiki": false,
|
|
"has_projects": false
|
|
}
|
|
```
|
|
|
|
**Request (restore)**:
|
|
```json
|
|
{
|
|
"description": "original description",
|
|
"homepage": "original homepage",
|
|
"has_wiki": true,
|
|
"has_projects": true
|
|
}
|
|
```
|
|
|
|
**Response** (200): Updated repository object
|
|
|
|
### PUT /repos/{owner}/{repo}/actions/permissions
|
|
|
|
**Used in**: Phase 6 (disable Actions), Phase 6 teardown (re-enable)
|
|
**Purpose**: Enable or disable GitHub Actions on a repo
|
|
|
|
**Request**:
|
|
```json
|
|
{
|
|
"enabled": false
|
|
}
|
|
```
|
|
|
|
**Response** (204): No Content
|
|
|
|
### GET /repos/{owner}/{repo}/pages
|
|
|
|
**Used in**: Phase 8 (snapshot Pages state for teardown)
|
|
**Purpose**: Get GitHub Pages configuration
|
|
|
|
**Response** (200):
|
|
```json
|
|
{
|
|
"cname": "string",
|
|
"source": {
|
|
"branch": "main",
|
|
"path": "/"
|
|
}
|
|
}
|
|
```
|
|
|
|
**404**: Pages not enabled for this repo
|
|
|
|
### DELETE /repos/{owner}/{repo}/pages
|
|
|
|
**Used in**: Phase 8 (disable Pages on mirror)
|
|
**Purpose**: Disable GitHub Pages
|
|
|
|
**Response** (204): No Content
|
|
|
|
### PUT /repos/{owner}/{repo}/pages
|
|
|
|
**Used in**: Phase 8 teardown (restore Pages configuration)
|
|
**Purpose**: Update GitHub Pages configuration
|
|
|
|
**Request**:
|
|
```json
|
|
{
|
|
"cname": "string",
|
|
"source": {
|
|
"branch": "main",
|
|
"path": "/"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response** (204): No Content
|
|
|
|
### POST /repos/{owner}/{repo}/pages
|
|
|
|
**Used in**: Phase 8 teardown (re-enable Pages if it was deleted)
|
|
**Purpose**: Create/enable GitHub Pages configuration
|
|
|
|
**Request**:
|
|
```json
|
|
{
|
|
"source": {
|
|
"branch": "main",
|
|
"path": "/"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response** (201): Pages configuration object
|
|
|
|
### GET /repos/{owner}/{repo}/commits
|
|
|
|
**Used in**: Phase 6 post-check (compare Gitea and GitHub HEAD SHAs after mirror sync)
|
|
**Purpose**: Get recent commits (GitHub side)
|
|
|
|
**Query params**: `?per_page=1`
|
|
|
|
**Response** (200): Array of commit objects
|
|
|
|
**Notes**: The Gitea version of this endpoint is documented above. This GitHub-side usage verifies that push mirror sync propagated the latest commit.
|
|
|
|
---
|
|
|
|
## Error Responses
|
|
|
|
All Gitea and GitHub API endpoints return standard HTTP error codes. The migration
|
|
scripts check `http_code >= 400` in `_api_call()` (lib/common.sh) and treat any
|
|
4xx/5xx as a failure.
|
|
|
|
### Common Error Codes
|
|
|
|
| Code | Meaning | Typical Cause |
|
|
|------|---------|---------------|
|
|
| 400 | Bad Request | Malformed JSON, missing required fields, invalid field values |
|
|
| 401 | Unauthorized | Token expired, revoked, or missing |
|
|
| 403 | Forbidden | Token lacks required scope (e.g., admin endpoint with non-admin token) |
|
|
| 404 | Not Found | Resource doesn't exist — used for idempotency checks (`GET` before `POST`) |
|
|
| 409 | Conflict | Resource already exists (e.g., duplicate token name, duplicate org name) |
|
|
| 422 | Unprocessable Entity | Validation failed (e.g., password too short, invalid repo name) |
|
|
| 500 | Internal Server Error | Server-side failure — retry may work for transient issues |
|
|
|
|
### Error Response Body
|
|
|
|
Gitea errors return JSON with a `message` field:
|
|
```json
|
|
{
|
|
"message": "string describing the error",
|
|
"url": "https://gitea.docs/api/endpoint"
|
|
}
|
|
```
|
|
|
|
GitHub errors return JSON with `message` and optionally `errors`:
|
|
```json
|
|
{
|
|
"message": "Validation Failed",
|
|
"errors": [
|
|
{
|
|
"resource": "Repository",
|
|
"field": "name",
|
|
"code": "already_exists"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### How Scripts Handle Errors
|
|
|
|
- `_api_call()` logs the full response body on any 4xx/5xx and returns exit code 1
|
|
- Callers either `exit 1` (fatal) or continue with `|| true` (non-fatal, e.g., sync triggers)
|
|
- Idempotency checks use 404 as a signal to proceed with creation — this is expected, not an error
|
|
|
|
---
|
|
|
|
## Pagination
|
|
|
|
Gitea list endpoints return paginated results. The migration scripts do NOT
|
|
paginate because the data volumes are typically small (a handful of repos, runners, and mirrors).
|
|
|
|
### Gitea Pagination Headers
|
|
|
|
```
|
|
x-total-count: 42
|
|
link: <url?page=2&limit=50>; rel="next", <url?page=5&limit=50>; rel="last"
|
|
```
|
|
|
|
### Default Limits
|
|
|
|
| Endpoint | Default per-page | Max per-page |
|
|
|----------|:----------------:|:------------:|
|
|
| GET /admin/runners | 50 | 50 |
|
|
| GET /repos/{owner}/{repo}/push_mirrors | 50 | 50 |
|
|
| GET /repos/{owner}/{repo}/commits | 50 | 50 |
|
|
| GET /repos/{owner}/{repo}/branch_protections | all (no pagination) | — |
|
|
|
|
### When Pagination Would Matter
|
|
|
|
If any of these grow beyond 50 items, scripts that use these endpoints would
|
|
silently miss results. For typical migrations (under 50 repos/runners), this
|
|
is not a concern. If migrating more than 50 repos, add `?limit=N` or
|
|
implement `Link` header following.
|