name: CI/CD Pipeline with Secure Ephemeral PiP on: push: branches: [main] pull_request: branches: [main] env: REGISTRY: ${{ secrets.REGISTRY_HOST }} APP_NAME: ${{ secrets.APP_NAME }} IMAGE_TAG: ${{ github.sha }} RUN_ID: ${{ github.run_id }} jobs: test-backend: runs-on: [self-hosted, ci] steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup ephemeral PiP container run: | chmod +x ./secure_pip_setup.sh ./secure_pip_setup.sh - name: Wait for PiP readiness run: | chmod +x ./pip_ready.sh ./pip_ready.sh - name: Setup SSH with pinned known_hosts run: | mkdir -p ~/.ssh chmod 700 ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts chmod 644 ~/.ssh/known_hosts git config --global core.sshCommand "ssh -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$HOME/.ssh/known_hosts" - name: Create integration test network run: | podman exec ci-pip-$RUN_ID podman network create integ-$RUN_ID - name: Start PostgreSQL on internal network run: | podman exec ci-pip-$RUN_ID podman run -d \ --name test-postgres \ --network integ-$RUN_ID \ -e POSTGRES_PASSWORD=testpassword \ -e POSTGRES_USER=testuser \ -e POSTGRES_DB=testdb \ postgres:15-alpine@sha256:def456abc1237890def456abc1237890def456abc1237890def456abc1237890 - name: Wait for PostgreSQL to be ready run: | podman exec ci-pip-$RUN_ID sh -lc \ 'timeout 60 sh -c "until podman exec test-postgres pg_isready -h test-postgres -p 5432 -U testuser; do sleep 1; done"' - name: Run backend unit tests run: | podman exec -e WORKSPACE="${GITHUB_WORKSPACE}" ci-pip-$RUN_ID sh -lc \ 'podman run --rm \ -v "$WORKSPACE":/workspace \ -w /workspace \ rust@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef \ sh -c "cargo test --lib -- --test-threads=1"' - name: Run backend integration tests run: | podman exec -e WORKSPACE="${GITHUB_WORKSPACE}" ci-pip-$RUN_ID sh -lc \ 'podman run --rm \ -v "$WORKSPACE":/workspace \ -w /workspace \ -e DATABASE_URL=postgres://testuser:testpassword@test-postgres:5432/testdb \ rust@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef \ sh -c "cargo test --test '*' -- --test-threads=1"' - name: Cleanup test resources if: always() run: | podman exec ci-pip-$RUN_ID podman stop test-postgres 2>/dev/null || true podman exec ci-pip-$RUN_ID podman rm test-postgres 2>/dev/null || true podman exec ci-pip-$RUN_ID podman network rm integ-$RUN_ID 2>/dev/null || true - name: Per-job cleanup (host socket) if: always() run: | SOCKET_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/podman-host-${RUN_ID}" pgrep -u "$(id -u)" -fa 'podman system service' | grep -F "unix://${SOCKET_DIR}/podman.sock" | awk '{print $1}' | xargs -r kill || true rm -rf "${SOCKET_DIR}" 2>/dev/null || true test-frontend: runs-on: [self-hosted, ci] needs: test-backend steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup ephemeral PiP container run: | chmod +x ./secure_pip_setup.sh ./secure_pip_setup.sh - name: Wait for PiP readiness run: | chmod +x ./pip_ready.sh ./pip_ready.sh - name: Run frontend tests in PiP run: | podman exec -e WORKSPACE="${GITHUB_WORKSPACE}" ci-pip-$RUN_ID sh -lc \ 'podman run --rm \ -v "$WORKSPACE":/workspace \ -w /workspace \ node:20@sha256:7890abcdef1234567890abcdef1234567890abcdef1234567890abcdef123456 \ sh -c "npm ci && npm run test"' - name: Per-job cleanup (host socket) if: always() run: | SOCKET_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/podman-host-${RUN_ID}" pgrep -u "$(id -u)" -fa 'podman system service' | grep -F "unix://${SOCKET_DIR}/podman.sock" | awk '{print $1}' | xargs -r kill || true rm -rf "${SOCKET_DIR}" 2>/dev/null || true build-backend: runs-on: [self-hosted, ci] needs: test-frontend steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup ephemeral PiP container run: | chmod +x ./secure_pip_setup.sh ./secure_pip_setup.sh - name: Wait for PiP readiness run: | chmod +x ./pip_ready.sh ./pip_ready.sh - name: Login to Forgejo Container Registry securely run: | echo "${{ secrets.REGISTRY_TOKEN }}" | podman exec -i ci-pip-$RUN_ID podman login ${{ secrets.REGISTRY_HOST }} \ -u ${{ secrets.REGISTRY_USERNAME }} \ --password-stdin - name: Build backend image run: | podman exec -e REGISTRY="$REGISTRY" -e APP_NAME="$APP_NAME" -e IMAGE_TAG="$IMAGE_TAG" \ ci-pip-$RUN_ID sh -lc 'cd /workspace/backend && podman build -t "$REGISTRY/$APP_NAME/backend:$IMAGE_TAG" .' - name: Push backend image run: | podman exec -e REGISTRY="$REGISTRY" -e APP_NAME="$APP_NAME" -e IMAGE_TAG="$IMAGE_TAG" \ ci-pip-$RUN_ID sh -lc 'podman push "$REGISTRY/$APP_NAME/backend:$IMAGE_TAG"' - name: Per-job cleanup (host socket) if: always() run: | SOCKET_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/podman-host-${RUN_ID}" pgrep -u "$(id -u)" -fa 'podman system service' | grep -F "unix://${SOCKET_DIR}/podman.sock" | awk '{print $1}' | xargs -r kill || true rm -rf "${SOCKET_DIR}" 2>/dev/null || true build-frontend: runs-on: [self-hosted, ci] needs: test-frontend steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup ephemeral PiP container run: | chmod +x ./secure_pip_setup.sh ./secure_pip_setup.sh - name: Wait for PiP readiness run: | chmod +x ./pip_ready.sh ./pip_ready.sh - name: Login to Forgejo Container Registry securely run: | echo "${{ secrets.REGISTRY_TOKEN }}" | podman exec -i ci-pip-$RUN_ID podman login ${{ secrets.REGISTRY_HOST }} \ -u ${{ secrets.REGISTRY_USERNAME }} \ --password-stdin - name: Build frontend image run: | podman exec -e REGISTRY="$REGISTRY" -e APP_NAME="$APP_NAME" -e IMAGE_TAG="$IMAGE_TAG" \ ci-pip-$RUN_ID sh -lc 'cd /workspace/frontend && podman build -t "$REGISTRY/$APP_NAME/frontend:$IMAGE_TAG" .' - name: Push frontend image run: | podman exec -e REGISTRY="$REGISTRY" -e APP_NAME="$APP_NAME" -e IMAGE_TAG="$IMAGE_TAG" \ ci-pip-$RUN_ID sh -lc 'podman push "$REGISTRY/$APP_NAME/frontend:$IMAGE_TAG"' - name: Per-job cleanup (host socket) if: always() run: | SOCKET_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/podman-host-${RUN_ID}" pgrep -u "$(id -u)" -fa 'podman system service' | grep -F "unix://${SOCKET_DIR}/podman.sock" | awk '{print $1}' | xargs -r kill || true rm -rf "${SOCKET_DIR}" 2>/dev/null || true cleanup: runs-on: [self-hosted, ci] needs: [build-backend, build-frontend] if: always() steps: - name: Cleanup PiP container and per-run socket run: | podman rm -f ci-pip-$RUN_ID 2>/dev/null || true SOCKET_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/podman-host-${RUN_ID}" if pgrep -u "$(id -u)" -fa 'podman system service' | grep -F "unix://${SOCKET_DIR}/podman.sock" >/dev/null; then pgrep -u "$(id -u)" -fa 'podman system service' | grep -F "unix://${SOCKET_DIR}/podman.sock" | awk '{print $1}' | xargs -r kill || true fi rm -rf "${SOCKET_DIR}" 2>/dev/null || true