name: Laravel CI-CD on: push: branches: ["main"] env: REGISTRY: gitea.leonmorival.com IMAGE_NAME: daily_meal/bemeal-api jobs: # ========================= # TESTS # ========================= test: name: Tests Unitaires runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Run Tests uses: docker://laravelsail/php84-composer:latest env: APP_ENV: testing APP_KEY: base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= DB_CONNECTION: sqlite DB_DATABASE: ":memory:" with: args: | bash -c "composer install --no-interaction --ignore-platform-req=ext-intl && php artisan test" # ========================= # BUILD & PUSH IMAGE # ========================= build: name: Build & Push Docker needs: test runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Login to Gitea Registry run: | echo "${{ secrets.TOKEN_GITEA }}" \ | docker login $REGISTRY \ -u "${{ github.actor }}" \ --password-stdin - name: Build Docker image run: | IMAGE=$REGISTRY/$IMAGE_NAME docker build -t "$IMAGE:${{ github.sha }}" -t "$IMAGE:latest" . docker push "$IMAGE:${{ github.sha }}" docker push "$IMAGE:latest" # ========================= # DEPLOY # ========================= deploy: name: Deploy Blue/Green needs: build runs-on: ubuntu-latest steps: - name: Checkout deploy files uses: actions/checkout@v4 - name: Upload Docker Compose file uses: appleboy/scp-action@v0.1.7 with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} source: docker-compose.prod.yml,docker/nginx/* target: ${{ secrets.DEPLOY_PATH || '/opt/bemeal' }} - name: Deploy via SSH (Blue/Green) uses: appleboy/ssh-action@v1.0.3 env: IMAGE_TAG: ${{ github.sha }} DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} NGINX_CONTAINER: ${{ secrets.NGINX_CONTAINER }} NGINX_UPSTREAM_FILE: ${{ secrets.NGINX_UPSTREAM_FILE }} with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} envs: REGISTRY,IMAGE_NAME,IMAGE_TAG,DEPLOY_PATH,NGINX_CONTAINER,NGINX_UPSTREAM_FILE script: | set -eu APP_DIR="${DEPLOY_PATH:-/opt/bemeal}" COMPOSE_FILE="docker-compose.prod.yml" COMPOSE_ENV_FILE=".env.prod" NGINX_CONTAINER="${NGINX_CONTAINER:-nginx}" NGINX_UPSTREAM_FILE="${NGINX_UPSTREAM_FILE:-/etc/nginx/conf.d/bemeal-upstream.inc}" DRAIN_SECONDS="${DRAIN_SECONDS:-20}" if [ ! -d "$APP_DIR" ]; then echo "Missing deploy directory: $APP_DIR" echo "Set the DEPLOY_PATH secret to the directory containing $COMPOSE_FILE." exit 1 fi cd "$APP_DIR" if [ ! -f "$COMPOSE_FILE" ]; then echo "Missing $APP_DIR/$COMPOSE_FILE" echo "Set the DEPLOY_PATH secret to the directory containing $COMPOSE_FILE, or copy the compose file there." exit 1 fi if ! docker inspect "$NGINX_CONTAINER" >/dev/null 2>&1; then echo "Missing Nginx container: $NGINX_CONTAINER" echo "Set the NGINX_CONTAINER secret to your reverse proxy container name." exit 1 fi if [ -f "$COMPOSE_ENV_FILE" ]; then COMPOSE="docker compose --env-file $COMPOSE_ENV_FILE -f $COMPOSE_FILE" else COMPOSE="docker compose -f $COMPOSE_FILE" fi IMAGE="$REGISTRY/$IMAGE_NAME:$IMAGE_TAG" export IMAGE_TAG echo "Pull image $IMAGE" docker pull "$IMAGE" echo "Ensure shared services are running" $COMPOSE up -d pgsql redis meilisearch ACTIVE="" if docker exec "$NGINX_CONTAINER" sh -c "test -f '$NGINX_UPSTREAM_FILE' && grep -q 'bemeal-api-blue:80' '$NGINX_UPSTREAM_FILE'" 2>/dev/null; then ACTIVE="blue" elif docker exec "$NGINX_CONTAINER" sh -c "test -f '$NGINX_UPSTREAM_FILE' && grep -q 'bemeal-api-green:80' '$NGINX_UPSTREAM_FILE'" 2>/dev/null; then ACTIVE="green" fi if [ "$ACTIVE" = "blue" ]; then TARGET="green" TARGET_HOST="bemeal-api-green" else TARGET="blue" TARGET_HOST="bemeal-api-blue" fi TARGET_SERVICE="bemeal-api-$TARGET" echo "Start $TARGET_SERVICE" $COMPOSE up -d --force-recreate --no-deps "$TARGET_SERVICE" echo "Wait for $TARGET_SERVICE healthcheck" HEALTH_STATUS="" for attempt in $(seq 1 60); do HEALTH_STATUS="$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$TARGET_SERVICE" 2>/dev/null || true)" if [ "$HEALTH_STATUS" = "healthy" ]; then break fi sleep 2 done if [ "$HEALTH_STATUS" != "healthy" ]; then docker logs --tail=200 "$TARGET_SERVICE" || true exit 1 fi docker exec "$TARGET_SERVICE" wget -qO- http://127.0.0.1/api/health >/dev/null NGINX_UPSTREAM_DIR="$(dirname "$NGINX_UPSTREAM_FILE")" docker exec "$NGINX_CONTAINER" sh -c "mkdir -p '$NGINX_UPSTREAM_DIR'" docker exec "$NGINX_CONTAINER" sh -c "test ! -f '$NGINX_UPSTREAM_FILE' || cp '$NGINX_UPSTREAM_FILE' '$NGINX_UPSTREAM_FILE.bak'" printf '%s\n' \ 'upstream bemeal_api {' \ " server $TARGET_HOST:80 max_fails=3 fail_timeout=10s;" \ ' keepalive 32;' \ '}' | docker exec -i "$NGINX_CONTAINER" sh -c "cat > '$NGINX_UPSTREAM_FILE'" if ! docker exec "$NGINX_CONTAINER" nginx -t; then if docker exec "$NGINX_CONTAINER" sh -c "test -f '$NGINX_UPSTREAM_FILE.bak'" 2>/dev/null; then docker exec "$NGINX_CONTAINER" sh -c "mv '$NGINX_UPSTREAM_FILE.bak' '$NGINX_UPSTREAM_FILE'" fi exit 1 fi echo "Switch Nginx traffic to $TARGET" docker exec "$NGINX_CONTAINER" nginx -s reload docker exec "$NGINX_CONTAINER" rm -f "$NGINX_UPSTREAM_FILE.bak" echo "Restart workers on the new image" $COMPOSE up -d --force-recreate --no-deps horizon scheduler if [ -n "$ACTIVE" ] && [ "$ACTIVE" != "$TARGET" ]; then echo "Drain old $ACTIVE service for $DRAIN_SECONDS seconds" sleep "$DRAIN_SECONDS" $COMPOSE stop "bemeal-api-$ACTIVE" || true $COMPOSE rm -f "bemeal-api-$ACTIVE" || true fi