From 831a8d46cfcabfe3fc1d07ba572c27328f086f34 Mon Sep 17 00:00:00 2001 From: Vladimir Bryzgalov Date: Sun, 17 May 2026 00:48:44 +0500 Subject: [PATCH] spec(wpmc S7): README + Gitea release + PUC v5.6 (mirror cf7stg flow) Co-Authored-By: Claude Opus 4.7 --- .../specs/2026-05-17-s7-release-puc-design.md | 368 ++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-17-s7-release-puc-design.md diff --git a/docs/superpowers/specs/2026-05-17-s7-release-puc-design.md b/docs/superpowers/specs/2026-05-17-s7-release-puc-design.md new file mode 100644 index 0000000..d00fcdb --- /dev/null +++ b/docs/superpowers/specs/2026-05-17-s7-release-puc-design.md @@ -0,0 +1,368 @@ +# S7 — README + Gitea release + plugin-update-checker + +- Beads: `cp-7cf` (parent epic `cp-cw4`) +- Depends on: `cp-la7` (S6 — CI + QA checklist, закрыт) +- Дата: 2026-05-17 + +## Цель + +Закрыть эпик `cp-cw4` (MVP-ядро) поставкой плагина в Gitea как self-hosted WordPress plugin с автоматическими обновлениями через Plugin Update Checker (PUC). Эталон flow — уже работающий `cf7-spam-to-telegram` (PUC v5.6 + `release.sh` + `wp-plugin-info.json` + Gitea releases). + +Версия для первого релиза — **1.0.0** (MVP завершён, semantic). + +S7 — infrastructure + docs only. Один новый приватный метод в `Plugin` (PUC bootstrap). Без новых функциональных классов. + +## Артефакты + +``` +wp-multi-city/ +├── wp-multi-city.php ← MODIFY: bump → 1.0.0, header Tested up to/Requires +├── includes/class-plugin.php ← MODIFY: + register_update_checker() +├── includes/lib/plugin-update-checker/ ← CREATE: vendored PUC v5.6 (copy из cf7stg) +├── wp-plugin-info.json ← CREATE: метаданные для PUC +├── readme.txt ← CREATE: WP.org формат, источник changelog для release.sh +├── CHANGELOG.md ← CREATE: human-readable mirror +├── README.md ← MODIFY: + Quick Start + Hook API + Migration guide +├── release.sh ← CREATE: адаптация cf7stg/release.sh +├── .gitattributes ← CREATE: export-ignore для git archive +└── tests/ + ├── PluginBootS7Test.php ← CREATE: boot() вызывает register_update_checker + ├── UpdateCheckerIntegrationTest.php ← CREATE: PucFactory loadable + └── ZipArchiveBuildTest.php ← CREATE: git archive integrity +``` + +## Plugin Update Checker integration + +### Vendor + +Скопировать `includes/lib/plugin-update-checker/` (~5 МБ, PUC v5.6 от Yahnis Elsts) из `cf7-spam-to-telegram`. Vendored код самодостаточен — нет composer-зависимости на проде. Namespace `YahnisElsts\PluginUpdateChecker\v5p6\*` версионирован — не конфликтует с другими плагинами, использующими PUC. + +### Bootstrap в `Plugin::boot()` + +```php +private static function register_update_checker(): void +{ + $loader = WPMC_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/wp-multi-city/raw/branch/main/wp-plugin-info.json', + WPMC_PLUGIN_FILE, + 'wp-multi-city' + ); +} +``` + +Вызов добавляется в `Plugin::boot()` ПОСЛЕ всех `*::register()` вызовов; защищён существующим `$booted` idempotency-flag-ом (двойной boot → второй `require_once` no-op, второй `buildUpdateChecker` не выполнится из-за `$booted` guard). + +### `wp-plugin-info.json` + +```json +{ + "name": "WP Multi City", + "slug": "wp-multi-city", + "version": "1.0.0", + "download_url": "https://git.netranking.ru/bryzgalov/wp-multi-city/releases/download/v1.0.0/wp-multi-city-1.0.0.zip", + "homepage": "https://git.netranking.ru/bryzgalov/wp-multi-city", + "requires": "6.0", + "tested": "6.9", + "requires_php": "8.0", + "last_updated": "2026-05-17 00:00:00", + "author": "Vladimir Bryzgalov", + "sections": { + "description": "Мультигородская подсистема: taxonomy town, [shortcode_dmr] для подстановок, URL-роутер /{city}/{path}/, ACF-driven page clones.", + "changelog": "

1.0.0

" + } +} +``` + +`release.sh` перегенерирует `download_url`, `version`, `last_updated`, `sections.changelog` (HTML из `readme.txt`) после каждого релиза. + +## release.sh + +Адаптация `cf7-spam-to-telegram/release.sh` (9 шагов). Изменения: + +```bash +REPO_OWNER="bryzgalov" +REPO_NAME="wp-multi-city" +GITEA_BASE="https://git.netranking.ru" +``` + +**STEP 2 — bump:** + +```bash +sed -i.bak -E \ + -e "s|^([[:space:]]*\\*[[:space:]]*Version:[[:space:]]*).*$|\\1${VERSION}|" \ + -e "s|define\\('WPMC_VERSION',[[:space:]]*'[^']+'\\)|define('WPMC_VERSION', '${VERSION}')|" \ + wp-multi-city.php +rm wp-multi-city.php.bak + +grep -q "^ \* Version: ${VERSION}$" wp-multi-city.php +grep -q "define('WPMC_VERSION', '${VERSION}')" wp-multi-city.php + +sed -i.bak -E "s|^(Stable tag:[[:space:]]*).*$|\\1${VERSION}|" readme.txt +rm readme.txt.bak +grep -q "^Stable tag: ${VERSION}$" readme.txt +``` + +**STEP 4 — zip prefix `wp-multi-city/`**, sanity-list (in/out): + +```bash +# MUST be in zip: +unzip -p ... wp-multi-city/wp-multi-city.php | grep "^ \* Version: ${VERSION}$" +unzip -p ... wp-multi-city/wp-multi-city.php | grep "WPMC_VERSION', '${VERSION}'" +grep "wp-multi-city/includes/lib/plugin-update-checker/plugin-update-checker.php" <<< "$ZIP_LISTING" +grep "wp-multi-city/wp-plugin-info.json" <<< "$ZIP_LISTING" + +# MUST NOT be in zip: +for dev in tests/ docs/ composer.json composer.lock phpunit.xml.dist patchwork.json \ + release.sh CLAUDE.md AGENTS.md .gitattributes .gitignore .gitea/ ; do + grep "wp-multi-city/${dev}" <<< "$ZIP_LISTING" && fail "dev file in zip: $dev" +done +``` + +Шаги 1, 3, 5–9 (валидация → git push → Gitea API create release → upload zip → regenerate `wp-plugin-info.json` → JSON commit + push → summary) — копия cf7stg без структурных изменений. Только `${REPO_NAME}` подставляется в URL. + +### Prerequisites (валидируются в STEP 1) + +- `~/.gitea-token` со scope `write:repository` (re-use от cf7stg). +- Чистый git tree на `main`, in sync с `origin/main`. +- `readme.txt` содержит секцию `= ${VERSION} =` (fail-fast перед side effects). +- Команды в PATH: `git curl jq unzip awk sed`. + +## .gitattributes + +``` +# Exclude dev files from `git archive` (release.sh build) +/tests/ export-ignore +/docs/ export-ignore +/phpunit.xml.dist export-ignore +/patchwork.json export-ignore +/composer.json export-ignore +/composer.lock export-ignore +/.phpunit.result.cache export-ignore +/.beads/ export-ignore +/.claude/ export-ignore +/.gitea/ 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 + +# NOTE: includes/lib/plugin-update-checker/ MUST NOT be excluded — +# required at runtime on target WP site. +``` + +## Документация + +Три файла, три аудитории: + +| Файл | Формат | Аудитория | Назначение | +|---|---|---|---| +| `README.md` | Markdown | Разработчик в Gitea | Quick Start, Hook API, Migration guide, roadmap | +| `readme.txt` | WP.org | `release.sh` + WP admin | `Stable tag`, `Tested up to`, Changelog в формате `= 1.0.0 =` | +| `CHANGELOG.md` | Markdown | Human readers | Mirror `readme.txt` changelog в Markdown | + +`readme.txt` — единственный источник правды для версии и changelog (`release.sh` парсит и `Stable tag`, и блоки `= X.Y.Z =`). `CHANGELOG.md` ведётся вручную при bump. + +### README.md — целевая структура + +```markdown +# WP Multi City + +Мультигородская подсистема для WordPress: один сайт обслуживает несколько городов +по схеме `/{city}/{path}/` с подстановкой контента, ссылок и шорткодов. + +## Требования +- WordPress 6.0+ +- PHP 8.0+ +- Advanced Custom Fields Pro + +## Установка +1. Скачать zip с https://git.netranking.ru/bryzgalov/wp-multi-city/releases +2. WP admin → Plugins → Add new → Upload (или распаковать в wp-content/plugins/) +3. Активировать. Обновления приходят автоматически (PUC опрашивает Gitea hourly). + +## Quick Start (5 минут) +1. WP admin → Шорткоды → Города → создать term `tyumen`, `spb`. +2. WP admin → WPMultiCity → main_town_slug = `spb`. +3. WP admin → Шорткоды → Добавить пост, repeater row town=tyumen text="Тюменский тариф". +4. Создать страницу с `[shortcode_dmr id=42]` → открыть на `/tyumen//` → видна подстановка. + +## Hook API +### Процедурные функции (wpmc_*) +- `wpmc_current_town(): ?string` — текущий town slug из URL. +- `wpmc_get_term_by_slug(string $slug): ?\WP_Term` +- `wpmc_alt_get_field(string $key, $post_id): mixed` — get_field с fallback. +- `wpmc_remove_domain_link(string $url): string` +- `wpmc_main_town_slug(): ?string` + +### WP-фильтры (от плагина) +- `the_title`/`single_cat_title` → `do_shortcode` (priority 9999) +- `home_url`/`post_link`/`acf/load_value` → `LinkFilter::add_town_prefix` +- `rest_url` → `RestUrlFilter::preserve_host` + +### Bypass URL-фильтров +\`\`\`php +$canonical = \WPMultiCity\Helpers::without_url_filters( + fn() => get_permalink($post_id) +); +\`\`\` + +## Migration guide (из темы washanyanya) +1. Закомментировать `require inc/town-functions.php` в теме. +2. Активировать wp-multi-city. +3. ACF Pro field group из темы → re-import в новую location (CPT shortcode-town). +4. Заменить `current_town()` → `wpmc_current_town()` (или прописать алиас). +5. Проверить: `/tyumen/about/` отдаёт правильный page; меню префиксится; `[shortcode_dmr]` рендерит. + +## Testing +[существующая секция: composer install; vendor/bin/phpunit; docs/QA-CHECKLIST.md] + +## CI +[существующая секция: .gitea/workflows/test.yml] + +## Roadmap MVP +| Этап | Содержание | Beads | Статус | +| S1 | Скелет | cp-8ci | done | +| S2 | Taxonomy + CPT + ACF | cp-q7g | done | +| S3 | [shortcode_dmr] + хелперы | cp-arq | done | +| S4 | URL-роутер | cp-83m | done | +| S5 | Глобальные фильтры | cp-i17 | done | +| S6 | CI + QA checklist | cp-la7 | done | +| S7 | Gitea release + PUC | cp-7cf | done | + +Эпик `cp-cw4` — CLOSED. +``` + +### readme.txt — целевая структура + +``` +=== WP Multi City === +Contributors: vbryzgalov +Tags: multisite, multi-city, taxonomy, acf, shortcode +Requires at least: 6.0 +Tested up to: 6.9 +Requires PHP: 8.0 +Stable tag: 1.0.0 +License: GPLv2 or later + +== Description == +Мультигородская подсистема: одна WP-инсталляция обслуживает несколько городов +по схеме /{city}/{path}/ с подстановками контента и URL. + +== Installation == +1. Загрузить wp-multi-city в /wp-content/plugins/. +2. Активировать в WP admin. +3. Требуется ACF Pro. + +== Changelog == + += 1.0.0 = +* Первый релиз. MVP-ядро завершено (S1–S7 эпика cp-cw4). +* taxonomy town + CPT shortcode-town + ACF list_shortcode repeater. +* [shortcode_dmr id=N] для подстановок по городу. +* URL-роутер /{city}/{path}/ с поддержкой page_unic/main_page_alt/page_no_rep. +* Глобальные фильтры the_title, home_url, post_link, acf/load_value, rest_url. +* Self-hosted updates через Gitea + plugin-update-checker v5.6. +``` + +### CHANGELOG.md — целевая структура + +```markdown +# Changelog + +All notable changes to wp-multi-city. Format mirrors `readme.txt`. + +## [1.0.0] — 2026-05-17 + +### Added +- Первый релиз. MVP-ядро завершено (S1–S7 эпика cp-cw4). +- `taxonomy town` + CPT `shortcode-town` + ACF `list_shortcode` repeater. +- `[shortcode_dmr id=N]` для подстановок по городу. +- URL-роутер `/{city}/{path}/` (page_unic, main_page_alt, page_no_rep). +- Глобальные фильтры (the_title, home_url, post_link, acf/load_value, rest_url). +- Self-hosted updates через Gitea + plugin-update-checker v5.6. +``` + +## wp-multi-city.php — изменения + +```php +/** + * Plugin Name: WP Multi City + * Description: Мультигородская подсистема: taxonomy town, шорткоды, URL-роутер /{city}/{path}/, ACF page-clones. + * Version: 1.0.0 + * Author: Vladimir Bryzgalov + * Requires PHP: 8.0 + * Requires at least: 6.0 + * Tested up to: 6.9 + * License: GPLv2 or later + * Text Domain: wp-multi-city + */ +... +defined('WPMC_VERSION') || define('WPMC_VERSION', '1.0.0'); +``` + +## Тестовый план + +### `tests/PluginBootS7Test.php` (2 теста) + +1. `boot()` загружает PUC factory: после `Plugin::instance()->boot()` `class_exists('YahnisElsts\\PluginUpdateChecker\\v5\\PucFactory')` возвращает true. +2. `boot()` идемпотентен для PUC: двойной `boot()` не бросает (защита от double-require + double-buildUpdateChecker — гарантируется `$booted` flag-ом). + +### `tests/UpdateCheckerIntegrationTest.php` (2 теста) + +3. `tests/UpdateCheckerIntegrationTest::test_puc_loader_exists`: файл `includes/lib/plugin-update-checker/plugin-update-checker.php` существует. +4. `tests/UpdateCheckerIntegrationTest::test_puc_factory_loadable`: после `require_once` PUC loader-а, `PucFactory` класс загружается. + +### `tests/ZipArchiveBuildTest.php` (2 теста) + +5. `git archive HEAD --worktree-attributes --format=zip --prefix=wp-multi-city/`: + - В zip присутствуют: `wp-multi-city.php`, `includes/lib/plugin-update-checker/plugin-update-checker.php`, `wp-plugin-info.json`, `readme.txt`, `README.md`. + - В zip отсутствуют: `tests/`, `docs/`, `composer.json`, `phpunit.xml.dist`, `patchwork.json`, `release.sh`, `CLAUDE.md`, `.gitattributes`, `.gitea/`. +6. `wp-plugin-info.json` — валидный JSON с обязательными ключами (`slug=wp-multi-city`, `version`, `download_url` содержит `git.netranking.ru/bryzgalov/wp-multi-city`). + +**Итого:** 98 + 6 = **104 теста / ~185 assertions**. + +## Acceptance verification + +1. `vendor/bin/phpunit` — 104 теста зелёные. +2. `git archive HEAD --worktree-attributes --format=zip --prefix=wp-multi-city/ -o /tmp/wpmc-test.zip && unzip -l /tmp/wpmc-test.zip` — никаких dev-файлов в архиве, PUC loader на месте, `wp-plugin-info.json` на месте. +3. `wp-multi-city.php` header: `Version: 1.0.0`, константа `WPMC_VERSION = '1.0.0'`. +4. `readme.txt` валидный: `Stable tag: 1.0.0`, секция `= 1.0.0 =` существует, `awk` извлечение возвращает не-пустой changelog body. +5. README обновлён: Quick Start, Hook API, Migration guide на месте; roadmap S7 = done; эпик cp-cw4 помечен CLOSED. +6. PHP-lint clean: `find includes -name '*.php' -not -path '*/lib/*' -exec php -l {} \;`. +7. (Manual, after S7 merge) Gitea репо `bryzgalov/wp-multi-city` создан; `./release.sh 1.0.0` отрабатывает все 9 шагов без ошибок; в Gitea появляется release `v1.0.0` с zip-asset; URL `https://git.netranking.ru/bryzgalov/wp-multi-city/raw/branch/main/wp-plugin-info.json` отдаёт обновлённый JSON. + +## Gitea репозиторий — ручные шаги (вне плана) + +1. В Gitea UI (`git.netranking.ru/bryzgalov`) → New Repository → name `wp-multi-city`, public/private по выбору, без README (он у нас уже есть локально). +2. На локальной машине после merge S7: + ```bash + cd wp-multi-city + git remote add origin https://git.netranking.ru/bryzgalov/wp-multi-city.git + git push -u origin main + ``` +3. `~/.gitea-token` уже существует от cf7stg релизов — используется как есть. Token scope: `write:repository`. +4. После первого push: `./release.sh 1.0.0` создаёт релиз `v1.0.0`. + +## Что НЕ входит в S7 + +- Программное создание Gitea-репозитория (ручной шаг через UI). +- PHP matrix в CI (8.0/8.1/8.2/8.3) — отложено. +- Lint job (phpcs/phpstan) в CI — отложено. +- Composer-based PUC (vendor-в-git проще: нет `composer install` на проде). +- Migration guide для тем кроме washanyanya (универсальный — out of MVP). +- Translations / `.pot` файл — out of MVP. + +## Open questions + +Нет. Все решения зафиксированы.