Blog image

Tutorial. "Programar" con GitHub Actions y Gemini 3 Pro

December 1, 2025

¿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

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?

Prerrequisitos: API Key y Secretos

[!IMPORTANT] Antes de empezar, necesitas configurar un par de cosas para que esto funcione. Sin la API Key, el script fallará.

  1. Obtener la API Key:

    • Accede a Google AI Studio e inicia sesión con tu cuenta de Google.
    • Haz clic en el botón azul "Get API key" en la esquina superior izquierda.
    • Selecciona "Create API key in new project" (o usa un proyecto existente si prefieres).
    • Copia la clave generada inmediatamente (¡guárdala bien, no podrás verla completa de nuevo!).
  2. Configurar el Secreto en GitHub:

    • Ve a tu repositorio en GitHub.
    • Entra en Settings > Secrets and variables > Actions.
    • Haz clic en New repository secret.
    • Nombre: GEMINI_API_KEY.
    • Valor: Pega tu API Key.

El Cerebro: Script de Python

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.

1. Configuración Inicial

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')

2. Filtrado de Archivos

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 ...

3. Construcción del Contexto

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"

4. El Prompt Maestro

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}
    """

5. Procesamiento de la Respuesta

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 ...

Resultado Final (Script)

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()

El Músculo: GitHub Actions

Ahora que tenemos el cerebro, necesitamos el músculo que lo ejecute. Para ello, definimos un workflow en .github/workflows/ai-coder.yml.

1. Disparadores (Triggers)

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

2. Permisos

Necesitamos permisos de escritura (contents: write) porque nuestro workflow va a modificar código y crear ramas nuevas.

permissions:
  contents: write

3. Pasos del Job

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

4. Creación de Pull Request

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" ...

Resultado Final (Workflow)

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

Ejecución y Validación

Una vez que tengas todo configurado, ¡es hora de probarlo!

  1. Ve a la pestaña Actions de tu repositorio.
  2. Selecciona el workflow Gemini Frontend Modifier.
  3. Haz clic en Run workflow.
  4. Introduce tu prompt (ej: "Cambia el color del botón de login a rojo") y, opcionalmente, el archivo objetivo.
Esperando a que termine el build...

Esperando a que termine el build...

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

  1. Ve a la pestaña Pull Requests.
  2. Abre la PR creada automáticamente.
  3. Revisa los cambios. ¡Verás que Gemini ha modificado tu código siguiendo tus órdenes!
¡Magia!

¡Magia!

Si estás contento con el resultado, solo tienes que hacer merge y desplegar. ¡Así de fácil!

Ejecución desde Terminal

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"
Conclusión

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!

Confesión

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).