name: Deploy Stacks on: push: branches: - dev - staging workflow_dispatch: permissions: contents: read jobs: detect-changes: name: Detect changed services runs-on: ${{ fromJson(format('["self-hosted", "{0}"]', github.ref_name)) }} outputs: matrix: ${{ steps.filter.outputs.matrix }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 2 - name: Determine changed services id: filter run: | set -euo pipefail ALL_SERVICES=( "freight-api" "freight-portal" "freight-backoffice" "gps-tracker" "passenger-api" "passenger-portal" "passenger-backoffice" "payment-api" ) if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then JSON=$(printf '%s\n' "${ALL_SERVICES[@]}" | jq -R . | jq -sc .) echo "matrix=${JSON}" >> "$GITHUB_OUTPUT" exit 0 fi CHANGED=$(git diff --name-only HEAD~1 HEAD) echo "=== Changed files ===" echo "$CHANGED" echo "=====================" SERVICES=() NON_DEPLOYABLE_PATTERN="^docs/|^README[.]md$|^DEPLOYMENT[.]md$|^CLAUDE[.]md$|^checkpoint[.]md$|^orgstructure[.]md$|^ITMLS_DB_Design[.]md$|.*[.]md$|^[.]eslintrc|^[.]prettierrc|^[.]editorconfig|^[.]gitignore|^[.]gitattributes|^commitlint[.]config[.]js$|^scripts/deploy/sync-env-from-server-jenkins[.]sh$" GLOBAL_PATTERN="^[.]github/|^docker-compose[.]yaml$|^turbo[.]json$|^tsconfig[.]json$|^tsconfig[.]base[.]json$|^pnpm-workspace[.]yaml$|^pnpm-lock[.]yaml$|^package[.]json$|^[.]env([.][a-z]+)?$|^packages/|^local-packages/|^infrastructure/|^scripts/deploy/|^wagon[.][^/]*[.]ts$|^cargo[.][^/]*[.]ts$|^container[.][^/]*[.]ts$|^use-[^/]*[.]ts$|^[^/]*[.]service[.]ts$|^[^/]*[.]entity[.]ts$|^[^/]*-types[.]ts$" DEPLOYABLE=$(echo "$CHANGED" | grep -vE "$NON_DEPLOYABLE_PATTERN" || true) if [ -z "$DEPLOYABLE" ]; then echo "Only non-deployable files changed. Skipping deploy." echo "matrix=[]" >> "$GITHUB_OUTPUT" exit 0 fi if echo "$CHANGED" | grep -qE "$GLOBAL_PATTERN"; then echo "Global file(s) changed — deploying all services." JSON=$(printf '%s\n' "${ALL_SERVICES[@]}" | jq -R . | jq -sc .) echo "matrix=${JSON}" >> "$GITHUB_OUTPUT" exit 0 fi echo "$CHANGED" | grep -q "^apps/edr-freight-api/" && SERVICES+=("freight-api") echo "$CHANGED" | grep -q "^apps/edr-freight-web/portal/" && SERVICES+=("freight-portal") echo "$CHANGED" | grep -q "^apps/edr-freight-web/backoffice/" && SERVICES+=("freight-backoffice") echo "$CHANGED" | grep -q "^apps/edr-gps-tracker/" && SERVICES+=("gps-tracker") echo "$CHANGED" | grep -q "^apps/edr-passenger-api/" && SERVICES+=("passenger-api") echo "$CHANGED" | grep -q "^apps/edr-passenger-web/portal/" && SERVICES+=("passenger-portal") echo "$CHANGED" | grep -q "^apps/edr-passenger-web/backoffice/" && SERVICES+=("passenger-backoffice") echo "$CHANGED" | grep -q "^apps/edr-payment-api/" && SERVICES+=("payment-api") SERVICES=($(printf '%s\n' "${SERVICES[@]}" | sort -u)) if [ ${#SERVICES[@]} -eq 0 ]; then echo "No deployable service changes detected." echo "matrix=[]" >> "$GITHUB_OUTPUT" else echo "Services to deploy: ${SERVICES[*]}" JSON=$(printf '%s\n' "${SERVICES[@]}" | jq -R . | jq -sc .) echo "matrix=${JSON}" >> "$GITHUB_OUTPUT" fi deploy: name: Deploy ${{ matrix.service }} needs: detect-changes if: ${{ needs.detect-changes.outputs.matrix != '[]' }} runs-on: ${{ fromJson(format('["self-hosted", "{0}"]', github.ref_name)) }} strategy: fail-fast: false matrix: service: ${{ fromJson(needs.detect-changes.outputs.matrix) }} env: BRANCH: ${{ github.ref_name }} DEPLOY_USER: tria DOCKER_BUILDKIT: "1" COMPOSE_DOCKER_CLI_BUILD: "1" steps: - name: Checkout uses: actions/checkout@v4 - name: Resolve project and build env file run: | case "${{ matrix.service }}" in freight-api|freight-portal|freight-backoffice|gps-tracker) echo "PROJECT=edr-freight" >> "$GITHUB_ENV" echo "BUILD_ENV_FILE=freight-web.build.env" >> "$GITHUB_ENV" ;; passenger-api|passenger-portal|passenger-backoffice) echo "PROJECT=edr-passenger" >> "$GITHUB_ENV" echo "BUILD_ENV_FILE=passenger-web.build.env" >> "$GITHUB_ENV" ;; payment-api) echo "PROJECT=edr-payment" >> "$GITHUB_ENV" echo "BUILD_ENV_FILE=payment-web.build.env" >> "$GITHUB_ENV" ;; *) echo "Unknown service: ${{ matrix.service }}" && exit 1 ;; esac - name: Sync environment from server run: | chmod +x scripts/deploy/*.sh ./scripts/deploy/sync-env-from-server.sh "${{ matrix.service }}" - name: Set compose project name run: | set -euo pipefail branch_slug=$(echo "${BRANCH}" | tr "[:upper:]" "[:lower:]" | sed -E "s/[^a-z0-9]+/-/g; s/^-+//; s/-+$//") echo "COMPOSE_PROJECT_NAME=${PROJECT}-${branch_slug}" >> "${GITHUB_ENV}" - name: Configure npm auth for Docker builds env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: ./scripts/deploy/create-npmrc.sh - name: Resolve env file path for ${{ matrix.service }} if: contains(fromJson('["passenger-api", "payment-api"]'), matrix.service) run: | case "${{ matrix.service }}" in passenger-api) echo "SERVICE_ENV_FILE=apps/edr-passenger-api/.env" >> "$GITHUB_ENV" ;; payment-api) echo "SERVICE_ENV_FILE=apps/edr-payment-api/.env" >> "$GITHUB_ENV" ;; esac - name: Build migration image for ${{ matrix.service }} if: contains(fromJson('["passenger-api", "payment-api"]'), matrix.service) run: | set -euo pipefail docker build \ --secret id=npmrc,src=.npmrc \ --target migration \ -f "apps/edr-${{ matrix.service }}/Dockerfile" \ -t "${COMPOSE_PROJECT_NAME}-${{ matrix.service }}-migration" \ . - name: Resolve failed migrations for ${{ matrix.service }} if: matrix.service == 'passenger-api' run: | set -euo pipefail docker run --rm --env-file "${SERVICE_ENV_FILE}" \ --entrypoint npx \ "${COMPOSE_PROJECT_NAME}-${{ matrix.service }}-migration" \ prisma migrate resolve \ --applied 20260718144855_unique_constraint_schedule_id_seat_id_departure_station_id \ || true - name: Run migrations for ${{ matrix.service }} if: contains(fromJson('["passenger-api", "payment-api"]'), matrix.service) run: | set -euo pipefail docker run --rm --env-file "${SERVICE_ENV_FILE}" "${COMPOSE_PROJECT_NAME}-${{ matrix.service }}-migration" - name: Build ${{ matrix.service }} run: | set -euo pipefail IMAGE_TAG="${COMPOSE_PROJECT_NAME}-${{ matrix.service }}:${GITHUB_SHA::8}" docker compose --project-name "${COMPOSE_PROJECT_NAME}" build "${{ matrix.service }}" # Tag with git SHA for rollback capability CONTAINER_NAME=$(docker compose --project-name "${COMPOSE_PROJECT_NAME}" config --services | grep "${{ matrix.service }}" | head -1) docker tag "${COMPOSE_PROJECT_NAME}-${{ matrix.service }}" "${IMAGE_TAG}" 2>/dev/null || true echo "IMAGE_TAG=${IMAGE_TAG}" >> "${GITHUB_ENV}" - name: Deploy ${{ matrix.service }} run: | set -euo pipefail docker compose --project-name "${COMPOSE_PROJECT_NAME}" up -d "${{ matrix.service }}" --force-recreate - name: Remove npm credentials from workspace if: always() run: rm -f .npmrc .npmrc_temp