From 4bfcc56e7d0c0a6723bfb44f1be523f334e89e8c Mon Sep 17 00:00:00 2001 From: nns <278048682+nurdotnet@users.noreply.github.com> Date: Sat, 25 Apr 2026 12:54:57 +0500 Subject: [PATCH] =?UTF-8?q?ci:=20path=20filters=20+=20buildkit=20cache=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=83=D1=81=D0=BA=D0=BE=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Цель: типичный 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) --- .forgejo/workflows/ci.yml | 33 ++++++++- .forgejo/workflows/docker.yml | 126 +++++++++++++++++++++++++++------- 2 files changed, 133 insertions(+), 26 deletions(-) diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 3a45a12..7c78d26 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -4,8 +4,16 @@ on: push: branches: [main] tags: ['v*'] + paths-ignore: + - '**.md' + - 'docs/**' + - '.github/**' pull_request: branches: [main] + paths-ignore: + - '**.md' + - 'docs/**' + - '.github/**' workflow_dispatch: concurrency: @@ -37,6 +45,16 @@ jobs: - name: 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 run: dotnet restore food-market.sln @@ -61,6 +79,20 @@ jobs: - name: Node + 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 run: pnpm install --frozen-lockfile @@ -79,4 +111,3 @@ jobs: 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 publish src/food-market.pos/food-market.pos.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish - diff --git a/.forgejo/workflows/docker.yml b/.forgejo/workflows/docker.yml index 84c8e81..e0e939d 100644 --- a/.forgejo/workflows/docker.yml +++ b/.forgejo/workflows/docker.yml @@ -3,59 +3,123 @@ name: Docker Images on: push: branches: [main] - paths: - - 'src/food-market.api/**' - - 'src/food-market.web/**' - - 'src/food-market.application/**' - - 'src/food-market.domain/**' - - 'src/food-market.infrastructure/**' - - 'src/food-market.shared/**' - - 'deploy/**' - - '.forgejo/workflows/docker.yml' + 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 + - name: Build + push API (buildx with registry cache) env: SHA: ${{ github.sha }} 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:latest . - for tag in $SHA latest; do - docker push $LOCAL_REGISTRY/food-market-api:$tag - done + -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 + - name: Build + push Web (buildx with registry cache) env: SHA: ${{ github.sha }} 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:latest . - for tag in $SHA latest; do - docker push $LOCAL_REGISTRY/food-market-web:$tag - done + -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] - needs: [api, web] steps: - uses: actions/checkout@v4 @@ -64,10 +128,15 @@ jobs: 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 < /dev/null - name: Notify Telegram on failure