docs(spec): DRAFT design for self-hosted updates from Gitea (1.2.0)

Brainstorm session ran out of context — saving fully-formed design as
DRAFT so the next session can resume from spec self-review + finalize +
writing-plans without redoing the brainstorm.

Beads epic: cf7stg-5wy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vladimir Bryzgalov
2026-04-21 18:46:35 +05:00
parent 4dbf3a6ded
commit f7d81a4517
@@ -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 эпик в будущем.