name: State backup # Ежедневный автоматический бэкап /export_state (см. README, раздел # "Диагностика") — раньше это был ручной curl владельца без расписания. # Требует секреты в настройках репозитория: # SPACE_HOST — хост HF Space (например, silverelixir-lumen.hf.space) # ADMIN_PANEL_KEY — см. curl -H "Authorization: Bearer " .../admin_keys # STATE_BACKUP_REPO — приватный репозиторий для бэкапов, например S1lverElixir/Lumen-state # STATE_BACKUP_TOKEN — fine-grained PAT с Contents: read+write только на STATE_BACKUP_REPO # Опционально (алерт о провале, аудит M11): # STATE_ALERT_TOKEN — токен бота-сканера (lumen_smoke_bot), без прав админа # STATE_ALERT_CHAT — личный chat_id владельца с этим ботом # Без них провал виден только в сводке Actions. # # ПРИВАТНОСТЬ (инцидент, сентябрь 2026): раньше файл заливался артефактом # Actions, а артефакты публичного репозитория может скачать любой. Теперь # бэкап пушится git-коммитом в отдельный ПРИВАТНЫЙ репозиторий — каждый день # отдельный коммит, история и есть глубина хранения. Ничего чувствительного # в артефакты больше не кладётся. on: schedule: - cron: "0 3 * * *" # 03:00 UTC ежедневно workflow_dispatch: # + ручной запуск по требованию permissions: contents: read # Два одновременных запуска (шедул + ручной) толкались бы в один бранч backup-репо. concurrency: group: state-backup cancel-in-progress: false jobs: backup: runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Fetch /export_state run: | curl -sf --retry 3 --max-time 120 \ -H "Authorization: Bearer ${{ secrets.ADMIN_PANEL_KEY }}" \ "https://${{ secrets.SPACE_HOST }}/export_state" \ -o export_state.json - name: Push to private backup repo env: STATE_BACKUP_TOKEN: ${{ secrets.STATE_BACKUP_TOKEN }} STATE_BACKUP_REPO: ${{ secrets.STATE_BACKUP_REPO }} run: | set -euo pipefail # Приватность: бэкап только в приватный репозиторий, иначе стоп. visibility="$(GH_TOKEN="${STATE_BACKUP_TOKEN}" gh repo view "${STATE_BACKUP_REPO}" --json visibility -q .visibility)" if [ "${visibility}" != "PRIVATE" ]; then echo "Refusing backup: ${STATE_BACKUP_REPO} is not PRIVATE (got: ${visibility})" >&2 exit 1 fi git config --global user.name "lumen-backup" git config --global user.email "lumen-backup@local" # PAT не в URL клона, а в extraheader: иначе светится в remote -v и логах. git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic $(printf 'x-access-token:%s' "${STATE_BACKUP_TOKEN}" | base64 -w0)" \ clone "https://github.com/${STATE_BACKUP_REPO}.git" backup-repo cp export_state.json backup-repo/export_state.json cd backup-repo git add export_state.json git diff --cached --quiet || git commit -m "Backup $(date -u +%Y-%m-%d)" # extraheader только на эту команду: в конфиге клона секрета нет. git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic $(printf 'x-access-token:%s' "${STATE_BACKUP_TOKEN}" | base64 -w0)" \ push origin HEAD:main - name: Alert backup failure to Telegram # Бэкап — единственный резерв истории чатов, и раньше его провал жил только # в логах Actions (аудит M11, 10.2026). Состояние выводится из секретов: # без них шаг тихо пропускается и падение всё равно уходит в сводку ниже. # STATE_ALERT_TOKEN — токен бота-сканера (лumen_smoke_bot), без прав админа # STATE_ALERT_CHAT — личный chat_id владельца с этим ботом # Токен не печатаем и не кладём в URL: он в extraheader-подобном Bearer. if: failure() env: STATE_ALERT_TOKEN: ${{ secrets.STATE_ALERT_TOKEN }} STATE_ALERT_CHAT: ${{ secrets.STATE_ALERT_CHAT }} run: | if [ -z "${STATE_ALERT_TOKEN}" ] || [ -z "${STATE_ALERT_CHAT}" ]; then echo "alert secrets not set — skipping Telegram alert" >> "$GITHUB_STEP_SUMMARY" exit 0 fi curl -sf --retry 2 --max-time 20 -X POST \ "https://api.telegram.org/bot${STATE_ALERT_TOKEN}/sendMessage" \ -H "Content-Type: application/json" \ -d "$(jq -nc --arg c "$STATE_ALERT_CHAT" \ --arg t "Lumen: ежедневный бэкап состояния ПРОВАЛИЛСЯ. Проверь вкладку Actions." \ '{chat_id:$c, text:$t}')" \ || echo "Telegram alert failed" >> "$GITHUB_STEP_SUMMARY" - name: Report backup failure if: failure() run: | echo "## State backup FAILED — проверьте вкладку Actions" >> "$GITHUB_STEP_SUMMARY" exit 1