diff --git a/docs/superpowers/specs/2026-04-21-cf7stg-self-hosted-updates-design-DRAFT.md b/docs/superpowers/specs/2026-04-21-cf7stg-self-hosted-updates-design-DRAFT.md new file mode 100644 index 0000000..cdb8bfa --- /dev/null +++ b/docs/superpowers/specs/2026-04-21-cf7stg-self-hosted-updates-design-DRAFT.md @@ -0,0 +1,221 @@ +# CF7 Spam → Telegram — Self-Hosted Updates from Gitea (1.2.0) — DRAFT + +**Статус:** DRAFT — дизайн обсуждён в брейнсторме, пользователь подтвердил разделы 1 и 2. Не финализировано (нет финального spec-документа без суффикса DRAFT, нет implementation plan, нет beads-подзадач). + +**Что нужно сделать в новой сессии:** + +1. Перечитать этот документ. +2. Спросить пользователя: «По дизайну self-hosted updates (1.2.0) всё ок? Если да — финализирую spec (без DRAFT), пробегусь по self-review, попрошу глянуть, потом writing-plans». +3. Если ок — `git mv ...DRAFT.md` в финальное имя `2026-04-21-cf7stg-self-hosted-updates-design.md`, повторить self-review, попросить пользователя глянуть финал, потом invoke `superpowers:writing-plans`. + +--- + +## Контекст + +**Trigger:** только что выпустили v1.1.0 (silent drop). Деплой 1.1.0 на домработница.рус сделан вручную через `rsync`. Этот же плагин будет деплоиться на другие сайты (washanyanya.ru, потенциально другие). Хочется, чтобы WP-сайты сами получали обновления через стандартный «Плагины → Обновления», без `rsync` и без знания внутренностей. + +**Решено в брейнсторме:** +- Gitea репо `bryzgalov/cf7-spam-to-telegram` сделан **публичным** (пользователь подтвердил). В коде нет секретов. +- Используем **Plugin Update Checker v5** от YahnisElsts, vendored в `includes/lib/plugin-update-checker/`. +- В режиме «Generic JSON server»: PUC ходит в статичный `wp-plugin-info.json` в корне репо (через Gitea raw URL). +- Zip-релизы хранятся как Gitea Release assets (создаются вручную через скрипт `release.sh`, чистые благодаря `.gitattributes export-ignore`). + +## Scope + +**В scope:** +- Vendored PUC v5 в `includes/lib/plugin-update-checker/`. +- Файл `wp-plugin-info.json` в корне репо (Generic JSON для PUC). +- Инициализация PUC в `Plugin::boot()` через `register_update_checker()`. +- `.gitattributes` с `export-ignore` для всех dev-файлов. +- Bash-скрипт `release.sh` (bump version → commit → tag → push → build clean zip → Gitea Release + asset upload). +- Минимальные тесты на инвариант (PUC файл существует, JSON валидный, version в JSON совпадает с константой плагина). +- Версия 1.2.0 + changelog. + +**Вне scope:** +- Кастомный `GiteaApi extends Api` (не нужен — публичный JSON). +- Update-сервер / прокси (не нужен — статичный JSON через raw URL). +- CI/Gitea Actions для авто-сборки релизов (вручную через release.sh достаточно). +- License key management. +- Авто-rollback на старую версию. +- Уведомления о доступном обновлении куда-либо кроме стандартной WP-админки. + +## Архитектура + +``` +Локальная разработка + ↓ ./release.sh 1.2.0 +git push origin main --tags +git archive --worktree-attributes → cf7-spam-to-telegram-1.2.0.zip +POST Gitea API /releases → создаёт Release объект +POST Gitea API /releases/{id}/assets → uploads zip + +WP-сайт (плагин 1.2.x уже установлен) + ↓ Plugin::boot() → register_update_checker() + ↓ PUC раз в 12 часов (стандартный WP transient) +GET https://git.netranking.ru/bryzgalov/cf7-spam-to-telegram/raw/branch/main/wp-plugin-info.json + ↓ парсит, сравнивает version с CF7STG_VERSION + ↓ если новее → WP «Плагины → Обновления» показывает кнопку «Обновить» +WP качает zip по download_url (Gitea release asset, public, без auth) + ↓ распаковывает в wp-content/plugins/cf7-spam-to-telegram/ +Плагин обновлён. +``` + +## Изменяемые файлы + +| Файл | Что | +|---|---| +| `includes/lib/plugin-update-checker/` (новая папка, ~50 файлов) | Распакованный PUC v5 release с https://github.com/YahnisElsts/plugin-update-checker. Закоммичен целиком. | +| `wp-plugin-info.json` (новый, корень репо) | Статичная metadata, формат Generic JSON для PUC. | +| `includes/class-plugin.php` | В `boot()` добавляется `self::register_update_checker()`. Новый private method. | +| `.gitattributes` (изменяется) | `export-ignore` правила для всех dev-файлов. | +| `release.sh` (новый, корень, `chmod +x`) | Bash-скрипт релиза. | +| `cf7-spam-to-telegram.php` | Bump до `1.2.0` (header `Version:` + константа `CF7STG_VERSION`). | +| `readme.txt` | Bump `Stable tag: 1.2.0` + Changelog `= 1.2.0 =`. | +| `.gitignore` | Добавляется `dist/`. | + +## `wp-plugin-info.json` — структура + +```json +{ + "name": "CF7 Spam → Telegram", + "slug": "cf7-spam-to-telegram", + "version": "1.2.0", + "download_url": "https://git.netranking.ru/bryzgalov/cf7-spam-to-telegram/releases/download/v1.2.0/cf7-spam-to-telegram-1.2.0.zip", + "homepage": "https://git.netranking.ru/bryzgalov/cf7-spam-to-telegram", + "requires": "6.0", + "tested": "6.9", + "requires_php": "7.4", + "last_updated": "2026-04-21 17:00:00", + "author": "Vladimir Bryzgalov", + "sections": { + "description": "Forwards CF7 spam submissions to Telegram with heuristic classification (client/unclear/spam/drop labels).", + "changelog": "<извлечённый из readme.txt блок == Changelog == ==>" + } +} +``` + +## `Plugin::boot()` изменение + +```php +public function boot(): void +{ + Settings::register_classifier_filters(); + Dispatcher::register_hooks(); + (new CF7Source())->register(); + self::register_update_checker(); // ← новый + + if (is_admin()) { + (new Settings_Page())->register(); + (new Dashboard_Widget())->register(); + } +} + +private static function register_update_checker(): void +{ + $loader = CF7STG_PLUGIN_DIR . 'includes/lib/plugin-update-checker/plugin-update-checker.php'; + if (!is_file($loader)) return; // не падаем, если папка отсутствует + require_once $loader; + if (!class_exists('YahnisElsts\\PluginUpdateChecker\\v5\\PucFactory')) return; + + \YahnisElsts\PluginUpdateChecker\v5\PucFactory::buildUpdateChecker( + 'https://git.netranking.ru/bryzgalov/cf7-spam-to-telegram/raw/branch/main/wp-plugin-info.json', + CF7STG_PLUGIN_FILE, + 'cf7-spam-to-telegram' + ); +} +``` + +## `release.sh` (главные шаги) + +``` +release.sh 1.2.0 → + 1. validate (semver, clean tree, на main, есть ~/.gitea-token) + 2. bump version в cf7-spam-to-telegram.php (header + define) и readme.txt (Stable tag) + 3. update wp-plugin-info.json (version, download_url, last_updated, sections.changelog ← из readme.txt) + 4. git add + git commit -m "release: 1.2.0" + git tag v1.2.0 + git push origin main --tags + 5. mkdir -p dist + git archive --worktree-attributes --format=zip --prefix=cf7-spam-to-telegram/ -o dist/cf7-spam-to-telegram-1.2.0.zip v1.2.0 + 6. POST /api/v1/repos/.../releases (создать Release объект) + 7. POST /api/v1/repos/.../releases/{id}/assets (загрузить zip как asset) + 8. echo URLs (release page, asset URL, PUC JSON URL) +``` + +Token хранится в `~/.gitea-token` (chmod 600), не в репо. Создание токена — единожды, в Gitea UI: `user/settings/applications` → scope `write:repository`. + +## `.gitattributes` правила + +``` +# Исключаем dev-файлы из git archive (release zip) +tests/ export-ignore +docs/ export-ignore +composer.json export-ignore +composer.lock export-ignore +phpunit.xml export-ignore +.phpunit.result.cache export-ignore +.beads/ export-ignore +.claude/ export-ignore +CLAUDE.md export-ignore +AGENTS.md export-ignore +release.sh export-ignore +dist/ export-ignore +.gitignore export-ignore +.gitattributes export-ignore +.DS_Store export-ignore +``` + +**ВАЖНО:** `includes/lib/plugin-update-checker/` НЕ исключаем — она нужна в zip! + +## Тесты + +`tests/UpdateCheckerIntegrationTest.php` (extends `Cf7stgTestCase`): + +- `test_puc_loader_file_exists()` — `includes/lib/plugin-update-checker/plugin-update-checker.php` существует. +- `test_puc_factory_class_loadable_after_require()` — после `require_once` лоадера класс `YahnisElsts\PluginUpdateChecker\v5\PucFactory` доступен. +- `test_wp_plugin_info_json_is_valid()` — `wp-plugin-info.json` парсится, содержит обязательные ключи `name, slug, version, download_url, sections`. +- `test_wp_plugin_info_json_version_matches_plugin_constant()` — `wp-plugin-info.json::version` совпадает с `CF7STG_VERSION` из `cf7-spam-to-telegram.php` (regex extract). Защита: release.sh забыл обновить JSON — тест поймает. +- `test_wp_plugin_info_json_download_url_matches_version()` — `download_url` содержит `/releases/download/v{version}/`. + +`tests/ZipArchiveBuildTest.php` (опционально, требует git в `$PATH`): +- `test_git_archive_includes_runtime_dirs()` — `git archive HEAD --worktree-attributes -o /tmp/test.zip` распаковывается, содержит `cf7-spam-to-telegram.php`, `includes/`, `admin/`, `includes/lib/plugin-update-checker/plugin-update-checker.php`. +- `test_git_archive_excludes_dev_dirs()` — НЕТ `tests/`, `docs/`, `composer.json`, `release.sh`. + +## Error handling + +- Отсутствие PUC папки на сайте: `register_update_checker` делает `is_file` + `class_exists` → плагин не падает, обновлений просто нет (как до 1.2.0). +- Недоступность Gitea (network/down/4xx): PUC сам логирует через `error_log`, ничего не показывает в админке. Следующая итерация (12ч) повторит. Юзер не видит «обновлений» — всё. +- Bad JSON в `wp-plugin-info.json`: PUC скипает, error_log. +- Версия в JSON совпадает с локальной → no update notice. Норм. + +## Rollout + +1. Локально: создать токен в Gitea (один раз), сохранить в `~/.gitea-token`. +2. Локально: подготовить v1.2.0 (vendor PUC, добавить файлы, написать release.sh, тесты). +3. PHPUnit зелёный → `./release.sh 1.2.0` → tag + push + Release + asset. +4. На домработница.рус (где сейчас 1.1.0) — один последний раз через `rsync` обновить до 1.2.0. Активировать плагин (auto-seed уже идемпотентный). PUC начинает работать. +5. Все будущие версии — `./release.sh 1.x.y`, и через 12ч (или после ручного «Проверить обновления») WP-сайты сами обновятся. + +## Версионирование + +- `1.1.0` → `1.2.0` (semver minor: новая фича, обратная совместимость). +- Changelog в `readme.txt`: + ``` + = 1.2.0 = + * New: Self-hosted plugin updates from Gitea via plugin-update-checker (Yahnis Elsts v5). + WP «Плагины → Обновления» теперь показывает обновления автоматически (раз в 12ч), + скачивая чистый zip из Gitea Releases. Источник: + https://git.netranking.ru/bryzgalov/cf7-spam-to-telegram/raw/branch/main/wp-plugin-info.json. + * Dev: release.sh — однострочный релиз (bump version → commit → tag → push → + build clean zip via git archive --worktree-attributes → Gitea Release + asset upload). + * Dev: .gitattributes export-ignore для tests/, docs/, composer.*, phpunit.xml, + .beads/, .claude/, CLAUDE.md, AGENTS.md, release.sh — git archive собирает + чистый zip без dev-файлов. + ``` + +## Открытые вопросы для следующей сессии + +Эти моменты НЕ обсуждены с пользователем явно — могут потребовать одного-двух уточнений перед finalizing spec: + +1. **Какую точно версию PUC v5 vendor'им?** Скачать последний release с https://github.com/YahnisElsts/plugin-update-checker/releases (на 2026-04-21 это видимо `5.x.x`). В spec зафиксировать конкретную версию для воспроизводимости. +2. **Уважает ли Gitea raw URL HTTPS на git.netranking.ru cache headers?** PUC ставит `If-Modified-Since`, важно чтобы сервер не ломал. Проверить вручную через `curl -I https://git.netranking.ru/.../raw/branch/main/wp-plugin-info.json` — если есть `Last-Modified` / `ETag` — отлично. +3. **Минификация PUC?** Vendored PUC ~50 файлов / ~300KB. Не критично, но можно опционально удалить `examples/`, `tests/`, `docs/` из vendored папки. Рассмотреть как nit. +4. **Поддержка Gitea Actions** — пользователь не запрашивал, но если есть желание авто-релизов на push tag — это +1 эпик в будущем.