5e38292d2e
- 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
997 lines
24 KiB
Markdown
997 lines
24 KiB
Markdown
# 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
|
|
|