011ccb8dbf
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1153 lines
37 KiB
Markdown
1153 lines
37 KiB
Markdown
# S7 — Release flow + PUC Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Закрыть эпик cp-cw4 self-hosted поставкой wp-multi-city в Gitea: vendored PUC v5.6, `release.sh` (адаптация cf7stg), расширенный README, version bump 0.1.0 → 1.0.0.
|
||
|
||
**Architecture:** Infrastructure + docs only. Один новый приватный метод `Plugin::register_update_checker()` для PUC bootstrap, всё остальное — файлы. Эталон — `cf7-spam-to-telegram` (то же owner, то же Gitea, тот же flow).
|
||
|
||
**Tech Stack:** PHP 8.0+, PHPUnit 9.6 + Brain Monkey 2.7, Plugin Update Checker v5.6 (Yahnis Elsts), Gitea Actions, git archive + bash + jq + curl для release.sh.
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-05-17-s7-release-puc-design.md`
|
||
|
||
**Beads:** parent `cp-7cf` (epic `cp-cw4`).
|
||
|
||
**Critical Path:**
|
||
1. Vendor PUC (без него нет updates)
|
||
2. PUC bootstrap в Plugin (без него PUC mертв)
|
||
3. `wp-plugin-info.json` (метаданные для PUC)
|
||
4. Version bump (1.0.0)
|
||
5. `.gitattributes` (без него dev-файлы попадут в zip)
|
||
6. `readme.txt` (источник правды для release.sh)
|
||
7. `CHANGELOG.md` (human-readable mirror)
|
||
8. `README.md` (Quick Start + Hook API + Migration guide)
|
||
9. `ZipArchiveBuildTest` (проверяет всё вместе)
|
||
10. `release.sh` (использует всё вышеперечисленное)
|
||
|
||
---
|
||
|
||
## Task 1: Vendor Plugin Update Checker v5.6
|
||
|
||
**Files:**
|
||
- Create: `includes/lib/plugin-update-checker/` (целая папка из cf7stg)
|
||
- Test: `tests/UpdateCheckerIntegrationTest.php`
|
||
|
||
**Source:** `/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/cf7-spam-to-telegram/includes/lib/plugin-update-checker/`
|
||
|
||
- [ ] **Step 1: Copy PUC vendor directory**
|
||
|
||
```bash
|
||
cp -R "/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/cf7-spam-to-telegram/includes/lib/plugin-update-checker" \
|
||
"/Users/vladimirbryzgalov/Yandex.Disk.localized/Работа/Сайты/+Разработка сайтов+/Плагины Wordpress/wp-multi-city/includes/lib/"
|
||
```
|
||
|
||
- [ ] **Step 2: Verify copy succeeded**
|
||
|
||
```bash
|
||
ls includes/lib/plugin-update-checker/plugin-update-checker.php
|
||
ls includes/lib/plugin-update-checker/load-v5p6.php
|
||
```
|
||
|
||
Expected: both files present, no errors.
|
||
|
||
- [ ] **Step 3: Write the failing test**
|
||
|
||
Create `tests/UpdateCheckerIntegrationTest.php`:
|
||
|
||
```php
|
||
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace WPMultiCity\Tests;
|
||
|
||
use PHPUnit\Framework\TestCase as BaseTestCase;
|
||
|
||
final class UpdateCheckerIntegrationTest extends BaseTestCase
|
||
{
|
||
private const PUC_LOADER = __DIR__ . '/../includes/lib/plugin-update-checker/plugin-update-checker.php';
|
||
|
||
public function test_puc_loader_exists(): void
|
||
{
|
||
self::assertFileExists(self::PUC_LOADER);
|
||
}
|
||
|
||
public function test_puc_factory_loadable(): void
|
||
{
|
||
require_once self::PUC_LOADER;
|
||
self::assertTrue(
|
||
class_exists('YahnisElsts\\PluginUpdateChecker\\v5\\PucFactory'),
|
||
'PucFactory must be loadable after requiring the PUC loader'
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Run test to verify both pass**
|
||
|
||
Run: `vendor/bin/phpunit --filter UpdateCheckerIntegrationTest`
|
||
Expected: OK (2 tests, 2 assertions).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add includes/lib/plugin-update-checker tests/UpdateCheckerIntegrationTest.php
|
||
git commit -m "feat(wpmc S7): vendor Plugin Update Checker v5.6"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: Create wp-plugin-info.json
|
||
|
||
**Files:**
|
||
- Create: `wp-plugin-info.json`
|
||
|
||
- [ ] **Step 1: Create initial JSON**
|
||
|
||
Create `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": "<h4>1.0.0</h4><ul><li>Первый релиз. MVP-ядро S1–S7.</li></ul>"
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Validate JSON**
|
||
|
||
Run: `jq empty wp-plugin-info.json && echo OK`
|
||
Expected: `OK`
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add wp-plugin-info.json
|
||
git commit -m "feat(wpmc S7): wp-plugin-info.json metadata for PUC"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: Plugin::register_update_checker() + boot integration
|
||
|
||
**Files:**
|
||
- Modify: `includes/class-plugin.php` (add private static method + call in boot())
|
||
- Create: `tests/PluginBootS7Test.php`
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
Create `tests/PluginBootS7Test.php`:
|
||
|
||
```php
|
||
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace WPMultiCity\Tests;
|
||
|
||
use Brain\Monkey\Functions;
|
||
use ReflectionClass;
|
||
use WPMultiCity\Plugin;
|
||
|
||
final class PluginBootS7Test extends TestCase
|
||
{
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
$reflection = new ReflectionClass(Plugin::class);
|
||
$prop = $reflection->getProperty('booted');
|
||
$prop->setAccessible(true);
|
||
$prop->setValue(null, false);
|
||
}
|
||
|
||
public function test_boot_loads_puc_factory(): void
|
||
{
|
||
Functions\stubs([
|
||
'register_taxonomy' => null,
|
||
'register_post_type' => null,
|
||
'add_action' => null,
|
||
'add_filter' => null,
|
||
'is_admin' => false,
|
||
'plugin_basename' => 'wp-multi-city/wp-multi-city.php',
|
||
]);
|
||
|
||
Plugin::instance()->boot();
|
||
|
||
self::assertTrue(
|
||
class_exists('YahnisElsts\\PluginUpdateChecker\\v5\\PucFactory'),
|
||
'boot() should require PUC loader and make PucFactory available'
|
||
);
|
||
}
|
||
|
||
public function test_boot_double_call_is_idempotent(): void
|
||
{
|
||
Functions\stubs([
|
||
'register_taxonomy' => null,
|
||
'register_post_type' => null,
|
||
'add_action' => null,
|
||
'add_filter' => null,
|
||
'is_admin' => false,
|
||
'plugin_basename' => 'wp-multi-city/wp-multi-city.php',
|
||
]);
|
||
|
||
Plugin::instance()->boot();
|
||
Plugin::instance()->boot();
|
||
|
||
self::assertTrue(true, 'Double boot must not throw');
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `vendor/bin/phpunit --filter PluginBootS7Test`
|
||
Expected: FAIL — PUC not registered by Plugin::boot() yet.
|
||
|
||
- [ ] **Step 3: Read current class-plugin.php to find insertion point**
|
||
|
||
```bash
|
||
grep -n "RestUrlFilter::register" includes/class-plugin.php
|
||
grep -n "private bool \$booted" includes/class-plugin.php
|
||
```
|
||
|
||
Expected: `RestUrlFilter::register();` is the last register call inside `boot()`. `$booted` flag exists.
|
||
|
||
- [ ] **Step 4: Add register_update_checker method and call**
|
||
|
||
In `includes/class-plugin.php`, after the existing `RestUrlFilter::register();` line inside `boot()`, add a new line:
|
||
|
||
```php
|
||
self::register_update_checker();
|
||
```
|
||
|
||
Then add the new private method right after `boot()` (or wherever other private static methods sit; if none, add at the end of the class before the closing brace):
|
||
|
||
```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'
|
||
);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: Run S7 tests**
|
||
|
||
Run: `vendor/bin/phpunit --filter PluginBootS7Test`
|
||
Expected: OK (2 tests, 2 assertions).
|
||
|
||
- [ ] **Step 6: Run full suite to verify nothing broke**
|
||
|
||
Run: `vendor/bin/phpunit`
|
||
Expected: OK (102 tests, 179 assertions) — 98 prior + 2 from Task 1 + 2 from this task.
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
git add includes/class-plugin.php tests/PluginBootS7Test.php
|
||
git commit -m "feat(wpmc S7): Plugin::register_update_checker wires PUC v5.6"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: Bump version 0.1.0 → 1.0.0
|
||
|
||
**Files:**
|
||
- Modify: `wp-multi-city.php` (header Version, WPMC_VERSION constant, add Tested up to)
|
||
|
||
- [ ] **Step 1: Edit wp-multi-city.php**
|
||
|
||
Replace plugin header and version constant. Final state of relevant lines:
|
||
|
||
```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
|
||
*/
|
||
```
|
||
|
||
```php
|
||
defined('WPMC_VERSION') || define('WPMC_VERSION', '1.0.0');
|
||
```
|
||
|
||
Apply via Edit tool (two replacements: `Version: 0.1.0` → `Version: 1.0.0` and add `Tested up to: 6.9` line; `'0.1.0'` → `'1.0.0'` in constant).
|
||
|
||
- [ ] **Step 2: Verify with grep**
|
||
|
||
```bash
|
||
grep -E "^ \* Version: 1\.0\.0$" wp-multi-city.php
|
||
grep -E "^ \* Tested up to: 6\.9$" wp-multi-city.php
|
||
grep "WPMC_VERSION', '1.0.0'" wp-multi-city.php
|
||
```
|
||
|
||
Expected: all three lines printed (each is a match).
|
||
|
||
- [ ] **Step 3: Run full test suite (no test changes, but verify nothing broke)**
|
||
|
||
Run: `vendor/bin/phpunit`
|
||
Expected: OK (102 tests, 179 assertions).
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add wp-multi-city.php
|
||
git commit -m "feat(wpmc S7): bump version to 1.0.0 + Tested up to 6.9"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: .gitattributes for git archive
|
||
|
||
**Files:**
|
||
- Create: `.gitattributes`
|
||
|
||
- [ ] **Step 1: Create .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.
|
||
```
|
||
|
||
- [ ] **Step 2: Smoke test git archive**
|
||
|
||
```bash
|
||
git add .gitattributes
|
||
git stash
|
||
git archive HEAD --worktree-attributes --format=zip --prefix=wp-multi-city/ -o /tmp/wpmc-smoke.zip
|
||
unzip -l /tmp/wpmc-smoke.zip | grep -E "(tests/|docs/|composer\.json|release\.sh)" || echo "OK: no dev files in archive"
|
||
unzip -l /tmp/wpmc-smoke.zip | grep "includes/lib/plugin-update-checker/plugin-update-checker.php"
|
||
git stash pop
|
||
rm /tmp/wpmc-smoke.zip
|
||
```
|
||
|
||
Expected: `OK: no dev files in archive` printed; PUC loader path printed.
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git commit -m "feat(wpmc S7): .gitattributes export-ignore for git archive build"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: readme.txt (WordPress.org format)
|
||
|
||
**Files:**
|
||
- Create: `readme.txt`
|
||
|
||
- [ ] **Step 1: Create 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
|
||
|
||
Мультигородская подсистема: одна WP-инсталляция обслуживает несколько городов по схеме /{city}/{path}/ с подстановками контента и URL.
|
||
|
||
== Description ==
|
||
|
||
Решает задачу мультигородского сайта без WordPress Multisite. Один WP, единая БД, города представлены термами `town` таксономии. URL формы `/{city}/{path}/` патчат `$wp_query`, чтобы загрузить нужную страницу. Контент по городу подставляется через `[shortcode_dmr id=N]` (ACF repeater `list_shortcode`).
|
||
|
||
Требует Advanced Custom Fields Pro.
|
||
|
||
== Installation ==
|
||
|
||
1. Загрузите архив wp-multi-city в `/wp-content/plugins/`.
|
||
2. Активируйте плагин в WP admin → Plugins.
|
||
3. Если ACF Pro не активен — увидите admin notice об ошибке зависимости.
|
||
|
||
== 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.
|
||
```
|
||
|
||
- [ ] **Step 2: Verify Stable tag + changelog section**
|
||
|
||
```bash
|
||
grep -E "^Stable tag: 1\.0\.0$" readme.txt
|
||
grep -E "^=[[:space:]]*1\.0\.0[[:space:]]*=[[:space:]]*$" readme.txt
|
||
```
|
||
|
||
Expected: both lines printed.
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add readme.txt
|
||
git commit -m "docs(wpmc S7): readme.txt WP.org format with 1.0.0 changelog"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: CHANGELOG.md
|
||
|
||
**Files:**
|
||
- Create: `CHANGELOG.md`
|
||
|
||
- [ ] **Step 1: Create 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.
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git add CHANGELOG.md
|
||
git commit -m "docs(wpmc S7): CHANGELOG.md mirror of readme.txt changelog"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: Расширение README.md
|
||
|
||
**Files:**
|
||
- Modify: `README.md` (полная переработка с добавлением Quick Start, Hook API, Migration guide; roadmap S7 → done)
|
||
|
||
- [ ] **Step 1: Replace README.md content**
|
||
|
||
Полностью переписать `README.md` со следующим содержимым:
|
||
|
||
```markdown
|
||
# WP Multi City
|
||
|
||
Мультигородская подсистема для WordPress: один сайт обслуживает несколько городов
|
||
по схеме `/{city}/{path}/` с подстановкой контента, ссылок и шорткодов.
|
||
|
||
**Версия:** 1.0.0 — MVP завершён.
|
||
|
||
## Требования
|
||
|
||
- 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/wp-multi-city/`).
|
||
3. Активировать. Если ACF Pro не активен — увидите admin notice об ошибке зависимости.
|
||
|
||
Обновления приходят автоматически: плагин-update-checker опрашивает Gitea hourly.
|
||
Force-check: WP admin → Plugins → «Check again».
|
||
|
||
## Quick Start (5 минут)
|
||
|
||
1. WP admin → **Шорткоды → Города** → создать term `tyumen`, `spb`.
|
||
2. WP admin → **WP Multi City** → выставить `main_town_slug` = `spb`.
|
||
3. WP admin → **Шорткоды → Добавить** пост:
|
||
- title: `Тарифы`
|
||
- ACF repeater `list_shortcode`: row town=tyumen, text="Тюменский тариф".
|
||
- Запомнить ID поста (например 42).
|
||
4. Создать страницу с контентом `[shortcode_dmr id=42]`.
|
||
5. Открыть `/tyumen/<slug-страницы>/` → увидеть «Тюменский тариф».
|
||
Открыть `/<slug-страницы>/` без префикса → пусто (main_town=spb, не tyumen).
|
||
|
||
## Hook API
|
||
|
||
### Процедурные функции
|
||
|
||
| Функция | Возвращает | Назначение |
|
||
|---|---|---|
|
||
| `wpmc_current_town()` | `?string` | town slug из текущего URL или null |
|
||
| `wpmc_get_term_by_slug($slug)` | `?\WP_Term` | `town`-term по slug |
|
||
| `wpmc_alt_get_field($key, $post_id)` | `mixed` | `get_field` с fallback на term meta |
|
||
| `wpmc_remove_domain_link($url)` | `string` | strip `home_url` prefix |
|
||
| `wpmc_main_town_slug()` | `?string` | slug из options page |
|
||
|
||
### WP-фильтры от плагина
|
||
|
||
- `the_title` / `single_cat_title` → `do_shortcode` (priority 9999, после Yoast SEO).
|
||
- `home_url` / `post_link` / `acf/load_value` → `LinkFilter::add_town_prefix` (auto-prefix URL текущим town).
|
||
- `rest_url` → `RestUrlFilter::preserve_host` (форсирует HTTPS + `HTTP_HOST`).
|
||
|
||
### Bypass URL-фильтров
|
||
|
||
Когда нужно построить «канонический» URL без городского префикса:
|
||
|
||
```php
|
||
$canonical = \WPMultiCity\Helpers::without_url_filters(
|
||
fn() => get_permalink($post_id)
|
||
);
|
||
```
|
||
|
||
Counter-based, поддерживает вложенные вызовы.
|
||
|
||
## Migration guide (из темы washanyanya)
|
||
|
||
1. В `wp-content/themes/washanyanya/functions.php` закомментировать
|
||
`require_once get_template_directory() . '/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()`. Либо прописать
|
||
алиас в functions.php темы:
|
||
```php
|
||
if (function_exists('wpmc_current_town')) {
|
||
function current_town(): ?string { return wpmc_current_town(); }
|
||
}
|
||
```
|
||
5. Проверить: `/tyumen/about/` отдаёт правильный page; меню префиксится;
|
||
`[shortcode_dmr]` рендерит.
|
||
|
||
## Testing
|
||
|
||
### Automated (PHPUnit)
|
||
|
||
```bash
|
||
composer install
|
||
vendor/bin/phpunit
|
||
```
|
||
|
||
Expected: 104 tests, ~185 assertions green. Unit-level coverage via Brain Monkey
|
||
+ Mockery + Patchwork — no real WordPress required; полный suite за <1 секунду.
|
||
|
||
### Manual QA
|
||
|
||
Для end-to-end проверки против реального WordPress см. `docs/QA-CHECKLIST.md` —
|
||
13 секций (активация, admin UI, helpers, shortcode, router, page clones,
|
||
URL filters, REST URL).
|
||
|
||
## CI
|
||
|
||
`.gitea/workflows/test.yml` запускает PHPUnit на каждый push в `main` и каждый PR.
|
||
Активируется автоматически после push в Gitea.
|
||
|
||
## Roadmap MVP
|
||
|
||
| Этап | Содержание | Beads | Статус |
|
||
|---|---|---|---|
|
||
| S1 | Скелет (header, autoload, bootstrap) | cp-8ci | done |
|
||
| S2 | Taxonomy `town` + CPT `shortcode-town` + ACF `list_shortcode` | cp-q7g | done |
|
||
| S3 | `[shortcode_dmr]` + хелперы | cp-arq | done |
|
||
| S4 | URL-роутер `/{city}/{path}/` | cp-83m | done |
|
||
| S5 | Глобальные фильтры (`the_title`/URL/`rest_url`) | cp-i17 | done |
|
||
| S6 | CI workflow + manual QA checklist | cp-la7 | done |
|
||
| S7 | Gitea release + plugin-update-checker | cp-7cf | done |
|
||
|
||
Эпик: `cp-cw4` — MVP CLOSED.
|
||
|
||
## Источник
|
||
|
||
Плагин — переработка кастомной мультигородской подсистемы темы `washanyanya`
|
||
(1555 строк `inc/town-functions.php`) в переиспользуемый плагин для других проектов.
|
||
|
||
## Лицензия
|
||
|
||
GPLv2 или новее.
|
||
```
|
||
|
||
- [ ] **Step 2: Verify content**
|
||
|
||
```bash
|
||
grep "Quick Start (5 минут)" README.md
|
||
grep "Hook API" README.md
|
||
grep "Migration guide" README.md
|
||
grep "S7 | Gitea release" README.md
|
||
```
|
||
|
||
Expected: all four lines printed.
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add README.md
|
||
git commit -m "docs(wpmc S7): README expand with Quick Start + Hook API + Migration guide"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 9: ZipArchiveBuildTest — verify git archive output
|
||
|
||
**Files:**
|
||
- Create: `tests/ZipArchiveBuildTest.php`
|
||
|
||
- [ ] **Step 1: Write the failing test**
|
||
|
||
Create `tests/ZipArchiveBuildTest.php`:
|
||
|
||
```php
|
||
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace WPMultiCity\Tests;
|
||
|
||
use PHPUnit\Framework\TestCase as BaseTestCase;
|
||
|
||
final class ZipArchiveBuildTest extends BaseTestCase
|
||
{
|
||
private const PLUGIN_ROOT = __DIR__ . '/..';
|
||
private const ARCHIVE_PREFIX = 'wp-multi-city/';
|
||
|
||
public function test_git_archive_includes_runtime_files_excludes_dev_files(): void
|
||
{
|
||
$tmp = tempnam(sys_get_temp_dir(), 'wpmc-zip-');
|
||
unlink($tmp);
|
||
$zipPath = $tmp . '.zip';
|
||
|
||
$cmd = sprintf(
|
||
'cd %s && git archive HEAD --worktree-attributes --format=zip --prefix=%s -o %s 2>&1',
|
||
escapeshellarg(self::PLUGIN_ROOT),
|
||
escapeshellarg(self::ARCHIVE_PREFIX),
|
||
escapeshellarg($zipPath)
|
||
);
|
||
exec($cmd, $out, $rc);
|
||
self::assertSame(0, $rc, 'git archive failed: ' . implode("\n", $out));
|
||
|
||
exec('unzip -l ' . escapeshellarg($zipPath), $listing);
|
||
$joined = implode("\n", $listing);
|
||
|
||
// MUST be in archive
|
||
$required = [
|
||
'wp-multi-city/wp-multi-city.php',
|
||
'wp-multi-city/includes/lib/plugin-update-checker/plugin-update-checker.php',
|
||
'wp-multi-city/wp-plugin-info.json',
|
||
'wp-multi-city/readme.txt',
|
||
'wp-multi-city/README.md',
|
||
];
|
||
foreach ($required as $path) {
|
||
self::assertStringContainsString($path, $joined, "missing in archive: {$path}");
|
||
}
|
||
|
||
// MUST NOT be in archive
|
||
$forbidden = [
|
||
'tests/',
|
||
'docs/',
|
||
'composer.json',
|
||
'phpunit.xml.dist',
|
||
'patchwork.json',
|
||
'release.sh',
|
||
'CLAUDE.md',
|
||
'.gitattributes',
|
||
'.gitea/',
|
||
];
|
||
foreach ($forbidden as $path) {
|
||
self::assertStringNotContainsString(
|
||
'wp-multi-city/' . $path,
|
||
$joined,
|
||
"dev file leaked into archive: {$path}"
|
||
);
|
||
}
|
||
|
||
unlink($zipPath);
|
||
}
|
||
|
||
public function test_wp_plugin_info_json_is_valid(): void
|
||
{
|
||
$path = self::PLUGIN_ROOT . '/wp-plugin-info.json';
|
||
self::assertFileExists($path);
|
||
|
||
$json = file_get_contents($path);
|
||
self::assertIsString($json);
|
||
|
||
$data = json_decode($json, true);
|
||
self::assertIsArray($data, 'wp-plugin-info.json must be valid JSON');
|
||
self::assertSame('wp-multi-city', $data['slug']);
|
||
self::assertArrayHasKey('version', $data);
|
||
self::assertArrayHasKey('download_url', $data);
|
||
self::assertStringContainsString(
|
||
'git.netranking.ru/bryzgalov/wp-multi-city',
|
||
$data['download_url']
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run test (note: requires .gitattributes, readme.txt, wp-plugin-info.json уже в HEAD)**
|
||
|
||
Run: `vendor/bin/phpunit --filter ZipArchiveBuildTest`
|
||
Expected: OK (2 tests, ~15 assertions).
|
||
|
||
Если тест падает с "git archive failed" — убедиться, что все предыдущие задачи закоммичены: `git log --oneline -10`.
|
||
|
||
- [ ] **Step 3: Run full test suite**
|
||
|
||
Run: `vendor/bin/phpunit`
|
||
Expected: OK (104 tests, ~185 assertions).
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add tests/ZipArchiveBuildTest.php
|
||
git commit -m "test(wpmc S7): ZipArchiveBuildTest verifies git archive integrity"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 10: release.sh — Gitea release helper
|
||
|
||
**Files:**
|
||
- Create: `release.sh` (executable)
|
||
|
||
- [ ] **Step 1: Create release.sh based on cf7stg pattern**
|
||
|
||
Create `release.sh` (содержимое — адаптация `cf7-spam-to-telegram/release.sh`):
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
#
|
||
# release.sh — Release helper for wp-multi-city.
|
||
#
|
||
# Usage: ./release.sh <semver>
|
||
# Example: ./release.sh 1.0.0
|
||
#
|
||
# Prerequisites (validated below):
|
||
# - git, curl, jq, unzip, awk, sed in PATH
|
||
# - clean git tree on main, in sync with origin
|
||
# - ~/.gitea-token exists (chmod 600), scope: write:repository
|
||
# - readme.txt already contains `= <version> =` section with changelog body
|
||
#
|
||
# See docs/superpowers/specs/2026-05-17-s7-release-puc-design.md
|
||
# for the full design rationale.
|
||
|
||
set -euo pipefail
|
||
|
||
REPO_OWNER="bryzgalov"
|
||
REPO_NAME="wp-multi-city"
|
||
GITEA_BASE="https://git.netranking.ru"
|
||
GITEA_API="${GITEA_BASE}/api/v1"
|
||
TOKEN_FILE="${HOME}/.gitea-token"
|
||
|
||
log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
|
||
warn() { printf '\033[1;33m==> WARN:\033[0m %s\n' "$*" >&2; }
|
||
fail() { printf '\033[1;31m==> FAIL:\033[0m %s\n' "$*" >&2; exit 1; }
|
||
|
||
# ---------- STEP 1: VALIDATE ---------------------------------------------------
|
||
|
||
VERSION="${1:-}"
|
||
[[ -n "$VERSION" ]] || fail "usage: $0 <semver> (e.g. $0 1.0.0)"
|
||
|
||
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
|
||
|| fail "invalid semver: '$VERSION'"
|
||
|
||
for cmd in git curl jq unzip awk sed; do
|
||
command -v "$cmd" >/dev/null 2>&1 || fail "missing command in PATH: $cmd"
|
||
done
|
||
|
||
[[ -f "$TOKEN_FILE" && -r "$TOKEN_FILE" ]] \
|
||
|| fail "$TOKEN_FILE missing or unreadable (chmod 600 and create token in Gitea UI)"
|
||
TOKEN="$(cat "$TOKEN_FILE")"
|
||
[[ -n "$TOKEN" ]] || fail "$TOKEN_FILE is empty"
|
||
|
||
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||
[[ "$BRANCH" == "main" ]] || fail "must be on main branch (currently: $BRANCH)"
|
||
|
||
if [[ -n "$(git status --porcelain)" ]]; then
|
||
fail "working tree not clean (commit/stash first):
|
||
$(git status --porcelain)"
|
||
fi
|
||
|
||
log "Fetching origin to compare main..."
|
||
git fetch origin main --quiet
|
||
LOCAL="$(git rev-parse main)"
|
||
REMOTE="$(git rev-parse origin/main)"
|
||
[[ "$LOCAL" == "$REMOTE" ]] \
|
||
|| fail "main is not in sync with origin/main (local=$LOCAL remote=$REMOTE — pull/push first)"
|
||
|
||
TAG="v${VERSION}"
|
||
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
|
||
fail "tag $TAG already exists locally"
|
||
fi
|
||
if git ls-remote --tags origin "$TAG" | grep -q "$TAG"; then
|
||
fail "tag $TAG already exists on origin"
|
||
fi
|
||
|
||
grep -qE "^=[[:space:]]*${VERSION}[[:space:]]*=[[:space:]]*$" readme.txt \
|
||
|| fail "readme.txt does not contain '= ${VERSION} =' changelog section — add it before running release"
|
||
|
||
log "Validation passed: version=$VERSION, tag=$TAG, branch=$BRANCH"
|
||
|
||
# ---------- STEP 2: BUMP VERSION ---------------------------------------------
|
||
|
||
log "Bumping version to $VERSION in wp-multi-city.php and readme.txt"
|
||
|
||
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 \
|
||
|| fail "bump failed: header Version in wp-multi-city.php"
|
||
grep -q "define('WPMC_VERSION', '${VERSION}')" wp-multi-city.php \
|
||
|| fail "bump failed: WPMC_VERSION constant in 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 \
|
||
|| fail "bump failed: Stable tag in readme.txt"
|
||
|
||
log "Bumped to $VERSION"
|
||
|
||
# ---------- STEP 3: RELEASE COMMIT + PUSH ------------------------------------
|
||
|
||
log "Creating release commit"
|
||
git add wp-multi-city.php readme.txt
|
||
git commit -m "release: ${VERSION}"
|
||
RELEASE_SHA="$(git rev-parse HEAD)"
|
||
log "Release commit: $RELEASE_SHA"
|
||
|
||
log "Pushing main to origin"
|
||
git push origin main
|
||
|
||
# ---------- STEP 4: BUILD ZIP + SANITY ---------------------------------------
|
||
|
||
ZIP_NAME="wp-multi-city-${VERSION}.zip"
|
||
ZIP_PATH="dist/${ZIP_NAME}"
|
||
|
||
log "Building release zip: $ZIP_PATH"
|
||
mkdir -p dist
|
||
rm -f "$ZIP_PATH"
|
||
git archive HEAD \
|
||
--worktree-attributes \
|
||
--format=zip \
|
||
--prefix=wp-multi-city/ \
|
||
-o "$ZIP_PATH"
|
||
|
||
log "Running zip sanity checks"
|
||
|
||
unzip -p "$ZIP_PATH" wp-multi-city/wp-multi-city.php \
|
||
| grep -q "^ \* Version: ${VERSION}$" \
|
||
|| fail "zip sanity: header Version mismatch inside $ZIP_PATH"
|
||
|
||
unzip -p "$ZIP_PATH" wp-multi-city/wp-multi-city.php \
|
||
| grep -q "WPMC_VERSION', '${VERSION}'" \
|
||
|| fail "zip sanity: WPMC_VERSION mismatch inside $ZIP_PATH"
|
||
|
||
ZIP_LISTING="$(unzip -l "$ZIP_PATH")"
|
||
|
||
grep -q "wp-multi-city/includes/lib/plugin-update-checker/plugin-update-checker.php" <<< "$ZIP_LISTING" \
|
||
|| fail "zip sanity: PUC loader missing from $ZIP_PATH"
|
||
|
||
grep -q "wp-multi-city/wp-plugin-info.json" <<< "$ZIP_LISTING" \
|
||
|| fail "zip sanity: wp-plugin-info.json missing from $ZIP_PATH"
|
||
|
||
for dev in tests/ docs/ composer.json composer.lock phpunit.xml.dist patchwork.json release.sh CLAUDE.md AGENTS.md .gitattributes .gitignore .gitea/; do
|
||
if grep -q "wp-multi-city/${dev}" <<< "$ZIP_LISTING"; then
|
||
fail "zip sanity: dev file must not be in zip: $dev"
|
||
fi
|
||
done
|
||
|
||
ZIP_SIZE="$(wc -c < "$ZIP_PATH")"
|
||
log "Zip built OK: $ZIP_PATH ($ZIP_SIZE bytes)"
|
||
|
||
# ---------- STEP 5: GITEA CREATE RELEASE -------------------------------------
|
||
|
||
log "Extracting changelog body for version $VERSION from readme.txt"
|
||
|
||
CHANGELOG_BODY="$(awk -v ver="$VERSION" '
|
||
BEGIN { in_block = 0 }
|
||
/^=[[:space:]]*[0-9]+\.[0-9]+\.[0-9]+[[:space:]]*=[[:space:]]*$/ {
|
||
if (in_block) exit
|
||
if ($0 ~ "^=[[:space:]]*" ver "[[:space:]]*=[[:space:]]*$") {
|
||
in_block = 1
|
||
next
|
||
}
|
||
}
|
||
in_block { print }
|
||
' readme.txt | sed '/^[[:space:]]*$/d')"
|
||
|
||
[[ -n "$CHANGELOG_BODY" ]] \
|
||
|| fail "could not extract changelog body for version $VERSION from readme.txt"
|
||
|
||
log "Creating Gitea release $TAG"
|
||
|
||
CREATE_PAYLOAD="$(jq -n \
|
||
--arg tag "$TAG" \
|
||
--arg name "$VERSION" \
|
||
--arg body "$CHANGELOG_BODY" \
|
||
--arg target "$RELEASE_SHA" \
|
||
'{tag_name:$tag, name:$name, body:$body, target_commitish:$target, draft:false, prerelease:false}')"
|
||
|
||
CREATE_RESPONSE="$(curl -fsSL \
|
||
-X POST "${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/releases" \
|
||
-H "Authorization: token ${TOKEN}" \
|
||
-H "Content-Type: application/json" \
|
||
-d "$CREATE_PAYLOAD")"
|
||
|
||
RELEASE_ID="$(jq -r '.id' <<< "$CREATE_RESPONSE")"
|
||
[[ -n "$RELEASE_ID" && "$RELEASE_ID" != "null" ]] \
|
||
|| fail "Gitea create-release response missing id: $CREATE_RESPONSE"
|
||
|
||
log "Created Gitea release id=$RELEASE_ID"
|
||
|
||
# ---------- STEP 6: UPLOAD ASSET ---------------------------------------------
|
||
|
||
log "Uploading asset $ZIP_NAME to release id=$RELEASE_ID"
|
||
|
||
curl -fsSL \
|
||
-X POST "${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \
|
||
-H "Authorization: token ${TOKEN}" \
|
||
-F "attachment=@${ZIP_PATH};type=application/zip" \
|
||
-o /dev/null
|
||
|
||
RELEASE_DETAIL="$(curl -fsSL \
|
||
"${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/releases/${RELEASE_ID}" \
|
||
-H "Authorization: token ${TOKEN}")"
|
||
|
||
DOWNLOAD_URL="$(jq -r --arg name "$ZIP_NAME" \
|
||
'.assets[] | select(.name == $name) | .browser_download_url' \
|
||
<<< "$RELEASE_DETAIL" | head -n 1)"
|
||
|
||
[[ -n "$DOWNLOAD_URL" && "$DOWNLOAD_URL" != "null" ]] \
|
||
|| fail "release $RELEASE_ID has no asset named $ZIP_NAME"
|
||
|
||
log "Asset uploaded: $DOWNLOAD_URL"
|
||
|
||
# ---------- STEP 7: REGENERATE wp-plugin-info.json ---------------------------
|
||
|
||
log "Converting readme.txt Changelog to HTML"
|
||
|
||
CHANGELOG_HTML="$(awk '
|
||
function escape(s) {
|
||
gsub(/&/, "\\&", s)
|
||
gsub(/</, "\\<", s)
|
||
gsub(/>/, "\\>", s)
|
||
return s
|
||
}
|
||
BEGIN { in_changelog = 0; ul_open = 0; html = ""; pending = "" }
|
||
/^== Changelog ==/ { in_changelog = 1; next }
|
||
/^==/ && in_changelog { exit }
|
||
!in_changelog { next }
|
||
/^=[[:space:]]*[0-9]+\.[0-9]+\.[0-9]+[[:space:]]*=[[:space:]]*$/ {
|
||
if (pending != "") { html = html "<li>" escape(pending) "</li>"; pending = "" }
|
||
if (ul_open) { html = html "</ul>"; ul_open = 0 }
|
||
match($0, /[0-9]+\.[0-9]+\.[0-9]+/)
|
||
ver = substr($0, RSTART, RLENGTH)
|
||
html = html "<h4>" ver "</h4><ul>"
|
||
ul_open = 1
|
||
next
|
||
}
|
||
/^\*[[:space:]]/ {
|
||
if (pending != "") { html = html "<li>" escape(pending) "</li>" }
|
||
sub(/^\*[[:space:]]+/, "")
|
||
pending = $0
|
||
next
|
||
}
|
||
/^[[:space:]]+[^[:space:]]/ {
|
||
if (pending != "") {
|
||
line = $0
|
||
sub(/^[[:space:]]+/, "", line)
|
||
pending = pending " " line
|
||
}
|
||
next
|
||
}
|
||
/^[[:space:]]*$/ {
|
||
if (pending != "") { html = html "<li>" escape(pending) "</li>"; pending = "" }
|
||
next
|
||
}
|
||
END {
|
||
if (pending != "") { html = html "<li>" escape(pending) "</li>" }
|
||
if (ul_open) { html = html "</ul>" }
|
||
print html
|
||
}
|
||
' readme.txt)"
|
||
|
||
[[ -n "$CHANGELOG_HTML" ]] || fail "changelog HTML conversion produced empty result"
|
||
|
||
LAST_UPDATED="$(date -u '+%Y-%m-%d %H:%M:%S')"
|
||
|
||
log "Regenerating wp-plugin-info.json"
|
||
TMP_JSON="$(mktemp)"
|
||
jq \
|
||
--arg version "$VERSION" \
|
||
--arg download_url "$DOWNLOAD_URL" \
|
||
--arg last_updated "$LAST_UPDATED" \
|
||
--arg changelog "$CHANGELOG_HTML" \
|
||
'.version = $version | .download_url = $download_url | .last_updated = $last_updated | .sections.changelog = $changelog' \
|
||
wp-plugin-info.json > "$TMP_JSON"
|
||
mv "$TMP_JSON" wp-plugin-info.json
|
||
|
||
jq empty wp-plugin-info.json || fail "regenerated wp-plugin-info.json is not valid JSON"
|
||
|
||
log "Regenerated wp-plugin-info.json (version=$VERSION, download_url=$DOWNLOAD_URL)"
|
||
|
||
# ---------- STEP 8: JSON COMMIT + PUSH ---------------------------------------
|
||
|
||
log "Committing wp-plugin-info.json update"
|
||
git add wp-plugin-info.json
|
||
git commit -m "wp-plugin-info.json: update for ${VERSION}"
|
||
git push origin main
|
||
|
||
# ---------- STEP 9: SUMMARY --------------------------------------------------
|
||
|
||
log "Release completed"
|
||
printf '\n'
|
||
printf ' Version : %s\n' "$VERSION"
|
||
printf ' Tag : %s (on %s/%s/%s/src/tag/%s)\n' "$TAG" "$GITEA_BASE" "$REPO_OWNER" "$REPO_NAME" "$TAG"
|
||
printf ' Release page : %s/%s/%s/releases/tag/%s\n' "$GITEA_BASE" "$REPO_OWNER" "$REPO_NAME" "$TAG"
|
||
printf ' Asset : %s\n' "$DOWNLOAD_URL"
|
||
printf ' PUC JSON : %s/%s/%s/raw/branch/main/wp-plugin-info.json\n' "$GITEA_BASE" "$REPO_OWNER" "$REPO_NAME"
|
||
printf '\n'
|
||
printf ' Next: in WordPress admin — «Plugins → Check again» (PUC re-checks hourly, force-check via transient delete if impatient).\n'
|
||
printf '\n'
|
||
```
|
||
|
||
- [ ] **Step 2: Make executable**
|
||
|
||
```bash
|
||
chmod +x release.sh
|
||
```
|
||
|
||
- [ ] **Step 3: Sanity-test parse (without running — validate syntax)**
|
||
|
||
```bash
|
||
bash -n release.sh && echo "OK: release.sh syntactically valid"
|
||
```
|
||
|
||
Expected: `OK: release.sh syntactically valid`.
|
||
|
||
- [ ] **Step 4: Run full PHPUnit suite (no test changes but verify nothing broke)**
|
||
|
||
Run: `vendor/bin/phpunit`
|
||
Expected: OK (104 tests, ~185 assertions).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add release.sh
|
||
git commit -m "feat(wpmc S7): release.sh Gitea release helper (mirror cf7stg flow)"
|
||
```
|
||
|
||
---
|
||
|
||
## Final verification
|
||
|
||
After all 10 tasks complete:
|
||
|
||
- [ ] **Run full PHPUnit suite**
|
||
|
||
Run: `vendor/bin/phpunit`
|
||
Expected: OK (104 tests, ~185 assertions).
|
||
|
||
- [ ] **PHP lint clean**
|
||
|
||
```bash
|
||
find includes -name '*.php' -not -path '*/lib/*' -exec php -l {} \;
|
||
```
|
||
|
||
Expected: каждая строка вида `No syntax errors detected in <file>`.
|
||
|
||
- [ ] **Working tree clean**
|
||
|
||
```bash
|
||
git status
|
||
```
|
||
|
||
Expected: `nothing to commit, working tree clean`.
|
||
|
||
- [ ] **Smoke git archive**
|
||
|
||
```bash
|
||
git archive HEAD --worktree-attributes --format=zip --prefix=wp-multi-city/ -o /tmp/wpmc-final.zip
|
||
unzip -l /tmp/wpmc-final.zip | head -40
|
||
rm /tmp/wpmc-final.zip
|
||
```
|
||
|
||
Expected: видны `wp-multi-city.php`, `README.md`, `readme.txt`, `wp-plugin-info.json`, `includes/lib/plugin-update-checker/`; НЕ видны `tests/`, `docs/`, `composer.json`, `release.sh`.
|
||
|
||
- [ ] **Manual Gitea steps (вне плана — выполняются пользователем после merge)**
|
||
|
||
1. В Gitea UI (`https://git.netranking.ru/bryzgalov`) → New Repository → name `wp-multi-city`.
|
||
2. Локально:
|
||
```bash
|
||
git remote add origin https://git.netranking.ru/bryzgalov/wp-multi-city.git
|
||
git push -u origin main
|
||
```
|
||
3. Запустить: `./release.sh 1.0.0`.
|
||
4. Проверить: `https://git.netranking.ru/bryzgalov/wp-multi-city/releases/tag/v1.0.0` отдаёт zip.
|
||
|
||
- [ ] **Close epic**
|
||
|
||
```bash
|
||
bd close cp-7cf --reason "S7 done: vendored PUC v5.6 + release.sh + extended README + version 1.0.0. 104 tests green."
|
||
bd close cp-cw4 --reason "MVP-ядро завершено: S1-S7 done. Self-hosted updates через Gitea + PUC."
|
||
```
|
||
|
||
---
|
||
|
||
## Spec coverage check
|
||
|
||
Все требования спека покрыты:
|
||
|
||
| Spec section | Tasks |
|
||
|---|---|
|
||
| Артефакты (10 файлов) | T1 (PUC vendor), T2 (JSON), T3 (Plugin), T4 (version bump), T5 (.gitattributes), T6 (readme.txt), T7 (CHANGELOG), T8 (README), T10 (release.sh) |
|
||
| PUC integration | T1 (vendor) + T3 (bootstrap) |
|
||
| release.sh | T10 |
|
||
| .gitattributes | T5 |
|
||
| Документация (README/readme.txt/CHANGELOG) | T6, T7, T8 |
|
||
| wp-multi-city.php изменения | T4 |
|
||
| 6 новых тестов | T1 (2), T3 (2), T9 (2) — итого 6 |
|
||
| Acceptance verification (1-6) | Final verification block |
|
||
| Manual Gitea (7) | Final verification block (manual steps) |
|