5 Commits

4 changed files with 223 additions and 10 deletions

View File

@@ -111,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 &&