Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8cfd64208 | ||
|
|
0d9418df48 | ||
|
|
bf3c00ca71 | ||
|
|
633a8f79af | ||
|
|
a239650075 | ||
|
|
718125b719 |
No files matched your search
@@ -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/...
|
||||
@@ -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
|
||||
@@ -0,0 +1,51 @@
|
||||
# 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
|
||||
# npx expo-doctor качает пакет в кэш npm; Metro хранит трансформации в /tmp/metro-cache
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.npm/_npx
|
||||
/tmp/metro-cache
|
||||
key: mobile-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.sha }}
|
||||
restore-keys: mobile-${{ hashFiles('pnpm-lock.yaml') }}-
|
||||
- run: npx expo-doctor
|
||||
- run: npx expo export --platform android --output-dir dist
|
||||
@@ -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
|
||||
@@ -0,0 +1,53 @@
|
||||
# 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
|
||||
# Локальный кэш turbo: typecheck/test/build пропускаются для пакетов, чьи входы не менялись
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo/cache
|
||||
key: turbo-ts-${{ github.sha }}
|
||||
restore-keys: turbo-ts-
|
||||
- run: pnpm i18n:check
|
||||
- run: pnpm test:scripts
|
||||
- run: pnpm typecheck
|
||||
- run: pnpm test
|
||||
- run: pnpm --filter @pipu/web build
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,10 +1,12 @@
|
||||
# Релиз по тегу 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 (права на запись в репозиторий)
|
||||
# Переменные (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
|
||||
|
||||
@@ -13,69 +15,58 @@ 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 }}
|
||||
PUBLIC_URL: ${{ vars.PUBLIC_URL || 'https://gitea.hsrv.site' }}
|
||||
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 }}
|
||||
PUBLIC_URL: ${{ vars.PUBLIC_URL || 'https://gitea.hsrv.site' }}
|
||||
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
|
||||
# при перезапуске одной этой сборки статус «не удалась» сменится на «собирается»
|
||||
@@ -93,8 +84,11 @@ jobs:
|
||||
with: { node-version: "${{ env.NODE_VERSION }}", cache: pnpm }
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Свободное место
|
||||
run: df -h / /tmp
|
||||
# Диск, память и наполненность постоянных кэшей: пустые ~/.gradle и ~/.android значат, что тома не подключились
|
||||
- name: Ресурсы раннера и кэши
|
||||
run: |
|
||||
df -h / /tmp; free -m
|
||||
du -sh ~/.gradle ~/.android 2>/dev/null || true
|
||||
|
||||
- name: Сборка APK
|
||||
working-directory: apps/mobile
|
||||
@@ -106,12 +100,20 @@ jobs:
|
||||
# вместе с 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
|
||||
./gradlew --no-daemon --max-workers=2 assembleRelease -PreactNativeArchitectures=arm64-v8a -Pkotlin.compiler.execution.strategy=in-process -Dorg.gradle.jvmargs="-Xmx2560m -XX:MaxMetaspaceSize=512m"
|
||||
gradle=(./gradlew --max-workers=2 --build-cache -PreactNativeArchitectures=arm64-v8a
|
||||
-Pkotlin.compiler.execution.strategy=in-process -Dorg.gradle.jvmargs="-Xmx2560m -XX:MaxMetaspaceSize=512m")
|
||||
"${gradle[@]}" generateCodegenArtifactsFromSchema
|
||||
"${gradle[@]}" assembleRelease -x lintVitalRelease
|
||||
./gradlew --stop
|
||||
|
||||
- name: Свободное место после сбоя
|
||||
- name: Диагностика сбоя (диск, память, OOM)
|
||||
if: failure()
|
||||
run: |
|
||||
df -h / /tmp; free -m || true
|
||||
@@ -120,7 +122,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 }}
|
||||
@@ -154,3 +156,8 @@ jobs:
|
||||
- name: Статус в релизе — сборка не удалась
|
||||
if: failure()
|
||||
run: node scripts/release.mjs android failure
|
||||
|
||||
# срабатывает при отмене из интерфейса; если раннер убит целиком, статус обновит следующий запуск
|
||||
- name: Статус в релизе — сборка отменена
|
||||
if: cancelled()
|
||||
run: node scripts/release.mjs android cancelled
|
||||
@@ -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).
|
||||
+10
-1
@@ -5,9 +5,18 @@
|
||||
FROM node:22-alpine AS build
|
||||
RUN npm install -g pnpm@11.9.0
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
# Сначала только манифесты workspace: слой с установкой зависимостей пересобирается лишь при их изменении,
|
||||
# а не при любой правке кода. Новый пакет в apps/ или packages/ — добавить его package.json сюда.
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY apps/web/package.json apps/web/
|
||||
COPY apps/mobile/package.json apps/mobile/
|
||||
COPY packages/api-client/package.json packages/api-client/
|
||||
COPY packages/core/package.json packages/core/
|
||||
COPY packages/design-tokens/package.json packages/design-tokens/
|
||||
COPY packages/i18n/package.json packages/i18n/
|
||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||
pnpm install --frozen-lockfile --filter @pipu/web...
|
||||
COPY . .
|
||||
# Сгенерированные клиенты и токены лежат в репозитории (их актуальность проверяет CI), поэтому generate здесь не нужен.
|
||||
RUN pnpm --filter @pipu/web build
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+74
-25
@@ -17,28 +17,43 @@ 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` (см. [настройку раннера](#настройка-раннера)).
|
||||
|
||||
### Кэши
|
||||
|
||||
| Что | Где кэшируется | Чем |
|
||||
|---|---|---|
|
||||
| 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` — по тегу
|
||||
|
||||
@@ -48,12 +63,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 +79,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,13 +87,45 @@ 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` собирается с отладочной подписью. |
|
||||
|
||||
Переменная репозитория `PUBLIC_URL` (Settings → Actions → Variables) — внешний адрес Gitea для ссылок
|
||||
в тексте релиза, по умолчанию `https://gitea.hsrv.site`. Нужна потому, что job обращается к API по внутреннему
|
||||
адресу, и ссылки из ответа API ведут туда же.
|
||||
|
||||
## Деплой
|
||||
|
||||
Автодеплой не обязателен. На сервере в клоне репозитория: `deploy/update.sh [vX.Y.Z]` — checkout тега,
|
||||
|
||||
+16
-5
@@ -2,10 +2,12 @@
|
||||
// Релиз в Gitea (docs/ci-cd.md#релиз): сначала создаётся релиз с заметками, потом сборка Android
|
||||
// докладывает в него APK и переписывает блок статуса в тексте.
|
||||
// node scripts/release.mjs create — создать релиз по тегу (при перезапуске — обновить)
|
||||
// node scripts/release.mjs android building|failure — статус сборки APK в тексте релиза
|
||||
// node scripts/release.mjs android building|failure|cancelled — статус сборки APK в тексте релиза
|
||||
// node scripts/release.mjs android success <file.apk> — приложить APK и .sha256, статус «собран»
|
||||
// Окружение: GITEA_API (<server>/api/v1/repos/<owner>/<repo>), GITEA_TOKEN, TAG, VERSION, VERSION_CODE,
|
||||
// PRERELEASE (true|false), GITHUB_RUN_ID; для success — SIGNING (release|debug).
|
||||
// PUBLIC_URL — внешний адрес Gitea для ссылок в тексте (job ходит в API по внутреннему адресу, и html_url
|
||||
// из ответа указывает на него же).
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
@@ -57,7 +59,12 @@ export function renderAndroidStatus({ status, runUrl, versionCode, apk, signing
|
||||
? ['⚠️ Подписан отладочным ключом: не установится поверх версии с релизной подписью.']
|
||||
: []),
|
||||
]
|
||||
: status === 'failure'
|
||||
: status === 'cancelled'
|
||||
? [
|
||||
`⏹ **Android:** сборка APK отменена — [лог сборки](${runUrl}).`,
|
||||
'APK появится здесь после перезапуска сборки.',
|
||||
]
|
||||
: status === 'failure'
|
||||
? [
|
||||
`❌ **Android:** сборка APK не удалась — [лог сборки](${runUrl}).`,
|
||||
'Серверная часть релиза от этого не зависит; APK появится здесь после перезапуска сборки.',
|
||||
@@ -182,9 +189,11 @@ function commitsBetween(prevTag, tag) {
|
||||
|
||||
async function context() {
|
||||
const repo = await api('GET', '');
|
||||
const publicUrl = process.env.PUBLIC_URL?.replace(/\/+$/, '');
|
||||
const repoUrl = publicUrl ? `${publicUrl}/${repo.full_name}` : repo.html_url;
|
||||
const tag = env('TAG');
|
||||
const release = await api('GET', `/releases/tags/${encodeURIComponent(tag)}`);
|
||||
return { repoUrl: repo.html_url, tag, release, runUrl: `${repo.html_url}/actions/runs/${env('GITHUB_RUN_ID')}` };
|
||||
return { repoUrl, tag, release, runUrl: `${repoUrl}/actions/runs/${env('GITHUB_RUN_ID')}` };
|
||||
}
|
||||
|
||||
async function create() {
|
||||
@@ -242,8 +251,10 @@ async function android(status, apkPath) {
|
||||
async function main() {
|
||||
const [cmd, ...args] = process.argv.slice(2);
|
||||
if (cmd === 'create') return create();
|
||||
if (cmd === 'android' && ['building', 'success', 'failure'].includes(args[0])) return android(args[0], args[1]);
|
||||
throw new Error('использование: release.mjs create | android building|failure | android success <file.apk>');
|
||||
if (cmd === 'android' && ['building', 'success', 'failure', 'cancelled'].includes(args[0])) {
|
||||
return android(args[0], args[1]);
|
||||
}
|
||||
throw new Error('использование: release.mjs create | android building|failure|cancelled | android success <file.apk>');
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
||||
|
||||
@@ -59,6 +59,7 @@ test('android status blocks', () => {
|
||||
assert.match(debug, /отладочным ключом/);
|
||||
|
||||
assert.match(renderAndroidStatus({ status: 'failure', runUrl: 'https://run' }), /не удалась — \[лог сборки\]/);
|
||||
assert.match(renderAndroidStatus({ status: 'cancelled', runUrl: 'https://run' }), /отменена — \[лог сборки\]/);
|
||||
});
|
||||
|
||||
test('replaces only the status block and is idempotent', () => {
|
||||
|
||||
Reference in new issue
Block a user