Files
pipu-music/.gitea/workflows/release.yml
T
mrleo1nidandClaude Opus 5.5 f7722e28ad
release / android (APK в релиз) (push) Successful in 3m19s
release / release (версия и заметки) (push) Successful in 8s
ci(release): rebalance APK build memory so it does not thrash and get OOM-killed
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>
2026-10-20 22:12:54 +03:00

194 lines
12 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Релиз по тегу 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 (необязательно) — токен для релиза; без него — встроенный токен job (права на запись в репозиторий)
# Переменные (Settings → Actions → Variables):
# PUBLIC_URL (необязательно) — внешний адрес Gitea для ссылок в тексте релиза; по умолчанию https://gitea.hsrv.site
# ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD — подпись APK
name: release
on:
push:
tags: ["v*.*.*"]
env:
NODE_VERSION: "22"
jobs:
# Тесты здесь не повторяются: тот же коммит уже проверен 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
# вся история и теги: заметки строятся по коммитам с прошлого релиза
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 || secrets.GITHUB_TOKEN }}
PUBLIC_URL: ${{ vars.PUBLIC_URL || 'https://gitea.hsrv.site' }}
TAG: ${{ github.ref_name }}
VERSION: ${{ steps.v.outputs.PIPU_VERSION }}
PRERELEASE: ${{ steps.v.outputs.PIPU_PRERELEASE }}
run: node scripts/release.mjs create
android:
name: android (APK в релиз)
needs: release
# Обычно 10–15 минут; дольше — значит, сборка упёрлась в память, лучше упасть, чем держать раннер.
timeout-minutes: 60
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 act запускает шаги через sh; скриптам нужен bash (массивы, pipefail)
defaults:
run:
shell: bash
container:
image: docker.gitea.com/runner-images:ubuntu-latest
# Жёсткие пределы контейнера: сборка не должна забирать всю VM раннера (а через неё — хост гипервизора).
# Своп запрещён (memory-swap = memory): лучше OOM внутри контейнера, чем подвисший хост.
options: --cpus=3 --memory=7g --memory-swap=7g
volumes:
- /opt/ci-cache/gradle:/root/.gradle
- /opt/ci-cache/android:/root/.android
env:
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 || secrets.GITHUB_TOKEN }}
PUBLIC_URL: ${{ vars.PUBLIC_URL || 'https://gitea.hsrv.site' }}
TAG: ${{ github.ref_name }}
VERSION: ${{ needs.release.outputs.version }}
VERSION_CODE: ${{ needs.release.outputs.version_code }}
steps:
- uses: actions/checkout@v4
# при перезапуске одной этой сборки статус «не удалась» сменится на «собирается»
- name: Статус в релизе — собирается
run: node scripts/release.mjs android building
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: "17" }
- uses: android-actions/setup-android@v3
with:
# пакета `tools` (значение по умолчанию) в репозитории SDK больше нет — sdkmanager падает;
# build-tools нужны шагу подписи (apksigner), версии — как у React Native 0.79
packages: platform-tools platforms;android-35 build-tools;35.0.0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: "${{ env.NODE_VERSION }}", cache: pnpm }
- run: pnpm install --frozen-lockfile
# Диск, память и наполненность постоянных кэшей: пустые ~/.gradle и ~/.android значат, что тома не подключились
- name: Ресурсы раннера и кэши
run: |
df -h / /tmp; free -m
du -sh ~/.gradle ~/.android 2>/dev/null || true
- name: Сборка APK
working-directory: apps/mobile
env:
# CI=1 — Metro без file watcher и с 2 воркерами (metro.config.js); NODE_ENV нужен expo для .env-файлов
CI: "1"
NODE_ENV: production
# CMake / ninja нативной части React Native по умолчанию берут все ядра — ограничиваем.
CMAKE_BUILD_PARALLEL_LEVEL: "2"
NINJAFLAGS: "-j2"
# JS-бандл (Metro) собирается внутри Gradle отдельным node — не даём ему разрастись.
NODE_OPTIONS: "--max-old-space-size=1536"
# Только arm64-v8a: C++-часть React Native собирается под каждую ABI, и четыре ABI
# вместе с NDK (~2.5 ГБ) не помещаются на диск раннера. arm64 — все устройства с 2017+.
# Память: на раннере 8 ГБ, и без ограничений Gradle + демон Kotlin + Metro вызывают OOM killer.
# Kotlin компилируется внутри JVM Gradle (без второго демона, поэтому heap 2.5 ГБ вместо 2), задач — не больше 2.
# -x lintVitalRelease: lint-проверки для Google Play, а APK публикуется только в Gitea (−1 мин).
# Codegen отдельным вызовом до сборки: CMake приложения читает JNI-код, который генерирует codegen
# библиотек, но плагин RN не объявляет эту зависимость, и с прогретым кэшем CMake успевал раньше codegen.
# Оба вызова идут через один демон Gradle (одинаковые jvmargs), поэтому второй не платит за старт JVM.
run: |
set -o pipefail
npx expo prebuild --platform android --clean --no-install
cd android
# nice / ionice: сборка уступает CPU и диск всему остальному на машине
gradle=(nice -n 19 ionice -c 3 ./gradlew --max-workers=2 --build-cache -PreactNativeArchitectures=arm64-v8a
-Pkotlin.compiler.execution.strategy=in-process -Dorg.gradle.jvmargs="-Xmx2048m -XX:MaxMetaspaceSize=512m")
# Без New Architecture (app.config.ts) CMake приложения не собирается — отдельный codegen не нужен.
if grep -q '^newArchEnabled=true' gradle.properties; then
"${gradle[@]}" generateCodegenArtifactsFromSchema
fi
# Постоянный кэш Gradle мог побиться (VM раннера перезагрузили посреди сборки): «Could not read workspace
# metadata» — удаляем кэш трансформаций и повторяем один раз, а не падаем до ручной чистки.
if ! "${gradle[@]}" assembleRelease -x lintVitalRelease 2>&1 | tee /tmp/gradle.log; then
if grep -qE "Could not read workspace metadata|Corrupted|is corrupt" /tmp/gradle.log; then
echo "::warning::кэш Gradle повреждён — чистим transforms и собираем заново"
./gradlew --stop || true
rm -rf ~/.gradle/caches/*/transforms ~/.gradle/caches/transforms-*
"${gradle[@]}" assembleRelease -x lintVitalRelease
else
exit 1
fi
fi
./gradlew --stop
- name: Диагностика сбоя (диск, память, OOM)
if: failure()
run: |
df -h / /tmp; free -m || true
du -sh ~/.gradle ~/.android apps/mobile/android 2>/dev/null || true
dmesg 2>/dev/null | grep -iE "killed process|out of memory" | tail -5 || true
- name: Подпись APK
env:
PRERELEASE: ${{ needs.release.outputs.prerelease }}
KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
set -euo pipefail
src=apps/mobile/android/app/build/outputs/apk/release/app-release.apk
out="pipu-music-${PIPU_VERSION}.apk"
bt="$(ls -d "$ANDROID_HOME"/build-tools/* | sort -V | tail -1)"
if [ -n "${KEYSTORE_B64}" ]; then
echo "$KEYSTORE_B64" | base64 -d > release.keystore
"$bt/apksigner" sign --ks release.keystore --ks-pass env:KEYSTORE_PASSWORD \
--ks-key-alias "$KEY_ALIAS" --key-pass env:KEY_PASSWORD --out "$out" "$src"
rm release.keystore
echo "SIGNING=release" >> "$GITHUB_ENV"
elif [ "$PRERELEASE" = "true" ]; then
echo "::warning::ANDROID_KEYSTORE_BASE64 не задан — APK подписан отладочным ключом и не обновится поверх релизного"
cp "$src" "$out"
echo "SIGNING=debug" >> "$GITHUB_ENV"
else
echo "::error::ANDROID_KEYSTORE_BASE64 не задан — релиз с отладочной подписью не выпускается (rc-теги допускают)"
exit 1
fi
"$bt/apksigner" verify --print-certs "$out" | head -3
sha256sum "$out" | cut -d' ' -f1 > "$out.sha256"
echo "APK=$out" >> "$GITHUB_ENV"
- name: APK в релиз
run: node scripts/release.mjs android success "$APK"
- name: Статус в релизе — сборка не удалась
if: failure()
run: node scripts/release.mjs android failure
# срабатывает при отмене из интерфейса; если раннер убит целиком, статус обновит следующий запуск
- name: Статус в релизе — сборка отменена
if: cancelled()
run: node scripts/release.mjs android cancelled