Auto-sync from website-creator
This commit is contained in:
198
skills/gitea-sync/SKILL.md
Normal file
198
skills/gitea-sync/SKILL.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# Gitea Sync Skill
|
||||
|
||||
**Skill Name:** `gitea-sync`
|
||||
**Category:** `quick`
|
||||
**Load Skills:** `[]` (standalone)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Purpose
|
||||
|
||||
Automatically sync repositories to Gitea (git.moreminimore.com):
|
||||
- Create new repositories
|
||||
- Update existing repositories
|
||||
- Push code automatically
|
||||
- Auto-detect new vs existing repos
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Prerequisites
|
||||
|
||||
### Gitea API Token
|
||||
|
||||
Get your API token from:
|
||||
`https://git.moreminimore.com/user/settings/applications`
|
||||
|
||||
1. Login to Gitea
|
||||
2. Go to Settings → Applications
|
||||
3. Generate new token (name it "opencode-skills")
|
||||
4. Copy the token
|
||||
5. Add to unified `.env` file
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
### Sync New Repository
|
||||
|
||||
```bash
|
||||
python3 scripts/sync.py \
|
||||
--repo my-website \
|
||||
--path ./my-website \
|
||||
--description "My PDPA-compliant website"
|
||||
```
|
||||
|
||||
### Sync Without Pushing
|
||||
|
||||
```bash
|
||||
python3 scripts/sync.py \
|
||||
--repo my-website \
|
||||
--path ./my-website \
|
||||
--no-push
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Required | Default | Description |
|
||||
|-----------|----------|---------|-------------|
|
||||
| `--repo` | ✅ | - | Repository name |
|
||||
| `--path` | ✅ | - | Path to code directory |
|
||||
| `--description` | ❌ | "" | Repository description |
|
||||
| `--no-push` | ❌ | false | Don't push code |
|
||||
| `--private` | ❌ | false | Make private (not implemented) |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Workflow
|
||||
|
||||
### Auto-Detection
|
||||
|
||||
The script automatically detects:
|
||||
- **New repository** → Creates with `auto_init`
|
||||
- **Existing repository** → Updates metadata
|
||||
|
||||
### Push Process
|
||||
|
||||
1. Initialize git (if not already)
|
||||
2. Add `.gitignore` (if not exists)
|
||||
3. Configure authentication (uses API token)
|
||||
4. Add all files
|
||||
5. Commit with message "Auto-sync from website-creator"
|
||||
6. Push to Gitea (force push for initial push)
|
||||
|
||||
---
|
||||
|
||||
## 📁 Files
|
||||
|
||||
```
|
||||
gitea-sync/
|
||||
├── SKILL.md
|
||||
└── scripts/
|
||||
├── sync.py # Main script
|
||||
├── .env.example # Configuration template
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Authentication
|
||||
|
||||
Uses Gitea API token for authentication:
|
||||
- Stored in unified `.env` file
|
||||
- Format: `Authorization: token <API_TOKEN>`
|
||||
- Token embedded in git URL for push operations
|
||||
|
||||
---
|
||||
|
||||
## ✅ Success Criteria
|
||||
|
||||
After sync:
|
||||
- ✅ Repository created/updated on Gitea
|
||||
- ✅ Code pushed to `main` branch
|
||||
- ✅ `.gitignore` created
|
||||
- ✅ Git remote configured
|
||||
- ✅ Repository URL returned
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Repository URL
|
||||
|
||||
Format:
|
||||
```
|
||||
https://git.moreminimore.com/<username>/<repo-name>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| 401 Unauthorized | Check API token in .env |
|
||||
| 409 Conflict | Repository already exists (normal) |
|
||||
| Push failed | Check git credentials, verify token |
|
||||
| Not a git repo | Script auto-initializes (shouldn't fail) |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Integration
|
||||
|
||||
Used by:
|
||||
- `website-creator` skill (auto-deploy workflow)
|
||||
- Manual sync (standalone usage)
|
||||
|
||||
---
|
||||
|
||||
## 📝 Example Output
|
||||
|
||||
```
|
||||
🔄 Gitea Sync
|
||||
==================================================
|
||||
Repository: my-website
|
||||
Path: ./my-website
|
||||
Description: My PDPA-compliant website
|
||||
==================================================
|
||||
|
||||
🔐 Authenticated as: kunthawatgreethong
|
||||
|
||||
📦 Creating repository: my-website
|
||||
✅ Repository created: my-website
|
||||
|
||||
🚀 Pushing code to Gitea
|
||||
→ Initializing git repository
|
||||
→ Adding remote: https://git.moreminimore.com/...
|
||||
→ Adding files
|
||||
→ Committing changes
|
||||
→ Pushing to Gitea
|
||||
✅ Code pushed successfully
|
||||
|
||||
🌐 Repository URL: https://git.moreminimore.com/kunthawatgreethong/my-website
|
||||
|
||||
==================================================
|
||||
✅ Sync complete!
|
||||
Repository: my-website
|
||||
URL: https://git.moreminimore.com/kunthawatgreethong/my-website
|
||||
Status: Created new repository
|
||||
==================================================
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 API Endpoints Used
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/api/v1/user` | GET | Verify authentication |
|
||||
| `/api/v1/repos/{user}/{repo}` | GET | Check if repo exists |
|
||||
| `/api/v1/user/repos` | POST | Create repository |
|
||||
| `/api/v1/repos/{user}/{repo}` | PATCH | Update repository |
|
||||
| Git push | POST | Push code (via git protocol) |
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
For issues with Gitea:
|
||||
- Check API token validity
|
||||
- Verify repository permissions
|
||||
- Review Gitea logs at: `https://git.moreminimore.com/explore`
|
||||
6
skills/gitea-sync/scripts/.env.example
Normal file
6
skills/gitea-sync/scripts/.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
# Gitea Configuration
|
||||
# Get API token from: https://git.moreminimore.com/user/settings/applications
|
||||
|
||||
GITEA_URL=https://git.moreminimore.com
|
||||
GITEA_API_TOKEN=your-api-token-here
|
||||
GITEA_USERNAME=your-username
|
||||
1
skills/gitea-sync/scripts/requirements.txt
Normal file
1
skills/gitea-sync/scripts/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
requests>=2.28.0
|
||||
333
skills/gitea-sync/scripts/sync.py
Normal file
333
skills/gitea-sync/scripts/sync.py
Normal file
@@ -0,0 +1,333 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Gitea Sync - Automatically sync repositories to Gitea
|
||||
|
||||
Creates/updates repositories and pushes code automatically.
|
||||
Auto-detects new vs existing repositories.
|
||||
|
||||
Usage:
|
||||
python3 sync.py --repo my-website --path ./my-website
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import requests
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_env():
|
||||
"""Load environment from .env file."""
|
||||
env_path = Path(__file__).parent / ".env"
|
||||
if env_path.exists():
|
||||
for line in env_path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
os.environ.setdefault(k.strip(), v.strip().strip("\"'"))
|
||||
|
||||
|
||||
load_env()
|
||||
|
||||
GITEA_URL = os.environ.get("GITEA_URL", "https://git.moreminimore.com")
|
||||
GITEA_API_TOKEN = os.environ.get("GITEA_API_TOKEN")
|
||||
GITEA_USERNAME = os.environ.get("GITEA_USERNAME")
|
||||
|
||||
|
||||
def check_auth():
|
||||
"""Verify Gitea authentication."""
|
||||
if not GITEA_API_TOKEN:
|
||||
print("Error: GITEA_API_TOKEN not set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
response = requests.get(
|
||||
f"{GITEA_URL}/api/v1/user",
|
||||
headers={"Authorization": f"token {GITEA_API_TOKEN}"}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"Error: Gitea authentication failed ({response.status_code})", file=sys.stderr)
|
||||
print(f"Check your API token at: {GITEA_URL}/user/settings/applications", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
user = response.json()
|
||||
return user.get("login", GITEA_USERNAME)
|
||||
|
||||
|
||||
def repo_exists(username, repo_name):
|
||||
"""Check if repository exists on Gitea."""
|
||||
response = requests.get(
|
||||
f"{GITEA_URL}/api/v1/repos/{username}/{repo_name}",
|
||||
headers={"Authorization": f"token {GITEA_API_TOKEN}"}
|
||||
)
|
||||
return response.status_code == 200
|
||||
|
||||
|
||||
def create_repo(repo_name, description="", private=False):
|
||||
"""Create new repository on Gitea."""
|
||||
print(f"📦 Creating repository: {repo_name}")
|
||||
|
||||
data = {
|
||||
"name": repo_name,
|
||||
"description": description,
|
||||
"private": private,
|
||||
"auto_init": True,
|
||||
"readme": "Default",
|
||||
"default_branch": "main"
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{GITEA_URL}/api/v1/user/repos",
|
||||
headers={"Authorization": f"token {GITEA_API_TOKEN}"},
|
||||
json=data
|
||||
)
|
||||
|
||||
if response.status_code == 201:
|
||||
print(f"✅ Repository created: {repo_name}")
|
||||
return response.json()
|
||||
elif response.status_code == 409:
|
||||
print(f"⚠️ Repository already exists: {repo_name}")
|
||||
return None
|
||||
else:
|
||||
print(f"❌ Failed to create repository: {response.text}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def update_repo(repo_name, description=""):
|
||||
"""Update existing repository."""
|
||||
print(f"🔄 Updating repository: {repo_name}")
|
||||
|
||||
data = {
|
||||
"description": description,
|
||||
"website": "",
|
||||
"has_issues": True,
|
||||
"has_pull_requests": True,
|
||||
"has_wiki": False
|
||||
}
|
||||
|
||||
response = requests.patch(
|
||||
f"{GITEA_URL}/api/v1/repos/{GITEA_USERNAME}/{repo_name}",
|
||||
headers={"Authorization": f"token {GITEA_API_TOKEN}"},
|
||||
json=data
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
print(f"✅ Repository updated: {repo_name}")
|
||||
return response.json()
|
||||
else:
|
||||
print(f"⚠️ Could not update repository: {response.text}")
|
||||
return None
|
||||
|
||||
|
||||
def get_repo_url(username, repo_name):
|
||||
"""Get HTTPS URL for repository."""
|
||||
return f"{GITEA_URL}/{username}/{repo_name}.git"
|
||||
|
||||
|
||||
def is_git_repo(path):
|
||||
"""Check if directory is a git repository."""
|
||||
git_dir = Path(path) / ".git"
|
||||
return git_dir.exists()
|
||||
|
||||
|
||||
def push_code(repo_path, git_url, branch="main"):
|
||||
"""Push code to Gitea repository."""
|
||||
repo_path = Path(repo_path)
|
||||
|
||||
if not repo_path.exists():
|
||||
print(f"Error: Path does not exist: {repo_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"🚀 Pushing code to Gitea...")
|
||||
|
||||
# Initialize git if needed
|
||||
if not is_git_repo(repo_path):
|
||||
print(" → Initializing git repository")
|
||||
subprocess.run(["git", "init"], cwd=repo_path, check=True, capture_output=True)
|
||||
|
||||
# Configure git to use token for authentication
|
||||
# This avoids interactive password prompts
|
||||
subprocess.run(
|
||||
["git", "config", "credential.helper", "store"],
|
||||
cwd=repo_path,
|
||||
check=True,
|
||||
capture_output=True
|
||||
)
|
||||
|
||||
# Add .gitignore if not exists
|
||||
gitignore = repo_path / ".gitignore"
|
||||
if not gitignore.exists():
|
||||
with open(gitignore, "w") as f:
|
||||
f.write("""node_modules
|
||||
dist
|
||||
.env
|
||||
.astro
|
||||
*.db
|
||||
*.log
|
||||
.DS_Store
|
||||
""")
|
||||
|
||||
# Add remote if not exists
|
||||
result = subprocess.run(
|
||||
["git", "remote", "get-url", "origin"],
|
||||
cwd=repo_path,
|
||||
capture_output=True
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f" → Adding remote: {git_url}")
|
||||
# Use token in URL for authentication
|
||||
auth_url = git_url.replace(
|
||||
f"{GITEA_URL}/",
|
||||
f"{GITEA_URL}/{GITEA_API_TOKEN}:@"
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "remote", "add", "origin", auth_url],
|
||||
cwd=repo_path,
|
||||
check=True,
|
||||
capture_output=True
|
||||
)
|
||||
else:
|
||||
# Update existing remote with auth
|
||||
auth_url = git_url.replace(
|
||||
f"{GITEA_URL}/",
|
||||
f"{GITEA_URL}/{GITEA_API_TOKEN}:@"
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "remote", "set-url", "origin", auth_url],
|
||||
cwd=repo_path,
|
||||
check=True,
|
||||
capture_output=True
|
||||
)
|
||||
|
||||
# Add all files
|
||||
print(" → Adding files")
|
||||
subprocess.run(["git", "add", "."], cwd=repo_path, check=True, capture_output=True)
|
||||
|
||||
# Check if there are changes to commit
|
||||
result = subprocess.run(
|
||||
["git", "status", "--porcelain"],
|
||||
cwd=repo_path,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.stdout.strip():
|
||||
# Commit changes
|
||||
print(" → Committing changes")
|
||||
subprocess.run(
|
||||
["git", "commit", "-m", "Auto-sync from website-creator"],
|
||||
cwd=repo_path,
|
||||
check=True,
|
||||
capture_output=True
|
||||
)
|
||||
|
||||
# Set main as default branch
|
||||
subprocess.run(
|
||||
["git", "branch", "-M", branch],
|
||||
cwd=repo_path,
|
||||
check=True,
|
||||
capture_output=True
|
||||
)
|
||||
|
||||
# Push with force to handle initial push
|
||||
print(" → Pushing to Gitea")
|
||||
result = subprocess.run(
|
||||
["git", "push", "-u", "-f", "origin", branch],
|
||||
cwd=repo_path,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"✅ Code pushed successfully")
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ Push output: {result.stderr}")
|
||||
# Try without -f if it fails
|
||||
subprocess.run(
|
||||
["git", "push", "-u", "origin", branch],
|
||||
cwd=repo_path,
|
||||
capture_output=True
|
||||
)
|
||||
print(f"✅ Code pushed (without force)")
|
||||
return True
|
||||
else:
|
||||
print(f"ℹ️ No changes to push")
|
||||
return True
|
||||
|
||||
|
||||
def sync_repo(repo_name, repo_path, description="", auto_push=True):
|
||||
"""Complete sync workflow."""
|
||||
|
||||
# Step 1: Check auth
|
||||
username = check_auth()
|
||||
print(f"🔐 Authenticated as: {username}")
|
||||
print("")
|
||||
|
||||
# Step 2: Check if repo exists
|
||||
exists = repo_exists(username, repo_name)
|
||||
|
||||
if exists:
|
||||
update_repo(repo_name, description)
|
||||
else:
|
||||
create_repo(repo_name, description)
|
||||
|
||||
print("")
|
||||
|
||||
# Step 3: Push code
|
||||
if auto_push:
|
||||
git_url = get_repo_url(username, repo_name)
|
||||
push_code(repo_path, git_url)
|
||||
print("")
|
||||
print(f"🌐 Repository URL: {git_url.replace('.git', '')}")
|
||||
|
||||
return {
|
||||
"username": username,
|
||||
"repo_name": repo_name,
|
||||
"git_url": get_repo_url(username, repo_name),
|
||||
"created": not exists
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Sync repository to Gitea")
|
||||
parser.add_argument("--repo", required=True, help="Repository name")
|
||||
parser.add_argument("--path", required=True, help="Path to repository")
|
||||
parser.add_argument("--description", default="", help="Repository description")
|
||||
parser.add_argument("--no-push", action="store_true", help="Don't push code")
|
||||
parser.add_argument("--private", action="store_true", help="Make repository private")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("🔄 Gitea Sync")
|
||||
print("=" * 50)
|
||||
print(f"Repository: {args.repo}")
|
||||
print(f"Path: {args.path}")
|
||||
print(f"Description: {args.description or '(none)'}")
|
||||
print("=" * 50)
|
||||
print("")
|
||||
|
||||
result = sync_repo(
|
||||
args.repo,
|
||||
args.path,
|
||||
args.description,
|
||||
auto_push=not args.no_push
|
||||
)
|
||||
|
||||
print("")
|
||||
print("=" * 50)
|
||||
print("✅ Sync complete!")
|
||||
print(f"Repository: {result['repo_name']}")
|
||||
print(f"URL: {result['git_url'].replace('.git', '')}")
|
||||
if result['created']:
|
||||
print("Status: Created new repository")
|
||||
else:
|
||||
print("Status: Updated existing repository")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user