ci: registry-based releases — multi-arch images built in CI, servers only pull

- release workflow: buildx multi-arch (amd64+arm64) build+push of bot and admin images to Gitea Container Registry (git.softuniq.eu/telegram-market/telegram-shop[-admin])
- docker-compose: bot/admin use registry images (IMAGE_TAG for pinning); tor-proxy stays local alpine build
- install.sh: pull from registry instead of local docker build (no native compilation on weak hardware); interactive .env setup preserved
- new buildx runner gitea-host-buildx (label buildx) on x86_64 host
- REGISTRY_TOKEN replaced with package-scoped token (write:package)
- README/VERSION/.env.example updated
This commit is contained in:
NW
2026-08-11 10:37:09 +01:00
parent 33ec420fc0
commit 5c7bc242c5
6 changed files with 272 additions and 85 deletions

View File

@@ -24,6 +24,7 @@ COMMISSION_ENABLED=true
COMMISSION_PERCENT=5
# --- Commission Wallets ---
# Начальные адреса комиссионных кошельков. Редактируются в админке (Кошельки → Edit Wallets), значения в БД имеют приоритет.
COMMISSION_WALLET_BTC=
COMMISSION_WALLET_LTC=
COMMISSION_WALLET_USDT=
@@ -70,3 +71,7 @@ DEFAULT_LANGUAGE=en
# OpenAI-совместимый endpoint и ключ (используются, если не заданы в site_settings)
CHATBOT_API_ENDPOINT=https://api.openai.com/v1
CHATBOT_API_KEY=
# --- Deploy ---
# Версия образов из Gitea Container Registry (по умолчанию latest)
IMAGE_TAG=latest

View File

@@ -1,70 +1,96 @@
name: "Release: ARM64 Docker Image"
name: "Release: multi-arch Docker images"
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
version:
description: "Version tag (without v prefix)"
required: false
default: ""
jobs:
build-arm64:
# Нативный ARM64 runner на Orange Pi Zero 2 (aarch64).
# GitHub недоступен в этой сети — НЕ используем actions/*, только чистые run: шаги.
# node:22-alpine не содержит bash → используем sh
runs-on: arm64
build-push:
# Buildx runner на x86_64 хосте (docker:27.5-cli + host docker.sock).
# Собирает linux/amd64 + linux/arm64 одним buildx build (qemu из коробки)
# и пушит оба образа (бот + Next.js админка) в Gitea Container Registry.
runs-on: buildx
defaults:
run:
shell: sh
env:
REGISTRY: git.softuniq.eu
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
VERSION: ""
steps:
- name: Checkout code
run: |
set -e
apk add --no-cache git >/dev/null 2>&1 || true
apk add --no-cache git curl >/dev/null 2>&1 || true
if [ -n "${GITHUB_REF_NAME}" ] && [ "${GITHUB_REF_NAME}" != "main" ]; then
# Запуск по тегу: clone конкретного тега
REF=${GITHUB_REF_NAME}
elif [ -n "${{ github.event.inputs.version }}" ]; then
REF=v${{ github.event.inputs.version }}
else
REF=${GITHUB_REF_NAME}
fi
echo "REF=${REF}"
cd /tmp
rm -rf release-src
git clone --depth 1 --branch "${GITHUB_REF_NAME}" \
"https://oauth2:${REGISTRY_TOKEN}@git.softuniq.eu/Telegram-Market/telegram-shop.git" release-src
git clone --depth 1 --branch "${REF}" \
"https://oauth2:${REGISTRY_TOKEN}@${REGISTRY}/Telegram-Market/telegram-shop.git" release-src
cd release-src
git rev-parse --short HEAD
- name: Extract version
run: |
VERSION=${GITHUB_REF_NAME#v}
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
echo "Build version: ${VERSION}"
if [ -n "${{ github.event.inputs.version }}" ]; then
V=${{ github.event.inputs.version }}
else
V=${GITHUB_REF_NAME#v}
fi
echo "VERSION=${V}" >> "$GITHUB_ENV"
echo "Build version: ${V}"
- name: Build ARM64 image
- name: Login to Gitea Container Registry
run: |
apk add --no-cache docker-cli >/dev/null 2>&1 || true
echo "${REGISTRY_TOKEN}" | docker login ${REGISTRY} -u oauth2 --password-stdin
- name: Build and push bot image (amd64 + arm64)
working-directory: /tmp/release-src
run: |
set -e
# docker-cli не входит в node:22-alpine — ставим через apk
apk add --no-cache docker-cli >/dev/null 2>&1
docker build \
docker buildx build \
--platform linux/amd64,linux/arm64 \
--file ./Dockerfile \
--tag git.softuniq.eu/telegram-market/telegram-shop:${VERSION} \
--tag git.softuniq.eu/telegram-market/telegram-shop:latest \
--tag ${REGISTRY}/telegram-market/telegram-shop:${VERSION} \
--tag ${REGISTRY}/telegram-market/telegram-shop:latest \
--push \
.
- name: Push to Gitea Container Registry (best effort)
- name: Build and push admin image (amd64 + arm64)
working-directory: /tmp/release-src
run: |
set -e
apk add --no-cache docker-cli >/dev/null 2>&1 || true
echo "${REGISTRY_TOKEN}" | docker login git.softuniq.eu -u oauth2 --password-stdin \
|| { echo "registry login failed, skipping push"; exit 0; }
docker push git.softuniq.eu/telegram-market/telegram-shop:${VERSION} \
|| echo "push tag failed (may need package scope)"
docker push git.softuniq.eu/telegram-market/telegram-shop:latest \
|| echo "push latest failed (may need package scope)"
docker logout git.softuniq.eu || true
docker buildx build \
--platform linux/amd64,linux/arm64 \
--file ./admin-next/Dockerfile \
--tag ${REGISTRY}/telegram-market/telegram-shop-admin:${VERSION} \
--tag ${REGISTRY}/telegram-market/telegram-shop-admin:latest \
--push \
./admin-next
- name: Save image tarball artifact
working-directory: /tmp/release-src
- name: Verify pushed manifests
run: |
set -e
apk add --no-cache docker-cli >/dev/null 2>&1 || true
mkdir -p /tmp/release-artifacts
docker save git.softuniq.eu/telegram-market/telegram-shop:${VERSION} \
-o /tmp/release-artifacts/telegram-shop-arm64-${VERSION}.tar
ls -lah /tmp/release-artifacts/
echo "${REGISTRY_TOKEN}" | docker login ${REGISTRY} -u oauth2 --password-stdin >/dev/null 2>&1 || true
docker buildx imagetools inspect ${REGISTRY}/telegram-market/telegram-shop:${VERSION} \
| grep -E "Platform|Digest" | head -4
docker buildx imagetools inspect ${REGISTRY}/telegram-market/telegram-shop-admin:${VERSION} \
| grep -E "Platform|Digest" | head -4
docker logout ${REGISTRY} || true

View File

@@ -32,9 +32,14 @@ bash install.sh
1. Определит архитектуру (x86_64 / ARM64 / ARMv7)
2. Установит Docker если не установлен
3. Создаст `.env` из шаблона
4. Проверит обязательные переменные
5. Соберёт Docker-образ под текущую архитектуру
6. Запустит контейнер и проверит health-check
4. Интерактивно запросит недостающие переменные (BOT_TOKEN, ADMIN_IDS) и сгенерирует ключи (ENCRYPTION_KEY, ADMIN_SECRET, SUPER_ADMIN_SECRET) если они не заданы
5. Установит SHOP_ACTIVATED=false для нового магазина (стартует заблокированным)
6. Автоматически определит LAN IP и установит ADMIN_URL
7. Соберёт Docker-образ под текущую архитектуру
8. Запустит контейнер и проверит health-check
#### Активация магазина
После установки магазин стартует заблокированным (SHOP_ACTIVATED=false). Оператор получает onion-адрес от клиента, заходит в админку по onion/LAN, входит с SUPER_ADMIN_SECRET, добавляет комиссионные кошельки (Кошельки → Edit Wallets), настраивает бота, нажимает "Activate Shop" в Настройках.
### Ручная установка
@@ -46,8 +51,8 @@ git clone <repo-url> && cd telegram-shop
cp .env.example .env
nano .env # заполнить BOT_TOKEN, ADMIN_IDS, ENCRYPTION_KEY
# 3. Собрать и запустить
docker compose up -d --build
# 3. Запустить (образы тянутся из Gitea Container Registry, сборка не нужна)
docker compose up -d
# 4. Проверить статус
docker compose ps
@@ -55,6 +60,8 @@ curl http://localhost:3001/health # бот (health-сервер)
curl -o /dev/null -w "%{http_code}\n" http://localhost:3000/login # новая админка
```
> Образы бота и админки собираются в CI (Gitea Actions, buildx multi-arch) и публикуются в Gitea Container Registry. На сервере ничего не компилируется — только `docker compose pull`. Для пиннинга версии: `IMAGE_TAG=1.2.9 docker compose up -d`. Локальная сборка (dev): `docker compose up -d --build`.
## Настройка .env
Скопируйте `.env.example` в `.env` и заполните:
@@ -82,6 +89,7 @@ curl -o /dev/null -w "%{http_code}\n" http://localhost:3000/login # новая
| `WG_ENDPOINT` | — | Адрес сервера WireGuard |
| `WG_ADDRESS` | — | Адрес интерфейса WireGuard |
| `WG_DNS` | — | DNS для WireGuard |
| `SHOP_ACTIVATED` | — | Флаг активации магазина. false = магазин заблокирован до активации супер-админом в админке (Настройки → Shop Activation). |
Генерация ключа шифрования:
```bash
@@ -194,8 +202,9 @@ Docker автоматически собирает нативные модули
# Запуск
docker compose up -d
# Пересборка после изменений
docker compose up -d --build
# Пересборка (dev-сборка локально) или обновление до новой версии из registry
docker compose up -d --build # dev: собрать локально
IMAGE_TAG=1.2.9 docker compose up -d # prod: взять конкретную версию из registry
# Логи
docker compose logs -f

View File

@@ -8,10 +8,17 @@
## Current Version
**v1.2.8** — 2026-08-09
**v1.2.9** — 2026-08-11
## Changelog
### v1.2.9 — 2026-08-11
- **ci**: релиз переведён на Gitea Container Registry — образы бота и админки собираются в CI (Gitea Actions, buildx multi-arch linux/amd64+arm64 на x86_64 хосте) и пушатся как `git.softuniq.eu/telegram-market/telegram-shop` и `.../telegram-shop-admin`
- **deploy**: `docker-compose.yml` больше не собирает образы на сервере — бот и админка тянутся из registry (`IMAGE_TAG` для пиннинга версии); tor-proxy остаётся локальной сборкой (минимальный alpine)
- **install**: `install.sh` не компилирует нативный код на слабом железе — только `docker compose pull` + `up -d` (Orange Pi / RPi)
- **ci**: добавлен новый buildx-runner `gitea-host-buildx` (label `buildx`) — сборка multi-arch вместо нативной ARM64 сборки на Orange Pi
- **ci**: `REGISTRY_TOKEN` обновлён на package-токен (`write:package`) — теперь push в registry проходит без ошибок авторизации
### v1.2.8 — 2026-08-09
- **chore**: repo cleanup — removed stale `docs/admin-frontend-spec.md` (old Express/EJS admin), unused `templates/` (SmartAdmin copy), dead `scripts/sync-agents.cjs`, committed `admin-next/.zscripts/dev.pid`
- **docs**: added `docs/API.md` (admin REST API reference) and `docs/DATABASE.md` (DB schema); README structure updated

View File

@@ -1,8 +1,17 @@
# Telegram Shop — production compose
# Образы НЕ собираются на сервере — тянутся из Gitea Container Registry:
# git.softuniq.eu/telegram-market/telegram-shop (бот, порт 3001)
# git.softuniq.eu/telegram-market/telegram-shop-admin (Next.js админка, порт 3000)
#
# Сборка/релиз: .gitea/workflows/release.yaml (Gitea Actions, buildx multi-arch).
# Для обновления: укажите IMAGE_TAG (по умолчанию latest) и docker compose up -d.
#
# Пример:
# IMAGE_TAG=1.2.9 docker compose up -d
services:
telegram_shop_prod:
build:
context: .
dockerfile: ./Dockerfile
image: git.softuniq.eu/telegram-market/telegram-shop:${IMAGE_TAG:-latest}
hostname: telegram_shop_prod
container_name: telegram_shop_prod
ports:
@@ -10,6 +19,7 @@ services:
restart: unless-stopped
environment:
- ADMIN_CHAT_URL=http://tg_shop_admin:3000/api/chat
- SHOP_ACTIVATED=${SHOP_ACTIVATED:-true}
volumes:
- ./db:/app/db/
- ./uploads:/app/uploads/
@@ -35,9 +45,7 @@ services:
# Новая Next.js админ-панель (Prisma, одна БД с ботом)
tg_shop_admin:
build:
context: ./admin-next
dockerfile: ./Dockerfile
image: git.softuniq.eu/telegram-market/telegram-shop-admin:${IMAGE_TAG:-latest}
container_name: tg_shop_admin
ports:
- "3000:3000"
@@ -73,9 +81,11 @@ services:
- GITEA_API_URL=${GITEA_API_URL}
- WG_PRIVATE_KEY=${WG_PRIVATE_KEY}
- WG_PRESHARED_KEY=${WG_PRESHARED_KEY}
- SHOP_ACTIVATED=${SHOP_ACTIVATED:-true}
volumes:
- ./db:/app/db
- ./uploads:/app/uploads
- ./tor-proxy/hosts:/onion-hosts:ro
deploy:
resources:
limits:
@@ -93,6 +103,7 @@ services:
- tor_proxy_net
tor-proxy:
# Миниатюрный alpine-контейнер (tor + entrypoint) — собирается локально за секунды
build:
context: ./tor-proxy
dockerfile: Dockerfile

View File

@@ -155,40 +155,138 @@ else
fi
# ============================================================
# 4. Проверка обязательных переменных
# 4. Настройка переменных окружения (интерактивно)
# ============================================================
print_step "Проверка переменных окружения"
print_step "Настройка переменных окружения (интерактивно)"
MISSING_COUNT=0
MISSING_LIST=""
for VAR in BOT_TOKEN ADMIN_IDS ENCRYPTION_KEY; do
VAL=$(grep -E "^${VAR}=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"' | tr -d '\r' || true)
if [ -z "$VAL" ] || [ "$VAL" = "your_bot_token_here" ] || [ "$VAL" = "123456789,987654321" ]; then
MISSING_COUNT=$((MISSING_COUNT + 1))
MISSING_LIST="$MISSING_LIST $VAR"
# Функция для обновления переменной в .env (безопасно для спецсимволов в значении)
update_env_var() {
var_name="$1"
var_value="$2"
if grep -q "^${var_name}=" .env 2>/dev/null; then
awk -v key="$var_name" -v val="$var_value" '
BEGIN { FS = OFS = "="; done = 0 }
/^[A-Za-z_][A-Za-z0-9_]*=/ {
if ($1 == key) { print key "=" val; done = 1; next }
}
{ print }
END { if (!done) print key "=" val }
' .env > .env.tmp && mv .env.tmp .env
else
echo "${var_name}=${var_value}" >> .env
fi
done
}
if [ "$MISSING_COUNT" -ne 0 ]; then
print_warn "Не заполнены или имеют placeholder значения:$MISSING_LIST"
# Скрытый ввод секретов (POSIX sh, работает в Alpine ash)
read_secret() {
printf "%s" "$1"
stty -echo 2>/dev/null
read -r input
stty echo 2>/dev/null
printf "\n"
printf "${YELLOW}Откройте .env в редакторе и заполните:${NC}\n"
for V in $MISSING_LIST; do
printf " - %s\n" "$V"
done
printf "\n"
printf "Продолжить всё равно? (y/N): "
read -r CONTINUE
case "$CONTINUE" in
[Yy]|[Yy][Ee][Ss])
;;
*)
print_info "Откройте .env, заполните значения и запустите install.sh снова."
exit 0
;;
esac
}
# Функция для генерации случайного hex-строки (32 байта)
generate_random_hex() {
if command -v node >/dev/null 2>&1; then
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
elif command -v openssl >/dev/null 2>&1; then
openssl rand -hex 32
else
od -An -tx32 /dev/urandom | tr -d ' \n'
fi
}
# BOT_TOKEN
current_bot_token=$(grep -E "^BOT_TOKEN=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
if [ -z "$current_bot_token" ] || [ "$current_bot_token" = "your_bot_token_here" ]; then
print_warn "BOT_TOKEN не задан или содержит placeholder."
read_secret "Введите BOT_TOKEN (от @BotFather): "
update_env_var "BOT_TOKEN" "$input"
bot_token="$input"
else
print_ok "Все обязательные переменные заполнены"
bot_token="$current_bot_token"
fi
# ADMIN_IDS
current_admin_ids=$(grep -E "^ADMIN_IDS=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
if [ -z "$current_admin_ids" ] || [ "$current_admin_ids" = "123456789,987654321" ]; then
print_warn "ADMIN_IDS не задан или содержит placeholder."
printf "Введите ADMIN_IDS (через запятую, например: 123456789,987654321): "
read -r input
update_env_var "ADMIN_IDS" "$input"
admin_ids="$input"
else
admin_ids="$current_admin_ids"
fi
# ENCRYPTION_KEY
current_encryption_key=$(grep -E "^ENCRYPTION_KEY=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
if [ -z "$current_encryption_key" ]; then
encryption_key=$(generate_random_hex)
update_env_var "ENCRYPTION_KEY" "$encryption_key"
print_ok "ENCRYPTION_KEY сгенерирован автоматически"
else
encryption_key="$current_encryption_key"
fi
# ADMIN_SECRET
current_admin_secret=$(grep -E "^ADMIN_SECRET=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
if [ -z "$current_admin_secret" ] || [ "$current_admin_secret" = "changeme" ]; then
admin_secret=$(generate_random_hex)
update_env_var "ADMIN_SECRET" "$admin_secret"
print_ok "ADMIN_SECRET сгенерирован автоматически"
else
admin_secret="$current_admin_secret"
fi
# SUPER_ADMIN_SECRET
current_super_admin_secret=$(grep -E "^SUPER_ADMIN_SECRET=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
if [ -z "$current_super_admin_secret" ] || [ "$current_super_admin_secret" = "changeme_super" ]; then
# Генерация до тех пор, пока не получим значение, отличное от ADMIN_SECRET
super_admin_secret=""
while [ -z "$super_admin_secret" ] || [ "$super_admin_secret" = "$admin_secret" ]; do
super_admin_secret=$(generate_random_hex)
done
update_env_var "SUPER_ADMIN_SECRET" "$super_admin_secret"
print_ok "SUPER_ADMIN_SECRET сгенерирован автоматически"
else
super_admin_secret="$current_super_admin_secret"
fi
# SHOP_ACTIVATED
current_shop_activated=$(grep -E "^SHOP_ACTIVATED=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
if [ -z "$current_shop_activated" ]; then
shop_activated="false"
update_env_var "SHOP_ACTIVATED" "$shop_activated"
print_info "SHOP_ACTIVATED установлен в 'false' (магазин стартует заблокированным)"
else
shop_activated="$current_shop_activated"
fi
# ADMIN_URL
current_admin_url=$(grep -E "^ADMIN_URL=" .env 2>/dev/null | cut -d'=' -f2- | tr -d '"') || true
if [ -z "$current_admin_url" ]; then
# Попытка определить LAN IP
lan_ip=$(hostname -I | awk '{print $1}')
if [ -z "$lan_ip" ]; then
lan_ip=$(ip route get 1 | awk '{print $7}' 2>/dev/null)
fi
if [ -n "$lan_ip" ]; then
admin_url="http://${lan_ip}:3000"
update_env_var "ADMIN_URL" "$admin_url"
print_info "ADMIN_URL установлен в $admin_url (определен автоматически)"
else
print_warn "Не удалось определить LAN IP. ADMIN_URL не установлен."
fi
else
admin_url="$current_admin_url"
fi
# Ограничиваем доступ к .env (содержит секреты)
if [ -f .env ]; then
chmod 600 .env
print_ok ".env защищён (chmod 600)"
fi
# ============================================================
@@ -199,22 +297,24 @@ mkdir -p db uploads
print_ok "db/, uploads/ готовы"
# ============================================================
# 6. Сборка образа
# 6. Загрузка образов из Gitea Container Registry
# ============================================================
print_step "Сборка Docker-образа (это может занять несколько минут)"
print_step "Загрузка образов из Gitea Container Registry"
print_info "Архитектура: $ARCH_NAME"
print_info "Нативные модули: better-sqlite3, tiny-secp256k1 (компилируются в builder)"
print_info "Образы собираются в CI (Gitea Actions, buildx multi-arch) — на сервере ничего не компилируется"
# Docker автоматически собирает под текущую архитектуру хоста
# buildx нужен только для multi-arch push в registry, для локальной сборки — обычный build
docker build -t telegram-shop:latest .
print_ok "Образ telegram-shop:latest собран"
# Релизная версия: по умолчанию latest, можно переопределить IMAGE_TAG=v1.2.9
IMAGE_TAG="${IMAGE_TAG:-latest}"
print_info "Используемая версия образа: $IMAGE_TAG"
$COMPOSE_CMD pull telegram_shop_prod tg_shop_admin
print_ok "Образы telegram-shop:${IMAGE_TAG} и telegram-shop-admin:${IMAGE_TAG} загружены"
# ============================================================
# 7. Запуск контейнеров
# ============================================================
print_step "Запуск контейнеров"
$COMPOSE_CMD up -d
IMAGE_TAG="${IMAGE_TAG}" $COMPOSE_CMD up -d
print_ok "Контейнеры запущены"
# ============================================================
@@ -257,7 +357,7 @@ else
fi
# ============================================================
# 9. Tor Proxy — onion-адреса
# 11. Tor Proxy — onion-адреса
# ============================================================
printf "\n"
print_step "Проверка Tor-прокси"
@@ -295,6 +395,35 @@ else
printf " docker logs tor-proxy\n"
fi
# ============================================================
# 12. Финальная сводка
# ============================================================
printf "\n"
print_step "Финальная сводка"
# LAN IP для админки (если не определен ранее)
if [ -z "${admin_url:-}" ]; then
lan_ip=$(hostname -I | awk '{print $1}')
if [ -z "$lan_ip" ]; then
lan_ip=$(ip route get 1 | awk '{print $7}' 2>/dev/null)
fi
if [ -n "$lan_ip" ]; then
admin_url="http://${lan_ip}:3000"
else
admin_url="Не определен"
fi
fi
printf " ${CYAN}${BOLD}LAN URL админки:${NC} %s\n" "$admin_url"
printf " ${CYAN}${BOLD}SSH onion:${NC} %s\n" "$SSH_ONION"
printf " ${CYAN}${BOLD}Admin onion:${NC} %s\n" "$ADMIN_ONION"
printf " ${CYAN}${BOLD}ADMIN_SECRET:${NC} %s\n" "$admin_secret"
printf " ${CYAN}${BOLD}SUPER_ADMIN_SECRET:${NC} %s\n" "$super_admin_secret"
printf "\n"
printf "${YELLOW}⚠ Магазин стартует заблокированным (SHOP_ACTIVATED=false).${NC}\n"
printf "${YELLOW} Войдите как супер-админ, добавьте комиссионные кошельки и активируйте магазин в Настройках.${NC}\n"
printf "\n"
# ============================================================
# Готово
# ============================================================