mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-06 19:46:44 +03:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b41438f686 | ||
|
|
df1738a44d |
133
.env.example
133
.env.example
@@ -30,41 +30,136 @@ DOCKER_IMAGE=arunavo4/gitea-mirror
|
|||||||
DOCKER_TAG=latest
|
DOCKER_TAG=latest
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# MIRROR CONFIGURATION (Optional)
|
# GITHUB CONFIGURATION
|
||||||
# Can also be configured via web UI
|
# All settings can also be configured via web UI
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|
||||||
# GitHub Configuration
|
# Basic GitHub Settings
|
||||||
# GITHUB_USERNAME=your-github-username
|
# GITHUB_USERNAME=your-github-username
|
||||||
# GITHUB_TOKEN=your-github-personal-access-token
|
# GITHUB_TOKEN=your-github-personal-access-token
|
||||||
# SKIP_FORKS=false
|
# GITHUB_TYPE=personal # Options: personal, organization
|
||||||
|
|
||||||
|
# Repository Selection
|
||||||
# PRIVATE_REPOSITORIES=false
|
# PRIVATE_REPOSITORIES=false
|
||||||
# MIRROR_ISSUES=false
|
# PUBLIC_REPOSITORIES=true
|
||||||
# MIRROR_WIKI=false
|
# INCLUDE_ARCHIVED=false
|
||||||
|
# SKIP_FORKS=false
|
||||||
# MIRROR_STARRED=false
|
# MIRROR_STARRED=false
|
||||||
|
# STARRED_REPOS_ORG=starred # Organization name for starred repos
|
||||||
|
|
||||||
|
# Organization Settings
|
||||||
# MIRROR_ORGANIZATIONS=false
|
# MIRROR_ORGANIZATIONS=false
|
||||||
# PRESERVE_ORG_STRUCTURE=false
|
# PRESERVE_ORG_STRUCTURE=false
|
||||||
# ONLY_MIRROR_ORGS=false
|
# ONLY_MIRROR_ORGS=false
|
||||||
# SKIP_STARRED_ISSUES=false
|
|
||||||
|
|
||||||
# Gitea Configuration
|
# Mirror Strategy
|
||||||
|
# MIRROR_STRATEGY=preserve # Options: preserve, single-org, flat-user, mixed
|
||||||
|
|
||||||
|
# Advanced GitHub Settings
|
||||||
|
# SKIP_STARRED_ISSUES=false # Enable lightweight mode for starred repos
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# GITEA CONFIGURATION
|
||||||
|
# All settings can also be configured via web UI
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# Basic Gitea Settings
|
||||||
# GITEA_URL=http://gitea:3000
|
# GITEA_URL=http://gitea:3000
|
||||||
# GITEA_TOKEN=your-local-gitea-token
|
# GITEA_TOKEN=your-local-gitea-token
|
||||||
# GITEA_USERNAME=your-local-gitea-username
|
# GITEA_USERNAME=your-local-gitea-username
|
||||||
# GITEA_ORGANIZATION=github-mirrors
|
# GITEA_ORGANIZATION=github-mirrors # Default organization for single-org strategy
|
||||||
# GITEA_ORG_VISIBILITY=public
|
|
||||||
# DELAY=3600
|
# Repository Settings
|
||||||
|
# GITEA_ORG_VISIBILITY=public # Options: public, private, limited, default
|
||||||
|
# GITEA_MIRROR_INTERVAL=8h # Mirror sync interval (e.g., 30m, 1h, 8h, 24h)
|
||||||
|
# GITEA_LFS=false # Enable LFS support
|
||||||
|
# GITEA_CREATE_ORG=true # Auto-create organizations
|
||||||
|
# GITEA_PRESERVE_VISIBILITY=false # Preserve GitHub repo visibility in Gitea
|
||||||
|
|
||||||
|
# Template Settings (for using repository templates)
|
||||||
|
# GITEA_TEMPLATE_OWNER=template-owner
|
||||||
|
# GITEA_TEMPLATE_REPO=template-repo
|
||||||
|
|
||||||
|
# Topic Settings
|
||||||
|
# GITEA_ADD_TOPICS=true # Add topics to repositories
|
||||||
|
# GITEA_TOPIC_PREFIX=gh- # Prefix for topics
|
||||||
|
|
||||||
|
# Fork Handling
|
||||||
|
# GITEA_FORK_STRATEGY=reference # Options: skip, reference, full-copy
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# OPTIONAL FEATURES
|
# MIRROR OPTIONS
|
||||||
|
# Control what gets mirrored from GitHub
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|
||||||
# Database Cleanup Configuration
|
# Release and Metadata
|
||||||
|
# MIRROR_RELEASES=false # Mirror GitHub releases
|
||||||
|
# MIRROR_WIKI=false # Mirror wiki content
|
||||||
|
|
||||||
|
# Issue Tracking (requires MIRROR_METADATA=true)
|
||||||
|
# MIRROR_METADATA=false # Master toggle for metadata mirroring
|
||||||
|
# MIRROR_ISSUES=false # Mirror issues
|
||||||
|
# MIRROR_PULL_REQUESTS=false # Mirror pull requests
|
||||||
|
# MIRROR_LABELS=false # Mirror labels
|
||||||
|
# MIRROR_MILESTONES=false # Mirror milestones
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# AUTOMATION CONFIGURATION
|
||||||
|
# Schedule automatic mirroring
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# Basic Schedule Settings
|
||||||
|
# SCHEDULE_ENABLED=false
|
||||||
|
# SCHEDULE_INTERVAL=3600 # Interval in seconds or cron expression (e.g., "0 2 * * *")
|
||||||
|
# DELAY=3600 # Legacy: same as SCHEDULE_INTERVAL, kept for backward compatibility
|
||||||
|
|
||||||
|
# Execution Settings
|
||||||
|
# SCHEDULE_CONCURRENT=false # Allow concurrent mirror operations
|
||||||
|
# SCHEDULE_BATCH_SIZE=10 # Number of repos to process in parallel
|
||||||
|
# SCHEDULE_PAUSE_BETWEEN_BATCHES=5000 # Pause between batches (ms)
|
||||||
|
|
||||||
|
# Retry Configuration
|
||||||
|
# SCHEDULE_RETRY_ATTEMPTS=3
|
||||||
|
# SCHEDULE_RETRY_DELAY=60000 # Delay between retries (ms)
|
||||||
|
# SCHEDULE_TIMEOUT=3600000 # Max time for a mirror operation (ms)
|
||||||
|
# SCHEDULE_AUTO_RETRY=true
|
||||||
|
|
||||||
|
# Update Detection
|
||||||
|
# SCHEDULE_ONLY_MIRROR_UPDATED=false # Only mirror repos with updates
|
||||||
|
# SCHEDULE_UPDATE_INTERVAL=86400000 # Check for updates interval (ms)
|
||||||
|
# SCHEDULE_SKIP_RECENTLY_MIRRORED=true
|
||||||
|
# SCHEDULE_RECENT_THRESHOLD=3600000 # Skip if mirrored within this time (ms)
|
||||||
|
|
||||||
|
# Maintenance
|
||||||
|
# SCHEDULE_CLEANUP_BEFORE_MIRROR=false # Run cleanup before mirroring
|
||||||
|
|
||||||
|
# Notifications
|
||||||
|
# SCHEDULE_NOTIFY_ON_FAILURE=true
|
||||||
|
# SCHEDULE_NOTIFY_ON_SUCCESS=false
|
||||||
|
# SCHEDULE_LOG_LEVEL=info # Options: error, warn, info, debug
|
||||||
|
# SCHEDULE_TIMEZONE=UTC
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# DATABASE CLEANUP CONFIGURATION
|
||||||
|
# Automatic cleanup of old events and data
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# Basic Cleanup Settings
|
||||||
# CLEANUP_ENABLED=false
|
# CLEANUP_ENABLED=false
|
||||||
# CLEANUP_RETENTION_DAYS=7
|
# CLEANUP_RETENTION_DAYS=7 # Days to keep events
|
||||||
|
|
||||||
# TLS/SSL Configuration
|
# Repository Cleanup
|
||||||
# GITEA_SKIP_TLS_VERIFY=false # WARNING: Only use for testing
|
# CLEANUP_DELETE_FROM_GITEA=false # Delete repos from Gitea
|
||||||
|
# CLEANUP_DELETE_IF_NOT_IN_GITHUB=true # Delete if not in GitHub
|
||||||
|
# CLEANUP_ORPHANED_REPO_ACTION=archive # Options: skip, archive, delete
|
||||||
|
# CLEANUP_DRY_RUN=true # Test mode without actual deletion
|
||||||
|
|
||||||
|
# Protected Repositories (comma-separated)
|
||||||
|
# CLEANUP_PROTECTED_REPOS=important-repo,critical-project
|
||||||
|
|
||||||
|
# Cleanup Execution
|
||||||
|
# CLEANUP_BATCH_SIZE=10
|
||||||
|
# CLEANUP_PAUSE_BETWEEN_DELETES=2000 # Pause between deletions (ms)
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# AUTHENTICATION CONFIGURATION
|
# AUTHENTICATION CONFIGURATION
|
||||||
@@ -79,3 +174,9 @@ DOCKER_TAG=latest
|
|||||||
# HEADER_AUTH_AUTO_PROVISION=false
|
# HEADER_AUTH_AUTO_PROVISION=false
|
||||||
# HEADER_AUTH_ALLOWED_DOMAINS=example.com,company.org
|
# HEADER_AUTH_ALLOWED_DOMAINS=example.com,company.org
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# OPTIONAL FEATURES
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# TLS/SSL Configuration
|
||||||
|
# GITEA_SKIP_TLS_VERIFY=false # WARNING: Only use for testing
|
||||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [3.2.4] - 2025-08-09
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed critical authentication issue causing "user does not exist [uid: 0]" errors during metadata mirroring (Issue #68)
|
||||||
|
- Fixed inconsistent token handling across Gitea API calls
|
||||||
|
- Fixed metadata mirroring functions attempting to operate on non-existent repositories
|
||||||
|
- Fixed organization creation failing silently without proper error messages
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Pre-flight authentication validation for all Gitea operations
|
||||||
|
- Repository existence verification before metadata mirroring
|
||||||
|
- Graceful fallback to user account when organization creation fails due to permissions
|
||||||
|
- Authentication validation utilities for debugging configuration issues
|
||||||
|
- Diagnostic test scripts for troubleshooting authentication problems
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Enhanced error messages with specific guidance for authentication failures
|
||||||
|
- Better identification and logging of permission-related errors
|
||||||
|
- More robust organization creation with retry logic and better error handling
|
||||||
|
- Consistent token decryption across all API operations
|
||||||
|
- Clearer error reporting for metadata mirroring failures
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Fixed potential exposure of encrypted tokens in API calls
|
||||||
|
- Improved token handling to ensure proper decryption before use
|
||||||
|
|
||||||
## [3.2.0] - 2025-07-31
|
## [3.2.0] - 2025-07-31
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ All other settings are configured through the web interface after starting.
|
|||||||
|
|
||||||
Supports extensive environment variables for automated deployment. See the full [docker-compose.yml](docker-compose.yml) for all available options including GitHub tokens, Gitea URLs, mirror settings, and more.
|
Supports extensive environment variables for automated deployment. See the full [docker-compose.yml](docker-compose.yml) for all available options including GitHub tokens, Gitea URLs, mirror settings, and more.
|
||||||
|
|
||||||
|
📚 **For a complete list of all supported environment variables, see the [Environment Variables Documentation](docs/ENVIRONMENT_VARIABLES.md).**
|
||||||
|
|
||||||
### LXC Container (Proxmox)
|
### LXC Container (Proxmox)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
environment:
|
environment:
|
||||||
|
# For a complete list of all supported environment variables, see:
|
||||||
|
# docs/ENVIRONMENT_VARIABLES.md or .env.example
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- DATABASE_URL=file:data/gitea-mirror.db
|
- DATABASE_URL=file:data/gitea-mirror.db
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ services:
|
|||||||
# Option 2: Mount system CA bundle (if your CA is already in system store)
|
# Option 2: Mount system CA bundle (if your CA is already in system store)
|
||||||
# - /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro
|
# - /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro
|
||||||
environment:
|
environment:
|
||||||
|
# For a complete list of all supported environment variables, see:
|
||||||
|
# docs/ENVIRONMENT_VARIABLES.md or .env.example
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- DATABASE_URL=file:data/gitea-mirror.db
|
- DATABASE_URL=file:data/gitea-mirror.db
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
|
|||||||
299
docs/ENVIRONMENT_VARIABLES.md
Normal file
299
docs/ENVIRONMENT_VARIABLES.md
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
# Environment Variables Documentation
|
||||||
|
|
||||||
|
This document provides a comprehensive list of all environment variables supported by Gitea Mirror. These can be used to configure the application via Docker or other deployment methods.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Core Configuration](#core-configuration)
|
||||||
|
- [GitHub Configuration](#github-configuration)
|
||||||
|
- [Gitea Configuration](#gitea-configuration)
|
||||||
|
- [Mirror Options](#mirror-options)
|
||||||
|
- [Automation Configuration](#automation-configuration)
|
||||||
|
- [Database Cleanup Configuration](#database-cleanup-configuration)
|
||||||
|
- [Authentication Configuration](#authentication-configuration)
|
||||||
|
- [Docker Configuration](#docker-configuration)
|
||||||
|
|
||||||
|
## Core Configuration
|
||||||
|
|
||||||
|
Essential application settings required for running Gitea Mirror.
|
||||||
|
|
||||||
|
| Variable | Description | Default | Required |
|
||||||
|
|----------|-------------|---------|----------|
|
||||||
|
| `NODE_ENV` | Application environment | `production` | No |
|
||||||
|
| `HOST` | Server host binding | `0.0.0.0` | No |
|
||||||
|
| `PORT` | Server port | `4321` | No |
|
||||||
|
| `DATABASE_URL` | Database connection URL | `sqlite://data/gitea-mirror.db` | No |
|
||||||
|
| `BETTER_AUTH_SECRET` | Secret key for session signing (generate with: `openssl rand -base64 32`) | - | Yes |
|
||||||
|
| `BETTER_AUTH_URL` | Base URL for authentication | `http://localhost:4321` | No |
|
||||||
|
| `ENCRYPTION_SECRET` | Optional encryption key for tokens (generate with: `openssl rand -base64 48`) | - | No |
|
||||||
|
|
||||||
|
## GitHub Configuration
|
||||||
|
|
||||||
|
Settings for connecting to and configuring GitHub repository sources.
|
||||||
|
|
||||||
|
### Basic Settings
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `GITHUB_USERNAME` | Your GitHub username | - | - |
|
||||||
|
| `GITHUB_TOKEN` | GitHub personal access token (requires repo and admin:org scopes) | - | - |
|
||||||
|
| `GITHUB_TYPE` | GitHub account type | `personal` | `personal`, `organization` |
|
||||||
|
|
||||||
|
### Repository Selection
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `PRIVATE_REPOSITORIES` | Include private repositories | `false` | `true`, `false` |
|
||||||
|
| `PUBLIC_REPOSITORIES` | Include public repositories | `true` | `true`, `false` |
|
||||||
|
| `INCLUDE_ARCHIVED` | Include archived repositories | `false` | `true`, `false` |
|
||||||
|
| `SKIP_FORKS` | Skip forked repositories | `false` | `true`, `false` |
|
||||||
|
| `MIRROR_STARRED` | Mirror starred repositories | `false` | `true`, `false` |
|
||||||
|
| `STARRED_REPOS_ORG` | Organization name for starred repos | `starred` | Any string |
|
||||||
|
|
||||||
|
### Organization Settings
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `MIRROR_ORGANIZATIONS` | Mirror organization repositories | `false` | `true`, `false` |
|
||||||
|
| `PRESERVE_ORG_STRUCTURE` | Preserve GitHub organization structure in Gitea | `false` | `true`, `false` |
|
||||||
|
| `ONLY_MIRROR_ORGS` | Only mirror organization repos (skip personal) | `false` | `true`, `false` |
|
||||||
|
| `MIRROR_STRATEGY` | Repository organization strategy | `preserve` | `preserve`, `single-org`, `flat-user`, `mixed` |
|
||||||
|
|
||||||
|
### Advanced Settings
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `SKIP_STARRED_ISSUES` | Enable lightweight mode for starred repos (skip issues) | `false` | `true`, `false` |
|
||||||
|
|
||||||
|
## Gitea Configuration
|
||||||
|
|
||||||
|
Settings for the destination Gitea instance.
|
||||||
|
|
||||||
|
### Connection Settings
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `GITEA_URL` | Gitea instance URL | - | Valid URL |
|
||||||
|
| `GITEA_TOKEN` | Gitea access token | - | - |
|
||||||
|
| `GITEA_USERNAME` | Gitea username | - | - |
|
||||||
|
| `GITEA_ORGANIZATION` | Default organization for single-org strategy | `github-mirrors` | Any string |
|
||||||
|
|
||||||
|
### Repository Settings
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `GITEA_ORG_VISIBILITY` | Default organization visibility | `public` | `public`, `private`, `limited`, `default` |
|
||||||
|
| `GITEA_MIRROR_INTERVAL` | Mirror sync interval | `8h` | Duration string (e.g., `30m`, `1h`, `8h`, `24h`) |
|
||||||
|
| `GITEA_LFS` | Enable LFS support | `false` | `true`, `false` |
|
||||||
|
| `GITEA_CREATE_ORG` | Auto-create organizations | `true` | `true`, `false` |
|
||||||
|
| `GITEA_PRESERVE_VISIBILITY` | Preserve GitHub repo visibility in Gitea | `false` | `true`, `false` |
|
||||||
|
|
||||||
|
### Template Settings
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `GITEA_TEMPLATE_OWNER` | Template repository owner | - | Any string |
|
||||||
|
| `GITEA_TEMPLATE_REPO` | Template repository name | - | Any string |
|
||||||
|
|
||||||
|
### Topic Settings
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `GITEA_ADD_TOPICS` | Add topics to repositories | `true` | `true`, `false` |
|
||||||
|
| `GITEA_TOPIC_PREFIX` | Prefix for repository topics | - | Any string |
|
||||||
|
|
||||||
|
### Fork Handling
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `GITEA_FORK_STRATEGY` | How to handle forked repositories | `reference` | `skip`, `reference`, `full-copy` |
|
||||||
|
|
||||||
|
### Additional Settings
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `GITEA_SKIP_TLS_VERIFY` | Skip TLS certificate verification (WARNING: insecure) | `false` | `true`, `false` |
|
||||||
|
|
||||||
|
## Mirror Options
|
||||||
|
|
||||||
|
Control what content gets mirrored from GitHub to Gitea.
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `MIRROR_RELEASES` | Mirror GitHub releases | `false` | `true`, `false` |
|
||||||
|
| `MIRROR_WIKI` | Mirror wiki content | `false` | `true`, `false` |
|
||||||
|
| `MIRROR_METADATA` | Master toggle for metadata mirroring | `false` | `true`, `false` |
|
||||||
|
| `MIRROR_ISSUES` | Mirror issues (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
||||||
|
| `MIRROR_PULL_REQUESTS` | Mirror pull requests (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
||||||
|
| `MIRROR_LABELS` | Mirror labels (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
||||||
|
| `MIRROR_MILESTONES` | Mirror milestones (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
||||||
|
|
||||||
|
## Automation Configuration
|
||||||
|
|
||||||
|
Configure automatic scheduled mirroring.
|
||||||
|
|
||||||
|
### Basic Schedule Settings
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `SCHEDULE_ENABLED` | Enable automatic mirroring | `false` | `true`, `false` |
|
||||||
|
| `SCHEDULE_INTERVAL` | Interval in seconds or cron expression | `3600` | Number or cron string (e.g., `"0 2 * * *"`) |
|
||||||
|
| `DELAY` | Legacy: same as SCHEDULE_INTERVAL | `3600` | Number (seconds) |
|
||||||
|
|
||||||
|
### Execution Settings
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `SCHEDULE_CONCURRENT` | Allow concurrent mirror operations | `false` | `true`, `false` |
|
||||||
|
| `SCHEDULE_BATCH_SIZE` | Number of repos to process in parallel | `10` | Number |
|
||||||
|
| `SCHEDULE_PAUSE_BETWEEN_BATCHES` | Pause between batches (milliseconds) | `5000` | Number |
|
||||||
|
|
||||||
|
### Retry Configuration
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `SCHEDULE_RETRY_ATTEMPTS` | Number of retry attempts | `3` | Number |
|
||||||
|
| `SCHEDULE_RETRY_DELAY` | Delay between retries (milliseconds) | `60000` | Number |
|
||||||
|
| `SCHEDULE_TIMEOUT` | Max time for a mirror operation (milliseconds) | `3600000` | Number |
|
||||||
|
| `SCHEDULE_AUTO_RETRY` | Automatically retry failed operations | `true` | `true`, `false` |
|
||||||
|
|
||||||
|
### Update Detection
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `SCHEDULE_ONLY_MIRROR_UPDATED` | Only mirror repos with updates | `false` | `true`, `false` |
|
||||||
|
| `SCHEDULE_UPDATE_INTERVAL` | Check for updates interval (milliseconds) | `86400000` | Number |
|
||||||
|
| `SCHEDULE_SKIP_RECENTLY_MIRRORED` | Skip recently mirrored repos | `true` | `true`, `false` |
|
||||||
|
| `SCHEDULE_RECENT_THRESHOLD` | Skip if mirrored within this time (milliseconds) | `3600000` | Number |
|
||||||
|
|
||||||
|
### Maintenance & Notifications
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `SCHEDULE_CLEANUP_BEFORE_MIRROR` | Run cleanup before mirroring | `false` | `true`, `false` |
|
||||||
|
| `SCHEDULE_NOTIFY_ON_FAILURE` | Send notifications on failure | `true` | `true`, `false` |
|
||||||
|
| `SCHEDULE_NOTIFY_ON_SUCCESS` | Send notifications on success | `false` | `true`, `false` |
|
||||||
|
| `SCHEDULE_LOG_LEVEL` | Logging level | `info` | `error`, `warn`, `info`, `debug` |
|
||||||
|
| `SCHEDULE_TIMEZONE` | Timezone for scheduling | `UTC` | Valid timezone string |
|
||||||
|
|
||||||
|
## Database Cleanup Configuration
|
||||||
|
|
||||||
|
Configure automatic cleanup of old events and data.
|
||||||
|
|
||||||
|
### Basic Settings
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `CLEANUP_ENABLED` | Enable automatic cleanup | `false` | `true`, `false` |
|
||||||
|
| `CLEANUP_RETENTION_DAYS` | Days to keep events | `7` | Number |
|
||||||
|
|
||||||
|
### Repository Cleanup
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `CLEANUP_DELETE_FROM_GITEA` | Delete repositories from Gitea | `false` | `true`, `false` |
|
||||||
|
| `CLEANUP_DELETE_IF_NOT_IN_GITHUB` | Delete repos not found in GitHub | `true` | `true`, `false` |
|
||||||
|
| `CLEANUP_ORPHANED_REPO_ACTION` | Action for orphaned repositories | `archive` | `skip`, `archive`, `delete` |
|
||||||
|
| `CLEANUP_DRY_RUN` | Test mode without actual deletion | `true` | `true`, `false` |
|
||||||
|
| `CLEANUP_PROTECTED_REPOS` | Comma-separated list of protected repository names | - | Comma-separated strings |
|
||||||
|
|
||||||
|
### Execution Settings
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `CLEANUP_BATCH_SIZE` | Number of items to process per batch | `10` | Number |
|
||||||
|
| `CLEANUP_PAUSE_BETWEEN_DELETES` | Pause between deletions (milliseconds) | `2000` | Number |
|
||||||
|
|
||||||
|
## Authentication Configuration
|
||||||
|
|
||||||
|
Configure authentication methods and SSO.
|
||||||
|
|
||||||
|
### Header Authentication (Reverse Proxy SSO)
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `HEADER_AUTH_ENABLED` | Enable header-based authentication | `false` | `true`, `false` |
|
||||||
|
| `HEADER_AUTH_USER_HEADER` | Header containing username | `X-Authentik-Username` | Header name |
|
||||||
|
| `HEADER_AUTH_EMAIL_HEADER` | Header containing email | `X-Authentik-Email` | Header name |
|
||||||
|
| `HEADER_AUTH_NAME_HEADER` | Header containing display name | `X-Authentik-Name` | Header name |
|
||||||
|
| `HEADER_AUTH_AUTO_PROVISION` | Auto-create users from headers | `false` | `true`, `false` |
|
||||||
|
| `HEADER_AUTH_ALLOWED_DOMAINS` | Comma-separated list of allowed email domains | - | Comma-separated domains |
|
||||||
|
|
||||||
|
## Docker Configuration
|
||||||
|
|
||||||
|
Settings specific to Docker deployments.
|
||||||
|
|
||||||
|
| Variable | Description | Default | Options |
|
||||||
|
|----------|-------------|---------|---------|
|
||||||
|
| `DOCKER_REGISTRY` | Docker registry URL | `ghcr.io` | Registry URL |
|
||||||
|
| `DOCKER_IMAGE` | Docker image name | `arunavo4/gitea-mirror` | Image name |
|
||||||
|
| `DOCKER_TAG` | Docker image tag | `latest` | Tag name |
|
||||||
|
|
||||||
|
## Example Docker Compose Configuration
|
||||||
|
|
||||||
|
Here's an example of how to use these environment variables in a `docker-compose.yml` file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
gitea-mirror:
|
||||||
|
image: ghcr.io/raylabshq/gitea-mirror:latest
|
||||||
|
container_name: gitea-mirror
|
||||||
|
environment:
|
||||||
|
# Core Configuration
|
||||||
|
- NODE_ENV=production
|
||||||
|
- DATABASE_URL=file:data/gitea-mirror.db
|
||||||
|
- BETTER_AUTH_SECRET=your-secure-secret-here
|
||||||
|
- BETTER_AUTH_URL=https://your-domain.com
|
||||||
|
|
||||||
|
# GitHub Configuration
|
||||||
|
- GITHUB_USERNAME=your-username
|
||||||
|
- GITHUB_TOKEN=ghp_your_token_here
|
||||||
|
- PRIVATE_REPOSITORIES=true
|
||||||
|
- MIRROR_STARRED=true
|
||||||
|
- SKIP_FORKS=false
|
||||||
|
|
||||||
|
# Gitea Configuration
|
||||||
|
- GITEA_URL=http://gitea:3000
|
||||||
|
- GITEA_USERNAME=admin
|
||||||
|
- GITEA_TOKEN=your-gitea-token
|
||||||
|
- GITEA_ORGANIZATION=github-mirrors
|
||||||
|
- GITEA_ORG_VISIBILITY=public
|
||||||
|
|
||||||
|
# Mirror Options
|
||||||
|
- MIRROR_RELEASES=true
|
||||||
|
- MIRROR_WIKI=true
|
||||||
|
- MIRROR_METADATA=true
|
||||||
|
- MIRROR_ISSUES=true
|
||||||
|
- MIRROR_PULL_REQUESTS=true
|
||||||
|
|
||||||
|
# Automation
|
||||||
|
- SCHEDULE_ENABLED=true
|
||||||
|
- SCHEDULE_INTERVAL=3600
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
- CLEANUP_ENABLED=true
|
||||||
|
- CLEANUP_RETENTION_DAYS=30
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
ports:
|
||||||
|
- "4321:4321"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
1. **First Run**: Environment variables are loaded when the container starts. The configuration is applied after the first user account is created.
|
||||||
|
|
||||||
|
2. **UI Priority**: Manual changes made through the web UI will be preserved. Environment variables only set values for empty fields.
|
||||||
|
|
||||||
|
3. **Token Security**: All tokens are encrypted before being stored in the database.
|
||||||
|
|
||||||
|
4. **Backward Compatibility**: The `DELAY` variable is maintained for backward compatibility but `SCHEDULE_INTERVAL` is preferred.
|
||||||
|
|
||||||
|
5. **Required Scopes**: The GitHub token requires the following scopes:
|
||||||
|
- `repo` (full control of private repositories)
|
||||||
|
- `admin:org` (read organization data)
|
||||||
|
- Additional scopes may be required for specific features
|
||||||
|
|
||||||
|
For more examples and detailed configuration, see the `.env.example` file in the repository.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "gitea-mirror",
|
"name": "gitea-mirror",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "3.2.2",
|
"version": "3.2.4",
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.2.9"
|
"bun": ">=1.2.9"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,20 +12,34 @@ interface EnvConfig {
|
|||||||
github: {
|
github: {
|
||||||
username?: string;
|
username?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
|
type?: 'personal' | 'organization';
|
||||||
privateRepositories?: boolean;
|
privateRepositories?: boolean;
|
||||||
|
publicRepositories?: boolean;
|
||||||
mirrorStarred?: boolean;
|
mirrorStarred?: boolean;
|
||||||
skipForks?: boolean;
|
skipForks?: boolean;
|
||||||
|
includeArchived?: boolean;
|
||||||
mirrorOrganizations?: boolean;
|
mirrorOrganizations?: boolean;
|
||||||
preserveOrgStructure?: boolean;
|
preserveOrgStructure?: boolean;
|
||||||
onlyMirrorOrgs?: boolean;
|
onlyMirrorOrgs?: boolean;
|
||||||
skipStarredIssues?: boolean;
|
skipStarredIssues?: boolean;
|
||||||
|
starredReposOrg?: string;
|
||||||
|
mirrorStrategy?: 'preserve' | 'single-org' | 'flat-user' | 'mixed';
|
||||||
};
|
};
|
||||||
gitea: {
|
gitea: {
|
||||||
url?: string;
|
url?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
organization?: string;
|
organization?: string;
|
||||||
visibility?: 'public' | 'private' | 'limited';
|
visibility?: 'public' | 'private' | 'limited' | 'default';
|
||||||
|
mirrorInterval?: string;
|
||||||
|
lfs?: boolean;
|
||||||
|
createOrg?: boolean;
|
||||||
|
templateOwner?: string;
|
||||||
|
templateRepo?: string;
|
||||||
|
addTopics?: boolean;
|
||||||
|
topicPrefix?: string;
|
||||||
|
preserveVisibility?: boolean;
|
||||||
|
forkStrategy?: 'skip' | 'reference' | 'full-copy';
|
||||||
};
|
};
|
||||||
mirror: {
|
mirror: {
|
||||||
mirrorIssues?: boolean;
|
mirrorIssues?: boolean;
|
||||||
@@ -34,14 +48,38 @@ interface EnvConfig {
|
|||||||
mirrorPullRequests?: boolean;
|
mirrorPullRequests?: boolean;
|
||||||
mirrorLabels?: boolean;
|
mirrorLabels?: boolean;
|
||||||
mirrorMilestones?: boolean;
|
mirrorMilestones?: boolean;
|
||||||
|
mirrorMetadata?: boolean;
|
||||||
};
|
};
|
||||||
schedule: {
|
schedule: {
|
||||||
delay?: number;
|
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
interval?: string;
|
||||||
|
concurrent?: boolean;
|
||||||
|
batchSize?: number;
|
||||||
|
pauseBetweenBatches?: number;
|
||||||
|
retryAttempts?: number;
|
||||||
|
retryDelay?: number;
|
||||||
|
timeout?: number;
|
||||||
|
autoRetry?: boolean;
|
||||||
|
cleanupBeforeMirror?: boolean;
|
||||||
|
notifyOnFailure?: boolean;
|
||||||
|
notifyOnSuccess?: boolean;
|
||||||
|
logLevel?: 'error' | 'warn' | 'info' | 'debug';
|
||||||
|
timezone?: string;
|
||||||
|
onlyMirrorUpdated?: boolean;
|
||||||
|
updateInterval?: number;
|
||||||
|
skipRecentlyMirrored?: boolean;
|
||||||
|
recentThreshold?: number;
|
||||||
};
|
};
|
||||||
cleanup: {
|
cleanup: {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
retentionDays?: number;
|
retentionDays?: number;
|
||||||
|
deleteFromGitea?: boolean;
|
||||||
|
deleteIfNotInGitHub?: boolean;
|
||||||
|
protectedRepos?: string[];
|
||||||
|
dryRun?: boolean;
|
||||||
|
orphanedRepoAction?: 'skip' | 'archive' | 'delete';
|
||||||
|
batchSize?: number;
|
||||||
|
pauseBetweenDeletes?: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,24 +87,43 @@ interface EnvConfig {
|
|||||||
* Parse environment variables into configuration object
|
* Parse environment variables into configuration object
|
||||||
*/
|
*/
|
||||||
function parseEnvConfig(): EnvConfig {
|
function parseEnvConfig(): EnvConfig {
|
||||||
|
// Parse protected repos from comma-separated string
|
||||||
|
const protectedRepos = process.env.CLEANUP_PROTECTED_REPOS
|
||||||
|
? process.env.CLEANUP_PROTECTED_REPOS.split(',').map(r => r.trim()).filter(Boolean)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
github: {
|
github: {
|
||||||
username: process.env.GITHUB_USERNAME,
|
username: process.env.GITHUB_USERNAME,
|
||||||
token: process.env.GITHUB_TOKEN,
|
token: process.env.GITHUB_TOKEN,
|
||||||
|
type: process.env.GITHUB_TYPE as 'personal' | 'organization',
|
||||||
privateRepositories: process.env.PRIVATE_REPOSITORIES === 'true',
|
privateRepositories: process.env.PRIVATE_REPOSITORIES === 'true',
|
||||||
|
publicRepositories: process.env.PUBLIC_REPOSITORIES === 'true',
|
||||||
mirrorStarred: process.env.MIRROR_STARRED === 'true',
|
mirrorStarred: process.env.MIRROR_STARRED === 'true',
|
||||||
skipForks: process.env.SKIP_FORKS === 'true',
|
skipForks: process.env.SKIP_FORKS === 'true',
|
||||||
|
includeArchived: process.env.INCLUDE_ARCHIVED === 'true',
|
||||||
mirrorOrganizations: process.env.MIRROR_ORGANIZATIONS === 'true',
|
mirrorOrganizations: process.env.MIRROR_ORGANIZATIONS === 'true',
|
||||||
preserveOrgStructure: process.env.PRESERVE_ORG_STRUCTURE === 'true',
|
preserveOrgStructure: process.env.PRESERVE_ORG_STRUCTURE === 'true',
|
||||||
onlyMirrorOrgs: process.env.ONLY_MIRROR_ORGS === 'true',
|
onlyMirrorOrgs: process.env.ONLY_MIRROR_ORGS === 'true',
|
||||||
skipStarredIssues: process.env.SKIP_STARRED_ISSUES === 'true',
|
skipStarredIssues: process.env.SKIP_STARRED_ISSUES === 'true',
|
||||||
|
starredReposOrg: process.env.STARRED_REPOS_ORG,
|
||||||
|
mirrorStrategy: process.env.MIRROR_STRATEGY as 'preserve' | 'single-org' | 'flat-user' | 'mixed',
|
||||||
},
|
},
|
||||||
gitea: {
|
gitea: {
|
||||||
url: process.env.GITEA_URL,
|
url: process.env.GITEA_URL,
|
||||||
username: process.env.GITEA_USERNAME,
|
username: process.env.GITEA_USERNAME,
|
||||||
token: process.env.GITEA_TOKEN,
|
token: process.env.GITEA_TOKEN,
|
||||||
organization: process.env.GITEA_ORGANIZATION,
|
organization: process.env.GITEA_ORGANIZATION,
|
||||||
visibility: process.env.GITEA_ORG_VISIBILITY as 'public' | 'private' | 'limited',
|
visibility: process.env.GITEA_ORG_VISIBILITY as 'public' | 'private' | 'limited' | 'default',
|
||||||
|
mirrorInterval: process.env.GITEA_MIRROR_INTERVAL,
|
||||||
|
lfs: process.env.GITEA_LFS === 'true',
|
||||||
|
createOrg: process.env.GITEA_CREATE_ORG === 'true',
|
||||||
|
templateOwner: process.env.GITEA_TEMPLATE_OWNER,
|
||||||
|
templateRepo: process.env.GITEA_TEMPLATE_REPO,
|
||||||
|
addTopics: process.env.GITEA_ADD_TOPICS === 'true',
|
||||||
|
topicPrefix: process.env.GITEA_TOPIC_PREFIX,
|
||||||
|
preserveVisibility: process.env.GITEA_PRESERVE_VISIBILITY === 'true',
|
||||||
|
forkStrategy: process.env.GITEA_FORK_STRATEGY as 'skip' | 'reference' | 'full-copy',
|
||||||
},
|
},
|
||||||
mirror: {
|
mirror: {
|
||||||
mirrorIssues: process.env.MIRROR_ISSUES === 'true',
|
mirrorIssues: process.env.MIRROR_ISSUES === 'true',
|
||||||
@@ -75,14 +132,38 @@ function parseEnvConfig(): EnvConfig {
|
|||||||
mirrorPullRequests: process.env.MIRROR_PULL_REQUESTS === 'true',
|
mirrorPullRequests: process.env.MIRROR_PULL_REQUESTS === 'true',
|
||||||
mirrorLabels: process.env.MIRROR_LABELS === 'true',
|
mirrorLabels: process.env.MIRROR_LABELS === 'true',
|
||||||
mirrorMilestones: process.env.MIRROR_MILESTONES === 'true',
|
mirrorMilestones: process.env.MIRROR_MILESTONES === 'true',
|
||||||
|
mirrorMetadata: process.env.MIRROR_METADATA === 'true',
|
||||||
},
|
},
|
||||||
schedule: {
|
schedule: {
|
||||||
delay: process.env.DELAY ? parseInt(process.env.DELAY, 10) : undefined,
|
|
||||||
enabled: process.env.SCHEDULE_ENABLED === 'true',
|
enabled: process.env.SCHEDULE_ENABLED === 'true',
|
||||||
|
interval: process.env.SCHEDULE_INTERVAL || process.env.DELAY, // Support both old DELAY and new SCHEDULE_INTERVAL
|
||||||
|
concurrent: process.env.SCHEDULE_CONCURRENT === 'true',
|
||||||
|
batchSize: process.env.SCHEDULE_BATCH_SIZE ? parseInt(process.env.SCHEDULE_BATCH_SIZE, 10) : undefined,
|
||||||
|
pauseBetweenBatches: process.env.SCHEDULE_PAUSE_BETWEEN_BATCHES ? parseInt(process.env.SCHEDULE_PAUSE_BETWEEN_BATCHES, 10) : undefined,
|
||||||
|
retryAttempts: process.env.SCHEDULE_RETRY_ATTEMPTS ? parseInt(process.env.SCHEDULE_RETRY_ATTEMPTS, 10) : undefined,
|
||||||
|
retryDelay: process.env.SCHEDULE_RETRY_DELAY ? parseInt(process.env.SCHEDULE_RETRY_DELAY, 10) : undefined,
|
||||||
|
timeout: process.env.SCHEDULE_TIMEOUT ? parseInt(process.env.SCHEDULE_TIMEOUT, 10) : undefined,
|
||||||
|
autoRetry: process.env.SCHEDULE_AUTO_RETRY === 'true',
|
||||||
|
cleanupBeforeMirror: process.env.SCHEDULE_CLEANUP_BEFORE_MIRROR === 'true',
|
||||||
|
notifyOnFailure: process.env.SCHEDULE_NOTIFY_ON_FAILURE === 'true',
|
||||||
|
notifyOnSuccess: process.env.SCHEDULE_NOTIFY_ON_SUCCESS === 'true',
|
||||||
|
logLevel: process.env.SCHEDULE_LOG_LEVEL as 'error' | 'warn' | 'info' | 'debug',
|
||||||
|
timezone: process.env.SCHEDULE_TIMEZONE,
|
||||||
|
onlyMirrorUpdated: process.env.SCHEDULE_ONLY_MIRROR_UPDATED === 'true',
|
||||||
|
updateInterval: process.env.SCHEDULE_UPDATE_INTERVAL ? parseInt(process.env.SCHEDULE_UPDATE_INTERVAL, 10) : undefined,
|
||||||
|
skipRecentlyMirrored: process.env.SCHEDULE_SKIP_RECENTLY_MIRRORED === 'true',
|
||||||
|
recentThreshold: process.env.SCHEDULE_RECENT_THRESHOLD ? parseInt(process.env.SCHEDULE_RECENT_THRESHOLD, 10) : undefined,
|
||||||
},
|
},
|
||||||
cleanup: {
|
cleanup: {
|
||||||
enabled: process.env.CLEANUP_ENABLED === 'true',
|
enabled: process.env.CLEANUP_ENABLED === 'true',
|
||||||
retentionDays: process.env.CLEANUP_RETENTION_DAYS ? parseInt(process.env.CLEANUP_RETENTION_DAYS, 10) : undefined,
|
retentionDays: process.env.CLEANUP_RETENTION_DAYS ? parseInt(process.env.CLEANUP_RETENTION_DAYS, 10) : undefined,
|
||||||
|
deleteFromGitea: process.env.CLEANUP_DELETE_FROM_GITEA === 'true',
|
||||||
|
deleteIfNotInGitHub: process.env.CLEANUP_DELETE_IF_NOT_IN_GITHUB === 'true',
|
||||||
|
protectedRepos,
|
||||||
|
dryRun: process.env.CLEANUP_DRY_RUN === 'true',
|
||||||
|
orphanedRepoAction: process.env.CLEANUP_ORPHANED_REPO_ACTION as 'skip' | 'archive' | 'delete',
|
||||||
|
batchSize: process.env.CLEANUP_BATCH_SIZE ? parseInt(process.env.CLEANUP_BATCH_SIZE, 10) : undefined,
|
||||||
|
pauseBetweenDeletes: process.env.CLEANUP_PAUSE_BETWEEN_DELETES ? parseInt(process.env.CLEANUP_PAUSE_BETWEEN_DELETES, 10) : undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -138,9 +219,11 @@ export async function initializeConfigFromEnv(): Promise<void> {
|
|||||||
.where(eq(configs.userId, userId))
|
.where(eq(configs.userId, userId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
// Determine mirror strategy based on environment variables
|
// Determine mirror strategy based on environment variables or use explicit value
|
||||||
let mirrorStrategy: 'preserve' | 'single-org' | 'flat-user' | 'mixed' = 'preserve';
|
let mirrorStrategy: 'preserve' | 'single-org' | 'flat-user' | 'mixed' = 'preserve';
|
||||||
if (envConfig.github.preserveOrgStructure === false && envConfig.gitea.organization) {
|
if (envConfig.github.mirrorStrategy) {
|
||||||
|
mirrorStrategy = envConfig.github.mirrorStrategy;
|
||||||
|
} else if (envConfig.github.preserveOrgStructure === false && envConfig.gitea.organization) {
|
||||||
mirrorStrategy = 'single-org';
|
mirrorStrategy = 'single-org';
|
||||||
} else if (envConfig.github.preserveOrgStructure === true) {
|
} else if (envConfig.github.preserveOrgStructure === true) {
|
||||||
mirrorStrategy = 'preserve';
|
mirrorStrategy = 'preserve';
|
||||||
@@ -149,17 +232,17 @@ export async function initializeConfigFromEnv(): Promise<void> {
|
|||||||
// Build GitHub config
|
// Build GitHub config
|
||||||
const githubConfig = {
|
const githubConfig = {
|
||||||
owner: envConfig.github.username || existingConfig?.[0]?.githubConfig?.owner || '',
|
owner: envConfig.github.username || existingConfig?.[0]?.githubConfig?.owner || '',
|
||||||
type: 'personal' as const,
|
type: envConfig.github.type || existingConfig?.[0]?.githubConfig?.type || 'personal',
|
||||||
token: envConfig.github.token ? encrypt(envConfig.github.token) : existingConfig?.[0]?.githubConfig?.token || '',
|
token: envConfig.github.token ? encrypt(envConfig.github.token) : existingConfig?.[0]?.githubConfig?.token || '',
|
||||||
includeStarred: envConfig.github.mirrorStarred ?? existingConfig?.[0]?.githubConfig?.includeStarred ?? false,
|
includeStarred: envConfig.github.mirrorStarred ?? existingConfig?.[0]?.githubConfig?.includeStarred ?? false,
|
||||||
includeForks: !(envConfig.github.skipForks ?? false),
|
includeForks: !(envConfig.github.skipForks ?? false),
|
||||||
includeArchived: existingConfig?.[0]?.githubConfig?.includeArchived ?? false,
|
includeArchived: envConfig.github.includeArchived ?? existingConfig?.[0]?.githubConfig?.includeArchived ?? false,
|
||||||
includePrivate: envConfig.github.privateRepositories ?? existingConfig?.[0]?.githubConfig?.includePrivate ?? false,
|
includePrivate: envConfig.github.privateRepositories ?? existingConfig?.[0]?.githubConfig?.includePrivate ?? false,
|
||||||
includePublic: existingConfig?.[0]?.githubConfig?.includePublic ?? true,
|
includePublic: envConfig.github.publicRepositories ?? existingConfig?.[0]?.githubConfig?.includePublic ?? true,
|
||||||
includeOrganizations: envConfig.github.mirrorOrganizations ? [] : (existingConfig?.[0]?.githubConfig?.includeOrganizations ?? []),
|
includeOrganizations: envConfig.github.mirrorOrganizations ? [] : (existingConfig?.[0]?.githubConfig?.includeOrganizations ?? []),
|
||||||
starredReposOrg: 'starred',
|
starredReposOrg: envConfig.github.starredReposOrg || existingConfig?.[0]?.githubConfig?.starredReposOrg || 'starred',
|
||||||
mirrorStrategy,
|
mirrorStrategy,
|
||||||
defaultOrg: envConfig.gitea.organization || 'github-mirrors',
|
defaultOrg: envConfig.gitea.organization || existingConfig?.[0]?.githubConfig?.defaultOrg || 'github-mirrors',
|
||||||
skipStarredIssues: envConfig.github.skipStarredIssues ?? existingConfig?.[0]?.githubConfig?.skipStarredIssues ?? false,
|
skipStarredIssues: envConfig.github.skipStarredIssues ?? existingConfig?.[0]?.githubConfig?.skipStarredIssues ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -168,42 +251,47 @@ export async function initializeConfigFromEnv(): Promise<void> {
|
|||||||
url: envConfig.gitea.url || existingConfig?.[0]?.giteaConfig?.url || '',
|
url: envConfig.gitea.url || existingConfig?.[0]?.giteaConfig?.url || '',
|
||||||
token: envConfig.gitea.token ? encrypt(envConfig.gitea.token) : existingConfig?.[0]?.giteaConfig?.token || '',
|
token: envConfig.gitea.token ? encrypt(envConfig.gitea.token) : existingConfig?.[0]?.giteaConfig?.token || '',
|
||||||
defaultOwner: envConfig.gitea.username || existingConfig?.[0]?.giteaConfig?.defaultOwner || '',
|
defaultOwner: envConfig.gitea.username || existingConfig?.[0]?.giteaConfig?.defaultOwner || '',
|
||||||
mirrorInterval: existingConfig?.[0]?.giteaConfig?.mirrorInterval || '8h',
|
mirrorInterval: envConfig.gitea.mirrorInterval || existingConfig?.[0]?.giteaConfig?.mirrorInterval || '8h',
|
||||||
lfs: existingConfig?.[0]?.giteaConfig?.lfs ?? false,
|
lfs: envConfig.gitea.lfs ?? existingConfig?.[0]?.giteaConfig?.lfs ?? false,
|
||||||
wiki: envConfig.mirror.mirrorWiki ?? existingConfig?.[0]?.giteaConfig?.wiki ?? false,
|
wiki: envConfig.mirror.mirrorWiki ?? existingConfig?.[0]?.giteaConfig?.wiki ?? false,
|
||||||
visibility: envConfig.gitea.visibility || existingConfig?.[0]?.giteaConfig?.visibility || 'public',
|
visibility: envConfig.gitea.visibility || existingConfig?.[0]?.giteaConfig?.visibility || 'public',
|
||||||
createOrg: true,
|
createOrg: envConfig.gitea.createOrg ?? existingConfig?.[0]?.giteaConfig?.createOrg ?? true,
|
||||||
addTopics: existingConfig?.[0]?.giteaConfig?.addTopics ?? true,
|
templateOwner: envConfig.gitea.templateOwner || existingConfig?.[0]?.giteaConfig?.templateOwner || undefined,
|
||||||
preserveVisibility: existingConfig?.[0]?.giteaConfig?.preserveVisibility ?? false,
|
templateRepo: envConfig.gitea.templateRepo || existingConfig?.[0]?.giteaConfig?.templateRepo || undefined,
|
||||||
forkStrategy: existingConfig?.[0]?.giteaConfig?.forkStrategy || 'reference',
|
addTopics: envConfig.gitea.addTopics ?? existingConfig?.[0]?.giteaConfig?.addTopics ?? true,
|
||||||
|
topicPrefix: envConfig.gitea.topicPrefix || existingConfig?.[0]?.giteaConfig?.topicPrefix || undefined,
|
||||||
|
preserveVisibility: envConfig.gitea.preserveVisibility ?? existingConfig?.[0]?.giteaConfig?.preserveVisibility ?? false,
|
||||||
|
forkStrategy: envConfig.gitea.forkStrategy || existingConfig?.[0]?.giteaConfig?.forkStrategy || 'reference',
|
||||||
|
// Mirror metadata options
|
||||||
mirrorReleases: envConfig.mirror.mirrorReleases ?? existingConfig?.[0]?.giteaConfig?.mirrorReleases ?? false,
|
mirrorReleases: envConfig.mirror.mirrorReleases ?? existingConfig?.[0]?.giteaConfig?.mirrorReleases ?? false,
|
||||||
mirrorMetadata: (envConfig.mirror.mirrorIssues || envConfig.mirror.mirrorPullRequests || envConfig.mirror.mirrorLabels || envConfig.mirror.mirrorMilestones) ?? existingConfig?.[0]?.giteaConfig?.mirrorMetadata ?? false,
|
mirrorMetadata: envConfig.mirror.mirrorMetadata ?? (envConfig.mirror.mirrorIssues || envConfig.mirror.mirrorPullRequests || envConfig.mirror.mirrorLabels || envConfig.mirror.mirrorMilestones) ?? existingConfig?.[0]?.giteaConfig?.mirrorMetadata ?? false,
|
||||||
mirrorIssues: envConfig.mirror.mirrorIssues ?? existingConfig?.[0]?.giteaConfig?.mirrorIssues ?? false,
|
mirrorIssues: envConfig.mirror.mirrorIssues ?? existingConfig?.[0]?.giteaConfig?.mirrorIssues ?? false,
|
||||||
mirrorPullRequests: envConfig.mirror.mirrorPullRequests ?? existingConfig?.[0]?.giteaConfig?.mirrorPullRequests ?? false,
|
mirrorPullRequests: envConfig.mirror.mirrorPullRequests ?? existingConfig?.[0]?.giteaConfig?.mirrorPullRequests ?? false,
|
||||||
mirrorLabels: envConfig.mirror.mirrorLabels ?? existingConfig?.[0]?.giteaConfig?.mirrorLabels ?? false,
|
mirrorLabels: envConfig.mirror.mirrorLabels ?? existingConfig?.[0]?.giteaConfig?.mirrorLabels ?? false,
|
||||||
mirrorMilestones: envConfig.mirror.mirrorMilestones ?? existingConfig?.[0]?.giteaConfig?.mirrorMilestones ?? false,
|
mirrorMilestones: envConfig.mirror.mirrorMilestones ?? existingConfig?.[0]?.giteaConfig?.mirrorMilestones ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build schedule config
|
// Build schedule config with support for interval as string or number
|
||||||
|
const scheduleInterval = envConfig.schedule.interval || (existingConfig?.[0]?.scheduleConfig?.interval ?? '3600');
|
||||||
const scheduleConfig = {
|
const scheduleConfig = {
|
||||||
enabled: envConfig.schedule.enabled ?? existingConfig?.[0]?.scheduleConfig?.enabled ?? false,
|
enabled: envConfig.schedule.enabled ?? existingConfig?.[0]?.scheduleConfig?.enabled ?? false,
|
||||||
interval: envConfig.schedule.delay ? String(envConfig.schedule.delay) : existingConfig?.[0]?.scheduleConfig?.interval || '3600',
|
interval: scheduleInterval,
|
||||||
concurrent: existingConfig?.[0]?.scheduleConfig?.concurrent ?? false,
|
concurrent: envConfig.schedule.concurrent ?? existingConfig?.[0]?.scheduleConfig?.concurrent ?? false,
|
||||||
batchSize: existingConfig?.[0]?.scheduleConfig?.batchSize ?? 10,
|
batchSize: envConfig.schedule.batchSize ?? existingConfig?.[0]?.scheduleConfig?.batchSize ?? 10,
|
||||||
pauseBetweenBatches: existingConfig?.[0]?.scheduleConfig?.pauseBetweenBatches ?? 5000,
|
pauseBetweenBatches: envConfig.schedule.pauseBetweenBatches ?? existingConfig?.[0]?.scheduleConfig?.pauseBetweenBatches ?? 5000,
|
||||||
retryAttempts: existingConfig?.[0]?.scheduleConfig?.retryAttempts ?? 3,
|
retryAttempts: envConfig.schedule.retryAttempts ?? existingConfig?.[0]?.scheduleConfig?.retryAttempts ?? 3,
|
||||||
retryDelay: existingConfig?.[0]?.scheduleConfig?.retryDelay ?? 60000,
|
retryDelay: envConfig.schedule.retryDelay ?? existingConfig?.[0]?.scheduleConfig?.retryDelay ?? 60000,
|
||||||
timeout: existingConfig?.[0]?.scheduleConfig?.timeout ?? 3600000,
|
timeout: envConfig.schedule.timeout ?? existingConfig?.[0]?.scheduleConfig?.timeout ?? 3600000,
|
||||||
autoRetry: existingConfig?.[0]?.scheduleConfig?.autoRetry ?? true,
|
autoRetry: envConfig.schedule.autoRetry ?? existingConfig?.[0]?.scheduleConfig?.autoRetry ?? true,
|
||||||
cleanupBeforeMirror: existingConfig?.[0]?.scheduleConfig?.cleanupBeforeMirror ?? false,
|
cleanupBeforeMirror: envConfig.schedule.cleanupBeforeMirror ?? existingConfig?.[0]?.scheduleConfig?.cleanupBeforeMirror ?? false,
|
||||||
notifyOnFailure: existingConfig?.[0]?.scheduleConfig?.notifyOnFailure ?? true,
|
notifyOnFailure: envConfig.schedule.notifyOnFailure ?? existingConfig?.[0]?.scheduleConfig?.notifyOnFailure ?? true,
|
||||||
notifyOnSuccess: existingConfig?.[0]?.scheduleConfig?.notifyOnSuccess ?? false,
|
notifyOnSuccess: envConfig.schedule.notifyOnSuccess ?? existingConfig?.[0]?.scheduleConfig?.notifyOnSuccess ?? false,
|
||||||
logLevel: existingConfig?.[0]?.scheduleConfig?.logLevel || 'info',
|
logLevel: envConfig.schedule.logLevel || existingConfig?.[0]?.scheduleConfig?.logLevel || 'info',
|
||||||
timezone: existingConfig?.[0]?.scheduleConfig?.timezone || 'UTC',
|
timezone: envConfig.schedule.timezone || existingConfig?.[0]?.scheduleConfig?.timezone || 'UTC',
|
||||||
onlyMirrorUpdated: existingConfig?.[0]?.scheduleConfig?.onlyMirrorUpdated ?? false,
|
onlyMirrorUpdated: envConfig.schedule.onlyMirrorUpdated ?? existingConfig?.[0]?.scheduleConfig?.onlyMirrorUpdated ?? false,
|
||||||
updateInterval: existingConfig?.[0]?.scheduleConfig?.updateInterval ?? 86400000,
|
updateInterval: envConfig.schedule.updateInterval ?? existingConfig?.[0]?.scheduleConfig?.updateInterval ?? 86400000,
|
||||||
skipRecentlyMirrored: existingConfig?.[0]?.scheduleConfig?.skipRecentlyMirrored ?? true,
|
skipRecentlyMirrored: envConfig.schedule.skipRecentlyMirrored ?? existingConfig?.[0]?.scheduleConfig?.skipRecentlyMirrored ?? true,
|
||||||
recentThreshold: existingConfig?.[0]?.scheduleConfig?.recentThreshold ?? 3600000,
|
recentThreshold: envConfig.schedule.recentThreshold ?? existingConfig?.[0]?.scheduleConfig?.recentThreshold ?? 3600000,
|
||||||
lastRun: existingConfig?.[0]?.scheduleConfig?.lastRun || null,
|
lastRun: existingConfig?.[0]?.scheduleConfig?.lastRun || null,
|
||||||
nextRun: existingConfig?.[0]?.scheduleConfig?.nextRun || null,
|
nextRun: existingConfig?.[0]?.scheduleConfig?.nextRun || null,
|
||||||
};
|
};
|
||||||
@@ -212,13 +300,13 @@ export async function initializeConfigFromEnv(): Promise<void> {
|
|||||||
const cleanupConfig = {
|
const cleanupConfig = {
|
||||||
enabled: envConfig.cleanup.enabled ?? existingConfig?.[0]?.cleanupConfig?.enabled ?? false,
|
enabled: envConfig.cleanup.enabled ?? existingConfig?.[0]?.cleanupConfig?.enabled ?? false,
|
||||||
retentionDays: envConfig.cleanup.retentionDays ? envConfig.cleanup.retentionDays * 86400 : existingConfig?.[0]?.cleanupConfig?.retentionDays ?? 604800, // Convert days to seconds
|
retentionDays: envConfig.cleanup.retentionDays ? envConfig.cleanup.retentionDays * 86400 : existingConfig?.[0]?.cleanupConfig?.retentionDays ?? 604800, // Convert days to seconds
|
||||||
deleteFromGitea: existingConfig?.[0]?.cleanupConfig?.deleteFromGitea ?? false,
|
deleteFromGitea: envConfig.cleanup.deleteFromGitea ?? existingConfig?.[0]?.cleanupConfig?.deleteFromGitea ?? false,
|
||||||
deleteIfNotInGitHub: existingConfig?.[0]?.cleanupConfig?.deleteIfNotInGitHub ?? true,
|
deleteIfNotInGitHub: envConfig.cleanup.deleteIfNotInGitHub ?? existingConfig?.[0]?.cleanupConfig?.deleteIfNotInGitHub ?? true,
|
||||||
protectedRepos: existingConfig?.[0]?.cleanupConfig?.protectedRepos ?? [],
|
protectedRepos: envConfig.cleanup.protectedRepos ?? existingConfig?.[0]?.cleanupConfig?.protectedRepos ?? [],
|
||||||
dryRun: existingConfig?.[0]?.cleanupConfig?.dryRun ?? true,
|
dryRun: envConfig.cleanup.dryRun ?? existingConfig?.[0]?.cleanupConfig?.dryRun ?? true,
|
||||||
orphanedRepoAction: existingConfig?.[0]?.cleanupConfig?.orphanedRepoAction || 'archive',
|
orphanedRepoAction: envConfig.cleanup.orphanedRepoAction || existingConfig?.[0]?.cleanupConfig?.orphanedRepoAction || 'archive',
|
||||||
batchSize: existingConfig?.[0]?.cleanupConfig?.batchSize ?? 10,
|
batchSize: envConfig.cleanup.batchSize ?? existingConfig?.[0]?.cleanupConfig?.batchSize ?? 10,
|
||||||
pauseBetweenDeletes: existingConfig?.[0]?.cleanupConfig?.pauseBetweenDeletes ?? 2000,
|
pauseBetweenDeletes: envConfig.cleanup.pauseBetweenDeletes ?? existingConfig?.[0]?.cleanupConfig?.pauseBetweenDeletes ?? 2000,
|
||||||
lastRun: existingConfig?.[0]?.cleanupConfig?.lastRun || null,
|
lastRun: existingConfig?.[0]?.cleanupConfig?.lastRun || null,
|
||||||
nextRun: existingConfig?.[0]?.cleanupConfig?.nextRun || null,
|
nextRun: existingConfig?.[0]?.cleanupConfig?.nextRun || null,
|
||||||
};
|
};
|
||||||
|
|||||||
202
src/lib/gitea-auth-validator.ts
Normal file
202
src/lib/gitea-auth-validator.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
/**
|
||||||
|
* Gitea authentication and permission validation utilities
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Config } from "@/types/config";
|
||||||
|
import { httpGet, HttpError } from "./http-client";
|
||||||
|
import { decryptConfigTokens } from "./utils/config-encryption";
|
||||||
|
|
||||||
|
export interface GiteaUser {
|
||||||
|
id: number;
|
||||||
|
login: string;
|
||||||
|
username: string;
|
||||||
|
full_name?: string;
|
||||||
|
email?: string;
|
||||||
|
is_admin: boolean;
|
||||||
|
created?: string;
|
||||||
|
restricted?: boolean;
|
||||||
|
active?: boolean;
|
||||||
|
prohibit_login?: boolean;
|
||||||
|
location?: string;
|
||||||
|
website?: string;
|
||||||
|
description?: string;
|
||||||
|
visibility?: string;
|
||||||
|
followers_count?: number;
|
||||||
|
following_count?: number;
|
||||||
|
starred_repos_count?: number;
|
||||||
|
language?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates Gitea authentication and returns user information
|
||||||
|
*/
|
||||||
|
export async function validateGiteaAuth(config: Partial<Config>): Promise<GiteaUser> {
|
||||||
|
if (!config.giteaConfig?.url || !config.giteaConfig?.token) {
|
||||||
|
throw new Error("Gitea URL and token are required for authentication validation");
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedConfig = decryptConfigTokens(config as Config);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await httpGet<GiteaUser>(
|
||||||
|
`${config.giteaConfig.url}/api/v1/user`,
|
||||||
|
{
|
||||||
|
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = response.data;
|
||||||
|
|
||||||
|
// Validate user data
|
||||||
|
if (!user.id || user.id === 0) {
|
||||||
|
throw new Error("Invalid user data received from Gitea: User ID is 0 or missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.username && !user.login) {
|
||||||
|
throw new Error("Invalid user data received from Gitea: Username is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Auth Validator] Successfully authenticated as: ${user.username || user.login} (ID: ${user.id}, Admin: ${user.is_admin})`);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpError) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
throw new Error(
|
||||||
|
"Authentication failed: The provided Gitea token is invalid or expired. " +
|
||||||
|
"Please check your Gitea configuration and ensure the token has the necessary permissions."
|
||||||
|
);
|
||||||
|
} else if (error.status === 403) {
|
||||||
|
throw new Error(
|
||||||
|
"Permission denied: The Gitea token does not have sufficient permissions. " +
|
||||||
|
"Please ensure your token has 'read:user' scope at minimum."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Failed to validate Gitea authentication: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the authenticated user can create organizations
|
||||||
|
*/
|
||||||
|
export async function canCreateOrganizations(config: Partial<Config>): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const user = await validateGiteaAuth(config);
|
||||||
|
|
||||||
|
// Admin users can always create organizations
|
||||||
|
if (user.is_admin) {
|
||||||
|
console.log(`[Auth Validator] User is admin, can create organizations`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the instance allows regular users to create organizations
|
||||||
|
// This would require checking instance settings, which may not be publicly available
|
||||||
|
// For now, we'll try to create a test org and see if it fails
|
||||||
|
|
||||||
|
if (!config.giteaConfig?.url || !config.giteaConfig?.token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedConfig = decryptConfigTokens(config as Config);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to list user's organizations as a proxy for permission check
|
||||||
|
const orgsResponse = await httpGet(
|
||||||
|
`${config.giteaConfig.url}/api/v1/user/orgs`,
|
||||||
|
{
|
||||||
|
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we can list orgs, we likely can create them
|
||||||
|
console.log(`[Auth Validator] User can list organizations, likely can create them`);
|
||||||
|
return true;
|
||||||
|
} catch (listError) {
|
||||||
|
if (listError instanceof HttpError && listError.status === 403) {
|
||||||
|
console.log(`[Auth Validator] User cannot list/create organizations`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// For other errors, assume we can try
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Auth Validator] Error checking organization creation permissions:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets or validates the default owner for repositories
|
||||||
|
*/
|
||||||
|
export async function getValidatedDefaultOwner(config: Partial<Config>): Promise<string> {
|
||||||
|
const user = await validateGiteaAuth(config);
|
||||||
|
const username = user.username || user.login;
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
throw new Error("Unable to determine Gitea username from authentication");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the configured defaultOwner matches the authenticated user
|
||||||
|
if (config.giteaConfig?.defaultOwner && config.giteaConfig.defaultOwner !== username) {
|
||||||
|
console.warn(
|
||||||
|
`[Auth Validator] Configured defaultOwner (${config.giteaConfig.defaultOwner}) ` +
|
||||||
|
`does not match authenticated user (${username}). Using authenticated user.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that the Gitea configuration is properly set up for mirroring
|
||||||
|
*/
|
||||||
|
export async function validateGiteaConfigForMirroring(config: Partial<Config>): Promise<{
|
||||||
|
valid: boolean;
|
||||||
|
user: GiteaUser;
|
||||||
|
canCreateOrgs: boolean;
|
||||||
|
warnings: string[];
|
||||||
|
errors: string[];
|
||||||
|
}> {
|
||||||
|
const warnings: string[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate authentication
|
||||||
|
const user = await validateGiteaAuth(config);
|
||||||
|
|
||||||
|
// Check organization creation permissions
|
||||||
|
const canCreateOrgs = await canCreateOrganizations(config);
|
||||||
|
|
||||||
|
if (!canCreateOrgs && config.giteaConfig?.preserveOrgStructure) {
|
||||||
|
warnings.push(
|
||||||
|
"User cannot create organizations but 'preserveOrgStructure' is enabled. " +
|
||||||
|
"Repositories will be mirrored to the user account instead."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate token scopes (this would require additional API calls to check specific permissions)
|
||||||
|
// For now, we'll just check if basic operations work
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
user,
|
||||||
|
canCreateOrgs,
|
||||||
|
warnings,
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(error instanceof Error ? error.message : String(error));
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
user: {} as GiteaUser,
|
||||||
|
canCreateOrgs: false,
|
||||||
|
warnings,
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,24 @@ let createOrgCalled = false;
|
|||||||
|
|
||||||
const mockHttpGet = mock(async (url: string, headers?: any) => {
|
const mockHttpGet = mock(async (url: string, headers?: any) => {
|
||||||
// Return different responses based on URL patterns
|
// Return different responses based on URL patterns
|
||||||
|
|
||||||
|
// Handle user authentication endpoint
|
||||||
|
if (url.includes("/api/v1/user")) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
login: "testuser",
|
||||||
|
username: "testuser",
|
||||||
|
email: "test@example.com",
|
||||||
|
is_admin: false,
|
||||||
|
full_name: "Test User"
|
||||||
|
},
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: new Headers()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (url.includes("/api/v1/repos/starred/test-repo")) {
|
if (url.includes("/api/v1/repos/starred/test-repo")) {
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -85,6 +85,25 @@ export async function getOrCreateGiteaOrgEnhanced({
|
|||||||
|
|
||||||
const decryptedConfig = decryptConfigTokens(config as Config);
|
const decryptedConfig = decryptConfigTokens(config as Config);
|
||||||
|
|
||||||
|
// First, validate the user's authentication by getting their information
|
||||||
|
console.log(`[Org Creation] Validating user authentication before organization operations`);
|
||||||
|
try {
|
||||||
|
const userResponse = await httpGet(
|
||||||
|
`${config.giteaConfig.url}/api/v1/user`,
|
||||||
|
{
|
||||||
|
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(`[Org Creation] Authenticated as user: ${userResponse.data.username || userResponse.data.login} (ID: ${userResponse.data.id})`);
|
||||||
|
} catch (authError) {
|
||||||
|
if (authError instanceof HttpError && authError.status === 401) {
|
||||||
|
console.error(`[Org Creation] Authentication failed: Invalid or expired token`);
|
||||||
|
throw new Error(`Authentication failed: Please check your Gitea token has the required permissions. The token may be invalid or expired.`);
|
||||||
|
}
|
||||||
|
console.error(`[Org Creation] Failed to validate authentication:`, authError);
|
||||||
|
throw new Error(`Failed to validate Gitea authentication: ${authError instanceof Error ? authError.message : String(authError)}`);
|
||||||
|
}
|
||||||
|
|
||||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
console.log(`[Org Creation] Attempting to get or create organization: ${orgName} (attempt ${attempt + 1}/${maxRetries})`);
|
console.log(`[Org Creation] Attempting to get or create organization: ${orgName} (attempt ${attempt + 1}/${maxRetries})`);
|
||||||
@@ -164,6 +183,18 @@ export async function getOrCreateGiteaOrgEnhanced({
|
|||||||
}
|
}
|
||||||
continue; // Retry the loop
|
continue; // Retry the loop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for permission errors
|
||||||
|
if (createError.status === 403) {
|
||||||
|
console.error(`[Org Creation] Permission denied: User may not have rights to create organizations`);
|
||||||
|
throw new Error(`Permission denied: Your Gitea user account does not have permission to create organizations. Please ensure your account has the necessary privileges or contact your Gitea administrator.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for authentication errors
|
||||||
|
if (createError.status === 401) {
|
||||||
|
console.error(`[Org Creation] Authentication failed when creating organization`);
|
||||||
|
throw new Error(`Authentication failed: The Gitea token does not have sufficient permissions to create organizations. Please ensure your token has 'write:organization' scope.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw createError;
|
throw createError;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ export const mirrorGithubRepoToGitea = async ({
|
|||||||
const decryptedConfig = decryptConfigTokens(config as Config);
|
const decryptedConfig = decryptConfigTokens(config as Config);
|
||||||
|
|
||||||
// Get the correct owner based on the strategy (with organization overrides)
|
// Get the correct owner based on the strategy (with organization overrides)
|
||||||
const repoOwner = await getGiteaRepoOwnerAsync({ config, repository });
|
let repoOwner = await getGiteaRepoOwnerAsync({ config, repository });
|
||||||
|
|
||||||
const isExisting = await isRepoPresentInGitea({
|
const isExisting = await isRepoPresentInGitea({
|
||||||
config,
|
config,
|
||||||
@@ -355,10 +355,37 @@ export const mirrorGithubRepoToGitea = async ({
|
|||||||
// Handle organization creation if needed for single-org, preserve strategies, or starred repos
|
// Handle organization creation if needed for single-org, preserve strategies, or starred repos
|
||||||
if (repoOwner !== config.giteaConfig.defaultOwner) {
|
if (repoOwner !== config.giteaConfig.defaultOwner) {
|
||||||
// Need to create the organization if it doesn't exist
|
// Need to create the organization if it doesn't exist
|
||||||
await getOrCreateGiteaOrg({
|
try {
|
||||||
orgName: repoOwner,
|
await getOrCreateGiteaOrg({
|
||||||
config,
|
orgName: repoOwner,
|
||||||
});
|
config,
|
||||||
|
});
|
||||||
|
} catch (orgError) {
|
||||||
|
console.error(`Failed to create/access organization ${repoOwner}: ${orgError instanceof Error ? orgError.message : String(orgError)}`);
|
||||||
|
|
||||||
|
// Check if we should fallback to user account
|
||||||
|
if (orgError instanceof Error &&
|
||||||
|
(orgError.message.includes('Permission denied') ||
|
||||||
|
orgError.message.includes('Authentication failed') ||
|
||||||
|
orgError.message.includes('does not have permission'))) {
|
||||||
|
console.warn(`[Fallback] Organization creation/access failed. Attempting to mirror to user account instead.`);
|
||||||
|
|
||||||
|
// Update the repository owner to use the user account
|
||||||
|
repoOwner = config.giteaConfig.defaultOwner;
|
||||||
|
|
||||||
|
// Log this fallback in the database
|
||||||
|
await db
|
||||||
|
.update(repositories)
|
||||||
|
.set({
|
||||||
|
errorMessage: `Organization creation failed, using user account. ${orgError.message}`,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(repositories.id, repository.id!));
|
||||||
|
} else {
|
||||||
|
// Re-throw if it's not a permission issue
|
||||||
|
throw orgError;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if repository already exists as a non-mirror
|
// Check if repository already exists as a non-mirror
|
||||||
@@ -1064,6 +1091,19 @@ export const mirrorGitRepoIssuesToGitea = async ({
|
|||||||
|
|
||||||
// Decrypt config tokens for API usage
|
// Decrypt config tokens for API usage
|
||||||
const decryptedConfig = decryptConfigTokens(config as Config);
|
const decryptedConfig = decryptConfigTokens(config as Config);
|
||||||
|
|
||||||
|
// Verify the repository exists in Gitea before attempting to mirror metadata
|
||||||
|
console.log(`[Issues] Verifying repository ${repository.name} exists at ${giteaOwner}`);
|
||||||
|
const repoExists = await isRepoPresentInGitea({
|
||||||
|
config,
|
||||||
|
owner: giteaOwner,
|
||||||
|
repoName: repository.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!repoExists) {
|
||||||
|
console.error(`[Issues] Repository ${repository.name} not found at ${giteaOwner}. Cannot mirror issues.`);
|
||||||
|
throw new Error(`Repository ${repository.name} does not exist in Gitea at ${giteaOwner}. Please ensure the repository is mirrored first.`);
|
||||||
|
}
|
||||||
|
|
||||||
const [owner, repo] = repository.fullName.split("/");
|
const [owner, repo] = repository.fullName.split("/");
|
||||||
|
|
||||||
@@ -1130,7 +1170,7 @@ export const mirrorGitRepoIssuesToGitea = async ({
|
|||||||
}/labels`,
|
}/labels`,
|
||||||
{ name, color: "#ededed" }, // Default color
|
{ name, color: "#ededed" }, // Default color
|
||||||
{
|
{
|
||||||
Authorization: `token ${config.giteaConfig!.token}`,
|
Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1167,7 +1207,7 @@ export const mirrorGitRepoIssuesToGitea = async ({
|
|||||||
}/issues`,
|
}/issues`,
|
||||||
issuePayload,
|
issuePayload,
|
||||||
{
|
{
|
||||||
Authorization: `token ${config.giteaConfig!.token}`,
|
Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1196,7 +1236,7 @@ export const mirrorGitRepoIssuesToGitea = async ({
|
|||||||
body: `@${comment.user?.login} commented on GitHub:\n\n${comment.body}`,
|
body: `@${comment.user?.login} commented on GitHub:\n\n${comment.body}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Authorization: `token ${config.giteaConfig!.token}`,
|
Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return comment;
|
return comment;
|
||||||
@@ -1312,6 +1352,19 @@ export async function mirrorGitRepoPullRequestsToGitea({
|
|||||||
|
|
||||||
// Decrypt config tokens for API usage
|
// Decrypt config tokens for API usage
|
||||||
const decryptedConfig = decryptConfigTokens(config as Config);
|
const decryptedConfig = decryptConfigTokens(config as Config);
|
||||||
|
|
||||||
|
// Verify the repository exists in Gitea before attempting to mirror metadata
|
||||||
|
console.log(`[Pull Requests] Verifying repository ${repository.name} exists at ${giteaOwner}`);
|
||||||
|
const repoExists = await isRepoPresentInGitea({
|
||||||
|
config,
|
||||||
|
owner: giteaOwner,
|
||||||
|
repoName: repository.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!repoExists) {
|
||||||
|
console.error(`[Pull Requests] Repository ${repository.name} not found at ${giteaOwner}. Cannot mirror PRs.`);
|
||||||
|
throw new Error(`Repository ${repository.name} does not exist in Gitea at ${giteaOwner}. Please ensure the repository is mirrored first.`);
|
||||||
|
}
|
||||||
|
|
||||||
const [owner, repo] = repository.fullName.split("/");
|
const [owner, repo] = repository.fullName.split("/");
|
||||||
|
|
||||||
@@ -1414,6 +1467,19 @@ export async function mirrorGitRepoLabelsToGitea({
|
|||||||
|
|
||||||
// Decrypt config tokens for API usage
|
// Decrypt config tokens for API usage
|
||||||
const decryptedConfig = decryptConfigTokens(config as Config);
|
const decryptedConfig = decryptConfigTokens(config as Config);
|
||||||
|
|
||||||
|
// Verify the repository exists in Gitea before attempting to mirror metadata
|
||||||
|
console.log(`[Labels] Verifying repository ${repository.name} exists at ${giteaOwner}`);
|
||||||
|
const repoExists = await isRepoPresentInGitea({
|
||||||
|
config,
|
||||||
|
owner: giteaOwner,
|
||||||
|
repoName: repository.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!repoExists) {
|
||||||
|
console.error(`[Labels] Repository ${repository.name} not found at ${giteaOwner}. Cannot mirror labels.`);
|
||||||
|
throw new Error(`Repository ${repository.name} does not exist in Gitea at ${giteaOwner}. Please ensure the repository is mirrored first.`);
|
||||||
|
}
|
||||||
|
|
||||||
const [owner, repo] = repository.fullName.split("/");
|
const [owner, repo] = repository.fullName.split("/");
|
||||||
|
|
||||||
@@ -1495,6 +1561,19 @@ export async function mirrorGitRepoMilestonesToGitea({
|
|||||||
|
|
||||||
// Decrypt config tokens for API usage
|
// Decrypt config tokens for API usage
|
||||||
const decryptedConfig = decryptConfigTokens(config as Config);
|
const decryptedConfig = decryptConfigTokens(config as Config);
|
||||||
|
|
||||||
|
// Verify the repository exists in Gitea before attempting to mirror metadata
|
||||||
|
console.log(`[Milestones] Verifying repository ${repository.name} exists at ${giteaOwner}`);
|
||||||
|
const repoExists = await isRepoPresentInGitea({
|
||||||
|
config,
|
||||||
|
owner: giteaOwner,
|
||||||
|
repoName: repository.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!repoExists) {
|
||||||
|
console.error(`[Milestones] Repository ${repository.name} not found at ${giteaOwner}. Cannot mirror milestones.`);
|
||||||
|
throw new Error(`Repository ${repository.name} does not exist in Gitea at ${giteaOwner}. Please ensure the repository is mirrored first.`);
|
||||||
|
}
|
||||||
|
|
||||||
const [owner, repo] = repository.fullName.split("/");
|
const [owner, repo] = repository.fullName.split("/");
|
||||||
|
|
||||||
|
|||||||
@@ -47,11 +47,31 @@ export async function httpRequest<T = any>(
|
|||||||
try {
|
try {
|
||||||
responseText = await responseClone.text();
|
responseText = await responseClone.text();
|
||||||
if (responseText) {
|
if (responseText) {
|
||||||
errorMessage += ` - ${responseText}`;
|
// Try to parse as JSON for better error messages
|
||||||
|
try {
|
||||||
|
const errorData = JSON.parse(responseText);
|
||||||
|
if (errorData.message) {
|
||||||
|
errorMessage = `HTTP ${response.status}: ${errorData.message}`;
|
||||||
|
} else {
|
||||||
|
errorMessage += ` - ${responseText}`;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not JSON, use as-is
|
||||||
|
errorMessage += ` - ${responseText}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore text parsing errors
|
// Ignore text parsing errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log authentication-specific errors for debugging
|
||||||
|
if (response.status === 401) {
|
||||||
|
console.error(`[HTTP Client] Authentication failed for ${url}`);
|
||||||
|
console.error(`[HTTP Client] Response: ${responseText}`);
|
||||||
|
if (responseText.includes('user does not exist') && responseText.includes('uid: 0')) {
|
||||||
|
console.error(`[HTTP Client] Token appears to be invalid or the user account is not properly configured in Gitea`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw new HttpError(
|
throw new HttpError(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
|||||||
161
src/tests/test-gitea-auth.ts
Normal file
161
src/tests/test-gitea-auth.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test script to validate Gitea authentication and permissions
|
||||||
|
* Run with: bun run src/tests/test-gitea-auth.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { validateGiteaAuth, canCreateOrganizations, validateGiteaConfigForMirroring } from "@/lib/gitea-auth-validator";
|
||||||
|
import { getConfigsByUserId } from "@/lib/db/queries/configs";
|
||||||
|
import { db, users } from "@/lib/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
async function testGiteaAuthentication() {
|
||||||
|
console.log("=".repeat(60));
|
||||||
|
console.log("GITEA AUTHENTICATION TEST");
|
||||||
|
console.log("=".repeat(60));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the first user for testing
|
||||||
|
const userList = await db.select().from(users).limit(1);
|
||||||
|
|
||||||
|
if (userList.length === 0) {
|
||||||
|
console.error("❌ No users found in database. Please set up a user first.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = userList[0];
|
||||||
|
console.log(`\n✅ Found user: ${user.email} (ID: ${user.id})`);
|
||||||
|
|
||||||
|
// Get the user's configuration
|
||||||
|
const configs = await getConfigsByUserId(user.id);
|
||||||
|
|
||||||
|
if (configs.length === 0) {
|
||||||
|
console.error("❌ No configuration found for user. Please configure Gitea settings.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = configs[0];
|
||||||
|
console.log(`✅ Found configuration (ID: ${config.id})`);
|
||||||
|
|
||||||
|
if (!config.giteaConfig?.url || !config.giteaConfig?.token) {
|
||||||
|
console.error("❌ Gitea configuration is incomplete. URL or token is missing.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n📡 Testing connection to: ${config.giteaConfig.url}`);
|
||||||
|
console.log("-".repeat(60));
|
||||||
|
|
||||||
|
// Test 1: Validate authentication
|
||||||
|
console.log("\n🔐 Test 1: Validating authentication...");
|
||||||
|
try {
|
||||||
|
const giteaUser = await validateGiteaAuth(config);
|
||||||
|
console.log(`✅ Authentication successful!`);
|
||||||
|
console.log(` - Username: ${giteaUser.username || giteaUser.login}`);
|
||||||
|
console.log(` - User ID: ${giteaUser.id}`);
|
||||||
|
console.log(` - Is Admin: ${giteaUser.is_admin}`);
|
||||||
|
console.log(` - Email: ${giteaUser.email || 'Not provided'}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Authentication failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Check organization creation permissions
|
||||||
|
console.log("\n🏢 Test 2: Checking organization creation permissions...");
|
||||||
|
try {
|
||||||
|
const canCreate = await canCreateOrganizations(config);
|
||||||
|
if (canCreate) {
|
||||||
|
console.log(`✅ User can create organizations`);
|
||||||
|
} else {
|
||||||
|
console.log(`⚠️ User cannot create organizations (will use fallback to user account)`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error checking permissions: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: Full validation for mirroring
|
||||||
|
console.log("\n🔍 Test 3: Full validation for mirroring...");
|
||||||
|
try {
|
||||||
|
const validation = await validateGiteaConfigForMirroring(config);
|
||||||
|
|
||||||
|
if (validation.valid) {
|
||||||
|
console.log(`✅ Configuration is valid for mirroring`);
|
||||||
|
} else {
|
||||||
|
console.log(`❌ Configuration is not valid for mirroring`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validation.warnings.length > 0) {
|
||||||
|
console.log(`\n⚠️ Warnings:`);
|
||||||
|
validation.warnings.forEach(warning => {
|
||||||
|
console.log(` - ${warning}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validation.errors.length > 0) {
|
||||||
|
console.log(`\n❌ Errors:`);
|
||||||
|
validation.errors.forEach(error => {
|
||||||
|
console.log(` - ${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Validation error: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 4: Check specific API endpoints
|
||||||
|
console.log("\n🔧 Test 4: Testing specific API endpoints...");
|
||||||
|
|
||||||
|
// Import HTTP client for direct API testing
|
||||||
|
const { httpGet } = await import("@/lib/http-client");
|
||||||
|
const { decryptConfigTokens } = await import("@/lib/utils/config-encryption");
|
||||||
|
const decryptedConfig = decryptConfigTokens(config);
|
||||||
|
|
||||||
|
// Test organization listing
|
||||||
|
try {
|
||||||
|
const orgsResponse = await httpGet(
|
||||||
|
`${config.giteaConfig.url}/api/v1/user/orgs`,
|
||||||
|
{
|
||||||
|
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(`✅ Can list organizations (found ${orgsResponse.data.length})`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`⚠️ Cannot list organizations: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test repository listing
|
||||||
|
try {
|
||||||
|
const reposResponse = await httpGet(
|
||||||
|
`${config.giteaConfig.url}/api/v1/user/repos`,
|
||||||
|
{
|
||||||
|
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(`✅ Can list repositories (found ${reposResponse.data.length})`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Cannot list repositories: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n" + "=".repeat(60));
|
||||||
|
console.log("TEST COMPLETE");
|
||||||
|
console.log("=".repeat(60));
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
console.log("\n📊 Summary:");
|
||||||
|
console.log(` - Gitea URL: ${config.giteaConfig.url}`);
|
||||||
|
console.log(` - Default Owner: ${config.giteaConfig.defaultOwner || 'Not set'}`);
|
||||||
|
console.log(` - Mirror Strategy: ${config.githubConfig?.mirrorStrategy || 'Not set'}`);
|
||||||
|
console.log(` - Organization: ${config.giteaConfig.organization || 'Not set'}`);
|
||||||
|
console.log(` - Preserve Org Structure: ${config.giteaConfig.preserveOrgStructure || false}`);
|
||||||
|
|
||||||
|
console.log("\n✨ All tests completed successfully!");
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("\n❌ Test failed with error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the test
|
||||||
|
testGiteaAuthentication().catch(console.error);
|
||||||
173
src/tests/test-metadata-mirroring.ts
Normal file
173
src/tests/test-metadata-mirroring.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test script to verify metadata mirroring authentication works correctly
|
||||||
|
* This tests the fix for issue #68 - "user does not exist [uid: 0, name: ]"
|
||||||
|
* Run with: bun run src/tests/test-metadata-mirroring.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { mirrorGitRepoIssuesToGitea } from "@/lib/gitea";
|
||||||
|
import { validateGiteaAuth } from "@/lib/gitea-auth-validator";
|
||||||
|
import { getConfigsByUserId } from "@/lib/db/queries/configs";
|
||||||
|
import { db, users, repositories } from "@/lib/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { Octokit } from "@octokit/rest";
|
||||||
|
import type { Repository } from "@/lib/db/schema";
|
||||||
|
|
||||||
|
async function testMetadataMirroringAuth() {
|
||||||
|
console.log("=".repeat(60));
|
||||||
|
console.log("METADATA MIRRORING AUTHENTICATION TEST");
|
||||||
|
console.log("=".repeat(60));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the first user for testing
|
||||||
|
const userList = await db.select().from(users).limit(1);
|
||||||
|
|
||||||
|
if (userList.length === 0) {
|
||||||
|
console.error("❌ No users found in database. Please set up a user first.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = userList[0];
|
||||||
|
console.log(`\n✅ Found user: ${user.email} (ID: ${user.id})`);
|
||||||
|
|
||||||
|
// Get the user's configuration
|
||||||
|
const configs = await getConfigsByUserId(user.id);
|
||||||
|
|
||||||
|
if (configs.length === 0) {
|
||||||
|
console.error("❌ No configuration found for user. Please configure GitHub and Gitea settings.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = configs[0];
|
||||||
|
console.log(`✅ Found configuration (ID: ${config.id})`);
|
||||||
|
|
||||||
|
if (!config.giteaConfig?.url || !config.giteaConfig?.token) {
|
||||||
|
console.error("❌ Gitea configuration is incomplete. URL or token is missing.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.githubConfig?.token) {
|
||||||
|
console.error("❌ GitHub configuration is incomplete. Token is missing.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n📡 Testing Gitea connection to: ${config.giteaConfig.url}`);
|
||||||
|
console.log("-".repeat(60));
|
||||||
|
|
||||||
|
// Test 1: Validate Gitea authentication
|
||||||
|
console.log("\n🔐 Test 1: Validating Gitea authentication...");
|
||||||
|
let giteaUser;
|
||||||
|
try {
|
||||||
|
giteaUser = await validateGiteaAuth(config);
|
||||||
|
console.log(`✅ Gitea authentication successful!`);
|
||||||
|
console.log(` - Username: ${giteaUser.username || giteaUser.login}`);
|
||||||
|
console.log(` - User ID: ${giteaUser.id}`);
|
||||||
|
console.log(` - Is Admin: ${giteaUser.is_admin}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Gitea authentication failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
console.error(` This is the root cause of the "user does not exist [uid: 0]" error`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Check if we can access a test repository
|
||||||
|
console.log("\n📦 Test 2: Looking for a test repository...");
|
||||||
|
|
||||||
|
// Get a repository from the database
|
||||||
|
const repos = await db.select().from(repositories)
|
||||||
|
.where(eq(repositories.userId, user.id))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (repos.length === 0) {
|
||||||
|
console.log("⚠️ No repositories found in database. Skipping metadata mirroring test.");
|
||||||
|
console.log(" Please run a mirror operation first to test metadata mirroring.");
|
||||||
|
} else {
|
||||||
|
const testRepo = repos[0] as Repository;
|
||||||
|
console.log(`✅ Found test repository: ${testRepo.fullName}`);
|
||||||
|
|
||||||
|
// Test 3: Verify repository exists in Gitea
|
||||||
|
console.log("\n🔍 Test 3: Verifying repository exists in Gitea...");
|
||||||
|
|
||||||
|
const { isRepoPresentInGitea } = await import("@/lib/gitea");
|
||||||
|
const giteaOwner = giteaUser.username || giteaUser.login;
|
||||||
|
|
||||||
|
const repoExists = await isRepoPresentInGitea({
|
||||||
|
config,
|
||||||
|
owner: giteaOwner,
|
||||||
|
repoName: testRepo.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!repoExists) {
|
||||||
|
console.log(`⚠️ Repository ${testRepo.name} not found in Gitea at ${giteaOwner}`);
|
||||||
|
console.log(` This would cause metadata mirroring to fail with authentication errors`);
|
||||||
|
console.log(` Please ensure the repository is mirrored first before attempting metadata sync`);
|
||||||
|
} else {
|
||||||
|
console.log(`✅ Repository exists in Gitea at ${giteaOwner}/${testRepo.name}`);
|
||||||
|
|
||||||
|
// Test 4: Attempt to mirror metadata (dry run)
|
||||||
|
console.log("\n🔄 Test 4: Testing metadata mirroring authentication...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create Octokit instance
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: config.githubConfig.token,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test by attempting to fetch labels (lightweight operation)
|
||||||
|
const { httpGet } = await import("@/lib/http-client");
|
||||||
|
const { decryptConfigTokens } = await import("@/lib/utils/config-encryption");
|
||||||
|
const decryptedConfig = decryptConfigTokens(config);
|
||||||
|
|
||||||
|
const labelsResponse = await httpGet(
|
||||||
|
`${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${testRepo.name}/labels`,
|
||||||
|
{
|
||||||
|
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Successfully authenticated for metadata operations`);
|
||||||
|
console.log(` - Can access repository labels endpoint`);
|
||||||
|
console.log(` - Found ${labelsResponse.data.length} existing labels`);
|
||||||
|
console.log(` - Authentication token is valid and has proper permissions`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message.includes('uid: 0')) {
|
||||||
|
console.error(`❌ CRITICAL: Authentication failed with "uid: 0" error!`);
|
||||||
|
console.error(` This is the exact issue from GitHub issue #68`);
|
||||||
|
console.error(` Error: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
console.error(`❌ Metadata operation failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n" + "=".repeat(60));
|
||||||
|
console.log("TEST COMPLETE");
|
||||||
|
console.log("=".repeat(60));
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
console.log("\n📊 Summary:");
|
||||||
|
console.log(` - Gitea URL: ${config.giteaConfig.url}`);
|
||||||
|
console.log(` - Gitea User: ${giteaUser?.username || giteaUser?.login || 'Unknown'}`);
|
||||||
|
console.log(` - Authentication: ${giteaUser ? '✅ Valid' : '❌ Invalid'}`);
|
||||||
|
console.log(` - Metadata Mirroring: ${config.giteaConfig.mirrorMetadata ? 'Enabled' : 'Disabled'}`);
|
||||||
|
if (config.giteaConfig.mirrorMetadata) {
|
||||||
|
console.log(` - Issues: ${config.giteaConfig.mirrorIssues ? 'Yes' : 'No'}`);
|
||||||
|
console.log(` - Pull Requests: ${config.giteaConfig.mirrorPullRequests ? 'Yes' : 'No'}`);
|
||||||
|
console.log(` - Labels: ${config.giteaConfig.mirrorLabels ? 'Yes' : 'No'}`);
|
||||||
|
console.log(` - Milestones: ${config.giteaConfig.mirrorMilestones ? 'Yes' : 'No'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n✨ If all tests passed, metadata mirroring should work without uid:0 errors!");
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("\n❌ Test failed with error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the test
|
||||||
|
testMetadataMirroringAuth().catch(console.error);
|
||||||
Reference in New Issue
Block a user