Files
edr-platform/.github/workflows/deploy.yml
Hagernesh 0339b89b54 ci: replace actions/checkout with plain git on self-hosted runners
Runners on this network intermittently time out downloading the action
tarball from codeload.github.com (HttpClient 100s limit, 3 attempts, job
dead before the first step). git fetch talks to github.com directly and
needs no action download at all.

- detect-changes: fetch --depth 2 (keeps the HEAD~1 diff working)
- deploy: fetch --depth 1
- token passed via env for the fetch, then scrubbed from .git/config so it
  doesn't persist in the runner workspace; git clean keeps checkout@v4's
  clean-workspace behaviour

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 11:13:21 +00:00

213 lines
9.1 KiB
YAML

name: Deploy Stacks
on:
push:
branches:
- main
- 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:
# Plain git instead of actions/checkout: self-hosted runners on this
# network intermittently time out downloading action tarballs from
# codeload.github.com (100s HttpClient limit x3 = dead job). git fetch
# talks to github.com directly and needs no action download at all.
- name: Checkout (plain git, depth 2)
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
git init -q .
git remote remove origin 2>/dev/null || true
git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
git fetch -q --depth 2 origin "${{ github.sha }}"
git checkout -q --force "${{ github.sha }}"
git clean -ffdq
# Don't leave the token in .git/config on the persistent runner workspace.
git remote set-url origin "https://github.com/${{ github.repository }}.git"
- name: Determine changed services
id: filter
run: |
set -euo pipefail
ALL_SERVICES=(
"freight-api"
"freight-portal"
"freight-backoffice"
"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-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:
# Same rationale as detect-changes: no action download on this network.
- name: Checkout (plain git)
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
git init -q .
git remote remove origin 2>/dev/null || true
git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
git fetch -q --depth 1 origin "${{ github.sha }}"
git checkout -q --force "${{ github.sha }}"
git clean -ffdq
# Don't leave the token in .git/config on the persistent runner workspace.
git remote set-url origin "https://github.com/${{ github.repository }}.git"
- name: Resolve project and build env file
run: |
case "${{ matrix.service }}" in
freight-api|freight-portal|freight-backoffice)
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: 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 --no-cache "${{ 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