diff --git a/.github/workflows/build_deploy.yml b/.github/workflows/build_deploy.yml index ab3dd15..21371ea 100644 --- a/.github/workflows/build_deploy.yml +++ b/.github/workflows/build_deploy.yml @@ -103,6 +103,7 @@ jobs: - name: Extract version for Docker build id: extract_version run: | + pip uninstall setuptools pip install setuptools-scm VERSION=$(python -c "from setuptools_scm import get_version; print(get_version())") echo "VERSION=${VERSION}" >> $GITHUB_ENV @@ -133,9 +134,9 @@ jobs: # Job 2: Deploy (only runs on main branch or tags) deploy: - needs: build + #needs: build runs-on: ubuntu-latest - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) + #if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) # Determine environment based on ref environment: ${{ (startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '-prerelease')) && 'production' || 'staging' }} steps: @@ -173,8 +174,13 @@ jobs: echo "๐Ÿ“ Setting deployment environment variables" echo "REPO_PROJECT_PATH=${REPO_PROJECT_PATH}" >> $GITHUB_ENV echo "REPO_NAME_ONLY=${REPO_NAME_ONLY}" >> $GITHUB_ENV + echo "REPO=${REPO}" >> $GITHUB_ENV echo "IMAGE_TAR_NAME=${REPO_NAME_ONLY}-${{ github.ref_name }}_${{ github.sha }}.tar" >> $GITHUB_ENV echo "PROD=${prod_value}" >> $GITHUB_ENV + echo "GIT_SHA=${{ github.sha }}" >> $GITHUB_ENV + echo "REPLICA_COUNT=${{ vars.REPLICA_COUNT }}" >> $GITHUB_ENV + echo "PRODUCTION_DOMAIN=${{ vars.PRODUCTION_DOMAIN }}" >> $GITHUB_ENV + echo "STAGING_DOMAIN=${{ vars.STAGING_DOMAIN }}" >> $GITHUB_ENV - name: Download container artifact uses: actions/download-artifact@v4 @@ -217,35 +223,40 @@ jobs: env: DOCKER_HOST: ssh://deploy REPO_PROJECT_PATH: ${{ env.REPO_PROJECT_PATH }} + REPO: ${{ env.REPO }} REPO_NAME_ONLY: ${{ env.REPO_NAME_ONLY }} IMAGE_TAR: ${{ runner.temp }}/${{ env.IMAGE_TAR_NAME }} - PROD: ${{ env.PROD }} + PRODrequire_var: ${{ env.PROD }} + GIT_SHA: ${{ github.sha }} + REPLICA_COUNT: ${{ env.REPLICA_COUNT }} + PRODUCTION_DOMAIN: ${{ vars.PRODUCTION_DOMAIN }} + STAGING_DOMAIN: ${{ vars.STAGING_DOMAIN }} run: | echo "โœ… Exit script on any error" set -eu -o pipefail - ./scripts/deploy-to-server.sh + ./scripts/deploy-blue-green.sh - - name: Health Check and Rollback - run: | - # Determine the correct URL based on environment - if [ "${{ env.PROD }}" = "true" ]; then - # Ensure PRODUCTION_DOMAIN is set - if [ -z "${{ vars.PRODUCTION_DOMAIN }}" ]; then - echo "Error: PRODUCTION_DOMAIN is not set" - exit 1 - fi - HEALTH_CHECK_URL="https://${{ vars.PRODUCTION_DOMAIN }}/health/" - else - # Ensure STAGING_DOMAIN is set - if [ -z "${{ vars.STAGING_DOMAIN }}" ]; then - echo "Error: STAGING_DOMAIN is not set" - exit 1 - fi - HEALTH_CHECK_URL="https://${{ vars.STAGING_DOMAIN }}/health/" - fi + # - name: Health Check and Rollback + # run: | + # # Determine the correct URL based on environment + # if [ "${{ env.PROD }}" = "true" ]; then + # # Ensure PRODUCTION_DOMAIN is set + # if [ -z "${{ vars.PRODUCTION_DOMAIN }}" ]; then + # echo "Error: PRODUCTION_DOMAIN is not set" + # exit 1 + # fi + # HEALTH_CHECK_URL="https://${{ vars.PRODUCTION_DOMAIN }}/health/" + # else + # # Ensure STAGING_DOMAIN is set + # if [ -z "${{ vars.STAGING_DOMAIN }}" ]; then + # echo "Error: STAGING_DOMAIN is not set" + # exit 1 + # fi + # HEALTH_CHECK_URL="https://${{ vars.STAGING_DOMAIN }}/health/" + # fi - # Copy script to remote and execute - scp scripts/health-check-and-rollback.sh deploy:/tmp/ - ssh deploy "chmod +x /tmp/health-check-and-rollback.sh" - ssh deploy "/tmp/health-check-and-rollback.sh '${{ env.REPO_PROJECT_PATH }}' '${{ env.PROD }}' '$HEALTH_CHECK_URL' 30" - ssh deploy "rm -f /tmp/health-check-and-rollback.sh" \ No newline at end of file + # # Copy script to remote and execute + # scp scripts/health-check-and-rollback.sh deploy:/tmp/ + # ssh deploy "chmod +x /tmp/health-check-and-rollback.sh" + # ssh deploy "/tmp/health-check-and-rollback.sh '${{ env.REPO_PROJECT_PATH }}' '$HEALTH_CHECK_URL' 30" + # ssh deploy "rm -f /tmp/health-check-and-rollback.sh" \ No newline at end of file diff --git a/scripts/common-lib.sh b/scripts/common-lib.sh new file mode 120000 index 0000000..191fd48 --- /dev/null +++ b/scripts/common-lib.sh @@ -0,0 +1 @@ +../server/scripts/common-lib.sh \ No newline at end of file diff --git a/scripts/deploy-blue-green.sh b/scripts/deploy-blue-green.sh new file mode 100755 index 0000000..2392d67 --- /dev/null +++ b/scripts/deploy-blue-green.sh @@ -0,0 +1,207 @@ +#!/bin/bash +set -euo pipefail + +# Blue-Green deployment script with versioned releases +# Usage: ./deploy-blue-green.sh + +# Source common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/common-lib.sh" + +# Validate required environment variables +require_var "DOCKER_HOST" +require_var "REPO_PROJECT_PATH" +require_var "REPO_NAME_ONLY" +require_var "REPO" +require_var "IMAGE_TAR" +require_var "ENV_FILE_BASE64" +require_var "CF_PEM_CERT" +require_var "CF_PEM_CA" +require_var "PROD" +require_var "PRODUCTION_DOMAIN" +require_var "STAGING_DOMAIN" +require_var "REPLICA_COUNT" + +validate_deployment_env + +echo "โš™๏ธ Docker host: $DOCKER_HOST" + +# Generate deployment timestamp +DEPLOYMENT_TIMESTAMP=$(date +%Y%m%d_%H%M%S) +NEW_RELEASE_PATH="${RELEASES_PATH}/${DEPLOYMENT_TIMESTAMP}" + +# Use Git SHA for image tag (if available, otherwise use timestamp) +if [ -n "${GIT_SHA:-}" ]; then + IMAGE_TAG="sha-${GIT_SHA:0:7}" +else + # Fallback for local testing without GIT_SHA + IMAGE_TAG="local-${DEPLOYMENT_TIMESTAMP}" +fi + +# Check for deployment in progress +if is_deployment_in_progress; then + echo "โš ๏ธ ERROR: Deployment appears to be in progress (both colors are running)" + echo " This might indicate a previous deployment didn't complete properly." + echo " Please check the deployment status and clean up any old containers." + echo " If you are sure that the deployment is complete, you can run the following command to clean up the old containers:" + echo " ssh deploy 'docker compose -p pkmntrade-club-blue down && docker compose -p pkmntrade-club-green down'" + exit 1 +fi + +# Get current and new colors +CURRENT_COLOR=$(get_current_color) +NEW_COLOR=$(switch_color "$CURRENT_COLOR") + +echo "๐Ÿ“… Deployment version: ${DEPLOYMENT_TIMESTAMP}" +echo "๐Ÿท๏ธ Image tag: ${IMAGE_TAG}" +echo "๐ŸŽจ Current: $CURRENT_COLOR โ†’ New: $NEW_COLOR" + +echo "๐Ÿš€ Enable and start docker service" +retry run_on_target "sudo systemctl enable --now docker.service" + +echo "๐Ÿ’พ Load the new docker image ($IMAGE_TAR)" +if [ ! -f "$IMAGE_TAR" ]; then + echo "Error: Docker image tar file not found: $IMAGE_TAR" + exit 1 +fi + +# Load the image - Docker handles the transfer via DOCKER_HOST +echo "๐Ÿ“ฆ Loading Docker image..." +#retry docker load -i "$IMAGE_TAR" + +# Verify the expected image exists +echo "๐Ÿ” Verifying image ${REPO}:${IMAGE_TAG} exists..." +if ! docker images -q "${REPO}:${IMAGE_TAG}" | grep -q .; then + echo "โŒ Expected image tag ${IMAGE_TAG} not found!" + echo "Available tags:" + docker images "${REPO}" --format "{{.Tag}}" + exit 1 +fi + +echo "๐Ÿ“ Create versioned release directory" +run_on_target "mkdir -p '${NEW_RELEASE_PATH}'" + +echo "๐Ÿ’พ Copy new files to server" +if [ -d "./server" ]; then + retry scp -pr ./server/* "deploy:${NEW_RELEASE_PATH}/" +else + echo "โš ๏ธ No server directory found, error" + exit 1 +fi + +echo "๐Ÿ“ Create new .env file with deployment configuration" +printf "%s" "${ENV_FILE_BASE64}" | base64 -d | run_on_target "cat > '${NEW_RELEASE_PATH}/.env' && chmod 600 '${NEW_RELEASE_PATH}/.env'" +# Add deployment color and image tag to .env +run_on_target "echo 'DEPLOYMENT_COLOR=${NEW_COLOR}' >> '${NEW_RELEASE_PATH}/.env'" +run_on_target "echo 'IMAGE_TAG=${IMAGE_TAG}' >> '${NEW_RELEASE_PATH}/.env'" + +# Add domain name based on environment +if [ "${PROD}" = "true" ]; then + DOMAIN_NAME="${PRODUCTION_DOMAIN:-pkmntrade.club}" +else + DOMAIN_NAME="${STAGING_DOMAIN:-staging.pkmntrade.club}" +fi +# if there is a third part to the domain name, remove it +BASE_DOMAIN_NAME="${BASE_DOMAIN:-pkmntrade.club}" +run_on_target "echo 'DOMAIN_NAME=${DOMAIN_NAME}' >> '${NEW_RELEASE_PATH}/.env'" +run_on_target "echo 'BASE_DOMAIN_NAME=${BASE_DOMAIN_NAME}' >> '${NEW_RELEASE_PATH}/.env'" +run_on_target "echo 'REPLICA_COUNT=${REPLICA_COUNT}' >> '${NEW_RELEASE_PATH}/.env'" + +echo "๐Ÿ”‘ Set up certs" +run_on_target "mkdir -p '${NEW_RELEASE_PATH}/certs' && chmod 550 '${NEW_RELEASE_PATH}/certs' && chown 99:root '${NEW_RELEASE_PATH}/certs'" +printf "%s" "$CF_PEM_CERT" | run_on_target "cat > '${NEW_RELEASE_PATH}/certs/crt.pem' && chmod 440 '${NEW_RELEASE_PATH}/certs/crt.pem' && chown 99:root '${NEW_RELEASE_PATH}/certs/crt.pem'" +printf "%s" "$CF_PEM_CA" | run_on_target "cat > '${NEW_RELEASE_PATH}/certs/ca.pem' && chmod 440 '${NEW_RELEASE_PATH}/certs/ca.pem' && chown 99:root '${NEW_RELEASE_PATH}/certs/ca.pem'" + +echo "๐Ÿ“ Save deployment metadata" +run_on_target "echo '${DEPLOYMENT_TIMESTAMP}' > '${NEW_RELEASE_PATH}/.deployment_version'" +run_on_target "echo '${PROD}' > '${NEW_RELEASE_PATH}/.deployment_is_prod'" +run_on_target "echo '${NEW_COLOR}' > '${NEW_RELEASE_PATH}/.deployment_color'" +run_on_target "echo '${IMAGE_TAG}' > '${NEW_RELEASE_PATH}/.image_tag'" +run_on_target "echo '${GIT_SHA:-unknown}' > '${NEW_RELEASE_PATH}/.git_sha'" + +# Save previous version info for potential rollback +run_on_target "if [ -L '${CURRENT_LINK_PATH}' ]; then readlink -f '${CURRENT_LINK_PATH}' > '${NEW_RELEASE_PATH}/.previous_version'; fi" + +# export PREVIOUS_RELEASE_PATH +if [ "$CURRENT_COLOR" != "none" ]; then + PREVIOUS_RELEASE_PATH=$(run_on_target "cat ${NEW_RELEASE_PATH}/.previous_version") +else + PREVIOUS_RELEASE_PATH="" +fi +run_on_target "export PREVIOUS_RELEASE_PATH='${PREVIOUS_RELEASE_PATH}'" + +echo "๐Ÿ”— Update current symlink to new release" +run_on_target "ln -sfn '${NEW_RELEASE_PATH}' '${CURRENT_LINK_PATH}'" + +# Get deployment configuration +PROJECT_NAME=$(get_project_name "$NEW_COLOR") +COMPOSE_FILES=$(get_compose_files) +WEB_SERVICE=$(get_web_service_name) + +# create network if it doesn't exist +echo "๐Ÿ”— Creating network ${PROJECT_NAME}_network" +run_on_target "docker network create ${REPO_NAME_ONLY}_network >/dev/null 2>&1 || true" + +# Handle core services +if [ "$CURRENT_COLOR" = "none" ]; then + echo "๐Ÿš€ Starting core services (first deployment)" + retry run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose -f docker-compose_core.yml -p ${CORE_PROJECT_NAME} up -d" + sleep 10 # Give core services time to start +else + echo "โ„น๏ธ Core services already running, checking for changes..." + PREVIOUS_SHA1=$(run_on_target "sha1sum '${PREVIOUS_RELEASE_PATH}/docker-compose_core.yml' | awk '{print \$1}'") + NEW_SHA1=$(run_on_target "sha1sum '${NEW_RELEASE_PATH}/docker-compose_core.yml' | awk '{print \$1}'") + echo "PREV_SHA1: ${PREVIOUS_SHA1}" + echo " NEW_SHA1: ${NEW_SHA1}" + if [ -n "$PREVIOUS_SHA1" ] && [ -n "$NEW_SHA1" ]; then + if [ "$PREVIOUS_SHA1" != "$NEW_SHA1" ]; then + echo "๐Ÿš€ Core services have changed, restarting..." + retry run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose -f docker-compose_core.yml -p ${CORE_PROJECT_NAME} down" + retry run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose -f docker-compose_core.yml -p ${CORE_PROJECT_NAME} up -d" + else + echo "โ„น๏ธ Core services have not changed, still restarting due to current folder change..." + retry run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose -f docker-compose_core.yml -p ${CORE_PROJECT_NAME} down" + retry run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose -f docker-compose_core.yml -p ${CORE_PROJECT_NAME} up -d" + fi + else + echo "โŒ Current or previous core services not found, exiting..." + exit 1 + fi +fi + +echo "๐Ÿš€ Start new ${NEW_COLOR} containers with image ${IMAGE_TAG}" +retry run_on_target "cd '${CURRENT_LINK_PATH}' && DEPLOYMENT_COLOR=${NEW_COLOR} IMAGE_TAG=${IMAGE_TAG} docker compose $COMPOSE_FILES -p ${PROJECT_NAME} up -d" + +# Wait for new containers to be healthy +if ! wait_for_healthy_containers "$PROJECT_NAME" "$WEB_SERVICE" "$REPLICA_COUNT"; then + echo "โŒ New containers failed health checks. Cancelling deployment..." + run_on_target "cd '${CURRENT_LINK_PATH}' && docker compose $COMPOSE_FILES -p ${PROJECT_NAME} down" + #echo "๐Ÿ”„ Rolling back deployment..." + #TODO: implement rollback + exit 1 +fi + +echo "โœ… New ${NEW_COLOR} deployment is healthy" + +# Refresh gatekeepers +refresh_gatekeepers + +# Wait for traffic to stabilize +wait_with_countdown 20 "โณ Waiting for traffic to stabilize..." + +# Clean up old containers if this isn't the first deployment +if [ "$CURRENT_COLOR" != "none" ]; then + # Get the old image tag before cleanup + OLD_IMAGE_TAG=$(get_deployment_image_tag "$CURRENT_COLOR") + echo "๐Ÿ“ท Old deployment was using image: ${OLD_IMAGE_TAG}" + + cleanup_color_containers "$CURRENT_COLOR" + echo "โœ… Old containers removed" +fi + +echo "๐Ÿ—‘๏ธ Clean up old releases (keep last 5)" +run_on_target "cd '${RELEASES_PATH}' && ls -dt */ 2>/dev/null | tail -n +6 | xargs -r rm -rf || true" + +echo "โœ… Blue-Green deployment completed" +echo " Active color: ${NEW_COLOR}" +echo " Image tag: ${IMAGE_TAG}" \ No newline at end of file diff --git a/scripts/deploy-to-server.sh b/scripts/deploy-to-server.sh deleted file mode 100644 index 1a375ac..0000000 --- a/scripts/deploy-to-server.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Main deployment script with versioned releases -# Usage: ./deploy-to-server.sh - -# Source retry function -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "${SCRIPT_DIR}/retry.sh" - -# Required environment variables (should be set by GitHub Actions) -: "${DOCKER_HOST:?Error: DOCKER_HOST not set}" -: "${REPO_PROJECT_PATH:?Error: REPO_PROJECT_PATH not set}" -: "${REPO_NAME_ONLY:?Error: REPO_NAME_ONLY not set}" -: "${IMAGE_TAR:?Error: IMAGE_TAR not set}" -: "${ENV_FILE_BASE64:?Error: ENV_FILE_BASE64 not set}" -: "${CF_PEM_CERT:?Error: CF_PEM_CERT not set}" -: "${CF_PEM_CA:?Error: CF_PEM_CA not set}" -: "${PROD:?Error: PROD not set}" - -echo "โš™๏ธ Docker host: $DOCKER_HOST" - -# Generate deployment timestamp -DEPLOYMENT_TIMESTAMP=$(date +%Y%m%d_%H%M%S) -RELEASES_PATH="${REPO_PROJECT_PATH}/releases" -NEW_RELEASE_PATH="${RELEASES_PATH}/${DEPLOYMENT_TIMESTAMP}" -CURRENT_LINK_PATH="${REPO_PROJECT_PATH}/current" - -echo "๐Ÿ“… Deployment version: ${DEPLOYMENT_TIMESTAMP}" - -echo "๐Ÿš€ Enable and start docker service" -retry ssh deploy "sudo systemctl enable --now docker.service" - -echo "๐Ÿ’พ Load the new docker image ($IMAGE_TAR)" -if [ ! -f "$IMAGE_TAR" ]; then - echo "Error: Docker image tar file not found: $IMAGE_TAR" - exit 1 -fi -retry docker load -i "$IMAGE_TAR" - -echo "๐Ÿ“ Create versioned release directory" -ssh deploy "mkdir -p '${NEW_RELEASE_PATH}'" - -echo "๐Ÿ’พ Copy new files to server" -# Check if server directory exists before copying -if [ -d "./server" ]; then - retry scp -pr ./server/* "deploy:${NEW_RELEASE_PATH}/" -else - echo "โš ๏ธ No server directory found, error" - exit 1 -fi - -echo "๐Ÿ“ Create new .env file" -printf "%s" "${ENV_FILE_BASE64}" | base64 -d | ssh deploy "cat > '${NEW_RELEASE_PATH}/.env' && chmod 600 '${NEW_RELEASE_PATH}/.env'" - -echo "๐Ÿ”‘ Set up certs" -ssh deploy "mkdir -p '${NEW_RELEASE_PATH}/certs' && chmod 550 '${NEW_RELEASE_PATH}/certs' && chown 99:root '${NEW_RELEASE_PATH}/certs'" -printf "%s" "$CF_PEM_CERT" | ssh deploy "cat > '${NEW_RELEASE_PATH}/certs/crt.pem' && chmod 440 '${NEW_RELEASE_PATH}/certs/crt.pem' && chown 99:root '${NEW_RELEASE_PATH}/certs/crt.pem'" -printf "%s" "$CF_PEM_CA" | ssh deploy "cat > '${NEW_RELEASE_PATH}/certs/ca.pem' && chmod 440 '${NEW_RELEASE_PATH}/certs/ca.pem' && chown 99:root '${NEW_RELEASE_PATH}/certs/ca.pem'" - -echo "๐Ÿ”„ Prepare deployment (stop current containers)" -# Copy script to remote and execute with parameters -scp "${SCRIPT_DIR}/prepare-deployment.sh" deploy:/tmp/ -ssh deploy "chmod +x /tmp/prepare-deployment.sh && /tmp/prepare-deployment.sh '${REPO_PROJECT_PATH}' '${PROD}' '${CURRENT_LINK_PATH}'" -ssh deploy "rm -f /tmp/prepare-deployment.sh" - -echo "๐Ÿ“ Save deployment metadata" -ssh deploy "echo '${DEPLOYMENT_TIMESTAMP}' > '${NEW_RELEASE_PATH}/.deployment_version'" -ssh deploy "echo '${PROD}' > '${NEW_RELEASE_PATH}/.deployment_env'" - -# Save previous version info for potential rollback -ssh deploy "if [ -L '${CURRENT_LINK_PATH}' ]; then readlink -f '${CURRENT_LINK_PATH}' > '${NEW_RELEASE_PATH}/.previous_version'; fi" - -echo "๐Ÿ”— Update current symlink to new release" -ssh deploy "ln -sfn '${NEW_RELEASE_PATH}' '${CURRENT_LINK_PATH}'" - -# TODO: implement zero-downtime deployment -# echo "๐Ÿš€ Start the new containers, zero-downtime" -# if [ "${PROD}" = true ]; then -# ssh deploy </dev/null | tail -n +6 | xargs -r rm -rf || true" - -echo "โœ… Deployment completed. Version: ${DEPLOYMENT_TIMESTAMP}" \ No newline at end of file diff --git a/scripts/generate-docker-tags.sh b/scripts/generate-docker-tags.sh old mode 100644 new mode 100755 diff --git a/scripts/health-check-and-rollback.sh b/scripts/health-check-and-rollback.sh deleted file mode 100644 index b07607b..0000000 --- a/scripts/health-check-and-rollback.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Perform health check and rollback if necessary -# Usage: ./health-check-and-rollback.sh REPO_PROJECT_PATH PROD HEALTH_CHECK_URL [MAX_ATTEMPTS] - -if [ $# -lt 3 ]; then - echo "Error: Invalid number of arguments" - echo "Usage: $0 REPO_PROJECT_PATH PROD HEALTH_CHECK_URL [MAX_ATTEMPTS]" - exit 1 -fi - -REPO_PROJECT_PATH="$1" -PROD="$2" -HEALTH_CHECK_URL="$3" -MAX_ATTEMPTS="${4:-30}" - -CURRENT_LINK_PATH="${REPO_PROJECT_PATH}/current" -RELEASES_PATH="${REPO_PROJECT_PATH}/releases" - -echo "๐Ÿฅ Performing health check..." -echo "Health check URL: $HEALTH_CHECK_URL" - -get_current_version() { - if [ -L "$CURRENT_LINK_PATH" ]; then - basename "$(readlink -f "$CURRENT_LINK_PATH")" - else - echo "unknown" - fi -} - -ATTEMPT=0 -while [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; do - # Check if the service is responding with 200 OK - HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -m 10 "$HEALTH_CHECK_URL" || echo '000') - - if [ "$HTTP_CODE" = "200" ]; then - echo "โœ… Health check passed! (HTTP $HTTP_CODE)" - CURRENT_VERSION=$(get_current_version) - echo "๐Ÿ“Œ Current version: ${CURRENT_VERSION}" - exit 0 - fi - - ATTEMPT=$((ATTEMPT + 1)) - if [ "$ATTEMPT" -eq "$MAX_ATTEMPTS" ]; then - echo "โŒ Health check failed after $MAX_ATTEMPTS attempts (Last HTTP code: $HTTP_CODE)" - echo "๐Ÿ”„ Rolling back deployment..." - - FAILED_VERSION=$(get_current_version) - echo "โŒ Failed version: ${FAILED_VERSION}" - - # Check if we have a previous version to roll back to - if [ -f "${CURRENT_LINK_PATH}/.previous_version" ]; then - PREVIOUS_VERSION_PATH=$(cat "${CURRENT_LINK_PATH}/.previous_version") - PREVIOUS_VERSION=$(basename "$PREVIOUS_VERSION_PATH") - - if [ -d "$PREVIOUS_VERSION_PATH" ]; then - echo "๐Ÿ”„ Rolling back to version: ${PREVIOUS_VERSION}" - - # Stop failed deployment containers - cd "$CURRENT_LINK_PATH" - echo "Stopping failed deployment containers..." - docker compose -f docker-compose_web.yml -p pkmntrade-club down || true - if [ "$PROD" = "false" ]; then - docker compose -f docker-compose_staging.yml -p pkmntrade-club down || true - fi - docker compose -f docker-compose_core.yml -p pkmntrade-club down || true - - # Switch symlink back to previous version - ln -sfn "$PREVIOUS_VERSION_PATH" "$CURRENT_LINK_PATH" - - # Start previous version containers - cd "$CURRENT_LINK_PATH" - docker compose -f docker-compose_core.yml -p pkmntrade-club up -d --no-build - if [ "$PROD" = "true" ]; then - docker compose -f docker-compose_web.yml -p pkmntrade-club up -d --no-build - else - docker compose -f docker-compose_web.yml -f docker-compose_staging.yml -p pkmntrade-club up -d --no-build - fi - - echo "โœ… Rollback completed to version: ${PREVIOUS_VERSION}" - - # Mark failed version - if [ -d "${RELEASES_PATH}/${FAILED_VERSION}" ]; then - touch "${RELEASES_PATH}/${FAILED_VERSION}/.failed" - echo "$(date): Health check failed, rolled back to ${PREVIOUS_VERSION}" > "${RELEASES_PATH}/${FAILED_VERSION}/.failure_reason" - fi - else - echo "โŒ Previous version directory not found: $PREVIOUS_VERSION_PATH" - exit 1 - fi - else - echo "โŒ No previous version information found. Cannot rollback!" - echo "๐Ÿ’ก This might be the first deployment or the previous version info is missing." - exit 1 - fi - exit 1 - fi - - echo "โณ Waiting for service to be healthy... (attempt $ATTEMPT/$MAX_ATTEMPTS, HTTP code: $HTTP_CODE)" - sleep 10 -done \ No newline at end of file diff --git a/scripts/manage-releases.sh b/scripts/manage-releases.sh deleted file mode 100644 index f7e16b7..0000000 --- a/scripts/manage-releases.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Manage deployment releases -# Usage: ./manage-releases.sh REPO_PROJECT_PATH COMMAND [ARGS] - -if [ $# -lt 2 ]; then - echo "Error: Invalid number of arguments" - echo "Usage: $0 REPO_PROJECT_PATH COMMAND [ARGS]" - echo "Commands:" - echo " list - List all releases" - echo " current - Show current release" - echo " rollback VERSION - Rollback to specific version" - echo " cleanup [KEEP] - Clean up old releases (default: keep 5)" - exit 1 -fi - -REPO_PROJECT_PATH="$1" -COMMAND="$2" -CURRENT_LINK_PATH="${REPO_PROJECT_PATH}/current" -RELEASES_PATH="${REPO_PROJECT_PATH}/releases" - -case "$COMMAND" in - list) - echo "๐Ÿ“‹ Available releases:" - if [ -d "$RELEASES_PATH" ]; then - for release in $(ls -dt "${RELEASES_PATH}"/*/); do - version=$(basename "$release") - status="" - - # Check if it's current - if [ -L "$CURRENT_LINK_PATH" ] && [ "$(readlink -f "$CURRENT_LINK_PATH")" = "$(realpath "$release")" ]; then - status=" [CURRENT]" - fi - - # Check if it failed - if [ -f "${release}/.failed" ]; then - status="${status} [FAILED]" - fi - - echo " - ${version}${status}" - done - else - echo "No releases found" - fi - ;; - - current) - if [ -L "$CURRENT_LINK_PATH" ]; then - current_version=$(basename "$(readlink -f "$CURRENT_LINK_PATH")") - echo "๐Ÿ“Œ Current version: ${current_version}" - else - echo "โŒ No current deployment found" - fi - ;; - - rollback) - if [ $# -lt 3 ]; then - echo "Error: VERSION required for rollback" - exit 1 - fi - TARGET_VERSION="$3" - TARGET_PATH="${RELEASES_PATH}/${TARGET_VERSION}" - - if [ ! -d "$TARGET_PATH" ]; then - echo "Error: Version ${TARGET_VERSION} not found" - exit 1 - fi - - echo "๐Ÿ”„ Rolling back to version: ${TARGET_VERSION}" - - # Read environment from target version - if [ -f "${TARGET_PATH}/.deployment_env" ]; then - PROD=$(cat "${TARGET_PATH}/.deployment_env") - else - echo "Warning: Could not determine environment, assuming staging" - PROD="false" - fi - - # Stop current containers - if [ -L "$CURRENT_LINK_PATH" ] && [ -d "$CURRENT_LINK_PATH" ]; then - cd "$CURRENT_LINK_PATH" - docker compose -f docker-compose_web.yml down || true - [ "$PROD" = "false" ] && docker compose -f docker-compose_staging.yml down || true - docker compose -f docker-compose_core.yml down || true - fi - - # Update symlink - ln -sfn "$TARGET_PATH" "$CURRENT_LINK_PATH" - - # Start containers - cd "$CURRENT_LINK_PATH" - docker compose -f docker-compose_core.yml up -d --no-build - if [ "$PROD" = "true" ]; then - docker compose -f docker-compose_web.yml up -d --no-build - else - docker compose -f docker-compose_web.yml -f docker-compose_staging.yml up -d --no-build - fi - - echo "โœ… Rollback completed" - ;; - - cleanup) - KEEP_COUNT="${3:-5}" - echo "๐Ÿ—‘๏ธ Cleaning up old releases (keeping last ${KEEP_COUNT})" - - if [ -d "$RELEASES_PATH" ]; then - cd "$RELEASES_PATH" - ls -dt */ 2>/dev/null | tail -n +$((KEEP_COUNT + 1)) | xargs -r rm -rf || true - echo "โœ… Cleanup completed" - else - echo "No releases directory found" - fi - ;; - - *) - echo "Error: Unknown command: $COMMAND" - exit 1 - ;; -esac \ No newline at end of file diff --git a/scripts/parse-repository-name.sh b/scripts/parse-repository-name.sh old mode 100644 new mode 100755 index dc1343f..54a0fca --- a/scripts/parse-repository-name.sh +++ b/scripts/parse-repository-name.sh @@ -16,14 +16,17 @@ echo "GITHUB_REPOSITORY: $GITHUB_REPOSITORY" > /dev/stderr if [[ "$GITHUB_REPOSITORY" == *".git" ]]; then if [[ "$GITHUB_REPOSITORY" == "https://"* ]]; then - echo "GITHUB_REPOSITORY ends in .git and is a URL" > /dev/stderr + echo "GITHUB_REPOSITORY ends in .git and is an HTTPS URI" > /dev/stderr REPO=$(echo "$GITHUB_REPOSITORY" | sed 's/\.git$//' | cut -d'/' -f4-5 | sed 's/[^a-zA-Z0-9\/-]/-/g') + elif [[ "$GITHUB_REPOSITORY" == "git@"* ]]; then + echo "GITHUB_REPOSITORY ends in .git and is an SSH URI" > /dev/stderr + REPO=$(echo "$GITHUB_REPOSITORY" | sed 's/\.git$//' | cut -d':' -f2 | sed 's/[^a-zA-Z0-9\/-]/-/g') else - echo "GITHUB_REPOSITORY ends in .git and is not a URL" > /dev/stderr + echo "GITHUB_REPOSITORY ends in .git and is not a URI" > /dev/stderr REPO=$(echo "$GITHUB_REPOSITORY" | sed 's/\.git$//' | sed 's/[^a-zA-Z0-9\/-]/-/g') fi else - echo "GITHUB_REPOSITORY is not a URL" > /dev/stderr + echo "GITHUB_REPOSITORY is not a URI" > /dev/stderr REPO=$(echo "$GITHUB_REPOSITORY" | sed 's/[^a-zA-Z0-9\/-]/-/g') fi diff --git a/scripts/prepare-deployment.sh b/scripts/prepare-deployment.sh deleted file mode 100644 index 15a41c4..0000000 --- a/scripts/prepare-deployment.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Prepare deployment by stopping containers -# Usage: ./prepare-deployment.sh REPO_PROJECT_PATH PROD CURRENT_LINK_PATH - -if [ $# -ne 3 ]; then - echo "Error: Invalid number of arguments" - echo "Usage: $0 REPO_PROJECT_PATH PROD CURRENT_LINK_PATH" - exit 1 -fi - -REPO_PROJECT_PATH="$1" -PROD="$2" -CURRENT_LINK_PATH="$3" - -# Ensure base directory exists -if [ ! -d "$REPO_PROJECT_PATH" ]; then - echo "โš ๏ธ Directory $REPO_PROJECT_PATH does not exist, creating it..." - mkdir -p "$REPO_PROJECT_PATH" -fi - -# If current symlink exists, stop containers in that directory -if [ -L "$CURRENT_LINK_PATH" ] && [ -d "$CURRENT_LINK_PATH" ]; then - echo "๐Ÿ›‘ Stopping containers in current deployment..." - cd "$CURRENT_LINK_PATH" - - # Stop containers - if [ -f "docker-compose_web.yml" ]; then - docker compose -f docker-compose_web.yml -p pkmntrade-club down || true - fi - - if [ "$PROD" = "false" ] && [ -f "docker-compose_staging.yml" ]; then - docker compose -f docker-compose_staging.yml -p pkmntrade-club down || true - fi - - if [ -f "docker-compose_core.yml" ]; then - docker compose -f docker-compose_core.yml -p pkmntrade-club down || true - fi - - echo "โœ… Containers stopped" -else - echo "โ„น๏ธ No current deployment found (symlink doesn't exist or point to valid directory)" -fi \ No newline at end of file diff --git a/scripts/retry.sh b/scripts/retry.sh deleted file mode 100644 index 42ee35c..0000000 --- a/scripts/retry.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -# Retry function with exponential backoff -# Usage: source retry.sh && retry - -retry() { - local max_attempts=3 - local delay=5 - local attempt=1 - - until "$@"; do - if [ "$attempt" -ge "$max_attempts" ]; then - echo "Command failed after $max_attempts attempts: $*" - return 1 - fi - - echo "Command failed (attempt $attempt/$max_attempts): $*" - echo "Retrying in $delay seconds..." - sleep "$delay" - attempt=$((attempt + 1)) - delay=$((delay * 2)) # Exponential backoff - done -} \ No newline at end of file diff --git a/server/docker-compose_core.yml b/server/docker-compose_core.yml index c49b1d2..83420c6 100644 --- a/server/docker-compose_core.yml +++ b/server/docker-compose_core.yml @@ -53,6 +53,8 @@ services: done env_file: - .env + labels: + - "deployment.core=true" loba: image: haproxy:3.1 stop_signal: SIGTERM @@ -64,11 +66,14 @@ services: volumes: - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg - ./certs:/certs + labels: + - "deployment.core=true" feedback: restart: always image: getfider/fider:stable labels: - "enable_gatekeeper=true" + - "deployment.core=true" env_file: - .env # cadvisor: @@ -91,6 +96,8 @@ services: timeout: 5s retries: 5 start_period: 10s + labels: + - "deployment.core=true" dockergen-health: image: nginxproxy/docker-gen:latest command: -wait 15s -watch /gatus/config.template.yaml /gatus/config.yaml @@ -98,6 +105,8 @@ services: volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./gatus:/gatus + labels: + - "deployment.core=true" dockergen-gatekeeper: image: nginxproxy/docker-gen:latest command: -wait 15s -watch /gatekeeper/gatekeepers.template.yml /gatekeeper/gatekeepers.yml -notify-sighup pkmntrade-club-gatekeeper-manager-1 @@ -105,6 +114,8 @@ services: volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./:/gatekeeper + labels: + - "deployment.core=true" gatekeeper-manager: image: docker:latest restart: always @@ -115,6 +126,8 @@ services: environment: - REFRESH_INTERVAL=60 entrypoint: ["/bin/sh", "-c"] + labels: + - "deployment.core=true" command: - | set -eu -o pipefail @@ -239,7 +252,7 @@ services: echo "$(date +'%Y-%m-%d %H:%M:%S') [INFO]: Periodic healthcheck and refresh triggered." if [ ! -f "$$COMPOSE_FILE_PATH" ]; then - echo "$(date +'%Y-%m-%d %H:%M:%S') [ERROR]: Gatekeepers.yml has not been generated after $$REFRESH_INTERVAL seconds. Please check dockergen-gatekeeper is running correctly. Exiting." + echo "$(date +'%Y-%m-%d %H:%M:%S') [ERROR]: gatekeepers.yml has not been generated after $$REFRESH_INTERVAL seconds. Please check dockergen-gatekeeper is running correctly. Exiting." exit 1 fi @@ -254,9 +267,21 @@ services: restart: always labels: - "enable_gatekeeper=true" + - "deployment.core=true" + # healthcheck: + # test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + # interval: 10s + # timeout: 5s + # retries: 5 + # start_period: 10s env_file: - .env environment: - GATUS_DELAY_START_SECONDS=30 volumes: - - ./gatus:/config \ No newline at end of file + - ./gatus:/config + +networks: + default: + name: pkmntrade-club_network + external: true \ No newline at end of file diff --git a/server/docker-compose_staging.yml b/server/docker-compose_staging.yml deleted file mode 100644 index 90020bc..0000000 --- a/server/docker-compose_staging.yml +++ /dev/null @@ -1,32 +0,0 @@ -x-common: &common - image: badbl0cks/pkmntrade-club:staging - restart: always - env_file: - - .env -services: - web-staging: - <<: *common - environment: - - DEBUG=False - - DISABLE_SIGNUPS=True - - PUBLIC_HOST=staging.pkmntrade.club - - ALLOWED_HOSTS=staging.pkmntrade.club,127.0.0.1 - labels: - - "enable_gatekeeper=true" - deploy: - mode: replicated - replicas: 2 - # healthcheck: - # test: ["CMD", "curl", "-f", "http://127.0.0.1:8000/health"] - # interval: 30s - # timeout: 10s - # retries: 3 - # start_period: 30s - celery-staging: - <<: *common - environment: - - DEBUG=False - - DISABLE_SIGNUPS=True - - PUBLIC_HOST=staging.pkmntrade.club - - ALLOWED_HOSTS=staging.pkmntrade.club,127.0.0.1 - command: ["celery", "-A", "pkmntrade_club.django_project", "worker", "-l", "INFO", "-B", "-E"] \ No newline at end of file diff --git a/server/docker-compose_web.yml b/server/docker-compose_web.yml index 6d453e0..15245e2 100644 --- a/server/docker-compose_web.yml +++ b/server/docker-compose_web.yml @@ -1,4 +1,7 @@ x-common: &common + image: badbl0cks/pkmntrade-club:${IMAGE_TAG:-stable} + #image: ghcr.io/xe/x/httpdebug + #entrypoint: ["/ko-app/httpdebug", "--bind", ":8000"] restart: always env_file: - .env @@ -6,31 +9,42 @@ x-common: &common services: web: <<: *common - image: ghcr.io/xe/x/httpdebug - entrypoint: ["/ko-app/httpdebug", "--bind", ":8000"] - #image: badbl0cks/pkmntrade-club:stable + environment: + - DEBUG=False + - DISABLE_SIGNUPS=True + - PUBLIC_HOST=${DOMAIN_NAME} + - ALLOWED_HOSTS=${DOMAIN_NAME},127.0.0.1 + - DEPLOYMENT_COLOR=${DEPLOYMENT_COLOR:-blue} + labels: + - "enable_gatekeeper=true" + - "deployment.color=${DEPLOYMENT_COLOR:-blue}" + - "deployment.image_tag=${IMAGE_TAG:-stable}" + deploy: + mode: replicated + replicas: ${REPLICA_COUNT} + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:8000/health/"] + interval: 5s + timeout: 3s + retries: 2 + start_period: 60s + stop_grace_period: 200s # 20s buffer + 180s workers-kill-timeout + + celery: + <<: *common environment: - DEBUG=False - DISABLE_SIGNUPS=True - PUBLIC_HOST=pkmntrade.club - ALLOWED_HOSTS=pkmntrade.club,127.0.0.1 + - DEPLOYMENT_COLOR=${DEPLOYMENT_COLOR:-blue} labels: - - "enable_gatekeeper=true" - deploy: - mode: replicated - replicas: 4 - # healthcheck: - # test: ["CMD", "curl", "-f", "http://127.0.0.1:8000/health"] - # interval: 30s - # timeout: 10s - # retries: 3 - # start_period: 30s - # celery: - # <<: *common - # image: badbl0cks/pkmntrade-club:stable - # environment: - # - DEBUG=False - # - DISABLE_SIGNUPS=True - # - PUBLIC_HOST=pkmntrade.club - # - ALLOWED_HOSTS=pkmntrade.club,127.0.0.1 - # command: ["celery", "-A", "pkmntrade_club.django_project", "worker", "-l", "INFO", "-B", "-E"] \ No newline at end of file + - "deployment.color=${DEPLOYMENT_COLOR:-blue}" + - "deployment.image_tag=${IMAGE_TAG:-stable}" + command: ["celery", "-A", "pkmntrade_club.django_project", "worker", "-l", "INFO", "-B", "-E"] + stop_grace_period: 200s # match our longest stop_grace_period (currently web service is 200s) + +networks: + default: + name: pkmntrade-club_network + external: true \ No newline at end of file diff --git a/server/gatekeepers.template.yml b/server/gatekeepers.template.yml index 701fbf1..f4d3b8f 100644 --- a/server/gatekeepers.template.yml +++ b/server/gatekeepers.template.yml @@ -1,23 +1,52 @@ services: {{ $all_containers := whereLabelValueMatches . "enable_gatekeeper" "true" }} - {{ $all_containers = sortObjectsByKeysAsc $all_containers "Name" }} + + # During deployment, both blue and green containers might exist + # So we generate gatekeepers for ALL containers with deployment.color label + {{ $color_containers := whereLabelExists $all_containers "deployment.color" }} + {{ $color_containers = sortObjectsByKeysAsc $color_containers "Name" }} - {{ range $container := $all_containers }} + {{ range $container := $color_containers }} {{ $serviceLabel := index $container.Labels "com.docker.compose.service" }} {{ $containerNumber := index $container.Labels "com.docker.compose.container-number" }} + {{ $deploymentColor := index $container.Labels "deployment.color" }} {{ $port := "" }} {{ if eq $serviceLabel "web" }} {{ $port = ":8000" }} {{ end }} - {{ if eq $serviceLabel "web-staging" }} - {{ $port = ":8000" }} + gatekeeper-{{ $serviceLabel }}-{{ $deploymentColor }}-{{ $containerNumber }}: + image: ghcr.io/techarohq/anubis:latest + container_name: pkmntrade-club-gatekeeper-{{ $serviceLabel }}-{{ $deploymentColor }}-{{ $containerNumber }} + env_file: + - .env + environment: + - TARGET=http://{{ $container.Name }}{{ $port }} + - DEPLOYMENT_COLOR={{ $deploymentColor }} + - TARGET_HOST=${DOMAIN_NAME} + labels: + - gatekeeper=true + - deployment.color={{ $deploymentColor }} + networks: + default: + aliases: + - pkmntrade-club-gatekeeper-{{ $serviceLabel }} + - gatekeeper-{{ $serviceLabel }} {{ end }} + + # Always include non-color-specific services + {{ $static_containers := whereLabelValueMatches . "enable_gatekeeper" "true" }} + {{ $static_containers = whereLabelDoesNotExist $static_containers "deployment.color" }} + {{ range $container := $static_containers }} + {{ $serviceLabel := index $container.Labels "com.docker.compose.service" }} + {{ $containerNumber := index $container.Labels "com.docker.compose.container-number" }} + {{ $port := "" }} {{ if eq $serviceLabel "feedback" }} {{ $port = ":3000" }} {{ end }} {{ if eq $serviceLabel "health" }} {{ $port = ":8080" }} {{ end }} + {{ if or (eq $serviceLabel "feedback") (eq $serviceLabel "health") }} gatekeeper-{{ $serviceLabel }}-{{ $containerNumber }}: image: ghcr.io/techarohq/anubis:latest container_name: pkmntrade-club-gatekeeper-{{ $serviceLabel }}-{{ $containerNumber }} @@ -25,12 +54,6 @@ services: - .env environment: - TARGET=http://{{ $container.Name }}{{ $port }} - {{ if eq $serviceLabel "web" }} - - TARGET_HOST=pkmntrade.club # pass this host to django, which checks it with ALLOWED_HOSTS - {{ end }} - {{ if eq $serviceLabel "web-staging" }} - - TARGET_HOST=staging.pkmntrade.club # pass this host to django, which checks it with ALLOWED_HOSTS - {{ end }} labels: - gatekeeper=true networks: @@ -39,7 +62,9 @@ services: - pkmntrade-club-gatekeeper-{{ $serviceLabel }} - gatekeeper-{{ $serviceLabel }} {{ end }} + {{ end }} + networks: default: - name: pkmntrade-club_default + name: pkmntrade-club_network external: true diff --git a/server/gatus/config.template.yaml b/server/gatus/config.template.yaml index 33351f4..8f01ea1 100644 --- a/server/gatus/config.template.yaml +++ b/server/gatus/config.template.yaml @@ -92,20 +92,15 @@ endpoints: - type: email {{ $all_containers := . }} {{ $web_containers := list }} - {{ $web_staging_containers := list }} {{ range $container := $all_containers }} {{ $serviceLabel := index $container.Labels "com.docker.compose.service" }} {{ if eq $serviceLabel "web" }} {{ $web_containers = append $web_containers $container }} {{ end }} - {{ if eq $serviceLabel "web-staging" }} - {{ $web_staging_containers = append $web_staging_containers $container }} - {{ end }} {{ end }} {{ $web_containers = sortObjectsByKeysAsc $web_containers "Name" }} - {{ $web_staging_containers = sortObjectsByKeysAsc $web_staging_containers "Name" }} {{ range $container := $web_containers }} {{ $containerNumber := index $container.Labels "com.docker.compose.container-number" }} @@ -113,7 +108,7 @@ endpoints: group: Main url: "http://{{ $container.Name }}:8000/health/" headers: - Host: "pkmntrade.club" + Host: "${DOMAIN_NAME}" interval: 60s conditions: - "[STATUS] == 200" @@ -122,21 +117,6 @@ endpoints: - type: email {{ end }} - {{ range $container := $web_staging_containers }} - {{ $containerNumber := index $container.Labels "com.docker.compose.container-number" }} - - name: "Web Worker {{ $containerNumber }}" - group: Staging - url: "http://{{ $container.Name }}:8000/health/" - headers: - Host: "staging.pkmntrade.club" - interval: 60s - conditions: - - "[STATUS] == 200" - # - "[BODY] == OK/HEALTHY" - alerts: - - type: email - {{ end }} - alerting: email: from: "${GATUS_SMTP_FROM}" diff --git a/server/haproxy.cfg b/server/haproxy.cfg index 14db5f6..673c22a 100644 --- a/server/haproxy.cfg +++ b/server/haproxy.cfg @@ -25,32 +25,27 @@ frontend haproxy_entrypoint bind :443 ssl crt /certs/crt.pem verify required ca-file /certs/ca.pem use_backend %[req.hdr(host),lower,word(1,:)] # strip out port from host -frontend checks +frontend healthchecks bind :80 - default_backend basic_check + default_backend basic_loba_check -backend basic_check +backend basic_loba_check http-request return status 200 content-type "text/plain" lf-string "OK/HEALTHY" -backend pkmntrade.club +backend "${DOMAIN_NAME}" balance leastconn - http-request set-header Host pkmntrade.club - server-template gatekeeper-web- 4 gatekeeper-web:8000 check resolvers docker_resolver init-addr libc,none + http-request set-header Host "${DOMAIN_NAME}" + server-template gatekeeper-web- "${REPLICA_COUNT}" gatekeeper-web:8000 check resolvers docker_resolver init-addr none -backend staging.pkmntrade.club +backend "feedback.${BASE_DOMAIN_NAME}" balance leastconn - http-request set-header Host staging.pkmntrade.club - server-template gatekeeper-web-staging- 4 gatekeeper-web-staging:8000 check resolvers docker_resolver init-addr libc,none + http-request set-header Host feedback."${BASE_DOMAIN_NAME}" + server-template gatekeeper-feedback- 1 gatekeeper-feedback:8000 check resolvers docker_resolver init-addr none -backend feedback.pkmntrade.club +backend "health.${BASE_DOMAIN_NAME}" balance leastconn - http-request set-header Host feedback.pkmntrade.club - server-template gatekeeper-feedback- 4 gatekeeper-feedback:8000 check resolvers docker_resolver init-addr libc,none - -backend health.pkmntrade.club - balance leastconn - http-request set-header Host health.pkmntrade.club - server-template gatekeeper-health- 4 gatekeeper-health:8000 check resolvers docker_resolver init-addr libc,none + http-request set-header Host health."${BASE_DOMAIN_NAME}" + server-template gatekeeper-health- 1 gatekeeper-health:8000 check resolvers docker_resolver init-addr none #EOF - trailing newline required diff --git a/server/scripts/common-lib.sh b/server/scripts/common-lib.sh new file mode 100755 index 0000000..70fa5a4 --- /dev/null +++ b/server/scripts/common-lib.sh @@ -0,0 +1,382 @@ +#!/bin/bash +# Common library for deployment scripts +# Source this file in other scripts: source "${SCRIPT_DIR}/common-lib.sh" + +# Common constants +readonly BLUE_COLOR="blue" +readonly GREEN_COLOR="green" +readonly CORE_PROJECT_NAME="pkmntrade-club" +readonly DEPLOYMENT_LABEL="deployment.color" +readonly RETRY_MAX_ATTEMPTS="${RETRY_MAX_ATTEMPTS:-5}" +readonly RETRY_DELAY="${RETRY_DELAY:-5}" + +# Dry run helper function +# Usage: execute_if_not_dry "description" command [args...] +execute_if_not_dry() { + local description="$1" + shift + + if [ "$DRY_RUN" = true ]; then + indent_output echo "[DRY RUN] Would execute: $description" + indent_output echo " Command: $*" + else + "$@" + fi +} + +# Execute with error handling +# Usage: execute_or_fail "description" command [args...] +execute_or_fail() { + local description="$1" + shift + + if [ "$DRY_RUN" = true ]; then + indent_output echo "[DRY RUN] Would execute: $description" + indent_output echo " Command: $*" + else + if ! "$@"; then + echo "โŒ Error: Failed to $description" + exit 1 + fi + fi +} + +# Execute with warning on failure (non-critical operations) +# Usage: execute_or_warn "description" command [args...] +execute_or_warn() { + local description="$1" + shift + + if [ "$DRY_RUN" = true ]; then + indent_output echo "[DRY RUN] Would execute: $description" + indent_output echo " Command: $*" + else + if ! "$@"; then + echo "โš ๏ธ Warning: Failed to $description (continuing anyway)" + fi + fi +} + +# Retry a command with exponential backoff +retry() { + local max_attempts=$RETRY_MAX_ATTEMPTS + local delay=$RETRY_DELAY + local attempt=1 + local exit_code=0 + + until "$@"; do + exit_code=$? + + if [ "$attempt" -ge "$max_attempts" ]; then + echo "โŒ Command failed after $max_attempts attempts: $*" >&2 + return $exit_code + fi + + echo "โš ๏ธ Attempt $attempt failed, retrying in ${delay}s..." >&2 + sleep "$delay" + + # Exponential backoff + delay=$((delay * 2)) + attempt=$((attempt + 1)) + done + + if [ $attempt -gt 1 ]; then + echo "โœ… Command succeeded after $attempt attempts" + fi + + return 0 +} + +run_on_target() { + # if DEPLOY_HOST is set, we are not on remote + if [[ -n "${DEPLOY_HOST}" ]]; then + ssh deploy "$*" + else + bash -c -- "$*" + fi +} + +# Function to check if a variable is set +require_var() { + local var_name=$1 + local var_value=${!var_name} + + if [ -z "$var_value" ]; then + echo "Error: ${var_name} not set" >&2 + exit 1 + fi +} + +# Function to get deployment color based on running containers +get_current_color() { + local blue_count=$(docker ps --filter "label=${DEPLOYMENT_LABEL}=${BLUE_COLOR}" -q 2>/dev/null | wc -l) + local green_count=$(docker ps --filter "label=${DEPLOYMENT_LABEL}=${GREEN_COLOR}" -q 2>/dev/null | wc -l) + + if [ "$blue_count" -gt 0 ] && [ "$green_count" -eq 0 ]; then + echo "$BLUE_COLOR" + elif [ "$green_count" -gt 0 ] && [ "$blue_count" -eq 0 ]; then + echo "$GREEN_COLOR" + elif [ "$blue_count" -gt 0 ] && [ "$green_count" -gt 0 ]; then + # Both colors running - return the newer one + local blue_newest=$(docker inspect --format='{{.Created}}' "$(docker ps -q --filter "label=${DEPLOYMENT_LABEL}=${BLUE_COLOR}" | head -1)" 2>/dev/null || echo '1970-01-01') + local green_newest=$(docker inspect --format='{{.Created}}' "$(docker ps -q --filter "label=${DEPLOYMENT_LABEL}=${GREEN_COLOR}" | head -1)" 2>/dev/null || echo '1970-01-01') + + if [[ "$blue_newest" > "$green_newest" ]]; then + echo "$BLUE_COLOR" + else + echo "$GREEN_COLOR" + fi + else + echo "none" + fi +} + +# Function to get deployment state (none, blue, green, both) +get_deployment_state() { + local blue_count=$(docker ps --filter "label=${DEPLOYMENT_LABEL}=${BLUE_COLOR}" -q 2>/dev/null | wc -l) + local green_count=$(docker ps --filter "label=${DEPLOYMENT_LABEL}=${GREEN_COLOR}" -q 2>/dev/null | wc -l) + + if [ "$blue_count" -gt 0 ] && [ "$green_count" -gt 0 ]; then + echo "both" + elif [ "$blue_count" -gt 0 ]; then + echo "$BLUE_COLOR" + elif [ "$green_count" -gt 0 ]; then + echo "$GREEN_COLOR" + else + echo "none" + fi +} + +# Function to check if deployment is in progress +is_deployment_in_progress() { + local blue_count=$(docker ps --filter "label=${DEPLOYMENT_LABEL}=${BLUE_COLOR}" -q 2>/dev/null | wc -l) + local green_count=$(docker ps --filter "label=${DEPLOYMENT_LABEL}=${GREEN_COLOR}" -q 2>/dev/null | wc -l) + + if [ "$blue_count" -gt 0 ] && [ "$green_count" -gt 0 ]; then + return 0 # true - deployment in progress + else + return 1 # false - no deployment in progress + fi +} + +# Function to switch color +switch_color() { + local current=$1 + if [ "$current" = "$BLUE_COLOR" ]; then + echo "$GREEN_COLOR" + else + echo "$BLUE_COLOR" + fi +} + +# Function to get project name for a color +get_project_name() { + local color=$1 + echo "${CORE_PROJECT_NAME}-${color}" +} + +# Function to get compose files based on PROD setting +get_compose_files() { + # Always use the same docker-compose file for both staging and production + echo "-f docker-compose_web.yml" +} + +# Function to refresh gatekeepers +refresh_gatekeepers() { + echo "๐Ÿ”„ Refreshing gatekeepers..." + docker kill -s SIGHUP ${CORE_PROJECT_NAME}-gatekeeper-manager-1 2>/dev/null || true +} + +# Function to count containers by filter +count_containers() { + local filters=$1 + docker ps ${filters} -q 2>/dev/null | wc -l | tr -d '\n' || echo 0 +} + +get_previous_release_path() { + local current_link_path=$1 + local previous_release_path=$(run_on_target "cat '${current_link_path}/.previous_version'") + echo "${previous_release_path}" +} + +# Function to stop and remove the previous release's containers for a color +cleanup_color_containers() { + local color=$1 + local project_name=$(get_project_name "$color") + # Use CLEANUP_RELEASE_PATH if set, otherwise default to the previous release. + # This is crucial for rollbacks to use the correct compose file for cleanup. + local release_path=${CLEANUP_RELEASE_PATH:-$(get_previous_release_path "${CURRENT_LINK_PATH}")} + + echo "๐Ÿ›‘ Stopping $color containers from release: ${release_path}" + run_on_target "cd '${release_path}' && docker compose -p '${project_name}' stop --timeout 30 2>/dev/null || true" + + echo "๐Ÿ—‘๏ธ Removing $color containers from release: ${release_path}" + run_on_target "cd '${release_path}' && docker compose -p '${project_name}' down --remove-orphans 2>/dev/null || true" +} + +# Function to wait with countdown +wait_with_countdown() { + local seconds=$1 + local message=$2 + + echo -n "$message" + for ((i=seconds; i>0; i--)); do + echo -n " $i" + sleep 1 + done + echo " done!" +} + +get_web_service_name() { + echo "web" # hardcoded for now +} +# Standard environment validation +validate_deployment_env() { + require_var "REPO_PROJECT_PATH" + require_var "PROD" + require_var "REPLICA_COUNT" + if [ "$PROD" = "true" ]; then + require_var "PRODUCTION_DOMAIN" + else + require_var "STAGING_DOMAIN" + fi + + # Set derived variables + export CURRENT_LINK_PATH="${REPO_PROJECT_PATH}/current" + export RELEASES_PATH="${REPO_PROJECT_PATH}/releases" + export REPLICA_COUNT="${REPLICA_COUNT}" +} + +get_health_check_status() { + # TODO: instead get the health check status from gatus container's api + local statuses=$(docker ps --format '{{.Names}} {{.Status}}') + local unhealthy_count=0 + local IFS=$'\n' + for status in $statuses; do + local name=$(echo $status | cut -d' ' -f1) + local status=$(echo $status | cut -d' ' -f2-) + if [[ "$status" == *"unhealthy"* ]]; then + unhealthy_count=$((unhealthy_count + 1)) + echo "โŒ Unhealthy: $name [$status]" + else + echo "โœ… Healthy: $name [$status]" + fi + done + return $unhealthy_count +} + +# Function to wait for containers to be healthy +wait_for_healthy_containers() { + local project_name=$1 + local service_name=$2 + local expected_count=$3 + local max_attempts=60 # 5 minutes with 5-second intervals + local attempt=0 + + echo "โณ Waiting for $service_name containers to be healthy..." + + while [ $attempt -lt $max_attempts ]; do + healthy_count=$(count_containers "--filter label=com.docker.compose.project=${project_name} --filter label=com.docker.compose.service=${service_name} --filter health=healthy") + + if [[ "$healthy_count" -eq "$expected_count" ]]; then + echo "โœ… All $service_name containers are healthy ($healthy_count/$expected_count)" + return 0 + fi + + echo "โณ Healthy containers: $healthy_count/$expected_count (attempt $((attempt+1))/$max_attempts)" + sleep 5 + attempt=$((attempt + 1)) + done + + echo "โŒ Timeout waiting for $service_name containers to be healthy" + return 1 +} + +list_releases() { + local REPO_PROJECT_PATH=$1 + local RELEASES_PATH="${REPO_PROJECT_PATH}/releases" + local CURRENT_LINK_PATH="${REPO_PROJECT_PATH}/current" + + echo "๐Ÿ“‹ Available releases:" + if [ -d "$RELEASES_PATH" ]; then + for release in $(ls -dt ${RELEASES_PATH}/*); do + version=$(basename "$release") + status="" + + # Check if it's current + if [ -L "$CURRENT_LINK_PATH" ] && [ "$(readlink -f "$CURRENT_LINK_PATH")" = "$(realpath "$release")" ]; then + status=" [CURRENT]" + fi + + # Check if it failed + if [ -f "${release}/.failed" ]; then + status="${status} [FAILED]" + fi + + indent_output echo "- ${version}${status}" + done + else + indent_output echo "No releases found" + fi +} + +# Function to get image tag from deployment +get_deployment_image_tag() { + local color=$1 + local container=$(docker ps --filter "label=com.docker.compose.project=${CORE_PROJECT_NAME}-${color}" --format '{{.Names}}'| head -1) + if [ -n "$container" ]; then + docker inspect "${container}" --format '{{index .Config.Labels "deployment.image_tag"}}' + else + echo "unknown" + fi +} + +# Function to run a command and prefix its output +# Usage: prefix_output "PREFIX" command [args...] +# Example: prefix_output " | " docker ps +# Example: prefix_output " => " docker compose ps +prefix_output() { + local prefix=" " + + if [ $# -lt 2 ]; then + echo "Error: prefix_output requires at least 2 arguments" >&2 + return 1 + fi + + prefix="$1" + shift + + # Run the command and prefix each line of output + "$@" 2>&1 | sed "s/^/${prefix}/" + + # Return the exit code of the original command (not sed) + return ${PIPESTATUS[0]} +} + +# Function to run a command and indent its output +# Usage: indent_output [INDENT_STRING] command [args...] +# Example: indent_output docker ps # Uses default 2 spaces +# Example: indent_output " " docker ps # Uses 4 spaces +indent_output() { + local indent=" " # Default to 2 spaces + + # Check if first argument looks like an indent string (starts with spaces or tabs) + if [[ "$1" =~ ^[[:space:]]+$ ]]; then + indent="$1" + shift + fi + + # Use prefix_output with the indent string + prefix_output "$indent" "$@" +} + +# Function to run command with header and indented output +# Usage: run_with_header "HEADER" command [args...] +# Example: run_with_header "Docker Containers:" docker ps +run_with_header() { + local header="$1" + shift + + echo "$header" + indent_output " " "$@" +} \ No newline at end of file diff --git a/server/scripts/manage.sh b/server/scripts/manage.sh new file mode 100755 index 0000000..d0c563b --- /dev/null +++ b/server/scripts/manage.sh @@ -0,0 +1,377 @@ +#!/bin/bash +set -euo pipefail + +# Manage deployment releases +# Usage: ./manage.sh [--dry-run] COMMAND [ARGS] + +# Source common functions +SCRIPT_DIR="$(cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")" && pwd)" +source "${SCRIPT_DIR}/common-lib.sh" + +# Global variables +DRY_RUN=false +COMMAND="" +ARGS=() + +# Parse global options +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) + DRY_RUN=true + shift + ;; + -*) + echo "Error: Unknown option: $1" + echo "Usage: $0 [--dry-run] COMMAND [ARGS]" + exit 1 + ;; + *) + # First non-option argument is the command + if [ -z "$COMMAND" ]; then + COMMAND="$1" + else + # Rest are command arguments + ARGS+=("$1") + fi + shift + ;; + esac +done + +if [ -z "$COMMAND" ]; then + echo "Error: No command specified" + echo "Usage: $0 [--dry-run] COMMAND [ARGS]" + echo "Commands:" + indent_output echo "status - Show deployment status" + indent_output echo "list - List all releases" + indent_output echo "version - Show current release" + indent_output echo "switch VERSION - Switch to a specific release version" + indent_output echo "cleanup [KEEP] - Clean up old releases (default: keep 5)" + echo "" + echo "Global options:" + indent_output echo "--dry-run - Show what would happen without making changes" + exit 1 +fi + +REPO_PROJECT_PATH="$(realpath "${SCRIPT_DIR}/../../../")" +CURRENT_LINK_PATH="${REPO_PROJECT_PATH}/current" +RELEASES_PATH="${REPO_PROJECT_PATH}/releases" + +# Announce dry-run mode if active +if [ "$DRY_RUN" = true ]; then + echo "๐Ÿ” DRY RUN MODE - No changes will be made" + echo "" +fi + +case "$COMMAND" in + status) + echo "๐Ÿ” Deployment Status" + echo "====================" + + # Check if deployment is downgraded + if [ -d "$RELEASES_PATH" ] && [ -L "$CURRENT_LINK_PATH" ]; then + CURRENT_RELEASE_DIR_NAME=$(basename "$(readlink -f "$CURRENT_LINK_PATH")") + + # Find the latest release by modification time. + LATEST_RELEASE_DIR_NAME=$(find "$RELEASES_PATH" -maxdepth 1 -mindepth 1 -type d -printf '%T@ %f\n' | sort -nr | head -n 1 | cut -d' ' -f2-) + + if [ -n "$LATEST_RELEASE_DIR_NAME" ]; then + if [ "$CURRENT_RELEASE_DIR_NAME" == "$LATEST_RELEASE_DIR_NAME" ]; then + echo "โœ… Deployment is on the latest release (${LATEST_RELEASE_DIR_NAME})." + else + echo "โš ๏ธ Deployment is downgraded." + indent_output echo "Current: ${CURRENT_RELEASE_DIR_NAME}" + indent_output echo "Latest: ${LATEST_RELEASE_DIR_NAME}" + fi + else + # This case happens if RELEASES_PATH is empty + echo "โ„น๏ธ No releases found in ${RELEASES_PATH}." + fi + elif [ ! -L "$CURRENT_LINK_PATH" ]; then + echo "โ„น๏ธ No current deployment symlink found." + else # RELEASES_PATH does not exist + echo "โ„น๏ธ Releases directory not found at ${RELEASES_PATH}." + fi + echo "" # Add a newline for spacing + + # Get current state + CURRENT_STATE=$(get_deployment_state) + + if [ "$CURRENT_STATE" = "both" ]; then + echo "๐ŸŸก Deployment State: both" + elif [ "$CURRENT_STATE" = "blue" ]; then + echo "๐Ÿ”ต Deployment State: blue" + elif [ "$CURRENT_STATE" = "green" ]; then + echo "๐ŸŸข Deployment State: green" + else + indent_output echo "Deployment State: none" + fi + + echo "โš™๏ธ Core Containers:" + indent_output docker ps --filter 'label=deployment.core=true' --format 'table {{.Names}}\t{{.Status}}\t{{.CreatedAt}}' + + # Show containers by color with image info + echo "๐Ÿ”ต Blue Containers:" + BLUE_COUNT=$(count_containers "--filter label=deployment.color=blue") + # make sure BLUE_COUNT is a number + BLUE_COUNT=$(echo "$BLUE_COUNT" | tr -d '\n') + if [ "$BLUE_COUNT" -gt 0 ]; then + BLUE_IMAGE=$(get_deployment_image_tag "blue") + indent_output echo "Image: ${BLUE_IMAGE}" + indent_output docker ps --filter 'label=deployment.color=blue' --format 'table {{.Names}}\t{{.Status}}\t{{.CreatedAt}}' + else + indent_output echo "No blue containers running" + fi + + echo "๐ŸŸข Green Containers:" + GREEN_COUNT=$(count_containers "--filter label=deployment.color=green") + if [ "$GREEN_COUNT" -gt 0 ]; then + GREEN_IMAGE=$(get_deployment_image_tag "green") + indent_output echo "Image: ${GREEN_IMAGE}" + indent_output docker ps --filter 'label=deployment.color=green' --format 'table {{.Names}}\t{{.Status}}\t{{.CreatedAt}}' + else + indent_output echo "No green containers running" + fi + + list_releases "${REPO_PROJECT_PATH}" + + # Health check summary + echo "โค๏ธ Health Check Summary:" + case "$CURRENT_STATE" in + "both") + indent_output echo "โš ๏ธ WARNING: Both blue and green containers are running!" + indent_output echo "This might indicate an incomplete deployment." + ;; + "none") + indent_output echo "โš ๏ธ WARNING: No web containers are running!" + ;; + *) + if [ "$CURRENT_STATE" = "blue" ]; then + indent_output echo "๐Ÿ”ต System is running on blue deployment" + else + indent_output echo "๐ŸŸข System is running on green deployment" + fi + indent_output echo "โค๏ธ Overall Healthcheck:" + indent_output " " get_health_check_status + ;; + esac + + # Show resource usage + echo "๐Ÿ“Š Resource Usage:" + indent_output docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemPerc}}\t{{.MemUsage}}\t{{.NetIO}}' + + # Show deployment images + echo "๐Ÿ“ฆ Deployment Images:" + indent_output docker images 'badbl0cks/pkmntrade-club' --format 'table {{.Tag}}\t{{.ID}}\t{{.CreatedAt}}\t{{.Size}}' | grep -E '^TAG|sha-.{7} ' || indent_output echo "No deployment images found" + ;; + + list) + list_releases "${REPO_PROJECT_PATH}" + ;; + + version) + if [ -L "$CURRENT_LINK_PATH" ]; then + current_version=$(basename "$(readlink -f "$CURRENT_LINK_PATH")") + echo "๐Ÿ“Œ Current version: ${current_version}" + else + echo "โŒ No current deployment found" + fi + ;; + + switch) + if [ ${#ARGS[@]} -lt 1 ]; then + echo "Error: VERSION required for switch" + echo "Usage: $0 [--dry-run] switch VERSION" + exit 1 + fi + + TARGET_VERSION="${ARGS[0]}" + TARGET_PATH="${RELEASES_PATH}/${TARGET_VERSION}" + + # Validate target version exists + if [ ! -d "$TARGET_PATH" ]; then + echo "โŒ Error: Version ${TARGET_VERSION} not found" + echo "Available releases:" + list_releases "${REPO_PROJECT_PATH}" + exit 1 + fi + + # Get current version if exists + CURRENT_VERSION="none" + CURRENT_VERSION_PATH="" + if [ -L "$CURRENT_LINK_PATH" ]; then + CURRENT_VERSION_PATH=$(readlink -f "$CURRENT_LINK_PATH") + CURRENT_VERSION=$(basename "$CURRENT_VERSION_PATH") + fi + + # Edge case: trying to switch to the same version + if [ "$CURRENT_VERSION" == "$TARGET_VERSION" ]; then + echo "โœ… Already on version ${TARGET_VERSION}. No action taken." + exit 0 + fi + + CURRENT_COLOR=$(get_current_color) + NEW_COLOR=$(switch_color "$CURRENT_COLOR") + + echo "๐Ÿ”„ Switch Plan:" + indent_output echo "Current version: ${CURRENT_VERSION}" + if [ "$CURRENT_VERSION" != "none" ]; then + indent_output echo "Current path: ${CURRENT_VERSION_PATH}" + indent_output echo "Current color: ${CURRENT_COLOR}" + fi + indent_output echo "Target version: ${TARGET_VERSION}" + indent_output echo "Target path: ${TARGET_PATH}" + indent_output echo "Target color: ${NEW_COLOR}" + + # Verify target release has necessary files + echo "๐Ÿ“‹ Checking target release integrity..." + MISSING_FILES=() + for file in "docker-compose_web.yml" "docker-compose_core.yml" ".env"; do + if [ ! -f "${TARGET_PATH}/${file}" ]; then + MISSING_FILES+=("$file") + fi + done + + if [ ${#MISSING_FILES[@]} -gt 0 ]; then + echo "โŒ Error: Target release is missing required files:" + printf " - %s\n" "${MISSING_FILES[@]}" + exit 1 + fi + + # Get compose files based on environment + COMPOSE_FILES=$(get_compose_files) + + echo "๐Ÿ›‘ Stopping current containers..." + if [ -d "$CURRENT_VERSION_PATH" ]; then + ( + cd "$CURRENT_VERSION_PATH" || exit 1 + WEB_PROJECT_NAME=$(get_project_name "$CURRENT_COLOR") + + indent_output echo "Stopping web containers for project: ${WEB_PROJECT_NAME}..." + execute_or_warn "stop web containers" docker compose ${COMPOSE_FILES} -p "${WEB_PROJECT_NAME}" down + + indent_output echo "Stopping core services for project: ${CORE_PROJECT_NAME}..." + execute_or_warn "stop core services" docker compose -f "docker-compose_core.yml" -p "${CORE_PROJECT_NAME}" down + ) + else + indent_output echo "No current deployment to stop" + fi + + echo "๐Ÿ“ Updating deployment metadata for ${TARGET_VERSION}..." + execute_or_fail "update .deployment_color to ${NEW_COLOR}" \ + bash -c "echo '${NEW_COLOR}' > '${TARGET_PATH}/.deployment_color'" + + execute_or_fail "update DEPLOYMENT_COLOR in .env" \ + bash -c "sed -i 's/^DEPLOYMENT_COLOR=.*/DEPLOYMENT_COLOR=${NEW_COLOR}/' '${TARGET_PATH}/.env'" + + # Update symlink + echo "๐Ÿ”— Updating deployment symlink..." + execute_or_fail "update symlink from $CURRENT_LINK_PATH to $TARGET_PATH" \ + ln -sfn "$TARGET_PATH" "$CURRENT_LINK_PATH" + + # Start containers + echo "๐Ÿš€ Starting containers from ${TARGET_VERSION}..." + ( + cd "$TARGET_PATH" || exit 1 + + TARGET_WEB_PROJECT_NAME=$(get_project_name "$NEW_COLOR") + + indent_output echo "Starting core services for project: ${CORE_PROJECT_NAME}..." + execute_or_fail "start core services" \ + docker compose -f "docker-compose_core.yml" -p "${CORE_PROJECT_NAME}" up -d + + indent_output echo "Starting web containers for project: ${TARGET_WEB_PROJECT_NAME}..." + execute_or_fail "start web containers" \ + docker compose ${COMPOSE_FILES} -p "${TARGET_WEB_PROJECT_NAME}" up -d + ) + + if [ "$DRY_RUN" = true ]; then + echo "" + echo "โœ… Dry run completed - no changes made" + else + echo "โœ… Switch completed to version: ${TARGET_VERSION}" + echo "Run '$0 status' to verify deployment health" + fi + ;; + + cleanup) + # Parse cleanup arguments + KEEP_COUNT=5 + for arg in "${ARGS[@]}"; do + if [[ "$arg" =~ ^[0-9]+$ ]]; then + KEEP_COUNT="$arg" + else + echo "Error: Invalid argument for cleanup: $arg" + echo "Usage: $0 [--dry-run] cleanup [KEEP_COUNT]" + exit 1 + fi + done + + echo "๐Ÿ—‘๏ธ Cleaning up old releases (keeping last ${KEEP_COUNT} and current)" + + if [ ! -L "$CURRENT_LINK_PATH" ]; then + echo "โŒ No current deployment symlink found. Aborting cleanup." + exit 1 + fi + + CURRENT_RELEASE_DIR_NAME=$(basename "$(readlink -f "$CURRENT_LINK_PATH")") + echo "๐Ÿ“Œ Current release: ${CURRENT_RELEASE_DIR_NAME}" + + if [ -d "$RELEASES_PATH" ]; then + cd "$RELEASES_PATH" + + # Get a list of inactive release directories, sorted by modification time (newest first). + INACTIVE_RELEASES=$(find . -maxdepth 1 -mindepth 1 -type d \ + -not -name "$CURRENT_RELEASE_DIR_NAME" \ + -printf '%T@ %f\n' | sort -nr | cut -d' ' -f2-) + + if [ -z "$INACTIVE_RELEASES" ]; then + echo "No inactive releases found to clean up." + exit 0 + fi + + # Count total inactive releases + TOTAL_INACTIVE=$(echo "$INACTIVE_RELEASES" | wc -l | xargs) + echo "๐Ÿ“Š Found ${TOTAL_INACTIVE} inactive release(s)" + + # Identify releases to delete by skipping the KEEP_COUNT newest ones. + RELEASES_TO_DELETE=$(echo "$INACTIVE_RELEASES" | tail -n +$((KEEP_COUNT + 1))) + + if [ -n "$RELEASES_TO_DELETE" ]; then + DELETE_COUNT=$(echo "$RELEASES_TO_DELETE" | wc -l | xargs) + echo "๐Ÿ—‘๏ธ The following ${DELETE_COUNT} old release(s) will be deleted:" + + # Show releases with their sizes + while IFS= read -r release; do + if [ -d "$release" ]; then + SIZE=$(du -sh "$release" 2>/dev/null | cut -f1) + indent_output echo "- $release (Size: $SIZE)" + fi + done <<< "$RELEASES_TO_DELETE" + + # Delete the releases + echo "" + while IFS= read -r release; do + execute_if_not_dry "delete release $release" rm -rf "$release" + done <<< "$RELEASES_TO_DELETE" + + if [ "$DRY_RUN" = true ]; then + echo "" + echo "โœ… Dry run completed - no releases were deleted" + else + echo "โœ… Cleanup completed - deleted ${DELETE_COUNT} release(s)" + fi + else + KEPT_COUNT=$(echo "$INACTIVE_RELEASES" | wc -l | tr -d '\n') + echo "No old releases to delete. Found ${KEPT_COUNT} inactive release(s), which is within the retention count of ${KEEP_COUNT}." + fi + else + echo "No releases directory found" + fi + ;; + + *) + echo "Error: Unknown command: $COMMAND" + exit 1 + ;; +esac \ No newline at end of file diff --git a/uv.lock b/uv.lock index fd3ec93..449d8dd 100644 --- a/uv.lock +++ b/uv.lock @@ -34,7 +34,7 @@ wheels = [ [[package]] name = "celery" -version = "5.5.2" +version = "5.5.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "billiard" }, @@ -46,9 +46,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "vine" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/03/5d9c6c449248958f1a5870e633a29d7419ff3724c452a98ffd22688a1a6a/celery-5.5.2.tar.gz", hash = "sha256:4d6930f354f9d29295425d7a37261245c74a32807c45d764bedc286afd0e724e", size = 1666892 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/7d/6c289f407d219ba36d8b384b42489ebdd0c84ce9c413875a8aae0c85f35b/celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5", size = 1667144 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/94/8e825ac1cf59d45d20c4345d4461e6b5263ae475f708d047c3dad0ac6401/celery-5.5.2-py3-none-any.whl", hash = "sha256:54425a067afdc88b57cd8d94ed4af2ffaf13ab8c7680041ac2c4ac44357bdf4c", size = 438626 }, + { url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775 }, ] [[package]] @@ -315,14 +315,14 @@ wheels = [ [[package]] name = "django-health-check" -version = "3.18.3" +version = "3.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/e9/0699ea3debfda75e5960ff99f56974136380e6f8202d453de7357e1f67fc/django_health_check-3.18.3.tar.gz", hash = "sha256:18b75daca4551c69a43f804f9e41e23f5f5fb9efd06cf6a313b3d5031bb87bd0", size = 20919 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/96/60db7257c05418b60ceb9d2c0a568e923394582111e809f1bb3749a7ee60/django_health_check-3.19.0.tar.gz", hash = "sha256:1a995ed4fa08a776beedff65f8f1ec0c22fb6764493f33fb1307fe4c6f23b8c3", size = 20088 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/1e/3b23b580762cca7456427731de9b90718d15eec02ebe096437469d767dfe/django_health_check-3.18.3-py2.py3-none-any.whl", hash = "sha256:f5f58762b80bdf7b12fad724761993d6e83540f97e2c95c42978f187e452fa07", size = 30331 }, + { url = "https://files.pythonhosted.org/packages/8f/35/c08be7e0012a7927c5f01185c0df39e0fa249cfc17234cce798c2afaf6bb/django_health_check-3.19.0-py3-none-any.whl", hash = "sha256:30b58d761f40fef47971b8dc145df15bdb71339108034860bbf1d505387aa1ec", size = 31969 }, ] [[package]] @@ -468,36 +468,35 @@ wheels = [ [[package]] name = "greenlet" -version = "3.2.2" +version = "3.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/c1/a82edae11d46c0d83481aacaa1e578fea21d94a1ef400afd734d47ad95ad/greenlet-3.2.2.tar.gz", hash = "sha256:ad053d34421a2debba45aa3cc39acf454acbcd025b3fc1a9f8a0dee237abd485", size = 185797 } +sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/a1/88fdc6ce0df6ad361a30ed78d24c86ea32acb2b563f33e39e927b1da9ea0/greenlet-3.2.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:df4d1509efd4977e6a844ac96d8be0b9e5aa5d5c77aa27ca9f4d3f92d3fcf330", size = 270413 }, - { url = "https://files.pythonhosted.org/packages/a6/2e/6c1caffd65490c68cd9bcec8cb7feb8ac7b27d38ba1fea121fdc1f2331dc/greenlet-3.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da956d534a6d1b9841f95ad0f18ace637668f680b1339ca4dcfb2c1837880a0b", size = 637242 }, - { url = "https://files.pythonhosted.org/packages/98/28/088af2cedf8823b6b7ab029a5626302af4ca1037cf8b998bed3a8d3cb9e2/greenlet-3.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c7b15fb9b88d9ee07e076f5a683027bc3befd5bb5d25954bb633c385d8b737e", size = 651444 }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0116ab876bb0bc7a81eadc21c3f02cd6100dcd25a1cf2a085a130a63a26a/greenlet-3.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:752f0e79785e11180ebd2e726c8a88109ded3e2301d40abced2543aa5d164275", size = 646067 }, - { url = "https://files.pythonhosted.org/packages/35/17/bb8f9c9580e28a94a9575da847c257953d5eb6e39ca888239183320c1c28/greenlet-3.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae572c996ae4b5e122331e12bbb971ea49c08cc7c232d1bd43150800a2d6c65", size = 648153 }, - { url = "https://files.pythonhosted.org/packages/2c/ee/7f31b6f7021b8df6f7203b53b9cc741b939a2591dcc6d899d8042fcf66f2/greenlet-3.2.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02f5972ff02c9cf615357c17ab713737cccfd0eaf69b951084a9fd43f39833d3", size = 603865 }, - { url = "https://files.pythonhosted.org/packages/b5/2d/759fa59323b521c6f223276a4fc3d3719475dc9ae4c44c2fe7fc750f8de0/greenlet-3.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4fefc7aa68b34b9224490dfda2e70ccf2131368493add64b4ef2d372955c207e", size = 1119575 }, - { url = "https://files.pythonhosted.org/packages/30/05/356813470060bce0e81c3df63ab8cd1967c1ff6f5189760c1a4734d405ba/greenlet-3.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a31ead8411a027c2c4759113cf2bd473690517494f3d6e4bf67064589afcd3c5", size = 1147460 }, - { url = "https://files.pythonhosted.org/packages/07/f4/b2a26a309a04fb844c7406a4501331b9400e1dd7dd64d3450472fd47d2e1/greenlet-3.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b24c7844c0a0afc3ccbeb0b807adeefb7eff2b5599229ecedddcfeb0ef333bec", size = 296239 }, - { url = "https://files.pythonhosted.org/packages/89/30/97b49779fff8601af20972a62cc4af0c497c1504dfbb3e93be218e093f21/greenlet-3.2.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3ab7194ee290302ca15449f601036007873028712e92ca15fc76597a0aeb4c59", size = 269150 }, - { url = "https://files.pythonhosted.org/packages/21/30/877245def4220f684bc2e01df1c2e782c164e84b32e07373992f14a2d107/greenlet-3.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc5c43bb65ec3669452af0ab10729e8fdc17f87a1f2ad7ec65d4aaaefabf6bf", size = 637381 }, - { url = "https://files.pythonhosted.org/packages/8e/16/adf937908e1f913856b5371c1d8bdaef5f58f251d714085abeea73ecc471/greenlet-3.2.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:decb0658ec19e5c1f519faa9a160c0fc85a41a7e6654b3ce1b44b939f8bf1325", size = 651427 }, - { url = "https://files.pythonhosted.org/packages/ad/49/6d79f58fa695b618654adac64e56aff2eeb13344dc28259af8f505662bb1/greenlet-3.2.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fadd183186db360b61cb34e81117a096bff91c072929cd1b529eb20dd46e6c5", size = 645795 }, - { url = "https://files.pythonhosted.org/packages/5a/e6/28ed5cb929c6b2f001e96b1d0698c622976cd8f1e41fe7ebc047fa7c6dd4/greenlet-3.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1919cbdc1c53ef739c94cf2985056bcc0838c1f217b57647cbf4578576c63825", size = 648398 }, - { url = "https://files.pythonhosted.org/packages/9d/70/b200194e25ae86bc57077f695b6cc47ee3118becf54130c5514456cf8dac/greenlet-3.2.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3885f85b61798f4192d544aac7b25a04ece5fe2704670b4ab73c2d2c14ab740d", size = 606795 }, - { url = "https://files.pythonhosted.org/packages/f8/c8/ba1def67513a941154ed8f9477ae6e5a03f645be6b507d3930f72ed508d3/greenlet-3.2.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:85f3e248507125bf4af607a26fd6cb8578776197bd4b66e35229cdf5acf1dfbf", size = 1117976 }, - { url = "https://files.pythonhosted.org/packages/c3/30/d0e88c1cfcc1b3331d63c2b54a0a3a4a950ef202fb8b92e772ca714a9221/greenlet-3.2.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1e76106b6fc55fa3d6fe1c527f95ee65e324a13b62e243f77b48317346559708", size = 1145509 }, - { url = "https://files.pythonhosted.org/packages/90/2e/59d6491834b6e289051b252cf4776d16da51c7c6ca6a87ff97e3a50aa0cd/greenlet-3.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:fe46d4f8e94e637634d54477b0cfabcf93c53f29eedcbdeecaf2af32029b4421", size = 296023 }, - { url = "https://files.pythonhosted.org/packages/65/66/8a73aace5a5335a1cba56d0da71b7bd93e450f17d372c5b7c5fa547557e9/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba30e88607fb6990544d84caf3c706c4b48f629e18853fc6a646f82db9629418", size = 629911 }, - { url = "https://files.pythonhosted.org/packages/48/08/c8b8ebac4e0c95dcc68ec99198842e7db53eda4ab3fb0a4e785690883991/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:055916fafad3e3388d27dd68517478933a97edc2fc54ae79d3bec827de2c64c4", size = 635251 }, - { url = "https://files.pythonhosted.org/packages/37/26/7db30868f73e86b9125264d2959acabea132b444b88185ba5c462cb8e571/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2593283bf81ca37d27d110956b79e8723f9aa50c4bcdc29d3c0543d4743d2763", size = 632620 }, - { url = "https://files.pythonhosted.org/packages/10/ec/718a3bd56249e729016b0b69bee4adea0dfccf6ca43d147ef3b21edbca16/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89c69e9a10670eb7a66b8cef6354c24671ba241f46152dd3eed447f79c29fb5b", size = 628851 }, - { url = "https://files.pythonhosted.org/packages/9b/9d/d1c79286a76bc62ccdc1387291464af16a4204ea717f24e77b0acd623b99/greenlet-3.2.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02a98600899ca1ca5d3a2590974c9e3ec259503b2d6ba6527605fcd74e08e207", size = 593718 }, - { url = "https://files.pythonhosted.org/packages/cd/41/96ba2bf948f67b245784cd294b84e3d17933597dffd3acdb367a210d1949/greenlet-3.2.2-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b50a8c5c162469c3209e5ec92ee4f95c8231b11db6a04db09bbe338176723bb8", size = 1105752 }, - { url = "https://files.pythonhosted.org/packages/68/3b/3b97f9d33c1f2eb081759da62bd6162159db260f602f048bc2f36b4c453e/greenlet-3.2.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:45f9f4853fb4cc46783085261c9ec4706628f3b57de3e68bae03e8f8b3c0de51", size = 1125170 }, - { url = "https://files.pythonhosted.org/packages/31/df/b7d17d66c8d0f578d2885a3d8f565e9e4725eacc9d3fdc946d0031c055c4/greenlet-3.2.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:9ea5231428af34226c05f927e16fc7f6fa5e39e3ad3cd24ffa48ba53a47f4240", size = 269899 }, + { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992 }, + { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820 }, + { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046 }, + { url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701 }, + { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747 }, + { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461 }, + { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190 }, + { url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055 }, + { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817 }, + { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732 }, + { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033 }, + { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999 }, + { url = "https://files.pythonhosted.org/packages/51/b4/ebb2c8cb41e521f1d72bf0465f2f9a2fd803f674a88db228887e6847077e/greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", size = 647368 }, + { url = "https://files.pythonhosted.org/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037 }, + { url = "https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402 }, + { url = "https://files.pythonhosted.org/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577 }, + { url = "https://files.pythonhosted.org/packages/86/94/1fc0cc068cfde885170e01de40a619b00eaa8f2916bf3541744730ffb4c3/greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", size = 1147121 }, + { url = "https://files.pythonhosted.org/packages/27/1a/199f9587e8cb08a0658f9c30f3799244307614148ffe8b1e3aa22f324dea/greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", size = 297603 }, + { url = "https://files.pythonhosted.org/packages/d8/ca/accd7aa5280eb92b70ed9e8f7fd79dc50a2c21d8c73b9a0856f5b564e222/greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86", size = 271479 }, + { url = "https://files.pythonhosted.org/packages/55/71/01ed9895d9eb49223280ecc98a557585edfa56b3d0e965b9fa9f7f06b6d9/greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97", size = 683952 }, + { url = "https://files.pythonhosted.org/packages/ea/61/638c4bdf460c3c678a0a1ef4c200f347dff80719597e53b5edb2fb27ab54/greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728", size = 696917 }, + { url = "https://files.pythonhosted.org/packages/22/cc/0bd1a7eb759d1f3e3cc2d1bc0f0b487ad3cc9f34d74da4b80f226fde4ec3/greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a", size = 692443 }, + { url = "https://files.pythonhosted.org/packages/67/10/b2a4b63d3f08362662e89c103f7fe28894a51ae0bc890fabf37d1d780e52/greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", size = 692995 }, + { url = "https://files.pythonhosted.org/packages/5a/c6/ad82f148a4e3ce9564056453a71529732baf5448ad53fc323e37efe34f66/greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", size = 655320 }, + { url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236 }, ] [[package]] @@ -523,16 +522,17 @@ wheels = [ [[package]] name = "kombu" -version = "5.5.3" +version = "5.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "amqp" }, + { name = "packaging" }, { name = "tzdata" }, { name = "vine" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/0a/128b65651ed8120460fc5af754241ad595eac74993115ec0de4f2d7bc459/kombu-5.5.3.tar.gz", hash = "sha256:021a0e11fcfcd9b0260ef1fb64088c0e92beb976eb59c1dfca7ddd4ad4562ea2", size = 461784 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d3/5ff936d8319ac86b9c409f1501b07c426e6ad41966fedace9ef1b966e23f/kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363", size = 461992 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/35/1407fb0b2f5b07b50cbaf97fce09ad87d3bfefbf64f7171a8651cd8d2f68/kombu-5.5.3-py3-none-any.whl", hash = "sha256:5b0dbceb4edee50aa464f59469d34b97864be09111338cfb224a10b6a163909b", size = 209921 }, + { url = "https://files.pythonhosted.org/packages/ef/70/a07dcf4f62598c8ad579df241af55ced65bed76e42e45d3c368a6d82dbc1/kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8", size = 210034 }, ] [[package]] @@ -848,11 +848,11 @@ wheels = [ [[package]] name = "redis" -version = "6.1.0" +version = "6.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/af/e875d57383653e5d9065df8552de1deb7576b4d3cf3af90cde2e79ff7f65/redis-6.1.0.tar.gz", hash = "sha256:c928e267ad69d3069af28a9823a07726edf72c7e37764f43dc0123f37928c075", size = 4629300 } +sdist = { url = "https://files.pythonhosted.org/packages/ea/9a/0551e01ba52b944f97480721656578c8a7c46b51b99d66814f85fe3a4f3e/redis-6.2.0.tar.gz", hash = "sha256:e821f129b75dde6cb99dd35e5c76e8c49512a5a0d8dfdc560b2fbd44b85ca977", size = 4639129 } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/5f/cf36360f80ae233bd1836442f5127818cfcfc7b1846179b60b2e9a4c45c9/redis-6.1.0-py3-none-any.whl", hash = "sha256:3b72622f3d3a89df2a6041e82acd896b0e67d9f54e9bcd906d091d23ba5219f6", size = 273750 }, + { url = "https://files.pythonhosted.org/packages/13/67/e60968d3b0e077495a8fee89cf3f2373db98e528288a48f1ee44967f6e8c/redis-6.2.0-py3-none-any.whl", hash = "sha256:c8ddf316ee0aab65f04a11229e94a64b2618451dab7a67cb2f77eb799d872d5e", size = 278659 }, ] [[package]] @@ -885,11 +885,11 @@ wheels = [ [[package]] name = "setuptools" -version = "80.8.0" +version = "80.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8d/d2/ec1acaaff45caed5c2dedb33b67055ba9d4e96b091094df90762e60135fe/setuptools-80.8.0.tar.gz", hash = "sha256:49f7af965996f26d43c8ae34539c8d99c5042fbff34302ea151eaa9c207cd257", size = 1319720 } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/29/93c53c098d301132196c3238c312825324740851d77a8500a2462c0fd888/setuptools-80.8.0-py3-none-any.whl", hash = "sha256:95a60484590d24103af13b686121328cc2736bee85de8936383111e421b9edc0", size = 1201470 }, + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 }, ] [[package]]