{"id":496,"date":"2026-03-09T10:38:43","date_gmt":"2026-03-09T10:38:43","guid":{"rendered":"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/setting-up-github-actions-for-python-applications\/"},"modified":"2026-03-18T22:31:25","modified_gmt":"2026-03-18T22:31:25","slug":"setting-up-github-actions-for-python-applications","status":"publish","type":"post","link":"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/setting-up-github-actions-for-python-applications\/","title":{"rendered":"Configuraci\u00f3n de GitHub Actions para Proyectos Python: Lo Que Aprend\u00ed a las Malas"},"content":{"rendered":"<p>Llevaba como tres meses postergando esto. Nuestro equipo de cuatro personas ten\u00eda un proyecto FastAPI que corr\u00eda tests localmente &#8220;cuando alguien se acordaba&#8221;, y el deploy era&#8230; bueno, el deploy era yo ejecutando <code>rsync<\/code> desde mi m\u00e1quina. Elegante no era.<\/p>\n<p>As\u00ed que un martes por la tarde me puse a configurar <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/configuracin-de-github-actions-para-aplicaciones-p\/\" title=\"GitHub Actions\">GitHub Actions<\/a>. &#8220;Dos horas m\u00e1ximo&#8221;, pens\u00e9. Seis d\u00edas despu\u00e9s, con el historial de commits m\u00e1s vergonzoso que he tenido en mi carrera, ten\u00eda algo que funcionaba <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/08\/alternativas-a-github-copilot-en-2026-cursor-codei\/\" title=\"de verdad\">de verdad<\/a>. <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/08\/benchmarks-de-asistentes-de-cdigo-ia-pruebas-de-re\/\" title=\"esto es\">Esto es<\/a> <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/langchain-vs-llamaindex-vs-haystack-building-produ\/\" title=\"Lo que aprend\u00ed\">lo que aprend\u00ed<\/a>.<\/p>\n<h2>El Primer Workflow que Escrib\u00ed Rompi\u00f3 Todo en 47 Segundos<\/h2>\n<p>El objetivo inicial era simple: que los tests corrieran en cada push. La documentaci\u00f3n oficial <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/configuracin-de-github-actions-para-aplicacione\/\" title=\"de GitHub\">de GitHub<\/a> es decente, as\u00ed que copi\u00e9 el ejemplo de Python que aparece ah\u00ed, lo pegu\u00e9 en <code>.github\/workflows\/ci.yml<\/code>, hice push. Fall\u00f3.<\/p>\n<p>El error era:<\/p>\n<pre><code>Error: No module named 'pytest'\n<\/code><\/pre>\n<p>Obvio en retrospectiva. Estaba instalando dependencias con <code>pip install -r requirements.txt<\/code>, pero mi <code>requirements.txt<\/code> de producci\u00f3n no inclu\u00eda <code>pytest<\/code>. Lo ten\u00eda en <code>requirements-dev.txt<\/code> separado. Primer aprendizaje: el entorno de CI es un entorno limpio <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/08\/alternativas-a-github-copilot-en-2026-cursor-codei\/\" title=\"de verdad\">de verdad<\/a> \u2014 no hay nada preinstalado, no hay cach\u00e9 del sistema, no hay ese paquete que instalaste hace seis meses y ya ni recuerdas.<\/p>\n<p>La estructura base que termin\u00e9 usando, <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/bun-vs-nodejs-in-production-2026-real-migration-st\/\" title=\"Despu\u00e9s de\">despu\u00e9s de<\/a> varios intentos fallidos:<\/p>\n<pre><code class=\"language-yaml\">name: CI\n\non:\n  push:\n    branches: [main, develop]\n  pull_request:\n    branches: [main]\n\njobs:\n  test:\n    runs-on: ubuntu-22.04\n\n    steps:\n      - uses: actions\/checkout@v4\n\n      - name: Configurar Python\n        uses: actions\/setup-python@v5\n        with:\n          python-version: &quot;3.12&quot;\n\n      - name: Instalar dependencias\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n          pip install -r requirements-dev.txt\n\n      - name: Correr tests\n        run: pytest tests\/ -v --tb=short\n<\/code><\/pre>\n<p>Simple. Funciona. Pero lento \u2014 3 minutos 40 segundos en cada run solo por la instalaci\u00f3n de dependencias. Y aqu\u00ed es donde la cosa se pone interesante.<\/p>\n<h2>Cach\u00e9 de Dependencias: De 3 Minutos 40 a 28 Segundos<\/h2>\n<p>Esto tuvo el mayor impacto en nuestro flujo de trabajo, y tambi\u00e9n donde pas\u00e9 m\u00e1s tiempo confundido.<\/p>\n<p>El mecanismo es simple: guardas un directorio entre runs, asociado a una clave. Si la clave coincide, usas lo guardado. Si no, instalas todo de nuevo y guardas el resultado para la pr\u00f3xima vez. La clave t\u00edpica es un hash del archivo de dependencias \u2014 si <code>requirements.txt<\/code> no cambi\u00f3, el hash es el mismo y te salt\u00e1s la instalaci\u00f3n.<\/p>\n<p><code>actions\/setup-python@v5<\/code> tiene cach\u00e9 integrado, lo cual est\u00e1 bien. Pero tard\u00e9 m\u00e1s de <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/langchain-vs-llamaindex-vs-haystack-building-produ\/\" title=\"Lo que\">lo que<\/a> me gustar\u00eda admitir en entender <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/redis-vs-valkey-in-2026-why-the-license-change-for\/\" title=\"Por Qu\u00e9\">por qu\u00e9<\/a> a veces el cach\u00e9 no se activaba. Si ten\u00e9s dependencias separadas (dev vs. prod, como yo), necesit\u00e1s especificarlo expl\u00edcitamente:<\/p>\n<pre><code class=\"language-yaml\">      - name: Configurar Python con cach\u00e9\n        uses: actions\/setup-python@v5\n        with:\n          python-version: &quot;3.12&quot;\n          cache: &quot;pip&quot;\n          cache-dependency-path: |\n            requirements.txt\n            requirements-dev.txt\n<\/code><\/pre>\n<p>Ese <code>cache-dependency-path<\/code> con m\u00faltiples archivos fue la pieza que me faltaba. Antes usaba solo <code>requirements.txt<\/code> y el cach\u00e9 se invalidaba cada vez que instalaba algo nuevo en dev. Con esto, se invalida si cambia cualquiera de los dos archivos.<\/p>\n<p>Si usas Poetry \u2014 y si est\u00e1s empezando un proyecto nuevo hoy, consider\u00e1 uv en serio, es notablemente m\u00e1s r\u00e1pido que pip para instalaci\u00f3n \u2014 la configuraci\u00f3n cambia un poco:<\/p>\n<pre><code class=\"language-yaml\">      - name: Instalar Poetry\n        uses: snok\/install-poetry@v1\n        with:\n          version: &quot;1.8.2&quot;\n          virtualenvs-create: true\n          virtualenvs-in-project: true\n\n      - name: Cach\u00e9 de dependencias\n        uses: actions\/cache@v4\n        with:\n          path: .venv\n          key: venv-${{ runner.os }}-${{ hashFiles('poetry.lock') }}\n\n      - name: Instalar dependencias\n        run: poetry install --no-interaction\n<\/code><\/pre>\n<p>Punto clave: us\u00e1 <code>poetry.lock<\/code> para el hash, no <code>pyproject.toml<\/code>. El lockfile tiene los hashes exactos de cada paquete \u2014 es el archivo <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/typescript-5x-in-2026-features-that-actually-matte\/\" title=\"que Realmente\">que realmente<\/a> determina qu\u00e9 se instala. Con <code>pyproject.toml<\/code> podr\u00edas estar cacheando una versi\u00f3n desactualizada sin darte cuenta.<\/p>\n<h2>Matrices de Versiones: Tres Pythons en Paralelo Sin Complicarte la Vida<\/h2>\n<p>Una vez que el workflow b\u00e1sico corr\u00eda bien, quise verificar compatibilidad con Python 3.10, 3.11 y 3.12 al mismo tiempo. Honestamente, la estrategia de matrices <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/configuracin-de-github-actions-para-aplicacione\/\" title=\"de GitHub Actions\">de GitHub Actions<\/a> fue de lo poco que me sorprendi\u00f3 bien durante todo el proceso \u2014 esperaba tener que escribir tres jobs a mano.<\/p>\n<p>La idea: defin\u00eds una lista de valores y GitHub crea un job por cada combinaci\u00f3n, en paralelo, autom\u00e1ticamente.<\/p>\n<pre><code class=\"language-yaml\">jobs:\n  test:\n    runs-on: ubuntu-22.04\n\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [&quot;3.10&quot;, &quot;3.11&quot;, &quot;3.12&quot;]\n\n    steps:\n      - uses: actions\/checkout@v4\n\n      - name: Configurar Python ${{ matrix.python-version }}\n        uses: actions\/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n          cache: &quot;pip&quot;\n          cache-dependency-path: |\n            requirements.txt\n            requirements-dev.txt\n\n      - name: Instalar dependencias\n        run: pip install -r requirements.txt -r requirements-dev.txt\n\n      - name: Tests\n        run: pytest tests\/ -v --tb=short\n<\/code><\/pre>\n<p>El <code>fail-fast: false<\/code> merece un p\u00e1rrafo propio. Por defecto, si un job de la matriz falla, GitHub cancela los dem\u00e1s. Eso parece eficiente, pero en pr\u00e1ctica es frustrante: si Python 3.10 falla por algo de compatibilidad, quer\u00e9s saber si 3.11 y 3.12 pasan tambi\u00e9n. Con <code>fail-fast: false<\/code>, todos corren hasta el final y ves el panorama completo.<\/p>\n<p>Algo que no esperaba: el cach\u00e9 es por versi\u00f3n de Python. Cada entrada de la matriz tiene su propio cach\u00e9 separado. L\u00f3gico una vez que lo pens\u00e1s, pero me cost\u00f3 un momento de &#8220;\u00bf<a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/redis-vs-valkey-in-2026-why-the-license-change-for\/\" title=\"Por Qu\u00e9\">por qu\u00e9<\/a> est\u00e1 instalando todo de nuevo en Python 3.10 si 3.12 ya lo ten\u00eda cacheado?&#8221; antes de entenderlo.<\/p>\n<p>\u00bfTestear tambi\u00e9n en m\u00faltiples sistemas operativos? Pod\u00e9s extender la matriz as\u00ed: <code>os: [ubuntu-22.04, windows-latest, macos-latest]<\/code>. Nosotros no lo hacemos \u2014 deployamos en Linux y somos cuatro personas, as\u00ed que agregar seis jobs m\u00e1s por run ser\u00eda ruido m\u00e1s que se\u00f1al. Depende del proyecto.<\/p>\n<h2>Linting y Tipos: Donde Perd\u00ed Una Tarde Entera un Viernes<\/h2>\n<p>Okay, <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/event-driven-architecture-in-2026-why-microservice\/\" title=\"esto fue\">esto fue<\/a> el momento m\u00e1s frustrante de todo el proceso.<\/p>\n<p>Quer\u00eda agregar linting y type checking al pipeline. El stack que uso localmente es ruff para linting y formateo (migr\u00e9 de flake8 + black + isort hace unos meses y no miro atr\u00e1s), y mypy para tipos. Pens\u00e9 que ser\u00eda copy-paste de la configuraci\u00f3n local al workflow. No lo fue.<\/p>\n<p>El problema con mypy en CI es que necesita los stubs de todos tus paquetes. Localmente ten\u00eda instalados <code>types-requests<\/code>, <code>types-redis<\/code> y otros paquetes de stubs manualmente \u2014 pero no estaban en <code>requirements-dev.txt<\/code>. As\u00ed que mypy corr\u00eda en CI y se quejaba de absolutamente todo. Empuj\u00e9 esa configuraci\u00f3n <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/kubernetes-vs-docker-swarm-vs-nomad-container-orch\/\" title=\"un Viernes\">un viernes<\/a> por la tarde y al lunes me encontr\u00e9 con 23 errores nuevos <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/05\/claude-vs-gpt-4o-vs-gemini-20-qu-modelo-de-ia-usar\/\" title=\"en el\">en el<\/a> pipeline, ninguno de los cuales era un error <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/kubernetes-vs-docker-swarm-vs-nomad-comparacin-de\/\" title=\"Real de\">real de<\/a> tipos \u2014 todos eran stubs faltantes. Cl\u00e1sico.<\/p>\n<p>La configuraci\u00f3n que qued\u00f3 funcionando:<\/p>\n<pre><code class=\"language-yaml\">  lint:\n    runs-on: ubuntu-22.04\n\n    steps:\n      - uses: actions\/checkout@v4\n\n      - name: Configurar Python\n        uses: actions\/setup-python@v5\n        with:\n          python-version: &quot;3.12&quot;\n          cache: &quot;pip&quot;\n          cache-dependency-path: requirements-dev.txt\n\n      - name: Instalar herramientas\n        run: pip install ruff mypy types-requests types-redis\n\n      - name: Ruff lint\n        run: ruff check .\n\n      - name: Ruff format check\n        run: ruff format --check .\n        # --check verifica sin modificar archivos \u2014 fundamental en CI\n\n      - name: Mypy\n        run: mypy src\/ --ignore-missing-imports\n<\/code><\/pre>\n<p>El <code>--ignore-missing-imports<\/code> en mypy es un compromiso que no me encanta. La alternativa es mantener una lista exhaustiva de paquetes de stubs, y con dependencias que cambian regularmente, esa lista se desactualiza sola. No estoy 100% seguro de que esto escale bien m\u00e1s all\u00e1 de equipos peque\u00f1os \u2014 si alguien tiene un enfoque mejor, me interesa genuinamente saberlo.<\/p>\n<p>Lo que s\u00ed me gust\u00f3 de este setup: ruff tarda 4 segundos <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/05\/claude-vs-gpt-4o-vs-gemini-20-qu-modelo-de-ia-usar\/\" title=\"en el\">en el<\/a> pipeline. Con flake8 + black + isort por separado tardaba alrededor de 25. No es cr\u00edtico, pero cuando ten\u00e9s 15 PRs abiertos <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/05\/claude-vs-gpt-4o-vs-gemini-20-qu-modelo-de-ia-usar\/\" title=\"en el\">en el<\/a> d\u00eda, esos segundos se acumulan.<\/p>\n<h2>C\u00f3mo Qued\u00f3 el Workflow Final <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/deno-20-in-production-2026-migration-from-nodejs-a\/\" title=\"y Qu\u00e9\">y Qu\u00e9<\/a> Har\u00eda Diferente<\/h2>\n<p>Despu\u00e9s de todo este proceso, el archivo que tenemos <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/typescript-5x-in-2026-features-that-actually-matte\/\" title=\"en Producci\u00f3n\">en producci\u00f3n<\/a> hace tres cosas en paralelo: tests en Python 3.10-3.12, linting con ruff y mypy, y un check r\u00e1pido de seguridad con <code>pip audit<\/code>. Los tres jobs corren al mismo tiempo \u2014 no hay raz\u00f3n para que linting espere a que terminen los tests. Eso recort\u00f3 el tiempo total de casi 6 minutos a poco m\u00e1s de 2.<\/p>\n<p>Si tuviera que empezar de nuevo, configurar\u00eda las notificaciones de Slack desde el primer d\u00eda. Tardamos semanas en darnos cuenta de que ten\u00edamos branches con tests fallando porque nadie revisaba el \u00edcono de estado en los PRs con suficiente cuidado. GitHub tiene integraci\u00f3n con Slack bastante directa \u2014 no es mucho trabajo configurarlo, y <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/05\/copilot-vs-cursor-vs-codeium\/\" title=\"Vale la Pena\">vale la pena<\/a> hacerlo antes de que el equipo desarrolle el h\u00e1bito de ignorar los \u00edconos rojos.<\/p>\n<p>Tambi\u00e9n usar\u00eda <code>continue-on-error: true<\/code> <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/05\/claude-vs-gpt-4o-vs-gemini-20-qu-modelo-de-ia-usar\/\" title=\"en el\">en el<\/a> job de mypy durante las primeras semanas de adoptarlo en un proyecto existente. Mypy en un proyecto nuevo es f\u00e1cil. Mypy en un proyecto con 30k l\u00edneas que nunca tuvo tipos es otra historia \u2014 si lo pon\u00e9s en modo bloqueante desde el principio, vas a tener el pipeline en rojo permanente <a href=\"https:\/\/blog.rebalai.com\/es\/2026\/03\/09\/kubernetes-vs-docker-swarm-vs-nomad-container-orch\/\" title=\"Hasta Que\">hasta que<\/a> alguien tenga tiempo de ir tipo por tipo. Mejor hacerlo pasar, ver los errores como warnings, e ir arregl\u00e1ndolos iterativamente.<\/p>\n<p>Empez\u00e1 con el workflow m\u00e1s simple posible \u2014 checkout, setup-python con cach\u00e9, instalar dependencias, correr pytest. Hac\u00e9 que eso funcione antes de agregar linting, matrices o cualquier otra cosa. Si agreg\u00e1s cinco cosas a la vez y algo falla, vas a perder tiempo tratando de aislar qu\u00e9 rompi\u00f3 qu\u00e9. Yo lo hice mal y pagu\u00e9 el precio.<\/p>\n<p>El cach\u00e9 de dependencias es el cambio de mayor impacto por menor esfuerzo \u2014 quince minutos de configuraci\u00f3n para cortar el tiempo de build a la mitad o menos. Hacelo desde el principio, no despu\u00e9s.<\/p>\n<p>Y si est\u00e1s empezando un proyecto nuevo, us\u00e1 uv. El workflow cambia un poco pero la instalaci\u00f3n de dependencias es tan r\u00e1pida que casi hace irrelevante el cach\u00e9 de todas formas.<\/p>\n<p><!-- Reviewed: 2026-03-09 | Status: ready_to_publish | Changes: removed formulaic \"Mi recomendaci\u00f3n concreta sin rodeos\" opener; broke parallel \"Una cosa\/El otro cambio\" structure in final section; replaced overly formal \"sorprendi\u00f3 gratamente\" phrasing; tightened cache mechanism explanation; minor rhythm adjustments throughout --><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Llevaba como tres meses postergando esto.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[1],"tags":[],"class_list":["post-496","post","type-post","status-publish","format-standard","hentry","category-general"],"_links":{"self":[{"href":"https:\/\/blog.rebalai.com\/es\/wp-json\/wp\/v2\/posts\/496","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.rebalai.com\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.rebalai.com\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.rebalai.com\/es\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.rebalai.com\/es\/wp-json\/wp\/v2\/comments?post=496"}],"version-history":[{"count":11,"href":"https:\/\/blog.rebalai.com\/es\/wp-json\/wp\/v2\/posts\/496\/revisions"}],"predecessor-version":[{"id":773,"href":"https:\/\/blog.rebalai.com\/es\/wp-json\/wp\/v2\/posts\/496\/revisions\/773"}],"wp:attachment":[{"href":"https:\/\/blog.rebalai.com\/es\/wp-json\/wp\/v2\/media?parent=496"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.rebalai.com\/es\/wp-json\/wp\/v2\/categories?post=496"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.rebalai.com\/es\/wp-json\/wp\/v2\/tags?post=496"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}