Skip to content

Fix Docker Hub tag format: remove registry from image tags, add valid… #14

Fix Docker Hub tag format: remove registry from image tags, add valid…

Fix Docker Hub tag format: remove registry from image tags, add valid… #14

Workflow file for this run

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