Skip to content

Merge pull request #21 from dordormin/fix/cors-204-nocontent #65

Merge pull request #21 from dordormin/fix/cors-204-nocontent

Merge pull request #21 from dordormin/fix/cors-204-nocontent #65

Workflow file for this run

name: TradeOffStack CI/CD Pipeline
on:
push:
branches: [ "main", "feature/postman-tests" ]
pull_request:
branches: [ "main", "feature/postman-tests" ]
jobs:
build-and-test:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: πŸ“₯ Checkout repository
uses: actions/checkout@v4
- name: βš™οΈ Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: πŸ“¦ Restore backend dependencies
run: dotnet restore TradeOffStackAPI.sln
- name: πŸ”¨ Build API
run: dotnet build TradeOffStackAPI.sln --no-restore --configuration Release
- name: πŸ§ͺ Run C# Backend Tests
run: dotnet test TradeOffStackAPI.sln --no-build --configuration Release --verbosity normal
- name: πŸš€ Start Services (API & Database) via Docker Compose
run: |
echo "POSTGRES_USER=tradeoff_admin" >> .env
echo "POSTGRES_PASSWORD=Tr@de0ff_Secure!2026_Db#X9" >> .env
echo "POSTGRES_DB=tradeoffstack" >> .env
echo "POSTGRES_PORT=5432" >> .env
echo "JWT_SECRET_KEY=Tr@de0ff_Super_Secret_Key_For_JWT_Auth_2026!" >> .env
docker compose up -d --build
- name: 🩺 Wait for API to be healthy
run: |
echo "Waiting for API to respond at http://localhost:5000/api/equipment..."
for i in {1..30}; do
if curl -s http://localhost:5000/api/equipment > /dev/null; then
echo "API is up and running!"
exit 0
fi
sleep 2
done
echo "API failed to start in time."
docker compose logs
exit 1
- name: 🟒 Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: πŸ“¦ Install Frontend Dependencies
run: |
cd frontend
npm ci
- name: 🎭 Install Playwright Browsers
run: |
cd frontend
npx playwright install --with-deps chromium
- name: ⚑ Start Frontend Vite Server
run: |
cd frontend
npm run dev &
echo "Waiting for Vite frontend to be responsive..."
for i in {1..15}; do
if curl -s http://localhost:5173 > /dev/null; then
echo "Vite is up!"
exit 0
fi
sleep 1
done
- name: πŸ§ͺ Run Playwright E2E UI Tests
run: |
cd frontend
node ui_test.js
- name: πŸ§ͺ Run Playwright E2E Global CRUD Lifecycle Tests
run: |
cd frontend
node ui_global_test.js
- name: πŸ“€ Upload E2E Screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-screenshots
path: frontend/screenshots/
# ==========================================
# DOCKER PUBLISH TO GHCR (Only on push to main or feature/postman-tests)
# ==========================================
- name: πŸ” Log in to GitHub Container Registry
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feature/postman-tests')
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 🐳 Build and Push Docker Images to GHCR
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feature/postman-tests')
run: |
OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
echo "Building and Pushing API Image..."
docker build -t ghcr.io/$OWNER_LC/tradeoffstack-api:latest -f Dockerfile .
docker push ghcr.io/$OWNER_LC/tradeoffstack-api:latest
echo "Building and Pushing Frontend Image..."
docker build -t ghcr.io/$OWNER_LC/tradeoffstack-frontend:latest -f frontend/Dockerfile frontend/
docker push ghcr.io/$OWNER_LC/tradeoffstack-frontend:latest
# ==========================================
# πŸš€ CONTINUOUS DEPLOYMENT (CD) TO HOSTINGER VPS
# ==========================================
- name: πŸ” Diagnose VPS Connectivity
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feature/postman-tests')
run: |
echo "Testing connectivity to VPS..."
echo "Target: ${{ secrets.VPS_IP }}:22"
timeout 15 bash -c "cat < /dev/null > /dev/tcp/${{ secrets.VPS_IP }}/22" 2>&1 && echo "βœ… Port 22 is OPEN" || echo "❌ Port 22 is UNREACHABLE"
- name: πŸš€ Deploy to VPS (All-in-One SSH)
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/feature/postman-tests')
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.VPS_IP }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
timeout: 60s
command_timeout: 10m
script: |
# Create deployment directory
mkdir -p /app/tradeoffstack
cd /app/tradeoffstack
# Write Caddyfile directly (no SCP needed)
cat > Caddyfile << 'CADDY_EOF'
tradeoffstack.dordorapps.tech {
handle /api/* {
reverse_proxy api:8080
}
handle {
reverse_proxy frontend:80
}
encode gzip zstd
}
CADDY_EOF
# Write docker-compose.prod.yml directly (no SCP needed)
cat > docker-compose.prod.yml << 'COMPOSE_EOF'
version: '3.8'
services:
db:
image: postgres:17-alpine
container_name: tradeoffstack-db-prod
environment:
POSTGRES_USER: ${POSTGRES_USER:-tradeoff_admin}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: tradeoffstack
volumes:
- pg_prod_data:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-tradeoff_admin} -d tradeoffstack"]
interval: 5s
timeout: 5s
retries: 5
api:
image: ghcr.io/dordormin/tradeoffstack-api:latest
container_name: tradeoffstack-api-prod
depends_on:
db:
condition: service_healthy
environment:
ConnectionStrings__DefaultConnection: Host=db;Database=tradeoffstack;Username=${POSTGRES_USER:-tradeoff_admin};Password=${POSTGRES_PASSWORD}
JWT_SECRET_KEY: ${JWT_SECRET_KEY}
ASPNETCORE_ENVIRONMENT: Production
restart: always
frontend:
image: ghcr.io/dordormin/tradeoffstack-frontend:latest
container_name: tradeoffstack-frontend-prod
restart: always
caddy:
image: caddy:2-alpine
container_name: tradeoffstack-proxy-prod
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- api
- frontend
restart: always
volumes:
pg_prod_data:
caddy_data:
caddy_config:
COMPOSE_EOF
# Inject production environment variables
cat > .env << ENV_EOF
POSTGRES_USER=tradeoff_admin
POSTGRES_PASSWORD=${{ secrets.DB_PASSWORD }}
JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }}
ENV_EOF
# Login to GHCR to pull private images
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin
# Pull latest images and update containers
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d --remove-orphans
# Cleanup unused images to keep disk space free
docker image prune -f