|
60 | 60 | build_and_publish: |
61 | 61 | name: Build, Docker and Push |
62 | 62 | runs-on: ubuntu-latest |
63 | | - needs: tests # Only runs if tests pass |
64 | | - if: ${{ needs.tests.result == 'success' }} |
| 63 | + needs: tests |
| 64 | + if: false |
65 | 65 | env: |
66 | 66 | # Base image name (adjust as needed) |
67 | 67 | APP_NAME: espacogeek-api |
@@ -592,22 +592,224 @@ jobs: |
592 | 592 | username: ${{ github.actor }} |
593 | 593 | password: ${{ secrets.GITHUB_TOKEN }} |
594 | 594 |
|
595 | | - # Tag and push the validated native image to GHCR. |
596 | | - # Only runs after a successful health check and only on pushes to master. |
597 | 595 | - name: Tag and push native Docker image |
598 | 596 | if: steps.vars.outputs.should_push == 'true' && github.event_name == 'push' |
599 | 597 | shell: bash |
600 | 598 | run: | |
601 | 599 | set -euo pipefail |
602 | | - IFS=',' read -ra TAGS <<< "${{ steps.vars.outputs.tags_csv }}" |
603 | | - for tag in "${TAGS[@]}"; do |
604 | | - FULL_TAG="ghcr.io/${{ env.GHCR_OWNER_LC }}/${{ env.APP_NAME }}-native:${tag}" |
605 | | - docker tag native-validation:ci "${FULL_TAG}" |
606 | | - docker push "${FULL_TAG}" |
607 | | - echo "Pushed: ${FULL_TAG}" |
608 | | - done |
| 600 | + FULL_TAG="ghcr.io/${{ env.GHCR_OWNER_LC }}/${{ env.APP_NAME }}:native" |
| 601 | + docker tag native-validation:ci "${FULL_TAG}" |
| 602 | + docker push "${FULL_TAG}" |
| 603 | + echo "Pushed: ${FULL_TAG}" |
| 604 | +
|
| 605 | + - name: Deploy to Hostinger via SSH |
| 606 | + uses: appleboy/ssh-action@v0.1.9 |
| 607 | + if: ${{ needs.tests.result == 'success' && steps.vars.outputs.should_push == 'true' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }} |
| 608 | + with: |
| 609 | + host: ${{ secrets.HOSTINGER_HOST }} |
| 610 | + username: ${{ secrets.HOSTINGER_USER }} |
| 611 | + key: ${{ secrets.HOSTINGER }} |
| 612 | + port: ${{ secrets.HOSTINGER_PORT }} |
| 613 | + envs: GHCR_OWNER_LC,APP_NAME |
| 614 | + script_stop: false |
| 615 | + script: | |
| 616 | + cleanup_on_exit() { |
| 617 | + echo "Running SSH-level cleanup..." |
| 618 | + rm -f .env.espacogeek 2>/dev/null || true |
| 619 | + docker logout ghcr.io 2>/dev/null || true |
| 620 | + } |
| 621 | + trap cleanup_on_exit EXIT INT TERM |
| 622 | +
|
| 623 | + DEPLOY_SCRIPT="/opt/espacogeek/deploy.sh" |
| 624 | +
|
| 625 | + mkdir -p "$(dirname "$DEPLOY_SCRIPT")" |
| 626 | +
|
| 627 | + cat > "$DEPLOY_SCRIPT" << 'DEPLOY_SCRIPT_EOF' |
| 628 | + #!/bin/bash |
| 629 | + set -euo pipefail |
| 630 | +
|
| 631 | + RED='\033[0;31m' |
| 632 | + GREEN='\033[0;32m' |
| 633 | + YELLOW='\033[1;33m' |
| 634 | + BLUE='\033[0;34m' |
| 635 | + NC='\033[0m' |
| 636 | +
|
| 637 | + log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } |
| 638 | + log_success() { echo -e "${GREEN}[✓]${NC} $*"; } |
| 639 | + log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } |
| 640 | + log_error() { echo -e "${RED}[✗]${NC} $*"; } |
| 641 | +
|
| 642 | + GHCR_OWNER_LC="${1:-}" |
| 643 | + APP_NAME="${2:-}" |
| 644 | + IMAGE_TAG="${3:-native}" |
| 645 | + ENV_FILE="${4:-.env.espacogeek}" |
| 646 | + CONTAINER_NAME="espacogeek-api" |
| 647 | + BACKUP_DIR="${HOME}/espacogeek-api-backups" |
| 648 | + OLD_CONTAINER_BACKUP="${CONTAINER_NAME}-old" |
| 649 | + TIMESTAMP=$(date +%Y%m%d_%H%M%S) |
| 650 | +
|
| 651 | + cleanup_env_file() { |
| 652 | + if [ -f "$ENV_FILE" ]; then |
| 653 | + log_info "Cleaning up environment file..." |
| 654 | + rm -f "$ENV_FILE" |
| 655 | + fi |
| 656 | + } |
| 657 | + trap cleanup_env_file EXIT |
| 658 | +
|
| 659 | + if [ -z "$GHCR_OWNER_LC" ] || [ -z "$APP_NAME" ]; then |
| 660 | + log_error "Usage: $0 <GHCR_OWNER_LC> <APP_NAME> [IMAGE_TAG] [ENV_FILE]" |
| 661 | + exit 1 |
| 662 | + fi |
| 663 | +
|
| 664 | + IMAGE="ghcr.io/${GHCR_OWNER_LC}/${APP_NAME}:${IMAGE_TAG}" |
| 665 | +
|
| 666 | + container_exists() { docker ps -a --format '{{.Names}}' | grep -q "^${1}$"; } |
| 667 | +
|
| 668 | + if container_exists "$CONTAINER_NAME"; then |
| 669 | + log_info "Creating backup of old container..." |
| 670 | + mkdir -p "$BACKUP_DIR" |
| 671 | + BACKUP_FILE="${BACKUP_DIR}/${CONTAINER_NAME}_backup_${TIMESTAMP}.tar" |
| 672 | + if docker export "$CONTAINER_NAME" > "$BACKUP_FILE"; then |
| 673 | + log_success "Container backup created" |
| 674 | + ls -t "${BACKUP_DIR}/${CONTAINER_NAME}_backup_"*.tar 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true |
| 675 | + else |
| 676 | + log_error "Failed to backup container" |
| 677 | + exit 1 |
| 678 | + fi |
| 679 | + fi |
| 680 | +
|
| 681 | + if container_exists "$CONTAINER_NAME"; then |
| 682 | + log_info "Stopping old container..." |
| 683 | + docker stop "$CONTAINER_NAME" 2>/dev/null || true |
| 684 | + docker rename "$CONTAINER_NAME" "${OLD_CONTAINER_BACKUP}" 2>/dev/null || true |
| 685 | + log_success "Old container renamed to ${OLD_CONTAINER_BACKUP}" |
| 686 | + fi |
| 687 | +
|
| 688 | + log_info "Pulling new image..." |
| 689 | + if ! docker pull "$IMAGE"; then |
| 690 | + log_error "Failed to pull image" |
| 691 | + if container_exists "${OLD_CONTAINER_BACKUP}"; then |
| 692 | + log_warn "Restoring old container..." |
| 693 | + docker rename "${OLD_CONTAINER_BACKUP}" "$CONTAINER_NAME" 2>/dev/null || true |
| 694 | + docker start "$CONTAINER_NAME" 2>/dev/null || true |
| 695 | + fi |
| 696 | + exit 1 |
| 697 | + fi |
| 698 | + log_success "Image pulled successfully" |
| 699 | +
|
| 700 | + log_info "Starting new container..." |
| 701 | + if ! docker run -d --name "$CONTAINER_NAME" --network infra_network -p 8080:8080 --restart unless-stopped --env-file "$ENV_FILE" "$IMAGE"; then |
| 702 | + log_error "Failed to start container" |
| 703 | + if container_exists "${OLD_CONTAINER_BACKUP}"; then |
| 704 | + log_warn "Restoring old container..." |
| 705 | + docker rename "${OLD_CONTAINER_BACKUP}" "$CONTAINER_NAME" 2>/dev/null || true |
| 706 | + docker start "$CONTAINER_NAME" 2>/dev/null || true |
| 707 | + fi |
| 708 | + exit 1 |
| 709 | + fi |
| 710 | + log_success "Container started successfully" |
| 711 | +
|
| 712 | + log_info "Waiting for application to initialize (up to 30s)..." |
| 713 | + MAX_RETRIES=6 |
| 714 | + SLEEP_SECONDS=5 |
| 715 | + retries=0 |
| 716 | + HEALTHY=0 |
| 717 | + while [ "$retries" -lt "$MAX_RETRIES" ]; do |
| 718 | + if ! container_exists "$CONTAINER_NAME"; then |
| 719 | + log_error "Health check: container '$CONTAINER_NAME' is not running" |
| 720 | + break |
| 721 | + fi |
| 722 | +
|
| 723 | + STATUS="$(docker inspect -f '{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo "unknown")" |
| 724 | + RESTARTING="$(docker inspect -f '{{.State.Restarting}}' "$CONTAINER_NAME" 2>/dev/null || echo "false")" |
| 725 | +
|
| 726 | + if [ "$STATUS" = "running" ] && [ "$RESTARTING" = "false" ]; then |
| 727 | + log_success "Health check passed: container is running (status=$STATUS, restarting=$RESTARTING)" |
| 728 | + HEALTHY=1 |
| 729 | + break |
| 730 | + fi |
| 731 | +
|
| 732 | + log_info "Health check: status=$STATUS, restarting=$RESTARTING. Retrying in ${SLEEP_SECONDS}s..." |
| 733 | + sleep "$SLEEP_SECONDS" |
| 734 | + retries=$((retries + 1)) |
| 735 | + done |
| 736 | +
|
| 737 | + if [ "$HEALTHY" -ne 1 ]; then |
| 738 | + log_error "Container failed health check after $((MAX_RETRIES * SLEEP_SECONDS))s" |
| 739 | + if container_exists "$CONTAINER_NAME"; then |
| 740 | + log_warn "Stopping and removing failed new container..." |
| 741 | + docker rm -f "$CONTAINER_NAME" 2>/dev/null || true |
| 742 | + fi |
| 743 | + if container_exists "${OLD_CONTAINER_BACKUP}"; then |
| 744 | + log_warn "Restoring old container..." |
| 745 | + docker rename "${OLD_CONTAINER_BACKUP}" "$CONTAINER_NAME" 2>/dev/null || true |
| 746 | + docker start "$CONTAINER_NAME" 2>/dev/null || true |
| 747 | + fi |
| 748 | + exit 1 |
| 749 | + fi |
| 750 | + if container_exists "${OLD_CONTAINER_BACKUP}"; then |
| 751 | + log_info "Removing old container backup..." |
| 752 | + docker rm -f "${OLD_CONTAINER_BACKUP}" 2>/dev/null || true |
| 753 | + log_success "Old container removed" |
| 754 | + fi |
| 755 | +
|
| 756 | + log_success "" |
| 757 | + log_success "=== DEPLOYMENT SUCCESSFUL ===" |
| 758 | + docker ps -a --filter name="$CONTAINER_NAME" |
| 759 | + log_info "" |
| 760 | + log_info "Recent logs:" |
| 761 | + docker logs --tail 10 "$CONTAINER_NAME" || true |
| 762 | + log_success "=============================" |
| 763 | + DEPLOY_SCRIPT_EOF |
| 764 | +
|
| 765 | + chmod +x "$DEPLOY_SCRIPT" |
| 766 | +
|
| 767 | + cat > .env.espacogeek << ENVEOF |
| 768 | + SPRING_DATASOURCE_URL=${{ secrets.SPRING_DATASOURCE_URL }} |
| 769 | + SPRING_DATASOURCE_USERNAME=${{ secrets.SPRING_DATASOURCE_USERNAME }} |
| 770 | + SPRING_DATASOURCE_PASSWORD=${{ secrets.SPRING_DATASOURCE_PASSWORD }} |
| 771 | + SPRING_MVC_CORS_ALLOWED_ORIGINS=${{ secrets.SPRING_MVC_CORS_ALLOWED_ORIGINS }} |
| 772 | + SECURITY_JWT_ISSUER=${{ secrets.SECURITY_JWT_ISSUER }} |
| 773 | + SECURITY_JWT_EXPIRATION_MS=${{ secrets.SECURITY_JWT_EXPIRATION_MS }} |
| 774 | + SECURITY_JWT_SECRET=${{ secrets.SECURITY_JWT_SECRET }} |
| 775 | + SAMESITE_WHEN_SAME_SITE=${{ secrets.SAMESITE_WHEN_SAME_SITE }} |
| 776 | + SECURITY_CSRF_COOKIE_DOMAIN=${{ secrets.SECURITY_CSRF_COOKIE_DOMAIN }} |
| 777 | + SECURITY_CSRF_COOKIE_SAME_SITE=${{ secrets.SECURITY_CSRF_COOKIE_SAME_SITE }} |
| 778 | + MAIL_HOST=${{ secrets.MAIL_HOST }} |
| 779 | + MAIL_PORT=${{ secrets.MAIL_PORT }} |
| 780 | + MAIL_USERNAME=${{ secrets.MAIL_USERNAME }} |
| 781 | + MAIL_PASSWORD=${{ secrets.MAIL_PASSWORD }} |
| 782 | + FRONTEND_URL=${{ secrets.FRONTEND_URL }} |
| 783 | + ENVEOF |
| 784 | +
|
| 785 | + echo "Validating environment file..." |
| 786 | + if ! grep -q "^SPRING_DATASOURCE_URL=" .env.espacogeek; then |
| 787 | + echo "ERROR: SPRING_DATASOURCE_URL not found in .env.espacogeek" |
| 788 | + cat .env.espacogeek |
| 789 | + rm -f .env.espacogeek |
| 790 | + exit 1 |
| 791 | + fi |
| 792 | + DATASOURCE_URL=$(grep "^SPRING_DATASOURCE_URL=" .env.espacogeek | cut -d'=' -f2) |
| 793 | + if [ -z "$DATASOURCE_URL" ]; then |
| 794 | + echo "ERROR: SPRING_DATASOURCE_URL is empty" |
| 795 | + rm -f .env.espacogeek |
| 796 | + exit 1 |
| 797 | + fi |
| 798 | + if [[ ! "$DATASOURCE_URL" =~ ^jdbc: ]]; then |
| 799 | + echo "ERROR: SPRING_DATASOURCE_URL must start with 'jdbc:' but got: $DATASOURCE_URL" |
| 800 | + rm -f .env.espacogeek |
| 801 | + exit 1 |
| 802 | + fi |
| 803 | + echo "✓ SPRING_DATASOURCE_URL is valid: $DATASOURCE_URL" |
| 804 | +
|
| 805 | + echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u "${{ secrets.GHCR_USER }}" --password-stdin |
| 806 | +
|
| 807 | + "$DEPLOY_SCRIPT" "${GHCR_OWNER_LC}" "${APP_NAME}" "native" ".env.espacogeek" |
| 808 | +
|
| 809 | + rm -f .env.espacogeek |
| 810 | +
|
| 811 | + docker logout ghcr.io |
609 | 812 |
|
610 | | - # Always clean up the validation container |
611 | 813 | - name: Cleanup validation container |
612 | 814 | if: always() |
613 | 815 | run: docker rm -f native-validation 2>/dev/null || true |
|
0 commit comments