---
name: github-api-push
description: Push files to GitHub via REST API when git is blocked (e.g. China servers)
category: github
---

Push files to GitHub when `git push` doesn't work (GitHub blocked, only api.github.com reachable).

## When to use
- Server in China, GitHub blocked but `api.github.com` responds (check with `curl -s -o /dev/null -w "%{http_code}" https://api.github.com`)
- Need to update existing files or create new ones in a repo

## How

### 1. Get current SHA (for existing files)
```python
import urllib.request, json
TOKEN = "ghp_xxx"
API = "https://api.github.com/repos/OWNER/REPO/contents"

req = urllib.request.Request(f"{API}/path/to/file")
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("Accept", "application/vnd.github+json")
req.add_header("User-Agent", "curl")
resp = urllib.request.urlopen(req, timeout=15)
sha = json.loads(resp.read()).get("sha")
```

### 2. Push file
```python
import base64
with open("local/path") as f:
    content = f.read()
b64 = base64.b64encode(content.encode()).decode()

body = {
    "message": "commit message",
    "content": b64,
    "branch": "main",
    "sha": sha  # omit for new files
}

req = urllib.request.Request(f"{API}/path/to/file", method="PUT")
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("Content-Type", "application/json")
req.add_header("Accept", "application/vnd.github+json")
req.add_header("User-Agent", "curl")
urllib.request.urlopen(req, json.dumps(body).encode(), timeout=15)
```

## Cross-Server File Transfer via GitHub

When two servers can't communicate directly (scp blocked, cloudflared tunnels hit bot protection) and file-sharing services are unreliable from China, use GitHub as an intermediary:

### Flow
1. **Create a temp repo** (if needed): `POST /user/repos` with `{"name": "temp-files", "auto_init": true}`
2. **Upload file** to the repo: `PUT /repos/{owner}/{repo}/contents/{path}` with base64 content
3. **Download on target server**: `curl -L -o file https://raw.githubusercontent.com/{owner}/{repo}/{branch}/{path}`

### Upload script (using requests in execute_code)
```python
import base64, requests
TOKEN = "ghp_xxx"
REPO = "owner/temp-files"

with open("file.tar.gz", "rb") as f:
    content = base64.b64encode(f.read()).decode()

resp = requests.put(
    f"https://api.github.com/repos/{REPO}/contents/file.tar.gz",
    headers={"Authorization": f"token {TOKEN}", "Accept": "application/vnd.github.v3+json"},
    json={"message": "Upload", "content": content, "branch": "main"}
)
# 201 = new file created, 200 = updated existing
print(resp.status_code, resp.json()["content"]["download_url"])
```

### Prerequisites
- `api.github.com` must be reachable (curl test: `curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 https://api.github.com`)
- Token with `repo` scope
- `raw.githubusercontent.com` reachable from the target server (usually is, even when api.github.com isn't)

### Comparison: GitHub vs file-sharing services
| Method | China reliability | File size limit | Notes |
|--------|:---:|---:|-------|
| **GitHub API upload** | ✅ High | 100 MB | api.github.com + raw.githubusercontent.com both reachable |
| transfer.sh | ❌ | ~10 GB | Connection refused from China |
| 0x0.st | ❌ | 512 MB | 405/blocked |
| file.io | ❌ | 2 GB | Cloudflare redirect |
| tmpfiles.org | ❌ | 100 MB | SSL handshake timeout |
| cloudflared tunnel w/ Access | ❌ | — | 403 for non-browser agents (curl rejected) |

### Pitfalls
- **Token truncation**: When writing `token = "ghp_fh...U1lE"` in execute_code, make sure the full token is used — truncated placeholder strings silently fail with 401
- **Repo must exist**: Upload to a non-existent repo returns 404. Create it first with `POST /user/repos`
- File paths with special chars like `(` `[` must be URL-encoded (e.g. `%5B` for `[`)
- Use `execute_code` with `urllib.request` instead of `terminal(curl)` for multi-file batches — faster and handles special chars
- 422 status means SHA missing or wrong — GET the file first to get current SHA
- 404 means new file — omit SHA from body
- **Repo path ≠ local path**: A repo cloned with `icoach/` subdirectory means paths are `icoach/src/lib/llm.ts`, not `src/lib/llm.ts`. Always check the repo structure with `GET /repos/{owner}/{repo}/contents/` before pushing.
- **Verify reachability first**: `curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 https://api.github.com` must return 200. If not, fall back to `scp` + local push.
- **Token in memory**: Save the GitHub token and repo name in memory so you don't need to ask every session.
