<?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>Sun, 07 Jun 2026 12:42:11 +0000</lastBuildDate>
	<language>es</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0</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>100.000 gracias, developers ❤️</title>
		<link>https://www.returngis.net/2026/06/100-000-gracias-developers-%e2%9d%a4%ef%b8%8f/</link>
					<comments>https://www.returngis.net/2026/06/100-000-gracias-developers-%e2%9d%a4%ef%b8%8f/#respond</comments>
		
		<dc:creator><![CDATA[Gisela Torres]]></dc:creator>
		<pubDate>Sat, 06 Jun 2026 13:52:06 +0000</pubDate>
				<category><![CDATA[Off-topic]]></category>
		<guid isPermaLink="false">https://www.returngis.net/?p=32094</guid>

					<description><![CDATA[<p>¡Hola developer 👋! Hoy quiero celebrar algo que todavía me cuesta un poco creer: hemos llegado a los 100.000 suscriptores en YouTube 🥹 Y digo “hemos” porque esto no ha sido algo que haya pasado de la noche a la mañana, ni algo que haya hecho sola. Empecé este camino un 25 de julio de 2023. Desde entonces, prácticamente todos los miércoles —salvo un par... </p>
<p class="more"><a class="more-link" href="https://www.returngis.net/2026/06/100-000-gracias-developers-%e2%9d%a4%ef%b8%8f/">Leer</a></p>
<p>La entrada <a href="https://www.returngis.net/2026/06/100-000-gracias-developers-%e2%9d%a4%ef%b8%8f/">100.000 gracias, developers ❤️</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">¡Hola developer 👋!</p>



<p class="wp-block-paragraph">Hoy quiero celebrar algo que todavía me cuesta un poco creer: <strong>hemos llegado a los 100.000 suscriptores en YouTube</strong> 🥹</p>



<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/06/100k-subs.mp4"></video></figure>



<p class="wp-block-paragraph">Y digo “hemos” porque esto no ha sido algo que haya pasado de la noche a la mañana, ni algo que haya hecho sola.</p>



<p class="wp-block-paragraph">Empecé este camino un <strong>25 de julio de 2023</strong>. Desde entonces, prácticamente todos los miércoles —salvo un par de semanas en todo este tiempo— he publicado un nuevo vídeo sobre temas que me apasionan: desarrollo, tecnología, GitHub, inteligencia artificial, contenedores, productividad… y todo aquello que creo que puede ayudar a otros developers a aprender, mejorar o simplemente atreverse a probar algo nuevo.</p>



<p class="wp-block-paragraph">Y no te voy a engañar: empecé con muchísimos miedos:</p>



<ul class="wp-block-list">
<li>Miedo a no hacerlo bien.</li>



<li>Miedo a que me criticaran.</li>



<li>Miedo a no saber explicarme.</li>



<li>Miedo a que las transiciones no fueran perfectas.</li>



<li>Miedo a que la portada no estuviera a la altura.</li>



<li>Miedo a no tenerlo todo bajo control.</li>
</ul>



<p class="wp-block-paragraph">En resumen: <strong>miedo a no hacerlo perfecto</strong>.</p>



<p class="wp-block-paragraph">Pero después de <strong>casi tres años</strong>, si algo he aprendido es esto:</p>



<p class="wp-block-paragraph"><strong>Hazlo. Aunque no sea perfecto. Aunque no tengas el 100%. Aunque te dé vergüenza. Aunque pienses que todavía no estás listx.</strong></p>



<p class="wp-block-paragraph">Porque muchas veces lo único que realmente nos limita en esta vida somos nosotros mismos.</p>



<p class="wp-block-paragraph">Mirando atrás, me emociona pensar en todos esos vídeos grabados a ratos, muchas veces cansada, muchas veces dudando, muchas veces pensando si merecía la pena seguir. Vídeos que he grabado incluso estando enferma, parando 20 veces para toser, para recuperar la voz o simplemente porque no me encontraba bien, pero aun así intentando cumplir con ese miércoles que ya se había convertido en una promesa conmigo misma y con la gente que estaba al otro lado.</p>



<p class="wp-block-paragraph">Y hoy, al ver ese número, siento que cada miércoles, cada intento, cada error, cada parada y cada aprendizaje han tenido sentido.</p>



<p class="wp-block-paragraph">Pero este logro no es solo mío.</p>



<p class="wp-block-paragraph">Quiero dar las gracias, de corazón, a <a target="_blank" rel="noreferrer noopener" href="https://www.linkedin.com/in/fdangel?miniProfileUrn=urn%3Ali%3Afs_miniProfile%3AACoAAAIMvxsBbXw9CIW8M4iK5OHw0hHj_Dcsrc0">Kiko de Angel Gimeno</a> , mi pareja, porque ha estado ahí desde el minuto uno.</p>



<p class="wp-block-paragraph">Por apoyarme incluso cuando yo no tenía claro hacia dónde iba todo esto. Por ocuparse de mil cosas, de nuestra pequeña, para que yo pudiera grabar. Por aguantar mis 20.000 vueltas a una idea, a un guion, a un vídeo o a una portada. Por animarme, y consolarme, cuando me sentía superada. Por darme espacio para crear, probar, equivocarme y seguir.</p>



<p class="wp-block-paragraph">Cuando tienes una familia, trabajo, responsabilidades y la vida va siempre a mil, encontrar tiempo para construir algo propio es mucho más difícil de lo que parece. Y sentir que tienes a alguien al lado que te apoya, que te ayuda y que cree en ti incluso cuando tú dudas, lo cambia todo.</p>



<p class="wp-block-paragraph">Sin él, muchas veces no habría encontrado ni el tiempo ni la motivación para continuar.</p>



<p class="wp-block-paragraph">Así que estos <strong>100.000 suscriptores</strong> son tan tuyos como míos.</p>



<p class="wp-block-paragraph"><strong>Kiko, te quiero muchísimo. Gracias por todo.</strong></p>



<p class="wp-block-paragraph">Gracias también a cada persona que ha visto un vídeo, ha dejado un comentario, ha compartido contenido, me ha escrito para decirme que algo le ayudó o simplemente ha estado ahí miércoles tras miércoles.</p>



<p class="wp-block-paragraph">De verdad: gracias.</p>



<p class="wp-block-paragraph">Y si tú estás pensando en empezar algo, pero estás esperando a tenerlo perfecto, a sentirte preparada o a que llegue el momento ideal… ojalá esto te sirva como recordatorio:</p>



<p class="wp-block-paragraph"><strong>Empieza. Hazlo con miedo. Hazlo imperfecto. Pero hazlo.</strong></p>



<p class="wp-block-paragraph">Porque a veces, lo que empieza con vergüenza, dudas y una cámara encendida en una habitación cualquiera, acaba convirtiéndose en algo mucho más grande de lo que imaginabas.</p>



<p class="wp-block-paragraph">Gracias, developers ❤️</p>
<p>La entrada <a href="https://www.returngis.net/2026/06/100-000-gracias-developers-%e2%9d%a4%ef%b8%8f/">100.000 gracias, developers ❤️</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.returngis.net/2026/06/100-000-gracias-developers-%e2%9d%a4%ef%b8%8f/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://www.returngis.net/wp-content/uploads/2026/06/100k-subs.mp4" length="42584949" type="video/mp4" />

		<post-id xmlns="com-wordpress:feed-additions:1">32094</post-id>	</item>
		<item>
		<title>Cómo crear un status line personalizado para GitHub Copilot CLI</title>
		<link>https://www.returngis.net/2026/05/como-crear-un-status-line-personalizado-para-github-copilot-cli/</link>
					<comments>https://www.returngis.net/2026/05/como-crear-un-status-line-personalizado-para-github-copilot-cli/#respond</comments>
		
		<dc:creator><![CDATA[Gisela Torres]]></dc:creator>
		<pubDate>Sun, 31 May 2026 07:38:50 +0000</pubDate>
				<category><![CDATA[IA]]></category>
		<category><![CDATA[GitHub Copilot]]></category>
		<category><![CDATA[GitHub Copilot CLI]]></category>
		<guid isPermaLink="false">https://www.returngis.net/?p=32049</guid>

					<description><![CDATA[<p>Aprende a crear un statusline personalizado para GitHub Copilot CLI que muestra en tiempo real la batería, CPU, RAM, temperatura y disco de tu Mac. Con emojis, indicadores de color y cache para rendimiento óptimo.</p>
<p>La entrada <a href="https://www.returngis.net/2026/05/como-crear-un-status-line-personalizado-para-github-copilot-cli/">Cómo crear un status line personalizado para GitHub Copilot CLI</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">¡Hola developer 👋🏻! GitHub Copilot CLI cada vez se está volviendo más personalizable y hoy quiero enseñarte cómo he creado un <strong>status line personalizado</strong> que muestra las métricas del sistema de mi Mac, el tiempo en mi zona y qué estoy escuchando en Spotify directamente en la interfaz del CLI 🤓. </p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.27.56-scaled.png?ssl=1"><img data-recalc-dims="1" fetchpriority="high" decoding="async" width="710" height="506" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.27.56-scaled.png?resize=710%2C506&#038;ssl=1" alt="" class="wp-image-32078" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.27.56-scaled.png?w=1200&amp;ssl=1 1200w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.27.56-scaled.png?resize=300%2C214&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.27.56-scaled.png?resize=1024%2C731&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.27.56-scaled.png?resize=768%2C548&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.27.56-scaled.png?resize=771%2C550&amp;ssl=1 771w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.27.56-scaled.png?resize=1060%2C757&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.27.56-scaled.png?resize=1536%2C1096&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.27.56-scaled.png?resize=2048%2C1462&amp;ssl=1 2048w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.27.56-scaled.png?resize=550%2C393&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.27.56-scaled.png?resize=701%2C500&amp;ssl=1 701w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.27.56-scaled.png?resize=1920%2C1370&amp;ssl=1 1920w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.27.56-scaled.png?resize=1513%2C1080&amp;ssl=1 1513w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<p class="wp-block-paragraph">¡Ah! y también lo he creado para Windows, <a href="#lo-mismo-para-windows" type="internal" id="#lo-mismo-para-windows">¡ver más abajo!</a></p>



<p class="wp-block-paragraph">Si quieres saber más sobre el CLI de este asistente aquí te dejo un vídeo de mi canal de YouTube donde repaso sus características principales:</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/r6bPhx6uaVM?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 class="wp-block-paragraph">Como puedes ver, en la parte inferior estoy mostrando varias métricas que me dicen: si estoy conectada por cable o no, cómo va la CPU, la memoria RAM, espacio en disco y por último el tiempo en mi zona y lo que estoy escuchando en Spotify 😃.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.30.08-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="98" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.30.08-scaled.png?resize=710%2C98&#038;ssl=1" alt="" class="wp-image-32080" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.30.08-scaled.png?w=1200&amp;ssl=1 1200w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.30.08-scaled.png?resize=300%2C42&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.30.08-scaled.png?resize=1024%2C142&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.30.08-scaled.png?resize=768%2C106&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.30.08-scaled.png?resize=1060%2C147&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.30.08-scaled.png?resize=1536%2C213&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.30.08-scaled.png?resize=2048%2C284&amp;ssl=1 2048w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.30.08-scaled.png?resize=550%2C76&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/CleanShot-2026-05-31-at-09.30.08-scaled.png?resize=1920%2C266&amp;ssl=1 1920w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<p class="wp-block-paragraph">En tu caso podría ser esto o cualquier otra cosa que consideres útil. Lo importante en este punto es que sepas qué es lo que espera GitHub Copilot CLI. como prepararlo todo.</p>



<h2 class="wp-block-heading">¿Cómo funciona el statusline de Copilot CLI?</h2>



<p class="wp-block-paragraph">GitHub Copilot CLI tiene una funcionalidad <strong><span style="text-decoration: underline;">experimental</span></strong> que permite ejecutar un script externo y mostrar su salida como <strong>statusline</strong>. En cada render tick, o lo que es lo mismo cada vez que refresca el terminal:</p>



<ol class="wp-block-list">
<li>Ejecuta tu script como un subproceso</li>



<li>Le envía un JSON payload por stdin (con info de la sesión, modelo, tokens…)</li>



<li>Renderiza lo que tu script imprima por stdout</li>
</ol>



<p class="wp-block-paragraph">Esto nos da total libertad para mostrar lo que queramos: el tiempo, música, métricas del sistema… ¡lo que se te ocurra! Lo que si que es importante en este punto es que si no tienes activadas las funcionalidades experimentales debes entrar en una sesión y escribir:</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>/experimental on</code></pre>



<h2 class="wp-block-heading">El script: ~/.copilot/statusline.sh</h2>



<p class="wp-block-paragraph">Como al final el script se me ha quedado un poco largo&#8230; no mucho&#8230; solo 295 líneas ¡pero con muchos comentarios! <a href="https://gist.github.com/0GiS0/645b62ccd33add7aff550d86cf60f95a" target="_blank" rel="noreferrer noopener">lo he dejado en este gist.</a> Así que lo único que tienes que hacer es copiarlo o descargarlo y con él crea el archivo <code>~/.copilot/statusline.sh</code> con el contenido y darle permisos de ejecución (<code>chmod +x</code>):</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>chmod +x <span style="background-color: initial; font-family: inherit; font-size: inherit; text-align: initial; color: initial;">~/.copilot/statusline.sh</span></code></pre>



<p class="wp-block-paragraph">Si te interesa saber cómo se ha conseguido cada métrica puedes seguir por aquí abajo 👇🏻 sino <a href="#configurar-github-copilot-cli" type="internal" id="#configurar-github-copilot-cli">puedes saltar al final </a>para ver cómo es el pegamento entre GitHub Copilot CLI y este nuevo archivo.</p>



<h2 class="wp-block-heading">¿Cómo se monta cada métrica?</h2>



<p class="wp-block-paragraph">Cada sección del statusline recoge una información diferentes de mi máquina:</p>



<h3 class="wp-block-heading"><strong>🔌 / 🔋 Batería</strong> </h3>



<p class="wp-block-paragraph">Detecta si estás en un Mac de sobremesa (muestra «🔌 AC») o en un portátil (muestra el porcentaje con icono de carga ⚡ si está enchufado)</p>



<pre class="wp-block-code language-bash has-small-font-size"><code>
# ── Batería ──────────────────────────────────────────────
# Usa pmset para leer el estado de energía.
# En un Mac de sobremesa (sin batería) muestra "🔌 AC".
# En un portátil muestra el porcentaje + icono de carga.
get_battery() {
  local raw
  raw=$(pmset -g batt 2&gt;/dev/null) || { safe; return; }

  local pct
  pct=$(echo "$raw" | grep -oE '&#91;0-9]+%' | head -1 | tr -d '%')

  # Mac de sobremesa o sin información de batería
  if &#91;&#91; -z "$pct" ]]; then
    if echo "$raw" | grep -qi "AC Power"; then
      echo "🔌 AC"
    else
      safe
    fi
    return
  fi

  # Icono según nivel de batería
  local icon state_icon
  if (( pct &gt;= 50 )); then icon="🔋"
  elif (( pct &gt;= 20 )); then icon="🪫"
  else icon="🪫"
  fi

  # ⚡ si está cargando o conectado a corriente
  if echo "$raw" | grep -qi "charging\|AC Power"; then
    state_icon="⚡"
  else
    state_icon=""
  fi

  echo "${icon} ${pct}%${state_icon:+ ${state_icon}}"
}</code></pre>



<h3 class="wp-block-heading"><strong>⚙️ CPU</strong> </h3>



<p class="wp-block-paragraph">Porcentaje de uso con indicador de color — 🟢 normal, 🟡 medio, 🔴 alto</p>



<pre class="wp-block-code language-bash has-small-font-size"><code># ── Uso de CPU ───────────────────────────────────────────
# `top` en macOS da una primera muestra poco fiable: suele representar
# el uso acumulado desde que arrancó el sistema. Por eso pedimos dos
# muestras (`-l 2`) y nos quedamos con la segunda, que refleja mejor el
# uso actual de CPU.
#
# Tomamos el porcentaje idle y calculamos: uso = 100 - idle.
# Así evitamos depender del formato exacto de user/sys.
#
# Color: 🟢 menos de 50% │ 🟡 50-79% │ 🔴 80% o más
get_cpu() {
  local cpu_line
  cpu_line=$(top -l 2 -n 0 2&gt;/dev/null | grep "CPU usage" | tail -1) || { safe; return; }

  # Extraer el % idle (siempre es el último valor antes de "idle")
  local idle total
  idle=$(echo "$cpu_line" | awk '{for(i=1;i&lt;=NF;i++) if($i=="idle") print $(i-1)}' | tr -d '%')
  &#91;&#91; -z "$idle" ]] &amp;&amp; { safe; return; }

  total=$(awk "BEGIN { t=100-${idle}; if(t&lt;0) t=0; if(t&gt;100) t=100; printf \"%.0f\", t }")

  local icon
  if (( total &gt;= 80 )); then icon="🔴"
  elif (( total &gt;= 50 )); then icon="🟡"
  else icon="🟢"
  fi

  # Ancho fijo (3 caracteres) en el porcentaje: "  9%", " 19%", "100%".
  # Mantiene constante el número de dígitos para que al pasar de "100%"
  # a "19%" no queden dígitos residuales del render anterior.
  printf '⚙️  %3d%% %s' "$total" "$icon"
}</code></pre>



<h3 class="wp-block-heading"><strong>🧠 RAM</strong></h3>



<p class="wp-block-paragraph">Memoria RAM en uso</p>



<pre class="wp-block-code language-bash"><code># ── RAM ──────────────────────────────────────────────────
# vm_stat devuelve páginas de memoria (no bytes directamente).
# Multiplicamos por el tamaño de página (4096 bytes en macOS)
# y sumamos: activa + wired + comprimida = RAM "en uso real".
get_ram() {
  local total_bytes vm page_size stats active wired compressed
  total_bytes=$(sysctl -n hw.memsize 2&gt;/dev/null) || { safe; return; }
  vm=$(vm_stat 2&gt;/dev/null) || { safe; return; }

  page_size=$(echo "$vm" | awk '/page size of/ { for (i=1; i&lt;=NF; i++) if ($i == "of") { print $(i+1); exit } }')
  &#91;&#91; -z "$page_size" ]] &amp;&amp; { safe; return; }

  stats=$(echo "$vm" | awk '
    /Pages active/ { gsub(/\./, "", $NF); active=$NF }
    /Pages wired/ { gsub(/\./, "", $NF); wired=$NF }
    /Pages occupied by compressor/ { gsub(/\./, "", $NF); compressed=$NF }
    END {
      if (active == "") exit 1
      printf "%d %d %d\n", active, wired, compressed
    }
  ') || { safe; return; }

  read -r active wired compressed &lt;&lt;&lt; "$stats"
  &#91;&#91; -z "$active" ]] &amp;&amp; { safe; return; }
  wired=${wired:-0}
  compressed=${compressed:-0}

  # Convertir páginas a GB: (páginas * tamaño_página) / 1024^3
  local used_gb total_gb
  used_gb=$(awk "BEGIN { printf \"%.1f\", (${active} + ${wired} + ${compressed}) * ${page_size} / 1073741824 }")
  total_gb=$(awk "BEGIN { printf \"%.0f\", ${total_bytes} / 1073741824 }")

  echo "🧠  ${used_gb}/${total_gb}GB"
}</code></pre>



<h3 class="wp-block-heading"><strong>💽 Disco</strong></h3>



<p class="wp-block-paragraph">Espacio libre en el disco principal</p>



<pre class="wp-block-code language-bash"><code># ── Disco ────────────────────────────────────────────────
# Espacio libre en el disco principal (/)
get_disk() {
  local avail
  avail=$(df -h / 2&gt;/dev/null | awk 'NR==2 { print $4 }') || { safe; return; }
  &#91;&#91; -z "$avail" ]] &amp;&amp; { safe; return; }

  echo "💽 ${avail} free"
}
</code></pre>



<h3 class="wp-block-heading">☀️<strong> El tiempo meteorológico</strong></h3>



<p class="wp-block-paragraph">Según tu IP el servicio de <a href="https://wttr.in/">wttr.in</a> detecta tu ubicación y te devuelve el resultado el cual puedes modelar.</p>



<pre class="wp-block-code language-bash has-small-font-size"><code># ── Tiempo meteorológico ─────────────────────────────────
# Usa wttr.in para obtener la temperatura de tu ciudad.
# No necesita API keys ni instalar nada — se auto-detecta por IP.
# Se cachea 10 minutos porque el tiempo no cambia tan rápido
# y la petición HTTP es más lenta que leer sensores locales.
get_weather() {
  # Comprobar si hay cache válido del tiempo
  if cache_is_fresh "$WEATHER_CACHE_FILE" "$WEATHER_CACHE_TTL"; then
    cat "$WEATHER_CACHE_FILE"
    return
  fi

  # Consultar wttr.in en una sola petición (más rápido)
  # %c = emoji del tiempo (wttr.in lo elige según condición + día/noche)
  # %t = temperatura, %l = localización
  local raw
  raw=$(curl -s --max-time 3 "wttr.in/?format=%c|%t|%l" 2&gt;/dev/null)
  &#91;&#91; -z "$raw" ]] &amp;&amp; { echo "🌡️ —"; return; }

  local icon temp city
  icon=$(echo "$raw" | cut -d'|' -f1 | tr -d ' ')
  temp=$(echo "$raw" | cut -d'|' -f2)
  city=$(echo "$raw" | cut -d'|' -f3 | sed 's/,.*//' | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')

  &#91;&#91; -z "$temp" ]] &amp;&amp; { echo "🌡️ —"; return; }

  local result="${icon} ${temp} ${city}"

  # Guardar en cache del tiempo
  mkdir -p "$(dirname "$WEATHER_CACHE_FILE")"
  echo "$result" &gt; "$WEATHER_CACHE_FILE"
  echo "$result"
}</code></pre>



<h3 class="wp-block-heading">🎵 <strong>Qué estoy escuchando en Spotify</strong></h3>



<p class="wp-block-paragraph">Usando AppleScript te olvidas de tener que usar la API de Spotify.</p>



<pre class="wp-block-code language-bash has-small-font-size"><code># ── Spotify ──────────────────────────────────────────────
# Definida al principio del script porque se usa en dos sitios:
# 1) En el path cacheado (para mostrar la canción actual en vivo)
# 2) En el path fresco (cuando se recalculan todas las métricas)
# Usa AppleScript (osascript) para consultar Spotify — no necesita
# API keys ni tokens, funciona nativamente en macOS.
get_spotify() {
  # Primero comprobamos si Spotify está corriendo, para no lanzarlo
  # accidentalmente (osascript abriría la app si no hacemos esta comprobación)
  local running
  running=$(osascript -e 'tell application "System Events" to (name of processes) contains "Spotify"' 2&gt;/dev/null)
  &#91;&#91; "$running" != "true" ]] &amp;&amp; return 1

  local info state track artist rest
  info=$(osascript &lt;&lt;'APPLESCRIPT' 2&gt;/dev/null
tell application "Spotify"
  set playbackState to player state as string
  if playbackState is not "playing" and playbackState is not "paused" then return ""
  return playbackState &amp; linefeed &amp; (name of current track) &amp; linefeed &amp; (artist of current track)
end tell
APPLESCRIPT
  ) || return 1

  &#91;&#91; -z "$info" ]] &amp;&amp; return 1
  state=${info%%$'\n'*}
  rest=${info#*$'\n'}
  track=${rest%%$'\n'*}
  artist=${rest#*$'\n'}
  &#91;&#91; "$state" != "playing" &amp;&amp; "$state" != "paused" ]] &amp;&amp; return 1
  &#91;&#91; -z "$track" ]] &amp;&amp; return 1

  # Truncar nombres largos para que el statusline quepa en pantalla
  (( ${#track} &gt; 25 )) &amp;&amp; track="${track:0:22}..."
  (( ${#artist} &gt; 20 )) &amp;&amp; artist="${artist:0:17}..."

  # 🎵 si está sonando, ⏸️ si está en pausa
  local icon="🎵"
  &#91;&#91; "$state" == "paused" ]] &amp;&amp; icon="⏸️"

  echo "${icon} ${track} – ${artist}"
}

add_live_spotify() {
  local line="$1" spotify
  spotify=$(get_spotify 2&gt;/dev/null)
  &#91;&#91; -n "$spotify" ]] &amp;&amp; line="${line} │ ${spotify}"
  echo "$line"
}</code></pre>



<h2 class="wp-block-heading">El truco del cache</h2>



<p class="wp-block-paragraph" id="configurar-">Copilot CLI ejecuta el script en cada render tick. Si cada ejecución tardara 10-15 segundos la experiencia sería horrible. Por eso el script implementa un <strong>sistema de cache</strong>:</p>



<ul class="wp-block-list">
<li>Guarda el resultado en <code>~/.copilot/.system-statusline.cache</code></li>



<li>Si el cache tiene menos de 30 segundos, devuelve el valor cacheado en lugar de recalcular</li>



<li>El TTL es configurable con la variable de entorno <code>SYSTEM_CACHE_TTL</code></li>
</ul>



<h2 id="configurar-github-copilot-cli" class="wp-block-heading">Configurar GitHub Copilot CLI</h2>



<p class="wp-block-paragraph">Con el script ya en su sitio lo último que te queda por hacer es editar tu archivo <code>~/.copilot/settings.json</code> y añade la configuración del statusline. Desde aquí también puedes habilitar el modo experimental:</p>



<pre class="wp-block-code language-json has-small-font-size"><code>{
  "experimental": true,
  "statusLine": {
    "type": "command",
    "command": "~/.copilot/statusline.sh",
    "padding": 1
  },
  "footer": {
    "showCustom": true
  }


}</code></pre>



<p class="wp-block-paragraph">Después, reinicia Copilot CLI con <code>/restart</code> o abre una nueva sesión. ¡Y listo! Verás las métricas de tu sistema, el tiempo y tu música directamente en la barra inferior.</p>



<h2 id="lo-mismo-para-windows" class="wp-block-heading">Lo mismo pero para Windows</h2>



<p class="wp-block-paragraph">Si tu sistema operativo es Windows, <a href="https://gist.github.com/0GiS0/bb1caccc261fd263111c11868af23f58" target="_blank" rel="noreferrer noopener">te dejo también este gist donde puedes ver la versión compatible para PowerShell</a>. Y así es como se ve:</p>



<figure class="wp-block-image size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/GitHub-Copilot-CLI-Statusline-Windows-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="455" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/GitHub-Copilot-CLI-Statusline-Windows.png?resize=710%2C455&#038;ssl=1" alt="" class="wp-image-32089" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/GitHub-Copilot-CLI-Statusline-Windows-scaled.png?resize=1024%2C656&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/GitHub-Copilot-CLI-Statusline-Windows-scaled.png?resize=300%2C192&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/GitHub-Copilot-CLI-Statusline-Windows-scaled.png?resize=768%2C492&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/GitHub-Copilot-CLI-Statusline-Windows-scaled.png?resize=858%2C550&amp;ssl=1 858w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/GitHub-Copilot-CLI-Statusline-Windows-scaled.png?resize=1060%2C680&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/GitHub-Copilot-CLI-Statusline-Windows-scaled.png?resize=1536%2C985&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/GitHub-Copilot-CLI-Statusline-Windows-scaled.png?resize=550%2C353&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/GitHub-Copilot-CLI-Statusline-Windows-scaled.png?resize=780%2C500&amp;ssl=1 780w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/GitHub-Copilot-CLI-Statusline-Windows-scaled.png?resize=1685%2C1080&amp;ssl=1 1685w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/05/GitHub-Copilot-CLI-Statusline-Windows-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>



<p class="wp-block-paragraph">¡Nos vemos 👋🏻!</p>
<p>La entrada <a href="https://www.returngis.net/2026/05/como-crear-un-status-line-personalizado-para-github-copilot-cli/">Cómo crear un status line personalizado para GitHub Copilot CLI</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.returngis.net/2026/05/como-crear-un-status-line-personalizado-para-github-copilot-cli/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">32049</post-id>	</item>
		<item>
		<title>VS Code Tunnels sin instalar VS Code: accede a tu máquina remota desde el navegador</title>
		<link>https://www.returngis.net/2026/05/vs-code-tunnels-sin-instalar-vs-code-accede-a-tu-maquina-remota-desde-el-navegador/</link>
					<comments>https://www.returngis.net/2026/05/vs-code-tunnels-sin-instalar-vs-code-accede-a-tu-maquina-remota-desde-el-navegador/#respond</comments>
		
		<dc:creator><![CDATA[Gisela Torres]]></dc:creator>
		<pubDate>Tue, 05 May 2026 11:34:42 +0000</pubDate>
				<category><![CDATA[Cloud & DevOps]]></category>
		<category><![CDATA[Visual Studio Code]]></category>
		<guid isPermaLink="false">https://www.returngis.net/?p=31934</guid>

					<description><![CDATA[<p>¡Hola developer 👋🏻! La semana pasada te compartí en mi canal de YouTube cómo puedes usar Visual Studio Code conectándote de forma remota desde la web o incluso desde otro Visual Studio Code. Esto es súper útil cuando estás trabajando con diferentes entornos y no quieres estar cambiando constantemente de ventana, o simplemente cuando necesitas acceder a esa máquina y no tienes la físicamente delante.... </p>
<p class="more"><a class="more-link" href="https://www.returngis.net/2026/05/vs-code-tunnels-sin-instalar-vs-code-accede-a-tu-maquina-remota-desde-el-navegador/">Leer</a></p>
<p>La entrada <a href="https://www.returngis.net/2026/05/vs-code-tunnels-sin-instalar-vs-code-accede-a-tu-maquina-remota-desde-el-navegador/">VS Code Tunnels sin instalar VS Code: accede a tu máquina remota desde el navegador</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">¡Hola developer 👋🏻!</p>



<p class="wp-block-paragraph">La semana pasada te compartí en mi canal de YouTube cómo puedes usar Visual Studio Code conectándote de forma remota desde la web o incluso desde otro Visual Studio Code.</p>



<p class="wp-block-paragraph">Esto es súper útil cuando estás trabajando con diferentes entornos y no quieres estar cambiando constantemente de ventana, o simplemente cuando necesitas acceder a esa máquina y no tienes la físicamente delante.</p>



<p class="wp-block-paragraph">Te dejo por aquí el vídeo por si quieres verlo 👇🏻</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/niuJpAKyb7c?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 class="wp-block-paragraph">Pero hoy quería enseñarte otra posibilidad bastante interesante: <strong>cómo crear un túnel usando la CLI de Visual Studio Code <span style="text-decoration: underline;">sin necesidad de tener Visual Studio Code instalado en la máquina destino</span></strong> 🤯</p>



<p class="wp-block-paragraph">La idea es poder acceder desde el navegador, u otra máquina con VS Code instalado, a un entorno remoto donde estás haciendo algún desarrollo, pero esa máquina no tiene interfaz gráfica o no tiene expuesto directamente el puerto de la aplicación.</p>



<p class="wp-block-paragraph">Esto me ha pasado recientemente con una máquina Ubuntu donde estaba haciendo pruebas con GitHub Copilot CLI —pero eso da para otro vídeo/artículo 😅— y necesitaba revisar de forma sencilla el contenido del proyecto. Además, quería acceder a uno de los puertos donde la aplicación estaba escuchando para ver el resultado en el navegador, pero ese puerto no estaba abierto públicamente en la máquina, y tampoco quería exponerlo de forma insegura.</p>



<p class="wp-block-paragraph">Así que en este artículo te muestro cómo instalar solamente la CLI de Visual Studio Code y abrir un túnel para acceder a tu máquina de forma remota a través de Visual Studio Code.</p>



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



<p class="wp-block-paragraph">Para seguir este ejemplo necesitas:</p>



<ul class="wp-block-list">
<li>Una máquina Linux con <code>curl</code> y <code>tar</code></li>



<li>Una cuenta de GitHub para autenticar el túnel</li>
</ul>



<h2 class="wp-block-heading">Instalación de la CLI de Visual Studio Code</h2>



<p class="wp-block-paragraph">Lo primero que vamos a hacer es descargar la CLI de Visual Studio Code en la máquina remota.</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>mkdir -p ~/.local/bin

TMP_DIR=$(mktemp -d)

# Descarga de la CLI de Visual Studio Code
# Algunas distribuciones pueden requerir el paquete "alpine"

curl -fsSL "https://code.visualstudio.com/sha/download?build=stable&amp;os=cli-linux-x64" \
  -o "$TMP_DIR/vscode-cli.tar.gz" || \
curl -fsSL "https://code.visualstudio.com/sha/download?build=stable&amp;os=cli-alpine-x64" \
  -o "$TMP_DIR/vscode-cli.tar.gz"

tar -xzf "$TMP_DIR/vscode-cli.tar.gz" -C "$TMP_DIR"

install -m 755 "$(find "$TMP_DIR" -maxdepth 3 -type f -name code | head -n 1)" \
  "$HOME/.local/bin/code"

~/.local/bin/code --version</code></pre>



<p class="wp-block-paragraph">Con esto ya deberías tener disponible el comando <code>code</code> en tu máquina destino 🎉</p>



<h2 class="wp-block-heading"><strong>Crear y autenticar el túnel</strong></h2>



<p class="wp-block-paragraph">Una vez instalada la CLI, podemos crear el túnel con el siguiente comando:</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>~/.local/bin/code tunnel \
  --accept-server-license-terms \
  --telemetry-level off \
  --name returngis-web</code></pre>



<p class="wp-block-paragraph">Al ejecutarlo, Visual Studio Code te mostrará un device code, para hacer la autenticación, y la URL a la que debes acceder para asociar el nuevo túnel a tu cuenta de GitHub.</p>



<h2 class="wp-block-heading">Verificar el estado del túnel</h2>



<p class="wp-block-paragraph">Cuando el túnel esté creado, puedes comprobar su estado ejecutando:</p>



<pre class="wp-block-code language-shell has-small-font-size"><code>~/.local/bin/code tunnel status</code></pre>



<p class="wp-block-paragraph">Si todo ha ido bien, ya podrás acceder a tu máquina remota desde el navegador, o un VS Code, usando una URL como esta: https://vscode.dev/tunnel/returngis-web</p>



<p class="wp-block-paragraph">En este caso, <code>returngis-web</code> es el nombre que le hemos dado al túnel con el parámetro <code>--name</code>.</p>



<h2 class="wp-block-heading">¿Y por qué mola esto?</h2>



<p class="wp-block-paragraph">Lo interesante de este enfoque no es solamente que puedas abrir un túnel para acceder al desarrollo que estás ejecutando en una máquina remota.</p>



<p class="wp-block-paragraph">Lo potente es que, además, tienes un Visual Studio Code completo en el navegador conectado a esa máquina.</p>



<p class="wp-block-paragraph">Desde ahí puedes revisar el código, abrir terminales, inspeccionar archivos y trabajar sobre ese entorno sin tener que exponer puertos públicamente ni instalar una interfaz gráfica en la máquina destino.</p>



<p class="wp-block-paragraph">Para escenarios donde estás probando cosas en servidores Linux, máquinas temporales, entornos cloud o incluso laboratorios con herramientas como GitHub Copilot CLI, esto puede ahorrarte tiempo.</p>



<p class="wp-block-paragraph">¡Nos vemos 👋🏻!</p>
<p>La entrada <a href="https://www.returngis.net/2026/05/vs-code-tunnels-sin-instalar-vs-code-accede-a-tu-maquina-remota-desde-el-navegador/">VS Code Tunnels sin instalar VS Code: accede a tu máquina remota desde el navegador</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.returngis.net/2026/05/vs-code-tunnels-sin-instalar-vs-code-accede-a-tu-maquina-remota-desde-el-navegador/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">31934</post-id>	</item>
		<item>
		<title>Control de calidad en Agentic DevOps: automatiza linting y formateo con hooks</title>
		<link>https://www.returngis.net/2026/04/agent-hooks-github-copilot-lint-formato-automatico/</link>
					<comments>https://www.returngis.net/2026/04/agent-hooks-github-copilot-lint-formato-automatico/#respond</comments>
		
		<dc:creator><![CDATA[Gisela Torres]]></dc:creator>
		<pubDate>Tue, 28 Apr 2026 11:49:18 +0000</pubDate>
				<category><![CDATA[IA]]></category>
		<category><![CDATA[Agent Hooks]]></category>
		<category><![CDATA[GitHub Copilot]]></category>
		<category><![CDATA[Inteligencia Artificial]]></category>
		<guid isPermaLink="false">https://www.returngis.net/?p=31907</guid>

					<description><![CDATA[<p>Los agent hooks de GitHub Copilot permiten ejecutar scripts de shell antes y después de que el agente use cualquier herramienta. En este artículo vemos cómo configurarlos para que el agente lance ESLint antes de escribir y Prettier después, de forma completamente automática.</p>
<p>La entrada <a href="https://www.returngis.net/2026/04/agent-hooks-github-copilot-lint-formato-automatico/">Control de calidad en Agentic DevOps: automatiza linting y formateo con hooks</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">¡Hola developer 👋🏻!</p>



<p class="wp-block-paragraph">Cuando trabajas con GitHub Copilot en modo agente, el agente puede leer y escribir archivos, ejecutar comandos y tomar decisiones de forma autónoma.</p>



<p class="wp-block-paragraph">Y eso está genial… hasta que te haces la pregunta incómoda:</p>



<p class="wp-block-paragraph">👉 ¿Qué pasa con la calidad del código que genera?<br>👉 ¿Y con el formato?</p>



<p class="wp-block-paragraph">Aquí es donde entran en juego los <strong><a href="https://code.visualstudio.com/docs/copilot/customization/hooks" target="_blank" rel="noreferrer noopener">Agent hooks</a></strong>.</p>



<p class="wp-block-paragraph">Estos no son más que «pequeños scripts» que se ejecutan automáticamente en momentos clave del ciclo de vida del agente, sin que tengas que pedírselo.<br>En este artículo te comparto dos que estoy usando muchísimo en proyectos JavaScript/TypeScript… y que ya se han vuelto imprescindibles en mi flujo de trabajo.</p>



<p class="wp-block-paragraph">Si quieres verlos en acción, aquí tienes el vídeo completo 👇🏻</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/15TUwdUgNUc?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">¿Qué son los agent hooks?</h2>



<p class="wp-block-paragraph">Los hooks son scripts que se ejecutan en distintos momentos del ciclo de vida de una sesión de agente.</p>



<p class="wp-block-paragraph">Aunque hay varios eventos disponibles, los que más estoy utilizando son:</p>



<ul class="wp-block-list">
<li><strong>preToolUse</strong> → justo antes de que el agente use una herramienta (por ejemplo, antes de modificar un archivo)</li>



<li><strong>postToolUse</strong> → justo después de que la use</li>
</ul>



<p class="wp-block-paragraph">Se configuran en un archivo <code>.github/hooks/hooks.json</code> dentro del repositorio:</p>



<pre class="wp-block-code language-json has-small-font-size"><code>{
  "version": 1,
  "hooks": {
    "preToolUse": &#91;
      {
        "type": "command",
        "bash": ".github/hooks/scripts/lint.sh"
      }
    ],
    "postToolUse": &#91;
      {
        "type": "command",
        "bash": ".github/hooks/scripts/format.sh"
      }
    ]
  }
}</code></pre>



<p class="wp-block-paragraph">El nombre del JSON puede ser diferente a hooks.json y dentro de este cada evento es un array por lo que podrías tener más de un script asociado a ese momento.</p>



<h2 class="wp-block-heading">Hook de lint (preToolUse)</h2>



<p class="wp-block-paragraph">El hook <code>lint.sh</code> se ejecuta antes de que el agente escriba o modifique un archivo. En este ejemplo, su función es pasar ESLint sobre el archivo en cuestión y, si hay errores, <strong>bloquear la operación</strong> devolviendo una decisión de <code>deny</code>.</p>



<p class="wp-block-paragraph">Los hooks reciben un JSON por stdin con información sobre la herramienta que se va a usar y sus parámetros. El script extrae el nombre de la herramienta y la ruta del archivo, y solo actúa en herramientas de escritura (<code>replace_string_in_file</code>, <code>create_file</code>, etc.):</p>



<pre class="wp-block-code language-bash has-small-font-size"><code>#!/bin/bash
# ─────────────────────────────────────────────────────────────
# 🔍 LINT — Verifica la calidad del código antes de escribir
# 📌 Se ejecuta en: preToolUse (antes de que el agente
#    escriba o modifique un archivo)
# 🛠️ Herramienta: ESLint
# ─────────────────────────────────────────────────────────────

# ── Helpers para responder al hook ──────────────────────────

allow() {
  jq -n --arg reason "$1" '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "allow",
      permissionDecisionReason: $reason
    }
  }'
  exit 0
}

deny() {
  jq -n --arg reason "$1" --arg context "$2" '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: $reason,
      additionalContext: $context
    }
  }'
  exit 1
}

# ── Lógica principal ────────────────────────────────────────

INPUT=$(cat)

TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
FILE=$(echo "$INPUT" | jq -r '.tool_input.filePath // empty')

# Solo lint en herramientas que escriben archivos
&#91;&#91; "$TOOL_NAME" =~ ^(replace_string_in_file|multi_replace_string_in_file|create_file|run_in_terminal|run_task)$ ]] \
  || allow "⏭️ Read-only tool, no lint needed"

# Necesitamos una ruta de archivo
&#91; -z "$FILE" ] &amp;&amp; allow "⏭️ No filePath in input"

# Solo lint en archivos JS/TS/Astro
EXT="${FILE##*.}"
&#91;&#91; "$EXT" =~ ^(js|jsx|ts|tsx|astro)$ ]] || allow "⏭️ No es un archivo JS/TS/Astro"

# El archivo debe existir
&#91; ! -f "$FILE" ] &amp;&amp; deny "⚠️ File not found: $FILE" ""

# Verificar que eslint está disponible
command -v npx &amp;> /dev/null || allow "⚠️ npx no disponible — saltando lint"

# Ejecutar ESLint
if LINT_OUTPUT=$(npx eslint "$FILE" 2>&amp;1); then
  allow "✅ Linting passed for $FILE"
else
  deny "❌ Linting failed for $FILE" "$LINT_OUTPUT"
fi
</code></pre>



<p class="wp-block-paragraph">Cuando el hook devuelve <code>deny</code>, el agente recibe el motivo y el contexto adicional (la salida de ESLint), lo que le permite corregir el código antes de volver a intentar la escritura.</p>



<h2 class="wp-block-heading">Hook de formato (postToolUse)</h2>



<p class="wp-block-paragraph">El hook <code>format.sh</code> se ejecuta después de cada escritura. Llama a <a href="https://prettier.io/" target="_blank" rel="noreferrer noopener">Prettier</a> sobre el archivo modificado para garantizar que el formato sea siempre consistente, independientemente de lo que haya generado el agente:</p>



<pre class="wp-block-code language-bash has-small-font-size"><code>#!/bin/bash
# ─────────────────────────────────────────────────────────────
# ✨ FORMAT — Formatea el código automáticamente después de
#    cada cambio
# 📌 Se ejecuta en: postToolUse (después de que el agente
#    escribe o modifica un archivo)
# 🛠️ Herramienta: Prettier
# ─────────────────────────────────────────────────────────────

INPUT=$(cat)

TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')

# Solo formatear después de herramientas que escriben archivos
if ! &#91;&#91; "$TOOL_NAME" =~ ^(replace_string_in_file|multi_replace_string_in_file|create_file|run_in_terminal|run_task)$ ]]; then
  echo "&#91;format.sh] ⏭️  Skipped (read-only tool): $TOOL_NAME"
  exit 0
fi

# Extraer rutas de archivo según el tipo de herramienta
if &#91;&#91; "$TOOL_NAME" == "multi_replace_string_in_file" ]]; then
  FILES=$(echo "$INPUT" | jq -r '.tool_input.replacements&#91;].filePath // empty' | sort -u)
else
  FILES=$(echo "$INPUT" | jq -r '.tool_input.filePath // empty')
fi

if &#91; -z "$FILES" ]; then
  echo "&#91;format.sh] ⚠️  No filePath found in input"
  exit 0
fi

# Formatear cada archivo
while IFS= read -r FILE; do
  &#91; -z "$FILE" ] &amp;&amp; continue

  # Solo formatear archivos del proyecto web
  EXT="${FILE##*.}"
  if ! &#91;&#91; "$EXT" =~ ^(js|jsx|ts|tsx|astro|css|html|json|md)$ ]]; then
    echo "&#91;format.sh] ⏭️  No es un archivo formateable: $FILE"
    continue
  fi

  # Verificar que prettier está disponible
  if ! command -v npx &amp;>/dev/null; then
    echo "&#91;format.sh] ⚠️  npx no disponible — saltando format"
    exit 0
  fi

  # El archivo debe existir
  if &#91; ! -f "$FILE" ]; then
    echo "&#91;format.sh] ⚠️  File not found: $FILE"
    continue
  fi

  # Ejecutar Prettier
  echo "&#91;format.sh] 📝 Formatting: $FILE"

  BEFORE=$(stat -f%z "$FILE" 2>/dev/null || stat -c%s "$FILE" 2>/dev/null)
  FORMAT_OUTPUT=$(npx prettier --write "$FILE" 2>&amp;1)
  EXIT_CODE=$?
  AFTER=$(stat -f%z "$FILE" 2>/dev/null || stat -c%s "$FILE" 2>/dev/null)

  if &#91; $EXIT_CODE -ne 0 ]; then
    echo "&#91;format.sh] ❌ Formatter error: $FORMAT_OUTPUT"
  elif &#91; "$BEFORE" = "$AFTER" ]; then
    echo "&#91;format.sh] ✅ No changes needed"
  else
    echo "&#91;format.sh] ✨ File formatted successfully"
  fi
done &lt;&lt;&lt; "$FILES"</code></pre>



<p class="wp-block-paragraph">Como puedes ver en este caso lo primero que revisa es cuál es la tool que se ha ejecutado con el objetivo de solo lanzar prettier cuando tiene sentido, es decir cuando hay nuevo código que debe ser formateado.</p>



<h2 class="wp-block-heading">Resultado en la práctica</h2>



<p class="wp-block-paragraph">Con estos dos hooks activos, el flujo de trabajo con el agente queda así:</p>



<ol class="wp-block-list">
<li>Le pido al agente hacer una implementación</li>



<li>El agente decide modificar un archivo</li>



<li><strong>preToolUse</strong> → ESLint analiza los cambios; si hay errores, el agente los corrige antes de escribir</li>



<li>El agente escribe el archivo</li>



<li><strong>postToolUse</strong> → Prettier formatea el resultado automáticamente</li>
</ol>



<p class="wp-block-paragraph">Todo esto ocurre de forma transparente, sin interrumpir el flujo del agente ni requerir intervención manual. Aunque en el vídeo te muestro otros ejemplos donde si puedes interactuar con el hook a través del chat.</p>



<h2 class="wp-block-heading">Una limitación importante</h2>



<p class="wp-block-paragraph">Los hooks solo se disparan cuando <strong>el agente</strong> usa una herramienta. Si tú editas un archivo directamente desde VS Code o la terminal, los hooks no se ejecutan. Para ese caso, lo mejor es configurar Prettier como formateador por defecto en VS Code con <code>editor.formatOnSave: true</code>.</p>



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



<p class="wp-block-paragraph">Los agent hooks son una herramienta sencilla pero muy potente para mantener la calidad del código cuando trabajas con GitHub Copilot en modo agente. Con unas pocas líneas de bash puedes garantizar que todo lo que el agente toque cumpla con tus estándares de calidad, de forma automática y sin fricción. Pero hay muchos otros casos de uso que puedes ver de forma rápida <a href="https://code.visualstudio.com/docs/copilot/customization/hooks#_hook-lifecycle-events">en esta tabla de aquí</a>.</p>



<p class="wp-block-paragraph">¡Nos vemos 👋🏻!</p>



<p class="wp-block-paragraph"></p>
<p>La entrada <a href="https://www.returngis.net/2026/04/agent-hooks-github-copilot-lint-formato-automatico/">Control de calidad en Agentic DevOps: automatiza linting y formateo con hooks</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/agent-hooks-github-copilot-lint-formato-automatico/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">31907</post-id>	</item>
		<item>
		<title>Seguridad en GitHub Pages con CSP</title>
		<link>https://www.returngis.net/2026/04/github-pages-seguridad-csp/</link>
					<comments>https://www.returngis.net/2026/04/github-pages-seguridad-csp/#respond</comments>
		
		<dc:creator><![CDATA[Gisela Torres]]></dc:creator>
		<pubDate>Thu, 16 Apr 2026 07:47:30 +0000</pubDate>
				<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[Content Security Policy]]></category>
		<category><![CDATA[CSP]]></category>
		<category><![CDATA[GitHub Pages]]></category>
		<category><![CDATA[Seguridad]]></category>
		<guid isPermaLink="false">https://www.returngis.net/?p=31883</guid>

					<description><![CDATA[<p>GitHub Pages es una de las opciones más populares para alojar sitios estáticos de forma gratuita. Lo puedes usar para documentación, portfolios, landing pages e incluso para aplicaciones web completas generadas con frameworks como Astro, Next.js o Hugo. Últimamente, además, estoy haciendo bastante uso de GitHub Pages para side projects o demos en las que simplemente necesito servir contenido estático sin complicarme demasiado. Y es... </p>
<p class="more"><a class="more-link" href="https://www.returngis.net/2026/04/github-pages-seguridad-csp/">Leer</a></p>
<p>La entrada <a href="https://www.returngis.net/2026/04/github-pages-seguridad-csp/">Seguridad en GitHub Pages con CSP</a> se publicó primero en <a href="https://www.returngis.net">return(GiS);</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">GitHub Pages es una de las opciones más populares para alojar sitios estáticos de forma gratuita. Lo puedes usar para documentación, portfolios, landing pages e incluso para aplicaciones web completas generadas con frameworks como Astro, Next.js o Hugo.</p>



<p class="wp-block-paragraph"><strong>Últimamente, además, estoy haciendo bastante uso de GitHub Pages para side projects</strong> o demos en las que simplemente necesito servir contenido estático <strong>sin complicarme demasiado</strong>. Y es que ya te conté en otro artículo que incluso puedes tener <strong>repositorios privados que desplieguen en GitHub Pages y que ese contenido sea público</strong>… algo que, sinceramente, <strong>me está resultando extremadamente útil en mi día a día</strong>.</p>



<p class="wp-block-paragraph">Pero claro… aquí viene la parte que no deberíamos pasar por alto 👀</p>



<p class="wp-block-paragraph">Porque aunque sea “solo estático”, <strong>seguimos exponiendo una aplicación web al mundo</strong>. Y eso implica que <strong>la seguridad sigue siendo importante</strong>.</p>



<p class="wp-block-paragraph">En este artículo te voy a contar qué puedes (y qué no puedes) hacer para <strong>mejorar la seguridad de un sitio alojado en GitHub Pages</strong>, centrándome en una de las herramientas más potentes que tenemos a nuestro alcance: <strong><a href="https://developer.mozilla.org/es/docs/Web/HTTP/Guides/CSP" target="_blank" rel="noreferrer noopener">Content Security Policy (CSP)</a></strong>.</p>



<h2 class="wp-block-heading">¿Qué es GitHub Pages y cuáles son sus limitaciones?</h2>



<p class="wp-block-paragraph">GitHub Pages es un servicio de hosting estático que sirve archivos HTML, CSS y JavaScript directamente desde un repositorio de GitHub. No hay servidor backend, no hay base de datos, no hay procesamiento del lado del servidor. Solo archivos estáticos.</p>



<p class="wp-block-paragraph">Esto tiene muchas ventajas (rapidez, simplicidad, coste cero), pero también una limitación importante para la seguridad: <strong>no puedes configurar headers HTTP personalizados</strong>. Y los headers HTTP son el mecanismo principal para aplicar políticas de seguridad en la web.</p>



<p class="wp-block-paragraph">Dicho esto, GitHub Pages sí aplica algunas protecciones por defecto:</p>



<ul class="wp-block-list">
<li><strong>HTTPS forzado</strong> con <code>Strict-Transport-Security</code></li>



<li>Certificados TLS gestionados automáticamente</li>



<li>Infraestructura de CDN global</li>
</ul>



<p class="wp-block-paragraph">Pero hay muchas cosas que <strong>no</strong> puedes controlar: <code>X-Frame-Options</code>, <code>Permissions-Policy</code>, o el propio <code>Content-Security-Policy</code> como header. Para eso tenemos un plan B.</p>



<h2 class="wp-block-heading">¿Qué es Content Security Policy (CSP)?</h2>



<p class="wp-block-paragraph">CSP es un estándar de seguridad web que permite definir <strong>qué recursos puede cargar tu página</strong>. Funciona como una lista blanca: solo se ejecutan scripts, se cargan estilos, fuentes o se hacen peticiones de red a los orígenes que tú autorices explícitamente.</p>



<p class="wp-block-paragraph"><strong>La forma habitual de aplicar CSP es mediante un header HTTP</strong>:</p>



<pre class="wp-block-code language-html has-small-font-size"><code>Content-Security-Policy: default-src 'self'; script-src 'self';</code></pre>



<p class="wp-block-paragraph">Pero como en GitHub Pages no podemos configurar headers, usamos la alternativa: una <strong>etiqueta <code>&lt;meta&gt;</code></strong> en el <code>&lt;head&gt;</code> del HTML:</p>



<pre class="wp-block-code language-html has-small-font-size"><code>&lt;meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self' 'unsafe-inline';"
/&gt;</code></pre>



<p class="wp-block-paragraph">El navegador lee esta etiqueta y aplica las mismas restricciones que si viniera como header. No importa si tu hosting es GitHub Pages o cualquier otro. <strong>Es el navegador quien lo interpreta</strong>.</p>



<h2 class="wp-block-heading">¿De qué te protege CSP?</h2>



<p class="wp-block-paragraph">El principal ataque que mitiga CSP es <strong>Cross-Site Scripting (XSS)</strong>. Si un atacante consigue inyectar un <code>&lt;script src="https://malicious.com/steal.js"&gt;</code> en tu página, el navegador lo bloqueará porque <code>malicious.com</code> no está en tu lista de orígenes permitidos.</p>



<p class="wp-block-paragraph">Además, CSP ayuda contra:</p>



<ul class="wp-block-list">
<li><strong>Exfiltración de datos</strong>: un script malicioso no puede enviar datos a servidores no autorizados si <code>connect-src</code> está restringido.</li>



<li><strong>Inyección de estilos</strong>: con <code>style-src</code> puedes evitar que se carguen CSS externos maliciosos.</li>



<li><strong>Carga de recursos no deseados</strong>: controlas de dónde vienen las imágenes (<code>img-src</code>), las fuentes (<code>font-src</code>) y todo lo demás.</li>
</ul>



<h2 class="wp-block-heading">Ejemplo práctico: un sitio Astro en GitHub Pages</h2>



<p class="wp-block-paragraph">Recientemente construí una aplicación de encuestas anónimas hecha con <strong>Astro + React</strong>, desplegada en GitHub Pages y estas son las decisiones de seguridad que tomé:</p>



<h3 class="wp-block-heading">La política CSP que apliqué</h3>



<pre class="wp-block-code language-html has-small-font-size"><code>&lt;meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self';
    script-src 'self' 'unsafe-inline';
    style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
    font-src https://fonts.gstatic.com;
    img-src 'self' data:;
    connect-src 'self' https://api.web3forms.com https://api.fpjs.io;
    base-uri 'self';
    form-action 'none';"
/&gt;</code></pre>



<p class="wp-block-paragraph">Si echamos un vistazo a cada una de las directivas:</p>



<figure class="wp-block-table"><table><thead><tr><th>Directiva</th><th>Qué permite</th><th>Por qué</th></tr></thead><tbody><tr><td><code>default-src 'self'</code></td><td>Solo recursos del propio dominio</td><td>Política restrictiva por defecto</td></tr><tr><td><code>script-src 'self' 'unsafe-inline'</code></td><td>Scripts propios + inline</td><td>Astro/React necesitan inline scripts para la hidratación</td></tr><tr><td><code>style-src 'self' 'unsafe-inline' fonts.googleapis.com</code></td><td>Estilos propios, inline y Google Fonts</td><td>Tailwind genera estilos inline; usamos Google Fonts</td></tr><tr><td><code>font-src fonts.gstatic.com</code></td><td>Fuentes de Google</td><td>Las fuentes se sirven desde gstatic</td></tr><tr><td><code>img-src 'self' data:</code></td><td>Imágenes propias y data URIs</td><td>Los SVG inline usan data:</td></tr><tr><td><code>connect-src 'self' api.web3forms.com api.fpjs.io</code></td><td>Fetch/XHR a servicios autorizados</td><td>Web3Forms para emails, FingerprintJS para deduplicación</td></tr><tr><td><code>base-uri 'self'</code></td><td>Solo base URI propia</td><td>Evita ataques que cambien el &lt;base&gt; del HTML</td></tr><tr><td><code>form-action 'none'</code></td><td>Bloquea envío de forms HTML</td><td>Usamos fetch(), no formularios nativos</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Otras meta etiquetas de seguridad</h3>



<p class="wp-block-paragraph">Además del CSP, añadí estas dos:</p>



<pre class="wp-block-code language-html has-small-font-size"><code>&lt;!-- Evita enviar el referrer a sitios externos --&gt;
&lt;meta name="referrer" content="no-referrer" /&gt;

&lt;!-- Evita que el navegador adivine el MIME type --&gt;
&lt;meta http-equiv="X-Content-Type-Options" content="nosniff" /&gt;</code></pre>



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



<p class="wp-block-paragraph">Para comprobar que esto que hemos configurado tiene su efecto puedes abrir las DevTools de tu navegador y en la sección de consola podrías intentar añadir algo como esto:</p>



<pre class="wp-block-code language-javascript has-small-font-size"><code>const s = document.createElement('script');
s.src = 'https://evil.example.com/test.js';
document.head.appendChild(s);</code></pre>



<p class="wp-block-paragraph">Si está bien configurado debería de devolver algo como lo siguiente:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/Probando-configuracion-CSP-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="250" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/Probando-configuracion-CSP.png?resize=710%2C250&#038;ssl=1" alt="" class="wp-image-31892" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/Probando-configuracion-CSP-scaled.png?resize=1024%2C361&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/Probando-configuracion-CSP-scaled.png?resize=300%2C106&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/Probando-configuracion-CSP-scaled.png?resize=768%2C271&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/Probando-configuracion-CSP-scaled.png?resize=1560%2C550&amp;ssl=1 1560w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/Probando-configuracion-CSP-scaled.png?resize=1060%2C374&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/Probando-configuracion-CSP-scaled.png?resize=1536%2C541&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/Probando-configuracion-CSP-scaled.png?resize=2048%2C722&amp;ssl=1 2048w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/Probando-configuracion-CSP-scaled.png?resize=550%2C194&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/Probando-configuracion-CSP-scaled.png?resize=1418%2C500&amp;ssl=1 1418w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/Probando-configuracion-CSP-scaled.png?resize=1920%2C677&amp;ssl=1 1920w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/Probando-configuracion-CSP-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<p class="wp-block-paragraph">Deberías ver un error de CSP en la consola confirmando que&nbsp;<code>script-src</code>&nbsp;bloquea el recurso. Si no aparece error, la política no se está aplicando.</p>



<p class="wp-block-paragraph">También es posible confirmar la configuración aplicada yendo a la sección <strong>Application</strong> &gt; <strong>Frames</strong> &gt; <strong>top</strong> y ahí podrás ver una sección sobre la configuración relacionada con Content Security Policy (CSP)</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/DevTools-Confirmar-la-configuracion-en-Application-Frames-top-scaled.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="710" height="279" src="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/DevTools-Confirmar-la-configuracion-en-Application-Frames-top.png?resize=710%2C279&#038;ssl=1" alt="" class="wp-image-31894" srcset="https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/DevTools-Confirmar-la-configuracion-en-Application-Frames-top-scaled.png?resize=1024%2C402&amp;ssl=1 1024w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/DevTools-Confirmar-la-configuracion-en-Application-Frames-top-scaled.png?resize=300%2C118&amp;ssl=1 300w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/DevTools-Confirmar-la-configuracion-en-Application-Frames-top-scaled.png?resize=768%2C302&amp;ssl=1 768w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/DevTools-Confirmar-la-configuracion-en-Application-Frames-top-scaled.png?resize=1401%2C550&amp;ssl=1 1401w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/DevTools-Confirmar-la-configuracion-en-Application-Frames-top-scaled.png?resize=1060%2C416&amp;ssl=1 1060w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/DevTools-Confirmar-la-configuracion-en-Application-Frames-top-scaled.png?resize=1536%2C603&amp;ssl=1 1536w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/DevTools-Confirmar-la-configuracion-en-Application-Frames-top-scaled.png?resize=2048%2C804&amp;ssl=1 2048w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/DevTools-Confirmar-la-configuracion-en-Application-Frames-top-scaled.png?resize=550%2C216&amp;ssl=1 550w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/DevTools-Confirmar-la-configuracion-en-Application-Frames-top-scaled.png?resize=1274%2C500&amp;ssl=1 1274w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/DevTools-Confirmar-la-configuracion-en-Application-Frames-top-scaled.png?resize=1920%2C754&amp;ssl=1 1920w, https://i0.wp.com/www.returngis.net/wp-content/uploads/2026/04/DevTools-Confirmar-la-configuracion-en-Application-Frames-top-scaled.png?w=1200&amp;ssl=1 1200w" sizes="(max-width: 710px) 100vw, 710px" /></a></figure>
</div>


<h2 class="wp-block-heading">Lo que NO puedes hacer con meta tags</h2>



<p class="wp-block-paragraph">Hay directivas CSP y headers de seguridad que <strong>solo funcionan como headers HTTP</strong> y no se pueden aplicar via <code>&lt;meta&gt;</code>:</p>



<ul class="wp-block-list">
<li><strong><code>frame-ancestors</code></strong>: controla quién puede embeber tu página en un iframe. En GitHub Pages no puedes configurarlo.</li>



<li><strong><code>Permissions-Policy</code></strong>: restringe el acceso a APIs del navegador (cámara, micrófono, geolocalización). No existe como meta tag.</li>



<li><strong><code>X-Frame-Options</code></strong>: la versión legacy de <code>frame-ancestors</code>. Tampoco funciona como meta tag.</li>
</ul>



<p class="wp-block-paragraph">Si necesitas control total sobre estos headers, tendrás que considerar un hosting que permita configurar headers HTTP personalizados.</p>



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



<p class="wp-block-paragraph">GitHub Pages es una opción más que suficiente para sitios estáticos, y aunque tiene limitaciones al no poder configurar headers HTTP, puedes hacer bastante con meta tags. Un CSP bien configurado via <code>&lt;meta&gt;</code> te protege contra XSS, controla qué recursos se cargan y limita las conexiones salientes.</p>



<p class="wp-block-paragraph">No esperes a tener un hosting «de pago» para aplicar buenas prácticas de seguridad. Con unas pocas líneas en tu <code>&lt;head&gt;</code> puedes mejorar significativamente la postura de seguridad de tu sitio.</p>



<p class="wp-block-paragraph">¡Nos vemos 👋🏻!</p>
<p>La entrada <a href="https://www.returngis.net/2026/04/github-pages-seguridad-csp/">Seguridad en GitHub Pages con CSP</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/github-pages-seguridad-csp/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">31883</post-id>	</item>
		<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 class="wp-block-paragraph">¡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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">Su función es sencilla: abrir la aplicación dentro del navegador integrado de VS Code, usando la URL local del frontend.</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">¡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 wp-block-paragraph">¡Hola developer 👋🏻!</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">Perfecto 👍<br>Pero… eso no responde a la pregunta importante: <strong>¿esto es realmente explotable en mi código?</strong></p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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" 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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">¡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 class="wp-block-paragraph">¡Hola developer 👋🏻!</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">👉 <em>“¿Puedo controlar qué analiza CodeQL?”</em><br>👉 <em>“¿Puedo limitarlo al OWASP Top 10 e incluso incluir mis propias reglas?”</em></p>



<p class="wp-block-paragraph">Y la respuesta es: <strong>sí</strong> y<strong> </strong>en este artículo te cuento cómo configurarlo. </p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">❌ 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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">En lugar de usar:</p>



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



<p class="wp-block-paragraph">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 class="wp-block-paragraph">Cada categoría (por ejemplo <em>Injection</em>) se corresponde con múltiples CWE y queries.</p>



<p class="wp-block-paragraph">👉 Ejemplo:</p>



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



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



<p class="wp-block-paragraph">Y CodeQL ya tiene queries que cubren estos casos.</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">Aquí es donde la cosa se pone interesante 🔥 CodeQL permite escribir queries propias porque:</p>



<p class="wp-block-paragraph">👉 trata el código como datos consultables<br>👉 puedes detectar patrones específicos de tu empresa</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">En <code><strong>.github/codeql/codeql-config.yml</strong></code>:</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">💡 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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">Aquí tienes varios patrones reales que puedes usar:</p>



<p class="wp-block-paragraph"><strong>✅ 1. OWASP-only scanning</strong></p>



<p class="wp-block-paragraph">👉 Ideal para:</p>



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



<li>auditorías</li>



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



<p class="wp-block-paragraph"><strong>⚡ 2. Only critical queries</strong></p>



<p class="wp-block-paragraph">👉 Reduce ruido<br>👉 Mejora adopción</p>



<p class="wp-block-paragraph"><strong>🏢 3. Security policies corporativas</strong></p>



<p class="wp-block-paragraph">👉 Custom queries + suites<br>👉 Enforcing en PRs</p>



<p class="wp-block-paragraph"><strong>🧪 4. Demo / training environments</strong></p>



<p class="wp-block-paragraph">👉 Como este repo 😄<br>👉 Muy útil para workshops o sesiones técnicas</p>



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



<p class="wp-block-paragraph">¡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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">Vamos parte por parte.</p>



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



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">Algunas cosas que me llevo:</p>



<p class="wp-block-paragraph"><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 class="wp-block-paragraph"><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 class="wp-block-paragraph"><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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">👉 <a href="https://github.com/0GiS0/markdown-view-for-agents">github.com/0GiS0/markdown-view-for-agents</a></p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">Y ahora crucemos los dedos a ver si me aceptan el plugin 🤞🏻</p>



<p class="wp-block-paragraph">¡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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><strong>Checkstyle valida, Spotless formatea.</strong> Y sí, es XML y parece sacado de los 90… pero es lo que hay 😄</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">¡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>
	</channel>
</rss>
