Automatizando procesos en la nube gratuitamente.

Una opción sencilla con Github Actions

Author
Affiliation

R-conomics

Published

October 3, 2025

La importancia de la nube para automatizar tareas

No es raro que, para ciertos proyectos, ejercicios o procesos establecidos en el trabajo, utilicemos scripts en R, Python, SQL, etc., para simplificar tareas, automatizar partes del proceso y ahorrarnos mucho tiempo. Saber programar casi siempre nos permitirá evitar dedicar horas a actividades rutinarias; sin embargo, tener un script en nuestra computadora no siempre es suficiente. Como anécdota breve: hace algunos años, en un trabajo previo, me encargaba de actualizar diariamente ciertas tablas vinculadas a reportes y tableros. Al principio solo eran un par, pero poco a poco la cantidad fue aumentando, por lo que decidí semi-automatizar las tareas en segundo plano en mi computadora mediante un CRON que ejecutaba un script de R al mediodía, mientras yo trabajaba en otras cosas. El problema surgió después: el negocio requería que las tablas se actualizaran a las dos de la mañana, ya que el cierre de ventas ocurría una hora antes. Esto hizo inviable seguir utilizando una máquina local para actualizar los reportes. Estos inconvenientes me llevaron a buscar opciones en la nube. Encontré varias interesantes, como Astronomer, máquinas virtuales con EC2 (AWS) o notebooks en Microsoft Fabric. Sin embargo, todas resultaban relativamente costosas o requerían suscripciones de precios elevados, por lo que decidí probar una opción gratuita (al menos hasta cierto punto): GitHub Actions.

Para automatizaciones en la nube, Github Actions es, a mi consideración, una de las mejores herramientas que podemos usar, ya que inicialmente lo podremos usar de forma gratuita, además de no tener un límite de uso para repositorios públicos, mientras que para repositorios privados tenemos 2,000 minutos gratuitos de ejecución al mes. Un aspecto particuarmente interesante de esta herramienta, es que podemos ejecutar los procesos que hagamos en servidores de Linux, Windows o MacOS, dependiendo de lo que necesitemos para cada caso.

Ahora, dejando de lado el espacio publicitario por el que no me están pagando, vamos a ver un ejemplo práctico de cómo utilizar este servicio con un script de R para actualizar un documento de Google Sheets con información proveniente de una API. Me interesa desarrollar este ejercicio en particular, ya que cuando lo hice por primera vez por mi cuenta tuve muchos dolores de cabeza para hacer que funcionara correctamente, y eso precisamente fue lo que me ayudó a comprender de mejor manera cómo configurar estos procesos.

Un ejercicio práctico

Comencemos por plantear un problema: ¿Qué pasaría si quisiera mantener siempre actualizado un reporte de, por ejemplo, un stock financiero? Para esto, como primer paso, vamos a necesitar los datos actualizables, por lo que podemos obtenerlos usando la API de Alpha Vantage. Para propósitos de este ejercicio, vamos a obtener la cotización en la bolsa más reciente de Google:

library(alphavantager)
library(dplyr)
library(lubridate)

# API KEY
av_api_key("QRD9JEXDGCDJYRRM")

# PETICIÓN
data <- av_get(symbol = "GOOG", 
               av_fun = "TIME_SERIES_DAILY", 
               outputsize = "full", 
               interval = "60min")

data |> 
  as_tibble() |> 
  tail()
# A tibble: 6 × 6
  timestamp   open  high   low close   volume
  <date>     <dbl> <dbl> <dbl> <dbl>    <dbl>
1 2025-09-25  245.  247.  242.  247. 17379793
2 2025-09-26  248.  250.  247.  247. 16594608
3 2025-09-29  248.  252.  243.  244. 23157245
4 2025-09-30  243.  244.  240.  244. 22541189
5 2025-10-01  241.  247.  239.  246. 23967662
6 2025-10-02  246.  248.  243.  246. 20657524

Ahora, supongamos que queremos enviar esos datos a un documento de Google Sheets. Para propósitos de este ejercicio, vamos a crear un documento visible para todo el mundo, al cual podrás acceder en el siguiente enlace. Como sabemos acceder a un documento de Google Sheets en R es bastante sencillo, ya que utilizamos la API de Tidyverse para autenticarnos, y mientras tengamos acceso al documento, simplemente podemos acceder a partir del ID de documento disponible en la URL del mismo:

library(googlesheets4)

ss <- "1F-3U7zg3G5tKvBCDxS3feUmUVhuPUryrItHsAlxROak"

sheet_properties(ss)
# A tibble: 2 × 8
  name     index        id type  visible grid_rows grid_columns data  
  <chr>    <int>     <int> <chr> <lgl>       <int>        <int> <list>
1 Raw data     0         0 GRID  TRUE         2900           26 <NULL>
2 October      1 427772048 GRID  TRUE         1000           26 <NULL>

Vamos ahora a crear una hoja general con los datos que obtuvimos, filtrados al año actual:

range_clear(ss = ss,
            sheet = "Raw data",
            range = "B:G")


range_write(ss = ss,
            sheet = "Raw data",
            range = "B2",
            data = data |> 
              filter(timestamp >= as_date("2025-01-01")),
            reformat = FALSE)

Para esta hoja, lo que hacemos es limpiar el rango en el que vamos a escribir los datos y después insertar la información más actualizada. De forma manual, podemos añadir un gráfico y un formato condicional, resultando en:

Vamos ahora a crear hojas de forma condicional para separar los datos de cada mes. Comencemos por añadir algunos parámetros:

# Hojas existentes en el documento
tabs <- sheet_properties(ss)

# Nombre de la hoja a buscar/crear
mes_actual <- format(today(), "%B")

# Fechas inicial/final del mes
in_date <- floor_date(today(), "months")
end_date <- floor_date(today(), "months") %m+% months(1) - 1

Y, con esto, aplicar un condicional sencillo:

# Condición si no existe la hoja del mes actual
if (length(tabs$name[tabs$name == mes_actual ]) == 0 ){
  
  sheet_add(ss = ss, sheet = mes_actual)
  
  range_write(ss = ss,
              sheet = mes_actual,
              range = "B2",
              data = data |> 
                filter(between(timestamp, in_date, end_date)) |> 
                mutate(
                  timestamp = floor_date(timestamp, "weeks")
                ),
              reformat = FALSE
  )

# Condición si ya existe la hoja del mes actual    
}else{
  
  range_clear(
    ss = ss,
    sheet = mes_actual,
    range = "B:G"
  )
  
  range_write(ss = ss,
              sheet = mes_actual,
              range = "B2",
              data = data |> 
                filter(between(timestamp, in_date, end_date)) |> 
                mutate(
                  timestamp = floor_date(timestamp, "weeks")
                ),
              reformat = FALSE
  )
  
}

Simplemente añadimos dos escenarios: Si la hoja no existe entonces la creamos e insertamos los datos, pero si la hoja ya existe entonces limpiamos el rango e insertamos los datos de nuevo.

Ya tenemos un proceso armado, sin embargo, aún hay un pequeño problema. Cada que iniciamos una sesión de R y queremos acceder a la hoja de cálculo de Google Sheets, esta nos va a pedir una autenticación manual, por lo que necesariamente requiere intervención humana y, tal cual tenemos nuestro código, no podemos automatizarlo por completo. Para solucionar este problema tenemos algunas opciones, como crear un archivo .secrets y usarlo para autenticación con la librería gargle, pero este requiere que nos autentiquemos manualmente la primera vez, lo que lo hace inviable en Github. Otra opción disponible es crear una autenticación Oauth a partir de una aplicación de escritorio en Google Cloud, sin embargo, esta opción no funciona en entornos de servidor a servidor. La opción que vamos a usar para este caso será crear una autenticación a partir de una cuenta de servicio en Google Cloud y crear un proyecto referente a este ejercicio (aunque puedes usar uno existente, eso es irrelevante):

Vamos a asegurarnos de que la API de Google Sheets este habilitada. Simplemente debemos ir a la barra de búsqueda, seleccionar Google Sheets API y habilitarla:

Una vez hecho esto, la página nos redirigirá automáticamente a la página de API’s y Servicios. En esta página nos dirigiremos a la sección de Credenciales y daremos click en Crear credenciales, lo que nos abrirá un dropdown con los tipos de autenticaciones que podemos usar, siendo en este caso una Cuenta de Servicio:

Ahora, solo debemos asignarle un nombre al proyecto y escoger un rol. En este caso deberás escoger el rol de Propietario, esto en caso de que estes usando una cuenta de una organización, en caso contrario será la única opción que te aparecerá (no es necesario agregar condiciones de IAM):

Otorgamos permisos (en caso de que queramos que alguien más tenga acceso a esta cuenta de servicio) y la creamos. La página nos redirigirá una vez más a la sección de Credenciales, por lo que ahora daremos click en la cuenta de servicio que hemos creado:

Dentro de la cuenta de servicio, como primer paso vamos a copiar y guardar el correo asociado, ya que lo utilizaremos despues:

Ahora, nos dirigiremos a la sección Claves y daremos click en Crear clave nueva:

La escogeremos de tipo JSON y esta se descargará automáticamente. La clave se descargará con un nombre particularmente largo, así que te recomiendo usar algo más corto, como key.json, claro que esto ya es a elección de cada persona.

Ahora, ya que tenemos nuestra clave para la autenticación, vamos a probarla rápidamente en nuestra máquina local de la siguiente manera:

googledrive::drive_auth(path = "key.json")
gs4_auth(token = googledrive::drive_token())

sheet_properties(ss)
# A tibble: 2 × 8
  name     index        id type  visible grid_rows grid_columns data  
  <chr>    <int>     <int> <chr> <lgl>       <int>        <int> <list>
1 Raw data     0         0 GRID  TRUE         2900           26 <NULL>
2 October      1 427772048 GRID  TRUE         1000           26 <NULL>

Como puedes darte cuenta, la autenticación funciona correctamente, sin embargo, esto solo aplica para la lectura del documento, ya que tiene permisos de lectura para cualquier cuenta que tenga el vínculo, y la escritura de datos no la estamos haciendo a través de nuestra cuenta de Google, si no a través de la cuenta de servicio creada. Si yo quisiera escribir datos en ella:

range_write(ss = ss,
            sheet = mes_actual,
            range = "B2",
            data = data |> 
              filter(between(timestamp, in_date, end_date)) |> 
              mutate(
                timestamp = floor_date(timestamp, "weeks")
              ),
            reformat = FALSE
)

Me devuelve un mensaje de error, de tipo:

✔ Editing EJERCICIO AUTOMATIZACION GITHUB.
✔ Writing to sheet October.
Error in `gargle::response_process()`:
! Client error: (403) PERMISSION_DENIED
• Client does not have sufficient permission. This can happen because the OAuth token does not have the right scopes,
  the client doesn't have permission, or the API has not been enabled for the client project.
• The caller does not have permission
Backtrace:
 1. googlesheets4::range_write(...)
 2. gargle::response_process(resp_raw)
Error in gargle::response_process(resp_raw) :
• Client does not have sufficient permission. This can happen because the OAuth token does not have the right scopes,
the client doesn't have permission, or the API has not been enabled for the client project.
• The caller does not have permission

Para solucionar esto simplemente debemos copiar el correo de la cuenta de servicio y darle permisos de escritura en el documento de Google Cloud:

Entonces, si volvemos a ejecutar el código:

tabs <- sheet_properties(ss)
mes_actual <- format(today(), "%B")
in_date <- floor_date(today(), "months")
end_date <- floor_date(today(), "months") %m+% months(1) - 1

range_write(ss = ss,
            sheet = mes_actual,
            range = "B2",
            data = data |> 
              filter(between(timestamp, in_date, end_date)) |> 
              mutate(
                timestamp = floor_date(timestamp, "weeks")
              ),
            reformat = FALSE
)
✔ Editing "EJERCICIO AUTOMATIZACION GITHUB".
✔ Writing to sheet 'October'.

Ya funcionará correctamente.

Antes de pasar a la automatización con Github Actions, vamos a establecer el script finanl que utilitzaremos para este ejercicio:

library(alphavantager)
library(dplyr)
library(lubridate)
library(googlesheets4)

# API KEY
av_api_key("QRD9JEXDGCDJYRRM")

# PETICIÓN
data <- av_get(symbol = "GOOG", 
               av_fun = "TIME_SERIES_DAILY", 
               outputsize = "full", 
               interval = "60min")

# AUTENTICACIÓN
googledrive::drive_auth(path = "key.json")
gs4_auth(token = googledrive::drive_token())

# PARAMETROS
# Hojas existentes en el documento
ss <- "1F-3U7zg3G5tKvBCDxS3feUmUVhuPUryrItHsAlxROak"
tabs <- sheet_properties(ss)

# Nombre de la hoja a buscar/crear
mes_actual <- format(today(), "%B")

# Fechas inicial/final del mes
in_date <- floor_date(today(), "months")
end_date <- floor_date(today(), "months") %m+% months(1) - 1

# DATOS GENERALES 
range_clear(ss = ss,
            sheet = "Raw data",
            range = "B:G")


range_write(ss = ss,
            sheet = "Raw data",
            range = "B2",
            data = data |> 
              filter(timestamp >= as_date("2025-01-01")),
            reformat = FALSE)

# HOJAS INDIVIDUALES 
# Condición si no existe la hoja del mes actual
if (length(tabs$name[tabs$name == mes_actual ]) == 0 ){
  
  sheet_add(ss = ss, sheet = mes_actual)
  
  range_write(ss = ss,
              sheet = mes_actual,
              range = "B2",
              data = data |> 
                filter(between(timestamp, in_date, end_date)) |> 
                mutate(
                  timestamp = floor_date(timestamp, "weeks")
                ),
              reformat = FALSE
  )

# Condición si ya existe la hoja del mes actual    
}else{
  
  range_clear(
    ss = ss,
    sheet = mes_actual,
    range = "B:G"
  )
  
  range_write(ss = ss,
              sheet = mes_actual,
              range = "B2",
              data = data |> 
                filter(between(timestamp, in_date, end_date)) |> 
                mutate(
                  timestamp = floor_date(timestamp, "weeks")
                ),
              reformat = FALSE
  )
  
}

Moviendonos ahora a Github. Como ya sabrás, vamos a crear un repositorio nuevo y añadir nuestro script de R, resultando en:

Como es obvio, subir directamente las claves de la cuenta de servicio (y cualquier clave en particular) a github es una mala idea, ya que es un repositorio público y cualquier persona puede hacer un mal uso de ellas, por lo que vamos a crear una variable secreta que después construirá el archivo JSON en el backend. Para esto debemos ir a Settings y seleccionar Actions en la sección de Secrets and variables:

Una vez aquí, vamos a dar click en New Repository Secret, le asignaremos un nombre a nuestra variable secreta (siendo en este caso GCP_SERVICE_ACCOUNT_KEY) y pegaremos la información del archivo JSON tal cual la tenemos:

Para motivos de este tutorial omití algunos datos sensibles de la clave, pero la tuya tendrá una PRIVATE KEY mucho más larga.

Una vez hecho esto, ya tenemos toda la información necesaria para crear nuestro flujo de trabajo. El siguiente paso será crear un archivo YAML en el que definiremos los pasos a seguir de nuestro flujo de trabajo, de una forma muy parecida a como lo haríamos si usáramos Airflow en Python. El proceso en cuestión será:

name: Run auth.R (Windows)

on:
  workflow_dispatch:
  schedule:
    - cron: "0 6 * * *"  # todos los días a las 6 AM UTC

jobs:
  run-r-script:
    runs-on: windows-latest

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

      - name: Set up R
        uses: r-lib/actions/setup-r@v2

      - name: Install R packages
        run: |
          Rscript -e "install.packages(c('alphavantager','dplyr','lubridate','googlesheets4','googledrive'), repos='https://cloud.r-project.org')"

      - name: Write service account key
        shell: bash
        run: |
          echo '${{ secrets.GCP_SERVICE_ACCOUNT_KEY }}' > key.json

      - name: Run auth.R script
        run: |
          Rscript auth.R

Vamos a explicarlo paso a paso:

Lo primero que hacemos en este flujo de trabajo es nombrar el proceso (esto puede cambiar en tus proyectos) y establecer un workflow_dispatch para poder ejecutarlo manualmente, ya que la primera ejecución funciona como un disparador para las siguientes.

El parámetro schedule nos sirve para establecer la frecuencia y el horario en el que se va a ejecutar nuestro flujo a partir de un CRON, siendo en este caso "0 6 * * *", lo que significa que se ejecutará todos los días a las 6 de la mañana para la zona horaria UTC.

Establecí la ejecución en Windows mediante runs-on: windows-latest, ya que a diferencia de Ubuntu, donde tiene que construir todo el ambiente de trabajo y toma cerca de 8 minutos, Windows ya lo tiene de forma nativa, por lo que solo nos preocupamos por descargar las librería necesarias.

Ahora, los pasos de ejecución:

  • Checkout repository (actions/checkout@v4): clona el repositorio en el runner para que auth.R y otros archivos estén disponibles.

  • Set up R (r-lib/actions/setup-r@v2): instala y configura R en el runner Windows. Esto prepara Rscript y define R_LIBS_USER.

  • Install R packages: instala las librerías que usamos en el script.

  • Write service account key: crea el archivo key.json desde el secret almacenado que armamos hace un momento. En este paso en particular, procura mantener comillas simples ’’ en lugar de comillas dobles, ya que estas últimas harán que cambie la forma de leer el secret, haciéndolo inválido.

  • Run auth.R script: ejecuta el script auth.R.

Como nota final, el archivo YAML debe ser guardado siempre en la ruta .github/workflows/ del repositorio, siendo en este caso .github/workflows/auth.yml

Finalmente, para comenzar a ejecutar nuestro JOB, simplemente debemos dirigirnos a Actions, dar click en Run auth.R (windows) y ejecutar Run Workflow, lo que comenzará a ejecutarlo en ese momento y activará el CRON.

Una vez hecho esto, refrescamos la página y nos aparecerá el JOB que estamos ejecutando. Si damos click en este nos llevará al backend del flujo, donde podremos ver que se ejecutó con éxito (o indentificar cualquier error):

Y así, de esta sencilla forma, podemos automatizar nuestros flujos de trabajo usando Github. Si quieres ver el ejemplo completo de este workflow te dejo el repositorio creado para este ejercicio.

Referencias

  • Kinsman, T., Wessel, M., Gerosa, M.A. and Treude, C., 2021, May. How do software developers use github actions to automate their workflows?. In 2021 IEEE/ACM 18th International Conference on Mining Software Repositories (MSR) (pp. 420-431). IEEE.

  • Decan, A., Mens, T., Mazrae, P.R. and Golzadeh, M., 2022, October. On the use of github actions in software development repositories. In 2022 IEEE International Conference on Software Maintenance and Evolution (ICSME) (pp. 235-245). IEEE.

  • Talekar, R., 2023. Insider Threat For Service Account in Google Cloud Platform.