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>
This commit is contained in:
nns 2026-04-25 12:54:57 +05:00
parent 87271281b0
commit 71b749fb35
2 changed files with 133 additions and 26 deletions

View file

@ -4,8 +4,16 @@ on:
push: push:
branches: [main] branches: [main]
tags: ['v*'] tags: ['v*']
paths-ignore:
- '**.md'
- 'docs/**'
- '.github/**'
pull_request: pull_request:
branches: [main] branches: [main]
paths-ignore:
- '**.md'
- 'docs/**'
- '.github/**'
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
@ -37,6 +45,16 @@ jobs:
- name: Dotnet version - name: Dotnet version
run: dotnet --version run: dotnet --version
# Кэшируем NuGet-пакеты по hash *.csproj — restore становится мгновенным,
# если зависимости не менялись.
- name: Cache NuGet
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj', 'food-market.sln') }}
restore-keys: |
nuget-${{ runner.os }}-
- name: Restore - name: Restore
run: dotnet restore food-market.sln run: dotnet restore food-market.sln
@ -61,6 +79,20 @@ jobs:
- name: Node + pnpm version - name: Node + pnpm version
run: node --version && pnpm --version run: node --version && pnpm --version
# Кэшируем pnpm store по hash pnpm-lock.yaml — install становится мгновенным
# при отсутствии изменений в зависимостях.
- name: Resolve pnpm store path
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-${{ runner.os }}-${{ hashFiles('src/food-market.web/pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-
- name: Install - name: Install
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@ -79,4 +111,3 @@ jobs:
dotnet restore src/food-market.pos/food-market.pos.csproj dotnet restore src/food-market.pos/food-market.pos.csproj
dotnet build src/food-market.pos/food-market.pos.csproj --no-restore -c Release dotnet build src/food-market.pos/food-market.pos.csproj --no-restore -c Release
dotnet publish src/food-market.pos/food-market.pos.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish dotnet publish src/food-market.pos/food-market.pos.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish

View file

@ -3,59 +3,123 @@ name: Docker Images
on: on:
push: push:
branches: [main] branches: [main]
paths: paths-ignore:
- 'src/food-market.api/**' - '**.md'
- 'src/food-market.web/**' - 'docs/**'
- 'src/food-market.application/**' - '.github/**'
- 'src/food-market.domain/**'
- 'src/food-market.infrastructure/**'
- 'src/food-market.shared/**'
- 'deploy/**'
- '.forgejo/workflows/docker.yml'
workflow_dispatch: workflow_dispatch:
env: env:
LOCAL_REGISTRY: 127.0.0.1:5001 LOCAL_REGISTRY: 127.0.0.1:5001
jobs: 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: api:
name: API image name: API image
needs: changes
if: needs.changes.outputs.api == 'true'
runs-on: [self-hosted, linux] runs-on: [self-hosted, linux]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Build + push api - name: Build + push API (buildx with registry cache)
env: env:
SHA: ${{ github.sha }} SHA: ${{ github.sha }}
run: | run: |
docker build -f deploy/Dockerfile.api \ 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:$SHA \
-t $LOCAL_REGISTRY/food-market-api:latest . -t $LOCAL_REGISTRY/food-market-api:latest \
for tag in $SHA latest; do --cache-from type=registry,ref=$LOCAL_REGISTRY/food-market-api:buildcache \
docker push $LOCAL_REGISTRY/food-market-api:$tag --cache-to type=registry,ref=$LOCAL_REGISTRY/food-market-api:buildcache,mode=max \
done --push .
web: web:
name: Web image name: Web image
needs: changes
if: needs.changes.outputs.web == 'true'
runs-on: [self-hosted, linux] runs-on: [self-hosted, linux]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Build + push web - name: Build + push Web (buildx with registry cache)
env: env:
SHA: ${{ github.sha }} SHA: ${{ github.sha }}
run: | run: |
docker build -f deploy/Dockerfile.web \ 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:$SHA \
-t $LOCAL_REGISTRY/food-market-web:latest . -t $LOCAL_REGISTRY/food-market-web:latest \
for tag in $SHA latest; do --cache-from type=registry,ref=$LOCAL_REGISTRY/food-market-web:buildcache \
docker push $LOCAL_REGISTRY/food-market-web:$tag --cache-to type=registry,ref=$LOCAL_REGISTRY/food-market-web:buildcache,mode=max \
done --push .
deploy-stage: deploy-stage:
name: 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] runs-on: [self-hosted, linux]
needs: [api, web]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -64,10 +128,15 @@ jobs:
SHA: ${{ github.sha }} SHA: ${{ github.sha }}
PGPASS: ${{ secrets.STAGE_POSTGRES_PASSWORD }} PGPASS: ${{ secrets.STAGE_POSTGRES_PASSWORD }}
run: | 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 cat > /home/nns/food-market-stage/deploy/.env <<ENV
REGISTRY=127.0.0.1:5001 REGISTRY=127.0.0.1:5001
API_TAG=$SHA API_TAG=$api_tag
WEB_TAG=$SHA WEB_TAG=$web_tag
POSTGRES_PASSWORD=$PGPASS POSTGRES_PASSWORD=$PGPASS
ENV ENV
cp deploy/docker-compose.yml /home/nns/food-market-stage/deploy/docker-compose.yml cp deploy/docker-compose.yml /home/nns/food-market-stage/deploy/docker-compose.yml
@ -96,10 +165,17 @@ jobs:
BOT: ${{ secrets.TELEGRAM_BOT_TOKEN }} BOT: ${{ secrets.TELEGRAM_BOT_TOKEN }}
CHAT: ${{ secrets.TELEGRAM_CHAT_ID }} CHAT: ${{ secrets.TELEGRAM_CHAT_ID }}
SHA: ${{ github.sha }} SHA: ${{ github.sha }}
API_BUILT: ${{ needs.changes.outputs.api }}
WEB_BUILT: ${{ needs.changes.outputs.web }}
run: | 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" \ curl -sS -X POST "https://api.telegram.org/bot$BOT/sendMessage" \
--data-urlencode "chat_id=$CHAT" \ --data-urlencode "chat_id=$CHAT" \
--data-urlencode "text=✅ stage deployed — ${SHA:0:7} → https://food-market.zat.kz" \ --data-urlencode "text=✅ stage deployed [${built}] — ${SHA:0:7} → https://food-market.zat.kz" \
> /dev/null > /dev/null
- name: Notify Telegram on failure - name: Notify Telegram on failure