¿Alguna vez has soñado con tener un asistente que modifique tu código automáticamente mientras duermes? ¿O simplemente te da pereza hacer refactorizaciones masivas? Hoy vamos a ver cómo podemos combinar la potencia de GitHub Actions con la inteligencia de Gemini 3 Pro para crear un flujo de trabajo que modifique nuestro código base mediante instrucciones en lenguaje natural.

El futuro es hoy, viejo
La idea es sencilla pero potente: crearemos un workflow en GitHub que se pueda disparar manualmente (o por eventos), el cual ejecutará un script de Python. Este script leerá nuestro proyecto, enviará el contexto y nuestras instrucciones a Gemini, y aplicará los cambios que la IA nos devuelva.
Esto es especialmente útil para tareas repetitivas, correcciones de estilo, generación de documentación o incluso para prototipar pequeñas funcionalidades sin abrir el editor. Suena bien, ¿verdad?
[!IMPORTANT] Antes de empezar, necesitas configurar un par de cosas para que esto funcione. Sin la API Key, el script fallará.
Obtener la API Key:
Configurar el Secreto en GitHub:
Settings > Secrets and variables > Actions.New repository secret.GEMINI_API_KEY.Lo primero que necesitamos es un script que haga de puente entre nuestro código y la API de Gemini. En este caso, hemos creado gemini_modifier.py en la carpeta .github/scripts. Vamos a analizarlo paso a paso.
Importamos las librerías necesarias y configuramos el cliente de Gemini usando la API Key que obtendremos de las variables de entorno. También definimos el modelo que vamos a usar, en este caso gemini-3-pro-preview.
import os
import google.generativeai as genai
# Configuración
api_key = os.getenv("GEMINI_API_KEY")
genai.configure(api_key=api_key)
# Usamos 'gemini-3-pro-preview'
model = genai.GenerativeModel('gemini-3-pro-preview')
No queremos enviar todo nuestro proyecto a la IA. Carpetas como node_modules, .git o los directorios de build de Next.js (.next, out) son irrelevantes y consumirían demasiados tokens. Por eso definimos listas de exclusión y una función get_all_files para recorrer solo lo que nos interesa.
IGNORED_DIRS = {'.git', '.github', 'node_modules', '__pycache__', 'venv', 'dist', 'build', 'coverage', '.idea', '.vscode', '.next', 'out'}
IGNORED_FILES = {'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'gemini_modifier.py', 'next-env.d.ts'}
def get_all_files(root_dir):
# ... lógica de recorrido con os.walk ...
Una vez tenemos la lista de archivos, leemos su contenido y construimos un gran string que servirá de contexto para Gemini. Delimitamos cada archivo claramente para que la IA sepa dónde empieza y termina cada uno.
# Construimos el contexto
codebase_context = ""
for rel_path in files_to_process:
# ... lectura del archivo ...
codebase_context += f"\n--- START OF FILE: {rel_path} ---\n"
codebase_context += content
codebase_context += f"\n--- END OF FILE: {rel_path} ---\n"
Aquí es donde ocurre la magia. Le decimos a Gemini que actúe como un Ingeniero de Software Senior experto en Next.js y React. Esto es crucial para que entienda el contexto de nuestro framework (App Router, Server Components, etc.). Además, le exigimos un formato de respuesta estricto (<<<< FILE: path >>>>) para poder procesar su salida programáticamente.
# Prompt para Gemini
full_prompt = f"""
Actúa como un Ingeniero de Software Senior experto en Next.js y React...
Usa ESTRICTAMENTE el siguiente formato para devolver el código:
<<<< FILE: path/to/file.ext >>>>
contenido del archivo...
<<<< END FILE >>>>
CONTEXTO (CÓDIGO BASE):
{codebase_context}
INSTRUCCIÓN DEL USUARIO:
{user_prompt}
"""
Finalmente, enviamos el prompt, recibimos la respuesta y la parseamos. Buscamos los delimitadores que definimos antes y escribimos los archivos correspondientes en el disco.
# ... llamada a model.generate_content ...
for line in lines:
if stripped_line.startswith("<<<< FILE:") and stripped_line.endswith(">>>>"):
# ... lógica para detectar inicio de archivo ...
elif stripped_line == "<<<< END FILE >>>>":
# ... lógica para guardar archivo ...
Aquí tienes el código completo de gemini_modifier.py:
import os
import sys
import google.generativeai as genai
# Configuración
api_key = os.getenv("GEMINI_API_KEY")
user_prompt = os.getenv("USER_PROMPT")
# Allow passing prompt as argument
if not user_prompt and len(sys.argv) > 1:
user_prompt = sys.argv[1]
target_path = os.getenv("TARGET_FILE")
if not target_path or target_path.strip() == "":
target_path = "."
if not api_key:
print("Error: No se encontró la GEMINI_API_KEY")
exit(1)
genai.configure(api_key=api_key)
# Usamos 'gemini-3-pro-preview'
model = genai.GenerativeModel('gemini-3-pro-preview')
IGNORED_DIRS = {'.git', '.github', 'node_modules', '__pycache__', 'venv', 'dist', 'build', 'coverage', '.idea', '.vscode', '.next', 'out'}
IGNORED_FILES = {'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'gemini_modifier.py', 'next-env.d.ts'}
def get_all_files(root_dir):
file_list = []
for root, dirs, files in os.walk(root_dir):
# Filter directories
dirs[:] = [d for d in dirs if d not in IGNORED_DIRS]
for file in files:
if file in IGNORED_FILES:
continue
# Skip non-text files (simple check based on extension)
if file.endswith(('.png', '.jpg', '.jpeg', '.gif', '.ico', '.pdf', '.exe', '.dll', '.bin', '.zip', '.tar', '.gz')):
continue
full_path = os.path.join(root, file)
# Skip the script itself if it's in the path (though IGNORED_FILES handles filename)
rel_path = os.path.relpath(full_path, root_dir)
file_list.append(rel_path)
return file_list
def read_file_content(file_path):
try:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
except Exception:
return None # Skip binary or unreadable files
def write_file(root, rel_path, content):
full_path = os.path.join(root, rel_path)
os.makedirs(os.path.dirname(full_path), exist_ok=True)
with open(full_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Escrito: {full_path}")
def modify_project():
# 1. Determine scope
project_root = "."
files_to_process = []
if os.path.isfile(target_path):
print(f"Modo archivo único: {target_path}")
files_to_process = [target_path]
# If target is a file, project root is current dir, but we treat file path as relative to it usually
else:
print(f"Modo proyecto completo: {target_path}")
project_root = target_path
files_to_process = get_all_files(project_root)
print(f"Archivos encontrados para contexto: {len(files_to_process)}")
# 2. Build Context
codebase_context = ""
for rel_path in files_to_process:
full_path = os.path.join(project_root, rel_path) if project_root != "." else rel_path
content = read_file_content(full_path)
if content is not None:
codebase_context += f"\n--- START OF FILE: {rel_path} ---\n"
codebase_context += content
codebase_context += f"\n--- END OF FILE: {rel_path} ---\n"
# 3. Build Prompt
full_prompt = f"""
Actúa como un Ingeniero de Software Senior experto en Next.js y React.
Tu tarea es modificar el código del proyecto basándote en una instrucción del usuario.
INSTRUCCIONES:
1. Analiza el código proporcionado (CONTEXTO) y la instrucción del usuario.
2. Realiza los cambios necesarios siguiendo las mejores prácticas de Next.js (App Router, Server Components, optimización de imágenes, etc.).
3. PUEDES modificar múltiples archivos, crear nuevos archivos o eliminar código obsoleto.
4. Usa ESTRICTAMENTE el siguiente formato para devolver el código:
<<<< FILE: path/to/file.ext >>>>
contenido del archivo...
<<<< END FILE >>>>
<<<< FILE: another/file.js >>>>
contenido...
<<<< END FILE >>>>
REGLAS:
- NO incluyas texto fuera de los bloques de archivo (ni saludos, ni explicaciones).
- El path debe ser relativo a la raíz del proyecto.
- Mantén el estilo de código existente.
- Si creas un archivo nuevo, usa el mismo formato.
CONTEXTO (CÓDIGO BASE):
{codebase_context}
INSTRUCCIÓN DEL USUARIO:
{user_prompt}
"""
# 4. Call Gemini
print("Enviando petición a Gemini...")
try:
response = model.generate_content(full_prompt)
response_text = response.text
except Exception as e:
print(f"Error llamando a Gemini: {e}")
# Try listing models if error looks like model not found (though we fixed this)
exit(1)
# 5. Parse and Write
print("Procesando respuesta de Gemini...")
# Clean potential markdown wrappers
lines = response_text.split('\\n')
if lines and lines[0].strip().startswith("```"):
lines = lines[1:]
if lines and lines[-1].strip() == "```":
lines = lines[:-1]
current_file = None
current_content = []
for line in lines:
stripped_line = line.strip()
if stripped_line.startswith("<<<< FILE:") and stripped_line.endswith(">>>>"):
# Save previous file if exists
if current_file:
write_file(project_root, current_file, "\\n".join(current_content))
current_content = []
# Start new file
current_file = stripped_line[10:-4].strip()
print(f"Detectado cambio para: {current_file}")
elif stripped_line == "<<<< END FILE >>>>":
if current_file:
write_file(project_root, current_file, "\\n".join(current_content))
current_file = None
current_content = []
else:
if current_file is not None:
current_content.append(line)
# Handle last file if missing end tag
if current_file and current_content:
write_file(project_root, current_file, "\\n".join(current_content))
print("Proceso completado.")
if __name__ == "__main__":
modify_project()
Ahora que tenemos el cerebro, necesitamos el músculo que lo ejecute. Para ello, definimos un workflow en .github/workflows/ai-coder.yml.
Queremos poder ejecutar esto a demanda. Usamos workflow_dispatch para definir inputs manuales: el prompt (qué queremos hacer) y el archivo objetivo (opcional, para limitar el alcance).
on:
workflow_dispatch:
inputs:
prompt:
description: "Instrucción para Gemini"
required: true
type: string
target_file:
description: "Ruta del archivo (ej: src/App.js)"
required: true
type: string
Necesitamos permisos de escritura (contents: write) porque nuestro workflow va a modificar código y crear ramas nuevas.
permissions:
contents: write
El job principal prepara el entorno: hace checkout del código, instala Python y las dependencias. Luego ejecuta nuestro script pasando los secretos necesarios.
- name: Ejecutar Script de Gemini
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
USER_PROMPT: ${{ inputs.prompt }}
TARGET_FILE: ${{ inputs.target_file }}
run: python .github/scripts/gemini_modifier.py
Finalmente, usamos la CLI de GitHub (gh) para crear una rama nueva con los cambios y abrir una Pull Request automáticamente.
- name: Crear Pull Request
run: |
# ... git checkout -b ...
gh pr create --title "$COMMIT_MSG" ...
Aquí tienes el archivo .github/workflows/ai-coder.yml completo:
name: Gemini Frontend Modifier
on:
push:
branches:
- master
workflow_dispatch:
inputs:
prompt:
description: "Instrucción para Gemini"
required: true
type: string
target_file:
description: "Ruta del archivo (ej: src/App.js)"
required: true
type: string
permissions:
contents: write
jobs:
modify-code:
runs-on: ubuntu-latest
steps:
- name: Checkout del código
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
# CAMBIO 1: Instalamos el SDK de Google Generative AI
- name: Instalar dependencias
run: pip install google-generativeai
- name: Ejecutar Script de Gemini
env:
# CAMBIO 2: Pasamos la Key de Gemini
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
USER_PROMPT: ${{ inputs.prompt || github.event.head_commit.message }}
TARGET_FILE: ${{ inputs.target_file || '.' }}
run: python .github/scripts/gemini_modifier.py
- name: Crear Pull Request
env:
GH_TOKEN: ${{ secrets.MY_GITHUB_TOKEN }}
run: |
git config --global user.name "{tuusuario}"
git config --global user.email "{tuemail}"
# Crear rama única
BRANCH_NAME="gemini-changes-${{ github.run_id }}"
git checkout -b $BRANCH_NAME
git add .
# Verificar si hay cambios
if git diff --staged --quiet; then
echo "No hay cambios generados."
exit 0
fi
# Commit y Push
COMMIT_MSG="Gemini: ${{ inputs.prompt || github.event.head_commit.message }}"
git commit -m "$COMMIT_MSG"
git push https://x-access-token:${{ secrets.MY_GITHUB_TOKEN }}@github.com/ivanheral/testIA.git $BRANCH_NAME
# Crear PR
gh pr create --title "$COMMIT_MSG" \
--body "Cambios generados automáticamente por Gemini basados en la instrucción: ${{ inputs.prompt || github.event.head_commit.message }}" \
--head $BRANCH_NAME \
--base master
Una vez que tengas todo configurado, ¡es hora de probarlo!

Esperando a que termine el build...
Si todo va bien, verás que el workflow termina exitosamente y crea una nueva Pull Request.

¡Magia!
Si estás contento con el resultado, solo tienes que hacer merge y desplegar. ¡Así de fácil!
Si eres de los que viven en la terminal, también puedes lanzar el workflow directamente usando el CLI de GitHub:
gh workflow run ai-coder.yml -f prompt="Cambia el background a rosa chicle"
O mejor aún, puedes crear un alias en tu package.json para hacerlo más cómodo:
"scripts": {
"gemini": "gh workflow run ai-coder.yml -f"
}
Y ejecutarlo así:
npm run gemini -- prompt="mejora el codigo"
Con apenas un script de Python y un archivo YAML, hemos montado un sistema de CI/CD potenciado por IA. Esto abre un abanico de posibilidades enorme: desde refactorizaciones automáticas, generación de documentación, desarrollar un nuevo GitHub Action que cuando detecte nuevos componentes de algún framework añada tests unitarios y revise que pasen todos los tests del proyecto, o incluso ejecutar un cron diario donde Gemini revise el proyecto e incluya propuestas de mejora en un fichero markdown.
La automatización no es el futuro, es el presente. Y con herramientas como Gemini y GitHub Actions, está al alcance de todos.
¡Nos vemos en el siguiente post!
A ver, no nos vamos a engañar. Iván ya no está para estos trotes de picar código y escribir tutoriales sesudos. La realidad es que prácticamente la totalidad de este post, el script de Python y el workflow de GitHub Actions han sido perpetrados por Gemini 3 Pro. Yo solo me he limitado a mirar desde la barrera con una cerveza en la mano mientras la IA hacía el trabajo sucio. Si algo explota, ya sabéis a quién reclamar (spoiler: a mí no).