Files
pipu-music/.gitea/workflows/release.yml
T
mrleo1nidandClaude Opus 5.5 4fb4e124a0
release / android (APK в релиз) (push) Failing after 2m4s
release / release (версия и заметки) (push) Successful in 9s
ci(release): cap the Android build so it cannot freeze the runner host
The APK job container now runs with --cpus=3 --memory=6g and no swap;
CMake / ninja for the React Native native code are limited to 2 jobs
(they used every core), and Gradle runs under nice / ionice. Notes for
hypervisor-hosted runners (Proxmox) are in docs/ci-cd.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:59:46 +03:00

178 lines
11 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
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=6g --memory-swap=6g
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"
# Только 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: |
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="-Xmx2560m -XX:MaxMetaspaceSize=512m")
# Без New Architecture (app.config.ts) CMake приложения не собирается — отдельный codegen не нужен.
if grep -q '^newArchEnabled=true' gradle.properties; then
"${gradle[@]}" generateCodegenArtifactsFromSchema
fi
"${gradle[@]}" assembleRelease -x lintVitalRelease
./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