From 718125b7193c216593ca893ede6b66b3b207180f Mon Sep 17 00:00:00 2001 From: mrleo1nid <43583385+mrleo1nid@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:55:45 +0300 Subject: [PATCH] ci: split CI by area with path filters and speed up releases - 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 --- .gitea/workflows/ci-api.yml | 32 ++++++++ .gitea/workflows/ci-contract.yml | 59 +++++++++++++++ .gitea/workflows/ci-mobile.yml | 43 +++++++++++ .gitea/workflows/ci-recommender.yml | 31 ++++++++ .gitea/workflows/ci-ts.yml | 47 ++++++++++++ .gitea/workflows/ci.yml | 111 ---------------------------- .gitea/workflows/docker.yml | 61 +++++++++++++++ .gitea/workflows/release.yml | 69 +++++++---------- README.md | 4 +- docs/architecture.md | 2 +- docs/ci-cd.md | 79 +++++++++++++------- 11 files changed, 358 insertions(+), 180 deletions(-) create mode 100644 .gitea/workflows/ci-api.yml create mode 100644 .gitea/workflows/ci-contract.yml create mode 100644 .gitea/workflows/ci-mobile.yml create mode 100644 .gitea/workflows/ci-recommender.yml create mode 100644 .gitea/workflows/ci-ts.yml delete mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitea/workflows/docker.yml diff --git a/.gitea/workflows/ci-api.yml b/.gitea/workflows/ci-api.yml new file mode 100644 index 0000000..d2cb77e --- /dev/null +++ b/.gitea/workflows/ci-api.yml @@ -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/... diff --git a/.gitea/workflows/ci-contract.yml b/.gitea/workflows/ci-contract.yml new file mode 100644 index 0000000..163e96c --- /dev/null +++ b/.gitea/workflows/ci-contract.yml @@ -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 diff --git a/.gitea/workflows/ci-mobile.yml b/.gitea/workflows/ci-mobile.yml new file mode 100644 index 0000000..e8ae0ea --- /dev/null +++ b/.gitea/workflows/ci-mobile.yml @@ -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 diff --git a/.gitea/workflows/ci-recommender.yml b/.gitea/workflows/ci-recommender.yml new file mode 100644 index 0000000..41dcd6b --- /dev/null +++ b/.gitea/workflows/ci-recommender.yml @@ -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 diff --git a/.gitea/workflows/ci-ts.yml b/.gitea/workflows/ci-ts.yml new file mode 100644 index 0000000..a792ceb --- /dev/null +++ b/.gitea/workflows/ci-ts.yml @@ -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 diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml deleted file mode 100644 index 863206d..0000000 --- a/.gitea/workflows/ci.yml +++ /dev/null @@ -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 diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml new file mode 100644 index 0000000..4d1f35b --- /dev/null +++ b/.gitea/workflows/docker.yml @@ -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 diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 9d1ca00..a7111e5 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -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 }} diff --git a/README.md b/README.md index d44f2c4..f0cb0ef 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Self-hosted музыкальный сервис: веб и Android-прилож | `packages/*` | Общий TS-код: `api-client`, `core`, `i18n`, `design-tokens` | | `deploy/` | Docker Compose, nginx, скрипты установки и обновления | | `tests/fixtures/music` | Генератор тестовой библиотеки (нужен ffmpeg) | -| `.gitea/workflows` | CI (`ci.yml`) и релиз по тегу (`release.yml`) | +| `.gitea/workflows` | CI по областям (`ci-*.yml`, `docker.yml`) и релиз по тегу (`release.yml`) | ## Разработка @@ -65,6 +65,6 @@ pnpm generate && (cd services/api && go generate ./...) ## Релиз -Тег `vX.Y.Z` (или `vX.Y.Z-rc.N`) запускает `.gitea/workflows/release.yml`: проверки → подписанный APK → Gitea Release. +Тег `vX.Y.Z` (или `vX.Y.Z-rc.N`) запускает `.gitea/workflows/release.yml`: Gitea Release с заметками → подписанный APK докладывается в него. Docker-образы не публикуются: сервер клонирует репозиторий и собирает их сам (`deploy/update.sh`, [docs/install-guide.md](docs/install-guide.md)). Нужные секреты — в шапке workflow и в [docs/ci-cd.md](docs/ci-cd.md). diff --git a/docs/architecture.md b/docs/architecture.md index 9af46c2..6494b99 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -114,7 +114,7 @@ pipu-music/ ├── tests/ │ ├── e2e/ # Playwright │ └── fixtures/music/ # генерируемые тестовые аудиофайлы -├── .gitea/workflows/ # ci.yml, release.yml +├── .gitea/workflows/ # ci-*.yml, docker.yml (по областям), release.yml ├── docs/ ├── turbo.json pnpm-workspace.yaml package.json └── README.md diff --git a/docs/ci-cd.md b/docs/ci-cd.md index 35d10d4..9b124b6 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -17,28 +17,27 @@ C++-сборка React Native). APK собирается только под `ar (тесты — `pnpm test:scripts`). - Changelog — `git-cliff` из Conventional Commits. -## `ci.yml` — на каждый push и PR +## CI — на каждый push в `main` и PR -```yaml -on: - push: { branches: [main] } - pull_request: +Проверки разбиты на workflow по областям, у каждого фильтр путей (`on.push.paths`): запускается только то, +что затронуто коммитом. Коммиты только в `docs/` и `*.md` CI не запускают вовсе. -jobs: - changes: # фильтр путей: api / recommender / web / mobile / packages / docs - contract: # spectral lint openapi.yaml; регенерация Go + TS клиентов; git diff --exit-code - api: # go vet, golangci-lint, go test -race ./... (testcontainers), покрытие - recommender: # uv sync; ruff; mypy --strict; pytest (вкл. оффлайн-оценку качества) - ts: # pnpm install --frozen-lockfile; turbo lint typecheck test; i18n:check - web-build: # vite build, проверка размера бандла - docker: # сборка всех Docker-образов без push (сервер собирает их сам — ADR-013) - mobile-check: # tsc, jest, expo prebuild + ./gradlew assembleDebug (только если менялся mobile/) - e2e: # needs: api, web-build; docker compose -f deploy/compose.test.yaml up; playwright test -``` +| Workflow | Job | Что делает | Когда | +|---|---|---|---| +| `ci-api.yml` | `api` | gofmt, go vet, `go test -race`, сборка | `services/api/**`, `api/openapi.yaml` | +| `ci-contract.yml` | `contract` | регенерация Go-сервера, TS-клиента и токенов; всё ли закоммичено | `openapi.yaml`, `gen/`, `packages/api-client`, `packages/design-tokens`, общие файлы JS | +| `ci-recommender.yml` | `recommender` | ruff, mypy, pytest | `services/recommender/**` | +| `ci-ts.yml` | `ts` | i18n, тесты скриптов, typecheck (вкл. mobile), vitest, сборка веба | `apps/**`, `packages/**`, `scripts/**`, общие файлы JS | +| `ci-mobile.yml` | `mobile` | `expo-doctor`, Metro-бандл Android | `apps/mobile/**`, `packages/**`, общие файлы JS | +| `docker.yml` | `docker` | сборка всех 4 образов без push | `Dockerfile`, `.dockerignore`, `deploy/**`, манифесты зависимостей | -- Кэши: Go modules / build cache, pnpm store, uv cache, Gradle, сгенерированные тестовые аудио. -- Ветка `main` защищена: обязательны `contract`, `api`, `recommender`, `ts`, `e2e`. -- Ночью (`schedule`) — Maestro E2E на эмуляторе и полный прогон без фильтров путей. +Общие файлы JS: `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `turbo.json`, `tsconfig.base.json`. +Изменение самого workflow запускает его. У каждого workflow своя группа `concurrency`: новый push в ту же +ветку отменяет устаревший прогон. + +`docker` собирает образы обычным `docker build` через Docker хоста раннера, а не в buildx-контейнере: +кэш слоёв переживает job, и повторная сборка занимает секунды. Кэш растёт — на хосте раннера раз в сутки: +`docker builder prune -af --filter until=72h` (см. [настройку раннера](#настройка-раннера)). ## `release.yml` — по тегу @@ -48,12 +47,14 @@ on: tags: ['v*.*.*'] jobs: - verify: # все проверки ci.yml - release: # needs: verify — сразу создаёт Gitea Release (scripts/release.mjs create) - android: # needs: verify, release — собирает APK и докладывает его в релиз + release: # версия из тега (scripts/version.mjs) и сразу Gitea Release (scripts/release.mjs create) + android: # needs: release — собирает APK и докладывает его в релиз ``` -Релиз появляется сразу после проверок, не дожидаясь APK. Текст строит `scripts/release.mjs`: +Тесты в релизе не повторяются: тег ставится на коммит, который CI уже проверил в `main`. **Ставьте тег +после зелёного CI** — релиз сам этого не проверяет. + +Релиз появляется в первую же минуту, не дожидаясь APK. Текст строит `scripts/release.mjs`: - **статус Android** вверху: «⏳ собирается» со ссылкой на ход сборки → «✅ собран» (имя файла, размер, versionCode, SHA-256; предупреждение, если подпись отладочная) или «❌ не удалась» со ссылкой на лог; @@ -62,7 +63,7 @@ jobs: финального, а не от последнего `-rc`; ссылка на полный diff; - **обновление сервера** — бэкап, `./update.sh vX.Y.Z`, проверка версии; **установка APK** и проверка SHA-256. -Job `android`: Android SDK, `expo prebuild`, `./gradlew assembleRelease` (только arm64-v8a), подпись `apksigner`, +Job `android`: Android SDK, `expo prebuild`, `./gradlew assembleRelease --build-cache` (только arm64-v8a), подпись `apksigner`, затем `release.mjs android success ` прикладывает APK и `.sha256` и меняет статус. При любом сбое — `release.mjs android failure`. Перезапуск job безопасен: статус снова «собирается», файлы заменяются. Перезапуск всего workflow обновляет текст существующего релиза, а не создаёт второй. @@ -70,11 +71,39 @@ Job `android`: Android SDK, `expo prebuild`, `./gradlew assembleRelease` (тол Docker-образы **не публикуются**: сервер собирает их сам из клонированного репозитория (ADR-013). Чтобы сборка на сервере не ломалась, job `docker` в `ci.yml` собирает все образы без push на каждый PR. +## Настройка раннера + +Раннер — `act_runner` на отдельной VM. Что ускоряет CI/CD (`config.yaml` раннера, затем перезапуск сервиса): + +```yaml +runner: + capacity: 2 # два job параллельно; только если у VM ≥ 8 ГБ RAM (иначе APK-сборка упирается в OOM) +cache: + enabled: true # кэш actions/cache: pnpm store, Go-модули, uv + host: 10.10.1.x # IP хоста раннера, доступный из job-контейнеров (не 127.0.0.1) + port: 8088 # фиксированный порт; открыть его для docker-сетей +container: + valid_volumes: + - /opt/ci-cache/** # постоянные кэши Gradle и Android SDK для job android в release.yml +``` + +Без `valid_volumes` раннер пропускает тома из `release.yml`, и каждый релиз заново качает Gradle +и NDK (~4 мин). Признак выключенного кэша actions в логах — `Cache action is only supported on GHES…`. + +Обслуживание хоста раннера (cron, раз в сутки): + +``` +0 4 * * * docker image prune -af --filter "until=72h"; docker builder prune -af --filter "until=72h" +``` + +Кэши в `/opt/ci-cache` занимают ~5–6 ГБ (Gradle, SDK, NDK); если место кончается — их можно удалить целиком, +следующий релиз скачает всё заново. + ## Секреты | Секрет | Назначение | |---|---| -| `RELEASE_TOKEN` | Создание и редактирование релиза, загрузка ассетов (scope `write:repository`) | +| `RELEASE_TOKEN` | Необязательно. Создание и редактирование релиза, загрузка ассетов (scope `write:repository`). Без него используется встроенный токен job (`GITHUB_TOKEN`), у которого есть права на запись в этот репозиторий | | `ANDROID_KEYSTORE_BASE64`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD` | Подпись APK. **Ключ один на всю жизнь приложения** — хранить резервную копию вне Gitea. Без них финальный релиз падает; `-rc` собирается с отладочной подписью. | ## Деплой