ci: split CI by area with path filters and speed up releases
ci / api / api (push) Successful in 54s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 54s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 53s
ci / recommender / recommender (push) Successful in 38s
ci / ts / ts (packages + web) (push) Successful in 37s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m47s
release / release (версия и заметки) (push) Successful in 7s
release / android (APK в релиз) (push) Canceled after 4m25s

- ci.yml becomes ci-api, ci-contract, ci-recommender, ci-ts, ci-mobile and
  docker workflows, each filtered by paths; docs-only commits run nothing
- docker builds all four images in one job with plain docker build on the
  runner host, so the layer cache survives between runs
- release no longer re-runs the test suite (the tagged commit already passed
  CI on main); version and release notes are one job
- the android job mounts persistent Gradle and Android SDK caches from
  /opt/ci-cache (when the runner allows the volumes) and uses --build-cache
- RELEASE_TOKEN is optional: fall back to the built-in job token
- docs: CI layout and act_runner settings (capacity, cache, valid_volumes)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-20 03:55:45 +03:00
1 parent d3b2e7bfc3
commit 718125b719
11 files changed
+358 -180

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
# Go: сервис api и storage-agent (docs/ci-cd.md). Только при изменении их кода или контракта.
name: ci / api
on:
push:
branches: [main]
paths:
- 'services/api/**'
- 'api/openapi.yaml'
- '.gitea/workflows/ci-api.yml'
pull_request:
paths:
- 'services/api/**'
- 'api/openapi.yaml'
- '.gitea/workflows/ci-api.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
api:
runs-on: ubuntu-latest
defaults: { run: { working-directory: services/api } }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: "1.24", cache-dependency-path: services/api/go.sum }
- run: test -z "$(gofmt -l .)" || (gofmt -l . && exit 1)
- run: go vet ./...
- run: go test -race -coverprofile=coverage.out ./...
- run: go build ./cmd/...
+59
View File
@@ -0,0 +1,59 @@
# Сгенерированный код (Go-сервер, TS-клиент, дизайн-токены) совпадает с исходниками (docs/ci-cd.md).
name: ci / contract
on:
push:
branches: [main]
paths:
- 'api/openapi.yaml'
- 'services/api/internal/api/gen/**'
- 'services/api/go.mod'
- 'services/api/go.sum'
- 'packages/api-client/**'
- 'packages/design-tokens/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'turbo.json'
- 'tsconfig.base.json'
- '.gitea/workflows/ci-contract.yml'
pull_request:
paths:
- 'api/openapi.yaml'
- 'services/api/internal/api/gen/**'
- 'services/api/go.mod'
- 'services/api/go.sum'
- 'packages/api-client/**'
- 'packages/design-tokens/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'turbo.json'
- 'tsconfig.base.json'
- '.gitea/workflows/ci-contract.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
contract:
name: contract (сгенерированный код совпадает с openapi.yaml)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: "1.24", cache-dependency-path: services/api/go.sum }
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: "22", cache: pnpm }
- run: pnpm install --frozen-lockfile
- name: Регенерация Go-сервера, TS-клиента и дизайн-токенов
run: |
(cd services/api && go generate ./...)
pnpm generate
- name: Проверка, что всё закоммичено
# status, а не diff: diff не видит новые неотслеживаемые файлы
run: |
changes="$(git status --porcelain -- services/api/internal/api/gen packages)"
if [ -n "$changes" ]; then echo "$changes"; git diff -- services/api/internal/api/gen packages; exit 1; fi
+43
View File
@@ -0,0 +1,43 @@
# Expo: expo-doctor и Metro-бандл Android (docs/ci-cd.md). Полная сборка APK — только в release.yml.
name: ci / mobile
on:
push:
branches: [main]
paths:
- 'apps/mobile/**'
- 'packages/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'turbo.json'
- 'tsconfig.base.json'
- '.gitea/workflows/ci-mobile.yml'
pull_request:
paths:
- 'apps/mobile/**'
- 'packages/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'turbo.json'
- 'tsconfig.base.json'
- '.gitea/workflows/ci-mobile.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
mobile:
name: mobile (Metro-бандл Android)
runs-on: ubuntu-latest
defaults: { run: { working-directory: apps/mobile } }
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: "22", cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: npx expo-doctor
- run: npx expo export --platform android --output-dir dist
+31
View File
@@ -0,0 +1,31 @@
# Python: recommender и analyzer (docs/ci-cd.md).
name: ci / recommender
on:
push:
branches: [main]
paths:
- 'services/recommender/**'
- '.gitea/workflows/ci-recommender.yml'
pull_request:
paths:
- 'services/recommender/**'
- '.gitea/workflows/ci-recommender.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
recommender:
runs-on: ubuntu-latest
defaults: { run: { working-directory: services/recommender } }
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with: { version: "0.10.2", enable-cache: true, cache-dependency-glob: services/recommender/uv.lock }
- run: uv sync --frozen
- run: uv run ruff check .
- run: uv run ruff format --check .
- run: uv run mypy pipu_rec
- run: uv run pytest -q
+47
View File
@@ -0,0 +1,47 @@
# TypeScript: пакеты, веб, typecheck мобильного приложения, скрипты релиза (docs/ci-cd.md).
name: ci / ts
on:
push:
branches: [main]
paths:
- 'apps/**'
- 'packages/**'
- 'scripts/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'turbo.json'
- 'tsconfig.base.json'
- '.gitea/workflows/ci-ts.yml'
pull_request:
paths:
- 'apps/**'
- 'packages/**'
- 'scripts/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'turbo.json'
- 'tsconfig.base.json'
- '.gitea/workflows/ci-ts.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ts:
name: ts (packages + web)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: "22", cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm i18n:check
- run: pnpm test:scripts
- run: pnpm typecheck
- run: pnpm test
- run: pnpm --filter @pipu/web build
-111
View File
@@ -1,111 +0,0 @@
# Проверки на каждый push в main и на каждый PR (docs/ci-cd.md).
name: ci
on:
push:
branches: [main]
pull_request:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
GO_VERSION: "1.24"
NODE_VERSION: "22"
jobs:
contract:
name: contract (сгенерированный код совпадает с openapi.yaml)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: "${{ env.GO_VERSION }}", cache-dependency-path: services/api/go.sum }
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: "${{ env.NODE_VERSION }}", cache: pnpm }
- run: pnpm install --frozen-lockfile
- name: Регенерация Go-сервера, TS-клиента и дизайн-токенов
run: |
(cd services/api && go generate ./...)
pnpm generate
- name: Проверка, что всё закоммичено
# status, а не diff: diff не видит новые неотслеживаемые файлы
run: |
changes="$(git status --porcelain -- services/api/internal/api/gen packages)"
if [ -n "$changes" ]; then echo "$changes"; git diff -- services/api/internal/api/gen packages; exit 1; fi
api:
runs-on: ubuntu-latest
defaults: { run: { working-directory: services/api } }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: "${{ env.GO_VERSION }}", cache-dependency-path: services/api/go.sum }
- run: test -z "$(gofmt -l .)" || (gofmt -l . && exit 1)
- run: go vet ./...
- run: go test -race -coverprofile=coverage.out ./...
- run: go build ./cmd/...
recommender:
runs-on: ubuntu-latest
defaults: { run: { working-directory: services/recommender } }
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with: { version: "0.10.2", enable-cache: true, cache-dependency-glob: services/recommender/uv.lock }
- run: uv sync --frozen
- run: uv run ruff check .
- run: uv run ruff format --check .
- run: uv run mypy pipu_rec
- run: uv run pytest -q
ts:
name: ts (packages + web)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: "${{ env.NODE_VERSION }}", cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm i18n:check
- run: pnpm test:scripts
- run: pnpm typecheck
- run: pnpm test
- run: pnpm --filter @pipu/web build
docker:
name: docker (образы собираются — их сервер собирает сам при обновлении)
runs-on: ubuntu-latest
strategy:
matrix:
include:
- { name: api, dockerfile: services/api/Dockerfile, target: api }
- { name: storage-agent, dockerfile: services/api/Dockerfile, target: storage-agent }
- { name: recommender, dockerfile: services/recommender/Dockerfile, target: "" }
- { name: web, dockerfile: apps/web/Dockerfile, target: "" }
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.dockerfile }}
target: ${{ matrix.target }}
push: false
build-args: VERSION=ci
mobile:
name: mobile (Metro-бандл Android)
runs-on: ubuntu-latest
defaults: { run: { working-directory: apps/mobile } }
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: "${{ env.NODE_VERSION }}", cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: npx expo-doctor
- run: npx expo export --platform android --output-dir dist
+61
View File
@@ -0,0 +1,61 @@
# Сборка всех Docker-образов без push: сервер собирает их сам при обновлении, и здесь ловятся поломки
# Dockerfile и зависимостей. Только при изменении Dockerfile, deploy/ и манифестов зависимостей.
name: ci / docker
on:
push:
branches: [main]
paths:
- '**/Dockerfile'
- '.dockerignore'
- 'deploy/**'
- 'services/api/go.mod'
- 'services/api/go.sum'
- 'services/recommender/pyproject.toml'
- 'services/recommender/uv.lock'
- 'apps/web/package.json'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'turbo.json'
- 'tsconfig.base.json'
- '.gitea/workflows/docker.yml'
pull_request:
paths:
- '**/Dockerfile'
- '.dockerignore'
- 'deploy/**'
- 'services/api/go.mod'
- 'services/api/go.sum'
- 'services/recommender/pyproject.toml'
- 'services/recommender/uv.lock'
- 'apps/web/package.json'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'turbo.json'
- 'tsconfig.base.json'
- '.gitea/workflows/docker.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
docker:
name: docker (образы собираются — их сервер собирает сам при обновлении)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Обычный docker build через Docker хоста раннера, а не buildx-контейнер: кэш слоёв и
# --mount=type=cache переживают job, повторная сборка занимает секунды. Фиксированные теги
# не дают копиться образам; кэш сборки чистит на хосте `docker builder prune` (docs/ci-cd.md).
- name: Сборка образов
env: { DOCKER_BUILDKIT: "1" }
run: |
set -euo pipefail
build() { echo "::group::$1"; docker build --build-arg VERSION=ci -t "pipu-ci/$1" "${@:2}" .; echo "::endgroup::"; }
build api -f services/api/Dockerfile --target api
build storage-agent -f services/api/Dockerfile --target storage-agent
build recommender -f services/recommender/Dockerfile
build web -f apps/web/Dockerfile
+28 -41
View File
@@ -1,10 +1,10 @@
# Релиз по тегу vMAJOR.MINOR.PATCH[-rc.N] (docs/ci-cd.md): проверки → Gitea Release с заметками → APK.
# Релиз создаётся сразу после проверок (статус Android — «собирается»); сборка APK докладывает в него файлы
# Релиз по тегу vMAJOR.MINOR.PATCH[-rc.N] (docs/ci-cd.md): Gitea Release с заметками → APK.
# Релиз создаётся сразу (статус Android — «собирается»); сборка APK докладывает в него файлы
# и переписывает статус: собран / не удалась (scripts/release.mjs).
# Docker-образы не публикуются: сервер собирает их сам из клонированного репозитория (deploy/update.sh).
#
# Секреты (Settings → Actions → Secrets):
# RELEASE_TOKEN — создание релиза и загрузка ассетов (scope: write:repository)
# RELEASE_TOKEN (необязательно) — токен для релиза; без него — встроенный токен job (права на запись в репозиторий)
# ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD — подпись APK
name: release
@@ -13,69 +13,56 @@ on:
tags: ["v*.*.*"]
env:
GO_VERSION: "1.24"
NODE_VERSION: "22"
jobs:
verify:
# Тесты здесь не повторяются: тот же коммит уже проверен CI в main (ci-*.yml). Ставьте тег после зелёного CI.
release:
name: release (версия и заметки)
runs-on: ubuntu-latest
outputs:
version: ${{ steps.v.outputs.PIPU_VERSION }}
version_code: ${{ steps.v.outputs.PIPU_VERSION_CODE }}
prerelease: ${{ steps.v.outputs.PIPU_PRERELEASE }}
steps:
- uses: actions/checkout@v4
- id: v
run: node scripts/version.mjs "${GITHUB_REF_NAME}" | tee -a "$GITHUB_OUTPUT"
- uses: actions/setup-go@v5
with: { go-version: "${{ env.GO_VERSION }}", cache-dependency-path: services/api/go.sum }
- uses: astral-sh/setup-uv@v6
with: { version: "0.10.2" }
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: "${{ env.NODE_VERSION }}", cache: pnpm }
- run: pnpm install --frozen-lockfile
- name: Go
working-directory: services/api
run: go vet ./... && go test ./...
- name: Python
working-directory: services/recommender
run: uv sync --frozen && uv run ruff check . && uv run mypy pipu_rec && uv run pytest -q
- name: TypeScript
run: pnpm i18n:check && pnpm test:scripts && pnpm typecheck && pnpm test
release:
name: release (заметки)
needs: verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# вся история и теги: заметки строятся по коммитам с прошлого релиза
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
with: { node-version: "${{ env.NODE_VERSION }}" }
- id: v
name: Версия из тега
run: node scripts/version.mjs "${GITHUB_REF_NAME}" | tee -a "$GITHUB_OUTPUT"
- name: Gitea Release
env:
GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
VERSION: ${{ needs.verify.outputs.version }}
PRERELEASE: ${{ needs.verify.outputs.prerelease }}
VERSION: ${{ steps.v.outputs.PIPU_VERSION }}
PRERELEASE: ${{ steps.v.outputs.PIPU_PRERELEASE }}
run: node scripts/release.mjs create
android:
name: android (APK в релиз)
needs: [verify, release]
needs: release
runs-on: ubuntu-latest
# Постоянные кэши на хосте раннера: дистрибутив и зависимости Gradle, его build cache, Android SDK и NDK
# переживают job (−4 мин на релиз). Работают, если в config.yaml раннера разрешены тома
# (container.valid_volumes: ['/opt/ci-cache/**'], docs/ci-cd.md); иначе раннер их пропускает и всё качается заново.
container:
image: docker.gitea.com/runner-images:ubuntu-latest
volumes:
- /opt/ci-cache/gradle:/root/.gradle
- /opt/ci-cache/android:/root/.android
env:
PIPU_VERSION: ${{ needs.verify.outputs.version }}
PIPU_VERSION_CODE: ${{ needs.verify.outputs.version_code }}
PIPU_VERSION: ${{ needs.release.outputs.version }}
PIPU_VERSION_CODE: ${{ needs.release.outputs.version_code }}
# для scripts/release.mjs
GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
VERSION: ${{ needs.verify.outputs.version }}
VERSION_CODE: ${{ needs.verify.outputs.version_code }}
VERSION: ${{ needs.release.outputs.version }}
VERSION_CODE: ${{ needs.release.outputs.version_code }}
steps:
- uses: actions/checkout@v4
# при перезапуске одной этой сборки статус «не удалась» сменится на «собирается»
@@ -109,7 +96,7 @@ jobs:
run: |
npx expo prebuild --platform android --clean --no-install
cd android
./gradlew --no-daemon --max-workers=2 assembleRelease -PreactNativeArchitectures=arm64-v8a -Pkotlin.compiler.execution.strategy=in-process -Dorg.gradle.jvmargs="-Xmx2560m -XX:MaxMetaspaceSize=512m"
./gradlew --no-daemon --max-workers=2 assembleRelease -PreactNativeArchitectures=arm64-v8a -Pkotlin.compiler.execution.strategy=in-process -Dorg.gradle.jvmargs="-Xmx2560m -XX:MaxMetaspaceSize=512m" --build-cache
- name: Свободное место после сбоя
if: failure()
@@ -120,7 +107,7 @@ jobs:
- name: Подпись APK
env:
PRERELEASE: ${{ needs.verify.outputs.prerelease }}
PRERELEASE: ${{ needs.release.outputs.prerelease }}
KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}