diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a79b0ea..5fd738a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,265 +1,152 @@ -# GitLab CI/CD Pipeline for LittleShop -# Builds and deploys to Hostinger VPS - stages: - build - - test - deploy variables: DOCKER_HOST: unix:///var/run/docker.sock - DOCKER_DRIVER: overlay2 - # Registry configuration - REGISTRY_URL: "localhost:5000" - IMAGE_NAME: "littleshop" - # Hostinger deployment configuration - DEPLOY_HOST: "10.13.13.1" - DEPLOY_PORT: "2255" - CONTAINER_NAME: "littleshop-admin" -# Build from .NET source and create Docker image build: stage: build - image: mcr.microsoft.com/dotnet/sdk:9.0 - services: - - docker:24-dind - before_script: - - apt-get update && apt-get install -y docker.io - - docker --version + image: docker:24 script: - - echo "Building LittleShop application..." - - cd LittleShop - - dotnet publish -c Production -o ../publish --verbosity minimal - - cd .. - - # Create optimized Dockerfile + - echo "Building LittleShop Docker image" + - docker build -t localhost:5000/littleshop:latest . - | - cat > Dockerfile << 'EOF' - FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine - WORKDIR /app - COPY ./publish . - - # Create required directories and user - RUN mkdir -p /app/data /app/wwwroot/uploads && \ - adduser -D -u 1658 appuser && \ - chown -R appuser:appuser /app - - USER appuser - EXPOSE 8080 - ENTRYPOINT ["dotnet", "LittleShop.dll"] - EOF - - # Build and tag Docker image - - export VERSION="${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}" - - docker build -t ${REGISTRY_URL}/${IMAGE_NAME}:${VERSION} . - - docker tag ${REGISTRY_URL}/${IMAGE_NAME}:${VERSION} ${REGISTRY_URL}/${IMAGE_NAME}:latest - - # Save image for deployment - - docker save ${REGISTRY_URL}/${IMAGE_NAME}:${VERSION} -o littleshop.tar - - echo "Build complete for version ${VERSION}" - artifacts: - paths: - - littleshop.tar - - publish/ - expire_in: 1 hour + if [ -n "$CI_COMMIT_TAG" ]; then + echo "Tagging as version $CI_COMMIT_TAG" + docker tag localhost:5000/littleshop:latest localhost:5000/littleshop:$CI_COMMIT_TAG + fi + - echo "Build complete" rules: - if: '$CI_COMMIT_BRANCH == "main"' - if: '$CI_COMMIT_TAG' tags: - docker -# Run tests -test: - stage: test - image: mcr.microsoft.com/dotnet/sdk:9.0 - script: - - echo "Running tests..." - - cd LittleShop.Tests - - dotnet test --no-restore --verbosity normal || true - allow_failure: true - rules: - - if: '$CI_COMMIT_BRANCH == "main"' - - if: '$CI_COMMIT_TAG' - -# Deploy to Hostinger VPS -deploy:hostinger: +deploy:vps: stage: deploy - image: alpine:latest - dependencies: - - build + image: docker:24 before_script: - - apk add --no-cache openssh-client sshpass curl - # Setup SSH key if provided - - | - if [ -n "$HOSTINGER_SSH_KEY" ]; then - echo "$HOSTINGER_SSH_KEY" | base64 -d > /tmp/hostinger_key - chmod 600 /tmp/hostinger_key - export SSH_CMD="ssh -i /tmp/hostinger_key" - export SCP_CMD="scp -i /tmp/hostinger_key" - else - export SSH_CMD="sshpass -p $DEPLOY_PASSWORD ssh" - export SCP_CMD="sshpass -p $DEPLOY_PASSWORD scp" - fi + - apk add --no-cache openssh-client bash curl + - echo "$VPS_SSH_KEY_B64" | base64 -d > /tmp/deploy_key + - chmod 600 /tmp/deploy_key + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + - ssh-keyscan -p "$VPS_PORT" "$VPS_HOST" >> ~/.ssh/known_hosts script: - export VERSION="${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA}" - - echo "Deploying version ${VERSION} to Hostinger..." + - echo "Deploying version $VERSION to VPS" + - echo "Building image from source..." + - docker build -t littleshop:$VERSION . - # Transfer Docker image to server - - $SCP_CMD -P ${DEPLOY_PORT} -o StrictHostKeyChecking=no littleshop.tar ${DEPLOY_USER}@${DEPLOY_HOST}:/tmp/ + - echo "Copying image to VPS via SSH..." + - docker save littleshop:$VERSION | ssh -i /tmp/deploy_key -p "$VPS_PORT" "$VPS_USER@$VPS_HOST" "docker load" - # Deploy on server + - echo "Deploying on VPS..." - | - $SSH_CMD -p ${DEPLOY_PORT} -o StrictHostKeyChecking=no ${DEPLOY_USER}@${DEPLOY_HOST} << DEPLOY_SCRIPT + ssh -i /tmp/deploy_key -p "$VPS_PORT" "$VPS_USER@$VPS_HOST" bash -s << EOF set -e + export VERSION="$VERSION" - # Load Docker image - echo "${DEPLOY_PASSWORD}" | sudo -S docker load -i /tmp/littleshop.tar + # Tag the image + docker tag littleshop:\$VERSION localhost:5000/littleshop:\$VERSION + docker tag littleshop:\$VERSION localhost:5000/littleshop:latest - # Stop and remove existing container - echo "${DEPLOY_PASSWORD}" | sudo -S docker stop ${CONTAINER_NAME} 2>/dev/null || true - echo "${DEPLOY_PASSWORD}" | sudo -S docker rm ${CONTAINER_NAME} 2>/dev/null || true + # Push to local registry + echo "Pushing to local Docker registry..." + docker push localhost:5000/littleshop:\$VERSION + docker push localhost:5000/littleshop:latest - # Run new container with authentication fix and all environment variables - echo "${DEPLOY_PASSWORD}" | sudo -S docker run -d \ - --name ${CONTAINER_NAME} \ - --restart unless-stopped \ - -p 5100:8080 \ - -v /var/opt/littleshop/data:/app/data \ - -v /var/opt/littleshop/uploads:/app/wwwroot/uploads \ - -e ASPNETCORE_ENVIRONMENT=Production \ - -e WebPush__VapidPublicKey='BDJtQu7zV0H3KF4FkrZ8nPwP3YD_3cEz3hqJvQ6L_gvNpG8ANksQB-FZy2-PDmFAu6duiN4p3mkcNAGnN4YRbws' \ - -e WebPush__VapidPrivateKey='Hm_ttUKUqoLn5R8WQP5O1SIGxm0kVJXMZGCPMD1tUDY' \ - -e WebPush__VapidSubject='mailto:admin@littleshop.local' \ - -e ConnectionStrings__DefaultConnection='Data Source=/app/data/littleshop-production.db' \ - -e Jwt__Key='2D7B5FE9C4A3E1D8B6A947F2C8E5D3A1B9F7E4C2D8A6B3E9F1C7D5A2E8B4F6C9' \ - -e Jwt__Audience='LittleShop-Production' \ - -e Jwt__ExpiryInHours='24' \ - -e Jwt__Issuer='LittleShop-Production' \ - -e SilverPay__AllowUnsignedWebhooks='false' \ - -e SilverPay__WebhookSecret='04126be1b2ca9a586aaf25670c0ddb7a9afa106158074605a1016a2889655c20' \ - --health-cmd='curl -f http://localhost:8080/health || exit 1' \ - --health-interval=30s \ - --health-timeout=10s \ - --health-retries=3 \ - ${REGISTRY_URL}/${IMAGE_NAME}:${VERSION} + # Navigate to deployment directory + cd /opt/littleshop - # Wait for container health - echo "Waiting for container to be healthy..." + # Force stop all littleshop containers (including orphans) + echo "Stopping all littleshop containers..." + docker stop \$(docker ps -q --filter "name=littleshop") 2>/dev/null || true + docker rm \$(docker ps -aq --filter "name=littleshop") 2>/dev/null || true + + # Stop services with compose (removes networks) + echo "Stopping compose services..." + docker-compose down --remove-orphans || true + + # Prune unused Docker networks to avoid conflicts + echo "Cleaning up Docker networks..." + docker network prune -f || true + + # Start services with new image + echo "Starting services with new image..." + docker-compose up -d + + # Wait for startup + echo "Waiting for services to start..." + sleep 30 + + # Health check + echo "Running health checks..." for i in 1 2 3 4 5 6; do - if echo "${DEPLOY_PASSWORD}" | sudo -S docker ps | grep -q "(healthy).*${CONTAINER_NAME}"; then - echo "✅ Container is healthy" - break + if curl -f -s http://localhost:5100/api/catalog/products > /dev/null 2>&1; then + echo "✅ Deployment successful - health check passed" + exit 0 fi - echo "Waiting for health check... attempt \$i/6" + echo "Health check attempt \$i/6 failed, waiting..." sleep 10 done - # Test authentication redirect - echo "Testing authentication redirect..." - curl -I http://localhost:5100/Admin 2>/dev/null | head -15 - - # Push to local registry for backup - echo "${DEPLOY_PASSWORD}" | sudo -S docker push ${REGISTRY_URL}/${IMAGE_NAME}:${VERSION} 2>/dev/null || true - echo "${DEPLOY_PASSWORD}" | sudo -S docker tag ${REGISTRY_URL}/${IMAGE_NAME}:${VERSION} ${REGISTRY_URL}/${IMAGE_NAME}:latest - echo "${DEPLOY_PASSWORD}" | sudo -S docker push ${REGISTRY_URL}/${IMAGE_NAME}:latest 2>/dev/null || true - - # Health check API - if curl -f -s http://localhost:5100/api/catalog/products > /dev/null 2>&1; then - echo "✅ Deployment successful - API health check passed" - else - echo "⚠️ API health check failed but container is running" - echo "${DEPLOY_PASSWORD}" | sudo -S docker logs ${CONTAINER_NAME} --tail 20 - fi - - # Cleanup - rm -f /tmp/littleshop.tar - - echo "Deployment of version ${VERSION} complete!" - DEPLOY_SCRIPT + echo "❌ Health check failed after deployment" + docker logs littleshop-admin --tail 50 + exit 1 + EOF environment: name: production - url: http://${DEPLOY_HOST}:5100 + url: http://hq.lan rules: - if: '$CI_COMMIT_BRANCH == "main"' - when: manual # Require manual approval for production + when: on_success - if: '$CI_COMMIT_TAG' when: manual tags: - docker -# Rollback job -rollback:hostinger: +rollback:vps: stage: deploy image: alpine:latest before_script: - - apk add --no-cache openssh-client sshpass + - apk add --no-cache openssh-client bash + - echo "$VPS_SSH_KEY_B64" | base64 -d > /tmp/deploy_key + - chmod 600 /tmp/deploy_key + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + - ssh-keyscan -p "$VPS_PORT" "$VPS_HOST" >> ~/.ssh/known_hosts script: - - echo "Rolling back to previous version..." + - echo "Rolling back to previous version" - | - if [ -n "$HOSTINGER_SSH_KEY" ]; then - echo "$HOSTINGER_SSH_KEY" | base64 -d > /tmp/hostinger_key - chmod 600 /tmp/hostinger_key - SSH_CMD="ssh -i /tmp/hostinger_key" - else - SSH_CMD="sshpass -p $DEPLOY_PASSWORD ssh" - fi - - | - $SSH_CMD -p ${DEPLOY_PORT} -o StrictHostKeyChecking=no ${DEPLOY_USER}@${DEPLOY_HOST} << ROLLBACK_SCRIPT + ssh -i /tmp/deploy_key -p "$VPS_PORT" "$VPS_USER@$VPS_HOST" bash -s << EOF set -e + cd /opt/littleshop - # Get previous image - PREVIOUS_IMAGE=\$(echo "${DEPLOY_PASSWORD}" | sudo -S docker images ${REGISTRY_URL}/${IMAGE_NAME} --format "{{.Tag}}" | grep -v latest | head -2 | tail -1) + # Pull previous image + docker tag localhost:5000/littleshop:previous localhost:5000/littleshop:latest - if [ -z "\$PREVIOUS_IMAGE" ]; then - echo "❌ No previous image found for rollback" - exit 1 - fi + # Restart services + echo "Restarting with previous version..." + docker-compose down + docker-compose up -d - echo "Rolling back to ${REGISTRY_URL}/${IMAGE_NAME}:\$PREVIOUS_IMAGE" - - # Stop current container - echo "${DEPLOY_PASSWORD}" | sudo -S docker stop ${CONTAINER_NAME} - echo "${DEPLOY_PASSWORD}" | sudo -S docker rm ${CONTAINER_NAME} - - # Start with previous image - echo "${DEPLOY_PASSWORD}" | sudo -S docker run -d \ - --name ${CONTAINER_NAME} \ - --restart unless-stopped \ - -p 5100:8080 \ - -v /var/opt/littleshop/data:/app/data \ - -v /var/opt/littleshop/uploads:/app/wwwroot/uploads \ - -e ASPNETCORE_ENVIRONMENT=Production \ - -e WebPush__VapidPublicKey='BDJtQu7zV0H3KF4FkrZ8nPwP3YD_3cEz3hqJvQ6L_gvNpG8ANksQB-FZy2-PDmFAu6duiN4p3mkcNAGnN4YRbws' \ - -e WebPush__VapidPrivateKey='Hm_ttUKUqoLn5R8WQP5O1SIGxm0kVJXMZGCPMD1tUDY' \ - -e WebPush__VapidSubject='mailto:admin@littleshop.local' \ - -e ConnectionStrings__DefaultConnection='Data Source=/app/data/littleshop-production.db' \ - -e Jwt__Key='2D7B5FE9C4A3E1D8B6A947F2C8E5D3A1B9F7E4C2D8A6B3E9F1C7D5A2E8B4F6C9' \ - -e Jwt__Audience='LittleShop-Production' \ - -e Jwt__ExpiryInHours='24' \ - -e Jwt__Issuer='LittleShop-Production' \ - -e SilverPay__AllowUnsignedWebhooks='false' \ - -e SilverPay__WebhookSecret='04126be1b2ca9a586aaf25670c0ddb7a9afa106158074605a1016a2889655c20' \ - --health-cmd='curl -f http://localhost:8080/health || exit 1' \ - --health-interval=30s \ - --health-timeout=10s \ - --health-retries=3 \ - ${REGISTRY_URL}/${IMAGE_NAME}:\$PREVIOUS_IMAGE - - # Wait and check health + # Health check sleep 30 if curl -f -s http://localhost:5100/api/catalog/products > /dev/null 2>&1; then - echo "✅ Rollback complete - service is healthy" + echo "✅ Rollback complete" + exit 0 else echo "❌ Rollback health check failed" - echo "${DEPLOY_PASSWORD}" | sudo -S docker logs ${CONTAINER_NAME} --tail 50 + docker logs littleshop-admin --tail 50 exit 1 fi - ROLLBACK_SCRIPT + EOF environment: name: production - when: manual rules: - - if: '$CI_COMMIT_BRANCH == "main"' - if: '$CI_COMMIT_TAG' + when: manual tags: - docker \ No newline at end of file