Статьи

Git и WordPress: как автоматически обновлять сообщения с помощью запросов Pull

На Bitfalls.com мы также используем WordPress и используем тот же подход к рецензированию контента, что и в SitePoint .

Мы решили создать инструмент, который автоматически извлекает контент из объединенных запросов на получение запросов в статьи, давая нам возможность исправлять опечатки и обновлять сообщения из Github, а также видеть изменения, отраженные на живом сайте. Из этого туториала вы узнаете, как создать этот инструмент, чтобы вы могли начать использовать его для своего собственного сайта WordPress или создать собственную версию.

План

Первая часть — это определение проблемы и ситуации вокруг нее.

  • мы используем WPGlobus для многоязычной поддержки, что означает, что контент сохраняется как: {:en}English content{:}{:hr}Croatian content{:} .
  • авторы отправляют PR через Github, PR проверяются и объединяются, а затем (в настоящее время) вручную импортируются в интерфейс публикаций WP через браузер.
  • каждый пост имеет одинаковую структуру папок: author_folder/post_folder/language/final.md
  • это медленно и подвержено ошибкам, а иногда и ошибкам. Это также делает обновление сообщений утомительным.

Решение заключается в следующем:

  • добавить обработчик хуков, который будет обнаруживать толчки в основной ветке (т. е. сливаться с PR)
  • процессор должен искать метафайл в коммите, который будет содержать информацию о том, где сохранить обновленный контент
  • процессор автоматически преобразует содержимое MD в HTML, объединяет языки в формате WPGlobus и сохраняет их в базе данных

Бутстрапирование

Если вы хотите следовать (настоятельно рекомендуется), загрузите хорошую среду виртуальной машины , установите на нее новейшую версию WordPress и добавьте плагин WPGlobus. Кроме того, вы можете использовать готовую коробку WordPress, как VVV . Кроме того, убедитесь, что в вашей среде установлен ngrok — мы будем использовать его для передачи триггеров Github-хука на наш локальный компьютер, чтобы мы могли проводить локальное тестирование вместо необходимости развертывания.

Крючки

Для этого эксперимента давайте создадим новый репозиторий. Я назову мой автопуш .

В настройках этого репозитория нам нужно добавить новый хук . Поскольку мы говорим о временном URL-адресе Ngrok, давайте сначала раскрутим это. В моем случае ввод следующего на хост-машине делает свое дело:

 ngrok http homestead.app:80 

Мне дали ссылку http://03672a64.ngrok.io , так что это то, что входит в webhook, с произвольным суффиксом, таким как githook . Нам нужны только события толчка. Тип данных json более чистый, поэтому он выбран в качестве предпочтения, и окончательная настройка webhook выглядит примерно так:

Настройка Webhook

Давайте проверим это сейчас.

 git clone https://github.com/swader/autopush cd autopush touch README.md echo "This is a README file" >> README.md git add -A git commit -am "We're pushing for the first time" git push origin master 

Экран журнала ngrok должен отображать что-то вроде этого:

 POST /githook/ 404 Not Found 

Это отлично. Мы еще не сделали /githook точку /githook .

Обработка веб-крючков

Мы будем читать эти новые данные в WordPress с пользовательской логикой. Из-за спагетти-кода самой WP очень легко обойти ее с помощью небольшого пользовательского приложения. Сначала мы создадим папку githook в корне проекта WordPress и файл index.php внутри нее. Это делает /githook/ path доступным, и ловушка больше не вернет 404, а 200 OK.

Согласно документации , полезная нагрузка будет иметь поле коммитов с modified полем в каждом коммите. Так как мы только обновляем обновления, а не планируем или удаляем их — эти шаги все еще выполняются вручную, для безопасности — мы будем обращать внимание только на это. Давайте посмотрим, сможем ли мы поймать это на тестовом толчке.

Сначала мы сохраним данные нашего запроса в текстовом файле для целей отладки. Мы можем сделать это, изменив наш githook/index.php :

 <?php file_put_contents('test.txt', file_get_contents('php://input')); 

Затем мы создадим новую ветку, добавим файл и отправим его в онлайн.

 git checkout -b test-branch touch testfile.md git add testfile.md git commit -am "Added test file" git push origin test-branch 

Конечно же, наш файл test.json заполнен полезной нагрузкой. Это полезная нагрузка, которую я получил. Вы можете видеть, что у нас есть только один коммит, и modified поле этого коммита пусто, а added поле имеет testfile.md . Мы также можем видеть, что это произошло в refs/heads/test-branch , следовательно, мы не заинтересованы в этом. Но что произойдет, если мы сделаем пиар из этой ветки и объединим его?

Наша полезная нагрузка выглядит иначе . В частности, теперь у нас есть refs/heads/master в качестве поля ref , что означает, что это произошло в master ветке, и мы должны обратить на это внимание. У нас также есть 2 коммита вместо одного: первый такой же, как в оригинальном PR, добавление файла. Второй соответствует изменению в основной ветке: само слияние. Оба ссылаются на один и тот же added файл.

Давайте сделаем один последний тест. Давайте отредактируем testfile.md , testfile.md , сделаем пиар и объединяем.

 echo "Hello" >> testfile.md git add testfile.md git commit -am "Added test file" git push origin test-branch 

Ааа, поехали . Теперь у нас есть измененный файл в полезной нагрузке.

Теперь давайте создадим «реальный» сценарий и смоделируем отправку обновлений. Сначала мы создадим папку по умолчанию для поста, а затем сделаем PR-обновление.

 git checkout master git pull mkdir -p authors/some-author/some-post/{en_EN,hr_HR,images} echo "English content" >> authors/some-author/some-post/en_EN/final.md echo "Croatian content" >> authors/some-author/some-post/hr_HR/final.md touch authors/some-author/some-post/images/.gitkeep git add -A git commit -am "Added some author" git push origin master 

Затем мы делаем редактирование.

 git checkout -b edit-for-some-post echo "This is a new line" >> authors/some-author/some-post/en_EN/final.md git add -A git commit -am "Added an update on the English version of the post" git push origin edit-for-some-post 

Новый пост редактировать PR предложил

Если мы превратим это в запрос на извлечение в веб-интерфейсе Github и объединим PR, мы получим эту полезную нагрузку .

Если мы следуем пути из измененных файлов в полезной нагрузке, мы можем легко определить папку, о которой мы говорим. Давайте index.php файл index.php из ранее.

 $payload = json_decode($json, true); $last_commit = array_pop($payload['commits']); $modified = $last_commit['modified']; $prefix = 'https://raw.githubusercontent.com/'; $repo = 'swader/autopush/master/'; $lvl = 2; $folders = []; foreach ($modified as $file) { $folder = explode('/', $file); $folder = implode('/', array_slice($folder, 0, -$lvl)); $folders[] = $folder; } $folders = array_unique($folders); var_dump($folders); 

Мы выбираем последний коммит в полезной нагрузке, извлекаем его список измененных файлов и находим родительскую папку каждого измененного файла. Родитель диктуется переменной $lvl — в нашем случае это 2, потому что папка на 2 уровня выше: один дополнительный для языка ( en_EN ).

И у нас это есть — путь к папке, в которой хранятся файлы, которые необходимо обновить. Теперь все, что нам нужно сделать, это извлечь содержимое, превратить Markdown этих файлов в HTML и сохранить его в базе данных.

Обработка уценки

Для обработки MarkDown мы можем использовать пакет Parsedown . Мы установим эти зависимости в самой папке githooks , чтобы сделать приложение максимально автономным.

 composer require erusev/parsedown 

Parsedown — это та же разновидность Markdown, которую мы используем в Bitfalls при написании с редактором Caret, поэтому она идеально подходит.

Теперь мы можем снова изменить index.php .

 $payload = json_decode($json, true); $last_commit = array_pop($payload['commits']); $modified = $last_commit['modified']; $prefix = 'https://raw.githubusercontent.com/'; $repo = 'swader/autopush/'; $branch = 'master/'; $languages = [ 'en_EN' => 'en', 'hr_HR' => 'hr' ]; $lvl = 2; $folders = []; foreach ($modified as $file) { $folder = explode('/', $file); $folder = implode('/', array_slice($folder, 0, -$lvl)); $folders[] = $folder; } $folders = array_unique($folders); foreach ($folders as $folder) { $fullFolderPath = $prefix.$repo.$branch.$folder.'/'; $content = ''; foreach ($languages as $langpath => $key) { $url = $fullFolderPath.$langpath.'/final.md'; $content .= "{:$key}".mdToHtml(getContent($url))."{:}"; } if (!empty($content)) { // Save to database } } function getContent(string $url): string { $ch = curl_init(); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_URL, $url.'?nonce='.md5(microtime())); curl_setopt($ch, CURLOPT_FRESH_CONNECT, TRUE); $data = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($code != 200) { return ''; } curl_close($ch); return $data; } function mdToHtml(string $text): string { $p = new Parsedown(); $p->setUrlsLinked(true); return $p->parse($text); } 

Мы сделали несколько действительно простых функций, чтобы избежать повторения. Мы также добавили отображение языковых папок (локали) в их ключи WPGlobus, чтобы при переборе всех файлов в папке мы знали, как их разделить в теле сообщения.

Примечание: мы должны обновить все языковые версии поста при обновлении только до одной, потому что WPGlobus не использует дополнительное поле или другую строку базы данных для сохранения другого языка поста — он сохраняет их все в одном поле, поэтому все значение этого поля необходимо обновить.

Мы перебираем папки, которые получили обновления (их может быть больше одной в одном PR), берем содержимое файла и преобразуем его в HTML, а затем сохраняем все это в строку, удобную для WPGlobus. Теперь пришло время сохранить это в базе данных.

Примечание: мы использовали одноразовый номер в конце URL-адреса, чтобы аннулировать возможную проблему с кешем с необработанным содержимым github.

Сохранение отредактированного контента

Мы понятия не имеем, где сохранить новый контент. Нам нужно добавить поддержку метафайлов.

Сначала мы добавим новую функцию, которая получает этот метафайл:

 function getMeta(string $folder): ?array { $data = getContent(trim($folder, '/').'/meta.json'); if (!empty($data)) { return json_decode($data, true); } return null; } 

Просто, если он существует, он вернет его содержимое. Метафайлы будут в формате JSON, поэтому весь анализ, который нам когда-либо понадобится, уже встроен в PHP.

Затем мы добавим проверку в наш основной цикл, чтобы процесс пропускал любую папку без метафайла.

 foreach ($folders as $folder) { $fullFolderPath = $prefix.$repo.$branch.$folder.'/'; $meta = getMeta($fullFolderPath); if (!$meta) { continue; } // ... 

Мы будем использовать WP CLI для обновления. CLI может быть установлен с помощью следующих команд:

 curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar sudo mv wp-cli.phar /usr/local/bin/wp sudo chmod +x /usr/local/bin/wp 

Это загружает инструмент WP-CLI, помещает его в путь к серверу (чтобы его можно было выполнить из любого места) и добавляет к нему разрешение «исполняемый».

Команде post update требуется идентификатор поста и поле для обновления. Сообщения WordPress сохраняются в wp_posts базы данных wp_posts , а поле, которое мы обновляем, — это поле post_content .

Давайте попробуем это в командной строке, чтобы убедиться, что все работает как задумано. Сначала мы добавим пример сообщения. Я дал ему пример заголовка «Пример поста» на английском языке и «Primjer» на хорватском языке, с основанием. This is some English content for a post! для английского контента, и Ovo je primjer! для хорватского контента. При сохранении это выглядит в базе данных:

Пример поста в базе данных

В моем случае ID поста — 428. Если ваша установка WP свежая, ваша, вероятно, будет ближе к 1.

Теперь давайте посмотрим, что произойдет, если мы выполним следующее в командной строке:

 wp post update 428 --post_content='{:en}This is some English content for a post - edited!{:}{:hr}Ovo je primjer - editiran!{:}' 

Конечно же, наш пост был обновлен.

Обновленный пост

Похоже, это может стать проблемой при работе с кавычками, которые нужно будет экранировать. Лучше, если мы обновимся из файла и позволим этому инструменту обрабатывать кавычки и тому подобное. Давайте попробуем.

Давайте поместим content :en}This is some English 'content' for a post - edited "again"!{:}{:hr}Ovo je 'primjer' - editiran "opet"!{:} файл с именем updateme.txt Потом…

 wp post update 428 updateme.txt 

Да, все хорошо.

Скриншот обновления из файла

Хорошо, теперь давайте добавим это в наш инструмент.

На данный момент наш метафайл будет иметь только идентификатор сообщения, поэтому давайте добавим один такой файл в репозиторий контента:

 git checkout master git pull echo '{"id": 428}' >> authors/some-author/some-post/meta.json git add -A git commit -am "Added meta file for post 428" git push origin master 

Примечание: обновите идентификатор, чтобы он соответствовал вашему.

На этом этапе наше хранилище контента должно выглядеть следующим образом (версия сохранена в виде релиза, не стесняйтесь клонировать).

Замените строку // Save to database в предыдущем коде и окружающие ее строки:

  if (!empty($content) && is_numeric($meta['id'])) { file_put_contents('/tmp/wpupdate', $content); exec('wp post update '.$meta['id'].' /tmp/wpupdate', $output); var_dump($output); } 

Мы удостоверимся, что содержимое и идентификатор обновляемого сообщения в какой-то степени действительны, а затем записываем содержимое во временный файл, из которого затем передаем его в инструмент wp cli .

Мы также должны добавить еще несколько проверок в начало скрипта, чтобы убедиться, что мы выполняем только те обновления, которые хотим выполнить:

 // ... $payload = json_decode($json, true); if (empty($json)) { header("HTTP/1.1 500 Internal Server Error"); die('No data provided for parsing, payload invalid.'); } if ($payload['ref'] !== 'refs/heads/master') { die('Ignored. Not master.'); } $last_commit = array_pop($payload['commits']); // ... 

Полный файл index.php теперь выглядит так:

 <?php require_once 'vendor/autoload.php'; $json = file_get_contents('php://input'); file_put_contents('test.json', $json); $payload = json_decode($json, true); if (empty($json)) { header("HTTP/1.1 500 Internal Server Error"); die('No data provided for parsing, payload invalid.'); } if ($payload['ref'] !== 'refs/heads/master') { die('Ignored. Not master.'); } $last_commit = array_pop($payload['commits']); $modified = $last_commit['modified']; $prefix = 'https://raw.githubusercontent.com/'; $repo = 'swader/autopush/'; $branch = 'master/'; $languages = [ 'en_EN' => 'en', 'hr_HR' => 'hr' ]; $lvl = 2; $folders = []; foreach ($modified as $file) { $folder = explode('/', $file); $folder = implode('/', array_slice($folder, 0, -$lvl)); $folders[] = $folder; } $folders = array_unique($folders); foreach ($folders as $folder) { $fullFolderPath = $prefix.$repo.$branch.$folder.'/'; $meta = getMeta($fullFolderPath); if (!$meta) { continue; } $content = ''; foreach ($languages as $langpath => $key) { $url = $fullFolderPath.$langpath.'/final.md'; $content .= "{:$key}".mdToHtml(getContent($url))."{:}"; } if (!empty($content) && is_numeric($meta['id'])) { file_put_contents('/tmp/wpupdate', $content); exec('wp post update '.$meta['id'].' /tmp/wpupdate', $output); var_dump($output); } } function getContent(string $url): ?string { $ch = curl_init(); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_URL, $url.'?nonce='.md5(microtime())); curl_setopt($ch, CURLOPT_FRESH_CONNECT, TRUE); $data = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($code != 200) { return null; } curl_close($ch); return $data; } function mdToHtml(string $text): string { $p = new Parsedown(); $p->setUrlsLinked(true); return $p->parse($text); } function getMeta(string $folder): ?array { $data = getContent(trim($folder, '/').'/meta.json'); if (!empty($data)) { return json_decode($data, true); } return null; } 

На данный момент мы можем проверить вещи. Прекрасный шанс и для новой отрасли.

 git checkout -b post-update echo 'Adding a new line yay!' >> authors/some-author/some-post/en_EN/final.md git add -A; git commit -am "Edit"; git push origin post-update 

Давайте проверим наш пост.

Финальный обновленный пост

Это работает — развернуть этот скрипт теперь так же просто, как развернуть код WP вашего приложения и обновить URL-адрес webhook для рассматриваемого репо.

Вывод

Настоящим способом WordPress мы взломали инструмент, который занял у нас меньше дня, но в конечном итоге сэкономил нам дни или недели. Инструмент теперь развернут и функционирует адекватно. Есть, конечно, место для обновлений. Если вы чувствуете себя вдохновленным, попробуйте добавить следующее:

  • измените процедуру пост-обновления так, чтобы он использовал stdin вместо файла, делая его совместимым с хостами no-writeable-filesystem, такими как AWS, Heroku или Google Cloud.
  • пользовательские типы вывода: вместо фиксированного {:en}{:}{:hr}{:} , возможно, кто-то другой использует другой многоязычный плагин или не использует его вообще. Это должно быть как-то настраиваемым.
  • автоматическая вставка изображений. Прямо сейчас это руководство, но изображения сохраняются в репозитории вместе с языковыми версиями и, вероятно, могут быть легко импортированы, автоматически оптимизированы и добавлены в сообщения.
  • промежуточный режим — убедитесь, что объединенное обновление сначала передается в промежуточную версию сайта, прежде чем перейти к основной, чтобы изменения могли быть проверены перед отправкой мастеру. Вместо того, чтобы активировать и деактивировать webhooks, почему бы не сделать это программируемым?
  • интерфейс плагина: было бы удобно определить все это в интерфейсе WP, а не в коде. Таким образом, будет полезна абстракция плагина WP вокруг функциональности.

В этом уроке мы намеревались показать вам, что оптимизация рабочего процесса не так уж и сложна, если вы потратите время на его выполнение, и окупаемость инвестиций в то время, когда вы потратите некоторое время на настройку и запуск автоматизации, может быть огромной, если долго думать срок.

Любые другие идеи или советы о том, как оптимизировать это? Дайте нам знать!