Build & Deploy #12
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |