food-market/.forgejo/workflows/docker.yml
nns 71b749fb35 ci: path filters + buildkit cache для ускорения сборки
Цель: типичный push должен катиться за 30-60 сек вместо 3-5 мин.

docker.yml — две оптимизации:
1. Job changes детектит что изменилось (api/web) через `git diff
   HEAD~1 HEAD`. Образ пересобирается только если затронуты его
   директории; `paths-ignore` отсекает docs/*.md/.github/**.
2. Сборка через `docker buildx build` с registry-cache:
   --cache-from / --cache-to type=registry,ref=...:buildcache,mode=max.
   Локальный 127.0.0.1:5001 уже разрешает DELETE, так что mutable
   buildcache работает. dotnet restore / pnpm install теперь почти
   мгновенные при отсутствии изменений в *.csproj / pnpm-lock.yaml.
3. deploy-stage запускается только если api или web реально
   пересобирался; для пропущенного образа в .env пишется :latest,
   compose pull тянет последний успешный slim. Telegram-сообщение
   указывает что именно деплоилось ([api web] / [web] / только compose).

ci.yml — actions/cache для NuGet (~/.nuget/packages по hash *.csproj)
и pnpm store (по hash pnpm-lock.yaml). paths-ignore такое же.

POS-job (windows-latest) не трогаем — он и так fires только на
тег v* / workflow_dispatch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:54:57 +05:00

192 lines
6.9 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

name: Docker Images
on:
push:
branches: [main]
paths-ignore:
- '**.md'
- 'docs/**'
- '.github/**'
workflow_dispatch:
env:
LOCAL_REGISTRY: 127.0.0.1:5001
jobs:
# Решает что именно изменилось в этом push'е, чтобы ниже собирать только нужные
# образы. Outputs `api` / `web` = "true"|"false". При workflow_dispatch — оба true.
changes:
name: Detect changes
runs-on: [self-hosted, linux]
outputs:
api: ${{ steps.filter.outputs.api }}
web: ${{ steps.filter.outputs.web }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- id: filter
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "api=true" >> "$GITHUB_OUTPUT"
echo "web=true" >> "$GITHUB_OUTPUT"
exit 0
fi
base=$(git rev-parse HEAD~1 2>/dev/null || git rev-parse HEAD)
changed=$(git diff --name-only "$base" HEAD)
echo "Changed files since $base:"
echo "$changed"
api=false; web=false
while IFS= read -r f; do
[ -z "$f" ] && continue
case "$f" in
src/food-market.api/*|\
src/food-market.application/*|\
src/food-market.domain/*|\
src/food-market.infrastructure/*|\
src/food-market.shared/*|\
deploy/Dockerfile.api|\
deploy/docker-compose.yml|\
.forgejo/workflows/docker.yml|\
food-market.sln)
api=true ;;
esac
case "$f" in
src/food-market.web/*|\
deploy/Dockerfile.web|\
deploy/nginx.conf|\
deploy/docker-compose.yml|\
.forgejo/workflows/docker.yml)
web=true ;;
esac
done <<< "$changed"
echo "api=$api" >> "$GITHUB_OUTPUT"
echo "web=$web" >> "$GITHUB_OUTPUT"
echo "Result: api=$api web=$web"
api:
name: API image
needs: changes
if: needs.changes.outputs.api == 'true'
runs-on: [self-hosted, linux]
steps:
- uses: actions/checkout@v4
- name: Build + push API (buildx with registry cache)
env:
SHA: ${{ github.sha }}
run: |
docker buildx create --use --name fmbuilder --driver docker-container 2>/dev/null \
|| docker buildx use fmbuilder
docker buildx build \
-f deploy/Dockerfile.api \
-t $LOCAL_REGISTRY/food-market-api:$SHA \
-t $LOCAL_REGISTRY/food-market-api:latest \
--cache-from type=registry,ref=$LOCAL_REGISTRY/food-market-api:buildcache \
--cache-to type=registry,ref=$LOCAL_REGISTRY/food-market-api:buildcache,mode=max \
--push .
web:
name: Web image
needs: changes
if: needs.changes.outputs.web == 'true'
runs-on: [self-hosted, linux]
steps:
- uses: actions/checkout@v4
- name: Build + push Web (buildx with registry cache)
env:
SHA: ${{ github.sha }}
run: |
docker buildx create --use --name fmbuilder --driver docker-container 2>/dev/null \
|| docker buildx use fmbuilder
docker buildx build \
-f deploy/Dockerfile.web \
-t $LOCAL_REGISTRY/food-market-web:$SHA \
-t $LOCAL_REGISTRY/food-market-web:latest \
--cache-from type=registry,ref=$LOCAL_REGISTRY/food-market-web:buildcache \
--cache-to type=registry,ref=$LOCAL_REGISTRY/food-market-web:buildcache,mode=max \
--push .
deploy-stage:
name: Deploy stage
needs: [changes, api, web]
# always() позволяет deploy запуститься даже если api/web был пропущен.
# Запускаем когда хотя бы один из api/web реально пересобрался ИЛИ менялся compose.
if: |
always()
&& (needs.api.result == 'success' || needs.api.result == 'skipped')
&& (needs.web.result == 'success' || needs.web.result == 'skipped')
&& (needs.changes.outputs.api == 'true' || needs.changes.outputs.web == 'true')
runs-on: [self-hosted, linux]
steps:
- uses: actions/checkout@v4
- name: Write .env + copy compose (runner = stage host)
env:
SHA: ${{ github.sha }}
PGPASS: ${{ secrets.STAGE_POSTGRES_PASSWORD }}
run: |
# Если в этом ране какой-то из образов не пересобирался, используем :latest —
# текущий compose и так указывает на 127.0.0.1:5001/food-market-{api,web}:$TAG.
api_tag="$SHA"; web_tag="$SHA"
[ "${{ needs.changes.outputs.api }}" = "true" ] || api_tag=latest
[ "${{ needs.changes.outputs.web }}" = "true" ] || web_tag=latest
cat > /home/nns/food-market-stage/deploy/.env <<ENV
REGISTRY=127.0.0.1:5001
API_TAG=$api_tag
WEB_TAG=$web_tag
POSTGRES_PASSWORD=$PGPASS
ENV
cp deploy/docker-compose.yml /home/nns/food-market-stage/deploy/docker-compose.yml
- name: docker compose pull + up
working-directory: /home/nns/food-market-stage/deploy
run: |
docker compose pull
docker compose up -d --remove-orphans
- name: Smoke /health
run: |
for i in 1 2 3 4 5 6; do
sleep 5
if curl -fsS http://127.0.0.1:8080/health | grep -q '"status":"ok"'; then
echo "Health OK"
exit 0
fi
done
echo "Health failed"
exit 1
- name: Notify Telegram on success
if: success()
env:
BOT: ${{ secrets.TELEGRAM_BOT_TOKEN }}
CHAT: ${{ secrets.TELEGRAM_CHAT_ID }}
SHA: ${{ github.sha }}
API_BUILT: ${{ needs.changes.outputs.api }}
WEB_BUILT: ${{ needs.changes.outputs.web }}
run: |
parts=()
[ "$API_BUILT" = "true" ] && parts+=("api")
[ "$WEB_BUILT" = "true" ] && parts+=("web")
built="${parts[*]}"
[ -z "$built" ] && built="(только compose)"
curl -sS -X POST "https://api.telegram.org/bot$BOT/sendMessage" \
--data-urlencode "chat_id=$CHAT" \
--data-urlencode "text=✅ stage deployed [${built}] — ${SHA:0:7} → https://food-market.zat.kz" \
> /dev/null
- name: Notify Telegram on failure
if: failure()
env:
BOT: ${{ secrets.TELEGRAM_BOT_TOKEN }}
CHAT: ${{ secrets.TELEGRAM_CHAT_ID }}
SHA: ${{ github.sha }}
run: |
curl -sS -X POST "https://api.telegram.org/bot$BOT/sendMessage" \
--data-urlencode "chat_id=$CHAT" \
--data-urlencode "text=❌ stage deploy FAILED — ${SHA:0:7}" \
> /dev/null