Add GitHub Actions deployment workflow
- Add deployment workflow with test, build, and deploy jobs - Add testing workflow for PRs - Add deployment scripts (deploy, healthcheck, rollback) - Add systemd service configuration - Update Makefile with CI/CD targets - Add comprehensive deployment documentation
This commit is contained in:
@@ -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
|
||||||
@@ -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 .
|
||||||
@@ -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`
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
# Development
|
||||||
dev:
|
dev:
|
||||||
@@ -57,15 +57,52 @@ docker-run:
|
|||||||
@echo "🐳 Running Docker container..."
|
@echo "🐳 Running Docker container..."
|
||||||
docker run -p 1999:1999 cv-server:latest
|
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
|
||||||
help:
|
help:
|
||||||
@echo "Available commands:"
|
@echo "Available commands:"
|
||||||
@echo " make dev - Run in development mode (hot-reload enabled)"
|
@echo " make dev - Run in development mode (hot-reload enabled)"
|
||||||
@echo " make prod - Run in production mode"
|
@echo " make prod - Run in production mode"
|
||||||
@echo " make build - Build production binary"
|
@echo " make build - Build production binary"
|
||||||
@echo " make run - Build and run binary"
|
@echo " make run - Build and run binary"
|
||||||
@echo " make test - Test all endpoints"
|
@echo " make test - Test all endpoints"
|
||||||
@echo " make test-errors - Test error handling"
|
@echo " make test-errors - Test error handling"
|
||||||
@echo " make clean - Remove build artifacts"
|
@echo " make clean - Remove build artifacts"
|
||||||
@echo " make docker-build - Build Docker image"
|
@echo " make docker-build - Build Docker image"
|
||||||
@echo " make docker-run - Run Docker container"
|
@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"
|
||||||
|
|||||||
@@ -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
|
||||||
Executable
+69
@@ -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
|
||||||
Executable
+28
@@ -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
|
||||||
Executable
+93
@@ -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 <app_name> <service_name> <version_number>"
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user