Fix Docker Hub tag format: remove registry from image tags, add valid… #14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI/CD Pipeline | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - develop | |
| pull_request: | |
| branches: | |
| - main | |
| - develop | |
| workflow_dispatch: | |
| env: | |
| REGISTRY: ${{ secrets.REGISTRY }} | |
| IMAGE_PREFIX: ${{ secrets.IMAGE_PREFIX }} | |
| KUBERNETES_NAMESPACE_STAGING: micro-demo-staging | |
| KUBERNETES_NAMESPACE_PRODUCTION: micro-demo | |
| NODE_VERSION: '18' | |
| jobs: | |
| lint-and-test: | |
| name: Lint and Test | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| service: [product-service, order-service, notification-service] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Get npm cache directory | |
| id: npm-cache-dir-path | |
| shell: bash | |
| run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT} | |
| - name: Get package-lock hash | |
| id: package-lock-hash | |
| working-directory: ./${{ matrix.service }} | |
| run: | | |
| if [ -f package-lock.json ]; then | |
| echo "hash=$(sha256sum package-lock.json | cut -d' ' -f1)" >> $GITHUB_OUTPUT | |
| else | |
| echo "hash=no-lockfile" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Cache npm dependencies | |
| uses: actions/cache@v3 | |
| id: npm-cache | |
| with: | |
| path: ${{ steps.npm-cache-dir-path.outputs.dir }} | |
| key: ${{ runner.os }}-node-${{ matrix.service }}-${{ steps.package-lock-hash.outputs.hash }} | |
| restore-keys: | | |
| ${{ runner.os }}-node-${{ matrix.service }}- | |
| ${{ runner.os }}-node- | |
| - name: Install dependencies | |
| working-directory: ./${{ matrix.service }} | |
| run: | | |
| if [ -f package-lock.json ]; then | |
| npm ci --prefer-offline --no-audit || npm install --no-audit | |
| else | |
| npm install --no-audit | |
| fi | |
| - name: Run ESLint | |
| working-directory: ./${{ matrix.service }} | |
| run: npm run lint | |
| continue-on-error: false | |
| - name: Run tests with coverage | |
| working-directory: ./${{ matrix.service }} | |
| run: npm test -- --coverage | |
| env: | |
| CI: true | |
| - name: Upload coverage reports | |
| uses: codecov/codecov-action@v3 | |
| if: always() | |
| with: | |
| file: ./${{ matrix.service }}/coverage/coverage-final.json | |
| flags: ${{ matrix.service }} | |
| name: ${{ matrix.service }}-coverage | |
| fail_ci_if_error: false | |
| - name: Check for security vulnerabilities | |
| working-directory: ./${{ matrix.service }} | |
| run: npm audit --audit-level=moderate || true | |
| build-and-push: | |
| name: Build and Push Docker Images | |
| runs-on: ubuntu-latest | |
| needs: lint-and-test | |
| if: github.event_name == 'push' | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| service: [product-service, order-service, notification-service] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| with: | |
| driver-opts: | | |
| image=moby/buildkit:latest | |
| network=host | |
| - name: Log in to Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ secrets.REGISTRY }} | |
| username: ${{ secrets.REGISTRY_USERNAME }} | |
| password: ${{ secrets.REGISTRY_PASSWORD }} | |
| continue-on-error: false | |
| - name: Set image tags | |
| id: image-tags | |
| run: | | |
| REGISTRY="${{ secrets.REGISTRY }}" | |
| PREFIX="${{ secrets.IMAGE_PREFIX }}" | |
| SERVICE="${{ matrix.service }}" | |
| USERNAME="${{ secrets.REGISTRY_USERNAME }}" | |
| echo "=== Debug Information ===" | |
| echo "REGISTRY: ${REGISTRY:-<empty>}" | |
| echo "PREFIX: ${PREFIX:-<empty>}" | |
| echo "USERNAME: ${USERNAME:-<empty>}" | |
| echo "SERVICE: $SERVICE" | |
| echo "========================" | |
| # Normalize registry (Docker Hub special handling) | |
| # Docker Hub: format is username/imagename (NO registry prefix) | |
| if [ -z "$REGISTRY" ] || [ "$REGISTRY" = "docker.io" ]; then | |
| echo "Using Docker Hub format (no registry in image name)" | |
| # Docker Hub: use username as prefix if no IMAGE_PREFIX | |
| if [ -z "$PREFIX" ]; then | |
| if [ -z "$USERNAME" ]; then | |
| echo "❌ Error: REGISTRY_USERNAME is required for Docker Hub" | |
| exit 1 | |
| fi | |
| PREFIX="$USERNAME" | |
| echo "Using REGISTRY_USERNAME as prefix: $PREFIX" | |
| fi | |
| # Docker Hub format: username/imagename (docker.io is NOT included in tag) | |
| IMAGE="$PREFIX/$SERVICE" | |
| CACHE_IMAGE="$PREFIX/$SERVICE:buildcache" | |
| else | |
| echo "Using other registry format: $REGISTRY" | |
| # Other registries: registry/prefix/service | |
| if [ -n "$PREFIX" ]; then | |
| IMAGE="$REGISTRY/$PREFIX/$SERVICE" | |
| CACHE_IMAGE="$REGISTRY/$PREFIX/$SERVICE:buildcache" | |
| else | |
| IMAGE="$REGISTRY/$SERVICE" | |
| CACHE_IMAGE="$REGISTRY/$SERVICE:buildcache" | |
| fi | |
| fi | |
| # Remove any double slashes and trailing slashes | |
| IMAGE=$(echo "$IMAGE" | sed 's|//|/|g' | sed 's|/$||') | |
| CACHE_IMAGE=$(echo "$CACHE_IMAGE" | sed 's|//|/|g' | sed 's|/$||') | |
| # Validate image name format | |
| if [[ "$IMAGE" == *"***"* ]] || [[ "$IMAGE" == *"//"* ]]; then | |
| echo "❌ Error: Invalid image name format: $IMAGE" | |
| echo "Check your REGISTRY, IMAGE_PREFIX, and REGISTRY_USERNAME secrets" | |
| exit 1 | |
| fi | |
| # Set tags | |
| if [ "${{ github.ref }}" = "refs/heads/main" ]; then | |
| echo "tags=$IMAGE:latest,$IMAGE:${{ github.sha }}" >> $GITHUB_OUTPUT | |
| else | |
| BRANCH=$(echo "${{ github.ref }}" | sed 's/refs\/heads\///' | tr '/' '-' | tr '_' '-') | |
| echo "tags=$IMAGE:$BRANCH-${{ github.sha }},$IMAGE:${{ github.sha }}" >> $GITHUB_OUTPUT | |
| fi | |
| echo "image=$IMAGE" >> $GITHUB_OUTPUT | |
| echo "cache-image=$CACHE_IMAGE" >> $GITHUB_OUTPUT | |
| echo "" | |
| echo "=== Final Image Names ===" | |
| echo "Image: $IMAGE" | |
| echo "Cache: $CACHE_IMAGE" | |
| echo "Tags: $(cat $GITHUB_OUTPUT | grep tags= | cut -d'=' -f2)" | |
| echo "=========================" | |
| # Final validation | |
| if [[ "$IMAGE" =~ ^[a-z0-9]([a-z0-9._-]*[a-z0-9])?/[a-z0-9]([a-z0-9._-]*[a-z0-9])?$ ]] || [[ "$IMAGE" =~ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?(:[0-9]+)?/[a-z0-9]([a-z0-9._-]*[a-z0-9])?$ ]]; then | |
| echo "✅ Image name format is valid" | |
| else | |
| echo "⚠️ Warning: Image name format may be invalid: $IMAGE" | |
| echo "Expected format: username/imagename or registry/username/imagename" | |
| fi | |
| - name: Build and push Docker image | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: ./${{ matrix.service }} | |
| push: true | |
| tags: ${{ steps.image-tags.outputs.tags }} | |
| cache-from: | | |
| type=registry,ref=${{ steps.image-tags.outputs.cache-image }} | |
| type=gha | |
| cache-to: | | |
| type=registry,ref=${{ steps.image-tags.outputs.cache-image }},mode=max | |
| type=gha,mode=max | |
| platforms: linux/amd64 | |
| build-args: | | |
| NODE_VERSION=${{ env.NODE_VERSION }} | |
| sbom: true | |
| provenance: true | |
| - name: Scan image for vulnerabilities | |
| uses: aquasecurity/trivy-action@master | |
| id: trivy-scan | |
| continue-on-error: true | |
| with: | |
| image-ref: ${{ secrets.REGISTRY }}/${{ secrets.IMAGE_PREFIX }}/${{ matrix.service }}:${{ github.sha }} | |
| format: 'sarif' | |
| output: 'trivy-results-${{ matrix.service }}.sarif' | |
| severity: 'CRITICAL,HIGH' | |
| - name: Upload Trivy results | |
| uses: github/codeql-action/upload-sarif@v3 | |
| if: always() && steps.trivy-scan.outcome == 'success' | |
| continue-on-error: true | |
| with: | |
| sarif_file: 'trivy-results-${{ matrix.service }}.sarif' | |
| deploy-staging: | |
| name: Deploy to Staging | |
| runs-on: ubuntu-latest | |
| needs: build-and-push | |
| if: github.ref == 'refs/heads/develop' && github.event_name == 'push' | |
| timeout-minutes: 20 | |
| environment: | |
| name: staging | |
| url: https://staging.example.com | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Install kubectl | |
| uses: azure/setup-kubectl@v3 | |
| with: | |
| version: 'latest' | |
| - name: Configure kubectl for Huawei Cloud | |
| run: | | |
| mkdir -p $HOME/.kube | |
| echo "${{ secrets.KUBECONFIG_STAGING }}" | base64 -d > $HOME/.kube/config | |
| kubectl config get-contexts | |
| - name: Install Helm | |
| uses: azure/setup-helm@v3 | |
| with: | |
| version: 'latest' | |
| - name: Create namespace if not exists | |
| run: | | |
| kubectl create namespace ${{ env.KUBERNETES_NAMESPACE_STAGING }} --dry-run=client -o yaml | kubectl apply -f - | |
| - name: Deploy to staging using Helm | |
| id: deploy | |
| run: | | |
| helm upgrade --install micro-demo-staging ./helm/micro-demo \ | |
| --namespace ${{ env.KUBERNETES_NAMESPACE_STAGING }} \ | |
| --set namespace=${{ env.KUBERNETES_NAMESPACE_STAGING }} \ | |
| --set product.image=${{ secrets.REGISTRY }}/${{ secrets.IMAGE_PREFIX }}/product-service \ | |
| --set product.tag=${{ github.sha }} \ | |
| --set order.image=${{ secrets.REGISTRY }}/${{ secrets.IMAGE_PREFIX }}/order-service \ | |
| --set order.tag=${{ github.sha }} \ | |
| --set notification.image=${{ secrets.REGISTRY }}/${{ secrets.IMAGE_PREFIX }}/notification-service \ | |
| --set notification.tag=${{ github.sha }} \ | |
| --wait \ | |
| --timeout 5m \ | |
| --atomic | |
| - name: Wait for deployments to be ready | |
| run: | | |
| kubectl wait --for=condition=available --timeout=300s \ | |
| deployment/product-service \ | |
| deployment/order-service \ | |
| deployment/notification-service \ | |
| -n ${{ env.KUBERNETES_NAMESPACE_STAGING }} | |
| - name: Run health checks | |
| run: | | |
| ./scripts/health-check.sh staging | |
| continue-on-error: false | |
| - name: Rollback on failure | |
| if: failure() | |
| run: | | |
| helm rollback micro-demo-staging -n ${{ env.KUBERNETES_NAMESPACE_STAGING }} || true | |
| exit 1 | |
| deploy-production-canary: | |
| name: Deploy Canary to Production | |
| runs-on: ubuntu-latest | |
| needs: build-and-push | |
| if: github.ref == 'refs/heads/main' && github.event_name == 'push' | |
| timeout-minutes: 30 | |
| environment: | |
| name: production | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Install kubectl | |
| uses: azure/setup-kubectl@v3 | |
| with: | |
| version: 'latest' | |
| - name: Install Helm | |
| uses: azure/setup-helm@v3 | |
| with: | |
| version: 'latest' | |
| - name: Configure kubectl for Huawei Cloud | |
| run: | | |
| mkdir -p $HOME/.kube | |
| echo "${{ secrets.KUBECONFIG_PRODUCTION }}" | base64 -d > $HOME/.kube/config | |
| kubectl config get-contexts | |
| - name: Create namespace if not exists | |
| run: | | |
| kubectl create namespace ${{ env.KUBERNETES_NAMESPACE_PRODUCTION }} --dry-run=client -o yaml | kubectl apply -f - | |
| - name: Deploy canary (10% traffic) | |
| run: | | |
| ./scripts/deploy-canary.sh ${{ github.sha }} 10 | |
| - name: Wait for canary to be ready | |
| run: | | |
| kubectl wait --for=condition=available --timeout=300s \ | |
| deployment/product-service-canary \ | |
| deployment/order-service-canary \ | |
| deployment/notification-service-canary \ | |
| -n ${{ env.KUBERNETES_NAMESPACE_PRODUCTION }} | |
| - name: Run health checks on canary | |
| run: | | |
| ./scripts/health-check.sh production canary | |
| - name: Monitor canary metrics (5 minutes) | |
| run: | | |
| ./scripts/monitor-canary.sh 300 | |
| - name: Promote canary to 100% (if health checks pass) | |
| id: promote | |
| run: | | |
| ./scripts/promote-canary.sh ${{ github.sha }} | |
| - name: Cleanup old canary deployments | |
| if: steps.promote.outcome == 'success' | |
| run: | | |
| ./scripts/cleanup-canary.sh | |
| - name: Rollback on failure | |
| if: failure() | |
| run: | | |
| ./scripts/rollback.sh |