diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 57463d0..a26f41a 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -29,8 +29,9 @@ jobs: DB_CONNECTION: sqlite DB_DATABASE: ":memory:" with: - args: | - bash -c "composer install --no-interaction --ignore-platform-req=ext-intl && php artisan test" + args: > + bash -c "composer install --no-interaction --ignore-platform-req=ext-intl + && php artisan test" # ========================= # BUILD & PUSH IMAGE @@ -41,7 +42,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout + - name: Checkout code uses: actions/checkout@v4 - name: Login to Gitea Registry @@ -51,16 +52,15 @@ jobs: -u "${{ github.actor }}" \ --password-stdin - - name: Build Docker image + - name: Build & Push 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 BLUE / GREEN # ========================= deploy: name: Deploy Blue/Green @@ -71,7 +71,7 @@ jobs: - name: Checkout deploy files uses: actions/checkout@v4 - - name: Upload Docker Compose file + - name: Upload docker-compose uses: appleboy/scp-action@v0.1.7 with: host: ${{ secrets.SSH_HOST }} @@ -80,130 +80,96 @@ jobs: source: docker-compose.prod.yml target: ${{ secrets.DEPLOY_PATH || '/opt/bemeal' }} - - name: Deploy via SSH (Blue/Green) + - name: Deploy via SSH 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 + envs: REGISTRY,IMAGE_NAME,IMAGE_TAG,DEPLOY_PATH 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-app-1}" - 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." + echo "Missing docker-compose.prod.yml" 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 + COMPOSE="docker compose -f $COMPOSE_FILE" IMAGE="$REGISTRY/$IMAGE_NAME:$IMAGE_TAG" - export IMAGE_TAG - echo "Pull image $IMAGE" + echo "Pull image: $IMAGE" docker pull "$IMAGE" - echo "Ensure shared services are running" + echo "Start shared services" $COMPOSE up -d pgsql redis meilisearch + # ========================= + # BLUE / GREEN LOGIC + # ========================= + 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" + + if docker ps --format '{{.Names}}' | grep -q "bemeal-api-blue"; then + if [ "$(docker inspect -f '{{.State.Health.Status}}' bemeal-api-blue 2>/dev/null || true)" = "healthy" ]; then + ACTIVE="blue" + fi + fi + + if docker ps --format '{{.Names}}' | grep -q "bemeal-api-green"; then + if [ "$(docker inspect -f '{{.State.Health.Status}}' bemeal-api-green 2>/dev/null || true)" = "healthy" ]; then + ACTIVE="green" + fi 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" + echo "Deploying to $TARGET_SERVICE" + + IMAGE="$REGISTRY/$IMAGE_NAME:$IMAGE_TAG" \ $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)" + echo "Waiting for healthcheck..." + for i in $(seq 1 60); do + STATUS="$(docker inspect -f '{{.State.Health.Status}}' "$TARGET_SERVICE" 2>/dev/null || true)" - if [ "$HEALTH_STATUS" = "healthy" ]; then + if [ "$STATUS" = "healthy" ]; then break fi sleep 2 done - if [ "$HEALTH_STATUS" != "healthy" ]; then + if [ "$STATUS" != "healthy" ]; then + echo "Healthcheck failed" 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 + echo "API is healthy on $TARGET_SERVICE" - 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'" + docker exec "$TARGET_SERVICE" wget -qO- http://127.0.0.1/api/health >/dev/null || exit 1 - 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 + echo "Deployment successful" + # optional cleanup old container if [ -n "$ACTIVE" ] && [ "$ACTIVE" != "$TARGET" ]; then - echo "Drain old $ACTIVE service for $DRAIN_SECONDS seconds" - sleep "$DRAIN_SECONDS" + echo "Stopping old $ACTIVE container" $COMPOSE stop "bemeal-api-$ACTIVE" || true $COMPOSE rm -f "bemeal-api-$ACTIVE" || true fi