name: CI/CD Pipeline on: push: branches: - development - main jobs: i18n-check: name: i18n Catalog Check runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Bun run: | if ! command -v unzip >/dev/null; then (command -v apt-get >/dev/null && apt-get update && apt-get install -y unzip) || \ (command -v apk >/dev/null && apk add --no-cache unzip) fi arch=$(uname -m) case "$arch" in x86_64) target=x64 ;; aarch64|arm64) target=aarch64 ;; *) echo "unsupported arch: $arch"; exit 1 ;; esac # musl images (Alpine) need the -musl build; the glibc one fails with spawn ENOENT libc="" if ldd --version 2>&1 | grep -qi musl; then libc="-musl"; fi curl -fsSL -o /tmp/bun.zip "https://github.com/oven-sh/bun/releases/latest/download/bun-linux-${target}${libc}.zip" unzip -q -o /tmp/bun.zip -d /tmp/bun-extract mkdir -p "$HOME/.bun/bin" mv /tmp/bun-extract/bun-linux-*/bun "$HOME/.bun/bin/bun" chmod +x "$HOME/.bun/bin/bun" echo "$HOME/.bun/bin" >> $GITHUB_PATH "$HOME/.bun/bin/bun" --version - name: Install dependencies run: bun install --frozen-lockfile - name: Catalogs in sync with source run: | bunx lingui extract --clean git diff --exit-code src/locales || { echo "::error::Locale catalogs are stale — run 'bun run extract' and commit src/locales" exit 1 } - name: No missing translations run: bunx lingui compile --strict build-app: name: Build and Push App Docker Image runs-on: ubuntu-latest needs: i18n-check steps: - name: Checkout code uses: actions/checkout@v4 - name: Set environment variables run: | if [ "${{ github.ref }}" == "refs/heads/main" ]; then echo "DOCKER_TAG=latest" >> $GITHUB_ENV echo "ENVIRONMENT=prod" >> $GITHUB_ENV else echo "DOCKER_TAG=dev" >> $GITHUB_ENV echo "ENVIRONMENT=dev" >> $GITHUB_ENV fi - name: Build and push app Docker image run: | docker login git.yohler.net -u ${{ github.actor }} -p ${{ secrets.PACKAGES_TOKEN }} docker build \ -f Dockerfile \ -t git.yohler.net/kyle/flxn-app:${{ env.DOCKER_TAG }} \ -t git.yohler.net/kyle/flxn-app:${{ github.sha }} \ . docker push git.yohler.net/kyle/flxn-app:${{ env.DOCKER_TAG }} docker push git.yohler.net/kyle/flxn-app:${{ github.sha }} build-pocketbase: name: Build and Push PocketBase Docker Image runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Check if PocketBase Dockerfile or migrations changed id: check_changes run: | if [ "${{ github.event.before }}" == "0000000000000000000000000000000000000000" ] || ! git cat-file -e ${{ github.event.before }} 2>/dev/null; then echo "changed=true" >> $GITHUB_OUTPUT elif git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -qE "(Dockerfile.pocketbase|pb_migrations/)"; then echo "changed=true" >> $GITHUB_OUTPUT else echo "changed=false" >> $GITHUB_OUTPUT fi - name: Build and push PocketBase Docker image if: steps.check_changes.outputs.changed == 'true' || github.event.before == '0000000000000000000000000000000000000000' run: | docker login git.yohler.net -u ${{ github.actor }} -p ${{ secrets.PACKAGES_TOKEN }} docker build \ -f Dockerfile.pocketbase \ -t git.yohler.net/kyle/flxn-pocketbase:latest \ -t git.yohler.net/kyle/flxn-pocketbase:${{ github.sha }} \ . docker push git.yohler.net/kyle/flxn-pocketbase:latest docker push git.yohler.net/kyle/flxn-pocketbase:${{ github.sha }} deploy: name: Deploy to Kubernetes needs: [build-app, build-pocketbase] runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: pin rancher host run: echo "192.168.4.43 rancher.yohler.net" >> /etc/hosts - name: Set environment variables run: | if [ "${{ github.ref }}" == "refs/heads/main" ]; then echo "ENVIRONMENT=prod" >> $GITHUB_ENV echo "NAMESPACE=flxn-prod" >> $GITHUB_ENV else echo "ENVIRONMENT=dev" >> $GITHUB_ENV echo "NAMESPACE=flxn-dev" >> $GITHUB_ENV fi - name: Install kubectl run: | curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" chmod +x kubectl mv kubectl /usr/local/bin/ - name: Configure kubectl run: | mkdir -p $HOME/.kube echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > $HOME/.kube/config chmod 600 $HOME/.kube/config kubectl config set-cluster local --insecure-skip-tls-verify=true - name: Verify kubectl access run: | kubectl version --client kubectl get nodes - name: Deploy shared services (SuperTokens) run: | kubectl apply -k k8s/overlays/shared/ - name: Deploy to ${{ env.ENVIRONMENT }} run: | kubectl apply -k k8s/overlays/${{ env.ENVIRONMENT }}/ - name: Force rollout to pull latest image run: | kubectl rollout restart deployment/flxn-app -n ${{ env.NAMESPACE }} kubectl rollout restart deployment/flxn-pocketbase -n ${{ env.NAMESPACE }} - name: Wait for rollout run: | kubectl rollout status deployment/flxn-app -n ${{ env.NAMESPACE }} --timeout=5m kubectl rollout status deployment/flxn-pocketbase -n ${{ env.NAMESPACE }} --timeout=5m - name: Verify deployment run: | kubectl get pods -n ${{ env.NAMESPACE }} -l app=flxn kubectl get svc -n ${{ env.NAMESPACE }} -l app=flxn kubectl get ingress -n ${{ env.NAMESPACE }} - name: Check app health run: | sleep 15 APP_POD=$(kubectl get pod -n ${{ env.NAMESPACE }} -l component=app -o jsonpath='{.items[0].metadata.name}') kubectl exec -n ${{ env.NAMESPACE }} $APP_POD -- wget -O- http://localhost:3000/api/health || echo "Health check failed (endpoint may not exist yet)" - name: Check PocketBase health run: | PB_POD=$(kubectl get pod -n ${{ env.NAMESPACE }} -l component=pocketbase -o jsonpath='{.items[0].metadata.name}') kubectl exec -n ${{ env.NAMESPACE }} $PB_POD -- wget -O- http://localhost:8090/api/health || echo "PocketBase health check completed"