diff --git a/.github/workflows/hotfix-deploy.yml b/.github/workflows/hotfix-deploy.yml new file mode 100644 index 0000000..9f5823a --- /dev/null +++ b/.github/workflows/hotfix-deploy.yml @@ -0,0 +1,155 @@ +name: Hotfix Deploy + +# Fast path: build + push the images for a ref and Helm-deploy to project-01, +# SKIPPING the full CI suite (lint / test / Trivy / Lighthouse). Manual only. +# Use for urgent hotfixes; normal merges to main still run full CI + auto-CD. +on: + workflow_dispatch: + inputs: + ref: + description: "Branch or commit SHA to build + deploy (default: main)." + required: false + default: main + type: string + +permissions: + contents: read + packages: write + +# Share the prod-deploy concurrency group with cd.yml so they never overlap. +concurrency: + group: cd-project-01 + cancel-in-progress: false + +env: + REGISTRY: ghcr.io + NAMESPACE: project-01 + RELEASE: loopless + CHART_PATH: devops/helm/loopless + HEALTH_URL: https://api.project-01.gjirafa.dev/health/ready + DEPLOY_TIMEOUT: 8m + +jobs: + hotfix: + name: Build + deploy (no tests) + runs-on: ubuntu-latest + environment: + name: production-project-01 + url: https://web.project-01.gjirafa.dev + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.ref }} + + - name: Resolve SHA + id: sha + shell: bash + run: echo "value=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute lowercase image owner + id: owner + shell: bash + run: echo "value=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_OUTPUT" + + # Retry on transient mcr.microsoft.com 403 (same hazard the CI security job hits). + - name: Build + push backend + shell: bash + env: + IMG: ${{ env.REGISTRY }}/${{ steps.owner.outputs.value }}/loopless-backend:sha-${{ steps.sha.outputs.value }} + run: | + attempt=1; max=3 + until docker buildx build --file devops/docker/backend/Dockerfile --push --tag "$IMG" backend; do + if [ "$attempt" -ge "$max" ]; then echo "::error::backend build failed after ${max} attempts"; exit 1; fi + echo "::warning::build attempt ${attempt} failed (likely transient registry 403); retrying in 20s..." + attempt=$((attempt + 1)); sleep 20 + done + + - name: Build + push frontend + shell: bash + env: + IMG: ${{ env.REGISTRY }}/${{ steps.owner.outputs.value }}/loopless-frontend:sha-${{ steps.sha.outputs.value }} + run: | + attempt=1; max=3 + until docker buildx build --file devops/docker/frontend/Dockerfile --push --tag "$IMG" \ + --build-arg NEXT_PUBLIC_API_BASE_URL=https://api.project-01.gjirafa.dev \ + --build-arg NEXT_PUBLIC_KEYCLOAK_URL=https://api.project-01.gjirafa.dev/auth \ + frontend/client; do + if [ "$attempt" -ge "$max" ]; then echo "::error::frontend build failed after ${max} attempts"; exit 1; fi + echo "::warning::build attempt ${attempt} failed (likely transient registry 403); retrying in 20s..." + attempt=$((attempt + 1)); sleep 20 + done + + - name: Install kubectl + uses: azure/setup-kubectl@v4 + with: + version: v1.30.0 + + - name: Install Helm + uses: azure/setup-helm@v4 + with: + version: v3.16.3 + + - name: Configure kubeconfig + shell: bash + env: + KUBECONFIG_PROJECT_01: ${{ secrets.KUBECONFIG_PROJECT_01 }} + run: | + if [ -z "$KUBECONFIG_PROJECT_01" ]; then + echo "::error::KUBECONFIG_PROJECT_01 secret is not set." + exit 1 + fi + mkdir -p "$HOME/.kube" + printf '%s' "$KUBECONFIG_PROJECT_01" | base64 -d > "$HOME/.kube/config" + chmod 600 "$HOME/.kube/config" + kubectl config current-context + + - name: Helm upgrade (pin images to the built SHA) + shell: bash + env: + IMAGE_TAG: sha-${{ steps.sha.outputs.value }} + run: | + helm upgrade --install "${RELEASE}" "${CHART_PATH}" \ + --namespace "${NAMESPACE}" \ + --set image.backend.tag="${IMAGE_TAG}" \ + --set image.frontend.tag="${IMAGE_TAG}" \ + --timeout "${DEPLOY_TIMEOUT}" + + - name: Wait for rollouts + shell: bash + run: | + kubectl -n "${NAMESPACE}" rollout status deployment/loopless-backend --timeout="${DEPLOY_TIMEOUT}" + kubectl -n "${NAMESPACE}" rollout status deployment/loopless-frontend --timeout="${DEPLOY_TIMEOUT}" + + - name: Health check + shell: bash + run: | + for attempt in 1 2 3 4 5 6; do + if curl -fsS --max-time 10 "${HEALTH_URL}"; then + echo "Health check passed on attempt ${attempt}." + exit 0 + fi + echo "Health check attempt ${attempt} failed; retrying in 10s..." + sleep 10 + done + echo "::error::Health check failed after 6 attempts (${HEALTH_URL})." + exit 1 + + - name: Roll back on failure + if: failure() + shell: bash + run: | + echo "::warning::Hotfix deploy failed — rolling back to the previous Helm revision." + helm rollback "${RELEASE}" -n "${NAMESPACE}" --timeout "${DEPLOY_TIMEOUT}" || \ + echo "::error::helm rollback failed — manual intervention required." diff --git a/frontend/client/components/ui/AppNavigation.tsx b/frontend/client/components/ui/AppNavigation.tsx index 95db2f4..e6a6ead 100644 --- a/frontend/client/components/ui/AppNavigation.tsx +++ b/frontend/client/components/ui/AppNavigation.tsx @@ -121,7 +121,7 @@ export function AppNavigation() {