Skip to main content

Custom Actions

La mayoría de las actions que ya usamos son públicas creadas por el equipo de GitHub o por la comunidad, aún no hemos creado ninguna action personalizada.

Podemos crear una action para simplificar varios steps en nuestro workflow de una sola vez creando una única action simplificando el workflow y mejorando la legibilidad.

Por ejemplo tenemos estos 2 steps en los jobs lint, test y build que podrían ser agrupados.

      - name: Cache dependencies
id: cache
uses: actions/cache@v3
with:
path: node_modules
key: deps-node-modules-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci

Aunque existen muchas actions disponibles en el github marketplace en algún momento podemos necesitar algo que aún no existe o sí existe, pero queremos personalizar el comportamiento. Es raro, pero sucede con frecuencia :D. Es una oportunidad de contribuir y publicar para que otros usuarios puedan ayudar a mejorar la action.

Existen 3 tipos de actions que son utilizadas de la misma forma siendo llamadas por el uses pero son construidas de forma diferente.

  • JavaScript Actions
    • El código está en Javascript y siempre que la action sea ejecutada llamará a un archivo .js. El runtime de ejecución será nodejs con sus recursos y paquetes.
    • Runtime Nativo de GitHub.
    • No lo abordaré aquí, vale la pena investigar en caso de no poder resolver usando docker.
  • Docker Actions
    • Utilizada para desarrollar en otros lenguajes. Lo que importa es la acción que la action va a ejecutar y ésta puede ser una acción del contenedor. Dentro del contenedor podemos desarrollar en cualquier lenguaje, pues estaremos en un entorno preparado para la ejecución.
    • Mayor flexibilidad y portabilidad.
    • No lo abordaré aquí, pero vale la pena investigar si las actions existentes no resuelven el problema.
  • Composite Actions
    • No desarrollamos nada, solo combinamos varios steps y actions para resolver el problema, o resumirlo.
    • Si no te gusta desarrollar es aquí precisamente donde vas a preferir quedarte.

Si queremos crear una action que esté disponible para otros repositorios o que sea pública esta debe estar en un repositorio exclusivo.

Para crear una action en un proyecto específico y accesible solamente para este repositorio podemos crearla en cualquier lugar del proyecto, pues vamos a referenciarla usando el path.

Es buena práctica que esté dentro de la carpeta .github/actions. Toda action necesita tener su carpeta y dentro de esta carpeta estar el archivo action.yml

cd proyecto
tree .github
.github
├── actions
└── workflows
└── deploy.yml

Vamos a crear una action llamada cache-deps con los dos steps que mencionamos anteriormente dentro de la carpeta actions.

Las actions también se definen en archivo yml. Las actions no sufren eventos entonces no poseen el on.

Aunque la action checkout también forme parte de varios steps estamos creando una action que solamente va a formar parte de este repositorio siendo necesario antes poder usar la action definida localmente. Si fuera creada una action en un repositorio separado tendría sentido incluir el checkout también siendo posible hacer referencia a la action por medio de la url del repositorio. Este es un caso específico pues no podemos hacer referencia al propio proyecto que estamos haciendo el checkout.

name: 'Get And Cache Deps'
description: 'Get dependencies via npm and cache them.'

runs:
using: 'composite' # Este es el tipo de la action
steps:
## Nuestro bloque de steps ##
- name: Cache dependencies
id: cache
uses: actions/cache@v3
with:
path: node_modules
key: deps-node-modules-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true' # También tiene soporte a las condiciones entonces podemos mantener
run: npm ci
shell: bash # Cuando vamos a usar un comando run necesitamos definir shell
###########################

Solo para mostrar cómo quedó la estructura de carpeta.

tree .github
.github
├── actions # Carpeta
│ └── cache-deps # Carpeta con nombre de la action
│ └── action.yml # archivo que define la action
└── workflows
└── deploy.yml

Ahora vamos a hacer referencia a esta action en el workflow abajo, dejaré comentado lo que sea removido

name: Deployment
on:
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
# - name: Cache dependencies
# id: cache
# uses: actions/cache@v3
# with:
# path: node_modules
# key: deps-node-modules-${{ hashFiles('**/package-lock.json') }}
# - name: Install dependencies
# if: steps.cache.outputs.cache-hit != 'true'
# run: npm ci
- name: Cache and Install Deps # <<<<
uses: ./.github/actions/cache-deps # <<<< Apuntamos a la carpeta que contiene el action.yaml
- name: Lint code
run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
# - name: Cache dependencies
# id: cache
# uses: actions/cache@v3
# with:
# path: node_modules
# key: deps-node-modules-${{ hashFiles('**/package-lock.json') }}
# - name: Install dependencies
# if: steps.cache.outputs.cache-hit != 'true'
# run: npm ci
- name: Cache and Install Deps # <<<<
uses: ./.github/actions/cache-deps # <<<< Apuntamos a la carpeta que contiene el action.yaml
- name: Test code
id: run-tests
run: npm run test
- name: Upload test report
if: failure() && steps.run-tests.outcome == 'failure'
uses: actions/upload-artifact@v3
with:
name: test-report
path: test.json
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
# - name: Cache dependencies
# id: cache
# uses: actions/cache@v3
# with:
# path: node_modules
# key: deps-node-modules-${{ hashFiles('**/package-lock.json') }}
# - name: Install dependencies
# if: steps.cache.outputs.cache-hit != 'true'
# run: npm ci
- name: Cache and Install Deps # <<<<
uses: ./.github/actions/cache-deps # <<<< Apuntamos a la carpeta que contiene el action.yaml
- name: Build website
run: npm run build
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: dist-files
path: dist
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Get build artifacts
uses: actions/download-artifact@v3
with:
name: dist-files
path: ./dist
- name: Output contents
run: ls
- name: Deploy site
run: echo "Deploying..."

Reduciendo el código.

name: Deployment
on:
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Cache and Install Deps
uses: ./.github/actions/cache-deps
- name: Lint code
run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Cache and Install Deps
uses: ./.github/actions/cache-deps
- name: Test code
id: run-tests
run: npm run test
- name: Upload test report
if: failure() && steps.run-tests.outcome == 'failure'
uses: actions/upload-artifact@v3
with:
name: test-report
path: test.json
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Cache and Install Deps
uses: ./.github/actions/cache-deps
- name: Build website
run: npm run build
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: dist-files
path: dist
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Get build artifacts
uses: actions/download-artifact@v3
with:
name: dist-files
path: ./dist
- name: Output contents
run: ls
- name: Deploy site
run: echo "Deploying..."

alt text

Inputs e Outputs

Anteriormente usamos la tag with para pasar los inputs a otras actions. Entonces vamos a hacer lo mismo.

Vamos a colocar un input si debemos o no hacer la caché. En caso de no ser pasado usará el valor por defecto y hará la caché normalmente.

name: 'Get And Cache Deps'
description: 'Get dependencies via npm and cache them.'
# Definiendo las variables
inputs:
caching:
description: 'Si vamos o no a hacer la caché'
required: false # No es obligatorio pasar el input
default: 'true' # valor por defecto en caso de no ser pasado
# Ejemplo de cómo sería otro input...
# other-var:
# description: 'other var'
# required: true
# default: 'idontknow'
runs:
using: 'composite'
steps:
- name: Cache dependencies
if: inputs.caching == 'true' # Solo hará la caché si es true
id: cache
uses: actions/cache@v3
with:
path: node_modules
key: deps-node-modules-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true' || inputs.caching != 'true'
run: npm ci
shell: bash # Cuando vamos a usar un comando run necesitamos definir shell
###########################

Cambiamos la condición del step install dependencies pues o va a hacer la caché cuando el primer step ejecute y dé un cache-hit o en caso de recibir el input diferente de true. Si no hará la caché, entonces necesita instalar forzadamente las dependencias.

vamos a alterar solamente el lint para no usar la caché.

...
lint:
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Cache and Install Deps
uses: ./.github/actions/cache-deps
with:
caching: 'false'
...

alt text

Vamos a colocar un output aquí solo para ilustrar cómo referenciar las cosas.

name: 'Get And Cache Deps'
description: 'Get dependencies via npm and cache them.'
inputs:
caching:
description: 'Si vamos o no a hacer la caché'
required: false
default: 'true'
runs:
using: 'composite'
steps:
- name: Cache dependencies
id: cache
if: ${{ inputs.caching == 'true' }}
uses: actions/cache@v3
with:
path: node_modules
key: deps-node-modules-${{ hashFiles('**/package-lock.json') }}

- name: Install dependencies
id: install
if: ${{ steps.cache.outputs.cache-hit != 'true' || inputs.caching != 'true' }}
shell: bash
run: npm ci

- name: Set output if cache was used
id: fill-outputs
shell: bash
run: |
if [[ "${{ steps.install.outcome }}" == "success" ]]; then
echo "used=false" >> $GITHUB_OUTPUT
else
echo "used=true" >> $GITHUB_OUTPUT
fi
# Arriba comprobamos si el step anterior tuvo o no éxito.
# Si tuvo éxito creamos una variable llamada used con el valor false pues no usó la caché
# Si no tuvo éxito entonces la caché fue usada definimos false.
outputs:
used-cache:
description: "Si la caché fue usada"
# Referenciamos la variable used creada en el step fill-outputs
value: ${{ steps.fill-outputs.outputs.used }}

Vamos a colocar una salida en el lint para comprobar.

...
lint:
runs-on: ubuntu-latest
steps:
- name: Get code
uses: actions/checkout@v3
- name: Cache and Install Deps
id: cache-deps # <<<added
uses: ./.github/actions/cache-deps
with:
caching: 'false'
- name: Output Info
run: echo "Cache used? ${{ steps.cache-deps.outputs.used-cache}}"
...

En el lint que definimos para no usar caché:

alt text