{"id":4601,"date":"2026-05-23T11:58:38","date_gmt":"2026-05-23T09:58:38","guid":{"rendered":"https:\/\/davidperezgar.com\/?p=4601"},"modified":"2026-05-24T10:37:43","modified_gmt":"2026-05-24T08:37:43","slug":"preview-de-plugins-en-wordpress-playground-directamente-desde-el-pull-request","status":"publish","type":"post","link":"https:\/\/davidperezgar.com\/es\/preview-de-plugins-en-wordpress-playground-directamente-desde-el-pull-request\/","title":{"rendered":"Preview de plugins en WordPress Playground directamente desde el Pull Request"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Cuando alguien abre un PR en un plugin de WordPress, el proceso habitual para revisarlo implica descargar la rama, instalarla en local, activar el plugin y probar los cambios a mano. Eso tiene fricciones: hay que tener un entorno local preparado y dedicar tiempo a instalar algo que igual est\u00e1 roto a medias. Si el revisor no es desarrollador, directamente no puede.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Con este setup, el propio PR genera un enlace que abre el plugin funcionando en el navegador. Un clic, y ya est\u00e1 probando.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">\u00bfQu\u00e9 es WordPress Playground?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">WordPress Playground es un entorno de WordPress que corre completamente en el navegador, sin servidor. Usa WebAssembly (WASM) para ejecutar PHP directamente en el cliente. No necesitas Docker, XAMPP, ni nada instalado en local.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Cualquier persona puede abrir una URL y tener un WordPress funcional en segundos, con el plugin instalado y activado. Cuando cierra la pesta\u00f1a, todo desaparece. Sin rastro, sin instalaciones, sin conflictos.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Esto lo hace perfecto para revisiones de c\u00f3digo, demos r\u00e1pidas y pruebas de plugins sin comprometer ning\u00fan entorno real.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">C\u00f3mo funciona el sistema completo<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Son tres piezas que encajan juntas:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li class=\"\"><strong>build-dist.yml<\/strong> \u2014 se ejecuta en cada push. Instala las dependencias de Composer sin <code>--dev<\/code>, genera un <code>blueprint.json<\/code> apuntando a esa rama concreta, y hace un push forzado a una rama <code>dist\/nombre-rama<\/code>. Tambi\u00e9n elimina la rama dist cuando se elimina la original.<\/li>\n\n\n\n<li class=\"\"><strong>playground-preview.yml<\/strong> \u2014 se ejecuta cuando se abre, reabre o actualiza un PR. Construye la URL de Playground con el blueprint de la rama dist y publica o actualiza un comentario en el PR. Usa un marcador HTML para no duplicar comentarios.<\/li>\n\n\n\n<li class=\"\"><strong>pr-blueprint.json<\/strong> \u2014 plantilla del blueprint para revisar el trunk manualmente, sin abrir un PR. Define la p\u00e1gina de aterrizaje dentro de wp-admin, hace login autom\u00e1tico como admin, instala el plugin desde la URL del zip y lo activa.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Gu\u00eda paso a paso<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1. Crea la estructura de carpetas<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Necesitas dos carpetas en la ra\u00edz del repositorio: <code>.github\/workflows\/<\/code> para los workflows y <code>.github\/<\/code> para el blueprint.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. A\u00f1ade build-dist.yml<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Este workflow hace tres cosas: instala dependencias, genera el blueprint con la rama correcta, y hace push a la rama dist. Ajusta la <code>landingPage<\/code> y el nombre del plugin en la secci\u00f3n de generaci\u00f3n del blueprint.<\/p>\n\n\n\n<div class=\"cbc-code-wrapper\"><div class=\"cbc-code-bar\"><button class=\"cbc-copy-button\">Copy<\/button><\/div><pre class=\"wp-block-code\"><code class=\"\">name: Build dist branch\n\non:\n  push:\n    branches:\n      - '**'\n  delete:\n\npermissions:\n  contents: write\n\njobs:\n  build:\n    if: github.event_name == 'push' &amp;&amp; !startsWith(github.ref_name, 'dist\/')\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions\/checkout@v4\n\n      - name: Setup PHP\n        uses: shivammathur\/setup-php@v2\n        with:\n          php-version: '8.2'\n          tools: composer\n\n      - name: Install production dependencies\n        run: composer install --no-dev --optimize-autoloader\n\n      - name: Generate Playground blueprint\n        env:\n          BRANCH_NAME: ${{ github.ref_name }}\n        run: |\n          mkdir -p .wordpress-playground\n          cat &gt; .github\/pr-blueprint.json &lt;&lt; EOF\n          {\n            \"\\$schema\": \"https:\/\/playground.wordpress.net\/blueprint-schema.json\",\n            \"landingPage\": \"\/wp-admin\/options-general.php?page=tu-plugin\",\n            \"steps\": [\n              {\n                \"step\": \"login\",\n                \"username\": \"admin\",\n                \"password\": \"password\"\n              },\n              {\n                \"step\": \"installPlugin\",\n                \"pluginZipFile\": {\n                  \"resource\": \"url\",\n                  \"url\": \"https:\/\/github.com\/tu-org\/tu-plugin\/archive\/refs\/heads\/dist\/${BRANCH_NAME}.zip\"\n                },\n                \"options\": { \"activate\": true }\n              }\n            ]\n          }\n          EOF\n\n      - name: Push to dist branch\n        env:\n          BRANCH_NAME: ${{ github.ref_name }}\n        run: |\n          git config user.name \"github-actions[bot]\"\n          git config user.email \"github-actions[bot]@users.noreply.github.com\"\n          git add -f vendor\/ .wordpress-playground\/\n          git diff --cached --quiet || git commit -m \"Build: add vendor and playground blueprint\"\n          git push origin HEAD:dist\/${BRANCH_NAME} --force\n\n  delete-dist:\n    if: github.event_name == 'delete' &amp;&amp; github.event.ref_type == 'branch' &amp;&amp; !startsWith(github.event.ref, 'dist\/')\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions\/checkout@v4\n\n      - name: Delete dist branch\n        env:\n          BRANCH_NAME: ${{ github.event.ref }}\n        run: |\n          git push origin \":refs\/heads\/dist\/${BRANCH_NAME}\" || true<\/code><\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">3. A\u00f1ade playground-preview.yml<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Este workflow publica el comentario en el PR. El marcador HTML evita que se creen comentarios duplicados en cada push. C\u00e1mbialo por el nombre de tu plugin.<\/p>\n\n\n\n<div class=\"cbc-code-wrapper\"><div class=\"cbc-code-bar\"><button class=\"cbc-copy-button\">Copy<\/button><\/div><pre class=\"wp-block-code\"><code class=\"\">name: Playground PR Preview\n\non:\n  pull_request:\n    types: [opened, reopened, synchronize]\n\npermissions:\n  issues: write\n  pull-requests: write\n\njobs:\n  playground-preview:\n    name: Comment with Playground preview link\n    runs-on: ubuntu-latest\n    steps:\n      - name: Post Playground preview comment\n        uses: actions\/github-script@v7\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          script: |\n            const marker = '&lt;!-- tu-plugin-playground-preview --&gt;';\n            const issue_number = context.payload.pull_request.number;\n            const branchName = context.payload.pull_request.head.ref;\n            const owner = context.repo.owner;\n            const repo = context.repo.repo;\n\n            const blueprintUrl = `https:\/\/raw.githubusercontent.com\/${owner}\/${repo}\/dist\/${branchName}\/.wordpress-playground\/blueprint.json`;\n            const playgroundUrl = `https:\/\/playground.wordpress.net\/?blueprint-url=${encodeURIComponent(blueprintUrl)}`;\n\n            const body = [\n              marker,\n              '## Test in WordPress Playground',\n              `[**Open PR #${issue_number} in Playground \u2197**](${playgroundUrl})`,\n              '&gt; El enlace se activa cuando termina el workflow **Build dist branch**.',\n              '- Login: `admin` \/ `password`',\n              '- El plugin se instala y activa autom\u00e1ticamente.',\n            ].join('\\n');\n\n            const comments = await github.paginate(\n              github.rest.issues.listComments,\n              { owner, repo, issue_number, per_page: 100 }\n            );\n            const existing = comments.find(c =&gt; c.body?.includes(marker));\n\n            if (existing) {\n              await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });\n            } else {\n              await github.rest.issues.createComment({ owner, repo, issue_number, body });\n            }<\/code><\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">4. Verifica los permisos del workflow<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">En <code>build-dist.yml<\/code> ya viene <code>permissions: contents: write<\/code>. En <code>playground-preview.yml<\/code> necesitas <code>issues: write<\/code> y <code>pull-requests: write<\/code>. Si tu repo tiene las Actions con permisos restringidos por defecto, rev\u00edsalo en <em>Settings \u2192 Actions \u2192 General \u2192 Workflow permissions<\/em>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5. Haz el primer push y verifica<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Empuja cualquier rama, ve a <em>Actions<\/em> y comprueba que el workflow <em>Build dist branch<\/em> acaba en verde. En las ramas del repo deber\u00eda aparecer <code>dist\/nombre-de-tu-rama<\/code> con el vendor y el blueprint dentro.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">6. Abre un PR y espera el comentario autom\u00e1tico<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Al abrir el PR se ejecuta el workflow <em>Playground PR Preview<\/em>. En unos segundos ver\u00e1s un comentario con el enlace directo a Playground con el plugin instalado, login hecho como admin y la p\u00e1gina de ajustes abierta.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Personalizar el blueprint<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">El blueprint es un JSON que Playground interpreta. Puedes a\u00f1adir m\u00e1s pasos seg\u00fan lo que necesite tu plugin.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"883\" height=\"320\" src=\"\/wp-content\/uploads\/2026\/10\/github-comment-playground.png\" alt=\"Github Comment Playground\" class=\"wp-image-5774\" title=\"\" srcset=\"https:\/\/davidperezgar.com\/wp-content\/uploads\/github-comment-playground.png 883w, https:\/\/davidperezgar.com\/wp-content\/uploads\/github-comment-playground-767x278.png 767w, https:\/\/davidperezgar.com\/wp-content\/uploads\/github-comment-playground-596x216.png 596w\" sizes=\"auto, (max-width: 883px) 100vw, 883px\" \/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Instalar un plugin de dependencia<\/h3>\n\n\n\n<div class=\"cbc-code-wrapper\"><div class=\"cbc-code-bar\"><button class=\"cbc-copy-button\">Copy<\/button><\/div><pre class=\"wp-block-code\"><code class=\"\">{\n  \"step\": \"installPlugin\",\n  \"pluginZipFile\": {\n    \"resource\": \"wordpress.org\/plugins\",\n    \"slug\": \"woocommerce\"\n  },\n  \"options\": { \"activate\": true }\n}<\/code><\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Crear datos de prueba con WP-CLI<\/h3>\n\n\n\n<div class=\"cbc-code-wrapper\"><div class=\"cbc-code-bar\"><button class=\"cbc-copy-button\">Copy<\/button><\/div><pre class=\"wp-block-code\"><code class=\"\">{\n  \"step\": \"wp-cli\",\n  \"command\": \"wp post create --post_title='Producto de prueba' --post_status=publish\"\n}<\/code><\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Fijar versi\u00f3n de WordPress o PHP<\/h3>\n\n\n\n<div class=\"cbc-code-wrapper\"><div class=\"cbc-code-bar\"><button class=\"cbc-copy-button\">Copy<\/button><\/div><pre class=\"wp-block-code\"><code class=\"\">{\n  \"$schema\": \"https:\/\/playground.wordpress.net\/blueprint-schema.json\",\n  \"preferredVersions\": {\n    \"php\": \"8.1\",\n    \"wp\": \"6.4\"\n  }\n}<\/code><\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Notas importantes<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">El vendor de Composer se registra en la rama dist con <code>git add -f vendor\/<\/code>. Es intencionado: Playground descarga el zip de esa rama y necesita todas las dependencias incluidas. No entra en conflicto con tu <code>.gitignore<\/code> porque solo ocurre en la rama dist.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">El build es forzado (<code>--force<\/code>), as\u00ed que la rama dist siempre refleja el estado actual. No hay historial acumulado.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Si un PR recibe varios pushes seguidos, el workflow actualiza el mismo comentario en lugar de crear uno nuevo. Eso lo controla el marcador HTML. C\u00e1mbialo por algo \u00fanico para tu plugin para evitar colisiones si tienes varios repos con el mismo setup.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Cuando alguien abre un PR en un plugin de WordPress, el proceso habitual para revisarlo implica descargar la rama, instalarla en local, activar el plugin y probar los cambios a mano. Eso tiene fricciones: hay que tener un entorno local preparado y dedicar tiempo a instalar algo que igual est\u00e1 roto a medias. Si el [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":4756,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":6,"footnotes":""},"categories":[168],"tags":[],"class_list":["post-4601","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-desarrollo-web"],"_links":{"self":[{"href":"https:\/\/davidperezgar.com\/es\/wp-json\/wp\/v2\/posts\/4601","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/davidperezgar.com\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/davidperezgar.com\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/davidperezgar.com\/es\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/davidperezgar.com\/es\/wp-json\/wp\/v2\/comments?post=4601"}],"version-history":[{"count":1,"href":"https:\/\/davidperezgar.com\/es\/wp-json\/wp\/v2\/posts\/4601\/revisions"}],"predecessor-version":[{"id":5977,"href":"https:\/\/davidperezgar.com\/es\/wp-json\/wp\/v2\/posts\/4601\/revisions\/5977"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/davidperezgar.com\/es\/wp-json\/wp\/v2\/media\/4756"}],"wp:attachment":[{"href":"https:\/\/davidperezgar.com\/es\/wp-json\/wp\/v2\/media?parent=4601"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/davidperezgar.com\/es\/wp-json\/wp\/v2\/categories?post=4601"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/davidperezgar.com\/es\/wp-json\/wp\/v2\/tags?post=4601"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}