Skip to content
This repository was archived by the owner on Jul 6, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions .github/workflows/hotfix-deploy.yml
Original file line number Diff line number Diff line change
@@ -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."
2 changes: 1 addition & 1 deletion frontend/client/components/ui/AppNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export function AppNavigation() {
<ProfileMenu
name={resolvedName}
avatarUrl={avatarUrl}
profileHref={isEnterprise ? "/company" : `/profile/${resolvedProfileId}`}
profileHref={`/profile/${resolvedProfileId}`}
variant={isEnterprise ? "enterprise" : "freelancer"}
/>
</div>
Expand Down
Loading