From a52b2d13f066d61d80612ec3b4bdad65d262f566 Mon Sep 17 00:00:00 2001 From: admin Date: Fri, 21 Nov 2025 15:03:31 +0300 Subject: [PATCH] Add Gitea workflows for CI/CD pipeline --- .gitea/workflows/build.yml | 122 +++++++++++++++++++++++ .gitea/workflows/deploy.yml | 188 ++++++++++++++++++++++++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 .gitea/workflows/build.yml create mode 100644 .gitea/workflows/deploy.yml diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..569b15d --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,122 @@ +name: Build Application Image + +on: + workflow_run: + workflows: ["Run Tests"] + types: + - completed + branches: + - main + - production + workflow_dispatch: + inputs: + image_tag: + description: 'Image tag (default: latest)' + required: false + default: 'latest' + +env: + IMAGE_NAME: jd-book-uploader + IMAGE_TAG: ${{ inputs.image_tag || 'latest' }} + REGISTRY: ${{ secrets.REGISTRY_URL || '' }} + +jobs: + build: + name: Build with Pack + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} + outputs: + image: ${{ steps.image.outputs.full }} + image-digest: ${{ steps.build.outputs.digest }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Configure Docker Socket + run: | + # Detect Docker socket location (handles rootless Docker) + if [ -S "/run/user/$(id -u)/docker.sock" ]; then + echo "PACK_DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock" >> $GITEA_ENV + elif [ -S "/var/run/docker.sock" ]; then + echo "PACK_DOCKER_HOST=unix:///var/run/docker.sock" >> $GITEA_ENV + else + echo "Error: Docker socket not found" + exit 1 + fi + docker info + + - name: Install Pack CLI + run: | + PACK_VERSION="0.32.0" + wget -q "https://github.com/buildpacks/pack/releases/download/v${PACK_VERSION}/pack-v${PACK_VERSION}-linux.tgz" + tar -xzf "pack-v${PACK_VERSION}-linux.tgz" + sudo mv pack /usr/local/bin/ + pack --version + + - name: Set default builder + run: | + pack config default-builder paketobuildpacks/builder-jammy-tiny:latest + + - name: Prepare build environment + working-directory: backend + run: | + # Create .env.production for build (no secrets, just structure) + cat > .env.production << EOF + PORT=8080 + # Database and Firebase config loaded at runtime + EOF + + - name: Build image + id: build + env: + PACK_DOCKER_HOST: ${{ env.PACK_DOCKER_HOST }} + run: | + PACK_ARGS=( + "${IMAGE_NAME}:${IMAGE_TAG}" + --path backend + ) + + if [ -n "$PACK_DOCKER_HOST" ]; then + PACK_ARGS+=(--docker-host "$PACK_DOCKER_HOST") + fi + + if [ -f "backend/.env.production" ]; then + PACK_ARGS+=(--env-file backend/.env.production) + fi + + pack build "${PACK_ARGS[@]}" + + IMAGE_DIGEST=$(docker inspect "${IMAGE_NAME}:${IMAGE_TAG}" --format='{{.Id}}') + echo "digest=${IMAGE_DIGEST}" >> $GITEA_OUTPUT + + - name: Tag image + id: image + run: | + if [ -n "${{ env.REGISTRY }}" ]; then + FULL_IMAGE="${{ env.REGISTRY }}/${IMAGE_NAME}:${IMAGE_TAG}" + docker tag "${IMAGE_NAME}:${IMAGE_TAG}" "${FULL_IMAGE}" + echo "full=${FULL_IMAGE}" >> $GITEA_OUTPUT + else + echo "full=${IMAGE_NAME}:${IMAGE_TAG}" >> $GITEA_OUTPUT + fi + + - name: Push to registry + if: env.REGISTRY != '' + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin + docker push "${{ steps.image.outputs.full }}" + + - name: Save image artifact + uses: actions/upload-artifact@v4 + with: + name: docker-image + path: /tmp/image.tar + retention-days: 1 + if: env.REGISTRY == '' + run: | + docker save "${IMAGE_NAME}:${IMAGE_TAG}" -o /tmp/image.tar + diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..82e8962 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,188 @@ +name: Deploy to Production + +on: + workflow_run: + workflows: ["Build Application Image"] + types: + - completed + branches: + - main + - production + workflow_dispatch: + inputs: + image_tag: + description: 'Image tag to deploy' + required: true + default: 'latest' + +env: + IMAGE_NAME: jd-book-uploader + IMAGE_TAG: ${{ inputs.image_tag || 'latest' }} + REGISTRY: ${{ secrets.REGISTRY_URL || '' }} + +jobs: + deploy: + name: Deploy to Production + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} + environment: production + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Configure SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts + chmod 600 ~/.ssh/known_hosts + + - name: Download image artifact + if: env.REGISTRY == '' + uses: actions/download-artifact@v4 + with: + name: docker-image + workflow: build.yml + run-id: ${{ github.event.workflow_run.id }} + + - name: Prepare deployment files + run: | + mkdir -p deployment/tmp + + # Create .env.production + cat > deployment/tmp/.env.production << EOF + PORT=${{ secrets.PORT || '8080' }} + FRONTEND_URL=${{ secrets.FRONTEND_URL }} + DB_HOST=${{ secrets.DB_HOST }} + DB_PORT=${{ secrets.DB_PORT }} + DB_USER=${{ secrets.DB_USER }} + DB_PASSWORD=${{ secrets.DB_PASSWORD }} + DB_NAME=${{ secrets.DB_NAME }} + FIREBASE_PROJECT_ID=${{ secrets.FIREBASE_PROJECT_ID }} + FIREBASE_STORAGE_BUCKET=${{ secrets.FIREBASE_STORAGE_BUCKET }} + FIREBASE_CREDENTIALS_FILE=${{ secrets.FIREBASE_CREDENTIALS_FILE_PATH || './firebase-credentials.json' }} + EOF + + # Create deployment script + cat > deployment/tmp/deploy.sh << 'DEPLOY_SCRIPT' + #!/bin/bash + set -e + + IMAGE_NAME="${{ env.IMAGE_NAME }}" + IMAGE_TAG="${{ env.IMAGE_TAG }}" + CONTAINER_NAME="jd-book-uploader" + + set -a + source .env.production + set +a + + # Stop existing container + if podman ps -a --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then + podman stop "${CONTAINER_NAME}" 2>/dev/null || true + podman rm "${CONTAINER_NAME}" 2>/dev/null || true + fi + + # Load image if artifact provided + if [ -f image.tar ]; then + podman load -i image.tar + rm -f image.tar + fi + + # Pull from registry if configured + if [ -n "${{ env.REGISTRY }}" ]; then + podman pull "${{ env.REGISTRY }}/${IMAGE_NAME}:${IMAGE_TAG}" + podman tag "${{ env.REGISTRY }}/${IMAGE_NAME}:${IMAGE_TAG}" "${IMAGE_NAME}:${IMAGE_TAG}" + fi + + # Build run command + PODMAN_CMD=( + podman run -d + --name "${CONTAINER_NAME}" + --network=host + --user root + --restart=unless-stopped + ) + + # Add environment variables + while IFS='=' read -r key value; do + [[ "$key" =~ ^#.*$ ]] && continue + [[ -z "$key" ]] && continue + value=$(echo "$value" | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//") + if [ "$key" != "FIREBASE_CREDENTIALS_FILE" ]; then + PODMAN_CMD+=(-e "${key}=${value}") + fi + done < .env.production + + # Mount Firebase credentials + FIREBASE_CREDS="${FIREBASE_CREDENTIALS_FILE}" + if [ -f "$FIREBASE_CREDS" ]; then + PODMAN_CMD+=(-v "${FIREBASE_CREDS}:/app/firebase-credentials.json:ro,z") + PODMAN_CMD+=(-e "FIREBASE_CREDENTIALS_FILE=/app/firebase-credentials.json") + fi + + PODMAN_CMD+=("${IMAGE_NAME}:${IMAGE_TAG}") + + "${PODMAN_CMD[@]}" + sleep 3 + + if podman ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then + echo "✓ Container started" + podman logs "${CONTAINER_NAME}" --tail 20 + else + echo "✗ Container failed" + podman logs "${CONTAINER_NAME}" --tail 50 + exit 1 + fi + DEPLOY_SCRIPT + + chmod +x deployment/tmp/deploy.sh + + - name: Transfer files + run: | + scp -r deployment/tmp/* ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/deployment/ + if [ -f image.tar ]; then + scp image.tar ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/image.tar + fi + + - name: Deploy + run: | + ssh ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << ENDSSH + set -e + cd ${{ secrets.DEPLOY_PATH }} + + if [ -f image.tar ]; then + podman load -i image.tar + rm -f image.tar + fi + + if [ ! -f "${{ secrets.FIREBASE_CREDENTIALS_FILE_PATH || './firebase-credentials.json' }}" ]; then + echo "Error: Firebase credentials not found" + exit 1 + fi + + cd deployment + ./deploy.sh + ENDSSH + + - name: Verify deployment + run: | + sleep 5 + HEALTH_URL="http://${{ secrets.DEPLOY_HOST }}:${{ secrets.PORT || '8080' }}/api/health" + + for i in {1..10}; do + if curl -f -s "$HEALTH_URL" > /dev/null; then + echo "✓ Health check passed" + curl -s "$HEALTH_URL" | jq . + exit 0 + fi + sleep 3 + done + + echo "✗ Health check failed" + exit 1 +