Initial commit: Hermes Agent Skills collection
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
# GitLab → Gitea Migration Technique
|
||||
|
||||
Session: 2026-06-29, migrating 12 repos from GitLab CE 15.8.1 (Docker on 10.0.30.100) to Gitea 1.25.5 (CT on 10.0.30.105).
|
||||
|
||||
## Scenario
|
||||
|
||||
GitLab and Gitea coexist temporarily. DNS (`git.familie-schoen.com`) has been repointed from GitLab to Gitea. GitLab is still running in a Docker container but no longer reachable via its former domain. Goal: move all repos to Gitea, then decommission GitLab.
|
||||
|
||||
## Key Challenges & Solutions
|
||||
|
||||
### Challenge 1: Gitea Migrate API Fails When DNS Repointed
|
||||
|
||||
The Gitea `/api/v1/repos/migrate` endpoint accepts a `clone_addr` URL. If the GitLab domain now resolves to Gitea (because DNS was repointed), Gitea tries to clone from itself → `Authentication failed`.
|
||||
|
||||
**Solution**: Use the GitLab container's internal Docker IP instead of the domain.
|
||||
|
||||
```bash
|
||||
# Get GitLab container internal IP
|
||||
docker inspect development-gitlab-1 --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
|
||||
# → 172.18.0.5 (and possibly 172.24.0.2 on a second network)
|
||||
```
|
||||
|
||||
### Challenge 2: Gitea Migrate API Creates "Stuck" Repos
|
||||
|
||||
Even when the migrate API call fails (authentication error), Gitea creates an empty repo in a permanent "Migrating from..." state. These repos show a migration status page and cannot receive pushes normally.
|
||||
|
||||
**Solution**: Delete the stuck repos via API, recreate as empty repos, then push content via `git push --mirror`.
|
||||
|
||||
```bash
|
||||
# Delete stuck repos
|
||||
curl -sk -X DELETE -H "Authorization: token $GITEA_TOKEN" \
|
||||
https://git.familie-schoen.com/api/v1/repos/$OWNER/$REPO
|
||||
|
||||
# Recreate as empty repos (no auto_init)
|
||||
curl -sk -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"$REPO","private":true,"auto_init":false}' \
|
||||
https://git.familie-schoen.com/api/v1/orgs/$OWNER/repos
|
||||
```
|
||||
|
||||
### Challenge 3: GitLab API Needs Token (No Anonymous Access)
|
||||
|
||||
With all repos private, the GitLab API returns empty results without auth.
|
||||
|
||||
**Solution**: Create a temporary Personal Access Token via the Rails console inside the GitLab container:
|
||||
|
||||
```bash
|
||||
docker exec development-gitlab-1 gitlab-rails runner -e production \
|
||||
"u=User.where(admin:true).first; t=u.personal_access_tokens.create(scopes:[:api], name:'migration', expires_at:7.days.from_now); puts t.token"
|
||||
# → glpat-xxxxxxxxxxxx
|
||||
```
|
||||
|
||||
**Revoke after migration**:
|
||||
```bash
|
||||
docker exec development-gitlab-1 gitlab-rails runner -e production \
|
||||
"u=User.where(admin:true).first; u.personal_access_tokens.where(name:'migration').each{|t| t.revoke!}; puts 'revoked'"
|
||||
```
|
||||
|
||||
### Challenge 4: Orgs/Users Must Exist in Gitea First
|
||||
|
||||
GitLab repos are namespaced under users/groups (e.g., `d.schoen/`, `codecamp-n/`). Gitea needs corresponding orgs before repos can be pushed.
|
||||
|
||||
**Solution**: Create orgs via API before pushing:
|
||||
```bash
|
||||
curl -sk -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"d.schoen","visibility":"private"}' \
|
||||
https://git.familie-schoen.com/api/v1/orgs
|
||||
```
|
||||
|
||||
## Working Migration Pattern (Recommended)
|
||||
|
||||
### Prerequisites
|
||||
- SSH access to the host running GitLab container (for `git clone --mirror`)
|
||||
- Gitea API token (admin)
|
||||
- GitLab API token (or Rails-console-generated PAT)
|
||||
|
||||
### Step 1: Create orgs in Gitea
|
||||
List all GitLab namespace prefixes, create as Gitea orgs if they don't exist.
|
||||
|
||||
### Step 2: Create empty repos in Gitea
|
||||
```bash
|
||||
for repo in "${repos[@]}"; do
|
||||
owner="${repo%%/*}"
|
||||
name="${repo##*/}"
|
||||
curl -sk -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"$name\",\"private\":true,\"auto_init\":false}" \
|
||||
https://git.familie-schoen.com/api/v1/orgs/$owner/repos
|
||||
done
|
||||
```
|
||||
|
||||
### Step 3: Mirror-push from GitLab to Gitea
|
||||
|
||||
Run from the host that has the GitLab container (for internal IP access):
|
||||
|
||||
```bash
|
||||
GITLAB_INTERNAL_IP="172.18.0.5"
|
||||
GITLAB_TOKEN="glpat-xxxx"
|
||||
GITEA_TOKEN="xxxx"
|
||||
GITEA_INTERNAL="10.0.30.105:3000"
|
||||
|
||||
for repo_path in "d.schoen/terraform-proxmox-k8s" "codecamp-n/corporate_website" ...; do
|
||||
name="${repo_path##*/}"
|
||||
|
||||
# Clone from GitLab internal IP
|
||||
git clone --mirror \
|
||||
"http://oauth2:${GITLAB_TOKEN}@${GITLAB_INTERNAL_IP}/${repo_path}.git" \
|
||||
"/tmp/migrate_${name}"
|
||||
|
||||
# Push to Gitea internal IP
|
||||
cd "/tmp/migrate_${name}"
|
||||
git push --mirror \
|
||||
"http://dominik:${GITEA_TOKEN}@${GITEA_INTERNAL}/${repo_path}.git"
|
||||
|
||||
# Cleanup
|
||||
cd /tmp && rm -rf "migrate_${name}"
|
||||
done
|
||||
```
|
||||
|
||||
**Key URLs**:
|
||||
- GitLab clone: `http://oauth2:TOKEN@INTERNAL_IP/namespace/repo.git`
|
||||
- Gitea push: `http://USER:TOKEN@GITEA_IP:PORT/namespace/repo.git`
|
||||
|
||||
### Step 4: Verify
|
||||
```bash
|
||||
# Check each repo has branches and non-zero size
|
||||
curl -sk -H "Authorization: token $GITEA_TOKEN" \
|
||||
https://git.familie-schoen.com/api/v1/repos/$repo_path/branches
|
||||
```
|
||||
|
||||
## Pitfalls
|
||||
|
||||
1. **Don't use Gitea migrate API when DNS is repointed** — Gitea will clone from itself. Use `git push --mirror` from a host with internal access to GitLab instead.
|
||||
2. **Failed migrations leave stuck repos** — always delete + recreate empty before retrying.
|
||||
3. **Empty GitLab repos** — some repos may have no commits. `git clone --mirror` prints "warning: You appear to have cloned an empty repository." These will remain empty in Gitea too — this is expected, not an error.
|
||||
4. **GitLab token expiry** — Rails-console tokens expire. Set a reasonable `expires_at` and revoke after migration.
|
||||
5. **Docker network IPs** — a container on multiple networks returns multiple IPs separated without newlines. Use the first one or inspect carefully.
|
||||
6. **Gitea auth for push** — use `http://USERNAME:TOKEN@host/path.git` format (username + token as password).
|
||||
7. **SSH heredoc through sshpass** — when running multi-line scripts via `sshpass ssh ... 'bash -s' << 'EOF'`, use a unique delimiter (e.g., `HERMES_EOF`) to avoid conflicts with embedded heredocs.
|
||||
Reference in New Issue
Block a user