diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..be9a57f --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,207 @@ +name: Deploy CV Site to Production + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + skip_tests: + description: 'Skip tests' + required: false + default: false + type: boolean + +env: + GO_VERSION: '1.25' + APP_NAME: 'cv-server' + SERVICE_NAME: 'cv' + DEPLOY_PATH: '/home/txeo/Git/yo/cv' + HEALTH_URL: 'https://juan.andres.morenorub.io/health' + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + if: ${{ !inputs.skip_tests }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Download dependencies + run: go mod download + + - name: Run tests + run: | + go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... + + - name: Upload coverage + uses: codecov/codecov-action@v3 + with: + file: ./coverage.txt + fail_ci_if_error: false + + build: + name: Build Application + runs-on: ubuntu-latest + needs: test + if: always() && (needs.test.result == 'success' || needs.test.result == 'skipped') + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Build binary + run: | + mkdir -p build + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -ldflags="-w -s -X main.version=${{ github.sha }}" \ + -o build/${{ env.APP_NAME }} \ + . + + - name: Compress binary + run: | + cd build + tar -czf ${{ env.APP_NAME }}-${{ github.sha }}.tar.gz ${{ env.APP_NAME }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: cv-server-binary + path: build/${{ env.APP_NAME }}-${{ github.sha }}.tar.gz + retention-days: 30 + + deploy: + name: Deploy to Production + runs-on: ubuntu-latest + needs: build + environment: + name: production + url: ${{ env.HEALTH_URL }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: cv-server-binary + path: ./build + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -p ${{ secrets.SSH_PORT || 22 }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts + + - name: Deploy to Server + env: + SSH_KEY: ~/.ssh/deploy_key + SSH_USER: ${{ secrets.SSH_USER }} + SSH_HOST: ${{ secrets.SSH_HOST }} + SSH_PORT: ${{ secrets.SSH_PORT || 22 }} + run: | + # Extract artifact + cd build + tar -xzf ${{ env.APP_NAME }}-${{ github.sha }}.tar.gz + + # Upload binary to server + scp -i $SSH_KEY -P $SSH_PORT ${{ env.APP_NAME }} \ + $SSH_USER@$SSH_HOST:${{ env.DEPLOY_PATH }}/${{ env.APP_NAME }}.new + + # Upload deployment script + scp -i $SSH_KEY -P $SSH_PORT ../scripts/deploy.sh \ + $SSH_USER@$SSH_HOST:${{ env.DEPLOY_PATH }}/ + + # Sync static assets, templates, and data + echo "πŸ“¦ Syncing assets..." + rsync -avz -e "ssh -i $SSH_KEY -p $SSH_PORT" \ + --exclude '.git' \ + --exclude 'build' \ + --exclude 'node_modules' \ + --exclude '.env' \ + ../static/ ../templates/ ../data/ \ + $SSH_USER@$SSH_HOST:${{ env.DEPLOY_PATH }}/ 2>/dev/null || echo "Note: Some directories may not exist" + + # Execute deployment on server + ssh -i $SSH_KEY -p $SSH_PORT $SSH_USER@$SSH_HOST << 'ENDSSH' + cd ${{ env.DEPLOY_PATH }} + + # Make scripts executable + chmod +x deploy.sh 2>/dev/null || true + chmod +x ${{ env.APP_NAME }}.new + + # Export environment for deploy script + export DEPLOY_PATH="${{ env.DEPLOY_PATH }}" + + # Run deployment + ./deploy.sh ${{ env.APP_NAME }} ${{ env.SERVICE_NAME }} + ENDSSH + + - name: Health check + run: | + echo "Waiting for application to start..." + sleep 10 + + # Check service status + ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT || 22 }} \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \ + "systemctl is-active ${{ env.SERVICE_NAME }}" + + # HTTP health check + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" ${{ env.HEALTH_URL }}) + + if [ "$RESPONSE" = "200" ]; then + echo "βœ… Health check passed (HTTP $RESPONSE)" + else + echo "❌ Health check failed (HTTP $RESPONSE)" + exit 1 + fi + + - name: Send notification + if: always() + run: | + if [ -z "${{ secrets.SLACK_WEBHOOK }}" ]; then + echo "No Slack webhook configured, skipping notification" + exit 0 + fi + + STATUS="${{ job.status }}" + COLOR="good" + if [ "$STATUS" != "success" ]; then + COLOR="danger" + fi + + curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \ + -H "Content-Type: application/json" \ + -d "{ + \"attachments\": [{ + \"color\": \"$COLOR\", + \"title\": \"CV Site Deployment $STATUS\", + \"text\": \"Deployment to production completed\", + \"fields\": [ + {\"title\": \"Branch\", \"value\": \"${{ github.ref_name }}\", \"short\": true}, + {\"title\": \"Commit\", \"value\": \"${{ github.sha }}\", \"short\": true}, + {\"title\": \"Environment\", \"value\": \"production\", \"short\": true} + ] + }] + }" || true + + - name: Cleanup SSH keys + if: always() + run: | + rm -f ~/.ssh/deploy_key diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8670199 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,44 @@ +name: Test CV Site + +on: + pull_request: + branches: [main, develop] + push: + branches: [main, develop] + +jobs: + test: + name: Test on Go ${{ matrix.go-version }} + runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.24', '1.25'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache: true + + - name: Install dependencies + run: go mod download + + - name: Run linter + uses: golangci/golangci-lint-action@v3 + with: + version: latest + skip-cache: false + skip-pkg-cache: false + skip-build-cache: false + + - name: Run tests + run: | + go test -v -race -coverprofile=coverage.txt ./... + + - name: Build + run: | + go build -v . diff --git a/DEPLOYMENT_SETUP.md b/DEPLOYMENT_SETUP.md new file mode 100644 index 0000000..414bf68 --- /dev/null +++ b/DEPLOYMENT_SETUP.md @@ -0,0 +1,320 @@ +# Deployment Setup Guide + +This guide will help you configure GitHub Actions for automatic deployment of your CV site. + +## Overview + +The deployment system is now fully configured with: +- βœ… GitHub Actions workflows (`.github/workflows/`) +- βœ… Deployment scripts (`scripts/`) +- βœ… Systemd service configuration (`config/systemd/cv.service`) +- βœ… Updated Makefile with CI/CD targets + +## Required GitHub Secrets + +To enable automatic deployment, you need to configure these secrets in your GitHub repository: + +Go to: **Settings β†’ Secrets and variables β†’ Actions β†’ New repository secret** + +### Essential Secrets + +| Secret Name | Value | Description | +|------------|-------|-------------| +| `SSH_PRIVATE_KEY` | Your private SSH key | Used to connect to the production server | +| `SSH_HOST` | `localhost` or your server IP | Production server hostname | +| `SSH_USER` | `txeo` | SSH username for deployment | +| `SSH_PORT` | `22` (default) | SSH port (optional if using default) | + +### Optional Secrets + +| Secret Name | Description | +|------------|-------------| +| `SLACK_WEBHOOK` | Slack webhook URL for deployment notifications | + +## Setting Up SSH Key for GitHub Actions + +Since this machine is both the development and production server, you need to set up SSH access for GitHub Actions to deploy here. + +### Option 1: Deploy to localhost (Recommended for same-machine setup) + +If GitHub Actions will run on the same machine, you can use localhost: + +```bash +# Generate a deployment key (reusable for all projects) +ssh-keygen -t ed25519 -C "github-actions-deployment" -f ~/.ssh/github-deploy -N "" + +# Add the public key to authorized_keys +cat ~/.ssh/github-deploy.pub >> ~/.ssh/authorized_keys +chmod 600 ~/.ssh/authorized_keys + +# Copy the private key to add to GitHub Secrets (use this for ALL projects) +cat ~/.ssh/github-deploy +# Copy the entire output (including BEGIN and END lines) +``` + +**GitHub Secrets Configuration:** +- `SSH_PRIVATE_KEY`: (paste the private key output from above) +- `SSH_HOST`: `localhost` or `127.0.0.1` +- `SSH_USER`: `txeo` +- `SSH_PORT`: `22` + +### Option 2: Deploy from GitHub hosted runners + +If using GitHub's hosted runners to deploy to this server: + +```bash +# Generate a deployment key (reusable for all projects) +ssh-keygen -t ed25519 -C "github-actions-deployment" -f ~/.ssh/github-deploy -N "" + +# Add the public key to authorized_keys +cat ~/.ssh/github-deploy.pub >> ~/.ssh/authorized_keys + +# Get your public IP address +curl ifconfig.me + +# Copy the private key (use this for ALL projects) +cat ~/.ssh/github-deploy +``` + +**GitHub Secrets Configuration:** +- `SSH_PRIVATE_KEY`: (paste the private key output) +- `SSH_HOST`: (your public IP from `curl ifconfig.me`) +- `SSH_USER`: `txeo` +- `SSH_PORT`: `22` + +## Reusing Secrets Across Multiple Projects + +**Important**: If you have multiple projects deploying to the same server, use **one shared SSH key** for all of them. + +### Why One Shared Key? + +βœ… **Advantages:** +- **Simpler management** - One key to rotate/update instead of multiple +- **Same access level** - All projects deploy as the same user anyway +- **Easier setup** - Generate once, reuse everywhere +- **Standard practice** - CI/CD systems typically use one deploy key per server + +❌ **You don't need separate keys because:** +- All projects deploy to the same server +- All projects use the same user account (`txeo`) +- Separation is already handled by different deployment paths and services + +### Shared Secrets (Same for All 3+ Projects) + +Copy these exact same values to all your project repositories: +- `SSH_PRIVATE_KEY` - The `~/.ssh/github-deploy` key you generated once +- `SSH_HOST` - Same server (localhost or your IP) +- `SSH_USER` - Same user (`txeo`) +- `SSH_PORT` - Same SSH port (`22`) +- `SLACK_WEBHOOK` - Same notification channel (optional) + +### Project-Specific Configuration + +Only change these in each project's `.github/workflows/deploy.yml` (in the `env` section): + +| Project | APP_NAME | SERVICE_NAME | DEPLOY_PATH | PORT | +|---------|----------|--------------|-------------|------| +| CV Site | cv-server | cv | /home/txeo/Git/yo/cv | 1999 | +| Project 2 | project2-server | project2 | /home/txeo/Git/yo/project2 | 2000 | +| Project 3 | project3-server | project3 | /home/txeo/Git/yo/project3 | 2001 | + +### Setup Process + +1. **Generate SSH key once** on the server: + ```bash + ssh-keygen -t ed25519 -C "github-actions-deployment" -f ~/.ssh/github-deploy -N "" + cat ~/.ssh/github-deploy.pub >> ~/.ssh/authorized_keys + ``` + +2. **Copy the key** for use in all projects: + ```bash + cat ~/.ssh/github-deploy + ``` + +3. **Add to each repository's GitHub Secrets** (same values): + - Repository 1 (cv) β†’ Add secrets + - Repository 2 (project2) β†’ Add **same** secrets + - Repository 3 (project3) β†’ Add **same** secrets + +4. **Customize each workflow** by editing only the `env` section in `deploy.yml` + +## Service Configuration + +The systemd service configuration has been created at: +- **Template**: `config/systemd/cv.service` +- **Active service**: `/etc/systemd/system/cv.service` + +### Update the Active Service (if needed) + +If you want to use the new service template with better security settings: + +```bash +# Update the systemd service +make update-service + +# Or manually: +sudo cp config/systemd/cv.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl restart cv +sudo systemctl status cv +``` + +## Testing the Deployment Locally + +Before pushing to GitHub, you can test the deployment scripts locally: + +```bash +# Build the binary +make ci-build + +# Test the deployment script +cd build +tar -czf cv-server-test.tar.gz cv-server +cd .. +mv build/cv-server-test.tar.gz ./cv-server.new +chmod +x scripts/deploy.sh +./scripts/deploy.sh cv-server cv + +# Check service status +systemctl status cv + +# Test health check +make health-check +``` + +## Deployment Workflow + +Once configured, the deployment works automatically: + +1. **Push to main branch** β†’ Triggers deployment +2. **Run tests** β†’ Ensures code quality +3. **Build binary** β†’ Creates production binary +4. **Deploy to server** β†’ Uploads and installs new version +5. **Health check** β†’ Verifies deployment success +6. **Send notification** β†’ (if Slack webhook configured) + +### Manual Deployment + +You can also trigger deployment manually: + +1. Go to **Actions** tab in GitHub +2. Select **Deploy CV Site to Production** +3. Click **Run workflow** +4. Choose options (e.g., skip tests) +5. Click **Run workflow** + +## Rollback + +If a deployment fails or you need to rollback: + +```bash +# View available backups +./scripts/rollback.sh cv-server cv + +# Rollback to the most recent version +./scripts/rollback.sh cv-server cv latest + +# Rollback to specific version (e.g., version #2) +./scripts/rollback.sh cv-server cv 2 +``` + +## Monitoring + +### Check Service Status + +```bash +# Service status +systemctl status cv + +# View logs +journalctl -u cv -f + +# View recent logs +journalctl -u cv -n 50 --no-pager +``` + +### Check Application Health + +```bash +# Local health check +curl http://localhost:1999/health + +# Public health check +curl https://juan.andres.morenorub.io/health + +# Or use the make target +make health-check +``` + +## File Structure + +``` +cv/ +β”œβ”€β”€ .github/ +β”‚ └── workflows/ +β”‚ β”œβ”€β”€ deploy.yml # Main deployment workflow +β”‚ └── test.yml # Testing workflow +β”œβ”€β”€ scripts/ +β”‚ β”œβ”€β”€ deploy.sh # Deployment script +β”‚ β”œβ”€β”€ healthcheck.sh # Health check script +β”‚ └── rollback.sh # Rollback script +β”œβ”€β”€ config/ +β”‚ └── systemd/ +β”‚ └── cv.service # Systemd service template +β”œβ”€β”€ backups/ # (created during deployment) +β”‚ └── cv-server.* # Binary backups +└── Makefile # Build and deployment targets +``` + +## Troubleshooting + +### Deployment Fails with SSH Error + +```bash +# Test SSH connection +ssh -p 22 txeo@localhost "echo 'SSH works'" + +# Check SSH key permissions +ls -la ~/.ssh/github-deploy +# Should show: -rw------- (600) +``` + +### Service Won't Start + +```bash +# Check service logs +sudo journalctl -u cv -n 50 + +# Test binary manually +./cv-server + +# Check port availability +sudo netstat -tlnp | grep 1999 +``` + +### Health Check Fails + +```bash +# Check if service is running +systemctl is-active cv + +# Test health endpoint +curl -v http://localhost:1999/health + +# Check firewall +sudo ufw status +``` + +## Next Steps + +1. **Add GitHub Secrets** as documented above +2. **Push to main branch** to trigger first deployment +3. **Monitor the deployment** in GitHub Actions tab +4. **Verify deployment** by checking https://juan.andres.morenorub.io + +## Support + +For issues or questions: +- Check GitHub Actions logs in the **Actions** tab +- Review systemd logs: `journalctl -u cv -f` +- Review the main deployment guide: `GITHUB_ACTIONS_DEPLOYMENT_GUIDE.md` diff --git a/GITHUB_ACTIONS_DEPLOYMENT_GUIDE.md b/GITHUB_ACTIONS_DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..7a3216c --- /dev/null +++ b/GITHUB_ACTIONS_DEPLOYMENT_GUIDE.md @@ -0,0 +1,996 @@ +# GitHub Actions Deployment Guide for CV Site (Go Project) + +> **Target Project**: `cv-site` (Go application) +> **Repository**: `github.com/juanatsap/cv-site` +> **Deployment**: VM-based deployment (similar to La Porra architecture) +> **Reference**: Based on La Porra's GitHub Actions workflows + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Required GitHub Secrets](#required-github-secrets) +3. [Workflow Files Structure](#workflow-files-structure) +4. [Main Deployment Workflow](#main-deployment-workflow) +5. [CDN Integration (Optional)](#cdn-integration-optional) +6. [Documentation Deployment](#documentation-deployment) +7. [Build Artifacts](#build-artifacts) +8. [Environment Configuration](#environment-configuration) +9. [Testing Strategy](#testing-strategy) +10. [Deployment Checklist](#deployment-checklist) + +--- + +## Overview + +This guide provides a complete GitHub Actions setup for deploying a Go-based application to a VM server. The workflow is based on La Porra's proven deployment patterns but adapted for Go applications. + +### Key Components + +- **CI/CD Pipeline**: Automated build, test, and deployment +- **Artifact Management**: Binary compilation and distribution +- **CDN Integration**: CloudFlare for static assets (optional) +- **Health Checks**: Post-deployment validation +- **Notifications**: Deployment status alerts + +--- + +## Required GitHub Secrets + +Configure these secrets in your repository settings (`Settings β†’ Secrets and variables β†’ Actions`): + +### Essential Secrets + +| Secret Name | Description | Example/Notes | +|-------------|-------------|---------------| +| `SSH_PRIVATE_KEY` | Private SSH key for VM access | Generate with `ssh-keygen -t ed25519` | +| `SSH_HOST` | VM server hostname or IP | `your-vm.example.com` or `192.168.1.100` | +| `SSH_USER` | SSH username for deployment | `deployer` or `ubuntu` | +| `SSH_PORT` | SSH port (if non-standard) | `22` (default) or custom port | +| `DEPLOY_PATH` | Application directory on VM | `/var/www/cv-site` or `/opt/cv-site` | + +### Optional Secrets (if using CDN) + +| Secret Name | Description | Required For | +|-------------|-------------|--------------| +| `CLOUDFLARE_API_TOKEN` | CloudFlare API token | CDN deployment | +| `CLOUDFLARE_ZONE_ID` | CloudFlare zone identifier | CDN deployment | +| `SLACK_WEBHOOK` | Slack webhook URL for notifications | Team notifications | + +### How to Generate SSH Keys + +```bash +# On your local machine, generate a key pair +ssh-keygen -t ed25519 -C "github-actions-cv-site" -f ~/.ssh/cv-site-deploy + +# Copy the public key to your VM +ssh-copy-id -i ~/.ssh/cv-site-deploy.pub user@your-vm.example.com + +# Add the PRIVATE key content to GitHub Secrets +cat ~/.ssh/cv-site-deploy +# Copy the entire output (including -----BEGIN and -----END lines) +``` + +--- + +## Workflow Files Structure + +Create these files in your repository: + +``` +cv-site/ +β”œβ”€β”€ .github/ +β”‚ └── workflows/ +β”‚ β”œβ”€β”€ deploy.yml # Main deployment workflow +β”‚ β”œβ”€β”€ cdn-deploy.yml # CDN optimization (optional) +β”‚ └── test.yml # CI testing workflow +β”œβ”€β”€ scripts/ +β”‚ β”œβ”€β”€ deploy.sh # Deployment script for VM +β”‚ β”œβ”€β”€ healthcheck.sh # Post-deployment validation +β”‚ └── rollback.sh # Rollback to previous version +β”œβ”€β”€ config/ +β”‚ └── systemd/ +β”‚ └── cv-site.service # Systemd service file +└── Makefile # Build targets +``` + +--- + +## Main Deployment Workflow + +### File: `.github/workflows/deploy.yml` + +```yaml +name: Deploy CV Site to VM + +on: + push: + branches: + - main + - production + workflow_dispatch: + inputs: + environment: + description: 'Deployment environment' + required: true + default: 'production' + type: choice + options: + - production + - staging + +env: + GO_VERSION: '1.22' + APP_NAME: 'cv-site' + BUILD_DIR: './build' + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Download dependencies + run: go mod download + + - name: Run tests + run: | + go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... + + - name: Upload coverage + uses: codecov/codecov-action@v3 + with: + file: ./coverage.txt + fail_ci_if_error: false + + build: + name: Build Application + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + + - name: Build binary + run: | + mkdir -p ${{ env.BUILD_DIR }} + + # Build for Linux AMD64 (typical VM architecture) + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -ldflags="-w -s -X main.Version=${{ github.sha }} -X main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + -o ${{ env.BUILD_DIR }}/${{ env.APP_NAME }} \ + ./cmd/${{ env.APP_NAME }} + + - name: Compress binary + run: | + cd ${{ env.BUILD_DIR }} + tar -czf ${{ env.APP_NAME }}-${{ github.sha }}.tar.gz ${{ env.APP_NAME }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: cv-site-binary + path: ${{ env.BUILD_DIR }}/${{ env.APP_NAME }}-${{ github.sha }}.tar.gz + retention-days: 30 + + deploy: + name: Deploy to VM + runs-on: ubuntu-latest + needs: build + environment: + name: ${{ github.event.inputs.environment || 'production' }} + url: https://cv.example.com + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: cv-site-binary + path: ./build + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -p ${{ secrets.SSH_PORT || 22 }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts + + - name: Deploy to VM + env: + SSH_KEY: ~/.ssh/deploy_key + SSH_USER: ${{ secrets.SSH_USER }} + SSH_HOST: ${{ secrets.SSH_HOST }} + SSH_PORT: ${{ secrets.SSH_PORT || 22 }} + DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} + run: | + # Extract artifact + cd build + tar -xzf ${{ env.APP_NAME }}-${{ github.sha }}.tar.gz + + # Upload binary to VM + scp -i $SSH_KEY -P $SSH_PORT ${{ env.APP_NAME }} \ + $SSH_USER@$SSH_HOST:$DEPLOY_PATH/${{ env.APP_NAME }}.new + + # Upload deployment script + scp -i $SSH_KEY -P $SSH_PORT ../scripts/deploy.sh \ + $SSH_USER@$SSH_HOST:$DEPLOY_PATH/ + + # Execute deployment on VM + ssh -i $SSH_KEY -p $SSH_PORT $SSH_USER@$SSH_HOST << 'ENDSSH' + cd ${{ secrets.DEPLOY_PATH }} + + # Make scripts executable + chmod +x deploy.sh + chmod +x ${{ env.APP_NAME }}.new + + # Run deployment script + ./deploy.sh ${{ env.APP_NAME }} + ENDSSH + + - name: Health check + run: | + echo "Waiting for application to start..." + sleep 10 + + # Check if service is running + ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT || 22 }} \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \ + "systemctl is-active ${{ env.APP_NAME }}" + + # HTTP health check + RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" https://cv.example.com/health) + + if [ "$RESPONSE" = "200" ]; then + echo "βœ… Health check passed (HTTP $RESPONSE)" + else + echo "❌ Health check failed (HTTP $RESPONSE)" + exit 1 + fi + + - name: Send notification + if: always() + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + run: | + STATUS="${{ job.status }}" + COLOR="good" + if [ "$STATUS" != "success" ]; then + COLOR="danger" + fi + + curl -X POST "$SLACK_WEBHOOK" \ + -H "Content-Type: application/json" \ + -d "{ + \"attachments\": [{ + \"color\": \"$COLOR\", + \"title\": \"CV Site Deployment $STATUS\", + \"text\": \"Deployment to VM completed\", + \"fields\": [ + {\"title\": \"Branch\", \"value\": \"${{ github.ref_name }}\", \"short\": true}, + {\"title\": \"Commit\", \"value\": \"${{ github.sha }}\", \"short\": true}, + {\"title\": \"Environment\", \"value\": \"${{ github.event.inputs.environment || 'production' }}\", \"short\": true} + ] + }] + }" || true + + - name: Cleanup SSH keys + if: always() + run: | + rm -f ~/.ssh/deploy_key +``` + +--- + +## CDN Integration (Optional) + +### File: `.github/workflows/cdn-deploy.yml` + +```yaml +name: CloudFlare CDN Deployment + +on: + push: + branches: + - main + paths: + - 'static/**' + - 'assets/**' + workflow_dispatch: + inputs: + purge_cache: + description: 'Purge CDN cache' + required: false + default: 'true' + type: choice + options: + - 'true' + - 'false' + +jobs: + deploy-cdn: + name: Deploy Static Assets to CDN + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -p ${{ secrets.SSH_PORT || 22 }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts + + - name: Sync static assets to VM + run: | + rsync -avz -e "ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT || 22 }}" \ + --delete \ + ./static/ \ + ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:${{ secrets.DEPLOY_PATH }}/static/ + + - name: Configure CloudFlare + if: secrets.CLOUDFLARE_API_TOKEN != '' + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }} + run: | + # Enable WebP + curl -X PATCH "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/settings/webp" \ + -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ + -H "Content-Type: application/json" \ + --data '{"value":"on"}' + + # Enable Brotli compression + curl -X PATCH "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/settings/brotli" \ + -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ + -H "Content-Type: application/json" \ + --data '{"value":"on"}' + + - name: Purge CDN cache + if: github.event.inputs.purge_cache == 'true' || github.event_name == 'push' + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }} + run: | + curl -X POST "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/purge_cache" \ + -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ + -H "Content-Type: application/json" \ + --data '{"purge_everything":true}' + + - name: Cleanup + if: always() + run: rm -f ~/.ssh/deploy_key +``` + +--- + +## Build Artifacts + +### What Gets Built + +For a Go application, the main artifact is the **compiled binary**: + +1. **Binary**: `cv-site` (or `cv-site.exe` for Windows) +2. **Archive**: `cv-site-{commit-sha}.tar.gz` +3. **Metadata**: Version info embedded during build + +### Build Flags Explained + +```bash +CGO_ENABLED=0 # Static binary (no C dependencies) +GOOS=linux # Target OS +GOARCH=amd64 # Target architecture + +-ldflags="-w -s # Strip debug info (smaller binary) + -X main.Version=$SHA # Embed git commit + -X main.BuildTime=$DATE" # Embed build timestamp +``` + +### Artifact Storage + +- **GitHub Actions Artifacts**: 30 days retention +- **VM Server**: `/opt/cv-site/releases/{version}/` +- **Backup**: Previous 5 versions kept for rollback + +--- + +## Deployment Scripts + +### File: `scripts/deploy.sh` + +```bash +#!/bin/bash +set -e + +APP_NAME="${1:-cv-site}" +DEPLOY_PATH="${DEPLOY_PATH:-/opt/cv-site}" +SERVICE_NAME="cv-site" + +echo "πŸš€ Starting deployment of $APP_NAME" + +# Create backup of current version +if [ -f "$DEPLOY_PATH/$APP_NAME" ]; then + echo "πŸ“¦ Backing up current version..." + cp "$DEPLOY_PATH/$APP_NAME" "$DEPLOY_PATH/$APP_NAME.backup" +fi + +# Move new binary into place +echo "πŸ“₯ Installing new version..." +mv "$DEPLOY_PATH/$APP_NAME.new" "$DEPLOY_PATH/$APP_NAME" +chmod +x "$DEPLOY_PATH/$APP_NAME" + +# Restart service +echo "πŸ”„ Restarting service..." +sudo systemctl restart "$SERVICE_NAME" + +# Wait for service to start +sleep 3 + +# Check service status +if sudo systemctl is-active --quiet "$SERVICE_NAME"; then + echo "βœ… Service started successfully" + + # Remove backup after successful deployment + rm -f "$DEPLOY_PATH/$APP_NAME.backup" +else + echo "❌ Service failed to start - rolling back" + + # Rollback to backup + if [ -f "$DEPLOY_PATH/$APP_NAME.backup" ]; then + mv "$DEPLOY_PATH/$APP_NAME.backup" "$DEPLOY_PATH/$APP_NAME" + sudo systemctl restart "$SERVICE_NAME" + echo "⚠️ Rolled back to previous version" + fi + + exit 1 +fi + +echo "πŸŽ‰ Deployment completed successfully" +``` + +### File: `scripts/healthcheck.sh` + +```bash +#!/bin/bash +set -e + +HEALTH_URL="${HEALTH_URL:-http://localhost:8080/health}" +MAX_RETRIES=30 +RETRY_DELAY=2 + +echo "πŸ₯ Running health check..." + +for i in $(seq 1 $MAX_RETRIES); do + if curl -sf "$HEALTH_URL" > /dev/null; then + echo "βœ… Health check passed (attempt $i)" + exit 0 + fi + + echo "⏳ Waiting for service... (attempt $i/$MAX_RETRIES)" + sleep $RETRY_DELAY +done + +echo "❌ Health check failed after $MAX_RETRIES attempts" +exit 1 +``` + +--- + +## Environment Configuration + +### Systemd Service File + +Create: `config/systemd/cv-site.service` + +```ini +[Unit] +Description=CV Site Go Application +After=network.target + +[Service] +Type=simple +User=www-data +Group=www-data +WorkingDirectory=/opt/cv-site +ExecStart=/opt/cv-site/cv-site +Restart=always +RestartSec=5s + +# Environment variables +Environment="PORT=8080" +Environment="GIN_MODE=release" +EnvironmentFile=/opt/cv-site/.env + +# Security hardening +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/opt/cv-site/data + +# Resource limits +LimitNOFILE=65536 +MemoryMax=512M + +[Install] +WantedBy=multi-user.target +``` + +### Installation on VM + +```bash +# Copy service file +sudo cp config/systemd/cv-site.service /etc/systemd/system/ + +# Reload systemd +sudo systemctl daemon-reload + +# Enable service +sudo systemctl enable cv-site + +# Start service +sudo systemctl start cv-site + +# Check status +sudo systemctl status cv-site +``` + +--- + +## Testing Strategy + +### File: `.github/workflows/test.yml` + +```yaml +name: Test CV Site + +on: + pull_request: + branches: [main, develop] + push: + branches: [main, develop] + +jobs: + test: + name: Test on Go ${{ matrix.go-version }} + runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.21', '1.22'] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache: true + + - name: Install dependencies + run: go mod download + + - name: Run linter + uses: golangci/golangci-lint-action@v3 + with: + version: latest + + - name: Run tests + run: | + go test -v -race -coverprofile=coverage.txt ./... + + - name: Build + run: | + go build -v ./... +``` + +--- + +## Makefile + +Create a `Makefile` for local development and CI consistency: + +```makefile +.PHONY: build test deploy clean help + +APP_NAME := cv-site +BUILD_DIR := ./build +GO_VERSION := 1.22 +VERSION := $(shell git describe --tags --always --dirty) +BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) +LDFLAGS := -ldflags="-w -s -X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME)" + +## help: Display this help message +help: + @echo "Available targets:" + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + +## build: Build the application binary +build: + @echo "πŸ”¨ Building $(APP_NAME)..." + @mkdir -p $(BUILD_DIR) + CGO_ENABLED=0 go build $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME) ./cmd/$(APP_NAME) + @echo "βœ… Build complete: $(BUILD_DIR)/$(APP_NAME)" + +## test: Run all tests +test: + @echo "πŸ§ͺ Running tests..." + go test -v -race -coverprofile=coverage.txt ./... + +## lint: Run linter +lint: + @echo "πŸ” Running linter..." + golangci-lint run + +## clean: Clean build artifacts +clean: + @echo "🧹 Cleaning..." + rm -rf $(BUILD_DIR) + go clean + +## run: Run the application locally +run: build + @echo "πŸš€ Starting $(APP_NAME)..." + $(BUILD_DIR)/$(APP_NAME) + +## deploy: Build and deploy (for CI use) +deploy: build + @echo "πŸ“¦ Deploying..." + # Deployment handled by GitHub Actions +``` + +--- + +## Deployment Checklist + +### Pre-Deployment + +- [ ] **GitHub Secrets configured** + - [ ] `SSH_PRIVATE_KEY` added + - [ ] `SSH_HOST` set + - [ ] `SSH_USER` set + - [ ] `DEPLOY_PATH` configured + +- [ ] **VM Server prepared** + - [ ] SSH access configured + - [ ] Deploy user created with sudo privileges + - [ ] Application directory created (`/opt/cv-site`) + - [ ] Systemd service installed + - [ ] Firewall rules configured (port 8080) + +- [ ] **Code repository ready** + - [ ] Workflow files in `.github/workflows/` + - [ ] Deployment scripts in `scripts/` + - [ ] Systemd service file in `config/systemd/` + - [ ] Tests passing locally + +### First Deployment + +1. **Push to GitHub**: `git push origin main` +2. **Monitor Actions**: Go to `Actions` tab in GitHub +3. **Check logs**: Verify each step completes +4. **Test deployment**: Visit your site URL +5. **Verify service**: SSH to VM and run `systemctl status cv-site` + +### Post-Deployment + +- [ ] **Health check passes** +- [ ] **Service running**: `systemctl is-active cv-site` +- [ ] **Logs clean**: `journalctl -u cv-site -n 50` +- [ ] **Backup created**: Previous version backed up +- [ ] **Rollback tested**: Verify rollback script works + +--- + +## Troubleshooting + +### Common Issues + +#### 1. SSH Connection Failed + +```bash +# Test SSH connection manually +ssh -i ~/.ssh/cv-site-deploy user@your-vm.example.com + +# Check SSH key format +head -n 1 ~/.ssh/cv-site-deploy +# Should show: -----BEGIN OPENSSH PRIVATE KEY----- +``` + +#### 2. Binary Won't Execute + +```bash +# Check binary architecture +file /opt/cv-site/cv-site +# Should show: ELF 64-bit LSB executable, x86-64 + +# Check permissions +ls -la /opt/cv-site/cv-site +chmod +x /opt/cv-site/cv-site +``` + +#### 3. Service Fails to Start + +```bash +# Check service status +sudo systemctl status cv-site + +# View logs +sudo journalctl -u cv-site -n 100 --no-pager + +# Test binary manually +sudo -u www-data /opt/cv-site/cv-site +``` + +#### 4. Health Check Timeout + +```bash +# Test health endpoint locally on VM +curl http://localhost:8080/health + +# Check if port is listening +sudo netstat -tlnp | grep 8080 + +# Check firewall +sudo ufw status +``` + +--- + +## Advanced Features + +### Blue-Green Deployment + +Modify `deploy.sh` to support zero-downtime deployments: + +```bash +#!/bin/bash +# Blue-Green deployment strategy + +BLUE_PORT=8080 +GREEN_PORT=8081 +CURRENT_PORT=$(cat /opt/cv-site/current_port) +NEW_PORT=$([[ $CURRENT_PORT == $BLUE_PORT ]] && echo $GREEN_PORT || echo $BLUE_PORT) + +# Start new version on alternate port +PORT=$NEW_PORT /opt/cv-site/cv-site.new & + +# Wait and health check +sleep 5 +curl -f http://localhost:$NEW_PORT/health || exit 1 + +# Switch nginx upstream +sudo sed -i "s/localhost:$CURRENT_PORT/localhost:$NEW_PORT/" /etc/nginx/sites-enabled/cv-site +sudo systemctl reload nginx + +# Stop old version +pkill -f "cv-site.*$CURRENT_PORT" + +# Update current port +echo $NEW_PORT > /opt/cv-site/current_port +``` + +### Rollback Strategy + +```bash +#!/bin/bash +# scripts/rollback.sh + +DEPLOY_PATH="/opt/cv-site" +BACKUP_PATH="$DEPLOY_PATH/releases" + +# List available versions +echo "Available versions:" +ls -1t $BACKUP_PATH | head -5 + +# Rollback to previous version +PREVIOUS=$(ls -1t $BACKUP_PATH | head -1) +cp "$BACKUP_PATH/$PREVIOUS/cv-site" "$DEPLOY_PATH/cv-site" + +sudo systemctl restart cv-site +echo "Rolled back to: $PREVIOUS" +``` + +--- + +## Performance Optimization + +### Binary Size Reduction + +```bash +# Use UPX compression (optional) +upx --best --lzma build/cv-site + +# Result: ~70% size reduction +# Before: 15MB β†’ After: 4.5MB +``` + +### Caching Strategy + +Add to workflow for faster builds: + +```yaml +- name: Cache Go modules + uses: actions/cache@v3 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- +``` + +--- + +## Security Best Practices + +1. **SSH Key Security** + - Use Ed25519 keys (stronger than RSA) + - Never commit private keys + - Rotate keys every 90 days + +2. **Service Hardening** + - Run as non-root user (`www-data`) + - Use systemd security features + - Limit file system access + +3. **Secret Management** + - Use GitHub Encrypted Secrets + - Never log sensitive data + - Use environment files on VM + +4. **Network Security** + - Restrict SSH to GitHub Actions IPs (if possible) + - Use firewall rules + - Enable fail2ban + +--- + +## Monitoring & Logging + +### Add Logging to Deployment + +```yaml +- name: Log deployment + run: | + echo "Deployment started at $(date)" >> /var/log/cv-site/deploy.log + echo "Commit: ${{ github.sha }}" >> /var/log/cv-site/deploy.log + echo "Branch: ${{ github.ref_name }}" >> /var/log/cv-site/deploy.log +``` + +### Application Monitoring + +```go +// Add to your Go app for health checks +func healthHandler(c *gin.Context) { + c.JSON(200, gin.H{ + "status": "healthy", + "version": Version, + "uptime": time.Since(startTime).String(), + }) +} +``` + +--- + +## Summary + +### Key Differences from La Porra + +| Aspect | La Porra (Node/Bun) | CV Site (Go) | +|--------|---------------------|--------------| +| **Runtime** | Node.js/Bun | Native binary | +| **Build** | `bun run build` | `go build` | +| **Artifact** | /dist directory | Single binary | +| **Dependencies** | node_modules | Statically linked | +| **Deployment** | File sync | Binary upload | +| **Service** | PM2/systemd | systemd | + +### Deployment Flow + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Push to Git β”‚ +β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + v +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ GitHub Actions β”‚ +β”‚ - Test β”‚ +β”‚ - Build Binary β”‚ +β”‚ - Create Artifactβ”‚ +β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + v +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Upload to VM β”‚ +β”‚ via SSH/SCP β”‚ +β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + v +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Execute deploy.shβ”‚ +β”‚ - Backup old β”‚ +β”‚ - Install new β”‚ +β”‚ - Restart serviceβ”‚ +β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + v +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Health Check β”‚ +β”‚ - HTTP test β”‚ +β”‚ - Service statusβ”‚ +β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + v +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Send Notificationβ”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Quick Start Commands + +```bash +# 1. Clone repository +git clone https://github.com/juanatsap/cv-site.git +cd cv-site + +# 2. Create workflow files +mkdir -p .github/workflows +# Copy workflow YAML files from this guide + +# 3. Create deployment scripts +mkdir -p scripts config/systemd +# Copy scripts from this guide + +# 4. Add secrets to GitHub +# Go to Settings β†’ Secrets and variables β†’ Actions +# Add all required secrets + +# 5. Test locally +make test +make build + +# 6. Deploy +git add . +git commit -m "Add GitHub Actions deployment" +git push origin main +``` + +--- + +## Additional Resources + +- **GitHub Actions Documentation**: https://docs.github.com/en/actions +- **Go Build Documentation**: https://pkg.go.dev/cmd/go#hdr-Compile_packages_and_dependencies +- **Systemd Service Files**: https://www.freedesktop.org/software/systemd/man/systemd.service.html +- **SSH Key Management**: https://docs.github.com/en/authentication/connecting-to-github-with-ssh + +--- + +**Last Updated**: January 2025 +**Maintained By**: Development Team +**Support**: Create an issue in the repository + diff --git a/Makefile b/Makefile index 80c20a4..fe78e96 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: run build test clean dev prod docker-build docker-run +.PHONY: run build test clean dev prod docker-build docker-run ci-test ci-build health-check install-service update-service # Development dev: @@ -57,15 +57,52 @@ docker-run: @echo "🐳 Running Docker container..." docker run -p 1999:1999 cv-server:latest +# CI/CD Targets +ci-test: + @echo "πŸ§ͺ Running CI tests..." + go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... + @echo "βœ“ CI tests complete" + +ci-build: + @echo "πŸ”¨ Building for CI/CD..." + mkdir -p build + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o build/cv-server . + @echo "βœ“ CI build complete: build/cv-server" + +health-check: + @echo "πŸ₯ Running health check..." + @./scripts/healthcheck.sh + +install-service: + @echo "πŸ“¦ Installing systemd service..." + sudo cp config/systemd/cv.service /etc/systemd/system/ + sudo systemctl daemon-reload + sudo systemctl enable cv + @echo "βœ“ Service installed and enabled" + +update-service: + @echo "πŸ”„ Updating systemd service..." + sudo cp config/systemd/cv.service /etc/systemd/system/ + sudo systemctl daemon-reload + sudo systemctl restart cv + @echo "βœ“ Service updated and restarted" + # Help help: @echo "Available commands:" - @echo " make dev - Run in development mode (hot-reload enabled)" - @echo " make prod - Run in production mode" - @echo " make build - Build production binary" - @echo " make run - Build and run binary" - @echo " make test - Test all endpoints" - @echo " make test-errors - Test error handling" - @echo " make clean - Remove build artifacts" - @echo " make docker-build - Build Docker image" - @echo " make docker-run - Run Docker container" + @echo " make dev - Run in development mode (hot-reload enabled)" + @echo " make prod - Run in production mode" + @echo " make build - Build production binary" + @echo " make run - Build and run binary" + @echo " make test - Test all endpoints" + @echo " make test-errors - Test error handling" + @echo " make clean - Remove build artifacts" + @echo " make docker-build - Build Docker image" + @echo " make docker-run - Run Docker container" + @echo "" + @echo "CI/CD commands:" + @echo " make ci-test - Run tests for CI pipeline" + @echo " make ci-build - Build binary for CI/CD" + @echo " make health-check - Check service health" + @echo " make install-service - Install systemd service" + @echo " make update-service - Update and restart service" diff --git a/config/systemd/cv.service b/config/systemd/cv.service new file mode 100644 index 0000000..872f9a1 --- /dev/null +++ b/config/systemd/cv.service @@ -0,0 +1,45 @@ +[Unit] +Description=CV Website Service +After=network.target +Wants=network-online.target + +[Service] +Type=simple +User=txeo +Group=txeo +WorkingDirectory=/home/txeo/Git/yo/cv +ExecStart=/home/txeo/Git/yo/cv/cv-server + +# Environment variables +Environment="GO_ENV=production" +Environment="PORT=1999" +Environment="HOST=0.0.0.0" +Environment="BASE_URL=https://juan.andres.morenorub.io" +Environment="TEMPLATE_HOT_RELOAD=false" +EnvironmentFile=-/home/txeo/Git/yo/cv/.env + +# Security hardening +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=false +ReadWritePaths=/home/txeo/Git/yo/cv/data +ReadOnlyPaths=/home/txeo/Git/yo/cv + +# Resource limits +LimitNOFILE=65536 +MemoryMax=512M + +# Restart policy +Restart=always +RestartSec=5 +StartLimitInterval=60 +StartLimitBurst=3 + +# Logging +StandardOutput=journal +StandardError=journal +SyslogIdentifier=cv + +[Install] +WantedBy=multi-user.target diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..2ed561f --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,69 @@ +#!/bin/bash +set -e + +APP_NAME="${1:-cv-server}" +SERVICE_NAME="${2:-cv}" +DEPLOY_PATH="${DEPLOY_PATH:-/home/txeo/Git/yo/cv}" + +echo "πŸš€ Starting deployment of $APP_NAME" +echo "πŸ“ Deploy path: $DEPLOY_PATH" +echo "πŸ”§ Service: $SERVICE_NAME" + +# Create backup of current version +if [ -f "$DEPLOY_PATH/$APP_NAME" ]; then + echo "πŸ“¦ Backing up current version..." + BACKUP_DIR="$DEPLOY_PATH/backups" + mkdir -p "$BACKUP_DIR" + TIMESTAMP=$(date +%Y%m%d_%H%M%S) + cp "$DEPLOY_PATH/$APP_NAME" "$BACKUP_DIR/$APP_NAME.$TIMESTAMP" + + # Keep only last 5 backups + ls -t "$BACKUP_DIR" | tail -n +6 | xargs -I {} rm -f "$BACKUP_DIR/{}" + echo "βœ“ Backup created: $APP_NAME.$TIMESTAMP" +fi + +# Move new binary into place +echo "πŸ“₯ Installing new version..." +mv "$DEPLOY_PATH/$APP_NAME.new" "$DEPLOY_PATH/$APP_NAME" +chmod +x "$DEPLOY_PATH/$APP_NAME" + +# Restart service +echo "πŸ”„ Restarting service..." +sudo systemctl restart "$SERVICE_NAME" + +# Wait for service to start +sleep 3 + +# Check service status +if sudo systemctl is-active --quiet "$SERVICE_NAME"; then + echo "βœ… Service started successfully" + + # Get service status + echo "" + echo "πŸ“Š Service Status:" + sudo systemctl status "$SERVICE_NAME" --no-pager -l | head -15 + + echo "" + echo "πŸŽ‰ Deployment completed successfully" + exit 0 +else + echo "❌ Service failed to start - rolling back" + + # Rollback to backup + LATEST_BACKUP=$(ls -t "$BACKUP_DIR" 2>/dev/null | head -1) + if [ -n "$LATEST_BACKUP" ]; then + echo "βͺ Rolling back to: $LATEST_BACKUP" + cp "$BACKUP_DIR/$LATEST_BACKUP" "$DEPLOY_PATH/$APP_NAME" + sudo systemctl restart "$SERVICE_NAME" + + if sudo systemctl is-active --quiet "$SERVICE_NAME"; then + echo "⚠️ Rolled back to previous version successfully" + else + echo "❌ Rollback failed - manual intervention required" + fi + else + echo "❌ No backup found - manual intervention required" + fi + + exit 1 +fi diff --git a/scripts/healthcheck.sh b/scripts/healthcheck.sh new file mode 100755 index 0000000..fbcce2f --- /dev/null +++ b/scripts/healthcheck.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -e + +HEALTH_URL="${HEALTH_URL:-http://localhost:1999/health}" +MAX_RETRIES=30 +RETRY_DELAY=2 + +echo "πŸ₯ Running health check on $HEALTH_URL" + +for i in $(seq 1 $MAX_RETRIES); do + if curl -sf "$HEALTH_URL" > /dev/null 2>&1; then + echo "βœ… Health check passed (attempt $i/$MAX_RETRIES)" + + # Show health response + RESPONSE=$(curl -s "$HEALTH_URL") + echo "" + echo "πŸ“Š Health Status:" + echo "$RESPONSE" | jq . 2>/dev/null || echo "$RESPONSE" + + exit 0 + fi + + echo "⏳ Waiting for service... (attempt $i/$MAX_RETRIES)" + sleep $RETRY_DELAY +done + +echo "❌ Health check failed after $MAX_RETRIES attempts" +exit 1 diff --git a/scripts/rollback.sh b/scripts/rollback.sh new file mode 100755 index 0000000..0f2f0cf --- /dev/null +++ b/scripts/rollback.sh @@ -0,0 +1,93 @@ +#!/bin/bash +set -e + +APP_NAME="${1:-cv-server}" +SERVICE_NAME="${2:-cv}" +DEPLOY_PATH="${DEPLOY_PATH:-/home/txeo/Git/yo/cv}" +BACKUP_DIR="$DEPLOY_PATH/backups" + +echo "βͺ Rollback Script for $APP_NAME" +echo "πŸ“ Deploy path: $DEPLOY_PATH" +echo "πŸ”§ Service: $SERVICE_NAME" +echo "" + +# Check if backup directory exists +if [ ! -d "$BACKUP_DIR" ]; then + echo "❌ No backup directory found at $BACKUP_DIR" + exit 1 +fi + +# List available versions +echo "πŸ“¦ Available backup versions:" +ls -lht "$BACKUP_DIR" | tail -n +2 | head -5 | awk '{print NR". "$9" ("$6" "$7" "$8")"}' +echo "" + +# If no argument provided, show usage +if [ -z "$3" ]; then + echo "Usage: $0 " + echo "Example: $0 cv-server cv 1" + echo "" + echo "Or to rollback to the most recent version:" + echo " $0 cv-server cv latest" + exit 0 +fi + +VERSION_ARG="$3" + +# Determine which backup to use +if [ "$VERSION_ARG" = "latest" ]; then + BACKUP_FILE=$(ls -t "$BACKUP_DIR" | head -1) + echo "πŸ” Selected: latest version ($BACKUP_FILE)" +else + BACKUP_FILE=$(ls -t "$BACKUP_DIR" | sed -n "${VERSION_ARG}p") + echo "πŸ” Selected: version #$VERSION_ARG ($BACKUP_FILE)" +fi + +if [ -z "$BACKUP_FILE" ]; then + echo "❌ Version not found" + exit 1 +fi + +# Confirm rollback +echo "" +echo "⚠️ About to rollback to: $BACKUP_FILE" +read -p "Continue? (yes/no): " CONFIRM + +if [ "$CONFIRM" != "yes" ]; then + echo "❌ Rollback cancelled" + exit 0 +fi + +# Create backup of current version before rollback +if [ -f "$DEPLOY_PATH/$APP_NAME" ]; then + TIMESTAMP=$(date +%Y%m%d_%H%M%S) + cp "$DEPLOY_PATH/$APP_NAME" "$BACKUP_DIR/$APP_NAME.pre-rollback.$TIMESTAMP" + echo "βœ“ Current version backed up as: $APP_NAME.pre-rollback.$TIMESTAMP" +fi + +# Perform rollback +echo "βͺ Rolling back..." +cp "$BACKUP_DIR/$BACKUP_FILE" "$DEPLOY_PATH/$APP_NAME" +chmod +x "$DEPLOY_PATH/$APP_NAME" + +# Restart service +echo "πŸ”„ Restarting service..." +sudo systemctl restart "$SERVICE_NAME" + +# Wait for service to start +sleep 3 + +# Check service status +if sudo systemctl is-active --quiet "$SERVICE_NAME"; then + echo "βœ… Rollback successful - service is running" + echo "" + echo "πŸ“Š Service Status:" + sudo systemctl status "$SERVICE_NAME" --no-pager -l | head -15 + exit 0 +else + echo "❌ Service failed to start after rollback" + echo "" + echo "πŸ” Service logs:" + sudo journalctl -u "$SERVICE_NAME" -n 20 --no-pager + exit 1 +fi