v0.7.3 crawled for 35 minutes at the 6 GB cap and was killed without a log. The container now gets 7 GB, the Gradle heap is 2 GB, the Metro node process is capped at 1.5 GB and the job times out after 60 minutes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
139 lines
11 KiB
Markdown
139 lines
11 KiB
Markdown
# CI/CD (Gitea Actions)
|
||
|
||
Workflows лежат в `.gitea/workflows/`. Синтаксис совместим с GitHub Actions; runner — существующий `act_runner`
|
||
с доступом к Docker.
|
||
|
||
Сборке APK нужно ~15 ГБ свободного места на хосте раннера (Android SDK, NDK ~2.5 ГБ, кэш Gradle,
|
||
C++-сборка React Native). APK собирается только под `arm64-v8a`. Если job падает с `No space left on device`,
|
||
освободите место на хосте раннера: `docker system prune -af` и старые кэши act в `~/.cache/act`.
|
||
|
||
## Версионирование
|
||
|
||
- SemVer, тег `vMAJOR.MINOR.PATCH` (предрелизы: `v1.2.0-rc.1` — помечаются как pre-release, мобильное их
|
||
предлагает только при включённой настройке «бета-версии»).
|
||
- Одна версия на весь продукт: образы, APK и веб собираются с одним номером.
|
||
- Android `versionCode = MAJOR·1 000 000 + MINOR·10 000 + PATCH·100 + (99 для релиза | N для -rc.N)`,
|
||
так что `1.2.0-rc.1 < 1.2.0-rc.2 < 1.2.0 < 1.2.1`; MINOR и PATCH ≤ 99. Считает `scripts/version.mjs`
|
||
(тесты — `pnpm test:scripts`).
|
||
- Changelog — `git-cliff` из Conventional Commits.
|
||
|
||
## CI — на каждый push в `main` и PR
|
||
|
||
Проверки разбиты на workflow по областям, у каждого фильтр путей (`on.push.paths`): запускается только то,
|
||
что затронуто коммитом. Коммиты только в `docs/` и `*.md` CI не запускают вовсе.
|
||
|
||
| 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/**`, манифесты зависимостей |
|
||
|
||
Общие файлы 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` (см. [настройку раннера](#настройка-раннера)).
|
||
|
||
### Кэши
|
||
|
||
| Что | Где кэшируется | Чем |
|
||
|---|---|---|
|
||
| pnpm store | все job с `pnpm install` | `setup-node` `cache: pnpm` (кэш actions раннера) |
|
||
| Go-модули и build cache | `api`, `contract` | `setup-go` |
|
||
| uv | `recommender` | `setup-uv` `enable-cache` |
|
||
| Результаты turbo (typecheck, test, build) | `ts` | `actions/cache` для `.turbo/cache` |
|
||
| `npx expo-doctor`, трансформации Metro | `mobile` | `actions/cache` для `~/.npm/_npx` и `/tmp/metro-cache` |
|
||
| Слои Docker и `--mount=type=cache` | `docker`, сборка на сервере | Docker хоста; манифесты копируются раньше кода |
|
||
| Gradle (дистрибутив, зависимости, build cache), Android SDK и NDK | `android` в `release.yml` | тома `/opt/ci-cache/*` |
|
||
| Node, Go, Java, скачанные setup-action'ами | все job | том `act-toolcache` раннера |
|
||
|
||
Кэш actions работает, только если он включён на раннере ([настройка](#настройка-раннера)); без него в логах
|
||
`Cache action is only supported on GHES…`, и всё скачивается заново.
|
||
|
||
## `release.yml` — по тегу
|
||
|
||
```yaml
|
||
on:
|
||
push:
|
||
tags: ['v*.*.*']
|
||
|
||
jobs:
|
||
release: # версия из тега (scripts/version.mjs) и сразу Gitea Release (scripts/release.mjs create)
|
||
android: # needs: release — собирает APK и докладывает его в релиз
|
||
```
|
||
|
||
Тесты в релизе не повторяются: тег ставится на коммит, который CI уже проверил в `main`. **Ставьте тег
|
||
после зелёного CI** — релиз сам этого не проверяет.
|
||
|
||
Релиз появляется в первую же минуту, не дожидаясь APK. Текст строит `scripts/release.mjs`:
|
||
|
||
- **статус Android** вверху: «⏳ собирается» со ссылкой на ход сборки → «✅ собран» (имя файла, размер,
|
||
versionCode, SHA-256; предупреждение, если подпись отладочная) или «❌ не удалась» со ссылкой на лог;
|
||
- **изменения** — коммиты (Conventional Commits) с прошлого тега по разделам «Новое», «Исправления»,
|
||
«Сборка и CI» и т. д., отдельно — несовместимые (`feat!:`); для финального релиза отсчёт от прошлого
|
||
финального, а не от последнего `-rc`; ссылка на полный diff;
|
||
- **обновление сервера** — бэкап, `./update.sh vX.Y.Z`, проверка версии; **установка APK** и проверка SHA-256.
|
||
|
||
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 обновляет текст существующего релиза, а не создаёт второй.
|
||
|
||
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
|
||
```
|
||
|
||
Сборка APK ограничена сама (`release.yml`: контейнер `--cpus=3 --memory=7g` без свопа, Gradle `-Xmx2g`, Metro `--max-old-space-size=1536`, таймаут 60 мин, CMake / ninja / Gradle
|
||
по 2 потока, `nice` / `ionice`). Если раннер — VM в гипервизоре (Proxmox), дополнительно ограничьте её там:
|
||
фиксированная RAM без ballooning, сумма RAM всех VM меньше RAM хоста, `cpulimit` ≤ половины ядер хоста,
|
||
лимит дискового I/O — иначе тяжёлая сборка может подвесить весь хост.
|
||
|
||
Без `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`). Без него используется встроенный токен job (`GITHUB_TOKEN`), у которого есть права на запись в этот репозиторий |
|
||
| `ANDROID_KEYSTORE_BASE64`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD` | Подпись APK. **Ключ один на всю жизнь приложения** — хранить резервную копию вне Gitea. Без них финальный релиз падает; `-rc` собирается с отладочной подписью. |
|
||
|
||
Переменная репозитория `PUBLIC_URL` (Settings → Actions → Variables) — внешний адрес Gitea для ссылок
|
||
в тексте релиза, по умолчанию `https://gitea.hsrv.site`. Нужна потому, что job обращается к API по внутреннему
|
||
адресу, и ссылки из ответа API ведут туда же.
|
||
|
||
## Деплой
|
||
|
||
Автодеплой не обязателен. На сервере в клоне репозитория: `deploy/update.sh [vX.Y.Z]` — checkout тега,
|
||
`docker compose build` и `up -d`; миграции применяются при старте `api` ([install-guide.md](install-guide.md)).
|
||
Опционально — job `deploy` в `release.yml` по SSH на сервер для не-pre-release тегов (с ручным approval через environment).
|