diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 278fa9f8..57112418 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -1,3 +1,5 @@ +# EN: Deploy GoodGo Platform production services to Kubernetes production +# VI: Trien khai cac service production cua GoodGo Platform len K8s production name: Deploy to Production on: @@ -6,49 +8,408 @@ on: - main paths: - 'services/iam-service-net/**' - - 'apps/web-client/**' + - 'services/merchant-service-net/**' + - 'services/order-service-net/**' + - 'services/fnb-engine-net/**' + - 'services/inventory-service-net/**' + - 'services/wallet-service-net/**' + - 'services/catalog-service-net/**' + - 'services/booking-service-net/**' + - 'apps/web-client-tpos-net/**' - 'deployments/production/**' workflow_dispatch: + inputs: + service: + description: 'Service to deploy (leave empty for all changed)' + required: false + default: '' + type: choice + options: + - '' + - iam-service + - merchant-service + - order-service + - fnb-engine + - inventory-service + - wallet-service + - catalog-service + - booking-service + - pos-web + +env: + REGISTRY: docker.io + NAMESPACE: production jobs: - deploy: + # ========================================================================= + # EN: Detect which services changed + # VI: Phat hien service nao thay doi + # ========================================================================= + detect-changes: + runs-on: ubuntu-latest + outputs: + services: ${{ steps.changes.outputs.services }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Detect changed services + id: changes + run: | + if [ -n "${{ github.event.inputs.service }}" ]; then + echo 'services=["${{ github.event.inputs.service }}"]' >> $GITHUB_OUTPUT + exit 0 + fi + + SERVICES=() + CHANGED=$(git diff --name-only HEAD~1 HEAD) + + declare -A SERVICE_MAP=( + ["services/iam-service-net"]="iam-service" + ["services/merchant-service-net"]="merchant-service" + ["services/order-service-net"]="order-service" + ["services/fnb-engine-net"]="fnb-engine" + ["services/inventory-service-net"]="inventory-service" + ["services/wallet-service-net"]="wallet-service" + ["services/catalog-service-net"]="catalog-service" + ["services/booking-service-net"]="booking-service" + ["apps/web-client-tpos-net"]="pos-web" + ) + + for path in "${!SERVICE_MAP[@]}"; do + if echo "$CHANGED" | grep -q "^${path}/"; then + SERVICES+=("\"${SERVICE_MAP[$path]}\"") + fi + done + + # EN: If deployment configs changed, deploy all + # VI: Neu cau hinh deployment thay doi, deploy tat ca + if echo "$CHANGED" | grep -q "^deployments/production/"; then + SERVICES=("\"iam-service\"" "\"merchant-service\"" "\"order-service\"" "\"fnb-engine\"" "\"inventory-service\"" "\"wallet-service\"" "\"catalog-service\"" "\"booking-service\"" "\"pos-web\"") + fi + + if [ ${#SERVICES[@]} -eq 0 ]; then + echo 'services=[]' >> $GITHUB_OUTPUT + else + JOINED=$(IFS=,; echo "${SERVICES[*]}") + echo "services=[${JOINED}]" >> $GITHUB_OUTPUT + fi + + # ========================================================================= + # EN: Build & Push Docker Images (tagged with commit SHA, never :latest) + # VI: Build & Push Docker Images (tag bang commit SHA, khong dung :latest) + # ========================================================================= + build-and-push: + needs: detect-changes + if: needs.detect-changes.outputs.services != '[]' + runs-on: ubuntu-latest + # EN: Require production environment approval + # VI: Yeu cau phe duyet moi truong production + environment: production + strategy: + matrix: + service: ${{ fromJSON(needs.detect-changes.outputs.services) }} + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Determine build context + id: context + run: | + declare -A CONTEXT_MAP=( + ["iam-service"]="./services/iam-service-net" + ["merchant-service"]="./services/merchant-service-net" + ["order-service"]="./services/order-service-net" + ["fnb-engine"]="./services/fnb-engine-net" + ["inventory-service"]="./services/inventory-service-net" + ["wallet-service"]="./services/wallet-service-net" + ["catalog-service"]="./services/catalog-service-net" + ["booking-service"]="./services/booking-service-net" + ["pos-web"]="./apps/web-client-tpos-net" + ) + + declare -A IMAGE_MAP=( + ["iam-service"]="goodgo/iam-service-net" + ["merchant-service"]="goodgo/merchant-service-net" + ["order-service"]="goodgo/order-service-net" + ["fnb-engine"]="goodgo/fnb-engine-net" + ["inventory-service"]="goodgo/inventory-service-net" + ["wallet-service"]="goodgo/wallet-service-net" + ["catalog-service"]="goodgo/catalog-service-net" + ["booking-service"]="goodgo/booking-service-net" + ["pos-web"]="goodgo/web-client-tpos-net" + ) + + echo "context=${CONTEXT_MAP[${{ matrix.service }}]}" >> $GITHUB_OUTPUT + echo "image=${IMAGE_MAP[${{ matrix.service }}]}" >> $GITHUB_OUTPUT + + # EN: Tag with commit SHA for production (never use :latest in prod) + # VI: Tag bang commit SHA cho production (khong bao gio dung :latest trong prod) + - name: Build and push ${{ matrix.service }} + uses: docker/build-push-action@v5 + with: + context: ${{ steps.context.outputs.context }} + push: true + tags: | + ${{ steps.context.outputs.image }}:${{ github.sha }} + ${{ steps.context.outputs.image }}:production + cache-from: type=registry,ref=${{ steps.context.outputs.image }}:buildcache-prod + cache-to: type=registry,ref=${{ steps.context.outputs.image }}:buildcache-prod,mode=max + + # ========================================================================= + # EN: Run Database Migrations + # VI: Chay Database Migrations + # ========================================================================= + migrations: + needs: [detect-changes, build-and-push] + if: needs.detect-changes.outputs.services != '[]' runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v4 - + - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: dotnet-version: '10.0.x' - - - name: Run database migrations + + - name: Install EF Core tools + run: dotnet tool install --global dotnet-ef || true + + - name: Run IAM migrations + if: contains(needs.detect-changes.outputs.services, 'iam-service') run: | - dotnet tool install --global dotnet-ef || true dotnet ef database update \ --project services/iam-service-net/src/IamService.Infrastructure/IamService.Infrastructure.csproj \ --startup-project services/iam-service-net/src/IamService.API/IamService.API.csproj env: - ConnectionStrings__DefaultConnection: ${{ secrets.NEON_DATABASE_URL_PRODUCTION }} - + ConnectionStrings__DefaultConnection: ${{ secrets.NEON_IAM_DATABASE_URL_PRODUCTION }} + + - name: Run Merchant migrations + if: contains(needs.detect-changes.outputs.services, 'merchant-service') + run: | + dotnet ef database update \ + --project services/merchant-service-net/src/MerchantService.Infrastructure/MerchantService.Infrastructure.csproj \ + --startup-project services/merchant-service-net/src/MerchantService.API/MerchantService.API.csproj + env: + ConnectionStrings__DefaultConnection: ${{ secrets.NEON_MERCHANT_DATABASE_URL_PRODUCTION }} + + - name: Run Order migrations + if: contains(needs.detect-changes.outputs.services, 'order-service') + run: | + dotnet ef database update \ + --project services/order-service-net/src/OrderService.Infrastructure/OrderService.Infrastructure.csproj \ + --startup-project services/order-service-net/src/OrderService.API/OrderService.API.csproj + env: + ConnectionStrings__DefaultConnection: ${{ secrets.NEON_ORDER_DATABASE_URL_PRODUCTION }} + + - name: Run FnB Engine migrations + if: contains(needs.detect-changes.outputs.services, 'fnb-engine') + run: | + dotnet ef database update \ + --project services/fnb-engine-net/src/FnbEngine.Infrastructure/FnbEngine.Infrastructure.csproj \ + --startup-project services/fnb-engine-net/src/FnbEngine.API/FnbEngine.API.csproj + env: + ConnectionStrings__DefaultConnection: ${{ secrets.NEON_FNB_DATABASE_URL_PRODUCTION }} + + - name: Run Inventory migrations + if: contains(needs.detect-changes.outputs.services, 'inventory-service') + run: | + dotnet ef database update \ + --project services/inventory-service-net/src/InventoryService.Infrastructure/InventoryService.Infrastructure.csproj \ + --startup-project services/inventory-service-net/src/InventoryService.API/InventoryService.API.csproj + env: + ConnectionStrings__DefaultConnection: ${{ secrets.NEON_INVENTORY_DATABASE_URL_PRODUCTION }} + + - name: Run Wallet migrations + if: contains(needs.detect-changes.outputs.services, 'wallet-service') + run: | + dotnet ef database update \ + --project services/wallet-service-net/src/WalletService.Infrastructure/WalletService.Infrastructure.csproj \ + --startup-project services/wallet-service-net/src/WalletService.API/WalletService.API.csproj + env: + ConnectionStrings__DefaultConnection: ${{ secrets.NEON_WALLET_DATABASE_URL_PRODUCTION }} + + - name: Run Catalog migrations + if: contains(needs.detect-changes.outputs.services, 'catalog-service') + run: | + dotnet ef database update \ + --project services/catalog-service-net/src/CatalogService.Infrastructure/CatalogService.Infrastructure.csproj \ + --startup-project services/catalog-service-net/src/CatalogService.API/CatalogService.API.csproj + env: + ConnectionStrings__DefaultConnection: ${{ secrets.NEON_CATALOG_DATABASE_URL_PRODUCTION }} + + - name: Run Booking migrations + if: contains(needs.detect-changes.outputs.services, 'booking-service') + run: | + dotnet ef database update \ + --project services/booking-service-net/src/BookingService.Infrastructure/BookingService.Infrastructure.csproj \ + --startup-project services/booking-service-net/src/BookingService.API/BookingService.API.csproj + env: + ConnectionStrings__DefaultConnection: ${{ secrets.NEON_BOOKING_DATABASE_URL_PRODUCTION }} + + # ========================================================================= + # EN: Deploy to Kubernetes + # VI: Trien khai len Kubernetes + # ========================================================================= + deploy: + needs: [detect-changes, build-and-push, migrations] + if: needs.detect-changes.outputs.services != '[]' + runs-on: ubuntu-latest + environment: production + steps: + - uses: actions/checkout@v4 + - name: Setup kubectl uses: azure/setup-kubectl@v3 - + - name: Configure kubectl run: | echo "${{ secrets.KUBECONFIG_PRODUCTION }}" | base64 -d > kubeconfig - export KUBECONFIG=./kubeconfig - - - name: Deploy IAM Service + echo "KUBECONFIG=$(pwd)/kubeconfig" >> $GITHUB_ENV + + - name: Apply namespace and config + run: | + kubectl apply -f deployments/production/kubernetes/namespace.yaml + kubectl apply -f deployments/production/kubernetes/configmap.yaml + + - name: Deploy Redis + run: | + kubectl apply -f deployments/production/kubernetes/redis.yaml + + # EN: Update image tags to commit SHA and deploy + # VI: Cap nhat image tag bang commit SHA va deploy + - name: Deploy services + run: | + SERVICES='${{ needs.detect-changes.outputs.services }}' + + declare -A DEPLOY_MAP=( + ["iam-service"]="iam-service.yaml" + ["merchant-service"]="merchant-service.yaml" + ["order-service"]="order-service.yaml" + ["fnb-engine"]="fnb-engine.yaml" + ["inventory-service"]="inventory-service.yaml" + ["wallet-service"]="wallet-service.yaml" + ["catalog-service"]="catalog-service.yaml" + ["booking-service"]="booking-service.yaml" + ["pos-web"]="pos-web.yaml" + ) + + declare -A IMAGE_MAP=( + ["iam-service"]="goodgo/iam-service-net" + ["merchant-service"]="goodgo/merchant-service-net" + ["order-service"]="goodgo/order-service-net" + ["fnb-engine"]="goodgo/fnb-engine-net" + ["inventory-service"]="goodgo/inventory-service-net" + ["wallet-service"]="goodgo/wallet-service-net" + ["catalog-service"]="goodgo/catalog-service-net" + ["booking-service"]="goodgo/booking-service-net" + ["pos-web"]="goodgo/web-client-tpos-net" + ) + + for svc in "${!DEPLOY_MAP[@]}"; do + if echo "$SERVICES" | grep -q "\"${svc}\""; then + echo "Deploying ${svc}..." + kubectl apply -f "deployments/production/kubernetes/${DEPLOY_MAP[$svc]}" + + # EN: Update image to commit SHA (never :latest in production) + # VI: Cap nhat image bang commit SHA (khong bao gio dung :latest trong production) + kubectl set image "deployment/${svc}" \ + "${svc}=${IMAGE_MAP[$svc]}:${{ github.sha }}" \ + -n production + + kubectl rollout restart "deployment/${svc}" -n production + fi + done + + - name: Apply ingress run: | - export KUBECONFIG=./kubeconfig - kubectl apply -f deployments/production/kubernetes/iam-service.yaml - kubectl apply -f deployments/production/kubernetes/iam-service-configmap.yaml kubectl apply -f deployments/production/kubernetes/ingress.yaml - kubectl rollout status deployment/iam-service -n production - - - name: Deploy Web App + + - name: Wait for rollouts run: | - export KUBECONFIG=./kubeconfig - kubectl apply -f deployments/production/kubernetes/web-app.yaml || echo "Web app deployment not configured" - kubectl rollout status deployment/web-app -n production || echo "Web app deployment not configured" + SERVICES='${{ needs.detect-changes.outputs.services }}' + + declare -A DEPLOY_NAMES=( + ["iam-service"]="iam-service" + ["merchant-service"]="merchant-service" + ["order-service"]="order-service" + ["fnb-engine"]="fnb-engine" + ["inventory-service"]="inventory-service" + ["wallet-service"]="wallet-service" + ["catalog-service"]="catalog-service" + ["booking-service"]="booking-service" + ["pos-web"]="pos-web" + ) + + FAILED=0 + for svc in "${!DEPLOY_NAMES[@]}"; do + if echo "$SERVICES" | grep -q "\"${svc}\""; then + echo "Waiting for ${svc}..." + if ! kubectl rollout status "deployment/${DEPLOY_NAMES[$svc]}" -n production --timeout=180s; then + echo "FAILED: ${svc} rollout did not complete in time" + FAILED=$((FAILED + 1)) + fi + fi + done + + if [ $FAILED -gt 0 ]; then + echo "ERROR: ${FAILED} service(s) did not complete rollout" + kubectl get pods -n production + exit 1 + fi + + - name: Verify deployment + run: | + echo "=== Pods ===" + kubectl get pods -n production -o wide + echo "" + echo "=== Services ===" + kubectl get svc -n production + echo "" + echo "=== HPAs ===" + kubectl get hpa -n production + echo "" + echo "=== Ingress ===" + kubectl get ingress -n production + + # EN: Post-deployment health check + # VI: Kiem tra suc khoe sau deploy + - name: Health check + run: | + SERVICES='${{ needs.detect-changes.outputs.services }}' + + declare -A DEPLOY_NAMES=( + ["iam-service"]="iam-service" + ["merchant-service"]="merchant-service" + ["order-service"]="order-service" + ["fnb-engine"]="fnb-engine" + ["inventory-service"]="inventory-service" + ["wallet-service"]="wallet-service" + ["catalog-service"]="catalog-service" + ["booking-service"]="booking-service" + ) + + echo "Running post-deployment health checks..." + for svc in "${!DEPLOY_NAMES[@]}"; do + if echo "$SERVICES" | grep -q "\"${svc}\""; then + POD=$(kubectl get pods -n production -l app=${svc} -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) + if [ -n "$POD" ]; then + HEALTH=$(kubectl exec "$POD" -n production -- curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health/live 2>/dev/null || echo "000") + if [ "$HEALTH" = "200" ]; then + echo "${svc}: HEALTHY (200)" + else + echo "${svc}: UNHEALTHY (${HEALTH})" + fi + fi + fi + done diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Pos/ResponsiveOrderPanel.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Pos/ResponsiveOrderPanel.razor new file mode 100644 index 00000000..9fe67c09 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Components/Pos/ResponsiveOrderPanel.razor @@ -0,0 +1,63 @@ +@* + EN: Responsive Order Panel — Adapts to screen size automatically. + Desktop: Renders inline as a right sidebar (pos-cart-panel). + Tablet/Mobile: Content is injected into PosLayout's order drawer via CascadingValue. + VI: Panel Đơn Hàng Responsive — Tự động thích ứng theo kích thước màn hình. + Desktop: Render inline dạng sidebar phải (pos-cart-panel). + Tablet/Mobile: Nội dung được inject vào order drawer của PosLayout qua CascadingValue. +*@ +@inject IJSRuntime JS +@implements IDisposable + +@* EN: Desktop view — inline sidebar / VI: View desktop — sidebar inline *@ +
Đang tải thực đơn...
+@_error
+Không tìm thấy món nào
+Thử tìm kiếm với từ khóa khác
+