Add Gitea Actions workflow templates and automation

- Add Docker build workflow template with semantic versioning
- Add branch protection automation script
- Add deployment strategies documentation
- Add Harbor registry setup guide
- Update Gitea README with runner troubleshooting
- Add workflow template snippets for auto-deploy

Templates support:
- Semantic version validation on PRs
- Docker build and push to Docker Hub
- Discord notifications (success/failure)
- Build summaries and metadata extraction
- GitHub Actions cache optimization

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-02-05 13:40:17 -06:00
parent 2017b56985
commit 93ea435237
9 changed files with 2259 additions and 0 deletions

View File

@ -0,0 +1,221 @@
# Gitea Configuration & Templates Index
Quick reference for all Gitea-related documentation and templates.
## 📁 Directory Structure
```
/server-configs/gitea/
├── README.md # Gitea LXC setup and configuration
├── INDEX.md # This file - quick reference
├── deployment-strategies.md # When/how to deploy (manual vs auto)
├── harbor-registry-setup.md # Self-hosted Docker registry guide
├── apply_branch_protection.py # Script for branch protection rules
└── workflow-templates/ # Reusable CI/CD templates
├── README.md # Template usage guide
├── docker-build-template.yml # Complete Docker CI/CD workflow
├── deploy-script-template.sh # Safe manual deployment script
└── snippets/ # Workflow code snippets
└── auto-deploy-with-rollback.yml
```
## 🚀 Quick Links
### Getting Started
- **New to Gitea Actions?** → Start with `workflow-templates/README.md`
- **Need a workflow?** → Copy `docker-build-template.yml`
- **Want to deploy?** → Read `deployment-strategies.md`
### Templates
#### Docker Build Workflow
**File:** `workflow-templates/docker-build-template.yml`
**Features:**
- ✅ Semantic version validation
- ✅ Docker build + push to Docker Hub
- ✅ Discord notifications
- ✅ Build caching
- ✅ Multi-tag strategy
**Use for:**
- Discord bots
- Web applications
- API services
- Any Dockerized project
#### Manual Deploy Script
**File:** `workflow-templates/deploy-script-template.sh`
**Features:**
- ✅ Pre-deployment checks
- ✅ Confirmation prompts
- ✅ Status verification
- ✅ Log viewing
- ✅ Rollback instructions
**Use when:**
- You want manual control
- Deploying to production
- Testing new deployments
#### Auto-Deploy with Rollback
**File:** `workflow-templates/snippets/auto-deploy-with-rollback.yml`
**Features:**
- ✅ SSH deployment
- ✅ Health checks
- ✅ Automatic rollback
- ✅ Deployment notifications
**Use when:**
- You have good test coverage
- Health checks are implemented
- Downtime is acceptable
### Guides
#### Deployment Strategies
**File:** `deployment-strategies.md`
**Covers:**
- Decision framework (when to auto-deploy)
- 5 levels of deployment automation
- Recommendations by project type
- Safety best practices
- Rollback procedures
**Read this before:** Adding auto-deploy to any project
#### Harbor Registry Setup
**File:** `harbor-registry-setup.md`
**Covers:**
- Self-hosted Docker registry
- Complete Harbor installation
- Integration with Gitea Actions
- Vulnerability scanning
- Backup strategies
**Use when:**
- You want private registries
- You hit Docker Hub rate limits
- You want full control
- Learning opportunity
## 📝 Reference Implementations
### Paper Dynasty Discord Bot
**Status:** ✅ Production
**Date:** 2026-02-04
**Setup:**
- Gitea Actions on LXC 225
- Docker build + push to Docker Hub
- Discord notifications working
- Manual deployment to sba-bots (10.10.0.88)
**Workflow:** Based on `docker-build-template.yml`
**What worked:**
- Semantic version validation
- Multi-tag strategy (latest, version, version+commit)
- Discord webhooks with ISO 8601 timestamps
- GitHub Actions cache for faster builds
**Lessons learned:**
- Timestamp format critical for Discord (ISO 8601)
- Health checks needed before auto-deploy
- Manual deploy preferred for production Discord bots
## 🔧 Common Tasks
### Create New Project Workflow
```bash
cd /path/to/your/repo
mkdir -p .gitea/workflows
cp /path/to/docker-build-template.yml .gitea/workflows/docker-build.yml
# Customize:
# - Replace "yourusername/yourrepo" with your Docker Hub repo
# - Replace "Your Project" in notifications
# - Replace Discord webhook URLs
# - Add secrets: DOCKERHUB_USERNAME, DOCKERHUB_TOKEN
# - Create VERSION file: echo "1.0.0" > VERSION
```
### Add Auto-Deploy (When Ready)
```bash
# Copy auto-deploy snippet
cat workflow-templates/snippets/auto-deploy-with-rollback.yml
# Add to your workflow after build step
# Configure secrets: DEPLOY_SSH_KEY, PRODUCTION_HOST, DEPLOY_USER
```
### Manual Deploy
```bash
# Copy deploy script to your repo
cp workflow-templates/deploy-script-template.sh /path/to/repo/deploy.sh
chmod +x /path/to/repo/deploy.sh
# Customize server details in script
# Then use: ./deploy.sh v1.2.3
```
## 🎓 Learning Path
1. **Start:** Read `workflow-templates/README.md`
2. **Setup:** Deploy Gitea Actions runner (see `README.md`)
3. **First workflow:** Copy `docker-build-template.yml`
4. **Deploy:** Use `deploy-script-template.sh`
5. **Advanced:** Read `deployment-strategies.md`
6. **Optional:** Set up Harbor with `harbor-registry-setup.md`
## 🆘 Troubleshooting
### Version validation failing
- Check VERSION file format: just `1.2.3`
- Ensure semver rules followed
- See template comments for valid bumps
### Docker Hub push failing
- Verify secrets set in Gitea
- Check Docker Hub token permissions
- Ensure repo name matches exactly
### Discord notifications not appearing
- Verify webhook URL still valid
- Check timestamp format (ISO 8601)
- Test webhook manually with curl
### More help
- Check specific template comments
- Read troubleshooting sections in guides
- Review Paper Dynasty as reference implementation
## 📚 External Resources
- [Gitea Actions Docs](https://docs.gitea.io/en-us/usage/actions/overview/)
- [Docker Build Push Action](https://github.com/docker/build-push-action)
- [Semantic Versioning](https://semver.org/)
- [Discord Webhooks](https://discord.com/developers/docs/resources/webhook)
- [Harbor Docs](https://goharbor.io/docs/)
## 🔄 Maintenance
**Update this index when:**
- Adding new templates
- Creating new guides
- Changing directory structure
- Adding reference implementations
**Last updated:** 2026-02-04
---
**Quick search tips:**
- Need deployment guide? → `deployment-strategies.md`
- Need workflow template? → `workflow-templates/`
- Need self-hosted registry? → `harbor-registry-setup.md`
- Need deploy script? → `deploy-script-template.sh`

View File

@ -304,3 +304,147 @@ This repository is mirrored on both GitHub and Gitea for redundancy:
- **GitHub**: https://github.com/calcorum/claude-home
- **Gitea**: https://git.manticorum.com/cal/claude-home
---
## Branch Protection Manager
Automated script to apply consistent branch protection rules across all your Gitea repositories.
### Features
- Applies branch protection rules to all repositories for a user/organization
- Supports dry-run mode to preview changes
- Configurable protection rules via environment variables
- Handles updating existing protection rules
- Clear success/error reporting
### Requirements
```bash
pip install requests
```
### Configuration
The script (`apply_branch_protection.py`) uses these settings:
- ✅ Disable direct pushes (force PR workflow)
- ✅ Require 1 approval before merge
- ✅ Restrict approvals to whitelisted users
- ✅ Dismiss stale approvals when new commits are pushed
- ✅ Enable status checks (for CI/CD)
- ✅ Restrict merging to whitelisted users
- ✅ Block merge on rejected reviews
- ✅ Block merge if pull request is outdated (critical for DB migrations)
### Usage
#### 1. Create a Gitea API Token
1. Go to https://git.manticorum.com/user/settings/applications
2. Click "Generate New Token"
3. Give it a name (e.g., "Branch Protection Script")
4. Select permissions: `repo` (full control)
5. Click "Generate Token"
6. Copy the token (you won't see it again!)
#### 2. Set Environment Variables
```bash
export GITEA_TOKEN='your-api-token-here'
export GITEA_URL='https://git.manticorum.com' # Optional, defaults to this
export GITEA_OWNER='cal' # Optional, defaults to cal
```
#### 3. Run the Script
**Dry run (preview changes without applying):**
```bash
cd /mnt/NV2/Development/claude-home/server-configs/gitea
python apply_branch_protection.py --dry-run
```
**Apply to all repositories:**
```bash
python apply_branch_protection.py
```
### Customizing Protection Rules
Edit the `BranchProtectionConfig` section in `main()` to customize the rules:
```python
config = BranchProtectionConfig(
branch_name="main", # Branch to protect
enable_push=False, # Disable direct pushes
required_approvals=1, # Number of required approvals
enable_approvals_whitelist=True, # Restrict who can approve
approvals_whitelist_usernames=[GITEA_OWNER],
dismiss_stale_approvals=True, # Dismiss approvals on new commits
enable_status_check=True, # Require status checks to pass
enable_merge_whitelist=True, # Restrict who can merge
merge_whitelist_usernames=[GITEA_OWNER],
block_on_rejected_reviews=True, # Block merge if reviews are rejected
block_on_outdated_branch=True, # Block merge if branch is outdated
require_signed_commits=False, # Require GPG signatures
)
```
### Example Output
```
============================================================
Gitea Branch Protection Configuration
============================================================
Gitea URL: https://git.manticorum.com
Owner: cal
Branch: main
============================================================
Protection Rules:
• Direct pushes: Disabled
• Required approvals: 1
• Approvals whitelist: cal
• Dismiss stale approvals: True
• Status checks enabled: True
• Merge whitelist: cal
• Block on rejected reviews: True
• Block on outdated branch: True
• Require signed commits: False
============================================================
Applying branch protection to 5 repositories...
📦 major-domo-database
🔄 Updating existing protection...
✅ Successfully applied branch protection
📦 major-domo-bot
✅ Successfully applied branch protection
...
```
### Troubleshooting Branch Protection Script
**"GITEA_TOKEN environment variable is required"**
- You need to set your API token. See usage step 2 above.
**"Error fetching repositories"**
- Check that your `GITEA_URL` is correct
- Verify your API token has `repo` permissions
- Ensure the `GITEA_OWNER` username is correct
**"Could not delete existing protection"**
- The script will still try to create the new protection
- If this fails, manually delete the old protection rule from Gitea web UI
**"Error: 422 Unprocessable Entity"**
This usually means:
- The branch doesn't exist in the repository
- Invalid username in whitelist
- Conflicting protection rule settings
### API References
This script uses the Gitea API branch protection endpoints:
- [Gitea API Documentation](https://docs.gitea.com/api/)
- [Protected Branches](https://docs.gitea.com/usage/access-control/protected-branches)
- [Create Branch Protection Endpoint](https://share.apidog.com/apidoc/docs-site/346218/api-3521477)

View File

@ -0,0 +1,299 @@
#!/usr/bin/env python3
"""
Apply branch protection rules to all Gitea repositories.
This script uses the Gitea API to apply consistent branch protection rules
across all repositories owned by a user or organization.
"""
import os
import sys
import requests
from typing import Optional
from dataclasses import dataclass
@dataclass
class BranchProtectionConfig:
"""Configuration for branch protection rules."""
# Branch to protect
branch_name: str = "main"
# Push settings
enable_push: bool = False # Disable direct pushes (force PR workflow)
enable_push_whitelist: bool = False
push_whitelist_usernames: list[str] = None
# Pull request approval settings
required_approvals: int = 1
enable_approvals_whitelist: bool = True
approvals_whitelist_usernames: list[str] = None
dismiss_stale_approvals: bool = True
# Status check settings
enable_status_check: bool = True
status_check_contexts: list[str] = None # Empty for now, add when CI/CD is set up
# Merge settings
enable_merge_whitelist: bool = True
merge_whitelist_usernames: list[str] = None
block_on_rejected_reviews: bool = True
block_on_outdated_branch: bool = True # Critical for database migrations
# Other settings
require_signed_commits: bool = False
protected_file_patterns: str = ""
unprotected_file_patterns: str = ""
def __post_init__(self):
"""Initialize mutable default values."""
if self.push_whitelist_usernames is None:
self.push_whitelist_usernames = []
if self.approvals_whitelist_usernames is None:
self.approvals_whitelist_usernames = []
if self.merge_whitelist_usernames is None:
self.merge_whitelist_usernames = []
if self.status_check_contexts is None:
self.status_check_contexts = []
class GiteaBranchProtectionManager:
"""Manages branch protection rules across Gitea repositories."""
def __init__(self, gitea_url: str, api_token: str, owner: str):
"""
Initialize the manager.
Args:
gitea_url: Base URL of your Gitea instance (e.g., https://git.example.com)
api_token: Gitea API token with repo access
owner: Username or organization name
"""
self.gitea_url = gitea_url.rstrip('/')
self.api_token = api_token
self.owner = owner
self.headers = {
'Authorization': f'token {api_token}',
'Content-Type': 'application/json'
}
def get_repositories(self) -> list[dict]:
"""Get all repositories for the authenticated user."""
# Use /user/repos for authenticated requests (works with API token)
url = f'{self.gitea_url}/api/v1/user/repos'
params = {'limit': 100} # Adjust if you have more than 100 repos
try:
response = requests.get(url, headers=self.headers, params=params)
response.raise_for_status()
repos = response.json()
# Filter to only repos owned by the specified owner if needed
if self.owner:
repos = [r for r in repos if r.get('owner', {}).get('login') == self.owner]
return repos
except requests.exceptions.RequestException as e:
print(f"❌ Error fetching repositories: {e}")
sys.exit(1)
def get_existing_branch_protections(self, repo_name: str) -> list[dict]:
"""Get existing branch protections for a repository."""
url = f'{self.gitea_url}/api/v1/repos/{self.owner}/{repo_name}/branch_protections'
try:
response = requests.get(url, headers=self.headers)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f" ⚠️ Could not fetch existing protections: {e}")
return []
def delete_branch_protection(self, repo_name: str, protection_name: str) -> bool:
"""Delete an existing branch protection rule."""
url = f'{self.gitea_url}/api/v1/repos/{self.owner}/{repo_name}/branch_protections/{protection_name}'
try:
response = requests.delete(url, headers=self.headers)
response.raise_for_status()
return True
except requests.exceptions.RequestException as e:
print(f" ⚠️ Could not delete existing protection: {e}")
return False
def apply_branch_protection(self, repo_name: str, config: BranchProtectionConfig) -> bool:
"""
Apply branch protection rules to a repository.
Args:
repo_name: Name of the repository
config: Branch protection configuration
Returns:
True if successful, False otherwise
"""
url = f'{self.gitea_url}/api/v1/repos/{self.owner}/{repo_name}/branch_protections'
payload = {
'branch_name': config.branch_name,
'enable_push': config.enable_push,
'enable_push_whitelist': config.enable_push_whitelist,
'push_whitelist_usernames': config.push_whitelist_usernames,
'required_approvals': config.required_approvals,
'enable_approvals_whitelist': config.enable_approvals_whitelist,
'approvals_whitelist_usernames': config.approvals_whitelist_usernames,
'dismiss_stale_approvals': config.dismiss_stale_approvals,
'enable_status_check': config.enable_status_check,
'status_check_contexts': config.status_check_contexts,
'enable_merge_whitelist': config.enable_merge_whitelist,
'merge_whitelist_usernames': config.merge_whitelist_usernames,
'block_on_rejected_reviews': config.block_on_rejected_reviews,
'block_on_outdated_branch': config.block_on_outdated_branch,
'require_signed_commits': config.require_signed_commits,
'protected_file_patterns': config.protected_file_patterns,
'unprotected_file_patterns': config.unprotected_file_patterns,
}
try:
response = requests.post(url, headers=self.headers, json=payload)
response.raise_for_status()
return True
except requests.exceptions.RequestException as e:
print(f" ❌ Error: {e}")
if hasattr(e.response, 'text'):
print(f" Response: {e.response.text}")
return False
def apply_to_all_repos(self, config: BranchProtectionConfig, dry_run: bool = False):
"""
Apply branch protection rules to all repositories.
Args:
config: Branch protection configuration
dry_run: If True, only show what would be done without making changes
"""
repos = self.get_repositories()
if not repos:
print("No repositories found.")
return
print(f"\n{'DRY RUN - ' if dry_run else ''}Applying branch protection to {len(repos)} repositories...\n")
success_count = 0
skip_count = 0
error_count = 0
for repo in repos:
repo_name = repo['name']
print(f"📦 {repo_name}")
# Check if branch exists
default_branch = repo.get('default_branch', 'main')
if config.branch_name != default_branch:
print(f" Default branch is '{default_branch}', protecting '{config.branch_name}' instead")
if dry_run:
print(f" ✓ Would apply protection to '{config.branch_name}' branch")
skip_count += 1
continue
# Check for existing protection
existing_protections = self.get_existing_branch_protections(repo_name)
existing_main_protection = None
for protection in existing_protections:
if protection.get('branch_name') == config.branch_name:
existing_main_protection = protection.get('id') or protection.get('branch_name')
break
# Delete existing protection if it exists
if existing_main_protection:
print(f" 🔄 Updating existing protection...")
self.delete_branch_protection(repo_name, existing_main_protection)
# Apply new protection
if self.apply_branch_protection(repo_name, config):
print(f" ✅ Successfully applied branch protection")
success_count += 1
else:
print(f" ❌ Failed to apply branch protection")
error_count += 1
print(f"\n{'=' * 60}")
print(f"Summary:")
print(f" ✅ Successful: {success_count}")
if dry_run:
print(f" ⏭️ Skipped (dry run): {skip_count}")
if error_count > 0:
print(f" ❌ Errors: {error_count}")
print(f"{'=' * 60}\n")
def main():
"""Main entry point."""
# Configuration - adjust these values or use environment variables
GITEA_URL = os.getenv('GITEA_URL', 'https://git.manticorum.com')
GITEA_TOKEN = os.getenv('GITEA_TOKEN')
GITEA_OWNER = os.getenv('GITEA_OWNER', 'cal')
# Check for required configuration
if not GITEA_TOKEN:
print("❌ Error: GITEA_TOKEN environment variable is required")
print("\nUsage:")
print(" export GITEA_TOKEN='your-api-token-here'")
print(" python apply_branch_protection.py [--dry-run]")
print("\nOptional environment variables:")
print(" GITEA_URL (default: https://git.manticorum.com)")
print(" GITEA_OWNER (default: cal)")
sys.exit(1)
# Parse command line arguments
dry_run = '--dry-run' in sys.argv
# Configure branch protection rules
# Adjust these settings as needed
config = BranchProtectionConfig(
branch_name="main",
enable_push=False, # Disable direct pushes
required_approvals=1,
enable_approvals_whitelist=True,
approvals_whitelist_usernames=[GITEA_OWNER],
dismiss_stale_approvals=True,
enable_status_check=True,
enable_merge_whitelist=True,
merge_whitelist_usernames=[GITEA_OWNER],
block_on_rejected_reviews=True,
block_on_outdated_branch=True, # Critical for DB repos
require_signed_commits=False,
)
# Display configuration
print(f"\n{'=' * 60}")
print(f"Gitea Branch Protection Configuration")
print(f"{'=' * 60}")
print(f"Gitea URL: {GITEA_URL}")
print(f"Owner: {GITEA_OWNER}")
print(f"Branch: {config.branch_name}")
print(f"{'=' * 60}")
print(f"Protection Rules:")
print(f" • Direct pushes: {'Disabled' if not config.enable_push else 'Enabled'}")
print(f" • Required approvals: {config.required_approvals}")
print(f" • Approvals whitelist: {', '.join(config.approvals_whitelist_usernames)}")
print(f" • Dismiss stale approvals: {config.dismiss_stale_approvals}")
print(f" • Status checks enabled: {config.enable_status_check}")
print(f" • Merge whitelist: {', '.join(config.merge_whitelist_usernames)}")
print(f" • Block on rejected reviews: {config.block_on_rejected_reviews}")
print(f" • Block on outdated branch: {config.block_on_outdated_branch}")
print(f" • Require signed commits: {config.require_signed_commits}")
print(f"{'=' * 60}\n")
if dry_run:
print("🔍 DRY RUN MODE - No changes will be made\n")
# Create manager and apply rules
manager = GiteaBranchProtectionManager(GITEA_URL, GITEA_TOKEN, GITEA_OWNER)
manager.apply_to_all_repos(config, dry_run=dry_run)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,356 @@
# Deployment Strategies for Gitea CI/CD
Guide to choosing and implementing deployment automation for your projects.
## 🎯 Decision Framework
### Consider These Factors
1. **Criticality**: How important is uptime?
- Discord bot for active league: HIGH
- Personal project: LOW
- Public API: VERY HIGH
2. **Test Coverage**: How confident are you in your tests?
- Comprehensive unit + integration tests: AUTO-DEPLOY OK
- Some tests: SEMI-AUTO
- No tests: MANUAL ONLY
3. **Rollback Complexity**: Can you easily revert?
- Docker with versioned images: EASY
- Database migrations: HARD
- Stateful services: MEDIUM
4. **Traffic Patterns**: When can you deploy?
- 24/7 users: Need zero-downtime
- Business hours only: Deploy off-hours
- Low traffic: Restart acceptable
## 📊 Deployment Options Comparison
| Strategy | Speed | Safety | Complexity | Best For |
|----------|-------|--------|------------|----------|
| **Manual** | Slow | High | Low | Critical systems, low test coverage |
| **Semi-Auto** | Medium | High | Low | Most projects (recommended) |
| **Auto + Health** | Fast | Medium | Medium | Good test coverage, health endpoints |
| **Blue-Green** | Fast | High | High | Zero-downtime required |
| **Canary** | Fast | Very High | Very High | Large user base, gradual rollout |
## 🚀 Recommended Progression
### Level 1: Manual Deploy (Start Here)
```bash
# Simple, safe, controllable
ssh production
docker compose pull
docker compose up -d
```
**Pros:**
- ✅ Full control over timing
- ✅ Can test/verify first
- ✅ Simple to understand
- ✅ Easy to abort
**Cons:**
- ❌ Requires manual SSH
- ❌ Inconsistent process
- ❌ Slower deployments
### Level 2: Deploy Script (Easy Win)
```bash
# One command from local machine
./deploy.sh v1.8.1
```
**Pros:**
- ✅ Consistent process
- ✅ Pre-deployment checks
- ✅ Still manual control
- ✅ Rollback instructions
**Cons:**
- ❌ Still requires human
- ❌ Can't deploy from CI
**Setup:** Copy `/tmp/deploy.sh` to your repo root
### Level 3: CI Notifies, Manual Deploy
```yaml
# Workflow builds, then notifies:
- Discord: "v1.8.1 ready to deploy"
- You run: ./deploy.sh v1.8.1
```
**Pros:**
- ✅ Fast build process
- ✅ Clear deployment signal
- ✅ Choose deployment timing
- ✅ Audit trail
**Cons:**
- ❌ Two-step process
- ❌ Can forget to deploy
**Current State:** This is where Paper Dynasty is now ✅
### Level 4: Auto-Deploy with Safeguards
```yaml
# Workflow builds, tests, deploys, health checks
- Build image
- Push to Docker Hub
- SSH to production
- Pull and restart
- Health check
- Rollback on failure
- Notify Discord
```
**Pros:**
- ✅ Fully automated
- ✅ Fast deployment
- ✅ Automatic rollback
- ✅ Consistent process
**Cons:**
- ❌ Complexity
- ❌ Brief downtime
- ❌ Requires good health checks
**Setup:** Use `/tmp/safe-auto-deploy-step.yml`
### Level 5: Blue-Green Deployment
```yaml
# Run two environments, switch traffic
- Deploy to "green" environment
- Health check green
- Switch load balancer to green
- Keep "blue" as instant rollback
```
**Pros:**
- ✅ Zero downtime
- ✅ Instant rollback
- ✅ Test before switching
- ✅ Very safe
**Cons:**
- ❌ Requires two environments
- ❌ Needs load balancer
- ❌ Database complexity
- ❌ 2x resources
**Best For:** High-traffic web services
## 🎯 Recommendations by Project Type
### Discord Bot (Paper Dynasty)
**Recommended:** Level 3 (CI Notify + Manual Deploy)
**Why?**
- Active users (league members)
- Brief downtime acceptable (1-2 min)
- Manual timing is valuable
- Can deploy during off-hours
**Next Step:** Add deploy script for consistency
### Internal Tool / Dashboard
**Recommended:** Level 4 (Auto-Deploy with Health Check)
**Why?**
- Limited users
- Downtime less critical
- Faster iteration valuable
- Easy rollback available
### Public API / High Traffic
**Recommended:** Level 5 (Blue-Green) or Canary
**Why?**
- Zero downtime required
- Large user base
- Complex rollback scenarios
- Worth the complexity
### Personal Project / Portfolio
**Recommended:** Level 2 or 3
**Why?**
- Simple is better
- Automation overhead not worth it
- Infrequent deploys
- Learning opportunity
## 🛠️ Implementation Guide
### Adding Auto-Deploy to Paper Dynasty
If you decide to try auto-deploy:
1. **Add deploy SSH key to secrets:**
```bash
# Generate deploy-only SSH key
ssh-keygen -t ed25519 -f ~/.ssh/paper_dynasty_deploy -C "gitea-deploy"
# Add public key to sba-bots
ssh-copy-id -i ~/.ssh/paper_dynasty_deploy.pub cal@10.10.0.88
# Add private key to Gitea secrets
cat ~/.ssh/paper_dynasty_deploy
# Copy to: Repo → Settings → Secrets → DEPLOY_SSH_KEY
```
2. **Add production host secret:**
- Secret name: `PRODUCTION_HOST`
- Value: `10.10.0.88`
3. **Add deploy user secret:**
- Secret name: `DEPLOY_USER`
- Value: `cal`
4. **Add deploy step to workflow:**
- Copy from `/tmp/safe-auto-deploy-step.yml`
- Paste after "Build Docker image" step
- Update webhook URL
5. **Test with a minor change:**
- Make a small PR (comment change)
- Merge and watch deployment
- Verify bot restarts successfully
- Check Discord for notifications
6. **Monitor first few deployments:**
- Watch for issues
- Check logs: `ssh sba-bots 'docker compose logs'`
- Verify no errors
### Adding Health Checks
For better auto-deploy safety, add a health endpoint to your bot:
```javascript
// Express health check endpoint
app.get('/health', (req, res) => {
// Check Discord connection
if (!client.ws.ping) {
return res.status(503).json({ status: 'unhealthy', reason: 'Discord disconnected' });
}
// Check database connection
if (!db.isConnected()) {
return res.status(503).json({ status: 'unhealthy', reason: 'Database disconnected' });
}
res.json({
status: 'healthy',
version: process.env.VERSION,
uptime: process.uptime(),
discord: {
guilds: client.guilds.cache.size,
ping: client.ws.ping
}
});
});
```
Then health check in deploy script:
```bash
curl -f http://localhost:3000/health || rollback
```
## ⚠️ Safety Best Practices
### Before Auto-Deploying
1. **✅ Add Comprehensive Tests**
- Unit tests for core logic
- Integration tests for Discord API
- Smoke tests for critical paths
2. **✅ Implement Health Checks**
- HTTP endpoint for status
- Check all critical services
- Return proper status codes
3. **✅ Set Up Monitoring**
- Error tracking (Sentry)
- Performance monitoring (Datadog)
- Discord webhook for errors
4. **✅ Plan Rollback Process**
- Keep last 3 images cached
- Document rollback steps
- Test rollback procedure
5. **✅ Document Deployment**
- What gets deployed
- How to abort
- How to rollback
- Who to contact
### During Deployment
1. **Monitor Logs**
```bash
docker compose logs -f
```
2. **Check Metrics**
- Response times
- Error rates
- Resource usage
3. **Verify Functionality**
- Test critical commands
- Check database connections
- Verify integrations
### After Deployment
1. **Watch for Issues**
- First 5 minutes are critical
- Check error channels
- Monitor user reports
2. **Validate Success**
- All services healthy
- No error spike
- Performance normal
3. **Document Issues**
- What went wrong
- How you fixed it
- How to prevent
## 🔄 Rollback Procedures
### Quick Rollback (Docker)
```bash
ssh production
cd /path/to/app
docker compose down
docker tag yourusername/repo:current yourusername/repo:rollback-backup
docker pull yourusername/repo:v1.7.0 # Last known good
docker compose up -d
```
### Emergency Rollback (CI)
```yaml
# Manually trigger workflow with old version
git tag -f v1.7.0-redeploy
git push -f origin v1.7.0-redeploy
# Watch actions deploy old version
```
## 📚 Further Reading
- [Deployment Strategies](https://www.redhat.com/en/topics/devops/deployment-strategies)
- [Blue-Green Deployments](https://martinfowler.com/bliki/BlueGreenDeployment.html)
- [Canary Releases](https://martinfowler.com/bliki/CanaryRelease.html)
- [Docker Health Checks](https://docs.docker.com/engine/reference/builder/#healthcheck)
---
**Last Updated:** 2026-02-04
**Applies To:** Paper Dynasty Discord bot, future Gitea CI/CD projects

View File

@ -0,0 +1,377 @@
# Harbor Docker Registry Setup Guide
Complete guide to setting up Harbor on a Proxmox LXC for self-hosted Docker registry.
## Prerequisites
- Proxmox LXC with Ubuntu 22.04
- 2 CPU cores, 4GB RAM, 50GB disk
- Docker and docker-compose installed
- Domain name (e.g., registry.manticorum.com)
## Quick Setup
### 1. Create LXC Container
```bash
# On Proxmox host
pct create 227 local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst \
--hostname harbor \
--cores 2 \
--memory 4096 \
--swap 512 \
--net0 name=eth0,bridge=vmbr0,ip=10.10.0.227/24,gw=10.10.0.1 \
--rootfs local-lvm:50 \
--unprivileged 1 \
--features nesting=1 \
--onboot 1 \
--start 1
```
### 2. Install Docker
```bash
ssh root@10.10.0.227
apt update && apt install -y curl
curl -fsSL https://get.docker.com | sh
systemctl enable docker
```
### 3. Download Harbor
```bash
cd /opt
wget https://github.com/goharbor/harbor/releases/download/v2.10.0/harbor-offline-installer-v2.10.0.tgz
tar xzvf harbor-offline-installer-v2.10.0.tgz
cd harbor
```
### 4. Configure Harbor
```bash
cp harbor.yml.tmpl harbor.yml
# Edit harbor.yml
nano harbor.yml
```
**Key settings to change:**
```yaml
hostname: registry.manticorum.com # Your domain
# HTTPS (configure after NPM setup, start with HTTP for now)
# https:
# port: 443
# certificate: /path/to/cert
# private_key: /path/to/key
# Or disable HTTPS initially
# Comment out entire https section
harbor_admin_password: YourSecurePassword123
database:
password: YourDBPassword123
data_volume: /mnt/harbor-data
```
### 5. Install Harbor
```bash
./install.sh
```
### 6. Access Harbor
Open: `http://10.10.0.227` (or `http://registry.manticorum.com` if DNS configured)
**Default login:**
- Username: `admin`
- Password: `YourSecurePassword123` (what you set)
### 7. Configure NPM Reverse Proxy
In Nginx Proxy Manager (10.10.0.16):
**Proxy Host:**
- Domain: `registry.manticorum.com`
- Scheme: `http`
- Forward Hostname: `10.10.0.227`
- Forward Port: `80`
- Websockets: ✅ Enabled
- Block Common Exploits: ✅ Enabled
- SSL: Let's Encrypt
**Custom Nginx Configuration:**
```nginx
# Increase timeouts for large image uploads
proxy_read_timeout 900;
proxy_send_timeout 900;
client_max_body_size 0; # No upload limit
# Required for Docker registry
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
```
## Using Your Registry
### 1. Login from Dev Machine
```bash
docker login registry.manticorum.com
# Username: admin
# Password: YourSecurePassword123
```
### 2. Tag and Push Image
```bash
# Tag existing image
docker tag manticorum67/paper-dynasty:latest registry.manticorum.com/paper-dynasty/bot:latest
# Push to your registry
docker push registry.manticorum.com/paper-dynasty/bot:latest
```
### 3. Pull from Production
```bash
# On sba-bots
docker login registry.manticorum.com
docker pull registry.manticorum.com/paper-dynasty/bot:latest
```
### 4. Update docker-compose
```yaml
services:
paper-dynasty:
# Old: image: manticorum67/paper-dynasty:latest
# New:
image: registry.manticorum.com/paper-dynasty/bot:latest
```
## Integrating with Gitea Actions
Update your workflow to push to both registries:
```yaml
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Harbor
uses: docker/login-action@v3
with:
registry: registry.manticorum.com
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.ref == 'refs/heads/main' }}
tags: |
manticorum67/paper-dynasty:latest
manticorum67/paper-dynasty:v${{ steps.meta.outputs.version }}
registry.manticorum.com/paper-dynasty/bot:latest
registry.manticorum.com/paper-dynasty/bot:v${{ steps.meta.outputs.version }}
```
## Harbor Features
### Create Projects
1. Login to Harbor UI
2. Click **New Project**
3. Name: `paper-dynasty`
4. Access Level: Private or Public
### Enable Vulnerability Scanning
1. Go to **Administration** → **Interrogation Services**
2. Enable **Trivy** scanner
3. Set scan on push: ✅ Enabled
Now images are auto-scanned for CVEs!
### Set Up Replication
Replicate between Harbor and Docker Hub:
1. **Administration** → **Replications**
2. **New Replication Rule**
- Name: `sync-to-dockerhub`
- Source: Local
- Destination: Docker Hub (add endpoint first)
- Trigger: Event Based
### Garbage Collection
Free up disk space from deleted images:
1. **Administration** → **Garbage Collection**
2. Schedule: Daily at 2 AM
3. Dry run first to see what would be deleted
## Backup Strategy
### What to Backup
1. **Harbor database** (PostgreSQL)
2. **Image storage** (`/mnt/harbor-data`)
3. **Configuration** (`/opt/harbor/harbor.yml`)
### Backup Script
```bash
#!/bin/bash
BACKUP_DIR="/mnt/backups/harbor"
DATE=$(date +%Y%m%d)
# Stop Harbor
cd /opt/harbor
docker-compose down
# Backup database
docker exec harbor-db pg_dumpall -U postgres > $BACKUP_DIR/harbor-db-$DATE.sql
# Backup data (incremental)
rsync -av /mnt/harbor-data/ $BACKUP_DIR/harbor-data/
# Backup config
cp /opt/harbor/harbor.yml $BACKUP_DIR/harbor-config-$DATE.yml
# Start Harbor
docker-compose up -d
# Keep last 7 days
find $BACKUP_DIR -name "harbor-db-*.sql" -mtime +7 -delete
```
## Monitoring
### Check Harbor Status
```bash
cd /opt/harbor
docker-compose ps
# View logs
docker-compose logs -f
```
### Disk Usage
```bash
du -sh /mnt/harbor-data
# By project
du -sh /mnt/harbor-data/docker/registry/v2/repositories/*
```
### API Health Check
```bash
curl -k https://registry.manticorum.com/api/v2.0/health
```
## Troubleshooting
### "401 Unauthorized" on push
**Problem:** Docker login not working
**Solution:**
```bash
# Clear old credentials
rm ~/.docker/config.json
# Login again
docker login registry.manticorum.com
```
### "413 Request Entity Too Large"
**Problem:** Nginx upload limit
**Solution:** Add to NPM custom config:
```nginx
client_max_body_size 0;
```
### Disk space full
**Problem:** Old images filling disk
**Solution:**
```bash
# Run garbage collection
cd /opt/harbor
docker-compose exec core /harbor/garbage-collection.sh
# Or via UI: Administration → Garbage Collection → Run Now
```
### Can't pull from registry
**Problem:** Firewall or network issue
**Solution:**
```bash
# Test connection
telnet 10.10.0.227 80
# Check Harbor logs
docker-compose logs registry
```
## Advanced: High Availability
For production-critical registries, set up HA:
1. Multiple Harbor instances
2. Shared storage (NFS, S3, Minio)
3. Load balancer in front
4. Database replication
## Cost Analysis
**LXC Resources:**
- CPU: 2 cores = $0 (spare capacity)
- RAM: 4GB = $0 (spare capacity)
- Disk: 50GB = $0 (local storage)
- Bandwidth: Internal = $0
**Total ongoing cost: $0/month**
**Docker Hub Pro alternative: $5/month**
**Time investment:**
- Setup: 2-3 hours
- Maintenance: 30 min/month
- Break-even: 3 months of learning value
## Next Steps
1. ✅ Set up Harbor on LXC 227
2. ✅ Configure NPM reverse proxy
3. ✅ Test push/pull from dev machine
4. ✅ Update one project to use Harbor
5. ✅ Set up Gitea Actions to push to both registries
6. ✅ Configure vulnerability scanning
7. ✅ Set up automated backups
---
**Created:** 2026-02-04
**For:** Manticorum Home Lab
**Reference:** Paper Dynasty as first use case

View File

@ -0,0 +1,221 @@
# Gitea Actions Workflow Templates
Reusable CI/CD workflow templates for Gitea Actions (GitHub Actions compatible).
## Templates
### `docker-build-template.yml`
Complete Docker build pipeline with semantic versioning validation, Docker Hub push, and Discord notifications.
**Features:**
- ✅ Semantic version validation on PRs
- ✅ Docker build on every push/PR
- ✅ Push to Docker Hub on main branch
- ✅ Discord notifications (success/failure)
- ✅ Build caching for faster builds
- ✅ Multi-tag strategy (latest, version, version+commit)
**Reference Implementation:**
Paper Dynasty Discord bot - First production use (2026-02-04)
## Quick Start
1. **Copy template to your repo:**
```bash
mkdir -p .gitea/workflows
cp docker-build-template.yml .gitea/workflows/docker-build.yml
```
2. **Customize placeholders:**
- Replace `yourusername/yourrepo` with your Docker Hub repository
- Replace `Your Project` in notification titles
- Replace `YOUR_DISCORD_WEBHOOK_URL_HERE` with your webhook URLs
3. **Add Gitea secrets:**
- Go to your repo → Settings → Secrets → Actions
- Add `DOCKERHUB_USERNAME` (your Docker Hub username)
- Add `DOCKERHUB_TOKEN` (access token from hub.docker.com)
4. **Create VERSION file:**
```bash
echo "1.0.0" > VERSION
git add VERSION
git commit -m "Add initial VERSION file"
```
5. **Push and test:**
- Create a PR to test version validation
- Merge to main to test Docker push and notifications
## Customization Guide
### Disable Features
**Don't want version validation?**
- Delete the "Check VERSION was bumped" step
**Don't want Discord notifications?**
- Delete both "Discord Notification" steps
**Don't want Docker Hub push?**
- Remove "Login to Docker Hub" step
- Change `push: ${{ github.ref == 'refs/heads/main' }}` to `push: false`
### Customize Version Validation
The template enforces strict semantic versioning. To modify:
**Allow any version bump:**
```bash
# Remove the validation logic, just check if changed:
if [ "$PR_VERSION" = "$MAIN_VERSION" ]; then
echo "❌ VERSION unchanged"
exit 1
fi
echo "✅ VERSION changed: $MAIN_VERSION → $PR_VERSION"
```
**Allow pre-release versions:**
```bash
# Modify parsing to handle versions like "1.2.3-beta"
IFS='-' read -r VERSION_NUMBER PRERELEASE <<< "$PR_VERSION"
```
### Add More Notifications
**Slack webhook:**
```yaml
- name: Slack Notification
if: success() && github.ref == 'refs/heads/main'
run: |
curl -X POST YOUR_SLACK_WEBHOOK_URL \
-H 'Content-Type: application/json' \
-d '{"text": "Build succeeded: v${{ steps.meta.outputs.version }}"}'
```
**Email notification:**
```yaml
- name: Email Notification
if: failure()
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.gmail.com
server_port: 465
username: ${{ secrets.EMAIL_USERNAME }}
password: ${{ secrets.EMAIL_PASSWORD }}
subject: Build Failed - ${{ github.repository }}
body: Build failed on commit ${{ github.sha }}
to: you@example.com
```
## Troubleshooting
### Version Validation Issues
**Problem:** PR fails validation but VERSION was bumped
**Solution:** Check VERSION file format - should be just `1.2.3` with no prefix, suffix, or extra text
**Problem:** Validation allows invalid bumps
**Solution:** Version parsing may be failing - check for special characters in VERSION file
### Docker Hub Push Issues
**Problem:** Login fails with "unauthorized"
**Solution:** Regenerate Docker Hub access token at hub.docker.com → Settings → Security
**Problem:** Push fails with "denied"
**Solution:** Check repository name matches exactly (case-sensitive)
**Problem:** Tags not appearing on Docker Hub
**Solution:** Wait a few seconds and refresh - tags may take a moment to appear
### Discord Notification Issues
**Problem:** Webhook succeeds but no message appears
**Solution:**
1. Check timestamp format is ISO 8601: `YYYY-MM-DDTHH:MM:SSZ`
2. Test webhook manually with curl
3. Verify webhook hasn't been deleted in Discord
**Problem:** Message appears malformed
**Solution:** Check for unescaped quotes or special characters in message content
**Problem:** Rate limited
**Solution:** Discord limits webhooks to ~5 messages per second - add delays if sending multiple
## Advanced Usage
### Multi-Stage Builds
Add test/lint steps before build:
```yaml
- name: Run tests
run: |
npm install
npm test
- name: Lint code
run: npm run lint
- name: Build Docker image
# ... existing build step
```
### Deploy After Build
Add deployment to production:
```yaml
- name: Deploy to production
if: success() && github.ref == 'refs/heads/main'
run: |
ssh production "docker pull yourusername/yourrepo:latest && docker-compose up -d"
```
### Multiple Docker Registries
Push to multiple registries:
```yaml
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@v5
with:
push: ${{ github.ref == 'refs/heads/main' }}
tags: |
yourusername/yourrepo:latest
ghcr.io/yourusername/yourrepo:latest
```
## Template Updates
This template was created based on the Paper Dynasty Discord bot workflow and represents battle-tested CI/CD practices. Future improvements might include:
- [ ] Automatic changelog generation from commits
- [ ] Security scanning (Trivy, Snyk)
- [ ] Multi-architecture builds (ARM, AMD64)
- [ ] Deployment strategies (blue-green, canary)
- [ ] Integration testing with docker-compose
- [ ] Performance benchmarking
## Contributing
Found a bug or improvement? Update this template and document the change in this README.
## License
Free to use and modify for any project.
---
**Template Version:** 1.0.0
**Last Updated:** 2026-02-04
**Maintained By:** Manticorum Home Lab

View File

@ -0,0 +1,82 @@
#!/bin/bash
# Paper Dynasty - Manual Deployment Script
#
# Usage: ./deploy.sh [version]
# Example: ./deploy.sh v1.8.1
#
# This script provides a safe, manual way to deploy Paper Dynasty
# with proper checks and rollback capability.
set -e
VERSION=${1:-latest}
SERVER="sba-bots"
SERVER_IP="10.10.0.88"
DEPLOY_PATH="/home/cal/container-data/paper-dynasty"
echo "🚀 Deploying Paper Dynasty ${VERSION} to production"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Pre-deployment checks
echo ""
echo "📋 Pre-deployment checks..."
# Check SSH connection
if ! ssh cal@${SERVER_IP} "echo 'SSH OK'" > /dev/null 2>&1; then
echo "❌ Cannot connect to ${SERVER}"
exit 1
fi
echo "✅ SSH connection OK"
# Check if container exists
if ! ssh cal@${SERVER_IP} "cd ${DEPLOY_PATH} && docker compose ps" > /dev/null 2>&1; then
echo "❌ Cannot find Paper Dynasty container on ${SERVER}"
exit 1
fi
echo "✅ Container found"
# Confirm deployment
echo ""
echo "⚠️ This will restart the Paper Dynasty bot (brief downtime)"
read -p "Continue with deployment? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "❌ Deployment cancelled"
exit 1
fi
# Deploy
echo ""
echo "📥 Pulling image manticorum67/paper-dynasty:${VERSION}..."
ssh cal@${SERVER_IP} << EOF
cd ${DEPLOY_PATH}
# Pull new image
docker compose pull
# Stop old container
echo "🛑 Stopping container..."
docker compose down
# Start new container
echo "▶️ Starting container..."
docker compose up -d
# Wait for startup
sleep 5
# Check status
echo ""
echo "📊 Container status:"
docker compose ps
echo ""
echo "📝 Recent logs:"
docker compose logs --tail 20
EOF
echo ""
echo "✅ Deployment complete!"
echo ""
echo "To check logs: ssh ${SERVER} 'cd ${DEPLOY_PATH} && docker compose logs -f'"
echo "To rollback: ssh ${SERVER} 'cd ${DEPLOY_PATH} && docker compose down && docker compose up -d'"

View File

@ -0,0 +1,392 @@
# Gitea Actions: Docker Build, Push, and Notify
#
# This workflow provides a complete CI/CD pipeline for Docker-based projects:
# - Validates semantic versioning on PRs
# - Builds Docker images on every push/PR
# - Pushes to Docker Hub on main branch merges
# - Sends Discord notifications on success/failure
#
# Template created: 2026-02-04
# For: Paper Dynasty Discord bot (reference implementation)
name: Build Docker Image
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
# ==============================================
# 1. CHECKOUT CODE
# ==============================================
- name: Checkout code
uses: actions/checkout@v4
# ==============================================
# 2. SEMANTIC VERSION VALIDATION (PRs only)
# ==============================================
# Enforces proper semantic versioning:
# - Blocks PRs that don't bump VERSION file
# - Validates version changes follow semver rules
# - Prevents skipping versions or going backwards
#
# Valid bumps:
# - Patch: 1.2.3 → 1.2.4 (bug fixes)
# - Minor: 1.2.3 → 1.3.0 (new features)
# - Major: 1.2.3 → 2.0.0 (breaking changes)
#
# Invalid bumps:
# - 1.2.3 → 1.4.0 (skipped minor version)
# - 1.2.3 → 1.2.0 (went backwards)
# - 1.2.3 → 1.3.1 (didn't reset patch)
#
- name: Check VERSION was bumped (semantic versioning)
if: github.event_name == 'pull_request'
run: |
# Get VERSION from this PR branch
PR_VERSION=$(cat VERSION 2>/dev/null || echo "0.0.0")
# Get VERSION from main branch
git fetch origin main:main
MAIN_VERSION=$(git show main:VERSION 2>/dev/null || echo "0.0.0")
echo "📋 Semantic Version Check"
echo "Main branch version: $MAIN_VERSION"
echo "PR branch version: $PR_VERSION"
echo ""
# Parse versions into components
IFS='.' read -r MAIN_MAJOR MAIN_MINOR MAIN_PATCH <<< "$MAIN_VERSION"
IFS='.' read -r PR_MAJOR PR_MINOR PR_PATCH <<< "$PR_VERSION"
# Remove any non-numeric characters
MAIN_MAJOR=${MAIN_MAJOR//[!0-9]/}
MAIN_MINOR=${MAIN_MINOR//[!0-9]/}
MAIN_PATCH=${MAIN_PATCH//[!0-9]/}
PR_MAJOR=${PR_MAJOR//[!0-9]/}
PR_MINOR=${PR_MINOR//[!0-9]/}
PR_PATCH=${PR_PATCH//[!0-9]/}
# Check if VERSION unchanged
if [ "$PR_VERSION" = "$MAIN_VERSION" ]; then
echo "❌ ERROR: VERSION file has not been updated!"
echo ""
echo "Please update the VERSION file in your PR."
echo "Current version: $MAIN_VERSION"
exit 1
fi
# Validate semantic version bump
VALID=false
BUMP_TYPE=""
# Check for major version bump (X.0.0)
if [ "$PR_MAJOR" -eq $((MAIN_MAJOR + 1)) ] && [ "$PR_MINOR" -eq 0 ] && [ "$PR_PATCH" -eq 0 ]; then
VALID=true
BUMP_TYPE="major"
# Check for minor version bump (x.X.0)
elif [ "$PR_MAJOR" -eq "$MAIN_MAJOR" ] && [ "$PR_MINOR" -eq $((MAIN_MINOR + 1)) ] && [ "$PR_PATCH" -eq 0 ]; then
VALID=true
BUMP_TYPE="minor"
# Check for patch version bump (x.x.X)
elif [ "$PR_MAJOR" -eq "$MAIN_MAJOR" ] && [ "$PR_MINOR" -eq "$MAIN_MINOR" ] && [ "$PR_PATCH" -eq $((MAIN_PATCH + 1)) ]; then
VALID=true
BUMP_TYPE="patch"
fi
if [ "$VALID" = true ]; then
echo "✅ Valid $BUMP_TYPE version bump: $MAIN_VERSION → $PR_VERSION"
else
echo "❌ ERROR: Invalid semantic version change!"
echo ""
echo "Current version: $MAIN_VERSION"
echo "PR version: $PR_VERSION"
echo ""
echo "Valid version bumps:"
echo " - Patch: $MAIN_MAJOR.$MAIN_MINOR.$((MAIN_PATCH + 1))"
echo " - Minor: $MAIN_MAJOR.$((MAIN_MINOR + 1)).0"
echo " - Major: $((MAIN_MAJOR + 1)).0.0"
echo ""
echo "Common issues:"
echo " ❌ Skipping versions (e.g., 2.5.0 → 2.7.0)"
echo " ❌ Going backwards (e.g., 2.5.0 → 2.4.0)"
echo " ❌ Not resetting lower components (e.g., 2.5.0 → 2.6.1)"
exit 1
fi
# ==============================================
# 3. DOCKER BUILDX SETUP
# ==============================================
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# ==============================================
# 4. DOCKER HUB LOGIN (main branch only)
# ==============================================
# Requires secrets in Gitea:
# - DOCKERHUB_USERNAME: Your Docker Hub username
# - DOCKERHUB_TOKEN: Docker Hub access token (not password!)
#
# To create token:
# 1. Go to hub.docker.com
# 2. Account Settings → Security → New Access Token
# 3. Copy token to Gitea repo → Settings → Secrets → Actions
#
- name: Login to Docker Hub
if: github.ref == 'refs/heads/main'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# ==============================================
# 5. EXTRACT METADATA
# ==============================================
# Reads VERSION file and generates image tags:
# - version: From VERSION file (e.g., "1.2.3")
# - sha_short: First 7 chars of commit SHA
# - version_sha: Combined version+commit (e.g., "v1.2.3-a1b2c3d")
# - branch: Current branch name
# - timestamp: ISO 8601 format for Discord
#
- name: Extract metadata
id: meta
run: |
VERSION=$(cat VERSION 2>/dev/null || echo "0.0.0")
SHA_SHORT=$(echo ${{ github.sha }} | cut -c1-7)
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "sha_short=${SHA_SHORT}" >> $GITHUB_OUTPUT
echo "version_sha=v${VERSION}-${SHA_SHORT}" >> $GITHUB_OUTPUT
echo "branch=${{ github.ref_name }}" >> $GITHUB_OUTPUT
echo "timestamp=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
# ==============================================
# 6. BUILD AND PUSH DOCKER IMAGE
# ==============================================
# Creates 3 tags for each build:
# - latest: Always points to newest build
# - v{VERSION}: Semantic version from VERSION file
# - v{VERSION}-{COMMIT}: Version + commit hash for traceability
#
# Example tags:
# - yourusername/yourrepo:latest
# - yourusername/yourrepo:v1.2.3
# - yourusername/yourrepo:v1.2.3-a1b2c3d
#
# Push behavior:
# - PRs: Build only (test), don't push
# - Main: Build and push to Docker Hub
#
# CUSTOMIZE: Replace "yourusername/yourrepo" with your Docker Hub repo
#
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.ref == 'refs/heads/main' }}
tags: |
yourusername/yourrepo:latest
yourusername/yourrepo:v${{ steps.meta.outputs.version }}
yourusername/yourrepo:${{ steps.meta.outputs.version_sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ==============================================
# 7. BUILD SUMMARY
# ==============================================
# Creates a formatted summary visible in Actions UI
# Shows: image tags, build details, push status
#
- name: Build Summary
run: |
echo "## 🐳 Docker Build Successful! ✅" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Image Tags:**" >> $GITHUB_STEP_SUMMARY
echo "- \`yourusername/yourrepo:latest\`" >> $GITHUB_STEP_SUMMARY
echo "- \`yourusername/yourrepo:v${{ steps.meta.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
echo "- \`yourusername/yourrepo:${{ steps.meta.outputs.version_sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Build Details:**" >> $GITHUB_STEP_SUMMARY
echo "- Branch: \`${{ steps.meta.outputs.branch }}\`" >> $GITHUB_STEP_SUMMARY
echo "- Commit: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "- Timestamp: \`${{ steps.meta.outputs.timestamp }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "🚀 **Pushed to Docker Hub!**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Pull with: \`docker pull yourusername/yourrepo:latest\`" >> $GITHUB_STEP_SUMMARY
else
echo "_PR build - image not pushed to Docker Hub_" >> $GITHUB_STEP_SUMMARY
fi
# ==============================================
# 8. DISCORD NOTIFICATION - SUCCESS
# ==============================================
# Sends green embed to Discord on successful builds
#
# Only fires on main branch pushes (not PRs)
#
# Setup:
# 1. Create webhook in Discord channel:
# Right-click channel → Edit → Integrations → Webhooks → New
# 2. Copy webhook URL
# 3. Replace the URL below
#
# CUSTOMIZE:
# - Replace webhook URL with yours
# - Replace "Your Project" in title
# - Replace Docker Hub URLs with your repo
#
- name: Discord Notification - Success
if: success() && github.ref == 'refs/heads/main'
run: |
curl -H "Content-Type: application/json" \
-d '{
"embeds": [{
"title": "✅ Your Project Build Successful",
"description": "Docker image built and pushed to Docker Hub!",
"color": 3066993,
"fields": [
{
"name": "Version",
"value": "`v${{ steps.meta.outputs.version }}`",
"inline": true
},
{
"name": "Image Tag",
"value": "`${{ steps.meta.outputs.version_sha }}`",
"inline": true
},
{
"name": "Branch",
"value": "`${{ steps.meta.outputs.branch }}`",
"inline": true
},
{
"name": "Commit",
"value": "`${{ steps.meta.outputs.sha_short }}`",
"inline": true
},
{
"name": "Author",
"value": "${{ github.actor }}",
"inline": true
},
{
"name": "Docker Hub",
"value": "[yourusername/yourrepo](https://hub.docker.com/r/yourusername/yourrepo)",
"inline": false
},
{
"name": "View Run",
"value": "[Click here](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})",
"inline": false
}
],
"timestamp": "${{ steps.meta.outputs.timestamp }}"
}]
}' \
YOUR_DISCORD_WEBHOOK_URL_HERE
# ==============================================
# 9. DISCORD NOTIFICATION - FAILURE
# ==============================================
# Sends red embed to Discord on build failures
#
# Only fires on main branch pushes (not PRs)
#
# CUSTOMIZE:
# - Replace webhook URL with yours
# - Replace "Your Project" in title
#
- name: Discord Notification - Failure
if: failure() && github.ref == 'refs/heads/main'
run: |
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
curl -H "Content-Type: application/json" \
-d '{
"embeds": [{
"title": "❌ Your Project Build Failed",
"description": "Docker build encountered an error.",
"color": 15158332,
"fields": [
{
"name": "Branch",
"value": "`${{ github.ref_name }}`",
"inline": true
},
{
"name": "Commit",
"value": "`${{ github.sha }}`",
"inline": true
},
{
"name": "Author",
"value": "${{ github.actor }}",
"inline": true
},
{
"name": "View Logs",
"value": "[Click here](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})",
"inline": false
}
],
"timestamp": "'"$TIMESTAMP"'"
}]
}' \
YOUR_DISCORD_WEBHOOK_URL_HERE
# ==============================================
# CUSTOMIZATION CHECKLIST
# ==============================================
# Before using this template in a new project:
#
# ✅ Replace "yourusername/yourrepo" with your Docker Hub repository
# ✅ Replace "Your Project" in Discord notification titles
# ✅ Replace Discord webhook URLs (both success and failure)
# ✅ Add secrets to Gitea repo: DOCKERHUB_USERNAME, DOCKERHUB_TOKEN
# ✅ Create VERSION file in repo root with initial version (e.g., "1.0.0")
# ✅ Update branch name if not using "main"
#
# Optional customizations:
# - Adjust runner labels (runs-on) if using self-hosted runners
# - Modify version validation rules if you don't want strict semver
# - Add additional notification channels (Slack, email, etc.)
# - Add deployment steps after Docker push
# - Customize Discord embed colors, fields, or formatting
#
# ==============================================
# TROUBLESHOOTING
# ==============================================
# Common issues and solutions:
#
# 1. VERSION validation failing unexpectedly
# - Ensure VERSION file exists in repo root
# - Check file contains only version number (no 'v' prefix or extra text)
# - Verify version follows semver: MAJOR.MINOR.PATCH
#
# 2. Docker Hub push failing
# - Verify DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets are set
# - Check Docker Hub token has push permissions
# - Ensure repository name matches your Docker Hub repo exactly
#
# 3. Discord notifications not appearing
# - Test webhook URL manually with curl
# - Check webhook still exists in Discord channel settings
# - Verify timestamp format is ISO 8601 (YYYY-MM-DDTHH:MM:SSZ)
# - Look for HTTP error codes in Actions logs
#
# 4. Build cache not working
# - GitHub Actions cache is stored per repository
# - Cache is shared across branches
# - May need to clear cache if corrupted
#
# ==============================================

View File

@ -0,0 +1,167 @@
# ==============================================
# SAFE AUTO-DEPLOY WITH HEALTH CHECK & ROLLBACK
# ==============================================
# Enhanced deployment with safety features:
# - Health check after deployment
# - Automatic rollback on failure
# - Deployment notifications
# - Downtime tracking
#
- name: Deploy to Production (Safe)
if: success() && github.ref == 'refs/heads/main'
run: |
# Set up SSH
mkdir -p ~/.ssh
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H ${{ secrets.PRODUCTION_HOST }} >> ~/.ssh/known_hosts
echo "🚀 Deploying Paper Dynasty v${{ steps.meta.outputs.version }} to production..."
DEPLOY_START=$(date +%s)
# Deploy with health check and rollback
ssh -i ~/.ssh/deploy_key ${{ secrets.DEPLOY_USER }}@${{ secrets.PRODUCTION_HOST }} bash << 'EOF'
set -e
cd /home/cal/container-data/paper-dynasty
# Save current image tag for rollback
CURRENT_IMAGE=$(docker compose images -q paper-dynasty 2>/dev/null || echo "none")
echo "Current image: $CURRENT_IMAGE"
# Pull new image
echo "📥 Pulling new image..."
docker compose pull
# Stop old container
echo "🛑 Stopping old container..."
docker compose down
# Start new container
echo "▶️ Starting new container..."
docker compose up -d
# Health check with retry
echo "🏥 Running health check..."
for i in {1..10}; do
sleep 3
if docker compose ps | grep -q "Up"; then
echo "✅ Container is up!"
# Additional health check: check bot is responding
# Adjust this based on your bot's health endpoint
# if curl -f http://localhost:YOUR_PORT/health; then
# echo "✅ Health check passed!"
# exit 0
# fi
exit 0
fi
echo "Waiting for container... ($i/10)"
done
# If we get here, deployment failed
echo "❌ Health check failed! Rolling back..."
# Rollback to previous image
if [ "$CURRENT_IMAGE" != "none" ]; then
docker compose down
# This assumes you have the old image still cached
# In production, you might want to keep the last N images
docker compose up -d
echo "⏪ Rolled back to previous version"
exit 1
else
echo "⚠️ No previous image to rollback to!"
exit 1
fi
EOF
DEPLOY_STATUS=$?
DEPLOY_END=$(date +%s)
DEPLOY_TIME=$((DEPLOY_END - DEPLOY_START))
if [ $DEPLOY_STATUS -eq 0 ]; then
echo "✅ Deployment successful! (${DEPLOY_TIME}s)"
else
echo "❌ Deployment failed after ${DEPLOY_TIME}s"
exit 1
fi
# ==============================================
# DEPLOYMENT NOTIFICATION - SUCCESS
# ==============================================
- name: Discord Notification - Deployed Successfully
if: success() && github.ref == 'refs/heads/main'
run: |
curl -H "Content-Type: application/json" \
-d '{
"embeds": [{
"title": "🚀 Paper Dynasty Deployed to Production",
"description": "New version is live and healthy!",
"color": 5793266,
"fields": [
{
"name": "Version",
"value": "`v${{ steps.meta.outputs.version }}`",
"inline": true
},
{
"name": "Deployed By",
"value": "${{ github.actor }}",
"inline": true
},
{
"name": "Server",
"value": "sba-bots (10.10.0.88)",
"inline": true
},
{
"name": "Status",
"value": "✅ Health check passed",
"inline": false
}
],
"timestamp": "${{ steps.meta.outputs.timestamp }}"
}]
}' \
https://discord.com/api/webhooks/YOUR_WEBHOOK_URL
# ==============================================
# DEPLOYMENT NOTIFICATION - FAILED/ROLLED BACK
# ==============================================
- name: Discord Notification - Deployment Failed
if: failure() && github.ref == 'refs/heads/main'
run: |
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
curl -H "Content-Type: application/json" \
-d '{
"embeds": [{
"title": "⚠️ Paper Dynasty Deployment Failed",
"description": "Deployment failed and was rolled back to previous version.",
"color": 16776960,
"fields": [
{
"name": "Attempted Version",
"value": "`v${{ steps.meta.outputs.version }}`",
"inline": true
},
{
"name": "Author",
"value": "${{ github.actor }}",
"inline": true
},
{
"name": "Action",
"value": "🔄 Rolled back to previous version",
"inline": false
},
{
"name": "View Logs",
"value": "[Click here](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})",
"inline": false
}
],
"timestamp": "'"$TIMESTAMP"'"
}]
}' \
https://discord.com/api/webhooks/YOUR_WEBHOOK_URL