Compare commits

..
1 Commits
Author SHA1 Message Date
mrleo1nidandClaude Opus 5.5 718125b719 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
release / release (версия и заметки) (push) Successful in 7s
ci / ts / ts (packages + web) (push) Successful in 37s
release / android (APK в релиз) (push) Canceled after 4m25s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m47s
- 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>
2026-10-20 03:55:45 +03:00
11 changed files with 358 additions and 180 deletions

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 }}
+2 -2
View File
@@ -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).
+1 -1
View File
@@ -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
+54 -25
View File
@@ -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>` прикладывает 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` собирается с отладочной подписью. |
## Деплой