<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>return(GiS);</title>
	<atom:link href="https://www.returngis.net/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.returngis.net/</link>
	<description>Blog sobre desarrollo, GitHub, DevOps, DevSecOps, AI y developer productivity</description>
	<lastBuildDate>Fri, 03 Apr 2026 09:10:27 +0000</lastBuildDate>
	<language>es</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://i0.wp.com/www.returngis.net/wp-content/uploads/2025/12/cropped-favicon-512-black.png?fit=32%2C32&#038;ssl=1</url>
	<title>return(GiS);</title>
	<link>https://www.returngis.net/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">49406099</site>	<item>
		<title>Cómo funciona launch.json en VS Code: configuraciones, compounds y arranques secuenciales</title>
		<link>https://www.returngis.net/2026/04/como-funciona-launch-json-vscode/</link>
					<comments>https://www.returngis.net/2026/04/como-funciona-launch-json-vscode/#respond</comments>
		
		<dc:creator><![CDATA[Gisela Torres]]></dc:creator>
		<pubDate>Wed, 01 Apr 2026 08:07:09 +0000</pubDate>
				<category><![CDATA[Backend]]></category>
		<category><![CDATA[Microsoft Azure]]></category>
		<guid isPermaLink="false">https://www.returngis.net/?p=31795</guid>

					<description><![CDATA[<p>Una guía práctica para entender launch.json en VS Code usando un ejemplo real con Spring Boot, Vite, serverReadyAction, navegador integrado y compounds.</p>
<p>La entrada <a href="https://www.returngis.net/2026/04/como-funciona-launch-json-vscode/">Cómo funciona launch.json en VS Code: configuraciones, compounds y arranques secuenciales</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>¡Hola developer 👋🏻! Cuando un proyecto tiene más de un componente ejecutable, por ejemplo un backend en Spring Boot y un frontend con Vite, o simplemente quieres poder ejecutar tu aplicación de diferentes formas, el archivo <code>launch.json</code> de VS Code pasa de ser una comodidad a convertirse en una herramienta de trabajo muy útil. Bien configurado, permite arrancar servicios, abrir el navegador y encadenar procesos sin depender de pasos manuales. Esta semana he publicado un vídeo en mi canal de YouTube donde puedes ver diferentes configuraciones para que puedas ver lo útil que es👇🏻</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe class="youtube-player" width="710" height="400" src="https://www.youtube.com/embed/l22L03Mrw2k?version=3&#038;rel=1&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;fs=1&#038;hl=es-ES&#038;autohide=2&#038;wmode=transparent" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe>
</div></figure>



<p>Y en este artículo quiero dejarte las configuraciones que mostré durante el mismo con su correspondiente explicación para que te sea lo más sencillo posible copiarlas y adaptarlas a tus aplicaciones 😃</p>



<h2 class="wp-block-heading">Qué es launch.json</h2>



<p><code>launch.json</code> es el archivo donde VS Code guarda las configuraciones de ejecución y depuración de un proyecto. Se encuentra normalmente dentro de la carpeta <code>.vscode</code> en la raíz del repositorio.</p>



<p>Cada entrada dentro de <code>configurations</code> es una receta. Esa receta le dice a VS Code qué debe ejecutar, con qué depurador, desde qué carpeta y con qué comportamiento adicional.</p>



<p>En este ejemplo real, el archivo define cuatro configuraciones principales y un <code>compound</code>:</p>



<pre class="wp-block-code language-javascript has-small-font-size"><code>{
  "configurations": &#91;
    {
      "type": "java",
      "name": "☕ Backend (Spring Boot)",
      "request": "launch"
    },
    {
      "type": "node",
      "name": "🌐 Frontend (Vite Dev Server)",
      "request": "launch"
    },
    {
      "type": "msedge",
      "name": "🔍 Edge: Debug Frontend",
      "request": "launch"
    },
    {
      "type": "editor-browser",
      "name": "🌐 Launch Integrated Browser",
      "request": "launch"
    }
  ],
  "compounds": &#91;
    {
      "name": "🚀 Full Stack (Backend + Frontend)"
    }
  ]
}</code></pre>



<p>Eso ya deja ver algo importante: <code>launch.json</code> no se limita a una sola forma de arrancar cosas. Puede lanzar procesos Java, ejecutar comandos de Node, abrir Edge para depuración o incluso abrir el navegador integrado de VS Code.</p>



<h2 class="wp-block-heading">La configuración del backend</h2>



<p>La primera pieza del ejemplo es el backend en Spring Boot:</p>



<pre class="wp-block-code language-javascript has-small-font-size"><code>{
  "type": "java",
  "name": "☕ Backend (Spring Boot)",
  "request": "launch",
  "mainClass": "com.ideaforge.quickstart.Application",
  "cwd": "${workspaceFolder}/backend",
  "env": {
    "SPRING_PROFILES_ACTIVE": "default"
  },
  "console": "integratedTerminal"
}</code></pre>



<p>Aquí hay varios campos que merece la pena entender:</p>



<ul class="wp-block-list">
<li><code>type</code> indica que se usará el depurador de Java.</li>



<li><code>mainClass</code> señala la clase principal de Spring Boot que se debe lanzar.</li>



<li><code>cwd</code> fija la carpeta de trabajo en <code>backend</code>.</li>



<li><code>env</code> permite definir variables de entorno, en este caso el perfil activo de Spring.</li>



<li><code>console</code> manda la salida al terminal integrado.</li>
</ul>



<p>Esta configuración sí representa una depuración clásica del backend: VS Code lanza la aplicación Java y puede engancharse al proceso para depurarla.</p>



<h2 class="wp-block-heading">La configuración del frontend con Vite</h2>



<p>La segunda pieza arranca el frontend:</p>



<pre class="wp-block-code language-javascript has-small-font-size"><code>{
  "type": "node",
  "name": "🌐 Frontend (Vite Dev Server)",
  "request": "launch",
  "runtimeExecutable": "npm",
  "runtimeArgs": &#91;
    "run",
    "dev"
  ],
  "cwd": "${workspaceFolder}/frontend",
  "console": "integratedTerminal",
  "skipFiles": &#91;
    "&lt;node_modules&gt;/**"
  ]
}</code></pre>



<p>Aquí el patrón es distinto al de Java. No se está arrancando una clase principal, sino ejecutando <code>npm run dev</code> desde la carpeta <code>frontend</code>. Eso pone en marcha el servidor de desarrollo de Vite.</p>



<p>Este detalle importa porque muchas veces se mezclan dos cosas distintas:</p>



<ul class="wp-block-list">
<li>arrancar el servidor del frontend</li>



<li>abrir un navegador o una sesión de depuración sobre la aplicación web</li>
</ul>



<p>En este <code>launch.json</code> esas responsabilidades están separadas, y eso está bien diseñado.</p>



<h2 class="wp-block-heading">Cómo entra en juego serverReadyAction</h2>



<p>La parte más interesante del ejemplo está dentro de la configuración del frontend, donde aparece <code>serverReadyAction</code>:</p>



<pre class="wp-block-code language-javascript has-small-font-size"><code>{
  "type": "node",
  "name": "🌐 Frontend (Vite Dev Server)",
  "request": "launch",
  "runtimeExecutable": "npm",
  "runtimeArgs": &#91;"run", "dev"],
  "cwd": "${workspaceFolder}/frontend",
  "serverReadyAction": {
    "pattern": "Local:.+(https?://\\S+)",
    "uriFormat": "%s",
    "action": "startDebugging",
    "name": "🌐 Launch Integrated Browser"
  }
}</code></pre>



<p>Esto significa que VS Code vigila la salida del proceso de Vite y espera a detectar una línea como la que imprime normalmente el servidor:</p>



<pre class="wp-block-code language-shell"><code>Local:   http://localhost:5176/</code></pre>



<p>En cuanto encuentra una URL que encaja con el patrón, dispara otra configuración con <code>action: "startDebugging"</code>. En este caso, la configuración que se lanza es <code>🌐 Launch Integrated Browser</code>.</p>



<p>Aquí sí hay una secuencia real: primero Vite, después el navegador. </p>



<h2 class="wp-block-heading">La configuración del navegador integrado</h2>



<p>La configuración que se dispara cuando Vite está listo es esta:</p>



<pre class="wp-block-code language-javascript has-small-font-size"><code>{
  "name": "🌐 Launch Integrated Browser",
  "request": "launch",
  "type": "editor-browser",
  "url": "http://localhost:5176",
  "webRoot": "${workspaceFolder}"
}</code></pre>



<p>Su función es sencilla: abrir la aplicación dentro del navegador integrado de VS Code, usando la URL local del frontend.</p>



<p>Además del navegador integrado, el archivo también incluye una configuración adicional para Edge:</p>



<pre class="wp-block-code language-javascript has-small-font-size"><code>{
  "type": "msedge",
  "name": "🔍 Edge: Debug Frontend",
  "request": "launch",
  "url": "http://localhost:5176",
  "webRoot": "${workspaceFolder}/frontend/src"
}</code></pre>



<p>Esa configuración está oculta en la presentación habitual y sirve como alternativa cuando interesa depurar en Edge con soporte de <em>source maps</em>, pero no es la que forma parte del flujo automático principal.</p>



<h2 class="wp-block-heading">Compounds</h2>



<p>Si lo que te interesa es ejecutar más de una configuración a la vez, que suele ser lo habitual cuando tienes un backend y un frontend, también te interesará configurar lo que se llaman compounds:</p>



<pre class="wp-block-code language-javascript has-small-font-size"><code>{
  "name": "🚀 Full Stack (Backend + Frontend)",
  "configurations": &#91;
    "☕ Backend (Spring Boot)",
    "🌐 Frontend (Vite Dev Server)"
  ],
  "stopAll": true
}</code></pre>



<p>Esto permite lanzar backend y frontend desde una única entrada. Es cómodo, pero conviene entenderlo bien: el <code>compound</code> no establece dependencia entre ambos procesos. Los dos arranques se lanzan en paralelo.</p>



<p>Eso quiere decir que, en este ejemplo, la secuencia controlada existe entre el frontend y el navegador, pero no entre el backend y el frontend.</p>



<p>Dicho de otra forma:</p>



<ol class="wp-block-list">
<li>el <code>compound</code> arranca backend y frontend al mismo tiempo</li>



<li>Vite termina de levantar su servidor</li>



<li><code>serverReadyAction</code> detecta la URL local</li>



<li>VS Code lanza el navegador integrado</li>
</ol>



<p>Este matiz es importante porque a veces se dice que todo está encadenado cuando en realidad solo una parte del flujo lo está.</p>



<h2 class="wp-block-heading">Cuándo usar cada pieza</h2>



<p>Una forma práctica de pensarlo es esta:</p>



<ul class="wp-block-list">
<li>Usa una configuración normal cuando quieres arrancar una sola pieza, como el backend o el frontend.</li>



<li>Usa un <code>compound</code> cuando quieres lanzar varias piezas desde una única orden, aunque eso ocurra en paralelo.</li>



<li>Usa <code>serverReadyAction</code> cuando necesitas reaccionar a una señal real de disponibilidad, como la URL que imprime Vite al arrancar.</li>



<li>Usa una configuración de navegador separada cuando quieres distinguir claramente entre arrancar un servidor y abrir la interfaz.</li>
</ul>



<p>Ese reparto de responsabilidades hace que el archivo sea más fácil de mantener y también más fácil de entender por parte de cualquier persona que se incorpore al proyecto.</p>



<h2 class="wp-block-heading">Si quisieras secuencia completa backend → frontend → navegador</h2>



<p>Este ejemplo no hace eso todavía. El backend y el frontend arrancan en paralelo porque así está definido el <code>compound</code>. Si el objetivo fuera esperar a que Spring Boot estuviera listo antes de lanzar Vite, haría falta introducir un encadenado adicional desde la configuración Java o reestructurar el flujo con tareas y dependencias.</p>



<p>Precisamente por eso este archivo es un buen ejemplo didáctico: muestra muy bien la diferencia entre agrupar procesos y secuenciarlos de verdad.</p>



<h2 class="wp-block-heading">Conclusión</h2>



<p><code>launch.json</code> es mucho más que un archivo para pulsar F5. Bien usado, te permite describir el flujo de arranque real de tu aplicación y reducir bastante la fricción diaria.</p>



<p>La idea más importante es esta: los <code>compounds</code> agrupan, pero no ordenan. El orden real aparece cuando introduces una señal verificable del proceso, como hace aquí <code>serverReadyAction</code> al detectar la URL de Vite y lanzar después el navegador integrado.</p>



<p>Cuando entiendes esa diferencia, <code>launch.json</code> deja de ser un archivo misterioso y se convierte en una herramienta muy potente para automatizar el desarrollo local 🚀</p>



<p>¡Nos vemos 👋🏻!</p>
<p>La entrada <a href="https://www.returngis.net/2026/04/como-funciona-launch-json-vscode/">Cómo funciona launch.json en VS Code: configuraciones, compounds y arranques secuenciales</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.returngis.net/2026/04/como-funciona-launch-json-vscode/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">31795</post-id>	</item>
		<item>
		<title>Dependabot te dice que es vulnerable… ¿pero es explotable de verdad?</title>
		<link>https://www.returngis.net/2026/03/dependabot-te-dice-que-es-vulnerable-pero-es-explotable-de-verdad/</link>
					<comments>https://www.returngis.net/2026/03/dependabot-te-dice-que-es-vulnerable-pero-es-explotable-de-verdad/#respond</comments>
		
		<dc:creator><![CDATA[Gisela Torres]]></dc:creator>
		<pubDate>Sun, 29 Mar 2026 07:24:31 +0000</pubDate>
				<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[Dependabot]]></category>
		<category><![CDATA[GitHub Agentic Workflows]]></category>
		<category><![CDATA[SCA]]></category>
		<guid isPermaLink="false">https://www.returngis.net/?p=31787</guid>

					<description><![CDATA[<p>Esta PoC usa un GitHub Agentic Workflow para analizar PRs de Dependabot con más contexto: consulta la advisory completa, busca el patrón vulnerable real, explica el papel del modelo usado y prioriza según si la explotabilidad está o no demostrada.</p>
<p>La entrada <a href="https://www.returngis.net/2026/03/dependabot-te-dice-que-es-vulnerable-pero-es-explotable-de-verdad/">Dependabot te dice que es vulnerable… ¿pero es explotable de verdad?</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="sg-ai-highlighted-block">¡Hola developer 👋🏻!</p>



<p>Cuando Dependabot te abre una pull request de seguridad, lo primero que sabes es esto: una versión concreta de una dependencia está dentro de un rango vulnerable.</p>



<p>Perfecto 👍<br>Pero… eso no responde a la pregunta importante: <strong>¿esto es realmente explotable en mi código?</strong></p>



<p>Porque una cosa es que exista una advisory…<br>y otra muy distinta es que <strong>tu aplicación esté usando justo ese patrón vulnerable</strong> y además <strong>se cumplan las condiciones para explotarlo</strong>.</p>



<p>Con esa idea en mente he jugado con una prueba de concepto en la que he estado trabajando en el repositorio <a href="https://github.com/0GiS0/dependabot-security-analyzer">dependabot-security-analyzer</a>. La idea en este caso es usar un <strong>GitHub Agentic Workflow</strong> para analizar una PR de Dependabot de forma más profunda: identificar la dependencia afectada, consultar la advisory completa, revisar el repositorio y <strong>decidir si el caso merece prioridad alta o si la exposición no está realmente demostrada</strong>. En este artículo te muestro cómo lo estoy comprobando gracias a estos nuevos flujos.</p>



<p>Si todavía no has visto qué son los GitHub Agentic Workflows, aquí tienes un vídeo donde te lo explico en 30 minutos:</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe class="youtube-player" width="710" height="400" src="https://www.youtube.com/embed/TGX30Z9YVnY?version=3&#038;rel=1&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;fs=1&#038;hl=es-ES&#038;autohide=2&#038;wmode=transparent" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe>
</div></figure>



<h2 class="wp-block-heading">El problema no es detectar vulnerabilidades (eso ya lo hacemos bien)</h2>



<p>Herramientas como Dependabot hacen muy bien la parte de detectar versiones afectadas y proponer actualizaciones. Pero entre <em>“la dependencia es vulnerable”</em> y <em>“mi aplicación es explotable por esa vulnerabilidad”</em> hay bastante distancia. A veces tu código ni siquiera usa la parte afectada. Otras veces sí la usa, pero sin cumplir las condiciones reales del exploit. Y en otros casos sí se da toda la cadena y entonces esa PR merece atención inmediata.</p>



<p>La intención de esta PoC <span style="text-decoration: underline;">no es discutir si hay que actualizar o no</span>. <strong>Las dependencias vulnerables conviene corregirlas igualmente</strong>. La cuestión aquí es otra: <strong>aportar más contexto para priorizar mejor</strong>, reducir ruido y distinguir entre presencia de una advisory y exposición real en el código.</p>



<h2 class="wp-block-heading">Cómo funciona el workflow</h2>



<p>El flujo está definido en lenguaje natural <a href="https://github.com/0GiS0/dependabot-security-analyzer/blob/main/.github/workflows/dependabot-security-analyzer.md" target="_blank" rel="noreferrer noopener">dentro del propio workflow</a> y se activa cuando un usuario de confianza comenta <code>/dependabot-analyze</code> en una pull request. El comando tiene que ser el primer token del comentario. A partir de ahí, el agente solo sigue adelante si el evento está realmente ligado a una PR y si esa PR ha sido abierta por <code>dependabot[bot]</code>.</p>



<p>Ese filtro es importante porque el objetivo no es analizar cualquier cambio, sino específicamente pull requests automáticas de seguridad. Si el comentario no está en una PR o la PR no es de Dependabot, el flujo se detiene y deja una explicación breve.</p>



<p>El frontmatter del workflow deja bastante claro cómo está configurado este ejemplo. Aquí se define el trigger, los permisos, las herramientas disponibles, la red permitida y también el engine y el modelo que quiero usar:</p>



<pre class="wp-block-code language-yaml has-small-font-size"><code>---
name: 🛡️ VulnScope PR Guard 🔎

on:
  slash_command:
    name: dependabot-analyze
    events: &#91;pull_request_comment]
  reaction: eyes

permissions:
  contents: read
  pull-requests: read
  security-events: read

tools:
  github:
    toolsets: &#91;context, repos, pull_requests, security_advisories]

network:
  allowed:
    - defaults

engine:
  id: copilot
  model: gpt-4.1

safe-outputs:
  add-labels:
    allowed: &#91;"🔴 vulnerability:in-use", "🟢 vulnerability:not-in-use", "🤔 vulnerability:unclear", "🚨 priority:high", "🕓 priority:low"]
    max: 2
  add-comment:
    max: 1
---</code></pre>



<p>En este ejemplo he usado <code>engine: copilot</code> y el modelo <code>gpt-4.1</code> por una razón bastante práctica: no consume <em>premium requests</em> y quería comprobar si era capaz de resolver bien un caso como este. La parte interesante es que esto no está cerrado. El workflow se puede adaptar para usar otros modelos <strong>e incluso otros agentes de IA si hiciera falta</strong>, dependiendo del nivel de razonamiento, del coste o del tipo de repositorio que quieras analizar.</p>



<p>También es importante tener en cuenta que este tipo de flujos no siempre se comportan igual con todos los modelos. A veces el prompt necesita pequeños ajustes en cómo formula las instrucciones, cómo restringe la salida o cómo pide justificar el veredicto. Yo lo voy iterando y afinando con el tiempo, y no me extrañaría que la versión óptima del prompt para un modelo no fuese exactamente la mejor para otro.</p>



<p>También es posible que en lugar de un slash command se lanzara siempre que ocurre una PR ejecutada por Dependabot, pero en este caso quería ser yo quien lo controlara. También por supuesto se puede «complicar más» y que cada vez que se haga push del código si todavía hay alertas de Dependabot pendientes que se ordene la re-ejecución de estos flujos para ver si en una siguiente iteración no solo no se ha arreglado la versión de la dependencia sino que encima ahora se está haciendo uso de la funcionalidad que la hace vulnerable 😅</p>



<p>Una vez validado el contexto, el workflow toma la propia pull request como fuente inicial para identificar qué paquete se actualiza, desde qué versión y hacia cuál. Si en el PR aparecen identificadores como un CVE o un GHSA, los usa como punto de partida; si no aparecen, intenta inferir la advisory a partir del paquete y del rango de versiones.</p>



<h2 class="wp-block-heading">La parte clave: ir a la advisory completa</h2>



<p>Aquí está una de las diferencias importantes respecto a una automatización más superficial. El flujo no se conforma con el resumen del PR de Dependabot. Va a buscar los detalles completos en GitHub Security Advisories para entender qué patrón concreto es vulnerable, qué versiones están afectadas, qué CWE aplica y, sobre todo, qué condiciones hacen falta para que el problema sea explotable.</p>



<p>Eso cambia bastante el tipo de análisis que se puede hacer. No es lo mismo detectar que una librería está instalada que saber que la advisory solo afecta a una función concreta, a cierto modo de invocación o a un caso donde el input del usuario llega sin validación a un punto determinado. El workflow intenta aterrizar precisamente eso.</p>



<p>Si la advisory pública no da suficiente detalle técnico, el planteamiento del flujo contempla ampliar contexto con referencias adicionales y fuentes públicas más detalladas. La idea es que el veredicto no salga de una lectura superficial del PR, sino de entender bien el patrón vulnerable que describe la advisory.</p>



<h2 class="wp-block-heading">El prompt también forma parte del diseño</h2>



<p>Otro punto que me parece muy interesante es que el comportamiento del workflow no está definido solo por el frontmatter. El prompt en sí también es parte central del diseño, porque es donde se obliga al agente a seguir un orden concreto, a no confundir riesgos adyacentes con la advisory analizada y a justificar por qué marca un caso como realmente explotable o no.</p>



<p>Este es un fragmento representativo del prompt actual:</p>



<pre class="wp-block-code language-text has-small-font-size"><code>## Trigger

This workflow runs when a trusted user comments `/dependabot-analyze` on a pull request.
The command must be the first token in the comment.

If the command was not used on a pull request, stop immediately and do nothing.

You must then check if the target PR was created by Dependabot (author is `dependabot&#91;bot]`).
If it was NOT created by Dependabot, stop here and leave a short comment explaining that this command only supports Dependabot PRs.

## Your tasks

### 3. Get complete vulnerability details from GitHub Security Advisories
- Extract from the advisory:
  - Vulnerable version range
  - Vulnerable code patterns
  - CWE classification
  - Exploitation conditions
  - References

### 4. Analyze the codebase
- Distinguish clearly between these three questions:
  - Is the vulnerable method/pattern present?
  - Are the advisory's exploitation preconditions actually satisfied in this codebase?
  - Is there a different security issue nearby that is real but outside the scope of this advisory?

### 6. Add labels to the PR
- `🔴 vulnerability:in-use` + `🚨 priority:high`
- `🟢 vulnerability:not-in-use` + `🕓 priority:low`
- `🤔 vulnerability:unclear` when the vulnerable pattern cannot be determined</code></pre>



<p>Me gusta enseñar esta parte porque aquí se ve muy bien que el valor no está solo en “usar IA”, sino en cómo se define el criterio. El prompt no le pide al agente que mire si una librería sale en un fichero, sino que determine si el patrón vulnerable existe de verdad, si las precondiciones del exploit están demostradas y si hay que separar ese análisis de otros problemas de seguridad que puedan aparecer cerca.</p>



<p>Y aquí vuelve a entrar el tema del modelo. Este prompt seguramente seguirá cambiando. Yo lo voy mejorando a medida que pruebo más escenarios y, muy probablemente, algunas decisiones de redacción, granularidad o formato de salida tendrán que ser un poco distintas según el modelo o el agente que quiera usar detrás.</p>



<p>Pero lo que si que es realmente importante aquí es: esta ayuda adicional es más que cero.</p>



<h2 class="wp-block-heading">Qué devuelve en la PR de Dependabot</h2>



<p>El resultado del análisis se deja directamente en la propia PR. El workflow comprueba primero si existen las labels que necesita y, si no están, las crea. Luego aplica una combinación de etiquetas según el veredicto:</p>



<ul class="wp-block-list">
<li><code>🔴 vulnerability:in-use</code> y <code>🚨 priority:high</code> si el patrón vulnerable está en uso y las precondiciones del exploit están demostradas o muy respaldadas por el código.</li>



<li><code>🟢 vulnerability:not-in-use</code> y <code>🕓 priority:low</code> si el patrón vulnerable no aparece, el uso es seguro o la explotabilidad no está demostrada.</li>



<li><code>🤔 vulnerability:unclear</code> si después de consultar advisories y fuentes públicas no hay suficiente detalle técnico para identificar el patrón vulnerable con confianza.</li>
</ul>



<p>Algo como esto:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/PR-de-Dependabot-Analizadas-por-la-IA-scaled.png?ssl=1"><img data-recalc-dims="1" fetchpriority="high" decoding="async" width="710" height="373" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/PR-de-Dependabot-Analizadas-por-la-IA.png?resize=710%2C373&#038;ssl=1" alt="" class="wp-image-31807" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/PR-de-Dependabot-Analizadas-por-la-IA-scaled.png?resize=1024%2C538&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/PR-de-Dependabot-Analizadas-por-la-IA-scaled.png?resize=300%2C158&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/PR-de-Dependabot-Analizadas-por-la-IA-scaled.png?resize=768%2C404&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/PR-de-Dependabot-Analizadas-por-la-IA-scaled.png?resize=1046%2C550&amp;ssl=1 1046w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/PR-de-Dependabot-Analizadas-por-la-IA-scaled.png?resize=1060%2C557&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/PR-de-Dependabot-Analizadas-por-la-IA-scaled.png?resize=1536%2C807&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/PR-de-Dependabot-Analizadas-por-la-IA-scaled.png?resize=2048%2C1077&amp;ssl=1 2048w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/PR-de-Dependabot-Analizadas-por-la-IA-scaled.png?resize=550%2C289&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/PR-de-Dependabot-Analizadas-por-la-IA-scaled.png?resize=951%2C500&amp;ssl=1 951w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/PR-de-Dependabot-Analizadas-por-la-IA-scaled.png?resize=1920%2C1009&amp;ssl=1 1920w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/PR-de-Dependabot-Analizadas-por-la-IA-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<p>Además deja un comentario estructurado con el resumen de la advisory, severidad, CWE, patrón vulnerable, condiciones de explotación, referencias y una tabla por archivos donde distingue entre uso del patrón, precondiciones cumplidas, riesgos adyacentes y veredicto final.</p>



<p>Ese detalle me parece importante porque convierte la PR en algo bastante más accionable. No es solo una actualización propuesta por Dependabot, sino una revisión con criterio explícito sobre si el riesgo está realmente activado en el código del repositorio.</p>



<h2 class="wp-block-heading">Lo que me parece interesante de esta PoC</h2>



<p>Lo que más me interesa aquí no es solo automatizar pasos, sino introducir mejor criterio de priorización en un problema muy ruidoso. En muchos repositorios acaban acumulándose alertas, PRs automáticas y decisiones rápidas basadas únicamente en la existencia de una advisory. Tener un flujo que intente responder si el patrón vulnerable está de verdad en uso me parece bastante más útil.</p>



<p>También me gusta porque obliga al análisis a no mezclar hallazgos distintos. Puede haber una advisory que no aplique realmente a tu código y, al mismo tiempo, puede existir otro problema de seguridad cerca de ese mismo flujo. El workflow intenta dejar claro cuándo está hablando de la advisory concreta de Dependabot y cuándo se trata de un riesgo adyacente distinto.</p>



<h2 class="wp-block-heading">Una forma más útil de revisar PRs de seguridad</h2>



<p>En definitiva, esta prueba de concepto busca responder una pregunta que Dependabot, y otras herramientas, no resuelven a día de hoy del todo: no solo si una dependencia está afectada por una advisory, sino si tu repositorio está usando realmente el patrón que convierte esa advisory en un problema práctico.</p>



<p>Para mí ese es el punto interesante de combinar GitHub Agentic Workflows con análisis de seguridad de dependencias: usar la automatización no solo para abrir PRs, sino para devolver contexto que ayude a decidir mejor qué corregir primero y por qué.</p>



<p>Si quieres explorar otros escenarios en los que GitHub Agentic Workflows pueden tener sentido aquí te dejo otro vídeo donde te muestro dónde lo estoy usando yo:</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe class="youtube-player" width="710" height="400" src="https://www.youtube.com/embed/L2hvVm10jLs?version=3&#038;rel=1&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;fs=1&#038;hl=es-ES&#038;autohide=2&#038;wmode=transparent" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe>
</div></figure>



<p>¡Nos vemos 👋🏻!</p>
<p>La entrada <a href="https://www.returngis.net/2026/03/dependabot-te-dice-que-es-vulnerable-pero-es-explotable-de-verdad/">Dependabot te dice que es vulnerable… ¿pero es explotable de verdad?</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.returngis.net/2026/03/dependabot-te-dice-que-es-vulnerable-pero-es-explotable-de-verdad/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">31787</post-id>	</item>
		<item>
		<title>🔐 Cómo configurar CodeQL para ejecutar solo las queries que necesitas (OWASP Top 10 + custom queries)</title>
		<link>https://www.returngis.net/2026/03/%f0%9f%94%90-como-configurar-codeql-para-ejecutar-solo-las-queries-que-necesitas-owasp-top-10-custom-queries/</link>
					<comments>https://www.returngis.net/2026/03/%f0%9f%94%90-como-configurar-codeql-para-ejecutar-solo-las-queries-que-necesitas-owasp-top-10-custom-queries/#respond</comments>
		
		<dc:creator><![CDATA[Gisela Torres]]></dc:creator>
		<pubDate>Fri, 20 Mar 2026 12:31:11 +0000</pubDate>
				<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[CodeQL]]></category>
		<category><![CDATA[GitHub Advanced Security]]></category>
		<category><![CDATA[OWASP]]></category>
		<guid isPermaLink="false">https://www.returngis.net/?p=31769</guid>

					<description><![CDATA[<p>¡Hola developer 👋🏻! Si estás evaluando GitHub Advanced Security es posible que cuando revises la parte de Code scanning, que analiza las vulnerabilidades que podemos introducir, sin querer claro está 😅, en nuestro propio código, puede que te preguntes: 👉 “¿Puedo controlar qué analiza CodeQL?”👉 “¿Puedo limitarlo al OWASP Top 10 e incluso incluir mis propias reglas?” Y la respuesta es: sí y en este... </p>
<p class="more"><a class="more-link" href="https://www.returngis.net/2026/03/%f0%9f%94%90-como-configurar-codeql-para-ejecutar-solo-las-queries-que-necesitas-owasp-top-10-custom-queries/">Leer</a></p>
<p>La entrada <a href="https://www.returngis.net/2026/03/%f0%9f%94%90-como-configurar-codeql-para-ejecutar-solo-las-queries-que-necesitas-owasp-top-10-custom-queries/">🔐 Cómo configurar CodeQL para ejecutar solo las queries que necesitas (OWASP Top 10 + custom queries)</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>¡Hola developer 👋🏻!</p>



<p>Si estás evaluando GitHub Advanced Security es posible que cuando revises la parte de Code scanning, que analiza las vulnerabilidades que podemos introducir, sin querer claro está 😅, en nuestro propio código, puede que te preguntes:</p>



<p>👉 <em>“¿Puedo controlar qué analiza CodeQL?”</em><br>👉 <em>“¿Puedo limitarlo al OWASP Top 10 e incluso incluir mis propias reglas?”</em></p>



<p>Y la respuesta es: <strong>sí</strong> y<strong> </strong>en este artículo te cuento cómo configurarlo. </p>



<p>Si no conoces muchos de GitHub Advanced Security aquí te dejo un vídeo de mi canal donde hago un repaso de sus funcionalidades principals:</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe class="youtube-player" width="710" height="400" src="https://www.youtube.com/embed/lHcLeZangg0?version=3&#038;rel=1&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;fs=1&#038;hl=es-ES&#038;autohide=2&#038;wmode=transparent" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe>
</div></figure>



<h2 class="wp-block-heading">🤔 ¿Por qué limitar las queries de CodeQL?</h2>



<p>Por defecto, CodeQL ejecuta cientos de queries de seguridad. <a href="https://codeql.github.com/codeql-query-help/codeql-cwe-coverage/">Aquí puedes encontrar las CWEs que cubre a día de hoy</a>. Esto está genial… pero en algunos casos algunos el sentimiento es:</p>



<p>❌ Ejecuciones muy largas en repositorios inmensos<br>❌ Genera ruido<br>❌ Puede ser difícil priorizar<br>❌ No siempre encaja con tus políticas de seguridad</p>



<p>Y aquí es donde entra el verdadero valor: 👉 <strong>definir tu propio conjunto de queries</strong> siguiendo marcos normativos o incluso añadiendo alguna query interna propia que también se quiere tener en consideración.</p>



<h2 class="wp-block-heading">📦 Repositorio de ejemplo</h2>



<p>Así para poder ilustrar esto <a href="https://github.com/0GiS0/ghas-owasp-codeql-demo" target="_blank" rel="noreferrer noopener">he creado un repositorio que te dejo en mi cuenta de GitHub</a>, en el cual me he marcado dos objetivos:</p>



<h3 class="wp-block-heading">1. Ejecutar solo un subconjunto de queries</h3>



<p>En lugar de usar:</p>



<pre class="wp-block-code language-yaml has-small-font-size"><code>queries: security-and-quality
</code></pre>



<p>Puedes definir exactamente qué quieres ejecutar:</p>



<ul class="wp-block-list">
<li>OWASP Top 10</li>



<li>solo ciertos CWE</li>



<li>queries críticas</li>



<li>etc.</li>
</ul>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<h3 class="wp-block-heading">OWASP Top 10 es el estándar más común en AppSec.</h3>
</blockquote>



<p>Cada categoría (por ejemplo <em>Injection</em>) se corresponde con múltiples CWE y queries.</p>



<p>👉 Ejemplo:</p>



<ul class="wp-block-list">
<li>SQL Injection → CWE-89</li>



<li>XSS → CWE-79</li>
</ul>



<p>Y CodeQL ya tiene queries que cubren estos casos.</p>



<p>Incluso puedes crear suites específicas tipo:</p>



<pre class="wp-block-code language-yaml has-small-font-size"><code>name: OWASP-Top-10
queries:
  - codeql/java/queries/security/sql-injection.ql
  - codeql/javascript/queries/security/xss.ql
</code></pre>



<p>Este enfoque está bastante extendido en la comunidad (<a href="https://medium.com/javarevisited/codeql-query-suites-in-ghas-a-practical-guide-for-java-teams-6374d612b989?utm_source=chatgpt.com">Medium</a>)</p>



<h3 class="wp-block-heading">2. Añadir custom queries</h3>



<p>Aquí es donde la cosa se pone interesante 🔥 CodeQL permite escribir queries propias porque:</p>



<p>👉 trata el código como datos consultables<br>👉 puedes detectar patrones específicos de tu empresa</p>



<p>Ejemplos reales:</p>



<ul class="wp-block-list">
<li>uso de librerías internas inseguras</li>



<li>validaciones corporativas obligatorias</li>



<li>patrones legacy peligrosos</li>
</ul>



<p>Vale, pues ahora ¿Cómo configuramos esto? Pues vamos a verlo 😃</p>



<h2 class="wp-block-heading">Configuración del flujo avanzado de GitHub Actions con queries personalizadas para CodeQL</h2>



<p>Para que todo esto sea posible tienes que tener configurado CodeQL en su modo avanzado. Esto lo que significa es que necesitamos tener el flujo definido en el repo o al menos tener un flujo reusable en algún sitio donde tener toda esta configuración. Pero antes de nada necesitamos un archivo de configuración con las reglas personalizadas.</p>



<h3 class="wp-block-heading">1. Crear tu archivo de configuración</h3>



<p>En <code><strong>.github/codeql/codeql-config.yml</strong></code>:</p>



<p>Lo que tengo es algo como lo siguiente:</p>



<pre class="wp-block-code language-yaml has-small-font-size"><code># Configuración de CodeQL para OWASP Top 10
# Este archivo indica a CodeQL que use SOLO la query suite personalizada
# de OWASP Top 10, en lugar de las suites predefinidas (default o security-extended).
#
# Documentación:
# https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning

name: "CodeQL - OWASP Top 10"

# ⚠️ CLAVE: Desactivar las queries predefinidas (default suite)
# Sin esto, CodeQL ejecuta las queries por defecto ADEMÁS de las personalizadas
disable-default-queries: true

# Queries a ejecutar:
# 1. Suite OWASP Top 10 (2025) - queries estándar filtradas por marco normativo
# 2. Queries personalizadas de negocio bancario (custom rules)
queries:
  - uses: ./.github/codeql/owasp-top-10-js.qls
  - uses: ./.github/codeql/custom-queries

# Paths a analizar (solo el código fuente de la aplicación)
paths:
  - src

# Paths a ignorar (tests, fixtures, etc.)
paths-ignore:
  - node_modules
  - "**/*.test.js"
  - "**/*.spec.js"</code></pre>



<p>Este me va a permitir indicar dónde están las queries que quiero lanzar. En el caso del archivo .github/codeql/owasp-top-10-js.qls lo que he hecho es algo como lo siguiente:</p>



<pre class="wp-block-code language-yaml has-small-font-size"><code># -----------------------------------------------------------
# CodeQL Query Suite: OWASP Top 10 (2025) - JavaScript/TypeScript
# -----------------------------------------------------------
#
# Este archivo define un conjunto personalizado de queries de CodeQL
# que cubren exclusivamente las categorías del OWASP Top 10 (2025).
#
# Al usar esta suite en lugar de "security-extended" o "default",
# solo se ejecutarán las queries relevantes para OWASP Top 10,
# filtrando cualquier otra regla que no aplique a este marco normativo.
#
# Documentación de query suites:
# https://codeql.github.com/docs/codeql-cli/creating-codeql-query-suites/
#
# Lista completa de CWEs cubiertos por CodeQL:
# https://codeql.github.com/codeql-query-help/full-cwe/
#
# -----------------------------------------------------------
# Para limitar aún más por severidad o precisión, descomenta:
#- include:
#    problem.severity:
#    - medium
#    - high
#    - very-high
#    - critical
#    precision:
#    - high
#    - very-high
# -----------------------------------------------------------

# Importar el pack de queries de JavaScript/TypeScript
# Necesario cuando se usa disable-default-queries: true en la config
- qlpack: codeql/javascript-queries

- include:
    id:

    # =========================================================
    # A01:2025 - Broken Access Control
    # Mantiene #1. Ahora incluye SSRF (antes era A10:2021)
    # CWE-22: Path Traversal
    # CWE-601: URL Redirection to Untrusted Site
    # CWE-918: SSRF (movido desde A10:2021)
    # =========================================================
    - js/path-injection
    - js/ml-powered/path-injection
    - js/server-side-unvalidated-url-redirection
    - js/client-side-unvalidated-url-redirection
    - js/user-controlled-bypass
    - js/exposure-of-private-files
    - js/request-forgery
    - js/client-side-request-forgery
    - js/file-access-to-http
    - js/http-to-file-access
    - js/sensitive-get-query

    # =========================================================
    # A02:2025 - Security Misconfiguration
    # Sube de A05:2021 a A02:2025
    # CWE-16: Configuration
    # CWE-611: XXE
    # =========================================================
    - js/cors-misconfiguration-for-credentials
    - js/disabling-certificate-validation
    - js/disabling-electron-websecurity
    - js/enabling-electron-insecure-content
    - js/functionality-from-untrusted-source
    - js/unsafe-external-link
    - js/double-escaping
    - js/incomplete-hostname-regexp
    - js/incomplete-url-scheme-check
    - js/incomplete-url-substring-sanitization
    - js/incorrect-suffix-check
    - js/incomplete-sanitization
    - js/incomplete-html-attribute-sanitization
    - js/incomplete-multi-character-sanitization
    - js/samesite-none-cookie

    # =========================================================
    # A03:2025 - Software Supply Chain Failures
    # Expandido desde A06:2021 (Vulnerable and Outdated Components)
    # (Dependabot + CodeQL para dependencias inseguras)
    # =========================================================
    - js/insecure-dependency
    - js/insecure-download

    # =========================================================
    # A04:2025 - Cryptographic Failures
    # Baja de A02:2021 a A04:2025
    # CWE-327: Use of Broken/Weak Crypto Algorithm
    # CWE-328: Reversible One-Way Hash
    # CWE-330: Insufficient Randomness
    # =========================================================
    - js/weak-cryptographic-algorithm
    - js/insufficient-key-size
    - js/biased-cryptographic-random
    - js/insecure-randomness
    - js/clear-text-storage-of-sensitive-data
    - js/clear-text-logging
    - js/clear-text-cookie

    # =========================================================
    # A05:2025 - Injection
    # Baja de A03:2021 a A05:2025
    # CWE-79: XSS
    # CWE-78: OS Command Injection
    # CWE-89: SQL Injection
    # CWE-94: Code Injection
    # CWE-917: Expression Language Injection
    # CWE-1321: Prototype Pollution
    # =========================================================
    - js/sql-injection
    - js/ml-powered/sql-injection
    - js/nosql-injection
    - js/ml-powered/nosql-injection
    - js/reflected-xss
    - js/stored-xss
    - js/xss
    - js/xss-through-dom
    - js/xss-through-exception
    - js/ml-powered/xss
    - js/command-line-injection
    - js/indirect-command-line-injection
    - js/shell-command-constructed-from-input
    - js/shell-command-injection-from-environment
    - js/code-injection
    - js/unsafe-code-construction
    - js/bad-code-sanitization
    - js/bad-tag-filter
    - js/html-constructed-from-input
    - js/unsafe-html-expansion
    - js/prototype-pollution
    - js/prototype-polluting-assignment
    - js/prototype-pollution-utility
    - js/tainted-format-string
    - js/template-object-injection
    - js/xpath-injection
    - js/xml-bomb
    - js/xxe
    - js/log-injection
    - js/regex/missing-regexp-anchor

    # =========================================================
    # A06:2025 - Insecure Design
    # Baja de A04:2021 a A06:2025
    # CWE-799: Improper Control of Interaction Frequency
    # =========================================================
    - js/missing-rate-limiting

    # =========================================================
    # A07:2025 - Authentication Failures
    # Mantiene #7 (renombrado de "Identification and Auth Failures")
    # CWE-798: Hardcoded Credentials
    # CWE-259: Hardcoded Password
    # CWE-384: Session Fixation
    # =========================================================
    - js/hardcoded-credentials
    - js/empty-password-in-configuration-file
    - js/password-in-configuration-file
    - js/jwt-missing-verification
    - js/missing-token-validation
    - js/missing-origin-check
    - js/session-fixation
    - js/insufficient-password-hash
    - js/client-exposed-cookie

    # =========================================================
    # A08:2025 - Software or Data Integrity Failures
    # Mantiene #8
    # CWE-502: Deserialization of Untrusted Data
    # =========================================================
    - js/unsafe-deserialization
    - js/unsafe-jquery-plugin
    - js/zipslip
    - js/identity-replacement

    # =========================================================
    # A09:2025 - Security Logging &amp; Alerting Failures
    # Mantiene #9 (renombrado, ahora enfatiza "Alerting")
    # CWE-778: Insufficient Logging
    # (CodeQL tiene cobertura limitada aquí)
    # =========================================================
    - js/stack-trace-exposure
    - js/cross-window-information-leak

    # =========================================================
    # A10:2025 - Mishandling of Exceptional Conditions (NUEVO)
    # Reemplaza SSRF (que se movió a A01)
    # CWE-248: Uncaught Exception
    # CWE-754: Improper Check for Unusual/Exceptional Conditions
    # CWE-391: Unchecked Error Condition
    # =========================================================
    - js/resource-exhaustion

    # =========================================================
    # GitHub Actions (bonus - seguridad en CI/CD)
    # =========================================================
    - js/actions/command-injection
    - js/actions/pull-request-target
    - js/build-artifact-leak
    - js/host-header-forgery-in-email-generation
    - js/remote-property-injection
    - js/unsafe-dynamic-method-access
    - js/useless-regexp-character-escape</code></pre>



<p>Aquí no te voy a engañar, le he pedido ayuda a GitHub Copilot para recopilar todas las reglas que me hacen falta y sería cuestión de probarlo bien para que no se nos olvide nada. </p>



<p>El segundo archivo apunta a una custom query en CodeQL que valida que haya una validación previa a ciertas operaciones que pueden ser críticas, por ejemplo en un banco. En la configuración a lo que se está apuntando en realidad es a una carpeta dentro de la cual hay dos archivos, el qlpack.yml:</p>



<pre class="wp-block-code language-yaml has-small-font-size"><code># CodeQL Query Pack personalizado para reglas de negocio bancarias
# Este pack contiene queries custom que no están en las suites estándar de CodeQL
name: custom-banking-queries
version: 0.0.1
dependencies:
  codeql/javascript-all: "*"</code></pre>



<p>Y por otro lado uno que he llamado <strong>missing-context-validation.ql</strong>:</p>



<pre class="wp-block-code language-sql has-small-font-size"><code>/**
 * @name Operación bancaria sensible sin validación de contexto
 * @description Detecta llamadas a operaciones bancarias sensibles que no están
 *              precedidas por una llamada de validación de contexto en la misma función.
 *              En entornos bancarios, toda operación crítica debe validar el contexto
 *              (sesión, autorización) antes de ejecutarse.
 * @kind problem
 * @problem.severity error
 * @precision high
 * @id custom/missing-context-validation
 * @tags security
 *       banking
 *       compliance
 *       custom
 */

import javascript

/**
 * Nombres de operaciones bancarias sensibles.
 * Cualquier llamada a estas funciones DEBE estar precedida
 * por una validación de contexto en la misma función.
 */
predicate isSensitiveOperationName(string name) {
  name = "executeTransaction" or
  name = "transferFunds" or
  name = "processPayment" or
  name = "approveCredit" or
  name = "withdrawFunds"
}

/**
 * Nombres de funciones de validación de contexto.
 * Al menos una de estas debe aparecer antes de cualquier operación sensible.
 */
predicate isValidationFunctionName(string name) {
  name = "validateContext" or
  name = "validateSession" or
  name = "verifyAuthorization"
}

/**
 * Obtiene el nombre de la función llamada, sea como llamada directa
 * (executeTransaction(...)) o como método (bankService.executeTransaction(...))
 */
string getCallName(CallExpr call) {
  result = call.getCalleeName()
  or
  exists(DotExpr dot |
    dot = call.getCallee() and
    result = dot.getPropertyName()
  )
}

from CallExpr sensitiveCall, Function enclosingFunc, string opName
where
  // Identificar llamadas a operaciones sensibles
  opName = getCallName(sensitiveCall) and
  isSensitiveOperationName(opName) and
  // Obtener la función que contiene la llamada
  enclosingFunc = sensitiveCall.getEnclosingFunction() and
  // Verificar que NO existe una llamada de validación ANTES en la misma función
  not exists(CallExpr validationCall, string valName |
    valName = getCallName(validationCall) and
    isValidationFunctionName(valName) and
    validationCall.getEnclosingFunction() = enclosingFunc and
    validationCall.getLocation().getStartLine() &lt; sensitiveCall.getLocation().getStartLine()
  )
select sensitiveCall,
  "La operación bancaria sensible '" + opName +
  "' se ejecuta sin validación de contexto previa. " +
  "Añade una llamada a validateContext(), validateSession() o verifyAuthorization() antes de esta operación."</code></pre>



<p>Con todo esto ya solo nos queda usarlo como parte de nuestro flujo de GitHub Actions para esa configuración avanzada de CodeQL.</p>



<h2 class="wp-block-heading">.github/workflows/codeql.yml</h2>



<p>💡 Aquí es donde conectas todo:</p>



<pre class="wp-block-code language-yaml has-small-font-size"><code>name: "🛡️ CodeQL - OWASP Top 10"

on:
  push:
    branches: &#91; "main" ]
  pull_request:
    branches: &#91; "main" ]
  # Escaneo programado semanal (lunes a las 8:00 UTC)
  schedule:
    - cron: '0 8 * * 1'

jobs:
  analyze:
    name: Analyze (OWASP Top 10)
    runs-on: ubuntu-latest
    permissions:
      security-events: write
      contents: read
      actions: read

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4

    - name: Initialize CodeQL
      uses: github/codeql-action/init@v4
      with:
        languages: javascript-typescript
        # Usar la configuración personalizada que apunta a la suite OWASP
        config-file: ./.github/codeql/codeql-config.yml

    - name: Autobuild
      uses: github/codeql-action/autobuild@v4

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v4
      with:
        category: "/language:javascript-typescript"</code></pre>



<p>Aquí lo importante es que en el paso <strong>init</strong> le pasamos el <strong>config file</strong> que definí más arriba y con ello ya sabe cuáles son las reglas, preconstruidas y generadas por mi, que tiene que utilizar.</p>



<p>En el caso de las personalizadas, en este ejemplo, verás algo como lo siguiente:</p>



<figure class="wp-block-image size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/CodeQL-custom-query-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="325" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/CodeQL-custom-query.png?resize=710%2C325&#038;ssl=1" alt="" class="wp-image-31781" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/CodeQL-custom-query-scaled.png?resize=1024%2C469&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/CodeQL-custom-query-scaled.png?resize=300%2C137&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/CodeQL-custom-query-scaled.png?resize=768%2C352&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/CodeQL-custom-query-scaled.png?resize=1201%2C550&amp;ssl=1 1201w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/CodeQL-custom-query-scaled.png?resize=1060%2C486&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/CodeQL-custom-query-scaled.png?resize=1536%2C704&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/CodeQL-custom-query-scaled.png?resize=2048%2C938&amp;ssl=1 2048w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/CodeQL-custom-query-scaled.png?resize=550%2C252&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/CodeQL-custom-query-scaled.png?resize=1091%2C500&amp;ssl=1 1091w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/CodeQL-custom-query-scaled.png?resize=1920%2C880&amp;ssl=1 1920w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/CodeQL-custom-query-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>



<p>y el detalle se vería así:</p>



<figure class="wp-block-image size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/Detalle-de-una-custom-query-de-CodeQL-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="422" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/Detalle-de-una-custom-query-de-CodeQL.png?resize=710%2C422&#038;ssl=1" alt="" class="wp-image-31782" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/Detalle-de-una-custom-query-de-CodeQL-scaled.png?resize=1024%2C609&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/Detalle-de-una-custom-query-de-CodeQL-scaled.png?resize=300%2C178&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/Detalle-de-una-custom-query-de-CodeQL-scaled.png?resize=768%2C457&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/Detalle-de-una-custom-query-de-CodeQL-scaled.png?resize=925%2C550&amp;ssl=1 925w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/Detalle-de-una-custom-query-de-CodeQL-scaled.png?resize=1060%2C630&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/Detalle-de-una-custom-query-de-CodeQL-scaled.png?resize=1536%2C914&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/Detalle-de-una-custom-query-de-CodeQL-scaled.png?resize=2048%2C1218&amp;ssl=1 2048w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/Detalle-de-una-custom-query-de-CodeQL-scaled.png?resize=550%2C327&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/Detalle-de-una-custom-query-de-CodeQL-scaled.png?resize=841%2C500&amp;ssl=1 841w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/Detalle-de-una-custom-query-de-CodeQL-scaled.png?resize=1920%2C1142&amp;ssl=1 1920w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/Detalle-de-una-custom-query-de-CodeQL-scaled.png?resize=1816%2C1080&amp;ssl=1 1816w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/03/Detalle-de-una-custom-query-de-CodeQL-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>



<h2 class="wp-block-heading">🧩 Estrategias que puedes aplicar</h2>



<p>Aquí tienes varios patrones reales que puedes usar:</p>



<p><strong>✅ 1. OWASP-only scanning</strong></p>



<p>👉 Ideal para:</p>



<ul class="wp-block-list">
<li>compliance</li>



<li>auditorías</li>



<li>equipos empezando con seguridad</li>
</ul>



<p><strong>⚡ 2. Only critical queries</strong></p>



<p>👉 Reduce ruido<br>👉 Mejora adopción</p>



<p><strong>🏢 3. Security policies corporativas</strong></p>



<p>👉 Custom queries + suites<br>👉 Enforcing en PRs</p>



<p><strong>🧪 4. Demo / training environments</strong></p>



<p>👉 Como este repo 😄<br>👉 Muy útil para workshops o sesiones técnicas</p>



<p>Además, encaja perfectamente con el enfoque <em>shift-left</em> de GitHub Advanced Security, donde detectas vulnerabilidades antes de producción.</p>



<p>¡Nos vemos 👋🏻!</p>
<p>La entrada <a href="https://www.returngis.net/2026/03/%f0%9f%94%90-como-configurar-codeql-para-ejecutar-solo-las-queries-que-necesitas-owasp-top-10-custom-queries/">🔐 Cómo configurar CodeQL para ejecutar solo las queries que necesitas (OWASP Top 10 + custom queries)</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.returngis.net/2026/03/%f0%9f%94%90-como-configurar-codeql-para-ejecutar-solo-las-queries-que-necesitas-owasp-top-10-custom-queries/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">31769</post-id>	</item>
		<item>
		<title>Mi configuración de Dev Container para desarrollar plugins de WordPress</title>
		<link>https://www.returngis.net/2026/03/mi-configuracion-de-dev-container-para-desarrollar-plugins-de-wordpress/</link>
					<comments>https://www.returngis.net/2026/03/mi-configuracion-de-dev-container-para-desarrollar-plugins-de-wordpress/#respond</comments>
		
		<dc:creator><![CDATA[Gisela Torres]]></dc:creator>
		<pubDate>Mon, 09 Mar 2026 15:16:03 +0000</pubDate>
				<category><![CDATA[Contenedores]]></category>
		<category><![CDATA[Dev Containers]]></category>
		<category><![CDATA[Wordpress]]></category>
		<guid isPermaLink="false">https://www.returngis.net/?p=31685</guid>

					<description><![CDATA[<p>Los que me seguís por aquí sabéis que este blog es mi bloc de notas. Aquí voy dejando todo lo que pruebo e implemento para poder volver a ello cuando lo necesite. Y durante mucho tiempo la única que lo consultaba era yo 😅. Pero últimamente eso ha cambiado: ahora también lo consultan mis agentes de IA. Cuando tengo que volver a implementar algo que... </p>
<p class="more"><a class="more-link" href="https://www.returngis.net/2026/03/mi-configuracion-de-dev-container-para-desarrollar-plugins-de-wordpress/">Leer</a></p>
<p>La entrada <a href="https://www.returngis.net/2026/03/mi-configuracion-de-dev-container-para-desarrollar-plugins-de-wordpress/">Mi configuración de Dev Container para desarrollar plugins de WordPress</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Los que me seguís por aquí sabéis que este blog es mi bloc de notas. Aquí voy dejando todo lo que pruebo e implemento para poder volver a ello cuando lo necesite. Y durante mucho tiempo la única que lo consultaba era yo 😅. Pero últimamente eso ha cambiado: ahora <strong>también lo consultan mis agentes de IA</strong>. Cuando tengo que volver a implementar algo que ya hice, les paso el enlace al artículo y se ponen al día ellos solos.</p>



<p>El problema es que <strong>los agentes trabajan mucho mejor con Markdown que con HTML</strong> lleno de clases CSS, menús, sidebars y estructuras de tema. Así que me puse a crear un plugin de WordPress que convierte los posts y las páginas en <strong>Markdown limpio</strong> para que los agentes puedan consumirlos directamente. El plugin se llama <strong><a href="https://github.com/0GiS0/markdown-view-for-agents">Markdown View for AI Agents</a></strong> y a día de hoy todavía está en revisión por WordPress.org, así que ya os contaré más cuando esté publicado. Aunque si eres curios@ ya lo puedes ver funcionando en este mismo blog 👆🏻😇: en cada artículo verás un botón que pone <strong>View as Markdown</strong> y voilà, el contenido pasa a ser algo mucho más manejable para nuestros amigos los agentes.</p>



<p>Pero este artículo, más allá de mi plugin, de lo que va es <strong>del entorno que he utilizado para desarrollarlo, probarlo y verificar que cumple con todos los requisitos</strong> antes de hacer la submission. Y como no podía ser de otra manera, la respuesta fue: <strong>Dev Containers</strong>.</p>



<p>Si no sabes de qué van los Dev Containers te dejo este vídeo que grabé hace tiempo para mi canal de YouTube:</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe class="youtube-player" width="710" height="400" src="https://www.youtube.com/embed/DkKs29etRis?version=3&#038;rel=1&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;fs=1&#038;hl=es-ES&#038;autohide=2&#038;wmode=transparent" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe>
</div></figure>



<h2 class="wp-block-heading">🎯 El problema</h2>



<p>Desarrollar un plugin de WordPress no es como hacer un <code>npm init</code> y a correr. Necesitas:</p>



<ul class="wp-block-list">
<li>Una instancia de <strong>WordPress</strong> funcionando</li>



<li><strong>PHP</strong> con las extensiones correctas</li>



<li><strong>MySQL</strong>/<strong>MariaDb</strong> como base de datos</li>



<li><strong>WP-CLI</strong> para gestionar WordPress desde la terminal</li>



<li><strong>Composer</strong> para las dependencias PHP</li>



<li><strong>PHP_CodeSniffer</strong> con los estándares de WordPress</li>
</ul>



<p>Configurar todo esto en tu máquina local es un dolor. Y si trabajas en más de un proyecto, los conflictos entre versiones son inevitables. Así que lo metí todo en un Dev Container 🎉</p>



<h2 class="wp-block-heading">📦 La estructura</h2>



<p>La configuración vive en la carpeta <code>.devcontainer/</code> del repositorio:</p>



<pre class="wp-block-code language-text has-small-font-size"><code>.devcontainer/
├── devcontainer.json
├── Dockerfile
├── docker-compose.yml
└── setup.sh
</code></pre>



<p>Vamos parte por parte.</p>



<h2 class="wp-block-heading">🐳 Docker Compose</h2>



<p>En el docker compose que utilizará mi configuración de Dev Container para levantar los servicios incluí tres contenedores:</p>



<pre class="wp-block-code language-yaml has-small-font-size"><code>services:
  workspace:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ..:/workspaces/wp-markdown-for-agents:cached
      - composer-cache:/home/vscode/.composer/cache
      - wordpress-data:/srv/wordpress
      - plugins:/home/vscode/.local/share/plugins
      - plugins:/srv/wordpress/wp-content/plugins
      - ..:/srv/wordpress/wp-content/plugins/markdown-view-for-ai-agents:cached
    environment:
      COMPOSER_MEMORY_LIMIT: -1
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
    command: sleep infinity
    depends_on:
      - db
      - wordpress

  wordpress:
    image: wordpress:php8.3-apache
    restart: unless-stopped
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
    volumes:
      - wordpress-data:/var/www/html
      - plugins:/var/www/html/wp-content/plugins
      - ..:/var/www/html/wp-content/plugins/markdown-view-for-ai-agents:cached
    depends_on:
      - db

  db:
    image: mariadb:11.4
    restart: unless-stopped
    environment:
      MARIADB_DATABASE: wordpress
      MARIADB_USER: wordpress
      MARIADB_PASSWORD: wordpress
      MARIADB_ROOT_PASSWORD: root
    volumes:
      - mariadb-data:/var/lib/mysql

volumes:
  composer-cache:
  plugins:
  wordpress-data:
  mariadb-data:</code></pre>



<p>El primero de ellos, <strong>workspace</strong>, es el contenedor principal, en el que estaré desarrollando mi plugin. Los otros dos son los contenedores que me facilitarán una instancia de WordPress, con su correspondiente base de datos, para poder probarlo de forma súper sencilla.</p>



<p>La clave aquí está en el <strong>bind mount del plugin</strong>. Al montar el directorio del proyecto directamente en <code>wp-content/plugins/</code>, cualquier cambio que hago en el código se refleja inmediatamente en la instancia de WordPress. No hay que copiar archivos, ni reconstruir nada. Edito, guardo, refresco el navegador y listo.</p>



<h2 class="wp-block-heading">🏗️ El Dockerfile</h2>



<p>Para que el contenedor llamado workspace funcione correctamente está esperando poder generar una imagen de Docker, para la que va a usar el Dockerfile que se encuentra en el mismo directorio, .devcontainer:</p>



<pre class="wp-block-code language-dockerfile has-small-font-size"><code>FROM mcr.microsoft.com/devcontainers/php:3-8.3-trixie

RUN apt-get update \
	&amp;&amp; export DEBIAN_FRONTEND=noninteractive \
	&amp;&amp; apt-get install -y --no-install-recommends default-mysql-client libmariadb-dev librsvg2-bin \
	&amp;&amp; docker-php-ext-install mysqli pdo_mysql \
	&amp;&amp; curl -fsSL -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
	&amp;&amp; chmod +x /usr/local/bin/wp \
	&amp;&amp; mkdir -p /home/vscode/.local/share/plugins \
	&amp;&amp; chown -R vscode:vscode /home/vscode/.local \
	&amp;&amp; apt-get clean \
	&amp;&amp; rm -rf /var/lib/apt/lists/*</code></pre>



<p>Con esto tengo:</p>



<ul class="wp-block-list">
<li><strong>PHP 8.3 </strong>en el contenedor donde realmente estoy desarrollando mi plugin</li>



<li><strong>Cliente de MySQL</strong></li>



<li>La librería <strong>librsvg2-bin</strong>, la cual me va a permitir de forma sencilla poder convertir imágenes svg a png.</li>



<li><strong>WP-CLI</strong> para gestionar WordPress desde la terminal (instalar plugins, crear posts de prueba, activar/desactivar el plugin…). Porque no me vale tener el WordPress pelado sino que es mucho más rápido tener ya artículos con los que poder probar el botón/URL en mi caso.</li>
</ul>



<h2 class="wp-block-heading">⚙️ El devcontainer.json</h2>



<p>Aquí es donde se conecta todo. El <code>devcontainer.json</code> le dice a VS Code qué servicio de Docker Compose usar, qué extensiones instalar, qué puertos reenviar y qué ejecutar después de crear el contenedor:</p>



<pre class="wp-block-code language-javascript has-small-font-size"><code>// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/php
{
	"name": "🧪 markdown-view-for-ai-agents",
	"dockerComposeFile": "compose.yml",
	"service": "workspace",
	"runServices": &#91;
		"workspace",
		"wordpress",
		"db"
	],
	"workspaceFolder": "/workspaces/wp-markdown-for-agents",
	"shutdownAction": "stopCompose",

	// Use 'forwardPorts' to make a list of ports inside the container available locally.
	"forwardPorts": &#91;
		8080
	],
	"portsAttributes": {
		"8080": {
			"label": "📰 WordPress",
			"onAutoForward": "notify"
		}
	},
	"postCreateCommand": "./install-git-hooks.sh",
	"postStartCommand": "bash .devcontainer/post-start.sh",
	"customizations": {
		"vscode": {
			"settings": {
				"php.validate.executablePath": "/usr/local/bin/php",
				"files.associations": {
					"*.inc": "php"
				},
				"&#91;php]": {
					"editor.formatOnSave": false
				},
				"intelephense.environment.phpVersion": "8.3.0",
				"intelephense.files.exclude": &#91;
					"vendor/**"
				],
				"search.exclude": {
					"vendor": true
				}
			},
			"extensions": &#91;
				"GitHub.vscode-github-actions",
				"bmewburn.vscode-intelephense-client",
				"DEVSENSE.composer-php-vscode",
				"xdebug.php-debug"
			]
		}
	},
	"features": {
		"ghcr.io/devcontainers/features/github-cli:1": {}
	}

}
</code></pre>



<p>Las extensiones que incluyo son las que uso para desarrollar en PHP con WordPress, pero aquí cada uno puede añadir las suyas. Lo importante es que al abrir el proyecto <strong>todo el mundo tiene el mismo entorno con las mismas herramientas</strong>.</p>



<h2 class="wp-block-heading">🚀 El script de setup</h2>



<p>Y aquí viene la magia para que el WordPress de pruebas esté configurado tal y como lo necesitas: El <code>postCreateCommand</code> ejecuta un script que se encarga de dejarlo todo listo:</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>#!/usr/bin/env bash

set -eu

wp_path="/srv/wordpress"
site_url="http://localhost:8080"
sample_seed_option="md_for_agents_sample_content_seeded"
plugin_check_dir="/home/vscode/.local/share/plugins/plugin-check"
plugin_check_main_file="$plugin_check_dir/plugin-check.php"
plugin_check_cli_file="$plugin_check_dir/cli.php"

export XDEBUG_MODE=off

wp_cli() {
	wp --path="$wp_path" "$@"
}

ensure_plugin_check_files() {
	mkdir -p "$plugin_check_dir"

	if &#91; -f "$plugin_check_main_file" ] &amp;&amp; &#91; -f "$plugin_check_cli_file" ]; then
		return 0
	fi

	tmp_dir="$(mktemp -d)"
	archive="$tmp_dir/plugin-check.zip"
	extracted_dir="$tmp_dir/plugin-check"

	cleanup_plugin_check_tmp() {
		rm -rf "$tmp_dir"
	}

	trap cleanup_plugin_check_tmp RETURN

	curl -fsSL -o "$archive" "https://downloads.wordpress.org/plugin/plugin-check.latest-stable.zip"
	unzip -q "$archive" -d "$tmp_dir"

	find "$plugin_check_dir" -mindepth 1 -maxdepth 1 ! -name '.gitkeep' -exec rm -rf {} +
	cp -R "$extracted_dir"/. "$plugin_check_dir"/

	trap - RETURN
	cleanup_plugin_check_tmp
}

upsert_post() {
	post_type="$1"
	post_slug="$2"
	post_title="$3"
	post_content="$4"

	post_id="$(wp_cli post list --post_type="$post_type" --name="$post_slug" --field=ID 2&gt;/dev/null | head -n 1 || true)"

	if &#91; -n "$post_id" ]; then
		wp_cli post update "$post_id" \
			--post_title="$post_title" \
			--post_name="$post_slug" \
			--post_content="$post_content" &gt;/dev/null
		echo "$post_id"
		return 0
	fi

	wp_cli post create \
		--post_type="$post_type" \
		--post_status=publish \
		--post_title="$post_title" \
		--post_name="$post_slug" \
		--post_content="$post_content" \
		--porcelain
}

set_post_terms() {
	post_slug="$1"
	taxonomy="$2"
	terms_csv="$3"

	post_id="$(wp_cli post list --post_type=post --name="$post_slug" --field=ID 2&gt;/dev/null | head -n 1 || true)"

	if &#91; -z "$post_id" ]; then
		return 0
	fi

	wp_cli post term set "$post_id" "$taxonomy" "$terms_csv" &gt;/dev/null
}

seed_sample_content() {
	getting_started_page_id="$(upsert_post \
		page \
		getting-started-markdown-agents \
		'Getting Started with Markdown for Agents' \
		"&lt;!-- wp:paragraph {\"fontSize\":\"large\"} --&gt;&lt;p class=\"has-large-font-size\"&gt;This local page exists to test the plugin output quickly from a realistic WordPress page.&lt;/p&gt;&lt;!-- /wp:paragraph --&gt;&lt;!-- wp:heading {\"level\":2} --&gt;&lt;h2&gt;Checklist&lt;/h2&gt;&lt;!-- /wp:heading --&gt;&lt;!-- wp:list --&gt;&lt;ul&gt;&lt;li&gt;WordPress installed through WP-CLI&lt;/li&gt;&lt;li&gt;Plugin activated automatically&lt;/li&gt;&lt;li&gt;Sample content seeded idempotently&lt;/li&gt;&lt;/ul&gt;&lt;!-- /wp:list --&gt;&lt;!-- wp:heading {\"level\":2} --&gt;&lt;h2&gt;Example prompt&lt;/h2&gt;&lt;!-- /wp:heading --&gt;&lt;!-- wp:paragraph --&gt;&lt;p&gt;Summarize this page for an autonomous coding agent.&lt;/p&gt;&lt;!-- /wp:paragraph --&gt;")"

	upsert_post \
		post \
		release-notes-draft \
		'Release Notes Draft' \
		"&lt;!-- wp:heading {\"level\":1} --&gt;&lt;h1&gt;Release Notes Draft&lt;/h1&gt;&lt;!-- /wp:heading --&gt;&lt;!-- wp:heading {\"level\":2} --&gt;&lt;h2&gt;Highlights&lt;/h2&gt;&lt;!-- /wp:heading --&gt;&lt;!-- wp:list --&gt;&lt;ul&gt;&lt;li&gt;Automated WordPress bootstrap&lt;/li&gt;&lt;li&gt;Local plugin activation&lt;/li&gt;&lt;li&gt;Repeatable demo content&lt;/li&gt;&lt;/ul&gt;&lt;!-- /wp:list --&gt;&lt;!-- wp:heading {\"level\":2} --&gt;&lt;h2&gt;Pending&lt;/h2&gt;&lt;!-- /wp:heading --&gt;&lt;!-- wp:list {\"ordered\":true} --&gt;&lt;ol&gt;&lt;li&gt;Verify markdown export output&lt;/li&gt;&lt;li&gt;Review button placement in the editor&lt;/li&gt;&lt;li&gt;Package the release zip&lt;/li&gt;&lt;/ol&gt;&lt;!-- /wp:list --&gt;" &gt;/dev/null

	upsert_post \
		post \
		agent-qa-scenario \
		'Agent QA Scenario' \
		"&lt;!-- wp:heading {\"level\":2} --&gt;&lt;h2&gt;Scenario&lt;/h2&gt;&lt;!-- /wp:heading --&gt;&lt;!-- wp:paragraph --&gt;&lt;p&gt;Use this post to test how the plugin exposes headings, lists, and links for agents.&lt;/p&gt;&lt;!-- /wp:paragraph --&gt;&lt;!-- wp:list --&gt;&lt;ul&gt;&lt;li&gt;Visit the generated markdown endpoint&lt;/li&gt;&lt;li&gt;Confirm headings remain stable&lt;/li&gt;&lt;li&gt;Confirm links remain absolute&lt;/li&gt;&lt;/ul&gt;&lt;!-- /wp:list --&gt;&lt;!-- wp:paragraph --&gt;&lt;p&gt;Related page ID: ${getting_started_page_id}&lt;/p&gt;&lt;!-- /wp:paragraph --&gt;" &gt;/dev/null

	upsert_post \
		post \
		headings-lists-and-frontmatter \
		'Headings, Lists, and Frontmatter' \
		"&lt;!-- wp:paragraph --&gt;&lt;p&gt;This post exists to check heading depth, ordered lists, unordered lists, and YAML frontmatter metadata.&lt;/p&gt;&lt;!-- /wp:paragraph --&gt;&lt;!-- wp:heading {\"level\":2} --&gt;&lt;h2&gt;Acceptance checklist&lt;/h2&gt;&lt;!-- /wp:heading --&gt;&lt;!-- wp:list --&gt;&lt;ul&gt;&lt;li&gt;Heading levels remain stable&lt;/li&gt;&lt;li&gt;Bullet items stay as bullet items&lt;/li&gt;&lt;li&gt;Categories and tags appear in the frontmatter&lt;/li&gt;&lt;/ul&gt;&lt;!-- /wp:list --&gt;&lt;!-- wp:heading {\"level\":3} --&gt;&lt;h3&gt;Release steps&lt;/h3&gt;&lt;!-- /wp:heading --&gt;&lt;!-- wp:list {\"ordered\":true} --&gt;&lt;ol&gt;&lt;li&gt;Run linting&lt;/li&gt;&lt;li&gt;Package the plugin&lt;/li&gt;&lt;li&gt;Smoke-test the markdown endpoint&lt;/li&gt;&lt;/ol&gt;&lt;!-- /wp:list --&gt;" &gt;/dev/null

	upsert_post \
		post \
		links-quotes-and-inline-code \
		'Links, Quotes, and Inline Code' \
		"&lt;!-- wp:paragraph --&gt;&lt;p&gt;Use this example to verify how inline links and inline code are represented in the markdown output.&lt;/p&gt;&lt;!-- /wp:paragraph --&gt;&lt;!-- wp:quote --&gt;&lt;blockquote class=\"wp-block-quote\"&gt;&lt;p&gt;Agents work best when the source content is explicit, stable, and easy to parse.&lt;/p&gt;&lt;cite&gt;Local demo note&lt;/cite&gt;&lt;/blockquote&gt;&lt;!-- /wp:quote --&gt;&lt;!-- wp:paragraph --&gt;&lt;p&gt;Open the &lt;a href=\"http://localhost:8080/getting-started-markdown-agents/\"&gt;getting started page&lt;/a&gt; and compare the endpoint output to &lt;code&gt;wp post get&lt;/code&gt;.&lt;/p&gt;&lt;!-- /wp:paragraph --&gt;" &gt;/dev/null

	upsert_post \
		post \
		code-blocks-and-tables \
		'Code Blocks and Tables' \
		"&lt;!-- wp:paragraph --&gt;&lt;p&gt;This sample covers fenced code blocks and HTML tables converted into markdown tables.&lt;/p&gt;&lt;!-- /wp:paragraph --&gt;&lt;!-- wp:code --&gt;&lt;pre class=\"wp-block-code\"&gt;&lt;code lang=\"bash\"&gt;wp plugin activate markdown-view-for-ai-agents --path=/srv/wordpress
wp option get siteurl --path=/srv/wordpress&lt;/code&gt;&lt;/pre&gt;&lt;!-- /wp:code --&gt;&lt;!-- wp:table --&gt;&lt;figure class=\"wp-block-table\"&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Check&lt;/th&gt;&lt;th&gt;Expected result&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Code fence&lt;/td&gt;&lt;td&gt;Preserved with bash language&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Table headers&lt;/td&gt;&lt;td&gt;Rendered as markdown table header row&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/figure&gt;&lt;!-- /wp:table --&gt;" &gt;/dev/null

	upsert_post \
		post \
		images-and-mixed-formatting \
		'Images and Mixed Formatting' \
		"&lt;!-- wp:paragraph --&gt;&lt;p&gt;This example mixes &lt;strong&gt;bold text&lt;/strong&gt;, &lt;em&gt;emphasis&lt;/em&gt;, and an image with alt text for markdown conversion checks.&lt;/p&gt;&lt;!-- /wp:paragraph --&gt;&lt;!-- wp:image {\"sizeSlug\":\"large\",\"linkDestination\":\"none\"} --&gt;&lt;figure class=\"wp-block-image size-large\"&gt;&lt;img src=\"https://wordpress.org/style/images/wp-header-logo.png\" alt=\"WordPress logo\" /&gt;&lt;/figure&gt;&lt;!-- /wp:image --&gt;&lt;!-- wp:paragraph --&gt;&lt;p&gt;Verify that the generated markdown keeps the image URL and preserves inline emphasis correctly.&lt;/p&gt;&lt;!-- /wp:paragraph --&gt;" &gt;/dev/null

	set_post_terms headings-lists-and-frontmatter category "Demo Content,Frontmatter"
	set_post_terms headings-lists-and-frontmatter post_tag "headings,lists,metadata"
	set_post_terms links-quotes-and-inline-code category "Demo Content,Links"
	set_post_terms links-quotes-and-inline-code post_tag "quotes,links,inline-code"
	set_post_terms code-blocks-and-tables category "Demo Content,Code"
	set_post_terms code-blocks-and-tables post_tag "code-blocks,tables"
	set_post_terms images-and-mixed-formatting category "Demo Content,Media"
	set_post_terms images-and-mixed-formatting post_tag "images,bold,italic"

	hello_world_id="$(wp_cli post list --post_type=post --name=hello-world --field=ID 2&gt;/dev/null | head -n 1 || true)"
	if &#91; -n "$hello_world_id" ]; then
		wp_cli post delete "$hello_world_id" --force &gt;/dev/null
	fi

	sample_page_id="$(wp_cli post list --post_type=page --name=sample-page --field=ID 2&gt;/dev/null | head -n 1 || true)"
	if &#91; -n "$sample_page_id" ]; then
		wp_cli post delete "$sample_page_id" --force &gt;/dev/null
	fi

	privacy_policy_id="$(wp_cli post list --post_type=page --name=privacy-policy --field=ID 2&gt;/dev/null | head -n 1 || true)"
	if &#91; -n "$privacy_policy_id" ]; then
		wp_cli option delete wp_page_for_privacy_policy &gt;/dev/null 2&gt;&amp;1 || true
		wp_cli post delete "$privacy_policy_id" --force &gt;/dev/null
	fi

	wp_cli option update show_on_front posts &gt;/dev/null
	wp_cli option delete page_on_front &gt;/dev/null 2&gt;&amp;1 || true
	wp_cli option delete page_for_posts &gt;/dev/null 2&gt;&amp;1 || true

	if wp_cli option get "$sample_seed_option" &gt;/dev/null 2&gt;&amp;1; then
		wp_cli option update "$sample_seed_option" 1 &gt;/dev/null
	else
		wp_cli option add "$sample_seed_option" 1 &gt;/dev/null
	fi
}

if &#91; ! -d "$wp_path" ]; then
	exit 0
fi

attempt=1
max_attempts=30

while &#91; "$attempt" -le "$max_attempts" ]; do
	if &#91; -f "$wp_path/wp-config.php" ]; then
		break
	fi
	sleep 2
	attempt=$((attempt + 1))
done

if &#91; ! -f "$wp_path/wp-config.php" ]; then
	if &#91; -n "${WORDPRESS_DB_HOST:-}" ] &amp;&amp; &#91; -n "${WORDPRESS_DB_NAME:-}" ] &amp;&amp; &#91; -n "${WORDPRESS_DB_USER:-}" ] &amp;&amp; &#91; -n "${WORDPRESS_DB_PASSWORD:-}" ]; then
		wp config create \
			--path="$wp_path" \
			--dbname="$WORDPRESS_DB_NAME" \
			--dbuser="$WORDPRESS_DB_USER" \
			--dbpass="$WORDPRESS_DB_PASSWORD" \
			--dbhost="$WORDPRESS_DB_HOST" \
			--skip-check \
			--force &gt;/dev/null 2&gt;&amp;1 || true
	fi

	if &#91; ! -f "$wp_path/wp-config.php" ]; then
		echo "WordPress config is not ready yet; skipping local bootstrap."
		exit 0
	fi
fi

installed=0
attempt=1

while &#91; "$attempt" -le "$max_attempts" ]; do
	if wp_cli core is-installed &gt;/dev/null 2&gt;&amp;1; then
		installed=1
		break
	fi

	if wp_cli core install \
		--url="$site_url" \
		--title="Markdown View for AI Agents" \
		--admin_user="admin" \
		--admin_password="admin" \
		--admin_email="admin@example.com" \
		--skip-email &gt;/dev/null 2&gt;&amp;1; then
		installed=1
		break
	fi

	sleep 2
	attempt=$((attempt + 1))
done

if &#91; "$installed" -eq 0 ]; then
	echo "WordPress database bootstrap is not ready yet; skipping local site installation."
	exit 0
fi

ensure_plugin_check_files

wp_cli option update blog_public 0 &gt;/dev/null 2&gt;&amp;1 || true
wp_cli rewrite structure '/%postname%/' &gt;/dev/null 2&gt;&amp;1 || true
wp_cli rewrite flush --hard &gt;/dev/null 2&gt;&amp;1 || true
wp_cli theme activate twentytwentythree &gt;/dev/null 2&gt;&amp;1 || true
wp_cli plugin activate markdown-view-for-ai-agents &gt;/dev/null 2&gt;&amp;1 || true
wp_cli plugin activate plugin-check &gt;/dev/null 2&gt;&amp;1 || true
seed_sample_content</code></pre>



<p>Si, lo sé, es un poco largo y un poco complicado leerlo directamente desde el artículo, pero es básicamente el que me va a permitir que cuando abro el proyecto, en unos segundos tengo un WordPress funcionando con el plugin activado y contenido de prueba para poder probarlo. No tengo que hacer nada manual. </p>



<h2 class="wp-block-heading">🔍 PHP_CodeSniffer y WordPress Coding Standards</h2>



<p>Si quieres subir un plugin al directorio oficial de WordPress.org, el proceso de revisión es bastante estricto. El código tiene que seguir los <strong><a href="https://developer.wordpress.org/coding-standards/wordpress-coding-standards/">WordPress Coding Standards</a></strong>. Y para eso, PHP_CodeSniffer es imprescindible.</p>



<p>El proyecto usa <strong>Composer</strong> para gestionar estas dependencias:</p>



<pre class="wp-block-code language-javascript has-small-font-size"><code>{
  "name": "0gis0/markdown-view-for-agents",
  "description": "Development tooling for the Markdown View for AI Agents WordPress plugin.",
  "type": "project",
  "require": {},
  "require-dev": {
    "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
    "phpcompatibility/phpcompatibility-wp": "^2.1",
    "squizlabs/php_codesniffer": "^3.10",
    "wp-coding-standards/wpcs": "^3.1"
  },
  "config": {
    "allow-plugins": {
      "dealerdirect/phpcodesniffer-composer-installer": true
    },
    "sort-packages": true
  },
  "scripts": {
    "lint": "phpcs",
    "lint:php": "phpcs --standard=phpcs.xml.dist"
  }
}
</code></pre>



<p>Y el archivo <code>phpcs.xml.dist</code> define las reglas que se aplican al proyecto:</p>



<pre class="wp-block-code language-xml has-small-font-size"><code>&lt;?xml version="1.0"?&gt;
&lt;ruleset name="Markdown View for AI Agents"&gt;
  &lt;description&gt;PHP quality and security checks for the plugin.&lt;/description&gt;

  &lt;file&gt;markdown-view-for-ai-agents.php&lt;/file&gt;
  &lt;file&gt;includes&lt;/file&gt;

  &lt;exclude-pattern&gt;vendor/*&lt;/exclude-pattern&gt;
  &lt;exclude-pattern&gt;dist/*&lt;/exclude-pattern&gt;
  &lt;exclude-pattern&gt;.wordpress-org/*&lt;/exclude-pattern&gt;
  &lt;exclude-pattern&gt;.github/*&lt;/exclude-pattern&gt;

  &lt;arg name="basepath" value="."/&gt;
  &lt;arg name="extensions" value="php"/&gt;
  &lt;arg value="sp"/&gt;

  &lt;config name="testVersion" value="7.4-"/&gt;

  &lt;rule ref="WordPress-Core"/&gt;
  &lt;rule ref="WordPress-Docs"/&gt;
  &lt;rule ref="WordPress-Extra"/&gt;
  &lt;rule ref="WordPress.Security"/&gt;
  &lt;rule ref="PHPCompatibilityWP"/&gt;

  &lt;rule ref="WordPress.NamingConventions.PrefixAllGlobals"&gt;
    &lt;properties&gt;
      &lt;property name="prefixes" type="array"&gt;
        &lt;element value="md_for_agents"/&gt;
      &lt;/property&gt;
    &lt;/properties&gt;
  &lt;/rule&gt;

  &lt;rule ref="WordPress.WP.EnqueuedResourceParameters.NotInFooter"&gt;
    &lt;exclude name="WordPress.WP.EnqueuedResourceParameters.MissingVersion"/&gt;
  &lt;/rule&gt;
&lt;/ruleset&gt;</code></pre>



<p>Al principio es frustrante porque te marca cosas que parecen menores (como el formato de los comentarios o los espacios dentro de los paréntesis), pero te fuerza a escribir código limpio y consistente. Y sobre todo, <strong>te ahorra que te rechacen el plugin</strong> en la revisión 😅.</p>



<p>Todo esto se instala con <code>composer install</code> que forma parte del <code>postCreateCommand</code> del Dev Container. Así que al abrir el proyecto ya tienes el linter configurado y listo para usar.</p>



<h2 class="wp-block-heading">🎣 Git Hooks</h2>



<p>Igual que en <a href="https://www.returngis.net/2026/03/pre-commit-hooks-en-java-sin-python-spotless-checkstyle-gradle/">el artículo sobre pre-commit hooks en Java</a>, aquí también he configurado un <strong>pre-commit hook</strong> que ejecuta PHP_CodeSniffer automáticamente antes de cada commit. Si el código no pasa el linter, el commit se bloquea:</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>#!/usr/bin/env bash

set -euo pipefail

if ! command -v php >/dev/null 2>&amp;1; then
  echo "PHP is not installed. Skipping PHP pre-commit checks."
  exit 0
fi

staged_php_files="$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.php$' || true)"

if &#91;&#91; -z "$staged_php_files" ]]; then
  echo "No staged PHP files."
  exit 0
fi

echo "Running PHP syntax checks on staged files..."
while IFS= read -r file; do
  &#91;&#91; -z "$file" ]] &amp;&amp; continue
  php -l "$file" >/dev/null
done &lt;&lt;&lt; "$staged_php_files"

if &#91;&#91; -x "vendor/bin/phpcs" ]]; then
  echo "Running PHPCS on staged files..."
  vendor/bin/phpcs --standard=phpcs.xml.dist $staged_php_files
else
  echo "PHPCS is not installed. Run 'composer install' to enable quality and security checks."
fi</code></pre>



<p>El hook vive en <code>.githooks/</code> y se instala con el script <code>install-git-hooks.sh</code>, que también se ejecuta como parte del setup del Dev Container. Así, al clonar y abrir el proyecto, los hooks ya están activos. <strong>Los desarrolladores no tienen que hacer nada manual</strong>.</p>



<h2 class="wp-block-heading">🔄 GitHub Actions</h2>



<p><strong>Pero no se trata solo de validar en local.</strong> Para mí es igual de importante que estas mismas reglas se ejecuten en el CI, de forma que todo el código que llegue al repositorio siga exactamente el mismo estilo y formato. Si el lint pasa en tu máquina pero no se valida en el pipeline, al final cada uno acaba formateando como quiere y las reglas se convierten en papel mojado.</p>



<p>Así que <a href="https://github.com/0GiS0/markdown-view-for-agents/blob/main/.github/workflows/ci.yml">el repositorio también tiene un workflow de <strong>CI</strong> en GitHub Actions </a>que ejecuta PHP_CodeSniffer en cada push y pull request. Las mismas reglas, el mismo resultado.</p>



<h2 class="wp-block-heading">💡 Lo que aprendí</h2>



<p>Algunas cosas que me llevo:</p>



<p><strong>WP-CLI es imprescindible en el Dev Container.</strong> Poder instalar WordPress, activar plugins, crear contenido de prueba y configurar permalinks desde un script automatizado hace que el setup sea completamente <em>hands-off</em>.</p>



<p><strong>El bind mount del plugin es la clave.</strong> Montar el directorio del proyecto directamente en <code>wp-content/plugins/</code> elimina cualquier fricción entre el código que editas y lo que WordPress ejecuta.</p>



<p><strong>El <code>readme.txt</code> de WordPress.org no es un README normal.</strong> Tiene un formato específico con secciones predefinidas que el directorio parsea para mostrar la información del plugin. Es diferente del <code>README.md</code> de GitHub, así que necesitas mantener los dos.</p>



<p><strong>PHP_CodeSniffer con WordPress Coding Standards es estricto pero necesario.</strong> Te fuerza a escribir código limpio y consistente desde el primer momento. Y tenerlo integrado en el Dev Container, en los git hooks y en el CI hace que no haya escapatoria 😄.</p>



<h2 class="wp-block-heading">🚀 Cómo probarlo</h2>



<p>Si quieres usar esta misma configuración para tu propio plugin de WordPress, puedes echar un vistazo a cómo lo tengo montado en el repositorio:</p>



<p>👉 <a href="https://github.com/0GiS0/markdown-view-for-agents">github.com/0GiS0/markdown-view-for-agents</a></p>



<p>Clona el repo, ábrelo en VS Code y te preguntará si quieres reabrir el proyecto en el contenedor. Acepta y en unos minutos tendrás un WordPress funcionando con el plugin listo para probar. También puedes abrirlo directamente en <strong>GitHub Codespaces</strong> desde el repositorio, sin necesidad de tener Docker instalado en tu máquina. Si quieres saber más sobre Codespaces aquí tienes otro vídeo de mi canal de YouTube 😊</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe class="youtube-player" width="710" height="400" src="https://www.youtube.com/embed/cO-oFpePy3c?version=3&#038;rel=1&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;fs=1&#038;hl=es-ES&#038;autohide=2&#038;wmode=transparent" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe>
</div></figure>



<p>Y ahora crucemos los dedos a ver si me aceptan el plugin 🤞🏻</p>



<p>¡Nos vemos 👋🏻!</p>
<p>La entrada <a href="https://www.returngis.net/2026/03/mi-configuracion-de-dev-container-para-desarrollar-plugins-de-wordpress/">Mi configuración de Dev Container para desarrollar plugins de WordPress</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.returngis.net/2026/03/mi-configuracion-de-dev-container-para-desarrollar-plugins-de-wordpress/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">31685</post-id>	</item>
		<item>
		<title>Pre-commit hooks en Java sin Python: Spotless + Checkstyle + Gradle</title>
		<link>https://www.returngis.net/2026/03/pre-commit-hooks-en-java-sin-python-spotless-checkstyle-gradle/</link>
					<comments>https://www.returngis.net/2026/03/pre-commit-hooks-en-java-sin-python-spotless-checkstyle-gradle/#respond</comments>
		
		<dc:creator><![CDATA[Gisela Torres]]></dc:creator>
		<pubDate>Sun, 08 Mar 2026 10:52:08 +0000</pubDate>
				<category><![CDATA[Backend]]></category>
		<category><![CDATA[Dev Containers]]></category>
		<category><![CDATA[Java]]></category>
		<guid isPermaLink="false">https://www.returngis.net/?p=31646</guid>

					<description><![CDATA[<p>Cuando me pongo a jugar con un lenguaje… ya has visto que juego de verdad 😄.Y una de las cosas que más me gusta hacer es traer ideas de otros ecosistemas e intentar adaptarlas para ver cómo encajan en el que estoy usando en ese momento. Después de haber estado afinando mis dev containers para Spring Boot y Quarkus, esta vez me dio por investigar... </p>
<p class="more"><a class="more-link" href="https://www.returngis.net/2026/03/pre-commit-hooks-en-java-sin-python-spotless-checkstyle-gradle/">Leer</a></p>
<p>La entrada <a href="https://www.returngis.net/2026/03/pre-commit-hooks-en-java-sin-python-spotless-checkstyle-gradle/">Pre-commit hooks en Java sin Python: Spotless + Checkstyle + Gradle</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Cuando me pongo a jugar con un lenguaje… ya has visto que juego <strong>de verdad</strong> 😄.<br>Y una de las cosas que más me gusta hacer es <strong>traer ideas de otros ecosistemas</strong> e intentar adaptarlas para ver cómo encajan en el que estoy usando en ese momento.</p>



<p>Después de haber estado afinando mis <strong>dev containers <a href="https://www.returngis.net/2026/02/mi-configuracion-de-dev-container-para-spring-boot/" type="post" id="31588">para Spring Boot</a> y <a href="https://www.returngis.net/2026/02/desarrollando-con-quarkus-en-vs-code-usando-dev-containers/" type="post" id="31606">Quarkus</a></strong>, esta vez me dio por investigar otra pieza importante del workflow de desarrollo: los <strong><a href="https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks">pre-commit hooks</a></strong>.</p>



<p>En muchos proyectos es bastante habitual usar como herramienta <strong><a href="https://pre-commit.com/" target="_blank" rel="noreferrer noopener">pre-commit</a></strong>, el framework escrito en Python que permite ejecutar checks automáticos antes de hacer un commit. Funciona muy bien y es extremadamente popular… pero tiene un pequeño inconveniente en el contexto Java: <strong>introduce una dependencia externa que realmente no necesitamos</strong>. Y cuando estamos trabajando con Dev Containers, y por extensión con contenedores, cuanto más pequeña sea la imagen mejor que mejor. Si no sabes de qué va esto de los Dev Containers te dejo este vídeo que grabé hace tiempo para mi canal de YouTube:</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe class="youtube-player" width="710" height="400" src="https://www.youtube.com/embed/DkKs29etRis?version=3&#038;rel=1&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;fs=1&#038;hl=es-ES&#038;autohide=2&#038;wmode=transparent" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe>
</div></figure>



<p>Por lo que en este caso mi objetivo era claro 🎯: conseguir <strong>pre-commit hooks en proyectos Java sin obligar a nadie a instalar Python</strong>, o tener que incluirlo en la configuración del Dev Container, solo para ejecutar unas validaciones básicas de formato o estilo de código. </p>



<p><strong>Pero no se trata solo de validar en local</strong>. Para mí es igual de importante que estas mismas reglas se ejecuten en el CI, de forma que todo el código que llegue al repositorio siga exactamente el mismo estilo y formato. Si el lint pasa en tu máquina pero no se valida en el pipeline, al final cada uno acaba formateando como quiere y las reglas se convierten en papel mojado.</p>



<p>Así que en este artículo te cuento cómo he configurado <strong>pre-commit hooks usando Spotless, Checkstyle y Gradle</strong>, de forma que podamos validar automáticamente el código antes de cada commit <strong>manteniéndonos completamente dentro del ecosistema Java</strong>.</p>



<h2 class="wp-block-heading">📝 Modificar el archivo build.gradle</h2>



<p>Lo primero que he necesitado hacer a sido <strong>modificar el archivo build.gradle</strong> que ya conoces, para poder instalar un par de plugins: <a href="https://checkstyle.sourceforge.io/">checkstyle</a> y <a href="https://github.com/diffplug/spotless" target="_blank" rel="noreferrer noopener">com.diffplug.spotless</a>:</p>



<pre class="wp-block-code language-text has-small-font-size"><code>plugins {
    id 'java'
    id 'checkstyle'                              // Plugin nativo de Gradle
    id 'com.diffplug.spotless' version '7.0.2'  // Formateo automático
}</code></pre>



<p>Después en este mismo archivo, necesitas añadir el siguiente bloque:</p>



<pre class="wp-block-code language-gradle has-small-font-size"><code>spotless {
    java {
        target 'src/*/java/**/*.java'
        googleJavaFormat('1.25.2')      // Formateador de Google
        removeUnusedImports()
        importOrder('java', 'javax', 'jakarta', '', 'com.tuempresa')
        trimTrailingWhitespace()
        endWithNewline()
    }

    format 'misc', {
        target '*.md', '*.yml', '*.yaml', '*.properties', '*.gradle', '.gitignore'
        trimTrailingWhitespace()
        endWithNewline()
    }
}</code></pre>



<p>Con este conseguimos:</p>



<ul class="wp-block-list">
<li>Un formato consistente usando Google Java Format</li>



<li>Eliminación automática de imports no usados</li>



<li>Un orden homogéneo de imports</li>



<li>Archivos sin espacios sobrantes</li>



<li>Aplicar las mismas reglas a ficheros no Java</li>
</ul>



<p>Y ahora el bloque de Checkstyle:</p>



<pre class="wp-block-code language-gradle has-small-font-size"><code>checkstyle {
    toolVersion = '10.21.4'
    configFile = file("${rootDir}/config/checkstyle/checkstyle.xml")
    ignoreFailures = false
    maxWarnings = 0
}</code></pre>



<p>Esto fuerza a que:</p>



<ul class="wp-block-list">
<li>Las reglas vivan dentro del repositorio</li>



<li>Se use una versión concreta de Checkstyle</li>



<li>El build falle ante cualquier warning</li>
</ul>



<h2 class="wp-block-heading">💄 Configuración de Checkstyle (<code>config/checkstyle</code>)</h2>



<p>Para Checkstyle he creado un directorio específico dentro del proyecto:</p>



<pre class="wp-block-code language-text has-small-font-size"><code>config/
└── checkstyle/
    ├── checkstyle.xml
    └── suppressions.xml</code></pre>



<p>Aquí es donde definimos las reglas principales:</p>



<pre class="wp-block-code language-xml has-small-font-size"><code>&lt;?xml version="1.0"?&gt;
&lt;!DOCTYPE module PUBLIC
    "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
    "https://checkstyle.org/dtds/configuration_1_3.dtd"&gt;

&lt;!--
    Checkstyle configuration based on Google Java Style Guide (simplified).
    Full guide: https://google.github.io/styleguide/javaguide.html

    Note: Formatting is handled by Spotless (google-java-format).
    This config focuses on naming conventions, best practices, and code quality.
--&gt;
&lt;module name="Checker"&gt;
    &lt;property name="charset" value="UTF-8"/&gt;
    &lt;property name="severity" value="error"/&gt;
    &lt;property name="fileExtensions" value="java"/&gt;

    &lt;!-- Suppressions for test files (allow snake_case method names) --&gt;
    &lt;module name="SuppressionFilter"&gt;
        &lt;property name="file" value="${config_loc}/suppressions.xml"/&gt;
    &lt;/module&gt;

    &lt;!-- Suppress checks on generated sources --&gt;
    &lt;module name="BeforeExecutionExclusionFileFilter"&gt;
        &lt;property name="fileNamePattern" value=".*&#91;\\/]build&#91;\\/].*"/&gt;
    &lt;/module&gt;

    &lt;!-- File-level checks --&gt;
    &lt;module name="FileTabCharacter"&gt;
        &lt;property name="eachLine" value="true"/&gt;
    &lt;/module&gt;

    &lt;module name="LineLength"&gt;
        &lt;property name="max" value="120"/&gt;
        &lt;property name="ignorePattern" value="^package.*|^import.*|a]* href|href|http://|https://|ftp://"/&gt;
    &lt;/module&gt;

    &lt;module name="TreeWalker"&gt;
        &lt;!-- ========== Naming Conventions ========== --&gt;
        &lt;module name="PackageName"&gt;
            &lt;property name="format" value="^&#91;a-z]+(\.&#91;a-z]&#91;a-z0-9]*)*$"/&gt;
        &lt;/module&gt;

        &lt;module name="TypeName"&gt;
            &lt;property name="format" value="^&#91;A-Z]&#91;a-zA-Z0-9]*$"/&gt;
        &lt;/module&gt;

        &lt;module name="MethodName"&gt;
            &lt;property name="format" value="^&#91;a-z]&#91;a-zA-Z0-9]*$"/&gt;
        &lt;/module&gt;

        &lt;module name="MemberName"&gt;
            &lt;property name="format" value="^&#91;a-z]&#91;a-zA-Z0-9]*$"/&gt;
        &lt;/module&gt;

        &lt;module name="ParameterName"&gt;
            &lt;property name="format" value="^&#91;a-z]&#91;a-zA-Z0-9]*$"/&gt;
        &lt;/module&gt;

        &lt;module name="LocalVariableName"&gt;
            &lt;property name="format" value="^&#91;a-z]&#91;a-zA-Z0-9]*$"/&gt;
        &lt;/module&gt;

        &lt;module name="ConstantName"&gt;
            &lt;property name="format" value="^&#91;A-Z]&#91;A-Z0-9]*(_&#91;A-Z0-9]+)*$"/&gt;
        &lt;/module&gt;

        &lt;!-- ========== Imports ========== --&gt;
        &lt;module name="AvoidStarImport"/&gt;
        &lt;module name="RedundantImport"/&gt;
        &lt;module name="UnusedImports"/&gt;

        &lt;!-- ========== Code Quality ========== --&gt;
        &lt;module name="EqualsHashCode"/&gt;
        &lt;module name="SimplifyBooleanExpression"/&gt;
        &lt;module name="SimplifyBooleanReturn"/&gt;
        &lt;module name="StringLiteralEquality"/&gt;
        &lt;module name="NestedTryDepth"&gt;
            &lt;property name="max" value="2"/&gt;
        &lt;/module&gt;

        &lt;!-- ========== Best Practices ========== --&gt;
        &lt;module name="OneStatementPerLine"/&gt;
        &lt;module name="MultipleVariableDeclarations"/&gt;
        &lt;module name="MissingSwitchDefault"/&gt;
        &lt;module name="DefaultComesLast"/&gt;
        &lt;module name="FallThrough"/&gt;

        &lt;!-- ========== Modifiers ========== --&gt;
        &lt;module name="ModifierOrder"/&gt;
        &lt;module name="RedundantModifier"/&gt;

        &lt;!-- ========== Annotations ========== --&gt;
        &lt;module name="MissingOverride"/&gt;

        &lt;!-- ========== Whitespace (complementary to Spotless) ========== --&gt;
        &lt;module name="NoWhitespaceAfter"&gt;
            &lt;property name="tokens" value="AT,INC,DEC,UNARY_MINUS,UNARY_PLUS,BNOT,LNOT,DOT,ARRAY_DECLARATOR,INDEX_OP"/&gt;
        &lt;/module&gt;
        &lt;module name="NoWhitespaceBefore"/&gt;
    &lt;/module&gt;
&lt;/module&gt;</code></pre>



<p>Está basado en la <strong><a href="https://google.github.io/styleguide/javaguide.html">Google Java Style Guide</a></strong>, pero simplificado y con una idea clara:</p>



<p><strong>Checkstyle valida, Spotless formatea.</strong> Y sí, es XML y parece sacado de los 90… pero es lo que hay 😄</p>



<p>El fichero incluye:</p>



<ul class="wp-block-list">
<li>Convenciones estrictas de nombres</li>



<li>Control de imports</li>



<li>Reglas de calidad de código</li>



<li>Buenas prácticas</li>



<li>Exclusión de código generado</li>
</ul>



<p>Luego por otro lado tengo otro archivo llamado <strong>supressions.xml</strong> por si hay de alguna cosa de la que no quiero que se queje:</p>



<pre class="wp-block-code language-xml has-small-font-size"><code>&lt;?xml version="1.0"?&gt;
&lt;!DOCTYPE suppressions PUBLIC
    "-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
    "https://checkstyle.org/dtds/suppressions_1_2.dtd"&gt;

&lt;!--
    Checkstyle suppressions for test files.
    Allows snake_case method names in tests (common pattern: methodName_shouldDoSomething).
--&gt;
&lt;suppressions&gt;
    &lt;!-- Allow underscores in test method names (e.g., givenX_whenY_thenZ pattern) --&gt;
    &lt;suppress checks="MethodName" files=".*Test\.java$"/&gt;
    &lt;suppress checks="MethodName" files=".*IT\.java$"/&gt;
&lt;/suppressions&gt;
</code></pre>



<h2 class="wp-block-heading">🎣 Script del pre-commit hook</h2>



<p>Aquí es donde echo un poco de menos herramientas como <em>pre-commit</em> o <em>Husky</em> 🥲, porque en este caso toca crear el hook a mano.</p>



<p>El script vive en <strong>gradle/hooks/pre-commit</strong>:</p>



<pre class="wp-block-code language-bash has-small-font-size"><code>#!/bin/bash
# =============================================================================
# Pre-commit hook for Sports Events Service
# Installed automatically by: ./gradlew installGitHooks
# =============================================================================

set -e

echo "🔍 Running pre-commit checks..."

# Colors for output
RED='\033&#91;0;31m'
GREEN='\033&#91;0;32m'
YELLOW='\033&#91;1;33m'
NC='\033&#91;0m' # No Color

# Get only staged Java files
STAGED_JAVA_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.java$' || true)

if &#91; -z "$STAGED_JAVA_FILES" ]; then
    echo -e "${YELLOW}No Java files staged for commit. Skipping Java checks.${NC}"
else
    # Check formatting with Spotless
    echo "📝 Checking code formatting (Spotless)..."
    if ! ./gradlew spotlessCheck --quiet 2&gt;/dev/null; then
        echo -e "${RED}❌ Code formatting issues found!${NC}"
        echo -e "${YELLOW}Run './gradlew spotlessApply' to fix formatting automatically.${NC}"
        exit 1
    fi
    echo -e "${GREEN}✓ Code formatting OK${NC}"

    # Run Checkstyle
    echo "🔍 Checking code style (Checkstyle)..."
    if ! ./gradlew checkstyleMain checkstyleTest --quiet 2&gt;/dev/null; then
        echo -e "${RED}❌ Checkstyle violations found!${NC}"
        echo "Check the report at: build/reports/checkstyle/"
        exit 1
    fi
    echo -e "${GREEN}✓ Checkstyle OK${NC}"
fi

# =============================================================================
# Agentic Workflows Validation
# Compile .md workflows in .github/workflows/ to ensure they are valid
# =============================================================================

# Get staged workflow markdown files
STAGED_WORKFLOW_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '^\.github/workflows/.*\.md$' || true)

if &#91; -z "$STAGED_WORKFLOW_FILES" ]; then
    echo -e "${YELLOW}No agentic workflow files staged. Skipping workflow validation.${NC}"
else
    echo "🤖 Validating agentic workflows..."
    
    # Check if gh aw is available
    if ! command -v gh &amp;&gt; /dev/null || ! gh aw --help &amp;&gt; /dev/null; then
        echo -e "${RED}❌ GitHub CLI with Agentic Workflows extension not found!${NC}"
        echo -e "${YELLOW}Install it with: gh extension install github/gh-aw${NC}"
        exit 1
    fi
    
    # Compile each staged workflow file
    for workflow in $STAGED_WORKFLOW_FILES; do
        echo "  Compiling: $workflow"
        if ! gh aw compile "$workflow" &gt; /dev/null 2&gt;&amp;1; then
            echo -e "${RED}❌ Workflow compilation failed: $workflow${NC}"
            echo -e "${YELLOW}Run 'gh aw compile $workflow' to see errors.${NC}"
            exit 1
        fi
    done
    echo -e "${GREEN}✓ Agentic workflows OK${NC}"
fi

echo -e "${GREEN}✅ All pre-commit checks passed!${NC}"
exit 0
</code></pre>



<p>Y hace lo siguiente:</p>



<ul class="wp-block-list">
<li>Detecta si hay archivos Java en staging</li>



<li>Ejecuta <code>spotlessCheck</code></li>



<li>Ejecuta <code>checkstyleMain</code> y <code>checkstyleTest</code></li>



<li>Bloquea el commit si algo falla</li>
</ul>



<p>Además de todo esto estoy lanzando la herramienta gh aw para forzar la compilación de <a href="https://youtu.be/TGX30Z9YVnY">mis flujos agenticos de GitHub</a>, que era uno de los motivos principales de querer todo esto 😇</p>



<h2 class="wp-block-heading">🔄 Instalación automática del pre-commit hook</h2>



<p>El último paso es instalar este script automáticamente usando Gradle:</p>



<pre class="wp-block-code language-gradle has-small-font-size"><code>// ============================================================================
// Git Hooks Installation
// ============================================================================

tasks.register('installGitHooks', Copy) {
    description = 'Installs pre-commit hooks into .git/hooks'
    group = 'git hooks'

    from("${rootDir}/gradle/hooks/") {
        include 'pre-commit'
    }
    into "${rootDir}/.git/hooks"
    filePermissions {
        user {
            read = true
            write = true
            execute = true
        }
        group {
            read = true
            execute = true
        }
        other {
            read = true
            execute = true
        }
    }
}

// Auto-install hooks on first build
tasks.named('compileJava') {
    dependsOn 'installGitHooks'
}</code></pre>



<h3 class="wp-block-heading">¿Qué conseguimos con esto?</h3>



<ul class="wp-block-list">
<li>El hook se copia automáticamente a <code>.git/hooks</code></li>



<li>Tiene permisos de ejecución (755)</li>



<li>Se instala en cualquier build</li>



<li>Los desarrolladores <strong>no tienen que hacer nada manual</strong></li>



<li>Al clonar y compilar, los hooks ya están activos</li>
</ul>



<p>Instalación manual si hiciera falta:</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>./gradlew installGitHooks  # Instalar/reinstalar hooks manualmente
</code></pre>



<p>y si quieres ver qué tareas tienes disponibles:</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>./gradlew tasks --group "git hooks"  # Ver tasks de hooks</code></pre>



<p>¡Nos vemos 👋🏻!</p>
<p>La entrada <a href="https://www.returngis.net/2026/03/pre-commit-hooks-en-java-sin-python-spotless-checkstyle-gradle/">Pre-commit hooks en Java sin Python: Spotless + Checkstyle + Gradle</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.returngis.net/2026/03/pre-commit-hooks-en-java-sin-python-spotless-checkstyle-gradle/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">31646</post-id>	</item>
		<item>
		<title>Desarrollando con Quarkus en VS Code usando Dev Containers</title>
		<link>https://www.returngis.net/2026/02/desarrollando-con-quarkus-en-vs-code-usando-dev-containers/</link>
					<comments>https://www.returngis.net/2026/02/desarrollando-con-quarkus-en-vs-code-usando-dev-containers/#respond</comments>
		
		<dc:creator><![CDATA[Gisela Torres]]></dc:creator>
		<pubDate>Sat, 21 Feb 2026 07:34:47 +0000</pubDate>
				<category><![CDATA[Contenedores]]></category>
		<category><![CDATA[Dev Containers]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Quarkus]]></category>
		<category><![CDATA[Visual Studio Code]]></category>
		<guid isPermaLink="false">https://www.returngis.net/?p=31606</guid>

					<description><![CDATA[<p>¡Hola developer 👋🏻!A principios de semana te contaba cómo estoy desarrollando actualmente mis aplicaciones con Spring Boot en Visual Studio Code gracias a Dev Containers, y todo lo que podemos configurar como parte del entorno de desarrollo. Pero… ¿y si tu framework preferido no es Spring Boot sino Quarkus?Entonces este artículo es para ti 😉 En esta ocasión quiero compartir la configuración de Dev Container... </p>
<p class="more"><a class="more-link" href="https://www.returngis.net/2026/02/desarrollando-con-quarkus-en-vs-code-usando-dev-containers/">Leer</a></p>
<p>La entrada <a href="https://www.returngis.net/2026/02/desarrollando-con-quarkus-en-vs-code-usando-dev-containers/">Desarrollando con Quarkus en VS Code usando Dev Containers</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>¡Hola developer 👋🏻!<br>A principios de semana te contaba <a href="https://www.returngis.net/2026/02/mi-configuracion-de-dev-container-para-spring-boot/" type="post" id="31588">cómo estoy desarrollando actualmente mis aplicaciones con <strong>Spring Boot</strong> en <strong>Visual Studio Code</strong> gracias a <strong>Dev Containers</strong></a>, y todo lo que podemos configurar como parte del entorno de desarrollo.</p>



<p>Pero… ¿y si tu framework preferido no es Spring Boot sino <strong>Quarkus</strong>?<br>Entonces este artículo es para ti 😉</p>



<p>En esta ocasión quiero compartir la configuración de <strong>Dev Container</strong> que estoy utilizando para proyectos con Quarkus. 🚨 Aviso: me ha dado un poquito más de guerra que la de Spring Boot, pero después de varios ajustes, esta es la que estoy usando ahora mismo y me funciona perfectamente.</p>



<p>🎥 Antes de entrar en la configuración, si todavía no estás familiarizado con Dev Containers o quieres ver cómo los uso en mi día a día con Visual Studio Code, te dejo por aquí un vídeo donde lo explico paso a paso 👇🏻</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe class="youtube-player" width="710" height="400" src="https://www.youtube.com/embed/DkKs29etRis?version=3&#038;rel=1&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;fs=1&#038;hl=es-ES&#038;autohide=2&#038;wmode=transparent" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe>
</div></figure>



<h2 class="wp-block-heading">📁 <code>.devcontainer/devcontainer.json</code></h2>



<p>Esta es la configuración completa del Dev Container:</p>



<pre class="wp-block-code language-javascript has-small-font-size"><code>{
  "name": "Quarkus Dev Environment",
  "dockerComposeFile": "compose.yml",
  "service": "app",
  "workspaceFolder": "/workspace",
  "customizations": {
    "vscode": {
      "extensions": &#91;
        "redhat.java",
        "vscjava.vscode-java-pack",
        "vscjava.vscode-gradle",
        "redhat.vscode-quarkus",
        "redhat.vscode-microprofile",
        "redhat.vscode-yaml",
        "mtxr.sqltools",
        "mtxr.sqltools-driver-pg",
        "ms-azuretools.vscode-docker",
        "humao.rest-client"
      ],
      "settings": {
        "java.configuration.updateBuildConfiguration": "automatic",
        "java.server.launchMode": "Standard",
        "quarkus.tools.starter.api.base": "https://code.quarkus.io",
        "editor.formatOnSave": true,
        "editor.codeActionsOnSave": {
          "source.organizeImports": "explicit"
        },
        "&#91;java]": {
          "editor.defaultFormatter": "redhat.java"
        },
        "sqltools.connections": &#91;
          {
            "name": "🐘 Sports DB (PostgreSQL)",
            "driver": "PostgreSQL",
            "server": "localhost",
            "port": 5432,
            "database": "sports_db",
            "username": "sports_user",
            "password": "sports_pass"
          }
        ]
      }
    }
  },
  "forwardPorts": &#91;
    8080,
    5432
  ],
  "portsAttributes": {
    "8080": {
      "label": "🚀 Quarkus App",
      "onAutoForward": "notify"
    },
    "5432": {
      "label": "🐘 PostgreSQL",
      "onAutoForward": "silent"
    }
  },
  "remoteEnv": {
    "JAVA_HOME": "/usr/lib/jvm/msopenjdk-current"
  },
  "remoteUser": "vscode",
  "features": {
    "ghcr.io/devcontainers/features/github-cli:1": {
      "installDirectlyFromGitHubRelease": true,
      "version": "latest"
    },
    "ghcr.io/devcontainers-extra/features/quarkus-sdkman:2": {
      "version": "latest",
      "jdkVersion": "none",
      "jdkDistro": "ms"
    },
    "ghcr.io/devcontainers/features/java:1": {
      "installGradle": true,
      "version": "none",
      "jdkDistro": "ms",
      "gradleVersion": "8.10.2"
    }
  }
}</code></pre>



<h2 class="wp-block-heading">🐳 Docker Compose: aplicación + base de datos</h2>



<p>En este caso estoy utilizando <strong>Docker Compose</strong> porque mi entorno está formado por <strong>dos contenedores</strong>:</p>



<ul class="wp-block-list">
<li>🧩 <strong>Aplicación Quarkus</strong></li>



<li>🐘 <strong>Base de datos PostgreSQL</strong></li>
</ul>



<pre class="wp-block-code language-yaml has-small-font-size"><code>services:
  app:
    image: mcr.microsoft.com/devcontainers/java:21-bookworm
    volumes:
      - ..:/workspace:cached
    command: sleep infinity
    network_mode: service:db
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: sports_user
      POSTGRES_PASSWORD: sports_pass
      POSTGRES_DB: sports_db
    ports:
      - "5432:5432"
      - "8080:8080"
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: &#91;"CMD-SHELL", "pg_isready -U sports_user -d sports_db"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  postgres-data:</code></pre>



<p>De esta forma consigo que <strong>todas las dependencias necesarias para el desarrollo formen parte del entorno</strong>, sin tener que instalar nada en local. En este ejemplo solo tengo una base de datos, pero el patrón escala muy bien si mañana necesitas Redis, Kafka o cualquier otro servicio.</p>



<h2 class="wp-block-heading">🧩 Extensiones y settings de VS Code</h2>



<p>Como parte del Dev Container incluyo varias extensiones clave:</p>



<ul class="wp-block-list">
<li>Soporte completo para <strong>Java y Gradle</strong></li>



<li>Extensiones específicas de <strong>Quarkus y MicroProfile</strong></li>



<li><strong>SQLTools</strong> para conectar directamente a PostgreSQL</li>



<li>Docker, REST Client y YAML</li>
</ul>



<p>Además, dejo preconfiguradas algunas <strong>settings útiles</strong>, como:</p>



<ul class="wp-block-list">
<li>Formateo automático al guardar</li>



<li>Organización explícita de imports</li>



<li>Conexión directa a la base de datos desde VS Code</li>
</ul>



<p>Todo listo para empezar a trabajar nada más abrir el repositorio 🚀</p>



<h2 class="wp-block-heading">🔌 Puertos y forwarding</h2>



<p>También configuro explícitamente los puertos que necesito:</p>



<ul class="wp-block-list">
<li class="language-shell"><code>8080</code> → Aplicación Quarkus 🚀</li>



<li class="language-shell"><code>5432</code> → PostgreSQL 🐘</li>
</ul>



<p>Incluyendo etiquetas y comportamiento al auto-forward para que la experiencia sea más limpia y visual dentro de VS Code.</p>



<h3 class="wp-block-heading">🧠 Por qué tuve que configurar <code>JAVA_HOME</code> en <code>remoteEnv</code></h3>



<p class="language-shell">En mi caso fue necesario definir explícitamente la variable de entorno <code>JAVA_HOME</code> dentro de la sección <code>remoteEnv</code> del Dev Container.</p>



<p class="language-shell">El motivo es que la <em>feature</em> <code>quarkus-sdkman</code> instala <strong>SDKMAN</strong> y configura automáticamente <code>JAVA_HOME</code> apuntando a la siguiente ruta:</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>/usr/local/sdkman/candidates/java/current
</code></pre>



<p class="language-shell">Sin embargo, en esta configuración <strong>no estoy instalando el JDK vía SDKMAN</strong>, ya que el JDK viene ya preinstalado en la imagen base de Microsoft (<code>mcr.microsoft.com/devcontainers/java:21-bookworm</code>).<br>Esto hace que ese <em>symlink</em> no exista realmente en el contenedor.</p>



<p class="language-shell">👉 Como consecuencia, herramientas como <strong>Gradle o Quarkus</strong> intentan usar un <code>JAVA_HOME</code> que apunta a una ruta inexistente y la ejecución falla.</p>



<p class="language-shell">La solución es sencilla: forzar <code>JAVA_HOME</code> a la ubicación real del JDK que trae la imagen base:</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>/usr/lib/jvm/msopenjdk-current
</code></pre>



<p>Con este override me aseguro de que todas las herramientas Java utilizan el JDK correcto y el entorno funciona de forma estable y predecible.</p>



<h2 class="wp-block-heading">⚠️ Muy importante: Quarkus debe escuchar en todas las interfaces</h2>



<p>Puede que hayas llegado hasta aquí con tu propia configuración y que, al arrancar la aplicación, te hayas encontrado con un <strong>timeout</strong> al intentar acceder desde el navegador.</p>



<p>Esto es muy habitual y tiene una causa clara:<br>👉 <strong>Quarkus, por defecto, no escucha en todas las interfaces de red</strong>.</p>



<p class="language-shell">La solución es sencilla. Solo tienes que añadir esta línea en tu <code>application.properties</code>:</p>



<pre class="wp-block-code language-text has-small-font-size"><code># HTTP configuration - escuchar en todas las interfaces
quarkus.http.host=0.0.0.0</code></pre>



<p>Con esto, tu aplicación será accesible correctamente desde tu máquina local, sin ensuciar tu entorno ni instalar nada en local 🎉</p>



<p>¡Nos vemos 👋🏻!</p>
<p>La entrada <a href="https://www.returngis.net/2026/02/desarrollando-con-quarkus-en-vs-code-usando-dev-containers/">Desarrollando con Quarkus en VS Code usando Dev Containers</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.returngis.net/2026/02/desarrollando-con-quarkus-en-vs-code-usando-dev-containers/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">31606</post-id>	</item>
		<item>
		<title>Mi configuración de Dev Container para Spring Boot</title>
		<link>https://www.returngis.net/2026/02/mi-configuracion-de-dev-container-para-spring-boot/</link>
					<comments>https://www.returngis.net/2026/02/mi-configuracion-de-dev-container-para-spring-boot/#respond</comments>
		
		<dc:creator><![CDATA[Gisela Torres]]></dc:creator>
		<pubDate>Mon, 16 Feb 2026 11:42:53 +0000</pubDate>
				<category><![CDATA[Contenedores]]></category>
		<category><![CDATA[Dev Containers]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Spring Boot]]></category>
		<guid isPermaLink="false">https://www.returngis.net/?p=31588</guid>

					<description><![CDATA[<p>¡Hola developer 👋🏻! Uno de los vídeos que más ha gustado de mi canal de YouTube es el que habla sobre Dev Containers y cómo pueden hacernos la vida mucho más sencilla. La idea es clara: contenerizar todo nuestro entorno de desarrollo y olvidarnos, de una vez por todas, del caos de tecnologías, lenguajes, versiones y dependencias con el que convivimos hoy en día. Si... </p>
<p class="more"><a class="more-link" href="https://www.returngis.net/2026/02/mi-configuracion-de-dev-container-para-spring-boot/">Leer</a></p>
<p>La entrada <a href="https://www.returngis.net/2026/02/mi-configuracion-de-dev-container-para-spring-boot/">Mi configuración de Dev Container para Spring Boot</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>¡Hola developer 👋🏻!</p>



<p>Uno de los vídeos que más ha gustado de <a href="https://www.youtube.com/@returngis" target="_blank" rel="noreferrer noopener">mi canal de YouTube</a> es el que habla sobre <strong>Dev Containers</strong> y cómo pueden hacernos la vida <em>mucho</em> más sencilla. La idea es clara: contenerizar todo nuestro entorno de desarrollo y olvidarnos, de una vez por todas, del caos de tecnologías, lenguajes, versiones y dependencias con el que convivimos hoy en día.</p>



<p>Si todavía no lo has visto, o no sabes de qué te estoy hablando, puedes acceder al vídeo desde aquí 👇🏻</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe class="youtube-player" width="710" height="400" src="https://www.youtube.com/embed/DkKs29etRis?version=3&#038;rel=1&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;fs=1&#038;hl=es-ES&#038;autohide=2&#038;wmode=transparent" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe>
</div></figure>



<p>A raíz de ese contenido —y también por otras vías 🥲— varias personas me han pedido si podía compartir una <strong>configuración de Dev Container para Java</strong>. Cada vez sois más quienes estáis utilizando <strong>VS Code</strong> como entorno de desarrollo también para este lenguaje, pero es cierto que al principio puede costar un poco dejar la configuración fina.</p>



<p>Durante estos días he estado trasteando tanto con <strong>Spring Boot</strong> como con <strong>Quarkus</strong> y, como no podía ser de otra manera, he terminado creando mis propias configuraciones de Dev Containers para ambos frameworks, cada uno con sus&#8230; particularidades 🙃</p>



<p>En este artículo vamos a empezar por <strong>Spring Boot</strong>, que ha sido la parte más sencilla. En un próximo artículo te contaré la configuración de <strong>Quarkus</strong>, que me ha dado un poquito más de guerra 😅</p>



<h2 class="wp-block-heading">🍃 Configuración de Dev Containers para Spring Boot</h2>



<p>Vamos a empezar por la fácil. La configuración de Spring Boot no me ha dado demasiados problemas y tiene un aspecto como el siguiente:</p>



<pre class="wp-block-code language-javascript has-small-font-size"><code>{
	"name": "🍃 Spring Boot Dev Environment",
	"dockerComposeFile": "docker-compose.yml",
	"service": "app",
	"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",

	"features": {
		"ghcr.io/devcontainers/features/java:1": {
			"version": "none",
			"installMaven": "false",
			"installGradle": "true"
		},
		"ghcr.io/devcontainers/features/github-cli:1": {}
	},

	// 🐘 PostgreSQL is available at localhost:5432
	"forwardPorts": &#91;8080, 5432],
	"portsAttributes": {
		"8080": { "label": "🚀 Spring Boot App" },
		"5432": { "label": "🐘 PostgreSQL" }
	},

	// ✅ Wait for DB to be ready before running commands
	"postCreateCommand": "echo '🐘 PostgreSQL ready at localhost:5432 | 🗄️ DB: sports_db | 👤 User: postgres'",

	"customizations": {
		"vscode": {
			"extensions": &#91;
				"vscjava.vscode-java-pack",
				"vmware.vscode-spring-boot",
				"mtxr.sqltools",
				"mtxr.sqltools-driver-pg",
				"vmware.vscode-boot-dev-pack"
			],
			"settings": {
				"java.compile.nullAnalysis.mode": "automatic",
				"java.jdt.ls.java.home": "/usr/lib/jvm/msopenjdk-current",
				"java.configuration.runtimes": &#91;
					{
						"name": "JavaSE-21",
						"path": "/usr/lib/jvm/msopenjdk-current",
						"default": true
					}
				],
				"java.import.gradle.java.home": "/usr/lib/jvm/msopenjdk-current"
			}
		}
	}
}
</code></pre>



<p>Como puedes ver, estoy <strong>delegando la creación de los contenedores en Docker Compose</strong>, ya que mi entorno está compuesto por dos contenedores:</p>



<ul class="wp-block-list">
<li>Uno para la aplicación en <strong>Spring Boot</strong></li>



<li>Otro para la base de datos <strong>PostgreSQL</strong> (que podrías cambiar sin problema por cualquier otra)</li>
</ul>



<p>La configuración de <code>docker-compose.yml</code> es la siguiente:</p>



<pre class="wp-block-code language-yaml has-small-font-size"><code># 🐳 Docker Compose configuration for local development

services:
  # ☕ Java Development Container
  app:
    image: mcr.microsoft.com/devcontainers/java:1-21-bullseye
    volumes:
      - ../..:/workspaces:cached
    command: sleep infinity
    network_mode: service:db
    depends_on:
      db:
        condition: service_healthy

  # 🐘 PostgreSQL Database
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    volumes:
      - postgres-data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: sports_db
    healthcheck:
      test: &#91;"CMD-SHELL", "pg_isready -U postgres -d sports_db"]
      interval: 5s
      timeout: 5s
      retries: 5

# 💾 Persistent volumes
volumes:
  postgres-data:
</code></pre>



<p>La configuración es bastante sencilla, pero si no tienes claro cómo funciona Docker Compose, te recomiendo este otro vídeo que grabé como parte de mi curso de Docker en YouTube 👀</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe class="youtube-player" width="710" height="400" src="https://www.youtube.com/embed/oP2yBGG2yE4?version=3&#038;rel=1&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;fs=1&#038;hl=es-ES&#038;autohide=2&#038;wmode=transparent" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe>
</div></figure>



<h2 class="wp-block-heading">Detalles importantes del Dev Container</h2>



<p>Si vuelves a la configuración del <code>devcontainer.json</code>, hay algunos puntos interesantes a destacar.</p>



<h3 class="wp-block-heading">Features</h3>



<p>He añadido dos features principales:</p>



<ul class="wp-block-list">
<li><strong>Java</strong>: no reinstalo Java porque la imagen que uso para el contenedor de la app ya viene con <strong>Java 21</strong>, pero sí aprovecho esta feature para instalar <strong>Gradle</strong>, que es la herramienta que suelo utilizar siempre que puedo 😅</li>



<li><strong>GitHub CLI</strong>: mis repos están en GitHub, así que tener <code>gh</code> dentro del contenedor me facilita muchísimo la autenticación y la interacción con el repositorio directamente desde el Dev Container.</li>
</ul>



<h3 class="wp-block-heading">Port forwarding</h3>



<p>Hago forwarding de:</p>



<ul class="wp-block-list">
<li><strong>8080</strong> → la aplicación Spring Boot</li>



<li><strong>5432</strong> → la base de datos PostgreSQL</li>
</ul>



<p>Además, añado etiquetas descriptivas para que desde la pestaña de <em>Ports</em> de VS Code sea muy fácil identificar qué es cada cosa.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Etiquetas-para-los-puertos-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="102" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Etiquetas-para-los-puertos.png?resize=710%2C102&#038;ssl=1" alt="" class="wp-image-31591" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Etiquetas-para-los-puertos-scaled.png?resize=1024%2C147&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Etiquetas-para-los-puertos-scaled.png?resize=300%2C43&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Etiquetas-para-los-puertos-scaled.png?resize=768%2C110&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Etiquetas-para-los-puertos-scaled.png?resize=1060%2C152&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Etiquetas-para-los-puertos-scaled.png?resize=1536%2C220&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Etiquetas-para-los-puertos-scaled.png?resize=2048%2C294&amp;ssl=1 2048w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Etiquetas-para-los-puertos-scaled.png?resize=550%2C79&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Etiquetas-para-los-puertos-scaled.png?resize=1920%2C275&amp;ssl=1 1920w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Etiquetas-para-los-puertos-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<p>🚨 <strong>Importante</strong> 🚨 Para poder acceder a tu aplicación debes utilizar http://127.0.0.1:8080 en lugar de localhost. De lo contrario te dará timeout.</p>



<h3 class="wp-block-heading">Extensiones de VS Code</h3>



<p>Por último, incluyo una serie de extensiones que considero básicas para trabajar cómodamente con Spring Boot:</p>



<ul class="wp-block-list">
<li><a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack" target="_blank" rel="noreferrer noopener">Extension Pack for Java</a>: Pack esencial con soporte de lenguaje, debugging, testing, gestión de proyectos Maven/Gradle e IntelliSense.</li>



<li><a href="https://marketplace.visualstudio.com/items?itemName=vmware.vscode-spring-boot" target="_blank" rel="noreferrer noopener">Spring Boot Tools</a>: Navegación inteligente, validación y autocompletado de <code>application.properties</code> / <code>application.yml</code>, y soporte para Live Application Information.</li>



<li><a href="https://marketplace.visualstudio.com/items?itemName=vmware.vscode-boot-dev-pack">Spring Boot Extension Pack</a>: Pack de VMware que agrupa Spring Boot Tools, Spring Initializr y Spring Boot Dashboard para una experiencia completa.</li>



<li><a href="https://marketplace.visualstudio.com/items?itemName=mtxr.sqltools">SQLTools</a>: Cliente SQL integrado en VS Code para conectarse a la base de datos, ejecutar queries y explorar tablas.</li>



<li><a href="https://marketplace.visualstudio.com/items?itemName=mtxr.sqltools-driver-pg">SQLTools PostgreSQL/Cockroach Driver</a>:Driver necesario para conectarse a la base de datos PostgreSQL incluida en el Dev Container.</li>
</ul>



<h2 class="wp-block-heading">🔧 Settings adicionales</h2>



<p>También he tenido que agregar unas settings adicionales a la configuración, para que las extensiones, especialmente por la de <strong>Gradle for Java</strong>, apunten a la ruta correcta para Java Home, porque de lo contrario no encontraba correctamente el SDK.</p>



<p>Si eres fan de <strong>Spring Boot</strong> y quieres empezar a trabajar con Dev Containers, aquí tienes todo lo que necesitas para ponerte en marcha 🚀<br>Y si echas algo en falta, ¡déjamelo en los comentarios! 👇🏻</p>



<p>En los próximos días te compartiré la <strong>configuración de Dev Containers para Quarkus</strong> 😉</p>



<p>¡Nos vemos! 👋🏻</p>
<p>La entrada <a href="https://www.returngis.net/2026/02/mi-configuracion-de-dev-container-para-spring-boot/">Mi configuración de Dev Container para Spring Boot</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.returngis.net/2026/02/mi-configuracion-de-dev-container-para-spring-boot/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">31588</post-id>	</item>
		<item>
		<title>🤖🚀 GitHub Agentic Workflows: tu primer workflow sin escribir YAML</title>
		<link>https://www.returngis.net/2026/02/github-agentic-workflows-tu-primer-workflow-sin-escribir-yaml/</link>
					<comments>https://www.returngis.net/2026/02/github-agentic-workflows-tu-primer-workflow-sin-escribir-yaml/#respond</comments>
		
		<dc:creator><![CDATA[Gisela Torres]]></dc:creator>
		<pubDate>Sat, 14 Feb 2026 07:36:19 +0000</pubDate>
				<category><![CDATA[IA]]></category>
		<category><![CDATA[Coding Agent]]></category>
		<category><![CDATA[GitHub]]></category>
		<category><![CDATA[GitHub Actions]]></category>
		<category><![CDATA[GitHub Agentic Workflows]]></category>
		<guid isPermaLink="false">https://www.returngis.net/?p=31530</guid>

					<description><![CDATA[<p>¡Hola developer 👋🏻!Justo acabo de grabar un vídeo que sale la semana que viene 😅, donde cuento qué flujos estoy utilizando yo a día de hoy con GitHub Copilot CLI para hacerme la vida más fácil. 👉 En cuanto el vídeo esté publicado, lo enlazaré directamente en este artículo. Y casi al mismo tiempo, ayer se anunció la disponibilidad en technical preview de una nueva... </p>
<p class="more"><a class="more-link" href="https://www.returngis.net/2026/02/github-agentic-workflows-tu-primer-workflow-sin-escribir-yaml/">Leer</a></p>
<p>La entrada <a href="https://www.returngis.net/2026/02/github-agentic-workflows-tu-primer-workflow-sin-escribir-yaml/">🤖🚀 GitHub Agentic Workflows: tu primer workflow sin escribir YAML</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>¡Hola developer 👋🏻!<br>Justo acabo de grabar un vídeo que sale <strong>la semana que viene</strong> 😅, donde cuento <strong>qué flujos estoy utilizando yo a día de hoy con GitHub Copilot CLI</strong> para hacerme la vida más fácil.</p>



<p>👉 En cuanto el vídeo esté publicado, <strong>lo enlazaré directamente en este artículo</strong>.</p>



<p>Y casi al mismo tiempo, <a href="https://github.blog/changelog/2026-02-13-github-agentic-workflows-are-now-in-technical-preview/" target="_blank" rel="noreferrer noopener">ayer se anunció la disponibilidad en <em>technical preview</em> de una nueva iniciativa para crear <strong>flujos agénticos automatizados apoyados en GitHub Actions</strong></a>… pero no exactamente de la forma que quizá esperabas.</p>



<p>No, no estamos hablando (todavía 😏) de integrar directamente GitHub Copilot CLI dentro de los workflows —de eso hablaremos en el vídeo de la semana que viene —, sino de algo bastante diferente y, en mi opinión, <strong>muy potente</strong>:<br>👉 definir workflows <strong>usando Markdown</strong>, olvidándote (en gran parte) de escribir YAML a mano como veníamos haciendo hasta ahora.</p>



<p>En este artículo te cuento:</p>



<ul class="wp-block-list">
<li>Qué es esta nueva aproximación</li>



<li>Qué necesitas instalar</li>



<li>Cómo crear tu <strong>primer flujo agéntico</strong></li>



<li>Y cómo funciona la “magia” por debajo ✨</li>
</ul>



<h2 class="wp-block-heading">🎥 Antes de seguir: cómo funcionaba GitHub Actions hasta ahora</h2>



<p>Antes de entrar en este nuevo enfoque, es importante tener claro <strong>cómo hemos estado definiendo workflows en GitHub Actions hasta hoy</strong>.</p>



<p>Si necesitas refrescar conceptos o entender bien la base sobre la que se construye todo esto, tengo una <strong>playlist completa en YouTube sobre GitHub Actions “clásicos”</strong> 🤭, definidos en YAML.</p>



<p>👉 En esa playlist explico:</p>



<ul class="wp-block-list">
<li>Cómo funciona GitHub Actions por dentro</li>



<li>Triggers, jobs y steps</li>



<li>Cómo se estructura un workflow en YAML</li>



<li>Patrones habituales y buenas prácticas</li>
</ul>



<p>📺 <strong>Playlist: GitHub Actions desde cero (enfoque tradicional)</strong><br></p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe class="youtube-player" width="710" height="400" src="https://www.youtube.com/embed?version=3&#038;rel=1&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;fs=1&#038;hl=es-ES&#038;autohide=2&#038;wmode=transparent&#038;listType=playlist&#038;list=PLO9JpmNAsqM7SVT4TgUpzL0ocYLGqiVVQ" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe>
</div></figure>



<p>💡 <em>Precisamente por eso este artículo es interesante:<br><strong>todo lo que ves en esa playlist sigue siendo válido</strong>, pero ahora aparece una nueva capa que nos permite definir workflows de forma más declarativa y agéntica, sin escribir directamente el YAML.</em></p>



<p>Y ahora sí, vamos al lío 👇</p>



<h2 class="wp-block-heading">🧠 ¿Qué es un flujo agéntico en GitHub Actions?</h2>



<p>Hasta ahora, cuando queríamos automatizar algo en GitHub Actions, lo normal era definir un workflow en YAML: eventos, jobs, pasos, condiciones, permisos… todo de forma bastante explícita.</p>



<p>Con esta nueva iniciativa en <em>technical preview</em>, la idea cambia ligeramente.</p>



<p>En lugar de describir <strong>cómo</strong> debe ejecutarse cada paso, pasamos a describir <strong>qué queremos que ocurra</strong>.</p>



<p>La base es sencilla:</p>



<ul class="wp-block-list">
<li>Creas un archivo <strong>Markdown</strong></li>



<li>En ese archivo defines la intención del flujo</li>



<li>Describes las tareas que debe realizar</li>



<li>Y GitHub se encarga de generar el workflow real por debajo</li>
</ul>



<p>Es decir, el YAML <strong>sigue existiendo</strong>, pero deja de ser algo que tengas que escribir y mantener a mano.</p>



<p>Este enfoque encaja especialmente bien con flujos más <strong>agénticos</strong>, donde lo importante no es tanto la secuencia exacta de pasos, sino el objetivo final y las reglas que deben cumplirse.</p>



<h2 class="wp-block-heading">🧩 Requisitos previos: GitHub CLI + extensión <code>aw</code></h2>



<p>Para crear este tipo de flujos <strong>no basta</strong> con añadir un <code>.md</code> dentro de <code>.github/workflows</code>.<br>Antes necesitas preparar tu entorno local.</p>



<h3 class="wp-block-heading">1️⃣ Instalar GitHub CLI</h3>



<p>Si no lo tienes ya, instala <strong>GitHub CLI</strong>.<br>En macOS, por ejemplo:</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>brew install gh</code></pre>



<h3 class="wp-block-heading">2️⃣ Instalar la extensión <code>aw</code> (Agentic Workflows)</h3>



<p>Esta nueva funcionalidad vive en una extensión llamada <strong><code>aw</code></strong>.</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>gh extension install github/gh-aw
</code></pre>



<p>Si este comando falla y ves un error como:</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>X Could not find extension 'github/gh-aw' on host github.com
</code></pre>



<p>puedes instalarla manualmente así:</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>curl -sL https://raw.githubusercontent.com/github/gh-aw/main/install-gh-aw.sh | bash
</code></pre>



<p>Una vez hecho esto, ya estás list@ para <strong>crear y compilar workflows agénticos</strong> 🎉</p>



<h2 class="wp-block-heading">🤖 Un flujo agéntico para GitHub Actions (ejemplo práctico)</h2>



<p>Vamos a verlo con un ejemplo sencillo y muy realista.</p>



<p>👉 Imagina que alguien crea un <em>Issue</em> “de aquella manera”<br>👉 Quieres que automáticamente se mejore: formato, idioma, claridad, labels…</p>



<p>Para ello he creado este archivo dentro de mi repositorio: <strong>.github/workflows/issue-quality-enhancer.md</strong> con el siguiente contenido:</p>



<pre class="wp-block-code language-markdown has-small-font-size"><code>---
on:
  issues:
    types: &#91;opened]

permissions:
  issues: read

safe-outputs:
  update-issue:
    title:
    body:

tools:
  github:
    toolsets: &#91;issues]
---

# Issue Quality Enhancer

Enhance new issues to make them clear, well-structured, and easy to understand.

## Issue to enhance

| Field  | Value          |
| ------ | -------------- |
| Number | #$ISSUE_NUMBER |
| Author | @$ISSUE_AUTHOR |
| Title  | $ISSUE_TITLE   |
| Body   | $ISSUE_BODY    |

## Your tasks

### 1. Get context

- Read the README to understand the project
- List the repository labels (you'll need them later)

### 2. Translate if needed

- If the issue is NOT in English, translate it to English
- Keep all technical details intact

### 3. Improve the title

Add an emoji prefix based on the issue type:

- 🐛 Bug
- ✨ Feature
- 📝 Docs
- 🔧 Refactor
- ⚡ Performance

Example: `🐛 Fix login error when password contains special characters`

### 4. Restructure the body

Use clear sections with emoji headers:

**For bugs:**

```
## 🐛 Description
## 📋 Steps to Reproduce
## ✅ Expected vs ❌ Actual Behavior
```

**For features:**

```
## ✨ Description
## 🎯 Why is this needed?
## 📐 Proposed Solution
```

### 5. Add footer

```
---
&gt; ✍️ *Enhanced by Copilot. Original author: @$ISSUE_AUTHOR*
```

### 6. Apply changes

- **Update** issue #$ISSUE_NUMBER with the new title and body
- **Assign** 1-3 relevant labels
- **Comment** with a brief summary of improvements

## Rules

- Never change the original meaning
- If already well-written, make minimal changes
- Keep it helpful, not verbose</code></pre>



<p>📌 <strong>Importante</strong>: aquí no estamos escribiendo YAML, estamos <strong>describiendo el comportamiento esperado</strong> del flujo. </p>



<p>Si lo revisamos detenidamente encontramos:</p>



<ul class="wp-block-list">
<li>Como en cualquier otro flujo, necesito indicarle cuándo se va a ejecutar el mismo a través de la propiedad <strong>on</strong>. Esto es casi igual que un flujo de GitHub Actions convencional.</li>



<li>La sección de <strong>permissions</strong> se combiba con la de <strong>safe-outputs</strong> para limitar el scope del agente, es decir qué puede hacer.</li>



<li>Como cualquier otro agente de IA es posible que necesite usar algunas <strong>tools</strong>. En este ejemplo estamos usando el MCP server de GitHub pero le tengo restringido únicamente al toolset que tiene que ver con los issues.</li>
</ul>



<p>De esta forma puedes ver que no puede hacer cualquier cosa pero a la vez facilitarnos la vida 😃</p>



<h2 class="wp-block-heading">⚙️ Compilar el flujo agéntico</h2>



<p>El archivo Markdown que acabamos de crear <strong>no es todavía un workflow ejecutable</strong> por GitHub Actions.</p>



<p>Antes, es necesario un paso intermedio: <strong>compilar el flujo</strong>.</p>



<p>Al compilarlo, GitHub traduce la intención que hemos definido en Markdown a un workflow real de GitHub Actions, generando automáticamente los archivos necesarios.</p>



<p>Para ello, basta con ejecutar el siguiente comando desde el repositorio:</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>gh aw compile</code></pre>



<p>Tras ejecutar este comando, verás que ocurren varias cosas:</p>



<ul class="wp-block-list">
<li>Se crea un nuevo directorio <code>.github/aw</code></li>



<li>Se genera un archivo <code>.lock.yml</code> dentro de <code>.github/workflows</code></li>



<li>Ese <code>.lock.yml</code> contiene el <strong>workflow real en YAML</strong>, listo para ser ejecutado por GitHub Actions</li>
</ul>



<p>🔍 <strong>Lo importante aquí</strong> es que:</p>



<ul class="wp-block-list">
<li>El YAML sigue existiendo</li>



<li>GitHub Actions sigue funcionando igual</li>



<li>Pero tú ya no tienes que escribir ni mantener ese YAML a mano</li>
</ul>



<p>A partir de este momento, cualquier cambio que hagas en el Markdown y vuelvas a compilar actualizará automáticamente el workflow generado.</p>



<h2 class="wp-block-heading">🔐 Crear un PAT con scope <em>Copilot Requests</em> para tu repositorio</h2>



<p>Estos flujos hacen uso de tu suscripción de <strong>GitHub Copilot</strong>, así que necesitas un <strong>Personal Access Token</strong> con el permiso adecuado.</p>



<p>Lo único que tienes que hacer es ir a <a href="https://github.com/settings/personal-access-tokens/new">https://github.com/settings/personal-access-tokens/new</a> y crear un token con el scope ✅ <strong>Copilot Requests</strong></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/PAT-con-el-scope-Copilot-Requests-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="433" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/PAT-con-el-scope-Copilot-Requests.png?resize=710%2C433&#038;ssl=1" alt="" class="wp-image-31543" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/PAT-con-el-scope-Copilot-Requests-scaled.png?resize=1024%2C624&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/PAT-con-el-scope-Copilot-Requests-scaled.png?resize=300%2C183&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/PAT-con-el-scope-Copilot-Requests-scaled.png?resize=768%2C468&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/PAT-con-el-scope-Copilot-Requests-scaled.png?resize=902%2C550&amp;ssl=1 902w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/PAT-con-el-scope-Copilot-Requests-scaled.png?resize=1060%2C646&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/PAT-con-el-scope-Copilot-Requests-scaled.png?resize=1536%2C936&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/PAT-con-el-scope-Copilot-Requests-scaled.png?resize=550%2C335&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/PAT-con-el-scope-Copilot-Requests-scaled.png?resize=820%2C500&amp;ssl=1 820w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/PAT-con-el-scope-Copilot-Requests-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<p>Puedes configurarlo en tu repositorio con el siguiente comando:</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>gh aw secrets set COPILOT_GITHUB_TOKEN --value TU_PAT
</code></pre>



<h2 class="wp-block-heading">🧪 Probando el resultado</h2>



<p>En mi caso, simplemente creé un Issue con:</p>



<ul class="wp-block-list">
<li><strong>Título</strong>: <code>Añadir endpoint para gestionar productos</code></li>



<li><strong>Descripción</strong>: <code>hacer el CRUD completo</code></li>
</ul>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-7-con-poco-detalle-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="230" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-7-con-poco-detalle.png?resize=710%2C230&#038;ssl=1" alt="" class="wp-image-31578" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-7-con-poco-detalle-scaled.png?resize=1024%2C331&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-7-con-poco-detalle-scaled.png?resize=300%2C97&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-7-con-poco-detalle-scaled.png?resize=768%2C248&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-7-con-poco-detalle-scaled.png?resize=1704%2C550&amp;ssl=1 1704w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-7-con-poco-detalle-scaled.png?resize=1060%2C342&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-7-con-poco-detalle-scaled.png?resize=1536%2C496&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-7-con-poco-detalle-scaled.png?resize=550%2C178&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-7-con-poco-detalle-scaled.png?resize=1549%2C500&amp;ssl=1 1549w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-7-con-poco-detalle-scaled.png?resize=1920%2C620&amp;ssl=1 1920w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-7-con-poco-detalle-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<p>Vamos… bastante justito 😅</p>



<p>En la pestaña <strong>Actions</strong> puedes ver que el flujo se ejecuta <strong>como cualquier otro workflow</strong>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Flujo-agentico-ejecutandose-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="197" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Flujo-agentico-ejecutandose-1024x284.png?resize=710%2C197&#038;ssl=1" alt="" class="wp-image-31579" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Flujo-agentico-ejecutandose-scaled.png?resize=1024%2C284&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Flujo-agentico-ejecutandose-scaled.png?resize=300%2C83&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Flujo-agentico-ejecutandose-scaled.png?resize=768%2C213&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Flujo-agentico-ejecutandose-scaled.png?resize=1980%2C550&amp;ssl=1 1980w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Flujo-agentico-ejecutandose-scaled.png?resize=1060%2C294&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Flujo-agentico-ejecutandose-scaled.png?resize=1536%2C427&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Flujo-agentico-ejecutandose-scaled.png?resize=2048%2C569&amp;ssl=1 2048w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Flujo-agentico-ejecutandose-scaled.png?resize=550%2C153&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Flujo-agentico-ejecutandose-scaled.png?resize=1800%2C500&amp;ssl=1 1800w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Flujo-agentico-ejecutandose-scaled.png?resize=1920%2C533&amp;ssl=1 1920w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Flujo-agentico-ejecutandose-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<p>Si te fijas en los pasos, verás que <strong>por debajo se apoya en GitHub Copilot CLI</strong>, aunque tú no hayas tenido que invocarlo directamente 👀</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/GitHub-Agentic-Workflows-se-apoya-en-GitHub-Copilot-CLI-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="532" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/GitHub-Agentic-Workflows-se-apoya-en-GitHub-Copilot-CLI.png?resize=710%2C532&#038;ssl=1" alt="" class="wp-image-31580" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/GitHub-Agentic-Workflows-se-apoya-en-GitHub-Copilot-CLI-scaled.png?resize=1024%2C767&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/GitHub-Agentic-Workflows-se-apoya-en-GitHub-Copilot-CLI-scaled.png?resize=300%2C225&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/GitHub-Agentic-Workflows-se-apoya-en-GitHub-Copilot-CLI-scaled.png?resize=768%2C575&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/GitHub-Agentic-Workflows-se-apoya-en-GitHub-Copilot-CLI-scaled.png?resize=734%2C550&amp;ssl=1 734w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/GitHub-Agentic-Workflows-se-apoya-en-GitHub-Copilot-CLI-scaled.png?resize=1060%2C794&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/GitHub-Agentic-Workflows-se-apoya-en-GitHub-Copilot-CLI-scaled.png?resize=1536%2C1150&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/GitHub-Agentic-Workflows-se-apoya-en-GitHub-Copilot-CLI-scaled.png?resize=2048%2C1534&amp;ssl=1 2048w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/GitHub-Agentic-Workflows-se-apoya-en-GitHub-Copilot-CLI-scaled.png?resize=550%2C412&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/GitHub-Agentic-Workflows-se-apoya-en-GitHub-Copilot-CLI-scaled.png?resize=668%2C500&amp;ssl=1 668w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/GitHub-Agentic-Workflows-se-apoya-en-GitHub-Copilot-CLI-scaled.png?resize=1920%2C1438&amp;ssl=1 1920w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/GitHub-Agentic-Workflows-se-apoya-en-GitHub-Copilot-CLI-scaled.png?resize=1442%2C1080&amp;ssl=1 1442w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/GitHub-Agentic-Workflows-se-apoya-en-GitHub-Copilot-CLI-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<p>Y, finalmente…<br>🎉 el Issue aparece <strong>mejorado, estructurado y etiquetado automáticamente</strong>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-mejorado-gracias-a-un-Agentic-Workflow-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="498" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-mejorado-gracias-a-un-Agentic-Workflow.png?resize=710%2C498&#038;ssl=1" alt="" class="wp-image-31583" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-mejorado-gracias-a-un-Agentic-Workflow-scaled.png?resize=1024%2C718&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-mejorado-gracias-a-un-Agentic-Workflow-scaled.png?resize=300%2C210&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-mejorado-gracias-a-un-Agentic-Workflow-scaled.png?resize=768%2C539&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-mejorado-gracias-a-un-Agentic-Workflow-scaled.png?resize=784%2C550&amp;ssl=1 784w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-mejorado-gracias-a-un-Agentic-Workflow-scaled.png?resize=1060%2C744&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-mejorado-gracias-a-un-Agentic-Workflow-scaled.png?resize=1536%2C1078&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-mejorado-gracias-a-un-Agentic-Workflow-scaled.png?resize=2048%2C1437&amp;ssl=1 2048w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-mejorado-gracias-a-un-Agentic-Workflow-scaled.png?resize=550%2C386&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-mejorado-gracias-a-un-Agentic-Workflow-scaled.png?resize=713%2C500&amp;ssl=1 713w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-mejorado-gracias-a-un-Agentic-Workflow-scaled.png?resize=1920%2C1347&amp;ssl=1 1920w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-mejorado-gracias-a-un-Agentic-Workflow-scaled.png?resize=1539%2C1080&amp;ssl=1 1539w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Issue-mejorado-gracias-a-un-Agentic-Workflow-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<p>¡Nos vemos 👋🏻!<br><em>(y ojo al vídeo de la semana que viene 😉)</em></p>
<p>La entrada <a href="https://www.returngis.net/2026/02/github-agentic-workflows-tu-primer-workflow-sin-escribir-yaml/">🤖🚀 GitHub Agentic Workflows: tu primer workflow sin escribir YAML</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.returngis.net/2026/02/github-agentic-workflows-tu-primer-workflow-sin-escribir-yaml/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">31530</post-id>	</item>
		<item>
		<title>Haz que tus custom agents sean subagents de GitHub Copilot</title>
		<link>https://www.returngis.net/2026/02/haz-que-tus-custom-agents-sean-subagents-de-github-copilot/</link>
					<comments>https://www.returngis.net/2026/02/haz-que-tus-custom-agents-sean-subagents-de-github-copilot/#respond</comments>
		
		<dc:creator><![CDATA[Gisela Torres]]></dc:creator>
		<pubDate>Mon, 09 Feb 2026 06:44:15 +0000</pubDate>
				<category><![CDATA[IA]]></category>
		<category><![CDATA[Coding Agent]]></category>
		<category><![CDATA[Custom Agents]]></category>
		<category><![CDATA[GitHub]]></category>
		<category><![CDATA[GitHub Copilot]]></category>
		<category><![CDATA[ia]]></category>
		<category><![CDATA[Sub agents]]></category>
		<category><![CDATA[Visual Studio Code]]></category>
		<guid isPermaLink="false">https://www.returngis.net/?p=31495</guid>

					<description><![CDATA[<p>¡Hola developer 👋🏻!¿Sabías que cada vez que creas un custom agent, ya sea a nivel de proyecto o de usuario, en realidad estás creando un posible subagent? 🧩🤖 En este artículo te explico cómo funcionan los subagents, qué papel juegan dentro de los flujos agentic y, sobre todo, cómo puedes sacarles partido en escenarios reales. Veremos cómo usarlos tanto desde Visual Studio Code como junto... </p>
<p class="more"><a class="more-link" href="https://www.returngis.net/2026/02/haz-que-tus-custom-agents-sean-subagents-de-github-copilot/">Leer</a></p>
<p>La entrada <a href="https://www.returngis.net/2026/02/haz-que-tus-custom-agents-sean-subagents-de-github-copilot/">Haz que tus custom agents sean subagents de GitHub Copilot</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>¡Hola developer 👋🏻!<br>¿Sabías que cada vez que creas un <strong>custom agent</strong>, ya sea a nivel de proyecto o de usuario, en realidad estás creando <strong>un posible subagent</strong>? 🧩🤖</p>



<p>En este artículo te explico <strong>cómo funcionan los subagents</strong>, qué papel juegan dentro de los flujos agentic y, sobre todo, <strong>cómo puedes sacarles partido en escenarios reales</strong>. Veremos cómo usarlos tanto desde <strong>Visual Studio Code</strong> como junto a <strong>Coding Agent</strong>, y por qué esta capacidad cambia por completo la forma en la que diseñamos agentes más especializados, reutilizables y escalables.</p>



<h2 class="wp-block-heading">Cómo funciona</h2>



<p> Cada vez que creamos un custom agent este <strong>por defecto tiene la propiedad infer a true</strong>.</p>



<pre class="wp-block-code language-yaml has-small-font-size"><code>---
name: '📺 YouTube'
description: 'Agente que me ayuda a escribir los READMEs de los repositorio que comparto en mis vídeos de YouTube'
infer: true # ⬅️ Esto por defecto es true
---

&#91;...]</code></pre>



<p>Esto lo que significa es que estamos permitiendo que, aunque no elijamos directamente este custom agent, si el contexto lo permite, usamos explícitamente la tool #agent y no hemos forzado a que este infer sea false podamos beneficiarnos de estos custom agents incluso en conversaciones en las que no lo hayamos seleccionado como parte de nuestro combo del chat o incluso cuando hacemos uso de Coding Agent en el cloud.</p>



<p>Si quieres saber más sobre Custom Agents puedes echarle un vistazo a este vídeo de mi canal de YouTube:</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe class="youtube-player" width="710" height="400" src="https://www.youtube.com/embed/5VchzuK6wTk?version=3&#038;rel=1&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;fs=1&#038;hl=es-ES&#038;autohide=2&#038;wmode=transparent" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe>
</div></figure>



<h2 class="wp-block-heading">Ejemplo en Visual Studio Code</h2>



<p>Para que lo veas con un ejemplo sencillo, tengo un custom agent a nivel global llamado 📺 YouTube que me permite dejar mis READMEs lo más bonitos posible para poder compartir el repo con información relevante. </p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Custom-agent-Youtube.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="483" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Custom-agent-Youtube.png?resize=710%2C483&#038;ssl=1" alt="" class="wp-image-31510" style="aspect-ratio:1.4698162729658792;width:476px;height:auto" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Custom-agent-Youtube.png?w=876&amp;ssl=1 876w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Custom-agent-Youtube.png?resize=300%2C204&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Custom-agent-Youtube.png?resize=768%2C523&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Custom-agent-Youtube.png?resize=808%2C550&amp;ssl=1 808w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Custom-agent-Youtube.png?resize=550%2C374&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Custom-agent-Youtube.png?resize=735%2C500&amp;ssl=1 735w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<p>Ahora bien, para que esta característica funcione desde Visual Studio Code necesitas habilitar un check en la configuración que dice «<strong>Custom Agent in Subagent</strong>«:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Habilitar-custom-agent-como-sub-agent-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="178" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Habilitar-custom-agent-como-sub-agent.png?resize=710%2C178&#038;ssl=1" alt="" class="wp-image-31504" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Habilitar-custom-agent-como-sub-agent-scaled.png?resize=1024%2C256&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Habilitar-custom-agent-como-sub-agent-scaled.png?resize=300%2C75&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Habilitar-custom-agent-como-sub-agent-scaled.png?resize=768%2C192&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Habilitar-custom-agent-como-sub-agent-scaled.png?resize=1060%2C265&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Habilitar-custom-agent-como-sub-agent-scaled.png?resize=1536%2C384&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Habilitar-custom-agent-como-sub-agent-scaled.png?resize=550%2C138&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Habilitar-custom-agent-como-sub-agent-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<p>Con ello cada ves que pidas una tarea puedes o bien usar la tool #agent para que el agente principal, sea otro custom agent o no, tome ese otro agente como subagent:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Invocando-un-sub-agent-usando-el-hashtag-agent-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="222" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Invocando-un-sub-agent-usando-el-hashtag-agent.png?resize=710%2C222&#038;ssl=1" alt="" class="wp-image-31511" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Invocando-un-sub-agent-usando-el-hashtag-agent-scaled.png?resize=1024%2C320&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Invocando-un-sub-agent-usando-el-hashtag-agent-scaled.png?resize=300%2C94&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Invocando-un-sub-agent-usando-el-hashtag-agent-scaled.png?resize=768%2C240&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Invocando-un-sub-agent-usando-el-hashtag-agent-scaled.png?resize=1060%2C331&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Invocando-un-sub-agent-usando-el-hashtag-agent-scaled.png?resize=1536%2C479&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Invocando-un-sub-agent-usando-el-hashtag-agent-scaled.png?resize=550%2C172&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Invocando-un-sub-agent-usando-el-hashtag-agent-scaled.png?resize=1602%2C500&amp;ssl=1 1602w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Invocando-un-sub-agent-usando-el-hashtag-agent-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<p>O incluso según el contexto de la conversación también puede ser utilizado de forma transparente para ti sin que tengas que añadir nada adicional:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Detecta-que-tiene-que-usar-un-custom-agent-como-sub-agent-por-el-contexto-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="697" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Detecta-que-tiene-que-usar-un-custom-agent-como-sub-agent-por-el-contexto.png?resize=710%2C697&#038;ssl=1" alt="" class="wp-image-31513" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Detecta-que-tiene-que-usar-un-custom-agent-como-sub-agent-por-el-contexto-scaled.png?resize=1024%2C1005&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Detecta-que-tiene-que-usar-un-custom-agent-como-sub-agent-por-el-contexto-scaled.png?resize=300%2C294&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Detecta-que-tiene-que-usar-un-custom-agent-como-sub-agent-por-el-contexto-scaled.png?resize=768%2C754&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Detecta-que-tiene-que-usar-un-custom-agent-como-sub-agent-por-el-contexto-scaled.png?resize=560%2C550&amp;ssl=1 560w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Detecta-que-tiene-que-usar-un-custom-agent-como-sub-agent-por-el-contexto-scaled.png?resize=1060%2C1040&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Detecta-que-tiene-que-usar-un-custom-agent-como-sub-agent-por-el-contexto-scaled.png?resize=1536%2C1507&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Detecta-que-tiene-que-usar-un-custom-agent-como-sub-agent-por-el-contexto-scaled.png?resize=550%2C540&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Detecta-que-tiene-que-usar-un-custom-agent-como-sub-agent-por-el-contexto-scaled.png?resize=510%2C500&amp;ssl=1 510w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Detecta-que-tiene-que-usar-un-custom-agent-como-sub-agent-por-el-contexto-scaled.png?resize=1101%2C1080&amp;ssl=1 1101w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/Detecta-que-tiene-que-usar-un-custom-agent-como-sub-agent-por-el-contexto-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<p>En este ejemplo, como puedes ver en el pantallazo, estoy usando el modo Agente que viene de caja pero aún así está delegando esta tarea en mi subagent 📺 YouTube.</p>



<h2 class="wp-block-heading">Ejemplo con Coding Agent</h2>



<p>Para este otro ejemplo he creado un issue donde le he pedido a Coding agent que me mejore el README de un proyecto y, como puedes ver en la sesión, se ha dado cuenta de que tengo un custom agent llamado YouTube configurado a nivel de cuenta que me permite hacer lo mismo que en el caso anterior y por lo tanto puede usarlo también del mismo modo.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/sub-agents-con-GitHub-Copilot-Coding-Agent-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="417" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/sub-agents-con-GitHub-Copilot-Coding-Agent.png?resize=710%2C417&#038;ssl=1" alt="" class="wp-image-31497" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/sub-agents-con-GitHub-Copilot-Coding-Agent-scaled.png?resize=1024%2C602&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/sub-agents-con-GitHub-Copilot-Coding-Agent-scaled.png?resize=300%2C176&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/sub-agents-con-GitHub-Copilot-Coding-Agent-scaled.png?resize=768%2C452&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/sub-agents-con-GitHub-Copilot-Coding-Agent-scaled.png?resize=935%2C550&amp;ssl=1 935w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/sub-agents-con-GitHub-Copilot-Coding-Agent-scaled.png?resize=1060%2C624&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/sub-agents-con-GitHub-Copilot-Coding-Agent-scaled.png?resize=1536%2C904&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/sub-agents-con-GitHub-Copilot-Coding-Agent-scaled.png?resize=2048%2C1205&amp;ssl=1 2048w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/sub-agents-con-GitHub-Copilot-Coding-Agent-scaled.png?resize=550%2C324&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/sub-agents-con-GitHub-Copilot-Coding-Agent-scaled.png?resize=850%2C500&amp;ssl=1 850w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/sub-agents-con-GitHub-Copilot-Coding-Agent-scaled.png?resize=1920%2C1130&amp;ssl=1 1920w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/sub-agents-con-GitHub-Copilot-Coding-Agent-scaled.png?resize=1836%2C1080&amp;ssl=1 1836w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/sub-agents-con-GitHub-Copilot-Coding-Agent-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<p>Y lo chulo de todo esto es que si pido la misma tarea a en otro repositorio:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/mismo-sub-agent-en-otro-repositorio-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="416" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/mismo-sub-agent-en-otro-repositorio.png?resize=710%2C416&#038;ssl=1" alt="" class="wp-image-31499" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/mismo-sub-agent-en-otro-repositorio-scaled.png?resize=1024%2C600&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/mismo-sub-agent-en-otro-repositorio-scaled.png?resize=300%2C176&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/mismo-sub-agent-en-otro-repositorio-scaled.png?resize=768%2C450&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/mismo-sub-agent-en-otro-repositorio-scaled.png?resize=939%2C550&amp;ssl=1 939w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/mismo-sub-agent-en-otro-repositorio-scaled.png?resize=1060%2C621&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/mismo-sub-agent-en-otro-repositorio-scaled.png?resize=1536%2C899&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/mismo-sub-agent-en-otro-repositorio-scaled.png?resize=2048%2C1199&amp;ssl=1 2048w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/mismo-sub-agent-en-otro-repositorio-scaled.png?resize=550%2C322&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/mismo-sub-agent-en-otro-repositorio-scaled.png?resize=854%2C500&amp;ssl=1 854w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/mismo-sub-agent-en-otro-repositorio-scaled.png?resize=1920%2C1124&amp;ssl=1 1920w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/mismo-sub-agent-en-otro-repositorio-scaled.png?resize=1844%2C1080&amp;ssl=1 1844w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/02/mismo-sub-agent-en-otro-repositorio-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<p>¡Ahí está de nuevo!</p>



<p>Si quieres saber más sobre dónde puedes configurar tus custom agents puedes echarle un vistazo a este otro vídeo de mi canal 😇</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe class="youtube-player" width="710" height="400" src="https://www.youtube.com/embed/vu4LooPm11c?version=3&#038;rel=1&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;fs=1&#038;hl=es-ES&#038;autohide=2&#038;wmode=transparent" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe>
</div></figure>



<p>¡Nos vemos 👋🏻!</p>
<p>La entrada <a href="https://www.returngis.net/2026/02/haz-que-tus-custom-agents-sean-subagents-de-github-copilot/">Haz que tus custom agents sean subagents de GitHub Copilot</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.returngis.net/2026/02/haz-que-tus-custom-agents-sean-subagents-de-github-copilot/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">31495</post-id>	</item>
		<item>
		<title>🔌 MCP Server como Proxy de otro MCP Server</title>
		<link>https://www.returngis.net/2026/02/%f0%9f%94%8c-mcp-server-como-proxy-de-otro-mcp-server/</link>
					<comments>https://www.returngis.net/2026/02/%f0%9f%94%8c-mcp-server-como-proxy-de-otro-mcp-server/#respond</comments>
		
		<dc:creator><![CDATA[Gisela Torres]]></dc:creator>
		<pubDate>Mon, 02 Feb 2026 14:48:52 +0000</pubDate>
				<category><![CDATA[IA]]></category>
		<category><![CDATA[ia]]></category>
		<category><![CDATA[MCP Servers]]></category>
		<category><![CDATA[Model Context Protocol]]></category>
		<guid isPermaLink="false">https://www.returngis.net/?p=31467</guid>

					<description><![CDATA[<p>¡Hola developer 👋🏻! Si llevas un tiempo trabajando con MCP Servers, seguro que te has encontrado con este escenario: el servidor funciona bien, el caso de uso es potente… pero en cuanto intentas usarlo en equipo, desde varios agentes, en una compañía o en remoto, todo empieza a complicarse. Y no, el problema no es solo stdio. En este artículo te voy a enseñar un... </p>
<p class="more"><a class="more-link" href="https://www.returngis.net/2026/02/%f0%9f%94%8c-mcp-server-como-proxy-de-otro-mcp-server/">Leer</a></p>
<p>La entrada <a href="https://www.returngis.net/2026/02/%f0%9f%94%8c-mcp-server-como-proxy-de-otro-mcp-server/">🔌 MCP Server como Proxy de otro MCP Server</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>¡Hola developer 👋🏻! Si llevas un tiempo trabajando con <strong>MCP Servers</strong>, seguro que te has encontrado con este escenario: el servidor funciona bien, el caso de uso es potente… pero en cuanto intentas usarlo <strong>en equipo</strong>, <strong>desde varios agentes</strong>, <strong>en una compañía</strong> o <strong>en remoto</strong>, todo empieza a complicarse. Y no, el problema <strong>no es solo <code>stdio</code></strong>.</p>



<p>En este artículo te voy a enseñar un patrón muy práctico y realista: <strong>usar un MCP Server para crear un proxy que permita ejecutar MCP Servers como un servicio HTTP, centralizando además las credenciales (PATs, API Keys, tokens)</strong>.</p>



<p>Un enfoque especialmente útil para entornos corporativos, Agentic DevOps y agentes compartidos.</p>



<h2 class="wp-block-heading">🤔 El problema real con muchos MCP Servers</h2>



<p>Cuando pasamos de pruebas locales a escenarios reales, suelen aparecer <strong>dos grandes frenos</strong>.</p>



<h3 class="wp-block-heading">1️⃣ MCP Servers basados en <code>stdio</code></h3>



<p>Muchos MCP Servers funcionan únicamente vía <strong>stdio</strong>, lo que implica que:</p>



<ul class="wp-block-list">
<li>❌ Solo pueden ejecutarse localmente</li>



<li>❌ No pueden exponerse directamente como servicios</li>



<li>❌ Cada cliente debe lanzar su propio proceso</li>
</ul>



<p>Esto ya es una limitación importante cuando quieres algo compartido o centralizado. <strong>Si tu problema principal no es este</strong>, te recomiendo que eches un vistazo a <a href="https://learn.microsoft.com/es-es/azure/api-management/expose-existing-mcp-server" target="_blank" rel="noreferrer noopener">Azure API Management, el cual, como servicio, también permite hacer de proxy de MCP Servers que soporten SSE o Streamable HTTP</a>.</p>



<h3 class="wp-block-heading">2️⃣ MCP Servers que requieren credenciales (PAT, API Keys, tokens…)</h3>



<p>Además, una gran parte de los MCP Servers:</p>



<ul class="wp-block-list">
<li>❌ Requieren <strong>Personal Access Tokens (PAT)</strong> u otros secretos</li>



<li>❌ Obligan a configurar credenciales <strong>en cada cliente</strong></li>



<li>❌ Dificultan el uso compartido entre agentes o personas</li>



<li>❌ Incrementan el riesgo de fugas de credenciales</li>
</ul>



<p>Y aquí viene el matiz importante:</p>



<p><strong>👉 Incluso aunque el MCP Server NO use <code>stdio</code>, el simple hecho de requerir credenciales ya complica muchísimo su adopción en equipos y entornos corporativos, especialmente si esas credenciales son diferentes a las corporativas.</strong></p>



<p>Si quieres saber más sobre MCP Servers puedes echarle un vistazo a mi playlist de mi canal de YouTube:</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe class="youtube-player" width="710" height="400" src="https://www.youtube.com/embed?version=3&#038;rel=1&#038;showsearch=0&#038;showinfo=1&#038;iv_load_policy=1&#038;fs=1&#038;hl=es-ES&#038;autohide=2&#038;wmode=transparent&#038;listType=playlist&#038;list=PLO9JpmNAsqM6hj9FIFIgt5UkHyBiepMrS" allowfullscreen="true" style="border:0;" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation allow-popups-to-escape-sandbox"></iframe>
</div></figure>



<h3 class="wp-block-heading">El verdadero problema</h3>



<p>El problema real no es solo técnico. Muchos MCP Servers <strong>no están pensados para ejecutarse como servicios compartidos</strong>, ni desde el punto de vista de ejecución <strong>ni</strong> de seguridad.</p>



<h2 class="wp-block-heading">✅ La idea: MCP Server como proxy de otro MCP Server</h2>



<p>La solución pasa por <strong>desacoplar completamente</strong> al cliente MCP del MCP Server real. Ejecutar el MCP Server real en una máquina o contenedor centralizado, y exponerlo vía <strong>HTTP</strong>, <strong>centralizando también las credenciales</strong>.</p>



<p>Con este enfoque conseguimos:</p>



<ul class="wp-block-list">
<li>✅ Ejecutar MCP Servers <strong>stdio o no-stdio</strong> en un entorno controlado</li>



<li>✅ Evitar distribuir <strong>PATs, API Keys o tokens</strong> a los clientes</li>



<li>✅ Acceder desde <strong>cualquier cliente MCP</strong> vía HTTP</li>



<li>✅ Compartir el acceso entre <strong>múltiples usuarios o agentes</strong></li>



<li>✅ Aplicar políticas de seguridad y autenticación en un único punto</li>



<li>✅ Desplegarlo como <strong>servicio remoto</strong> (VM, contenedor, Kubernetes…)</li>
</ul>



<p>FastMCP encaja perfectamente como capa de <strong>proxy y control</strong>, porque además viene integrado como parte del SDK a día de hoy.</p>



<h2 class="wp-block-heading">🎯 Caso práctico: Azure DevOps MCP Server</h2>



<p>En el repositorio uso como ejemplo el <strong>Azure DevOps MCP Server</strong>, porque concentra <strong>los dos problemas más habituales</strong>:</p>



<ol class="wp-block-list">
<li>Usa <strong>stdio</strong> → no puede exponerse directamente como servicio</li>



<li>Requiere un <strong>Personal Access Token (PAT)</strong> o autenticarse usando Azure CLI.</li>
</ol>



<h2 class="wp-block-heading">🏗️ Arquitectura del proxy MCP</h2>



<p>El flujo es el siguiente:</p>



<ul class="wp-block-list">
<li>El <strong>cliente MCP</strong> (Copilot, Claude, VS Code…) habla HTTP</li>



<li>FastMCP actúa como <strong>proxy</strong></li>



<li>El MCP Server original sigue funcionando como siempre</li>



<li>Las <strong>credenciales viven solo en el servidor</strong>, no en los clientes</li>
</ul>



<p>Esto simplifica muchísimo la arquitectura y mejora la seguridad.</p>



<h2 class="wp-block-heading">📋 Requisitos</h2>



<p>Para ejecutar este ejemplo necesitas:</p>



<ul class="wp-block-list">
<li><strong>Python 3.10+</strong></li>



<li><strong>Node.js 20+</strong> (en el caso del ejemplo de Azure DevOps)</li>



<li>Las <strong>credenciales</strong> necesarias del MCP Server que vayas a hacer de proxy</li>
</ul>



<h2 class="wp-block-heading">🚀 Instalación paso a paso</h2>



<h3 class="wp-block-heading">1️⃣ Clonar el repositorio</h3>



<pre class="wp-block-code language-shell has-small-font-size"><code>git clone https://github.com/0GiS0/mcp-server-as-a-proxy.git
cd mcp-server-as-a-proxy
</code></pre>



<h3 class="wp-block-heading">2️⃣ Instalar dependencias</h3>



<pre class="wp-block-code language-shell has-small-font-size"><code>pip install -e .
</code></pre>



<h3 class="wp-block-heading">3️⃣ Configurar variables de entorno</h3>



<pre class="wp-block-code language-shell has-small-font-size"><code>cp .env.example .env
</code></pre>



<p>Ejemplo para Azure DevOps:</p>



<pre class="wp-block-code language-text has-small-font-size"><code>AZURE_DEVOPS_ORG=tu-organizacion
ADO_MCP_AUTH_TOKEN=tu-personal-access-token
</code></pre>



<h3 class="wp-block-heading">4️⃣ Ejecutar el proxy</h3>



<pre class="wp-block-code language-shell has-small-font-size"><code>python proxy_server.py
</code></pre>



<p>El proxy quedará disponible en http://localhost:8080/mcp<br></p>



<h2 class="wp-block-heading">🍿 El resultado</h2>



<figure class="wp-block-video"><video height="2160" style="aspect-ratio: 3840 / 2160;" width="3840" controls src="https://www.returngis.net/wp-content/uploads/2026/02/MCP-Server-como-proxy-de-otro-MCP-Server.mp4"></video></figure>



<h2 class="wp-block-heading">⚠️ <strong>Importante</strong></h2>



<p>Este ejemplo es una PoC para demostrar que técnicamente es posible hacer de proxy de otro MCP Server pero no se ha tenido en cuenta (o si, pero en pensamiento 🥲) la escalabilidad de este escenario. Es decir: en este ejemplo se está lanzado el MCP Server definitivo como un proceso único, lo cual implica que cada petición espera al mismo backend, por lo que sin balanceo de carga no hay distribución de las requests y por lo tanto habría un cuello de botella. Existen varias formas de escalar como ya sabes:</p>



<ol class="wp-block-list">
<li>Escalar horizontalmente (multiples instancias de esta implemetación)</li>



<li>Usar Gunicorn con workers</li>



<li>Pool de procesos en backend</li>



<li>Kubernetes/container con réplicas</li>
</ol>



<p>Por otro lado, es importante saber que en general todos los servicios tienen limites en cuanto al número de peticiones que se pueden hacer, normalmente, al minuto usando el mismo PAT por lo que es probable que si estás usando «el mismo para todo» porque no hay alternativa, puedas necesitar múltiples PATs con rotación. </p>



<p>Esta implementación no es la ideal pero en muchos casos a día de hoy es la única medianamente viable.</p>



<h2 class="wp-block-heading">🔧 Adaptar el proxy a otro MCP Server</h2>



<p>Aquí está la clave de todo el patrón.</p>



<p>Usamos <code>StdioTransport</code> (o el transport que necesites) para lanzar el MCP Server real y lo envolvemos con <code>FastMCP.as_proxy</code>:</p>



<pre class="wp-block-code language-python has-small-font-size"><code>from fastmcp import FastMCP
from fastmcp.server.proxy import ProxyClient
from fastmcp.client.transports import StdioTransport

transport = StdioTransport(
    command="npx",                    # o python, node, etc.
    args=&#91;"-y", "@tu-mcp/server"],
    env={
        "API_KEY": "tu-api-key",
        "OTHER_SECRET": "..."
    }
)

proxy = FastMCP.as_proxy(
    ProxyClient(transport),
    name="MiMCPProxy"
)

if __name__ == "__main__":
    proxy.run(transport="http", host="0.0.0.0", port=8080)
</code></pre>



<p>👉 <strong>El MCP Server no necesita modificarse.</strong><br>👉 Las credenciales se quedan en el servidor.<br>👉 Los clientes solo hablan HTTP.</p>



<h2 class="wp-block-heading">🔌 Configuración del cliente MCP</h2>



<h3 class="wp-block-heading">VS Code (<code>mcp.json</code>)</h3>



<pre class="wp-block-code language-javascript has-small-font-size"><code>{
  "servers": {
    "mi-proxy": {
      "type": "http",
      "url": "http://localhost:8080/mcp"
    }
  }
}
</code></pre>



<h3 class="wp-block-heading">Claude Desktop</h3>



<pre class="wp-block-code language-javascript has-small-font-size"><code>{
  "mcpServers": {
    "mi-proxy": {
      "url": "http://localhost:8080/mcp",
      "transport": "http"
    }
  }
}
</code></pre>



<h2 class="wp-block-heading">⚠️ Seguridad: no lo ignores</h2>



<p>Este ejemplo <strong>no incluye autenticación propia</strong> por simplicidad 👉 <strong>No lo expongas tal cual en producción.</strong></p>



<h3 class="wp-block-heading">🔐 Autenticación con Microsoft Entra ID</h3>



<p>FastMCP incluye soporte nativo para autenticación con Azure / Entra ID:</p>



<pre class="wp-block-code language-python has-small-font-size"><code>from fastmcp.server.auth.providers.azure import AzureProvider

auth_provider = AzureProvider(
    client_id="tu-app-client-id",
    client_secret="tu-client-secret",
    tenant_id="tu-tenant-id",
    base_url="http://localhost:8080",
    required_scopes=&#91;"mcp-access"],
)

proxy = FastMCP.as_proxy(
    ProxyClient(transport),
    name="MiProxy",
    auth=auth_provider
)
</code></pre>



<p>Ideal para entornos corporativos con control de acceso real.</p>



<h2 class="wp-block-heading">🧠 Conclusión</h2>



<p>Este patrón <strong>no va solo de <code>stdio</code></strong>.</p>



<p>Va de algo mucho más importante:</p>



<ul class="wp-block-list">
<li>Centralizar <strong>ejecución</strong></li>



<li>Centralizar <strong>credenciales</strong> (PATs, API Keys, tokens)</li>



<li>Evitar configuración local en cada cliente</li>



<li>Hacer MCP <strong>usable de verdad en entornos reales</strong></li>
</ul>



<p>Con este enfoque puedes:</p>



<ul class="wp-block-list">
<li>Exponer MCP Servers como servicios</li>



<li>Compartirlos entre agentes</li>



<li>Proteger secretos</li>



<li>Integrarlos en infraestructuras corporativas</li>
</ul>



<p>¡Nos vemos 👋🏻!</p>
<p>La entrada <a href="https://www.returngis.net/2026/02/%f0%9f%94%8c-mcp-server-como-proxy-de-otro-mcp-server/">🔌 MCP Server como Proxy de otro MCP Server</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.returngis.net/2026/02/%f0%9f%94%8c-mcp-server-como-proxy-de-otro-mcp-server/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://www.returngis.net/wp-content/uploads/2026/02/MCP-Server-como-proxy-de-otro-MCP-Server.mp4" length="33205331" type="video/mp4" />

		<post-id xmlns="com-wordpress:feed-additions:1">31467</post-id>	</item>
	</channel>
</rss>
