7 Commits

12 changed files with 250 additions and 409 deletions

View File

@@ -1,35 +0,0 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
groups:
# Схлопываем шумные обновления в один PR, чтобы не плодить десятки веток.
dev-dependencies:
dependency-type: "development"
production-minor-patch:
dependency-type: "production"
update-types:
- "minor"
- "patch"
labels:
- "dependencies"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "ci"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "docker"

View File

@@ -1,60 +0,0 @@
name: CI
on:
push:
branches: [main, dev]
pull_request:
branches: [main, dev]
jobs:
lint:
uses: ./.github/workflows/lint.yml
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test
build:
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
VITE_API_URL: /api
VITE_TELEGRAM_BOT_USERNAME: test_bot
VITE_APP_NAME: Cabinet
VITE_APP_LOGO: V
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7

View File

@@ -1,37 +0,0 @@
name: CodeQL
on:
push:
branches: [main, dev]
pull_request:
branches: [main, dev]
schedule:
# Еженедельный полный проход, чтобы ловить новые правила даже без коммитов.
- cron: "27 3 * * 1"
permissions:
contents: read
jobs:
analyze:
name: Analyze (javascript-typescript)
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: javascript-typescript
queries: security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:javascript-typescript"

View File

@@ -1,100 +0,0 @@
name: Docker Build
on:
push:
branches: [main]
tags:
- 'v*'
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
permissions:
contents: read
jobs:
lint:
uses: ./.github/workflows/lint.yml
build-and-push:
needs: lint
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VITE_API_URL=/api
VITE_TELEGRAM_BOT_USERNAME=
VITE_APP_NAME=Cabinet
VITE_APP_LOGO=V
# OCI-референсы требуют lowercase, а github.repository сохраняет регистр
# (BEDOLAGA-DEV/…) — Trivy падал бы на 'repository name must be lowercase'.
- name: Lowercase image name for Trivy
if: github.event_name != 'pull_request'
id: image
run: echo "name=${IMAGE_NAME,,}" >> "$GITHUB_OUTPUT"
# Сканируем запушенный образ на CVE. Пока non-blocking (exit-code: 0):
# находки уезжают в Security → Code scanning, но сборку не роняют.
# Для ужесточения — поставить severity-фильтр в exit-code и убрать PR-гейт.
- name: Scan image with Trivy
if: github.event_name != 'pull_request'
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0
with:
image-ref: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}@${{ steps.build.outputs.digest }}
format: sarif
output: trivy-results.sarif
severity: HIGH,CRITICAL
exit-code: "0"
- name: Upload Trivy results to Security tab
if: github.event_name != 'pull_request'
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: trivy-results.sarif
category: trivy

View File

@@ -1,29 +0,0 @@
name: Lint & Format
on:
workflow_call:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Check formatting
run: npm run format:check
- name: Run TypeScript check
run: npx tsc --noEmit

View File

@@ -1,18 +0,0 @@
name: Release Please
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
release-type: node

View File

@@ -1,85 +0,0 @@
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
lint:
uses: ./.github/workflows/lint.yml
create-release:
needs: lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
env:
VITE_API_URL: /api
VITE_TELEGRAM_BOT_USERNAME:
VITE_APP_NAME: Bedolaga Cabinet
VITE_APP_LOGO: V
- name: Create dist archive
run: |
cd dist
tar -czf ../bedolaga-cabinet-${{ github.ref_name }}.tar.gz .
cd ..
zip -r bedolaga-cabinet-${{ github.ref_name }}.zip dist/
- name: Generate changelog
id: changelog
run: |
# Get previous tag
PREV_TAG=$(git describe --tags --abbrev=0 ${{ github.ref_name }}^ 2>/dev/null || echo "")
if [ -z "$PREV_TAG" ]; then
echo "changelog=First release" >> $GITHUB_OUTPUT
else
CHANGELOG=$(git log ${PREV_TAG}..${{ github.ref_name }} --pretty=format:"- %s (%h)" --no-merges)
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
body: |
## Changes
${{ steps.changelog.outputs.changelog }}
## Docker Image
```bash
docker pull ghcr.io/${{ github.repository }}:${{ github.ref_name }}
```
## Installation
See [README.md](https://github.com/${{ github.repository }}/blob/${{ github.ref_name }}/README.md) for installation instructions.
files: |
bedolaga-cabinet-${{ github.ref_name }}.tar.gz
bedolaga-cabinet-${{ github.ref_name }}.zip
draft: false
prerelease: ${{ contains(github.ref_name, 'beta') || contains(github.ref_name, 'alpha') || contains(github.ref_name, 'rc') }}

View File

@@ -1,35 +0,0 @@
name: Security Audit
on:
push:
branches: [main, dev]
pull_request:
branches: [main, dev]
schedule:
- cron: "27 4 * * 1"
permissions:
contents: read
jobs:
npm-audit:
name: npm audit
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm ci
# Пока non-blocking: сообщаем о HIGH/CRITICAL, но не валим пайплайн.
# Когда долги по зависимостям разгребём — убрать continue-on-error.
- name: Run npm audit (high+)
continue-on-error: true
run: npm audit --audit-level=high

View File

@@ -52,6 +52,33 @@ jobs:
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
- name: Strip upstream-only CI/CD files
# Апстрим хранит у себя dependabot.yml и CI-workflow'ы, которых нам
# не нужно в приватном форке (плодят PR/раны, требуют чужих секретов).
# Merge может притащить их обратно — вычищаем перед пушем.
if: steps.sync.outputs.changed == 'true'
run: |
FILES=(
.github/dependabot.yml
.github/workflows/ci.yml
.github/workflows/codeql.yml
.github/workflows/docker.yml
.github/workflows/lint.yml
.github/workflows/release-please.yml
.github/workflows/release.yml
.github/workflows/security-audit.yml
)
REMOVED=false
for f in "${FILES[@]}"; do
if [ -e "$f" ]; then
git rm -q "$f"
REMOVED=true
fi
done
if [ "$REMOVED" = true ]; then
git commit -q -m "chore: strip upstream CI/CD workflows not needed in this fork"
fi
- name: Push merged main back to fork
if: steps.sync.outputs.changed == 'true'
run: git push origin HEAD:main
@@ -84,13 +111,48 @@ jobs:
tar -czf ../cabinet-dist.tar.gz .
- name: Publish release
id: publish
if: steps.sync.outputs.changed == 'true' || github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="build-$(date -u +%Y%m%d-%H%M%S)-$(git rev-parse --short HEAD)"
# Тег как в апстриме (v1.63.0 и т.п.) вместо build-<timestamp> — меньше
# путаницы при сверке с апстримной версией. Если тег уже существует
# (повторный ручной запуск без нового апстримного коммита) — пересоздаём.
VERSION=$(node -p "require('./package.json').version")
TAG="v${VERSION}"
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release delete "$TAG" --repo "$GITHUB_REPOSITORY" --yes --cleanup-tag
fi
gh release create "$TAG" cabinet-dist.tar.gz \
--repo "$GITHUB_REPOSITORY" \
--title "$TAG" \
--notes "Автосборка: апстрим синхронизирован + локальный патч (скрыт блок «Дополнительные опции»)." \
--notes "Синхронизировано с апстримом (v${VERSION}) + локальный патч (скрыт блок «Дополнительные опции»)." \
--latest
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Notify Telegram on new release
# TG_BOT_TOKEN/TG_CHAT_ID задаются в Settings → Secrets and variables →
# Actions, значения сюда не попадают.
if: success() && (steps.sync.outputs.changed == 'true' || github.event_name == 'workflow_dispatch')
env:
TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }}
TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }}
run: |
curl -s -X POST "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \
-d chat_id="${TG_CHAT_ID}" \
-d text="✅ Новая сборка кабинета готова: ${{ steps.publish.outputs.tag }} — https://github.com/${GITHUB_REPOSITORY}/releases/tag/${{ steps.publish.outputs.tag }}" \
> /dev/null
- name: Notify Telegram on failure
# Срабатывает, если упал любой предыдущий шаг (в первую очередь —
# конфликт при merge апстрима).
if: failure()
env:
TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }}
TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }}
run: |
curl -s -X POST "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \
-d chat_id="${TG_CHAT_ID}" \
-d text="⚠️ sync-and-build упал: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
> /dev/null

152
docs/RUNBOOK.md Normal file
View File

@@ -0,0 +1,152 @@
# Раннбук: автосинк и деплой кабинета
Как устроено скрытие блока «Дополнительные опции» на странице Подписки и вся
автоматика вокруг него: приватный форк, ежедневный синк с апстримом, сборка
статики и выкладка на VPS.
Репозиторий: `chillpadclub/bedolaga-cabinet` (приватный форк
`BEDOLAGA-DEV/bedolaga-cabinet`).
## Суть задачи
На странице `Подписка` нужно было скрыть блок «Дополнительные опции» (докупка
устройств, трафика, управление серверами), не потеряв возможность и дальше
получать обновления апстрима — проект активно развивается, жить на статичном
форке было бы риском самим по себе.
Решение: приватный форк со своим патчем поверх + GitHub Action, который каждый
день сам подтягивает апстрим, накатывает патч и собирает готовую статику для
VPS.
## Патч: как спрятан блок
Файл — `src/pages/Subscription.tsx`. Условие рендера обёрнуто в `false &&`, но
не правкой существующей строки, а **обёрткой снаружи** — так, чтобы ни одна
оригинальная строка апстрима не менялась, только добавлены две строки до и две
после:
```tsx
{/* Additional Options — disabled on purpose */}
{false && (
<>
{subscription && // ← строка апстрима не тронута
(subscription.is_active || subscription.is_limited) &&
...
<div>...</div>
)}
</>
)}
```
Благодаря этому будущий `git merge` с апстримом конфликтует, только если
апстрим правит саму первую или последнюю строку блока — а не любую строку
внутри (докупка устройств, трафика и т.д.), которую вы всё равно не
используете.
Компоненты `DeviceTopupSheet`, `DeviceReductionSheet`, `TrafficTopupSheet`,
`ServerManagementSheet` остаются в коде и в сборке — просто не рендерятся. Это
осознанный выбор в пользу простоты патча, а не баг.
## Как работает автоматика
Один workflow — `.github/workflows/sync-and-build.yml`. Запускается по
расписанию (ежедневно, 06:00 UTC) и вручную кнопкой `Run workflow`.
1. **Merge апстрима**`git fetch` + `merge upstream/main` поверх форка.
2. **Чистка CI** — удаляет `dependabot.yml` и чужие workflow, если merge их
принёс.
3. **Push в форк** — только если merge реально что-то изменил.
4. **Сборка**`npm ci` + `npm run build:docker` (те же build-args, что и в
официальном образе).
5. **Релиз**`tar.gz` статики → GitHub Release с тегом `vX.Y.Z`.
6. **Уведомление** — сообщение в Telegram: готово или упало.
Если апстрим правит ровно ту же строку, что и патч, — шаг merge падает
**намеренно**. Чужой код не должен домёрживаться в обход человека. Приходит
сообщение в Telegram, конфликт разрешается руками локально, затем push — синк
продолжается.
Сборка и релиз происходят, только если merge реально что-то поменял (или при
ручном запуске) — вхолостую по расписанию ничего не собирается и не
публикуется.
## Файлы
| Путь | Назначение |
|---|---|
| `.github/workflows/sync-and-build.yml` | Весь пайплайн: синк, чистка, сборка, релиз, уведомления |
| `scripts/deploy-cabinet.sh` | Деплой на VPS: скачивает релиз, бэкапит текущую статику, раскатывает |
| `src/pages/Subscription.tsx` | Патч — скрытый блок «Дополнительные опции» |
| `docs/RUNBOOK.md` | Этот файл |
Удалено из форка сознательно (было в апстриме, плодило шум):
| Файл | Почему убран |
|---|---|
| `.github/dependabot.yml` | Открывал десятки PR на бамп версий — оставили только Dependabot alerts (без авто-PR) |
| `ci.yml`, `lint.yml` | Тесты/линт апстрима — код уже проверен в апстриме до мёржа |
| `codeql.yml`, `security-audit.yml` | Security-сканы апстрима, дублируют Dependabot alerts |
| `docker.yml` | Пуш официального образа в GHCR — не нужен, мы публикуем статику |
| `release-please.yml`, `release.yml` | Автобамп версии апстрима — версию читаем из `package.json` |
## Секреты и токены
| Что | Где хранится | Права |
|---|---|---|
| `TG_BOT_TOKEN` | GitHub → Settings → Secrets and variables → Actions | Отправка сообщений ботом |
| `TG_CHAT_ID` | Там же | — |
| Fine-grained PAT | VPS: `~/.cabinet-deploy-token`, права файла `600` | Только `Contents: Read-only` на этот репозиторий |
Токен для VPS специально ограничен одним репозиторием и правом только на
чтение — даже если файл утечёт, им нельзя ничего запушить или изменить.
## Как выкатить на прод
На VPS:
```bash
./deploy-cabinet.sh # последний релиз
./deploy-cabinet.sh v1.63.0 # откат/установка конкретной версии
```
Скрипт сам:
1. скачивает `cabinet-dist.tar.gz` через `gh release download`;
2. бэкапит текущую статику в `cabinet-dist.bak`;
3. раскатывает новую через `rsync -a --delete` (чистит устаревшие
хэшированные JS-чанки, не трогает `50x.html`).
Бэкап один, перезаписывается при каждом запуске. Откатиться на конкретную
версию надёжнее через `./deploy-cabinet.sh vX.Y.Z` (GitHub хранит все
релизы), а не полагаться только на `.bak`.
## Если что-то пошло не так
**Пришло Telegram-сообщение «⚠️ упал»** — почти всегда конфликт merge с
апстримом. Локально:
```bash
git fetch upstream main && git merge upstream/main
# разрешить конфликт (скорее всего в месте патча Subscription.tsx)
git add -A && git commit && git push origin main
```
Следующий прогон workflow снова пойдёт сам.
**Деплой на VPS выкатил битую сборку**:
```bash
rsync -a --delete cabinet-dist.bak/ cabinet-dist/ # мгновенный откат
# либо
./deploy-cabinet.sh vX.Y.Z # конкретный рабочий релиз
```
**Security-алерт в GitHub про уязвимость** — это Dependabot alerts (без
авто-PR). Апстрим обычно фиксит сам, фикс прилетит следующим синком. Если
критично и срочно — можно поправить зависимость руками отдельным коммитом.
## Что стоит проверять периодически
- **Раз в 12 месяца — вкладка Actions.** GitHub отключает scheduled-триггер
после ~60 дней без активности в репозитории. Один ручной `Run workflow`
включает обратно.
- **Бэк и фронт бота обновлять вместе.** Синк фронта и апдейт бэкенда бота —
по-прежнему ручной процесс, специально не автоматизировали дальше деплоя.
- **CHANGELOG апстрима перед выкаткой** — на случай breaking changes в API
между бэкендом и фронтом.

View File

@@ -1,10 +1,15 @@
#!/usr/bin/env bash
# Скачивает последнюю сборку из GitHub Release форка и раскатывает её
# Скачивает сборку из GitHub Release приватного репозитория и раскатывает её
# поверх продакшен-статики кабинета на VPS, с автоматическим бэкапом.
#
# Использование: ./deploy-cabinet.sh
# Использование:
# ./deploy-cabinet.sh — последний релиз
# ./deploy-cabinet.sh v1.63.0 — конкретный релиз (откат на версию)
#
# Откат при проблемах:
# Требует gh CLI, авторизованный токеном с правом Contents: Read-only на этот
# репозиторий. Токен лежит отдельно от скрипта — см. TOKEN_FILE ниже.
#
# Откат вручную при проблемах:
# rsync -a --delete "$DEST.bak/" "$DEST/"
set -euo pipefail
@@ -12,12 +17,28 @@ set -euo pipefail
REPO="chillpadclub/bedolaga-cabinet"
DEST="/home/etkr4k/remnawave-bedolaga-telegram-bot/cabinet-dist"
ASSET="cabinet-dist.tar.gz"
TOKEN_FILE="$HOME/.cabinet-deploy-token"
TAG="${1:-}"
if [ ! -f "$TOKEN_FILE" ]; then
echo "Не найден файл токена: $TOKEN_FILE" >&2
echo "Создайте его: echo '<токен>' > $TOKEN_FILE && chmod 600 $TOKEN_FILE" >&2
exit 1
fi
export GH_TOKEN
GH_TOKEN="$(cat "$TOKEN_FILE")"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
echo "Скачиваю последнюю сборку из $REPO..."
curl -fsSL "https://github.com/$REPO/releases/latest/download/$ASSET" -o "$TMP/$ASSET"
if [ -z "$TAG" ]; then
echo "Скачиваю последний релиз из $REPO..."
gh release download --repo "$REPO" --pattern "$ASSET" --dir "$TMP" --clobber
else
echo "Скачиваю релиз $TAG из $REPO..."
gh release download "$TAG" --repo "$REPO" --pattern "$ASSET" --dir "$TMP" --clobber
fi
echo "Распаковываю..."
mkdir -p "$TMP/dist"

View File

@@ -1496,9 +1496,12 @@ export default function Subscription() {
</div>
)}
{/* Additional Options (Buy Devices) */}
{false &&
subscription &&
{/* Additional Options (Buy Devices) — disabled on purpose. Wrapped (not
edited in place) so future upstream changes to the condition/body
below merge cleanly instead of conflicting on this toggle. */}
{false && (
<>
{subscription &&
(subscription.is_active || subscription.is_limited) &&
!subscription.is_trial &&
subscription.device_limit !== 0 && (
@@ -1577,6 +1580,8 @@ export default function Subscription() {
)}
</div>
)}
</>
)}
{/* Reissue Subscription — standalone block, not dependent on device_limit */}
{subscription &&