Skip to content

Build & Deploy

Build & Deploy #12

Workflow file for this run

name: Build & Deploy
on:
push:
tags:
- 'web-*'
- 'api-*'
- 'full-*'
workflow_dispatch:
inputs:
deploy_target:
description: 'What to deploy'
required: true
default: 'full'
type: choice
options:
- web
- api
- full
concurrency:
group: pp-deploy
cancel-in-progress: true
permissions:
contents: read
packages: write
env:
IMAGE_WEB: ghcr.io/${{ github.repository_owner }}/pp-frontend
IMAGE_API: ghcr.io/${{ github.repository_owner }}/pp-api
jobs:
detect-target:
runs-on: ubuntu-latest
outputs:
deploy_web: ${{ steps.detect.outputs.deploy_web }}
deploy_api: ${{ steps.detect.outputs.deploy_api }}
version: ${{ steps.detect.outputs.version }}
steps:
- name: Detect deployment target
id: detect
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TARGET="${{ inputs.deploy_target }}"
else
TAG="${GITHUB_REF_NAME}"
if [[ "$TAG" == web-* ]]; then
TARGET="web"
elif [[ "$TAG" == api-* ]]; then
TARGET="api"
elif [[ "$TAG" == full-* ]]; then
TARGET="full"
else
TARGET="full"
fi
fi
VERSION="${GITHUB_REF_NAME#*-}"
[ -z "$VERSION" ] && VERSION="${GITHUB_SHA:0:7}"
echo "Target: $TARGET, Version: $VERSION"
if [ "$TARGET" = "web" ] || [ "$TARGET" = "full" ]; then
echo "deploy_web=true" >> $GITHUB_OUTPUT
else
echo "deploy_web=false" >> $GITHUB_OUTPUT
fi
if [ "$TARGET" = "api" ] || [ "$TARGET" = "full" ]; then
echo "deploy_api=true" >> $GITHUB_OUTPUT
else
echo "deploy_api=false" >> $GITHUB_OUTPUT
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
build-web:
needs: detect-target
if: needs.detect-target.outputs.deploy_web == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & Push Frontend
uses: docker/build-push-action@v6
with:
context: .
file: ./pp-web/Dockerfile
push: true
build-args: |
NEXT_PUBLIC_GA_ID=${{ secrets.NEXT_PUBLIC_GA_ID }}
tags: |
${{ env.IMAGE_WEB }}:${{ github.sha }}
${{ env.IMAGE_WEB }}:${{ needs.detect-target.outputs.version }}
${{ env.IMAGE_WEB }}:latest
platforms: linux/amd64
provenance: false
build-api:
needs: detect-target
if: needs.detect-target.outputs.deploy_api == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & Push API
uses: docker/build-push-action@v6
with:
context: ./pp-api
file: ./pp-api/Dockerfile
push: true
build-args: |
VERSION=${{ needs.detect-target.outputs.version }}
tags: |
${{ env.IMAGE_API }}:${{ github.sha }}
${{ env.IMAGE_API }}:${{ needs.detect-target.outputs.version }}
${{ env.IMAGE_API }}:latest
platforms: linux/amd64
provenance: false
deploy:
needs: [detect-target, build-web, build-api]
if: always() && (needs.build-web.result == 'success' || needs.build-web.result == 'skipped') && (needs.build-api.result == 'success' || needs.build-api.result == 'skipped') && !(needs.build-web.result == 'skipped' && needs.build-api.result == 'skipped')
runs-on: ubuntu-latest
steps:
- name: Deploy on VPS
uses: appleboy/ssh-action@v1.2.2
env:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_OWNER: ${{ github.repository_owner }}
DOMAIN: ${{ secrets.PP_DOMAIN }}
JWT_SECRET: ${{ secrets.PP_JWT_SECRET }}
SMTP_HOST: ${{ secrets.PP_SMTP_HOST }}
SMTP_PORT: ${{ secrets.PP_SMTP_PORT }}
SMTP_USER: ${{ secrets.PP_SMTP_USER }}
SMTP_PASS: ${{ secrets.PP_SMTP_PASS }}
IMAGE_TAG: ${{ github.sha }}
DEPLOY_WEB: ${{ needs.detect-target.outputs.deploy_web }}
DEPLOY_API: ${{ needs.detect-target.outputs.deploy_api }}
with:
host: ${{ secrets.SSH_HOST }}
port: ${{ secrets.SSH_PORT }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
envs: GITHUB_ACTOR,GITHUB_TOKEN,GH_OWNER,DOMAIN,JWT_SECRET,SMTP_HOST,SMTP_PORT,SMTP_USER,SMTP_PASS,IMAGE_TAG,DEPLOY_WEB,DEPLOY_API
script: |
set -eo pipefail
DOMAIN="${DOMAIN:-demo.dkarczewski.com}"
DATA_PATH="/home/$USER/pp/data"
# Ensure network exists
docker network ls --format '{{.Name}}' | grep -qx web || docker network create web
# Create directories
mkdir -p ~/pp/data/content ~/pp/data/uploads/gallery
# Create docker-compose file
cat > ~/pp/docker-compose.yml <<'COMPOSE'
services:
frontend:
image: ghcr.io/${GH_OWNER}/pp-frontend:${TAG:-latest}
container_name: pp_frontend
restart: unless-stopped
environment:
- NODE_ENV=production
- PORT=3000
volumes:
- ${DATA_PATH}/content:/app/content:ro
- ${DATA_PATH}/uploads:/app/public/uploads:ro
networks:
- web
- pp_internal
labels:
traefik.enable: "true"
traefik.docker.network: "web"
traefik.http.routers.pp-web.rule: "Host(`${DOMAIN}`)"
traefik.http.routers.pp-web.entrypoints: "websecure"
traefik.http.routers.pp-web.tls.certresolver: "le"
traefik.http.routers.pp-web.priority: "1"
traefik.http.services.pp-web.loadbalancer.server.port: "3000"
traefik.http.routers.pp-web-http.rule: "Host(`${DOMAIN}`)"
traefik.http.routers.pp-web-http.entrypoints: "web"
traefik.http.routers.pp-web-http.middlewares: "pp-https@docker"
traefik.http.middlewares.pp-https.redirectscheme.scheme: "https"
traefik.http.middlewares.pp-https.redirectscheme.permanent: "true"
backend:
image: ghcr.io/${GH_OWNER}/pp-api:${TAG:-latest}
container_name: pp_backend
restart: unless-stopped
environment:
- ASPNETCORE_ENVIRONMENT=Production
- CONTENT_PATH=/app/content
- UPLOADS_PATH=/app/uploads
- JWT_SECRET=${JWT_SECRET}
- SMTP_HOST=${SMTP_HOST:-}
- SMTP_PORT=${SMTP_PORT:-587}
- SMTP_USER=${SMTP_USER:-}
- SMTP_PASS=${SMTP_PASS:-}
- CONTACT_EMAIL_TO=szkolenia@psie-przedszkole.pl
volumes:
- ${DATA_PATH}/content:/app/content
- ${DATA_PATH}/uploads:/app/uploads
networks:
- web
- pp_internal
labels:
traefik.enable: "true"
traefik.docker.network: "web"
traefik.http.routers.pp-api.rule: "Host(`${DOMAIN}`) && PathPrefix(`/api`)"
traefik.http.routers.pp-api.entrypoints: "websecure"
traefik.http.routers.pp-api.tls.certresolver: "le"
traefik.http.routers.pp-api.priority: "10"
traefik.http.services.pp-api.loadbalancer.server.port: "5000"
networks:
web:
external: true
pp_internal:
driver: bridge
COMPOSE
# Create .env
cat > ~/pp/.env <<EOF
GH_OWNER=${GH_OWNER}
TAG=${IMAGE_TAG}
DOMAIN=${DOMAIN}
DATA_PATH=${DATA_PATH}
JWT_SECRET=${JWT_SECRET}
SMTP_HOST=${SMTP_HOST}
SMTP_PORT=${SMTP_PORT}
SMTP_USER=${SMTP_USER}
SMTP_PASS=${SMTP_PASS}
EOF
# Login to GHCR
echo "${GITHUB_TOKEN}" | docker login ghcr.io -u "${GITHUB_ACTOR}" --password-stdin
cd ~/pp
# Pull and deploy based on what changed
if [ "$DEPLOY_WEB" = "true" ] && [ "$DEPLOY_API" = "true" ]; then
docker compose pull
docker compose up -d --force-recreate --remove-orphans
elif [ "$DEPLOY_WEB" = "true" ]; then
docker compose pull frontend
docker compose up -d --force-recreate --no-deps frontend
elif [ "$DEPLOY_API" = "true" ]; then
docker compose pull backend
docker compose up -d --force-recreate --no-deps backend
fi
echo "✅ Deployment complete"
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" | grep pp_ || true