18 Commits
Author SHA1 Message Date
austnv 6d543361c4 fix: add step "Set repository name to lowercase"
Build and Deploy Backend / build-and-deploy (push) Failing after 32s
2026-06-26 13:10:32 +03:00
austnv c17eb9584b fix: add docker push step to build.yml
Build and Deploy Backend / build-and-deploy (push) Failing after 32s
2026-06-26 13:06:13 +03:00
austnv a3c7b5bdd1 v0.7.5
Build and Deploy Backend / build-and-deploy (push) Successful in 33s
2026-06-26 12:55:38 +03:00
austnv 6c7adb1028 feat: add country_code field to hosts response 2026-06-26 12:51:45 +03:00
austnv 3661882c05 fix: volume data/tariffs.json -> data/
Build and Deploy Backend / build-and-deploy (push) Successful in 46s
2026-06-26 12:40:55 +03:00
austnv abcdb040ef fix: release author 2026-06-21 22:43:36 +03:00
austnv 718b9deebe fix: delete docker image from workflow file 2026-06-21 22:41:01 +03:00
austnv 3e961e8462 v0.7.3
Build and Deploy Backend / build-and-deploy (push) Successful in 29s
2026-06-21 22:34:33 +03:00
austnv bdee2fe336 microfix: env.REGISTRY -> git.uvpn.shop 2026-06-21 22:31:08 +03:00
austnv 6e07ccca3e fix token (ACCESS_TOKEN)
Build and Deploy Backend / build-and-deploy (push) Successful in 26s
2026-06-21 22:26:34 +03:00
austnv 53a2ba5885 fix token 2026-06-21 22:24:04 +03:00
austnv 0c9464d624 fix release workflow 2026-06-21 22:22:19 +03:00
austnv b2c6a30104 fix release workflow 2026-06-21 22:18:25 +03:00
austnv 5d95c2dea3 fix release workflow 2026-06-21 22:15:39 +03:00
austnv 0c7cc7cdb8 fix: problem with gitea actions 2026-06-21 22:12:39 +03:00
austnv b27d615773 v0.7.2
Build and Deploy Backend / build-and-deploy (push) Failing after 6s
2026-06-21 21:58:40 +03:00
austnv d9de5a1a5c v0.7.1
Build and Deploy Backend / build-and-deploy (push) Successful in 59s
2026-06-21 21:42:24 +03:00
austnv e172ed6e43 fix: clean host name 2026-06-21 21:41:47 +03:00
5 changed files with 110 additions and 20 deletions
+64 -16
View File
@@ -11,15 +11,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# 1. Клонируем код
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
# 2. Настраиваем Docker Buildx
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
# 3. Логинимся в Container Registry Gitea
- name: Login to Gitea Container Registry - name: Login to Gitea Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@@ -27,39 +26,88 @@ jobs:
username: ${{ gitea.repository_owner }} username: ${{ gitea.repository_owner }}
password: ${{ secrets.ACCESS_TOKEN }} password: ${{ secrets.ACCESS_TOKEN }}
# 4. Собираем и пушим образ - name: Set repository name to lowercase
id: repo
run: |
OWNER=$(echo "${{ gitea.repository_owner }}" | tr '[:upper:]' '[:lower:]')
REPO=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]')
echo "owner=$OWNER" >> $GITHUB_OUTPUT
echo "repo=$REPO" >> $GITHUB_OUTPUT
echo "full_repo=$OWNER/$REPO" >> $GITHUB_OUTPUT
# Также сохраняем полный путь к образу
echo "image=git.uvpn.shop/$OWNER/$REPO" >> $GITHUB_OUTPUT
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
push: true push: true
tags: | tags: |
git.uvpn.shop/uvpn/backend:latest ${{ steps.repo.outputs.image }}:latest
git.uvpn.shop/uvpn/backend:${{ gitea.sha }} ${{ steps.repo.outputs.image }}:${{ gitea.ref_name }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILD_VERSION=${{ gitea.ref_name }}
BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
VCS_REF=${{ github.sha }}
- name: Create Gitea Release via API
run: |
TAG_MESSAGE=$(git tag -l --format='%(contents)' "${{ gitea.ref_name }}" 2>/dev/null || echo "Автоматический релиз через CI/CD")
TAG_MESSAGE_ESCAPED=$(echo "$TAG_MESSAGE" | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
JSON_PAYLOAD=$(cat <<EOF
{
"tag_name": "${{ gitea.ref_name }}",
"name": "Release ${{ gitea.ref_name }}",
"body": "## Релиз ${{ gitea.ref_name }}\n\n📝 ${TAG_MESSAGE_ESCAPED}\n\n---\n\n**🏷️ Тег:** \`${{ gitea.ref_name }}\`\n**👤 Автор:** @${{ gitea.actor }}\n**📦 Образ:** \`${{ steps.repo.outputs.image }}:${{ gitea.ref_name }}\`\n\n✅ Автоматически собрано и развёрнуто",
"draft": false,
"prerelease": false
}
EOF
)
RESPONSE=$(curl -s -w "\n%{http_code}" \
-X POST \
-H "Authorization: token ${{ secrets.ACCESS_TOKEN }}" \
-H "Content-Type: application/json" \
-d "$JSON_PAYLOAD" \
"https://git.uvpn.shop/api/v1/repos/${{ gitea.repository }}/releases")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
BODY=$(echo "$RESPONSE" | sed '$d')
if [ "$HTTP_CODE" -eq 201 ]; then
echo "✅ Релиз успешно создан!"
echo "$BODY" | jq '.'
elif [ "$HTTP_CODE" -eq 409 ]; then
echo "⚠️ Релиз с таким тегом уже существует, пропускаем"
else
echo "❌ Ошибка создания релиза (HTTP $HTTP_CODE)"
echo "$BODY"
exit 1
fi
# 5. Устанавливаем SSH-ключ для деплоя
- name: Install SSH key - name: Install SSH key
uses: webfactory/ssh-agent@v0.9.0 uses: webfactory/ssh-agent@v0.9.0
with: with:
ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }} ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }}
# 6. Деплой на сервер
- name: Deploy on server - name: Deploy on server
run: | run: |
ssh -o StrictHostKeyChecking=no ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF' ssh -o StrictHostKeyChecking=no ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF'
# Переходим в папку с проектом (где лежит docker-compose.yml)
cd /opt/uvpn.shop/backend cd /opt/uvpn.shop/backend
# Логинимся в реестр Gitea (чтобы docker pull мог скачать образ)
echo "${{ secrets.ACCESS_TOKEN }}" | docker login git.uvpn.shop -u ${{ gitea.repository_owner }} --password-stdin echo "${{ secrets.ACCESS_TOKEN }}" | docker login git.uvpn.shop -u ${{ gitea.repository_owner }} --password-stdin
# Скачиваем свежий образ # Используем образ с правильным именем в нижнем регистре
export IMAGE_NAME="git.uvpn.shop/uvpn/backend"
docker compose pull docker compose pull
docker compose down || true
# Перезапускаем контейнер с новым образом
docker compose down || true # Если контейнер не запущен — игнорируем ошибку
docker compose up -d docker compose up -d
# Очищаем неиспользуемые образы
docker image prune -f docker image prune -f
docker compose ps
EOF EOF
+17
View File
@@ -1,6 +1,12 @@
FROM ghcr.io/astral-sh/uv:python3.12-alpine AS builder FROM ghcr.io/astral-sh/uv:python3.12-alpine AS builder
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
WORKDIR /app WORKDIR /app
# Аргументы для метаданных
ARG BUILD_VERSION=unknown
ARG BUILD_DATE=unknown
ARG VCS_REF=unknown
RUN --mount=type=cache,target=/root/.cache/uv \ RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
@@ -8,7 +14,18 @@ RUN --mount=type=cache,target=/root/.cache/uv \
FROM python:3.12-alpine FROM python:3.12-alpine
WORKDIR /app WORKDIR /app
# Метки для образа
LABEL org.opencontainers.image.title="uVPN Backend"
LABEL org.opencontainers.image.description="uVPN Shop Backend Service"
LABEL org.opencontainers.image.version="${BUILD_VERSION}"
LABEL org.opencontainers.image.created="${BUILD_DATE}"
LABEL org.opencontainers.image.revision="${VCS_REF}"
LABEL org.opencontainers.image.source="https://git.uvpn.shop/${{ gitea.repository }}"
LABEL org.opencontainers.image.licenses="MIT"
COPY --from=builder /app/.venv /app/.venv COPY --from=builder /app/.venv /app/.venv
COPY . /app COPY . /app
ENV PATH="/app/.venv/bin:$PATH" ENV PATH="/app/.venv/bin:$PATH"
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
+1 -1
View File
@@ -3,7 +3,7 @@ services:
image: git.uvpn.shop/uvpn/backend:latest image: git.uvpn.shop/uvpn/backend:latest
container_name: uvpn-backend container_name: uvpn-backend
volumes: volumes:
- /opt/remnawave-minishop/data/tariffs.json:/app/data/tariffs.json - /opt/remnawave-minishop/data/:/app/data/
env_file: env_file:
- .env - .env
networks: networks:
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "uvpn-backend" name = "uvpn-backend"
version = "0.7.0" version = "0.7.7"
description = "FastAPI backend with Docker for uvpn.shop" description = "FastAPI backend with Docker for uvpn.shop"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
+27 -2
View File
@@ -3,21 +3,46 @@ import logging
from os import getenv from os import getenv
from remnawave import RemnawaveSDK from remnawave import RemnawaveSDK
from remnawave.models.hosts import GetAllHostsResponseDto from remnawave.models.hosts import GetAllHostsResponseDto
from remnawave.models.nodes import GetOneNodeResponseDto
import re
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
router = APIRouter(prefix='/api/v1', tags=['hosts']) router = APIRouter(prefix='/api/v1', tags=['hosts'])
remnawave = RemnawaveSDK(base_url='http://remnawave:3000', token=getenv('REMNAWAVE_API_TOKEN')) remnawave = RemnawaveSDK(base_url='http://remnawave:3000', token=getenv('REMNAWAVE_API_TOKEN'))
def clean_host_name(name):
"""
Очищает имя хоста от флагов и лишних символов.
Пример: '🇷🇺Russia Speed 🚀' -> 'Russia Speed'
"""
# Удаляем все флаги (региональные индикаторы)
flag_pattern = re.compile(r'[\U0001F1E6-\U0001F1FF]{2}')
cleaned = flag_pattern.sub('', name)
# Удаляем другие эмодзи (опционально)
emoji_pattern = re.compile(
"[\U0001F300-\U0001F9FF\U00002600-\U000027BF\U0001F000-\U0001F02F\U0001F0A0-\U0001F0FF]+",
flags=re.UNICODE
)
cleaned = emoji_pattern.sub('', cleaned)
# Убираем лишние пробелы
return cleaned.strip()
@router.get('/hosts') @router.get('/hosts')
async def get_nodes(): async def get_nodes():
hosts: GetAllHostsResponseDto = await remnawave.hosts.get_all_hosts() hosts: GetAllHostsResponseDto = await remnawave.hosts.get_all_hosts()
return [ return [
{ {
'name': host.remark, 'name': clean_host_name(host.remark),
'address': host.address, 'address': host.address,
'port': host.port 'port': host.port,
'country_code': (await remnawave.nodes.get_one_node(uuid=host.nodes[0])).country_code
} }
for host in hosts.root if not host.is_disabled and not host.is_hidden for host in hosts.root if not host.is_disabled and not host.is_hidden