Add CI/CD pipeline with Harbor registry and SSH deployment
Some checks failed
CI/CD Pipeline - Apartment Dashboard / Run Linter (push) Failing after 4m52s
CI/CD Pipeline - Apartment Dashboard / Build & Push Image (push) Has been skipped
CI/CD Pipeline - Apartment Dashboard / Deploy to Production (push) Has been skipped

- Replace local deployment with proper CI/CD pipeline
- Add lint job as first stage (fail-fast)
- Build and push Docker image to Harbor registry
- Deploy via SSH with image tag from commit SHA
- Add deployment verification with health check
- Update Dockerfile to Node 20 and fix npm ci
- Support IMAGE variable in docker-compose for deployments

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-22 16:59:51 -07:00
parent d033483633
commit de08b3ddfa
3 changed files with 147 additions and 58 deletions

View File

@ -1,12 +1,22 @@
name: Deploy Apartment Dashboard name: CI/CD Pipeline - Apartment Dashboard
on: on:
push: push:
branches: [ main ] branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch: workflow_dispatch:
env:
NODE_VERSION: '20'
IMAGE_NAME: apartment-dashboard
jobs: jobs:
deploy: # ============================================================
# Lint Job - Runs first, blocks everything if linting fails
# ============================================================
lint:
name: Run Linter
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -16,46 +26,124 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '18' node-version: ${{ env.NODE_VERSION }}
cache: 'npm' cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Build application - name: Run linter
run: npm run build run: npm run lint
- name: Build Docker image # ============================================================
run: | # Build & Push Job - Build image and push to Harbor
docker build -t apartment-dashboard:${{ github.sha }} . # ============================================================
docker tag apartment-dashboard:${{ github.sha }} apartment-dashboard:latest build:
name: Build & Push Image
runs-on: ubuntu-latest
needs: lint
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
- name: Deploy to server outputs:
image_tag: ${{ steps.set-tag.outputs.tag }}
full_image: ${{ steps.set-tag.outputs.full_image }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set image tag
id: set-tag
run: | run: |
# Copy files to deployment directory SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
sudo mkdir -p /media/stephen/Storage_Linux/infrastructure/services/apartments echo "tag=${SHORT_SHA}" >> $GITHUB_OUTPUT
sudo cp -r . /media/stephen/Storage_Linux/infrastructure/services/apartments/ echo "full_image=${{ secrets.HARBOR_REGISTRY }}/${{ secrets.HARBOR_PROJECT }}/${{ env.IMAGE_NAME }}:${SHORT_SHA}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Harbor
uses: docker/login-action@v3
with:
registry: ${{ secrets.HARBOR_REGISTRY }}
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ secrets.HARBOR_REGISTRY }}/${{ secrets.HARBOR_PROJECT }}/${{ env.IMAGE_NAME }}:${{ steps.set-tag.outputs.tag }}
${{ secrets.HARBOR_REGISTRY }}/${{ secrets.HARBOR_PROJECT }}/${{ env.IMAGE_NAME }}:latest
# ============================================================
# Deploy Job - Pull image and restart on production server
# ============================================================
deploy:
name: Deploy to Production
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: ${{ secrets.SSH_PORT || 22 }}
script_stop: true
script: |
set -e
# Image to deploy (using specific SHA tag, not latest)
IMAGE="${{ needs.build.outputs.full_image }}"
echo "Deploying image: $IMAGE"
# Navigate to deployment directory # Navigate to deployment directory
cd /media/stephen/Storage_Linux/infrastructure/services/apartments cd ${{ secrets.DEPLOY_PATH }}
# Stop existing container if running # Pull the specific image (using pre-configured Docker credentials)
sudo docker compose down || true docker pull "$IMAGE"
# Start new container # Update and restart with new image
sudo docker compose up -d export IMAGE
docker compose up -d
# Clean up old images # Clean up old images
sudo docker image prune -f docker image prune -f
- name: Verify deployment # Wait for container to start
run: | echo "Waiting for container to start..."
# Wait a moment for container to start
sleep 10 sleep 10
# Check if container is running # Verify container is running
cd /media/stephen/Storage_Linux/infrastructure/services/apartments if docker compose ps | grep -q "Up"; then
sudo docker compose ps echo "Container is running successfully"
else
echo "ERROR: Container failed to start"
docker compose logs --tail=50
exit 1
fi
# Test endpoint (optional) - name: Verify deployment
curl -f http://localhost:80 || echo "Container may still be starting..." uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: ${{ secrets.SSH_PORT || 22 }}
script: |
# Test health endpoint (frontend serves index.html)
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://apartments.maverickapplications.com/ || echo "000")
if [ "$HTTP_CODE" = "200" ]; then
echo "Health check passed (HTTP $HTTP_CODE)"
else
echo "WARNING: Health check returned HTTP $HTTP_CODE"
echo "Container may still be initializing..."
exit 1
fi

View File

@ -1,13 +1,13 @@
# Build stage # Build stage
FROM node:18-alpine as build FROM node:20-alpine as build
WORKDIR /app WORKDIR /app
# Copy package files # Copy package files
COPY package*.json ./ COPY package*.json ./
# Install dependencies # Install dependencies (including devDependencies needed for build)
RUN npm ci --only=production RUN npm ci
# Copy source code # Copy source code
COPY . . COPY . .

View File

@ -1,14 +1,15 @@
version: '3.8'
services: services:
apartment-dashboard: apartment-dashboard:
image: ${IMAGE:-apartment-dashboard:latest}
build: . build: .
container_name: apartment-dashboard container_name: apartment-dashboard
restart: unless-stopped restart: unless-stopped
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.docker.network=traefik"
- "traefik.http.routers.apartments.rule=Host(`apartments.maverickapplications.com`)" - "traefik.http.routers.apartments.rule=Host(`apartments.maverickapplications.com`)"
- "traefik.http.routers.apartments.entrypoints=websecure" - "traefik.http.routers.apartments.entrypoints=websecure"
- "traefik.http.routers.apartments.tls=true"
- "traefik.http.routers.apartments.tls.certresolver=letsencrypt" - "traefik.http.routers.apartments.tls.certresolver=letsencrypt"
- "traefik.http.services.apartments.loadbalancer.server.port=80" - "traefik.http.services.apartments.loadbalancer.server.port=80"
networks: networks: